
簡介面向嵌入式開發者的AD7656與AD5676 STM32接口程序包聚焦高精度模擬信號采集與輸出控制。AD7656是16位ADCAD5676是16位DAC源碼通過SPI或I2C與STM32銜接涵蓋初始化配置、數據讀取、轉換控制等核心函數適合工業控制、醫療設備、儀器儀表等需要精確模擬量處理的場景。壓縮包內僅含1個C語言源文件整體大小僅2KB代碼精簡無冗余便于直接閱讀也可快速移植到HAL或標準庫工程。該資源已有1183人學習說明其在同類參考代碼中具備一定熱度。程序經多項目調試優化穩定性可靠能幫助開發者縮短ADC/DAC驅動開發周期降低底層排錯成本源碼內部函數劃分清晰還可作為SPI外設驅動編寫的范例特別適合有一定STM32基礎、希望復用成熟代碼的嵌入式工程師。1. AD7656 與 AD5676 同時掛在 STM32 上的典型場景六通道同步采樣 ADC 加八通道 16 位 DAC同時掛在同一顆 STM32 上這種組合常見于伺服驅動器電流環回采、測試機箱的信號源與回波驗證、老化臺架的時序控制。AD7656 負責把多路模擬量在同一時刻鎖存轉換AD5676 負責輸出另一組精確模擬量兩顆芯片拼在一起才能組成一個完整的模擬前端。真正把它們放到一條 SPI 總線上時最先踩的坑往往不是采樣率而是片選設計、24 位 D/A 幀與 16 位 A/D 幀的共存以及 DAC 更新和 ADC 轉換之間的同步一致性。這篇按硬件連接、SPI 讀寫、定時器硬同步、雙緩沖數據流和標定驗證的順序往下排新手可以按步驟跑通老手重點看同步與地回路部分。2. AD7656 與 AD5676 的硬件連接與 SPI 總線規劃很多工程一開始是兩塊板各自獨立ADC 板用并行總線接 STM32 的 FSMCDAC 板用另一路 SPI等到整機聯調時引腳全部擠滿。如果是我來排會從第一步就把兩顆芯片規劃到同一條 SPI2 總線上AD7656 走串行模式AD5676 只發不收兩個片選分開占用的引腳量最小。2.1 AD7656 串行模式下的關鍵引腳固定AD7656 上電后默認是并行模式必須把 SER/PAR 拉低才能進入串行。W/B 決定串行輸出字寬16 位時拉高RANGE 決定模擬輸入是 /-5V 還是 /-10V。SEL A/B/C 決定哪一組合通道被納入轉換例如全接低時只轉換 V1/V2DOUTA 引腳串行輸出這兩個通道的結果。下面這張表是固定接法引腳連接說明SER/PARGND強制串行輸出W/BVDRIVE16 位字寬RANGE3.3V 或 GND3.3V 為 /-10VGND 為 /-5VSEL A/B/CGND本方案只回采 V1/V2VDRIVE3.3V數字電平與 STM32 一致REFCAP100nF 10uF參考電壓去耦電容盡量靠近引腳這里最容易被忽略的是 W/B 要接 VDRIVE 而不是 VDD。VDD 是 5V 模擬供電如果按 5V 接DOUT 高電平會被拉到 VDRIVE 電壓超過 STM32 IO 的容忍范圍。VDRIVE 單獨接 3.3V數字信號才能和 MCU 電平對齊。2.2 雙芯片共用一條 SPI 的引腳分配SPI2 在這塊板上的分配關系如下MISO 只接 AD7656 的一根 DOUTAMOSI 只接 AD5676 的 DINSCLK 和兩個片選分別獨立控制。信號STM32 引腳方向說明SPI2_SCKPB13輸出兩芯片共用SPI2_MISOPB14輸入AD7656 DOUTASPI2_MOSIPB15輸出AD5676 DINAD7656_CSPE8輸出低有效AD7656_BUSYPB7輸入轉換期間為高AD7656_CONVSTPA8輸出TIM1_CH1定時器觸發AD5676_CSPB9輸出低有效AD5676_LDACPA9輸出TIM1_CH2低脈沖刷新CONVST 放到定時器通道上不在軟件里手動拉這樣采樣間隔是硬件周期決定的不受主循環抖動影響。LDAC 也放到定時器通道和 CONVST 同源D/A 更新時刻與 A/D 采樣時刻才能對齊。BUSY 必須用普通 IO 讀不能復用成其他功能否則轉換完成信號會被中斷處理搶占。如果六通道全要采集SPI 只有一根 MISO 就不夠用了。AD7656 串行輸出有三根 DOUTA/DOUTB/DOUTC每根輸出兩組通道CS 拉低后需要連續讀 48 位。真想六通道全采要么換帶多路 MISO 的 MCU要么把 AD7656 切到并行模式并用 FSMC 接口。只回采前兩個通道時一根 DOUTA 正好夠用成本最低。2.3 電源、參考電壓與地回路設計AD7656 需要 5V 模擬供電AD5676 建議也由 5V 供電數字接口部分再統一由 3.3V 驅動。參考電壓方面AD7656 內部自帶 2.5V 參考REFCAP 引腳按手冊要求接 100nF 與 10uF 電容去耦。這顆電容不能放遠它直接決定轉換結果的穩定度。地回路采用單點接地模擬地平面和數字地平面在電源入口處匯合DAC 輸出走線盡量遠離 ADC 模擬輸入線否則 DAC 每次跳變都會通過寄生電容耦合到 ADC 輸入端表現為輸出碼字上的周期性毛刺。我一般會把引腳宏定義全部集中在 ad76xx_io.h 里不依賴 CubeMX 生成的 main.h。這樣換單片機型號時只需要改這個頭文件的映射關系和 SPI 時鐘分頻不用在重新生成的代碼里到處找引腳CubeMX 重新生成工程也不會覆蓋這塊配置。3. STM32 SPI 初始化與 AD7656/AD5676 讀寫時序接線完成后進入驅動層。AD7656 串行讀是 16 位一幀AD5676 寫入是 24 位一幀兩種幀長不同處理方式也不同。先說 SPI2 的初始化參數再分別給讀和寫的實現。3.1 SPI2 時鐘極性、相位與分頻參數參數取值說明CPOL0空閑時 SCLK 為低CPHA1第二個邊沿采樣數據幀長16 位AD7656 讀幀波特率PCLK1/8以 PCLK1 為準片選軟件控制不使用硬件 NSSCPOL0、CPHA1 的意思是SCLK 空閑為低主機在下降沿把數據放到線上從機在上升沿采樣。AD7656 的 DOUT 在 SCLK 下降沿移出數據正好匹配AD5676 的 DIN 在 SCLK 上升沿被采入同樣匹配。所以兩顆芯片可以用同一組時鐘參數。波特率計算要結合 PCLK1例如 PCLK145MHz 時BR 字段選 8 分頻得到 5.625MHz這個速度對 AD7656 的串行時鐘上限來說余量充足。void spi2_init_raw(void) { RCC-APB1ENR | RCC_APB1ENR_SPI2EN; SPI2-CR1 SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_CPHA | SPI_CR1_BR_2; /* BR010, 按 PCLK1/8 */ SPI2-CR2 SPI_CR2_DS_1 | SPI_CR2_DS_0; /* 16 位數據幀 */ SPI2-CR1 | SPI_CR1_SPE; }默認的查詢方式里主機要產生時鐘就必須先寫 SPI2-DR然后等 RXNE 置位。CPHA 位如果不顯式加上默認是 CPHA0在上升沿放數據下降沿采樣讀回來經常是錯位的數據。寄存器方式初始化還有一個好處驅動代碼不依賴 HAL 的鎖和超時邏輯可以在中斷上下文直接調用不會因為 HAL 內部的狀態機把讀寫時機拖偏。3.2 等 BUSY 下降沿與 AD7656 16 位讀取啟動轉換后不能立刻讀 DOUT。AD7656 的 BUSY 引腳在轉換期間保持高電平轉換完成才會拉低。讀取代碼必須把等待 BUSY 放在片選拉低之前否則會讀到轉換中的中間數據。static uint32_t bus_timeout; int ad7656_convert_once(void) { /* 啟動轉換: 上升沿鎖存當前模擬輸入 */ HAL_GPIO_WritePin(CONVST_GPIO_Port, CONVST_Pin, GPIO_PIN_SET); __NOP(); HAL_GPIO_WritePin(CONVST_GPIO_Port, CONVST_Pin, GPIO_PIN_RESET); /* 等待轉換完成, 必須帶超時 */ bus_timeout 100000; while (HAL_GPIO_ReadPin(BUSY_GPIO_Port, BUSY_Pin) GPIO_PIN_SET) { if (--bus_timeout 0) return -1; } /* 片選拉低后移出 16 位數據 */ AD7656_CS_LOW(); uint16_t raw spix_read16(); AD7656_CS_HIGH(); return (int)raw; } static inline uint16_t spix_read16(void) { while (!(SPI2-SR SPI_SR_TXE)) { } /* 等待發送寄存器空 */ SPI2-DR 0x0000; /* 寫 0, 產生 16 個時鐘 */ while (!(SPI2-SR SPI_SR_RXNE)) { } /* 等待接收完成 */ return (uint16_t)SPI2-DR; }代碼里的SPI2-DR 0x0000是必須的。主機模式下 SPI 不會自己產生時鐘必須有數據寫入發送寄存器SCLK 才會翻轉。讀到的 16 位數據中最高位對應 DOUT 移出的第一個位直接作為原始碼字使用。超時值bus_timeout根據實際轉換時間來定AD7656 在最快模式下的轉換時間在微秒量級10 萬次遞減足夠余量寧可多一點也不能用裸 while 死等否則一旦 BUSY 信號異常整個控制循環都會卡死。3.3 AD5676 寫入為什么用 GPIO 模擬AD5676 的 24 位幀格式是高 4 位命令中間 4 位地址低 16 位數據。寫命令、寫 DAC 寄存器、更新輸出都通過這 24 位完成。很多人直接把 STM32 的 SPI 配成 16 位幀分兩次發送第二幀多出來的時鐘會被 AD5676 當成新的寫命令結果寄存器被反復覆蓋。void ad5676_write(uint8_t cmd, uint8_t addr, uint16_t data) { uint32_t frame ((uint32_t)(cmd 0x0F) 20) | ((uint32_t)(addr 0x0F) 16) | data; AD5676_CS_LOW(); for (int i 23; i 0; i--) { SCLK_LOW(); if (frame (1u i)) { DIN_HIGH(); } else { DIN_LOW(); } SCLK_HIGH(); } AD5676_CS_HIGH(); /* LDAC 低脈沖刷新輸出, 需要同步時放到定時器中斷里做 */ LDAC_LOW(); __NOP(); __NOP(); __NOP(); LDAC_HIGH(); }用 GPIO 模擬是為了避開幀長不匹配的問題24 個時鐘一個不差。這個函數在控制周期 10us 的場景里性能足夠因為每個周期最多更新幾個通道幾位 GPIO 翻轉的開銷遠小于一次 SPI 傳輸加狀態切換。如果輸出通道數多、更新頻率高可以改用硬件 SPI 配成 8 位幀CS 拉低后連續發 3 個字節同樣滿足 24 位格式要求。4. 同步采樣與同步輸出定時器、DMA 與雙緩沖單獨調通 ADC 和 DAC 之后最核心的問題變成同步ADC 什么時候采樣DAC 什么時候更新輸出。很多毛刺問題的根源不是數據算錯而是兩個動作發生在不同的時間基準上。4.1 用定時器輸出比較通道直接驅動 CONVST 與 LDAC一種常見做法是在主循環里先調用ad7656_convert_once()再調用ad5676_write()間隔取決于主循環執行時間抖動不可控。同步采樣場景下ADC 的轉換起點和 DAC 的刷新時刻必須由同一個時鐘源觸發。用 STM32 的定時器輸出比較功能可以做到TIM1 的兩個通道分別輸出 CONVST 和 LDAC 波形更新事件由計數器周期自動觸發和軟件中斷延遲無關。TIM_HandleTypeDef htim1 {0}; TIM_OC_InitTypeDef sConfigOC {0}; __HAL_RCC_TIM1_CLK_ENABLE(); htim1.Instance TIM1; htim1.Init.Period 900 - 1; /* 90MHz/900 100kHz */ htim1.Init.Prescaler 0; htim1.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_OC_Init(htim1); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 2; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_OC_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_1); /* PA8, CONVST */ sConfigOC.Pulse 898; sConfigOC.OCPolarity TIM_OCPOLARITY_LOW; HAL_TIM_OC_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_2); /* PA9, LDAC */ HAL_TIM_OC_Start(htim1, TIM_CHANNEL_1); HAL_TIM_OC_Start(htim1, TIM_CHANNEL_2);PA8 輸出周期 10us 的窄脈沖上升沿觸發一次 AD7656 轉換。PA9 平時保持高電平只在計數器接近末尾時拉低一個窄脈沖這個下降沿把 AD5676 之前寫入的數據同時刷新到模擬輸出端。CCR1 和 CCR2 的差值決定了 DAC 更新相對 ADC 采樣點的相位90MHz 下調整粒度約 11ns。這比在中斷里翻轉 GPIO 的抖動小了三個數量級以上。4.2 DMA 讀取與雙緩沖防止丟采轉換由定時器觸發后每次 BUSY 下降沿都代表一次采樣完成。如果在 BUSY 中斷里用 CPU 查詢 SPI 接收控制算法或 DAC 寫入隨時可能搶占總線讀取延時的微小變化會引入采樣抖動。繞開辦法是讓 BUSY 下降沿啟動一次 DMA 讀取ADC 數據由 DMA 自動搬進內存。uint16_t dma_tx[2] {0x0000, 0x0000}; uint16_t dma_rx[2] {0, 0}; /* BUSY 下降沿中斷中只做一件事 */ void EXTI9_5_IRQHandler(void) { if (EXTI-PR (1 7)) { EXTI-PR (1 7); AD7656_CS_LOW(); HAL_SPI_TransmitReceive_DMA(hspi2, (uint8_t*)dma_tx, (uint8_t*)dma_rx, 1); } } void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { AD7656_CS_HIGH(); process_sample(dma_rx[0]); }DMA 傳輸完成中斷里只做兩件事拉高片選、把數據交給處理函數。處理函數里不要做浮點換算或控制算法只把結果放入環形緩沖或者直接切換雙緩沖指針。static uint16_t adc_buf[2][64]; static uint8_t active_buf 0; static volatile uint8_t ready 0; void process_sample(uint16_t raw) { adc_buf[active_buf][buf_index] raw; } void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { AD7656_CS_HIGH(); active_buf ^ 1; ready 1; }主循環或者控制任務只在ready置位后處理adc_buf[active_buf ^ 1]。這個結構的價值在于采樣動作由硬件觸發數據搬運由 DMA 完成CPU 只在數據準備就緒后才介入計算不會出現“等 SPI 接收”造成的采樣空洞。4.3 中斷優先級與資源沖突雙緩沖跑起來之后中斷優先級分配決定系統是否會在高負載下丟數據。中斷源搶占優先級說明BUSY 下降沿2啟動 DMA 讀取越早越好SPI2 DMA 完成3切換雙緩沖標記數據就緒TIM1 更新4周期任務算控制量、準備 DAC 數據SysTick1系統節拍但中斷里不能做重活BUSY 優先級如果太低轉換結束到發起 DMA 之間的窗口會被其他中斷擠掉表現為采樣值偶發回跳。TIM1 更新中斷用來準備下一拍的控制量不能在里面調用 SPI 讀寫函數否則會阻塞其他低優先級任務。這里要特別提醒TIM1 中斷和 DMA 中斷里都不能調用 HAL_Delay它依賴 SysTick在中斷里如果 SysTick 被關掉就會直接卡死這是很多工程在聯調階段才暴露出來的坑。5. AD7656 與 AD5676 的增益偏移標定與軟件修正芯片手冊給的線性度指標是理論值實際板上還有參考電壓誤差、輸入分壓電阻誤差、DAC 輸出緩沖失調。做伺服或測試設備時這些誤差靠軟件標定吃掉比換更高精度芯片便宜得多。5.1 雙極性碼值換算AD7656 工作在雙極性 /-10V 范圍時按偏移二進制格式0x8000 對應 0V0x0000 對應 -10V0xFFFF 對應接近 10V。換算公式float adc_code_to_volt(uint16_t code, float range_v) { return ((int32_t)code - 32768) * range_v / 32768.0f; }range_v傳 10.0 或 5.0對應 RANGE 引腳設置的滿量程。這里有個常見誤用直接用code / 65535 * 20算出來零點會偏到 32767/65535 的位置偏移量不大但會在零點附近造成持續的系統偏差標定時很難查。5.2 端點標定與定點修正標定流程用兩個或多個已知電壓點記錄 ADC 原始碼字然后計算增益與偏移修正量。步驟輸入電壓讀取碼字10Vcode_mid210Vcode_high3-10Vcode_low理想情況下 code_mid 應是 32768code_high 減 code_mid 應是 32768。實際板子上會有偏差把這兩個偏差量固化成修正系數typedef struct { int32_t gain_q15; /* Q15 定點增益, 1.0 表示為 32768 */ int32_t offset; /* 零輸入時的碼字偏移 */ } adc_cal_t; void adc_cal_init(adc_cal_t *cal, int32_t code_mid, int32_t code_high) { cal-offset code_mid - 32768; int32_t delta code_high - code_mid; if (delta 0) delta 1; cal-gain_q15 (32768 15) / delta; } int32_t adc_cal_convert(const adc_cal_t *cal, int32_t raw) { return (int32_t)(((int64_t)(raw - cal-offset) * cal-gain_q15) 15); }增益用 Q15 定點表示而不是浮點因為 F103 這類沒有 FPU 的 MCU 上浮點運算會占用大量 CPU 時間而在 100kHz 控制周期里每個樣本都要做一次修正定點乘法的開銷比浮點小一個數量級。raw - cal-offset可能出現負數所以中間變量用 int64_t避免符號位在移位時被丟棄。標定時多采幾個點做最小二乘法擬合比只看兩個端點更穩兩點法只適合初步驗證。5.3 DAC 輸出修正的逆映射AD5676 輸出 0 到 5V 時理想情況下 code 與電壓是線性關系0x0000 對應 0V0x8000 對應 2.5V0xFFFF 對應接近 5V。實際輸出的問題是零碼偏移和增益誤差。標定時寫入兩個 DAC 碼用萬用表測實際電壓步驟寫入碼字實測電壓10x4000V_low20x8000V_midfloat dac_slope; /* V/code */ float dac_offset; /* 零碼輸出電壓 */ void dac_cal_init(float v_low, float v_mid) { dac_slope (v_mid - v_low) / (0x8000 - 0x4000); dac_offset v_mid - dac_slope * 0x8000; } uint16_t dac_code_for_voltage(float vtarget) { float code (vtarget - dac_offset) / dac_slope; if (code 0.0f) code 0.0f; if (code 65535.0f) code 65535.0f; return (uint16_t)(code 0.5f); }dac_slope的單位是 V/code如果在 0x4000 到 0x8000 之間測得 2.5V 跨度斜率約為 2.5V / 16384。逆映射階段不要用單純的比例系數因為 AD5676 輸出緩沖在接近軌電壓附近線性度會下降標定時取中段兩點軟件里限制輸出范圍避免進入非線性區。只標一個點不標斜率低端輸出會明顯偏大。6. 上電后的驗證順序與異常排除驅動和同步結構寫完上電調試別急著跑控制算法按順序驗證幾個關鍵點能省下半天定位時間。先把 ADC 輸入短接到地不加任何信號讀到的碼字應該穩定落在 0x7F80 到 0x8080 之間波動范圍取決于 PCB 噪聲和參考電壓去耦。如果偏出這個范圍優先查 RANGE 引腳電平與代碼里range_v是否一致。再用信號源輸入 1V理論上讀數約 36045允許 /- 幾十個 LSB。這一步過了再接入 DAC 輸出做閉環。等待 BUSY 的邏輯建議用 DWT 周期計數器代替 HAL_Delay這樣即使 SysTick 被關掉也不會卡死。void dwt_init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } int busy_wait_us(uint32_t us) { uint32_t start DWT-CYCCNT; while (HAL_GPIO_ReadPin(BUSY_GPIO_Port, BUSY_Pin) GPIO_PIN_SET) { uint32_t elapsed DWT-CYCCNT - start; /* 無符號回卷安全 */ if (elapsed (SystemCoreClock / 1000000u) * us) return -1; } return 0; }DWT-CYCCNT 是 CPU 周期計數器讀取本身是單條指令不依賴任何中斷。之前遇到過delay卡死的工程多數是在中斷里調 HAL_DelaySysTick 被更高優先級中斷搶占后永遠等不到節拍換成 DWT 后這個問題直接消失。現象常見原因讀出全 0xFF片選沒拉低 / VDRIVE 電壓不足 / CPHA 配錯讀出全 0x00DOUT 懸空 / SPI2 MISO 配置錯誤數值比實際大 2 倍RANGE 引腳與換算范圍不匹配零點在 0x8000 附近跳動等待 BUSY 超時太短讀到轉換中間值偶發大毛刺DAC 輸出耦合到 ADC 輸入 / LDAC 與 CONVST 未對齊手摸輸入信號碼值不變前端 RC 濾波時間常數太大示波器同時看 CONVST、BUSY、CS 三個信號每次 CONVST 上升沿后 BUSY 拉高BUSY 下降沿后 CS 才拉低時序就正確了。最后把 DAC 輸出接到采樣前端觀察 100kHz 控制周期內是否出現周期毛刺沒有毛刺說明地回路和同步配置都過關。本文還有配套的精品資源點擊獲取