動L298N電機(jī)控制:PWM調(diào)速與正反轉(zhuǎn)代碼詳解)
簡介這份資源面向嵌入式初學(xué)者與電機(jī)控制入門開發(fā)者圍繞STM32微控制器與L298N H橋驅(qū)動芯片的配合使用解決按鍵控制直流電機(jī)轉(zhuǎn)速與正反轉(zhuǎn)的典型實踐問題。壓縮包共143個文件約2.62MB以35個h頭文件與34個c源文件為核心涵蓋GPIO、定時器PWM、中斷等外設(shè)驅(qū)動代碼另含Keil工程配置、編譯中間文件與hex燒錄文件構(gòu)成可直接編譯運行的完整工程。已有5740人學(xué)習(xí)下載說明其在入門實踐中具有較高參考價值。資源中的示例工程完整呈現(xiàn)了按鍵中斷響應(yīng)、PWM占空比調(diào)節(jié)與L298N輸入引腳狀態(tài)切換的實現(xiàn)思路讀者可據(jù)此理解GPIO模式配置、定時器參數(shù)設(shè)定及電機(jī)正反轉(zhuǎn)控制邏輯并在此基礎(chǔ)上調(diào)試優(yōu)化加深對軟硬件協(xié)同工作的理解。1. 拿到一份 STM32 L298N 代碼包先別急著燒錄上周有個做實訓(xùn)車的朋友把一份 STM32 L298N 代碼包丟給我說電機(jī)只會一個方向轉(zhuǎn)問我是不是代碼寫錯了。我打開一看PWM 初始化里定時器通道配反了方向引腳又和 PWM 搶同一個端口這種問題燒一百次也跑不對。這份資源就是圍繞 STM32 驅(qū)動 L298N 雙 H 橋的完整代碼包覆蓋 GPIO 方向控制、定時器 PWM 調(diào)速、正反轉(zhuǎn)與剎車邏輯適合剛接觸電機(jī)驅(qū)動的學(xué)生、做小車底盤的開發(fā)者以及需要把現(xiàn)成驅(qū)動移植到自己板子上的從業(yè)者。它解決的不是“能不能轉(zhuǎn)”而是“轉(zhuǎn)得穩(wěn)、調(diào)得準(zhǔn)、換板子還能用”。下面按我拆包復(fù)現(xiàn)的順序講參數(shù)怎么設(shè)、坑在哪都落到具體位置。2. 拆開代碼包看結(jié)構(gòu)L298N 到底怎么被 STM32 管住2.1 先認(rèn)清 L298N 的引腳語義再談代碼L298N 是雙 H 橋每個橋有 IN1/IN2 兩個邏輯輸入和 ENA 使能端。邏輯真值很直接IN1 高、IN2 低正轉(zhuǎn)反過來反轉(zhuǎn)兩個同高是剎車快速停止兩個同低是惰行自由滑行。ENA 接 PWM 時占空比決定輸出平均電壓也就是轉(zhuǎn)速。很多人翻車在把 ENA 直接拉高當(dāng)使能結(jié)果只能全速跑調(diào)速功能形同虛設(shè)。代碼包里對每個電機(jī)抽象出三個引腳方向 A、方向 B、PWM。這個抽象很關(guān)鍵因為不同板子的引腳分配不一樣只要改宏定義就能換端口不用動驅(qū)動邏輯。我一般會先畫一張引腳映射表再動手避免后面改一處漏一處。信號L298N 側(cè)STM32 側(cè)常見接法作用IN1電機(jī) A 方向 1任意 GPIO 推挽輸出正反轉(zhuǎn)組合IN2電機(jī) A 方向 2任意 GPIO 推挽輸出正反轉(zhuǎn)組合ENA電機(jī) A 使能定時器 PWM 通道調(diào)速IN3/IN4電機(jī) B 方向任意 GPIO第二路電機(jī)ENB電機(jī) B 使能另一路 PWM第二路調(diào)速注意 ENA/ENB 上的跳線帽很多模塊出廠默認(rèn)短接到 5V插著跳線帽接 PWM 是沒反應(yīng)的得先拔掉。這個細(xì)節(jié)代碼里不會寫但它是“代碼沒錯電機(jī)不轉(zhuǎn)”的頭號原因。2.2 定時器 PWM 的配置邏輯別只抄 CubeMX 默認(rèn)值代碼包里 PWM 走的是通用定時器配置順序是開時鐘、設(shè)預(yù)分頻和自動重裝、配通道為 PWM 模式、設(shè)比較值、使能輸出、啟動定時器。核心參數(shù)就兩個預(yù)分頻 PSC 和自動重裝 ARR它們共同決定 PWM 頻率f_pwm f_clk / ((PSC 1) * (ARR 1))L298N 的開關(guān)頻率建議落在 1kHz 到 20kHz 之間。太低電機(jī)會有可聞嘯叫和抖動太高又受 H 橋開關(guān)損耗限制。我一般取 8kHz 到 10kHz避開人耳敏感段又留足余量。假設(shè)主頻 72MHz取 PSC71、ARR99得到 72M/(72*100)10kHz比較值范圍 0 到 99分辨率 1%夠用。// 定時器 PWM 初始化以 TIM3 通道1 為例72MHz 主頻 void MotorPWM_Init(void) { // 1. 開定時器和 GPIO 時鐘 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 2. PWM 引腳復(fù)用推挽輸出 GPIO_InitTypeDef gpio; gpio.GPIO_Pin GPIO_Pin_6; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); // 3. 時基72M/(711)/(991) 10kHz TIM_TimeBaseInitTypeDef tb; tb.TIM_Prescaler 71; tb.TIM_Period 99; tb.TIM_CounterMode TIM_CounterMode_Up; tb.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, tb); // 4. 通道1 設(shè)為 PWM1 模式初始占空比 0 TIM_OCInitTypeDef oc; TIM_OCStructInit(oc); oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_Pulse 0; oc.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, oc); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE); }這段代碼里TIM_Pulse就是比較值 CCR改它等于改占空比。TIM_OCPreload_Enable讓新占空比在下一個周期生效避免中途改值產(chǎn)生毛刺。TIM_OCPolarity_High表示比較值越大高電平越長如果你的驅(qū)動電路是低有效這里要改成 Low否則調(diào)速方向是反的——這是第二個高頻翻車點。2.3 方向控制與調(diào)速的封裝把真值表寫進(jìn)函數(shù)方向引腳用普通推挽輸出即可速度靠 PWM。代碼包把兩者封成一個Motor_Set(speed)speed 取 -100 到 100正負(fù)表方向絕對值表占空比。這樣上層調(diào)用不用關(guān)心真值表換電機(jī)、換接線只改底層。// 方向引腳初始化IN1PA4, IN2PA5 void MotorDir_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef gpio; gpio.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_5; gpio.GPIO_Mode GPIO_Mode_Out_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); GPIO_ResetBits(GPIOA, GPIO_Pin_4 | GPIO_Pin_5); // 默認(rèn)惰行 } // speed: -100 ~ 100正轉(zhuǎn)正、反轉(zhuǎn)負(fù)、0 惰行 void Motor_Set(int8_t speed) { uint16_t duty; if (speed 100) speed 100; if (speed -100) speed -100; if (speed 0) { // 正轉(zhuǎn) GPIO_SetBits(GPIOA, GPIO_Pin_4); GPIO_ResetBits(GPIOA, GPIO_Pin_5); duty speed; } else if (speed 0) { // 反轉(zhuǎn) GPIO_ResetBits(GPIOA, GPIO_Pin_4); GPIO_SetBits(GPIOA, GPIO_Pin_5); duty -speed; } else { // 惰行 GPIO_ResetBits(GPIOA, GPIO_Pin_4); GPIO_ResetBits(GPIOA, GPIO_Pin_5); duty 0; } TIM_SetCompare1(TIM3, duty); // 0~100 映射到 CCR }這里有個隱含假設(shè)ARR 是 99所以 duty 直接當(dāng) CCR 用。如果改了 ARR映射要跟著改寫成duty * (ARR1) / 100更穩(wěn)妥。另外int8_t范圍是 -128 到 127限幅到 ±100 是安全的。剎車功能沒放進(jìn)這個函數(shù)需要時單獨調(diào)Motor_Brake()把兩個方向腳都拉高再配合 PWM 歸零急停場景才用日常別亂剎車H 橋電流沖擊不小。3. 把代碼跑起來從接線到閉環(huán)調(diào)速的完整步驟3.1 供電與共地先解決“上電就復(fù)位”L298N 模塊有 12V 電機(jī)供電和 5V 邏輯供電兩路。常見做法是電機(jī)用獨立電池經(jīng)模塊的 12V 端子模塊自帶的 5V 輸出給 STM32 供電——但要注意模塊 5V 輸出能力有限帶不動板子加外設(shè)時STM32 會反復(fù)復(fù)位現(xiàn)象是電機(jī)轉(zhuǎn)一下停一下。血淚經(jīng)驗邏輯供電和電機(jī)供電分開至少保證共地電機(jī)大電流回流不要走信號地。接線順序我固定為先接共地再接邏輯電源最后接電機(jī)電源。上電前用萬用表量一下 5V 和 GND 有沒有短路這一步省不得。電機(jī)線先不接燒錄后先用 LED 或示波器看 PWM 腳有沒有波形確認(rèn)定時器跑起來了再接電機(jī)能少燒幾片驅(qū)動。3.2 用串口指令驗證正反轉(zhuǎn)與占空比代碼包帶一個簡單的串口解析收到字符加速、-減速、0停、r換向。驗證時不要一上來就接輪子先空載看電機(jī)軸。步驟是燒錄、開串口、發(fā)看轉(zhuǎn)速是否階梯上升、發(fā)r看是否反向、發(fā)0看是否惰行。如果方向?qū)Φ俣炔蛔兓厝ゲ?ENA 跳線帽和TIM_SetCompare1有沒有被調(diào)用如果速度變但方向不換查方向腳是不是被別的外設(shè)復(fù)用了。// 串口接收回調(diào)里的簡易指令處理 void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { char c USART_ReceiveData(USART1); static int8_t spd 0; if (c ) spd 10; else if (c -) spd - 10; else if (c 0) spd 0; else if (c r) spd -spd; Motor_Set(spd); USART_SendData(USART1, c); // 回顯確認(rèn)收到 } }回顯這行別省它是判斷“指令到底進(jìn)沒進(jìn)中斷”的最快手段。串口沒反應(yīng)先查波特率和中斷優(yōu)先級別急著懷疑電機(jī)代碼。3.3 加編碼器做閉環(huán)把“能轉(zhuǎn)”變成“轉(zhuǎn)得準(zhǔn)”開環(huán)調(diào)速在電池電壓下降后轉(zhuǎn)速會漂做循跡或定距就得加編碼器。代碼包預(yù)留了編碼器接口用定時器的編碼器模式讀 AB 相。配置要點把編碼器對應(yīng)的兩個引腳設(shè)為浮空輸入定時器選編碼器模式 TI1 和 TI2 都計數(shù)ARR 設(shè)最大定期讀計數(shù)差算速度。常見做法是 10ms 讀一次差值除以線數(shù)得到轉(zhuǎn)速再進(jìn)一個簡單 PI 環(huán)修正 PWM。// 定時器編碼器模式初始化TIM4 CH1/CH2 接編碼器 A/B 相 void Encoder_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitTypeDef gpio; gpio.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, gpio); TIM_TimeBaseInitTypeDef tb; tb.TIM_Prescaler 0; tb.TIM_Period 65535; tb.TIM_CounterMode TIM_CounterMode_Up; tb.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM4, tb); TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM4, 0); TIM_Cmd(TIM4, ENABLE); }TIM_EncoderMode_TI12表示 AB 相都觸發(fā)計數(shù)四倍頻分辨率最高。讀的時候用TIM_GetCounter取當(dāng)前值和上次值做差注意計數(shù)器會溢出回繞差值要用有符號處理。PI 參數(shù)先給 P2、I0.1 這種小值試積分飽和會讓電機(jī)在目標(biāo)速度附近來回沖必要時加積分限幅。4. 避坑與排查這幾處翻車我見過太多次4.1 電機(jī)不轉(zhuǎn)代碼編譯通過現(xiàn)象燒錄成功串口有回顯電機(jī)紋絲不動。原因多半是 ENA 跳線帽沒拔PWM 根本沒進(jìn)到使能端或者方向腳和 PWM 腳在 CubeMX 里被分配到同一個引腳后配置的覆蓋了前者。解決拔跳線帽用萬用表量 ENA 對地電壓是否隨占空比變化再核對引腳分配表一個引腳只歸一個功能。4.2 只有一個方向能轉(zhuǎn)反向沒反應(yīng)現(xiàn)象正轉(zhuǎn)正常發(fā)反向指令電機(jī)停住或抖動。原因通常是方向真值表寫反或者某個方向腳被配置成了復(fù)用功能而非普通輸出。解決單獨把兩個方向腳拉高拉低測電平確認(rèn) GPIO 模式是Out_PP再對照真值表逐行核對。別信“我肯定寫對了”用表量。4.3 調(diào)速時電機(jī)嘯叫、低速抖動現(xiàn)象占空比低時電機(jī)嗡嗡響甚至不轉(zhuǎn)占空比高才順。原因是 PWM 頻率落在可聞段或者低速時平均電壓不足以克服靜摩擦。解決把頻率提到 8kHz 以上低速段做死區(qū)補(bǔ)償占空比低于某個閾值比如 15%時直接給一個起步值別讓它卡在臨界點。4.4 一接電機(jī) STM32 就復(fù)位現(xiàn)象空載正常帶電機(jī)就重啟。原因是電機(jī)啟動電流把邏輯電源拉垮或者地線走法讓回流串進(jìn)信號地。解決電機(jī)電源和邏輯電源分開供共地單點連接電機(jī)兩端并一個續(xù)流二極管或 RC 吸收電源入口加足夠大的電解電容。這是硬件問題改代碼救不回來。4.5 編碼器讀數(shù)只增不減或亂跳現(xiàn)象電機(jī)正反轉(zhuǎn)編碼器計數(shù)都往一個方向走或者靜止時數(shù)值亂跳。原因是 AB 相接線順序反了或者輸入沒上拉、受干擾。解決對調(diào) A/B 相兩根線看方向是否恢復(fù)輸入改上拉或外部加 10k 上拉信號線遠(yuǎn)離電機(jī)線走。計數(shù)亂跳還要查定時器是不是被別的功能復(fù)用了。5. 進(jìn)階把驅(qū)動層抽出來換板子只改一個頭文件代碼包最值錢的地方不是那幾行 PWM而是把“引腳映射”和“驅(qū)動邏輯”分開了。我后來把它重構(gòu)成一個motor_port.h里面只有宏定義和三個函數(shù)聲明換 STM32 型號或換引腳時只動這個文件motor.c一行不改。這個習(xí)慣救過我很多次——同一套底盤代碼在 F1 和 F4 上跑只改時鐘和引腳宏半天就能移植完。驗證驅(qū)動是否真的解耦我有個笨辦法故意把 PWM 引腳宏改到一個沒接電機(jī)的空閑腳編譯燒錄如果除了電機(jī)不轉(zhuǎn)之外沒有任何編譯錯誤和運行異常說明抽象是干凈的如果一改就報一堆未定義說明邏輯里還硬編碼了端口。這個方法比讀代碼快。重構(gòu)前重構(gòu)后收益端口號散落在各函數(shù)集中在motor_port.h換板只改一處占空比直接寫 CCR統(tǒng)一 0~100 映射改 ARR 不影響上層方向真值表內(nèi)聯(lián)封裝成Motor_Set調(diào)用方不碰硬件還有一個細(xì)節(jié)PWM 啟動前先把比較值清零再使能定時器能避免上電瞬間電機(jī)猛沖一下。這個“上電沖擊”在帶輪子的車上表現(xiàn)為突然竄一下很嚇人加一行TIM_SetCompare1(TIM3, 0)就壓住了。從那以后我每次寫電機(jī)初始化都強(qiáng)制先歸零再使能這個順序已經(jīng)成了肌肉記憶。希望這份拆解幫到你少走我當(dāng)年那些彎路。本文還有配套的精品資源點擊獲取