摘 要
计步器是一种颇受欢迎的日常锻炼进度监控器,可以激励人们挑战自己,增强体质,帮助瘦身。如今,先进的计步器利用MEMS(微机电系统)惯性传感器和复杂的软件来精确检测真实的步伐。MEMS惯性传感器可以更准确地检测步伐,误检率更低。MEMS惯性传感器具有低成本、小尺寸和低功耗的特点,因此越来越多的便携式消费电子设备开始集成计步器功能,如音乐播放器和手机等。
如今,各种各样的手机内置计步器已成为人们生活中不可缺少的一部分,而通过姿态传感器进行数据融合处理成为当今计步器算法中的主流。通过对x、y、z三轴加速度活跃轴的判断与动态精度的比较,计算人行走的步数,成为大多数嵌入式开发者青睐的手段和方法。
本系统采用STM32F103C8T6最小系统板作为主控芯片,姿态传感器选择MPU6050,利用三轴加速度进行姿态解算,识别迈步,收步的过程。采用动态阈值和峰值检测算法,最终将步数通过OLED屏幕显示。
本文以对步伐特征的研究为基础,描述一个采用 3 轴加速度计ADXL345 的全功能计步器参考设计,它能辨别并计数步伐,测量距离、速度甚至所消耗的卡路里。开发工具:Keil For MDK-ARM Standard Vrsion 5.17,编程语言:C/C++ 。
关键词: STM32F103C8T6;MPU6050;姿态解算;动态阈值;峰值检测;
一. 引言
编写目的和设计思路:
本设计基于ARM Cortex-M3内核的32位单片机STM32F103C8T6,对带有MPU6050的计步器实现辨别并计数步伐,测量距离、速度甚至所消耗的卡路里的功能。
用户在水平步行运动中,垂直和前进两个加速度会呈现周期性变化,如图所示。在步行收脚的动作中,由于重心向上单只脚触地,垂直方向加速度是呈正向增加的趋势,之后继续向前,重心下移两脚触底,加速度相反。水平加速度在收脚时减小,在迈步时增加。
在步行运动中,垂直和前进产生的加速度与时间大致为一个正弦曲线,而且在某点有一个峰值。其中,垂直方向的加速度变化最大,通过对轨迹的峰值进行检测计算和加速度阀值决策,即可实时计算用户运动的步数,还可依此进一步估算用户步行距离。
因为用户在运动中可能用手平持设备,或者将设备置于口袋中。所以,设备的放置方向不定。为此,通过计算三个加速度的矢量长度,我们可以获得一条步行运动的正弦曲线轨迹。
第二步是峰值检测,我们记录了上次矢量长度和运动方向,通过矢量长度的变化,可以判断目前加速度的方向,并和上一次保存的加速度方向进行比较最后,就是去干扰。在平时用户对手持设备的抖动过程中也会产生加速度值的变化,此时我们就需要将轻微的抖动通过滤波等方式去除,以保证计步的准确性。
二. 系统硬件设计
1. 硬件选型
本系统选择了STM32F103C8T6作为主控芯片,姿态传感器MPU6050检测步伐,OLED屏幕实时显示当前步数以及行走的距离、速度和消耗的卡路里,外部通过8.4V锂电池经LM2596降压后给单片机供电。
系统设计实物图:

图1. 实物设计图
2. 姿态传感器MPU6050
MPU-6050是世界上第一款集成 6 轴MotionTracking设备。它集成了3轴MEMS陀螺仪,3轴MEMS加速度计,以及一个可扩展的数字运动处理器 DMP(DigitalMotion Processor),可用I2C接口连接一个第三方的数字传感器。扩展之后就可以通过其 I2C或SPI接口输出一个9轴的信号。
MPU-6050对陀螺仪和加速度计分别用了三个16位的ADC,将其测量的模拟量转化为可输出的数字量。为了精确跟踪快速和慢速的运动,传感器的测量范围都是用户可控的。
在读取三轴加速度值时,并没有选用MPU6050自身带有DMP驱动库,第一点原因在于如果利用DMP库会导致MPU6050数值的初始化特别麻烦,对于数值初始化位置要求很高;第二点是DMP库将MPU6050读回的数据进行了处理,三轴加速度的变化范围不能明显的表示出来,对于设计中步伐的判断会带来困难。
3. 主控制器STM32F103C8T6
内核:ARM32位Cortex-M3 CPU,最高工作频率72MHz,1.25DMIPS/MHz。单周期乘法和硬件除法。
DMA:12通道DMA控制器。支持的外设:定时器,ADC,DAC,SPI,IIC和UART。
3个12位的us级的A/D转换器(16通道):A/D测量范围:0-3.6V。双采样和保持能力。片上集成一个温度传感器。
最多多达13个通信接口:2个IIC接口。5个USART接口。3个SPI接口,两个和IIS复用。CAN接口。
STM32F103C8T6单片机虽然外设资源不如zet6丰富,但是性能同zet6一样,对于简易计步器的设计已经足够,并且c8t6体积小,便于计步器的随身携带。

图2. stm32c8t6最小系统图 图3. MPU6050结构图
三. 系统软件设计
1
2
1. 程序流程图

图4. 软件程序流程图
2. 软件结构

图5. 软件结构设计说明
四. 原理分析
1. 加速度数值分析
使用加速度传感器获取到用户迈步加速度的变化,然后通过算法来检测步数。仔细想想平时走路的情景,人体重心的加速度由前向和纵向的两个分量合成,

图6. 迈步过程图
合成之后的加速度波形就很规律了,我们只需要检测波峰/波谷,再过滤掉其他的干扰,就能估算出步数了(绝对的加速度需要算上重力加速度,本文后面所说的都指相对的,人体行走时产生的加速度)。

