简介:面向单片机毕业设计及电子竞赛人群,资源整合两套完整方案:其一是基于51单片机实现步数检测与无线蓝牙APP上传,其二是基于STM32同时采集温度、心率与步数,适用于智能穿戴、健康监测等毕设选题。两套资料均配备原理图、完整源程序、元件清单、开题报告、论文及实物图,覆盖从硬件搭建、程序编写到论文撰写的全流程。压缩包共290个文件,整体约25MB,核心文件为C/H语言源码、schdoc原理图、PDF论文和DOC文档,另含mp4演示视频、安卓APK、Hex烧录文件与A51启动文件,极大便利二次开发与实物调试;原理图与源程序分区存放,方便按模块查找。其中包含的JPG图片还原了实物搭建过程,可辅助核对硬件连线。当前已有338人学习下载,适合需要快速参考完整项目、完善自身毕设内容的毕业生和嵌入式学习者。
1. 这个题目真正难的不是蓝牙,是步数算法
很多人看到“基于单片机步数检测仪&计步器&无线蓝牙APP”这个毕业设计题目,第一反应是蓝牙模块连手机很难。实际做过一遍就知道,HC-05蓝牙模块用AT指令配好串口,十分钟就能把数据发到手机;真正反复调一两个星期的是计步器“乱跳”的问题——放在桌上静止时步数自己涨,挂在腰间走楼梯却漏计。这个题目的核心价值在于把加速度传感器信号变成稳定可靠的步数,蓝牙APP只是把结果搬上手机。对做毕设的人来说,两套资料(通常一套51单片机版、一套STM32版)里最有分量的也是源程序里的步数检测算法和调试记录,而不是原理图本身。下面按硬件选型、固件实现、蓝牙与APP、资料整理这条线把完整方案讲清楚,后面可以直接照着搭。
2. 硬件选型与原理图设计:从ADXL345到HC-05的完整链路
2.1 主控单片机怎么选:51与STM32的取舍
这种题目最常见的做法是51单片机(STC89C52或STC12C5A60S2)和STM32F103C8T6各做一套,刚好构成标题里的“两套资料”。51的优势是入门门槛低、课程里学过、资料多,缺点是主频只有12MHz左右,做步数检测时的浮点运算和滤波要写成整数运算,否则实时性吃紧。STM32F103C8T6主频72MHz,硬件I2C、硬件串口都有,做动态阈值算法和蓝牙透传更从容,但工程结构复杂,不适合零基础硬啃。
如果让我推荐,毕设主体用STC89C52已经足够:ADXL345通过I/O口模拟I2C读取,蓝牙模块走串口TXD/RXD,OLED显示用I2C或SPI,三个外设的资源占用对51来说完全可行。选择STC89C52还有一个隐性好处:STC的芯片支持串口下载,不需要额外买仿真器,实物演示时现场烧录更方便,这在答辩时是加分项。STM32那套可以做成“升级版”,重点展示价格更高的传感器和更平滑的滤波效果。
2.2 加速度传感器:计步器的“脚感”来源
计步器的物理基础是人体行走时腰部或手腕产生的周期性加速度。比较常见的是ADXL345和MPU6050。查一下芯片手册就能看出差别:
| 项目 | ADXL345 | MPU6050 |
|---|---|---|
| 输出 | 三轴加速度 | 三轴加速度+三轴陀螺仪 |
| 接口 | I2C/SPI | I2C |
| 量程 | ±2g/±4g/±8g/±16g | ±2g/±4g/±8g/±16g |
| 分辨率 | 13位 | 16位 |
| 功耗 | 低功耗模式 | 略高 |
| 毕设友好度 | 寄存器简单,资料多 | 需要处理陀螺仪融合,容易跑偏 |
做纯计步器,MPU6050的陀螺仪其实用不上,反而会让算法复杂度上升。ADXL345的13位分辨率在±2g量程下约为3.9mg/LSB,已经能分辨走路时的微小震动。实测挂在裤兜侧边时,Z轴和Y轴的加速度变化幅度在0.3g到0.8g之间;放在桌上静止时噪声通常小于0.05g,这就是设计阈值的重要参考。
原理图里ADXL345的接法比较简单:VCC接3.3V,GND接地,SDA接P2.0,SCL接P2.1(用GPIO模拟I2C),CS引脚接VCC(选择I2C模式),SDO接地(器件地址0x53)。很多人在这里踩坑:如果SDO悬空或接高,地址会变成0x1D,读不到数据时会误以为芯片坏了。
2.3 蓝牙模块与显示:无线链路和本地读数
蓝牙模块最经典的是HC-05(双模,可主从模式切换)和HC-06(从机专用)。对于手机APP接数据,HC-06就够了,因为手机永远做主机。HC-05虽然贵一点,但能配置成主机模式,以后可以扩展用单片机去连别的蓝牙设备,毕设题目里写“无线蓝牙APP”并没有硬性要求双向控制,用HC-06更省事。
连接时注意电平匹配。51单片机TXD是5V电平,HC-05/HC-06的逻辑电平是3.3V,直接连RXD会长期超压,轻则通信不稳定,重则烧模块。正规做法是在单片机TXD到模块RXD之间串一个1kΩ电阻分压,或者用两个电阻构成3.3V分压。原理图上这个串阻一定要画出来,这在答辩时经常被老师追问。
显示部分用0.96寸OLED(SSD1306)最合适,4个引脚(VCC、GND、SCL、SDA),I2C接口,和传感器共用一个总线,代码里只要7条指令就能显示步数和状态。LED数码管也能用,但显示“步数”“距离”“卡路里”这些多行信息时OLED的排版优势太明显,实物图也更高级。
2.4 元件清单与电源设计
元件清单不是简单罗列型号,要给出封装、数量、参数和用途。一份能直接拿去买料的清单长这样:
| 序号 | 元件名称 | 规格/封装 | 数量 | 用途 |
|---|---|---|---|---|
| 1 | 单片机 | STC89C52RC/DIP40 | 1 | 主控 |
| 2 | 加速度传感器 | ADXL345模块/PCB | 1 | 步态检测 |
| 3 | 蓝牙模块 | HC-06 | 1 | 无线传输 |
| 4 | OLED屏 | 0.96寸 SSD1306 | 1 | 本地显示 |
| 5 | 晶振 | 11.0592MHz | 1 | 串口波特率基准 |
| 6 | 电容 | 22pF/瓷片 | 2 | 晶振负载 |
| 7 | 电解电容 | 10uF/50V | 2 | 电源滤波 |
| 8 | 电阻 | 1kΩ/0805 | 2 | 蓝牙串口分压 |
| 9 | 按键 | 轻触开关 | 2 | 复位/清零 |
| 10 | 电源 | 3.7V锂电池+充放电模块 | 1 | 便携供电 |
| 11 | 开关 | 拨动开关 | 1 | 电源控制 |
电源是最容易被忽略的坑。ADXL345和OLED都需要3.3V,如果用5V直接供,必须加稳压芯片(常用AMS1117-3.3)。锂电池放电电压在3.7V到4.2V之间,经过AMS1117-3.3后输出稳定3.3V;而单片机用5V供电时,最好先用一节18650配合升压板到5V,再把模拟部分单独用LDO供电,避免电机或蓝牙突发电流导致加速度数据跳变。原理图上的电源部分要画两路:5V数字主供电和3.3V传感器供电,电容靠近每个芯片引脚放置。
3. 源程序实现:I2C驱动、步数检测与滤波参数
3.1 I2C读ADXL345:起始代码
ADXL345用I2C读三轴数据,不需要外部库,直接GPIO模拟即可。下面这段是51环境的读取函数,重点在时序初始化和数据拼接:
#include <reg51.h> #include <intrins.h> #define ADXL345_ADDR 0xA6 // SDO接GND时地址为0x53,左移1位得0xA6 sbit SCL = P2^1; sbit SDA = P2^0; void I2C_Start(void) { SDA = 1; SCL = 1; _nop_(); SDA = 0; _nop_(); SCL = 0; } void I2C_Stop(void) { SDA = 0; SCL = 1; _nop_(); SDA = 1; } void I2C_SendByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { SDA = (dat & 0x80) ? 1 : 0; dat <<= 1; SCL = 1; _nop_(); SCL = 0; } SDA = 1; // 释放SDA,等待应答 SCL = 1; _nop_(); SCL = 0; } unsigned char I2C_ReadByte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { dat <<= 1; SCL = 1; _nop_(); if (SDA) dat |= 0x01; SCL = 0; } return dat; } void ADXL345_WriteReg(unsigned char addr, unsigned char val) { I2C_Start(); I2C_SendByte(ADXL345_ADDR); I2C_SendByte(addr); I2C_SendByte(val); I2C_Stop(); } void ADXL345_ReadXYZ(int *x, int *y, int *z) { unsigned char buf[6]; unsigned char i; I2C_Start(); I2C_SendByte(ADXL345_ADDR); I2C_SendByte(0x32); // DATAX0寄存器地址 I2C_Start(); I2C_SendByte(ADXL345_ADDR | 0x01); // 读模式 for (i = 0; i < 6; i++) { buf[i] = I2C_ReadByte(); } I2C_Stop(); *x = (int)((buf[1] << 8) | buf[0]); *y = (int)((buf[3] << 8) | buf[2]); *z = (int)((buf[5] << 8) | buf[4]); } void ADXL345_Init(void) { ADXL345_WriteReg(0x31, 0x08); // DATA_FORMAT: 0x08表示±4g量程,13位分辨率 ADXL345_WriteReg(0x2D, 0x08); // POWER_CTL: 0x08表示测量模式 }这里关键参数有两个:0x31寄存器的值设为0x08,作用是选择±4g量程和13位输出,让每LSB对应约3.9mg;0x2D寄存器设为0x08,把芯片从待机切到测量模式。读三轴数据时,寄存器地址0x32到0x37分别是X、Y、Z的低字节和高字节,注意I2C读阶段要先发一个Start,表示切换为读操作。
如果读出来的XYZ值始终是0或满量程跳变,第一步检查SDO引脚是否接地,第二步检查I2C起始信号后是否有应答——可以在I2C_SendByte后面加一个读应答位的判断,但初版调试时直接用逻辑分析仪看SDA波形最快。
3.2 步数检测核心:阈值、峰值与动态更新
单纯读加速度值不等于步数。人体走路时加速度波形是近似正弦的周期信号,一个步态周期包含一个“波峰”和一个“波谷”。最容易理解的算法是峰值检测加阈值过滤:
#define SAMPLE_INTERVAL_MS 20 // 采样周期50Hz #define THRESHOLD_INIT 35 // 初始阈值,对应约0.14g #define PEAK_DELAY_MS 300 // 相邻步最小间隔 static int step_count = 0; static int last_peak = 0; static unsigned char have_peak = 0; static unsigned long last_step_time = 0; int calculate_steps(int ax, int ay, int az, unsigned long now_ms) { int magnitude = (int)sqrt((long)ax*ax + (long)ay*ay + (long)az*az); int threshold = THRESHOLD_INIT; // 动态阈值:取最近50次样本的平均振幅的一半 static int samples[50]; static unsigned char idx = 0, is_full = 0; samples[idx] = magnitude; idx = (idx + 1) % 50; if (idx == 0) is_full = 1; if (is_full) { long sum = 0; for (unsigned char i = 0; i < 50; i++) sum += samples[i]; threshold = (sum / 50) / 2; if (threshold < 20) threshold = 20; // 防止静止时阈值过低 } // 峰值检测:比前后样本都大,且超过阈值 static int prev_mag = 0; int next_mag = magnitude; // 实际应用中应读取下一周期值 if (magnitude > prev_mag && magnitude > next_mag && magnitude - samples[(idx+49)%50] > threshold) { if (now_ms - last_step_time > PEAK_DELAY_MS) { step_count++; last_step_time = now_ms; } } prev_mag = magnitude; return step_count; }这段代码展示了计步算法的三个关键点:
- 矢量幅度:把三轴加速度合成一个
magnitude,这样不管设备是横放、竖放还是斜放,只要运动幅度达到阈值就能触发,避免只检测单轴的朝向问题。 - 动态阈值:静止时加速度幅度稳定在1g附近,走路时则在1g上下波动。用滑动窗口的平均值作为基线,再取基线的一半作为阈值,比固定阈值更抗漂移。注意代码里限定了
threshold最小值为20,否则静止时噪声可能偶尔超过阈值导致误计。 - 最小间隔300ms:正常步行时步频在每分钟90到130步,即每一步间隔约为460ms到670ms。把相邻步最小间隔设为300ms,可以滤掉跑步时约250ms间隔的部分场景,如果题目还要测跑步,这个值要降到200ms。
实际工程中不会用sqrt和long乘法来算幅度,因为51单片机做浮点运算慢。可以把三轴绝对值求和近似为幅度,比如mag = abs(ax) + abs(ay) + abs(az),效果差异不大,但速度快几倍。
3.3 参数调优与误计排除
调计步参数最有效的方式不是看手机APP,而是先把步数结果通过串口输出,配合串口绘图工具(比如VOFA+或SerialPlot)观察波形。我一般会写一个调试模式,把magnitude值和阈值实时打印出来,然后做几组对照测试:
| 测试场景 | 预期行为 | 需要调整的参数 |
|---|---|---|
| 桌面静止1分钟 | 步数为0 | 阈值下限 |
| 正常步行20步 | 计步误差≤2步 | 峰值判定条件、最小间隔 |
| 上下楼梯20级 | 不翻倍、不漏计 | 采样率、阈值 |
| 快速抖动手腕 | 步数尽量不增加 | 幅度变化率限制 |
| 放口袋慢走 | 能正确计步 | 动态阈值基线窗口 |
楼梯场景最容易出问题。因为上下楼时垂直方向加速度峰值更大,一个完整步伐可能出现两个明显的峰值(脚跟落地和脚尖发力),如果只按峰值计数就会翻倍。解决办法有两个:一是把PEAK_DELAY_MS提高到400ms;二是要求本次峰值与前一次谷值之差超过阈值,且峰值点必须伴随明显的方向反转,而不仅仅是幅度大。
常见的误计原因是传感器固定不牢。如果用杜邦线连接模块,走路时导线晃动会让加速度数据毛刺变大。实物制作时要把ADXL345用热熔胶固定在外壳的硬质内壁上,不能悬空。另一个容易忽略的是电源纹波:蓝牙模块在发射数据时的电流尖峰可达几十毫安,如果传感器和蓝牙共用一条细导线供电,纹波会叠加进加速度数据里,表现为步数莫名跳动。
4. 蓝牙通信与APP端:从AT指令到手机实时显示
4.1 HC-05/HC-06的AT配置
HC-06上电后会自动进入透传模式,不需要AT指令就能用。但如果你想改配对密码、修改设备名称,或者用HC-05做主从机切换,就需要先进入AT模式。HC-06的AT模式是:按住模块上的EN键上电,此时串口波特率固定为9600。HC-05则要拉高KEY引脚再上电,AT指令的波特率常见是38400。
进入AT模式后用USB-TTL连接模块,打开串口助手,发送以下指令:
# 测试通信 AT # 修改名称 AT+NAME=StepMeter # 修改配对密码 AT+PIN=8888 # HC-05设置为从机模式 AT+ROLE=0 # 查询版本 AT+VERSION每条指令以回车换行结尾,串口助手要勾选“发送新行”。注意HC-06的AT指令没有AT+ROLE,它本身就是从机。配置完成后重新上电,用手机搜索蓝牙,能找到名为“StepMeter”的设备,配对密码是8888。
这里有一个和单片机无关的坑:USB-TTL模块和HC-06的TXD、RXD要交叉连接,而且VCC接5V还是3.3V要看模块型号。老版本HC-06的VCC可以接5V,板上自带稳压;但有些带逻辑电平转换的新版本只支持3.3V,接5V会烧。如果是毕设,建议直接用3.3V供电,同时让单片机侧的TXD经过分压再连接HC-06的RXD。
4.2 单片机与蓝牙的数据帧协议
蓝牙透传本质是串口,单片机往串口发什么,手机就收到什么。为了让APP端解析稳定,需要自定义一个简单的数据帧。下面是我常用的格式:
#define FRAME_HEADER 0xAA #define FRAME_TYPE_STEP 0x01 void send_step_frame(unsigned int steps, unsigned int distance_m) { unsigned char buf[8]; buf[0] = FRAME_HEADER; buf[1] = FRAME_TYPE_STEP; buf[2] = steps >> 8; buf[3] = steps & 0xFF; buf[4] = distance_m >> 8; buf[5] = distance_m & 0xFF; buf[6] = (steps ^ distance_m) & 0xFF; // 简单校验 buf[7] = 0x0D; // 结束符 for (unsigned char i = 0; i < 8; i++) { SBUF = buf[i]; while (!TI); TI = 0; } }这个帧固定8字节:帧头0xAA、类型字节、两个16位数据(步数和距离)、一个校验字节、一个回车结束符。校验用的是异或,虽然不如CRC可靠,但应付蓝牙传输足够,APP端收到后如果校验不对就直接丢弃整帧,等下一帧。
为什么不用简单粗暴的sprintf("STEP:%d\r\n", steps)?因为手机APP解析字符串时要处理变长内容,而且如果单片机端的打印和蓝牙发送共用同一串口,调试信息和业务数据混在一起,APP根本没法区分。固定长度二进制帧解析简单,一个switch就搞定。
4.3 APP端实现:串口转蓝牙的通用思路
手机APP接收蓝牙数据,本质就是一个蓝牙串口终端。Android上最常用的是调用系统的BluetoothSocket,UUID使用标准的SPP服务UUID00001101-0000-1000-8000-00805F9B34FB。下面是一个简化的连接和接收线程代码:
// 伪代码,假设已有UUID和已配对设备device BluetoothSocket socket = device.createRfcommSocketToServiceRecord(SPP_UUID); socket.connect(); InputStream inputStream = socket.getInputStream(); byte[] buffer = new byte[128]; int bytes; while (true) { bytes = inputStream.read(buffer); for (int i = 0; i < bytes; i++) { // 把字节放入环形缓冲区,解析帧 parseFrame(buffer[i]); } } private void parseFrame(byte b) { // 状态机解析:0=等待AA,1=等待类型,2=等待数据及校验 }这里要点是“状态机解析”,每次只处理一个字节,而不是等一帧完整到达才处理。因为蓝牙串口底层是流式数据,一帧可能被拆成好几次read,也可能一次读回多个帧。状态机思路:第一个字节匹配0xAA才进入状态1,状态1读取类型,状态2累计数据长度,最后校验+回车判断一帧结束。这个逻辑在任何语言里都能复用,Flutter里也有flutter_bluetooth_serial这个库,事件回调同样是按字节流给的。
APP界面不需要做太复杂,实时显示步数、距离、一个连接按钮、一个清零按钮就够了。答辩演示时最忌讳的是APP界面功能太多但每个都不稳定。我建议在手机上再装一个现成的“蓝牙串口助手”作为备选,万一自己写的APP在演示时崩溃,用串口助手一样能显示数据,不至于当场翻车。
5. 毕业设计资料整理:让评审一眼看到工作量
5.1 原理图、元件清单与实物图的对应关系
标题里的“共两套资料”通常指51版和STM32版,在做资料包时要保证两套之间不是完全复制,而是有递进关系。原理图可以用立创EDA或Altium Designer画,导出PDF和图片版本。元件清单要与原理图里的网络标号一一对应,最好在清单里增加一列“原理图位号”,比如R1、C3,这样老师检查时能快速定位。实物图一定要拍三个角度:正面、侧面、电源接线特写。并且拍照前把杜邦线整理好,用扎带固定,模糊的背景换成一本书或硬纸板,观感差别很大。
5.2 开题报告与论文的写法重点
开题报告的核心是“为什么做”和“怎么做”。不要写“因为老师选了题目”,要写市场上有运动手环但成本高,或者老人步数监测需要低成本的蓝牙传输方案,这类合理的需求描述。论文的章节结构建议按“绪论、硬件设计、软件设计、系统测试、总结”展开,其中系统测试要放真实数据:静止30分钟步数0、步行50步实测48步、误差率4%,以及蓝牙传输距离在空旷环境10米内丢包率低于1%这类表格。
5.3 验证与演示:让计步器“当场”可靠
答辩前的最后一步,不是写代码,而是做一次完整的“三分钟演示”预演。把手机蓝牙断开再重新连接,确认自动配对;把计步器放在桌上,确认步数不跳;然后走20步,确认APP显示数字变化。如果演示时单片机突然不工作,先检查是不是电池电压跌到3.3V以下——很多实物作品在演示中途死机都是供电不足导致的。准备一个移动电源和一根USB线作为备用电源,比反复换电池更稳妥。演示成功后,顺手把串口打印接上电脑,让老师看原始加速度波形,这是一道“加分题”,能证明你不是只会抄代码。
本文还有配套的精品资源,点击获取