前言说明

现在有很多单片机的硬件定时器都具备PWM输出功能,不过有时候会因为器件成本或硬件设计等原因,导致数量不够或者所使用的引脚不支持定时器输出。尴尬的是,笔者接手的项目两种情况都存在,项目需要支持 8 个电机,每个电机支持正反转,因此需要 8 * 2 = 16 路 PWM 信号。

思考了一阵,觉得可以基于一个普通的定时器来实现一个易于移植和扩展使用的PWM控制接口库,该接口库所支持的 PWM 通道数量仅受单片机的主频限制。

完整实例

以下实例代码是基于 GD32E230C 实现支持 16 路 PWM 通道的代码,可以实现调整每一路通道的占空比和频率,也可以单独关闭和启用某一个通道。用于电机控制上,代码预设的所有通道为 40Hz 频率,占空比范围 0 ~ 100。

/**
******************************************************************************
* @文件 pwm_control.h
* @版本 V1.0.0
* @日期
* @概要 PWM 控制接口
* @作者 lmx
* @邮箱 lovemengx@qq.com
******************************************************************************
* @注意
*
******************************************************************************
*/
#ifndef PWM_CONTROL_H
#define PWM_CONTROL_H void pwm_control_init(); // 初始化 PWM 控制接口
void pwm_control_release(); // 释放 PWM 控制接口 void pwm_control_suspend(); // PWM 控制器暂停运行, 在比较多PWM通道需要同时生效的情况下使用
void pwm_control_resume(); // PWM 控制器暂停运行, 在比较多PWM通道需要同时生效的情况下使用 void pwm_control_set(unsigned int channel, unsigned int ratio); // 设置指定通道的占空比
void pwm_control_setall(unsigned int ratio); // 设置所有通道的占空比
void pwm_control_set_percent(unsigned int channel, unsigned char percent); // 以百分比设置指定通道的占空比
void pwm_control_setall_percent(unsigned char percent); // 以百分比设置所有通道的占空比 void pwm_control_enable(unsigned int channel); // 启用指定通道
void pwm_control_disable(unsigned int channel); // 关闭指定通道 #endif
/**
******************************************************************************
* @文件 pwm_control.h
* @版本 V1.0.0
* @日期
* @概要 PWM 控制接口
* @作者 lmx
* @邮箱 lovemengx@qq.com
******************************************************************************
* @注意
*
******************************************************************************
*/
#include "gd32e23x.h"
#include "pwm_control.h" // PWM 配置
typedef volatile struct
{
unsigned char en:1; // 使能开关
unsigned int gpio_group; // 指定引脚组
unsigned int gpio_pin; // 指定引脚
unsigned int pwm_high; // 高电平维持次数(占空比)
unsigned int pwm_max; // 最多统计次数(周期)
unsigned int pwm_cnt; // 计数器
}pwm_config_t; // 定时器配置
#define CFG_PWM_TIMER TIMER5
#define CFG_PWM_RCU_CLOCK RCU_TIMER5
#define CFG_PWM_IRQ_NUMBER TIMER5_IRQn
#define CFG_PWM_IRQ_FUNCTION TIMER5_IRQHandler #define PWM_CNT_MAX 100
#define PWM_CONFIG_GPIO(__GROUP__, __PIN__) {1,__GROUP__,__PIN__,0,PWM_CNT_MAX,0}
volatile pwm_config_t pwm_configs[] = {
PWM_CONFIG_GPIO(GPIOA, GPIO_PIN_8), PWM_CONFIG_GPIO(GPIOA, GPIO_PIN_9),
PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_12), PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_13),
PWM_CONFIG_GPIO(GPIOA, GPIO_PIN_12), PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_8),
PWM_CONFIG_GPIO(GPIOA, GPIO_PIN_10), PWM_CONFIG_GPIO(GPIOA, GPIO_PIN_11),
PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_14), PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_15),
PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_3), PWM_CONFIG_GPIO(GPIOA, GPIO_PIN_15),
PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_5), PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_4),
PWM_CONFIG_GPIO(GPIOC, GPIO_PIN_13), PWM_CONFIG_GPIO(GPIOC, GPIO_PIN_15),
};
#define PWM_CONFIG_MAX (sizeof(pwm_configs) / sizeof(pwm_configs[0])) // 定时器中断程序
void CFG_PWM_IRQ_FUNCTION()
{
volatile unsigned int i = 0; if( SET != timer_interrupt_flag_get(CFG_PWM_TIMER, TIMER_INT_FLAG_UP) ){
return ;
}
timer_interrupt_flag_clear(CFG_PWM_TIMER, TIMER_INT_FLAG_UP); for(i = 0; i < PWM_CONFIG_MAX; i++)
{
// 未使能则不检测
if(0 == pwm_configs[i].en)
continue; // 计数器累加
pwm_configs[i].pwm_cnt = pwm_configs[i].pwm_cnt >= pwm_configs[i].pwm_max ? 1 : pwm_configs[i].pwm_cnt+1; // 判断高电平维持次数是否结束, 如果结束则至低电平
if(pwm_configs[i].pwm_cnt <= pwm_configs[i].pwm_high)
gpio_bit_set(pwm_configs[i].gpio_group, pwm_configs[i].gpio_pin);
else
gpio_bit_reset(pwm_configs[i].gpio_group, pwm_configs[i].gpio_pin);
} return ;
} // PWM 配置初始化
static void pwm_config_init()
{
// 初始化 GPIO 配置
for(unsigned char i = 0; i < PWM_CONFIG_MAX; i++){
gpio_mode_set(pwm_configs[i].gpio_group, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, pwm_configs[i].gpio_pin);
gpio_output_options_set(pwm_configs[i].gpio_group, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, pwm_configs[i].gpio_pin);
gpio_bit_reset(pwm_configs[i].gpio_group, pwm_configs[i].gpio_pin);
}
return;
} // PWM 配置释放
static void pwm_config_release()
{
// 释放 GPIO
for(unsigned char i = 0; i < PWM_CONFIG_MAX; i++){
pwm_configs[i].en = 0;
gpio_bit_reset(pwm_configs[i].gpio_group, pwm_configs[i].gpio_pin);
}
} // 定时器初始化
static void pwm_timer_init()
{
timer_parameter_struct para;
rcu_periph_clock_enable(CFG_PWM_RCU_CLOCK); // 72000000/72/250 = 4KHz;
// 1s/4000Hz = 0.00025s = 0.25ms = 250us
timer_deinit(CFG_PWM_TIMER);
para.prescaler = 72-1; // 设置预分频率
para.period = 250-1; // 设置为 4KHz 频率
para.alignedmode = TIMER_COUNTER_EDGE;
para.counterdirection = TIMER_COUNTER_UP; // 向上计数
para.clockdivision = 0; // 不分频
para.repetitioncounter = 0; // 计数重复值0
timer_init(CFG_PWM_TIMER, &para); nvic_irq_enable(CFG_PWM_IRQ_NUMBER, 0U);
timer_interrupt_enable(CFG_PWM_TIMER, TIMER_INT_UP);
timer_auto_reload_shadow_enable(CFG_PWM_TIMER);
timer_enable(CFG_PWM_TIMER); return ;
} // 定时器释放
static void pwm_timer_release()
{
timer_interrupt_disable(CFG_PWM_TIMER, TIMER_INT_UP);
nvic_irq_disable(CFG_PWM_IRQ_NUMBER);
timer_disable(CFG_PWM_TIMER);
rcu_periph_clock_disable(CFG_PWM_RCU_CLOCK); return ;
} // PWM 控制器初始化
void pwm_control_init()
{
pwm_config_init();
pwm_timer_init();
} // PWM 控制器释放
void pwm_control_release()
{
pwm_timer_release();
pwm_config_release();
} // PWM 控制器暂停运行, 在比较多PWM通道需要同时生效的情况下使用
void pwm_control_suspend()
{
timer_interrupt_disable(CFG_PWM_TIMER, TIMER_INT_UP);
} // PWM 控制器暂停运行, 在比较多PWM通道需要同时生效的情况下使用
void pwm_control_resume()
{
timer_interrupt_enable(CFG_PWM_TIMER, TIMER_INT_UP);
} // 设置指定通道的占空比
void pwm_control_set(unsigned int channel, unsigned int ratio)
{
if( channel >= PWM_CONFIG_MAX){
return ;
} pwm_configs[channel].en = 0;
pwm_configs[channel].pwm_high = ratio;
pwm_configs[channel].en = 1;
return ;
} // 设置所有通道的占空比
void pwm_control_setall(unsigned int ratio)
{
timer_interrupt_disable(CFG_PWM_TIMER, TIMER_INT_UP);
for(unsigned char i = 0; i < PWM_CONFIG_MAX; i++){
pwm_configs[i].pwm_cnt = 0;
pwm_configs[i].pwm_high = ratio;
}
timer_interrupt_enable(CFG_PWM_TIMER, TIMER_INT_UP);
return ;
} //以百分比来设置占空比比例
void pwm_control_set_percent(unsigned int channel, unsigned char percent)
{
pwm_control_set(channel, (unsigned int)(((float)percent / 100) * pwm_configs[channel].pwm_max));
return;
} //以百分比来设置所有占空比比例
void pwm_control_setall_percent(unsigned char percent)
{
pwm_control_setall((unsigned int)(((float)percent / 100) * pwm_configs[channel].pwm_max));
return ;
} // 启用指定通道
void pwm_control_enable(unsigned int channel)
{
if( channel >= PWM_CONFIG_MAX){
return ;
} pwm_configs[channel].en = 1;
return ;
} // 关闭指定通道
void pwm_control_disable(unsigned int channel)
{
if( channel >= PWM_CONFIG_MAX){
return ;
} pwm_configs[channel].en = 0;
gpio_bit_reset(pwm_configs[channel].gpio_group, pwm_configs[channel].gpio_pin); return ;
}
/**
******************************************************************************
* @文件 main.c
* @版本 V1.0.0
* @日期
* @概要 主程序文件
* @作者 lmx
* @邮箱 lovemengx@qq.com
******************************************************************************
* @注意
*
******************************************************************************
*/
#include <stdlib.h>
#include <stdio.h>
#include "pwm_control.h" int main(void)
{
unsigned char percent = 0; // 公共的时钟使能和关闭应当由专门的源文件集中处理, 不应当与具体的某个功能模块挂钩
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_GPIOB);
rcu_periph_clock_enable(RCU_GPIOC);
rcu_periph_clock_enable(RCU_GPIOF); pwm_control_init();
pwm_control_set(9, 50); while(1)
{
percent = percent >= 100 ? 0 : percent + 1;
pwm_control_suspend();
pwm_control_setall_percent(percent);
pwm_control_resume();
delay_1ms(100);
} return 0;
}

