光原理與代碼實現(xiàn))
簡介面向STM32初學者的按鍵控制LED燈亮度工程包基于STM32F103最小系統(tǒng)板利用六個按鍵調(diào)節(jié)六檔PWM占空比實現(xiàn)LED從微光到高亮的多級亮度切換。工程覆蓋通用輸入輸出接口的輸入輸出配置、定時器PWM波形生成、外部中斷觸發(fā)以及按鍵消抖策略完整演示了從檢測按鍵事件到更新捕獲比較寄存器、改變輸出信號平均電壓的軟硬件協(xié)同流程。壓縮包共202個文件整體約6.76MB核心為C語言源碼與頭文件另含Keil工程配置文件、編譯鏈接生成的目標文件與映射文件、輔助說明文檔等類型覆蓋源碼閱讀、工程構(gòu)建與結(jié)果驗證各環(huán)節(jié)。目前已有19477人學習下載資源內(nèi)部分模塊劃分清晰針對按鍵中斷與定時器輸出等關(guān)鍵功能均有獨立文件組織便于對照查閱適合正在學習STM32定時器與PWM應用的開發(fā)者作為課程設(shè)計或入門實戰(zhàn)的參考模板。通過修改占空比參數(shù)還可擴展為呼吸燈、按鍵長按連續(xù)調(diào)光等進階功能實踐價值較強。 最近總有人問我STM32 怎么用按鍵控制 LED 燈亮度很多朋友剛?cè)腴T點個 LED 沒問題一說到“調(diào)節(jié)亮度”就卡住了。其實這個項目特別適合練手因為它同時牽扯到按鍵輸入、PWM 輸出、中斷和定時器這些嵌入式開發(fā)的核心知識點做完這個項目你再去看舵機控制、電機調(diào)速、屏幕背光調(diào)節(jié)思路基本是通的。這篇筆記就把我從硬件電路到代碼實現(xiàn)再到踩坑排查的完整過程寫下來給正在做類似練習的同學一個參考。1. 整體設(shè)計與思路拆解1.1 為什么亮度控制必須用 PWM先把最核心的問題說透想讓 LED 可調(diào)亮度為什么大家第一反應都是 PWM“亮度可調(diào)”說白了就是讓 LED 上的平均電流可調(diào)。你當然可以在 LED 的回路里串一個可調(diào)電阻或者用 DAC 輸出一個可變的模擬電壓來控制驅(qū)動管的導通程度這種方法叫“線性調(diào)節(jié)”。但它有個很大的毛病除了讓 LED 發(fā)光消耗的能量多余的功率全變成了熱量白白浪費掉。尤其在電池供電的設(shè)備里這種方案基本不劃算。PWM脈沖寬度調(diào)制的思路完全不一樣。用一個頻率固定的方波去驅(qū)動 LED通過改變方波的“占空比”來控制 LED 在一個周期內(nèi)的平均導通時間。占空比越高平均電流越大LED 看起來就越亮占空比越低LED 看起來就越暗。我對新手最喜歡打的比方是PWM 調(diào)光就是你在快速開關(guān)燈。假設(shè)你一秒內(nèi)開關(guān)燈一百次燈開的時間占 30%人眼看起來就是“暗一點”燈開的時間占 80%人眼看起來就是“亮一點”。因為開關(guān)頻率夠高人眼完全察覺不到閃爍只感覺到亮暗變化。STM32 定時器輸出的 PWM 信號頻率和占空比都可以通過寄存器精確控制。這個方案的優(yōu)勢非常明顯幾乎不產(chǎn)生額外熱量效率接近 100%只需一個 GPIO 引腳而且 STM32 的定時器硬件自動產(chǎn)生 PWM 波形CPU 完全不用管可以在調(diào)節(jié)間隙去干別的事。這也是為什么幾乎所有 LED 驅(qū)動、電機驅(qū)動方案都在用 PWM。1.2 方案選型按鍵掃描用輪詢還是中斷定了用 PWM 之后還有個關(guān)鍵選擇按鍵怎么讀很多入門同學的第一反應是“用外部中斷”。因為聽到“按鍵就是輸入”覺得外部中斷高級。在這個項目里我卻更推薦用定時器輪詢掃鍵。原因很簡單這個項目只需要一個按鍵輪詢掃描的代碼邏輯非常清晰配合定時器固定周期去讀一次引腳天然就帶“采樣”的概念。外部中斷適合按鍵需要“立即響應”的場景比如急停按鈕但從按下到識別出來單片機執(zhí)行一小段代碼也就是微秒級別的事人的手指頭根本感覺不出差別。中斷里最忌諱做耗時的操作比如延時消抖。如果你在中斷里用了 delay很容易造成主循環(huán)卡死或者其它中斷丟失這是新手最容易踩的坑。所以我最終選擇的方案是TIM 定時器產(chǎn)生 PWM 信號驅(qū)動 LED再用一個定時器或者復用同一個定時器的更新中斷周期性掃描按鍵配合軟件消抖和一個簡單的狀態(tài)機來改變目標占空比。2. 硬件細節(jié)解析按鍵電路、LED 驅(qū)動與消抖2.1 STM32 按鍵電路怎么接才算合理按鍵電路看著簡單但連接方式直接決定你要不要開內(nèi)部上拉以及代碼里怎么判斷“按下”。STM32 的 GPIO 在輸入模式下有三種常見接法接法電路形式按下時電平空閑時電平代碼判斷方式 A內(nèi)部上拉GPIO 配置為上拉輸入按鍵接 GND低電平高電平讀引腳為 0 表示按下方式 B外部上拉GPIO 浮空輸入 外部 10kΩ 上拉電阻按鍵接 GND低電平高電平讀引腳為 0 表示按下方式 C外部下拉GPIO 浮空輸入 外部 10kΩ 下拉電阻按鍵接 VCC高電平低電平讀引腳為 1 表示按下我最推薦方式 A電路最干凈只用一個按鍵兩條線內(nèi)部上拉是 STM32 硬件自帶的能力完全夠用。接線時按鍵一端接 GND另一端接 PA0 之類的 GPIO 引腳。這里有一個非常容易被忽略的點不要用“外部上拉 3.3V 直接進 GPIO”以外的做法去省電阻。有些同學圖省事按鍵一端接 3.3V一端接 GPIO也不開內(nèi)部上拉結(jié)果就是引腳懸空電平隨機跳變按鍵按下沒反應松手反而觸發(fā)了。這種“引腳浮空”的錯誤在初學者里出現(xiàn)率極高。2.2 LED 驅(qū)動電路能不能直接把燈接在 IO 上很多人直接拿一個 LED 串上一個電阻接到 STM32 的引腳上這是沒問題的但前提是電流不能超限。以 STM32F103 為例GPIO 的灌電流和拉電流能力大概在 ±25mA 左右實際上我們通常把工作電流控制在 10mA-20mA 以內(nèi)避免長期滿負荷運行導致芯片發(fā)熱或者引腳老化。單個 LED 正常工作電流在 5-20mA 之間所以一個 LED 直連引腳是可行的。電阻值怎么選LED 紅色普通指示燈的壓降大約 1.8V-2.0VSTM32 GPIO 輸出高電平大約 3.3V如果目標電流是 10mA那么串聯(lián)電阻就是R (3.3V - 2.0V) / 0.01A 130Ω取整后選 100Ω 或 220Ω 都能接受。220Ω 時電流大約 6mA亮度偏暗一些但也足夠用100Ω 時電流約 13mA更亮一點。我自己習慣用 220Ω省電而且足夠肉眼觀察。如果你以后要用大功率 LED 或者 LED 燈帶那就不建議直接接在 GPIO 上了得用三極管或 MOS 管做驅(qū)動開關(guān)這時候?qū)iT的高效 LED 驅(qū)動芯片或者專用的恒流驅(qū)動芯片會是更好的選擇但那是另一個話題了。2.3 按鍵消抖到底在消什么說到按鍵就繞不開“消抖”。很多人剛開始不理解一個機械開關(guān)怎么會有抖動手指按下去的那一下金屬簧片不是一秒鐘之內(nèi)就穩(wěn)定閉合的而是在閉合和斷開之間來回彈跳幾次整個過程可能持續(xù) 5-20ms。如果不做處理單片機會以為你按了好幾次鍵——本來想調(diào)節(jié)一檔亮度結(jié)果直接跳了好幾檔。消抖有兩種思路硬件消抖在按鍵兩端并聯(lián)一個 0.1uF 的電容利用電容兩端電壓不能突變的特性把抖動濾掉。好處是 CPU 代碼簡單壞處是增加元件成本而且電容選大了會影響按鍵的響應速度。軟件消抖目前最通用的做法是“延時消抖”初次檢測到按鍵引腳電平變化后延時 10-20ms再次讀取確認。更高級的做法是狀態(tài)機掃描每 5ms 或者 10ms 掃描一次按鍵只有連續(xù)兩次以上電平才認為有效。這也是我實際項目里最推薦的在主循環(huán)或定時器中斷里按固定周期去采樣既不阻塞 CPU又能保證消抖效果。3. 實操過程與核心環(huán)節(jié)實現(xiàn)3.1 用 STM32CubeMX 快速搭建工程現(xiàn)在做 STM32 開發(fā)基本都離不開 STM32CubeMX。這里我以最常見的 STM32F103C8T6 核心板為例把配置步驟列出來每一步都說清楚為什么。時鐘配置F103 最高主頻 72MHz一般直接在 Clock Configuration 里把 HSE 選為 Crystal/Ceramic Resonator然后把 HCLK 設(shè)為 72MHz軟件會自動分配好 PLL 參數(shù)。定時器配置我們需要一路 PWM 輸出。TIM2、TIM3、TIM4 都可以。我選 TIM3配置 Channel 1 為 PWM Generation CH1輸出引腳通常是 PA6。如果你的板子上 PA6 剛好連著其它外設(shè)也可以換 CH2引腳作相應調(diào)整。PWM 頻率我設(shè)置在 1kHz也就是 Period 設(shè)置為 999Prescaler 設(shè)置為 71這樣 72MHz / 72 / 1000 1kHz占空比的最小調(diào)節(jié)精度是 0.1%。按鍵引腳配置按鍵接 PA0GPIO 模式選 Input mode with Pull-up/Pull-down然后在上拉下拉選擇里勾選 Pull-up。這樣按鍵就是高電平空閑按下變低。生成代碼的時候我習慣勾選 “Generate peripheral initialization as a pair of .c/.h files per peripheral”這樣每個外設(shè)的初始化代碼獨立成文件看起來清爽后期維護也方便。3.2 核心邏輯代碼狀態(tài)機 占空比調(diào)節(jié)工程生成之后就要寫主邏輯了。我的思路是這樣的用全局變量 g_target_duty 記錄當前目標占空比初始值是 20。每次檢測到按鍵短按就把目標占空比加 10達到 100 后回到 0。檢測按鍵安排在 TIM 更新中斷里做每 10ms 進入一次掃描 PA0 電平并做軟件消抖。主循環(huán)里只需要做一件事調(diào)用 __HAL_TIM_SET_COMPARE 把目標占空比寫入定時器比較寄存器LED 亮度立刻改變。核心代碼長這樣/* main.h 或者 main.c 里定義 */ #define LED_PWM_TIM htim3 #define LED_PWM_CH TIM_CHANNEL_1 #define KEY_GPIO_PORT GPIOA #define KEY_PIN GPIO_PIN_0 uint8_t g_target_duty 20; /* 目標占空比單位 % */ uint8_t g_key_state 0; /* 當前按鍵狀態(tài)1 表示按下且已確認 */ uint8_t g_key_prev 0; /* 上一次采樣電平用于消抖判斷 */ uint8_t g_key_debounce_cnt 0; /* 在定時器更新中斷回調(diào)里調(diào)用 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim htim3) /* 這里用的是同一個定時器復用做按鍵掃描 */ { KeyScan_10ms(); } } void KeyScan_10ms(void) { uint8_t level HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_PIN); /* 軟件消抖確認翻轉(zhuǎn)后需要連續(xù) 2 次采樣有效 */ if (level ! g_key_prev) { g_key_debounce_cnt; if (g_key_debounce_cnt 2) { g_key_debounce_cnt 0; g_key_prev level; if (level 0) /* 確認按下 */ { g_key_state 1; /* 置按鍵事件標志 */ } } } else { g_key_debounce_cnt 0; } } int main(void) { /* ... 初始化代碼由 CubeMX 生成 ... */ HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); /* 啟動 PWM 輸出 */ __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 200); /* 初次設(shè)為 20% 占空比 */ while (1) { if (g_key_state) { g_key_state 0; /* 調(diào)節(jié)目標占空比 */ g_target_duty 10; if (g_target_duty 100) { g_target_duty 0; } /* 寫入比較寄存器 */ uint32_t compare (uint32_t)g_target_duty * (TIM3-ARR 1) / 100; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, compare); /* 用串口或者調(diào)試器觀察當前值便于調(diào)試 */ printf(Duty: %d%%\r\n, g_target_duty); } } }這里有一個重點要說計算比較寄存器值。很多人直接把占空比數(shù)字當比較值用結(jié)果 LED 亮度要么永遠不亮要么永遠全亮。原因很簡單——比較值和占空比不是一回事。如果你的定時器 ARR 是 999那么 50% 占空比對應比較值應該是 499 或 500而不是 50。我用compare g_target_duty * (ARR 1) / 100這一步就是把百分數(shù)映射成定時器的實際比較值這樣占空比變量可以一直用 0-100 的整數(shù)來表示邏輯更清晰。3.3 我踩過的坑引腳重映射和 PWM 頻率選擇第一次做這個項目的時候我在 TIM3 的 PWM 引腳上栽了跟頭。F103 上 TIM3_CH1 默認映射在 PA6但如果你在 CubeMX 里不小心勾了 Remap引腳就變成了 PB4。板子上 PA6 和 PB4 旁邊都有焊盤插線的時候很容易插錯結(jié)果 LED 怎么都不亮。解決的辦法很簡單回到 CubeMX 的 Pinout 視圖波形圖標上點擊一下看它提示的實際引腳號對照原理圖確認最終位置。這是我在新手時期養(yǎng)成的最重要的習慣之一。PWM 頻率的選擇也有講究。人眼對 100Hz 以下的光源閃爍比較敏感所以 PWM 頻率至少要選 500Hz 以上1kHz 是很穩(wěn)妥的選擇。如果結(jié)構(gòu)上有振動比如電機帶動 LED頻率太低會出現(xiàn)“頻閃”現(xiàn)象看起來 LED 沒抖動但拍照或高速運動時能看到明顯的條紋。現(xiàn)在很多 LED 驅(qū)動做到 20kHz 以上一是避開人耳聽覺范圍二是讓手機攝像頭也拍不出條紋不過 1kHz 對我們這個項目足夠用了。4. 常見問題與排查技巧實錄做這個項目每個人幾乎都會遇到下面幾個問題我按出現(xiàn)頻率從高到低排個序把排查思路寫出來?,F(xiàn)象可能原因排查方法按鍵按下去沒反應GPIO 模式配錯浮空輸入、引腳搞錯、按鍵另一端沒接 GND用萬用表量按鍵引腳對地電壓按下時應變低LED 永遠不亮PWM 沒啟動、比較值一直是 0、引腳復用配置錯先查 GPIO 電平用示波器看 PWM 波形亮度只能跳兩檔按鍵消抖沒做好連續(xù)觸發(fā)縮短消抖時間檢查按鍵采樣邏輯輸出占空比正確但亮度不對LED 接錯限流電阻、電流方向反了確認 LED 正負極確認電阻阻值編譯下載時報錯調(diào)試器連接問題檢查 ST-Link 接線和驅(qū)動4.1 按鍵按下去沒反應最先查什么八成是硬件接線問題。我之前見過一個同學把按鍵接在 GND 和 PB1 上代碼里卻是讀 PA0查了半小時才發(fā)現(xiàn)。所以我的習慣是先量引腳空閑電平和按下電平確認引腳編號和代碼一致再看軟件邏輯。還有個常見問題用 CubeMX 配置時按鍵引腳沒有勾選 Pull-up 上拉。此時引腳懸空STM32 內(nèi)部既沒有上拉也沒有下拉讀到的電平完全不確定。這樣按鍵按下去可能偶爾有反應松手之后也可能誤觸發(fā)。排查方式是按下前先打印引腳電平看是不是穩(wěn)定在高電平。4.2 LED 亮度調(diào)節(jié)不線性中間檔位跳變明顯如果你把占空比從 10 調(diào)到 20肉眼幾乎看不出變化但從 80 調(diào)到 90變化卻很明顯這是因為人眼的亮度感知不是線性的而是近似對數(shù)的關(guān)系。也就是說占空比從 20% 變到 40%看起來像是在“變亮初期”而 80% 到 90% 的變化就很刺激。實際操作中如果你發(fā)現(xiàn)檔位之間跳躍太明顯可以考慮做一個非線性映射表讓低亮度區(qū)間的調(diào)節(jié)步長小一些。但說實話業(yè)余項目用線性調(diào)節(jié)也完全能用只是“體驗細節(jié)”有差別。這里也引出一個思考為什么那么多燈具控制器都支持“無極調(diào)光”本質(zhì)上就是調(diào)整 PWM 占空比的映射曲線讓人眼覺得亮度的變化是均勻的。4.3 編譯下載報錯error: no stm32 target found這個錯誤我剛開始玩 ST-Link 的時候經(jīng)常遇到問題基本都出在調(diào)試器連接上。檢查步驟我總結(jié)了四步看 ST-Link 的 SWDIO、SWCLK、GND、3.3V 四個引腳有沒有接對SWDIO 和 SWCLK 交叉接錯的概率特別高確認板子供電是否正常很多 ST-Link 和板子共用一個 USB 供電如果 USB 口供電不足或者接觸不良就找不到 target在 Keil 或 STM32CubeProgrammer 里把調(diào)試接口選成 SW 模式不要選 JTAG如果還是不行按住板子上的復位鍵不放點擊下載等進度條出現(xiàn)后再松開復位這個方法能救回很多“鎖死”的芯片。4.4 PWM 波形輸出異常有示波器的話這個問題特別好排查。沒有示波器就把 LED 串一個低阻值電阻用萬用表的交流電壓檔去測一端如果電壓波動明顯不正常那大概率是——定時器沒啟動。很多人初始化完之后忘了調(diào)用HAL_TIM_PWM_StartGPIO 電平是有的但就是沒有方波LED 一直不亮。GPIO 復用沒配置。CubeMX 里生成了代碼但你在手寫工程里忘了把 GPIO 配置成復用推挽模式。重映射搞錯。F103 系列 TIM 引腳有重映射功能配置不對會輸出到錯誤引腳上。5. 從按鍵調(diào)光到更多有趣玩法5.1 改成呼吸燈按鍵控制亮度做熟之后把項目擴展成呼吸燈特別有意思。邏輯非常簡單主循環(huán)里用一個變量讓占空比從 0 緩慢加到 100再從 100 減到 0周而復始。每次調(diào)節(jié)的時候不要太快建議每 5ms 改變 1-2 個百分點的占空比這樣視覺上才像呼吸。int dir 1; uint16_t duty 0; while (1) { __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, duty * 10); HAL_Delay(5); duty dir; if (duty 100) dir -1; if (duty 0) dir 1; }這段代碼我在開發(fā)板上實測下來效果很自然不會感覺到跳變推薦有興趣的同學試試。5.2 長短按和檔位記憶按鍵連續(xù)循環(huán)調(diào)光最大的問題是從最暗調(diào)回最亮中間要按十幾次。一個更實用的做法是支持長按連續(xù)調(diào)節(jié)按下超過 500ms 后每隔 100ms 自動加一檔松手就停在當前檔位。實現(xiàn)起來也不復雜掃描按鍵時記錄按下時間戳在主循環(huán)中判斷如果當前處于“按下狀態(tài)”且時間差超過閾值就自動執(zhí)行一次檔位遞增。這樣從最暗調(diào)到最亮只需要按住兩三秒體驗好很多。檔位記憶也不難利用 STM32 內(nèi)部 Flash 的一個扇區(qū)每次檔位改變后把值寫進去下次上電直接讀出來初始化。不過寫 Flash 要注意擦寫壽命頻繁寫入會磨損 Flash實際產(chǎn)品里通常把“最后保存的檔位”延遲 5 秒再寫入避免每次微調(diào)都觸發(fā)擦寫。5.3 使用中斷讀取按鍵的注意事項如果你確實要用外部中斷我建議的方式是中斷服務函數(shù)里只設(shè)置標志位不做消抖延時也不改變 PWM 占空比退出中斷后在主循環(huán)里檢測到標志位再執(zhí)行 10ms 的軟件消抖和后續(xù)邏輯。這樣既保證了按鍵響應的及時性又不會阻塞主循環(huán)。我自己測試過用外部中斷做按鍵調(diào)節(jié)亮度體驗上和輪詢掃描幾乎沒有差別。這個方案在低功耗設(shè)備里會更重要因為主循環(huán)可能大部分時間都處于休眠狀態(tài)依靠中斷喚醒。最后這個項目做完之后我最大的感受是嵌入式入門不要追求“高大上”把按鍵、PWM、定時器這幾塊基本功吃透后面很多項目都是這幾個知識點換著花樣組合??刂?LED 亮度只是 PWM 的一個應用同樣的思路放到直流電機調(diào)速、舵機角度控制、逆變器輸出上只是驅(qū)動電路和參數(shù)不一樣核心邏輯完全通用。如果條件允許建議你買一個邏輯分析儀或者二手示波器把按鍵按下瞬間的波形抓出來看看親眼看一下“抖動”是什么樣子觀察 PWM 占空比變化時波形寬度的變化這個體驗比看十篇文章都有用。調(diào)好看一眼波形你心里就特別有底了。本文還有配套的精品資源點擊獲取