STM32F1/F7使用HAL庫DMA方式輸出PWM詳解(輸出精確數量且可調週期與佔空比)

一. STM32的DMA PWM原理

最開始疑惑過STM32如何才能實現精確數量的脈衝輸出從而控制步進電機,直到作WS2812B燈珠的驅動程序時才知道原來有DMA-PWM模式。使用DMA輸出PWM能夠精確控制脈衝數量,且能夠精確控制脈衝週期與佔空比,更重要的是使用DMA傳輸不消耗CPU資源。因而乎上網搜索資源與教程,遺憾的是網上的教程要麼語焉不詳,要麼代碼不全,要麼只講表層不講原理。秉承本身動手豐衣足食的古訓,因而去翻閱參考手冊,從DMA章節看到定時器章節,結合代碼實戰,總算搞清些端倪,也分享一下在此過程當中遇到的問題。javascript

1. DMA簡介

官方釋義:DMA,全稱爲: Direct Memory Access,即直接存儲器訪問。 DMA 傳輸方式無需 CPU 直接控制傳輸,也沒有中斷處理方式那樣保留現場和恢復現場的過程,經過硬件爲 RAM 與 I/O 設備開闢一條直接傳送數據的通路, 能使 CPU 的效率大爲提升。下面這張圖是網上流行的摘自參考手冊上的DMA框圖。java

在這裏插入圖片描述
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。ui

/* Blocking mode: Polling */
HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
HAL_StatusTypeDef HAL_TIM_PWM_Stop(TIM_HandleTypeDef *htim, uint32_t Channel);
/* Non-Blocking mode: Interrupt */
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);
/* Non-Blocking mode: DMA */
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傳輸,不然它不會本身中止的。
spa

/* Callback in non blocking modes (Interrupt and 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爲例講解。debug

如圖,新建基於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燈珠。

相關文章
相關標籤/搜索