实现原理

借助定时器来产生固定频率的中断,通过对中断进行计数来确定何时设置 GPIO 的高低电平状态,如设定 100 个中断为一个周期,拉高 GPIO 维持50个中断,再拉低 GPIO 维持 50 个中断,即可实现一个 50% 占空比的 PWM 方波。这里的一个周期为 100 个中断,即决定了产生 PWM 的频率,而维持多少数量中断设定 GPIO 高低电平,则决定了占空比多少。如果调整个数无法依然无法满足所需要的频率,可以适当通过调整定时器的中断频率来进一步提升,但过高的定时器中断频率会加重 CPU 的负担,可能会影响其他的业务逻辑处理。

要实现众多数量的 PWM 信号,通过设计良好的数据结构,将这些必要的参数组织起来:GPIO、高电平维护次数、一周期次数、当前计数、控制开关。在定时器中断里面根据这些配置参数去决定 GPIO 的输出状态,即可实现各路不同频率、不同占空比比例、可单独开关的PWM 信号输出。

// PWM 配置
typedef struct
{
unsigned char en:1; // 使能开关
unsigned int gpio_group; // 指定引脚组
unsigned int gpio_pin; // 指定引脚
unsigned int pwm_high; // 高电平维持次数,决定占空比
unsigned int pwm_max; // 最多统计次数,PWM周期,决定输出频率
unsigned int pwm_cnt; // 计数器
}pwm_config_t;

定时器配置计算方法

如本文提供的例子,可输出 16 路 40Hz 频率的 PWM 信号,占空比精度为 100,我的 SDK 中设定的 GD32E230C 工作频率为 72MHz,因此计算方法为:

  1. 计算 40Hz 一周期的耗时时间:1秒 / 40Hz = 0.025秒
  2. 计算每一级占空比所的的时间:0.025秒 / 100 = 0.00025秒
  3. 计算定时器最低要求的中断频率:1秒 / 0.00025秒 = 4000Hz
  4. 计算以 72 为预分频参数的定时器频率:72MHz / 72 = 1MHz = 1000Khz = 1000000Hz
  5. 计算定时器产生所需中断频率的周期参数:1000000Hz / 4000Hz =250 个周期,定时器设定参数如下:
timer_parameter_struct para;

timer_deinit(TIMER5);
para.prescaler = 72-1; // 设置时钟频率为 1MHz
para.period = 250-1; // 72MHz=72000000Hz/72prescaler/250period = 4000Hz = 4KHz
para.alignedmode = TIMER_COUNTER_EDGE;
para.counterdirection = TIMER_COUNTER_UP; // 向上计数
para.clockdivision = 0; // 不分频
para.repetitioncounter = 0; // 计数重复值0
timer_init(TIMER5, &para);

