
簡介本資源是一套面向嵌入式初學者與STM32實踐者的C語言電機驅動工程專為正點原子STM32F103核心板適配L298N雙H橋驅動模塊設計解決直流電機正反轉、差速拐彎及PWM無級調速等典型控制需求適用于智能小車、機器人底盤等硬件項目開發。壓縮包共76個文件含33個頭文件.h定義外設配置與函數接口、32個源文件.c實現GPIO初始化、PWM輸出、電機狀態切換等核心邏輯另有Keil工程配置.uvprojx/.uvoptx、啟動文件、系統庫及LICENSE等完整開發支撐文件總大小312KB。已有125人下載學習。讀者可直接導入Keil MDK編譯運行獲得可驗證的電機控制功能代碼框架涵蓋GPIO推挽輸出配置、TIM定時器PWM生成、IN1–IN4電平組合邏輯封裝及左右輪獨立調速函數目錄結構遵循正點原子標準模板CORE/SYSTEM/HARDWARE/USER便于理解STM32固件庫工程組織方式與電機驅動分層設計思想。 給單片機小車寫驅動繞不開的坎就是電機控制。我從一塊正點原子F103核心板和一塊L298N驅動模塊開始把正反轉、差速拐彎、PWM調速這幾個功能全部跑通前前后后踩了不少坑今天把這套驅動程序的完整設計思路、接線方案、代碼實現和排查經驗一次性整理出來。這篇東西寫給剛入門STM32、想玩小車但卡在電機驅動這一層的朋友也寫給那些已經能點燈但一接電機就“冒煙”的同學。先說結論L298N驅動直流電機這件事本身不難難的是把GPIO方向控制、定時器PWM調速、按鍵交互邏輯這三塊整合在一起還要保證小車跑起來不抖、不偏、不燒板子。我的這套驅動用標準庫風格實現代碼量不大但每一行后面都有原理支撐照著抄能跑理解了能改。1. 項目背景與整體設計思路1.1 做這個小車驅動要解決什么問題很多人第一次接觸STM32都是從點燈開始的可點完燈之后想做個能跑的小車突然發現電機驅動這塊的資料特別碎有的教程只講L298N怎么接不講代碼怎么寫有的代碼寫出來了但一上電電機嗡嗡叫就是不轉還有的轉向邏輯搞得特別復雜根本看不懂。我的核心訴求很簡單用正點原子F103核心板作為主控通過L298N驅動兩個直流減速電機實現三個最基本的功能——電機正反轉、小車拐彎、速度連續可調。這三個功能是任何輪式機器人的地基搞懂了之后后面做循跡、避障、遙控小車都是在這個基礎上加傳感器和邏輯而已。這個項目中真正值得研究的是兩個層面的問題硬件層面是STM32和L298N怎么正確匹配包括電平、電源、共地這些容易踩坑的地方軟件層面是如何把“方向”和“速度”這兩個控制維度分離——方向控制走GPIO電平速度控制走定時器PWM兩者互不干擾這樣的架構寫出來的代碼才清晰后面加功能也容易。1.2 硬件選型為什么是F103加L298N的組合正點原子F103核心板用的是STM32F103系列芯片72MHz主頻內置多個定時器每個定時器又能輸出多路PWM對于控制兩路直流電機這種場景來說性能和資源都綽綽有余。市面上很多小車套件用的就是這款芯片資料多、例程全、出了問題也好查。L298N則是經典的電機驅動芯片內部集成雙H橋一片就能獨立驅動兩個直流電機。它和單片機之間只需要四根方向控制引腳加兩根PWM使能引腳邏輯非常簡單。很多新手會糾結要不要換成TB6612或者DRV8833這類更“現代”的驅動芯片——確實它們體積更小、壓降更低但L298N勝在皮實耐操、引腳排列清晰、出錯容錯率高作為理解電機驅動原理的第一個芯片再合適不過了。選型還有一層考慮正點原子核心板已經把最小系統、調試接口、按鍵和LED都做上去了省去了自己焊最小系統的麻煩調試時直接用板載按鍵模擬控制信號連JLINK或STLINK就能下載程序可以讓我把精力全部集中在電機驅動程序本身的邏輯上而不是浪費在外圍電路排查上。1.3 軟件架構把“方向控制”和“速度控制”拆開寫驅動最忌諱的就是把所有功能堆在一起。我一開始也犯過這個毛病在一個函數里既翻轉GPIO又修改PWM占空比結果想改個轉速還得在控制邏輯里翻來翻去。后來重構了一把把軟件拆成三個層次。底層是硬件抽象層負責初始化GPIO和定時器提供“設置某個引腳高/低電平”“設置某路PWM占空比”這樣的原子操作。中間是電機控制層把底層操作組合成有意義的電機動作比如電機正轉、反轉、停止、左側減速、右側減速。最上層是業務邏輯層處理按鍵輸入、模式切換這類交互邏輯它只需要調用中間層提供的函數接口完全不用關心硬件細節。這樣分層的直接好處是調試方便小車不動了先查最底層GPIO和PWM有沒有波形輸出再查中間層控制函數有沒有被正確調用最后才查業務邏輯。每一層都可以獨立測試問題定位速度快很多。后面想加超聲波避障或者紅外循跡也只需要在業務邏輯層加判斷完全不碰底層驅動代碼。這套思路其實就是嵌入式開發里經典的分層設計花小車的錢提前養成好習慣值。2. 硬件接線與模塊原理2.1 L298N內部H橋到底是怎么轉的先不急著接線把L298N內部的東西搞清楚。L298N內部是兩個完整的H橋電路每個H橋由四個開關管組成電機接在H橋的中間橫臂上。所謂“正轉”就是讓對角線上的兩個開關管導通電流從電機一端流向另一端所謂“反轉”就是換另一條對角線導通電流方向反過來。對應到L298N模塊的引腳上每個電機有三個控制輸入端IN1、IN2和ENA。其中IN1和IN2負責控制H橋四個開關管的組合狀態ENA是使能端。當ENA為高電平時H橋電路才真正接通IN1和IN2的電平組合才有意義當ENA為低電平時不管IN1和IN2是什么狀態電機都不會轉相當于被“關斷”了。所以就有了最關鍵的設計思路用兩個GPIO引腳給IN1、IN2輸出高低電平決定電機轉動的方向用一路PWM信號接到ENA通過改變PWM的占空比來控制電機的平均電壓從而實現調速。方向控制是開關量速度控制是模擬量兩種不同性質的信號各走各的通道互不干擾這就是整個驅動程序的底層邏輯。2.2 F103核心板引腳規劃與接線表引腳規劃上我遵循一個原則方向控制引腳隨便選普通GPIO就行但PWM引腳必須挑定時器的通道引腳。STM32F103的每個定時器輸出通道都有對應的引腳映射比如TIM3的CH3在PB0上CH4在PB1上。我選PB0和PB1作為兩路PWM輸出用來接L298N的ENA和ENB。方向和使能引腳的分配是這樣的功能L298N引腳STM32引腳左電機方向1IN1PA4左電機方向2IN2PA5左電機使能/調速ENAPB0 (TIM3_CH3)右電機方向1IN3PA6右電機方向2IN4PA7右電機使能/調速ENBPB1 (TIM3_CH4)電源地GNDGND電機供電我用的是一塊12V鋰電池組直接接L298N的12V輸入端子。L298N模塊上的5V輸出引腳給F103核心板供電但前提是L298N的12V輸入足夠穩定。這個方案接線少、使用簡單但后面我會專門講為什么實際跑車時我建議給單片機單獨供電別長期依賴L298N的5V輸出。2.3 電源與共地問題最容易燒板子的一步接線接錯最容易出的問題就是電源。L298N模塊上通常有VCC、GND和5V三個電源相關的端子。VCC接電機電源范圍通常是7V到12V我實測12V沒問題GND是電源地5V是模塊自帶的穩壓輸出可以給外部邏輯電路供電。這里有個至關重要的細節單片機的GND必須和L298N的GND連接在一起。只有兩邊共地了單片機輸出的高電平對L298N來說才是真正的高電平。如果地不共單片機認為的3.3V高電平在L298N參考地眼里可能只有2V甚至更低導致H橋邏輯判斷混亂電機要么不動要么亂抖。我見過很多新手接完線電機死活不轉最后發現就是一根GND跳線沒插。還要注意不要把L298N的5V輸出當成核心板的唯一供電來源。如果你用的電機是12V大功率減速電機啟動瞬間電流可能到1A以上L298N的穩壓芯片壓力很大輸出的5V可能被拉垮連帶單片機復位重啟。所以我實際跑車的時候給F103核心板單獨接了一路5V電源L298N的5V輸出只作為一個參考電源或干脆不接。3. 驅動核心實現正反轉與PWM調速3.1 GPIO初始化方向控制的基礎方向控制的本質就是讓GPIO引腳輸出高電平或者低電平。STM32的GPIO在使用前必須初始化告訴芯片這個引腳是輸入還是輸出、輸出模式是什么、速度多快。對于L298N這種邏輯輸入引腳我全部配置成推挽輸出模式頻率選50MHz就夠用了。GPIO初始化代碼中有一個關鍵點是看引腳所在的端口總線。STM32的GPIO掛在APB2總線上所以初始化前必須開啟對應端口的時鐘。我以前剛學的時候經常忘了開時鐘結果引腳電平怎么都拉不上去后來才明白是時鐘沒打通寄存器操作根本沒生效。初始化完成后可以先用萬用表量一下引腳電壓來驗證配置是否正確。程序里把IN1引腳拉高、IN2拉低用萬用表測PA4應該能讀到大約3.3V電壓PA5應該接近0V。這一步驗證做完才說明GPIO層面的驅動是通的再去接L298N就不會一頭霧水了。3.2 定時器PWM配置轉速怎么調PWM調速的原理是改變一個周期內高電平所占的比例這個比例就叫占空比。電機兩端看到的平均電壓 電源電壓 × 占空比占空比越高電機轉速越快。STM32的定時器產生PWM不需要CPU干預只要配置好定時器的工作模式和比較值硬件會自動翻轉引腳電平這大大減輕了CPU負擔。我使用TIM3的CH3和CH4輸出兩路PWM。定時器配置時有三個關鍵參數預分頻系數PSC、自動重裝載值ARR、比較值CCR。這三個值決定了PWM的周期和占空比。PSC用來把72MHz的系統時鐘分頻到一個合理的計數頻率ARR決定計數周期CCR則是比較值當計數器計數到CCR時輸出電平翻轉。具體數值這樣算我想讓PWM頻率在1kHz左右。定時器輸入時鐘是72MHz先經過預分頻PSC設為71實際上是72分頻計數頻率變成72MHz ÷ 72 1MHz也就是計數器每1微秒加1。然后ARR設為999計數器從0數到999剛好需要1000微秒所以PWM頻率就是1kHz。此時把CCR設為500占空比就是500/1000 50%電機得到一半的平均電壓。那為什么選1kHz而不是更高頻率L298N使用的續流二極管和開關管的開關速度有限頻率太高會導致開關損耗增大、管子發熱頻率太低又會讓電機產生“咔咔”的抖動聲。實測下來1kHz到10kHz之間都是可以接受的我選1kHz只是為了方便示波器觀察和計算你可以根據自己的電機特性調整。3.3 電機控制函數與拐彎邏輯方向控制函數本質上是IN1、IN2引腳的邏輯組合。同一個電機IN1高、IN2低為正轉IN1低、IN2高為反轉兩個引腳電平相同則電機制動停止。我封裝了三個基礎函數分別對應正轉、反轉和停止這樣業務邏輯里就不需要關心具體引腳了。對于兩個電機的小車底盤正轉前進和反轉后退直接調用基礎函數就行。但拐彎就稍微復雜一點我用了差速轉向的方案左轉的時候左側電機速度降低右側電機速度保持不變這樣兩邊輪子轉速不同小車就向左拐彎右轉同理反過來。速度差越大轉彎半徑就越小。這種拐彎方式的優點是不需要額外硬件完全靠軟件調節PWM占空比實現。我把差速比例抽成一個參數比如左轉時左側PWM占空比設為目標速度的40%右側保持100%實測下來轉彎半徑大約在30厘米左右比較適合需要機器人原地轉彎的場景。如果想要更急的轉彎可以讓一側電機反轉兩個輪子朝相反方向轉動小車就能原地轉圈這個后續按需擴展就行。4. 完整代碼與關鍵實現4.1 代碼結構說明整個工程分成三個模塊main.c負責主循環和按鍵邏輯motor.c負責電機相關初始化和控制函數tim_pwm.c負責定時器PWM的初始化。這樣的分離讓每個模塊的職責非常單一代碼可讀性高也方便后面移植。我在正點原子F103核心板例程的工程模板基礎上修改復用了它的按鍵初始化和延時函數。時鐘頻率方面系統默認是外部晶振倍頻到72MHz這也是正常開發時的標準配置。核心的電機和PWM代碼都是自己寫的不依賴例程方便你看明白每一步在干什么。4.2 關鍵代碼逐段解讀先看定時器PWM初始化函數這個函數的作用是把PB0和PB1配置成TIM3的CH3和CH4輸出工作模式是PWM1模式。PWM1模式指的是計數器計數到CCR之前輸出高電平之后變低電平占空比越大平均電壓越高。void TIM3_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // PB0和PB1復用為定時器輸出功能 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 定時器基礎配置計數器向上計數到arr后自動清零 TIM_TimeBaseStructure.TIM_Period arr; TIM_TimeBaseStructure.TIM_Prescaler psc; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // CH3和CH4都配置為PWM1模式輸出極性高電平有效 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2Init(TIM3, TIM_OCInitStructure); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }調用參數上我傳入了PSC71ARR999對應前面說的1kHz頻率。要注意這里用的TIM_OC2Init對應于CH3和CH4嗎不是OC1或OC2對應的是TIM3的CH1/CH2。我實現里實際上是把TIM3的CH3和CH4分別對應到PB0和PB1所以在配置OC時要選OC3和OC4。上面OC1只是示意實際工程里要寫OC3和OC4的初始化這里提醒你注意對應關系。GPIO方向的初始化比較簡單就不貼完整代碼了核心就是把PA4到PA7都配置成推挽輸出初始狀態全部拉低保證上電瞬間電機不會偷跑。接下來是電機控制函數這是整個驅動最核心的部分。我實現了一個底層設置函數接收兩個方向的邏輯值和兩個PWM比較值然后一次性輸出到引腳和定時器void Motor_Set(int left_direction, int left_speed, int right_direction, int right_speed) { // left_direction和right_direction取1表示正轉0表示反轉 if(left_direction 1) { GPIO_SetBits(GPIOA, GPIO_Pin_4); GPIO_ResetBits(GPIOA, GPIO_Pin_5); } else { GPIO_ResetBits(GPIOA, GPIO_Pin_4); GPIO_SetBits(GPIOA, GPIO_Pin_5); } if(right_direction 1) { GPIO_SetBits(GPIOA, GPIO_Pin_6); GPIO_ResetBits(GPIOA, GPIO_Pin_7); } else { GPIO_ResetBits(GPIOA, GPIO_Pin_6); GPIO_SetBits(GPIOA, GPIO_Pin_7); } // 通過修改TIM3的CCR寄存器來改變占空比 TIM_SetCompare3(TIM3, left_speed); TIM_SetCompare4(TIM3, right_speed); }在這個基礎上前進后退拐彎的函數就是對這個底層設置函數的封裝。比如前進就是左側正轉加右側正轉左轉就是左側速度減半右側全速。這樣封裝的好處是任何新動作都只需要一次函數調用業務邏輯層非常干凈。4.3 按鍵控制邏輯與實測效果我在主循環里用正點原子F103核心板上的三個按鍵做交互KEY1按下小車前進KEY0按下小車后退KEY_UP每按一次速度加一檔。因為按鍵會抖動所以我復用了例程里的按鍵掃描函數檢測到有效電平變化后延時消抖再確認。速度我用的是一個全局變量speed_level取值范圍從200到900對應PWM比較值也就是占空比20%到90%。設置下限20%是有講究的直流減速電機存在靜摩擦力占空比太低時電機兩端電壓不足以克服靜摩擦會出現“叫喚但不轉”的現象。我給了一個200的初始值這樣上電默認小車就能動而不是卡在0%的占空比上讓新手以為程序寫錯了。實測效果上PWM占空比從200到900線性變化電機轉速能明顯感覺到從慢到快的平滑過渡沒有階梯感。轉向方面40%差速比實現的轉彎半徑適中在普通桌面上就能測試不會瞬間沖出桌面。整個系統連續跑了十分鐘電機微熱L298N芯片有發熱但手能按住屬于正常范圍。5. 常見問題與排查技巧5.1 電機不轉/抖動的排查順序小車接完線死活不轉這是每個做驅動的人都會遇到的事。我的排查順序是從電源開始往前查一步步縮小范圍。先看L298N上的電源指示燈是否點亮再量12V輸入和5V輸出是否正常接著檢查STM32和L298N是否共地然后用萬用表量IN1、IN2引腳在程序運行后是否能正常翻轉電平。如果電機是“嗡嗡叫但轉子不動”大概率是PWM頻率設置不合理或者占空比太低。前者把頻率調到1kHz到10kHz之間即可后者把占空比抬高到20%以上。電機抖動還有一個隱蔽原因使能引腳ENA懸空。有些模塊的ENA上有跳線帽如果你用PWM控制調速必須把跳線帽拔掉讓PWM信號真正接入如果不拔模塊內部把ENA拉到高電平你的PWM信號根本進不去轉速永遠是100%。5.2 PWM沒反應或調速不線性PWM完全沒反應先查定時器和引腳的對應關系。很多STM32的引腳有多重復用功能你配置的是TIM3_CH3輸出但引腳的復用功能映射可能默認不是這個。F103的引腳不需要額外的AFIO重映射時通常默認就能用但如果你選錯引腳比如用了PA6卻以為它能輸出TIM3_CH3那肯定沒有波形。遇到這個問題我建議直接看一下芯片的數據手冊中定時器通道和引腳對應的表格或者用示波器量一下引腳有沒有波形這個問題兩分鐘就能定位。調速不線性是另一個常見問題。PWM占空比和電機轉速并不是嚴格線性的關系因為電機內部的線圈電感、摩擦阻力、電源內阻都會影響實際轉速。表現為占空比在30%到50%的時候轉速變化不明顯到了70%以上突然變快。這不是程序問題是電機本身的特性。要改善可以讓PWM比較值按指數曲線映射而不是線性映射或者直接忽略這種非線性畢竟小車控制精度要求沒那么高。5.3 L298N發熱和電源問題的避坑L298N發熱是很多人的心頭病但這其實是一個正常現象。L298N用的是雙極型晶體管工藝導通壓降比MOSFET方案大得多電流越大發熱越厲害。實測兩路電機同時工作在1A電流下芯片表面溫度到60度以上不奇怪。解決辦法是加大散熱片、降低PWM頻率減少開關損耗或者干脆換MOSFET方案的驅動芯片。電源問題上面已經強調過再補一個實操經驗不要用USB給F103核心板供電的同時指望核心板再通過GPIO輸出引腳給L298N的邏輯部分供電。雖然L298N邏輯部分電流不大但如果你把外部傳感器也接在核心板上總電流可能超過USB口的500mA極限導致核心板電壓被拖垮、程序跑飛。正確的做法是電機電源、主控電源、邏輯電源各管各的只在信號地線上共地。最后再分享一個我實測中發現的細節上電順序很關鍵。理想順序是先給L298N供電再給STM32供電這樣STM32初始化程序時能正確配置所有GPIO為低電平避免L298N在輸入引腳懸空時誤動作。如果反過來STM32先上電但程序還在跑初始化之前懸空引腳可能被內部上拉電阻拉到高電平導致電機在上電瞬間猛地動一下對機械結構不友好也容易嚇人一跳。6. 實操心得與后續擴展整套驅動寫下來我最想強調的一個經驗就是先讓硬件在最小范圍內動起來再去堆軟件功能。我第一次調試時直接寫了全部功能結果電機不轉排查了半天到最后發現是ENA跳線帽沒拔浪費了一個晚上的時間。后來改成先把GPIO方向控制調通讓電機能正轉反轉再加PWM調速讓電機轉速可控最后才加按鍵和轉向邏輯。每加一個功能都驗證一個問題出現時一下子就能定位到新加的環節。這套驅動寫完之后后面可以擴展的方向很多。最簡單的擴展是加一個藍牙串口模塊把按鍵控制改成手機控制代碼只需要在業務邏輯層加一個串口解析函數。稍微復雜一點可以加編碼器測速用TIM定時器的編碼器模式讀取電機轉速做一個閉環PID調速這樣小車走直線就不容易跑偏了。再往后加超聲波模塊做避障加紅外傳感器做循跡核心的電機驅動層代碼幾乎不用動這也是我為什么強調一開始就要把驅動層和控制邏輯層分離的原因。有同學問過我代碼是用標準庫還是HAL庫。正點原子的老教程大量用的是標準庫網上資料多、直接能搜到答案HAL庫則更貼近STM32CubeMX的圖形化配置流程代碼生成效率高。兩種庫本身的控制原理完全一樣都是操作寄存器。學的時候建議選一種主流庫吃透不要頻繁切換否則容易陷入工具選擇的糾結中反而忽略了真正重要的電機控制原理本身。這篇驅動最核心的收獲就是你親手搞懂了“方向靠電平、速度靠PWM”這個電機控制的基本功這套概念可以平移到幾乎所有電機驅動場景。哪怕以后用更高級的驅動芯片、更大功率的電機控制的本質都不會變。我的建議是拿到代碼之后把每一部分和實物接線、芯片手冊對照著看一遍改一改速度參數試試效果然后試著從零自己寫一遍只有自己能寫出來才算真的掌握。本文還有配套的精品資源點擊獲取