简介:此压缩包是一套面向FPGA学习者的I2C接口RTC实时时钟工程,基于Verilog实现PCF8563芯片的读写控制,配套Quartus 18.0完整工程文件,适用Cyclone IV E系列EP4CE10F17C8器件。包内共124个文件,涵盖rtc顶层模块、i2c_dri驱动程序、按键消抖与数码管显示源码等关键.v文件,同时包含qpf/qsf工程配置、sof/jic下载文件以及时序约束、编译报告等辅助内容,整体体积3.78MB,结构清晰便于直接打开学习。目前已有820人浏览学习。通过这套工程,读者可以掌握I2C时序状态机设计、PCF8563寄存器初始化与读写流程,并结合按键调时和数码管显示完成完整的实时时钟应用,适合作为FPGA入门I2C通信与RTC驱动的参考项目。
1. FPGA接PCF8563:为什么寄存器级I2C驱动比软核方案更值得做
PCF8563是I2C接口的实时时钟(RTC)芯片,很多FPGA板卡上都有它的位置,但真正用Verilog把I2C时序写明白的人并不多。原因是这个驱动卡在中间层:说简单,SCL和SDA两根线,读写时序就几行状态机;说复杂,开漏输出、上拉电阻、应答位、重复起始条件,任何一环错了,总线上就是一片FF或者死等ACK。与其在FPGA里嵌一个软核处理器去跑I2C驱动,不如直接用状态机实现,占用资源少、时序完全可控,还能把PCF8563的掉电走时、闹钟、CLKOUT功能都用起来。这篇笔记适合正在写PCF8563驱动、或者已经写完但上板不工作的FPGA工程师。
2. PCF8563寄存器与I2C时序细节:先看懂接口,再谈Verilog
2.1 PCF8563的寄存器映射:时间存在0x02~0x08,控制位在0x00和0x01
PCF8563是全CMOS的低功耗RTC,供电范围1.8V到5.5V,和3.3V的FPGA I/O直接连接基本没有电平问题。它内部有32.768kHz晶体振荡器,产生秒、分、时、日、月、年、星期这些时间寄存器,同时还有一个关键的VL标志位(0x02的最高位),用来指示电源电压是否曾经掉到过RTC停止工作的程度。
先看寄存器表,这是后面所有读写代码的地图。
| 寄存器地址 | 名称 | bit7 | bit6:4 | bit3:0 | 取值范围 |
|---|---|---|---|---|---|
| 0x00 | 控制/状态1 | 定时器中断标志 | STOP位、TEST位 | - | - |
| 0x01 | 控制/状态2 | 报警/定时器标志 | - | - | - |
| 0x02 | 秒 | VL | 秒十位(BCD) | 秒个位(BCD) | 00~59 |
| 0x03 | 分 | - | 分十位(BCD) | 分个位(BCD) | 00~59 |
| 0x04 | 时 | - | 时十位(BCD) | 时个位(BCD) | 00~23 |
| 0x05 | 日 | - | 日十位(BCD) | 日个位(BCD) | 01~31 |
| 0x06 | 星期 | - | 星期值 | - | 00~06 |
| 0x07 | 月 | C(世纪位) | 月十位(BCD) | 月个位(BCD) | 01~12 |
| 0x08 | 年 | - | 年十位(BCD) | 年个位(BCD) | 00~99 |
注意0x02到0x08的数据全部是BCD码。比如秒是45,读出来就是0x45,bit7的VL默认是0,所以寄存器值就是0x45。如果VL是1,寄存器值变成0xC5,这是上电后最容易见到的异常值。
0x00控制寄存器是驱动初始化第一个要写的寄存器:bit5是STOP位,写1时振荡器停振,整个RTC不走时;bit3是TEST1测试位,正常工作时必须保持0。很多开发板的例程里根本不初始化0x00,结果也能工作,那是因为芯片上电默认STOP=0。但如果你想控制时钟启停,比如校时的瞬间让秒清零,就得操作这一位。
0x11是CLKOUT输出寄存器,bit7是CLKOUT使能,写0使能输出,写1禁用。它有两个作用,一是输出32.768kHz方波,用示波器量一下就知道芯片有没有在工作;二是如果不需要它,一定要写0x80禁用,否则额外耗电。
实时时钟的硬件连接也值得说一句。PCF8563的SCL和SDA是开漏输出,FPGA这侧的IO如果设置成普通推挽输出,两边的电平就会打架。正确做法是FPGA IO配置成三态,外部接4.7k上拉电阻到VCC,这也是I2C总线最常见的硬件连接方式。上拉电阻选型时看两个指标:阻值太小,低电平时灌电流偏大,信号边沿变缓;阻值太大,上升沿的RC常数变大,100kHz下就可能出现波形头尾圆钝。
提示:如果你的板卡是5V供电的系统,PCF8563的SCL/SDA不能直接用5V上拉,会让3.3V的FPGA IO承受反向电流。确认PCF8563的VDD供电电压和FPGA IO bank电压一致,再统一配一个上拉电源。
2.2 100kHz下I2C时序图拆解:时钟参数怎么算
I2C通信协议里没有片选线,从机靠什么知道主机开始通信了?靠SDA和SCL的组合跳变:SCL保持高电平时,SDA产生一个高到低的跳变,这是起始条件;SCL保持高电平时,SDA产生低到高的跳变,这是停止条件。
数据位在SCL高电平期间有效,SCL低电平期间允许SDA变化,这就是为什么状态机里要在SCL低的时候改变SDA、SCL高的时候锁存SDA。I2C时序图上最容易被忽略的是建立时间和保持时间,虽然Verilog状态机不容易违反保持时间,但建立时间不足会导致从机采样到错误数据。
标准模式100kHz下,几个必须满足的时间参数:
| 参数 | 含义 | 标准模式最小要求 | 50MHz / 100kHz取值 |
|---|---|---|---|
| tHD;STA | 起始条件保持时间 | 4.0us | 5us |
| tLOW | SCL低电平时间 | 4.7us | 5us |
| tHIGH | SCL高电平时间 | 4.0us | 5us |
| tSU;STA | 重复起始建立时间 | 4.7us | 5us |
| tBUF | 两次传输间总线空闲时间 | 4.7us | 10us |
| tSU;STO | 停止条件建立时间 | 4.0us | 5us |
50MHz系统时钟下,SCL一个周期想做到10us,就需要500个时钟周期,高电平250拍、低电平250拍。代码里常见的CLK_DIV = 250就是这么来的。如果你想用400kHz,半周期就是62拍,SCL高电平只有1.25us,这个值已经低于标准模式tHIGH的4us要求,所以400kHz下必须查PCF8563手册的高速模式参数,并且硬件上要适当减小上拉电阻。
上电初始化顺序也依赖时序理解。PCF8563上电后,0x00的默认值是STOP=0,理论上振荡器已经运行。但为了确保控制字干净,正规驱动里第一步仍然是写0x00为0x00,再写0x01为0x00,把两个控制状态寄存器的所有中断清掉。这一步做完,再去写时间寄存器,否则可能出现报警标志残留导致中断引脚一直有效。
2.3 读PCF8563时的伪写和重复起始条件:方向切换的关键
写操作流程相对直接:起始条件、从机地址加写位0xA2、寄存器地址、数据、停止条件。读操作就不一样了,因为I2C总线上同一时刻只能有一个方向。
PCF8563的读时序是:第一步,主机发起始条件,然后发0xA2(器件地址0x51加写位0),这个看似是写操作,实际是告诉从机“我要访问哪个寄存器”。第二步,主机发要读的寄存器地址,比如0x02。第三步,主机再发一个重复起始条件,然后发0xA3(器件地址加读位1),这时从机才把数据放到SDA上。第四步,主机读走一个字节,在应答位时回一个NACK,然后发停止条件。
很多翻车案例就是漏了第三步的重复起始。如果只发一次起始,写完寄存器地址直接读,从机根本不会响应,SDA一直保持高电平,读回来的数据就是0xFF。这一点在Verilog状态机设计里要单独分配一个状态,别和图省事把读序列和写序列混在一起。
另外,PCF8563的连续读有一个内部锁存行为:当0x02秒寄存器被读取时,芯片会把当前时间锁存到缓冲区,直到0x08年寄存器被读完才释放。这是为了消除读取过程中发生秒进位导致的数据漂移,所以一次完整的时间读取,常规做法是连续读0x02到0x08,凑齐一整组快照。
3. 用Verilog实现PCF8563的I2C驱动:状态机与关键代码
3.1 先从分频器开始:把50MHz变成100kHz的SCL节拍
I2C驱动里所有状态跳变都需要一个稳定的节拍。比较常见的做法是先产生一个scl_tick脉冲,脉冲宽度是系统时钟一个周期,周期是SCL时钟半周期。也就是说,每250个50MHz时钟周期,scl_tick高一个周期。状态机在scl_tick有效时前进,这样驱动代码就不用关心系统时钟是多少了。
// clk_div_100k.v module clk_div_100k( input wire clk, // 系统时钟 50MHz input wire rst_n, output reg scl_tick // 100kHz 半周期脉冲 ); reg [8:0] cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 9'd0; scl_tick <= 1'b0; end else if (cnt == 9'd249) begin cnt <= 9'd0; scl_tick <= 1'b1; // 触发一次状态机前进 end else begin cnt <= cnt + 1'b1; scl_tick <= 1'b0; end end endmodule参数说明:249这个计数上限由50MHz除以100kHz再除以2得到,500/2-1=249。如果系统时钟是25MHz,这里就是124;如果板子的PCF8563上拉电阻用的10k,上升沿变缓,可以保持100kHz不变,这个参数就够用。
为什么要用脉冲而不是直接翻转SCL?因为SCL的翻转要受控于读写状态机:起始条件要求SCL高时SDA拉低,SCL翻转必须和SDA跳变严格错开半拍,如果SCL独立翻转,状态机的相位就很难对齐。用scl_tick驱动,状态机每次只在半周期边界动作,波形对齐就简单了。
3.2 写操作状态机:从起始条件到停止条件的一次完整跳变
I2C写一条寄存器数据的序列是:起始、发送7位地址加写位(0xA2)、等待ACK、发送寄存器地址、等待ACK、发送数据、等待ACK、停止。状态机里把这八步拆成IDLE、START、SHIFT、ACK、STOP五个状态,核心代码如下。
// i2c_write_seq.v,写状态机核心 localparam IDLE = 3'd0; localparam START = 3'd1; localparam SHIFT = 3'd2; // 逐位移出tx_shift localparam ACK = 3'd3; // 释放SDA,采样从机应答 localparam STOP = 3'd4; reg [3:0] bit_cnt; reg [7:0] tx_shift; reg [1:0] byte_cnt; // 0:器件地址 1:寄存器地址 2:写数据 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; end else if (scl_tick) begin case (state) IDLE: begin if (wr_start) begin byte_cnt <= 2'd0; state <= START; end end START: begin sda_out <= 1'b0; // SCL高时拉低SDA,起始条件 tx_shift <= {7'b1010001, 1'b0}; // 0xA2:PCF8563地址+写位 bit_cnt <= 4'd8; state <= SHIFT; end SHIFT: begin sda_out <= tx_shift[7]; // 先移出最高位 tx_shift <= {tx_shift[6:0], 1'b0}; bit_cnt <= bit_cnt - 1'b1; if (bit_cnt == 4'd1) state <= ACK; // 8位移完进入应答 end ACK: begin // 释放SDA,由从机拉低应答;主机不驱动sda_out if (byte_cnt == 2'd0) begin tx_shift <= reg_addr; byte_cnt <= 2'd1; bit_cnt <= 4'd8; state <= SHIFT; end else if (byte_cnt == 2'd1) begin tx_shift <= wdata; byte_cnt <= 2'd2; bit_cnt <= 4'd8; state <= SHIFT; end else begin state <= STOP; end end STOP: begin sda_out <= 1'b1; // SCL先高,SDA再拉高 state <= IDLE; done <= 1'b1; end endcase end end逻辑说明:START状态在SCL保持高电平时把SDA拉低,这个动作就是I2C时序图里的起始条件。SHIFT状态每个scl_tick移出一位,但要注意SDA必须在SCL低电平时变化,所以SHIFT要放在SCL低半周期里执行,ACK要放在SCL高半周期采样。上面的代码为了展示跳转逻辑,省略了SCL相位对齐,实际工程里要用case (scl_phase)区分高低半周期,或者把SCL翻转也纳入状态机控制。
参数说明:dev_addr是7位地址0x51,左移一位补写位0,得到0xA2;byte_cnt用来区分当前传输的是地址、寄存器还是数据,三段传输共用一个SHIFT和ACK状态,代码量比拆成独立状态小得多。done信号在STOP状态拉高一拍,供上层模块产生写完成中断。
3.3 读操作状态机:伪写后重复起始,再读一个字节
读时序比写时序多了一个重复起始条件,状态机里需要多两个状态:RESTART和READ。整体序列是:起始、0xA2、寄存器地址、重复起始、0xA3、读一个字节、NACK、停止。
// i2c_read_seq.v,读状态机追加状态 localparam RSTART = 3'd5; // 重复起始 localparam READ = 3'd6; // 读取一个字节 localparam NACK = 3'd7; // 主机回非应答 // 在ACK状态里,byte_cnt == 2'd1时进入RSTART RSTART: begin sda_out <= 1'b1; // SDA先释放 // 重复起始:SCL高电平期间SDA拉低 tx_shift <= {7'b1010001, 1'b1}; // 0xA3:地址+读位 bit_cnt <= 4'd8; state <= READ; end READ: begin // 每拍采样SDA,存入rx_shift rx_shift <= {rx_shift[6:0], sda_in}; bit_cnt <= bit_cnt - 1'b1; if (bit_cnt == 4'd1) state <= NACK; end NACK: begin sda_out <= 1'b1; // 第9拍不拉低,告诉从机读完了 state <= STOP; end逻辑说明:RSTART最关键,它发生在SCL高电平期间,SDA产生一个高到低的跳变,这个跳变相当于一次新的起始条件,从机收到后重新解析后面的地址。注意重复起始和停止条件的区别:停止条件是SDA低到高跳变,重复起始是高到低跳变,跳变方向相反。
READ状态里SDA方向要从输出切到输入,代码里用sda_en信号控制三态:输出模式下sda_out有效,输入模式下sda_out置高阻、采样sda_in。ACK/NACK的判断也要放在SCL高电平中间采样,如果采样点偏到边沿,从机的应答信号还没稳定,读回来的ACK就是错的。这算是I2C时序图上最容易踩的软坑。
如果项目里既要读秒分时,又要写闹钟寄存器,建议把读写状态机集中在同一个模块里,用byte_cnt区分当前传输的是控制字段、寄存器地址、写数据还是读数据。分开写两个状态机,反而会因为总线的互斥问题引入不少麻烦。
4. 在Quartus中搭建PCF8563工程:从新建工程到SignalTap抓波形
4.1 新建工程与文件添加:Lite版本已经够用
Quartus Prime Lite是免费版本,对于PCF8563这种小规模驱动完全够用。新建工程的一般流程是:File -> New Project Wizard,填工程名,选择目标FPGA器件(比如Cyclone IV E系列EP4CE6F17C8,或者Cyclone 10 LP),然后添加之前写好的clk_div_100k.v、i2c_write_seq.v、i2c_read_seq.v和顶层模块pcf8563_top.v。
添加文件后建议第一时间做一次Analysis & Synthesis,确认语法和模块例化没有问题。常见错误是模块名和文件名不一致、端口列表漏了inout类型、或者把inout信号在顶层错误地连线。inout在顶层仿真里还有一处坑:testbench里如果同时给sda赋值又去监控sda,仿真器会报多驱动冲突,试验阶段可以用wire型外加pullup辅助上拉来缓解。
Quartus工程里还需要确认综合设置里没有把三态信号优化掉。有些旧版本工程会把未使用的inout引脚默认接GND,导致SDA永远为低。检查方法是在Assignment Editor里看i2c_sda的Mode是不是Bidir,如果综合后变成Output,就要回到顶层代码检查sda_en逻辑。
4.2 引脚分配与RTC硬件连接:FPGA侧要配成三态
编译通过后下一步是分配引脚。PCF8563的SCL和SDA是开漏双向信号,FPGA侧IO要设置成三态,Quartus里默认IO标准是LVTTL,还需要根据板卡的bank电压做匹配。分配引脚可以用Quartus的Pin Planner图形界面操作,也可以用TCL脚本直接跑:
# 引脚分配脚本(根据实际板卡修改引脚号) set_location_assignment PIN_E15 -to i2c_scl set_location_assignment PIN_E16 -to i2c_sda set_location_assignment PIN_C1 -to clk_50m set_location_assignment PIN_B1 -to rst_n # IO标准:与PCF8563供电电压一致,常见3.3V set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to i2c_scl set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to i2c_sda引脚分配说明:i2c_scl和i2c_sda在FPGA内部是三态输出,外部必须接上拉电阻到3.3V,阻值4.7k。如果开发板已经把PCF8563模块集成好了,通常板卡上已经有上拉电阻,只需要确认是4.7k还是10k。10k在100kHz下也能工作,但边沿会稍缓;板卡电源如果是3.3V,10k的静态电流只有0.33mA,完全正常。
另外一个容易忽略的设置:在Quartus的Device and Pin Options里,如果给SDA和SCL引脚勾选了“Enable weak pull-up on input pins”,会和外部4.7k上拉并联,导致总线低电平被抬高,PCF8563无法正确识别低电平。这个坑的排查成本很高,信号看起来也有波形,但就是通信不稳定。常规做法是保持默认的off,外部上拉已经足够。
4.3 SignalTap抓取I2C波形:没有逻辑分析仪时的调试方案
SignalTap II Logic Analyzer是Quartus自带的在线逻辑分析仪,可以把FPGA内部的信号实时抓出来看。对于I2C这种低速信号,完全可以用SignalTap代替传统逻辑分析仪。
使用步骤:先编译工程,然后在Quartus里添加SignalTap文件,选择采样时钟(用50MHz系统时钟即可),添加要观察的信号:scl、sda、state、bit_cnt、tx_shift、rx_shift、done。触发条件设置成posedge done,这样每次读写完成时抓一拍。然后重新编译,用USB-Blaster下载到板卡,在SignalTap窗口里就能看到完整的I2C时序。
实际的波形检查顺序:先看有没有START条件,再看SDA上是否出现0xA2,然后看ACK位是否被从机拉低,再往后看寄存器地址和数据。如果卡在ACK状态,说明从机没响应,优先检查地址和上拉电阻。SignalTap抓SDA时要注意,SDA是inout信号,SignalTap抓的是FPGA内部的三态输入路径,如果SDA方向控制把内部输入路径切断了,需要单独设计一个sda_loopback信号,把pad上的SDA复制一份给SignalTap。
4.4 板级验证流程:先把时间写进去,再读回来比对
驱动上板之后不要直接去看秒有没有在走,先用一个最简单、最确定的功能验证:往0x11 CLKOUT寄存器写0x00,然后读回来,应该读到0x00。这个验证的好处是,CLKOUT寄存器不会因为时间变化产生随机干扰,读写正确就是I2C通路正确。
下一步是写时间。常见做法是将时间寄存器全部清零:0x02写0x00、0x03写0x00、0x04写0x12,然后用SignalTap观察0x02秒寄存器是否在走。PCF8563秒寄存器只有一秒跳变一次,如果用的是SignalTap的手动触发,最少要等两秒再抓第二次,才能确认它真的在走。
还可以写0x00寄存器把STOP位置1,观察时钟是否停住,再写0恢复走时。这样就把“时钟走时受控制”这个功能也验证了。这一步做完,驱动才算真正可用。RTC硬件连接上如果还有独立备份电池,还要确认VBAT引脚电压,电池没接或者电压过低,主电源掉电后时间就会归零。
5. PCF8563驱动避坑:5个常见故障现象、原因与解决方法
5.1 SDA一直为低,总线被牢牢攥住
现象:逻辑分析仪看SDA,一直是低电平,SCL却有波形。用万用表量SDA对地,阻值很小。换成只读不写,现象也一样,整个总线上看不到一个正常的高电平。
原因:I2C是开漏输出,SDA的高电平完全依赖上拉电阻。如果PCF8563模块上的上拉电阻焊错成0欧电阻,或者FPGA侧IO配置成了推挽输出并且输出低,总线就会被一直拉低。还有一种情况是SDA走线对地短路,这种情况在手工焊接的转接板上比较多见。
解决:先断开FPGA侧的SDA连接,单独量模块上SDA的静态电平,应当为高。再检查FPGA的IO配置,确认sda_en信号在空闲状态是0,也就是三态释放。很多翻车现场其实是FPGA内部逻辑把SDA锁死在输出低,不是硬件问题。逐个排查比盲目改代码有效。
5.2 读回的数据全是0xFF
现象:读0x02返回0xFF,读0x03也是0xFF,多次读结果不变。SCL和SDA用SignalTap看都有波形,地址字节也能看到,但从机好像一直没反应。
原因:从机没有把SDA拉低。要么是器件地址不对,PCF8563的7位地址是0x51,加上写位是0xA2,加上读位是0xA3;要么是在读时序里漏了重复起始条件,从机还在等寄存器地址之后的下一个命令,主机却开始采样数据。
解决:用SignalTap抓一次完整读序列,检查0xA3是否真的出现在SDA上。如果地址和重复起始都对,再检查ACK采样点,确认是在SCL高电平中间采样而不是上升沿采样。还有一个容易踩的细节,很多开发板原理图把PCF8563的A0地址脚接成1,地址就不是0x51了,要对照原理图看。
5.3 时间能读出来,但秒寄存器一直是0,不走时
现象:读0x02一直是0x00,隔一分钟再读还是0x00,分和时也不变。写时间寄存器能写进去,写完后读回来是正确的,但就是不继续走。
原因:PCF8563振荡器停了。最常发生在两类情况:一是0x00控制寄存器的STOP位被置1,二是32.768kHz晶振虚焊或负载电容不匹配。STOP位如果在上电后被某个初始化序列误写为1,时钟就会完全停住。
解决:初始化时明确写0x00寄存器为0x00,把STOP位清零。如果是晶振问题,用示波器量PCF8563的CLKOUT引脚,配置0x11寄存器为0x00使能CLKOUT输出,看有没有32.768kHz方波。没有方波就看晶振的两个引脚,用示波器探头直接量晶振脚很容易停振,这一步要用10x探头加低输入电容,最好通过CLKOUT引脚间接判断。
5.4 仿真波形完全正确,上板后I2C没有任何反应
现象:ModelSim或QuestaSim下读写仿真都正常,时序图完美,烧到板卡上SDA和SCL都没有波形,或者只有一个方向的脉冲。
原因:仿真里没有建模开漏和上拉。Verilog的inout信号在仿真里如果两端都驱动,仿真器会报冲突,新手习惯把SDA直接assign成输出,这在仿真里看起来没问题,但综合后SDA的方向逻辑和被实测的pad行为不一致。另一个常见原因是FPGA引脚没有分配、或者分配了但编译后的.sof没重新生成。
解决:仿真testbench里给SDA加上拉建模,用pullup(sda)语句模拟真实总线。上板前在SignalTap里抓sda和scl,确认信号确实翻转到物理引脚上。Quartus编译完成后还要确认Programmer对话框里加载的是重新编译后的.sof,有时候改了引脚分配,但烧录文件还是旧版本,就会白折腾。
5.5 连续读时间时,分钟和秒对不上
现象:读秒寄存器时看到0x12,再读分寄存器看到0x58,但实际上秒刚进位,分应该已经变成0x59。数据看起来是同一时刻的快照,但内部控制状态却混乱。
原因:PCF8563在读取0x02时会把时间寄存器锁存到内部缓冲区,直到0x08年寄存器被读取才释放。如果驱动只读0x02和0x03,不去读0x08,锁存就一直生效,读回来的数据永远是同一时刻的旧值,下一次开始读又触发新的锁存,造成秒和分可能来自两次不同锁存周期。
解决:一次完整读取必须覆盖0x02到0x08,即使你只需要秒分时,也要把日、月、年读一遍,把锁存释放掉。读回来的数据再用一个寄存器组缓存,上层逻辑从缓存里取。这个坑不查手册很难想到,是PCF8563特有的内部锁存行为,换成DS1302就不会遇到。
6. 把PCF8563驱动参数化:适配不同系统时钟与I2C速率
如果这个驱动只在某一块板子上用,CLK_DIV写死250没有问题。换一块25MHz的板子就要改代码,这不是一个可维护的驱动。常见做法是用parameter把系统时钟和I2C速率传进模块,顶层只改两个数字:
module pcf8563_driver #( parameter CLK_HZ = 50_000_000, // 板级系统时钟 parameter SCL_HZ = 100_000 // I2C总线速率 )( input wire clk, input wire rst_n, inout wire i2c_scl, inout wire i2c_sda, input wire wr_req, input wire rd_req, input wire [7:0] reg_addr, input wire [7:0] wdata, output reg [7:0] rdata, output reg rdata_valid ); localparam DIV_CNT = CLK_HZ / SCL_HZ / 2 - 1;顶层例化时写#(.CLK_HZ(27_000_000), .SCL_HZ(100_000))就能直接换平台。SCL_HZ不建议超过400kHz,否则tHIGH不满足标准模式要求。
我自己的习惯是驱动里不直接做BCD转二进制,而是输出原始寄存器值,由上层模块决定怎么显示。这样驱动只负责I2C协议,上层负责业务逻辑,调试时也容易隔离问题。验证方法上用五分钟做个回环测试:任意写一个寄存器地址,写一串固定数据,再读回来比对。连续读写十个地址都一致,再去测真实时间寄存器。
PCF8563的驱动写完之后,最值得做的事是把它和上层的时间处理逻辑分开。驱动只管“往0x02写0x00”,不管这个0x00代表几点几分。这样后续无论是做数码管显示、串口上传、还是网络校时,都不需要再动I2C部分。希望这篇笔记能帮你把PCF8563这块一次跑通。
本文还有配套的精品资源,点击获取