ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FPGA实现PCF8563的I2C驱动:寄存器级Verilog状态机详解

FPGA实现PCF8563的I2C驱动:寄存器级Verilog状态机详解 简介此压缩包是一套面向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时序细节先看懂接口再谈Verilog2.1 PCF8563的寄存器映射时间存在0x02~0x08控制位在0x00和0x01PCF8563是全CMOS的低功耗RTC供电范围1.8V到5.5V和3.3V的FPGA I/O直接连接基本没有电平问题。它内部有32.768kHz晶体振荡器产生秒、分、时、日、月、年、星期这些时间寄存器同时还有一个关键的VL标志位0x02的最高位用来指示电源电压是否曾经掉到过RTC停止工作的程度。先看寄存器表这是后面所有读写代码的地图。寄存器地址名称bit7bit6:4bit3:0取值范围0x00控制/状态1定时器中断标志STOP位、TEST位--0x01控制/状态2报警/定时器标志---0x02秒VL秒十位BCD秒个位BCD00~590x03分-分十位BCD分个位BCD00~590x04时-时十位BCD时个位BCD00~230x05日-日十位BCD日个位BCD01~310x06星期-星期值-00~060x07月C世纪位月十位BCD月个位BCD01~120x08年-年十位BCD年个位BCD00~99注意0x02到0x08的数据全部是BCD码。比如秒是45读出来就是0x45bit7的VL默认是0所以寄存器值就是0x45。如果VL是1寄存器值变成0xC5这是上电后最容易见到的异常值。0x00控制寄存器是驱动初始化第一个要写的寄存器bit5是STOP位写1时振荡器停振整个RTC不走时bit3是TEST1测试位正常工作时必须保持0。很多开发板的例程里根本不初始化0x00结果也能工作那是因为芯片上电默认STOP0。但如果你想控制时钟启停比如校时的瞬间让秒清零就得操作这一位。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.0us5ustLOWSCL低电平时间4.7us5ustHIGHSCL高电平时间4.0us5ustSU;STA重复起始建立时间4.7us5ustBUF两次传输间总线空闲时间4.7us10ustSU;STO停止条件建立时间4.0us5us50MHz系统时钟下SCL一个周期想做到10us就需要500个时钟周期高电平250拍、低电平250拍。代码里常见的CLK_DIV 250就是这么来的。如果你想用400kHz半周期就是62拍SCL高电平只有1.25us这个值已经低于标准模式tHIGH的4us要求所以400kHz下必须查PCF8563手册的高速模式参数并且硬件上要适当减小上拉电阻。上电初始化顺序也依赖时序理解。PCF8563上电后0x00的默认值是STOP0理论上振荡器已经运行。但为了确保控制字干净正规驱动里第一步仍然是写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 9d0; scl_tick 1b0; end else if (cnt 9d249) begin cnt 9d0; scl_tick 1b1; // 触发一次状态机前进 end else begin cnt cnt 1b1; scl_tick 1b0; end end endmodule参数说明249这个计数上限由50MHz除以100kHz再除以2得到500/2-1249。如果系统时钟是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 3d0; localparam START 3d1; localparam SHIFT 3d2; // 逐位移出tx_shift localparam ACK 3d3; // 释放SDA采样从机应答 localparam STOP 3d4; 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 2d0; state START; end end START: begin sda_out 1b0; // SCL高时拉低SDA起始条件 tx_shift {7b1010001, 1b0}; // 0xA2PCF8563地址写位 bit_cnt 4d8; state SHIFT; end SHIFT: begin sda_out tx_shift[7]; // 先移出最高位 tx_shift {tx_shift[6:0], 1b0}; bit_cnt bit_cnt - 1b1; if (bit_cnt 4d1) state ACK; // 8位移完进入应答 end ACK: begin // 释放SDA由从机拉低应答主机不驱动sda_out if (byte_cnt 2d0) begin tx_shift reg_addr; byte_cnt 2d1; bit_cnt 4d8; state SHIFT; end else if (byte_cnt 2d1) begin tx_shift wdata; byte_cnt 2d2; bit_cnt 4d8; state SHIFT; end else begin state STOP; end end STOP: begin sda_out 1b1; // SCL先高SDA再拉高 state IDLE; done 1b1; 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得到0xA2byte_cnt用来区分当前传输的是地址、寄存器还是数据三段传输共用一个SHIFT和ACK状态代码量比拆成独立状态小得多。done信号在STOP状态拉高一拍供上层模块产生写完成中断。3.3 读操作状态机伪写后重复起始再读一个字节读时序比写时序多了一个重复起始条件状态机里需要多两个状态RESTART和READ。整体序列是起始、0xA2、寄存器地址、重复起始、0xA3、读一个字节、NACK、停止。// i2c_read_seq.v读状态机追加状态 localparam RSTART 3d5; // 重复起始 localparam READ 3d6; // 读取一个字节 localparam NACK 3d7; // 主机回非应答 // 在ACK状态里byte_cnt 2d1时进入RSTART RSTART: begin sda_out 1b1; // SDA先释放 // 重复起始SCL高电平期间SDA拉低 tx_shift {7b1010001, 1b1}; // 0xA3地址读位 bit_cnt 4d8; state READ; end READ: begin // 每拍采样SDA存入rx_shift rx_shift {rx_shift[6:0], sda_in}; bit_cnt bit_cnt - 1b1; if (bit_cnt 4d1) state NACK; end NACK: begin sda_out 1b1; // 第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.3V10k的静态电流只有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这块一次跑通。本文还有配套的精品资源点击获取
返回列表