简介:这份文档面向关注可穿戴健康监测的电子工程学生与嵌入式开发者,围绕手腕佩戴场景,解决运动状态识别与生命体征实时监测的融合问题。内容以三轴加速度传感器ADXL345采集前臂加速度信号,经MATLAB分析处理识别散步、跑步、跳绳、羽毛球、骑单车等常见运动;同时用生物传感器MAX30100以光体积容积法采集心率等体征,结合运动强度评估身体状况。硬件方案还涵盖STM32F103主控、蓝牙传输、SD卡存储与LM1117稳压电源设计,并附硬件框图和电路原理图,可帮助读者理解从传感器选型到数据采集、存储与分析的完整链路。资源包仅含1个docx文件,约283KB,结构紧凑便于查阅。已有187人学习,适合作为课程设计或毕业设计的方案参考与硬件设计对照材料。
1. 手腕上的运动识别与心率监测:这套方案到底能跑出什么
手腕上戴个东西,跑两步就能认出你在散步还是跳绳,同时把心率曲线画出来——这套穿戴式运动及心率监测系统做的就是这件事。它用一颗三轴加速度传感器 ADXL345 采手腕加速度,用一颗生物传感器 MAX30100 采心率,主控是 STM32F103,数据落 SD 卡,再上传到电脑用 MATLAB 做预处理、特征提取和分类。适合谁?做嵌入式课程设计、可穿戴设备原型、运动识别算法入门的人。硬件不复杂,算法链路完整,从采数到识别一条龙,拿来当毕设底稿或者二次开发起点都够用。下面按硬件、软件、算法、避坑、改造五块拆开讲。
2. 硬件方案拆解:五模块怎么连、电源怎么稳
2.1 主控与传感器选型的取舍逻辑
STM32F103 是这套方案的主控,选它的理由很实际:外设够用、资料多、3.3V 供电和两颗传感器电压一致,省掉电平转换。ADXL345 走 IIC,分辨率 13 位,量程最高 ±16g,数字输出是 16 位二进制补码,手腕这种抖动幅度不大的场景完全够。MAX30100 同样走 IIC,用光体积容积法测心率和血氧,待机电流可忽略,适合长时间佩戴。
两颗传感器都挂 IIC 总线,这里有个细节要注意:ADXL345 的 ADDR 接高电平,器件地址是 0x1D(不含最低位),所以写地址 0x3A、读地址 0x3B。MAX30100 的地址是固定的 0xAE/0xAF(7 位地址 0x57)。两个地址不冲突,可以共用一条 IIC 总线,但初始化顺序和总线速率要统一,常见做法是都跑 400kHz 快速模式。
/* IIC 初始化:两颗传感器共用 PB6/PB7,400kHz */ void IIC_Init(void) { GPIO_InitTypeDef gpio; I2C_InitTypeDef i2c; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); /* PB6=SCL, PB7=SDA,开漏复用 */ 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.I2C_Mode = I2C_Mode_I2C; i2c.I2C_ClockSpeed = 400000; /* 400kHz,两颗传感器都支持 */ i2c.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; i2c.I2C_Ack = I2C_Ack_Enable; I2C_Init(I2C1, &i2c); I2C_Cmd(I2C1, ENABLE); }这段代码把 IIC1 配成 400kHz 快速模式,PB6/PB7 开漏复用。参数里I2C_ClockSpeed别贪高,手腕线束长、干扰大,400kHz 是稳妥值;再往上跑容易在长导线上出误码。I2C_Ack必须使能,否则读传感器时收不到应答,读回全 0xFF 这种玄学现象多半就是这里没开。
2.2 电源与存储:LM1117 稳压和 SD 卡落盘
整机只用 3.3V,5V 锂电池经 LM1117-3.3 降到 3.3V。LM1117 是线性稳压,压差 1.2V 左右,5V 进 3.3V 出没问题,但要注意散热——如果蓝牙模块和 SD 卡同时工作,电流能到 100mA 以上,LM1117 会发热,PCB 上给它留个铺铜焊盘。输入输出各挂 10μF 和 0.1μF 电容滤波,这是手册里的标准做法,别省。
SD 卡走 SPI 模式接 STM32,用来存加速度和心率原始数据,方便后面训练分类器。存储这块的坑在于写入频率:如果每采一个点就写一次 SD 卡,SPI 时序会被频繁打断,采样率掉得厉害。常见做法是开一个环形缓冲区,攒够 512 字节再整块写,既保证采样连续,又减少擦写次数。
/* SD 卡块写入:攒满 512 字节再落盘 */ #define BUF_SIZE 512 static uint8_t ring_buf[BUF_SIZE]; static uint16_t buf_idx = 0; void data_sink(uint8_t byte) { ring_buf[buf_idx++] = byte; if (buf_idx >= BUF_SIZE) { /* 整块写入,避免逐字节写拖慢采样 */ SD_WriteBlock(ring_buf, BUF_SIZE); buf_idx = 0; } }data_sink每收到一个字节就塞进缓冲区,满 512 字节触发一次块写。SD_WriteBlock是底层 SPI 块写函数,一次写一个扇区。这样采样中断里只做内存拷贝,不碰 SD 卡时序,采样率稳得住。缓冲区大小取 512 是因为 SD 卡扇区就是 512 字节,对齐后写入效率最高。
3. 软件主流程:从初始化到上位机显示的完整链路
3.1 上电初始化顺序与中断配置
系统上电后先配时钟,再配引脚,然后初始化传感器、SD 卡、蓝牙,最后开 NVIC 中断。顺序不能乱:传感器初始化依赖 IIC 总线,IIC 又依赖 GPIO 和时钟,SD 卡依赖 SPI,蓝牙依赖 USART。任何一步提前都会导致后续模块读不到设备。
int main(void) { SystemInit(); /* 72MHz 主频 */ delay_init(); IIC_Init(); /* 先通总线 */ ADXL345_Init(); /* 再初始化加速度传感器 */ MAX30100_Init(); /* 心率传感器 */ SD_Init(); /* SD 卡 */ USART1_Init(115200); /* 蓝牙串口 */ NVIC_Config(); /* 最后开中断 */ while (1) { /* 主循环只做状态机调度,采集在中断里 */ if (sample_ready) { sample_ready = 0; process_sample(); } } }SystemInit把主频拉到 72MHz,delay_init配好 SysTick。传感器初始化放在 IIC 之后,是因为ADXL345_Init内部要写配置寄存器(设置量程、数据速率、中断使能),总线没通就会卡在等待应答。NVIC_Config放最后,避免中断提前触发时外设还没配好。主循环里只做标志位调度,真正的采集放在定时器中断里,保证采样周期稳定。
3.2 采样中断与数据打包上传
采样用定时器中断触发,比如 50Hz,每次中断读 ADXL345 的三轴数据和 MAX30100 的 FIFO。读完后打包成一帧,通过蓝牙串口发给上位机,同时写 SD 卡。帧格式要设计好,常见做法是帧头 + 时间戳 + 三轴加速度 + 心率 + 校验。
/* 50Hz 定时器中断:采一帧并打包 */ void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); int16_t ax, ay, az; uint8_t hr; ADXL345_ReadXYZ(&ax, &ay, &az); /* 三轴原始值 */ MAX30100_ReadFIFO(&hr); /* 心率值 */ uint8_t frame[12]; frame[0] = 0xAA; /* 帧头 */ frame[1] = 0x55; frame[2] = tick & 0xFF; /* 时间戳低字节 */ frame[3] = (tick >> 8) & 0xFF; frame[4] = ax & 0xFF; /* 加速度小端 */ frame[5] = (ax >> 8) & 0xFF; frame[6] = ay & 0xFF; frame[7] = (ay >> 8) & 0xFF; frame[8] = az & 0xFF; frame[9] = (az >> 8) & 0xFF; frame[10] = hr; frame[11] = checksum(frame, 11); /* 前 11 字节异或校验 */ USART_SendData(USART1, frame, 12); data_sink_frame(frame, 12); /* 同时落 SD 卡 */ tick++; } }中断里读传感器、组帧、发串口、写缓冲,四件事一气呵成。ADXL345_ReadXYZ读的是 16 位补码,直接存int16_t不用转换。帧头 0xAA55 用来做上位机同步,校验用异或最简单,够用。注意中断里不要做浮点运算和长延时,data_sink_frame只做内存拷贝,真正的 SD 写放在主循环,避免中断超时。
3.3 上位机侧的数据接收与显示
上位机用 MATLAB 写,串口读帧、解析、实时画图。核心是找帧头、校验、拆字段。MATLAB 的serialport对象配好波特率 115200,回调里读数据。
% MATLAB 上位机:串口收帧并实时绘图 s = serialport("COM3", 115200); configureCallback(s, "terminator", @readFrame); function readFrame(src, ~) data = read(src, 12, "uint8"); if data(1) == 170 && data(2) == 85 % 0xAA 0x55 if bitxor_all(data(1:11)) == data(12) % 校验通过 ax = typecast(uint8([data(5) data(6)]), "int16"); ay = typecast(uint8([data(7) data(8)]), "int16"); az = typecast(uint8([data(9) data(10)]), "int16"); hr = data(11); updatePlot(ax, ay, az, hr); % 更新曲线 end end endread一次读 12 字节,先对帧头,再校验,通过才拆字段。typecast把两个字节还原成int16,注意小端顺序要和下位机一致。updatePlot里用drawnow limitrate刷新,别用drawnow,否则高频刷新会拖垮界面。这套接收逻辑简单但够稳,血泪经验是校验一定要做,串口丢字节时没校验会画出乱七八糟的曲线。
4. 算法链路:预处理、特征提取与阈值分类器
4.1 卡尔曼滤波去毛刺与归一化
手腕加速度信号毛刺多,来源是肌肉抖动和设备机械振动。方案用卡尔曼滤波去噪,核心是预测 + 更新两步。工程上不用手推矩阵,直接套一维卡尔曼的简化形式就够。
% 一维卡尔曼滤波:对单轴加速度去噪 function y = kalman1d(x, Q, R) n = length(x); y = zeros(1, n); P = 1; xhat = x(1); for k = 1:n % 预测 xhat = xhat; % 状态转移为 1 P = P + Q; % 更新 K = P / (P + R); xhat = xhat + K * (x(k) - xhat); P = (1 - K) * P; y(k) = xhat; end endQ是过程噪声协方差,R是观测噪声协方差。手腕场景下Q取 1e-4、R取 1e-2 是个不错的起点,Q调大响应快但毛刺多,R调大平滑但滞后。滤波后做归一化,把幅度统一到 [-1,1],消除个体动作幅度差异,这样训练出的分类器换个人也能用。
4.2 滑动窗口与四类特征值
加窗用滑动窗口,窗口长度常见取 2 秒(50Hz 下 100 个点),相邻窗口重叠 50%。重叠率 50% 是文献里验证过识别效果较好的值,既能保证窗口间平滑过渡,又不至于计算量翻倍。每个窗口提四个特征:平均速度、最大速度、最小速度、最大频率。
% 滑动窗口特征提取 win_len = 100; step = 50; % 2 秒窗,50% 重叠 feat = []; for i = 1:step:(length(acc) - win_len) seg = acc(i : i + win_len - 1); avg_v = mean(abs(seg)); % 平均速度 max_v = max(abs(seg)); % 最大速度 min_v = min(abs(seg)); % 最小速度 f = fft(seg - mean(seg)); [~, idx] = max(abs(f(1:win_len/2))); max_f = idx * 50 / win_len; % 最大频率(Hz) feat = [feat; avg_v, max_v, min_v, max_f]; endmean(abs(seg))反映整体运动强度,max/min抓极值,FFT 取前半段找主频。idx * 50 / win_len把频点换算成赫兹,50 是采样率。这四个特征对应原文的分析:散步和跑步差在水平速度,骑车速度更快但频率平,跳绳频率快且速度接近 0,羽毛球有短暂静止加高频挥拍。
4.3 多层阈值分类器与心率联动预警
分类器用多层阈值,先按水平平均速度分三档:小于 7 判散步,7 到 15 判跑步,大于 15 判骑车。再用竖直方向频率区分羽毛球和跳绳。判完运动类型,结合心率做身体状态评估。
% 多层阈值分类 + 心率预警 function [motion, alert] = classify(feat, hr) avg_v = feat(1); max_f = feat(4); if avg_v < 7 motion = "walk"; elseif avg_v < 15 motion = "run"; else motion = "cycle"; end % 高频且低速 → 跳绳;高频但有静止段 → 羽毛球 if max_f > 2.5 && avg_v < 3 motion = "rope"; elseif max_f > 2.0 && avg_v >= 3 && avg_v < 7 motion = "badminton"; end % 心率预警:不同运动对应不同安全上限 limits = struct("walk", 120, "run", 160, "cycle", 150, ... "rope", 170, "badminton", 165); alert = hr > limits.(motion); end阈值 7 和 15 是原文给的,单位是归一化后的速度值,实际部署时要按你的量程和归一化方式重新标定。心率上限按运动类型区分,散步 120、跑步 160、跳绳 170,超了置预警位。这套分类器简单、可解释、易调,缺点是阈值靠人工标定,换传感器或换佩戴位置要重标,这也是后面改造要解决的点。
5. 避坑与排查:五个真实会翻车的地方
5.1 IIC 读回全 0xFF
现象:初始化 ADXL345 后读设备 ID,返回 0xFF。原因多半是 IIC 总线没通——上拉电阻没焊、SDA/SCL 接反、或者 ADDR 引脚悬空导致地址不对。解决:先用示波器看 SCL 有没有时钟,再量 SDA 空闲时是不是高电平,最后确认 ADDR 接高电平、写地址用 0x3A。上拉电阻常见取 4.7kΩ,别用 10kΩ 以上,400kHz 下上升沿会太慢。
5.2 心率值跳变剧烈
现象:MAX30100 读出的心率在 40 到 180 之间乱跳。原因通常是手指/手腕没贴紧,环境光漏进光电探测器,或者 LED 驱动电流设太小。解决:结构上做遮光,传感器紧贴皮肤;软件上把 LED 电流调到 50mA 左右,开 FIFO 平均,连续 5 帧取中位数再输出。别直接用单帧值,单帧噪声大是常态。
5.3 SD 卡写入导致采样丢点
现象:开 SD 卡记录后,加速度曲线出现规律性缺口。原因就是前面说的逐字节写 SD 卡,SPI 块写耗时几十毫秒,把采样中断拖住了。解决:改成环形缓冲区攒块写,采样中断只做内存拷贝,SD 写在主循环低优先级任务里做。如果还丢,把 SD 卡时钟从 18MHz 降到 9MHz 试试。
5.4 归一化后分类器换人就失效
现象:自己戴识别准,换个人散步被判成跑步。原因是归一化用了全局最大最小值,不同人动作幅度差异大,归一化基准不一致。解决:改成按窗口内最大值归一化,或者用滑动窗口的均值方差做标准化(z-score),让每个窗口独立归一,消除个体差异。这一步不做,分类器就是“认人不认动作”。
5.5 蓝牙串口丢帧
现象:上位机画图偶尔断一截。原因是串口波特率 115200 下,中断里发 12 字节耗时约 1ms,如果采样周期也是 1ms 级别,发送会挤占采样。解决:串口发送改成 DMA,中断里只填缓冲区;或者把采样率降到 50Hz 以下,给串口留余量。校验位一定要留,丢帧时上位机能靠校验丢弃坏帧而不是画错。
6. 二次开发:把阈值分类器换成可训练模型
这套方案的分类器是人工阈值,优点是轻、可解释,缺点是换传感器、换佩戴位置就得重标。二次开发最值得动的一刀,是把阈值分类器换成可训练的轻量模型,同时保留原来的特征提取链路。
具体做法:先用现有硬件采一批带标签的数据,每种运动至少 5 分钟,覆盖不同人、不同佩戴松紧。特征还是那四个(平均速度、最大最小速度、最大频率),但不再手写阈值,而是喂给一个三分类/五分类的小模型。MATLAB 里用fitcecoc做多类 SVM 最快,几十行就能跑通;要上嵌入式,就把训练好的树模型或线性 SVM 系数导成 C 数组,在 STM32 上做定点推理。
% 用采集的特征训练多类 SVM,替代手写阈值 load("features.mat"); % feat: N×4, label: N×1 mdl = fitcecoc(feat, label); % 多类 SVM pred = predict(mdl, feat_val); % 验证集预测 acc = mean(pred == label_val); fprintf("验证集准确率: %.2f%%\n", acc * 100); % 导出线性系数供嵌入式定点推理 % 注意:fitcecoc 多类模型导出较复杂, % 工程上更常见的是用 fitctree 单棵树,直接导出分裂节点验证方法很关键:别用训练集自测,一定留出独立受试者的数据做验证,否则准确率虚高。我一般按人划分训练/验证集,同一个人不能同时出现在两边,这样测出来的才是跨个体泛化能力。改造后如果准确率反而降了,先查特征是否做了窗口内标准化,再查标签是否对齐——这两处翻车最多。
心率联动那块也可以升级:现在是一刀切上限,改成用运动强度做动态阈值,比如用最近 30 秒心率斜率判断是否过载,比固定上限更贴合实际。从那以后我每次改分类器,都强制先跑一遍跨受试者验证再上板子,不然板子上看着准、换个人就废。希望帮到你。
本文还有配套的精品资源,点击获取