核心原理與項(xiàng)目實(shí)戰(zhàn):從架構(gòu)解析到智能小車開發(fā))
1. 項(xiàng)目概述為什么51單片機(jī)依然是工程師的“老朋友”如果你在電子、自動化或者嵌入式開發(fā)領(lǐng)域待過一段時(shí)間哪怕只是剛?cè)腴T大概率都繞不開“51單片機(jī)”這個(gè)名字。它不像現(xiàn)在流行的ARM Cortex-M系列那樣性能強(qiáng)悍也不像ESP32那樣自帶Wi-Fi和藍(lán)牙但在我十多年的項(xiàng)目經(jīng)歷里它就像一位可靠的老朋友總是在一些基礎(chǔ)、穩(wěn)定、成本敏感的場景里默默發(fā)光。今天我們不聊那些高大上的新架構(gòu)就聊聊這位老朋友——51單片機(jī)它到底是什么為什么到今天還有如此旺盛的生命力以及我們?nèi)绾斡煤盟:唵蝸碚f51單片機(jī)是一個(gè)基于Intel 8051內(nèi)核的8位微控制器家族的統(tǒng)稱。它的核心設(shè)計(jì)誕生于上世紀(jì)80年代但憑借其極致的簡單性、成熟的生態(tài)和低廉的成本被全球無數(shù)半導(dǎo)體公司如STC、Atmel、NXP等生產(chǎn)了海量的兼容型號。當(dāng)你聽到“STC89C52”、“AT89S51”這些型號時(shí)它們都屬于51家族。它的核心價(jià)值在于用最低的學(xué)習(xí)成本和硬件成本讓你理解計(jì)算機(jī)最底層的運(yùn)作原理并實(shí)現(xiàn)從想法到物理世界的控制。無論是讓一個(gè)LED燈閃爍還是驅(qū)動一個(gè)小車底盤或是做一個(gè)溫濕度計(jì)51單片機(jī)都是絕佳的起點(diǎn)和可靠的量產(chǎn)選擇。2. 51單片機(jī)的核心架構(gòu)與工作原理解析要駕馭好這位老朋友不能只停留在“燒錄代碼讓它跑”的層面得先摸清它的“脾氣秉性”也就是其內(nèi)部架構(gòu)和工作原理。這對于后續(xù)的編程、調(diào)試和問題排查至關(guān)重要。2.1 經(jīng)典哈佛結(jié)構(gòu)與存儲空間51單片機(jī)采用經(jīng)典的哈佛結(jié)構(gòu)這意味著它的程序存儲器ROM和數(shù)據(jù)存儲器RAM在物理上是分開的有各自獨(dú)立的地址總線和數(shù)據(jù)總線。這與我們常用的PC馮·諾依曼結(jié)構(gòu)不同。這種設(shè)計(jì)的好處是指令讀取和數(shù)據(jù)訪問可以并行提高了執(zhí)行效率尤其適合簡單的控制任務(wù)。對于開發(fā)者而言這直接體現(xiàn)在存儲空間的劃分上程序存儲器ROM/Flash存放我們編寫的程序代碼和常量數(shù)據(jù)。比如STC89C52就有8KB的Flash。代碼在這里是“只讀”的上電后CPU從這里逐條讀取指令執(zhí)行。數(shù)據(jù)存儲器RAM用于存放程序運(yùn)行時(shí)的變量、堆棧等臨時(shí)數(shù)據(jù)。通常很小只有128字節(jié)或256字節(jié)。這就要求編程時(shí)必須精打細(xì)算避免定義大型數(shù)組或復(fù)雜數(shù)據(jù)結(jié)構(gòu)。特殊功能寄存器SFR這是51單片機(jī)編程的核心所在。它是一組映射在RAM高地址區(qū)80H-FFH的特定寄存器每一個(gè)都直接對應(yīng)著一個(gè)硬件功能模塊的控制開關(guān)、狀態(tài)標(biāo)志或數(shù)據(jù)緩沖區(qū)。例如P1寄存器控制著P1口8個(gè)引腳的電平TCON寄存器控制著定時(shí)器的啟停和標(biāo)志位。注意51的RAM資源非常緊張。我曾在一個(gè)需要處理多組傳感器數(shù)據(jù)的項(xiàng)目里因?yàn)槎x了幾個(gè)全局?jǐn)?shù)組很快就耗盡了128字節(jié)的RAM導(dǎo)致程序跑飛。解決方案是1) 盡量使用data/idata區(qū)的小內(nèi)存模式2) 大的查表數(shù)據(jù)放在code區(qū)ROM3) 謹(jǐn)慎使用遞歸和深度函數(shù)調(diào)用以免堆棧溢出。2.2 CPU時(shí)序、機(jī)器周期與指令集51單片機(jī)的CPU心臟是它的振蕩電路外接一個(gè)晶振比如經(jīng)典的11.0592MHz。CPU的所有動作都基于這個(gè)時(shí)鐘節(jié)拍。這里有個(gè)關(guān)鍵概念機(jī)器周期。在標(biāo)準(zhǔn)51架構(gòu)中1個(gè)機(jī)器周期 12個(gè)時(shí)鐘周期。大部分指令的執(zhí)行需要1個(gè)或2個(gè)機(jī)器周期。這意味著當(dāng)你使用12MHz晶振時(shí)一個(gè)機(jī)器周期是1微秒。一條單周期指令如NOP執(zhí)行時(shí)間就是1微秒。這個(gè)特性使得編寫精確的短延時(shí)函數(shù)變得非常簡單for(i0; i125; i) { _nop_(); }大約延時(shí)125微秒。同時(shí)這也決定了定時(shí)器、串口波特率等所有與時(shí)間相關(guān)功能的計(jì)算基礎(chǔ)。它的指令集是精簡的大約111條指令分為數(shù)據(jù)傳送、算術(shù)運(yùn)算、邏輯運(yùn)算、控制轉(zhuǎn)移和位操作幾大類。其中位操作指令是51單片機(jī)的一大特色。它可以對RAM中可位尋址區(qū)20H-2FH和大部分SFR的每一個(gè)位進(jìn)行直接置1、清0、判斷等操作這使得對單個(gè)IO口引腳的控制變得極其高效和直觀比如LED 0;點(diǎn)亮LED這樣的語句背后就是一條位操作指令。2.3 核心外設(shè)單元淺析51單片機(jī)的功能通過其內(nèi)置的外設(shè)模塊實(shí)現(xiàn)理解它們是項(xiàng)目實(shí)戰(zhàn)的關(guān)鍵并行I/O口P0, P1, P2, P3這是單片機(jī)與外界溝通最直接的橋梁。每個(gè)口有8個(gè)引腳。需要特別注意P0口內(nèi)部沒有上拉電阻當(dāng)作為通用IO口輸出時(shí)需要外接上拉電阻才能輸出高電平否則是開漏狀態(tài)。而P1, P2, P3口則內(nèi)部有上拉。作為輸入口時(shí)必須先向端口鎖存器寫“1”使其處于高阻態(tài)才能正確讀取外部電平。定時(shí)器/計(jì)數(shù)器Timer51通常有2個(gè)16位定時(shí)器/計(jì)數(shù)器T0和T1。它們有兩種模式定時(shí)器模式對內(nèi)部機(jī)器周期脈沖計(jì)數(shù)用于產(chǎn)生精確時(shí)間間隔是實(shí)現(xiàn)延時(shí)、PWM、測量脈寬的基礎(chǔ)。計(jì)數(shù)器模式對外部引腳T0/P3.4, T1/P3.5的下降沿計(jì)數(shù)用于測量外部事件頻率。 通過設(shè)置相關(guān)的SFRTMOD,TCON,THx,TLx可以配置其工作方式和初始值。溢出時(shí)會產(chǎn)生中斷這是實(shí)現(xiàn)多任務(wù)時(shí)間片輪詢的基石。串行通信口UART這是51與電腦、其他單片機(jī)或模塊如藍(lán)牙、Wi-Fi通信的主要方式。通過SCON和PCON寄存器配置波特率、數(shù)據(jù)位、停止位等。波特率發(fā)生器通常由定時(shí)器T1工作在模式28位自動重載來產(chǎn)生這也是為什么在串口通信項(xiàng)目中晶振常選用11.0592MHz——因?yàn)樗梢苑浅>_地計(jì)算出標(biāo)準(zhǔn)的波特率如9600避免累積誤差導(dǎo)致通信失敗。中斷系統(tǒng)51單片機(jī)支持5個(gè)中斷源2個(gè)外部中斷INT0,INT12個(gè)定時(shí)器中斷TF0,TF11個(gè)串口中斷RI/TI。通過IE中斷使能和IP中斷優(yōu)先級寄存器進(jìn)行管理。中斷是單片機(jī)響應(yīng)外部緊急事件的核心機(jī)制比如按鍵按下、數(shù)據(jù)接收完成。3. 從零開始51單片機(jī)開發(fā)環(huán)境搭建與第一個(gè)項(xiàng)目理論懂了手會癢。接下來我們搭建一個(gè)最經(jīng)典、最通用的開發(fā)環(huán)境并完成一個(gè)“Hello World”級的硬件項(xiàng)目——LED閃爍。3.1 工具鏈選型與安裝對于51開發(fā)我強(qiáng)烈推薦“Keil C51 STC-ISP”的組合。這套組合拳經(jīng)歷了無數(shù)項(xiàng)目的考驗(yàn)穩(wěn)定且高效。集成開發(fā)環(huán)境IDEKeil μVision。它是ARM公司旗下的產(chǎn)品對C51的支持非常成熟。你需要安裝Keil C51版本注意不是MDK-ARM。安裝過程簡單主要是注意安裝路徑不要有中文。安裝后需要破解獲取License網(wǎng)上有詳細(xì)教程這里不展開。編譯器/鏈接器Keil C51 IDE自帶。它將我們寫的C代碼編譯成51單片機(jī)可以執(zhí)行的機(jī)器碼HEX文件。下載編程工具STC-ISP。這是深圳宏晶科技STC提供的免費(fèi)下載軟件因其界面友好、功能強(qiáng)大附帶串口助手、范例程序等已成為廣大51開發(fā)者即使不用STC芯片最常用的下載工具之一。它通過串口USB轉(zhuǎn)TTL給單片機(jī)燒錄程序。硬件連接你需要一個(gè)USB轉(zhuǎn)TTL串口模塊如CH340G、CP2102模塊用于連接電腦USB口和單片機(jī)的串口P3.0/RxD, P3.1/TxD。同時(shí)該模塊的VCC和GND為單片機(jī)供電。對于最小系統(tǒng)板還需連接復(fù)位電路和晶振電路。3.2 創(chuàng)建第一個(gè)工程LED閃爍假設(shè)我們使用STC89C52RC單片機(jī)連接一個(gè)LED到P1.0引腳負(fù)極接LED正極通過限流電阻接VCC。步驟一在Keil中新建工程打開KeilProject - New μVision Project...選擇好工程存放目錄和名稱。在彈出的設(shè)備選擇窗口中選擇STC如果你安裝了STC的器件包或Atmel下的AT89C52完全兼容。選擇器件主要是為了初始化啟動文件對代碼影響不大。當(dāng)詢問“Copy Standard 8051 Startup Code to Project Folder?”時(shí)選擇“是”。這個(gè)啟動文件STARTUP.A51負(fù)責(zé)初始化內(nèi)存和堆棧指針。步驟二編寫主程序在工程中新建一個(gè)main.c文件。輸入以下代碼#include REG52.H // 包含51單片機(jī)特殊功能寄存器定義的頭文件 #include INTRINS.H // 包含_nop_()延時(shí)函數(shù) sbit LED P1^0; // 將P1.0引腳定義為LED // 簡單延時(shí)函數(shù)大約延時(shí)ms毫秒基于12MHz晶振的粗略計(jì)算 void DelayMs(unsigned int ms) { unsigned int i, j; for(i0; ims; i) for(j0; j123; j); // 這個(gè)循環(huán)次數(shù)需要根據(jù)實(shí)際晶振校準(zhǔn) } void main() { // 主函數(shù)程序入口 while(1) { // 無限循環(huán) LED 0; // P1.0輸出低電平LED點(diǎn)亮假設(shè)共陽接法 DelayMs(500); // 延時(shí)500ms LED 1; // P1.0輸出高電平LED熄滅 DelayMs(500); // 延時(shí)500ms } }這段代碼做了幾件事包含必要的頭文件用sbit關(guān)鍵字進(jìn)行位定義這是51特有的非常方便寫了一個(gè)基于循環(huán)的粗略延時(shí)函數(shù)在主循環(huán)中不斷翻轉(zhuǎn)LED的狀態(tài)。步驟三配置與編譯點(diǎn)擊工具欄的Options for Target魔術(shù)棒圖標(biāo)。在Output選項(xiàng)卡勾選Create HEX File。HEX文件是最終要燒錄到單片機(jī)里的機(jī)器碼文件。點(diǎn)擊BuildF7編譯。如果下方Build Output窗口顯示“0 Error(s), 0 Warning(s)”并生成了.hex文件說明成功。步驟四硬件連接與程序下載按照原理圖將USB轉(zhuǎn)TTL模塊的TXD接單片機(jī)RXDP3.0RXD接單片機(jī)TXDP3.1GND接GNDVCC接VCC注意電壓通常是5V。打開STC-ISP軟件。步驟1選擇單片機(jī)型號如STC89C52RC。步驟2選擇正確的串口號在電腦設(shè)備管理器中查看。步驟3點(diǎn)擊“打開程序文件”選擇剛才Keil生成的.hex文件。步驟4點(diǎn)擊“下載/編程”按鈕。最關(guān)鍵的一步給單片機(jī)關(guān)電再上電冷啟動。STC單片機(jī)下載需要冷啟動握手。此時(shí)STC-ISP會顯示“正在檢測目標(biāo)單片機(jī)...”然后開始擦除、編程、校驗(yàn)。成功后你就能看到板載的LED開始閃爍了實(shí)操心得第一次下載失敗十有八九是串口線接反了TXD與RXD要對調(diào)或冷啟動時(shí)機(jī)不對。多試兩次這是每個(gè)51玩家的必經(jīng)之路。另外DelayMs函數(shù)是非常不精確的延時(shí)因?yàn)樗鼤恢袛啻驍嗲沂芫幾g器優(yōu)化影響。在需要精確定時(shí)的場合必須使用定時(shí)器中斷。4. 核心外設(shè)項(xiàng)目實(shí)戰(zhàn)與代碼剖析掌握了基本操作我們來攻克幾個(gè)標(biāo)志性的項(xiàng)目深入理解定時(shí)器、中斷、PWM和串口這些核心外設(shè)。4.1 項(xiàng)目一基于定時(shí)器中斷的精準(zhǔn)流水燈使用循環(huán)延時(shí)做流水燈燈會“卡頓”因?yàn)镃PU被延時(shí)函數(shù)獨(dú)占。用定時(shí)器中斷則可以解放CPU實(shí)現(xiàn)“后臺”定時(shí)前臺處理其他任務(wù)。核心思路配置定時(shí)器T0每10ms產(chǎn)生一次中斷。在中斷服務(wù)程序中用一個(gè)計(jì)數(shù)器累加每計(jì)滿100次即1秒就改變一次LED的顯示模式。#include REG52.H #define LED_PORT P1 // 假設(shè)8個(gè)LED接在P1口 unsigned int T0_Count 0; // 中斷次數(shù)計(jì)數(shù)器 unsigned char LED_Mode 0x01; // LED模式初始值0x010000 0001B void Timer0_Init() { TMOD 0xF0; // 低四位清零不影響T1的設(shè)置 TMOD | 0x01; // 設(shè)置T0為工作方式116位定時(shí)器 TH0 0xD8; // 裝入初值定時(shí)10ms (65536 - 10000) / 256 TL0 0xF0; // (65536 - 10000) % 256 ET0 1; // 允許T0中斷 EA 1; // 開啟總中斷 TR0 1; // 啟動T0 } void main() { Timer0_Init(); LED_PORT ~LED_Mode; // 初始狀態(tài)低電平點(diǎn)亮取反 while(1) { // 主循環(huán)可以在這里做其他事情比如按鍵掃描 // 流水燈的效果完全由中斷服務(wù)函數(shù)控制不會阻塞這里 } } // 定時(shí)器0中斷服務(wù)函數(shù) void Timer0_ISR() interrupt 1 { // interrupt 1 對應(yīng)T0中斷 TH0 0xD8; // 重新裝載初值方式1需手動重裝 TL0 0xF0; T0_Count; if(T0_Count 100) { // 1秒到 T0_Count 0; LED_Mode _crol_(LED_Mode, 1); // 循環(huán)左移一位使用 intrins.h 中的函數(shù) LED_PORT ~LED_Mode; // 更新LED顯示 } }代碼解析TMOD寄存器用于設(shè)置定時(shí)器工作方式。0x01表示T0為16位定時(shí)器模式。初值計(jì)算目標(biāo)定時(shí)10ms。機(jī)器周期12/11.0592MHz≈1.085us。需計(jì)次數(shù)10ms/1.085us≈9216。對于16位定時(shí)器最大值65535初值65536-921656320轉(zhuǎn)換為十六進(jìn)制即0xDC00。所以TH00xDCTL00x00。示例中用了近似值。中斷函數(shù)interrupt 1是編譯器關(guān)鍵字表示這是T0的中斷服務(wù)程序。編譯器會自動進(jìn)行現(xiàn)場保護(hù)壓棧和恢復(fù)。_crol_()循環(huán)左移函數(shù)需要包含INTRINS.H。它讓流水燈效果更簡潔。4.2 項(xiàng)目二利用定時(shí)器生成PWM控制電機(jī)速度PWM脈沖寬度調(diào)制是通過調(diào)節(jié)一個(gè)周期內(nèi)高電平所占的比例占空比來模擬不同電壓/功率的技術(shù)廣泛應(yīng)用于電機(jī)調(diào)速、LED調(diào)光。核心思路固定PWM周期例如10ms使用一個(gè)定時(shí)器中斷來維護(hù)周期計(jì)時(shí)。在中斷中根據(jù)設(shè)定的“高電平時(shí)間”即占空比來翻轉(zhuǎn)IO口電平。#include REG52.H sbit PWM_OUT P2^1; // PWM輸出引腳接電機(jī)驅(qū)動信號 unsigned int High_Time 3000; // 高電平時(shí)間單位微秒(us) 占空比 3000/10000 30% unsigned int Low_Time 7000; // 低電平時(shí)間 unsigned int Timer_Count 0; // 定時(shí)器微秒計(jì)數(shù)器 void Timer1_Init() { // 使用T1做1us精確定時(shí) TMOD 0x0F; // 高四位清零不影響T0 TMOD | 0x10; // 設(shè)置T1為方式116位定時(shí)器 TH1 (65536 - (11059200/12/1000000)) / 256; // 1us中斷一次晶振11.0592MHz TL1 (65536 - (11059200/12/1000000)) % 256; ET1 1; EA 1; TR1 1; } void main() { Timer1_Init(); PWM_OUT 1; // 初始輸出高電平 while(1) { // 這里可以通過按鍵或串口來動態(tài)改變 High_Time 和 Low_Time實(shí)現(xiàn)調(diào)速 // 例如if(Key_Pressed) High_Time 100; } } void Timer1_ISR() interrupt 3 { TH1 (65536 - (11059200/12/1000000)) / 256; // 重裝初值 TL1 (65536 - (11059200/12/1000000)) % 256; Timer_Count; if(PWM_OUT 1) { if(Timer_Count High_Time) { PWM_OUT 0; Timer_Count 0; } } else { if(Timer_Count Low_Time) { PWM_OUT 1; Timer_Count 0; } } }注意事項(xiàng)這個(gè)例子中定時(shí)器中斷頻率很高1MHz會大量占用CPU資源。在實(shí)際項(xiàng)目中更常見的做法是使用一個(gè)定時(shí)器固定周期如100us中斷在中斷里對一個(gè)全局變量累加主循環(huán)中根據(jù)這個(gè)變量和預(yù)設(shè)的占空比比較來翻轉(zhuǎn)IO口?;蛘咭恍┰鰪?qiáng)型51單片機(jī)如STC12系列自帶硬件PWM模塊直接配置寄存器即可不占用CPU。4.3 項(xiàng)目三串口通信實(shí)現(xiàn)單片機(jī)與電腦對話串口是調(diào)試和交互的利器。我們實(shí)現(xiàn)一個(gè)功能單片機(jī)每秒向電腦發(fā)送一次“Hello 51!”并且能接收電腦發(fā)來的一個(gè)字符然后回顯該字符。#include REG52.H #include stdio.h // 為了使用 printf需要重定向 putchar unsigned char UART_Rx_Data 0; // 接收數(shù)據(jù)緩存 bit UART_Rx_Flag 0; // 接收完成標(biāo)志位 void UART_Init() { SCON 0x50; // 串口方式1允許接收REN1 TMOD 0x0F; // 清零T1控制位 TMOD | 0x20; // 設(shè)置T1為方式28位自動重載 TH1 0xFD; // 波特率9600 (11.0592MHz晶振) TL1 0xFD; ET1 0; // 禁止T1中斷因?yàn)椴ㄌ芈拾l(fā)生器不需要中斷 TR1 1; // 啟動T1 ES 1; // 允許串口中斷 EA 1; // 開總中斷 } void UART_SendByte(unsigned char dat) { SBUF dat; // 數(shù)據(jù)寫入發(fā)送緩沖區(qū)自動啟動發(fā)送 while(TI 0); // 等待發(fā)送完成 TI 0; // 軟件清零發(fā)送中斷標(biāo)志位 } void UART_SendString(unsigned char *str) { while(*str ! \0) { UART_SendByte(*str); } } void main() { unsigned int i; UART_Init(); UART_SendString(UART Initialized!\r\n); // 發(fā)送初始化完成信息 while(1) { if(UART_Rx_Flag) { // 如果收到數(shù)據(jù) UART_Rx_Flag 0; UART_SendByte(UART_Rx_Data); // 回顯收到的數(shù)據(jù) UART_SendByte(\n); // 換行 } // 模擬每秒發(fā)送一次數(shù)據(jù) for(i0; i30000; i); // 粗略延時(shí) UART_SendString(Hello 51!\r\n); } } // 串口中斷服務(wù)函數(shù) void UART_ISR() interrupt 4 { if(RI) { // 如果是接收中斷 RI 0; // 軟件清零接收中斷標(biāo)志 UART_Rx_Data SBUF; // 讀取接收到的數(shù)據(jù) UART_Rx_Flag 1; // 置位接收標(biāo)志 } // 發(fā)送中斷 TI 在查詢方式發(fā)送中處理此處可不處理 }關(guān)鍵點(diǎn)解析SCON 0x500101 0000B即方式18位UART并允許接收REN1。TH1 TL1 0xFD這是11.0592MHz晶振下產(chǎn)生9600波特率的經(jīng)典初值。計(jì)算原理定時(shí)器T1在方式2下溢出率 晶振頻率 / (12 * (256 - TH1))。令其等于波特率×16串口模式1的需要即可反推出TH1。中斷與查詢結(jié)合發(fā)送采用查詢等待while(TI0);因?yàn)榘l(fā)送時(shí)機(jī)由程序主動控制接收采用中斷方式因?yàn)閿?shù)據(jù)何時(shí)到來不確定中斷能及時(shí)響應(yīng)。\r\n是回車換行保證在串口助手上能正確換行顯示。5. 進(jìn)階實(shí)戰(zhàn)智能小車驅(qū)動系統(tǒng)設(shè)計(jì)結(jié)合熱詞“51單片機(jī)智能小車”我們來設(shè)計(jì)一個(gè)最基礎(chǔ)的直流電機(jī)驅(qū)動系統(tǒng)。這里以最常見的L298N電機(jī)驅(qū)動模塊為例。5.1 硬件連接與驅(qū)動原理L298N是一個(gè)雙H橋驅(qū)動芯片可以同時(shí)驅(qū)動兩個(gè)直流電機(jī)。每個(gè)H橋可以控制電流的方向從而實(shí)現(xiàn)電機(jī)的正轉(zhuǎn)、反轉(zhuǎn)和剎車。連接方式單片機(jī)側(cè)IN1,IN2- 控制電機(jī)A的轉(zhuǎn)向。例如P2.0, P2.1。IN3,IN4- 控制電機(jī)B的轉(zhuǎn)向。例如P2.2, P2.3。ENA,ENB- 電機(jī)A/B的使能端接PWM信號可實(shí)現(xiàn)調(diào)速??梢灾苯咏覸CC全速或接單片機(jī)PWM引腳。電機(jī)側(cè)OUT1,OUT2接電機(jī)A的兩極OUT3,OUT4接電機(jī)B的兩極。電源驅(qū)動邏輯部分VCC接5V電機(jī)驅(qū)動部分VS接7-12V根據(jù)電機(jī)額定電壓。GND共地??刂七壿嬕噪姍C(jī)A為例IN1IN2ENA電機(jī)A狀態(tài)00X停止/剎車101正轉(zhuǎn)011反轉(zhuǎn)11X剎車5.2 軟件驅(qū)動層實(shí)現(xiàn)我們封裝一個(gè)電機(jī)驅(qū)動函數(shù)庫便于上層控制。// motor.h #ifndef __MOTOR_H__ #define __MOTOR_H__ typedef enum { MOTOR_STOP, MOTOR_FORWARD, MOTOR_BACKWARD, MOTOR_BRAKE } Motor_State; void Motor_Init(void); void Motor_A_Set(Motor_State state); void Motor_B_Set(Motor_State state); void Motor_PWM_Set(unsigned char pwm_a, unsigned char pwm_b); // 如果使用PWM調(diào)速 #endif// motor.c #include REG52.H #include motor.h // 引腳定義根據(jù)實(shí)際連接修改 sbit MOTOR_A_IN1 P2^0; sbit MOTOR_A_IN2 P2^1; sbit MOTOR_B_IN3 P2^2; sbit MOTOR_B_IN4 P2^3; sbit MOTOR_A_EN P2^4; // PWM使能 sbit MOTOR_B_EN P2^5; // PWM使能 void Motor_Init(void) { MOTOR_A_IN1 0; MOTOR_A_IN2 0; MOTOR_B_IN3 0; MOTOR_B_IN4 0; MOTOR_A_EN 1; // 默認(rèn)使能全速 MOTOR_B_EN 1; } void Motor_A_Set(Motor_State state) { switch(state) { case MOTOR_STOP: case MOTOR_BRAKE: // 剎車和停止邏輯相同 MOTOR_A_IN1 0; MOTOR_A_IN2 0; break; case MOTOR_FORWARD: MOTOR_A_IN1 1; MOTOR_A_IN2 0; break; case MOTOR_BACKWARD: MOTOR_A_IN1 0; MOTOR_A_IN2 1; break; } } void Motor_B_Set(Motor_State state) { // 類似 Motor_A_Set 的實(shí)現(xiàn) switch(state) { case MOTOR_STOP: case MOTOR_BRAKE: MOTOR_B_IN3 0; MOTOR_B_IN4 0; break; case MOTOR_FORWARD: MOTOR_B_IN3 1; MOTOR_B_IN4 0; break; case MOTOR_BACKWARD: MOTOR_B_IN3 0; MOTOR_B_IN4 1; break; } }在主程序中你就可以像搭積木一樣控制小車了Motor_A_Set(MOTOR_FORWARD); Motor_B_Set(MOTOR_FORWARD); // 小車直行 DelayMs(2000); Motor_A_Set(MOTOR_BACKWARD); Motor_B_Set(MOTOR_STOP); // 小車左轉(zhuǎn) DelayMs(1000); Motor_A_Set(MOTOR_STOP); Motor_B_Set(MOTOR_STOP); // 小車停止5.3 集成傳感器與閉環(huán)控制基礎(chǔ)驅(qū)動之上可以增加紅外、超聲波等傳感器實(shí)現(xiàn)自動避障、循跡。例如接入一個(gè)HC-SR04超聲波模塊測距。HC-SR04基本驅(qū)動觸發(fā)給Trig引腳一個(gè)至少10us的高電平脈沖?;仨懩K自動發(fā)送8個(gè)40kHz方波并檢測回波。Echo引腳會輸出一個(gè)高電平其持續(xù)時(shí)間與距離成正比。計(jì)算距離 (高電平時(shí)間 * 聲速) / 2。sbit HC_TRIG P3^2; sbit HC_ECHO P3^3; float Get_Distance(void) { unsigned int time_us 0; HC_TRIG 0; DelayUs(2); // 短暫延時(shí) HC_TRIG 1; DelayUs(10); // 發(fā)出10us觸發(fā)脈沖 HC_TRIG 0; while(HC_ECHO 0); // 等待Echo變高 TR0 1; // 啟動定時(shí)器T0計(jì)時(shí) while(HC_ECHO 1); // 等待Echo變低 TR0 0; // 停止計(jì)時(shí) time_us (TH0 8) | TL0; // 組合定時(shí)器值假設(shè)T0已配置為1us計(jì)數(shù) TH0 0; TL0 0; // 定時(shí)器清零 return (time_us * 0.017); // 聲速340m/s 換算成cm: 34000cm/s * 1e-6 s/us / 2 ≈ 0.017 }然后在主循環(huán)中根據(jù)測得的距離控制電機(jī)dist Get_Distance(); if(dist 20) { // 距離小于20cm Motor_A_Set(MOTOR_BACKWARD); Motor_B_Set(MOTOR_FORWARD); DelayMs(500); // 倒車并轉(zhuǎn)彎 } else { Motor_A_Set(MOTOR_FORWARD); Motor_B_Set(MOTOR_FORWARD); // 直行 }這樣一個(gè)具備基礎(chǔ)避障功能的智能小車框架就搭建起來了。你可以在此基礎(chǔ)上增加更多的傳感器如紅外循跡模塊和更復(fù)雜的控制算法如PID調(diào)速。6. 開發(fā)中的常見“坑”與調(diào)試心法玩了這么多年51踩過的坑比焊過的板子還多。下面這些經(jīng)驗(yàn)希望能幫你少走彎路。6.1 電源與復(fù)位問題現(xiàn)象程序跑飛、無故復(fù)位、IO口驅(qū)動能力弱。排查電源濾波單片機(jī)電源引腳VCC附近一定要接一個(gè)1040.1uF的瓷片電容到地且盡量靠近引腳。這是消除高頻噪聲的標(biāo)配缺了它系統(tǒng)穩(wěn)定性大打折扣。復(fù)位電路確保復(fù)位引腳RST在上電時(shí)有足夠時(shí)間通常24個(gè)時(shí)鐘周期的高電平。典型的RC復(fù)位電路10uF電容10K電阻在電源紋波大時(shí)可能不可靠。對于要求高的場合使用專用復(fù)位芯片如MAX809。驅(qū)動能力51單片機(jī)IO口拉電流輸出高電平能力很弱幾十uA灌電流輸出低電平能力較強(qiáng)十幾mA。所以驅(qū)動LED、繼電器等盡量采用低電平驅(qū)動器件接VCCIO口接器件負(fù)極。驅(qū)動大電流負(fù)載如電機(jī)必須加三極管或MOS管絕不能用IO口直驅(qū)。6.2 程序跑飛與內(nèi)存溢出現(xiàn)象程序運(yùn)行一段時(shí)間后死機(jī)、功能錯(cuò)亂。排查堆棧溢出51的RAM小堆棧區(qū)在RAM中向上生長。如果函數(shù)調(diào)用嵌套太深或中斷函數(shù)里調(diào)用大量函數(shù)極易導(dǎo)致堆棧覆蓋其他數(shù)據(jù)區(qū)。用Keil的調(diào)試模式可以查看SP寄存器的最大值。經(jīng)驗(yàn)是中斷服務(wù)函數(shù)盡量短小只置標(biāo)志位復(fù)雜處理放到主循環(huán)。數(shù)組越界這是C語言的經(jīng)典問題。訪問了不屬于你的內(nèi)存區(qū)域。仔細(xì)檢查所有數(shù)組的索引??撮T狗如果是STC單片機(jī)檢查看門狗定時(shí)器WDT是否被意外開啟。如果開啟了必須在程序循環(huán)中定期喂狗WDT_CONTR 0x37;否則會導(dǎo)致復(fù)位。未初始化變量局部變量和全局變量最好都賦予初始值。隨機(jī)值可能導(dǎo)致不可預(yù)知的行為。6.3 通信與時(shí)序故障現(xiàn)象串口亂碼、I2C/SPI設(shè)備無響應(yīng)。排查波特率誤差確保串口波特率計(jì)算準(zhǔn)確。11.0592MHz晶振是“串口之友”。如果用了12MHz晶振計(jì)算9600波特率會有誤差長時(shí)間通信可能出錯(cuò)。電平匹配51是5V TTL電平。與3.3V設(shè)備如很多傳感器、ESP8266通信時(shí)必須加電平轉(zhuǎn)換電路如分壓電阻或電平轉(zhuǎn)換芯片否則可能損壞3.3V設(shè)備。時(shí)序問題模擬I2C、單總線如DS18B20等協(xié)議時(shí)對時(shí)序要求嚴(yán)格。務(wù)必關(guān)閉中斷再進(jìn)行底層時(shí)序操作否則一個(gè)中斷進(jìn)來延時(shí)就不準(zhǔn)了。操作完再打開中斷。上拉電阻I2C的SDA、SCL線以及開漏輸出的信號線如單總線必須接上拉電阻通常4.7K-10K否則無法輸出高電平。6.4 抗干擾與可靠性設(shè)計(jì)現(xiàn)象在電機(jī)啟動、繼電器吸合時(shí)單片機(jī)復(fù)位或誤動作。對策物理隔離電機(jī)、繼電器等大功率負(fù)載與單片機(jī)控制電路使用獨(dú)立的電源供電地線在單點(diǎn)相連。信號連接使用光耦隔離。電源去耦除了104小電容在板子電源入口處加一個(gè)10-100uF的電解電容緩沖低頻干擾。信號濾波對按鍵、限位開關(guān)等機(jī)械觸點(diǎn)輸入軟件上必須做消抖處理延時(shí)20ms再檢測。硬件上可以在觸點(diǎn)兩端并聯(lián)一個(gè)104電容。未用引腳處理將未使用的IO口設(shè)置為準(zhǔn)雙向口模式默認(rèn)并輸出一個(gè)固定電平通常為1不要懸空防止靜電積累或感應(yīng)干擾。調(diào)試時(shí)善用Keil的軟件仿真和串口打印。軟件仿真可以單步跟蹤查看變量和寄存器變化。串口打印則是“printf大法好”在關(guān)鍵位置輸出狀態(tài)信息是定位問題最直觀的手段。對于51由于內(nèi)存小通常需要自己實(shí)現(xiàn)一個(gè)精簡的printf函數(shù)或者直接使用UART_SendString輸出特定字符串來標(biāo)記程序執(zhí)行到了哪里。