上一章,我們介紹了 STM32 的通用定時器 TIM3,用該定時器的中斷來控制 DS1 的
閃爍,這一章,我們將向大家介紹如何使用 STM32 的 TIM3 來產生 PWM 輸出。在
本章中,我們將使用 TIM3 的通道 2,並把通道 2 重映射到 PB5,產生 PWM 來控
制 DS0 的亮度。本章分為如下幾個部分:
14.1 PWM 簡介
14.2 硬體設計
14.3 軟體設計
14.4 下載驗證
14.1 PWM 簡介
脈衝寬度調製(PWM),是英文「Pulse Width Modulation」的縮寫,簡稱脈寬調
制,是利用微處理器的數字輸出來對模擬電路進行控制的一種非常有效的技術。簡單
一點,就是對脈衝寬度的控制,PWM 原理如圖 14.1.1 所示:
圖 14.1.1 就是一個簡單的 PWM 原理示意圖。圖中,我們假定定時器工作在向上計數 PWM
模式,且當 CNT<CCRx 時,輸出 0,當 CNT>=CCRx 時輸出 1。那麼就可以得到如上的 PWM
示意圖:當 CNT 值小於 CCRx 的時候,IO 輸出低電平(0),當 CNT 值大於等於 CCRx 的時候,
IO 輸出高電平(1),當 CNT 達到 ARR 值的時候,重新歸零,然後重新向上計數,依次循環。
改變 CCRx 的值,就可以改變 PWM 輸出的佔空比,改變 ARR 的值,就可以改變 PWM 輸出的
頻率,這就是 PWM 輸出的原理。
STM32 的定時器除了 TIM6 和 7。其他的定時器都可以用來產生 PWM 輸出。其中高級定
時器 TIM1 和 TIM8 可以同時產生多達 7 路的 PWM 輸出。而通用定時器也能同時產生多達 4
路的 PWM 輸出,這樣,STM32 最多可以同時產生 30 路 PWM 輸出!這裡我們僅使用 TIM3
的 CH2 產生一路 PWM 輸出。如果要產生多路輸出,大家可以根據我們的代碼稍作修改即可。
要使 STM32 的通用定時器 TIMx 產生 PWM 輸出,除了上一章介紹的寄存器外,我們還會
用到 3 個寄存器,來控制 PWM 的。這三個寄存器分別是:捕獲/比較模式寄存器
(TIMx_CCMR1/2)、捕獲/比較使能寄存器(TIMx_CCER)、捕獲/比較寄存器(TIMx_CCR1~4)。
接下來我們簡單介紹一下這三個寄存器。
首先是捕獲/比較模式寄存器(TIMx_CCMR1/2),該寄存器總共有 2 個,TIMx _CCMR1
和 TIMx _CCMR2。TIMx_CCMR1 控制 CH1 和 2,而 TIMx_CCMR2 控制 CH3 和 4。該寄存器
的各位描述如圖 14.1.2 所示:
該寄存器的有些位在不同模式下,功能不一樣,所以在圖 14.1.2 中,我們把寄存器分了 2
層,上面一層對應輸出而下面的則對應輸入。關於該寄存器的詳細說明,請參考《STM32 中文
參考手冊》第 288 頁,14.4.7 一節。這裡我們需要說明的是模式設置位 OCxM,此部分由 3 位
組成。總共可以配置成 7 種模式,我們使用的是 PWM 模式,所以這 3 位必須設置為 110/111。
這兩種 PWM 模式的區別就是輸出電平的極性相反。另外 CCxS 用於設置通道的方向(輸入/輸
出)默認設置為 0,就是設置通道作為輸出使用。
接下來,我們介紹捕獲/比較使能寄存器(TIMx_CCER),該寄存器控制著各個輸入輸出通
道的開關。該寄存器的各位描述如圖 14.1.3 所示:
該寄存器比較簡單,我們這裡只用到了 CC2E 位,該位是輸入/捕獲 2 輸出使能位,要想
PWM 從 IO 口輸出,這個位必須設置為 1,所以我們需要設置該位為 1。該寄存器更詳細的介
紹了,請參考《STM32 中文參考手冊》第 292 頁,14.4.9 這一節。
最後,我們介紹一下捕獲/比較寄存器(TIMx_CCR1~4),該寄存器總共有 4 個,對應 4 個
輸通道 CH1~4。因為這 4 個寄存器都差不多,我們僅以 TIMx_CCR1 為例介紹,該寄存器的各
位描述如圖 14.1.4 所示:
在輸出模式下,該寄存器的值與 CNT 的值比較,根據比較結果產生相應動作。利用這點,
我們通過修改這個寄存器的值,就可以控制 PWM 的輸出脈寬了。本章,我們使用的是 TIM3
的通道 2,所以我們需要修改 TIM3_CCR2 以實現脈寬控制 DS0 的亮度。
我們要使用 TIM3 的 CH2 輸出 PWM 來控制 DS0 的亮度,但是 TIM3_CH2 默認是接在 PA7
上面的,而我們的 DS0 接在 PB5 上面,如果普通 MCU,可能就只能用飛線把 PA7 飛到 PB5
上來實現了,不過,我們用的是 STM32,它比較高級,可以通過重映射功能,把 TIM3_CH2
映射到 PB5 上。
STM32 的重映射控制是由復用重映射和調試 IO 配置寄存器(AFIO_MAPR)控制的,該
寄存器的各位描述如圖 14.1.5 所示:
我們這裡用到的是 TIM3 的重映射,從上圖可以看出,TIM3_REMAP 是由[11:10]這
2 個位控制的。TIM3_REMAP[1:0]重映射控制表如表 14.1.1 所示:
默認條件下,TIM3_REMAP[1:0]為 00,是沒有重映射的,所以 TIM3_CH1~TIM3_CH4 分
別是接在 PA6、PA7、PB0 和 PB1 上的,而我們想讓 TIM3_CH2 映射到 PB5 上,則需要設置
TIM3_REMAP[1:0]=10,即部分重映射,這裡需要注意,此時 TIM3_CH1 也被映射到 PB4 上了。
至此,我們把本章要用的幾個相關寄存器都介紹完了,本章要實現通過重映射 TIM3_CH2
到 PB5 上,由 TIM3_CH2 輸出 PWM 來控制 DS0 的亮度。下面我們介紹配置步驟:
首先要提到的是,PWM 實際跟上一章節一樣使用的是定時器的功能,所以相關的函數設
置同樣在庫函數文件 stm32f1xx_hal_tim.h 和 stm32f1xx_hal_tim.c 文件中。
1)開啟 TIM3 和 GPIO 時鐘,配置 PB5 選擇復用功能輸出。
要使用 TIM3,我們必須先開啟 TIM3 的時鐘,這點相信大家看了這麼多代碼,應該明白了。
這裡我們還要配置復用輸出,才可以實現TIM3_CH2的PWM經過PB5輸出。HAL庫使能TIM3
時鐘和 GPIO 時鐘方法是:
__HAL_RCC_TIM3_CLK_ENABLE(); //使能 TIM3 時鐘
__HAL_RCC_GPIOB_CLK_ENABLE (); //開啟 GPIOB 時鐘
接下來便是要配置 PB5 復用映射為 TIM3 的 PWM 輸出引腳。關於 IO 口復用映射,在串口
通信實驗中有詳細講解,主要是通過函數 HAL_GPIO_Init 來實現的:
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_TIM3_CLK_ENABLE(); //使能定時器 3
__HAL_AFIO_REMAP_TIM3_PARTIAL(); //TIM3 通道引腳部分重映射使能
__HAL_RCC_GPIOB_CLK_ENABLE(); //開啟 GPIOB 時鐘
GPIO_Initure.Pin=GPIO_PIN_5; //PB5
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //復用推輓輸出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;//高速
HAL_GPIO_Init(GPIOB,&GPIO_Initure);
2)初始化 TIM3,設置 TIM3 的 ARR 和 PSC 等參數。
根據前面的講解,初始化定時器的 ARR 和 PSC 等參數是通過函數 HAL_TIM_Base_Init 來
實現的,但是這裡大家要注意,對於我們使用定時器的 PWM 輸出功能時,HAL 庫為我們提供
了一個獨立的定時器初始化函數 HAL_TIM_PWM_Init,該函數聲明為:
HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);
該函數實現的功能以及使用方法和 HAL_TIM_Base_Init 都是類似的,作用都是初始化定時
器 的 ARR 和 PSC 等參 數 。 為 什 麼 HAL 庫要 提 供 這 個 函 數 而 不直 接 讓 我 們 使 用
HAL_TIM_Base_Init 函數呢?
這 是 因 為 HAL 庫 為 定 時 器 的 PWM 輸 出 定 義 了 單 獨 的 MSP 回 調 函 數
HAL_TIM_PWM_MspInit,也就是說,當我們調用HAL_TIM_PWM_Init進行PWM初始化之後,
該函數內部會調用 MSP 回調函數 HAL_TIM_PWM_MspInit。而當我們使用 HAL_TIM_Base_Init
初始化定時器參數的時候,它內部調用的回調函數為 HAL_TIM_Base_MspInit,這裡大家注意
區分。
所以大家一定要注意,使用 HAL_TIM_PWM_Init 初始化定時器時,回調函數為:
HAL_TIM_PWM_MspInit,該函數聲明為:
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim);
一般情況下,上面步驟 1 的時鐘使能和 IO 口初始化映射都編寫在回調函數內部。
3)設置 TIM3_CH2 的 PWM 模式,輸出比較極性,比較值等參數。
接下來,我們要設置 TIM3_CH2 為 PWM 模式(默認是凍結的),因為我們的 DS0 是低電
平亮,而我們希望當 CCR2 的值小的時候,DS0 就暗,CCR2 值大的時候,DS0 就亮,所以我
們要通過配置 TIM3_CCMR1 的相關位來控制 TIM3_CH2 的模式。
在 HAL 庫中,PWM 通道設置是通過函數 HAL_TIM_PWM_ConfigChannel 來設置的:
HAL_StatusTypeDef HAL_TIM_PWM_ConfigChannel(TIM_HandleTypeDef *htim,
TIM_OC_InitTypeDef* sConfig, uint32_t Channel);
第一個參數 htim 是定時器初始化句柄,也就是 TIM_HandleTypeDef 結構體指針類型,這
和 HAL_TIM_PWM_Init 函數調用時候參數保存一致即可。
第二個參數 sConfig 是 TIM_OC_InitTypeDef 結構體指針類型,這也是該函數最重要的參數。
該參數用來設置 PWM 輸出模式,極性,比較值等重要參數。首先我們來看看結構體定義:
typedef struct
{
uint32_t OCMode; //PWM 模式
uint32_t Pulse; //捕獲比較值
uint32_t OCPolarity; //極性
uint32_t OCNPolarity; //快速模式
uint32_t OCIdleState;
uint32_t OCFastMode;
uint32_t OCNIdleState;
} TIM_OC_InitTypeDef;
該結構體成員我們重點關注前三個。成員變量 OCMode 用來設置模式,也就是我們前面講解的
7 種模式,這裡我們設置為 PWM 模式 2。成員變量 Pulse 用來設置捕獲比較值。成員變量
TIM_OCPolarity 用 來 設 置 輸 出 極 性 是 高 還 是 低 。 其 他 的 參 數 TIM_OutputNState ,
TIM_OCNPolarity,TIM_OCIdleState 和 TIM_OCNIdleState 是高級定時器才用到的。
第 三 個 參 數 Channel 用 來 選 擇 定 時 器 的 通 道 , 取 值 範 圍 為 TIM_CHANNEL_1~
TIM_CHANNEL_4。這裡我們使用的是定時器3的通道2,所以取值為TIM_CHANNEL_2即可。
例如我們要初始化定時器 3 的通道 2 為 PWM 模式 1,輸出極性為低,那麼實例代碼為:
TIM_OC_InitTypeDef TIM3_CH2Handler; //定時器 3 通道 2 句柄
TIM3_CH2Handler.OCMode=TIM_OCMODE_PWM1; //模式選擇 PWM1
TIM3_CH2Handler.Pulse=arr/2; //設置比較值,此值用來確定佔空比,
//默認比較值為自動重裝載值的一半,即佔空比為 50%
TIM3_CH2Handler.OCPolarity=TIM_OCPOLARITY_LOW; //輸出比較極性為低
HAL_TIM_PWM_ConfigChannel(&TIM3_Handler,&TIM3_CH2Handler,TIM_CHANNEL_2);
//配置 TIM3 通道 2
4)使能 TIM3,使能 TIM3 的 CH2 輸出。
在完成以上設置了之後,我們需要使能 TIM3。使能 TIM3 的方法前面已經講解過:
HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
該函數第二個入口參數 Channel 是用來設置要使能輸出的通道號。
對於單獨使能定時器的方法,在上一章定時器實驗我們已經講解。實際上,HAL 庫也同樣
提供了單獨使能定時器的輸出通道函數,函數為:
void TIM_CCxChannelCmd(TIM_TypeDef* TIMx, uint32_t Channel, uint32_t ChannelState);
5)修改 TIM3_CCR2 來控制佔空比。
最後,在經過以上設置之後,PWM 其實已經開始輸出了,只是其佔空比和頻率都是固定
的,而我們通過修改比較值TIM3_CCR2 則可以控制CH2的輸出佔空比。繼而控制DS0 的亮度。
HAL 庫中並沒有提供獨立的修改佔空比函數,這裡我們可以編寫這樣一個函數如下:
//設置 TIM 通道 2 的佔空比
//compare:比較值
void TIM_SetTIM3Compare2 (u32 compare)
{
TIM3->CCR2=compare;
}
實際上,因為調用函數 HAL_TIM_PWM_ConfigChanne 進行 PWM 配置的時候可以設置比
較值,所以我們也可以直接使用該函數來達到修改佔空比的目的:
void TIM_SetCompare1(TIM_TypeDef *TIMx,u32 compare)
{
TIM3_CH2Handler.Pulse=compare;
HAL_TIM_PWM_ConfigChannel(&TIM3_Handler,&TIM3_CH2Handler,
TIM_CHANNEL_2);
}
這種方法因為要調用 HAL_TIM_PWM_ConfigChannel 函數對各種初始化參數進行重新設
置,所以大家在使用中一定要注意,例如在實時系統中如果多個線程同時修改初始化結構體相
關參數,可能導致結果混亂。
14.2 硬體設計
本實驗用到的硬體資源有:
1) 指示燈 DS0
2) 定時器 TIM3
這兩個前面都有介紹,但是我們這裡用到了 TIM3 的部分重映射功能,把 TIM3_CH2 直接
映射到了 PB5 上,而通過前面的學習,我們知道 PB5 和 DS0 是直接連接的,所以電路上並沒
有任何變化。
14.3 軟體設計
打開 PWM 輸出實驗工程可以看到,我們相比上一節,並沒有添加其他任何 HAL 庫文件,
因為 PWM 是使用的定時器資源,所以跟上一講使用的是同樣的 HAL 庫文件。同時我們修改了
timer.c 和 timer.h 的內容,刪掉了上一章實驗源碼,直接把 PWM 功能相關函數和定義放在了這
兩個文件中。
timer.c 源文件代碼如下:
TIM_HandleTypeDef TIM3_Handler; //定時器句柄
TIM_OC_InitTypeDef TIM3_CH2Handler; //定時器 3 通道 2 句柄
//TIM3 PWM 部分初始化
//arr:自動重裝值。
//psc:時鐘預分頻數
//定時器溢出時間計算方法:Tout=((arr+1)*(psc+1))/Ft us.
//Ft=定時器工作頻率,單位:Mhz
void TIM3_PWM_Init(u16 arr,u16 psc)
{
TIM3_Handler.Instance=TIM3; //定時器 3
TIM3_Handler.Init.Prescaler=psc; //定時器分頻
TIM3_Handler.Init.CounterMode=TIM_COUNTERMODE_UP;//向上計數模式
TIM3_Handler.Init.Period=arr; //自動重裝載值
TIM3_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&TIM3_Handler); //初始化 PWM
TIM3_CH2Handler.OCMode=TIM_OCMODE_PWM1; //模式選擇 PWM1
TIM3_CH2Handler.Pulse=arr/2; //設置比較值,此值用來確定佔空比,
//默認比較值為自動重裝載值的一半,即佔空比為 50%
TIM3_CH2Handler.OCPolarity=TIM_OCPOLARITY_LOW; //輸出比較極性為低
HAL_TIM_PWM_ConfigChannel(&TIM3_Handler,&TIM3_CH2Handler,TIM_CHANNEL_2);
//配置 TIM3 通道 2
HAL_TIM_PWM_Start(&TIM3_Handler,TIM_CHANNEL_2);//開啟 PWM 通道 2
}
//定時器底冊驅動,開啟時鐘,設置中斷優先級
//此函數會被 HAL_TIM_Base_Init()函數調用
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM3)
{
__HAL_RCC_TIM3_CLK_ENABLE(); //使能 TIM3 時鐘
HAL_NVIC_SetPriority(TIM3_IRQn,1,3);
//設置中斷優先級,搶佔優先級 1,子優先級 3
HAL_NVIC_EnableIRQ(TIM3_IRQn); //開啟 ITM3 中斷
}
}
//定時器底層驅動,時鐘使能,引腳配置
//此函數會被 HAL_TIM_PWM_Init()調用
//htim:定時器句柄
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
GPIO_InitTypeDef GPIO_Initure;
if(htim->Instance==TIM3)
{
__HAL_RCC_TIM3_CLK_ENABLE(); //使能定時器 3
__HAL_AFIO_REMAP_TIM3_PARTIAL(); //TIM3 通道引腳部分重映射使能
__HAL_RCC_GPIOB_CLK_ENABLE(); //開啟 GPIOB 時鐘
GPIO_Initure.Pin=GPIO_PIN_5; //PB5
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //復用推輓輸出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH; //高速
HAL_GPIO_Init(GPIOB,&GPIO_Initure);
}
}
//設置 TIM 通道 2 的佔空比
//compare:比較值
void TIM_SetTIM3Compare2(u32 compare)
{
TIM3->CCR2=compare;
}
此部分代碼包含三個函數,完全實現了前面 13.1 小節講解的 5 個配置步驟。第一個函數
TIM3_PWM_Init 實現的是 13.1 小節講解的步驟 2-4,首先通過調用定時器 HAL 庫函數
HAL_TIM_PWM_Init 初始化 TIM3 並設置 TIM3 的 ARR 和 PSC 等參數,其次通過調用函數
HAL_TIM_PWM_ConfigChannel 設置 TIM3_CH4 的 PWM 模式以及比較值等參數,最後通過調
用函數 HAL_TIM_PWM_Start 來使能 TIM3 以及使能 PWM 通道 TIM3_CH4 輸出。第二個函數
HAL_TIM_PWM_MspInit 是 PWM 的 MSP 初始化回調函數,該函數實現的是 13.1 小節步驟 1,
主要是使能相應時鐘以及初始化定時器通道 TIM3_CH4 對應的 IO 口模式,同時設置復用映射
關係。第三個函數 TIM_SetTIM3Compare 4 是用戶自定義的設置比較值函數,這在我們 13.1 小
節步驟 5 有詳細講解。
接下來,我們看看 main 函數內容如下:
int main(void)
{
u8 dir=1;
u16 led0pwmval=0;
HAL_Init(); //初始化 HAL 庫
Stm32_Clock_Init(RCC_PLL_MUL9); //設置時鐘,72M
delay_init(72); //初始化延時函數
uart_init(115200); //初始化串口
LED_Init(); //初始化 LED
KEY_Init(); //初始化按鍵
TIM3_PWM_Init(500-1,72-1); //72M/72=1M 的計數頻率,自動重裝載為 500,
//那麼 PWM 頻率為 1M/500=2kHZ
while(1)
{
delay_ms(10);
if(dir)led0pwmval++; //dir==1 led0pwmval 遞增
else led0pwmval--; //dir==0 led0pwmval 遞減
if(led0pwmval>300)dir=0; //led0pwmval 到達 300 後,方向為遞減
if(led0pwmval==0)dir=1; //led0pwmval 遞減到 0 後,方向改為遞增
TIM_SetTIM3Compare2(led0pwmval);//修改比較值,修改佔空比
}
}
這裡,我們從死循環函數可以看出,我們控制 LED0_PWM_VAL 的值從 0 變到 300,然後
又從 300 變到 0,如此循環,因此 DS0 的亮度也會跟著從暗變到亮,然後又從亮變到暗。至於
這裡的值,我們為什麼取 300,是因為 PWM 的輸出佔空比達到這個值的時候,我們的 LED 亮
度變化就不大了(雖然最大值可以設置到 499),因此設計過大的值在這裡是沒必要的。至此,
我們的軟體設計就完成了。
14.4 下載驗證
在完成軟體設計之後,將我們將編譯好的文件下載到戰艦 STM32 V3 上,觀看其運行結果
是否與我們編寫的一致。如果沒有錯誤,我們將看 DS0 不停的由暗變到亮,然後又從亮變到暗。
每個過程持續時間大概為 3 秒鐘左右。
實際運行結果如下圖 14.4.1 所示: