指南:初始化、DMA多通道與踩坑詳解)
簡介面向STM32嵌入式開發(fā)者的ADC模數(shù)轉(zhuǎn)換HAL庫開發(fā)資源包聚焦基于HAL庫完成ADC初始化、通道配置、觸發(fā)源設(shè)置、啟動(dòng)轉(zhuǎn)換、結(jié)果讀取與錯(cuò)誤處理等完整流程適合正在學(xué)習(xí)HAL庫或需要快速上手ADC多通道采集、DMA傳輸及校準(zhǔn)應(yīng)用的開發(fā)者。壓縮包共1000個(gè)文件大小24.08MB以C源碼559個(gè).c、243個(gè).h為主體包含STM32CubeMX生成的.ioc與.mxproject工程配置、KEIL/IAR工程文件、鏈接腳本.icf/.sct、編譯產(chǎn)物.o/.axf/.hex/.map以及相關(guān)庫文件便于直接對照工程學(xué)習(xí)或移植。已有226人瀏覽學(xué)習(xí)。內(nèi)容預(yù)覽顯示其中集成CMSIS DSP數(shù)學(xué)庫及ADC轉(zhuǎn)換示例涵蓋線性插值、DCT、FFT等常用算法源文件可輔助開發(fā)者理解ADC采樣數(shù)據(jù)在數(shù)字信號(hào)處理中的應(yīng)用。學(xué)習(xí)后可掌握HAL庫ADC驅(qū)動(dòng)編寫思路、常見配置項(xiàng)含義及工程構(gòu)建方法減少查閱手冊的時(shí)間。1. 直接用寄存器寫 ADC 和用 HAL 庫寫 ADC差在哪里做過標(biāo)準(zhǔn)外設(shè)庫或者直接操作寄存器的開發(fā)者初次換到 HAL 庫寫 ADC 時(shí)多半會(huì)有一個(gè)共同的困惑一個(gè)本來十幾行就能讀完數(shù)據(jù)的 ADC 初始化HAL 庫硬生生拆成了ADC_HandleTypeDef、ADC_ChannelConfTypeDef再加上一堆HAL_ADC_ConfigChannel、HAL_ADC_Start、HAL_ADC_PollForConversion看起來繞了一大圈好像什么都沒干。但真正切換到 HAL 庫做 ADC 開發(fā)之后你會(huì)發(fā)現(xiàn)這套封裝的價(jià)值不在「少寫代碼」而在「把狀態(tài)機(jī)、DMA 觸發(fā)、中斷回調(diào)、多通道掃描和過采樣這些容易出錯(cuò)的細(xì)節(jié)收斂到一套統(tǒng)一的接口上」。這篇筆記圍繞一個(gè)具體目標(biāo)展開拋開圖形化配置工具的自動(dòng)生成搞清楚 HAL 庫 ADC 從初始化到拿到穩(wěn)定數(shù)值的完整鏈路——怎么配參數(shù)、怎么選阻塞/中斷/DMA、怎么處理校準(zhǔn)以及那些讓新手反復(fù)翻車的隱蔽細(xì)節(jié)。適合正在從寄存器或標(biāo)準(zhǔn)庫遷移、或者被 HAL 庫的 ADC 回調(diào)機(jī)制搞暈的開發(fā)者看完就能在本地把最小工程跑通并把采樣數(shù)值的可靠性調(diào)到夠用。2. 最小可跑的 ADC 工程從引腳到第一組電壓數(shù)據(jù)2.1 初始化到底初始化了什么時(shí)鐘、引腳與 ADC 實(shí)例HAL 庫的 ADC 初始化代碼看起來冗長但核心只有三件事給 ADC 外設(shè)供時(shí)鐘、把 GPIO 引腳配置成模擬輸入、把分辨率/對齊方式/掃描模式這些基本參數(shù)寫進(jìn)ADC_HandleTypeDef并調(diào)用HAL_ADC_Init。常見做法是在圖形化配置工具里選好引腳和時(shí)鐘樹再由工具生成MX_ADC1_Init不過很多開發(fā)者并不清楚這段生成代碼每一行在干什么后續(xù)排查問題時(shí)只能靠猜。先看一個(gè)最精簡的初始化函數(shù)以單 ADC 實(shí)例、單通道、右對齊、12 位分辨率為例void MX_ADC1_Init(void) { hadc1.Instance ADC1; // 選中 ADC1 外設(shè) hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; // ADC 時(shí)鐘 PCLK2 / 4 hadc1.Init.Resolution ADC_RESOLUTION_12B; // 12 位分辨率 hadc1.Init.ScanConvMode DISABLE; // 單通道模式不需要掃描 hadc1.Init.ContinuousConvMode ENABLE; // 連續(xù)轉(zhuǎn)換 hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_NONE; // 軟件觸發(fā) hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; // 右對齊 hadc1.Init.NbrOfConversion 1; // 轉(zhuǎn)換通道數(shù)量 hadc1.Init.DMAContinuousRequests DISABLE; hadc1.Init.EOCSelection ADC_EOC_SINGLE_CONV; // 每次轉(zhuǎn)換結(jié)束都產(chǎn)生 EOC if (HAL_ADC_Init(hadc1) ! HAL_OK) { Error_Handler(); } }這段配置里最容易被忽略的是ADC_CLOCK_SYNC_PCLK_DIV4這一項(xiàng)。ADC 的輸入時(shí)鐘不能超過芯片手冊規(guī)定的上限一般常見系列是 36MHz 或 40MHz 左右如果 PCLK2 是 72MHz 而你不分頻ADC 時(shí)鐘就超限了表現(xiàn)是轉(zhuǎn)換結(jié)果整體漂移而且沒有任何報(bào)錯(cuò)。Resolution選 12 位時(shí)單次轉(zhuǎn)換周期固定選 8 位或 6 位可以略微加快轉(zhuǎn)換速度但換來的是量化噪聲變大后面會(huì)細(xì)說。配置完hadc1之后還有一個(gè)隱形步驟HAL 庫會(huì)通過HAL_ADC_MspInit回調(diào)函數(shù)完成 GPIO 和時(shí)鐘的底層初始化。這段函數(shù)在圖形化工具生成代碼時(shí)是自動(dòng)出現(xiàn)的手動(dòng)建工程時(shí)往往要自己補(bǔ)void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_0; // 假設(shè)用 PA0 GPIO_InitStruct.Mode GPIO_MODE_ANALOG; // 模擬輸入 GPIO_InitStruct.Pull GPIO_NOPULL; // ADC 引腳通常不要上下拉 HAL_GPIO_Init(GPIOA, GPIO_InitStruct); }這里有個(gè)新手經(jīng)常踩的點(diǎn)GPIO 配置成模擬輸入時(shí)Pull一定要設(shè)成GPIO_NOPULL。如果內(nèi)部上拉被打開就等于在待測信號(hào)上并聯(lián)了一個(gè)幾十千歐的電阻分壓效應(yīng)會(huì)直接導(dǎo)致讀數(shù)偏低尤其被測源是高內(nèi)阻的時(shí)候誤差可能到百分之十幾。MspInit 這個(gè)名字看起來像某個(gè)庫函數(shù)內(nèi)部的東西但它是整個(gè)初始化鏈路里唯一負(fù)責(zé)「引腳復(fù)用」的地方項(xiàng)目里 ADC 換了引腳卻忘了同步改這里是極常見的翻車原因。2.2 阻塞式采集的完整代碼與返回值陷阱初始化做完最直觀的讀取方式是阻塞式發(fā)起一次轉(zhuǎn)換然后原地等待轉(zhuǎn)換結(jié)束再從數(shù)據(jù)寄存器拿數(shù)值。HAL 庫對應(yīng)的三個(gè)函數(shù)是HAL_ADC_Start、HAL_ADC_PollForConversion和HAL_ADC_GetValue。最小代碼如下uint32_t read_adc_once(void) { uint32_t adc_val 0; if (HAL_ADC_Start(hadc1) ! HAL_OK) { return 0xFFFFFFFF; // 啟動(dòng)失敗返回一個(gè)不可能出現(xiàn)的值 } // 等待轉(zhuǎn)換完成超時(shí) 100ms if (HAL_ADC_PollForConversion(hadc1, 100) ! HAL_OK) { return 0xFFFFFFFF; } adc_val HAL_ADC_GetValue(hadc1); // 讀取轉(zhuǎn)換結(jié)果 return adc_val; }這段代碼邏輯上是對的但實(shí)際項(xiàng)目里很少有人會(huì)每讀一次就調(diào)用一次Start。原因在于HAL_ADC_Start不只是「開始轉(zhuǎn)換」它還負(fù)責(zé)清狀態(tài)標(biāo)志位、復(fù)位 DMA 或中斷相關(guān)寄存器。頻繁調(diào)用會(huì)讓 ADC 的狀態(tài)機(jī)反復(fù)重啟如果發(fā)生在中斷里還有可能和其他讀操作沖突。常見做法是開機(jī)初始化后調(diào)用一次HAL_ADC_Start之后在循環(huán)里反復(fù)執(zhí)行PollForConversion和GetValue。PollForConversion的超時(shí)參數(shù)也有講究。阻塞模式里這個(gè)參數(shù)的單位是毫秒但它不是「轉(zhuǎn)換時(shí)間」而是「等待狀態(tài)翻轉(zhuǎn)的最長時(shí)間」。ADC 單次轉(zhuǎn)換通常只有幾微秒到幾十微秒100ms 的超時(shí)已經(jīng)非常寬裕。如果這里頻繁返回HAL_TIMEOUT先別急著加超時(shí)值——說明 ADC 可能根本沒有啟動(dòng)成功或者時(shí)鐘配置有誤。把超時(shí)加長只是掩蓋癥狀查時(shí)鐘和外設(shè)時(shí)鐘開關(guān)才是正路。另外注意返回值有三個(gè)層級(jí)HAL_OK表示轉(zhuǎn)換完成HAL_TIMEOUT表示超時(shí)HAL_ERROR或HAL_BUSY表示外設(shè)狀態(tài)不允許當(dāng)前操作。很多開發(fā)者只判斷HAL_OK和非HAL_OK導(dǎo)致HAL_BUSY被當(dāng)成超時(shí)處理方向一開始就偏了。2.3 電壓值換算前 200 次采樣要丟棄嗎拿到原始 ADC 數(shù)值之后下一步必然要換算成實(shí)際電壓。這個(gè)公式本身很簡單float voltage (float)adc_val * 3.3f / 4095.0f;但有幾個(gè)細(xì)節(jié)會(huì)直接影響換算精度。第一基準(zhǔn)電壓不一定是精確的 3.3V。開發(fā)板上的穩(wěn)壓芯片輸出往往有正負(fù) 1% 到 2% 的誤差講究一點(diǎn)的場合應(yīng)該用萬用表實(shí)測 VREF 引腳的電壓再把實(shí)測值代進(jìn)公式。第二GetValue返回的數(shù)據(jù)是右對齊的原始值12 位分辨率下范圍是 0 到 4095拿它乘 3.3 再除以 4096 還是 4095 會(huì)有千分位級(jí)別的誤差——通常無所謂但如果你在做校準(zhǔn)表建議統(tǒng)一用 4095 作為除數(shù)避免和芯片手冊里的滿量程定義產(chǎn)生歧義。第三剛上電的前幾次轉(zhuǎn)換結(jié)果往往偏大或偏小這不是芯片壞了而是 ADC 內(nèi)部采樣電容還沒穩(wěn)定。經(jīng)驗(yàn)做法是丟棄前 200 次采樣或者至少丟棄前幾十次再開始做濾波或平均——這個(gè)習(xí)慣在從待機(jī)模式喚醒后同樣適用。3. 中斷與 DMA把 CPU 從采樣循環(huán)里放出來3.1 三組啟動(dòng) API 到底有什么區(qū)別Start、Start_IT、Start_DMAHAL 庫給 ADC 提供了三組啟動(dòng)方式HAL_ADC_Start阻塞、HAL_ADC_Start_IT中斷、HAL_ADC_Start_DMADMA。同一個(gè) ADC 實(shí)例三組 API 背后的狀態(tài)機(jī)其實(shí)是同一個(gè)區(qū)別只在于「轉(zhuǎn)換結(jié)束誰來通知你」。用Start_IT時(shí)每次轉(zhuǎn)換完成會(huì)產(chǎn)生中斷CPU 在中斷服務(wù)函數(shù)里拿到數(shù)據(jù)。適合需要每轉(zhuǎn)換一次就立刻處理一次的場合比如過零檢測、快速均值濾波。DMA 方式則完全不打擾 CPUADC 轉(zhuǎn)換完成后由 DMA 把數(shù)據(jù)搬到內(nèi)存緩沖區(qū)CPU 可以干別的事等一批數(shù)據(jù)攢夠了再集中處理。三者選型的判斷標(biāo)準(zhǔn)很簡單——如果讀取頻率高于 1kHz 或者數(shù)據(jù)要連續(xù)采集用 DMA如果只是按鍵掃描、電池電壓監(jiān)控這類低頻任務(wù)阻塞式足夠別把簡單問題復(fù)雜化。下面把Start_IT的最小使用方式完整展示因?yàn)樗幕卣{(diào)機(jī)制是 HAL 庫新手最容易迷路的地方// 啟動(dòng)中斷方式采集每次轉(zhuǎn)換完成自動(dòng)進(jìn)入回調(diào) HAL_ADC_Start_IT(hadc1); // 這個(gè)回調(diào)函數(shù)由 HAL 庫在 ADC 中斷里自動(dòng)調(diào)用 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc-Instance ADC1) { raw_value HAL_ADC_GetValue(hadc1); // 讀取本次結(jié)果 HAL_ADC_Start_IT(hadc1); // 重新啟動(dòng)下一次轉(zhuǎn)換 } }注意回調(diào)結(jié)束前的那行HAL_ADC_Start_IT(hadc1)在非連續(xù)轉(zhuǎn)換模式下一次轉(zhuǎn)換完成后 ADC 會(huì)停下來必須重新調(diào)用啟動(dòng)函數(shù)才能觸發(fā)下一次轉(zhuǎn)換。很多人第一次用回調(diào)會(huì)發(fā)現(xiàn)「只進(jìn)一次回調(diào)后面再也不來了」原因就是這里漏了重啟。如果你的配置里把ContinuousConvMode設(shè)為ENABLE轉(zhuǎn)換會(huì)自動(dòng)連續(xù)進(jìn)行中斷也會(huì)持續(xù)觸發(fā)不需要重復(fù)調(diào)用啟動(dòng)——但那樣的話中斷頻率等于 ADC 轉(zhuǎn)換頻率CPU 可能在中斷里忙不過來反而不如 DMA 合適。Start_IT還有個(gè)隱患回調(diào)函數(shù)執(zhí)行期間時(shí)長不能超過一個(gè)轉(zhuǎn)換周期否則下一次轉(zhuǎn)換完成產(chǎn)生的同優(yōu)先級(jí)中斷會(huì)被掛起數(shù)據(jù)讀取會(huì)延遲。所以回調(diào)里不要做浮點(diǎn)換算、不要打印日志、不要做延時(shí)。只把原始數(shù)值存到一個(gè)全局變量或隊(duì)列里換算放到主循環(huán)。3.2 多通道掃描 DMA 循環(huán)模式緩存開多大、數(shù)據(jù)怎么對齊多通道采集在 HAL 庫里的寫法和單通道有一個(gè)明顯的分界點(diǎn)需要把ScanConvMode設(shè)為ENABLE并在ChannelConfig里按順序配置每個(gè)通道的 Rank轉(zhuǎn)換順序。DMA 模式下數(shù)據(jù)會(huì)按 Rank 的順序依次填入緩沖區(qū)這是最容易出錯(cuò)的環(huán)節(jié)——不是數(shù)值讀錯(cuò)了而是數(shù)值和通道對不上。以常見的四通道循環(huán)采樣為例配置如下ADC_ChannelConfTypeDef sConfig {0}; // 配置注入通道順序Rank 1 先轉(zhuǎn)換 sConfig.Channel ADC_CHANNEL_0; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_84CYCLES; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_1; sConfig.Rank 2; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_2; sConfig.Rank 3; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_3; sConfig.Rank 4; HAL_ADC_ConfigChannel(hadc1, sConfig);DMA 緩沖區(qū)和啟動(dòng)代碼#define ADC_CHANNEL_COUNT 4 #define ADC_BUF_SIZE 4000 // 4 個(gè)通道 × 1000 輪需要是通道數(shù)的整數(shù)倍 uint16_t adc_buffer[ADC_BUF_SIZE]; // DMA 傳輸目標(biāo) void adc_dma_start(void) { HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, ADC_BUF_SIZE); }這里特別強(qiáng)調(diào)ADC_BUF_SIZE必須是通道數(shù)的整數(shù)倍不是隨便寫個(gè) 1024 就完事。DMA 循環(huán)模式下緩沖區(qū)寫滿一輪會(huì)觸發(fā)一次傳輸完成中斷如果長度不是通道數(shù)的整數(shù)倍下一輪數(shù)據(jù)會(huì)從緩沖區(qū)的中間位置開始覆蓋導(dǎo)致后續(xù)只能用「每 N 個(gè)數(shù)據(jù)從 index 0 開始對通道」的錯(cuò)位方式處理很容易把通道 0 的數(shù)據(jù)當(dāng)成通道 1。在多通道 DMA 場景里通道數(shù)映射到緩沖區(qū)下標(biāo)是固定的index rank - 1 n × channel_count。每次 DMA 傳輸完成中斷里你從緩沖區(qū)頭部開始讀取一組channel_count個(gè)數(shù)據(jù)就是一輪完整的掃描結(jié)果。3.3 采樣頻率的邊界怎么算算錯(cuò)了會(huì)怎樣使用 DMA 連續(xù)采樣時(shí)采樣頻率不是隨便定的它由 ADC 時(shí)鐘和采樣時(shí)間共同決定。一個(gè)典型的計(jì)算過程假設(shè) ADC 時(shí)鐘 36MHz12 位分辨率需要 12.5 個(gè)時(shí)鐘周期的轉(zhuǎn)換時(shí)間采樣時(shí)間配 84 個(gè)時(shí)鐘周期那么單次轉(zhuǎn)換總周期數(shù)為12.5 84 96.5轉(zhuǎn)換頻率等于36MHz / 96.5 ≈ 373kHz。這個(gè)值是多通道掃描時(shí)所有通道共享的——如果你配了 4 個(gè)通道每個(gè)通道的實(shí)際采樣率要再除以 4約 93kHz。算這個(gè)數(shù)不是為了好看是決定 DMA 緩沖區(qū)和數(shù)據(jù)處理邏輯的起點(diǎn)。如果你的系統(tǒng)每 10ms 需要一組四通道數(shù)據(jù)那采樣率遠(yuǎn)高于需求可以考慮調(diào)低 ADC 時(shí)鐘分頻或減少采樣時(shí)間降低功耗。反過來如果采樣需求是每通道 200kHz四通道掃描總吞吐需要 800kHz而當(dāng)前配置只有 373kHz那唯一出路是縮短采樣時(shí)間或者降低分辨率或者改用多個(gè) ADC 實(shí)例并行工作——配參數(shù)之前先把這筆賬算清楚能避免后面調(diào)試時(shí)對著波形懷疑人生。4. HAL ADC 踩坑記錄校準(zhǔn)、采樣時(shí)間與狀態(tài)機(jī)玄學(xué)4.1 忘了校準(zhǔn)離譜的讀數(shù)偏差讓你懷疑芯片是壞的現(xiàn)象新板子第一次上電ADC 讀到的電壓比實(shí)測電壓偏低了接近 20%而且所有通道都一致偏低。用萬用表量引腳電壓明明是 2.0V讀回來只有 1.6V 左右。查了基準(zhǔn)、查了分壓電阻都沒問題。原因部分系列的 ADC 在復(fù)位后內(nèi)部偏移誤差沒有被修正必須在首次使用前執(zhí)行一次校準(zhǔn)。這個(gè)步驟在圖形化配置工具自動(dòng)生成代碼時(shí)通常是有的但如果你手動(dòng)裁剪過初始化流程或者用的是移植的工程很容易把校準(zhǔn)這段漏掉。校準(zhǔn)的本質(zhì)是把 ADC 內(nèi)部的電容陣列失調(diào)電壓測量出來并存儲(chǔ)到校準(zhǔn)寄存器不做這一步轉(zhuǎn)換結(jié)果會(huì)帶一個(gè)固定的偏移誤差。解決在HAL_ADC_Init之后、第一次啟動(dòng)轉(zhuǎn)換之前加一段校準(zhǔn)調(diào)用// ADC 初始化后、首次轉(zhuǎn)換前必須校準(zhǔn) HAL_ADCEx_Calibration_Start(hadc1, ADC_SINGLE_ENDED);注意校準(zhǔn)函數(shù)必須在 ADC 空閑狀態(tài)下調(diào)用不能在轉(zhuǎn)換中途執(zhí)行校準(zhǔn)完成后也不需要反復(fù)調(diào)用——同一顆芯片、同樣的電源條件下校準(zhǔn)一次就夠了。如果你的系統(tǒng)經(jīng)常會(huì)從低功耗模式喚醒建議喚醒后重新校準(zhǔn)因?yàn)榈凸哪J娇赡芮袛嗔?ADC 的內(nèi)部參考源導(dǎo)致校準(zhǔn)數(shù)據(jù)失效。4.2 采樣時(shí)間太短高阻抗信號(hào)源的讀數(shù)整體偏低現(xiàn)象把同一個(gè)傳感器分別接到兩個(gè)開發(fā)板一個(gè)讀數(shù)正常另一個(gè)整體偏低且波動(dòng)很大。代碼完全一樣唯一區(qū)別是傳感器引線長短和 PCB 走線寬度。原因ADC 內(nèi)部采樣電容需要在采樣時(shí)間內(nèi)充滿電充滿速度取決于外部源阻抗和電容的乘積。源阻抗越高需要的采樣時(shí)間越長。如果把SamplingTime配成最短檔位外部源阻抗又比較高采樣電容充不滿轉(zhuǎn)換結(jié)果自然偏低。這屬于用量程和分辨率查不出來的軟故障。解決把采樣時(shí)間從一個(gè)極端調(diào)到另一個(gè)極端做對照實(shí)驗(yàn)?zāi)茏钪庇^地確診。假設(shè)之前用的是 1.5 周期先換成ADC_SAMPLETIME_84CYCLES或更大值看讀數(shù)是否明顯上升。如果上升幅度超過 1%基本可以確認(rèn)是采樣時(shí)間不足。常見的經(jīng)驗(yàn)值直接測量運(yùn)放輸出這類低阻抗信號(hào)用短采樣時(shí)間沒問題直接測電位器、分壓電阻網(wǎng)絡(luò)、離子傳感器這類高阻抗源至少要用 84 周期以上。注意采樣時(shí)間加長會(huì)降低最高轉(zhuǎn)換頻率它是精度和速度的交換不是白拿的好處。4.3 連續(xù)轉(zhuǎn)換模式下調(diào)低分頻過采樣數(shù)據(jù)出現(xiàn)規(guī)律毛刺現(xiàn)象開啟連續(xù)轉(zhuǎn)換和 DMA 循環(huán)后當(dāng)把 ADC 時(shí)鐘從分頻 4 調(diào)成分頻 2即提高 ADC 時(shí)鐘頻率采集到的波形數(shù)據(jù)出現(xiàn)每 N 個(gè)點(diǎn)重復(fù)一次的毛刺頻率恰好和 PWM 中斷頻率一致。原因ADC 時(shí)鐘頻率提高之后單次轉(zhuǎn)換時(shí)間變短此時(shí)如果系統(tǒng)里同時(shí)存在高頻開關(guān)噪聲比如 PWM 驅(qū)動(dòng)、DC-DC 開關(guān)這些噪聲的頻譜會(huì)落入采樣帶寬內(nèi)混疊后表現(xiàn)為規(guī)律性毛刺。調(diào)低分頻前因?yàn)椴蓸訒r(shí)間較長等效于對高頻噪聲做了一定程度的平均掩蓋了這個(gè)問題。解決一是把采樣時(shí)間調(diào)回去恢復(fù)原本的低速高密室模式二是從時(shí)域上想辦法把 ADC 采樣時(shí)刻和 PWM 開關(guān)時(shí)刻錯(cuò)開——比如用定時(shí)器觸發(fā) ADC把觸發(fā)點(diǎn)放在 PWM 關(guān)斷的穩(wěn)定窗口。如果兩種方案都不可行就只能在軟件上做中值濾波或滑動(dòng)平均。這條坑的本質(zhì)是ADC 采樣時(shí)鐘不是越高越好尤其當(dāng)你的系統(tǒng)里還有其他開關(guān)噪聲源時(shí)過高的采樣時(shí)鐘只會(huì)讓噪聲混疊更嚴(yán)重。4.4 HAL_ADC_PollForConversion 返回 HAL_BUSY狀態(tài)機(jī)卡死現(xiàn)象代碼第一次調(diào)用HAL_ADC_Start后正常讀到了數(shù)據(jù)第二次再讀時(shí)PollForConversion直接返回HAL_BUSY而且之后再啟動(dòng)也始終是HAL_BUSY。重啟程序又能正常讀一次周而復(fù)始。原因HAL 庫的 ADC 狀態(tài)機(jī)里如果上一次轉(zhuǎn)換沒有正常結(jié)束比如你在轉(zhuǎn)換中途調(diào)用了HAL_ADC_Stop或者某次讀取超時(shí)后沒有把轉(zhuǎn)換序列徹底關(guān)掉ADC 會(huì)一直停在忙狀態(tài)。HAL_ADC_Start在忙狀態(tài)下不會(huì)重啟轉(zhuǎn)換而是返回HAL_BUSY。代碼里只對HAL_ADC_Start做了! HAL_OK判斷于是把這個(gè)錯(cuò)誤當(dāng)成了「啟動(dòng)失敗」沒有意識(shí)到是狀態(tài)機(jī)卡死。解決不要在PollForConversion超時(shí)之后直接進(jìn)入下一次讀取循環(huán)。超時(shí)之后應(yīng)該先調(diào)用HAL_ADC_Stop把轉(zhuǎn)換序列復(fù)位再重新 Start。更穩(wěn)妥的做法是在每次讀取完成后主動(dòng)調(diào)用HAL_ADC_Stop讓狀態(tài)機(jī)回到可再次啟動(dòng)的姿態(tài)。如果你是單通道低頻輪詢這個(gè)操作的開銷可以忽略如果是 DMA 循環(huán)模式不要隨便Stop否則 DMA 會(huì)停在一個(gè)半滿狀態(tài)緩存里殘留無效數(shù)據(jù)。4.5 多通道 DMA 數(shù)據(jù)錯(cuò)位緩存大小不是隨便填的現(xiàn)象四通道 DMA 采集緩沖長度設(shè)成 1000前 1000 個(gè)數(shù)據(jù)看起來通道順序正常之后每 1000 個(gè)數(shù)據(jù)里就有一次通道錯(cuò)位——通道 0 的數(shù)據(jù)出現(xiàn)在通道 1 的位置持續(xù)一小段后又自己恢復(fù)。原因DMA 循環(huán)模式和 ADC 的轉(zhuǎn)換序列之間存在對齊問題。ADC 每完成一輪四通道掃描會(huì)產(chǎn)生一次 DMA 請求DMA 把這 4 個(gè)數(shù)據(jù)搬入緩沖區(qū)。當(dāng)緩沖區(qū)寫滿時(shí)如果恰好寫到了某個(gè)通道的中間位置即緩沖區(qū)大小不是 4 的整數(shù)倍DMA 從頭開始覆蓋時(shí)通道和緩沖區(qū)的下標(biāo)對應(yīng)關(guān)系就亂了。之后每次滿緩沖區(qū)中斷時(shí)你按「每 4 個(gè)一組」切分?jǐn)?shù)據(jù)通道標(biāo)簽從中間開始錯(cuò)位直到湊巧再次對齊。解決緩沖區(qū)長度必須設(shè)成通道數(shù)的整數(shù)倍。實(shí)際項(xiàng)目里我更建議把緩沖區(qū)開成通道數(shù)的倍數(shù)乘以一個(gè)較大的輪數(shù)比如四通道 × 250 輪 1000這樣每次 DMA 傳輸完成中斷時(shí)緩沖區(qū)邊界天然落在某一輪轉(zhuǎn)換的結(jié)束位置。如果你需要更高頻率的實(shí)時(shí)處理可以把 DMA 設(shè)置成半傳輸中斷和數(shù)據(jù)傳輸完成中斷兩個(gè)回調(diào)這樣緩沖區(qū)可以做雙緩沖CPU 在 DMA 寫滿后半?yún)^(qū)時(shí)處理前半?yún)^(qū)數(shù)據(jù)不會(huì)互相覆蓋。5. 精度下限與驗(yàn)證三種實(shí)測手段與一處長期習(xí)慣HAL 庫不是精度問題的解藥它只是把「讀 ADC」這個(gè)動(dòng)作標(biāo)準(zhǔn)化了。真正決定你能從 ADC 里挖出多少有效位數(shù)的是布線、參考源和校準(zhǔn)這三個(gè)環(huán)節(jié)。這里分享三個(gè)我常用來驗(yàn)證 ADC 鏈路是否健康的實(shí)測手段。第一種短路測試。把 ADC 輸入引腳直接接地連續(xù)采樣 1000 個(gè)點(diǎn)理想情況下所有數(shù)值都是 0 或極少數(shù) 1。如果讀到的基數(shù)偏高且波動(dòng)明顯說明存在外部串?dāng)_或 PCB 漏電。再把引腳接到 VREF采樣值應(yīng)該在滿量程附近最大偏差不超過幾個(gè) LSB。這套測試不用任何精密儀器五分鐘就能定位大部分「數(shù)值漂移」問題。第二種分壓線性度測試。用一個(gè)精密電位器多圈可調(diào)那種別用碳膜微調(diào)從 VREF 到地搭一個(gè)分壓器中點(diǎn)在 ADC 引腳上。每旋轉(zhuǎn)一小格記錄一次讀數(shù)然后以實(shí)測電壓為橫軸、ADC 原始值為縱軸畫散點(diǎn)圖。理想情況下所有點(diǎn)應(yīng)該落在一條過原點(diǎn)的直線上。如果中間段有彎曲通常不是 ADC 的問題而是電位器負(fù)載效應(yīng)——這說明你的信號(hào)源阻抗太高需要縮短采樣時(shí)間或加一級(jí)運(yùn)放緩沖。第三種校準(zhǔn)前后對比。同一條件下讀一組數(shù)據(jù)執(zhí)行一次校準(zhǔn)再讀一組把前后均值差記錄下來。這個(gè)差值代表芯片出廠失調(diào)的真實(shí)幅度。你可以把這個(gè)數(shù)值寫進(jìn)配置文件的常量里以后換芯片批次的時(shí)候重新測一次就能通過簡單補(bǔ)償把失調(diào)誤差壓到最底。最后說一個(gè)我在多項(xiàng)目里養(yǎng)成的習(xí)慣ADC 相關(guān)的所有配置參數(shù)——采樣時(shí)間、分辨率、時(shí)鐘分頻、校準(zhǔn)狀態(tài)——全部集中到一個(gè)結(jié)構(gòu)體且在每個(gè)參數(shù)的旁邊加一行注釋寫明這個(gè)值是拍板定的依據(jù)是什么以及如果調(diào)大調(diào)小會(huì)犧牲什么。因?yàn)?ADC 的參數(shù)調(diào)節(jié)不像邏輯代碼那樣有明確的錯(cuò)誤與正確的分界它更像一組旋鈕你把采樣時(shí)間擰大了噪聲小了但吞吐低了你把分辨率從 12 位降到 8 位速度快了但精度沒了。過了兩個(gè)月回來看代碼如果沒有注釋你根本記不住當(dāng)時(shí)為什么在這組參數(shù)上停手。這也是我反復(fù)提醒團(tuán)隊(duì)里新人的一句話ADC 配置沒有標(biāo)準(zhǔn)答案只有可復(fù)現(xiàn)的權(quán)衡。參數(shù)定下來之前先用手段一和手段二把硬件底子驗(yàn)證一遍再談優(yōu)化——否則你優(yōu)化的可能是噪聲本身。希望這篇筆記能幫你在 HAL 庫的 ADC 鏈路上少走幾段冤枉路。本文還有配套的精品資源點(diǎn)擊獲取