關注公眾號,回復“入門資料”獲取單片機入門到高級開掛教程
?開發板帶你入門,我們帶你飛
文 | 無際(微信:2777492857)
全文約3900字,閱讀大約需要?10?分鐘
每個工程師都會有一段看到指針、結構體、位操作就頭大的經歷,特別是學完C語言,還沒做過幾個項目,基礎不扎實的時候。
? ? ? ? ?? ?
做嵌入式開發,它們可不是什么選修課,而是必修的核心技能。
? ? ? ? ?? ?
為啥這么說?舉例幾點:
? ? ? ? ?? ?
第一,單片機的靈魂在于控制硬件。而硬件寄存器就是內存里的特定地址。不會指針,你怎么直接、高效地訪問這些地址?難道每次都靠庫函數封裝?那遇到庫沒提供的功能或者需要極致優化時,咋辦?
? ? ? ? ?? ?
第二,單片機資源(內存、CPU速度)通常很寶貴。指針用得好,可以避免大量數據拷貝,提升運行效率;結構體用得妙,能清晰地組織數據,方便管理;位操作更是精準控制硬件、節省內存空間的利器。反之,你的代碼可能臃腫、緩慢,甚至在資源緊張的芯片上根本跑不起來。
? ? ? ? ?? ?
第三,良好的結構體設計和清晰的位操作(配合宏定義),能讓代碼邏輯一目了然,易于維護和功能迭代。
? ? ? ? ?? ?
第四,無法深入理解驅動層、底層協議棧、操作系統內核(如果你用RTOS的話)的工作原理。這些地方大量運用了指針、結構體和位操作。掌握不了這些,你可能長期停留在“調包俠”的階段,難以成為真正獨當一面的單片機系統工程師。
? ? ? ? ?? ?
這篇文章將用最接地氣的方式,帶你重新認識并掌握單片機C語言的三大核心:指針、結構體、位操作。讀完它,你將能夠:
? ? ? ? ?? ?
?理解指針在單片機開發中的真正價值和應用場景。
?學會如何利用結構體優雅地管理硬件寄存器和數據。
?熟練運用位操作,對硬件進行精準到比特級別的控制。
?寫出更專業、更高效、更健壯的單片機代碼。
?為自己向更高階的單片機開發之路掃清障礙。
? ? ? ? ?? ?
系好安全帶,準備發車。
? ? ? ? ?? ?
一、指針
很多初學者怕指針,主要是怕它指向不明或者操作失誤導致程序崩潰。在有內存管理單元(MMU)的系統里,這確實是個大問題。但在大多數裸跑的單片機里,內存地址是直接對應的物理地址,指針更像是一個精確的門牌號,用好了就是神器。
? ? ? ? ?? ?
為什么單片機離不開指針?
? ? ? ? ?? ?
1.訪問硬件寄存器:這是最最核心的用途。單片機的外設(GPIO, UART, SPI, I2C, Timer等)都是通過讀寫特定內存地址上的寄存器來控制的。這些地址是固定的,定義在芯片的數據手冊(Datasheet)里。我們必須通過指針來操作它們。? ??

