简介:一套基于STM32与Proteus的数字时钟设计完整工程包,面向嵌入式入门学习者、电子设计爱好者及竞赛学生,涵盖时钟计时、LCD液晶显示与闹钟触发等核心功能,适合作为单片机课程设计或毕业设计参考。整个压缩包共182个文件,大小仅6.16MB,包含stm32f10x系列C语言源码(如定时器、RCC、USART等驱动)与头文件、Keil MDK工程文件(uvprojx/uvoptx/hex/axf/map等编译产物)、Proteus仿真工程(pdsprj/pdsbak)以及汇编启动文件(.s)等,文件类型覆盖源代码、工程配置、仿真电路与烧录镜像,既可直接编译烧录到实物,也可在Proteus中虚拟运行验证逻辑。该资源已有10436人学习下载,实战性较强。通过这份工程,读者可掌握STM32的RTC实时时钟配置、LCD显示驱动编写、中断与定时器应用、低功耗电源管理等关键技术,同时理解从源码编写、编译链接到仿真调试的完整开发流程,对系统学习嵌入式设计非常有帮助。
1. 基于STM32的数字时钟设计:显示容易,走准才是本事
很多人以为数字时钟项目就是“LCD1602显示个时间”这么简单,真正做完才发现,难点从来不在于显示,而在于“时间怎么保持准确”。这个基于STM32F103C8T6的数字时钟设计,把单片机入门阶段最值得玩的三件事都包含了:DS1302实时时钟芯片的时序操作、LCD1602液晶显示驱动的移植、按键中断/查询方式的时间和闹钟设置。配合Protues仿真环境,你可以先不做实物就用软件把整套逻辑跑通,等硬件到齐了再无缝迁移过去。
这套资源适合两类人:一是正在做课程设计、需要一份能讲清楚原理的嵌入式小项目的人;二是想搞明白“STM32如何模拟IO时序来驱动外部芯片”的初学者。项目本身没有用复杂的RTOS或GUI,全部逻辑集中在main.c和几个外设驱动文件里,按照裸机状态机的思路组织,改起来非常直观。
2. 硬件架构拆解:一颗主控、三颗芯片、四路输入
2.1 系统组成:STM32F103C8T6核心板与外部器件选型
先说清楚这套系统里都有什么。主控用的是STM32F103C8T6,这是蓝丸核心板常用的型号,Flash 64KB,RAM 20KB,主频最高72MHz,跑这种任务性能绰绰有余,甚至还有大量余量可以加功能。时钟芯片选用DS1302,这是一颗经典的串行实时时钟芯片,内置32.768kHz晶振接口和涓流充电电路,通过三线接口与MCU通信——SCLK、I/O、RST三根线就能完成所有读写操作。显示部分用LCD1602字符液晶,16列2行,5V供电那种最常见。
按键设计为四路独立输入,分别承担“菜单/切换”“加”“减”“确认”四个职能。这里有个值得注意的设计细节:DS1302和LCD1602对GPIO电平要求不同,DS1302工作在5V下更稳定,而STM32的GPIO是3.3V电平,所以DS1302的供电直接接5V,而I/O线上的信号则依赖DS1302芯片本身对输入电平的兼容性——这个在实际硬件中确实能工作,但如果你要做产品而不是课程设计,建议加上电平转换或开漏输出加外部上拉到5V的方案。
2.2 GPIO引脚映射:一张表搞懂所有连接
下面是标准的接线映射。按这个接线去搭建硬件或者在Protues里放置元件,都能直接对应上:
| 外设 | 引脚 | STM32 GPIO | 说明 |
|---|---|---|---|
| DS1302 RST | 5 | PB12 | 片选/复位线,高电平使能 |
| DS1302 SCLK | 6 | PB13 | 串行时钟,下降沿采样 |
| DS1302 I/O | 7 | PB14 | 数据线,双向 |
| LCD1602 RS | 4 | PA0 | 寄存器选择,0命令1数据 |
| LCD1602 RW | 5 | PA1 | 读写选择,0写1读 |
| LCD1602 EN | 6 | PA2 | 使能信号,下降沿锁存 |
| LCD1602 D0-D7 | 7-14 | PA4-PA7 + PB0-PB3 | 8位数据总线 |
| 按键1-4 | - | PA8-PA11 | 外部中断/查询输入,内部上拉 |
DS1302和LCD1602的驱动方式完全不同,这是这个项目最有学习价值的地方之一。DS1302是单总线式访问,只不过多了两根控制线;LCD1602虽然可以8位并口驱动,但也可以只接高4位做4线模式,代码里要写两个版本的驱动函数用来对比学习,其实可以把4线模式作为练习自己改。
2.3 掉电走时:为什么数字时钟必须配DS1302而不是STM32内部RTC
这里有一个很多初学者一开始不明白的问题:STM32F103C8T6内部明明有RTC模块,为什么还要外挂DS1302?原因在于内部RTC的时钟源是LSI或LSE,其中LSE需要外接32.768kHz晶振,校准精度一般,而且整个MCU掉电之后RTC就没法维持走时——除非用VBAT引脚接备用电池并且配置到位。
DS1302的价值就在这里:它自带VCC1和VCC2双电源引脚,主电源掉电后自动切换到备份电池。你在Protues里仿真时间走得挺正常,但做成实物后发现每次断电时间就归零,通常不是代码问题,而是DS1302的VCC2引脚没有接电池。这个项目选了DS1302,核心目标就是“断电能走时”,这是整个设计的第一需求。
3. 软件架构与工程搭建:裸机状态机 + 分层驱动
3.1 工程目录结构与模块划分
打开工程后先看目录结构,理解代码组织方式比读具体某一行更重要。工程基于标准外设库(StdPeriph_Lib)构建,不是HAL库——这有点年头,但Protues仿真和网上大多数教程都对得上,反而更容易查资料。
Project/ ├── User/ │ ├── main.c // 主函数、状态机循环 │ ├── stm32f10x_it.c // 中断服务函数 │ └── system_stm32f10x.c // 系统时钟配置 ├── Hardware/ │ ├── ds1302.c/.h // DS1302驱动:时间读写、初始化 │ ├── lcd1602.c/.h // LCD1602驱动:指令、字符、字符串 │ ├── key.c/.h // 按键扫描与消抖 │ ├── beep.c/.h // 蜂鸣器提示(闹钟和按键反馈) │ └── bsp.c/.h // GPIO初始化汇总 ├── Core/ │ ├── startup_stm32f10x_hd.s │ └── core_cm3.c/.h └── Libraries/ // 标准外设库源码main.c里最外层是一个超级循环,通过一个mode变量切换“正常显示”“调时模式”“闹钟设置模式”三个状态。这种写法在裸机小项目里非常常见,好处是逻辑清楚、调试时可预测,缺点是一个状态里的耗时操作会阻塞其它任务,但数字时钟这种场景毫秒级阻塞完全可接受。
3.2 Protues仿真需要注意的配置细节
在用Protues跑这个工程之前,有几个配置必须提前检查。第一,STM32F103C8T6在Protues里需要设置晶振频率为8MHz,这是代码里SystemInit()锁相环倍频到72MHz的基准,如果设错了,所有时间相关的延时函数都会偏移。第二,DS1302的库模型在Protues中可能会走慢或者不走,往往是晶振频率参数没设成32.768kHz。
第三,LCD1602在Protues里的响应速度比真实硬件慢,如果代码里写的是“短延时后用忙标志判断”,仿真时可能因为忙标志时序差异导致显示乱码。常见做法是直接用延时函数而不是查忙标志位,牺牲一点实时性换兼容性,在仿真环境里反而更稳。第四,按键在Protues里点击不会自动产生抖动信号,但真实硬件会有,所以代码里仍然要保留消抖逻辑——仿真跑通了不代表硬件就能跑,这一点做实物时要格外留意。
3.3 时钟树与定时器配置:延时函数从哪里来
在stm32f10x_it.c或者main.c里用到了delay_ms(),它内部依赖SysTick定时器。SysTick是ARM Cortex-M内核自带的24位倒计时定时器,配置非常简单:先把时钟源设为HCLK(72MHz),再设置重装载值为72000000/1000就得到1ms中断。中断服务函数里做递减计数,delay_ms()就通过这个计数实现阻塞延时。
SysTick除了提供延时函数,另一个用途是时钟显示刷新。正常显示模式下,每250ms刷新一次LCD1602上的时间文本,这个周期如果比DS1302的读周期短,反复读同一秒会显得数字跳得很频繁但时间却没变,不太美观。代码里通常会把刷新周期放在主循环的while里用时间戳判断,而不是在SysTick中断里直接操作LCD,因为LCD1602的写时序较长,放在中断里会影响时钟精确性。
4. 核心代码拆解:一个请求从写入到显示的完整链路
4.1 DS1302单字节读写:时序是这门课的核心
DS1302是三线串行接口,所有操作都是先发命令字节再读写数据字节。命令字节的格式是:最高位固定为1,bit6表示读写方向(1读0写),bit5~bit1是寄存器地址,bit0固定为0。下面是读时间的核心函数:
uint8_t DS1302_ReadByte(uint8_t command) { uint8_t i, temp = 0; DS1302_IO_IN(); // IO口设为输入 DS1302_RST_HIGH(); // 片选拉高,启动通信 // 发送命令字节(写入方向) DS1302_IO_OUT(); // IO口切为输出 for (i = 0; i < 8; i++) { if (command & 0x01) // 低位在前 DS1302_IO_HIGH(); else DS1302_IO_LOW(); DS1302_SCLK_LOW(); // SCLK低电平期间放数据 delay_us(1); DS1302_SCLK_HIGH(); // 上升沿锁存数据 delay_us(1); command >>= 1; // 下一位 } // 读取数据(读方向) DS1302_IO_IN(); // 切换为输入模式 for (i = 0; i < 8; i++) { temp >>= 1; if (DS1302_IO_READ()) // 读取当前IO电平 temp |= 0x80; DS1302_SCLK_HIGH(); delay_us(1); DS1302_SCLK_LOW(); // 下降沿输出数据 delay_us(1); } DS1302_RST_LOW(); // 结束通信 return temp; }这段代码的关键在于IO方向切换:命令阶段IO口是输出,数据阶段IO口是输入,切换之间要留出时间让引脚状态稳定。初学者最容易在这里翻车——漏了IO_IN/OUT切换,读出来的数据全是0xFF。另一个坑是时钟极性:DS1302手册写明数据在SCLK上升沿写入芯片、下降沿从芯片输出,但不同网上代码有写先拉高再拉低的,也有先拉低再拉高的,如果你读到的数据错位,把SCLK的翻转顺序反过来试试。
DS1302_ReadByte()是通用函数,往外封装一层就得到读取具体寄存器的时间:
uint8_t DS1302_ReadTimeReg(uint8_t reg) { uint8_t value; DS1302_RST_LOW(); DS1302_SCLK_LOW(); value = DS1302_ReadByte(reg | 0x01); // 读命令 = 地址 + 0x81 return value; }调用时传入0x80(秒寄存器)就得到秒,0x82得到分,0x84得到时。返回值是BCD码,比如读到0x59表示59秒,换算十进制是十位×16 + 个位。这个BCD码的处理在main.c里会有专门的小函数做转换。
4.2 初始化与校时接口:一次性写入当前时间
DS1302上电后的默认时间是2000年1月1日0点0分0秒,需要用写寄存器的方式校时。下面是时间设置函数,在首次上电或者用户按下“设置时间”键时调用:
void DS1302_SetTime(uint8_t year, uint8_t month, uint8_t date, uint8_t hour, uint8_t minute, uint8_t second) { uint8_t buf[7] = { DecToBcd(second) & 0x7F, // 秒,bit7是暂停位,清零即启用 DecToBcd(minute), DecToBcd(hour) & 0x3F, // 小时,bit7最高位保留 DecToBcd(date), DecToBcd(month), year, // 年直接存储,例如25代表2025 0x00 // 星期,暂未使用 }; DS1302_WriteBurst(buf, 7); // 连续写模式 }这里有个容易被忽略的点:秒寄存器的bit7是时钟暂停位,如果写入秒时把最高位置1了,DS1302会停止振荡,时间不走。代码里用& 0x7F把这位置0,就是确保芯片正常走秒。小时寄存器的bit6是12/24小时制标志,bit5是AM/PM,24小时制必须把bit6和bit5都清零,所以& 0x3F做的就是这个保护。初始化代码一般还会顺带关闭涓流充电功能,因为课程设计如果接了可充电电池,充电电阻没选对会伤电池。
4.3 LCD1602驱动移植:从标准例程到本项目
LCD1602的驱动几乎是最成熟的移植单元,网上随便找一份标准例程都能用,但关键不在抄,而在理解为什么会留出初始化命令序列。初始化时要按手册走“8位模式设置 -> 显示开 -> 清屏 -> 输入模式设置”这四步,每一步之间要留几毫秒延时,这是LCD内部控制器上电自检的要求。
void LCD1602_Init(void) { delay_ms(20); // 等待LCD上电稳定 LCD1602_WriteCmd(0x38); // 8位模式、2行显示、5x8点阵 delay_ms(5); LCD1602_WriteCmd(0x38); delay_us(100); LCD1602_WriteCmd(0x08); // 显示关闭 LCD1602_WriteCmd(0x01); // 清屏 delay_ms(2); LCD1602_WriteCmd(0x06); // 光标右移、不移动屏幕 LCD1602_WriteCmd(0x0C); // 显示开、光标关、闪烁关 }这个初始化序列几乎不用改,最多根据LCD厂商的手册微调延时。移植的核心工作是把底层引脚宏改到自己的GPIO配置上,比如把原来用的PE口改成PA口。还有一点,如果你用4线模式接法,初始化序列必须发两次0x28(第一次是让控制器确认4位模式),否则显示错位。项目里默认提供8线模式,4线改法留作练习。
4.4 按键状态机:单次触发与长按连续调节
按键最容易出的问题不是消抖,而是“一次按键被识别成多次”。机械按键按下到稳定通常有5~10ms的抖动,如果你在main循环里每1ms扫一次键且不加消抖,一次物理按下就会触发四五次逻辑按键。简单而有效的方案是用时间窗口消抖:检测到电平变化后,等20ms再次确认电平状态,确认是稳定变化再执行按键动作。
uint8_t Key_Scan(void) { static uint8_t key_state = 0; // 0未按下,1可能按下,2已确认 uint8_t key_pressed = 0; uint8_t pin_val = (GPIOA->IDR & KEY_MASK); // 读取PA8~PA11 switch (key_state) { case 0: if (pin_val == 0) key_state = 1; // 检测到按下,进入确认 break; case 1: if (pin_val == 0) { key_state = 2; key_pressed = KEY_PRESSED; // 20ms后仍为按下,确认有效 } else { key_state = 0; // 抖动,复位 } break; case 2: if (pin_val != 0) key_state = 0; // 按键释放 break; default: key_state = 0; break; } return key_pressed; }这个状态机的好处是不需要显式的delay,每次调用时依据当前状态做分支,配合主循环的时间戳调用,每5ms调一次即可。状态2里如果持续按住,不会连续触发,只有从状态1进入状态2这一刻返回一次KEY_PRESSED,从而实现单次触发。如果你期望长按连续加数字,需要在状态2里再加一个计时分支:按住超500ms后,每100ms返回一次KEY_PRESSED。
4.5 主循环:三态切换与时间刷新
main.c里用一个mode变量控制状态。mode=0是正常显示,mode=1是调分,mode=2是调时,mode=3是设置闹钟。这里贴出主循环的核心骨架:
int main(void) { NVIC_Config(); GPIO_Config(); DS1302_Init(); LCD1602_Init(); Key_Init(); BEEP_Init(); DS1302_SetTime(25, 3, 18, 10, 30, 0); // 第一次上电校时,后续可注释 LCD1602_Clear(); LCD1602_WriteString(0, 0, "STM32 CLOCK"); LCD1602_WriteString(0, 1, "20XX-XX-XX XX:XX"); while (1) { uint8_t key = Key_Scan(); if (key == KEY_PRESSED) { // 根据当前mode切换显示内容 } if (mode == 0) { // 250ms刷新一次显示 } else if (mode == 1) { // 调秒/调分状态,按键处理 } else if (mode == 2) { // 调时状态 } else if (mode == 3) { // 闹钟设置 } } }主循环思路很直接:按键负责切换状态,状态决定当前屏幕显示与按键含义。DS1302_SetTime在每次重新上电后都执行一遍,就会把当前时间覆盖为默认值,所以真正做实物时就应该把它注释掉,只在第一次写程序时放开。这也是常见的一个问题,评论里很多人说“为什么我每次重新上电时间都是默认值”往往就是这行代码没注释。
5. 常见问题避坑:从仿真到实物的5个血泪经验
这节整理的是我在调试DS1302数字时钟时踩过、以及在评论区和论坛里高频出现的排查记录。每一条都按“现象 → 原因 → 解决”写,你可以对照自己的问题直接找。
5.1 LCD1602只亮背光但无字符
现象:背光亮,对比度调到最大也看不到任何字符。原因排查下来就两类:一是LCD1602的Vo引脚(3脚)没有接电位器或直接接地,导致液晶无法显影;二是初始化代码根本没有被执行到,最常见的是GPIO配置里把RS/RW/EN接到5V单片机的引脚上,电平不匹配导致写不进去。解决:先量Vo电压是否在0.5~1V区间,没有就把电位器串上;再把初始化函数里第一步的delay_ms(20)加大到50ms,排除上电时序问题。Protues里则检查元件电源电压是不是5V,LCD1602模型不接3脚电压也会显示空白。
5.2 时间每秒钟跳两次或跳一次但数字不对称
现象:秒数从1直接跳到3,或者分钟进位时秒数会闪一下。原因:DS1302数据手册规定读写时间要大于4us,代码里对应的延时用的是delay_us(1),编译器优化级别不同会导致循环延时被裁剪,实际时序就不满足。解决:把延时函数里的循环变量声明为volatile,防止编译器优化掉循环体,同时把单次延时加大到2us,用示波器量SCLK周期应该在4~8us之间,低于这个范围就是时序不够。
5.3 Protues仿真时间完全不走或走了但LCD显示不变
现象:DS1302在仿真环境里时间停在初始化那一刻,或者LCD上时间数字不动。原因:Protues对DS1302模型的处理比较特殊,它把DS1302的晶振频率默认设为12pF负载电容对应的参数,但仿真库里DS1302没有独立晶振引脚,它内部自动结合模型频率。解决方法是双击DS1302元件,在属性里把Crystal Frequency设为32768Hz,同时确保代码里的分钟读数周期不要短于250ms,太频繁读寄存器会影响模型的内部状态推进。
5.4 按键按下没反应或偶尔失灵
现象:机械按键按下十次有两次不执行,或者按一下变成连按两三次。原因:按键扫描用了外部中断触发,但中断服务函数里做了耗时操作(比如直接刷新LCD),导致按键消抖状态机在中断里被卡住,后台循环没法及时处理。解决:中断服务函数里只置标志位,按键处理全部放主循环;再用GPIO内部上拉而不是外面飞线接上拉电阻,内部上拉电阻值大约40kΩ,比外接10kΩ要弱但抗干扰更好。如果你用的是Protues,按一次触发两次很可能是仿真模型的行为差异,看代码比看仿真更可靠。
5.5 拔掉USB线再上电,时间清零
现象:断电重新上电后,LCD显示的还是初始化代码里的默认时间。原因:归根结底是DS1302的VCC2备份电池没有接,或者VCC1和VCC2都接在同一路电源上了。DS1302的电源设计是VCC1接主电源(5V),VCC2接备份电池(3.6V纽扣电池),主电源掉电后芯片自动切换。解决:检查原理图,VCC2必须接到电池正极,且电池地要与系统共地。同时留意DS1302的涓流充电控制寄存器,如果初始化时把它设成了充电模式且电阻配置为2kΩ,会对纽扣电池过充,所以初始化里最好显式关闭涓流充电。
6. 二次开发推演:从数字时钟到多功能桌面设备
很多人在跑通这套数字时钟之后就把它丢在一边了,其实它的扩展空间比想象中大得多。这套硬件底座——STM32F103C8T6 + LCD1602 + DS1302 + 四个按键——已经覆盖了“显示、人机交互、外部通信、时间基准”四个要素,换句话说,它是一个可以承载多种功能的微型桌面设备底座。
第一个可以直接动手的改造是把LCD1602换成OLED(比如SSD1306驱动的0.96寸I2C OLED)。换屏的难点不在接线,而在于把8位并行的LCD驱动换成I2C时序驱动。I2C OLED底层只需要两个GPIO模拟SCL和SDA,参考DS1302的时序思路,核心就是START信号、ACK判断、DATA字节高低位顺序。这个改造做好之后,显示内容丰富度会大幅提升——可以画进度条、小图标甚至简洁的菜单界面,代价是代码量比LCD1602多出约一倍。OLED的驱动库可以直接用网上开源的SSD1306驱动,重点在I2C的延时和ACK处理要适配STM32F103的GPIO翻转速度。
第二个值得做的是加入温湿度传感器,推荐DHT11,单总线协议,一根数据线就能通信。DHT11的数据读取时序比DS1302稍复杂一点,需要精确到微秒级捕捉信号沿,但代码核心仍然是GPIO输入输出方向切换和延时控制。叠加DS1302的时间,可以在LCD上显示“当前时间 + 温度 + 湿度”,再把温湿度数据存档到AT24C02(I2C接口EEPROM)里,就有点环境监测站的味道了。这里就会用到STM32的硬件I2C或者软件模拟I2C,建议先用软件模拟,代码可读性好,调试也方便。
第三个改造方向是给它加上报警输出。硬件上把蜂鸣器换成一个继电器模块(或者保留蜂鸣器但加驱动三极管),在闹钟设置里增加“响铃时长”参数,闹钟触发后通过状态机控制提醒结束的时机。这些逻辑都可以沿用原有的按键状态机和模式切换框架,只需在状态2(调时模式)里加一个“返回首页后检查闹钟时间”的步骤。
从技术角度看,这套项目的价值不只是“时钟能跑”,而在于它把GPIO模拟时序、外设初始化、状态机设计、中断和查询的选择、Protues与实物调试的差异这几个嵌入式基础问题全部串起来了。做完这个再去碰I2C总线的EEPROM、SPI接口的Flash,会发现套路极其相似——先看数据手册时序图,再搞清时钟极性和边沿,然后是方向切换,最后踩一遍“为什么读回来全是FF”的坑。我希望你下载这套资源后,不要只满足于仿真跑通,尽量去验证一下DS1302的时序细节,调过一轮再回去看单片机的IO引脚怎么工作,收获会比单纯跑通大得多。
本文还有配套的精品资源,点击获取