图7. 三轴加速度传感器数值图
从上往下,第一条曲线为X轴的加速度值,第二条曲线为Y轴的加速度值,第三条曲线为Z轴的加速度值。可以看到,当翻转计步器时,X轴、Y轴和Z轴的加速度值有明显的改观,并且波动较为明显,通过算法设计动态阈值和峰值检测就能判断出当前是否有效迈出步伐。
2. 算法分析
波峰检测:既然目标是检测波峰,那就首先要知道波形的状况,加速度传感器采样的频率比较高,一般大于30Hz,正常人走一步的时间内,可以采样大概二十次了,也就是说在一个上升波段中,检查到的加速度会连续增大好多次,同样在下降波段中加速度也会连续减小好多次。于是,从波形图看,检测波峰的方法可以概括为四个条件:①上一次波形上升状态。②目前为下降状态。③到波峰为止,连续上升多次。④波峰/波谷值在一定范围内
动态阈值和动态精度:系统每采样 20次更新一次动态阈值。接下来的 20 次采样利用此阈值判断个体是否迈出步伐。通过这种不断更新的方法,能够保持系统当前判断步伐算法的准确度,比固定阈值更加接近于实际应用的情况。在定时器中每5ms采样一个数值,当采样20次后更新一次动态阈值。
五. 功能模块介绍
[1]
[2]
[3]
1. 主模块
计数器系统主要由控制核心(MCU)模块、LM2596降压模块、MPU6050姿态传感器模块及oled显示模块等组成,姿态传感器MPU6050主要用于得到人行走时的加速度数据; MCU 采用意法半导体公司的 stm32f103c8t6单片机作为控制芯片,抗干扰能力强; 显示模块的主要功能为显示计步器的倾角信息,当前步数值以及行走距离、速度和消耗卡路里值。
2. 滤波算法
通过iic协议读取MPU6050三轴加速度值,采用均值滤波算法和中位值滤波算法使运动时的波形更加平滑,便于后续算法处理。
3. 步伐检测
动态阈值和动态精度:系统持续更新 3 轴加速度的最大值和最小值,每采样 20次更新一次。平均值(Max + Min)/2 称为“动态阈值”。接下来的 20 次采样利用此阈值判断个体是否迈出步伐。
由于此阈值每 20 次采样更新一次,因此它是动态的。这种选择具有自适应性,并且足够快。除动态阈值外,还利用动态精度来执行进一步滤波。
利用一个线性移位寄存器和动态阈值判断个体是否有效地迈出一步。该线性移位寄存器含有 2 个寄存器: sample_new 寄存器和 sample_old 寄存器。这些寄存器中的数据分别称为 sample_new和 sample_old。当新采样数据到来时, sample_new 无条件移入sample_old 寄存器。然而, sample_result 是否移入 sample_new 寄存器取决于下述条件:如果加速度变化大于预定义精度,则最新的采样结果 sample_result 移入 sample_new 寄存器,否则sample_new 寄存器保持不变。因此,移位寄存器组可以消除高频噪声,从而保证结果更加精确。
步伐迈出的条件定义为:当加速度曲线跨过动态阈值下方时,加速度曲线的斜率为负值(sample_new < sample_old)。
峰值检测:步伐计数器根据 x、 y、 z 三轴中加速度变化最大的一个轴计算步数。如果加速度变化太小,步伐计数器将忽略。步伐计数器利用此算法可以很好地工作,但有时显得太敏感。当计步器因为步行或跑步之外的原因而非常迅速或非常缓慢地振动时,步伐计数器也会认为它是步伐。
4. 距离参数
根据上述算法计算步伐参数之后,我们可以使用下面公式获得距离参数。
距离 = 步数 × 每步距离
每步距离取决于用户的速度和身高。如果用户身材较高或以较快速度跑步,步长就会较长。参考设计每 1.5 秒更新一次距离、速度和卡路里参数。因此,我们使用每 1.5 秒计数到的步数判断当前跨步长度。右表显示了用于判断当前跨步长度的实验数据。
5. 速度参数
速度 = 距离/时间,而每 1.5 秒步数和跨步长度均可根据上述算法计算,因此可以使用下面公式获得速度参数。 速度 = 每 1.5 秒步数 × 跨步/1.5 s
6. 卡路里参数
我们无法精确计算卡路里的消耗速率。决定其消耗速率的一些因素包括体重、健身强度、运动水平和新陈代谢。不过,我们可以使用常规近似法进行估计。右图显示了卡路里消耗与跑步速度的典型关系。
卡路里(C/kg/h) = 1.25 × 跑步速度(km/h)
7. OLED参数显示模块
采用0.96寸中景园电子oled显示屏,利用stm32f103单片机模块向OLED参数显示模块传送一些数据并显示。可以显示步数、距离、速度、消耗卡路里等参数。
六. 总结体会
在本次计步器的设计中,遇到了一些问题,比如MPU6050原始数据的读取,原先是使用了正点原子的DMP库,在数据处理的时候问题并不大,DMP库处理的数据还是比较稳定的,但是问题就在于MPU6050的初始化总有问题,有一句DMP_init初始化的while循环总是跳不出来,经常会出现一些要按好几次复位才行的情况,还有就是每次初始化后MPU6050总把当前放置的位置置为0,每次初始化的数据变化区间都会发生变化。后来去上网找了找办法,找到了一篇MPU6050原始数据读取的知乎,编者是用了F4编写的程序,我稍将F4的程序移植为F1后能够正常读取MPU6050的三轴加速度值了,并且再也不受DMP初始化问题的影响。用虚拟示波器观察MPU6050的原始数据波形时,波形也是比较稳定,不会在同一位置发生明显的跳变。通过这次计步器的设计对于MPU6050数据读取,iic时序都有了一些体会。姿态传感器的9个数据我只运用到了三轴加速度数据,角度和三轴陀螺仪并未使用。
把所有模块的功能都测试好后,就是焊接硬件电路,我选择通过一块8.4V锂电池通过LM2596降压模块给主控制器供电,同时oled和MPU6050的供电都通过单片机板载3.3V和5V口进行供电。硬件电路的设计难度并不大,在洞洞板上很快就完成了整个计步器的硬件制作。
然后就通过usb-ttl和DAP下载器进行在线调试,通过串口去找迈步时的波形变化规律,参考了一些CSDN博客,最终选择使用动态阈值、动态精度和波峰检测的方法去进行步伐判断。刚开始对于信号数据的采样周期做了一些想法,一开始是50ms读取一次,读取5次平均值滤波后判断步伐,但后来做完后觉得这样判断相应太慢了,大致估算人迈步的频率,最后改成了5ms读一次数据,读取10次后滤波处理进行步伐判断。这样做能够加快计步器的相遇,提高计步器检测步伐的效率。在算法编写的时候也遇到了不少问题,因为对三轴加速度采取了结构体运算,在指针数据传递的时候因为不够熟悉,常常传回来的变量读不到数据,所以就运用串口和虚拟示波器一个个排查过去,逐一解决了地址与值的问题。
能够判断步伐后,计步器加入了其他的一些功能,比如说距离计算,速度计算和卡路里计算,这些计算都基于对正常人跨一步的步幅的假设进行,跨步大小在设计过程中是假设为身高/5,并且人的身高就暂且估计为180cm。这样在定时器中1.5s更新一次在过去1.5s中走出的步数,然后进行距离计算,距离是总步数*跨步长度。速度为1.5行走的步数*跨步长度/1.5s。卡路里是根据速度进行计算的,并且前后进行了累和,计算的是总消耗的卡路里大小。
在整个计步器系统的设计过程中,遇到了一些困难,但总的来说还是比较顺利的。通过这次设计,我对于MPU6050的读取、stm32f1的编程、oled屏幕的使用都有了更清楚的认识,对于串口调试和虚拟示波器的使用也更加熟练。总的来说还是非常有收获的。
七. 参考文献
1.《嵌入式微处理器原理与应用》 清华大学出版社
2.《计步器原理与算法总结》 CSDN博客
3.《MPU6050原始数据读取与F4算法》 知乎文章
八. 附件:部分程序
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "DataScope_DP.h"
#include "mpu6050.h"
#include "timer.h"
#include "oled.h"
#include "stdio.h"
#include "calculator.h"
/* 传感器数据修正值(消除芯片固定误差,根据硬件进行调整) */
#define X_ACCEL_OFFSET -350
#define Y_ACCEL_OFFSET 200
#define Z_ACCEL_OFFSET 2130
#define X_GYRO_OFFSET 32
#define Y_GYRO_OFFSET -11
#define Z_GYRO_OFFSET 1
void MPU6050_Display(void);
u8 k=0,flag=0;
u8 step_count=0;
int step_long=0;
int last_step_long=0;
u32 distance=0;
u32 speed=0;
u32 calorie=0;
int X,Y,Z;
axis_info_t accelerate[FILTER_CNT];
filter_avg_t filter;
axis_info_t sample;
peak_value_t peak;
axis_info_t cur_sample;
slid_reg_t slid;
u8 sample_size = 0;
int main(void)
{
unsigned char Send_Count; //发送数据通道数
unsigned char i; //循环变量
char a;
char avtive_zhou;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
delay_init(); //延时函数初始化
uart_init(115200); //串口1初始化
MPU6050_Init();
TIM3_Int_Init(7199,49); // (4999,719)为50ms (7199,49)为5ms (1999,719)为20ms
OLED_Init(); //初始化OLED
OLED_Clear() ;
sample_Init(&slid,&cur_sample);
peak_value_init(&peak);
while(1)
{
if(flag==1)
{
TIM_Cmd(TIM3, DISABLE);
for(i=0;i<FILTER_CNT;i++)
{
filter.info[i].x=accelerate[i].x; //给滤波器赋值
filter.info[i].y=accelerate[i].y;
filter.info[i].z=accelerate[i].z;
}
flag=0;
k=0;
filter_calculate(&filter,&sample);
cur_sample.x=sample.x;
cur_sample.y=sample.y;
cur_sample.z=sample.z;
X=cur_sample.x;
Y=cur_sample.y;
Z=cur_sample.z;
a=slid_update(&slid,&cur_sample);
if (sample_size > SAMPLE_SIZE)
{
peak_update(&peak,&cur_sample);
}
detect_step(&peak,&slid,&cur_sample);
TIM_Cmd(TIM3, ENABLE);
}
oled_show();
}
}
void MPU6050_Display(void)
{
/* 打印 x, y, z 轴加速度 */
printf("ACCEL_X: %d\t", MPU6050_Get_Data(ACCEL_XOUT_H) + X_ACCEL_OFFSET);
printf("ACCEL_Y: %d\t", MPU6050_Get_Data(ACCEL_YOUT_H) + Y_ACCEL_OFFSET);
printf("ACCEL_Z: %d\t", MPU6050_Get_Data(ACCEL_ZOUT_H) + Z_ACCEL_OFFSET);
/* 打印温度 */
printf("TEMP: %0.2f\t", MPU6050_Get_Data(TEMP_OUT_H) / 340.0 + 36.53);
/* 打印 x, y, z 轴角速度 */
printf("GYRO_X: %d\t", MPU6050_Get_Data(GYRO_XOUT_H) + X_GYRO_OFFSET);
printf("GYRO_Y: %d\t", MPU6050_Get_Data(GYRO_YOUT_H) + Y_GYRO_OFFSET);
printf("GYRO_Z: %d\t", MPU6050_Get_Data(GYRO_ZOUT_H) + Z_GYRO_OFFSET);
printf("\r\n");
}
#include "calculator.h"
//滤波器
extern u8 sample_size;
void filter_calculate(filter_avg_t *filter, axis_info_t *sample)
{
unsigned int i;
short x_sum = 0, y_sum = 0, z_sum = 0;
for (i = 0; i < FILTER_CNT; i++) {
x_sum += filter->info[i].x;
y_sum += filter->info[i].y;
z_sum += filter->info[i].z;
}
sample->x = x_sum / FILTER_CNT;
sample->y = y_sum / FILTER_CNT;
sample->z = z_sum / FILTER_CNT;
sample_size ++;
}
//动态阈值
void peak_update(peak_value_t *peak, axis_info_t *cur_sample)
{
if (sample_size > SAMPLE_SIZE) {
/*采样20个更新一次*/
sample_size = 1;
peak->oldmax = peak->newmax;
peak->oldmin = peak->newmin;
}
peak->newmax.x = MAX(peak->newmax.x, cur_sample->x);
peak->newmax.y = MAX(peak->newmax.y, cur_sample->y);
peak->newmax.z = MAX(peak->newmax.z, cur_sample->z);
peak->newmin.x = MIN(peak->newmin.x, cur_sample->x);
peak->newmin.y = MIN(peak->newmin.y, cur_sample->y);
peak->newmin.z = MIN(peak->newmin.z, cur_sample->z);
}
//动态精度
char slid_update(slid_reg_t *slid, axis_info_t *cur_sample)
{
char res = 0;
if (ABS((cur_sample->x - slid->new_sample.x)) > DYNAMIC_PRECISION) {
slid->old_sample.x = slid->new_sample.x;
slid->new_sample.x = cur_sample->x;
res = 1;
} else {
slid->old_sample.x = slid->new_sample.x;
}
if (ABS((cur_sample->y - slid->new_sample.y)) > DYNAMIC_PRECISION) {
slid->old_sample.y = slid->new_sample.y;
slid->new_sample.y = cur_sample->y;
res = 1;
} else {
slid->old_sample.y = slid->new_sample.y;
}
if (ABS((cur_sample->z - slid->new_sample.z)) > DYNAMIC_PRECISION) {
slid->old_sample.z = slid->new_sample.z;
slid->new_sample.z = cur_sample->z;
res = 1;
} else {
slid->old_sample.z = slid->new_sample.z;
}
return res;
}
/*判断当前最活跃轴*/
char is_most_active(peak_value_t *peak)
{
char res = MOST_ACTIVE_NULL;
short x_change = ABS((peak->newmax.x - peak->newmin.x));
short y_change = ABS((peak->newmax.y - peak->newmin.y));
short z_change = ABS((peak->newmax.z - peak->newmin.z));
if (x_change > y_change && x_change > z_change && x_change >= ACTIVE_PRECISION) {
res = MOST_ACTIVE_X;
} else if (y_change > x_change && y_change > z_change && y_change >= ACTIVE_PRECISION) {
res = MOST_ACTIVE_Y;
} else if (z_change > x_change && z_change > y_change && z_change >= ACTIVE_PRECISION) {
res = MOST_ACTIVE_Z;
}
return res;
}
extern u8 step_count;
/*判断是否走步*/
void detect_step(peak_value_t *peak, slid_reg_t *slid, axis_info_t *cur_sample)
{
static int step_cnt = 0;
char res = is_most_active(peak);
switch (res) {
case MOST_ACTIVE_NULL: {
break;
}
case MOST_ACTIVE_X: {
short threshold_x = (peak->oldmax.x + peak->oldmin.x) / 2;
if (slid->old_sample.x > threshold_x && slid->new_sample.x < threshold_x) {
step_cnt ++;
}
break;
}
case MOST_ACTIVE_Y: {
short threshold_y = (peak->oldmax.y + peak->oldmin.y) / 2;
if (slid->old_sample.y > threshold_y && slid->new_sample.y < threshold_y) {
step_cnt ++;
}
break;
}
case MOST_ACTIVE_Z: {
short threshold_z = (peak->oldmax.z + peak->oldmin.z) / 2;
if (slid->old_sample.z > threshold_z && slid->new_sample.z < threshold_z) {
step_cnt ++;
}
break;
}
default:
break;
}
step_count=step_cnt;
}
void peak_value_init(peak_value_t *peak)
{
peak->newmax.x=0;
peak->newmax.y=0;
peak->newmax.z=0;
peak->newmin.x=0;
peak->newmin.y=0;
peak->newmin.z=0;
peak->oldmax.x=0;
peak->oldmax.y=0;
peak->oldmax.z=0;
peak->oldmin.x=0;
peak->oldmin.y=0;
peak->oldmin.z=0;
}
void sample_Init(slid_reg_t *slid,axis_info_t *cur_sample)
{
cur_sample->x=0;
cur_sample->y=0;
cur_sample->z=0;
slid->new_sample.x=0;
slid->new_sample.y=0;
slid->new_sample.z=0;
slid->old_sample.x=0;
slid->old_sample.y=0;
slid->old_sample.z=0;
}
extern axis_info_t accelerate[FILTER_CNT];
extern u8 k,flag;
extern int step_long;
extern int last_step_long;
extern u8 step_count;
extern u32 distance;
extern u32 speed;
extern u32 calorie;
int i=0;
#define X_ACCEL_OFFSET -350
#define Y_ACCEL_OFFSET 200
#define Z_ACCEL_OFFSET 2130
//定时器3中断服务程序
void TIM3_IRQHandler(void) //TIM3中断
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查TIM3更新中断发生与否
{
TIM_ClearITPendingBit(TIM3, TIM_IT_Update ); //清除TIMx更新中断标志
accelerate[k].x = MPU6050_Get_Data(ACCEL_XOUT_H) + X_ACCEL_OFFSET;
accelerate[k].y = MPU6050_Get_Data(ACCEL_YOUT_H) + Y_ACCEL_OFFSET;
accelerate[k].z = MPU6050_Get_Data(ACCEL_ZOUT_H) + Z_ACCEL_OFFSET;
k++;
i++;
if(k==FILTER_CNT)
{
flag=1;
}
if(i==300)
{
i=0;
step_long=step_count-last_step_long;
last_step_long=step_count;
if(step_long<=2&&step_long>=0)
{
distance=(u32)height * step_count/5;
}
else if(step_long<=4&&step_long>=3)
{
distance=(u32)height * step_count/4;
}
speed=(u32)step_long * height/7.5;
calorie += (u32)speed * 1.25;
}
}
}