微软公司宣布不再支持你正在使用的 IE浏览器,这会严重影响浏览网页,请使用微软最新的Edge浏览器
您好, 登录| 注册|

STM32F1/F7使用HAL库DMA方式输出PWM详解(输出精确数量且可调周期与占空比)

  • 04-22 15:25
  • 675

    浏览

  • 4

    回复

  • 0

    获赞

  • 核心提示:文章目录一. STM32的DMA PWM原理1. DMA简介2. DMA方式输出PWM是怎么回事3. HAL库DMA配置PWM的几个函数二. STM32CubeMx配置 DMA PWM三. 波形调试过程分析一. STM32的DMA PWM原理最开始疑惑过STM32如何才能实现精确数量的脉冲输出从而控制步进电机,直到做WS2812B灯珠的驱动程序时才知道原来有DMA-PWM模式。使用DMA输出PWM可以精确控制脉冲数量,且可以精确控制脉冲周期与占空比,更重要的是使用DMA传输不消耗CPU资源。于是乎

    文章目录

    一. STM32的DMA PWM原理1. DMA简介2. DMA方式输出PWM是怎么回事3. HAL库DMA配置PWM的几个函数二. STM32CubeMx配置 DMA PWM三. 波形调试过程分析

    一. STM32的DMA PWM原理

    最开始疑惑过STM32如何才能实现精确数量的脉冲输出从而控制步进电机,直到做WS2812B灯珠的驱动程序时才知道原来有DMA-PWM模式。使用DMA输出PWM可以精确控制脉冲数量,且可以精确控制脉冲周期与占空比,更重要的是使用DMA传输不消耗CPU资源。于是乎上网搜索资源与教程,遗憾的是网上的教程要么语焉不详,要么代码不全,要么只讲表层不讲原理。秉承自己动手丰衣足食的古训,于是去翻阅参考手册,从DMA章节看到定时器章节,结合代码实战,总算搞清些端倪,也分享一下在此过程中遇到的问题。

    1. DMA简介

    官方释义:DMA,全称为: Direct Memory Access,即直接存储器访问。 DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备开辟一条直接传送数据的通路, 能使 CPU 的效率大为提高。下面这张图是网上流行的摘自参考手册上的DMA框图。

    STM32F1系列有两个DMA,分别有7个通道和5个通道(Channel)。F7系列每个DMA分别有8个数据流(StreamX),每个数据流对应8个通道(CHannel),这里稍微区分一下两个系列的表述,F1所说的Channel应该对应F7中的Stream。例如DMA1的通道对应表如下。STM32的ADC、SPI、IIS、USART、IIC、TIM、DAC等数据传输外设都可以设置为DMA方式传输,在手动配置的时候查表选择通道即可,当然如果用Cubemx工具的话就会自动选择了。

    DMA传输有什么好处?举个例子,使用HAL_UART_Transmit()和HAL_UART_Transmit_DMA(),前者使用普通模式,CPU会进入执行函数,直到数据传输完成退出,然后才执行下一条指令。后者使用DMA传输,DMA启动传输之后CPU就不管了,直接往下执行其他指令。CPU干什么呢?只需要处理DMA传输完成、半传输完成、传输错误等中断,或者通过查询寄存器检查DMA传输到啥情况了。是不是瞬间快起来了!

    2. DMA方式输出PWM是怎么回事

    使用DMA传输数据很好理解,为什么DMA可以控制PWM脉冲数量和占空比呢?这里我们回归本质,在DMA控制PWM输出的过程中,DMA依然传输的是数据,只不过它送过去的是比较值,即TIMx_CCRx的值,这个值不用多解释了,和自动重装载寄存器(TIMx_ARR)的值分别决定周期和占空比。看一下手册中定时器的DMA连续传送模式的解释。

    注意黄色部分,什么是更新事件?回顾一下,在向上计数模式下当计数到自动重装载值就会发生更新事件(溢出)。也就是说每单个PWM波结束后就会自动将比较值设置成DMA传输来的数据。以本例设置的周期1ms为例,设置send_Buf[] = {10,20,30,…,100},最终的波形就是高电平时间分别为10,20,30,…100us的十个方波。很简单有木有!!

    3. HAL库DMA配置PWM的几个函数

    说实话使用HAL库还是有点弯弯绕,很多操作层层封装,可能用寄存器几句代码的事情到了HAL库要调用好几个函数转几道弯,但这也是大势所趋吧,将底层都封装起来,让用户专注于应用程序。最近用Cubemx自动生成代码的感觉就是,真香!!

    搬运stm32F7xx_hal_tim.h中的函数定义,以下分别是以阻塞模式、中断模式、DMA模式启动和停止PWM。

    
    HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
    HAL_StatusTypeDef HAL_TIM_PWM_Stop(TIM_HandleTypeDef *htim, uint32_t Channel);
    
    HAL_StatusTypeDef HAL_TIM_PWM_Start_IT(TIM_HandleTypeDef *htim, uint32_t Channel);
    HAL_StatusTypeDef HAL_TIM_PWM_Stop_IT(TIM_HandleTypeDef *htim, uint32_t Channel);
    
    HAL_StatusTypeDef HAL_TIM_PWM_Start_DMA(TIM_HandleTypeDef *htim, uint32_t Channel, uint32_t *pData, uint16_t Length);
    HAL_StatusTypeDef HAL_TIM_PWM_Stop_DMA(TIM_HandleTypeDef *htim, uint32_t Channel);
    

    以下是中断回调函数的声明,这里我们只关注void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim);每次PWM输出完成之后调用这个函数,在中断里面我们需要调用HAL_TIM_PWM_Stop_DMA(TIM_HandleTypeDef *htim, uint32_t Channel)停止DMA传输,否则它不会自己停止的。

    
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);
    void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim);
    void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim);
    void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim);
    void HAL_TIM_TriggerCallback(TIM_HandleTypeDef *htim);
    void HAL_TIM_ErrorCallback(TIM_HandleTypeDef *htim);
    

    二. STM32CubeMx配置 DMA PWM

    以STM32F1和F7系列板子为例进行测试,经过测试两者配置基本是一样的,结果也是一样,所以这里以F1为例讲解。

    如图,新建基于STM32F103ZET6的工程,先进行时钟配置,系统时钟设定到最大72MHz。

    然后设置定时器,我使用的是T2,四个通道都选上了,根据需要来即可。分频系数设为71,即72分频,pwm频率1MHz,自动重装载值为1000,得到周期为1ms.

    接下来设置DMA,如图,四个通道DMA都选上了,这里CH2和CH4共用了一个通道,暂且不管它。

    可以看到此时DMA中断已经开启

    此外如果使用ST-Link下载程序,注意图示这个地方设置debug模式为Serial Wire,不然会出现ST-Link只能下载一次程序的情况。

    到此设置完毕,点击GENERATE CODE即可。

    三. 波形调试过程分析

    打开工程,可以看到TIM的初始化和DMA的初始化函数,这里在main函数中调用HAL_TIM_PWM_Start函数就可以正常输出连续波形了。

    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1)
    

    调用__HAL_TIM_SET_COMPARE函数可以改变占空比

    __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,200);
    

    如设置成200,则高电平时间为200us,占空比为200/1000。

    因为我们要使用DMA方式,在main函数中定义一个发送数据缓冲区

    #define NUM 21
    uint32_t send_Buf[NUM] = { 0};
    

    在main函数中增加以下代码

    for (i = 0; i < NUM; i++)
    { 
     	send_Buf[i] = 20 * (i + 1);
    }
    
    while (1)
    { 
    	 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET); 
    	 HAL_GPIO_WritePin(GPIOE, GPIO_PIN_5, GPIO_PIN_RESET); 
    	 HAL_Delay(200);
    	 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET); 
    	 HAL_GPIO_WritePin(GPIOE, GPIO_PIN_5, GPIO_PIN_SET); 
    	 HAL_Delay(200);
    	 HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_3,(uint32_t*)send_Buf,NUM);
    }
    

    添加如下函数

    // PWM DMA 完成回调函数
    void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
    { 
    	HAL_TIM_PWM_Stop_DMA(&htim2, TIM_CHANNEL_3);
    }
    

    这就是第一部分讲的需要在回调函数中调用HAL_TIM_PWM_Stop_DMA函数停止PWM输出。理论上讲到这里应该如我们所愿输出期望波形了,也就是占空比递增的21个波。但遗憾的是我的波是这样的:

    三个问题:(1)数据只有一半;(2)波的周期变成了正常的两倍(2ms)(3)最后一个数据跑到最前边了。

    生气ing…

    于是开始调bug,第一个问题发现了,由于HAL_TIM_PWM_Start_DMA(TIM_HandleTypeDef *htim, uint32_t Channel, uint32_t *pData, uint16_t Length);函数中的发送数据指针是指向32位的,我的send_Buf也是定义的32位,但是DMA传输我的设置是半字16位,如图示。

    也就是从uint32_t *pData开始指针每移一位,地址偏移两个字节。这样就能解释上面的第一二问题,因为希望传输的数据是{sendBuf[0],sendBuf[1], …,sendBuf[20]},实际传输的数据却是:sendBuf[0]低16位sendBuf[0]高16位(即0)…sendBuf[9]低16位sendBuf[9]高16位(即0)sendBuf[10]低16位

    接下来修改传输字宽为Word(32)位,或者把send_Buf改为uint16_t型,经测试结果都对了,如图所示。所以提醒朋友们,DMA传输位宽和定义的缓冲区位宽一定要一致!!DMA传输位宽和定义的缓冲区位宽一定要一致!!DMA传输位宽和定义的缓冲区位宽一定要一致!!

    问题3依然存在,于是把最后一个数据改为0试试,添加send_Buf[NUM - 1] = 0;波形正常了。

    原因尚不清楚,是不是因为DMA传输的起始和结束有什么不稳定因素,既然如此,那就每次在正常数据后面补一个或者多个0就行了。不影响使用。

    至此DMA控制PWM输出成功,下一篇用HAL-DMA-PWM点亮WS2812灯珠。

    同是电子工程师,请一定不要吝啬你的赞!

    0人已赞

    编辑 举报

    LV.1

    4277828

    2678422

    50

    597597

    说说你的看法

  • LV.

    @

    编辑

    删除

    举报

    #该内容仅管理员可见#

    #回复内容已被删除#

    #该内容正在审核#

    回复:

    取消
    发送
  • 现在还没有回复呢,说说你的想法

    现在还没有回复呢,说说你的想法

    全部回复(4)

  • ruohan

    LV.1

    05-18 09:33

    @

    继续啊,

    0

    设为最佳答案

    置顶

    编辑

    删除

    举报

    #该内容仅管理员可见#

    #回复内容已被删除#

    #该内容正在审核#

    回复:

    4278647

    2678422

    50

    213

    取消
    发送
    2
  • ruohan

    LV.1

    05-20 10:08

    @

    HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_3,(uint32_t*)send_Buf,NUM);

    这句是不是可以不用
    (uint32_t*)send_Buf强制转换啊,,,数组本身就是指针类型的,,
    0

    设为最佳答案

    置顶

    编辑

    删除

    举报

    #该内容仅管理员可见#

    #回复内容已被删除#

    #该内容正在审核#

    回复:

    4278661

    2678422

    50

    213

    取消
    发送
    3
  • lihui710884923

    LV.1

    05-25 14:59

    @ruohan

    HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_3,(uint32_t*)send_Buf,NUM);

    这句是不是可以不用
    (uint32_t*)send_Buf强制转换啊,,,数组本身就是指针类型的,,

    可以的,这个我实测了,最后一个数据跑到最前边了,没找到问题

    0

    设为最佳答案

    置顶

    编辑

    删除

    举报

    #该内容仅管理员可见#

    #回复内容已被删除#

    #该内容正在审核#

    回复:

    4278717

    2678422

    50

    597597

    取消
    发送
    4
  • ruohan

    LV.1

    05-26 08:00

    @lihui710884923

    可以的,这个我实测了,最后一个数据跑到最前边了,没找到问题

    HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_3,(uint32_t*)send_Buf,NUM+1);
    发的数改成NUM+1也可以把最后一个调整回来,
    原因是不是最后一个数配给定时器后,中断把
    HAL_TIM_PWM_Stop_DMA(&htim2, TIM_CHANNEL_3);这个指令执行了,造成最后一个没有发出来,而经过WHILE(1)循环执行HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_3,(uint32_t*)send_Buf,NUM);的时候,就把之前的那个比较寄存器的值开始跑了,,,,
    0

    设为最佳答案

    置顶

    编辑

    删除

    举报

    #该内容仅管理员可见#

    #回复内容已被删除#

    #该内容正在审核#

    回复:

    4278730

    2678422

    50

    213

    取消
    发送
    5
  • 现在还没有回复呢,说说你的想法

  • 回复

  • 收藏

  • 点赞

  • 举报有害信息

  • 已超出发布时间24小时,无法编辑与删除
    关于我们 联系方法 广告服务 会议服务 电子星球APP 网站地图 不良信息举报 热线:400-003-2006
    © 2002-2021 Netbroad(网博互动)公司版权所有 津ICP备:11006234号 联网备案号:12010402000747 增值电信业务经营许可证:津B2-20120058