基于STM32F103C8的LED全彩点阵屏日历时钟项目(128×64 HUB75 + DS1302
·
基于STM32F103C8的LED全彩点阵屏日历时钟项目(128×64 HUB75 + DS1302)
作者:knight
日期:2026年6月1日
技术栈:STM32、C语言、HUB75、DS1302、嵌入式系统
📋 项目简介
本项目使用STM32F103C8微控制器,搭配128×64 HUB75全彩LED点阵屏和DS1302实时时钟模块,实现了一台功能完整的桌面日历时钟。项目特点包括:
- 自研图形栈:完全自主实现的字体引擎和绘图原语,零第三方图形库依赖
- 断电不丢时:DS1302 + CR2032电池备份,掉电后时间继续走
- 视觉精致:8色显示、冒号闪烁、秒进度条、中文星期显示
- 工程化打磨:DS1302驱动包含多重抗干扰设计,通信失败自动降级
🛠️ 硬件方案
主要元器件清单
| 元器件 | 型号/规格 | 数量 |
|---|---|---|
| MCU | STM32F103C8 (Cortex-M3 @ 72MHz) | 1 |
| LED点阵屏 | 128×64 HUB75 1/32扫全彩 | 1 |
| RTC模块 | DS1302 + CR2032电池座 | 1 |
| 杜邦线 | 20cm 公对公 | 若干 |
| 电源 | 5V/2A DC电源 | 1 |
引脚分配表
| 信号 | 引脚 | 信号 | 引脚 |
|---|---|---|---|
| R1 | PB0 | A | PA3 |
| G1 | PB1 | B | PA4 |
| B1 | PA2 | C | PA5 |
| R2 | PB12 | D | PA6 |
| G2 | PB8 | E | PA7 |
| B2 | PB5 | CLK | PA1 |
| OE | PB6 | LAT | PA0 |
| DS-CLK | PA8 | DS-DAT | PA9 |
| DS-RST | PA10 |
注意:关闭JTAG仅保留SWD,避免PB3/PB4/PA15引脚冲突
🏗️ 软件架构
系统框图
应用层(主循环):
读RTC → 渲染帧缓冲 → 扫描刷屏
中断层(TIM2 1ms ISR):
仅做"标志位翻转 + 冒号闪烁",重活留给主循环
驱动层:
DS1302驱动 / HUB75扫描 / 自研字体引擎
关键技术实现
1. 自研图形栈
- 64×128字节帧缓冲:每像素3-bit RGB,共8色
- 自研5×7 ASCII字体:支持1×/2×整数倍缩放
- 16×16中文字模:"周 / 一 / 二 / 三 / 四 / 五 / 六 / 日"
- 绘图原语:像素/横线/虚线/矩形/L形装饰角
- Sakamoto算法:直接从年/月/日推算星期,与DS1302星期字段解耦
2. HUB75扫描原理
- 32行扫描:每行同时点亮上下两半屏(行r与r+32)
- 单行流程:关OE → 写行选ABCDE → 串行移位128列像素 → LAT锁存 → 开OE → 保持时间决定亮度
- 性能优化:直接寄存器操作ODR/BSRR/BRR,绕开标准库开销
- 防鬼影:每行开始前关OE,整屏扫完再关OE,避免末行常亮
3. DS1302驱动的工程化打磨
- GPIO速度优化:从50MHz降到2MHz,抑制长杜邦线边沿振铃与地反弹
- 读模式配置:PA9切换为"上拉输入",应对廉价模块无外部上拉
- 上电静默:先100ms等待DS1302内部POR完成
- 双路自检:RAM 0xA5/0x5A写读 + 秒寄存器写读还原
- BCD范围校验:检测历史数据损坏,自动整体重写时间寄存器
- 通信失败软降级:本地秒进位 + 闰年日历,"看起来仍在走"
4. 烧录时间同步策略
- 编译指纹:用
__DATE__ __TIME__生成16-bit指纹存入DS1302内部RAM - 智能同步:新固件首次上电同步时间,之后复位/掉电不重置
- 工程价值:开关
SYNC_RTC_ON_BOOT=1也不会因为复位重置时间
🎨 UI视觉设计
- 顶部:"Dream Chaser"标题(青色
#22D3EE) - 中部:HH:MM:SS大号黄色2×字体(
#FFC83D),冒号每500ms闪烁 - 分隔线:蓝色虚线(
#3B82F6) - 下部:YYYY/MM/DD白色(
#E6EDF7)+ 16×16中文星期(洋红#E879F9) - 进度条:0-19s红 / 20-39s黄 / 40-59s绿
- 装饰:四角青色L形装饰角
💻 代码实现详解
主程序框架(main.c)
/* ============================================================
* LED 全彩点阵屏日历时钟 (128 x 64 / HUB75 / 1-32 扫)
* MCU: STM32F103C8 @ 72 MHz
* ============================================================ */
#include "stm32f10x.h"
#include "ds1302.h"
/* 烧录时间同步配置 */
#define SYNC_RTC_ON_BOOT 0 /* 1=新固件首次上电写入下方时间, 0=永不写 */
#define SYNC_YEAR 2026
#define SYNC_MONTH 5
#define SYNC_DAY 30
#define SYNC_HOUR 21
#define SYNC_MINUTE 35
#define SYNC_SECOND 0
#define SYNC_WEEK 6
/* 帧缓冲:每像素 1 字节,低 3 位有效 */
static uint8_t frame_buffer[64][128];
/* 时间状态 */
static volatile uint8_t s_hour = 0;
static volatile uint8_t s_minute = 0;
static volatile uint8_t s_second = 0;
static volatile uint16_t s_year = 2026;
static volatile uint8_t s_month = 1;
static volatile uint8_t s_day = 1;
static volatile uint8_t s_dirty = 1;
static volatile uint8_t s_blink_on = 1;
static volatile uint8_t s_need_rtc_read = 1;
static uint8_t s_rtc_ok = 0;
/* 主循环 */
int main(void)
{
/* 1. 初始化GPIO */
RCC->APB2ENR |= RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO;
AFIO->MAPR = (AFIO->MAPR & ~AFIO_MAPR_SWJ_CFG) | AFIO_MAPR_SWJ_CFG_JTAGDISABLE;
GPIOA->CRH = 0x33333333; GPIOA->CRL = 0x33333333;
GPIOB->CRH = 0x33333333; GPIOB->CRL = 0x33333333;
GPIOA->ODR = 0; GPIOB->ODR = OE_PIN;
/* 2. 初始化DS1302 */
ds1032_init();
s_rtc_ok = ds1302_self_test();
if (s_rtc_ok) {
#if SYNC_RTC_ON_BOOT
/* 编译指纹时间同步逻辑 */
const char *stamp = __DATE__ __TIME__;
uint16_t fp = 0xA53C;
for (uint8_t i = 0; stamp[i]; i++) {
fp = (uint16_t)((fp << 5) - fp + (uint8_t)stamp[i]);
}
uint8_t magic = ds1302_ram_read(0);
uint8_t fp_hi = ds1302_ram_read(1);
uint8_t fp_lo = ds1302_ram_read(2);
if (magic != 0x5A || fp_hi != (uint8_t)(fp >> 8) || fp_lo != (uint8_t)(fp & 0xFF)) {
ds1302_set_time(SYNC_YEAR, SYNC_MONTH, SYNC_DAY,
SYNC_HOUR, SYNC_MINUTE, SYNC_SECOND, SYNC_WEEK);
ds1302_ram_write(0, 0x5A);
ds1302_ram_write(1, (uint8_t)(fp >> 8));
ds1302_ram_write(2, (uint8_t)(fp & 0xFF));
}
#endif
sync_from_rtc();
}
/* 3. 首屏渲染 */
render_screen(s_hour, s_minute, s_second,
s_year, s_month, s_day, s_blink_on);
/* 4. 初始化定时器 */
TIM2_Init_1ms();
/* 5. 主循环 */
while (1) {
if (s_rtc_ok && s_need_rtc_read) {
s_need_rtc_read = 0;
sync_from_rtc();
}
if (s_dirty) {
s_dirty = 0;
render_screen(s_hour, s_minute, s_second,
s_year, s_month, s_day, s_blink_on);
}
scan_screen();
}
}
DS1302驱动关键函数(ds1302.c)
/* DS1302自检函数 */
u8 ds1302_self_test(void)
{
u8 result = 0;
u8 a, b;
u8 sec_backup, sec_check;
ds1302_wirte_rig(DS1302_REG_WP, 0x00);
/* 方法1: RAM测试 */
ds1302_wirte_rig(DS1302_REG_RAM0, 0xA5);
a = ds1302_read_rig(DS1302_REG_RAM0 | 0x01);
ds1302_wirte_rig(DS1302_REG_RAM0, 0x5A);
b = ds1302_read_rig(DS1302_REG_RAM0 | 0x01);
if (a == 0xA5 && b == 0x5A) result |= 0x01;
/* 方法2: 秒寄存器写读测试 */
sec_backup = ds1302_read_rig(DS1302_REG_SEC | 0x01);
ds1302_wirte_rig(DS1302_REG_SEC, 0x49);
sec_check = ds1302_read_rig(DS1302_REG_SEC | 0x01);
ds1302_wirte_rig(DS1302_REG_SEC, sec_backup & 0x7F);
if ((sec_check & 0x7F) == 0x49) result |= 0x02;
ds1302_wirte_rig(DS1302_REG_WP, 0x80);
return result;
}
/* 设置时间函数 */
void ds1302_set_time(u16 year, u8 month, u8 day,
u8 hour, u8 minute, u8 second, u8 week)
{
if (year >= 2000) year -= 2000;
ds1302_wirte_rig(DS1302_REG_WP, 0x00);
ds1302_wirte_rig(DS1302_REG_SEC, dec_to_bcd(second) & 0x7F);
ds1302_wirte_rig(DS1302_REG_MIN, dec_to_bcd(minute));
ds1302_wirte_rig(DS1302_REG_HOUR, dec_to_bcd(hour) & 0x3F);
ds1302_wirte_rig(DS1302_REG_DATE, dec_to_bcd(day));
ds1302_wirte_rig(DS1302_REG_MONTH, dec_to_bcd(month));
ds1302_wirte_rig(DS1302_REG_DAY, dec_to_bcd(week));
ds1302_wirte_rig(DS1302_REG_YEAR, dec_to_bcd((u8)year));
ds1302_wirte_rig(DS1302_REG_WP, 0x80);
}
HUB75扫描函数
/* 扫描刷屏函数 */
static void scan_screen(void)
{
for (uint8_t scan_row = 0; scan_row < 32; scan_row++) {
/* 1) 关闭显示,避免移位/换行时鬼影 */
GPIOB->BSRR = OE_PIN;
/* 2) 行选 */
uint16_t row_sel =
((scan_row & 0x01) ? A_PIN : 0) |
((scan_row & 0x02) ? B_PIN : 0) |
((scan_row & 0x04) ? C_PIN : 0) |
((scan_row & 0x08) ? D_PIN : 0) |
((scan_row & 0x10) ? E_PIN : 0);
GPIOA->ODR = row_sel;
/* 3) 串行移位128列像素 */
for (int col = 0; col < SCREEN_W; col++) {
uint8_t up = frame_buffer[scan_row][col];
uint8_t dn = frame_buffer[scan_row + 32][col];
uint16_t pb = OE_PIN;
if (up & COLOR_RED) pb |= R1_PIN;
if (up & COLOR_GREEN) pb |= G1_PIN;
if (dn & COLOR_RED) pb |= R2_PIN;
if (dn & COLOR_GREEN) pb |= G2_PIN;
if (dn & COLOR_BLUE) pb |= B2_PIN;
GPIOB->ODR = pb;
if (up & COLOR_BLUE) GPIOA->BSRR = B1_PIN;
else GPIOA->BRR = B1_PIN;
GPIOA->BSRR = CLK_PIN;
GPIOA->BRR = CLK_PIN;
}
/* 4) 锁存 */
GPIOA->BSRR = LAT_PIN;
GPIOA->BRR = LAT_PIN;
/* 5) 打开显示 */
GPIOB->BRR = OE_PIN;
/* 6) 保持时间决定亮度 */
for (volatile uint32_t i = 0; i < 80; i++) __NOP();
}
/* 全部行扫完后关闭显示,避免最后一行常亮 */
GPIOB->BSRR = OE_PIN;
}
字体定义(5×7 ASCII)
/* 5×7 ASCII字体定义 */
static const glyph_t g_font[] = {
{'0', {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}},
{'1', {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E}},
{'2', {0x0E,0x11,0x01,0x06,0x08,0x10,0x1F}},
{'3', {0x1E,0x01,0x01,0x0E,0x01,0x01,0x1E}},
{'4', {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}},
{'5', {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E}},
{'6', {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}},
{'7', {0x1F,0x01,0x02,0x04,0x08,0x08,0x08}},
{'8', {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}},
{'9', {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C}},
{':', {0x00,0x06,0x06,0x00,0x06,0x06,0x00}},
{'/', {0x01,0x02,0x02,0x04,0x08,0x08,0x10}},
};
📊 项目成果展示
运行效果
- STM32+点阵屏+DS1302的完整连接图:

- 显示效果图:

- 电路原理图

- PCB图:

性能指标
- 刷新率:32行 × 每行128像素移位 ≈ 流畅显示
- RAM占用:64×128 = 8KB帧缓冲(主要开销)
- CPU占用:TIM2 1ms中断负载 < 1%
- 时间精度:DS1302典型误差 ±2ppm(约每月±5秒)
🚧 遇到的问题与解决方案
| 问题 | 解决方法 | 效果 |
|---|---|---|
| 1. 硬件连接出错 | 利用AI辅助快速定位问题点 | 节省大量排查时间 |
| 2. 特殊功能引脚冲突 | 上网查阅STM32数据手册,关闭JTAG保留SWD | 解决PB3/PB4/PA15复用问题 |
| 3. RTC显示乱码 | 检查DS1302电池,向室友求助学习 | 发现电池接触不良问题 |
| 4. 代码过于复杂 | 从头开始系统化排查:划模块→读注释→打日志 | 理清多线交织逻辑 |
📚 学习心得
1. 不要过于自信,注意每一个微小细节
很多看似不会出错的地方往往是bug的根源,尤其是硬件接线。"我以为没问题"四个字是排查时最大的盲点 —— 反复确认每根线、每个引脚比凭直觉跳过更省时间。
2. 多利用AI,顺着提示一步步排查问题
把AI当成一个永远在线的"高年级学长"。给清楚现象、贴上代码片段或寄存器配置,让它给出可执行的排查清单,然后真的一条条照着试 —— AI给方向,自己做验证。
3. 系统化检查,从硬件到软件按层排查
先看电源和接线,再看时钟与GPIO配置,最后才是业务逻辑。按层逐一排查问题,避免在错误层面来回纠结 —— 是工程化思维而非"灵感修bug"。
📥 资源下载
完整工程代码
# 项目文件结构
LED全彩点阵屏日历时钟/
├── User/
│ ├── main.c # 主程序
│ ├── ds1302.c # DS1302驱动
│ ├── ds1302.h # DS1302头文件
│ ├── led_matrix.c # LED点阵驱动
│ └── delay.c # 延时函数
├── Library/ # STM32标准外设库
├── Start/ # 启动文件
└── Project.uvprojx # Keil工程文件
代码获取方式:
相关工具
- 开发环境:Keil MDK 5 / STM32CubeIDE
- 调试工具:ST-Link V2 / J-Link
- 串口工具:Putty / XCOM
🎯 总结
本项目实现了一台128×64全彩、走时稳、视觉精致、可靠性深度打磨的STM32桌面时钟。通过以下工程亮点展示了嵌入式系统开发的完整流程:
- 自研图形栈:从零实现字体引擎和绘图原语
- DS1302抗扰驱动:多重自检 + 通信失败软降级
- 编译指纹时间同步:智能同步策略,避免重复写RTC
- HUB75扫描优化:直接寄存器操作,性能最大化
声明:本文为原创技术分享,转载请注明出处。项目代码遵循MIT开源协议,欢迎学习使用和改进。
更多推荐



所有评论(0)