ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32 SPI总线上AD7656与AD5676的同步采样与输出设计

STM32 SPI总线上AD7656与AD5676的同步采样与输出设计 简介面向嵌入式开发者的AD7656与AD5676 STM32接口程序包聚焦高精度模拟信号采集与输出控制。AD7656是16位ADCAD5676是16位DAC源码通过SPI或I²C与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 控制周期内是否出现周期毛刺没有毛刺说明地回路和同步配置都过关。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进