前言说明

现在有很多单片机的硬件定时器都具备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. 苹果 App Store 开始支持隐藏上架应用:只能通过链接下载

      据MacRumors报道,苹果公司最近宣布,正如其开发者网站上所概述的那样,App Store现在支持只能通过直接链接才能发现的隐藏应用.   图片来自 Apple 拥有不适合公开发布的应用的开发 ...

  2. Django Admin save 重写 保存

    在 django admin管理控制台中,用户按下"Save and add another",表单的字段值仍然填充最后插入的值 并且保存 在ModelAdmin中添加选项save ...

  3. while、for循环结合else

    """1.while else,当while循环正常结束时,才走else里的代码块,也就是没有被break打断的情况下2.此处只是不被break打断,也就是遇到break ...

  4. 利用nginx自带的反向代理以及轮询功能实现应用的负载均衡

    针对中间件部署的应用(war包),可使用nginx自带的反向代理以及轮询功能,实现应用的负载均衡. 一.架构图 二.环境准备 准备2套环境,如19.1.0.18:7001,19.1.0.16:7001 ...

  5. Java 同步锁ReentrantLock与抽象同步队列AQS

    AbstractQueuedSynchronizer 抽象同步队列,它是个模板类提供了许多以锁相关的操作,常说的AQS指的就是它.AQS继承了AbstractOwnableSynchronizer类, ...

  6. 2022-11-13 Acwing每日一题

    本系列所有题目均为Acwing课的内容,发表博客既是为了学习总结,加深自己的印象,同时也是为了以后回过头来看时,不会感叹虚度光阴罢了,因此如果出现错误,欢迎大家能够指出错误,我会认真改正的.同时也希望 ...

  7. 手把手,完整的从0搭建vite-vue3-ts项目框架:配置less+svg+pinia+vant+axios

    项目同步git:https://gitee.com/lixin_ajax/vue3-vite-ts-pinia-vant-less.git 为避免赘述,过于基础的点会直接省略或贴图,比如创建文件夹/文 ...

  8. zabbix6.0安装

    一.简述 zabbix6.0 对相关软件版本要求较高,需要php7.25以上php8.0以下版本支持,若使用mysql数据库,其最低要求为mysql8.0,本此搭建采用的是使用较广的lnmp架构 za ...

  9. React基础学习知识笔记

    React项目的核心就是index.js 第一个程序 import React from 'react'; import ReactDOM from 'react-dom';ReactDOM.rend ...

  10. 处理get请求中文乱码tomcat请求

    修改tomcat中server配置:添加 URIEncoding="UTF-8" <Connector port="8090" protocol=&quo ...