
简介面向嵌入式开发入门与课程设计/毕业设计人群这套基于STM32F103C8T6的智能手环主控工程方案实现了ADXL345计步、心率检测、体温监测与LCD1602动态显示是一份可直接运行和二次开发的完整参考。工程使用STM32F10x标准外设库源码按HARDWARE、SYSTEM、CORE分层组织传感器驱动与显示逻辑分离模块边界清楚便于阅读存储算法、传感器数据解析和显示刷新逻辑。压缩包共92个文件以C/H源码为主含38个h、37个c同时提供原理图SchDoc、PCB设计PDF、设计说明文档、功能清单txt、Hex固件、keilkilll.bat清理脚本及demo.py工具脚本整体大小约454KB结构紧凑、检索方便。目前已有62人学习下载适合对照硬件原理图和源码快速上手也可作为电子设计竞赛、毕设或智能可穿戴项目的基础工程在此基础上可继续扩展显示界面、数据存储或无线上传功能。 STM32F103C8T6ADXL345LCD1602这三个词放在一起基本就是一个典型的单片机健康监测项目。我做这个手环主控工程的初衷很简单手头正好有一堆最小系统板和传感器模块与其让它们吃灰不如整合成一个能戴在手腕上、实时显示步数、心率和体温的小设备。整套东西的成本压得非常低核心板加传感器加LCD几十块钱就能拿下。适合谁看一是学过GPIO、定时器但不知道传感器怎么组合的在校同学二是想拿一个完整软硬件方案做课设或作品演示的开发者。如果你只是想抄一套能跑的代码它也足够直接后面我会把选型、接法、核心算法和踩过的坑都讲清楚。1. 项目整体设计与器件选型1.1 为什么选STM32F103C8T6当主控很多人一听手环会觉得要用蓝牙SoC或者ARM Cortex-M4以上的芯片但实际做原型验证时STM32F103C8T6完全够用。它最高72MHz主频64KB Flash和20KB RAM跑一个计步算法和两三个传感器采集绰绰有余外设上I2C、SPI、USART、ADC、定时器一个不少价格还便宜而且国产替代型号非常多买不到原厂也能用兼容芯片直接顶上。这个项目里我用到的基本就是I2C、GPIO和定时器F103的库函数和参考例程到处都是遇到问题搜索一下就有答案这对新手来说是最重要的。开发环境用Keil MDK加标准外设库就够了不需要上复杂RTOS。HAL库也行但我觉得这个体量的工程用标准库阅读起来更直观。整个工程编译出来Flash占用不到30KB距离64KB上限还有一大截后续想加个串口打印、蓝牙透传或者OLED扩展都还有空间。RAM方面注意别在函数里开太大的局部数组就行后面会提到具体处理。1.2 传感器选型ADXL345、心率、体温模块怎么挑传感器选择上计步我用了ADXL345。它是三轴数字加速度计支持I2C和SPI功耗低到微安级测量范围可以从±2g调到±16g13位分辨率对手腕晃动检测来说足够细腻。最方便的是它带中断输出实际产品里可以触发MCU休眠唤醒不过这个原型我没用中断后面可以扩展。心率模块我用的是MAX30102同样是I2C接口内部集成红光和红外LED能同时测心率和血氧。它比模拟心率传感器好做因为直接输出数字信号省掉了一堆运放和滤波电路。缺点是必须紧贴皮肤手一抖信号就乱所以代码里要做滤波和波峰检测。体温部分我选了DS18B20单总线数字温度传感器测温范围-55℃到125℃分辨率和精度做演示足够。最重要的是它便宜传感器和主控之间只占一根IO。如果要做非接触体温枪换MLX90614就行代码结构差不多。传感器接口主要优点注意点ADXL345I2C/SPI低功耗、三轴、分辨率高I2C地址由SDO引脚决定MAX30102I2C心率血氧一体、体积小需要紧贴皮肤功耗偏高DS18B20单总线数字输出、成本低、可防水转换时间750ms需要4.7k上拉MLX90614I2C非接触测温价格贵代码逻辑类似1.3 整体架构与数据流设计整体架构比较简单传感器都挂在PB6和PB7组成的I2C1总线上ADXL345芯片地址默认是0x53MAX30102默认是0x57总线地址不冲突所以不需要I2C切换。DS18B20单独用PA1走单总线协议。LCD1602用PA2到PA7的并口模式。数据流是主循环里不停读加速度计、心率和温度计步算法在读到加速度后立即处理然后统一刷新LCD。我给刷新时间定在200ms太快没有意义LCD本身也跟不上太慢又会让心率显示滞后200ms体感刚刚好。这个架构最大的优点是每个模块之间相互独立出问题时容易定位。ADXL345坏了不影响DS18B20读数LCD驱动挂了也不会让传感器采集崩掉。主循环虽然简单但对于这种低速率健康监测设备完全够用不需要上状态机或者RTOS。2. 硬件原理图与PCB设计要点2.1 供电与最小系统设计电源是整个板子最容易翻车的地方。我一开始直接用USB的5V给LCD供电再用AMS1117-3.3给主控和传感器供电结果LCD亮度偏高并且容易复位后来在每个IC的电源脚旁边加了100nF瓷片电容和10uF电解电容才稳定下来。原理图里BOOT0要下拉到GNDBOOT1随意复位脚加10k上拉和100nF对地电容。晶振我用的是8MHz无源晶振两个20pF负载电容这是F103最小系统最常见配置。如果打算用锂电池供电建议TP4056充电板出来后先到5V再进3.3V LDO不要直接把电池电压接给F103。还有一点容易被忽略就是传感器和LCD最好不要共用一条细走线供电。手环板上空间小很多人会图省事用一根3.3V飞线串所有模块结果DS18B20转换时电流一抽ADXL345的供电就掉到3.0V以下I2C通信开始随机失败。正确的做法是从LDO输出端分三路每路过一个2.2欧电阻或者磁珠再进传感器既能隔离噪声又能限流。2.2 传感器接口与I2C总线连接传感器接线我都整理在表里。ADXL345的SDO引脚很多人忘记接SDO接地后I2C地址才是0x53接高电平就会变成0x1D代码里读地址对不上就会一直返回0xFF。CS引脚也要拉高否则芯片可能进SPI模式。MAX30102一共就6个脚VCC、GND、SCL、SDA、INT这个项目不用中断INT悬空即可但保险起见可以上拉。DS18B20的DQ必须加4.7k上拉到3.3V这是单总线协议的硬性要求不加的话读出来永远是0xFFFF或者85℃这样的错值。模块引脚STM32F103C8T6引脚说明ADXL345 VCC3.3V供电旁路电容100nFADXL345 GNDGND共同地ADXL345 SDAPB7I2C数据ADXL345 SCLPB6I2C时钟ADXL345 SDOGND决定I2C地址为0x53ADXL345 CS3.3V强制I2C模式MAX30102 SDAPB7与ADXL345共用I2C总线MAX30102 SCLPB6与ADXL345共用I2C总线DS18B20 DQPA1单总线需4.7k上拉I2C总线上拉电阻只加一组阻值4.7k放在靠近主控的位置。如果两个传感器模块板上都已经自带4.7k上拉那总线等效上拉就是2.35k这种情况下通信一般还能工作但边沿会变缓。我实际测试下来杜邦线较长时2.35k并联容易导致信号振荡标准做法是计算总线上所有上拉并联值确保在2k到10k之间。2.3 LCD1602接口与电平匹配LCD1602是5V器件和3.3V的STM32之间有个电平匹配问题。网上很多例程直接把STM32引脚接LCD也能显示因为LCD的高电平输入阈值大约2V3.3V足够触发。但要注意LCD的读/写信号和背光电流如果同时驱动多个模块GPIO灌电流过大容易引起复位。我的做法是RW引脚直接接地只做写操作数据线D4到D7以及RS、EN各串一个100欧电阻既不影响电平又能限流。如果你用的是5V供电的LCD建议把电阻和引脚换成数据手册里标注FT的引脚或者加一个74HC245做电平转换。V0脚接一个10k电位器到GND调对比度用画板一定要留这个位置不然屏幕上全是黑块。LCD背光串联一个100欧电阻限流然后把背光正极接3.3V这样即使模块标注是5V大多数1602背光在3.3V下也能正常工作只是亮度略低。LCD1602引脚STM32引脚功能RSPA2寄存器选择RWGND固定写模式ENPA3使能D4PA4数据位4D5PA5数据位5D6PA6数据位6D7PA7数据位7V010k电位器到GND对比度调节2.4 原理图绘制避坑心得原理图绘制上有几个经验。第一电源去耦电容要靠近芯片电源引脚不要放在原理图角落里PCB上走线长了等于白放。第二ADXL345的VDD和VDD_IO我直接并在一起接3.3V省一个引脚。第三I2C上拉电阻只要一组挂在总线上靠近主控端即可不要每个传感器都加上拉否则等效电阻太小边沿会变差。第四MAX30102的PCB区域要避开机械应力否则LED和光电二极管之间容易漏光测心率时波形会非常难看。第五画板时把I2C的SCL、SDA、GND三个测试点引出来调试接逻辑分析仪时不用翻来翻去。还有一个血泪教训画原理图时给DS18B20封装选错了选成了TO-92和防水探头焊盘不一致导致样板回来只能飞线。做手环这种小尺寸板子元件封装一定要在打样前对着实物量一遍尤其是传感器这类异形器件。另外LCD1602的接口尽量画成排针或FPC座这样后期换OLED或者换屏幕尺寸不用重做主板。3. 软件工程结构与核心代码实现3.1 工程框架与初始化流程软件我用的是标准外设库不用HAL是因为这个项目代码量不大标准库更直接速度也更快。工程里把功能拆成adxl345.c、max30102.c、ds18b20.c、lcd1602.c主程序只负责调度。初始化顺序很关键先延时和LCDLCD能显示就方便观察然后ADXL345、MAX30102、DS18B20依次初始化。每个模块初始化完最好能返回一个状态比如读ID寄存器ADXL345的DEVID应该返回0xE5MAX30102的PART ID应该返回0x15对不上就直接在LCD上报错省去后面排查。int main(void) { delay_init(); LCD_Init(); LCD_ShowString(0, 0, HealthBand); if (ADXL345_Init() ! 0) LCD_ShowString(0, 1, ADXL ERR); if (MAX30102_Init() ! 0) LCD_ShowString(0, 1, MAX ERR); DS18B20_Init(); uint32_t step_count 0; uint16_t heart_rate 0; float temperature 0.0f; while (1) { step_count ADXL345_StepDetect(); heart_rate MAX30102_GetBPM(); if (heart_rate 0) LCD_ShowString(0, 1, HR:--); temperature DS18B20_GetTemp(); LCD_DisplayData(step_count, heart_rate, temperature); delay_ms(200); } }这段代码是主循环骨架实际跑起来很稳定。要注意的是MAX30102_GetBPM如果一直读取可能会让I2C总线被占满所以我在这块加了一个简单的超时保护如果连续读到无效数据直接返回0不阻塞主循环。3.2 ADXL345计步算法从原始数据到步数ADXL345计步算法是最值得讲的部分。原始三轴数据里重力分量占了很大比重如果直接用raw值做阈值手环换个方向阈值就失效。我建议先算矢量幅值再用高通滤波把重力去掉每次采样读x、y、z算出sqrt(x²y²z²)然后用一阶高通算法filtered raw - lowpasslowpass会跟随缓慢变化的趋势这样得到的信号以0为中心走步时会形成明显上冲。然后做一个状态机信号从下往上超过正阈值时认为出现一个可能的步同时检查距离上一步的时间是否大于300ms满足才计步否则忽略。阈值我取下限1.2g、上限动态设置实际佩戴松紧不同可以调整。uint8_t ADXL345_StepDetect(void) { static uint8_t state 0; static uint32_t last_time 0; static float lowpass 0.0f; int16_t x, y, z; float raw, filtered; ADXL345_ReadXYZ(x, y, z); raw sqrtf(x*x y*y z*z) / 16384.0f; // 量程±2g归一化到g lowpass 0.1f * (raw - lowpass); filtered raw - lowpass; if (filtered 0.4f state 0) { state 1; if (HAL_GetTick() - last_time 300) { last_time HAL_GetTick(); return 1; } } else if (filtered -0.2f) { state 0; } return 0; }注意这里return 1只能说明检测到一个有效步峰主函数里做step_count。如果计步还是偏多可以再加一个条件两个相邻波峰间隔必须在250ms到1000ms之间超出这个范围就丢弃。用这个方法我在慢走、快走两种场景下测试计步误差基本能控制在10%以内。跑步时加速度幅度大阈值可以适当提高比如0.6g否则手腕的甩动很容易被误判成步数。3.3 心率与体温数据采集处理MAX30102的心率读取比想象中麻烦因为它输出的原始红外数据有一个很大的直流偏置。代码里我先读FIFO数据寄存器取IR通道低16位然后用滑动平均计算直流分量把瞬时值减掉直流得到AC信号。接着检测AC信号的波峰当AC值从负变正并超过阈值时记录当前系统时间每两个相邻波峰之间的时间差就是一次心跳周期BPM60/周期。为了防止单个抖动干扰我连续收集3个周期求平均才更新显示。初始化时把采样率和ADC范围设置成适合手指佩戴的档位具体寄存器值参考MAX30102数据手册第6章输出是18位数据读的时候要分3个字节拼起来。uint16_t MAX30102_GetBPM(void) { static uint32_t last_peak_time 0; static uint16_t bpm 0; uint32_t ir; int16_t ac; ir MAX30102_ReadFIFO_IR(); ac (int16_t)(ir - dc_estimate); // dc_estimate通过滑动平均更新 if (ac PEAK_TH) { uint32_t now HAL_GetTick(); if (now - last_peak_time 250) bpm 60000 / (now - last_peak_time); last_peak_time now; } return bpm; }这个代码是简化版实际工程里dc_estimate需要用一个环形缓冲区实时更新不能靠一次值否则呼吸、手抖都会让基线漂移。DS18B20读取比较简单先复位总线写0xCC跳过ROM写0x44启动温度转换等待750ms再复位写0xCC写0xBE读暂存器。返回的16位数据低4位是小数部分实际温度等于原始值乘以0.0625。注意转换期间总线会被拉低如果主循环里每200ms读一次温度会读到同一个旧值所以我单独把温度读取周期拉长到2秒一次并且只在读温度时才操作单总线避免和I2C任务穿插导致时序问题。3.4 LCD1602显示驱动与界面组织LCD1602我用4线并口模式只占6个GPIO。驱动核心是两条命令写命令和写数据。初始化时先延时50ms发送0x33、0x32切换成4线模式再发0x28设置两行0x0C关闭光标0x06增量写入最后清屏。显示布局我放在第一行显示步数第二行显示心率和体温比如“S:12345 H:087”第二行“T36.5C”。如果心率模块没接触到手指判断为0时显示“--”避免显示一堆无效数字。用sprintf格式化成字符串再逐字符送LCD简单但要注意F103的RAM很小printf重定向到串口可以但往LCD上打印尽量用snprintf。void LCD_DisplayData(uint32_t step, uint16_t hr, float temp) { char buf[17]; snprintf(buf, sizeof(buf), S:%-5lu H:%03u, (unsigned long)step, hr); LCD_ShowString(0, 0, buf); snprintf(buf, sizeof(buf), T:%5.1f C, temp); LCD_ShowString(0, 1, buf); }显示刷新不要太频繁LCD1602本身写一个字符要几十微秒一屏16个字符再加上命令200ms刷新一次完全够用。如果数据变化不剧烈还可以延长到500ms。界面上我留了“HR:--”这种占位比直接显示0要专业很多实际演示时老师或者朋友看到也不至于觉得系统坏了。4. 调试过程、常见问题与经验总结4.1 I2C通信异常的排查调试中遇到最多的是I2C问题。第一类是读ADXL345一直返回0xFF去查SDO和CS引脚SDO接地、CS拉高后现象消失。第二类是用硬件I2C时程序偶尔卡在等待事件标志位的循环里后来我把I2C速率从400kHz降到100kHz并把超时判断加上问题不再出现。第三类是SCL/SDA没有上拉直接用杜邦线接模块模块板上如果有上拉就没事自己画板时忘加上拉就会通讯失败。最快捷的排查办法是准备一个逻辑分析仪抓波形没有的话就写一个I2C扫描程序遍历0x01到0x7F地址看能不能在0x53和0x57两个地址上收到ACK。这一步能在五分钟内确认硬件链路是否正常。现象可能原因解决方案读ADXL345返回0xFFSDO/CS电平不对SDO接地、CS拉高I2C卡死无响应上拉电阻缺失或I2C速率过高加4.7k上拉降到100kHz读到的数据跳动供电不稳或杜邦线过长加去耦电容缩短杜邦线传感器地址找不到地址左移一位弄错用7位地址0x53读写地址分0xA6/0xA74.2 计步误触与校准经验计步误触发是我花了最长时间调的地方。刚开始阈值设得低手一甩就蹦好几步。后来我加了间隔限制、状态机和峰值过滤误计还是会有尤其快速挥手臂和走路频率相近时无法完全避免。一个有效改进是把手环佩戴方式固定传感器Z轴垂直手腕算法只用Z轴做判断减少X/Y轴晃动干扰。另一个改进是动态阈值维护一个长度为30的窗口实时计算加速度最大值和最小值的平均值作为阈值这样跑步、慢走、挥手都能自适应。注意窗口不要太短否则坐下来静止时环境噪声也会被当成步态。测试的时候建议把ADXL345原始数据通过串口打印出来保存成文本后用Excel画个曲线看一眼波形再定阈值比自己猜靠谱得多。我最初调阈值时靠肉眼盯着LCD数字效率很低后来改成串口输出原始值很快发现走路时主峰的幅值其实非常稳定。4.3 LCD1602显示乱码/白屏处理LCD1602白屏或者显示黑块十有八九是对比度没调好。V0脚对地接10k电位器上电后慢慢拧直到第一行出现一排黑影、再继续拧到黑影消失这时候对比度就是最佳位置。如果拧遍了都白屏检查RW是否接地以及EN、RS和数据线有没有接反。还有个坑是初始化太快LCD1602上电后要等50ms以上等内部复位完成再发初始化命令否则第一条命令被吃掉后面全乱。另外3.3V供电时某些LCD模块可能显示发虚可以把背光电阻减小一点或者干脆改5V供电。我遇到过一种很奇葩的情况程序在开发板上显示正常换到自制手环板就花屏最后发现是自制板LCD电源走线太细背光一开电压就被拉低。给LCD背光单独走一根稍粗的线问题就消失了。所以LCD1602这种并行接口器件供电问题往往比逻辑问题更隐蔽。4.4 数据融合与功耗优化补充数据和功耗上的优化可以往后做。第一ADXL345不需要每毫秒都读我100ms读一次功耗和实时性平衡最好MAX30102因为要测脉动波形必须持续读取但可以在不显示心率时切换成待机模式把LED关掉DS18B20读取周期2秒一次既省时间又减少总线占用。第二LCD背光是耗电大户手环如果只用纽扣电池不要把背光常开可以做成按键按下时点亮几秒。第三所有传感器采集到的原始值先丢进一个小的环形缓冲区做均值滤波显示出来的数据会稳定很多。我的实际经验是显示心率从72跳到200再跳回72多数不是算法问题而是滤波没做够。最后再说一句关于做这个项目的整体感受。不要一上来就画PCB先用面包板和杜邦线把每个模块单独调通再把它们合到一起等显示、计步、心率都稳定了再按我前面说的原理图去画板成功率会高很多。整个手环主控工程做下来我对I2C时序、单总线协议和传感器数据处理的把握比看书半个月都管用。如果你照着做遇到问题建议先从电源和I2C波形开始查很多坑都是相通的。本文还有配套的精品资源点击获取