動SG90舵機(jī):TIM PWM頻率與占空比映射實(shí)戰(zhàn))
簡介這份源碼面向嵌入式初學(xué)者與需要快速驗(yàn)證舵機(jī)控制的開發(fā)者圍繞STM32F103C8T6驅(qū)動SG90舵機(jī)這一典型場景提供可直接參考的PWM控制實(shí)現(xiàn)。資源包共4個文件以2個C源文件與1個頭文件為核心分別承擔(dān)舵機(jī)角度控制邏輯、初始化配置與接口聲明另附1張舵機(jī)接口圖輔助接線理解壓縮包約24KB體量輕便、便于移植到自己的工程中。內(nèi)容涉及定時器PWM模式配置、GPIO復(fù)用推挽輸出、預(yù)分頻與自動重裝載寄存器設(shè)置以及按角度換算脈寬占空比等關(guān)鍵環(huán)節(jié)并給出初始化與角度設(shè)定函數(shù)的組織方式方便讀者對照修改周期與CCR值。目前已有5074人學(xué)習(xí)下載適合作為機(jī)器人、遙控模型等項(xiàng)目中舵機(jī)驅(qū)動的入門參考也能幫助讀者理解PWM信號與角度之間的對應(yīng)關(guān)系并在此基礎(chǔ)上擴(kuò)展多路舵機(jī)或中斷更新方案。1. 從一次舵機(jī)抖動的排查說起STM32F103C8T6 驅(qū)動 SG90 到底該怎么寫很多人第一次用 STM32F103C8T6 點(diǎn) SG90 舵機(jī)都會遇到同一個玄學(xué)現(xiàn)象明明代碼里寫了 50Hz 的 PWM舵機(jī)要么紋絲不動要么像抽風(fēng)一樣來回抖偶爾還伴隨電源電壓被拉低導(dǎo)致單片機(jī)復(fù)位。問題往往不在舵機(jī)本身而在于 PWM 頻率、占空比映射和供電這三件事沒對齊。這份源碼就是圍繞這三個痛點(diǎn)展開的用 TIM 定時器輸出 50Hz PWM把 0.5ms2.5ms 脈寬映射成 0°180°再配合獨(dú)立的 5V 供電讓 SG90 穩(wěn)定工作。它適合剛上手 STM32 標(biāo)準(zhǔn)外設(shè)庫、想快速把舵機(jī)跑起來或者準(zhǔn)備做機(jī)械臂、云臺、智能小車這類需要多路舵機(jī)控制的開發(fā)者。2. 項(xiàng)目定位與技術(shù)選型為什么是 TIM PWM 而不是軟件延時2.1 硬件與軟件棧的搭配邏輯STM32F103C8T6 是 Cortex-M3 內(nèi)核主頻 72MHz有 4 個通用定時器 TIM1TIM4其中 TIM1 是高級定時器TIM2TIM4 是通用定時器。SG90 屬于模擬舵機(jī)控制信號是周期 20ms、高電平 0.5ms2.5ms 的 PWM 波。用硬件定時器輸出 PWMCPU 只需要初始化一次之后完全不占用主循環(huán)這是它比軟件延時或軟件 PWM 更靠譜的根本原因。源碼采用 STM32 標(biāo)準(zhǔn)外設(shè)庫不是 HAL 庫。標(biāo)準(zhǔn)庫的寄存器操作更直接代碼體積小對 F103C8T6 這種 64KB Flash、20KB RAM 的芯片更友好。常見做法是把 TIM2 或 TIM3 的某個通道映射到 PA0PA3 或 PB0PB1 這類支持定時器復(fù)用的引腳上通過 GPIO 復(fù)用推挽輸出把 PWM 送出去。選型上還有幾個細(xì)節(jié)值得說清楚。第一SG90 的工作電壓是 4.8V6V而 STM32 的 IO 輸出是 3.3VPWM 信號線可以直接接但舵機(jī)電源必須單獨(dú)供 5V不能從單片機(jī) 3.3V 穩(wěn)壓器上取電。第二多路舵機(jī)時最好用同一個定時器的不同通道這樣四路 PWM 的頻率和相位天然一致代碼也好維護(hù)。第三如果項(xiàng)目里還要跑串口、I2C 等外設(shè)注意定時器通道和這些外設(shè)的引腳不要沖突。2.2 目錄結(jié)構(gòu)與文件職責(zé)這份源碼的目錄結(jié)構(gòu)比較典型按標(biāo)準(zhǔn)庫工程的慣例組織SG90_STM32F103/ ├── CMSIS/ # 內(nèi)核啟動文件與系統(tǒng)初始化 ├── FWLIB/ │ ├── inc/ # 標(biāo)準(zhǔn)外設(shè)庫頭文件 │ └── src/ # 標(biāo)準(zhǔn)外設(shè)庫源文件 ├── USER/ │ ├── main.c # 主程序舵機(jī)角度控制邏輯 │ ├── stm32f10x_it.c # 中斷服務(wù)函數(shù) │ └── system_stm32f10x.c # 系統(tǒng)時鐘配置 ├── HARDWARE/ │ ├── TIMER/ │ │ ├── timer.c # PWM 初始化與占空比設(shè)置 │ │ └── timer.h │ └── LED/ # 可選用于狀態(tài)指示 └── OBJ/ # 編譯輸出目錄USER 目錄放業(yè)務(wù)邏輯HARDWARE 目錄放外設(shè)驅(qū)動這種分層讓改舵機(jī)引腳或換定時器時只需要動 HARDWARE/TIMER 下的文件main.c 里的角度控制邏輯基本不用改。如果你要加第二路、第三路舵機(jī)擴(kuò)展點(diǎn)也集中在這里。2.3 關(guān)鍵參數(shù)速查參數(shù)取值說明PWM 周期20ms對應(yīng) 50Hz舵機(jī)標(biāo)準(zhǔn)控制頻率高電平范圍0.5ms2.5ms對應(yīng) 0°180°定時器時鐘72MHzAPB1 定時器倍頻后預(yù)分頻系數(shù)72-1得到 1MHz 計數(shù)頻率自動重裝載值20000-11MHz 下計 20000 次為 20ms比較值范圍5002500對應(yīng) 0.5ms2.5ms這張表是后面代碼里所有計算的依據(jù)改頻率或改角度范圍時對著它調(diào)就行。3. 核心模塊拆解TIM PWM 初始化與角度映射的完整鏈路3.1 定時器初始化從時鐘使能到 PWM 輸出先看 TIM3 通道 1 的初始化代碼這是整個項(xiàng)目最核心的一段#include timer.h // 通用定時器3通道1的PWM初始化 // arr: 自動重裝載值 // psc: 時鐘預(yù)分頻數(shù) void TIM3_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 1. 使能TIM3和GPIOA時鐘 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 2. 配置PA6為復(fù)用推挽輸出TIM3_CH1默認(rèn)映射在PA6 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 定時器基礎(chǔ)配置 TIM_TimeBaseStructure.TIM_Period arr; // 自動重裝載值 TIM_TimeBaseStructure.TIM_Prescaler psc; // 預(yù)分頻值 TIM_TimeBaseStructure.TIM_ClockDivision 0; // 時鐘分割 TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; // 向上計數(shù) TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 4. PWM模式配置通道1 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; // PWM模式1 TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; // 初始比較值 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; // 高電平有效 TIM_OC1Init(TIM3, TIM_OCInitStructure); // 5. 使能預(yù)裝載寄存器 TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); // 6. 啟動定時器 TIM_Cmd(TIM3, ENABLE); }這段代碼的邏輯鏈條是先開時鐘再配 GPIO 復(fù)用然后設(shè)定時器時基接著配 PWM 通道最后啟動。每一步的順序不能亂尤其是時鐘使能必須放在 GPIO 和定時器配置之前否則配置寫不進(jìn)去。參數(shù)說明arr和psc由調(diào)用方傳入。在 main 里調(diào)用TIM3_PWM_Init(19999, 71)代入公式PWM頻率 72MHz / ((psc1) * (arr1))得到72000000 / (72 * 20000) 50Hz正好是舵機(jī)需要的頻率。TIM_Pulse就是比較值 CCR決定高電平時間后面改角度就是改這個值。3.2 角度到占空比的映射函數(shù)初始化完成后控制角度就是改 CCR 的值。寫一個映射函數(shù)把 0°180° 轉(zhuǎn)成 5002500 的比較值// 設(shè)置舵機(jī)角度范圍0~180度 // 角度0度對應(yīng)0.5ms高電平180度對應(yīng)2.5ms高電平 void SG90_SetAngle(float angle) { u16 ccr; // 限幅防止超出舵機(jī)機(jī)械行程 if (angle 0) angle 0; if (angle 180) angle 180; // 線性映射angle / 180 * (2500 - 500) 500 ccr (u16)(500 angle * (2000.0f / 180.0f)); TIM_SetCompare1(TIM3, ccr); }映射公式是線性的0° 對應(yīng) 500180° 對應(yīng) 2500中間按比例插值。用浮點(diǎn)算是為了角度分辨率更細(xì)如果項(xiàng)目對 Flash 和速度敏感可以改成整數(shù)運(yùn)算ccr 500 angle * 2000 / 180效果一樣。限幅這一步別省。SG90 的實(shí)際機(jī)械行程可能略小于標(biāo)稱的 180°如果直接給 2500 以上的值舵機(jī)會頂?shù)较尬话l(fā)出咔咔聲長時間堵轉(zhuǎn)容易燒。我一般會在調(diào)試階段先用 10° 和 170° 試確認(rèn)不頂限位再放開到 0° 和 180°。3.3 主程序里的調(diào)用節(jié)奏main.c 里的邏輯很直白初始化之后循環(huán)改變角度#include stm32f10x.h #include timer.h #include delay.h int main(void) { delay_init(); // 延時函數(shù)初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); TIM3_PWM_Init(19999, 71); // 50Hz PWM while (1) { SG90_SetAngle(0); // 轉(zhuǎn)到0度 delay_ms(1000); SG90_SetAngle(90); // 轉(zhuǎn)到90度 delay_ms(1000); SG90_SetAngle(180); // 轉(zhuǎn)到180度 delay_ms(1000); } }這里有個容易被忽略的點(diǎn)舵機(jī)從 0° 轉(zhuǎn)到 180° 需要時間SG90 的規(guī)格是 0.1s/60°轉(zhuǎn) 180° 大約 0.3s。如果延時太短比如 100ms 就切下一個角度舵機(jī)還沒到位就收到新指令表現(xiàn)出來就是抖動或不到位。延時給 500ms 以上比較穩(wěn)妥具體看你的舵機(jī)批次。3.4 多路舵機(jī)的擴(kuò)展方式如果要控制四路舵機(jī)用 TIM3 的四個通道就行PA6、PA7、PB0、PB1 分別對應(yīng) CH1CH4。初始化時把四個通道都配上然后封裝一個按通道設(shè)角度的函數(shù)// 按通道設(shè)置角度ch范圍1~4 void SG90_SetAngleCh(u8 ch, float angle) { u16 ccr; if (angle 0) angle 0; if (angle 180) angle 180; ccr (u16)(500 angle * (2000.0f / 180.0f)); switch (ch) { case 1: TIM_SetCompare1(TIM3, ccr); break; case 2: TIM_SetCompare2(TIM3, ccr); break; case 3: TIM_SetCompare3(TIM3, ccr); break; case 4: TIM_SetCompare4(TIM3, ccr); break; default: break; } }四路共用一個定時器頻率天然同步不會出現(xiàn)某路 PWM 周期漂移的問題。注意 GPIO 初始化時要把四個引腳都配上別只配一個。4. 編譯、燒錄與上電驗(yàn)證從零把舵機(jī)跑起來4.1 工程編譯與下載配置源碼是標(biāo)準(zhǔn)庫工程用 Keil MDK 打開 .uvprojx 文件即可。編譯前確認(rèn)兩件事一是 Options for Target 里的 Device 選的是 STM32F103C8二是 Debug 選項(xiàng)卡里選對了下載器常見的是 ST-Link 或 J-Link。如果用的是串口下載需要把 BOOT0 拉高、BOOT1 拉低下載完再改回 BOOT0 拉低運(yùn)行。編譯輸出在 OBJ 目錄生成的 .hex 文件可以直接用下載器燒錄。如果編譯報錯找不到 stm32f10x.h檢查 C/C 選項(xiàng)卡里的 Include Paths 是否包含了 FWLIB/inc、CMSIS 和 USER 目錄。4.2 接線與供電接線是新手翻車最多的地方列一下SG90 線色接到說明棕色/黑色GND與單片機(jī)共地紅色5V 外部電源正極不要接單片機(jī) 3.3V橙色/黃色PA6PWM 信號共地這一步必須做否則 PWM 信號沒有參考電平舵機(jī)會亂動。5V 電源的電流能力至少 1ASG90 空載電流約 100mA堵轉(zhuǎn)能到 700mA 以上用單片機(jī)板上的 3.3V 或 USB 口供電很容易觸發(fā)復(fù)位。4.3 上電后的驗(yàn)證步驟燒錄完成后按這個順序驗(yàn)證先不接舵機(jī)用示波器或邏輯分析儀看 PA6 有沒有 50Hz、高電平 1.5ms 的波形初始角度 90° 時。沒有示波器的話接一個 LED 加電阻到 PA6能看到微弱閃爍說明 PWM 在輸出。然后接上舵機(jī)上電后舵機(jī)應(yīng)該先轉(zhuǎn)到 90° 附近再按 main 里的節(jié)奏在 0°、90°、180° 之間循環(huán)。如果舵機(jī)不動先量 PA6 電壓萬用表測平均值應(yīng)該在 0.25V1.25V 之間變化。如果電壓不變說明 PWM 沒輸出回去查定時器初始化和 GPIO 復(fù)用配置。如果電壓在變但舵機(jī)不動查供電和共地。5. 避坑與常見問題排查舵機(jī)抖動、復(fù)位、角度不準(zhǔn)的根因5.1 舵機(jī)持續(xù)抖動或嗡嗡響現(xiàn)象舵機(jī)停在某個角度附近小幅抖動伴隨嗡嗡聲。原因PWM 信號不穩(wěn)定或者供電電壓波動導(dǎo)致舵機(jī)內(nèi)部比較電路反復(fù)調(diào)整。常見的是電源電流不足舵機(jī)一動電壓就掉舵機(jī)又往回找位置。解決換獨(dú)立 5V 2A 電源電源正負(fù)極到舵機(jī)之間并一個 470μF1000μF 電解電容信號線盡量短。如果還抖檢查定時器配置里TIM_OCPolarity是不是設(shè)成了 High設(shè)反了會導(dǎo)致脈寬邏輯顛倒。5.2 單片機(jī)上電就復(fù)位現(xiàn)象接上舵機(jī)后單片機(jī)反復(fù)重啟串口打印亂碼或程序跑飛。原因舵機(jī)啟動瞬間電流沖擊把 5V 或 3.3V 拉低觸發(fā)單片機(jī)欠壓復(fù)位。解決舵機(jī)電源和單片機(jī)電源分開供只在 GND 上共地。如果必須共用加大濾波電容并在軟件里避免上電瞬間就讓舵機(jī)大角度轉(zhuǎn)動可以先給 90° 再慢慢轉(zhuǎn)。5.3 角度和預(yù)期對不上現(xiàn)象寫 0° 舵機(jī)轉(zhuǎn)到 20°寫 180° 只到 160°。原因SG90 個體差異不同批次的脈寬范圍不完全一致或者機(jī)械安裝時舵盤沒對準(zhǔn)。解決先做校準(zhǔn)寫一個循環(huán)從 500 到 2500 每次加 100記錄舵機(jī)實(shí)際能到的兩個極限位置把映射公式里的 500 和 2500 換成實(shí)測值。機(jī)械上把舵盤拆下來讓舵機(jī)先轉(zhuǎn)到 90°再把舵盤裝到機(jī)械中位。5.4 多路舵機(jī)互相干擾現(xiàn)象單獨(dú)控制一路正常四路一起動就有的不動或抖動。原因四路同時啟動電流疊加電源扛不住或者 GPIO 初始化漏配了某個通道的引腳。解決電源按每路 1A 余量配四路至少 4A。軟件上避免四路同時大角度跳變可以錯開幾十毫秒依次給指令。檢查 GPIO 初始化里四個引腳是否都設(shè)成了GPIO_Mode_AF_PP。5.5 改了定時器或引腳后不工作現(xiàn)象把 TIM3 換成 TIM2或者把 PA6 換成 PA0代碼編譯通過但沒波形。原因不同定時器通道對應(yīng)的默認(rèn)復(fù)用引腳不同TIM2_CH1 默認(rèn)在 PA0TIM3_CH1 默認(rèn)在 PA6換定時器必須同時換引腳。另外 TIM2 掛在 APB1 上時鐘使能要用RCC_APB1PeriphClockCmd別寫成 APB2。解決查芯片參考手冊的復(fù)用功能映射表確認(rèn)定時器通道和引腳的對應(yīng)關(guān)系。如果要用重映射還得開 AFIO 時鐘并調(diào)GPIO_PinRemapConfig。6. 二次開發(fā)改造從單路舵機(jī)到多路聯(lián)動與角度平滑拿到這份源碼后最常做的改造有三個方向加路數(shù)、加平滑、加聯(lián)動。加路數(shù)前面已經(jīng)講了用同一 TIM 的多通道把SG90_SetAngle擴(kuò)展成帶通道參數(shù)的版本就行。這里重點(diǎn)說平滑和聯(lián)動這兩個是讓舵機(jī)從「能轉(zhuǎn)」到「轉(zhuǎn)得好看」的關(guān)鍵。平滑的核心是別讓 CCR 一步跳到位而是分多次小步逼近。寫一個帶速度參數(shù)的函數(shù)// 平滑轉(zhuǎn)動到目標(biāo)角度step為每步角度delay_ms為每步間隔 void SG90_MoveSmooth(float target, float step, u16 interval) { static float current 90.0f; // 記錄當(dāng)前角度 if (target 0) target 0; if (target 180) target 180; while (current target - step) { current step; SG90_SetAngle(current); delay_ms(interval); } while (current target step) { current - step; SG90_SetAngle(current); delay_ms(interval); } current target; SG90_SetAngle(current); }step控制平滑度給 12 度比較自然給 10 度以上就接近直接跳。interval控制速度給 1020ms 時轉(zhuǎn)動看起來連續(xù)。注意current用 static 保存多路舵機(jī)時每路要各自維護(hù)一個當(dāng)前角度變量不能共用一個。聯(lián)動改造適合機(jī)械臂或云臺場景。比如做一個兩軸云臺X 軸和 Y 軸要同時從當(dāng)前位置轉(zhuǎn)到目標(biāo)位置如果分別調(diào)用平滑函數(shù)會一個轉(zhuǎn)完另一個再轉(zhuǎn)看起來別扭。做法是把兩個軸的目標(biāo)和當(dāng)前值都拿出來在一個循環(huán)里同時更新void Gimbal_MoveTo(float tx, float ty, float step, u16 interval) { float cx 90.0f, cy 90.0f; // 當(dāng)前角度實(shí)際項(xiàng)目里應(yīng)保存為全局 while (fabs(cx - tx) step || fabs(cy - ty) step) { if (cx tx) cx step; else if (cx tx) cx - step; if (cy ty) cy step; else if (cy ty) cy - step; SG90_SetAngleCh(1, cx); SG90_SetAngleCh(2, cy); delay_ms(interval); } SG90_SetAngleCh(1, tx); SG90_SetAngleCh(2, ty); }這樣兩個軸同步逼近運(yùn)動軌跡是直線插補(bǔ)的近似視覺效果比先后轉(zhuǎn)動好很多。如果要做圓弧插補(bǔ)就在循環(huán)里按角度算坐標(biāo)原理一樣。驗(yàn)證改造效果時我習(xí)慣用邏輯分析儀抓兩路 PWM 的波形看高電平時間是不是同步變化、有沒有跳變。沒有邏輯分析儀就用兩個舵機(jī)裝在一個十字架上肉眼觀察運(yùn)動是否協(xié)調(diào)。改完平滑參數(shù)后重點(diǎn)看低速時有沒有卡頓、高速時有沒有過沖過沖就減小 step 或加大 interval。從那以后我每次改舵機(jī)代碼都強(qiáng)制先用示波器確認(rèn) 50Hz 和脈寬范圍再上舵機(jī)避免把機(jī)械結(jié)構(gòu)擰壞。這套流程幫我省了不少返工。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取