若希望默认输出 40Hz 频率,50% 占空比的信号,则配置 pwm_config_t.pwm_max=100,配置 pwm_config_t.pwm_hight=50 即可。

若希望默认输出 80Hz 频率,50% 占空比的信号,则配置 pwm_config_t.pwm_max=50,配置 pwm_config_t.pwm_hight=25 即可。

如果希望输出的频率,通过修改 pwm_max 无法实现,那么可以通过适当的调整定时器的中断频率来配合实现。但过高的定时器中断频率会增加 CPU 的负担,如果影响了业务逻辑处理,那么可以通过降低占空比的精度来降低定时器的中断频率。

如上例要实现 40Hz 频率输出,但却需要定时器产生 4KHz 频率的中断,我们可以将占空比的精度由 100 降至 15,即可将定时器频率降低至 600Hz:

  1. 配置 pwm_config_t.pwm_max=15
  2. 计算 40Hz 一周期的耗时时间:1秒 / 40Hz = 0.025秒
  3. 计算每一级占空比所的的时间:0.025秒 / 15 = 0.0016666666666667秒
  4. 计算定时器最低要求的中断频率:1秒 / 0.0016666666666667秒 = 599.999999999988Hz ≈ 600Hz
  5. 计算以 72 为预分频参数的定时器频率:72MHz / 72 = 1MHz = 1000Khz = 1000000Hz
  6. 计算定时器产生所需中断频率的周期参数:1000000Hz / 600Hz =1,666.6666666667≈1667个周期,定时器设定参数如下:
timer_parameter_struct para;

timer_deinit(TIMER5);
para.prescaler = 72-1; // 设置时钟频率为 1MHz
para.period = 1667-1; // 72MHz=72000000Hz/72prescaler/1667period ≈ 600Hz
para.alignedmode = TIMER_COUNTER_EDGE;
para.counterdirection = TIMER_COUNTER_UP; // 向上计数
para.clockdivision = 0; // 不分频
para.repetitioncounter = 0; // 计数重复值0
timer_init(TIMER5, &para);

如果过高的中断频率影响到了其他硬件控制器的中断响应时间,可以降低定时器的中断优先级。

【单片机】通过定时器实现模拟任意路PWM通道(带实例和计算方法)的更多相关文章

  1. 定时器同步+触发三ADC采样+输出6路PWM波

    为了熟悉定时器定时器和ADC 用STM32F407DIS做了一个简单的工程: 通过高级定时器TIM1溢出更新时间作为触发输出信号(TRGO),触发TIM8开始计数: 同时TIM1的通道1.2.3以及分 ...

  2. 打打基础,回头看看avr单片机的定时器、中断和PWM(转)

    以前小看了定时器,发现这东西还真的很讲究,那先复习复习吧. 先提提中断:我的理解就是cpu执行时,遇到中断——根据对应的中断源(硬件或软件)——pc定位中断入口地址,然后根据这里的函数指针——跳转到相 ...

  3. 430单片机之定时器A功能的大致介绍

    总的来说,430单片机一共有三个定时器,定时器A,定时器B,还有就是看门狗定时器,这里我们主要是讨论430单片机的定时器A的功能,定时器A的功能是我目前见过最厉害的定时器,视频上说用好定时器A的话,对 ...

  4. 16路PWM输出的pca9685模块

    今天要介绍的就是该模块,该模块是16路pwm模块,使用I2C总线可以控制16路舵机(led). 接线OE空着就可以,其他VCC是芯片供电+5,SCL时钟线,SDA信号线,GND地线. 芯片介绍可以看: ...

  5. 【STM32H7教程】第34章 STM32H7的定时器应用之TIM1-TIM17的PWM实现

    完整教程下载地址:http://www.armbbs.cn/forum.php?mod=viewthread&tid=86980 第34章       STM32H7的定时器应用之TIM1-T ...

  6. nRF51822 的两路 PWM 极性

    忙了一阵这个PWM,玩着玩着终于发现了些规律.Nordic 也挺会坑爹的. nRF51822 是没有硬件 PWM 的,只能靠一系列难以理解的 PPI /GPIOTE/TIMER来实现,其实我想说,我醉 ...

  7. Linux使用一个定时器实现设置任意数量定时器功能【转】

    转自:https://www.jb51.net/article/120748.htm 为什么需要这个功能,因为大多数计算机软件时钟系统通常只能有一个时钟触发一次中断.当运行多个任务时,我们会想要多个定 ...

  8. MCS-51单片机的定时器/计数器概念

    一.MCS-51单片机的定时器/计数器概念 单片机中,脉冲计数与时间之间的关系十分密切,每输入一个脉冲,计数器的值就会自动累加1,而花费的时间恰好是1微秒;只要相邻两个计数脉冲之间的时间间隔相等,则计 ...

  9. PIC单片机的定时器

    PIC单片机的定时器有3个 timer0 timer1 timer2 定时器的计算方法 256*k*Tcy=定时时间 (256-Init-value)*k*Tcy=定时时间

  10. div模拟textarea以实现高度自适应实例页面

    作为多行文本域功能来讲,textarea满足了我们大部分的需求.然而,textarea有一个不足就是不能像普通div标签一样高度可以跟随内容自适应.textarea总是很自信地显摆它的滚动条,高度固执 ...

随机推荐

  1. PageRank原理分析

    pagerank是将众多网页看成一个有向图,每个页面就是有向图中的节点.计算每个节点的出度和入度.如果一个网站被大量其他的网页引用,那么他就会有更高的pr分数. 原理 对于所有与节点i相连的节点,用他 ...

  2. vue-axios增加操作

    <template> <div class="Insert"> <label for="name">名称:</labe ...

  3. 看了同事这10个IDEA神级插件,我也悄悄安装了

    昨天,有读者私信发我一篇文章,说里面提到的 Intellij IDEA 插件真心不错,基本上可以一站式开发了,希望能分享给更多的小伙伴,我在本地装了体验了一下,觉得确实值得推荐,希望小伙伴们有时间也可 ...

  4. day11-Servlet01

    Servlet01 官方api文档:https://tomcat.apache.org/tomcat-8.0-doc/servletapi/index.html Servlet和Tomcat的关系:一 ...

  5. webpack 配置echarts 按需加载

    引入babel-plugin-equire插件,方便使用.yarn add babel-plugin-equire -D 在.babelrc文件中的配置 { "presets": ...

  6. Go语言核心36讲21

    提到Go语言中的错误处理,我们其实已经在前面接触过几次了. 比如,我们声明过error类型的变量err,也调用过errors包中的New函数.今天,我会用这篇文章为你梳理Go语言错误处理的相关知识,同 ...

  7. Training: Get Sourced

    原题链接:http://www.wechall.net/challenge/training/get_sourced/index.php 提示告诉我们答案就藏在这个界面中,使用View Sourcec ...

  8. 大前端系统学-了解html

    标签: 使用尖括号包起来的就是标签,例如我们看到的  <html></html> 一对标签 <head>  开始标签 </head> 结束标签 < ...

  9. js-day01-商品订单信息

    学会表格表单(html+css) 表格的默认CSS属性 *{             margin: 0;             padding: 0;         }         tabl ...

  10. Linux 系统环境监测

    Linux系统环境监测 Linux系统环境主要监测CPU.内存.磁盘I/O和网络流量. 1. CPU (1) 查看CPU的负载情况:uptime 可以通过uptime查看系统整体的负载情况. 如果服务 ...