到實(shí)戰(zhàn):寄存器直控、按鍵消抖與排查技巧)
很多人第一次聽(tīng)到GPIO控制這幾個(gè)字以為是什么高深莫測(cè)的東西其實(shí)它就是嵌入式開(kāi)發(fā)里最基礎(chǔ)、也最離不開(kāi)的一項(xiàng)技能。說(shuō)白了GPIO就是芯片上那些可以編程控制的引腳你讓它輸出高電平就輸出高電平讓它讀外部信號(hào)就能讀外部信號(hào)。這篇文章我想以GPIO控制1為起點(diǎn)串起我從接到一個(gè)小任務(wù)到把功能穩(wěn)穩(wěn)跑通的全過(guò)程既聊聊原理也講講實(shí)操和踩過(guò)的坑希望能給剛接觸嵌入式或者被GPIO配置折磨過(guò)的人一點(diǎn)參考。這個(gè)系列我打算分幾篇寫(xiě)第一篇就專(zhuān)注在GPIO本身電氣特性、模式選擇、寄存器操作、常見(jiàn)故障排查。后面再逐步加定時(shí)器、PWM、中斷嵌套、串口聯(lián)動(dòng)這些進(jìn)階內(nèi)容。GPIO控制1雖然聽(tīng)起來(lái)簡(jiǎn)單但恰恰是這些簡(jiǎn)單的引腳配置出了問(wèn)題時(shí)最讓人頭疼。1. 項(xiàng)目概述與整體設(shè)計(jì)思路1.1 這個(gè)GPIO控制項(xiàng)目到底在做什么我的任務(wù)背景很簡(jiǎn)單在一塊基于Cortex-M內(nèi)核的國(guó)產(chǎn)單片機(jī)上實(shí)現(xiàn)幾個(gè)最典型的GPIO應(yīng)用場(chǎng)景——點(diǎn)亮并控制LED亮度、讀取按鍵狀態(tài)、用外部中斷捕捉按鍵動(dòng)作。聽(tīng)起來(lái)是不是很像大學(xué)課程里的點(diǎn)燈實(shí)驗(yàn)但實(shí)際動(dòng)手之后你會(huì)發(fā)現(xiàn)點(diǎn)燈只是表象背后涉及的問(wèn)題一個(gè)都不少引腳模式怎么選、內(nèi)部上拉下拉怎么配、驅(qū)動(dòng)能力夠不夠、代碼怎么寫(xiě)才可維護(hù)。先交代一下硬件環(huán)境主控是某國(guó)產(chǎn)Cortex-M0內(nèi)核芯片主頻48MHz板上帶一個(gè)用戶(hù)LED、兩個(gè)按鍵、一個(gè)無(wú)源蜂鳴器調(diào)試用板載DAP-Link。為了不依賴(lài)廠商IDE我直接用開(kāi)源工具鏈編譯燒錄調(diào)試信息通過(guò)串口打印輸出。這個(gè)組合很常見(jiàn)幾乎可以平移到任何類(lèi)似的MCU平臺(tái)上。這個(gè)項(xiàng)目的目標(biāo)不是堆功能而是建立一套可復(fù)用的GPIO控制方法論。也就是說(shuō)每接一個(gè)外設(shè)都能快速判斷應(yīng)該用哪種GPIO模式、需不需要外部上拉、電平極性如何定義、初始化順序怎么安排。這些判斷看似瑣碎卻是后面所有外設(shè)驅(qū)動(dòng)的地基。1.2 為什么堅(jiān)持寄存器直控而不是只用庫(kù)函數(shù)很多教程一上來(lái)就教大家調(diào)HAL庫(kù)或者標(biāo)準(zhǔn)外設(shè)庫(kù)幾行代碼就能點(diǎn)亮LED確實(shí)省事。但我這次刻意選擇了寄存器直控原因有三點(diǎn)。第一寄存器直控迫使你去看數(shù)據(jù)手冊(cè)。芯片廠商的庫(kù)函數(shù)封裝得很友好但屏蔽了太多細(xì)節(jié)。比如你把引腳配置成推挽輸出庫(kù)函數(shù)背后到底改的是哪個(gè)寄存器的哪幾個(gè)位上拉電阻又是怎么使能的這些如果不看手冊(cè)永遠(yuǎn)只是調(diào)通了但不知道為什么通。而搞嵌入式最怕的就是能用但說(shuō)不清原理。第二寄存器直控生成的代碼體積更小、執(zhí)行路徑更短。雖然現(xiàn)代MCU的Flash和RAM都大了但有些場(chǎng)景仍然對(duì)時(shí)序敏感比如用GPIO模擬通信協(xié)議時(shí)每一次庫(kù)函數(shù)調(diào)用的開(kāi)銷(xiāo)都可能是壓垮時(shí)序的最后一根稻草。寄存器操作通常只是幾條賦值語(yǔ)句省去了函數(shù)調(diào)用和結(jié)構(gòu)體指針尋址的開(kāi)銷(xiāo)。第三可移植性反而更好。有人覺(jué)得用庫(kù)函數(shù)好移植其實(shí)恰恰相反。不同廠商的庫(kù)API差異巨大而你直接操作寄存器時(shí)只要內(nèi)核架構(gòu)相同寄存器映射雖然不同但操作邏輯是可以快速遷移的。我后來(lái)把這段代碼從Cortex-M0遷移到Cortex-M4上時(shí)只需要改寄存器地址和位定義整體思路完全沒(méi)變。當(dāng)然這不是說(shuō)庫(kù)函數(shù)一無(wú)是處。如果是快速做原型驗(yàn)證用庫(kù)函數(shù)確實(shí)效率高。我的建議是初學(xué)階段一定要用寄存器把一個(gè)引腳從配置到輸出的完整路徑走一遍等原理通了再看庫(kù)函數(shù)封裝了什么這時(shí)候你的視野會(huì)完全不同。2. 硬件與原理基礎(chǔ)GPIO的內(nèi)部結(jié)構(gòu)與工作模式2.1 看懂GPIO的內(nèi)部結(jié)構(gòu)與工作模式GPIO引腳不是一根簡(jiǎn)單的導(dǎo)線它在芯片內(nèi)部有復(fù)雜的結(jié)構(gòu)?;窘M成包括輸出數(shù)據(jù)寄存器、輸入數(shù)據(jù)寄存器、方向控制邏輯、上下拉電阻、施密特觸發(fā)器、以及各種復(fù)用選擇器。在Cortex-M內(nèi)核的MCU上每個(gè)GPIO端口通常有16個(gè)引腳PA0~PA15、PB0~PB15這樣每個(gè)引腳可以獨(dú)立配置為輸入、輸出、模擬或者復(fù)用功能。每一個(gè)模式背后還有細(xì)分選項(xiàng)比如輸出模式分為推挽輸出和開(kāi)漏輸出輸入模式分為浮空輸入、上拉輸入、下拉輸入、模擬輸入。推挽輸出是最常用的引腳可以主動(dòng)輸出高電平和低電平驅(qū)動(dòng)能力較強(qiáng)。它的內(nèi)部相當(dāng)于兩個(gè)互補(bǔ)的MOS管一個(gè)負(fù)責(zé)拉高一個(gè)負(fù)責(zé)拉低所以無(wú)論輸出哪種電平驅(qū)動(dòng)能力都不錯(cuò)。開(kāi)漏輸出則不一樣。它只有拉低的能力高電平必須靠外部上拉電阻提供。換句話說(shuō)開(kāi)漏輸出相當(dāng)于只接了一個(gè)接地開(kāi)關(guān)你要讓它輸出高電平必須在引腳外部接一個(gè)上拉到電源的電阻。開(kāi)漏模式常用于以下場(chǎng)景多個(gè)設(shè)備共享一根信號(hào)線比如I2C的總線仲裁或者需要實(shí)現(xiàn)電平轉(zhuǎn)換——輸出端通過(guò)上拉到不同電壓的電源就可以用3.3V的單片機(jī)去控制5V的設(shè)備。輸入模式就更講究了。浮空輸入意味著引腳內(nèi)部沒(méi)有任何電阻電平完全由外部電路決定如果外部懸空引腳電平就是不確定的很容易受干擾。所以按鍵檢測(cè)這類(lèi)場(chǎng)景通常會(huì)在硬件上加上拉或下拉電阻。如果硬件沒(méi)接你可以用芯片內(nèi)部的上拉/下拉電阻這就是圖里那個(gè)內(nèi)部上下拉的意義。還有一個(gè)容易被忽略的點(diǎn)施密特觸發(fā)器。它能讓輸入信號(hào)在高低電平閾值之間形成一個(gè)滯回區(qū)間。簡(jiǎn)單說(shuō)信號(hào)不是一超過(guò)閾值就翻轉(zhuǎn)而是要高過(guò)上限或低過(guò)下限才翻轉(zhuǎn)這樣能有效濾除輸入信號(hào)上的毛刺干擾。很多MCU的GPIO輸入默認(rèn)就帶這個(gè)功能但有些芯片需要在配置寄存器里專(zhuān)門(mén)使能。生活里打個(gè)比方GPIO引腳就像一個(gè)多功能插座插孔本身一樣但你想插什么設(shè)備、需要什么樣的電就得按不同的方式接線和配置。推挽輸出是主動(dòng)供電壓開(kāi)漏輸出是只能拉地輸入模式是只看不動(dòng)而內(nèi)部上下拉電阻則是內(nèi)置的固定電平吸附器。2.2 硬件連接要點(diǎn)與驅(qū)動(dòng)能力邊界硬件連接看起來(lái)是拿杜邦線插一插的事實(shí)際上有很多細(xì)節(jié)能決定項(xiàng)目成敗。第一個(gè)是驅(qū)動(dòng)能力。MCU的GPIO引腳不是無(wú)限供流的Cortex-M內(nèi)核的芯片通常單個(gè)引腳能承受的電流在4mA到20mA不等具體看數(shù)據(jù)手冊(cè)。比如我用的這款推挽輸出拉電流約5mA灌電流約8mA。這意味著直接點(diǎn)亮一個(gè)20mA額定電流的LED會(huì)很吃力更別說(shuō)驅(qū)動(dòng)繼電器、直流電機(jī)這類(lèi)負(fù)載了。正確做法是給LED串接限流電阻控制在2mA到5mA之間。公式很簡(jiǎn)單R (VCC - V_led) / I_led。比如供電3.3V紅色LED壓降約1.8V目標(biāo)電流3mA那R (3.3 - 1.8) / 0.003 500Ω取標(biāo)稱(chēng)值510Ω或470Ω都行。如果你非要用GPIO直接驅(qū)動(dòng)蜂鳴器也要算清楚蜂鳴器的額定電流是不是在引腳承受范圍內(nèi)否則輕則聲音發(fā)悶重則燒引腳。第二個(gè)是電平匹配。3.3V的系統(tǒng)去控制5V的外設(shè)直接接是會(huì)出問(wèn)題的。常見(jiàn)方案有三極管電平轉(zhuǎn)換、MOS管雙向電平轉(zhuǎn)換模塊、光耦隔離。我在這個(gè)項(xiàng)目里驅(qū)動(dòng)無(wú)源蜂鳴器時(shí)就用了一個(gè)NPN三極管GPIO輸出高電平去控制三極管的基極蜂鳴器接在集電極和電源之間這樣GPIO只負(fù)責(zé)提供微安級(jí)的基極電流蜂鳴器由外部電源驅(qū)動(dòng)互不干擾。第三個(gè)是去耦和布線。凡是涉及外部信號(hào)輸入比如按鍵如果你用長(zhǎng)杜邦線連接線材本身就像天線容易接收噪聲。優(yōu)先選擇使用內(nèi)部上拉電阻配合按鍵接地的方式這樣常態(tài)下引腳是高電平按鍵按下變成低電平信號(hào)變化方向清晰且抗干擾。如果按鍵離主控很遠(yuǎn)建議在按鍵兩端并聯(lián)一個(gè)100nF的電容做硬件消抖。硬件上還有一個(gè)我反復(fù)提醒自己的原則上電默認(rèn)態(tài)。芯片復(fù)位后所有GPIO默認(rèn)是浮空輸入模式引腳電平是不確定的。如果你的硬件設(shè)計(jì)里有一個(gè)上電默認(rèn)必須為高的控制信號(hào)但MCU復(fù)位期間引腳輸出低電平就會(huì)導(dǎo)致外設(shè)誤動(dòng)作。處理方案有兩種硬件上加下拉或上拉電阻把默認(rèn)態(tài)鉗住或者軟件里在系統(tǒng)啟動(dòng)的最早階段甚至進(jìn)入main函數(shù)前就把對(duì)應(yīng)引腳初始化成指定狀態(tài)。3. 軟件實(shí)現(xiàn)與核心環(huán)節(jié)實(shí)操3.1 推挽輸出驅(qū)動(dòng)LED呼吸燈先從最簡(jiǎn)單的LED開(kāi)始。我要做的不是單純的點(diǎn)亮和熄滅而是實(shí)現(xiàn)一個(gè)呼吸燈效果這樣能順帶講清楚PWM的GPIO級(jí)實(shí)現(xiàn)又不需要引入定時(shí)器外設(shè)。在不使用硬件PWM模塊的前提下用定時(shí)器中斷配合GPIO翻轉(zhuǎn)是常規(guī)方案但這里為了聚焦GPIO我用了純軟件延時(shí)實(shí)現(xiàn)占空比變化。代碼雖然簡(jiǎn)單但能直觀展示GPIO輸出狀態(tài)的操控過(guò)程。#include gd32f1x0.h #define LED_PIN GPIO_PIN_1 #define LED_PORT GPIOA void gpio_led_init(void) { rcu_periph_clock_enable(RCU_GPIOA); gpio_mode_set(LED_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, LED_PIN); gpio_output_options_set(LED_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, LED_PIN); gpio_bit_reset(LED_PORT, LED_PIN); } void delay_us(uint32_t us) { // 簡(jiǎn)單軟件延時(shí)具體時(shí)長(zhǎng)需依據(jù)主頻校準(zhǔn) while (us--) { for (volatile uint32_t i 0; i 12; i); } } void led_breathe(void) { for (int duty 0; duty 100; duty) { int high_us duty * 20; int low_us 2000 - high_us; for (int period 0; period 50; period) { gpio_bit_set(LED_PORT, LED_PIN); delay_us(high_us); gpio_bit_reset(LED_PORT, LED_PIN); delay_us(low_us); } } for (int duty 100; duty 0; duty--) { int high_us duty * 20; int low_us 2000 - high_us; for (int period 0; period 50; period) { gpio_bit_set(LED_PORT, LED_PIN); delay_us(high_us); gpio_bit_reset(LED_PORT, LED_PIN); delay_us(low_us); } } }這段代碼的思路是固定周期2ms按占空比分配高電平時(shí)間和低電平時(shí)間循環(huán)50個(gè)周期后再調(diào)整占空比實(shí)現(xiàn)平滑呼吸。注意后面我提到的純軟件延時(shí)的缺陷它會(huì)阻塞整個(gè)CPU期間無(wú)法響應(yīng)其他任務(wù)。真正做產(chǎn)品時(shí)應(yīng)該用硬件定時(shí)器生成PWM或者用非阻塞的狀態(tài)機(jī)結(jié)構(gòu)。有一個(gè)小細(xì)節(jié)值得點(diǎn)出來(lái)gpio_output_options_set里我配置為推挽輸出50MHz。這里的速度等級(jí)不是引腳實(shí)際輸出50MHz的信號(hào)而是指引腳內(nèi)部驅(qū)動(dòng)電路的翻轉(zhuǎn)速率上限。對(duì)于LED這種低速應(yīng)用配置成最低速檔就足夠了反而能減少EMI和過(guò)沖。我之所以選50MHz是為了驗(yàn)證高速翻轉(zhuǎn)電平質(zhì)量后面用示波器看波形時(shí)會(huì)說(shuō)到這兩者的差異。3.2 輸入模式與按鍵消抖處理按鍵是GPIO輸入的經(jīng)典場(chǎng)景。硬件連接還是老一套按鍵一端接GND另一端接GPIO引腳同時(shí)GPIO引腳通過(guò)內(nèi)部上拉電阻接到高電平。這樣按鍵釋放時(shí)讀到高電平按下時(shí)讀到低電平邏輯清晰。配置代碼和輸出模式相比只改了兩處模式設(shè)為輸入使能上拉。#define KEY_PIN GPIO_PIN_2 #define KEY_PORT GPIOA void gpio_key_init(void) { rcu_periph_clock_enable(RCU_GPIOA); gpio_mode_set(KEY_PORT, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, KEY_PIN); } uint8_t key_read(void) { return gpio_input_bit_get(KEY_PORT, KEY_PIN); }直接讀按鍵電平很容易踩坑因?yàn)闄C(jī)械按鍵存在抖動(dòng)。按鍵從按下到穩(wěn)定通常有5ms到20ms的機(jī)械抖動(dòng)期間電平會(huì)快速跳變多次。如果主循環(huán)每次都直接讀引腳很可能會(huì)把一次按鍵當(dāng)成幾十次觸發(fā)。解決方法就是消抖檢測(cè)到電平變化后延遲10ms再讀一次兩次一致才確認(rèn)狀態(tài)變化。我做的消抖邏輯如下#define KEY_DEBOUNCE_MS 15 uint8_t key_scan(void) { static uint8_t last_state 1; static uint32_t last_time 0; uint8_t current_state key_read(); if (current_state ! last_state) { if (get_tick_ms() - last_time KEY_DEBOUNCE_MS) { last_state current_state; last_time get_tick_ms(); return current_state 0 ? 1 : 0; // 按下返回1 } } else { last_time get_tick_ms(); } return 0; }這種非阻塞的消抖方式讓主循環(huán)可以繼續(xù)做其他事情而不是干等15ms。get_tick_ms()是我用SysTick維護(hù)的毫秒計(jì)數(shù)器這也是嵌入式開(kāi)發(fā)的基本功永遠(yuǎn)不要在延時(shí)上浪費(fèi)CPU。消抖的本質(zhì)是在時(shí)間維度上濾波只認(rèn)穩(wěn)定電平。這和硬件上用電容濾波的道理是一樣的只是一個(gè)在物理層面濾掉毛刺一個(gè)在邏輯層面濾掉毛刺。兩者結(jié)合使用效果會(huì)更好。3.3 外部中斷與上升沿捕獲LED和按鍵都搞定了接下來(lái)是按鍵的外部中斷。我希望實(shí)現(xiàn)按下按鍵時(shí)中斷觸發(fā)在中斷服務(wù)函數(shù)里切換LED狀態(tài)這種用法在低功耗系統(tǒng)中很常見(jiàn)系統(tǒng)平時(shí)睡眠按鍵喚醒MCU再干活。外部中斷配置涉及三個(gè)層面GPIO層面要配置成輸入并選擇中斷觸發(fā)邊沿EXTI層面要把引腳映射到外部中斷線NVIC層面要允許對(duì)應(yīng)IRQ被響應(yīng)。void gpio_irqn_init(void) { rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_SYSCFG); gpio_mode_set(KEY_PORT, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, KEY_PIN); gpio_af_set(KEY_PORT, GPIO_AF_0, KEY_PIN); syscfg_exti_line_config(EXTI_SOURCE_GPIOA, EXTI_SOURCE_PIN2); exti_init(exti_handle, EXTI_2, EXTI_INTERRUPT, EXTI_TRIG_FALLING); exti_interrupt_flag_clear(EXTI_2); nvic_irq_enable(EXTI2_3_IRQn, 1, 0); } void EXTI2_3_IRQHandler(void) { if (exti_interrupt_flag_get(EXTI_2)) { exti_interrupt_flag_clear(EXTI_2); gpio_bit_toggle(LED_PORT, LED_PIN); } }注意我配置的是下降沿觸發(fā)也就是按鍵按下的瞬間觸發(fā)??紤]到按鍵抖動(dòng)這樣可能會(huì)導(dǎo)致一次按下觸發(fā)多次中斷。真正的工程做法是在中斷里加一個(gè)軟件鎖或者用定時(shí)器在中斷后開(kāi)啟一個(gè)消抖窗口窗口內(nèi)再次觸發(fā)直接忽略。我實(shí)際測(cè)試時(shí)按鍵質(zhì)量一般的確出現(xiàn)了單次按下LED偶爾翻轉(zhuǎn)兩次的現(xiàn)象后來(lái)加了一個(gè)簡(jiǎn)單的鎖存標(biāo)志才解決。中斷服務(wù)函數(shù)里做的事越少越好這是我反復(fù)強(qiáng)調(diào)的。盡量只做一些標(biāo)志位的更新實(shí)際的業(yè)務(wù)邏輯放到主循環(huán)里處理。中斷里做延時(shí)、打印、復(fù)雜運(yùn)算都是大忌會(huì)導(dǎo)致中斷響應(yīng)超時(shí)影響其他中斷的實(shí)時(shí)性。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄4.1 電平異常浮空輸入惹的禍有一次調(diào)試按鍵發(fā)現(xiàn)按鍵沒(méi)按下時(shí)讀取到的電平隨機(jī)跳變有時(shí)高有時(shí)低。我用示波器一看引腳上的波形確實(shí)在0V和3.3V之間亂跳。問(wèn)題一下就暴露了按鍵連接電路里漏焊了外部上拉電阻而我的GPIO配置用的依然是浮空輸入引腳等于完全懸空。解決辦法很簡(jiǎn)單把輸入模式改成上拉輸入啟用內(nèi)部上拉電阻問(wèn)題立刻消失。這件事告訴我一個(gè)教訓(xùn)硬件設(shè)計(jì)不能完全依賴(lài)PCB的走線軟件初始化必須明確指定上下拉狀態(tài)絕不要用浮空輸入去接任何沒(méi)有外部確定的信號(hào)。用內(nèi)部上拉還有一個(gè)好處節(jié)省一顆電阻降低物料清單成本。但要注意內(nèi)部上拉的阻值通常在30kΩ到50kΩ之間比外部常見(jiàn)10kΩ要弱一些在強(qiáng)干擾環(huán)境下可能不夠穩(wěn)固。如果是工業(yè)現(xiàn)場(chǎng)的長(zhǎng)線連接還是建議外部并一個(gè)10kΩ電阻穩(wěn)一點(diǎn)。4.2 引腳失效復(fù)用功能沖突另一個(gè)頭疼的問(wèn)題是有幾個(gè)引腳怎么配置都沒(méi)反應(yīng)。寫(xiě)入寄存器讀回來(lái)是對(duì)的但引腳就是保持高電平不動(dòng)。查了半天才想起來(lái)這幾個(gè)引腳默認(rèn)復(fù)用到調(diào)試接口或者晶振功能上。比如PA13、PA14、PA15和PB3、PB4在Cortex-M芯片上通常是SWD調(diào)試口的引腳默認(rèn)不是普通GPIO。如果你想把這些引腳當(dāng)作普通GPIO使用必須先把調(diào)試接口的引腳復(fù)用給釋放出來(lái)或者把調(diào)試接口映射到其他引腳。這一點(diǎn)在項(xiàng)目設(shè)計(jì)初就應(yīng)該考慮到等PCB都打樣回來(lái)了才發(fā)現(xiàn)就只能飛線或者放棄那幾個(gè)引腳代價(jià)很大。排查這種問(wèn)題時(shí)最直接的辦法是逐個(gè)翻閱手冊(cè)里的引腳復(fù)用功能表確認(rèn)目標(biāo)引腳在復(fù)位后的默認(rèn)功能是什么有哪些復(fù)用功能可以切換。不要憑經(jīng)驗(yàn)猜數(shù)據(jù)手冊(cè)永遠(yuǎn)是第一參考。4.3 驅(qū)動(dòng)能力不足帶不動(dòng)負(fù)載怎么辦前面提到過(guò)驅(qū)動(dòng)能力有限的問(wèn)題實(shí)際測(cè)試時(shí)我把一個(gè)5V繼電器直接接到GPIO上想試試能不能帶動(dòng)。結(jié)果繼電器完全不動(dòng)作測(cè)引腳電壓從3.3V掉到了1V左右。這說(shuō)明引腳被繼電器線圈拉得很慘屬于典型的過(guò)載。正確做法是用三極管或MOS管做開(kāi)關(guān)用GPIO控制低功率的開(kāi)關(guān)管再由開(kāi)關(guān)管驅(qū)動(dòng)大電流負(fù)載。我實(shí)際用的是NPN三極管GPIO輸出高電平通過(guò)1kΩ基極電阻控制三極管導(dǎo)通繼電器線圈接在電源和三極管集電極之間同時(shí)線圈兩端并聯(lián)一個(gè)續(xù)流二極管防止感性負(fù)載斷電時(shí)產(chǎn)生反電動(dòng)勢(shì)打壞三極管。這里需要特別注意續(xù)流二極管的方向二極管陰極接電源陽(yáng)極接三極管集電極。沒(méi)有這個(gè)二極管繼電器斷開(kāi)瞬間的反電動(dòng)勢(shì)很容易擊穿三極管。這個(gè)細(xì)節(jié)如果遺漏后續(xù)排查會(huì)非常痛苦。4.4 快速排查速查表為了節(jié)省排查時(shí)間我把常見(jiàn)的GPIO問(wèn)題整理成了表格每次出問(wèn)題先對(duì)著表格過(guò)一遍。異?,F(xiàn)象可能原因驗(yàn)證方法解決方案引腳始終輸出低/高電平GPIO模式未正確配置或被復(fù)用功能占用讀寫(xiě)寄存器確認(rèn)配置值按手冊(cè)配置模式檢查復(fù)用映射輸出電平偏低過(guò)載超出引腳驅(qū)動(dòng)能力測(cè)量空載與帶載電壓差增加三極管驅(qū)動(dòng)或減小負(fù)載電流輸入電平飄忽不定引腳懸空未啟用上下拉示波器觀察波形亂跳啟用內(nèi)部上拉/下拉或外部加電阻按鍵觸發(fā)多次硬件抖動(dòng)未消除示波器看按下時(shí)多次跳變?cè)黾榆浖痘虿⒙?lián)濾波電容中斷不觸發(fā)NVIC未使能或觸發(fā)邊沿選擇錯(cuò)誤單步調(diào)試檢查中斷標(biāo)志檢查NVIC配置確認(rèn)邊沿極性引腳燒毀發(fā)燙長(zhǎng)期過(guò)流或電平接反用萬(wàn)用表測(cè)引腳電流重新設(shè)計(jì)外圍驅(qū)動(dòng)電路這張表雖然簡(jiǎn)單但覆蓋了80%以上的GPIO問(wèn)題。日常調(diào)試時(shí)我習(xí)慣先軟件后硬件用寄存器讀回值確認(rèn)配置有沒(méi)有生效再用示波器確認(rèn)引腳實(shí)際物理狀態(tài)最后再懷疑芯片本身。5. 踩坑心得與后續(xù)擴(kuò)展這個(gè)GPIO控制1做下來(lái)我最大的體會(huì)就是GPIO控制不是點(diǎn)燈這么簡(jiǎn)單每一根引腳背后都有一整套寄存器映射、電氣特性和復(fù)用邏輯。如果你剛?cè)腴T(mén)千萬(wàn)不要滿(mǎn)足于能亮這個(gè)結(jié)果多問(wèn)問(wèn)自己為什么這個(gè)引腳能輸出高電平內(nèi)部上拉電阻是怎么接進(jìn)去的推挽和開(kāi)漏區(qū)別在哪個(gè)環(huán)節(jié)這些問(wèn)題想明白一個(gè)比調(diào)通十個(gè)點(diǎn)燈實(shí)驗(yàn)都管用。以寄存器直控作為切入口還有個(gè)額外收獲是代碼體積。整個(gè)GPIO驅(qū)動(dòng)部分加上外部中斷處理編譯出來(lái)的固件不到3KB這在很多對(duì)Flash空間敏感的場(chǎng)景里很重要。雖然現(xiàn)在的芯片容量越來(lái)越大但養(yǎng)成精簡(jiǎn)代碼的習(xí)慣不會(huì)錯(cuò)。另外建議花點(diǎn)時(shí)間研究一下你自己芯片的GPIO速度等級(jí)配置。我后來(lái)用示波器實(shí)測(cè)了低速檔和高速檔在翻轉(zhuǎn)方波時(shí)的波形差異低速檔上升沿大約50ns高速檔可以壓到20ns以?xún)?nèi)但高速檔的過(guò)沖也明顯增大。如果是做電磁兼容敏感的消費(fèi)類(lèi)產(chǎn)品選低速檔能減小輻射做高速信號(hào)模擬時(shí)再考慮高速檔。這個(gè)系列后面我打算接著寫(xiě)GPIO控制2用定時(shí)器輸出硬件PWM把呼吸燈從軟件延時(shí)改成定時(shí)器硬件輸出同時(shí)講講多通道PWM如何聯(lián)動(dòng)控制RGB燈珠以及PWM和GPIO電平切換的時(shí)序邊界問(wèn)題。最后再分享一個(gè)小技巧調(diào)試GPIO時(shí)先把所有用到的引腳統(tǒng)一做一次只讀巡檢把每個(gè)引腳的寄存器配置值、當(dāng)前輸入電平打印出來(lái)。這個(gè)簡(jiǎn)單的巡檢函數(shù)能幫你快速定位硬件和軟件不匹配的問(wèn)題我在這個(gè)項(xiàng)目里靠它省了不少事。下一步動(dòng)手之前記得先看數(shù)據(jù)手冊(cè)的引腳定義表再?zèng)Q定你的電路怎么畫(huà)真的能避開(kāi)幾個(gè)大坑。懟不了。