/* STM32 嵌入式學習入門(5)——PWM的實現 上一篇博文介紹了定時器和PWM的基本的原理,本篇博文從代碼層面來介紹PWM的具體實現。同樣,還是以博主所用的開發板——正點原子開發板STM32F103ZET6為例。 一、基於STM32的PWM輸出配置步驟(初始化操作): 1. 操作步驟(基於STM32固件庫、使用定時器3的PWM功能): (1)使能相關時鍾(定時器3和相關IO口時鍾。): //要使用什么外設就要先使能相關外設所掛載的時鍾,這些內容在最開始GPIO那塊就有提到STM32的GPIO介紹 ①使能定時器3時鍾:RCC_APB1PeriphClockCmd(); ②使能GPIOB時鍾:RCC_APB2PeriphClockCmd(); (2)初始化IO口為復用功能輸出。函數:GPIO_Init(); //同樣,IO口初始化的操作在STM32的GPIO介紹那篇文章里也說到了。 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //這里我們是要把引腳用作定時器的PWM輸出引腳,因此要重映射配置。所以需要開啟AFIO時鍾。同時設置重映射。 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); (3)初始化定時器:ARR,PSC等:TIM_TimeBaseInit();ARR寄存器在上一篇文章講原理時候已經說到了。PWM原理 (4)初始化輸出比較參數:TIM_OC2Init(); (5)使能預裝載寄存器: TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); (6)使能定時器。TIM_Cmd(); (7)不斷改變比較值CCRx,達到不同的占空比效果:TIM_SetCompare2(); 2.初始化源代碼: (1)以STM32F103ZET6為芯片的開發板的PWM初始化,這里只是初始化一個通道用作PWM輸出 */ //TIM3 PWM部分初始化 //PWM輸出初始化 //arr:自動重裝值 //psc:時鍾預分頻數 void TIM3_PWM_Init(u16 arr,u16 psc)//STM32F103ZET6 { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //使能定時器3時鍾 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_AFIO, ENABLE); //使能GPIO外設和AFIO復用功能模塊時鍾 GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); //Timer3部分重映射 TIM3_CH2->PB5 //設置該引腳為復用輸出功能,輸出TIM3 CH2的PWM脈沖波形 GPIOB.5 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //TIM_CH2 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //復用推挽輸出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIO //初始化TIM3 TIM_TimeBaseStructure.TIM_Period = arr; //設置在下一個更新事件裝入活動的自動重裝載寄存器周期的值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //設置用來作為TIMx時鍾頻率除數的預分頻值 TIM_TimeBaseStructure.TIM_ClockDivision = 0; //設置時鍾分割:TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上計數模式 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根據TIM_TimeBaseInitStruct中指定的參數初始化TIMx的時間基數單位 //初始化TIM3 CH2 PWM模式 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //選擇定時器模式:TIM脈沖寬度調制模式2 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比較輸出使能 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //輸出極性:TIM輸出比較極性高 TIM_OC2Init(TIM3, &TIM_OCInitStructure); //根據T指定的參數初始化外設TIM3 OC2 TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); //使能TIM3在CCR2上的預裝載寄存器 TIM_Cmd(TIM3, ENABLE); //使能TIM3 } /* (2)下面的代碼是博主做嵌入式循跡小車的項目中的PWM初始化,用了TIM1的兩個通道(通道1和通道4)去分別控制兩個驅動輪的轉速,從而實現讓小車轉向的功能。和上面的初始化代碼的不同主要在於小車項目中用了兩個通道,同時項目使用的開發板的芯片是STM32F103RCT6。在這里貼出源代碼,做一比較。 */ //PWM輸出初始化 //arr:自動重裝值 //psc:時鍾預分頻數 void TIM1_PWM_Init(u16 arr,u16 psc)//STM32F103RCT6 { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //使能GPIOA外設時鍾使能 //設置該引腳為復用輸出功能,輸出TIM1 CH1和CH4的PWM脈沖波形 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_11;//TIM_CH1 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //復用推挽輸出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = arr; //設置在下一個更新事件裝入活動的自動重裝載寄存器周期的值 80K TIM_TimeBaseStructure.TIM_Prescaler = psc; //設置用來作為TIMx時鍾頻率除數的預分頻值 不分頻 TIM_TimeBaseStructure.TIM_ClockDivision = 0; //設置時鍾分割:TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上計數模式 TIM_TimeBaseInit(TIM1,&TIM_TimeBaseStructure); //根據TIM_TimeBaseInitStruct中指定的參數初始化TIMx的時間基數單位 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //選擇定時器模式:TIM脈沖寬度調制模式2 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比較輸出使能 TIM_OCInitStructure.TIM_Pulse = 0; //設置待裝入捕獲比較寄存器的脈沖值 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //輸出極性:TIM輸出比較極性高 TIM_OC1Init(TIM1, &TIM_OCInitStructure); //根據TIM_OC1InitStruct中指定的參數初始化外設TIM1的通道1 TIM_OC4Init(TIM1, &TIM_OCInitStructure); //根據TIM_OC2InitStruct中指定的參數初始化外設TIM1的通道4 TIM_CtrlPWMOutputs(TIM1,ENABLE); //MOE 主輸出使能 TIM_OC1PreloadConfig(TIM1,TIM_OCPreload_Enable); //CH1預裝載使能 通道1 TIM_OC4PreloadConfig(TIM1,TIM_OCPreload_Enable); //CH4預裝載使能 通道4 TIM_ARRPreloadConfig(TIM1,ENABLE); //使能TIMx在ARR上的預裝載寄存器(x==1) TIM_Cmd(TIM1,ENABLE); //使能TIM1 } /* 初始化函數在主函數的開頭處調用,調用函數的兩個參數根據自己需要去設定。 二、PWM的操作: 上面的初始化結束后就可以利用相關引腳輸出PWM波了。這個輸出過程簡單說就是改變比較值CCRx的操作,就是往CCR寄存器里寫值。這一點上一篇文章里有分析到。下面先介紹一下往CCRx寄存器里寫值操作的函數(來自官方庫函數):TIM_SetCompare1(); */ /** * @brief Sets the TIMx Capture Compare1 Register value * @param TIMx: where x can be 1 to 17 except 6 and 7 to select the TIM peripheral. * @param Compare1: specifies the Capture Compare1 register new value. * @retval None */ void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1) { /* Check the parameters */ assert_param(IS_TIM_LIST8_PERIPH(TIMx));//有效性判斷 /* Set the Capture Compare1 Register value */ TIMx->CCR1 = Compare1; } /* 說明:和這個函數類似的一共有四個:TIM_SetCompare1(); TIM_SetCompare2(); TIM_SetCompare3(); TIM_SetCompare4();其實現框架都是一樣的,四個函數分別對應四個通道,使用哪個通道調用哪個函數,要對應起來。 下面先介紹還是以自己嵌入式循跡小車的項目為例,比如小車直行時就是在執行下面這個函數。 */ u8 Case_F_SIGN(void)//直行 { while(1) { delay_ms(10); pwmvalR=413; pwmvalL=413; TIM_SetCompare1(TIM1,pwmvalR); TIM_SetCompare4(TIM1,pwmvalL); t=SensorScan(); if(t!=F_SIGN) return t; } } /* pwmvalR和pwmvalL是兩個全局變量,它們的值是根據項目需要和小車硬件情況實際測出來的,然后調用兩個函數,寫入相關寄存器,,下面是調用SensorScan();看小車是不是還沿着軌跡在行駛,如果不是,那么跳出while(1)循環,否則,返回主調函數當前小車的狀態。這段代碼現在看寫得不是特別好了,但是說明這個函數的用法還是沒問題的。 高級控制定時器(TIM1 和TIM8) TIM1和TIM8定時器的功能包括: ● 16位向上、向下、向上/ 下自動裝載計數器 ● 16位可編程( 可以實時修改)預分頻器,計數器時鍾頻率的分頻系數為1~65535 之間的任意數值 ● 多達4個獨立通道: ─ 輸入捕獲 ─ 輸出比較 ─ PWM生成(邊緣或中間對齊模式) ─ 單脈沖模式輸出 ● 死區時間可編程的互補輸出 ● 使用外部信號控制定時器和定時器互聯的同步電路 ● 允許在指定數目的計數器周期之后更新定時器寄存器的重復計數器 ● 剎車輸入信號可以將定時器輸出信號置於復位狀態或者一個已知狀態 ● 如下事件發生時產生中斷/DMA : ─ 更新:計數器向上溢出/ 向下溢出,計數器初始化(通過軟件或者內部/ 外部觸發) ─ 觸發事件(計數器啟動、停止、初始化或者由內部/ 外部觸發計數) ─ 輸入捕獲 ─ 輸出比較 ─ 剎車信號輸入 ● 支持針對定位的增量(正交)編碼器和霍爾傳感器電路 ● 觸發輸入作為外部時鍾或者按周期的電流管理 通用定時器(TIMx) 通用TIMx (TIM2、TIM3、TIM4和TIM5)定時器功能包括: ● 16位向上、向下、向上/ 向下自動裝載計數器 ● 16位可編程( 可以實時修改)預分頻器,計數器時鍾頻率的分頻系數為1~65536 之間的任意數值 ● 4個獨立通道: ─ 輸入捕獲 ─ 輸出比較 ─ PWM生成(邊緣或中間對齊模式) ─ 單脈沖模式輸出 ● 使用外部信號控制定時器和定時器互連的同步電路 ● 如下事件發生時產生中斷/DMA : ─ 更新:計數器向上溢出/ 向下溢出,計數器初始化(通過軟件或者內部/ 外部觸發) ─ 觸發事件(計數器啟動、停止、初始化或者由內部/ 外部觸發計數) ─ 輸入捕獲 ─ 輸出比較 ● 支持針對定位的增量(正交)編碼器和霍爾傳感器電路 ● 觸發輸入作為外部時鍾或者按周期的電流管理 基本定時器(TIM6 和TIM7) TIM6和TIM7定時器的主要功能包括: ● 16位自動重裝載累加計數器 ● 16位可編程( 可實時修改)預分頻器,用於對輸入的時鍾按系數為1~65536 之間的任意數值 分頻 ● 觸發DAC的同步電路 ● 在更新事件(計數器溢出)時產生中斷/DMA 請求 STM32的通用定時器是一個通過可編程預分頻器(PSC)驅動的16 位自動裝載計數器(CNT)構成。STM32的通用定時器可以被用於:測量輸入信號的脈沖長度(輸入捕獲)或者產生輸出波形(輸出比較和PWM)等。 使用定時器預分頻器和RCC時鍾控制器預分頻器,脈沖長度和波形周期可以在幾個微秒到幾個毫秒間調整。STM32的每個通用定時器都是完全獨立的,沒有互相共享的任何資源。 脈沖寬度調制(PWM),是英文“Pulse Width Modulation”的縮寫,簡稱脈寬調制,是利用微處理器的數字輸出來對模擬電路進行控制的一種非常有效的技術。簡單一點,就是對脈沖寬度的控制。STM32的定時器除了TIM6和7。其他的定時器都可以用來產生PWM輸出。其中高級定時器TIM1和TIM8可以同時產生多達7路的PWM輸出。而通用定時器也能同時產生多達4路的PWM輸出,這樣,STM32最多可以同時產生30路PWM輸出! 要使STM32的通用定時器TIMx產生PWM輸出,除了定時器介紹的寄存器外,我們還會用到3 個寄存器,來控制PWM 的。這三個寄存器分別是:捕獲/比較模式寄存器(TIMx_CCMR1/2)、捕獲/比較使能寄存器(TIMx_CCER)、捕獲/比較寄存器(TIMx_CCR1~4)。 TIM3_CH2默認是接在PA7面的,而我們的DS0接在PB5上面,如果普通MCU,可能就只能用飛線把PA7飛到PB5上來實現了,不過,我們用的是STM32,它比較高級,可以通過重映射功能,把TIM3_CH2映射到PB5上。 STM32的重映射控制是由復用重映射和調試IO 配置寄存器(AFIO_MAPR)控制的。 1)開啟TIM3時鍾以及復用功能時鍾,配置PB5為復用輸出。 要使用TIM3,我們必須先開啟TIM3的時鍾,還要配置PB5為復用輸出,這是因為TIM3_CH2通道將重映射到PB5上,此時,PB5屬於復用功能輸出。 庫函數使能TIM3時鍾的方法是: RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //使能定時器3時鍾 庫函數設置AFIO時鍾的方法是: RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //復用時鍾使能 設置PB5為復用功能輸出的方法:GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //復用推挽輸出 2)設置TIM3_CH2重映射到PB5上。 因為TIM3_CH2默認是接在PA7上的,所以我們需要設置TIM3_REMAP為部分重映射(通過AFIO_MAPR配置),讓TIM3_CH2重映射到PB5上面。在庫函數函數里面設置重映射的函數是: void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState); STM32重映射只能重映射到特定的端口。第一個入口參數可以理解為設置重映射的類型,比如TIM3部分重映射入口參數為 GPIO_PartialRemap_TIM3,這點可以顧名思義了。所以TIM3部分重映射的庫函數實現方法是: GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); 3)初始化TIM3,設置TIM3的ARR和PSC。 在開啟了TIM3的時鍾之后,我們要設置ARR和PSC兩個寄存器的值來控制輸出PWM的周期。當PWM周期太慢(低於50Hz)的時候,我們就會明顯感覺到閃爍了。因此,PWM周期在這里不宜設置的太小。這在庫函數是通過TIM_TimeBaseInit函數實現的,在上一節定時器中斷章節我們已經有講解,這里就不詳細講解,調用的格式為: TIM_TimeBaseStructure.TIM_Period = arr; //設置自動重裝載值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //設置預分頻值 TIM_TimeBaseStructure.TIM_ClockDivision = 0; //設置時鍾分割:TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上計數模式 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根據指定的參數初始化TIMx的 4)設置TIM3_CH2的PWM模式,使能TIM3的CH2輸出。 接下來,我們要設置TIM3_CH2為PWM模式(默認是凍結的),因為我們的DS0是低電平亮,而我們希望當CCR2的值小的時候,DS0就暗,CCR2值大的時候,DS0就亮,所以我們要通過配置TIM3_CCMR1的相關位來控制TIM3_CH2的模式。在庫函數中,PWM通道設置是通過函數TIM_OC1Init()~TIM_OC4Init()來設置的,不同的通道的設置函數不一樣,這里我們使用的是通道2,所以使用的函數是TIM_OC2Init()。 void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct); 5)使能TIM3。 在完成以上設置了之后,我們需要使能TIM3。使能TIM3的方法前面已經講解過: TIM_Cmd(TIM3, ENABLE); //使能TIM3 6)修改TIM3_CCR2來控制占空比。 最后,在經過以上設置之后,PWM其實已經開始輸出了,只是其占空比和頻率都是固定的,而我們通過修改TIM3_CCR2則可以控制CH2的輸出占空比。繼而控制DS0的亮度。 在庫函數中,修改TIM3_CCR2占空比的函數是: void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2); 理所當然,對於其他通道,分別有一個函數名字,函數格式為TIM_SetComparex(x=1,2,3,4)。 通過以上6個步驟,我們就可以控制TIM3的CH2輸出PWM波了。 */
