1. 项目概述与核心价值
在精密数据采集领域,比如心电监护仪、高精度工业传感器或者电力质量分析设备,我们常常面临一个核心挑战:如何从模拟世界获取的微弱信号,经过模数转换(ADC)后,能毫发无损、准确无误地传送到主控芯片(MCU)手中。这不仅仅是速度问题,更是关乎数据“保真度”的生死线。一个被噪声干扰或传输错误污染的采样值,可能导致系统误判、控制失灵,在医疗或工业场景下,后果不堪设想。
我接触过不少ADC芯片,发现很多工程师把SPI通信当作一个简单的“读数据”动作,配置好时钟极性相位,然后就开始循环读取。这在小批量、低干扰的实验室环境下或许可行,但一旦放到复杂的电磁环境或长线传输中,数据错误的幽灵就会悄然浮现。德州仪器(TI)的ADS131A0x系列Σ-Δ ADC,正是为应对这种严苛环境而生的高精度、多通道解决方案。它的魅力不仅在于其优异的噪声性能和灵活的增益设置,更在于其SPI接口层内置的一套强大的“数据安全卫士”组合拳:灵活的数据帧结构、可配置的循环冗余校验(CRC)以及可选的汉明码(Hamming Code)纠错机制。
理解并正确配置这些机制,意味着你构建的数据链路具备了“自检”与“自愈”能力。本文将深入芯片手册的细节,结合我实际调试中的踩坑经验,为你拆解ADS131A0x的SPI数据帧如何组织,CRC校验如何为每一帧数据贴上“防伪标签”,以及汉明码如何像一位细心的校对员,在数据抵达前就修正其中的单比特谬误。无论你是正在评估此芯片,还是已在产品中应用但遇到了偶发数据异常,相信这篇深度解析都能为你提供清晰的排查思路和可靠的配置指南。
2. 数据帧结构深度解析:固定帧与动态帧
SPI通信的本质是在时钟节拍下交换数据位。但对于ADS131A0x这类多通道、高精度的ADC,一“帧”数据里到底包含了什么,顺序如何,长度是否可变,是驱动程序设计的基础。芯片提供了两种核心帧模式:固定帧(Fixed-Frame)和动态帧(Dynamic-Frame)。这两种模式直接决定了你每次通信需要发送和接收的时钟周期数,理解它们的选择逻辑至关重要。
2.1 固定帧模式:确定性时延的保障
固定帧模式,顾名思义,每一次SPI通信事务(从CS拉低到拉高)的数据长度是固定的。无论当前ADC使能了几个通道,也无论你是否启用了CRC校验,主机(MCU)需要发送(DIN)和接收(DOUT)的“设备字(Device Word)”数量保持不变。
为什么需要固定帧?在实时性要求极高的控制系统中,处理器的时序预算往往是精确计算好的。固定帧模式提供了最确定性的通信时延。主机的SPI DMA可以设置为固定长度,中断服务程序的处理时间恒定,这极大地简化了系统调度和资源管理。当你需要以非常稳定和可预测的速率读取数据时(例如用于数字滤波或闭环控制),固定帧是首选。
固定帧的组成结构:以一个四通道的ADS131A04为例,在固定帧模式下,一帧数据总是包含以下部分(假设设备字长度设置为24位,CRC禁用):
- DIN侧(主机发送给ADC):
- 命令字(Command Word,16位):第一个设备字。用于发送指令,如读取寄存器(RREG)、写入配置(WREG)或空操作(NULL)。高16位为命令码,低8位在24位模式下补零。
- 填充字(Padding Words):为了填满固定长度的帧,后续需要发送若干个全零的设备字。数量 = 固定帧总设备字数 - 1(命令字)。
- DOUT侧(ADC返回给主机):
- 状态字(Status Word,16位):第一个设备字。包含设备状态信息(如复位标志、数据就绪标志、CRC错误标志等)以及对上一个命令的响应(ACK)。同样,在24位模式下低8位补零。
- 通道数据字(Channel Data Words):紧接着状态字,每个使能的通道会按顺序(CH1, CH2, ...)输出其24位转换数据。关键点来了:即使某个通道被禁用(在配置寄存器中关闭),它在数据帧中的“位置”仍然会被保留,并以全零数据字填充。这是固定帧的核心特征。
- 填充字:如果使能的通道数少于最大通道数,或者帧长度设置大于“状态字+所有通道数据字”的总和,剩余位置也用全零设备字填充。
实操心得:固定帧模式下,最常犯的错误就是主机发送的时钟数不足。你一定要根据
D_SYS_CFG寄存器中的FIXED位和CHn_EN寄存器设置,精确计算出帧长度(设备字数)。计算不准,会导致帧未结束就拉高CS,造成通信错位,后续数据全部乱套。我的习惯是,在初始化代码中用一个宏或常量明确声明这个帧长度,并在每次SPI传输配置中显式使用它。
2.2 动态帧模式:追求传输效率的极致
动态帧模式是效率至上的选择。在此模式下,一帧SPI通信的长度不再是固定的,而是动态适应于当前ADC的有效数据量。只有那些真正被使能(Enabled)的通道,它们的数据字才会出现在DOUT帧中。
动态帧的优势与应用场景:最大的优势是节省了通信时间。例如,你的ADS131A04只使用了通道1和通道4进行采样。在固定帧模式下,你仍然需要为通道2和通道3的“空位”花费时钟周期。而在动态帧模式下,DOUT帧将只包含状态字、通道1数据、通道4数据,通信时间大幅缩短。这在电池供电的便携设备中,能直接降低功耗;在多任务系统中,能释放更多CPU时间。
动态帧的结构特点:
- 长度可变:帧长度 = 1(状态字) + N(使能通道数)。主机必须能够处理这种可变长度的接收。通常,这需要MCU的SPI接口支持“在指定事件(如接收缓冲区满或超时)后自动停止”的功能,或者由软件根据已知的使能通道数来精确控制SCLK的脉冲数量。
- 无无效填充:DOUT帧中紧挨着状态字的,就是所有使能通道的有效数据,顺序与通道号一致,但中间没有禁用通道的占位符。DIN侧的填充规则也可能相应变化(取决于CRC等设置)。
模式选择背后的工程权衡:选择固定帧还是动态帧,是一个典型的“确定性 vs 效率”的权衡。
- 选固定帧:当你需要极致的时序确定性,或者你的MCU SPI驱动/ DMA难以优雅地处理可变长度传输时。许多实时操作系统(RTOS)的任务和中断设计更偏好固定周期的事件。
- 选动态帧:当功耗敏感、需要最大化数据吞吐率,或者MCU的SPI外设非常智能(如支持硬件CRC和自动帧结束检测)时。在复杂的多传感器系统中,动态帧能有效减少总线占用时间。
注意事项:动态帧模式下,主机程序必须严格根据配置寄存器中记录的使能通道数,来推算每次需要读取的数据长度。一个常见的坑是:在系统运行中动态地启用或禁用某个通道(虽然不常见),但读取数据的代码没有同步更新长度计算,导致要么数据读不完,要么多读了无用的零,造成后续数据解析的错位。建议将通道使能状态与数据读取长度绑定在一个函数或结构体中管理。
3. CRC校验:为数据帧加上“数字指纹”
循环冗余校验(CRC)是一种检错能力极强的散列函数。在ADS131A0x中,CRC是一个可选项,通过设置D_SYS_CFG寄存器的CRC_EN位来启用。一旦启用,每一帧数据的末尾(最后一个设备字)都会附加一个16位的CRC值。
3.1 CRC在ADS131A0x中的工作逻辑
芯片采用的算法是标准的CRC-16/CCITT-FALSE,其多项式为0x1021,初始值为0xFFFF。这个算法在工业通信(如Modbus)中广泛应用,有大量现成的库和硬件加速支持。
CRC的计算与校验范围:这是配置中最容易混淆的地方,因为它受到CRC_MODE和FIXED两个位的共同控制。
对于输入(DIN,主机到ADC):
- 当
FIXED = 0(动态帧)时,所有设备字都参与CRC计算。 - 当
FIXED = 1(固定帧)且CRC_MODE = 1时,同样是所有设备字参与计算。 - 当
FIXED = 1(固定帧)且CRC_MODE = 0时,只有命令字和寄存器数据字(对于WREGS命令)参与计算,填充的零值字被排除在外。这种模式用于优化,避免为无意义的零值计算CRC。
- 当
对于输出(DOUT,ADC到主机):
- 当
FIXED = 0(动态帧)时,帧中所有设备字都参与CRC计算。 - 当
FIXED = 1(固定帧)且CRC_MODE = 1时,所有设备字参与计算。 - 当
FIXED = 1(固定帧)且CRC_MODE = 0时,只有命令字、状态字和有效的通道数据字参与计算,填充的零值字被排除。
- 当
CRC校验的流程与错误处理:
- 发送端计算:主机在发送一帧数据前,根据上述规则,对DIN帧中需要校验的部分计算CRC,并将结果填入帧的最后一个设备字的高16位。
- 接收端验证:ADC在接收到DIN帧后,会立即用相同的算法和规则计算接收到的数据的CRC值,并与接收到的CRC字进行比较。
- 如果匹配:命令正常执行(WREGS命令除外,它执行后检查)。
- 如果不匹配:
STAT_1寄存器中的F_CHECK标志位会被置1,并且除了WREGS命令外,当前帧的命令将被忽略,不会执行。同时,一个错误计数器(ERROR_CNT)会递增。
- ADC响应:在下一帧DOUT数据的状态字中,ADC会包含其计算出的CRC值(针对上一帧DOUT数据)。主机在收到数据后,也应按照相同规则重新计算CRC,并与ADC返回的CRC进行比对,以验证下行数据的完整性。
3.2 配置CRC的实操步骤与代码示例
假设我们使用固定帧模式,使能了4个通道,设备字长为24位,并希望启用CRC校验。
步骤一:确定帧结构首先,我们需要知道一帧有多少个设备字。查阅手册可知,在固定帧、CRC使能、4通道使能的情况下,DOUT帧结构为:[状态字] + [CH1数据] + [CH2数据] + [CH3数据] + [CH4数据] + [CRC字]。共6个设备字。DIN帧结构类似:[命令字] + [填充/数据字...] + [CRC字],也需要6个字。
步骤二:配置寄存器通过SPI写入配置寄存器D_SYS_CFG,设置CRC_EN = 1。同时,根据你的需求设置CRC_MODE和FIXED位。例如,我们选择FIXED=1, CRC_MODE=0,这样CRC计算会忽略填充的零,效率稍高。
步骤三:实现CRC计算函数以下是一个C语言实现的CRC-16/CCITT-FALSE计算函数,适用于大多数嵌入式平台:
uint16_t calculate_crc16_ccitt(const uint8_t *data, size_t length) { uint16_t crc = 0xFFFF; // 初始值 for (size_t i = 0; i < length; ++i) { crc ^= (uint16_t)data[i] << 8; // 将字节移入高位 for (int j = 0; j < 8; ++j) { if (crc & 0x8000) { crc = (crc << 1) ^ 0x1021; // 多项式 } else { crc <<= 1; } } } return crc; }步骤四:组装数据帧并发送在发送命令或数据时,先组装除CRC字之外的所有设备字(24位对齐,低8位可能补零)。将这些字的高16位(因为CRC计算基于16位字)按顺序提取出来,组成一个字节数组,传入CRC计算函数。将得到的16位CRC值,放入最后一个设备字的高16位,低8位置零,然后发送整个帧。
步骤五:接收验证接收数据时,同样地,在解析通道数据前,先提取DOUT帧中前5个设备字的高16位,计算CRC,与接收到的第6个设备字(CRC字)的高16位进行比较。如果不匹配,则应丢弃本帧数据,触发错误处理流程(如重读、记录日志、报警等)。
踩坑记录:我曾在一个项目中遇到间歇性的配置写入失败,但读取回配置值又是正确的假象。后来发现,是CRC计算的范围搞错了。我使用的库函数默认对整个数据缓冲区(包括命令字后的所有零填充字)计算CRC,而我的
CRC_MODE设置的是0(忽略填充字)。这导致ADC端计算的CRC与我发送的CRC总是不匹配,因此我的WREG命令实际上被ADC静默忽略了!但由于我紧接着发送了RREG命令去读取,读回的是寄存器旧值,我却误以为写入成功了。教训是:在启用CRC后,任何配置变更后,一定要读取状态字(STAT_1)中的F_CHECK标志,确认命令是否被真正执行。
4. 汉明码:从检错到纠错的飞跃
如果说CRC是一位严格的质检员,发现错误就整框退货(丢弃帧),那么汉明码(Hamming Code)就更像一位拥有“修正液”的编辑,它不仅能发现错误,还能自动纠正单个比特的错误,并检测两个比特的错误。这对于要求极高可靠性和连续性的系统(如某些医疗影像或高速旋转机械监测)来说,价值巨大。
4.1 汉明码在ADS131A0x中的实现机制
在ADS131A0x中,汉明码是一个通过硬件引脚M2来全局启用或禁用的功能(与M1引脚配合决定字长),无法通过软件寄存器单独控制。启用后,每一个设备字(无论是命令字、状态字、数据字还是CRC字)都会在原有的数据位之后,额外附加一个8位的汉明码字节。
这8位汉明码的构成如下:
- 5位汉明校验位(H0-H4):用于纠错的核心位。它们通过奇偶校验覆盖数据位中特定的子集。
- 2位校验和位(ChS):用于增强对多比特错误的检测能力。
- 1位固定零位:总是为0。
当芯片发送数据时,它会根据24位(或16位)有效数据,实时计算出这8位汉明码并附加上去。当主机收到一个32位(24位数据+8位汉明码)或24位(16位数据+8位汉明码)的设备字后,可以利用汉明码算法进行校验和纠错。
纠错原理简述:汉明码通过将校验位插入到数据位的特定位置,形成一个“校验矩阵”。每个校验位负责对一组数据位进行奇偶校验。接收端重新计算这些校验位,并与接收到的校验位进行比较,产生一个称为“症状(Syndrome)”的指错码。这个指错码的值直接对应了出错比特的位置(如果是单比特错误)。如果是0,则表示无错;如果非0且能映射到某个位置,则纠正该位;如果指错码指示了一个不可能的位置(如校验位本身出错模式),则可能检测到多比特错误。
4.2 启用汉明码的硬件配置与权衡
启用汉明码需要将M2引脚上拉到IOVDD(通过一个小于1kΩ的电阻)。此时,设备字长和行为取决于M1引脚:
M1接IOVDD:设备字长 =32位。其中24位是ADC转换数据,8位是汉明码。这是高数据完整性模式,你获得了完整的24位分辨率和纠错能力。M1接GND:设备字长 =24位。其中16位是ADC转换数据(高16位),8位是汉明码。这是兼容性与纠错折衷模式,你损失了8位分辨率(LSB被截断),但获得了纠错能力。
启用汉明码带来的影响:
- 数据吞吐率下降:每个通道的数据传输量增加了33%(从24位到32位)或50%(从16位到24位)。这意味着同样的采样率下,SPI时钟频率需要更高,或者总线的带宽占用更大。
- CRC计算的变化:当汉明码启用时,CRC计算会自动忽略每个设备字末尾的8位汉明码字节。CRC只保护原始的数据部分。这是合理的,因为汉明码本身是用于保护数据的,它不应该再被CRC保护,否则就循环了。
- 主机端处理开销:MCU需要在接收数据后,对每一个设备字执行汉明码解码算法,以检查并可能纠正错误。这增加了软件复杂度或需要硬件协处理。
4.3 汉明码的实战应用与算法实现
是否使用汉明码,取决于你的应用对数据完整性的要求等级。对于大多数工业环境,CRC检错+重传机制已经足够可靠。但在强干扰环境,且重传延迟不可接受(如高速连续采样)时,汉明码的实时纠错能力就成为关键。
一个简单的汉明码(24, 19)解码纠错函数示意(针对ADS131A0x的24位数据+5位汉明码情况,简化模型):实际芯片的汉明码位插入位置是交错的(见其手册中的覆盖表),以下代码展示原理。
#define HAMMING_PARITY_MASK_0 0x... // 根据手册Table 11定义校验位0覆盖的数据位掩码 #define HAMMING_PARITY_MASK_1 0x... // 校验位1掩码 // ... 定义 HAMMING_PARITY_MASK_2,3,4 typedef struct { uint32_t raw_word; // 接收到的32位原始字(24位数据 + 8位汉明码) uint24_t corrected_data; // 纠正后的24位数据 uint8_t syndrome; // 指错码 bool is_single_error; // 是否是单比特错误 bool is_double_error; // 是否是双比特错误(可检测,不可纠正) } hamming_result_t; hamming_result_t hamming_decode(uint32_t received_word) { hamming_result_t result; result.raw_word = received_word; uint24_t data = (received_word >> 8) & 0xFFFFFF; // 提取24位数据 uint8_t received_hamming = received_word & 0xFF; // 提取8位汉明码 uint8_t calculated_hamming = calculate_hamming_bits(data); // 根据数据重新计算汉明码 uint8_t syndrome = received_hamming ^ calculated_hamming; // 计算指错码 result.syndrome = syndrome; result.is_single_error = false; result.is_double_error = false; if (syndrome == 0) { // 无错误 result.corrected_data = data; } else if (is_power_of_two(syndrome)) { // 指错码是2的幂,表示一个汉明校验位本身出错 // 数据本身没错,无需纠正,但记录发生了校验位传输错误 result.corrected_data = data; result.is_single_error = true; // 标记为单比特错误(发生在校验位) } else { // 指错码非零且不是2的幂,可能指示数据位错误 int error_bit_position = lookup_error_position(syndrome); // 查表找到出错比特位 if (error_bit_position >= 0 && error_bit_position < 24) { // 有效位置,纠正单比特错误 data ^= (1UL << error_bit_position); result.corrected_data = data; result.is_single_error = true; } else { // 无法映射到单个数据位,可能是双比特错误 result.corrected_data = data; // 无法纠正,返回原始数据 result.is_double_error = true; // 标记为检测到不可纠正错误 } } return result; }在实际使用中,你需要根据芯片手册Table 11. ADS131A0x Hamming Codes精确实现calculate_hamming_bits和lookup_error_position函数。这个表定义了24个数据位(D0-D23)与5个汉明校验位(H0-H4)之间的奇偶校验关系。
经验之谈:汉明码的纠错能力非常诱人,但它不是免费的午餐。除了增加带宽开销,最大的挑战在于实时解码的计算量。如果采样率很高(如几十kSPS),对每个样本进行软件汉明解码可能会成为MCU的沉重负担。我的建议是:首先评估你的电磁环境是否真的恶劣到需要汉明码;其次,如果必须使用,尽量寻找MCU的硬件加速支持,或者使用FPGA来预处理SPI数据流,完成汉明解码后再将干净的数据交给MCU。对于STM32等主流MCU,可以考虑使用查表法来加速指错码到错误位置的映射。
5. 三种SPI接口模式详解与选型
ADS131A0x提供了三种SPI接口模式,通过M0引脚在上电时锁存配置。选择哪种模式,决定了谁控制时钟、如何同步数据,这直接关系到系统整体架构和软件驱动设计。
5.1 异步中断模式:最经典灵活的用法
这是最常用、也最符合传统SPI主从思维的模式。在此模式下:
- MCU是绝对的主机(Master),提供片选
CS、时钟SCLK。 - ADC是从设备(Slave),通过
DRDY引脚输出中断信号,告知MCU“新数据已就绪”。 - 通信由MCU主动发起:MCU检测到
DRDY变低后,在合适的时间拉低CS,并产生SCLK读取数据。
关键信号解析:
DRDY:数据就绪输出。一次转换完成后,DRDY产生一个高到低的跳变。它会在整个数据帧传输期间保持低电平,直到CS被拉高或新数据再次就绪前才变高。特别注意:如果上一帧数据还没读完,新数据又准备好了,DRDY会短暂变高再变低,并且STAT_1寄存器的F_DRDY标志会被置位,表示新数据因覆盖而丢失。这提示你的读取速度必须快于采样率。CS:帧同步信号。必须在一个完整的数据帧传输期间保持低电平。拉高CS会复位ADC的SPI接口状态机。DIN/DOUT:在SCLK的下降沿采样DIN,上升沿更新DOUT。
适用场景:绝大多数由MCU主导的采集系统。MCU可以灵活控制读取时机,方便与其他任务协作。需要MCU有一个外部中断引脚连接DRDY。
5.2 同步主机模式:ADC反客为主
这是一种比较特殊的模式,ADC变成了时钟的提供者。
- ADC是时钟主机:
SCLK和DRDY都成为ADC的输出信号。 - MCU变成时钟从设备:MCU的SPI接口需要配置为从模式(Slave),使用ADC提供的
SCLK来接收数据。 - 连接方式:
CS引脚需要与DONE引脚短接。DRDY的下降沿不仅指示数据就绪,还自动启动一帧数据的输出。
工作流程:ADC内部转换完成 ->DRDY输出下降沿 -> ADC开始从DOUT引脚在自产的SCLK上升沿同步输出数据 -> MCU作为从设备,在同样的SCLK下读取DOUT。帧结束时,DRDY变高。
优势与挑战:
- 优势:时序完全由ADC内部时钟派生,极其精准和稳定,避免了MCU软件延迟或中断响应抖动带来的时序问题。简化了MCU端的驱动,MCU无需生成时钟,只需被动读取。
- 挑战:MCU的SPI从模式可能不如主模式常用,需要仔细配置。
SCLK的频率由ADC的ICLK决定,必须确保MCU的SPI从模式能跟上这个速度。此外,DRDY启动帧后,MCU必须及时响应并读完整个帧,否则数据会丢失。
适用场景:需要ADC作为精确时序源,或者MCU主SPI端口被其他更重要设备占用,可以利用从SPI端口来接收ADC数据流。
5.3 同步从模式:外部事件同步的利器
此模式下,ADC是完全的从设备,但引入了一个关键的同步概念。
- MCU提供一切:
CS,SCLK,DIN均由MCU提供。 DRDY变为输入信号:这是一个关键区别!MCU需要向ADC的DRDY引脚输入一个脉冲信号,其下降沿用于同步ADC内部的数据更新时刻。- 同步目的:这个模式主要用于多片ADC的采样同步。一个MCU可以产生一个统一的
DRDY同步脉冲,同时发给多个处于同步从模式的ADS131A0x,这样所有ADC的采样转换周期将严格对齐,这对于需要计算通道间相位关系的应用(如功率分析、多轴振动分析)至关重要。
注意事项:
- MCU输入的
DRDY脉冲频率必须等于或整数倍于ADC编程设置的数据输出率(f_DATA)。否则,ADC会检测到失步,置位F_RESYNC标志,并复位其数字滤波器,导致数据无效。 - 为了节省连线,可以将
CS和DRDY短接,用一个信号同时控制片选和同步。但必须满足手册中t_SU(DRDY-CS)等严格的时序要求。
模式选择决策树:
- 是否需要多片ADC严格同步采样?是 -> 选择同步从模式。
- 是否需要ADC提供最稳定、与内部时钟锁定的数据输出时序?是 -> 选择同步主机模式(需评估MCU SPI从模式性能)。
- 以上都不是,追求最大的配置灵活性和通用性?是 -> 选择异步中断模式。
6. 命令集详解与可靠通信编程实践
ADS131A0x的所有控制和配置都通过SPI命令完成。命令是一个16位的字,总是放在DIN帧的第一个设备字的高16位。理解每个命令的语义和响应,是编写稳定驱动的基础。
6.1 核心命令解析
- NULL (0x0000):空操作。最常用的命令,用于在单纯读取转换数据时填充命令字位置。发送NULL命令后,下一帧DOUT将返回状态字和最新的转换数据。
- RESET (0x0011):软件复位。将使所有寄存器恢复默认值,并需要等待一段启动时间(见手册
t_RST)。重要:发送RESET命令后,ADC会返回一个特殊的READY状态字(0xFFdd,其中dd是器件ID),而不是通常的ACK。你必须先收到READY,然后发送UNLOCK命令,之后ADC才会响应其他命令。 - STANDBY (0x0022) / WAKEUP (0x0033):进入低功耗待机模式和唤醒。进入待机后,ADC停止转换,功耗大幅降低。唤醒后需要重新配置寄存器吗?根据我的测试,寄存器配置会保留,但为了保险起见,唤醒后重新配置关键寄存器(如增益、数据速率)是一个好习惯。
- LOCK (0x0555) / UNLOCK (0x0655):锁定与解锁。
LOCK后,除了NULL,RREGS,UNLOCK外的所有命令将被忽略。这是一种软件保护机制,防止配置被意外修改。UNLOCK用于解除锁定,或在上电复位后使设备进入可操作状态。 - RREG (0x1AAA NNNN):读单个寄存器。AAAA是4位寄存器地址,NNNN是保留位(写0)。下一帧DOUT的状态字位置,将返回该寄存器的值(格式:
0b001a_aaaa_dddd_dddd)。 - RREGS (0x3AAA NNNN):读多个寄存器。AAAA是起始地址,NNNN是寄存器数量减一。这是一个多帧事务。发送此命令后,ADC会在后续连续的多个DOUT帧中,每个帧返回两个寄存器的值(共16*2=32位,填充成24或32位字),直到所有请求的寄存器读完。在此期间,发送给ADC的任何新命令都会被忽略。
- WREG (0x4AAA DDDD):写单个寄存器。AAAA是地址,DDDD是16位数据。命令执行后,下一帧DOUT的状态字会回显写入的地址和数据(格式同RREG响应),作为确认。
- WREGS (0x5AAA NNNN):写多个寄存器。最复杂的命令。AAAA是起始地址,NNNN是寄存器数量减一。发送此命令后,主机必须在同一帧内,紧接着发送足够多的设备字来提供要写入的数据(每两个寄存器用一个设备字,高16位是第一个寄存器数据,低16位是第二个)。帧会被动态扩展。特别注意:此命令的CRC校验是在整个扩展帧完成后才进行的,且即使CRC错误,寄存器写入操作也可能已发生(但会置位错误标志)。
6.2 命令交互的状态机与超时处理
与ADS131A0x通信本质是一个状态机。可靠的驱动必须处理以下状态:
- 上电/复位后状态:设备处于“锁定”态,只响应
NULL,RREGS,UNLOCK。驱动上电后应:等待电源稳定 -> 发送NULL命令直至收到READY状态字 -> 发送UNLOCK命令收到ACK (0x0655)-> 进入正常配置态。 - 正常操作态:可以发送任何命令。每次发送命令后,必须在下一帧DOUT的第一个字验证状态响应。
- 收到
ACK (cmd):命令成功执行。 - 收到
STATUS/REG (0b001a_aaaa_dddd_dddd):这是对RREG或WREG的响应,其中包含寄存器数据。 - 收到
RREGS响应头:接下来进入连续读寄存器数据子状态。
- 收到
- 错误态:读取状态字
STAT_1,检查F_CHECK(CRC错)、F_DRDY(数据丢失)、F_RESYNC(同步错) 等标志。根据错误类型进行重试、复位或报警。
必须实现的超时机制:
DRDY等待超时:在异步中断模式,等待DRDY下降沿的超时。如果超时,可能意味着ADC未正常工作或时钟有问题。- 命令响应超时:发送一个命令后,应在合理时间内(通常远小于数据输出周期)收到DOUT帧。如果超时,说明SPI通信链路可能断开。
- 多帧事务超时:对于
RREGS,需要为读取所有预期数据设置一个总超时。
6.3 驱动层封装示例
一个健壮的驱动应该将底层SPI收发、CRC计算、命令组装/解析、状态机管理和错误处理封装起来,向上提供简洁的API。
typedef enum { ADS131AXX_OK = 0, ADS131AXX_ERROR_TIMEOUT, ADS131AXX_ERROR_CRC, ADS131AXX_ERROR_DRDY_LOST, ADS131AXX_ERROR_NOT_READY, ADS131AXX_ERROR_CMD_FAILED, } ads131a_error_t; typedef struct { SPI_HandleTypeDef *hspi; GPIO_TypeDef *cs_port; uint16_t cs_pin; GPIO_TypeDef *drdy_port; uint16_t drdy_pin; uint8_t device_word_len; // 16, 24, 32 bool crc_enabled; bool fixed_frame; // ... 其他配置 } ads131a_dev_t; // 核心发送-接收一帧数据(包含CRC处理) static ads131a_error_t ads131a_transfer_frame(ads131a_dev_t *dev, uint32_t *tx_data, uint32_t *rx_data, uint16_t word_count) { // 1. 计算TX数据的CRC(如果使能) if (dev->crc_enabled) { uint16_t crc = compute_crc_for_frame(tx_data, word_count - 1, dev->fixed_frame, dev->device_word_len); tx_data[word_count - 1] = (crc << 16); // 假设24位模式,CRC放高16位 } // 2. 拉低CS HAL_GPIO_WritePin(dev->cs_port, dev->cs_pin, GPIO_PIN_RESET); // 3. SPI传输 if (HAL_SPI_TransmitReceive(dev->hspi, (uint8_t*)tx_data, (uint8_t*)rx_data, word_count, TIMEOUT_MS) != HAL_OK) { HAL_GPIO_WritePin(dev->cs_port, dev->cs_pin, GPIO_PIN_SET); return ADS131AXX_ERROR_TIMEOUT; } // 4. 拉高CS HAL_GPIO_WritePin(dev->cs_port, dev->cs_pin, GPIO_PIN_SET); // 5. 验证RX数据的CRC(如果使能) if (dev->crc_enabled) { if (!verify_crc_for_frame(rx_data, word_count, dev->fixed_frame, dev->device_word_len)) { dev->last_error_flag |= ERROR_FLAG_CRC; return ADS131AXX_ERROR_CRC; } } // 6. 解析状态字,检查错误标志(F_CHECK, F_DRDY等) uint16_t status_word = (rx_data[0] >> 16) & 0xFFFF; if ((status_word & STATUS_F_CHECK_MASK) != 0) { return ADS131AXX_ERROR_CMD_FAILED; } // ... 检查其他状态位 return ADS131AXX_OK; } // 上层API:读取转换数据 ads131a_error_t ads131a_read_data(ads131a_dev_t *dev, int32_t *ch_data, uint8_t num_active_ch) { ads131a_error_t err; uint32_t tx_frame[FRAME_LEN]; uint32_t rx_frame[FRAME_LEN]; // 1. 等待DRDY(异步模式)或延时(同步模式) if (dev->mode == ASYNC_INTERRUPT) { if (wait_for_drdy_low(dev, DRDY_TIMEOUT_MS) != ADS131AXX_OK) { return ADS131AXX_ERROR_TIMEOUT; } } // 2. 组装TX帧:命令字(NULL) + 填充字 tx_frame[0] = CMD_NULL << 16; for (int i = 1; i < FRAME_LEN - (dev->crc_enabled ? 1 : 0); i++) { tx_frame[i] = 0; } // 3. 传输帧 err = ads131a_transfer_frame(dev, tx_frame, rx_frame, FRAME_LEN); if (err != ADS131AXX_OK) { return err; } // 4. 从RX帧中提取通道数据(跳过状态字) for (int ch = 0; ch < num_active_ch; ch++) { uint32_t data_word = rx_frame[1 + ch]; // 假设状态字后紧跟数据 // 根据字长和汉明码设置,提取并转换数据... // 例如,24位数据,补码转有符号整数 int32_t raw = (data_word >> 8) & 0xFFFFFF; // 去掉低8位(零或汉明码) if (raw & 0x800000) { // 检查符号位(第23位) raw |= 0xFF000000; // 符号扩展至32位 } ch_data[ch] = raw; } return ADS131AXX_OK; }通过这样的分层设计,应用层只需调用ads131a_read_data即可获得可靠的采样值,所有底层的复杂性、错误处理和状态管理都被封装在驱动层内。