简介这份压缩包是一份基于STM32的数字时钟完整工程面向嵌入式初学者与单片机爱好者以STM32为主控集成LCD显示、RTC实时时钟和闹钟中断功能并通过Proteus提供虚拟仿真环境便于快速验证程序逻辑。包内共182个文件主要包括C/H源码、Keil工程文件uvprojx/uvoptx、Proteus仿真工程、hex烧录文件以及编译中间文件压缩包大小约6.16MB可直接用于学习、仿真和烧录。目前已有10436人浏览学习。通过阅读源码和工程可以掌握RTC日历配置、时钟系统初始化、LCD驱动移植、定时器与中断服务程序设计、低功耗模式选择等核心知识点同时了解STM32标准外设库的常见调用方式和调试思路。对于希望从基础实验过渡到综合项目开发的学习者这份资料提供了完整、清晰的参考样例能有效缩短入门周期。1. 基于STM32的数字时钟设计从一颗时钟芯片到四位数码管一块最小系统板能做的事很多但最直观的反馈就是把时间正确地显示出来。这个基于STM32的数字时钟项目做的就是这件事以STM32F103为主控外挂DS1302实时时钟芯片用4位一体数码管动态扫描显示时分配合独立按键调整时间整套设计在Proteus里仿真与实物共用同一份Keil工程。它非常适合刚学完GPIO、定时器和中断想拿一个完整周边项目练手的人也是课程设计或电子设计竞赛里最常被参考的底子。别以为它只是个玩具DS1302的三线时序、数码管动态扫描、按键状态机这三样后面做复杂系统时全是高频复用的基本功。2. 系统架构与硬件设计先从选型和连线把地基打牢2.1 先想清楚为什么是 DS1302 而不是内部 RTCSTM32F103内部自带RTC理论上不需要外挂芯片也能走时。但实际做数字时钟绝大多数教程和工程还是选DS1302。原因很现实内部RTC要维持掉电走时需要VBAT引脚外接电池同时要把LSE低速外部晶振接好复位或下载程序时偶尔还会把RTC寄存器弄丢排查起来比较玄学。DS1302是独立芯片自带32.768kHz晶振接口和后备电池引脚主电源断了它自己靠纽扣电池继续走MCU这边只需要用三个GPIO模拟出一套三线时序就能交换数据。更重要的是Proteus里有现成的DS1302仿真模型仿真结果和实物行为一致这给没有现成板子的同学提供了很大的试错空间。这套设计的信息流是这样的按键设定时间写入DS1302寄存器主循环定期把时间读出来BCD码转成十进制分段映射成段码再通过位选切换让4位数码管轮流点亮利用人眼视觉暂留形成一组稳定的“时:分”显示。整个过程就是一个“写时序、读时序、动态刷新”的循环不依赖操作系统也不涉及复杂算法但每一环都对时序精度有要求。2.2 引脚分配与 Proteus 仿真搭建我一般会把资源里给的引脚分配整理成一张表方便对着接线也方便后面改板子时快速查外设引脚说明DS1302 CEPB12片选高电平有效DS1302 SCLKPB13串行时钟DS1302 IOPB14数据线双向数码管段选PA0 - PA7共阳数码管8个段数码管位选PB0 - PB34位分别点亮按键 功能/加/减PA8 / PA9 / PA10调时模式切换与数值调整蜂鸣器PB8预留做闹钟扩展用段选用PA0-PA7直接驱动位选加三极管或者直接在仿真里拉高拉低都可以。这里有个细节PB12、PB13、PB14这三个引脚尽量不要和下载口复用的JTAG引脚冲突用标准库初始化前先关闭对应引脚的复用功能我在第一次画板子时就是因为这个原因折腾了半天才让DS1302正常通信。Proteus搭建时主控选STM32F103C8时钟芯片搜DS1302显示器件用7SEG-MPX4-CA这种4位共阳数码管按键用普通按钮晶振电路和复位电路按最小系统标准接法画。不少人搜索时会把Proteus拼成protues不影响找到资料但元件库里还是按正确名字找比较快。DS1302在仿真模型里对晶振的处理和实物不太一样这个坑放到第4章详细说先记住一句仿真里不需要像实物那样纠结32.768kHz晶振的起振问题。2.3 工程目录Driver 层与 App 层分开的意义拿到工程之后先别急着打开main.c猛看我习惯先看目录结构。这个项目典型的组织方式是├─Doc/ 原理图、引脚分配说明 ├─Driver/ │ ├─ds1302.c/.h 时钟芯片三线时序读写 │ ├─smg.c/.h 数码管段码表与动态扫描 │ ├─key.c/.h 按键消抖与事件上报 │ └─delay.c/.h 毫秒/微秒延时 ├─App/ │ └─clock_task.c 调时状态机与时间刷新逻辑 └─Project/ Keil MDK 工程Driver层每个文件只做一件事ds1302只负责往寄存器里写数据和读数据不关心读出来的时间要显示到哪里smg只负责把传入的数字画到对应位不关心这个数字是秒还是分。App层的clock_task才是业务逻辑它决定“现在处于运行模式还是调时模式”“按下加键应该加到小时还是分钟”。这么拆分之后后面把数码管换成LCD1602只需要重写smg.c的接口实现把DS1302换成DS3231也只是改ds1302.c内部的时序代码App层几乎不用动。这个项目虽然小但这个分层习惯是从一开始就值得带进后续项目里的。3. 核心代码拆解三线时序、动态扫描与调时状态机3.1 DS1302 读写时序命令字节与寄存器操作DS1302的通信协议核心是命令字节。每次访问寄存器先发出一个8位命令bit0是读写标志1读0写bit1到bit5是寄存器地址bit7固定为1。然后根据读写方向要么再发一个8位数据写进去要么由芯片在时钟下降沿把数据吐出来。整个过程由CE片选保护CE拉高后时序才开始结束后必须拉低。先看写时序的实现这是最底层的原语static void DS1302_WriteByte(uint8_t cmd, uint8_t dat) { uint8_t i; DS1302_CE_H(); // CE拉高启动一次传输 for (i 0; i 8; i) { DS1302_IO_Write((cmd i) 0x01); // 先发命令字节LSB在前 DS1302_SCLK_H(); // SCLK上升沿数据被锁存 DS1302_SCLK_L(); } for (i 0; i 8; i) { DS1302_IO_Write((dat i) 0x01); // 再写数据字节 DS1302_SCLK_H(); DS1302_SCLK_L(); } DS1302_CE_L(); // CE拉低结束传输 }这里命令字采用LSB先行所以每次用(cmd i) 0x01取出最低位。写寄存器之前必须先向写保护寄存器0x8E写入0x00把WP位清零否则后续所有写操作都会被芯片拒绝。这一点经常有人忽略表现出来就是“第一次上电能设置时间之后再设就写不进去”。我一般会在系统初始化时固定执行一次DS1302_WriteByte(0x8E, 0x00)把写保护默认关掉。读时序比写时序麻烦一点因为IO线需要在命令发完后从输出模式切换成输入模式static uint8_t DS1302_ReadByte(uint8_t cmd) { uint8_t i, dat 0; DS1302_CE_H(); for (i 0; i 8; i) { DS1302_IO_Write((cmd i) 0x01); DS1302_SCLK_H(); DS1302_SCLK_L(); } DS1302_IO_ModeInput(); // IO方向切换为输入示意写法 for (i 0; i 8; i) { dat 1; DS1302_SCLK_H(); DS1302_SCLK_L(); // 下降沿到来DS1302输出当前位 if (DS1302_IO_Read()) dat | 0x80; } DS1302_IO_ModeOutput(); // 恢复输出模式 DS1302_CE_L(); return dat; }很多第一次写DS1302驱动的同学会挂在同一个地方读回来全是0xFF或者全部是0x00原因就是漏了IO方向切换。标准库里切换方向要重新初始化GPIOHAL库则用HAL_GPIO_ReadPin和寄存器配置组合实现核心思想是一样的都是“发命令时当输出读数据时当输入”。DS1302常用寄存器地址和数据格式值得存一张表寄存器读命令写命令说明秒0x810x80低7位BCD码bit7为CH停振标志分0x850x84BCD码bit7为1时暂停时0x890x8812/24小时模式由bit7控制日0x870x86BCD码范围1到31月0x8B0x8ABCD码周0x8D0x8C1到7写保护0x8F0x8Ebit7为1时禁止写所有寄存器读出来的数据是BCD码例如0x57表示57秒。要转成十进制用(bcd 4) * 10 (bcd 0x0F)反过来写时间时用(dec / 10) 4 | (dec % 10)。不少人在显示端看到乱码或者“59后面跟5A”就是忘记做这个转换。3.2 数码管动态扫描从段码表到人眼暂留4位数码管如果每个位都单独接一组IO至少需要12个引脚资源不够。动态扫描的思路是段选共用位选分时同一时刻只有一位数码管被点亮但切换速度足够快人眼就会把四个位“补全”成一幅完整的画面。void SMG_Scan(void) { static uint8_t pos 0; uint8_t num time_buf[pos]; GPIO_Write(GPIOA, 0x00); // 段码全灭消隐 GPIO_ResetBits(GPIOB, 0x0F); // 关闭所有位选 GPIO_Write(GPIOA, seg_table[num]);// 放入当前位段码 GPIO_SetBits(GPIOB, (1 pos)); // 打开当前位 pos (pos 1) 0x03; // 0、1、2、3循环 }注意三条语句的顺序先消隐再切位选。如果在新位选打开之后才改段码前一位显示的残影就会在切换瞬间被新位显示出来视觉上出现拖影。段码表seg_table里存的是共阳数码管编码比如数字0对应0xC0这个根据你的硬件接法取反或调整都行。扫描频率有门槛。视觉暂留的时间大约20ms意味着每一位的点亮周期不能超过20ms否则人眼就能分辨出闪烁。4位数码管走完一轮需要4次扫描每次按2ms算一轮8ms刷新率达到125Hz远高于50Hz的临界点。这个参数很关键如果发现显示闪得厉害先检查扫描函数是不是被主循环里的DS1302读取卡顿拖慢了基本都在刷新周期超过20ms这个坎上。3.3 调时状态机与主循环编排有了底层驱动主循环的逻辑就可以写得很干净。这里用一个mode变量表示当前状态0是正常运行1是调小时2是调分钟功能键在三个状态间切换加减键根据mode决定改哪一位。int main(void) { SYSTEM_Init(); Clock_Init(); // 首次上电写初始时间之后只读 while (1) { key_scan(); // 按键扫描带回弹处理 clock_task(); // 调时状态机 if (time_update_flag) { time_update_flag 0; Read_DS1302_Time();// 每秒读一次时分秒 } SMG_Scan(); // 动态扫描 } }按键处理我是按三拍写的检测到低电平先延时20ms二次确认确认后再等按键释放才执行一次事件。第一拍过滤机械抖动第三拍防止按一次触发多次。调时状态机拿到事件后根据当前mode决定“加1分钟”还是“加1小时”改完立即写回DS1302的对应寄存器同时重新读出来刷新显示这样调时过程中用户能实时看到数字变化。运行模式下显示内容只随每秒一次的读时任务更新动态扫描则一直高频跑着两者一个管内容一个管刷新职责分开。4. 联调避坑指南仿真不走、显示闪烁、按键误触的排查记录这类板子在Proteus联调阶段问题从来不是某个函数写不出来而是几个高频坑反复出现。我按实际排查顺序整理了几条血的教训每一条都是“现象 → 原因 → 解决”的完整链路。4.1 Proteus里时钟不走秒位卡住现象程序烧进去之后数码管有显示但秒位永远不动或者只跳了一次就停住。原因DS1302的Proteus仿真模型内部已经带了完整的时钟源外部再接32.768kHz晶振反而容易让模型状态异常更常见的原因是没有往秒寄存器写入初值芯片上电后内部寄存器是未知状态CH停振位为1时钟自然不走。解决仿真里优先按模型要求接法做拿不准晶振就先不接让模型用自己的内部时钟。代码里每次初始化时先读秒寄存器如果发现bit7为1或者BCD码非法就执行一次完整的“关写保护→写秒分时→开写保护”流程给芯片一个明确的起点。另外要检查CE引脚的时序CE必须保持到一帧读写全部结束再拉低不能中途掉电。4.2 数码管闪烁或亮度不均匀现象四个位有的亮有的暗或者整体在轻微闪烁看起来非常廉价。原因主循环里DS1302读取用了几十毫秒的延时模拟时序把整个循环周期拖慢动态扫描频率跌到人眼可分辨区间还有一种情况是位选切换前没有消隐拖影叠加导致视觉亮度不一致。解决把SMG_Scan函数往高频周期里挪我一般放到SysTick的1ms中断里调用定时器只做一件事不受主循环业务影响。MS1302读取尽量一次读完秒分时多个寄存器不要在两次读取中间穿插耗时的按键处理。消隐那句GPIO_Write(GPIOA, 0x00)一定要在切位选之前执行顺序反了就会出现残影。4.3 断电再上电时间总是复位现象仿真或实物重新上电后显示时间回到初始值之前调好的时间全丢。原因VCC1后备电池引脚没接东西断电后DS1302直接掉电或者代码里每次启动都无条件执行一遍时间初始化把芯片里存的正确时间覆盖掉了。解决VCC1接一个3V电池仿真里直接放一个3V直流电源或电池模型就行。代码改成带标记的初始化只在上电后首次检测到“芯片时间无效”时才写入初始时间之后启动只读不写。标记可以放在DS1302内部RAM或者用一个EEPROM位简单一点的做法就是用秒寄存器本身的状态来判断。4.4 按键一次跳多个数调时像抽风现象按一下“加”时间连续加了好几分钟根本停不下来。原因按键消抖只做了延时确认没有等待弹起。主循环跑得飞快第一次确认之后代码还在按键函数里循环函数返回前又被同一个低电平触发了一次于是重复执行。解决按键处理必须走完整的三拍低电平检测 → 延时20ms再次确认 → 等待高电平弹起后才上报“一次按下”事件。弹起检测放进去之后问题基本绝迹。另外功能键和加减键的扫描尽量分开避免模式切换瞬间的残余电平被当成加减事件。5. 二次开发方向从数码管到LCD1602与闹钟功能5.1 显示层替换成 LCD1602 或 OLED四位数码管的显示能力有限只能显示数字分割符还得靠独立点来表现。把这套工程改成LCD1602或OLED显示是一个性价比极高的二次开发方向。驱动层完全不用动DS1302的读写函数原样保留只需要重写显示模块显示方案占用引脚显示内容上手难度数码管12个数字为主低LCD16026个含背光字母、数字、自定义字符中OLED2个I2C任意文字、图形中LCD1602无非就是把“显示时分”这件事从段码映射改成字符串拼接和写命令字核心不变。OLED更简单用I2C接口只需要SCL和SDA两根线刷新函数里把时间格式化打印上去就行。我给这个改造预留的接口是Display_Time(uint8_t hour, uint8_t minute)所有上层逻辑都走这个函数替换硬件时只改实现。5.2 加闹钟状态机并列与验证方法闹钟的本质就是“比较时间”现在已经在每秒刷新时间内容比较逻辑直接挂在读时任务后面if (now_h alarm_h now_m alarm_m sec 0 !ring_flag) { ring_flag 1; GPIO_SetBits(GPIOB, GPIO_Pin_8); // 拉高蜂鸣器 }难度不在比较本身而在状态机扩展。原来的mode有0、1、2三种状态加闹钟后至少多出两个状态设置闹钟小时、设置闹钟分钟。建议把闹钟相关状态独立成新模块不要往原来的调时状态机里硬塞否则按键处理会越来越绕。验证方法很直接把闹钟时间设成当前时间加1分钟等一分钟看蜂鸣器是否拉高如果响的时间不对多半是秒匹配条件出了问题我习惯给比较条件加一个“秒等于0”的窗口避免整分钟时重复触发。5.3 一个值得固化的调试顺序每次拿到这类有时钟芯片的新工程我的固定流程是先跑通底层读写时序直接把读回来的寄存器值打印或显示出来确认再写显示和按键逻辑确认时间能走能改之后才去动闹钟这种业务功能。跳过第一步直接写业务逻辑后面多半要回头查时序浪费时间。这个顺序被我在好几个项目里反复验证过从那以后我每次都会强制走一遍“时序先通显示跟上业务最后”。希望帮到你。本文还有配套的精品资源点击获取