ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

STM32F103+LSM6DSL六轴传感器驱动与姿态解算实战

STM32F103+LSM6DSL六轴传感器驱动与姿态解算实战 简介一份基于STM32F103CBT6主控的LSM6DSL六轴运动传感器驱动代码包面向需要入门加速度计与陀螺仪开发的嵌入式爱好者或项目开发者。压缩包完整演示了通过IIC接口读取传感器数据并利用串口打印输出同时也提供SPI方式驱动代码方便用户根据硬件环境灵活切换。资源共144个文件大小4.53MB工程中既包含头文件、源文件等核心驱动也带有编译生成的中间文件、链接脚本及Keil工程配置个别PDF与TXT文档能辅助梳理寄存器配置与调试思路。已有3091人学习下载适合刚接触运动传感器或想快速在STM32F103平台上验证LSM6DSL性能的开发者参考。通过阅读与运行该工程可以省去自行查阅数据手册和搭建底层IIC时序的时间快速获得可实际运行的传感器读取与打印范例对于学习嵌入式传感器驱动也有很好的参考价值。 拿到这个STM32F103LSM6DSL.zip的时候你大概率和我当初一样左手一块 STM32F103 最小系统板右手一颗 LSM6DSL 六轴传感器模块想赶紧读到加速度和陀螺仪数据却不知道该从哪句代码开始。这个压缩包是今年年初我做计步手环原型时整理的工程文件和驱动都在里面。这篇文章不是简单贴代码而是把这包里的东西拆开揉碎为什么选这两颗芯片、硬件连接有哪些看不见的坑、驱动寄存器怎么配置、最后怎么把原始数据变成有用的姿态角。刚接触 STM32 和 IMU 的朋友看完应该能少走至少三天弯路。1. 项目整体设计与思路拆解1.1 为什么是 STM32F103 LSM6DSL先说选型思路。STM32F103 的优势大家都知道便宜、资料多、生态成熟72MHz 主频处理六轴数据加一个互补滤波CPU 占用率可以压到非常低。而且对大多数开发者来说手头最多的就是 F103 最小系统板学习成本几乎为零。LSM6DSL 则是一颗很成熟的低功耗六轴 IMU三轴加速计加三轴陀螺仪I2C 和 SPI 接口都支持内置 FIFO还带硬件计步器、倾斜检测这些功能。它的静态功耗只有 270µA 左右低功耗模式下更低非常适合做电池供电的可穿戴设备。很多人习惯用 MPU6050但 MPU6050 在寄存器设计和功耗表现上已经偏老LSM6DSL 的数据格式更现代量程配置也更灵活。我当时选它就是看中它噪声低、功耗低而且对 I2C 时序宽容不容易一上来就卡死在总线上。1.2 压缩包里的工程结构我整理这个 zip 的时候刻意把驱动和业务逻辑分开了。打开之后的结构大概是这样的STM32F103LSM6DSL/ ├─ Project/ │ ├─ MDK-ARM/ │ └─ EWARM/ ├─ Drivers/ │ ├─ BSP/ │ │ ├─ lsm6dsl_reg.c │ │ ├─ lsm6dsl_reg.h │ │ └─ imu_bsp.c │ └─ CMSIS/ ├─ App/ │ ├─ main.c │ ├─ sensor_reader.c │ └─ simple_fusion.c └─ Docs/ └─ wiring.mdProject下直接放了 MDK 和 IAR 两个版本的工程文件。Drivers/BSP里是我从 ST 官方寄存器映射文件里抽出来的精简驱动没有用官方那套复杂回调结构只保留了ReadReg/WriteReg/ReadBytes/WriteBytes这几个最核心的函数。App下放的是业务层包括传感器周期读取、原始数据到物理量的换算以及一个轻量互补滤波。Docs/wiring.md是接线说明这个文件特别关键因为很多问题最后都能追溯到接线。我不建议你拿到压缩包就直接把例程灌进板子。先按照这个分层逻辑搞明白每一层负责什么后面换传感器、换 MCU 的时候才知道怎么动刀。1.3 整体流程和状态设计工程里的主流程并不复杂上电初始化 I2C → 校验传感器 ID → 配置量程和数据输出频率 → 周期读取六轴原始值 → 换算成 g 和 dps 单位 → 互补滤波算出姿态角 → 通过串口输出。我在裸机状态下用状态机把这套流程串了起来IDLE → INIT → WAIT_DATA_READY → READ → PROCESS → OUTPUT。初始化阶段如果读不到 WHO_AM_I 就会进ERROR状态通过 LED 闪烁频率提示不同故障。这样调试的时候不用接仿真器看一眼灯就知道卡在哪一环。如果你之后要上 FreeRTOS也可以把这个状态机里的读取任务丢进一个独立任务用信号量等待数据就绪中断。2. 硬件连接与最小系统搭建的关键点2.1 最小系统板和 LSM6DSL 接线STM32F103 的 I2C1 默认在 PB6SCL和 PB7SDA。LSM6DSL 模块我使用的是 I2C 模式而不是 SPI 模式所以接线比较简单LSM6DSL 引脚STM32F103 引脚说明VDD3.3V主电源GNDGND共地SCLPB6I2C1 时钟SDAPB7I2C1 数据SA0/SDOGND把 7 位地址设为 0x6ACS3.3VI2C 模式下必须拉高INT1PB1可选数据准备好中断有个特别容易忽略的点LSM6DSL 的CS引脚。如果用 I2CCS必须接到 VDD否则芯片会认为你要用 SPI然后 SDA 和 SCL 上的时序就对不上了WHO_AM_I 怎么读都是错的。很多模块板上已经把 CS 默认拉高但如果你用裸片或自己画的板子一定要检查这一脚。2.2 上拉电阻和电源纹波别省I2C 总线必须有上拉电阻这是常识但新手最容易栽在这里。我的模块测试板用的是 4.7kΩ 上拉到 3.3VSCL 和 SDA 各一颗。如果没有上拉I2C 总线在 400kHz 模式下会经常卡死SDA 被拉低后释放不了整个通信就挂了。实测过之前在面包板上搭测试电路忘加上拉读数据偶尔成功、偶尔失败还以为是代码问题查了半天才发现是电气问题。另外LSM6DSL 的电源脚旁边最好加一颗 100nF 去耦电容位置尽量靠近芯片。传感器对电源纹波比较敏感尤其是陀螺仪供电不干净会导致零漂变大静态数据乱跳。这类问题在软件里很难补偿不如硬件上先做好。2.3 内部晶振到底能不能跑实测结论热词里有人问过“如何让 STM32F103 工作在内部晶振模式下”。我也把工程改成纯 HSI 内部 8MHz 时钟跑过一段时间。结论是只跑 I2C 读 LSM6DSLHSI 完全没问题I2C 时序本身对时钟精度要求不算苛刻内部 RC 的误差足够应付。但如果你同时开了串口调试并且波特率是 115200HSI 的误差可能会导致偶发乱码。串口波特率对时钟误差比较敏感尤其是长时间通信、累计误差上来以后。我的建议很简单传感器读取部分用 HSI 没问题但整个工程如果不是为了省一颗外部晶振就别在时钟源上省。外部 8MHz 晶振加两个 20pF 负载电容成本几毛钱能换来所有外设的稳定工作。3. 驱动代码实现从寄存器到物理量3.1 I2C 初始化和传感器 ID 校验驱动用标准外设库实现。I2C1 初始化时 GPIO 配置为开漏输出因为我们需要外部上拉来保证电平正确不能使用推挽输出否则和外部上拉会形成冲突。void I2C1_Init(void) { GPIO_InitTypeDef gpio; I2C_InitTypeDef i2c; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_AF_OD; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpio); I2C_DeInit(I2C1); i2c.I2C_ClockSpeed 400000; i2c.I2C_Mode I2C_Mode_I2C; i2c.I2C_DutyCycle I2C_DutyCycle_2; i2c.I2C_Ack I2C_Ack_Enable; i2c.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, i2c); I2C_Cmd(I2C1, ENABLE); }初始化后第一件事是读 WHO_AM_I地址是0x0FLSM6DSL 的 ID 固定是0x6A。注意 SA0 引脚如果接 VDDI2C 地址会变成0x6B很多驱动里直接写死0x6A换模块就翻车。uint8_t id 0; LSM6DSL_ReadReg(0x0F, id); if (id 0x6A) { // 传感器在线 } else { // 接线或地址错误进入 ERROR 状态 }3.2 加速度计和陀螺仪寄存器配置LSM6DSL 有 CTRL1_XL 和 CTRL2_G 两个核心控制寄存器。CTRL1_XL 地址0x10控制加速度计的输出频率和量程CTRL2_G 地址0x11控制陀螺仪的相关参数。我在这个工程里配置成加速度计 ±2g、陀螺仪 ±250dps两者输出频率都是 104Hz。这样的组合适合手持姿态测量量程小、分辨率高又不至于让数据量太大。uint8_t reg_val; reg_val (0x4 4) | (0x0 2); // 104Hz ODR, ±2g LSM6DSL_WriteReg(0x10, reg_val); reg_val (0x4 4) | (0x0 2); // 104Hz ODR, ±250dps LSM6DSL_WriteReg(0x11, reg_val); reg_val 0x04; // 连续更新模式小端输出 LSM6DSL_WriteReg(0x12, reg_val);CTRL3_C 地址是0x12里面第 2 位BDU位置 1 表示输出寄存器在未读完前不更新。这个很重要如果没设读取陀螺仪的高低字节时可能遇到两个字节不是同一时刻采样的尴尬情况。3.3 读取六轴数据和单位换算加速度计原始数据寄存器从0x28到0x2D陀螺仪从0x22到0x27都是 16 位补码低字节在前。我封装了一个读取函数typedef struct { int16_t ax, ay, az; int16_t gx, gy, gz; } imu_raw_t; void IMU_ReadRaw(imu_raw_t *raw) { uint8_t buf[6]; LSM6DSL_ReadBytes(0x28, buf, 6); raw-ax (int16_t)(buf[0] | (buf[1] 8)); raw-ay (int16_t)(buf[2] | (buf[3] 8)); raw-az (int16_t)(buf[4] | (buf[5] 8)); LSM6DSL_ReadBytes(0x22, buf, 6); raw-gx (int16_t)(buf[0] | (buf[1] 8)); raw-gy (int16_t)(buf[2] | (buf[3] 8)); raw-gz (int16_t)(buf[4] | (buf[5] 8)); }原始值要换算成物理单位。加速度计 ±2g 对应满量程 32768所以每个 LSB 代表2.0 / 32768g陀螺仪 ±250dps每个 LSB 代表250.0 / 32768dps。换算成弧度用于积分时再乘π/180。float acc_sens 2.0f / 32768.0f; float gyro_sens 250.0f / 32768.0f; float ax_g raw.ax * acc_sens; float gx_dps raw.gx * gyro_sens; float gx_rads gx_dps * 0.0174532925f;这里提醒一句静止时加速度计读数不会正好是 0/0/1g陀螺仪也不会正好全是 0都会有微小偏差。这是传感器出厂误差和环境噪声叠加的结果不要一开始就怀疑代码写错了。4. 从原始数据到姿态角一版适合裸机的轻量滤波4.1 加速度计算角度为什么不能直接用加速度计可以直接用atan2计算静态倾角。比如设备平放时Z 轴输出约 1gX 和 Y 接近 0roll 和 pitch 可以这样算float roll atan2f(acc_y, acc_z) * 180.0f / PI; float pitch atan2f(-acc_x, sqrtf(acc_y * acc_y acc_z * acc_z)) * 180.0f / PI;但问题在于加速度计测到的是“比力”包括重力和运动加速度。你拿着板子甩一下加速度计输出的就不再是重力方向直接算出来的角度会瞬间飞掉。所以动态场景下不能只用加速度计。陀螺仪则正好相反短时间积分很准但长时间会有零漂累积角度会慢慢飘走。两者互补就是互补滤波的基本思想。4.2 互补滤波的简单实现互补滤波的核心就是高通滤波处理陀螺仪积分低通滤波处理加速度计角度按权重融合。我工程里默认的采样周期是 10ms滤波器系数ALPHA取 0.98。#define ALPHA 0.98f #define DT 0.01f static float angle_roll 0.0f; static float angle_pitch 0.0f; void ComplementaryFilter(float gx_rads, float gy_rads, float acc_roll, float acc_pitch) { angle_roll ALPHA * (angle_roll gx_rads * DT) (1.0f - ALPHA) * acc_roll; angle_pitch ALPHA * (angle_pitch gy_rads * DT) (1.0f - ALPHA) * acc_pitch; }这里有个物理细节roll、pitch 的积分不能简单用“角速度乘以 dt”来完全替代因为在三维旋转中所谓的“绕 X 轴角速度”和 “roll 角变化率”并不是一回事。互补滤波在倾角不大、旋转不剧烈的场景下误差可以接受但也仅仅适用于这个场景。如果你做的是需要自由旋转 360 度的无人机、平衡车还是得老老实实上四元数 Mahony 或 Madgwick 算法。我不会为了省事骗你说这个补滤波是万能的。4.3 采样节奏一定要稳定姿态解算对采样周期的一致性非常敏感。即使你用互补滤波如果相邻两次采样的间隔忽大忽小融合出来的角度也会跟着抖。我试过在主循环里轮询读取传感器结果主循环里一加打印函数采样间隔就飘了姿态角就开始晃。正确做法是固定采样节奏。在裸机工程里我用的是 TIM2 定时器中断10ms 触发一次读取每次读取后置一个标志主循环发现标志后做换算、滤波和输出。如果以后上 FreeRTOS建议用数据就绪中断EXTI 触发后用信号量唤醒传感器任务不要用简单的 delay 轮询。5. 常见问题与排查技巧实录5.1 WHO_AM_I 读不到八成是电气问题这类问题 90% 出在硬件不是代码。我整理了一个排查顺序确认 SCL/SDA 上拉电阻已经接好阻值 4.7kΩ 左右。检查 CS 引脚I2C 模式下必须接高电平。检查 SA0 引脚接地时地址是0x6A接 VDD 是0x6B驱动里地址写对没。用示波器或逻辑分析仪看 SCL/SDA 上有没有波形。没有波形先看 GPIO 配置和时钟是否打开。如果这些都没问题再怀疑代码。但说实话我遇到的所有“读不到 ID”问题最后都查到是飞线松动或模块电源没上电。5.2 数据跳变、静态漂移要怎么调先说一个最基础也最容易犯的错量程和换算系数不匹配。你配置成 ±2g换算时用了 ±4g 的分母数据看起来就会比实际小一半。这个错误很难一眼看出因为输出波形形状还是对的。陀螺仪静态零漂是正常现象不是坏了。我建议上电完成配置后保持设备静止连续读取 100 个陀螺仪样本求平均作为零偏值之后每次读取都减去这个值。这个操作放在初始化末尾做效果立竿见影。加速度计如果出现轻微偏移也可以对静止情况下的轴向做校准但一般不太影响姿态解算。另外第一次上电后的前几百毫秒传感器内部还在自检建议初始化后至少延时 100ms 再开始采集数据不然前几帧数据会明显异常。5.3 低功耗场景停机模式别忽略外设漏电热词里有人问 STM32F103 掉电保存数据和停机模式这里多说一句。STM32F103 的停机模式确实能把内核电流降到很低但如果你把 LSM6DSL、I2C 上拉电阻、LED 全都留在板上整个系统在停机模式下照样会吃掉几百微安。低功耗不是让 MCU 睡过去就完事要把所有外设一起算进去。真正常见的做法是LSM6DSL 自己进入低功耗模式通过 INT1 产生中断唤醒 STM32外部上拉电阻在休眠前关闭或者选用带唤醒功能的引脚如果要保存标定数据或运行日志写内部 Flash 或外接一颗 EEPROM。掉电保存数据不是简单写个“保存函数”还要考虑 Flash 擦写寿命和掉电瞬间的电压余量这些在可穿戴产品里都是大坑。5.4 工程扩展PWMDMA、FreeRTOS、I2C 防卡死这个 zip 里我顺手放了两个扩展 demo。一个是 PWMDMA 驱动的呼吸灯放在App/extra_led.c用 TIM 输出 PWMDMA 自动更新占空比可以做到完全不用 CPU 介入效果非常顺滑。另一个是 FreeRTOS 版本的读取模板核心思路是传感器任务通过信号量等待数据就绪中断避免任务轮询把总线占死。如果你真的被 STM32F103 的硬件 I2C 卡死问题反复折磨有一个非常实在的办法改成软件模拟 I2C。哪怕主频跑到 72MHz用 GPIO 翻转模拟 400kHz 的 I2C 时序也完全不吃力而且稳定得让人安心。做原型验证阶段我经常直接用软件 I2C省得查硬件 I2C 的各种标志位。最后再分享一个个人习惯我每次打包工程时一定会在Docs目录里放一份wiring.md把接线的颜色、上拉位置、I2C 地址都写清楚。因为两周后回看自己的工程你忘掉的往往不是代码逻辑而是那根没接的 CS 线以及当时调试到凌晨才发现的某个低功耗配置。这个包里的接线说明就是你从零复现整个项目的最短路径。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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