戰(zhàn):從TIM3配置到引腳重映射的完整解析)
簡介這是一個(gè)面向STM32F1微控制器的雙極性SPWM波形生成代碼工程適用于逆變器、電機(jī)驅(qū)動、開關(guān)電源等需要正弦波輸出的應(yīng)用場景也適合正在學(xué)習(xí)電力電子與嵌入式實(shí)時(shí)控制的開發(fā)者參考可作為高校相關(guān)課程設(shè)計(jì)與畢業(yè)設(shè)計(jì)的參考模板。壓縮包共142個(gè)文件、約5.84MB工程內(nèi)既包含Keil項(xiàng)目文件.uvprojx/.uvoptx、CubeMX圖形化配置.ioc也包含19個(gè).c與46個(gè).h的HAL庫源碼以及.hex/.axf等已編譯產(chǎn)物和.map/.lst等映射文件整體是一套可直接導(dǎo)入重新編譯的完整Keil工程。已有784人學(xué)習(xí)/下載。代碼展示了從三角載波與正弦調(diào)制波比較生成SPWM到通過低通濾波提取近似正弦信號的完整流程通過修改載波頻率、調(diào)制比和濾波參數(shù)即可調(diào)節(jié)最終輸出頻率。對深入理解SPWM數(shù)字控制、STM32定時(shí)器比較輸出以及逆變器前級波形生成都有實(shí)際幫助。 最近拿到一個(gè)叫sjx_PWM.zip的工程包解壓后看到熟悉的Core、HARDWARE、SYSTEM、OBJ目錄我就知道這是個(gè)標(biāo)準(zhǔn)的 STM32F103ZET6 工程而且核心功能大概率躲不開 PWM。果不其然LED 呼吸燈、TIM3 輸出、引腳重映射、占空比調(diào)節(jié)全都集成在一個(gè)看似不起眼的壓縮包里。對很多剛接觸定時(shí)器輸出的朋友來說PWM 的概念不難理解但真要在工程里跑起來從引腳復(fù)用、時(shí)鐘配置到波形調(diào)試每一步都藏著坑。這篇文章就借這個(gè)sjx_PWM.zip把 PWM 從原理到實(shí)操完整拆一遍。如果你正在學(xué) STM32想搞懂 PWM 輸出的完整鏈路或者做完了一個(gè)簡單的 LED 點(diǎn)亮實(shí)驗(yàn)但不知道接下來怎么深入這個(gè)工程值得認(rèn)真研究。它不只是一個(gè)呼吸燈更像一個(gè) PWM 知識的小型集合體把重映射、占空比漸變、故障保護(hù)、波形調(diào)試這些點(diǎn)都串了起來。1. 項(xiàng)目解讀一個(gè)呼吸燈zip里裝了什么1.1 為什么小小的呼吸燈能串聯(lián)這么多知識點(diǎn)呼吸燈的邏輯本身非常簡單讓 LED 的亮度從暗到亮、再從亮到暗循環(huán)。但這個(gè)效果背后牽扯的東西遠(yuǎn)比你想象的多。LED 要呼吸得順滑輸出 PWM 的定時(shí)器頻率要足夠高否則會有肉眼可感知的閃爍占空比需要一步一步漸變步進(jìn)值剎車太猛會看到明顯跳變太慢又顯得死氣沉沉引腳必須復(fù)用到正確的通道上不然你在寄存器里寫了一堆配置信號卻根本出不來如果后面還想控制電機(jī)或大功率負(fù)載故障保護(hù)和死區(qū)配置又是繞不開的話題。sjx_PWM.zip這個(gè)工程名義上是做呼吸燈實(shí)際上把這些 PWM 開發(fā)里的關(guān)鍵點(diǎn)都覆蓋了一遍屬于那種“麻雀雖小五臟俱全”的教學(xué)型代碼。所以我建議拿到這個(gè)工程的朋友別只顧著編譯下載看燈亮而是先把整個(gè)數(shù)據(jù)流走一遍時(shí)鐘樹怎么給的、TIM3 掛在哪條總線上、引腳怎么從默認(rèn)位置映射到目標(biāo)引腳、CCR 寄存器的值怎么改變輸出脈寬把這些都理清楚你才算真正學(xué)會了 PWM。1.2 定時(shí)器選型TIM3 和引腳重映射背后的考量做呼吸燈大部分人的第一反應(yīng)是用 GPIO 翻轉(zhuǎn)加延時(shí)來做軟件 PWM代碼確實(shí)簡單但 CPU 會被完全占死而且如果頻率高一點(diǎn)延時(shí)函數(shù)帶來的抖動會讓波形很不穩(wěn)定。硬件定時(shí)器 PWM 的好處在于定時(shí)器一旦啟動引腳翻轉(zhuǎn)完全由硬件完成CPU 只需要修改比較寄存器 CCR 的值就能實(shí)時(shí)改變占空比。這也是工程里選 TIM3 而不是軟件模擬的根本原因。那為什么偏偏是 TIM3而不是 TIM1、TIM2 或者 TIM4STM32F103ZET6 的資源非常充裕TIM1 是高級定時(shí)器帶互補(bǔ)輸出和死區(qū)功能強(qiáng)但對這個(gè)實(shí)驗(yàn)來說有點(diǎn)牛刀殺雞TIM2 和 TIM3 都是通用定時(shí)器TIM3 的四個(gè)通道分布在 PA6、PA7、PB0、PB1其中一個(gè)有意思的地方是有部分引腳默認(rèn)被 JTAG 占用。把這個(gè)工程選 TIM3 的一個(gè)重要原因就是順理成章地引出“引腳重映射”這個(gè)很多新手非常困惑的操作。方案優(yōu)點(diǎn)缺點(diǎn)適用場景GPIO 軟件翻轉(zhuǎn)簡單直觀CPU 占用高頻率不穩(wěn)定臨時(shí)驗(yàn)證TIM3 硬件 PWM穩(wěn)定、資源占用低需要配置重映射和復(fù)用呼吸燈、電機(jī)調(diào)速TIM1 高級定時(shí)器帶剎車/死區(qū)/互補(bǔ)輸出配置復(fù)雜電機(jī)控制、逆變?nèi)绻愕?PCB 上已經(jīng)把 LED 接到了 PB4/PB5 這類非默認(rèn)引腳就要通過重映射讓 TIM3 的通道信號跑到你需要的引腳上去。這個(gè)設(shè)計(jì)思路在實(shí)際項(xiàng)目里非常常見芯片引腳規(guī)劃從來不是“這個(gè)定時(shí)器只能用這幾個(gè)引腳”而是通過 AFIO 重新分配功能讓硬件設(shè)計(jì)更靈活。2. PWM 原理與關(guān)鍵參數(shù)計(jì)算2.1 從 LED 亮度到占空比PWM 的本質(zhì)PWM脈寬調(diào)制的本質(zhì)是用一個(gè)固定頻率的方波通過改變高電平在一個(gè)周期內(nèi)所占的比例來改變輸出的平均電壓。人眼對高頻率亮滅切換并不敏感看到的只是平均亮度占空比越高等效電壓越高LED 越亮占空比越低LED 越暗。呼吸燈的任務(wù)就是讓占空比隨時(shí)間周期性變化。為了更容易理解可以把定時(shí)器想象成一個(gè)不斷往杯子里倒水再倒掉的循環(huán)自動重裝載寄存器 ARR 決定杯子能裝多少水比較寄存器 CCR 決定倒到哪個(gè)位置時(shí)打開閥門。當(dāng)計(jì)數(shù)值 CNT 小于 CCR 時(shí)引腳輸出高電平當(dāng)計(jì)數(shù)超過 CCR 但還沒超過 ARR 時(shí)引腳輸出低電平。CNT 到達(dá) ARR 后溢出重新從零開始下一個(gè)周期繼續(xù)重復(fù)這個(gè)過程。改變 CCR就是改變閥門打開的位置也就是改變占空比。2.2 引腳重映射細(xì)節(jié)PB4/PB5 與 JTAG 的沖突很多人在做sjx_PWM.zip這個(gè)工程時(shí)最頭疼的就是明明照著代碼配了引腳卻沒有任何波形輸出。這時(shí)候十有八九是踩了 JTAG 和重映射的坑。STM32F103 的 TIM3 通道默認(rèn)映射是CH1 在 PA6CH2 在 PA7CH3 在 PB0CH4 在 PB1。但如果你查看芯片數(shù)據(jù)手冊的 AFIO 部分會發(fā)現(xiàn) TIM3 支持部分重映射和完全重映射。比如完全重映射后CH3 可以挪到 PB4CH4 可以挪到 PB5。問題在于PB4 和 PB3 在芯片復(fù)位后的默認(rèn)功能是 JTAG 的 NJTRST 和 JTDO。要讓這兩個(gè)引腳變成普通 GPIO 或者復(fù)用功能必須先關(guān)閉 JTAG 功能保留 SWD 下載調(diào)試口。工程里常見的寫法是GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_TIM3, ENABLE);第一條語句把 JTAG 關(guān)掉釋放 PB3、PB4、PA15 這幾個(gè)被 JTAG 占用的引腳第二條語句把 TIM3 的功能重映射到新的引腳組合上。順序不能反而且要注意操作 AFIO 時(shí)鐘也要打開否則寄存器配置不生效。如果你在 CubeMX 里操作就不用管這個(gè)細(xì)節(jié)了但用標(biāo)準(zhǔn)外設(shè)庫的老工程里這兩行是很多問題的根源。2.3 時(shí)序參數(shù)計(jì)算頻率、占空比和呼吸節(jié)奏怎么搭配sjx_PWM.zip里默認(rèn)用的時(shí)鐘是外部 8MHz 晶振經(jīng)過 PLL 倍頻到 72MHzTIM3 掛載在 APB1 總線上所以定時(shí)器輸入時(shí)鐘就是 72MHz。PWM 頻率、占空比、呼吸節(jié)奏三者是互相關(guān)聯(lián)的。以sjx_PWM.zip的常用配置為例PSC 設(shè)為 71ARR 設(shè)為 999這樣 72MHz 先被分頻到 1MHz再計(jì)數(shù) 1000 次溢出得到 1kHz 的 PWM 頻率。占空比計(jì)算公式為占空比 CCR / (ARR 1) * 100%呼吸燈節(jié)奏則取決于 CCR 的更新頻率和步進(jìn)值。如果每 10ms 讓 CCR 加 1從 0 加到 999 需要差不多 10 秒呼吸一次太長燈看起來會很慵懶。工程里一般把步進(jìn)值設(shè)成 5 或者 10也就是每 10ms 讓 CCR 加 5這樣從暗到亮大概需要 2 秒視覺上剛剛好。PWM頻率 72MHz / ((PSC1) * (ARR1))舉個(gè)例子如果 PSC 71ARR 999那么 PWM 頻率 72MHz / (72 * 1000) 1kHz。這個(gè)頻率驅(qū)動 LED 完全沒有問題但如果驅(qū)動舵機(jī)一般要用 50Hz那就需要把 ARR 拉大或者調(diào) PSC參數(shù)計(jì)算邏輯是一樣的只是目標(biāo)頻率不同。3. 從 CubeMX 配置到跑通呼吸燈3.1 關(guān)鍵配置項(xiàng)逐項(xiàng)說明如果用 CubeMX 新建工程配置步驟并不復(fù)雜但有幾個(gè)地方容易漏。第一步在 RCC 里把 HSE 設(shè)為 Crystal/Ceramic Resonator外部時(shí)鐘源選好第二步在 SYS 里把 Debug 設(shè)置為 Serial Wire這一步非常關(guān)鍵否則一旦關(guān)閉 JTAG下次燒錄可能連不上芯片第三步在定時(shí)器 TIM3 里把 Clock Source 設(shè)為 Internal ClockChannel1 模式選 PWM Generation CH1第四步把引腳從默認(rèn)位置拖到目標(biāo)引腳比如 PB4CubeMX 會自動生成重映射相關(guān)代碼第五步在 TIM3 的參數(shù)配置里填入 PSC、ARRPulse 初始值可以設(shè) 500這樣上電時(shí) LED 正好是一半亮度方便確認(rèn)配置是否生效。這里特別提醒一下引腳重映射在 CubeMX 里看似只是拖拽但工具自動幫你處理了 AFIO 和重映射寄存器。如果手寫標(biāo)準(zhǔn)外設(shè)庫代碼千萬不要漏掉RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE)這一條。3.2 核心代碼解讀初始化部分HAL 庫的寫法比較固定void MX_TIM3_Init(void) { TIM_OC_InitTypeDef sConfigOC {0}; htim3.Instance TIM3; htim3.Init.Prescaler 71; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 999; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim3); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 500; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); }呼吸燈的主循環(huán)邏輯不復(fù)雜關(guān)鍵是怎么讓 CCR 平滑地來回變化。我推薦用一個(gè)目標(biāo)值和方向標(biāo)志位的思路比單純兩個(gè) for 循環(huán)嵌套更干凈uint16_t ccr 0; uint16_t target 999; uint16_t step 5; while (1) { if (ccr target) { ccr step; if (ccr target) { target 0; } } else if (ccr target) { ccr - step; if (ccr target) { target 999; } } __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, ccr); HAL_Delay(10); }為什么用target而不是 dash 兩個(gè) for 循環(huán)因?yàn)槟繕?biāo)值方式更容易擴(kuò)展以后你想讓燈停在某個(gè)亮度再開始呼吸只需要修改 target 和 ccr 的初始值反復(fù) while 循環(huán)嵌套也可以但代碼嵌套深了可讀性差。步進(jìn)值 5 是我實(shí)測比較舒服的數(shù)值既不會看到燈光跳變也不會因?yàn)樽兓屓朔咐А?.3 故障保護(hù)與死區(qū)給 PWM 加一道“保險(xiǎn)”PWM 不只用來點(diǎn)燈更多場合是用來驅(qū)動電機(jī)、加熱器等功率設(shè)備。在這些應(yīng)用里“故障保護(hù)”就非常重要了。STM32 的高級定時(shí)器 TIM1/TIM8 支持剎車輸入Break Input可以把過流、過壓等故障信號接到 BKIN 引腳。一旦該信號有效定時(shí)器輸出會被硬件強(qiáng)制拉低或拉高同時(shí)觸發(fā)中斷通知 CPU 處理。這種保護(hù)不受程序卡死影響是純硬件級別的響應(yīng)比軟件里判斷一個(gè) GPIO 再手動關(guān)閉 PWM 快得多。熱詞里還提到“28335 配置的 PWM 死區(qū) DBFED 和 DBRED”。這讓我想到很多做電機(jī)控制的朋友會混淆死區(qū)配置。STM32 上 DTG 寄存器負(fù)責(zé)死區(qū)插入而 TI 的 28335 DSP 把死區(qū)分成了上升沿延時(shí)DBRED和下降沿延時(shí)DBFED兩個(gè)獨(dú)立寄存器。雖然命名和寄存器架構(gòu)不同但目的一致在互補(bǔ) PWM 的上下橋臂切換之間插入一段“同時(shí)關(guān)斷”的安全時(shí)間防止上下管直通燒毀。如果你將來做 H 橋或者三相電機(jī)驅(qū)動這個(gè)知識點(diǎn)幾乎必定會遇到?;氐絪jx_PWM.zip這個(gè)工程如果只是點(diǎn)燈硬件剎車用不上但代碼里仍然可以寫一個(gè)軟保護(hù)邏輯檢測到某個(gè)按鍵或外部信號時(shí)立刻把 CCR 清零并停止定時(shí)器這個(gè)習(xí)慣值得養(yǎng)成。我見過很多新手在點(diǎn)燈階段不去想保護(hù)真到驅(qū)動電機(jī)時(shí)又沒有保護(hù)意識結(jié)果一上電就燒板子。從點(diǎn)燈階段開始建立故障保護(hù)思維成本非常低。4. 調(diào)試實(shí)錄常見問題與排查技巧4.1 輸出恒高或 100% 占空比異常的真相我在用 STM32 做 PWM 的時(shí)候遇到過好幾次 100% 占空比輸出異常的情況。表現(xiàn)形式是理論上滿占空比應(yīng)該輸出恒高電平但示波器上看波形偶爾會突然出現(xiàn)一個(gè)很窄的低電平毛刺或者燈亮度不是滿亮而是略偏暗。排查到最后問題往往出在 CCR 的值超過了 ARR。定時(shí)器比較邏輯是 CNT 等于 CCR 的時(shí)候翻轉(zhuǎn)如果 CCR 被設(shè)置成大于 ARR 的數(shù)硬件行為會變得不可預(yù)測。另外如果在更新 CCR 時(shí)沒有考慮 ARR 的值比如把 CCR 設(shè)成了 1000而 ARR 只有 999某些芯片版本可能出現(xiàn)異常波形。解決辦法很簡單更新 CCR 之前做一次鉗位確保ccr ARR。在sjx_PWM.zip這類工程里呼吸燈代碼通常不會出錯(cuò)但如果你改過 ARR 卻沒有同步修改 step 和 target就要特別注意了。4.2 Keil 仿真和邏輯分析儀怎么看波形很多人來問 Keil 5 怎么查看 PWM 波形。Keil 的 Debug 模式里確實(shí)有邏輯分析窗口可以添加變量并配置成 bit 顯示能看到 CNT 和 CCR 的變化但如果想看引腳真實(shí)的電平波形我更推薦用邏輯分析儀直接量引腳。具體在 Keil 里操作進(jìn)入調(diào)試模式后打開 View - Watch 窗口添加TIM3-CNT、TIM3-CCR1再打開 View - Analysis Windows - Logic Analyzer把這兩個(gè)變量拖進(jìn)去配置格式選擇 Bit 或 Analog就能看到時(shí)間軸上的波形變化。這個(gè)方式適合確認(rèn)程序執(zhí)行的邏輯但它模擬的是寄存器狀態(tài)不是芯片引腳上實(shí)際輸出的電平。真正驗(yàn)證 PWM 波形還是用示波器或邏輯分析儀量 PB4或你重映射后的目標(biāo)引腳最直接。我個(gè)人的習(xí)慣是先在邏輯分析儀里看到干凈的 1kHz 方波再去看 LED 亮度變化。如果波形正常但燈不亮那是硬件問題如果燈忽明忽暗但波形一直正常那是占空比漸變邏輯問題。按這個(gè)思路排查能把問題快速分成“軟件邏輯”和“硬件連接”兩個(gè)方向。4.3 引腳沒波形的排查順序如果代碼編譯下載后引腳上沒有波形按順序做這幾步檢查。第一步確認(rèn)定時(shí)器時(shí)鐘有沒有打開標(biāo)準(zhǔn)外設(shè)庫工程最容易漏掉這一點(diǎn)第二步確認(rèn) GPIO 模式是否設(shè)置成復(fù)用推挽輸出AF_PP如果你不小心配置成通用推挽輸出信號是出不去第三步確認(rèn)重映射是否生效PB4 這種引腳如果不關(guān)閉 JTAG永遠(yuǎn)是高阻或具體芯片默認(rèn)狀態(tài)第四步確認(rèn)初始化和 PWM 啟動函數(shù)有沒有執(zhí)行如果你在初始化之后不小心把 CCR 清 0 或者關(guān)閉了輸出波形自然沒有。還有一個(gè)小細(xì)節(jié)接 LED 時(shí)要注意引腳極性。PWM 高電平有效時(shí)LED 正極接引腳、負(fù)極經(jīng)限流電阻接地如果你把 LED 接反了占空比越小反而越亮容易誤判成程序邏輯反了。這個(gè)低級錯(cuò)誤我在帶著學(xué)生做實(shí)驗(yàn)時(shí)遇到過不止一次。4.4 呼吸不順暢、閃爍和亮度不均勻的問題呼吸燈如果看起來一頓一頓的問題大概率出在步進(jìn)值和延時(shí)時(shí)間的配合上。比如你每 50ms 才更新一次 CCR每次只加 1LED 的變化就會非常生硬。我的經(jīng)驗(yàn)是讓 CCR 每 10ms 變化 5 到 10基本能達(dá)到平滑呼吸的效果。但如果 PWM 頻率太低比如低于 500Hz即使占空比變化再平滑人眼還是能看到高頻閃爍。這時(shí)候優(yōu)先調(diào)高 PWM 頻率而不是拼命改步進(jìn)。亮度不均勻則是另一個(gè)問題。人眼對亮度的感知不是線性的CCR 從 0 加到 100 的亮度變化在人眼看來可能比從 500 加到 600 更明顯。如果追求完美的呼吸效果可以對占空比做對數(shù)或指數(shù)校正但工程精度要求不高的話線性漸變反而更有呼吸燈原本的神韻。注意用HAL_Delay做主循環(huán)延時(shí)非常簡單但它會阻塞主循環(huán)。如果工程里以后要同時(shí)處理按鍵、顯示等其他任務(wù)建議把占空比更新放到定時(shí)器中斷里做或者用非阻塞的狀態(tài)機(jī)避免延時(shí)期間系統(tǒng)完全沒有響應(yīng)。最后再分享一個(gè)我在實(shí)際調(diào)試這個(gè)工程時(shí)收獲很大的小技巧不要一上來就調(diào)代碼先在初始化函數(shù)里把 CCR 設(shè)成固定值比如 500確認(rèn) LED 以固定亮度點(diǎn)亮。這一步能快速驗(yàn)證硬件鏈路和 PWM 基本輸出是否正常。固定亮度沒問題之后再改成漸變邏輯去調(diào)呼吸節(jié)奏。把一個(gè)大目標(biāo)拆成兩個(gè)“必現(xiàn)”狀態(tài)來驗(yàn)證比直接跑完整程序、出了問題一頭霧水要高效得多。sjx_PWM.zip這個(gè)工程值得你反復(fù)燒錄幾遍每次改一個(gè)參數(shù)觀察現(xiàn)象變化很快你就能把 PWM 玩明白了。本文還有配套的精品資源點(diǎn)擊獲取