ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AD7768与STM32H5的SPI时序实战:多通道同步采集难点解析

AD7768与STM32H5的SPI时序实战:多通道同步采集难点解析 1. 为什么说AD7768的SPI接口是“非标准”的1.1 项目来由多通道同步高精度采集不是随便选个ADC就行先交代一下这个项目的背景要做一块8通道同步振动采集板每通道24位精度采样率能跑到几十kSPS以上同时要求通道之间没有相位差。这个需求一摆出来单片机自带的ADC基本就不用考虑了普通多路ADC也不够看因为很多廉价ADC是靠内部模拟多路开关逐次切换通道的切换采样天然存在通道间时差放在振动分析或者电能质量分析这种场景里差那么几个采样周期可能就带来无法容忍的相位误差。所以当时直接锁定到AD7768。这颗芯片是ADI家8通道、同步采样、24位Σ-Δ ADC内部带数字抽取滤波单端或差分输入都能接动态范围在合适配置下能做到100dB以上。最关键的是它所有通道是同时采样的采样保持之后在内部依次输出通道间串扰小、相位一致性好这是多通道高精度同步采集场景里非常典型的选型。如果通道数再多AD7768还支持多片菊花链级联扩展性也从源头考虑好了。但真正动手的时候才发现这颗ADC的SPI接口跟“标准SPI从机”完全不是一个套路。市面上多数ADC是“主控发命令、从机回数据”的响应式模型而AD7768更像是一个“边沿触发后持续往外面吐数据”的同步串行设备。如果照搬标准SPI驱动逻辑很容易出现读不到数据、读错通道、寄存器配置没生效这类问题。1.2 AD7768和普通SPI ADC的区别到底在哪普通SPI从机ADC比如ADS1256、MCP3208这一类的行为模式一般是这样的主控先把CS拉低发一个包含通道号和启动标志的指令帧然后继续给SCLK从机就把转换结果从SDO上移出来。所有动作都是一“问”一“答”SCLK脉冲数和数据位一一对应主控完全可控。AD7768完全不是这个逻辑。它工作时始终在后台做采样和滤波每当一组新的8通道数据就绪DRDY引脚就会输出一个下降沿作为“数据有效”通知。这个时候主控必须在一个DRDY周期内把CS拉低然后连续发送至少192个SCLK8通道 × 24位SDO会把8个通道的数据依次送出来。期间不允许中途拉高CS也不允许只读其中某几个通道而跳过其他通道——要么一次读完整帧要么就不要在这一轮读。这个行为模式带给开发者的第一个冲击就是你没法用常规SPI驱动函数“每次传输一个固定帧”来完成数据读取。标准SPI主机每次片选拉低后传输一个8位、16位或32位帧然后片选释放如果照这个习惯去操作AD7768CS会在两个SCLK之间来回闪动ADC输出逻辑会被彻底打乱读回来的数据大概率是乱码。1.3 为什么用STM32H5来做这个控制器主控这边选择STM32H5当时考虑了几点。第一是性能H5系列基于Cortex-M33内核主频能到250MHz处理8通道24位数据外加后续数字滤波、FFT都足够不是那种靠超频凑合的场景。第二是SPI外设的灵活性STM32H5的SPI支持4到32位可变数据长度理论上可以一帧读24位但这不代表实战中就该用24位帧这个问题我后文会专门讲。第三是低功耗表现和性价比H5定位在中高端工业控制外设资源比F系列丰富价格又不像H7那么夸张做量产设备压力小一些。另外还有一点容易被忽略STM32H5支持TrustZone外设默认可能被划分到安全区或非安全区如果你只是在CubeMX里简单初始化外设而没注意安全属性的设置运行时会出莫名其妙的总线错误或外设无法访问。这个坑我在后文排查清单里会专门提一句新上手H5的人多数都会在这里卡一下。2. SPI时序拆解寄存器帧和数据帧根本是两套玩法2.1 寄存器读写的SPI命令帧长什么样AD7768的寄存器访问虽然也是走SPI但它的命令帧结构和普通SPI从机不太一样实际是在一次CS低电平期间传输16位数据前8位是“命令字节”后8位是“数据字节”。命令字节的最高位表示读写方向低7位是寄存器地址。写寄存器时把命令字节和数据字节一并发出即可读寄存器时命令字节之后还要再发一个哑字节因为从机需要这8个SCLK才能把寄存器内容从SDO上吐出来。具体到代码就是下面这种形式#define AD7768_CS_LOW() HAL_GPIO_WritePin(AD7768_CS_GPIO_Port, AD7768_CS_Pin, GPIO_PIN_RESET) #define AD7768_CS_HIGH() HAL_GPIO_WritePin(AD7768_CS_GPIO_Port, AD7768_CS_Pin, GPIO_PIN_SET) void AD7768_WriteReg(uint8_t regAddr, uint8_t regData) { uint8_t frame[2]; frame[0] regAddr 0x7F; // 写命令最高位为0 frame[1] regData; AD7768_CS_LOW(); HAL_SPI_Transmit(hspi1, frame, 2, 10); AD7768_CS_HIGH(); } uint8_t AD7768_ReadReg(uint8_t regAddr) { uint8_t frame[2]; uint8_t rxBuf[2]; frame[0] (1 7) | (regAddr 0x7F); // 读命令最高位置1 frame[1] 0x00; // 哑字节用于提供SCLK AD7768_CS_LOW(); HAL_SPI_TransmitReceive(hspi1, frame, rxBuf, 2, 10); AD7768_CS_HIGH(); return rxBuf[1]; }这里有几个细节值得注意第一帧数组必须显式处理字节顺序不能用uint16_t一个变量去拼否则在小端模式下字节序会颠倒命令字和数据字会换位置寄存器写入大概率失败。第二寄存器读写时SCLK不需要连续很多个16个就够和后面数据读取的长脉冲是两回事。第三寄存器地址最好做成宏定义不同后缀的AD7768寄存器表会有差异不要网上抄一份就硬套我建议以你手里芯片型号对应数据手册为准。2.2 DRDY、SCLK、CS谁等谁谁说了算AD7768这个系统里时序的“主时钟”其实不是MCU的SCLK而是DRDY。DRDY下降沿代表新数据已经准备好这是开始读取的唯一合法信号。你不能拍脑袋决定什么时候去读数据必须用外部中断捕捉DRDY边沿然后立刻启动SPI读取流程。具体到一根线一根线来看顺序是这样DRDY拉低后主控把CS拉低此时SDO的MSB位已经准备好随后主控开始产生SCLK每个SCLK下降沿SDO更新下一位上升沿时主控采样连续192个SCLK之后8个通道的24位数据全部移出主控再拉高CS结束本轮读取等待下一个DRDY下降沿。CS在这个过程中的作用更像一个“帧使能”信号而不是“每次传输使能”。它在整个读取期间必须保持低电平中间任何一次拉高都会让AD7768认为当前帧被终止接下来的数据输出状态就不可预期了。DRDY低电平持续的时间窗口是有限的全部8通道读取必须在下一个DRDY下降沿到来之前完成。如果SPI时钟太慢导致192个SCLK还没打完新数据又要输出就会造成数据覆盖和通道错位。我在实际板子上一般把SPI时钟设在20到30MHz左右配合系统配置足够读完8通道如果AD7768输出数据率很高那就要同步提高SPI时钟或者减少单次读取的通道数不能只调一边。2.3 8通道24bit数据帧一次CS低电平里连续读192bitAD7768数据输出的位序是MSB first每个通道固定24位通道顺序从0到7依次排列。这意味着一个完整读取帧是8×24192个时钟周期数据格式如下示意通道0 [23:0] | 通道1 [23:0] | ... | 通道7 [23:0]这里还有个容易搞混的地方AD7768输出的24位是有符号的二进制补码格式满量程对应±VREF。也就是说当输入为正满量程附近时码值接近0x7FFFFF负满量程附近接近0x800000。转换到16位或32位变量时必须自己把24位符号扩展到32位否则后续计算电压会出问题。在程序设计上我建议维护一个uint8_t rxBuf[24]的接收缓冲区DMA收到24字节后软件再按3字节一组重组8个通道的码值。这样虽然多一步重组但好处是DMA配置简单、逻辑分析仪看数据也直观而且不依赖STM32H5 SPI的24位帧长模式兼容性最好。3. STM32H5侧SPI和DMA的配置思路3.1 SPI外设参数我选了8bit帧而不是24bit帧很多人看到STM32H5的SPI支持4-32位可变数据长度第一反应是直接把DataSize设成24位认为这样读取AD7768时一帧刚好对应一个通道。理论上确实可以但实际用起来你会发现DMA缓冲区的组织非常别扭24位帧在内存里是按字对齐的DMA搬运时要么处理32位宽度的“有效低24位”要么忍受跨字节边界带来的错位调试起来比想象中麻烦得多。所以我最终选择了经典方案SPI配置成8位数据帧DMA一次读24个字节软件再按每3字节重组一个24位码。SCLK个数由传送字节数决定AD7768并关心SPI硬件眼里的“帧”是什么它只关心CS低电平期间一共来了多少个SCLK。192个SCLK正好对应8通道完整数据。这种做法的另外一个好处是寄存器的读写帧也可以复用同一套8位SPI传输函数不需要为了2字节命令帧和24字节数据帧分别维护不同配置。整个驱动代码逻辑统一排查问题的时候思路也清晰。实际配置代码如下hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(hspi1);时钟极性这里我用了CPOL0、CPHA1沿也就是SCLK空闲为低、上升沿采样。不同板子如果AD7768的INVSCLK引脚接法不同极性可能要做相应调整不能一成不变地抄参数。3.2 软件CS和硬件NSS的取舍STM32的SPI外设虽然自带硬件NSS信号但在AD7768这种长时间CS低电平、多字节连续读的场合硬件NSS反而是一个坑。标准SPI主机在每传输完一个数据帧后硬件NSS会按照配置自动拉高这个行为如果落在AD7768的192bit长读取过程中间就会把ADC的数据输出帧切断后读的数据就会错位。所以我的做法是SPI外设的NSS配置成软件模式SPI_NSS_SOFT完全不使用芯片的NSS引脚CS线单独接到一个普通GPIO上由软件在整个DMA传输期间保持低电平DMA传输完成再把它拉高。这样做虽然多占用一个GPIO但CS的时序完全可控AD7768的长数据帧读取才能真正稳定。另外提醒一句CS引脚最好选择靠近SPI引脚的GPIO比如SPI1常用PA5、PA6、PA7那么CS可以放在PB6或PA4这样布线时SCLK、SDO、CS基本能保持等长对高速SPI通信更友好。3.3 DMA缓冲、中断优先级和回调设计DMA传输是整个采集驱动里的关键一环。我把GPIO的初始化、SPI初始化和DMA初始化都放在CubeMX里生成。DMA的接收缓冲是uint8_t adc_rx_buf[24]发送缓冲是一个全局的哑数据数组里面全部为0用来提供SCLK时钟。发送缓冲和接收缓冲必须定义成全局变量不能是局部数组因为DMA传输是异步的函数退出后局部变量就失效了DMA拿到了野指针跑起来必现问题。中断设计上DRDY接一个外部中断引脚触发方式设为下降沿。DRDY中断优先级应该高于DMA传输完成中断因为一旦DRDY来了要保证CPU能立即启动下一轮DMA读取而不是被其他中断卡住。DMA完成中断优先级可以低一些它在所有数据都到齐之后置一个标志位主循环检测到这个标志再去做数据解析和电压换算。void EXTI4_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(AD7768_DRDY_Pin) ! RESET) { __HAL_GPIO_EXTI_CLEAR_IT(AD7768_DRDY_Pin); AD7768_CS_LOW(); HAL_SPI_TransmitReceive_DMA(hspi1, spi_tx_dummy, adc_rx_buf, 24); } } void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { AD7768_CS_HIGH(); adc_data_ready 1; } }有几个容易忽略的细节第一在DRDY中断里做CS拉低和启动DMA这个动作要尽量短不能在中断里做浮点运算或电压换算。第二如果上一轮DMA还没完成新的DRDY又到了要加一个标志位判断避免重复启动传输导致DMA指针错乱。第三回调函数里拉高CS这个动作不能省否则CS会一直保持低电平AD7768的输出状态就会一直延续到下一轮逻辑混乱。4. 完整代码实现从寄存器配置到电压换算4.1 寄存器配置顺序和关键寄存器示例首先要明确一点下面这部分代码里的寄存器地址是按我手头这个项目固件里的宏展开来写的AD7768和AD7768-4这类不同后缀的芯片寄存器数量和地址定义会有差异大家一定要对照自己芯片型号的数据手册核对不要看到地址就直接搬。我把配置过程分成了三步第一复位芯片让模块回到已知状态第二配置抽取率、通道使能等关键项第三等待DRDY稳定输出。#define AD7768_REG_CH_EN 0x03 // 按实际手册核对 #define AD7768_REG_CLK_CTRL 0x0B // 按实际手册核对 #define AD7768_REG_DEC_RATE 0x0D // 按实际手册核对 #define AD7768_CH_ALL 0xFF void AD7768_Init(void) { // 拉低复位引脚保持至少20us再释放复位 HAL_GPIO_WritePin(AD7768_RESET_GPIO_Port, AD7768_RESET_Pin, GPIO_PIN_RESET); HAL_Delay(2); HAL_GPIO_WritePin(AD7768_RESET_GPIO_Port, AD7768_RESET_Pin, GPIO_PIN_SET); HAL_Delay(10); // 使能全部8个通道 AD7768_WriteReg(AD7768_REG_CH_EN, AD7768_CH_ALL); // 配置抽取率具体值影响输出数据率 AD7768_WriteReg(AD7768_REG_DEC_RATE, 0x03); // 读回验证配置 uint8_t chEn AD7768_ReadReg(AD7768_REG_CH_EN); if (chEn ! AD7768_CH_ALL) { // 配置失败这里打错误日志或者点亮错误灯 Error_Handler(); } }寄存器配置生效后AD7768会立刻开始在DRDY上输出周期脉冲。如果你在STM32的调试模式下单步执行很可能会看到DRDY在持续跳动这是正常现象。如果DRDY完全没有反应优先检查MCLK是否到了没有MCLK这颗ADC完全不会工作。4.2 24字节DMA接收后的数据重组DMA完成中断置位adc_data_ready后主循环里做数据重组。接收缓冲adc_rx_buf里存的是连续的24字节每3字节对应一个通道顺序是通道0到通道7每组的字节序是高字节在前、低字节在后。int32_t adc_code[8]; void ParseAD7768Data(uint8_t *rxBuf, int32_t *codeOut) { for (int ch 0; ch 8; ch) { uint32_t raw ((uint32_t)rxBuf[ch * 3] 16) | ((uint32_t)rxBuf[ch * 3 1] 8) | ((uint32_t)rxBuf[ch * 3 2]); // 24位有符号数扩展为32位有符号数 if (raw 0x800000) { raw | 0xFF000000; } codeOut[ch] (int32_t)raw; } }这段代码重点在“符号扩展”四字。如果忽略这个操作负电压对应的码值会被当成一个很大的正数后续电压计算完全错误。扩展的依据是24位最高位也就是第23位一旦为1说明是负数要把高8位全部补成1。4.3 码值到实际电压的换算AD7768的24位码值范围对应模拟输入范围是±VREF。比如你的基准电压VREF是4.096V那么码值0x7FFFFF约等于4.096V0x800000约等于-4.096V。电压换算公式可以写成电压(V) code * VREF / 8388608其中8388608等于2的23次方。用代码表示就是#define ADC_VREF 4.096f #define ADC_FULL_SCALE 8388608.0f float adc_voltage[8]; void UpdateVoltage(float *voltageOut, int32_t *codeIn) { for (int ch 0; ch 8; ch) { voltageOut[ch] (float)codeIn[ch] * ADC_VREF / ADC_FULL_SCALE; } }如果模拟前端有衰减或放大还需要把比例系数乘进去。这里有一个细节不要用整数除法必须先转float再算否则结果会被截断成整数。IAR或GCC编译器在优化等级较高时也不会自动帮你处理这个逻辑代码里必须显式转换。实际在板上验证的时候我一般先给所有输入通道接到GND这时候读出来的码值应该在0附近跳动幅度大概是±几十个LSB然后接一个已知精度的直流电压比如1.000V看换算结果是否在误差范围内。这样做比上来就接复杂信号更容易定位问题出在数字链路还是模拟链路。5. 踩坑记录与排查实战5.1 五个最容易翻车的问题及现象我把实际调试中遇到的和同行分享过的典型问题整理成一张表供大家排查时参考问题现象可能原因解决方向DRDY有脉冲但读到的数据全为0xFF或0x00CS时序不对或者SPI时钟极性配反用逻辑分析仪看CS和SCLK确认CS在整个192个SCLK期间保持低能读到数据但8个通道顺序错乱读取启动时刻不固定或者上一轮DMA未完成就启动下一轮检查DRDY中断是否被其他高优先级中断阻塞增加busy标志位寄存器写进去读回来不对命令字节和字节序错误或者CS拉高过早确认写命令时最高位为0读命令时最高位为1帧数组手动定义字节顺序数据偶尔跳变像是某个bit被吞掉SCLK时钟过快或SDO信号质量差降低SPI分频检查SDO/SCLK走线是否过长、是否缺少串联电阻程序一运行SPI就进HardFaultSTM32H5的TrustZone外设安全属性未配置在CubeMX或代码中把用到的SPI、DMA、EXTI外设设为非安全可访问这些坑我基本都踩过一遍尤其是第一个CS时序的问题卡了我整整一个下午。当时以为是ADC配置问题反复读寄存器后来用逻辑分析仪抓到CS在192个SCLK中间跳了一次高才知道是CubeMX默认生成的硬件NSS配置在捣乱。5.2 通过逻辑分析仪验证时序的常规流程调试SPI这种同步接口示波器看单信号逻辑分析仪才是“破案”主力。抓到CS、SCLK、SDO、DRDY四根线的时序之后按照下面几步来排查先把DRDY下降沿和CS下降沿对齐看CS拉低是否紧跟DRDY。如果DRDY已经来了很久CS才拉低要检查外部中断配置和中断优先级。接着数SCLK的脉冲个数一轮完整读取必须是192个多一个或者少一个都说明SPI传输长度配置有问题。然后检查CS低电平期间SCLK是否连续无中断如果中间有因为DMA配置不当造成的SCLK断续AD7768的输出位序会乱。最后看SDO上的第一位数据和AD7768手册里的通道0 MSB时序是否对应。如果第一位数据就明显不对不用往下数了基本是SPI极性配错或者CS结束时机过早。逻辑分析仪的采样率建议至少是SCLK的4倍以上不然边沿细节看不到。我用的分析仪采样率开到100MHz以上抓20MHz的SPI信号边沿抖动看得清清楚楚。5.3 几个让系统更稳定的细节习惯调试跑通只是第一步想长时间稳定采集还有几个细节值得养成习惯。第一MCLK尽量用外部有源晶振或者至少用低抖动的时钟源给AD7768提供。MCLK的抖动会直接影响采样精度用MCU的定时器产生方波虽然省成本但抖动往往比较大在高精度采集场合会让信噪比变差。我在这块板上直接给AD7768独立供了一个2.048MHz有源晶振MCU这边完全不用操心MCLK的事代码也少了一段。第二VREF基准芯片的噪声要重视。AD7768是24位ADC参考电压一点点纹波都会体现在结果里。我用的是低噪声基准源并且在VREF引脚附近加了足够的去耦电容。如果项目对精度要求更高可以参考数据手册推荐的基准驱动电路做缓冲。第三SPI时钟速率留30%左右的余量。AD7768最高能支持的SCLK频率并不低但实际PCB走线的寄生电容、排针连接器的接触电阻都会影响信号边沿。与其把SCLK拉到极限然后天天担心误码不如降一档速率换稳定性。我的板子在31.25MHz分频档跑起来没任何问题再往上拉虽然也能跑但余量明显变小所以最终量产代码里就锁定了这个档位。第四DMA接收缓冲必须对齐。我在代码里把adc_rx_buf定义成了uint8_t数组看着没什么问题但如果要配合D-Cache使用最好把缓冲区放到一个独立的cache line对齐区域并在读取前做一次cache invalidate。STM32H5的D-Cache在默认情况下可能是关闭的一旦打开这个操作不能省。第五中断服务函数要做“忙判断”。如果上一轮数据还没处理完下一轮DRDY已经到来此时再启动DMA就会导致缓冲覆盖。我在主循环加了一个处理耗时统计确保一轮DMA传输的周期远大于数据解析的时间同时在DRDY中断里加了标志位检查双保险。最后再分享一点个人心得AD7768这类非标准SPI设备看起来难搞实际把它的“帧”概念从标准SPI里抽离出来一切就顺了。记住一句话这里不是“每次传输一帧数据”而是“一次片选周期里连续读完整个数据块”。把握好CS低电平的持续时间和SCLK的总数问题就解决了一大半。如果后续还要扩展更多通道AD7768的菊花链功能是一条很实用的路子多片ADC级联之后一次片选周期里读出来的数据会翻倍代码结构可以从“单帧解析”升级成“按链式排列解析”。这个扩展方向对多通道同步采集系统来说非常有价值等你有空了也值得研究一下。
返回列表