基于STM32F103C8的LED全彩点阵屏日历时钟项目(128×64 HUB75 + DS1302)

作者:knight
日期:2026年6月1日
技术栈:STM32、C语言、HUB75、DS1302、嵌入式系统

📋 项目简介

本项目使用STM32F103C8微控制器,搭配128×64 HUB75全彩LED点阵屏DS1302实时时钟模块,实现了一台功能完整的桌面日历时钟。项目特点包括:

  • 自研图形栈:完全自主实现的字体引擎和绘图原语,零第三方图形库依赖
  • 断电不丢时:DS1302 + CR2032电池备份,掉电后时间继续走
  • 视觉精致:8色显示、冒号闪烁、秒进度条、中文星期显示
  • 工程化打磨:DS1302驱动包含多重抗干扰设计,通信失败自动降级

🛠️ 硬件方案

主要元器件清单

元器件型号/规格数量
MCUSTM32F103C8 (Cortex-M3 @ 72MHz)1
LED点阵屏128×64 HUB75 1/32扫全彩1
RTC模块DS1302 + CR2032电池座1
杜邦线20cm 公对公若干
电源5V/2A DC电源1

引脚分配表

信号引脚信号引脚
R1PB0APA3
G1PB1BPA4
B1PA2CPA5
R2PB12DPA6
G2PB8EPA7
B2PB5CLKPA1
OEPB6LATPA0
DS-CLKPA8DS-DATPA9
DS-RSTPA10

注意:关闭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}},
};

📊 项目成果展示

运行效果

  1. STM32+点阵屏+DS1302的完整连接图:
  2. 显示效果图
  3. 电路原理图
  4. 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工程文件

代码获取方式

  1. Github链接:knightshh/LED-Matrix-Calendar-Clock: STM32F103C8 LED全彩点阵屏日历时钟项目

相关工具

  • 开发环境:Keil MDK 5 / STM32CubeIDE
  • 调试工具:ST-Link V2 / J-Link
  • 串口工具:Putty / XCOM

🎯 总结

本项目实现了一台128×64全彩、走时稳、视觉精致、可靠性深度打磨的STM32桌面时钟。通过以下工程亮点展示了嵌入式系统开发的完整流程:

  1. 自研图形栈:从零实现字体引擎和绘图原语
  2. DS1302抗扰驱动:多重自检 + 通信失败软降级
  3. 编译指纹时间同步:智能同步策略,避免重复写RTC
  4. HUB75扫描优化:直接寄存器操作,性能最大化

声明:本文为原创技术分享,转载请注明出处。项目代码遵循MIT开源协议,欢迎学习使用和改进。

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