STM32数字时钟实战:DS1302实时时钟与LCD1602驱动设计 简介一套基于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 64KBRAM 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 RST5PB12片选/复位线高电平使能DS1302 SCLK6PB13串行时钟下降沿采样DS1302 I/O7PB14数据线双向LCD1602 RS4PA0寄存器选择0命令1数据LCD1602 RW5PA1读写选择0写1读LCD1602 EN6PA2使能信号下降沿锁存LCD1602 D0-D77-14PA4-PA7 PB0-PB38位数据总线按键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位倒计时定时器配置非常简单先把时钟源设为HCLK72MHz再设置重装载值为72000000/1000就得到1ms中断。中断服务函数里做递减计数delay_ms()就通过这个计数实现阻塞延时。SysTick除了提供延时函数另一个用途是时钟显示刷新。正常显示模式下每250ms刷新一次LCD1602上的时间文本这个周期如果比DS1302的读周期短反复读同一秒会显得数字跳得很频繁但时间却没变不太美观。代码里通常会把刷新周期放在主循环的while里用时间戳判断而不是在SysTick中断里直接操作LCD因为LCD1602的写时序较长放在中断里会影响时钟精确性。4. 核心代码拆解一个请求从写入到显示的完整链路4.1 DS1302单字节读写时序是这门课的核心DS1302是三线串行接口所有操作都是先发命令字节再读写数据字节。命令字节的格式是最高位固定为1bit6表示读写方向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/PM24小时制必须把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变量控制状态。mode0是正常显示mode1是调分mode2是调时mode3是设置闹钟。这里贴出主循环的核心骨架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里则检查元件电源电压是不是5VLCD1602模型不接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接主电源5VVCC2接备份电池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上显示“当前时间 温度 湿度”再把温湿度数据存档到AT24C02I2C接口EEPROM里就有点环境监测站的味道了。这里就会用到STM32的硬件I2C或者软件模拟I2C建议先用软件模拟代码可读性好调试也方便。第三个改造方向是给它加上报警输出。硬件上把蜂鸣器换成一个继电器模块或者保留蜂鸣器但加驱动三极管在闹钟设置里增加“响铃时长”参数闹钟触发后通过状态机控制提醒结束的时机。这些逻辑都可以沿用原有的按键状态机和模式切换框架只需在状态2调时模式里加一个“返回首页后检查闹钟时间”的步骤。从技术角度看这套项目的价值不只是“时钟能跑”而在于它把GPIO模拟时序、外设初始化、状态机设计、中断和查询的选择、Protues与实物调试的差异这几个嵌入式基础问题全部串起来了。做完这个再去碰I2C总线的EEPROM、SPI接口的Flash会发现套路极其相似——先看数据手册时序图再搞清时钟极性和边沿然后是方向切换最后踩一遍“为什么读回来全是FF”的坑。我希望你下载这套资源后不要只满足于仿真跑通尽量去验证一下DS1302的时序细节调过一轮再回去看单片机的IO引脚怎么工作收获会比单纯跑通大得多。本文还有配套的精品资源点击获取