用51单片机做智能闹钟可以说是电子爱好者的经典练手项目了。我十年前第一次接触单片机就是从闹钟开始的,现在回头看虽然方案简单,但包含了定时器、中断、显示驱动、按键处理等核心知识点,特别适合新手入门。
这个项目的核心功能其实就三块:时间显示、闹钟设置和模式切换。时间显示部分需要单片机持续计时并驱动LCD屏幕;闹钟设置要能存储预定时间并触发蜂鸣器;模式切换则通过按键在不同功能间跳转。别看功能简单,实际做起来会遇到不少坑,比如按键消抖没处理好会导致设置乱跳,定时器中断配置错误会让时间走得忽快忽慢。
硬件上你需要准备这些材料:
软件部分主要用Keil C51开发,代码结构我会在后面详细拆解。先说说这个项目的几个亮点设计:
先看单片机最小系统,这是整个项目的基础。STC89C52的18、19脚接11.0592MHz晶振(这个频率特别适合串口通信),配合两个30pF电容组成振荡电路。9脚接10k电阻和10μF电容组成复位电路,记得电容正极接VCC。
LCD1602的连接要注意对比度调节:
四个按键我推荐这样布局:
蜂鸣器连接最简单,我用的是有源蜂鸣器,正极接P3.7,负极接GND。如果要用无源蜂鸣器,需要加三极管驱动电路。
新手最容易犯的错是LCD不显示,按这个顺序检查:
按键失灵多半是消抖没做好,硬件上可以在按键两端并联104电容,软件上要加20ms延时检测。有次我偷懒没做消抖,结果按一次键寄存器值跳了好几次,调了半天才发现问题。
定时不准可能是晶振负载电容不匹配,11.0592MHz晶振配30pF电容是经过验证的组合。如果走时还是不准,可以微调定时器初值,具体方法后面编程部分会讲。
时间基准是整个系统的核心,我们用定时器0产生1ms中断:
c复制void Timer0Init(void)
{
TMOD &= 0xF0; //不清高四位
TMOD |= 0x01; //设置T0为模式1
TL0 = 0x18; //初值计算:65536-9216/12*1
TH0 = 0xFC;
TF0 = 0; //清除标志
TR0 = 1; //启动定时器
ET0 = 1; //使能中断
EA = 1; //开总中断
}
中断服务程序里要实现秒、分、时的进位逻辑,特别注意闰年判断:
c复制void Timer0_Routine() interrupt 1
{
TL0 = 0x18; //重装初值
TH0 = 0xFC;
if(++tt==1000) //1秒到
{
tt=0;
Sec++;
if(Sec==60){
Sec=0;
Min++;
if(Min==60){
Min=0;
Hour++;
if(Hour==24){
Hour=0;
Day++;
//月份天数处理
if(Mon==2){ //二月特殊处理
if((Year%4==0 && Year%100!=0) || Year%400==0)
maxDay=29;
else
maxDay=28;
}
//...其他月份处理
}
}
}
}
}
四个按键通过外部中断0检测,消抖采用延时法:
c复制void anjian() interrupt 0
{
TR0 = 0; //暂停计时
delay_ms(20); //消抖
if(K1 == 0){ //模式键
if(++moshi==4) moshi=0; //循环切换0-3
while(!K1); //等待释放
}
//其他按键处理...
TR0 = 1; //恢复计时
}
四种工作模式对应不同显示内容:
LCD1602的驱动要注意初始化顺序:
c复制void LCD_Init()
{
LCD_WriteCommand(0x38); //8位数据,双行显示
LCD_WriteCommand(0x0C); //开显示,关光标
LCD_WriteCommand(0x06); //写入后地址自增
LCD_WriteCommand(0x01); //清屏
}
时间显示采用自定义格式,比如"2023-08-15 Tue"在第一行,"12:30:45"在第二行。设置模式下要让当前修改位闪烁,通过定时器1控制500ms的闪烁周期。
用串口打印当前时间值,对比标准时间计算误差:
c复制void UART_Init()
{
SCON = 0x50; //模式1
TMOD |= 0x20; //T1模式2
TH1 = 0xFD; //9600bps
TR1 = 1;
}
void PrintTime()
{
printf("%02d-%02d-%02d %02d:%02d:%02d\r\n",
Year, Mon, Day, Hour, Min, Sec);
}
如果发现每天快3秒,可以调整定时器初值:
c复制//原初值
TH0 = 0xFC; TL0 = 0x18; //对应9216计数
//调整为
TH0 = 0xFC; TL0 = 0x28; //增加16个计数周期
闹钟比较采用全等判断,触发后启动蜂鸣器:
c复制void CheckAlarm()
{
if(Hour==AlarmHour && Min==AlarmMin && Sec==AlarmSec){
Buzzer = 0; //启动蜂鸣
Delay_ms(500);
Buzzer = 1; //关闭蜂鸣
}
}
建议增加贪睡功能,按任意键暂停闹铃,5分钟后再次触发。可以在中断中维护一个计数器:
c复制if(SnoozeCnt>0 && --SnoozeCnt==0){
Buzzer = 0; //再次响铃
}
如果要用电池供电,可以采取这些措施:
实测电流可以从20mA降到5mA以下,两节AA电池能用一个月。有个坑要注意:掉电模式会停止定时器,唤醒后要重新初始化时钟。