實(shí)現(xiàn)16路ADC通道擴(kuò)展)
我們項(xiàng)目里有一塊要同時(shí)采 16 路模擬量的板子主控是 STM32F103片上 ADC 只有 10 個通道去掉內(nèi)部參考電壓和溫度傳感器之后真正能自由分配給外部信號的通道不夠用。當(dāng)時(shí)接了一堆電位器、溫度傳感器、分壓電阻網(wǎng)絡(luò)轉(zhuǎn)念一想最省事的辦法不是換大封裝 MCU也不是加獨(dú)立 ADC 芯片而是用一顆 CD74HC4067 模擬開關(guān)做通道擴(kuò)展。這顆芯片本質(zhì)上是一個 16 選 1 的模擬多路復(fù)用器通過 4 根地址線選通 16 個通道中的一路把信號送到公共端 COM再由 MCU 的 ADC 去采樣。就這一顆芯片把可用模擬輸入從 4 路直接變成了 16 路成本只有幾塊錢。排查過程中踩了兩個典型的調(diào)試坑一個是采樣建立時(shí)間不足導(dǎo)致的數(shù)據(jù)偏置另一個是地址切換毛刺導(dǎo)致的串?dāng)_讀數(shù)。這篇筆記把硬件連接、掃描固件和兩個坑的定位過程完整記錄下來給準(zhǔn)備用 MUX 擴(kuò)展 ADC 的讀者一個參照。1. 為什么要在方案里塞一顆多路開關(guān)CD74HC4067的定位1.1 內(nèi)置ADC不夠時(shí)有哪些擴(kuò)展方案MCU 內(nèi)置 ADC 通道不夠通常有三條路可以走。第一條是換芯片找 ADC 通道更多的型號。這條路的缺點(diǎn)是牽一發(fā)動全身引腳兼容性、封裝、成本、庫存全部要重新評估很可能只為了 4 個模擬量就換了整個平臺。第二條是外掛獨(dú)立 ADC比如 ADS1115 這種 16 位、I2C 接口的芯片分辨率高、接口省引腳但一片只提供 4 路。如果要做 16 路要么并 4 片要么選多通道版本BOM 成本直線上升I2C 總線上還要做地址區(qū)分軟件復(fù)雜度也上去。第三條就是本文用的方案保留 MCU 自身的 ADC前面加一顆 CD74HC4067用時(shí)間換通道。MCU 的 ADC 內(nèi)核、DMA、中斷、濾波邏輯全部不用動只是在每個采樣周期里多了一步“切換開關(guān)并等待信號穩(wěn)定”的動作。三條路放在一起對比如下方案成本通道擴(kuò)展能力信號帶寬軟件改動量換更多通道 MCU高受選型限制不損失大要遷移平臺外掛獨(dú)立 ADC中高按芯片顆數(shù)累加取決于接口速率中要寫驅(qū)動模擬開關(guān) 內(nèi)置 ADC低16路起步受開關(guān)導(dǎo)通電阻和電容限制小只加切換邏輯對于環(huán)境監(jiān)控這種低頻采樣場景溫度、濕度和電池電壓變化本身就慢每秒采幾十次已經(jīng)完全夠用通道切換帶來的時(shí)間開銷完全可以接受。1.2 CD74HC4067核心參數(shù)與選型理由CD74HC4067 是 TI 出品的 16 通道模擬多路復(fù)用器/多路分配器同類芯片還有 74HC4067、74HCT4067以及國產(chǎn)的 74HC4067 兼容型號引腳和功能基本一致。之所以選它而不是其他型號核心原因是 16 路、低成本、易采購、TTL 電平兼容。關(guān)鍵參數(shù)里最需要注意的是導(dǎo)通電阻 (R_{ON})。在 VCC 4.5V 到 5V 時(shí)典型值大約是幾十歐姆但 VCC 降到 3.3V 時(shí)(R_{ON}) 會明顯增大可能到 100 歐姆以上。這個電阻本身不會讓 ADC 讀數(shù)產(chǎn)生靜態(tài)誤差因?yàn)?ADC 輸入阻抗極高正常穩(wěn)態(tài)下幾乎沒有電流。但當(dāng) ADC 開始采樣時(shí)采樣電容要從信號源取電荷這時(shí) (R_{ON}) 就會和信號源內(nèi)阻、采樣電容一起決定電壓建立的快慢。這也是后面第一個調(diào)試坑的根源。另一個重要特性是 EN 引腳低電平有效。EN 0 時(shí)芯片正常工作A0~A3 決定哪一路開關(guān)閉合EN 1 時(shí)所有開關(guān)全部斷開芯片呈現(xiàn)高阻態(tài)。這個引腳很多人直接接地省事但在實(shí)際調(diào)試?yán)锼∏∈墙鉀Q地址毛刺問題的關(guān)鍵后面會專門講。供電方面CD74HC4067 的工作電壓范圍是 2V 到 6V信號電壓范圍是 0 到 VCC。設(shè)計(jì)時(shí)必須注意被采樣的信號不能超過 VCC也不能低于 GND否則內(nèi)部保護(hù)二極管會導(dǎo)通輕則采樣值被鉗位重則損壞芯片。2. CD74HC4067硬件搭建弄清楚COM、A0-A3和EN再接線2.1 引腳功能與選中邏輯CD74HC4067 的內(nèi)部結(jié)構(gòu)可以理解成一個單刀 16 擲開關(guān)加一個 4-16 譯碼器16 個通道分別命名為 IO0 到 IO15公共端叫做 COM另有 4 個地址引腳 A0、A1、A2、A3 和 1 個使能引腳 EN。地址線和通道的對應(yīng)關(guān)系就是二進(jìn)制編碼A0 是最低位A3 是最高位A3A2A1A0選通通道0000IO00001IO10010IO2...............1111IO15使用時(shí)把需要測量的 16 路信號分別接到 IO0 ~ IO15把 COM 引腳接到 MCU 的 ADC 輸入引腳。MCU 通過 GPIO 控制 A0~A3 的電平組合就能讓對應(yīng)的通道與 COM 導(dǎo)通ADC 采樣到的就是那個通道的電壓。模塊化的 CD74HC4067 板子現(xiàn)在也很常見上面一般把 IO0~IO15 做成插針或者接線端子COM、EN、A0~A3、VCC、GND 各有單獨(dú)引腳接線比較方便。如果是自己畫板建議在 VCC 和 GND 之間放一個 0.1uF 去耦電容位置盡量靠近芯片電源腳。2.2 參考電路與PCB走線經(jīng)驗(yàn)我實(shí)際接線的方式是這樣的16 路傳感器信號經(jīng)過各自的前端電路接到 IO0~IO15COM 引腳直接接 STM32 的 PA0作為 ADC 輸入A0、A1、A2、A3 接 PA1、PA2、PA3、PA4EN 接 PA5VCC 接 3.3VGND 共地信號0 ----- IO0 信號1 ----- IO1 ... 信號15 ----- IO15 | CD74HC4067 | COM ------ PA0 (ADC輸入) A0 ------ PA1 A1 ------ PA2 A2 ------ PA3 A3 ------ PA4 EN ------ PA5PCB 布局上有一條重要經(jīng)驗(yàn)?zāi)M通道線要盡量短而且要和數(shù)字地址線拉開距離。CD74HC4067 的地址線是數(shù)字信號切換瞬間會有沿跳變?nèi)绻湍M信號線平行走線幾厘米毛刺可以通過寄生電容耦合到模擬通道上。對于一個 ADC 精度只有 12 位或 10 位的系統(tǒng)這點(diǎn)耦合不一定致命但如果后面要追求更穩(wěn)定的讀數(shù)走線分離絕對值得做。COM 到 MCU 的走線同樣要短并且不要在中間額外串聯(lián)大電阻。如果板上干擾確實(shí)嚴(yán)重可以在 COM 到 GND 之間加一個 1nF 到 10nF 的電容和 (R_{ON}) 構(gòu)成低通濾波器把高頻毛刺濾掉。代價(jià)是信號建立時(shí)間變長需要相應(yīng)增加切換后的等待時(shí)間這是一對需要權(quán)衡的矛盾。3. 輪詢掃描固件先讓16路數(shù)據(jù)流動起來3.1 引腳與ADC初始化固件部分我用的是 STM32 HAL 庫。先把地址引腳和 EN 引腳配成推挽輸出初始狀態(tài)把 EN 拉高保證芯片上電后所有通道都處于斷開狀態(tài)避免上電瞬間誤采樣。GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // EN 初始為高斷開所有通道 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);ADC 配置成單通道單次轉(zhuǎn)換用起來最簡單。實(shí)際項(xiàng)目里如果后續(xù)要提升吞吐率可以改成掃描加 DMA但排錯階段單次轉(zhuǎn)換更清晰每讀一次都能和萬用表讀數(shù)對照。hadc.Instance ADC1; hadc.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc.Init.ScanConvMode DISABLE; hadc.Init.ContinuousConvMode DISABLE; hadc.Init.DiscontinuousConvMode DISABLE; hadc.Init.NbrOfConversion 1; HAL_ADC_Init(hadc); ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel ADC_CHANNEL_0; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(hadc, sConfig);這里我把采樣時(shí)間一開始就選得比較長239.5 個 ADC 時(shí)鐘周期。為什么這樣選第四部分會詳細(xì)算。3.2 掃描主循環(huán)先斷開再切地址后采樣掃描 16 路的基本邏輯是每次切換一個通道讀取 ADC然后切下一個通道。uint16_t adc_raw[16]; void Mux_Disable(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // EN1 全部斷開 } void Mux_Enable(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // EN0 開關(guān)有效 } void Mux_SetChannel(uint8_t ch) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4, (ch 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); // 實(shí)際寫多引腳時(shí)建議用 ODR 一次完成避免多次寫引腳造成的中間狀態(tài) // GPIOA-ODR (GPIOA-ODR 0xFFE1) | ((uint16_t)(ch 0x0F) 1); } uint16_t ReadMuxChannel(uint8_t ch) { Mux_Disable(); Mux_SetChannel(ch); delay_us(2); Mux_Enable(); delay_us(10); HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 100); return (uint16_t)HAL_ADC_GetValue(hadc); } void ScanAllChannels(void) { for (uint8_t i 0; i 16; i) { adc_raw[i] ReadMuxChannel(i); } }注意Mux_SetChannel里我寫了一段注釋建議用 ODR 直接寫入地址位而不是分 4 次調(diào)用HAL_GPIO_WritePin。原因在于如果 A0~A3 分開寫4 次操作之間會產(chǎn)生多個中間電平組合。比如要從通道 3 切到通道 7先改了 A0芯片會瞬間選中通道 2再改 A1 變成通道 0最后才穩(wěn)定到通道 7。雖然這段時(shí)間極短但芯片的開關(guān)狀態(tài)會跟著跳好幾下放在高頻采樣場景里就是隱患。一次性寫 ODR 可以讓地址線近乎同時(shí)變化盡量縮短中間態(tài)。4. 調(diào)試坑一切換通道后馬上啟動ADC采出來的值跟實(shí)際對不上4.1 現(xiàn)象讀數(shù)偏低還有相鄰?fù)ǖ赖挠白拥谝话婀碳镂覟榱藞D快切換通道后沒有做任何延時(shí)直接啟動 ADC。結(jié)果發(fā)現(xiàn)用萬用表測 IO3 引腳明明是 2.5VADC 讀出來只有 1.8V把 IO2 接到 3.3V、IO3 接 GND 時(shí)IO3 的讀數(shù)又變成了 0.6V 左右。更奇怪的是如果把 16 個通道全接上固定電壓按順序掃描前面幾個通道讀得還算準(zhǔn)越往后誤差越不穩(wěn)定像是所有通道都在互相干擾。這個現(xiàn)象很容易被誤判成“多路開關(guān)串?dāng)_嚴(yán)重”。但串?dāng)_通常指高頻信號通過寄生電容耦合這里信號全是直流電壓談不上高頻耦合問題實(shí)際上出在 ADC 采樣瞬間的電荷建立過程。4.2 原理采樣電容沒充滿就開始轉(zhuǎn)換了STM32 的 ADC 內(nèi)部有一個采樣保持電容開始轉(zhuǎn)換前會先接通模擬輸入給這個電容充電。充電時(shí)間就是寄存器里配置的 SamplingTime。在充電時(shí)間內(nèi)輸入信號要經(jīng)過信號源內(nèi)阻、CD74HC4067 的導(dǎo)通電阻 (R_{ON})、MCU 引腳內(nèi)部串聯(lián)電阻才能給采樣電容充上電。等效電路就是一個 RC 充電回路時(shí)間常數(shù)τ R_total × C_total R_total R_source R_ON R_ADC_in(內(nèi)阻) C_total C_mux_out C_ADC_sample C_parasitic假如信號源是一個 10kΩ 的分壓電阻網(wǎng)絡(luò)CD74HC4067 的 (R_{ON}) 在 3.3V 下大約 100ΩSTM32 內(nèi)部采樣開關(guān)等效電阻大約 1kΩ總的 R_total 就在 11kΩ 左右。再看電容CD74HC4067 輸出端電容、STM32 采樣電容加上 PCB 寄生電容大概 30pF 到 50pF。取 40pF 算τ ≈ 11kΩ × 40pF 440ns12 位 ADC 要穩(wěn)定到 1 個 LSB 以內(nèi)的誤差大約需要 8 到 9 個時(shí)間常數(shù)也就是t_settle ≈ 9 × 440ns ≈ 4us如果我把 ADC 采樣時(shí)間配成 1.5 個周期而 ADC 時(shí)鐘是 12MHz實(shí)際充電時(shí)間只有t_sample 1.5 / 12MHz ≈ 125ns125ns 只有 0.28 個時(shí)間常數(shù)采樣電容上的電壓只能充到目標(biāo)值的 25% 左右ADC 讀到的自然遠(yuǎn)遠(yuǎn)小于真實(shí)電壓。這就解釋了為什么結(jié)果“偏低”。同時(shí)CD74HC4067 切換后打開的那個開關(guān)相當(dāng)于把一個新電壓接到 COM 引腳而 COM 引腳和 ADC 采樣電容上都還殘留著上一個通道的電壓。如果上一個通道是 3.3V這次要采 0V初始狀態(tài)下電容是從 3.3V 開始放電充電時(shí)間不足就導(dǎo)致讀數(shù)偏正反過來上一個通道是 0V這次要采 3.3V讀數(shù)就偏負(fù)。這就是“相鄰?fù)ǖ赖挠白印钡膩碓幢举|(zhì)是殘余電荷沒有放完或充完。4.3 解決長采樣周期、延時(shí)和丟棄首樣本解決辦法有三個層次在實(shí)際項(xiàng)目中我最終用了前兩個組合。第一把 ADC 采樣時(shí)間調(diào)大。STM32F103 的采樣時(shí)間有 1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5 個周期這幾檔。直接用最大檔 239.5 個周期在 12MHz 時(shí)鐘下大約是 20us足夠讓前面的 RC 回路完全建立。代價(jià)是單次轉(zhuǎn)換時(shí)間變長但對低頻場景毫無影響。第二通道切換后延時(shí)。在代碼里我切完地址線、拉低 EN 之后又加了 10us 左右延時(shí)才啟動 ADC。這一步可以進(jìn)一步確保 COM 引腳上的模擬電壓完全穩(wěn)定同時(shí)把地址線毛刺消散的時(shí)間也算進(jìn)去。第三如果追求極致速度又不想每個通道都等那么久可以在代碼里切完通道后先做一次虛假轉(zhuǎn)換丟棄結(jié)果再用第二次轉(zhuǎn)換作為有效值。原理是第一次轉(zhuǎn)換期間采樣電容有足夠時(shí)間完成大部分的電荷補(bǔ)充雖然第一次結(jié)果不對但轉(zhuǎn)換結(jié)束后電容上的電壓已經(jīng)接近真實(shí)值緊接著第二次采樣就能采到比較準(zhǔn)的數(shù)據(jù)。// 丟棄式方案用更短的等待換更快的掃描 Mux_Enable(); delay_us(1); HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 100); HAL_ADC_GetValue(hadc); // 第一次結(jié)果丟棄 HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 100); adc_val HAL_ADC_GetValue(hadc); // 第二次取用當(dāng)時(shí)排查這個坑的時(shí)候我特意用示波器鉤住 COM 引腳觀察通道切換瞬間的波形可以看到一段指數(shù)上升過程從切通道到電壓穩(wěn)定花了大幾十微秒。這比我估算的 4us 大得多原因是我信號源內(nèi)阻實(shí)際還不止 10kΩ前面還串了濾波電阻。所以“延時(shí)寧多勿少”別卡著理論值剛好夠留足裕量。5. 調(diào)試坑二地址切換毛刺導(dǎo)致“搶讀”錯值需要把EN用起來5.1 現(xiàn)象數(shù)據(jù)偶爾跳到另一個通道的讀數(shù)建立時(shí)間的問題解決后數(shù)據(jù)大部分時(shí)間正常但偶爾會有那么一兩路數(shù)據(jù)“抽風(fēng)”直接從正常值跳成了另一個通道的讀數(shù)。出現(xiàn)頻率不固定有時(shí)候連續(xù)掃描幾百次都不出問題有時(shí)候連續(xù)出好幾個。我開始以為是電源干擾往 ADC 輸入加了 RC 濾波沒用又以為是濾波算法不夠強(qiáng)把滑動窗口拉大毛刺還是定期出現(xiàn)。直到某次我用邏輯分析儀同時(shí)抓了地址線 A0~A3 和 ADC 轉(zhuǎn)換完成標(biāo)志才發(fā)現(xiàn)問題不在模擬輸入而在地址切換的瞬間。5.2 原理地址線跳變時(shí)開關(guān)狀態(tài)不確定當(dāng)時(shí)我的代碼流程是先寫地址線然后開啟 ADC 轉(zhuǎn)換。執(zhí)行完地址寫入后我理所當(dāng)然地認(rèn)為通道已經(jīng)切好了但事實(shí)上CD74HC4067 內(nèi)部通過 4-16 譯碼器控制 16 個開關(guān)管地址線電平變化后譯碼輸出并不是瞬間完成狀態(tài)切換的內(nèi)部各種門電路存在傳播延遲。更關(guān)鍵的是在地址線跳變的極短瞬間如果舊通道對應(yīng)的開關(guān)還沒有完全關(guān)斷新通道對應(yīng)的開關(guān)又已經(jīng)開始導(dǎo)通就會出現(xiàn)兩個開關(guān)同時(shí)處于半導(dǎo)通狀態(tài)的窗口。這個窗口只有幾納秒到幾十納秒但足以讓 COM 引腳被兩個通道同時(shí)驅(qū)動。如果此時(shí) ADC 恰好在這個窗口內(nèi)啟動采樣采樣電容捕獲到的就是這個疊加后的不確定電壓而不是任何一路的準(zhǔn)確電壓。在多路復(fù)用器的數(shù)據(jù)手冊里這類開關(guān)通常標(biāo)注為“先斷后合”或“先合后斷”。如果芯片是“先合后斷”的時(shí)序在通道切換瞬間就會存在短暫的通道短路即使是“先斷后合”地址線和講碼器之間的延遲也可能造成一瞬間的懸空狀態(tài)讓 COM 引腳電壓漂移。無論哪種情況在通道切換過程中直接啟動 ADC 采樣都是危險(xiǎn)的。5.3 解決把EN用起來嚴(yán)格按照“斷開-切地址-接通-采樣”時(shí)序正確做法是切換地址線的過程中先用 EN 把所有開關(guān)全部斷開讓 COM 引腳懸空等地址線穩(wěn)定后再拉低 EN讓選中通道的開關(guān)閉合再等模擬信號建立起來最后才啟動 ADC。這樣做的本質(zhì)是給芯片提供一個“隔離期”。地址線怎么跳都無所謂因?yàn)?EN 1 時(shí)所有開關(guān)都不導(dǎo)通COM 引腳不受地址線變化影響等到地址線穩(wěn)定了再一次性把選中通道接通COM 上看到的就是一個干凈的跳變不會出現(xiàn)多個開關(guān)同時(shí)導(dǎo)通的混亂狀態(tài)。void SafeReadChannel(uint8_t ch) { Mux_Disable(); // 1. EN1斷開全部開關(guān)隔離地址毛刺 Mux_SetChannel(ch); // 2. 更新地址線 delay_us(2); // 3. 等待地址穩(wěn)定和毛刺消散 Mux_Enable(); // 4. EN0僅選中目標(biāo)通道 delay_us(10); // 5. 等待模擬電壓建立 HAL_ADC_Start(hadc); // 6. 開始采樣 HAL_ADC_PollForConversion(hadc, 100); adc_val HAL_ADC_GetValue(hadc); }這套流程里第 1 步是最關(guān)鍵的。如果沒有第 1 步即使后面延時(shí)再長都無法消除地址切換瞬間多個開關(guān)同時(shí)半導(dǎo)通的窗口有了第 1 步那個窗口就被徹底屏蔽了因?yàn)檫@時(shí)候所有開關(guān)都被 EN 關(guān)斷譯碼器的輸出狀態(tài)不會造成任何物理通路。如果硬件上已經(jīng)把 EN 接地那這個時(shí)序改進(jìn)就用不了。所以設(shè)計(jì)階段一定要把 EN 引到 MCU 的 GPIO不要直接接地。一個 GPIO 引腳的代價(jià)換來的是整個掃描過程的可控性非常值得。同樣重要的是地址線更新那一步最好一次性寫入 ODR而不是分四次寫引腳。分四次寫導(dǎo)致 A0~A3 之間可能出現(xiàn)多個中間組合即使 EN 保持?jǐn)嚅_也會有更多不必要的內(nèi)部狀態(tài)跳變增加毛刺和功耗。一次性寫完之后再用一個短延時(shí)把地址線的電平穩(wěn)定下來。6. 數(shù)據(jù)優(yōu)化和標(biāo)定濾波與校準(zhǔn)讓16個通道真正可用6.1 簡單滑動平均去抖解決了建立時(shí)間和通道切換毛刺之后16 路數(shù)據(jù)已經(jīng)基本穩(wěn)定但 ADC 本身還有量化噪聲和外部的輕微干擾單次采樣的抖動依然存在。我在每個通道上做了滑動平均每路連續(xù)采 5 次去掉最大值和最小值再把中間 3 次取平均。這種方式對脈沖噪聲有很強(qiáng)的抑制能力又不會像單純平均那樣被一個極端值帶偏。#define FILTER_N 5 uint16_t AdcFilterValue(uint16_t *buf, uint8_t n) { uint32_t sum 0; uint16_t min buf[0]; uint16_t max buf[0]; for (uint8_t i 0; i n; i) { if (buf[i] min) min buf[i]; if (buf[i] max) max buf[i]; sum buf[i]; } sum - min; sum - max; return (uint16_t)(sum / (n - 2)); }每路采 5 次、中間 3 次取平均16 路算下來每次全掃描要執(zhí)行 80 次 ADC 轉(zhuǎn)換。如果一次轉(zhuǎn)換加切換延時(shí)約 100us全掃描一輪約 8ms也就是 125Hz 的全幀率單通道更新率約 7.8Hz完全滿足溫度、電壓監(jiān)控類應(yīng)用。6.2 零點(diǎn)、跨度和三點(diǎn)校準(zhǔn)16 路模擬量還有一個繞不開的問題每一路的前端電路都不可能完全一致比如電阻分壓的精度、傳感器供電的微小差異、CD74HC4067 不同通道之間 (R_{ON}) 的離散性都會讓同一電壓在不同通道上讀出不同的原始值。最實(shí)用的做法是線性校準(zhǔn)用兩點(diǎn)或者三點(diǎn)建立“原始 ADC 值到實(shí)際物理量”的映射。很多人的做法是只做零點(diǎn)和滿量程兩點(diǎn)校準(zhǔn)但我建議有條件的話做三點(diǎn)校準(zhǔn)。兩點(diǎn)校準(zhǔn)假設(shè) ADC 特性是一條完美直線但實(shí)際 ADC 的中段誤差和兩端誤差往往不一致三點(diǎn)校準(zhǔn)零點(diǎn)、中值點(diǎn)、滿量程點(diǎn)可以做分段線性插值把中段的非線性誤差也修正掉。具體的做法是給采樣端輸入三個已知電壓記錄下每個通道對應(yīng)的 ADC 原始值然后在代碼里建一個查找表用線性插值把任意原始值映射到實(shí)際電壓。16 個通道存 16 組系數(shù)每組 3 個點(diǎn)內(nèi)存占用很小但校準(zhǔn)效果立竿見影。6.3 關(guān)于采樣率、緩沖和后續(xù)擴(kuò)展的收尾建議很多人拿到這類型擴(kuò)展方案后第一反應(yīng)是“能不能提高掃描速度”。CD74HC4067 本身開關(guān)速度是納秒級的瓶頸并不在芯片上而在 ADC 采樣建立時(shí)間上。如果確實(shí)要提速有幾個思路可以組合使用。第一個思路是縮短信號源內(nèi)阻。盡量降低每一個通道前端的串聯(lián)電阻信號源內(nèi)阻從 10kΩ 降到 1kΩ充電時(shí)間常數(shù)直接下降一個數(shù)量級。第二個思路是在 COM 和 ADC 之間加一級運(yùn)放緩沖。運(yùn)放輸入高阻輸出低阻把 CD74HC4067 的 (R_{ON}) 和 ADC 采樣電容隔離開這樣 ADC 采樣時(shí)從運(yùn)放輸出端取電荷運(yùn)放輸出端能提供足夠大的電流采樣電容在極短時(shí)間內(nèi)就能充滿。這是所有方案里效果提升最大的一種代價(jià)是增加一顆運(yùn)放的成本。第三個思路是并行使用兩顆 CD74HC4067分成兩組各 8 路分別接 ADC1 和 ADC2用 DMA 同時(shí)采集兩路。這樣全通道掃描時(shí)間幾乎減半但軟件復(fù)雜度會明顯上升。在我的實(shí)際項(xiàng)目里最終是“CD74HC4067 運(yùn)放緩沖 每路 5 次濾波 三點(diǎn)校準(zhǔn)”的組合16 路傳感器數(shù)據(jù)全部穩(wěn)定在 ±2mV 以內(nèi)對于電池電壓、環(huán)境溫度、位置電位器這類信號精度完全夠用。如果只是玩玩低速率采集不追求極限精度直接按本文的“斷開-切地址-接通-延時(shí)-采樣”流程配合較長 ADC 采樣周期就已經(jīng)能解決絕大多數(shù)問題了。