资讯详情

资讯详情

STM32 IIC驱动ADXL345三轴加速度计:从寄存器配置到串口输出避坑指南

简介这份资源面向正在学习STM32嵌入式开发、需要驱动加速度传感器的初学者与工程师解决ADXL345数据采集与姿态角度换算的实操问题。包内以STM32F103为主控采用模拟IIC协议与ADXL345通信并将三轴加速度值经串口换算为角度输出代码经作者亲自测试可用。压缩包共128个文件约2.12MB包含32个c源文件与33个h头文件覆盖stm32f10x系列外设驱动及ADXL345相关逻辑另有uvproj、uvopt等Keil工程文件、hex与axf可执行文件、map与lst编译映射清单以及若干bak备份与htm说明文档工程结构完整可直接编译下载验证。目前已有3757人学习下载。读者可从中获得模拟IIC时序实现、传感器寄存器配置、角度换算与串口打印的完整参考并借助工程文件快速复现调试适合作为课程设计、毕业设计或嵌入式入门练手的实践素材。1. 从零手搓 IIC 驱动为什么 ADXL345 总在串口里装死如果你手头有一块带 ADXL345 的模块想用 STM32 通过 IIC 协议把三轴加速度读出来再从串口打印到电脑上看波形大概率会遇到一个很玄学的现象代码编译通过烧录成功串口助手打开结果屏幕上干干净净一个字符都没有。或者更诡异串口有输出但三个轴的数据全是 0或者全是 65535。这不是你一个人的问题。ADXL345 这颗三轴加速度计在嵌入式圈子里属于“经典但容易翻车”的器件。它便宜、好用、量程可调、支持中断但它的 IIC 时序、寄存器上电顺序、数据格式尤其是补码和 13 位对齐有几个非常隐蔽的坑。很多教程只贴一段初始化代码却不告诉你为什么某个寄存器必须先写 0x2D 再写 0x31也不解释为什么读出来的数据要拆成高低字节再拼成有符号整数。这篇笔记面向的是已经会用 STM32 点灯、会配置串口、但对 IIC 和传感器驱动还不太熟的工程师。我会从 ADXL345 的寄存器地图讲起把 IIC 读写时序、初始化序列、数据解析、串口输出格式、以及最常见的五个翻车点全部拆开。目标只有一个你照着做串口助手里能稳定看到三轴加速度的数值并且知道每个参数为什么这么设。2. ADXL345 的寄存器地图与 IIC 读写时序先搞懂再动手2.1 为什么 ADXL345 的 IIC 地址是 0x53 而不是 0xA6ADXL345 支持 IIC 和 SPI 两种接口通过 CS 引脚电平选择。当 CS 接高电平时器件工作在 IIC 模式。它的 7 位 IIC 从机地址是 0x53这个地址是固定的不可通过引脚更改。但很多新手在写代码时会困惑为什么有的例程写 0xA6有的写 0x53原因是 IIC 协议在传输时地址字节是 7 位地址左移一位最低位表示读/写方向。所以 7 位地址 0x53 左移一位得到 0xA6写操作时地址字节为 0xA6读操作时为 0xA7。如果你用 STM32 的硬件 IIC 外设HAL 库函数通常要求传入左移后的 8 位地址如果你用软件模拟 IIC可以自己决定传 7 位还是 8 位。我一般统一用 7 位地址 0x53在发送时再左移并或上读写位这样逻辑更清晰。ADXL345 的寄存器地址是 8 位的从 0x00 到 0x39。常用的寄存器包括寄存器地址名称功能常用值0x00DEVID器件 ID固定为 0xE5只读0x2DPOWER_CTL电源控制决定是否进入测量模式0x08 或 0x000x31DATA_FORMAT数据格式量程、对齐方式0x0B 或 0x080x32~0x37DATAX0~DATAZ1三轴加速度数据低字节在前只读0x38FIFO_CTLFIFO 控制一般默认0x39FIFO_STATUSFIFO 状态只读上电后ADXL345 默认处于待机模式此时它不更新输出数据寄存器。你必须先向 POWER_CTL 写入 0x08把器件切换到测量模式否则读出来的数据永远是 0。这是第一个大坑后面避坑章节会详细说。2.2 用软件模拟 IIC 还是硬件 IIC选型理由与引脚配置STM32 的硬件 IIC 外设在一些老型号上有已知的时序缺陷容易在总线仲裁或时钟拉伸时卡死。虽然新出的 HAL 库做了不少修补但如果你用的是 F1 或 F4 系列我建议直接用两个普通 GPIO 模拟 IIC。模拟 IIC 的好处是时序完全可控出问题容易用逻辑分析仪抓而且不占用硬件 IIC 外设可以留出来接其他器件。模拟 IIC 需要两个引脚SCL 和 SDA。我一般选 PB6 和 PB7因为这两个引脚在很多开发板上已经接了上拉电阻。如果没有外部上拉需要开启 STM32 内部上拉但内部上拉阻值较大约 40kΩ在 400kHz 速率下可能上升沿太慢。稳妥做法是在模块端或板子上加 4.7kΩ 外部上拉。下面是模拟 IIC 的 GPIO 初始化代码以 STM32 HAL 库为例// 模拟 IIC 引脚初始化PB6-SCL, PB7-SDA void IIC_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); // 配置 SCL 和 SDA 为开漏输出开漏模式需要外部上拉 GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; // 开漏输出 GPIO_InitStruct.Pull GPIO_PULLUP; // 内部上拉外部最好再加 4.7k GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); // 初始状态SCL 和 SDA 都拉高 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6 | GPIO_PIN_7, GPIO_PIN_SET); }这段代码把 PB6 和 PB7 设为开漏输出并开启内部上拉。开漏模式的好处是 SDA 线可以实现双向通信输出低电平时拉低输出高电平时释放总线由外部上拉电阻拉高。在读取 ACK 或数据时需要先把 SDA 切换为输入模式或者直接读引脚电平。我习惯在读数据前把 SDA 设为输入读完再切回输出。2.3 IIC 起始、停止、应答三个必须自己写的底层函数模拟 IIC 的核心是六个基本时序起始条件、停止条件、发送一个字节、接收一个字节、发送应答、接收应答。这些函数必须自己写因为不同传感器的时序容忍度不同。ADXL345 支持标准模式 100kHz 和快速模式 400kHz我一般用 100kHz 起步稳定后再提速。// 起始条件SCL 高时SDA 由高变低 void IIC_Start(void) { SDA_OUT(); // SDA 设为输出 SDA_HIGH(); SCL_HIGH(); delay_us(4); SDA_LOW(); // SCL 高时拉低 SDA产生起始条件 delay_us(4); SCL_LOW(); // 钳住 SCL准备发送数据 delay_us(4); } // 停止条件SCL 高时SDA 由低变高 void IIC_Stop(void) { SDA_OUT(); SCL_LOW(); SDA_LOW(); delay_us(4); SCL_HIGH(); delay_us(4); SDA_HIGH(); // SCL 高时拉高 SDA产生停止条件 delay_us(4); } // 发送一个字节返回从机应答位0 表示应答1 表示非应答 uint8_t IIC_SendByte(uint8_t txd) { uint8_t t; SDA_OUT(); SCL_LOW(); for (t 0; t 8; t) { // 从最高位开始发送 if (txd 0x80) SDA_HIGH(); else SDA_LOW(); txd 1; delay_us(2); SCL_HIGH(); // 拉高时钟从机在此时采样 delay_us(2); SCL_LOW(); // 拉低时钟准备下一位 delay_us(2); } // 第 9 个时钟周期读取从机应答 SDA_IN(); // SDA 设为输入 SDA_HIGH(); // 释放 SDA由从机拉低表示应答 delay_us(2); SCL_HIGH(); delay_us(2); t SDA_READ(); // 读取 SDA 电平 SCL_LOW(); delay_us(2); return t; }起始条件的关键是 SCL 为高时 SDA 产生下降沿停止条件是 SCL 为高时 SDA 产生上升沿。发送字节时数据在 SCL 低电平期间变化在 SCL 高电平期间保持稳定从机在 SCL 高电平时采样。第 9 个时钟周期主机释放 SDA从机如果拉低 SDA 表示应答。如果返回 1说明从机没有应答常见原因是地址错误或器件没接好。读字节和读应答的逻辑类似只是方向反过来。这里不再重复贴代码但要注意读数据前必须把 SDA 设为输入并且主机在接收完最后一个字节后要发送非应答NACK然后发停止条件。3. 初始化序列与数据读取把 0xE5 读出来再谈其他3.1 上电后第一件事读 DEVID 确认 IIC 链路通了在写任何初始化之前必须先读 0x00 寄存器确认返回值是 0xE5。这一步能排除 80% 的硬件问题。如果读出来是 0x00 或 0xFF说明 IIC 通信根本没建立后面所有寄存器操作都是徒劳。// 读取 ADXL345 的器件 ID正常应返回 0xE5 uint8_t ADXL345_ReadID(void) { uint8_t id 0; IIC_Start(); IIC_SendByte(0x53 1); // 发送 7 位地址左移 写方向 if (IIC_WaitAck() ! 0) { // 等待应答 IIC_Stop(); return 0; // 无应答返回 0 } IIC_SendByte(0x00); // 发送寄存器地址 DEVID IIC_WaitAck(); IIC_Start(); // 重复起始条件 IIC_SendByte((0x53 1) | 0x01); // 地址 读方向 IIC_WaitAck(); id IIC_ReadByte(0); // 读取一个字节发送 NACK IIC_Stop(); return id; }这段代码展示了 IIC 读寄存器的标准流程起始 → 发送地址写 → 发送寄存器地址 → 重复起始 → 发送地址读 → 读数据 → 发送 NACK → 停止。注意在发送寄存器地址后没有停止条件而是用重复起始条件切换方向这是 IIC 读操作的规范做法。如果这里返回 0xE5说明硬件连接和基本时序都没问题可以继续。3.2 初始化四步走0x2D、0x31、0x38 的顺序不能乱ADXL345 的初始化顺序有讲究。我一般按以下四步走第一步向 0x2D 写入 0x00先让器件进入待机模式确保在配置其他寄存器时不会产生意外中断或数据更新。第二步向 0x31 写入 0x0B。这个值的含义是全分辨率模式bit 3 1±16g 量程bit 1:0 11数据右对齐补码。全分辨率模式下输出分辨率固定为 3.9mg/LSB不随量程变化。如果你只需要 ±2g 量程可以写 0x08此时分辨率是 10 位灵敏度约 3.9mg/LSB。第三步向 0x2D 写入 0x08切换到测量模式。这一步必须在配置完 DATA_FORMAT 之后做否则量程设置可能不生效。第四步如果需要 FIFO配置 0x38如果不用保持默认即可。// ADXL345 初始化序列 void ADXL345_Init(void) { ADXL345_WriteReg(0x2D, 0x00); // 先进入待机模式 HAL_Delay(10); ADXL345_WriteReg(0x31, 0x0B); // 全分辨率±16g右对齐 HAL_Delay(10); ADXL345_WriteReg(0x2D, 0x08); // 切换到测量模式 HAL_Delay(10); }写寄存器的函数和读类似只是方向全是写不需要重复起始。注意每次写操作之间加 10ms 延时给器件内部寄存器更新时间。虽然数据手册说写入立即生效但实际调试中发现不加延时偶尔会出现配置丢失尤其是电源模式切换。3.3 读取三轴数据高低字节拼接与补码转换ADXL345 的数据寄存器从 0x32 到 0x37共 6 个字节依次是 DATAX0、DATAX1、DATAY0、DATAY1、DATAZ0、DATAZ1。低字节在前高字节在后。拼接时要注意先读低字节再读高字节然后组合成 16 位有符号整数。// 读取三轴加速度原始值 void ADXL345_ReadAccel(int16_t *x, int16_t *y, int16_t *z) { uint8_t buf[6]; ADXL345_ReadRegs(0x32, buf, 6); // 连续读 6 个寄存器 // 低字节在前高字节在后拼接成 16 位有符号数 *x (int16_t)((buf[1] 8) | buf[0]); *y (int16_t)((buf[3] 8) | buf[2]); *z (int16_t)((buf[5] 8) | buf[4]); }这里的关键是强制转换为 int16_t。如果直接用 uint16_t 拼接负加速度会变成很大的正数。ADXL345 输出的是二进制补码全分辨率模式下 13 位有效但寄存器是 16 位高 3 位是符号扩展。所以直接按 int16_t 解释即可。连续读多个寄存器时ADXL345 支持地址自动递增。你只需要发送起始地址 0x32然后连续读 6 个字节器件会自动切换到下一个寄存器。这个特性在读取三轴数据时非常方便避免了多次起始-停止的开销。4. 串口输出格式化与数据校验让数据能看、能信、能存4.1 串口配置与 printf 重定向STM32 的串口配置用 HAL 库初始化即可波特率我一般用 115200数据位 8停止位 1无校验。为了能用 printf 直接输出需要重定向 fputc 函数// 重定向 printf 到串口 1 int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; }注意在 Keil 中需要勾选 Use MicroLIB否则 printf 可能无法正常工作。如果你用的是 GCC 工具链需要自己实现 _write 函数。重定向后就可以用 printf 格式化输出三轴数据了。4.2 输出格式设计CSV 还是自定义帧头串口输出的格式决定了你后续怎么用数据。如果只是肉眼观察可以输出成X: 123, Y: -45, Z: 980这样的可读格式。如果要存到电脑上用 Excel 或 Python 画波形建议输出 CSV 格式每行三个数用逗号分隔不加单位。// 主循环中读取并输出加速度 while (1) { int16_t x, y, z; ADXL345_ReadAccel(x, y, z); // 原始值转换为 mg 单位全分辨率下 3.9mg/LSB float x_mg x * 3.9f; float y_mg y * 3.9f; float z_mg z * 3.9f; // CSV 格式输出方便后续解析 printf(%d,%d,%d\r\n, x, y, z); HAL_Delay(50); // 20Hz 输出频率 }这里输出的是原始值不是 mg 值。原始值的好处是保留了全部精度后续在电脑上可以自己换算。如果你直接输出 mg 值浮点数打印会占用更多串口带宽而且在低波特率下可能丢数据。20Hz 的输出频率对 115200 波特率来说绰绰有余每行大约 20 字节每秒 400 字节远低于串口容量。4.3 用静止和翻转测试验证数据正确性数据能打印出来不代表正确。我一般做两个测试第一个是静止测试。把模块平放在桌面上Z 轴应该输出接近 1g 的值X 和 Y 接近 0。在全分辨率 ±16g 模式下1g 对应的原始值约为 256因为 3.9mg/LSB1000mg / 3.9 ≈ 256。所以静止时 Z 轴原始值应该在 256 左右X 和 Y 在 0 附近波动。第二个是翻转测试。把模块翻转 180 度Z 轴应该从 256 变成 -256。如果翻转后数值不变或者变化很小说明数据解析有问题大概率是高低字节拼接顺序错了或者补码转换没做对。这两个测试能快速定位大部分数据异常。如果静止时 Z 轴是 0检查 POWER_CTL 是否写入了 0x08如果数值跳变剧烈检查电源是否干净ADXL345 对电源纹波比较敏感建议在 VCC 和 GND 之间加 0.1uF 和 10uF 电容。5. 避坑与排查ADXL345 调试中最容易翻车的五个点5.1 读出来全是 0POWER_CTL 没切到测量模式现象串口有输出但三轴数据全是 0无论怎么晃动模块都不变。原因ADXL345 上电默认处于待机模式此时输出数据寄存器不更新。很多例程只写了 DATA_FORMAT 就以为初始化完了漏掉了 POWER_CTL 的 0x08。解决在初始化序列的最后确认向 0x2D 写入了 0x08。可以读回 0x2D 验证如果返回值不是 0x08说明写入失败检查 IIC 写函数是否正常。5.2 读出来全是 65535 或负数高低字节拼接顺序反了现象静止时 Z 轴显示 65535 或 -1晃动时数值乱跳。原因ADXL345 的数据寄存器是低字节在前高字节在后。如果先读高字节再读低字节或者拼接时把高低字节位置搞反就会得到完全错误的值。解决确认读取顺序是 DATAX0、DATAX1、DATAY0、DATAY1、DATAZ0、DATAZ1。拼接时用(buf[1] 8) | buf[0]不要写成(buf[0] 8) | buf[1]。5.3 IIC 读不出 DEVID上拉电阻缺失或地址错误现象读 0x00 寄存器返回 0x00 或 0xFFIIC 通信完全失败。原因SDA 和 SCL 没有上拉电阻或者上拉阻值太大导致上升沿太慢。另一个常见原因是 IIC 地址搞错把 7 位地址 0x53 直接当 8 位地址发送。解决用万用表测量 SDA 和 SCL 对 VCC 的电阻正常应该在 4.7kΩ 左右。如果无穷大需要加外部上拉。地址方面确认发送的是0x53 1或0xA6而不是0x53。5.4 数据波动大电源噪声或输出速率设置不当现象模块静止时数据在几十个 LSB 范围内跳动无法稳定。原因ADXL345 对电源噪声敏感尤其是数字电源和模拟电源共用时。另外输出速率ODR设置过高也会引入噪声。解决在 VCC 和 GND 之间加 0.1uF 陶瓷电容和 10uF 钽电容。如果不需要高速采样把 ODR 降到 50Hz 或 100Hz。ADXL345 的 ODR 由 BW_RATE 寄存器 0x2C 控制默认是 100Hz一般够用。5.5 串口输出乱码波特率不匹配或 printf 重定向失败现象串口助手收到乱码或者完全没有输出。原因STM32 的串口波特率和串口助手设置不一致或者 printf 重定向没有生效。解决确认串口助手波特率是 115200数据位 8停止位 1无校验。如果用的是 Keil勾选 Use MicroLIB。如果用的是 GCC检查 _write 函数是否实现。还可以在初始化后先发送一个固定的字符串测试比如printf(ADXL345 Test\r\n)确认串口链路通了再读传感器。6. 进阶技巧用 FIFO 和中断做低功耗数据采集6.1 FIFO 模式配置BYPASS、FIFO、STREAM 怎么选ADXL345 内置一个 32 级 FIFO可以在 MCU 休眠时缓存数据减少唤醒次数。FIFO_CTL 寄存器 0x38 的低 5 位设置水印级别bit 6:5 设置模式bit 7 设置触发中断的条件。常用模式有三种BYPASS 模式FIFO 关闭只保留最新数据、FIFO 模式填满后停止适合一次性采集、STREAM 模式填满后覆盖最旧数据适合连续采集。我一般用 STREAM 模式水印设为 16这样 FIFO 半满时触发中断MCU 醒来一次性读 16 组数据。// 配置 FIFO 为 STREAM 模式水印 16 void ADXL345_ConfigFIFO(void) { // bit 7:0 0, bit 6:5 10 (STREAM), bit 4:0 16 (水印) ADXL345_WriteReg(0x38, 0x80 | 0x10); // 0x80 使能 FIFO 中断触发 HAL_Delay(10); }水印值设为 16 意味着 FIFO 中积累 16 组数据后INT1 或 INT2 引脚会产生中断。MCU 在中断服务函数中读取 FIFO_STATUS 寄存器 0x39确认有效数据组数然后连续读取相应数量的字节。6.2 中断引脚配置与 MCU 唤醒ADXL345 的中断引脚可以映射到 INT1 或 INT2。需要配置 INT_ENABLE 寄存器 0x2E 使能 FIFO 中断bit 2 1然后配置 INT_MAP 寄存器 0x2F 决定映射到哪个引脚。// 使能 FIFO 中断并映射到 INT1 void ADXL345_EnableFIFOInterrupt(void) { ADXL345_WriteReg(0x2E, 0x02); // 使能 FIFO 中断 ADXL345_WriteReg(0x2F, 0x00); // 映射到 INT1 引脚 HAL_Delay(10); }MCU 端把 INT1 引脚配置为外部中断下降沿触发。在中断服务函数中先读 FIFO_STATUS 确认数据组数然后连续读取 6 倍组数的字节解析后通过串口输出或存入缓冲区。注意在中断中不要做浮点运算和 printf只做数据搬运主循环再处理。6.3 用 Python 脚本解析串口 CSV 并画波形数据输出到电脑后可以用 Python 快速验证。下面是一个最小脚本读取串口 CSV 并实时绘制三轴波形import serial import matplotlib.pyplot as plt import matplotlib.animation as animation ser serial.Serial(COM3, 115200, timeout1) fig, ax plt.subplots() xs, ys, zs [], [], [] def update(frame): line ser.readline().decode(utf-8, errorsignore).strip() if not line: return try: x, y, z map(int, line.split(,)) except ValueError: return xs.append(x) ys.append(y) zs.append(z) # 只保留最近 200 个点 for lst in (xs, ys, zs): if len(lst) 200: lst.pop(0) ax.clear() ax.plot(xs, labelX) ax.plot(ys, labelY) ax.plot(zs, labelZ) ax.legend(locupper right) ax.set_ylim(-600, 600) ani animation.FuncAnimation(fig, update, interval50) plt.show()这个脚本假设串口每行输出三个整数用逗号分隔。运行后应该能看到三条实时波形。静止时 Z 轴在 256 附近X 和 Y 在 0 附近。晃动模块时波形会相应变化。如果波形是一条直线或者噪声极大回到第 5 章排查。6.4 我踩过的最后一个坑FIFO 读完后必须重新使能FIFO 在 STREAM 模式下读空后会自动继续填充不需要额外操作。但在 FIFO 模式下读完后 FIFO 会停止必须重新写 FIFO_CTL 才能再次启动。我一开始用 FIFO 模式做低功耗采集发现第二次中断再也不来了查了半天才发现是 FIFO 满了之后没有重新配置。后来统一改用 STREAM 模式这个问题就再也没出现过。另外读取 FIFO 数据时建议一次性连续读取不要分多次读。因为每次起始-停止都会占用总线时间而且在读的过程中 FIFO 可能继续填充导致数据错位。我一般根据 FIFO_STATUS 返回的组数计算总字节数然后一次连续读出来。如果你也在用 ADXL345 做姿态检测或振动监测建议先把本文的初始化序列和串口输出跑通确认静止和翻转测试通过再往上加 FIFO 和中断。传感器调试没有捷径先把最基础的 IIC 读写和寄存器配置搞扎实后面的功能都是水到渠成。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →