第三十四章 RTC实时时钟LCD显示实验

本章在讲什么

上一章我们造好了一个通用 I2C 驱动模块,本章立刻兑现它的复用价值:换一颗芯片——RTC 实时时钟 PCF8563(板上焊接的是兼容的国产 AT8563),把芯片里的实时时间读出来,显示在 RGB LCD 屏幕上。

PCF8563 是一款低功耗时钟/日历芯片,内部自带 32.768KHz 晶振计时,即使系统断电(有后备电池时)也能持续走时。它有 16 个 8 位寄存器:00H/01H 是控制/状态寄存器,02H~08H 是秒、分、时、日、月、年计时寄存器——本章只需要打交道的就是这 7 个。

实验任务:FPGA 先给 PCF8563 写入初始时间,然后不断读取当前时间,在 LCD 屏上分两行显示"年月日"和"时分秒",秒在不停跳动。

本章最大的学习点是看懂同一套 I2C 驱动如何通过改参数适配新芯片,以及 BCD 码数据如何直接映射到字符显示。

核心概念拆解

34.1 PCF8563 的三个关键特性

① 器件地址 7'h51:注意和 EEPROM(7'h50)不同!两者挂在同一条 I2C 总线上,全靠地址区分。顶层模块中:

parameter SLAVE_ADDR = 7'b101_0001;  // PCF8563 器件地址(EEPROM 是 101_0000)
parameter BIT_CTRL   = 1'b0;         // 字地址 8 位(EEPROM 是 16 位)

② BCD 码存储:时间寄存器中的数值用 8421 BCD 码存储——4 个二进制位表示 1 位十进制数。比如 23 秒存的是 8'b0010_0011(0x23),而不是二进制的 0x17。这个特性带来巨大便利:读出的高 4 位就是十位、低 4 位就是个位,可以直接当字符数组下索引字模,不需要做二进制-十进制转换。

③ 上电要等 8ms:芯片手册规定,上电到第一个起始信号之间需等待 8ms 消除不稳定状态。代码里用 WAIT_TIME = 13'd8_000(dri_clk 为 1MHz,8000 个周期 = 8ms)实现。

另外注意秒寄存器的最高位 VL 是"数据有效性标志"(VL=1 表示时钟数据不保证准确),所以代码读秒时用 i2c_data_r[6:0] 而不是整个字节;月寄存器的最高位是世纪位 C,读月时取 i2c_data_r[4:0]

34.2 系统架构:I2C 驱动零改动复用

top_rtc_lcd
 ├── pcf8563_ctrl   (新增:初始化 + 循环读时间,输出 sec/min/hour/day/mon/year)
 ├── i2c_dri        (原封不动复用上一章的驱动!)
 └── lcd_rgb_char   (由字符显示实验改造:lcd_display 模块重写显示内容)

对比上一章,i2c_dri 一行代码都没改——只是顶层参数从 SLAVE_ADDR=7'h50, BIT_CTRL=1 变成了 SLAVE_ADDR=7'h51, BIT_CTRL=0。这正是上一章把器件地址、字地址位数做成参数的回报。

34.3 pcf8563_ctrl:一条"写6个寄存器→循环读6个寄存器"的状态流

业务流程:上电等 8ms → 依次写秒(02h)、分(03h)、时(04h)、天(05h)、月(07h)、年(08h)寄存器 → 拉高 i2c_rh_wl 切读模式 → 从 flow_cnt=1 重新开始循环读。用 flow_cnt[3:0] 编排,每两个状态处理一个寄存器(一个触发、一个等完成):

4'd1: begin                       // 触发写/读秒寄存器
    i2c_exec   <= 1'b1;
    i2c_addr   <= 16'h02;
    flow_cnt   <= flow_cnt + 4'b1;
    i2c_data_w <= TIME_INIT[7:0]; // 初始时间的秒字段
end
4'd2: begin                       // 等本次操作完成
    if(i2c_done == 1'b1) begin
        sec     <= i2c_data_r[6:0];  // 写阶段存初始值,读阶段存实时值
        flow_cnt<= flow_cnt + 4'b1;
    end
end

初始时间通过参数传入:TIME_INIT = 48'h24_01_01_09_30_00,从高到低依次是年、月、日、时、分、秒(即 2024 年 01 月 01 日 09:30:00)。年只写 0~99 两位,显示时千位百位固定为"20"。

巧妙之处在于同一组状态既用于写也用于读:写阶段 i2c_data_w 从 TIME_INIT 取值,读阶段同一个 sec <= i2c_data_r[6:0] 语句自动接收实时数据;年寄存器读完时 i2c_rh_wl <= 1'b1 并把 flow_cnt 拉回 1,实现无限循环读——因为时钟在走,必须持续刷新。

初始化只执行一次:PCF8563 会从初始时间开始自行累计计时,之后每次读到的都是最新时间。

34.4 LCD 显示:BCD 直接索引字模数组

显示方案:每个字符 8×16 像素,第一行从 (1,1) 起显示"20XX XX XX"(年月日),第二行从 (17,17) 起显示"XX XX XX"(时分秒),黑字白底。

数字 0~9 的字模由取模软件生成,存成 10 个 128 位的数组(16 行 × 8 列,一行占 8 位):

reg [127:0] char [9:0];
char[2] <= 128'h0000003C4242420404081020427E0000;  // "2"的字模

每个像素位置判断的核心公式(以第一行第一个字符"2"为例):

else if((pixel_xpos >= CHAR_POS_X_1 - 11'd1)
     && (pixel_xpos <  CHAR_POS_X_1 + CHAR_WIDTH_1/10*1 - 11'd1)
     && (pixel_ypos >= CHAR_POS_Y_1)
     && (pixel_ypos <  CHAR_POS_Y_1 + CHAR_HEIGHT)) begin
    if(char[2][(CHAR_HEIGHT+CHAR_POS_Y_1 - pixel_ypos)*8
              - ((pixel_xpos - (CHAR_POS_X_1-11'd1)) % 8) - 11'd1])
        pixel_data <= BLACK;   // 字模位为1 → 黑色
    else
        pixel_data <= WHITE;   // 为0 → 白色背景
end

理解这段代码的关键是把 128 位字模看成 16×8 的二维点阵:

  • (CHAR_HEIGHT + CHAR_POS_Y_1 - pixel_ypos) * 8:算出当前是第几行(每换一行跨 8 位);
  • (pixel_xpos - 起点) % 8:算出当前是这一行的第几列;
  • 最终索引到 128 位中的某一位,为 1 涂黑、为 0 涂白。

显示年月日/时分秒数值时,只需把字面量 char[2] 换成变量索引:年的十位用 char[year[7:4]]、个位用 char[year[3:0]]——BCD 码的高 4 位/低 4 位恰好就是十位/个位的数值,直接当下标取字模,一行转换代码都不用写。这就是 34.1 说的 BCD 红利。

图片对照

PCF8563接口原理图

图:PCF8563 原理图——与 EEPROM 挂在同一条 I2C 总线,OSCI/OSCO 接 32.768KHz 晶振

RTC实时时钟显示系统架构图

图:系统架构——I2C 驱动模块原样复用,只新增 pcf8563_ctrl 控制模块

RTC实时时钟显示效果图

图:LCD 显示效果——第一行年月日、第二行时分秒

实验结果

图:上板现象——屏幕时间持续走动

初学者容易踩的坑

  1. 器件地址没换:直接复制 EEPROM 工程却忘了把 SLAVE_ADDR7'h50 改成 7'h51,PCF8563 永远不应答。同样 BIT_CTRL 要从 1 改成 0(PCF8563 寄存器地址是单字节 02H~08H)。
  2. 忘了 8ms 上电等待:跳过 WAIT_TIME 直接配置寄存器,芯片还没准备好,初始化可能失败或数据错乱。
  3. 把 BCD 当二进制处理:如果对读出的秒做二进制运算(如 sec + 1),得到的结果显示出来会乱跳。记住寄存器里 0x59 表示 59 秒;想显示就直接拆 nibble 索引字模,想做算术才需要 BCD→二进制转换。
  4. 寄存器保留位没屏蔽:秒寄存器 bit7 是 VL 标志、月寄存器 bit7 是世纪位、bit6~5 保留,直接用整个字节索引字模会越界(索引到 10 以上的字模数组)。必须按位截取:[6:0][4:0] 等。
  5. 时间不刷新或一直重新初始化:读循环必须回到"读秒"状态无限往复;如果不小心让 flow_cnt 每圈都走写路径,屏幕时间会永远停在初始值。区分点就是 i2c_rh_wl 只在年寄存器读完那一次被拉高。
  6. 字模数组索引公式抄错(x % 8)、行偏移乘 8、起点坐标减 1,三处细节错一处,字符就会显示成乱码或镜像。建议先用固定字符"2"验证公式,再替换成变量索引。

小结 & 下一步

本章的核心收获不是新协议,而是复用与适配的方法论

  • 驱动复用:上一章参数化的 i2c_dri 零改动跑在新芯片上——好的分层设计会让"换器件"变成"换参数";
  • 器件适配三要素:换一颗 I2C 芯片只需要改三件事——器件地址、字地址位数、寄存器布局(地址+含义+保留位);
  • BCD 码的显示红利:拆 nibble 直接索引字模,省去进制转换;
  • 持续轮询模型:对"数据自己在变"的器件(时钟),业务层是无限循环读,而不是 EEPROM 那种"一次测试就结束"。

下一步:第三十五章将进入测量仪器领域——等精度频率计。将学习如何用 FPGA 精确测量信号频率,涉及闸门时间、等精度测量法如何消除 ±1 误差等经典测频技术。