简介:这是一份基于STM32的计步器设计全套资料与详细文档,面向嵌入式、电子、自动化、物联网等专业学生和开发者,可作为毕业设计、课程设计或项目初期演示使用。项目以加速度计/陀螺仪姿态检测为核心,源码中包含STM32的定时器、ADC、Flash、RCC等标准外设驱动,以及MPU6050的DMP运动驱动,能够在Keil环境中直接编译运行,帮助读者理解从传感器数据采集、姿态解算到计步输出的完整流程。压缩包内共256个文件,整体大小26.14MB。文件以C源文件、H头文件、Keil工程文件和编译输出文件(axf、hex、map、lst等)为主,另含SolidWorks三维零件与装配体模型以及md/txt/json等说明文档,便于同时参考软件实现与结构设计。这是作者的高分项目,答辩评审分达到95分,代码已经过实际运行验证;目前已有68人学习下载,可直接用于毕设课设,也适合在此基础上进行二次开发或算法进阶。
1. 计步器设计的核心问题:加速度数据怎么变成步数
一个常见误解是计步器的难点在做硬件,实际上 PCB 焊完、串口能打印加速度原始值时,项目才走完一半。传感器输出的数值不是步数,而是一串包含重力分量、身体晃动和传感器噪声的混合信号;走路时它呈现周期性波动,坐下时它几乎静止,而把这种波形模式识别成“一步”,才是整个设计真正要回答的问题。基于 STM32 的计步器之所以适合作为毕业设计或入门项目,是因为它同时覆盖了 I2C 外设读取、定时器中断、数据处理算法和低功耗这几块嵌入式基本功,且每一块的错误都能在步数误差上直观暴露出来。这里按硬件选型、驱动编写、算法调参到出 bin 文件的顺序展开,最终给出的是一套能自己复现、能用数据说话的完整方案。
2. 硬件选型与原理图设计:STM32 最小系统与加速度计的连接方式
计步器的硬件部分并不复杂,但选型决定了后面算法写起来是顺利还是折腾。这一章先对比两种主流加速度计,再梳理 STM32 最小系统的电路要点,最后处理 I2C 总线上挂多个器件时最容易被忽略的地址和上拉问题。
2.1 加速度计选型:MPU6050 与 ADXL345 怎么选
计步器需要的是加速度数据而不是角速度。MPU6050 同时包含三轴加速度计和三轴陀螺仪,陀螺仪在这个场景里几乎用不上,但因为资料多、现成库全,它仍然是初学者的首选;ADXL345 是纯三轴加速度计,功耗更低,自带运动/静止检测中断,做低功耗计步反而更合适。两者都支持 I2C 和 SPI 接口,STM32 驱动代码结构几乎一样,中途切换的成本不高。
| 对比项 | MPU6050 | ADXL345 |
|---|---|---|
| 测量对象 | 加速度 + 陀螺仪 | 仅加速度 |
| 接口 | I2C / SPI | I2C / SPI |
| 加速度量程 | ±2/4/8/16g | ±2/4/8/16g |
| 输出位数 | 16 位 | 16 位 |
| 内置运动中断 | 无 | 有 |
| 测量模式功耗 | 约 3.9mA | 约 23μA |
| 资料丰富度 | 极高 | 高 |
如果计划把低功耗作为答辩亮点,比如用锂电池供电并希望续航两周以上,ADXL345 的硬件运动检测能让 STM32 大部分时间停在停机模式;如果只是完成功能验证,MPU6050 的教程和现成驱动最多,遇到问题容易查到参考。我的建议是:第一次做或时间紧张,选 MPU6050;想在文档里放一组漂亮的功耗数据,选 ADXL345。
2.2 STM32 最小系统电路要点:晶振、复位、Boot 与下载接口
以 STM32F103C8T6 为例,最小系统需要电源滤波、8MHz 主晶振、复位电路和 BOOT 引脚配置。晶振的两个负载电容按照芯片手册给出的 CL 计算,F103 的 CL 典型值是 18pF,实际常用 20pF 或 22pF,估算公式是 C = 2 × CL − Cstray,Cstray 取 3~5pF,算出来在 20pF 附近。这个计算不用太纠结,晶振不起振时优先检查电容虚焊和负载电容是否过大,而不是直接换晶振。
BOOT0 引脚通过 10kΩ 电阻下拉到地,保证从主 Flash 启动;BOOT1 可以不接。SWD 下载接口只需要 SWDIO、SWCLK、GND 三根线,比 JTAG 少四根且占用引脚更少。需要注意,如果 PCB 上同时设计了 JTAG 接口,默认状态下 JTAG 引脚被复用,可能导致 SWD 连接失败,解决办法是先用 BOOT0 拉高进入 ISP 模式擦除芯片,再恢复正常启动。
电源部分,使用 3.7V 锂电池供电时需要一颗 LDO 稳定到 3.3V。AMS1117-3.3 的压差在 1V 以上,锂电池满电 4.2V 时没问题,但电量低于 3.8V 后压差不足,可能导致系统复位。更稳的方案是 ME6211 或 XC6206 这类低压差 LDO,压差只有 100mV 左右。输入输出各加 10μF 和 100nF 电容,布局时电容尽量靠近引脚。
2.2.1 I2C 总线上同时挂 MPU6050 和 OLED 的地址规划
OLED 模块常用的 SSD1306 和 MPU6050 都走 I2C,挂同一条总线时,MPU6050 的 AD0 引脚接低电平,地址为 0x68;SSD1306 的地址由 SA0 决定,常见是 0x3C。两个地址不冲突,可以直接并联。需要注意 I2C 引脚是开漏输出,必须在 SCL 和 SDA 上各接一颗 4.7kΩ 上拉电阻到 3.3V。很多现成 OLED 模块板上已经自带 4.7kΩ 上拉,再外接会导致上拉过强、信号边沿变缓,通信偶发失败。判断方法是看模块板上是否有标注 R2、R3 的贴片电阻,有就不用再外接。
还有一个容易被忽略的点:MPU6050 的 VLOGIC 引脚决定 I2C 接口电平,接 3.3V 即可,不能接 5V。如果开发板上同时引出 5V 和 3.3V,确认 OLED 和 MPU6050 的 VCC 都接 3.3V,避免电平不匹配导致读回数据全是 0xFF。
2.2.2 用一段启动前的自检代码确认硬件连接
焊接完成后先不要急着写计步算法,先写一个引脚与通信自检程序,打印三个关键信息:MPU6050 的 WHO_AM_I 寄存器、SSD1306 的 ACK 响应、以及静止时三轴加速度的原始值。以下代码在标准库工程中可直接用于启动阶段检查。
/* 硬件自检:检查传感器与屏幕是否在线 */ void hardware_self_test(void) { uint8_t who_am_i = 0; uint8_t ack_oled = 0; /* 读取MPU6050的WHO_AM_I寄存器(0x75),正常应返回0x68 */ who_am_i = I2C_ReadOneByte(MPU6050_ADDR, WHO_AM_I_REG); printf("MPU6050 WHO_AM_I = 0x%02X\r\n", who_am_i); /* 向OLED发送0x00命令字节,能收到ACK说明SSD1306在线 */ ack_oled = I2C_WriteByte(OLED_ADDR, 0x00); printf("SSD1306 ACK = %d\r\n", ack_oled); /* 初始化后延时100ms,读一次原始加速度并打印 */ MPU6050_Init(); delay_ms(100); int16_t ax = MPU6050_ReadRawAccel(AXIS_X); printf("RAW AX = %d\r\n", ax); }这段代码解决的是“焊完不知道从哪里开始排查”的问题。WHO_AM_I 读不到 0x68,先查供电和地址;OLED ACK 为 0,先查上拉电阻和 SDA/SCL 是否接反。静止时 AX 的原始值通常落在 10000~20000 之间,对应 ±2g 量程下约 1g 的重力分量,如果读到 0 或 65535,说明 I2C 时序或寄存器配置有误。把这些信息确认到位,后续算法调参才有意义。
3. STM32 工程搭建与传感器驱动:I2C 读取加速度的标准写法
无论使用标准库还是 HAL 库,工程搭建的流程都遵循同一顺序:配置时钟树、初始化 I2C、通过寄存器操作读取传感器数据。这一章把两种路径的取舍讲清楚,然后给出可直接使用的驱动代码和定时器采样框架。
3.1 标准库与 HAL 库的选择:新建工程时就要定下来
标准库的特点是寄存器封装薄,代码直白,出问题能直接看到寄存器操作过程;HAL 库封装层次多,但 CubeMX 图形化配置方便外设初始化。计步器用到的外设只有 I2C、定时器、GPIO 和串口,标准库完全够用,而且网上现成的 MPU6050 驱动大多是标准库写的,移植成本低。如果后续想快速换芯片型号测试,HAL 库更合适,CubeMX 里改一下型号重新生成工程即可。
新建工程时有两类常见错误。第一类是 Keil5 里没有安装对应芯片包,Device 列表找不到 STM32F103C8,需要在 Pack Installer 中安装 STM32F1 系列的 Device Family Pack;第二类是 C/C++ 选项卡里没定义 STM32F10X_HD 或 STM32F10X_MD,导致启动文件里的中断向量表和外设库的宏定义不匹配,编译能过但运行进不了中断。新建工程后第一件事是确认芯片型号和宏定义,其次才是写代码。
3.2 用标准库写一个可移植的软件 I2C 驱动
计步器项目里 I2C 承担两个任务:读加速度数据和向 OLED 发送显示内容。软件 I2C 比硬件 I2C 更容易调试,因为时序完全由 GPIO 控制,发送端和接收端的时钟极性都掌握在自己手里。以下是用 GPIO 模拟 I2C 的初始化与读寄存器函数。
/* 软件I2C引脚初始化,SCL=PB6, SDA=PB7 */ void I2C_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; /* 开漏输出 */ GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); I2C_SCL_HIGH(); I2C_SDA_HIGH(); } /* 读取MPU6050指定寄存器的值 */ uint8_t I2C_ReadOneByte(uint8_t dev_addr, uint8_t reg_addr) { uint8_t val = 0; I2C_Start(); I2C_SendByte(dev_addr << 1); /* 器件地址+写位 */ I2C_WaitAck(); I2C_SendByte(reg_addr); /* 寄存器地址 */ I2C_WaitAck(); I2C_Start(); I2C_SendByte((dev_addr << 1) | 1); /* 器件地址+读位 */ I2C_WaitAck(); val = I2C_RecvByte(); I2C_SendNotAck(); I2C_Stop(); return val; }GPIO 配置为开漏输出,是为了配合外部上拉电阻实现线与特性,这是 I2C 协议的硬件要求。读操作分三段:先发器件地址和写位,再发寄存器地址,最后重新发起 START 并切换为读模式。收到最后一个字节后主机要回 NACK,表示后面没有数据要读了,否则从机会继续发数据导致时序错乱。
如果改用硬件 I2C,需要小心的是 BUSY 标志卡死的问题,标准库中在主循环里反复调用时容易遇到。软件 I2C 的代价是占用 CPU,但计步器的采样率只需要 50Hz,每次采样读取 6 个字节的耗时远小于一个采样周期,性能影响可以忽略。
3.3 初始化 MPU6050 的寄存器配置与量程选择
MPU6050 上电后默认处于睡眠模式,必须清除电源管理寄存器 1(地址 0x6B)的 SLEEP 位才能读到有效数据。同时建议把数字低通滤波器配置到 10Hz 以上,用于滤除高频振动噪声。以下配置适合计步器场景。
/* MPU6050基础初始化:唤醒 + 设置量程和滤波带宽 */ void MPU6050_Init(void) { /* 0x6B: bit7=DEVICE_RESET, bit6=SLEEP, 写0唤醒 */ I2C_WriteOneByte(MPU6050_ADDR, MPU6050_PWR_MGMT_1, 0x00); delay_ms(50); /* 0x1C: 加速度量程配置,±2g对应0x00,±4g对应0x08 */ I2C_WriteOneByte(MPU6050_ADDR, MPU6050_ACCEL_CONFIG, 0x00); /* 0x1A: DLPF配置,值4对应21Hz带宽,滤除高频抖动 */ I2C_WriteOneByte(MPU6050_ADDR, MPU6050_CONFIG, 0x04); }量程选择直接影响算法里的阈值范围。±2g 量程下 16 位 ADC 的分辨率是 16384 LSB/g,走路时腰部加速度峰值一般在 1.5~2.5g 之间,存在截断风险;±4g 量程下分辨率减半,但峰值基本不会截断。我一般选 ±4g,因为后续算法只关心加速度的相对变化量,不需要那么高的绝对精度。
滤波器带宽 21Hz 是一个合适的起点。人走路的步频大约 1.5~2.5Hz,信号基频和谐波都在 10Hz 以内,21Hz 截止频率既能保留步态信息,又能滤掉肌肉抖动和传感器高频噪声。调参时如果发现步数偏多,可以先把 DLPF 带宽降到 10Hz 再测。
提示:MPU6050 唤醒后必须等待 50ms 以上再读取数据,这是内部稳压器稳定所需的时间,读得过早可能得到全 0xFF。
3.4 用定时器中断按 50Hz 采样,而不是在主循环里延时
计步算法要求数据按固定时间间隔到达。如果使用delay_ms(20)配合主循环读取,OLED 刷新和按键扫描会打断采样节奏,导致波形时间轴不均匀,阈值判断出现随机误差。正确做法是把采样放进定时器中断:配置 TIM3 产生 1ms 中断,每 20 次中断执行一次采样。
/* TIM3中断服务函数:1ms中断,每20次采一次样 */ void TIM3_IRQHandler(void) { static uint16_t tick = 0; if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); tick++; if (tick >= 20) { /* 20ms = 50Hz */ tick = 0; sample_accel_and_detect(); /* 读加速度并调一步检测 */ } } }定时器采样保证两个采样点之间严格间隔 20ms,误差来自晶振精度而不是代码执行路径。主循环里可以放心做 OLED 刷新和按键扫描。需要留意的是中断服务函数里不要调用 printf 或长时间运行的函数,调试打印可以先把数据存到缓冲区,主循环里再统一输出。
4. 计步算法实现:合成矢量、滑动滤波与动态阈值检测
硬件和驱动就绪后,计步器的技术核心集中在算法上。这一章先说明为什么用三轴合成加速度,再给出滤波实现,最后是完整的动态阈值检测流程和参数调优方法。
4.1 为什么不能用单轴加速度判断步数
一个常见的第一个想法是“走路时 Y 轴上下变化最大,只判断 Y 轴就行”。这个想法在传感器固定且姿态不变时成立,但计步器戴在手腕或挂在腰间时会倾斜,走路时手臂摆动还会让坐标系跟着转。单轴数据在传感器倾斜 45 度时,重力分量会分摊到两个轴上,单轴波动幅度明显变小,阈值就不好设。三轴合成矢量不受姿态影响,直接对所有方向上的运动幅度求模:
acc_mag = sqrt(ax*ax + ay*ay + az*az)合成矢量包含重力分量(约 1g)和运动产生的动态分量。走路时每迈一步,合成矢量的模会先增大后减小,形成类似正弦波的起伏,计步算法的本质就是检测这个波形中“一次完整的起伏周期”。使用合成矢量的另一个好处是,传感器无论怎么旋转,重力分量大小都是 1g,波形基线稳定,给阈值设定提供了参照。
4.2 滑动均值滤波:去掉高频毛刺但保留步态波形
原始加速度数据即使经过 DLPF,依然存在幅度约 ±0.05g 的高频波动,如果不处理,峰值检测会把一次晃动当成好几步。滑动均值滤波是嵌入式里代价最低的方案:维护一个长度 N 的环形缓冲区,每来一个新样本就计算窗口内平均值。N 取 8 到 16 之间,对应 50Hz 采样率下 160ms 到 320ms 的窗口,既能平滑毛刺又不会把步态波形压平。
/* 滑动均值滤波器,window_size=8 */ #define FILTER_WINDOW 8 float filter_buffer[FILTER_WINDOW]; uint8_t filter_index = 0; float accel_filter(float new_sample) { float sum = 0.0f; uint8_t i = 0; filter_buffer[filter_index] = new_sample; filter_index = (filter_index + 1) % FILTER_WINDOW; for (i = 0; i < FILTER_WINDOW; i++) { sum += filter_buffer[i]; } return sum / FILTER_WINDOW; }滤波窗口长度有个权衡:窗口太短,毛刺滤不干净,容易误检;窗口太长,波形被拉平,峰值幅度变小,真实步数被漏检。调试时先在串口上打印滤波前后的波形,用串口绘图工具直接看曲线,确认滤波后的波形依然是平滑的单峰形态,再进入阈值检测环节。滤波的目的是让后续阈值判断更稳定,而不是把波形变得“完美”。
4.3 动态阈值与峰值检测:适应不同走路速度的核心
固定阈值的问题在于走路速度变化时失效:慢走时加速度波动小,固定阈值会漏检;快走或跑步时波动大,固定阈值又可能把一次摆动拆成两步。动态阈值的思路是持续跟踪一段时间内的加速度最大值和最小值,把阈值设为两者之间的一个比例位置,这样波动幅度无论大还是小,检测窗口都跟随变化。
/* 动态阈值+峰值检测,50Hz采样下每个样本调用一次 */ #define STEP_HYSTERESIS 0.3f /* 滞回比例 */ #define STEP_MIN_INTERVAL 15 /* 最小步间隔(采样点),对应300ms */ float peak_max = 1.3f; /* 动态上限,初始为静止值 */ float peak_min = 0.7f; /* 动态下限,初始为静止值 */ uint8_t step_cooldown = 0; uint8_t last_state = 0; uint8_t step_detector(float acc_mag) { /* 更新动态上下限,缓慢跟踪避免突变 */ if (acc_mag > peak_max) peak_max = acc_mag; else peak_max -= 0.002f; /* 每个采样点向下衰减 */ if (acc_mag < peak_min) peak_min = acc_mag; else peak_min += 0.002f; float threshold = (peak_max + peak_min) / 2.0f; uint8_t state = (acc_mag > threshold) ? 1 : 0; uint8_t step_counted = 0; /* 上升沿且冷却期结束才计一步 */ if (state == 1 && last_state == 0 && step_cooldown == 0) { step_counted = 1; step_cooldown = STEP_MIN_INTERVAL; } if (step_cooldown > 0) step_cooldown--; last_state = state; return step_counted; }这段代码的核心是两个机制。动态上下限的衰减:峰值从上方回落时以固定速率衰减,这样走路突然变慢时,过时的峰值不会一直卡在高位导致阈值降不下来。最小步间隔:人正常走路一步至少需要 300ms,50Hz 采样下就是 15 个采样点,这个冷却期直接过滤掉因抖动产生的密集脉冲。
衰减速率 0.002 是在 50Hz 采样下实验得到的折中值。太快会导致阈值紧跟噪声,太慢则跟不上走路速度的快速变化。步频较高或跑步场景,可以把这个值降到 0.001;佩戴位置离脚较近时波形波动更大,可以把滞回比例从 0.3 调高到 0.4。这些参数没有通吃所有场景的答案,最终以实测为准。
4.4 关键参数速查表与调试流程
| 参数 | 推荐值 | 调整方向 |
|---|---|---|
| 采样率 | 50Hz | 低于 25Hz 会丢失波形细节 |
| DLPF 带宽 | 21Hz | 误检多则降至 10Hz |
| 滤波窗口 | 8 | 波形毛刺多则增至 16 |
| 步间隔冷却 | 15 个采样点 | 跑步场景可降至 8 |
| 峰值衰减速率 | 0.002 | 慢走场景降至 0.001 |
调试流程固定三步:先静止 30 秒确认没有额外步数;再匀速走 100 步记录误差;最后变速走 100 步,检查动态阈值是否跟得上。每一步只改一个参数,避免两个参数的影响叠加在一起难以归因。串口调试数据里最关键的是 peak_max、peak_min 和 threshold 三个值,打印出来可以直接判断动态阈值是否正常工作。
5. 生成 bin 文件、低功耗处理与实测数据对比
设计调试完成后还有三件收尾工作:把程序打包成方便烧录的 bin 文件、确认待机功耗、用数据验证计步精度。
5.1 Keil 中配置生成 bin 文件
基于 STM32 的工程默认输出 hex 文件,但批量烧录时 bin 更常用,体积更小,也方便做固件比对。在 Keil 的 Options for Target 的 User 选项卡中,勾选 After Build 下的 Run #1,写入命令:
fromelf --bin --output=./Output/step_counter.bin ./Output/step_counter.axffromelf 是 Keil 自带的格式转换工具,这条命令把链接器生成的 axf 文件转成纯二进制 bin 文件。如果找不到生成文件,优先检查 axf 的实际输出目录,魔术棒 Output 选项卡修改过生成路径时,命令里的路径要跟着改。
5.2 低功耗优化:待机测量与唤醒策略
使用 MPU6050 且不引入运动中断时,STM32 只能定期唤醒采样。常见做法是设置一个 500ms 的周期唤醒定时器,醒来后读取一次加速度,判断是否有持续波动,连续 N 次超过阈值才进入正式计步模式;静置 5 分钟没有波动就重新进入停机模式。实测中 STM32 停机模式加外部唤醒,整体平均电流可控制在 200μA 以内,若换成 ADXL345 的运动中断唤醒,还能更低。
测量待机电流时,把万用表串入电池正极和系统电源输入之间,用 mA 档测待机,用 μA 档测深睡模式。实测电流远高于预期时,优先排查板上是否有 LED 常亮、LDO 静态电流偏大、以及传感器是否真的进睡眠。
5.3 用三组数据验证算法是否达标
不要用“感觉挺准”来验收计步器。固定传感器佩戴位置,分别以慢走(约 90 步/分钟)、正常(约 110 步/分钟)、快走(约 130 步/分钟)三种速度各走 100 步,记录输出步数。误差在 ±3% 以内说明参数基本可用;慢走误差大优先调峰值衰减速率,快走误差大优先检查冷却期和滤波窗口。把这三组数据连同 peak_max、peak_min 的范围写进实验记录,直接作为文档里的图表素材。快走场景下步数偏多超过 10% 时,先把STEP_MIN_INTERVAL从 15 提高到 20,这个调整通常比改滤波窗口更直接。
本文还有配套的精品资源,点击获取