:從寄存器配置到濾波校準)
簡介面向嵌入式初學者的MC9S12DG128 AD采集實驗工程圍繞飛思卡爾S12系列單片機片上ATD模塊演示多通道8位高速采樣、數據轉換與12864液晶實時顯示適合學習CodeWarrior環(huán)境下外設驅動開發(fā)也適用于工業(yè)控制、汽車電子等需要模擬量采集的場景。壓縮包共79個文件以cmd調試命令、prm鏈接參數、map映射、s19燒錄文件和c源碼為主另有o目標文件、ini工程配置、mcp工程文件等涵蓋PE、TBDML、SofTec等常見調試器配置整體僅432KB便于快速下載。已有131人學習瀏覽。工程內含完整CodeWarrior項目可直接打開運行或對照閱讀幫助理解AD轉換器初始化、采樣觸發(fā)、中斷讀取與LCD顯示驅動的配合方式同時多套調試腳本和鏈接參數可作為模板遷移到類似HCS12項目中使用。1. MC9S12DG128的AD實驗讀電位器誰都會難在把它當成可靠數據源MC9S12DG128的AD實驗在很多開發(fā)板上是第一道“外設入門”關擰一個電位器在數碼管或者串口上看到 01023 的變化就宣布自己學會 AD 了??烧娴阶鲰椖繒r這塊芯片的 ATD 模塊遠比表面復雜——參考電壓怎么接、ATD 時鐘分到多少兆、通道之間會不會串擾、第一次采樣為什么永遠不對這些才是真正決定你能不能把模擬量交給控制邏輯去用的關鍵。這篇文章我按做這顆芯片的常見路徑走一遍從寄存器原理講到最小驅動代碼再講到多通道掃描、濾波和校準最后把最容易翻車的幾個現象按“現象→原因→解決”拆開。適合手里有這塊芯片的開發(fā)板、想繞過一堆舊資料里的坑、把 AD 實驗直接做實的人。2. 先認識 ATD 模塊參考電壓、ATD 時鐘與采樣時間三個參數決定成敗2.1 ATD0 和 ATD1、AN0 到 AN7引腳怎么找和哪些外設共用MC9S12DG128 內部有兩個獨立的 ADC 模塊分別叫 ATD0 和 ATD1每個模塊最多支持 8 個模擬輸入通道對應到芯片引腳上通常標成 AN0AN7 和 AN8AN15也有板子直接復用 AD0AD7 的絲印。多數實驗板只把 ATD0 的 8 個通道引出來因為 ATD0 的引腳排布更靠近電位器、傳感器插座而 ATD1 的引腳往往和普通 IO、串口、PWM 復用在同一個管腳上布局上不夠友好。所以新手做實驗優(yōu)先找 ATD0 對應的一組引腳別一上來就對著芯片管腳圖找“AD”字樣。找引腳有個非常實用的辦法先看板子原理圖找電位器或者傳感器輸出接到了哪個管腳那個管腳名字里帶 AN、AD、PAD 字樣基本就是模擬輸入。如果手頭沒有原理圖就看開發(fā)板絲印很多板子會在電位器旁邊標注 AD0 或 AN0。找不到絲印時再用萬用表蜂鳴檔量電位器中間抽頭連到芯片的哪個管腳這個方法雖然土但比對著幾十頁管腳表猜要快得多。還有一個非常容易踩的共用問題這些模擬輸入引腳在上電復位后是普通 IO要等到你配置完 ATD 寄存器才會切到模擬功能。如果同一個引腳上還接了按鍵、LED、數碼管段選之類的數字器件測量結果會變成“數字電平和模擬電壓疊加”的奇怪值而不是單純的傳感器電壓。我見過不止一個同學在 AN7 上接了一個開關然后死活想不通為什么讀數只有 0 和 1023 兩個值。所以配置模擬通道之前先確認這個引腳沒有被其他外設占用。2.2 VRH 與 VRL 基準為什么 5V 供電不等于 5V 參考ATD 模塊并不是拿芯片的 VDD 當作滿量程參考它使用的是獨立的參考電壓引腳 VRH 和 VRL。多數實驗板上 VRH 直接接 5V、VRL 接 GND所以你在軟件里換算電壓時默認滿量程是 5.0V這沒問題。但要注意VRH 上的電壓紋波、毛刺會直接進入轉換結果的最高位數字電源的開關噪聲會通過這個引腳耦合進 ADC導致數據在某個固定值附近來回跳。如果你的應用只有一顆芯片和一個電位器直接共用 5V 數字電源也可以但要在 VRH 引腳旁邊放一個 0.1μF 陶瓷電容走線盡量短這是成本最低的改善手段。更講究一點的做法是用一顆低噪聲穩(wěn)壓芯片單獨給 VRH 供電模擬地和數字地單點連接。做項目時模擬部分和數字部分的地如果混在一起采樣值的抖動會明顯到用肉眼都能看出來。判斷是不是基準問題很簡單把輸入通道短接到 GND讀到的應該接近 0再把輸入短接到 VRH讀到的應該接近滿量程。如果這兩個數都不穩(wěn)定說明基準或者地線有問題不是軟件濾波能救回來的。2.3 ATD 時鐘分頻與采樣時間一張表算出一次轉換的真實耗時ATD 模塊有一個內部轉換時鐘它是由總線時鐘分頻得到的公式是ATD 時鐘 總線時鐘 / (2 × (PRS 1))PRS 是 ATD0CTL4 寄存器低 4 位的值范圍 015。芯片手冊里通常要求 ATD 時鐘落在 0.5MHz2MHz 之間太高轉換結果不可靠太低轉換速度沒法看。實際調板子時我一般把 ATD 時鐘壓在 1MHz2MHz 之間這樣既穩(wěn)定速度也夠用。很多開發(fā)板的總線時鐘是 24MHz在這個條件下不同 PRS 的典型結果如下PRS 值分頻系數ATD 時鐘結果0212MHz嚴重超規(guī)格不可用264MHz偏高容易跳動5122MHz處于上限能用7161.5MHz推薦1532750kHz偏保守但很穩(wěn)采樣時間由 ATD0CTL4 里的采樣時間位控制常見的設置有 2 個 ATD 時鐘周期和 4 個 ATD 時鐘周期兩種。采樣時間短適合信號源阻抗低的場景信號源阻抗高比如直接用一個大電阻串到 ADC 引腳時就要用更長的采樣時間否則內部采樣電容還沒充滿就進入逐次比較讀出來的值會系統(tǒng)性偏低。拿一個 10kΩ 電阻和 0.1μF 電容組成的傳感器電路來舉例2 個周期的采樣時間很可能不夠換成 4 個周期之后讀數立刻穩(wěn)定。這個參數不需要頻繁改但遇到“怎么采都是偏小”的怪異現象時優(yōu)先懷疑采樣時間。3. 最小驅動代碼寄存器初始化、輪詢讀取與電壓換算3.1 初始化序列ATD0CTL2 到 ATD0CTL5 一次寫對看別人工程的初始化代碼會發(fā)現 ATD 這部分通常只有四五行寄存器賦值但順序和值都有講究。我的習慣是先上電、再配置模式、最后選通道中間留一個上電穩(wěn)定延時。void AD_Delay(unsigned long count) { while (count--) /* 簡單的軟件延時用于ATD模塊上電穩(wěn)定 */ ; } void ATD_Init(void) { ATD0CTL2 0xC0; /* ADPU1 模塊上電AFFC1 讀結果寄存器后自動清完成標志 */ AD_Delay(100000); /* 給模塊上電和內部基準留出幾毫秒穩(wěn)定時間 */ ATD0CTL3 0x00; /* 轉換序列長度取最短單次只出一個結果結果放在DR0 */ ATD0CTL4 0x83; /* PRS3ATD時鐘約1.5MHz采樣時間取較長的一檔 */ ATD0CTL5 0x00; /* 單通道模式選中AN0寫入同時觸發(fā)第一次轉換 */ }這段代碼里ATD0CTL2 的 bit6 也就是 AFFC 位很關鍵。不設置它讀完結果寄存器后必須手動去清完成標志否則下一次循環(huán)會拿到上一次的舊數據設置之后只要讀一次結果寄存器硬件自動把標志位清掉代碼會簡潔很多。ATD0CTL3 控制轉換序列長度0x00 是最短序列對只讀一個通道來說已經足夠。ATD0CTL4 的低 4 位是分頻值這里用 3配合 24MHz 總線時鐘算出來的 ATD 時鐘是 1.5MHz比較居中。ATD0CTL5 是啟動和通道選擇的關鍵重新寫這個寄存器會觸發(fā)一輪新的轉換這也是接下來的讀取函數能反復調用的原因。上電穩(wěn)定延時不能省。ATD 模塊剛上電時內部采樣電容和參考電壓網絡還沒有建立好立即啟動轉換容易把第一輪數據弄成 0 或滿量程。這個延時不需要精確到毫秒級一個足夠長的空循環(huán)就行但不要把它寫成“先啟動轉換再延時”的順序。3.2 單通道輪詢讀取一個 ATD_ReadChannel 函數解決 90% 的需求初始化完成之后讀取單個通道的邏輯非常簡單選通道、啟動轉換、等待完成標志、讀結果。下面這個函數適配 AN0AN7 任意單通道。unsigned int ATD_ReadChannel(unsigned char channel) { unsigned int value; ATD0CTL5 channel 0x07; /* 低3位選擇AN0~AN7寫入即觸發(fā)新轉換 */ while ((ATD0STAT0 0x80) 0) /* CCF0置1表示第一個結果已經出來 */ ; value ATD0DR0; /* AFFC模式下讀操作同時清掉CCF0標志 */ return value; }調用方式很直接raw ATD_ReadChannel(0);就能拿到 AN0 的原始值。這里channel 0x07是為了防止傳入大于 7 的通道號雖然寫越界值也能被寄存器吞掉但會選中一個你根本沒用到的內部通道。while 循環(huán)等待的是 CCF0 標志也就是結果寄存器 0 的轉換完成位單通道模式下結果一定放在 ATD0DR0 里。有一點要提醒這個函數是輪詢方式轉換期間 CPU 會卡在循環(huán)里。對大多數實驗場景這不是問題比如讀溫度、讀電壓、讀電位器速度要求都不高。但如果某個項目的主循環(huán)里還有實時性很強的任務就不要在循環(huán)里頻繁調用這個函數等待可以用中斷方式這部分我們放到多通道章節(jié)里講。3.3 原始值到真實電壓除法、偏移和取整的寫法ATD 結果寄存器里的是一個無量綱的原始值。10 位分辨率下滿量程是 1024 個碼也就是 01023對應的電壓范圍是 VRH 和 VRL 之間的電壓差。如果 VRH 接 5.0V、VRL 接地真實電壓就是原始值 × 5.0 / 1024。實際工程里我建議直接輸出毫伏而不是輸出“0.823456V”這種高精度假象。因為 10 位的分辨率換算成電壓一位碼大約對應 4.88mV把結果寫成七八位小數沒有任何工程意義。unsigned int ATD_ReadMillivolt(unsigned char channel) { unsigned int raw; unsigned long mv; raw ATD_ReadChannel(channel); mv (unsigned long)raw * 5000UL; /* 假設VRH5.0V先放大到毫伏 */ mv mv 10; /* 右移10位等價于除以1024 */ return (unsigned int)mv; }這里有個容易寫錯的小地方很多人會除以 1023其實對 10 位 ADC 來說滿量程值 1023 對應的是“無限接近參考電壓”而不是等于參考電壓。校準和換算時用 1024 做分母數學上更干凈也避免出現除以 1023 后最大讀數算出 5000.xx mV 的尷尬。如果你用的參考電壓不是標準 5V把代碼里的 5000 換成實際量到的毫伏數即可。4. 多通道掃描與連續(xù)轉換把采樣變成一套穩(wěn)定機制4.1 多通道順序掃描的配置MULT 與序列長度的組合一個項目里通常不會只采一路模擬量電機電流一路、電池電壓一路、電位器一路這時候就要用多通道掃描。ATD 模塊的 MULT 位用來開啟多通道模式配合轉換序列長度可以實現“一次啟動自動把 AN0 到 AN7 按順序采一遍”。void ATD_MultiScan_Init(void) { ATD0CTL2 0xC0; /* 上電 AFFC和單通道模式保持一致 */ AD_Delay(100000); ATD0CTL3 0x07; /* 序列長度設為8對應8個通道輪詢一次 */ ATD0CTL4 0x83; /* PRS3采樣時間與單通道時相同 */ ATD0CTL5 0x30; /* MULT1從AN0開始單次掃描8個通道 */ }啟動掃描之后轉換結果按順序自動填入 ATD0DR0ATD0DR7。也就是說AN0 的結果在 DR0AN1 的結果在 DR1依此類推。讀取時先判斷序列完成標志再一口氣把 8 個結果全部搬走。注意ATD0CTL3 的低 4 位序列長度字段我這里的 0x07 對應 8 個轉換這是按“長度 字段值 1”的規(guī)則來的。網上有些工程里寫 0x08 甚至 0x10那是因為不同版本的芯片資料對這個字段的表述有差異你在自己板子上以官方寄存器表為準不必照抄陌生代碼。多通道模式下AFFC 的優(yōu)勢更明顯如果不啟用 AFFC讀一個結果就要手動清一次標志8 個通道加序列標志位代碼量翻倍還容易漏。開了 AFFC讀 DR 寄存器就順帶清了對應標志最后檢查 SCF 序列完成標志即可。4.2 結果寄存器的順序對齊D0 到 D7 與 AN0 到 AN7 的映射讀取掃描結果的代碼如下void ATD_ReadMulti(unsigned int buffer[8]) { unsigned char i; ATD0CTL5 0x30; /* 重新寫入CTL5觸發(fā)一輪新的多通道掃描 */ while ((ATD0STAT1 0x80) 0) /* SCF置1表示整輪序列轉換完成 */ ; for (i 0; i 8; i) buffer[i] ATD0DR[i]; /* DR0對應AN0DR1對應AN1以此類推 */ }這里最容易翻車的地方是結果對應關系。有些開發(fā)板的絲印把通道標成 AD0、AD1和芯片內部的 DR0、DR1 在順序上一致但也有板子中間插了別的硬件跳線導致 AN2 和 AN3 在 PCB 上互換過位置。不要依賴絲印名字直接拿一根杜邦線把 AN0 接到一個固定電壓比如 3.3V然后看程序里哪一個 buffer 元素讀出來是滿量程附近用這個實測結果做一張通道映射表再寫進代碼注釋里。4.3 中斷方式收結果比輪詢更快、比 DMA 更簡單的中間選項輪詢方式在單通道實驗里沒有問題但多通道掃描時 CPU 會空轉等待整輪轉換完成這段時間雖然不長在控制類程序里卻很可能耽誤事。中斷方式是一個更好的中間選項ATD 轉換完成時觸發(fā)中斷在中斷服務程序里把結果讀走主循環(huán)完全不用等待。void ATD_Interrupt_Init(void) { ATD0CTL2 0xE0; /* ADPU1AFFC1ADIE1 使能轉換完成中斷 */ AD_Delay(100000); ATD0CTL3 0x07; ATD0CTL4 0x83; ATD0CTL5 0x30; /* 啟動多通道掃描 */ ATD0STAT0 0x00; /* 清掉殘留的完成標志 */ } #pragma CODE_SEG __NEAR_SEG NON_BANKED void ATD0_ISR(void) { unsigned char i; for (i 0; i 8; i) sample[i] ATD0DR[i]; /* 中斷里只搬數據不做浮點運算 */ ATD0STAT1 0x80; /* 清序列完成標志SCF */ } #pragma CODE_SEG DEFAULT中斷服務程序里要注意三個習慣。第一只做數據搬運和標志清理不要做電壓換算、濾波、顯示這類耗時操作否則中斷時間過長會阻塞其他中斷。第二讀取 DR 寄存器之后要確認標志確實被清了如果用的是非 AFFC 模式必須手動寫 ATD0STAT1 清 SCF否則會反復進入中斷。第三中斷服務函數名和中斷向量表的對應關系由開發(fā)環(huán)境決定移植代碼時先查一下官方的中斷向量配置不要直接復制網上不同芯片工程的函數名。5. AD 實驗常見問題排查跳變、串擾、首測失效與毛刺5.1 現象采樣值整體偏高或偏低還是無規(guī)律跳動現象一把輸入接到一個精確的 2.5V 電壓上讀出來一直在 2.47V 到 2.53V 之間晃現象二讀出來穩(wěn)定在 2.45V用萬用表量卻是 2.50V。兩種現象的根因完全不同。先看整體偏移。最常見原因是 VRH 實際電壓不是 5V而是開發(fā)板上某個穩(wěn)壓器輸出的 4.9V 或者 5.05V。你按 5.0V 換算電壓自然整體偏移。解決方法是把 VRH 引腳的電壓用萬用表實測然后把換算代碼里的 5000 改成實際毫伏數。還有一種偏移是因為信號源內阻太大采樣時間不足導致采樣電容沒充滿結果會比真實值偏低。再看無規(guī)律跳動。跳動通常和地線有關特別是電烙鐵焊接的杜邦線、面包板長跳線都會引入幾十毫伏的噪聲。解決方法是縮短模擬信號線長度、使用帶屏蔽的線、在 ADC 引腳和 GND 之間加 0.1μF 電容。不要把鍋都甩給“芯片精度不夠”這塊 10 位 ADC 的靜態(tài)精度沒這么差多數跳動是外部布局造成的。5.2 現象改讀 AN1AN0 的讀數跟著被拉過去實驗板上有兩個電位器分別接 AN0 和 AN1。單獨讀 AN0 時正常單獨讀 AN1 也正常但只要快速交替讀兩個通道兩個讀數都會變得不準甚至 AN0 的值明顯偏向 AN1。這個現象叫通道串擾根因有兩個層面。第一芯片內部采樣電容在轉換完 AN1 之后還殘留一部分 AN1 的電荷立刻去采 AN0 時殘留電荷還沒來得及釋放就被并入了 AN0 的采樣結果。第二外部電路上兩個通道的引腳距離很近PCB 或者說杜邦線之間存在寄生電容高阻抗的傳感器信號很容易被相鄰通道耦合。解決通道串擾最有效的辦法是放慢切換節(jié)奏。多通道掃描時序列長度已經給足的話芯片自己就帶了“先復位采樣電容再采樣”的機制問題往往出現在你自己寫的輪流調用單通道讀取函數、切換速度非??斓那闆r下。另一種做法是硬件上在每個通道引腳串一個幾百歐姆的電阻限制寄生電容的沖擊電流效果立竿見影。如果最終讀數還是要用于精確控制還有一個軟件補救每個通道切換后丟第一次采樣結果用第二次以后的數據代價是多花一點轉換時間。5.3 現象板子上電后第一次轉換結果永遠是 0x0000 或滿量程很多人在主循環(huán)里一開始就調用 ATD_ReadChannel結果發(fā)現程序跑起來后第一次數據要么是 0 要么是 1023之后的數據完全正常。這個問題出現得極其規(guī)律基本可以斷定是上電時序問題。原因有兩類。第一類是 ATD 模塊上電后沒有留穩(wěn)定時間ADPU 位置 1 只是打開電源模塊內部的偏置電路和參考電壓建立需要一段時間在這個期間啟動轉換得到的就是無效值。第二類是外部傳感器或電位器的供電還沒穩(wěn)定芯片已經開始采樣采到的是電源爬升過程的中間值。解決方法是約定俗成的初始化之后先做一次“空轉采樣”把結果直接丟棄再用后面輸出的數據。如果連丟一次都覺得不夠穩(wěn)就做兩次空轉。這不算浪費因為 ADC 的建立時間本來就是硬件特性不是代碼能靠邏輯繞過去的。5.4 現象加了濾波還是抖問題出在結果寄存器被重復讀有人寫代碼時為了取平均值在同一個轉換序列結束前反復讀同一個結果寄存器以為能拿到多個不同時刻的采樣值。實際結果可能是濾波之后依舊抖動甚至抖動更厲害。原因在于沒有清完成標志就重復讀寄存器。AFFC 模式下讀 DR0 的首次操作會清掉 CCF0第二次讀同一個寄存器時轉換序列并沒有真正產生新數據你讀到的只是 CPU 某個緩存或寄存器里殘留的舊值。這時候做的“平均”實際上是把同一個數加了 N 次再除以 N等于沒做。正確做法是把讀到的原始值存進一個數組通過數組做滑動平均或去極值平均而不是重復讀寄存器。6. 讓 AD 實驗能上項目滑動平均濾波與兩點校準的十行收尾最后這個階段把實驗代碼往工程上收。濾波和校準這兩個環(huán)節(jié)是實驗代碼和量產代碼之間最常見的分水嶺。濾波我推薦用去極值滑動平均原理簡單、占用資源小每輪連續(xù)讀 5 個原始值去掉最大值和最小值剩下三個取平均。這種濾波方式對尖峰脈沖免疫又不像純滑動平均那樣滯后嚴重。unsigned int ATD_FilteredRead(unsigned char channel) { unsigned int sample[5]; unsigned long sum 0; unsigned char i; unsigned int max, min; for (i 0; i 5; i) sample[i] ATD_ReadChannel(channel); /* 連續(xù)讀5次 */ max sample[0]; min sample[0]; for (i 0; i 5; i) { if (sample[i] max) max sample[i]; if (sample[i] min) min sample[i]; } for (i 0; i 5; i) sum sample[i]; sum - max min; /* 去掉一個最高和一個最低 */ return (unsigned int)(sum / 3); }校準更值得做。直接用原始值 × 5000 / 1024換算電壓默認了滿量程精確等于 5V、零點精確等于 0V實際芯片和外圍電路總有一點誤差。我的做法是在板子上做兩點校準給某個通道輸入 0V記錄 raw0再輸入一個已知精確電壓 ref_mV記錄 raw1。之后所有這個通道的轉換都套用斜率和偏移修正。unsigned int ATD_CalibratedMillivolt(unsigned int raw, int raw0, int raw1, unsigned int ref_mv) { if (raw1 raw0) return 0; return ((unsigned long)(raw - raw0) * ref_mv) / (raw1 - raw0); }注意 raw 和 raw0 的差值要用 unsigned long 計算防止乘法規(guī)格溢出。兩點校準只修正線性誤差大多數情況下能把 10 位 ADC 的整體誤差控制在幾個毫伏量級。校準參數建議存到芯片內部的 EEPROM 或者 Flash 參數區(qū)不要在每次啟動時重新測量否則溫度漂移后你會分不清是硬件漂了還是校準參數過期了。關鍵不是把代碼寫得多花哨而是每次改動后做同一個驗證動作拿萬用表量參考電壓和程序輸出的毫伏值對比至少測 0V、中間電壓、接近滿量程三個點誤差在預期范圍內再加濾波和校準。我自己的習慣是永遠保留一路“裸讀”調試入口校驗濾波效果時切到原始值看變化這樣能很快判斷到底是傳感器信號本身在抖、還是電源在抖、還是算法把趨勢給削平了。這些留一手的方法都是被各種莫名其妙的模擬量問題教育出來的希望你少走一遍彎路。本文還有配套的精品資源點擊獲取