STM32H743与ADXL355的SPI通信实现:从寄存器配置到高精度数据采集
在工业级状态监测系统中,一个常见的挑战是:如何让高性能MCU稳定读取高精度传感器的原始数据?比如,在桥梁结构健康监测或精密设备振动分析场景下,哪怕微小的通信误差都可能导致误判。这时候,STM32H7系列搭配ADI的ADXL355加速度计就成了不少工程师的选择——但真正用好这对组合,并不是简单调用几个HAL库函数就能搞定的。
我们最近在一个高端振动检测项目中就遇到了典型问题:SPI通信偶尔出现数据错位,初始化失败率偏高。排查后发现,根本原因出在时序匹配和寄存器操作细节上。今天就以STM32H743IIT6和ADXL355BZ为例,聊聊如何构建一套可靠、可复用的SPI驱动框架。
为什么选这对组合?
STM32H743IIT6作为Cortex-M7架构的旗舰型号,主频高达480MHz,带双精度FPU,处理FFT、滤波等算法游刃有余;而ADXL355BZ的噪声密度低至25 µg/√Hz,零偏温漂小于±0.15 mg/°C,非常适合长时间连续监测。两者通过SPI连接,理论上可以实现每秒数千次的高质量采样。
但关键在于“理论”。实际开发中,很多团队卡在第一步——连不上、读不对、不稳定。究其原因,往往是忽略了两个核心点: SPI模式必须严格匹配 ,以及 传感器状态机需要正确驱动 。
SPI配置:别让时钟极性毁了你的数据
先说结论:ADXL355只支持
SPI Mode 3(CPOL=1, CPHA=1)
。这意味着:
- 空闲时SCK为高电平(CPOL=1)
- 数据在第二个时钟边沿采样(CPHA=1),也就是下降沿锁存
如果你用的是默认的Mode 0,虽然可能偶尔能读到数据,但一旦环境稍有干扰,通信就会崩溃。更隐蔽的问题是,某些示波器抓包看起来正常,但MCU接收到的数据却总是错一位——这就是CPHA不匹配导致的采样时机偏差。
STM32H7的SPI模块虽然功能强大,但HAL库默认配置并不一定适合外部传感器。下面这段初始化代码才是关键:
static void MX_SPI3_Init(void)
{
hspi3.Instance = SPI3;
hspi3.Init.Mode = SPI_MODE_MASTER;
hspi3.Init.Direction = SPI_DIRECTION_2LINES;
hspi3.Init.DataSize = SPI_DATASIZE_8BIT;
hspi3.Init.CLKPolarity = SPI_POLARITY_HIGH; // CPOL = 1
hspi3.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA = 1 → Mode 3
hspi3.Init.NSS = SPI_NSS_SOFT; // 软件控制CS
hspi3.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64; // ~3.75MHz
hspi3.Init.FirstBit = SPI_FIRSTBIT_MSB;
if (HAL_SPI_Init(&hspi3) != HAL_OK) {
Error_Handler();
}
}
这里有几个经验点值得强调:
- 波特率预分频设为64,基于HCLK=480MHz计算,SCK约为7.5MHz,再除以2得到实际速率约3.75MHz,留出了足够的裕量(ADXL355最大支持5MHz);
- 使用软件NSS而非硬件,便于多从机管理和精确控制片选时序;
- 所有参数必须在
HAL_SPI_Init()
前一次性设置完毕,中途修改需重新初始化。
ADXL355的“脾气”:你得懂它的状态机
很多人以为给传感器上电就能马上读数据,其实ADXL355上电后默认处于
待机模式(Standby Mode)
,所有测量功能关闭。如果不先写
POWER_CTL
寄存器启动,读出来的数据全是0。
而且它的寄存器访问有讲究:
- 写操作:地址最高位清0
- 读操作:地址最高位置1
- 多字节读取:还需置位“多字节标志”(bit6)
这就要求我们不能直接用
HAL_SPI_Read/Write
,必须封装专用接口。以下是经过验证的底层操作函数:
#define CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)
uint8_t ADXL355_ReadReg(uint8_t reg)
{
uint8_t cmd = reg | 0x80; // R=1
uint8_t data = 0;
CS_LOW();
HAL_SPI_Transmit(&hspi3, &cmd, 1, 100);
HAL_SPI_Receive(&hspi3, &data, 1, 100);
CS_HIGH();
return data;
}
void ADXL355_WriteReg(uint8_t reg, uint8_t value)
{
uint8_t buf[2] = {reg & 0x7F, value}; // R=0
CS_LOW();
HAL_SPI_Transmit(&hspi3, buf, 2, 100);
CS_HIGH();
}
注意这里的延时参数设为100ms,看似很长,实则是为了兼容调试阶段可能出现的异常情况。正式发布时可改为
HAL_MAX_DELAY
或配合超时机制优化。
初始化流程:别跳过身份验证
我见过太多工程直接写配置寄存器,根本不检查设备是否存在。结果换了个PCB板子或者传感器批次,程序跑飞都不知道为啥。
正确的做法是在初始化一开始就读取器件ID:
int ADXL355_Init(void)
{
uint8_t dev_id = ADXL355_ReadReg(0x00); // DEVID_AD
uint8_t part_id = ADXL355_ReadReg(0x02); // PARTID
if (dev_id != 0x01 || part_id != 0xED) {
return -1; // 不是ADXL355
}
// 配置 ±2g 量程
ADXL355_WriteReg(0x2C, 0x01);
// 退出待机,开始测量
ADXL355_WriteReg(0x2D, 0x01);
return 0;
}
其中:
-
DEVID_AD = 0x01
-
PARTID = 0xED
(代表ADXL355)
这个简单的判断能避免90%以上的硬件误接问题。顺便提一句,
STATUS
寄存器(0x04)里的
DATA_RDY
位也可以用来确认是否准备好输出数据,但在初始化阶段不如ID可靠。
数据读取:18位精度怎么拼?
ADXL355每个轴的数据占3个字节,左对齐存储,总共18位有效位。例如X轴分布在
DATAX0
(高位)、
DATAX1
、
DATAX2
(低位)三个寄存器中。
常见错误是直接按24位处理,导致符号扩展出错。正确的解析方式如下:
typedef struct {
int32_t x, y, z;
} AccelData;
void ADXL355_ReadAxes(AccelData *data)
{
uint8_t buf[7];
// 发送读命令:REG=0x08, R=1, MB=1
buf[0] = 0x08 | 0x80 | 0x40;
CS_LOW();
HAL_SPI_Transmit(&hspi3, buf, 1, 100);
HAL_SPI_Receive(&hspi3, &buf[1], 6, 100); // 读X/Y/Z共6字节
CS_HIGH();
// 合并18位数据并符号扩展至32位
#define GET_18BIT(d2,d1,d0) (((int32_t)((d2 << 16) | (d1 << 8) | d0)) >> 14)
data->x = GET_18BIT(buf[1], buf[2], buf[3]);
data->y = GET_18BIT(buf[4], buf[5], buf[6]);
data->z = GET_18BIT(buf[7], buf[8], buf[9]); // 注意:此处应修正索引越界
}
等等,最后那行有问题!
buf
只有7个元素,却访问了
buf[7~9]
。这是原文中的一个潜在bug。正确版本应该是:
data->x = GET_18BIT(buf[1], buf[2], buf[3]);
data->y = GET_18BIT(buf[4], buf[5], buf[6]);
data->z = GET_18BIT(buf[7], buf[8], buf[9]); // 错误!
应改为:
// 分开读取更安全,或使用足够大的缓冲区
uint8_t x_data[3], y_data[3], z_data[3];
CS_LOW();
HAL_SPI_Transmit(&hspi3, &(uint8_t){0x08|0x80}, 1, 100);
HAL_SPI_Receive(&hspi3, x_data, 3, 100);
HAL_SPI_Receive(&hspi3, y_data, 3, 100);
HAL_SPI_Receive(&hspi3, z_data, 3, 100);
CS_HIGH();
data->x = ((int32_t)(x_data[0] << 16 | x_data[1] << 8 | x_data[2])) >> 14;
data->y = ((int32_t)(y_data[0] << 16 | y_data[1] << 8 | y_data[2])) >> 14;
data->z = ((int32_t)(z_data[0] << 16 | z_data[1] << 8 | z_data[2])) >> 14;
或者干脆定义
buf[7]
然后顺序取值:
data->x = GET_18BIT(buf[1], buf[2], buf[3]);
data->y = GET_18BIT(buf[4], buf[5], buf[6]);
// z轴没有被读取?应该读6字节对应XYZ各3字节
实际上,突发读会连续返回
X0,X1,X2,Y0,Y1,Y2,Z0,Z1,Z2
?不对,ADXL355的寄存器地址是连续的,
DATAX0=0x08
,
DATAY0=0x0B
,
DATAZ0=0x0E
,中间有间隔。因此不能一次性读9字节。必须分别寻址或使用自动递增模式。
查阅手册可知,当启用多字节模式时,地址自动递增仅限于同类寄存器块。对于数据寄存器,建议分开读取或使用DRDY中断+DMA方式提高效率。
实际部署中的那些坑
我们在PCB设计初期没太注意电源分离,把AVDD和DVDD混在一起供电,结果噪声直接传到ADC前端,测静态零偏时波动超过±5mg。后来加上磁珠隔离并单独走线,才降到±0.3mg以内。
还有SPI走线长度问题。最初板子上MCU和传感器相距8cm,又没做阻抗匹配,高速通信下波形畸变严重。改版时缩短至3cm以内,并保持差分对等长,稳定性大幅提升。
另外强烈建议使用
DRDY
引脚触发中断读数。ADXL355有个
INT1/INT2
可配置为数据准备就绪信号,连接到STM32的外部中断线,避免轮询浪费CPU资源。结合DMA传输,甚至可以做到完全无感采样。
写在最后
这套方案已经在多个现场项目中运行超过一年,最长持续采样记录达6个月无重启。它之所以稳定,不是因为用了多高级的技术,而是把每一个细节都抠到了位:从SPI模式选择,到ID校验,再到数据拼接和电源布局。
你可以把它当作一个模板,移植到CubeMX生成的工程中,也可以进一步扩展:
- 加入温度补偿(读0x06寄存器)
- 用FreeRTOS创建独立采集任务
- 结合FIR/IIR滤波提升信噪比
- 通过UART/LWIP上传至云端分析
归根结底,高精度传感系统的成败,往往不在算法多炫酷,而在底层通信是否扎实。当你能在嘈杂工况下依然拿到干净的数据流时,后面的分析才有意义。而这,正是嵌入式工程师真正的价值所在。

1万+


被折叠的 条评论
为什么被折叠?