? ? ? ? ?? ?
? ? ? ? ?? ?
2.高效傳遞參數:向函數傳遞大型數據結構(比如一個配置信息結構體)時,直接傳值會拷貝整個結構體,既慢又浪費棧空間。傳遞指向該結構體的指針,只需要傳遞一個地址(通常是2字節或4字節),效率極高。? ??
? ? ? ? ?? ?
3.實現某些數據結構和算法:鏈表、緩沖區管理等,都離不開指針。
? ? ? ? ?? ?
用指針,記住幾點:
?明確指向:確保你的指針指向一個有效的、你想要操作的內存地址。對于硬件寄存器,地址是固定的;對于變量,要取其地址(&)。
? ? ? ? ?? ?
?類型匹配:指針類型決定了它一次訪問多少字節以及如何解釋這些字節。char *一次訪問1字節,int *(假設int為4字節)一次訪問4字節。訪問寄存器時,務必使用與寄存器位寬匹配的指針類型(通常是?unsigned int *?或?unsigned short *?或?unsigned char *)。
? ? ? ? ?? ?
?volatile關鍵字:訪問硬件寄存器或在中斷服務程序中修改的全局變量時,務必使用?volatile?修飾指針(或指針指向的變量)。這防止編譯器進行不當優化,確保每次都從內存中真實讀寫數據。
? ? ? ? ?? ?
?空指針檢查:雖然裸機環境相對簡單,但養成檢查指針是否為空(NULL)的好習慣總沒錯,特別是在處理動態分配或可能無效的指針時。
? ? ? ? ?? ?
指針不是魔法,它就是地址。理解內存布局,知道你要操作哪里,指針就是你最得力的工具。
? ? ? ? ?? ?
二、結構體
單獨操作一個個寄存器地址,是不是感覺很零散,容易出錯?如果一個外設(比如UART)有十幾個關聯的寄存器,你難道要定義十幾個宏和指針?太不優雅了!這時,結構體閃亮登場。? ??
? ? ? ? ?? ?
結構體在單片機中的妙用:
1.封裝硬件寄存器組:這是結構體在單片機領域最光輝的應用!我們可以把一個外設的所有寄存器,按照它們在內存中的布局,定義成一個結構體。然后,只需要一個指向這個結構體類型的指針,就可以訪問該外設的所有寄存器了。
// 假設一個簡化的UART外設有以下寄存器 (地址連續)? ??// 0x40004400: UART_CR1 (控制寄存器1) - 32位? ??// 0x40004404: UART_SR (狀態寄存器) - 32位? ??// 0x40004408: UART_DR (數據寄存器) - 32位? ??// 定義UART寄存器結構體? ??typedef?struct? ? {? ? ? ??volatile?unsigned?int?CR1;?// 控制寄存器1? ? ? ??volatile?unsigned?int?SR; ?// 狀態寄存器? ? ? ??volatile?unsigned?int?DR; ?// 數據寄存器? ? ? ??// ... 可能還有其他寄存器? ? } UART_TypeDef;? ??// 定義指向該結構體類型的指針,指向UART外設的基地址? ??? ? UART_TypeDef *pUART1 = (UART_TypeDef *)UART1_BASE_ADDR;? ??// 操作寄存器變得非常直觀? ??// 啟用UART發送功能 (假設CR1的第3位是發送使能位 TE)? ? pUART1->CR1 |= (1?<3);?? ??// 檢查發送數據寄存器是否為空 (假設SR的第7位是TXE標志)? ??while?(!(pUART1->SR & (1?<7)))? ? {? ? ? ??// 等待為空? ? }? ??// 發送一個字節 'A'? ? pUART1->DR =?'A';
? ? ? ? ?? ?
看,是不是比操作零散的地址宏清晰多了?代碼可讀性、可維護性大大提高。這正是所有標準外設庫(如STM32 HAL/LL庫、NXP MCUXpresso SDK等)都在用的方法。
? ? ? ? ?? ?
2.組織數據:管理設備狀態、配置參數、通信協議的數據包等,用結構體來打包相關信息,再自然不過了。
? ? ? ? ?? ?
用結構體,關注幾點:
?內存對齊:編譯器可能會為了優化訪問速度,在結構體成員之間插入填充字節,導致結構體大小不等于成員大小之和。在直接映射硬件寄存器時,要確保結構體成員的布局與硬件手冊中的寄存器偏移完全一致。有時需要使用?__packed(不同編譯器的關鍵字可能不同)或?#pragma pack?指令來控制內存對齊。不過,對于按順序定義的32位或16位寄存器組,通常默認對齊就能正確工作。
? ? ? ? ?? ?
?指針訪問成員:通過指向結構體的指針訪問成員時,使用?->?運算符;通過結構體變量本身訪問成員時,使用?.?運算符。別搞混了。
? ? ? ? ?? ?
?typedef簡化:使用?typedef?為結構體類型創建一個別名,代碼更簡潔。
? ? ? ? ?? ?
結構體是代碼的組織者,善用它,你的代碼會像書架一樣整齊有序。
? ? ? ? ?? ?
三、位操作? ??
單片機的寄存器里,每一位(bit)通常都有特定的含義,代表一個開關、一個狀態標志、或者某個配置值的一部分。我們必須能夠精確地操作這些位,而不是影響到旁邊的位。
? ? ? ? ?? ?
為什么必須掌握位操作?
1.精準控制硬件:寄存器的配置往往就是設置或清除其中的某幾位。比如,控制一個GPIO引腳輸出高電平,可能就是設置某個寄存器的某一位為1;啟用某個中斷,也是設置相應寄存器的某一位。
? ? ? ? ?? ?
2.節省內存:有時可以用一個字節(8位)的不同位來存儲8個不同的布爾狀態標志,而不是定義8個char或int變量,極大地節省了寶貴的RAM。
? ? ? ? ?? ?
3.協議解析與封裝:很多通信協議(如CAN、I2C的某些部分)的數據格式是按位定義的,需要用位操作來解析收到的數據或封裝要發送的數據。
? ? ? ? ?? ?
常用的位操作符和技巧:
? ? ? ? ?? ?
?按位與?&:
?清零特定位 (Clear bits):reg & (~(1 << n))?將寄存器?reg?的第?n?位清零,其他位不變。~?是按位取反。
?檢查特定位是否為1 (Check bit):if (reg & (1 << n))?判斷?reg?的第?n?位是否為1。
? ? ? ? ?? ?
?按位或?|:
?設置特定位為1 (Set bits):reg | (1 << n)?將寄存器?reg?的第?n?位設置為1,其他位不變。
? ? ? ? ?? ?
?按位異或?^:
?翻轉特定位 (Toggle bits):reg ^ (1 << n)?將寄存器?reg?的第?n?位翻轉(0變1,1變0),其他位不變。
? ? ? ? ?? ?
?左移?<<:1 << n?生成一個只有第?n?位是1,其余位是0的掩碼(mask),是位操作中最常用的輔助工具。
?右移?>>:用于提取某個位或某個位域的值。例如,提取?reg?的第?n?位的值:(reg >> n) & 1。
? ? ? ? ?? ?
位操作實戰示例:
// 假設 GPIOA_MODER 寄存器用于配置GPIO模式,每兩位控制一個引腳// 引腳5需要配置為通用輸出模式 (模式代碼為 01)// 引腳5對應的位是 bit 11 和 bit 10volatile?unsigned?int?*pGPIOA_MODER = (volatile?unsigned?int?*)GPIOA_MODER_ADDR;unsigned?int?reg_val;// 1. 讀取當前寄存器值reg_val = *pGPIOA_MODER;// 2. 清除引腳5對應的模式位 (bit 11 和 bit 10)// ? ?掩碼為 (0b11 << 10) = (3 << 10)reg_val &= ~(3?<10);?// 3. 設置引腳5為通用輸出模式 (01)// ? ?模式值為 (0b01 << 10) = (1 << 10)reg_val |= (1?<10);// 4. 將修改后的值寫回寄存器*pGPIOA_MODER = reg_val;// 更簡潔的原子操作 (如果寄存器支持直接位帶操作或者用庫函數,可能更佳)// 但上述讀 - 改 - 寫是通用且安全的方法,避免影響其他位
? ? ? ? ?? ?
用位操作,注意:
?可讀性:直接寫?reg |= 0x08;?不如寫?reg |= (1 << 3);?清晰。最好使用宏定義來表示位的位置和掩碼,例如?#define PIN5_MODE_MASK (3 << 10)?和?#define PIN5_MODE_OUTPUT (1 << 10)。
? ? ? ? ?? ?
?操作符優先級:位操作符的優先級通常低于算術運算符和比較運算符,不確定時多用括號?()?保證運算順序。
? ? ? ? ?? ?
?讀 - 改 - 寫:操作寄存器位時,最安全的方式是先讀出整個寄存器的值,然后在本地變量中進行位修改,最后再把修改后的完整值寫回寄存器。這避免了直接在寄存器上進行?|=?或?&=?操作時可能產生的競態條件(尤其是在中斷可能修改同一寄存器時)。
? ? ? ? ?? ?
指針、結構體、位操作,是深入單片機底層、拿捏硬件資源的“三板斧”。吃透它們,告別青澀,邁向硬核!
end

下面是更多無際原創的個人成長經歷、行業經驗、技術干貨。
1.電子工程師是怎樣的成長之路?10年5000字總結
2.如何快速看懂別人的代碼和思維
3.單片機開發項目全局變量太多怎么管理?
4.C語言開發單片機為什么大多數都采用全局變量的形式?
5.單片機怎么實現模塊化編程?實用程度讓人發指!
6.c語言回調函數的使用及實際作用詳解
7.手把手教你c語言隊列實現代碼,通俗易懂超詳細!
8.c語言指針用法詳解,通俗易懂超詳細!