简介这是一份基于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 驱动代码结构几乎一样中途切换的成本不高。对比项MPU6050ADXL345测量对象加速度 陀螺仪仅加速度接口I2C / SPII2C / 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 − CstrayCstray 取 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 引脚接低电平地址为 0x68SSD1306 的地址由 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引脚初始化SCLPB6, SDAPB7 */ 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: bit7DEVICE_RESET, bit6SLEEP, 写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_size8 */ #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; }这段代码的核心是两个机制。动态上下限的衰减峰值从上方回落时以固定速率衰减这样走路突然变慢时过时的峰值不会一直卡在高位导致阈值降不下来。最小步间隔人正常走路一步至少需要 300ms50Hz 采样下就是 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这个调整通常比改滤波窗口更直接。本文还有配套的精品资源点击获取
