板從入門到實(shí)戰(zhàn):點(diǎn)燈、外設(shè)與避坑指南)
STM32F103的開發(fā)板剛到手很多人都是這個(gè)狀態(tài)快遞拆開板子拿在手里翻來覆去看了幾遍數(shù)據(jù)線一插電腦“叮咚”一聲然后就沒有然后了。板子吃灰三個(gè)月再想起來的時(shí)候連Keil都打不開了。這個(gè)標(biāo)題——“stm32-103的開發(fā)板買回來了想學(xué)stm32的可以點(diǎn)個(gè)關(guān)注”——看著像是個(gè)新手報(bào)到帖但過來人都知道這恰恰是學(xué)習(xí)嵌入式最關(guān)鍵的一個(gè)節(jié)點(diǎn)。板子到手之后那一周干了什么基本決定了你是三個(gè)月后能自己寫驅(qū)動(dòng)、調(diào)外設(shè)、做小項(xiàng)目還是把板子放在書架頂層落灰。這篇就當(dāng)是我陪你一起開箱把這板子從第一次通電到獨(dú)立做項(xiàng)目的完整路徑捋一遍。我盡量說人話該給代碼給代碼該講原理講原理該排坑排坑不搞云里霧里那一套。1. 為什么是F103一塊“老古董”憑什么還在被大家買STM32F103這塊芯片2007年問世到現(xiàn)在十幾年了。你要說它性能強(qiáng)那真算不上72MHz主頻64KB Flash20KB SRAM放在今天也就是個(gè)“中老年”水平。但你要是想學(xué)STM32在2025年買開發(fā)板F103依然是最合理的選擇之一。1.1 Cortex-M3的內(nèi)核底子STM32F103用的是ARM Cortex-M3內(nèi)核屬于經(jīng)典中的經(jīng)典。Cortex-M3是個(gè)什么概念它定義了中斷向量表怎么放、NVIC中斷控制器怎么工作、Thumb-2指令集怎么執(zhí)行、系統(tǒng)定時(shí)器SysTick長(zhǎng)什么樣。這些底層機(jī)制在M3、M4、M7甚至后來的M33內(nèi)核上大同小異。你在F103上學(xué)到的中斷優(yōu)先級(jí)分組、SysTick延時(shí)、位帶操作、啟動(dòng)文件里的向量表跳轉(zhuǎn)流程換到F407、H743上照樣成立只是外設(shè)寄存器和時(shí)鐘樹變了。這就像學(xué)開車用了一輛老捷達(dá)離合手感清晰、油門響應(yīng)直接開熟了換奔馳寶馬你缺的只是適應(yīng)過程而不是重新學(xué)一遍。換句話說F103把“芯片內(nèi)部是怎么跑起來”這件事暴露得足夠清楚沒有那么多自動(dòng)化的隱藏邏輯特別適合用來建立嵌入式的地基認(rèn)知。1.2 生態(tài)和資料是真正的護(hù)城河F103真正無敵的地方不是性能是生態(tài)。你去搜“STM32 GPIO配置”打開十個(gè)網(wǎng)頁(yè)九個(gè)半是F103的例程去GitHub搜stm32f103相關(guān)的項(xiàng)目幾千個(gè)起步B站、CSDN、電子發(fā)燒友上關(guān)于F103的教程視頻和圖文多到你看不完。這個(gè)生態(tài)意味著什么意味著你在學(xué)習(xí)過程中遇到任何問題基本都有人踩過、問過、回答過。比如后面我講到的ILI9341讀ID讀到0xA1A1、CAN通信突然連不上、JTAG引腳被復(fù)用導(dǎo)致燒錄失敗這些坑全網(wǎng)都有大量討論你能找到幾十種排查思路。對(duì)新手來說資料密度就是最大的安全感。1.3 和其他開發(fā)板怎么選很多人買板子之前糾結(jié)F103、F407、ESP32、Arduino到底買哪個(gè)我直接說我的看法。板子主頻生態(tài)資料上手門檻適合目標(biāo)STM32F10372MHz海量中等打基礎(chǔ)、學(xué)底層、做畢設(shè)STM32F407168MHz多較難需要算力/攝像頭/DSPESP32240MHz雙核多低WiFi藍(lán)牙物聯(lián)網(wǎng)快速落地Arduino16MHz極多極低體驗(yàn)編程、極速原型我的建議是如果你目標(biāo)明確要做物聯(lián)網(wǎng)產(chǎn)品直接ESP32沒毛病如果你想認(rèn)真理解嵌入式、以后吃這碗飯F(tuán)103起步是最穩(wěn)的。而且F103開發(fā)板便宜50到80塊錢就能買到帶ST-Link的完整核心板燒壞了也不心疼。2. 從開箱到點(diǎn)亮第一顆LED環(huán)境搭建與時(shí)鐘系統(tǒng)很多新手第一步就卡住了卡住的點(diǎn)不是編程是環(huán)境。開發(fā)板到手第一件事不是寫代碼而是把工具鏈整明白。2.1 開箱先做三件事板子到手別急著上電先做三件事。第一認(rèn)清板載資源。看板子上有哪些東西ST-Link調(diào)試器、電源指示燈通常是紅色PWR、一個(gè)用戶LED大多在PC13藍(lán)色或紅色、兩個(gè)按鍵復(fù)位鍵和用戶鍵、USB轉(zhuǎn)串口芯片如CH340、BOOT0和BOOT1跳線或按鍵。把這些在板子上挨個(gè)找出來對(duì)照原理圖看一遍。第二確認(rèn)供電來源。F103核心板一般有兩種供電方式通過ST-Link的USB口供電或者通過板子上的5V/3.3V引腳外部供電。新手階段用USB供電即可但要注意如果你的板子是那種純核心板沒有板載ST-Link需要外接ST-Link調(diào)試器那就要確認(rèn)調(diào)試器是否給目標(biāo)板供電不然燒錄時(shí)會(huì)報(bào)“No target connected”。第三安裝驅(qū)動(dòng)。如果是CH340 USB轉(zhuǎn)串口芯片Windows第一次插上會(huì)提示安裝驅(qū)動(dòng)如果識(shí)別不了去WCH官網(wǎng)下最新的CH340驅(qū)動(dòng)裝完以后在設(shè)備管理器里能看到COM口。2.2 Keil MDK與芯片包把編譯器伺候好寫STM32最常用的IDE是Keil MDK-ARM。安裝過程不展開細(xì)說核心幾步裝Keil我建議裝5.x版本兼容性最好、在Pack Installer里在線安裝STM32F1系列的Device Family Pack、新建工程時(shí)選擇對(duì)應(yīng)芯片型號(hào)STM32F103C8T6或RCT6。這里有個(gè)容易被忽略的坑只裝Keil不裝芯片支持包新建工程時(shí)器件列表是空的什么都選不了。很多人卡在這一步半天以為Keil裝壞了。Pack安裝完成后還要確認(rèn)魔術(shù)棒Options for Target里的Debug頁(yè)面選的是ST-Link Debugger而不是默認(rèn)的ULINKSettings里確認(rèn)SWD模式、接口速率然后點(diǎn)“Settings”能看到芯片ID才說明調(diào)試器連接正常。再說說代碼編輯器很多教程默認(rèn)用Keil自帶的編輯器代碼補(bǔ)全能力和VS Code比確實(shí)有差距但新手階段用Keil沒問題少折騰一項(xiàng)是一樣。想用VS Code寫STM32的話需要搭EIDE插件或者PlatformIO這個(gè)后面學(xué)習(xí)路線里再說現(xiàn)階段先別把自己繞進(jìn)去。2.3 新建工程的三條路標(biāo)準(zhǔn)庫(kù)、HAL庫(kù)還是寄存器新建工程你有三條路標(biāo)準(zhǔn)外設(shè)庫(kù)Standard Peripheral Library、HAL庫(kù)Hardware Abstraction LayerSTM32CubeMX、純寄存器操作。我的建議是這樣分的如果你想把底層搞明白用標(biāo)準(zhǔn)庫(kù)代碼里能看到GPIO_InitStructure、RCC_APB2PeriphClockCmd這種直白的結(jié)構(gòu)體配置每個(gè)函數(shù)后面都能追到寄存器層面如果你想要快速做產(chǎn)品原型、或者以后打算跨芯片復(fù)用代碼就上HAL庫(kù)CubeMX圖形化配置時(shí)鐘和引腳代碼自動(dòng)生成。但記住一個(gè)核心原則不管用哪個(gè)庫(kù)芯片參考手冊(cè)RM0008針對(duì)F103必須放在手邊。庫(kù)只是把你和寄存器之間的操作包裝了一下最終寫的都是那些外設(shè)寄存器。你學(xué)會(huì)了標(biāo)準(zhǔn)庫(kù)配置GPIO的流程再看HAL庫(kù)代碼就會(huì)覺得它們只是“換了一層皮”底層邏輯完全一樣。標(biāo)準(zhǔn)庫(kù)新建工程的完整配置步驟老手都覺得煩要添加stm32f10x_conf.h、stm32f10x.h、啟動(dòng)文件startup_stm32f10x_md.s中等容量C8T6選md、core_cm3.h還要在外設(shè)庫(kù)目錄里把需要的gpio、rcc等源文件加進(jìn)來C/C選項(xiàng)卡里定義USE_STDPERIPH_DEVICE和STM32F10X_MD勾選MicroLIB以支持后面的printf重定向。這幾步少一個(gè)編譯就報(bào)錯(cuò)。零報(bào)錯(cuò)配置文件我強(qiáng)烈建議直接抄現(xiàn)成模板。網(wǎng)上找到“STM32F103標(biāo)準(zhǔn)庫(kù)空白工程模板”下載下來先跑通編譯再自己照著搭一個(gè)比對(duì)著文檔敲效率高太多。2.4 時(shí)鐘系統(tǒng)點(diǎn)亮LED前你必須先懂RCC點(diǎn)燈代碼本身只有幾行但里面的門道很深。先看代碼#include stm32f10x.h #include stm32f10x_gpio.h #include stm32f10x_rcc.h void Delay(void) { for (uint32_t i 0; i 500000; i); } int main(void) { GPIO_InitTypeDef GPIO_InitStructure; // 打開GPIOC外設(shè)的時(shí)鐘 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 配置PC13為推挽輸出最高翻轉(zhuǎn)速度50MHz GPIO_InitStructure.GPIO_Pin GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOC, GPIO_InitStructure); while (1) { GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 拉低引腳LED點(diǎn)亮 Delay(); GPIO_SetBits(GPIOC, GPIO_Pin_13); // 拉高引腳LED熄滅 Delay(); } }注意里面有一行很多人不理解但極其重要的代碼RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);為什么點(diǎn)燈之前要“打開時(shí)鐘”因?yàn)镾TM32為了省電默認(rèn)情況下幾乎所有外設(shè)的時(shí)鐘都是關(guān)閉的。你往GPIO配置寄存器里寫數(shù)據(jù)如果對(duì)應(yīng)的外設(shè)時(shí)鐘門控還是關(guān)著的寫進(jìn)去的數(shù)據(jù)根本不會(huì)起作用。這個(gè)“時(shí)鐘門控”機(jī)制分布在AHB、APB1、APB2三根總線橋后面每根總線都有對(duì)應(yīng)的RCC外設(shè)時(shí)鐘使能寄存器。打個(gè)比方時(shí)鐘就像是整個(gè)芯片內(nèi)部各單位共用的節(jié)拍器。你要讓GPIOC這個(gè)單元開始工作必須先打開它的節(jié)拍器——這就是RCC_APB2PeriphClockCmd做的事情。GPIO在APB2總線上所以用APB2Periph的時(shí)鐘命令。ST官方系統(tǒng)啟動(dòng)后默認(rèn)使用內(nèi)部8MHz RC振蕩器HSI所以哪怕你不配置外部晶振芯片也能跑只是主頻是8MHz而不是72MHz。真正的72MHz要通過外部8MHz晶振HSE經(jīng)過PLL鎖相環(huán)9倍頻得到8MHz × 9 72MHz。這條鏈路HSE8MHz→ PLLMUL×9→ PLLCLK72MHz→ AHB預(yù)分頻÷1→ APB1預(yù)分頻÷2得到36MHz→ APB2預(yù)分頻÷1得到72MHz。APB1上掛著USART2/3、SPI2/3、I2C1/2、定時(shí)器TIM2-7、看門狗這些外設(shè)最高36MHzAPB2上掛著GPIOA-E、USART1、SPI1、ADC、TIM1和TIM8最高72MHz。這個(gè)分頻關(guān)系在編寫串口波特率、定時(shí)器周期時(shí)非常關(guān)鍵后面你會(huì)無數(shù)次用到。系統(tǒng)時(shí)鐘初始化這一套標(biāo)準(zhǔn)庫(kù)默認(rèn)在SystemInit()函數(shù)里幫你做了它會(huì)自動(dòng)檢測(cè)HSE并配置PLL到72MHz但作為學(xué)習(xí)者必須把這條鏈路背下來。因?yàn)楫?dāng)你改用外部不同頻率的晶振或者以后用HAL庫(kù)時(shí)你會(huì)發(fā)現(xiàn)自己需要手動(dòng)改PLL配置不懂時(shí)鐘樹的人就是一頓亂試。2.5 點(diǎn)燈代碼與“為什么燈不亮”的排查思路寫完了代碼、編譯通過、點(diǎn)擊下載結(jié)果燈不亮。怎么辦我見過太多新手卡在這里然后開始懷疑人生。排查思路應(yīng)該是線性的別跳步驟看電源指示燈亮不亮。不亮說明沒供電檢查USB線是不是數(shù)據(jù)線很多手機(jī)充電線不能傳數(shù)據(jù)檢查ST-Link是否供電。看編譯下載提示是否提示成功。如果是“Error: Flash Download failed - Cortex-M3”說明芯片連接有問題或Flash鎖定先“Settings”里降速到1MHz再試或者檢查BOOT0跳線。程序下載成功但燈不亮先Debug看程序是否在main()里跑。如果停不下來/一直在SystemInit里循環(huán)多半是啟動(dòng)文件配置不對(duì)選錯(cuò)了md/ld/hd容量型號(hào)。檢查GPIO配置。板載LED是PC13還是PA1、PA2每個(gè)板子不一樣查原理圖。我前幾年買某家的板子LED在PB12照著網(wǎng)上通用的PA8例程抄燈當(dāng)然不亮。檢查L(zhǎng)ED點(diǎn)亮電平。如果LED接在3.3V和引腳之間灌電流方式引腳拉低才亮如果接在引腳和GND之間拉電流方式引腳拉高才亮。代碼里的GPIO_SetBits和GPIO_ResetBits要對(duì)調(diào)才行。這也是很多人明明程序“沒問題”燈卻反著亮的原因。上面這五步走完基本能解決99%的點(diǎn)燈問題。剩下的1%是硬件問題比如板子出廠時(shí)LED虛焊——那我就只能說恭喜你可以開始學(xué)焊接了。3. 會(huì)點(diǎn)燈只是開始外設(shè)實(shí)戰(zhàn)與那些繞不開的“疑難雜癥”點(diǎn)燈是STM32世界的“Hello World”但它只是熱身。真正拉開新手和老手差距的是對(duì)中斷、定時(shí)器、串口、I2C/SPI這些外設(shè)的理解深度。這里我挑幾個(gè)核心外設(shè)說一說它們背后的原理和常見坑。3.1 定時(shí)器PWM呼吸燈與輸入捕獲測(cè)頻率定時(shí)器是STM32里最常用也最好用的一類外設(shè)。F103里有TIM1-TIM8其中TIM1和TIM8是高級(jí)定時(shí)器TIM2/3/4是通用定時(shí)器TIM6和TIM7是基本定時(shí)器只能做定時(shí)沒有輸入捕獲和輸出比較通道。學(xué)習(xí)定時(shí)器我建議分三步走。第一步是定時(shí)的本質(zhì)。定時(shí)器的核心是一個(gè)計(jì)數(shù)器在時(shí)鐘脈沖驅(qū)動(dòng)下遞增。當(dāng)計(jì)數(shù)值到達(dá)我們?cè)O(shè)置的自動(dòng)重裝載值A(chǔ)RR時(shí)計(jì)數(shù)器歸零并產(chǎn)生更新事件/中斷。定時(shí)時(shí)長(zhǎng)計(jì)算公式為定時(shí)時(shí)間 (ARR1) × (預(yù)分頻PSC1) / 定時(shí)器時(shí)鐘頻率。以TIM2掛在APB1上時(shí)鐘36MHz為例想讓定時(shí)器每1ms進(jìn)一次中斷設(shè)置PSC35999、ARR0這里換成36000-1和1000-1也可以多種組合都能得到1ms關(guān)鍵是理解預(yù)分頻把36MHz分到1kHz再數(shù)1000個(gè)數(shù)就是1ms。第二步是輸出PWM。PWM本質(zhì)上就是定時(shí)器的輸出比較功能計(jì)數(shù)器在ARR范圍內(nèi)循環(huán)計(jì)數(shù)輸出引腳在計(jì)數(shù)值小于比較值CCR時(shí)輸出一種電平大于比較值時(shí)輸出另一種電平從而形成占空比可調(diào)的方波。呼吸燈就是把CCR的值從0到ARR循環(huán)變化。舵機(jī)控制也是同理50Hz頻率、0.5ms到2.5ms的脈寬對(duì)應(yīng)0到180度角度。第三步是輸入捕獲測(cè)頻率。這個(gè)功能用來測(cè)量外部信號(hào)的頻率非常實(shí)用。原理很簡(jiǎn)單定時(shí)器開啟輸入捕獲通道當(dāng)外部引腳檢測(cè)到上升沿時(shí)把當(dāng)前計(jì)數(shù)器的值鎖存到捕獲寄存器。兩次捕獲的計(jì)數(shù)器差值除以定時(shí)器時(shí)鐘頻率就是信號(hào)的周期再取倒數(shù)就是頻率。公式為頻率 定時(shí)器時(shí)鐘頻率 / (兩次捕獲值之差)。用這個(gè)方法讓F103可以測(cè)幾Hz到幾十kHz的信號(hào)具體上限取決于定時(shí)器位寬和預(yù)分頻配置超聲波測(cè)距的核心也在這HC-SR04超聲波模塊發(fā)一個(gè)10us的TTL脈沖然后回波引腳拉高測(cè)量距離高電平持續(xù)時(shí)間就是超聲波從發(fā)射到反射回來的時(shí)間距離 高電平時(shí)間 × 340m/s / 2。高電平時(shí)間的測(cè)量用的就是定時(shí)器輸入捕獲。這里必須提醒一句輸入捕獲測(cè)頻率時(shí)如果信號(hào)頻率很低兩次捕獲之間定時(shí)器會(huì)發(fā)生多次溢出更新事件你沒處理溢出計(jì)數(shù)的話計(jì)算出來的周期就會(huì)嚴(yán)重偏小。正確的做法是在定時(shí)器更新中斷里累加溢出次數(shù)計(jì)算時(shí)把溢出次數(shù)也考慮進(jìn)去得到完整的周期。這個(gè)細(xì)節(jié)很多教程不會(huì)主動(dòng)講但實(shí)際測(cè)量低頻信號(hào)時(shí)100%會(huì)遇到。3.2 串口printf重定向與GBK/UTF-8編碼坑串口是STM32和外界溝通最廉價(jià)的方式。點(diǎn)燈只能告訴我們“程序在跑”而串口能把程序里的變量值、狀態(tài)信息發(fā)出來相當(dāng)于芯片在對(duì)你說話。串口配置流程開啟USART和GPIO時(shí)鐘、配置TX/RX引腳復(fù)用、設(shè)置波特率/數(shù)據(jù)位/停止位/校驗(yàn)位、使能USART。波特率必須是雙方約定一致的值收發(fā)兩端不一致的話出來全是亂碼。常見的有9600、115200等115200對(duì)F103來說很輕松。串口最常用的技能是把printf重定向到USART。標(biāo)準(zhǔn)庫(kù)環(huán)境下你需要做兩件事勾選Keil魔術(shù)棒Target標(biāo)簽頁(yè)里的MicroLIB然后自己實(shí)現(xiàn)fputc函數(shù)int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; }不勾MicroLIB的話標(biāo)準(zhǔn)C庫(kù)的printf會(huì)引起堆棧配置問題導(dǎo)致程序一進(jìn)printf就死機(jī)這是新手經(jīng)常碰到的的“串口打印就卡死”問題。還有一個(gè)非常實(shí)際的坑中文亂碼。代碼里寫printf(溫度%d, temp);串口助手上顯示“娓╁害”這種亂碼。原因不是串口配置而是編碼不匹配。Keil的源碼文件默認(rèn)是GB2312/GBK編碼而很多現(xiàn)代串口助手默認(rèn)用UTF-8解碼或者反過來。解決辦法有兩個(gè)一是源碼文件保存為UTF-8格式后再編譯注意如果使用中文注釋Keil舊版本會(huì)有中文注釋亂碼風(fēng)險(xiǎn)二是在串口助手切換解碼方式為GBK。還有一個(gè)一勞永逸的辦法串口輸出的中文字符串統(tǒng)一先轉(zhuǎn)成UTF-8再發(fā)送這就涉及到字符串編碼轉(zhuǎn)換網(wǎng)上也有GBK轉(zhuǎn)UTF-8的現(xiàn)成C代碼可以直接抄。我的建議是如果只是調(diào)試用途串口輸出盡量用英文或拼音如果要做產(chǎn)品交互信息老老實(shí)實(shí)研究編碼轉(zhuǎn)換。3.3 SPI顯示屏的真實(shí)一課ILI9341讀ID為什么是0xA1A1ILI9341是2.4寸/2.8寸TFTLCD屏最常見的驅(qū)動(dòng)芯片支持SPI和MCU并口兩種接口。很多人照著某教程初始化屏幕卡在第一步讀芯片ID怎么讀都讀出0xA1A1。這個(gè)0xA1A1太經(jīng)典了新版網(wǎng)絡(luò)熱詞都帶它。問題出在哪我復(fù)盤一下。先說正確流程。ILI9341讀ID需要發(fā)送命令0xD3而不是0x04然后連續(xù)讀4個(gè)字節(jié)數(shù)據(jù)。在SPI模式下你發(fā)的每個(gè)命令字節(jié)后面要跟一個(gè)“讀操作”的字節(jié)——這個(gè)字節(jié)的數(shù)據(jù)無效但時(shí)鐘必須有因?yàn)镾PI是同步協(xié)議所有數(shù)據(jù)傳輸都需要時(shí)鐘。接著再讀兩個(gè)字節(jié)才是真實(shí)的ID一個(gè)0x93一個(gè)0x41合起來就是9341。很多新手直接調(diào)用讀函數(shù)發(fā)送0xD3后就期望下一次讀取返回0x93結(jié)果返回的是0xA1然后一臉茫然。這0xA1是哪來的0xA1通常是你上一條命令/數(shù)據(jù)的遺存或者是命令字節(jié)本身被回讀出來了。SPI接口的屏主控發(fā)送數(shù)據(jù)時(shí)MISO線上會(huì)同時(shí)返回?cái)?shù)據(jù)當(dāng)芯片沒有主動(dòng)輸出時(shí)MISO狀態(tài)取決于模塊設(shè)計(jì)很多模塊MISO上會(huì)有一個(gè)0xA1的高位垃圾數(shù)據(jù)。讀ID讀出來“A1A1”幾乎可以斷定是讀取時(shí)序錯(cuò)了把命令字節(jié)或空閑字節(jié)當(dāng)成ID了。正確操作應(yīng)該是發(fā)送0xD3命令前先拉低CS片選發(fā)送命令后要發(fā)送一個(gè)假字節(jié)比如0x00產(chǎn)生時(shí)鐘同時(shí)讀取到的第一個(gè)字節(jié)丟棄繼續(xù)發(fā)送假字節(jié)讀到0x93再發(fā)第三個(gè)假字節(jié)讀到0x41。然后拉高CS結(jié)束。另一個(gè)常見原因是對(duì)復(fù)位時(shí)序不敏感。有些模塊上電后需要給RESET引腳一個(gè)低脈沖至少10us然后等待120ms才能初始化。如果復(fù)位時(shí)序不對(duì)屏幕狀態(tài)不確定ID也可能讀不對(duì)。最后還有一個(gè)非常隱蔽的坑SPI模式選擇。ILI9341模塊背面的電阻配置決定了SPI模式Mode1還是Mode3——CPOL和CPHA的組合。如果主控配置的SPI極性和相位和模塊不一致數(shù)據(jù)會(huì)錯(cuò)位讀出來的ID自然是垃圾值。一般SPI模式0是CPOL0、CPHA0模式3是CPOL1、CPHA1都試一下就知道模塊到底工作在哪個(gè)模式。這個(gè)“讀ID是A1A1”的坑非常典型它把SPI時(shí)序、命令時(shí)序、復(fù)位時(shí)序三個(gè)知識(shí)點(diǎn)全考了一遍。你要是能獨(dú)立排查解決這個(gè)問題SPI協(xié)議你基本就掌握了。3.4 超聲波測(cè)距和步進(jìn)電機(jī)一個(gè)完整的傳感控制閉環(huán)學(xué)外設(shè)不能光學(xué)外設(shè)要串聯(lián)成一個(gè)系統(tǒng)。我常建議新手做的第一個(gè)組合項(xiàng)目是超聲波測(cè)距 OLED顯示 五線四相步進(jìn)電機(jī)跟隨。超聲波模塊HC-SR04發(fā)一個(gè)10us的Trig脈沖等待Echo高電平用定時(shí)器捕獲測(cè)得回波時(shí)間算出距離。把這個(gè)距離顯示在OLED或LCD屏上同時(shí)讓步進(jìn)電機(jī)根據(jù)距離轉(zhuǎn)動(dòng)一定角度比如距離小于30cm就往回轉(zhuǎn)一點(diǎn)大于50cm就往前轉(zhuǎn)一點(diǎn)。這里會(huì)碰到一個(gè)新的驅(qū)動(dòng)對(duì)象五線四相步進(jìn)電機(jī)最典型的是28BYJ-48配合ULN2003驅(qū)動(dòng)板使用。這種步進(jìn)電機(jī)是四相八拍驅(qū)動(dòng)的繞組通電順序決定轉(zhuǎn)動(dòng)方向和步距角。核心代碼邏輯是定義一個(gè)換相表const uint8_t STEP_CODE[8] {0x01, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x09};對(duì)應(yīng)A-AB-B-BC-C-CD-D-DA八拍循環(huán)切換這八個(gè)狀態(tài)電機(jī)就持續(xù)轉(zhuǎn)起來。每切換一拍電機(jī)轉(zhuǎn)過一個(gè)最小步距角28BYJ-48的步距角是5.625°/64也就是說內(nèi)部經(jīng)過64倍減速后輸出軸轉(zhuǎn)動(dòng)一圈需要2048個(gè)脈沖。新手容易在這里栽跟頭直接用GPIO翻轉(zhuǎn)來模擬脈沖發(fā)現(xiàn)電機(jī)轉(zhuǎn)一陣就發(fā)燙、還丟步。原因是28BYJ-48的額定轉(zhuǎn)速很低減速后每分鐘15轉(zhuǎn)左右換相太快會(huì)丟步甚至堵轉(zhuǎn)。給每次換相之間加個(gè)延時(shí)比如1ms速度就差不多了。想調(diào)速就調(diào)延時(shí)想控制轉(zhuǎn)角就計(jì)數(shù)脈沖數(shù)。這個(gè)項(xiàng)目的價(jià)值在于超聲波測(cè)距鍛煉定時(shí)器捕獲、OLED驅(qū)動(dòng)鍛煉SPI/I2C、步進(jìn)電機(jī)鍛煉GPIO輸出和時(shí)序控制三個(gè)外設(shè)串在一起就是一個(gè)完整的“感知-決策-執(zhí)行”閉環(huán)以后做小車、做機(jī)械臂都是這套邏輯。4. 我踩過的幾個(gè)真坑排查鏈路完整復(fù)盤連著講三個(gè)經(jīng)典死機(jī)/連接問題把完整排查鏈路寫出來。這些問題我基本都親自踩過寫出來的都是血淚。4.1 延時(shí)函數(shù)delay卡死SysTick與編譯優(yōu)化現(xiàn)象程序燒錄成功燈亮了但一進(jìn)延時(shí)函數(shù)就死機(jī)或者整個(gè)程序卡在那里不走了。這個(gè)問題的本質(zhì)可能有好幾層必須一層層剝。第一層延時(shí)函數(shù)本身寫錯(cuò)了。很多人第一次自己寫延時(shí)用的是for (i 0; i 500000; i);但i定義成了uint8_t最大值255循環(huán)條件永遠(yuǎn)不成立延時(shí)變成死循環(huán)。這不是卡死這是循環(huán)根本出不來。排查方法是看調(diào)試器里i變量的值或者直接把i定義成uint32_t。第二層SysTick中斷被錯(cuò)誤配置了。標(biāo)準(zhǔn)庫(kù)的Delay()延時(shí)實(shí)現(xiàn)如果用了SysTick中斷方式而你在配置優(yōu)先級(jí)時(shí)把SysTick的搶占優(yōu)先級(jí)設(shè)得太高/太低或者沒有啟動(dòng)SysTick中斷延時(shí)自然不可用。更常見的是在新版HAL庫(kù)里調(diào)用HAL_Delay()之前必須先調(diào)用HAL_Init()完成滴答定時(shí)器SysTick配置否則HAL_Delay進(jìn)到while (uwTick uwTickTarget)循環(huán)里永遠(yuǎn)出不來。第三層編譯器優(yōu)化導(dǎo)致延時(shí)循環(huán)被“優(yōu)化掉”。Keil在-O0和-O2下產(chǎn)生的代碼差別很大優(yōu)化級(jí)別高時(shí)一個(gè)沒有副作用的空循環(huán)會(huì)被編譯器直接省略掉于是“延時(shí)”沒了程序運(yùn)行飛快、執(zhí)行邏輯混亂。解決方法是把循環(huán)變量聲明為volatile告訴編譯器這個(gè)變量不能優(yōu)化或者干脆用SysTick做延時(shí)不開空循環(huán)。第四層就是時(shí)鐘配置不對(duì)提到72MHz果然后SysTick的時(shí)鐘源或者重裝載值算錯(cuò)延時(shí)時(shí)長(zhǎng)和預(yù)期完全不一致現(xiàn)象表現(xiàn)為“延時(shí)不準(zhǔn)”嚴(yán)重時(shí)會(huì)讓外設(shè)邏輯崩掉比如I2C時(shí)序錯(cuò)亂。排查鏈路的正確順序應(yīng)該是先判斷是“卡住”還是“沒執(zhí)行”——用調(diào)試器打斷點(diǎn)、看PC指針位置再檢查延時(shí)實(shí)現(xiàn)方式——空循環(huán)還是SysTick最后檢查編譯優(yōu)化。絕大多數(shù)卡死都是這個(gè)鏈路走出來的。4.2 禁用JTAG后板子“變磚”SWD保命法現(xiàn)象程序里使用了PB3、PB4、PA15這些引腳做普通GPIO代碼里調(diào)用了GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);禁用JTAG來釋放這幾個(gè)引腳。程序燒錄運(yùn)行后突然發(fā)現(xiàn)自己用ST-Link連不上芯片了。這個(gè)坑非常經(jīng)典。F103的PA13、PA14是SWDPA15、PB3、PB4是JTAG。你禁用了JTAG確實(shí)釋放了PB3/PB4/PA15但如果不小心同時(shí)也影響了SWD引腳配置ST-Link就無法通信了——板子看起來就像變磚了。但別慌這板子幾乎不可能真磚。三種恢復(fù)辦法如果只是禁用了JTAGSWDPA13/PA14還沒被禁用那就直接用SWD模式燒錄——ST-Link默認(rèn)就是SWD模式通常沒問題。如果SWD也一起被關(guān)了按住板子上的復(fù)位鍵在Keil里點(diǎn)擊Download然后快速松開復(fù)位鍵讓芯片在復(fù)位期間被連接。趁芯片還沒來得及跑飛之前的幾毫秒擦除Flash。這招實(shí)測(cè)有效但手速要快。終極辦法STM32的BOOT0拉高后上電芯片從系統(tǒng)存儲(chǔ)器啟動(dòng)程序不執(zhí)行Flash里的代碼。此時(shí)ST-Link可以正常連接把Flash擦掉然后把BOOT0跳線跳回低電平芯片就活了。如果還是連不上大概率你的ST-Link排線接觸不良檢查SWDIO、SWCLK、GND三根線的連接——順帶一提SWDIO和SWCLK在空板子上測(cè)量應(yīng)該能測(cè)到約1.8V的上拉電平?jīng)]有這個(gè)電平就說明調(diào)試器沒連好。4.3 CAN通信突然連不上終端電阻與波特率之謎現(xiàn)象兩塊F103開發(fā)板用CAN收發(fā)器如TJA1050對(duì)接上午還能正常通信下午突然連不上了。程序沒改過接線也沒動(dòng)過。新手遇到這個(gè)第一反應(yīng)是代碼bug但其實(shí)硬件層面的嫌疑最大。CAN總線是差分信號(hào)要求總線兩端都必須接120歐姆終端電阻用于匹配阻抗、消除反射。很多新手兩塊板子都沒接終端電阻或者只接一端短距離低速通信比如波特率125kbps線長(zhǎng)幾十厘米可能沒問題一旦線長(zhǎng)超過幾米或者波特率提高反射信號(hào)會(huì)直接干掉整個(gè)通信。排查順序先量CAN_H和CAN_L之間的靜態(tài)電壓正常應(yīng)該約2.5V隱性顯性時(shí)CAN_H比CAN_L高至少2V。如果電壓異?;?yàn)?說明收發(fā)器供電問題或總線短路。檢查終端電阻萬用表量CAN_H-CAN_L之間沒接電阻時(shí)阻值很高接了正確終端電阻時(shí)應(yīng)該在60歐姆左右兩塊板各120歐姆并聯(lián)。波特率匹配F103的CAN外設(shè)時(shí)鐘是APB136MHz波特率通過“預(yù)分頻時(shí)間段”配置。如果兩端用相同的波特率配置代碼但一個(gè)板子晶振有偏差標(biāo)稱8MHz實(shí)際8.1MHz兩個(gè)節(jié)點(diǎn)的階段采樣點(diǎn)就會(huì)錯(cuò)位高波特率下必現(xiàn)偶發(fā)斷連。處理方式是降低波特率或者用CAN分析儀抓波形核對(duì)位時(shí)間。最后再看代碼異常處理里有沒有處理CAN_ESR寄存器的Bus-Off位總線關(guān)閉后CAN控制器會(huì)退出總線不再參與通信需要在錯(cuò)誤中斷里做恢復(fù)。CAN通信這類工業(yè)總線問題的排查核心原則就是先物理層、再數(shù)據(jù)鏈路層、最后才應(yīng)用層。別一上來就懷疑代碼。4.4 五個(gè)會(huì)讓新手當(dāng)場(chǎng)崩潰的“祖?zhèn)鲉栴}”最后再列五個(gè)我見過的高頻崩潰現(xiàn)場(chǎng)給個(gè)速查表現(xiàn)象大概率原因解決辦法下載報(bào)錯(cuò)No Target ConnectedSWD線接反/接觸不良、目標(biāo)板沒供電檢查三根線降下載速率按住復(fù)位鍵下載Flash Download failedFlash讀保護(hù)、芯片鎖定BOOT0拉高上電后全片擦除程序能跑但外設(shè)沒反應(yīng)外設(shè)時(shí)鐘沒開、引腳復(fù)用不對(duì)檢查RCC使能檢查AFIO重映射液晶屏幕花屏電源不穩(wěn)、SPI速率太高、復(fù)位時(shí)序不對(duì)單獨(dú)供電降低SPI分頻到18MHz以下復(fù)位后延時(shí)120ms引腳輸出電平異常引腳被JTAG/SWD占用、復(fù)用沖突禁用JTAG檢查PA13/PA14不能當(dāng)普通IO“芯片第一腳怎么確認(rèn)”這個(gè)熱搜詞也經(jīng)常和這些混淆在一起。芯片LQFP封裝一般在第一腳附近會(huì)有圓點(diǎn)/斜切角標(biāo)記圖中左上角有絲印白點(diǎn)或者芯片一角被切掉那一腳就是第一腳。PCB原理圖上通常也有標(biāo)注不確認(rèn)前千萬別硬懟電源正負(fù)極——STM32F103耐壓5V但很多核心板上的3.3V引腳懟5V進(jìn)去會(huì)直接冒煙燒一片。5. 買開發(fā)板之后的三個(gè)月一份可照抄的學(xué)習(xí)路線最后來說說從買到入門這三個(gè)月怎么安排。很多人覺得自己“學(xué)不會(huì)STM32”其實(shí)不是因?yàn)楸渴且驗(yàn)椴恢缹W(xué)完GPIO之后該學(xué)什么沒有路線圖東一榔頭西一棒子最后放棄。5.1 第一周別急著寫代碼先建立“引腳感”這一周的目標(biāo)不是寫多少代碼而是把板子“摸熟”??丛韴D搞清楚每個(gè)引腳的功能跑通點(diǎn)燈、跑通按鍵控制LED找一本講STM32的書或者一份完整教程把GPIO的四種輸入模式浮空、上拉、下拉、模擬和兩種輸出模式推挽、開漏弄明白。開漏輸出為什么需要外接上拉電阻推挽輸出為什么能直接驅(qū)動(dòng)LED這些概念一周時(shí)間建立起來后面的路會(huì)順很多。5.2 第2-4周六大外設(shè)逐個(gè)過按這個(gè)順序來外部中斷EXTI、定時(shí)器TIM定時(shí)中斷、PWM輸出、輸入捕獲、USART串口收發(fā)、printf重定向、ADC讀電位器電壓、DMA串口收發(fā)不占CPU、I2C和SPI驅(qū)動(dòng)OLED/Flash/傳感器。每個(gè)外設(shè)都要經(jīng)歷“看例程→抄例程→改例程→自己從空白工程寫一遍”的過程。抄一遍永遠(yuǎn)只是會(huì)“跑代碼”自己寫一遍才是會(huì)“配外設(shè)”。外設(shè)這關(guān)過了STM32的80%就學(xué)了。5.3 第5-8周第一個(gè)小型項(xiàng)目別再做“流水燈”了做一個(gè)有實(shí)際意義的小東西。我的推薦清單超聲波測(cè)距OLED顯示屏做成一個(gè)便攜測(cè)距儀DHT11溫濕度傳感器OLED串口打印做一個(gè)環(huán)境監(jiān)測(cè)器步進(jìn)電機(jī)紅外遙控做一個(gè)自動(dòng)窗簾模型紅外遙控需要學(xué)習(xí)NEC協(xié)議解碼也是個(gè)好東西如果對(duì)物聯(lián)網(wǎng)感興趣ESP8266/ESP01通過串口AT指令連接巴法云這類免費(fèi)MQTT云平臺(tái)把傳感器數(shù)據(jù)傳到云端再用手機(jī)看——這一套做完你就打通了“設(shè)備端-云端-應(yīng)用端”的完整鏈路。做項(xiàng)目的時(shí)候我強(qiáng)烈建議用一個(gè)“需求拆解”的方法把最終功能拆成多個(gè)模塊每個(gè)模塊單獨(dú)調(diào)通再拼接。比如測(cè)距儀拆成“超聲波測(cè)距模塊→OLED顯示模塊→主邏輯模塊”每個(gè)模塊先在main函數(shù)里單獨(dú)測(cè)試最后合并。千萬不要一次性寫完一坨600行代碼然后指望它一次跑通——那是在折磨自己。5.4 第9-12周往“產(chǎn)品級(jí)”靠攏學(xué)到這里你對(duì)F103已經(jīng)比較熟了。這時(shí)候可以有兩條進(jìn)階路。一條是底層方向開始看寄存器手冊(cè)把之前用庫(kù)函數(shù)配置過的寄存器手動(dòng)寫一遍比如直接操作GPIOx-CRL和CRH寄存器配置引腳模式理解庫(kù)函數(shù)的本質(zhì)其實(shí)就是給這些寄存器賦值。然后研究啟動(dòng)文件startup_stm32f10x_md.s理解上電后是怎么進(jìn)入main函數(shù)的中斷向量表到底是什么。另一條是系統(tǒng)方向引入FreeRTOS操作系統(tǒng)把前面的點(diǎn)燈、串口、按鍵放到不同的任務(wù)里學(xué)習(xí)任務(wù)調(diào)度、信號(hào)量、消息隊(duì)列。用F103跑FreeRTOS綽綽有余以后做復(fù)雜項(xiàng)目也不至于面對(duì)裸機(jī)狀態(tài)的很多問題束手無策這是從“玩單片機(jī)”到“做嵌入式系統(tǒng)”的關(guān)鍵一步。同時(shí)工具鏈也可以趁這段時(shí)間升級(jí)一下嘗試用VS Code加EIDE/PlatformIO搭建STM32開發(fā)環(huán)境git做版本管理。新手階段用Keil沒問題但以后工作大概率不用Keil早熟悉現(xiàn)代工具鏈沒有壞處。開發(fā)板并不是只能配Keil用PlatformIO管理F103工程也很方便。最后的幾句大實(shí)話板子買回來那天熱血上頭的樣子我記得很清楚。我那塊F103核心板到現(xiàn)在還在抽屜里偶爾拿來驗(yàn)證個(gè)新點(diǎn)子依然順手。以我?guī)氯说慕?jīng)驗(yàn)學(xué)STM32這件事最大的坎從來不是技術(shù)難度而是“卡住了沒人告訴你怎么走”。這篇里寫的每一個(gè)坑——從點(diǎn)燈不亮、串口亂碼、ILI9341讀ID、延時(shí)卡死、JTAG禁用變磚到CAN掉線——你大概率在接下來三個(gè)月里至少碰到兩三個(gè)。真遇到了回來對(duì)著排查鏈路一步步走大多能走出來。我的建議是別貪多一個(gè)外設(shè)一個(gè)外設(shè)過一個(gè)項(xiàng)目一個(gè)項(xiàng)目做。F103這塊板子能教給你的東西比你想的多得多。動(dòng)起來今天就把環(huán)境搭好先讓LED閃起來再說。