1. 项目缘起与整体设计思路
1.1 为什么选择做一款智能午休枕
办公室午休这件事,趴桌子睡过的人都懂——手臂发麻、脖子酸胀、脸上压出红印子,醒来比没睡还累。市面上确实有不少午休枕产品,但要么功能单一(就是个普通抱枕),要么价格虚高(动辄两三百),要么智能化程度很低(顶多带个震动闹钟)。我当时的想法很简单:能不能用自己手头熟悉的STM32单片机,做一个既能监测温度、又能定时提醒、还能显示时间的小设备,把它塞进一个午休枕里,让午休体验真正提升一个档次。
这个项目的核心定位是“低成本、高实用性、可复现”。主控选STM32F103C8T6,这是最经典的入门级ARM Cortex-M3芯片,资料多、价格便宜、性能足够。温度采集用DS18B20,单总线数字温度传感器,精度够用且抗干扰能力强。时钟用DS1302,虽然是比较老的方案,但胜在简单可靠、成本极低。显示用0.96寸OLED,I2C接口,体积小、功耗低、显示效果好。整个方案下来,硬件成本控制在三十块钱以内,非常适合学生做课程设计或者毕业设计,也适合电子爱好者练手。
适合阅读这篇博文的人群包括:正在找STM32练手项目的电子类专业学生、想做毕业设计但不知道选什么题目的同学、对嵌入式开发感兴趣想入门的爱好者,以及想给自己或家人做一个实用小物件的DIY玩家。不管你之前有没有接触过STM32,只要跟着思路走,都能把这个项目跑起来。
1.2 系统整体架构与模块划分
整个智能午休枕的系统架构可以分成四个层次来理解。最底层是电源层,负责给各个模块提供稳定的3.3V和5V供电。往上是感知层,包括DS18B20温度传感器和DS1302时钟模块,分别负责采集环境温度和提供实时时间。再往上是控制层,也就是STM32F103C8T6最小系统板,负责协调各个模块的工作、处理数据、执行逻辑判断。最上面是交互层,包括OLED显示屏和按键模块,负责向用户展示信息和接收用户操作。
模块之间的通信方式也需要提前规划好。DS18B20用的是单总线协议,只需要一根数据线就能完成通信,但时序要求比较严格。DS1302用的是三线串行接口,包括CE(片选)、IO(数据)、SCLK(时钟)三根线。OLED用的是I2C协议,只需要SDA和SCL两根线。按键用普通的GPIO输入,配合内部上拉电阻即可。这样算下来,STM32的IO口完全够用,甚至还有不少余量可以扩展其他功能。
注意:在规划IO口分配的时候,尽量避开JTAG调试占用的那几个引脚(PA13、PA14、PA15、PB3、PB4),否则调试的时候会遇到莫名其妙的冲突问题。我一开始就是把DS1302的CE脚接到了PA15上,结果调试器一连接就通信失败,排查了半天才发现是引脚复用的问题。
1.3 方案选型背后的考量
为什么不用DHT11而选DS18B20?DHT11虽然便宜,但精度只有±2℃,而且响应速度慢,每次读取间隔不能少于1秒。DS18B20的精度可以做到±0.5℃,分辨率可配置到0.0625℃,而且支持多点组网,一根总线上可以挂多个传感器。对于午休枕这种需要精确监测温度的场景来说,DS18B20明显更合适。另外DS18B20有防水封装版本,如果以后想扩展到监测枕面温度,可以直接换探头。
为什么不用STM32内部的RTC而选DS1302?STM32F103的RTC功能确实可以用,但需要外接32.768kHz晶振,而且精度受晶振质量影响很大。DS1302虽然是一款老芯片,但它自带晶振补偿电路,走时精度更稳定,而且有备用电池引脚,断电后时间不会丢失。最关键的是,DS1302的驱动代码非常成熟,网上随便一搜就有大量现成的参考代码,对于新手来说学习成本更低。
为什么选OLED而不是LCD1602?LCD1602只能显示两行字符,信息量太少,而且体积大、功耗高。OLED可以显示图形和汉字,0.96寸的屏幕虽然不大,但足够显示时间、温度、模式状态等信息。而且OLED是自发光,不需要背光,在午休这种光线较暗的环境下看起来更舒服,不会刺眼。
2. 核心硬件模块详解与实操要点
2.1 STM32F103C8T6最小系统板的使用要点
STM32F103C8T6最小系统板是整個项目的核心。这块板子通常包含主控芯片、8MHz晶振、32.768kHz晶振(用于RTC)、复位电路、BOOT选择跳线、以及一个Micro USB接口(用于供电和下载程序)。拿到板子后第一件事是检查跳线帽的位置:BOOT0要接低电平(GND),BOOT1随意,这样芯片才能从主闪存启动。如果BOOT0接高电平,芯片会进入系统存储器启动模式,程序就跑不起来。
供电方面,最小系统板通常支持两种供电方式:Micro USB直接供电,或者通过3.3V引脚外部供电。调试阶段建议直接用USB供电,方便同时下载程序和查看串口输出。实际装进枕头里的时候,可以用3.7V锂电池配合升压模块,或者用四节AA电池串联(6V)经过降压模块降到3.3V。注意STM32的供电范围是2.0V到3.6V,超过3.6V会烧芯片,所以降压模块的输出一定要用万用表确认后再接。
下载程序的方式有两种:SWD模式和串口模式。SWD模式需要ST-Link调试器,连接SWDIO、SWCLK、GND、3.3V四根线,优点是支持在线调试和断点,推荐使用。串口模式需要USB转TTL模块,连接PA9(TX)和PA10(RX),下载时需要把BOOT0接高电平,下载完成后改回低电平再复位。串口下载的优点是便宜,一个USB转TTL模块只要几块钱,但调试起来没有SWD方便。
实操心得:我建议新手直接买一个ST-Link V2调试器,二十块钱左右,支持SWD下载和调试,省去频繁插拔跳线的麻烦。另外,Keil MDK的工程配置里,Debug选项卡要选ST-Link Debugger,然后点Settings确认能识别到芯片。如果识别不到,检查一下驱动有没有装好,或者换一根USB线试试。
2.2 DS18B20温度传感器的时序与接线
DS18B20是一款单总线数字温度传感器,测量范围-55℃到+125℃,在-10℃到+85℃范围内的精度是±0.5℃。它有三个引脚:VDD(电源)、GND(地)、DQ(数据)。供电方式有两种:外部供电(VDD接3.3V)和寄生供电(VDD接GND,靠DQ线供电)。推荐用外部供电,稳定性更好。DQ引脚需要接一个4.7kΩ的上拉电阻到3.3V,否则通信会失败。
DS18B20的通信时序是整个项目里比较难啃的部分。单总线协议对时间要求非常严格,微秒级的误差都可能导致通信失败。主要操作包括:复位脉冲(拉低总线至少480μs,然后释放,等待15-60μs后检测存在脉冲)、写时隙(拉低总线1-15μs,然后根据写0还是写1决定拉低持续时间)、读时隙(拉低总线至少1μs,然后在15μs内采样总线电平)。在STM32上实现这些时序,需要用微秒级延时函数,通常用SysTick或者定时器来实现。
// DS18B20复位函数示例 uint8_t DS18B20_Reset(void) { uint8_t presence; DS18B20_DQ_OUT(); // 配置DQ为输出 DS18B20_DQ_LOW(); // 拉低总线 delay_us(480); // 保持至少480us DS18B20_DQ_HIGH(); // 释放总线 DS18B20_DQ_IN(); // 配置DQ为输入 delay_us(60); // 等待15-60us presence = DS18B20_DQ_READ(); // 读取存在脉冲 delay_us(420); // 等待时序完成 return presence; // 0表示存在,1表示不存在 }温度转换和读取的流程是:发送跳过ROM命令(0xCC),发送温度转换命令(0x44),等待转换完成(12位精度需要750ms),再次复位,发送跳过ROM命令,发送读取暂存器命令(0xBE),连续读取两个字节的温度数据,低字节在前,高字节在后。温度值的计算方法:如果高字节的高5位是0,说明是正温度,直接把两个字节组合成一个16位数,乘以0.0625即可;如果是负温度,需要取反加一后再乘。
注意:DS18B20的转换时间跟分辨率有关,9位精度需要93.75ms,10位需要187.5ms,11位需要375ms,12位需要750ms。如果对实时性要求不高,可以用12位精度;如果要求快速响应,可以降到9位或10位。我实测下来,12位精度在午休枕这个场景下完全够用,毕竟温度变化本身就很缓慢。
2.3 DS1302时钟模块的驱动与备用电池
DS1302是一款涓流充电时钟芯片,可以计算秒、分、时、日、月、周、年,并且有闰年补偿功能。它通过三根线与单片机通信:CE(使能)、IO(数据)、SCLK(串行时钟)。通信协议是SPI的变种,但时序略有不同。每次读写操作都需要先拉高CE,然后在SCLK的上升沿写入数据,下降沿读出数据。数据是低位先传。
DS1302的寄存器分为两类:时钟寄存器和RAM寄存器。时钟寄存器地址从0x80到0x8C,分别对应秒、分、时、日、月、周、年。写入时需要先解除写保护(向0x8E写入0x00),写完后再开启写保护(向0x8E写入0x80)。读取时直接读对应地址即可。注意DS1302的秒寄存器最高位是时钟停止位(CH),上电后需要先清零这一位,时钟才会开始走。
// DS1302写一个字节 void DS1302_WriteByte(uint8_t dat) { uint8_t i; for(i=0; i<8; i++) { DS1302_IO = dat & 0x01; // 先写低位 DS1302_SCLK = 1; // 上升沿写入 DS1302_SCLK = 0; dat >>= 1; } } // DS1302写寄存器 void DS1302_WriteReg(uint8_t addr, uint8_t dat) { DS1302_CE = 0; DS1302_SCLK = 0; DS1302_CE = 1; DS1302_WriteByte(addr); // 先写地址 DS1302_WriteByte(dat); // 再写数据 DS1302_CE = 0; }备用电池方面,DS1302的VCC1引脚接3V纽扣电池(CR2032),VCC2接主电源3.3V。当主电源断电时,DS1302会自动切换到电池供电,保持时间继续走。实测下来,一颗CR2032电池可以维持DS1302走时至少两年。注意电池的正负极不要接反,否则芯片会损坏。
实操心得:DS1302的晶振频率是32.768kHz,这个晶振对走时精度影响很大。如果发现时间走得不准,可以尝试更换质量好一点的晶振,或者在晶振两端各接一个6pF左右的电容到地。另外,DS1302的IO引脚是开漏输出,需要接一个4.7kΩ到10kΩ的上拉电阻到3.3V,否则通信会不稳定。
2.4 OLED显示屏的I2C驱动与显示优化
0.96寸OLED显示屏通常使用SSD1306驱动芯片,支持I2C和SPI两种接口。I2C接口只需要SDA和SCL两根线,接线简单,但刷新速度比SPI慢。对于午休枕这种不需要高频刷新的应用来说,I2C完全够用。OLED的I2C地址通常是0x78(写地址)或0x79(读地址),具体取决于模块上的电阻配置。
OLED的驱动流程是:上电初始化(发送一系列配置命令)、设置显示模式、清屏、写入显示数据。初始化命令包括:关闭显示、设置时钟分频、设置多路复用率、设置显示偏移、设置起始行、设置充电泵、设置内存寻址模式、设置段重映射、设置COM扫描方向、设置对比度、设置预充电周期、设置COM引脚配置、设置对比度、开启电荷泵、开启显示。这些命令看起来很多,但大部分用默认值就行,只需要根据屏幕的具体型号微调几个参数。
// OLED初始化命令序列(部分) void OLED_Init(void) { OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0xD5); // 设置时钟分频 OLED_WriteCmd(0x80); OLED_WriteCmd(0xA8); // 设置多路复用率 OLED_WriteCmd(0x3F); OLED_WriteCmd(0xD3); // 设置显示偏移 OLED_WriteCmd(0x00); OLED_WriteCmd(0x40); // 设置起始行 OLED_WriteCmd(0x8D); // 设置电荷泵 OLED_WriteCmd(0x14); // 开启电荷泵 OLED_WriteCmd(0x20); // 设置内存寻址模式 OLED_WriteCmd(0x00); // 水平寻址 OLED_WriteCmd(0xA1); // 设置段重映射 OLED_WriteCmd(0xC8); // 设置COM扫描方向 OLED_WriteCmd(0xDA); // 设置COM引脚配置 OLED_WriteCmd(0x12); OLED_WriteCmd(0x81); // 设置对比度 OLED_WriteCmd(0xCF); OLED_WriteCmd(0xD9); // 设置预充电周期 OLED_WriteCmd(0xF1); OLED_WriteCmd(0xDB); // 设置VCOMH OLED_WriteCmd(0x40); OLED_WriteCmd(0xA4); // 全局显示开启 OLED_WriteCmd(0xA6); // 正常显示 OLED_WriteCmd(0xAF); // 开启显示 OLED_Clear(); }显示汉字需要用到字模提取软件,比如PCtoLCD2002。设置模式为“阴码+逐列式+顺向”,输出格式为C51格式,然后输入汉字生成字模数组。每个16x16的汉字需要32个字节的存储空间。显示的时候,先设置页地址和列地址,然后连续写入32个字节即可。为了节省内存,可以把常用的汉字做成一个数组,用索引来调用。
注意:OLED的I2C通信速率不能太高,SSD1306的最大I2C时钟频率是400kHz。如果STM32的I2C配置成快速模式(400kHz)还是通信失败,可以降到标准模式(100kHz)试试。另外,OLED的供电电压是3.3V,不要接5V,否则会烧毁驱动芯片。我见过有人把OLED接到5V上,结果屏幕直接冒烟了。
3. 软件架构与核心逻辑实现
3.1 主程序框架与任务调度
整个软件架构采用前后台模式,主循环负责轮询各个任务,中断负责处理按键和定时器。主循环的主要任务包括:读取DS18B20温度、读取DS1302时间、更新OLED显示、处理按键输入、执行闹钟逻辑。为了不让某个任务阻塞太久,每个任务都采用状态机的方式实现,每次只执行一小步,然后立即返回。
int main(void) { // 系统初始化 SystemInit(); delay_init(); OLED_Init(); DS18B20_Init(); DS1302_Init(); KEY_Init(); TIM3_Init(999, 7199); // 定时器3,1ms中断 // 显示开机画面 OLED_ShowString(0, 0, "Smart Pillow", 16); OLED_ShowString(0, 2, "Initializing...", 16); delay_ms(1000); OLED_Clear(); while(1) { // 任务1:读取温度(每2秒一次) if(temp_tick >= 2000) { temp_tick = 0; DS18B20_Start(); DS18B20_ReadTemp(&temperature); } // 任务2:读取时间(每500ms一次) if(time_tick >= 500) { time_tick = 0; DS1302_ReadTime(&time); } // 任务3:更新显示(每200ms一次) if(display_tick >= 200) { display_tick = 0; OLED_ShowTime(&time); OLED_ShowTemp(temperature); OLED_ShowMode(mode); } // 任务4:处理按键 KEY_Scan(); // 任务5:闹钟判断 if(alarm_enable && time.hour == alarm_hour && time.min == alarm_min) { if(time.sec == 0) // 只在整秒触发一次 { Alarm_Trigger(); } } } }定时器3配置为1ms中断,在中断服务函数里对各个任务的计数器进行累加。这种做法的好处是任务的时间基准统一,不会因为某个任务执行时间过长而影响其他任务。注意中断服务函数里不要做耗时操作,只做计数器累加和标志位设置,具体的处理逻辑放到主循环里执行。
实操心得:我一开始把OLED刷新放在主循环里无条件执行,结果发现屏幕闪烁很严重。后来改成200ms刷新一次,并且只刷新变化的部分,闪烁问题就解决了。另外,DS18B20的温度转换需要750ms,如果放在主循环里等待,会阻塞其他任务。我的做法是发送转换命令后立即返回,等750ms后再来读取结果,这样就不会阻塞了。
3.2 按键交互与菜单设计
午休枕需要用户交互的功能不多,主要是设置闹钟时间、切换显示模式、开关闹钟。我用三个按键来实现:KEY1用于切换设置项,KEY2用于增加数值,KEY3用于减少数值。按键扫描采用状态机的方式,每个按键有四个状态:空闲、按下消抖、等待释放、释放消抖。消抖时间设置为20ms,既能有效消除抖动,又不会让用户感觉反应迟钝。
// 按键扫描状态机 void KEY_Scan(void) { static uint8_t key1_state = 0, key2_state = 0, key3_state = 0; static uint16_t key1_tick = 0, key2_tick = 0, key3_tick = 0; // KEY1处理 switch(key1_state) { case 0: // 空闲 if(KEY1_READ() == 0) { key1_state = 1; key1_tick = 0; } break; case 1: // 按下消抖 if(++key1_tick >= 20) { if(KEY1_READ() == 0) { key1_state = 2; KEY1_Event(); } else key1_state = 0; } break; case 2: // 等待释放 if(KEY1_READ() == 1) { key1_state = 3; key1_tick = 0; } break; case 3: // 释放消抖 if(++key1_tick >= 20) { if(KEY1_READ() == 1) key1_state = 0; else key1_state = 2; } break; } // KEY2、KEY3类似处理 }菜单设计采用扁平结构,只有两级:主界面和设置界面。主界面显示时间、温度、闹钟状态。按下KEY1进入设置界面,依次设置闹钟小时、闹钟分钟、闹钟开关。设置完成后再次按下KEY1保存并返回主界面。设置界面下,当前设置项会闪烁显示,提示用户正在编辑哪一项。
注意:按键的硬件消抖也很重要。我在每个按键两端并联了一个0.1μF的电容,配合软件消抖,效果非常好。另外,按键最好用轻触开关,手感清晰,不要用那种软绵绵的薄膜按键,按下去没有反馈,用户体验很差。
3.3 闹钟触发与震动提醒
闹钟触发后,需要给用户一个提醒。我用的是震动马达,通过一个NPN三极管(S8050)来驱动,基极接STM32的GPIO,集电极接马达负极,发射极接地,马达正极接3.3V。GPIO输出高电平时,三极管导通,马达震动。注意马达两端要反向并联一个续流二极管(1N4148),防止马达断电时产生的反向电动势击穿三极管。
// 闹钟触发函数 void Alarm_Trigger(void) { uint8_t i; for(i=0; i<10; i++) // 震动10次 { MOTOR_ON(); delay_ms(500); MOTOR_OFF(); delay_ms(500); } }震动模式可以设计成间歇震动,比如震动500ms、停止500ms,循环10次。这样既能有效提醒用户,又不会因为持续震动而让人感到烦躁。如果用户想关闭闹钟,按下任意按键即可。闹钟关闭后,alarm_enable标志位清零,当天不再触发。
实操心得:震动马达的启动电流比较大,可能达到100mA以上,所以三极管的基极电阻不能太大,我用的是1kΩ。如果发现马达震动无力,可以减小基极电阻到470Ω试试。另外,马达的震动强度跟安装方式有关,我用热熔胶把马达固定在枕头内侧,震动传递效果很好。
3.4 低功耗设计与电源管理
午休枕是电池供电的设备,低功耗设计很重要。STM32F103C8T6支持三种低功耗模式:睡眠模式、停止模式、待机模式。睡眠模式下,内核停止,外设继续运行,功耗约几毫安。停止模式下,所有时钟停止,功耗约几十微安。待机模式下,功耗最低,约几微安,但唤醒后相当于复位,需要重新初始化。
对于午休枕这个场景,我选择在用户不操作的时候进入停止模式,通过按键中断或者RTC闹钟中断来唤醒。停止模式的进入方法是:配置好唤醒源,然后执行WFI指令。唤醒后,系统时钟会切换回HSI(8MHz),需要重新配置时钟树。
// 进入停止模式 void Enter_Stop_Mode(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); PWR_WakeUpPinCmd(ENABLE); // 使能唤醒引脚 PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 唤醒后重新配置时钟 SystemInit(); }不过说实话,在实际使用中,我发现低功耗模式带来的复杂度提升和实际收益不太成正比。因为午休枕的使用时间通常只有一两个小时,一块1000mAh的锂电池足够用好几周。所以最终我并没有启用停止模式,而是让系统一直运行,只是把OLED的亮度调低,关闭不必要的LED指示灯。这样代码简单,稳定性也更好。
4. 常见问题排查与实战经验
4.1 硬件调试中的典型问题
问题一:OLED不亮或者花屏。这是最常见的问题。首先检查接线是否正确,SDA和SCL有没有接反。然后检查供电电压是不是3.3V,接5V会烧芯片。如果接线和供电都没问题,用万用表量一下SDA和SCL的对地电压,正常应该在1.6V到3.3V之间。如果电压不对,可能是上拉电阻没接或者阻值太大。I2C总线的上拉电阻一般用4.7kΩ,太小会增加功耗,太大则上升沿变缓,通信失败。
问题二:DS18B20读不到温度。首先检查DQ引脚的上拉电阻,必须是4.7kΩ左右。然后检查时序延时是否准确,可以用示波器抓一下波形,看看复位脉冲的宽度是不是480μs。如果没有示波器,可以用逻辑分析仪,几十块钱一个,非常方便。另外,DS18B20对电源质量比较敏感,如果电源纹波太大,也会导致通信失败,可以在VDD和GND之间并联一个0.1μF的电容。
问题三:DS1302走时不准。最常见的原因是晶振质量不好。DS1302对晶振的要求比较高,建议用原厂推荐的32.768kHz晶振,负载电容6pF。如果还是不准,可以在晶振两端各接一个6pF的电容到地,微调频率。另外,DS1302的备用电池电压不能低于2.0V,否则时间会丢失。如果发现每次断电后时间都归零,检查一下电池有没有电,或者电池座有没有接触不良。
问题四:程序下载失败。首先检查BOOT0跳线是不是接在GND上。然后检查ST-Link的驱动有没有装好,设备管理器里能不能识别到。如果用的是串口下载,检查TX和RX有没有接反(TX接RX,RX接TX),BOOT0有没有接高电平。另外,有些最小系统板的USB接口只供电不通信,需要确认一下板子上的USB座是不是连到了PA11和PA12(USB数据线),如果是,那就不能用串口下载,只能用SWD。
4.2 软件调试中的坑与解决方案
坑一:I2C通信死锁。STM32的硬件I2C在某些情况下会出现死锁,表现为SCL被拉低不放,总线一直忙。这个问题在F103系列上比较常见。解决方案有两种:一是用软件模拟I2C,完全用GPIO控制时序,虽然速度慢一点,但稳定可靠;二是加入超时检测,如果I2C操作超过一定时间没有完成,就强制复位I2C外设。我最终选择了软件模拟I2C,代码虽然长一点,但从来没出过问题。
坑二:DS18B20温度值跳变。有时候读出来的温度值会突然跳到85℃或者-55℃,这是DS18B20的默认上电值,说明通信出了问题。解决方法是在读取温度后加一个范围判断,如果温度值超出合理范围(比如-10℃到50℃),就丢弃这次数据,用上一次的有效值代替。另外,DS18B20的转换时间要留够,12位精度需要750ms,如果提前读取,也会得到错误值。
坑三:OLED显示汉字乱码。字模提取软件的设置很关键。PCtoLCD2002的设置必须是:阴码、逐列式、顺向、C51格式。如果设置成逐行式,显示出来的汉字就是乱的。另外,OLED的页地址和列地址设置也要注意,0.96寸OLED的分辨率是128x64,分成8页,每页8行。显示16x16的汉字时,需要占用两页,列地址从0到127。
坑四:定时器中断优先级冲突。如果同时用了多个中断(比如定时器中断、串口中断、外部中断),要注意优先级配置。优先级高的中断可以打断优先级低的中断。如果定时器中断优先级太高,可能会影响其他中断的响应。我的配置是:定时器中断优先级最低,串口中断中等,外部中断最高。这样按键响应最及时,定时器稍微延迟一点也没关系。
4.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| OLED不亮 | 供电不对、接线错误 | 万用表测电压、检查SDA/SCL | 确保3.3V供电,SDA/SCL接对 |
| OLED花屏 | I2C速率过高、上拉电阻不合适 | 降低I2C速率试试 | 降到100kHz,上拉电阻用4.7kΩ |
| DS18B20读不到 | 上拉电阻缺失、时序不准 | 检查DQ上拉、示波器看波形 | 加4.7kΩ上拉,调整延时 |
| DS18B20跳变 | 转换时间不够、电源纹波大 | 增加等待时间、加滤波电容 | 等待750ms,VDD并0.1μF电容 |
| DS1302走时不准 | 晶振质量差、负载电容不对 | 更换晶振、调整电容 | 用6pF晶振,两端各接6pF电容 |
| DS1302断电归零 | 备用电池没电、接触不良 | 测电池电压、检查电池座 | 更换CR2032电池,调整弹片 |
| 程序下载失败 | BOOT0接错、驱动未装 | 检查跳线、设备管理器 | BOOT0接GND,重装ST-Link驱动 |
| 按键无反应 | 上拉电阻缺失、消抖时间不当 | 测按键引脚电平、调整消抖 | 加10kΩ上拉,消抖20ms |
| 闹钟不触发 | 时间比较逻辑错误、标志位未置位 | 串口打印调试信息 | 检查比较条件,确保整秒触发 |
| 震动马达不工作 | 三极管接错、基极电阻太大 | 测三极管各极电压 | 检查引脚,基极电阻用1kΩ |
实操心得:调试嵌入式项目,串口打印是最有效的调试手段。我习惯在关键位置加printf输出,比如温度读取成功打印“Temp: 25.6C”,时间读取成功打印“Time: 12:30:45”。这样一眼就能看出程序跑到哪一步出了问题。注意printf需要重定向fputc函数到串口,具体做法是在代码里实现fputc,把字符通过USART_SendData发出去。
4.4 从原型到成品的经验总结
把电路板装进枕头里,跟放在桌面上调试完全是两码事。首先,枕头内部空间有限,所有模块必须紧凑排列。我用的是5x7cm的洞洞板,把STM32最小系统板、DS18B20、DS1302、OLED、按键、马达驱动电路全部焊上去。注意模块之间要留出足够的间距,避免短路。焊完之后用热熔胶固定,防止使用过程中松动。
其次,供电方案要可靠。我用的是1000mAh的锂电池,配合TP4056充电模块和AMS1117-3.3V降压模块。TP4056负责给电池充电,AMS1117负责把电池电压降到3.3V给系统供电。注意AMS1117的压差是1.1V左右,电池电压降到3.4V以下时,输出可能不稳定,所以锂电池不要用到太低再充电。
最后,枕头的填充物要选好。我用的是记忆棉,柔软但有支撑力,不会把电路板压坏。在电路板的位置挖一个凹槽,把板子嵌进去,上面再盖一层薄海绵,这样既不会硌头,又能保护电路。OLED屏幕的位置要开一个窗口,用透明亚克力板盖住,防止刮花。按键的位置要标记清楚,方便盲操作。
这个项目我从开始构思到最终成品,前前后后花了大概两周时间,其中大部分时间都花在调试DS18B20的时序和OLED的显示上。如果你也在做类似的项目,我的建议是:先把每个模块单独调通,再整合到一起。不要一上来就把所有模块都接上,那样出了问题很难定位。另外,多利用串口打印和逻辑分析仪,它们能帮你快速找到问题的根源。