本实验主要用于演示FPGA读取RTC芯片PCF8563的时间信息。
1.实验说明
1.1引脚说明
set_pin_assignment	{ sys_clk }	{ LOCATION = P11; IOSTANDARD = LVCMOS33; }
set_pin_assignment	{ scl_rtc }	{ LOCATION = P4; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment	{ sda_rtc }	{ LOCATION = P10; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP;  }
set_pin_assignment	{ tm_scl }	{ LOCATION = P13; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP;  }
set_pin_assignment	{ tm_sda }	{ LOCATION = P16; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP;  }
- scl_rtc,RTC模块的IIC时钟脚
 - sda_rtc,RTC模块的IIC数据脚
 - tm_scl,数码管的时钟脚
 - tm_sda,数码管的数据脚
 
注意:RTC模块的COT和INT信号我们不需要使用,故可以不接。

1.2实验现象
连好两个模块后,下载程序就可以看到数码管上在显示时间,同时 :在跳跃。
2. 实验原理
FPGA通过IIC读取PCF8563的时间信息,然后将时间信息投送给数码管模块进行显示。

PCF8536的资料只需要关心其规格书。
2.1. 简介
PCF8563 是一款低功耗的 CMOS1实时时钟/日历芯片,支持可编程时钟输出、中断输出 和低压检测。所有地址和数据通过双线双向 I2C 总线串联传输,最高速率: 400 kbps。每 次读写数据字节后,寄存器地址自动累加。
2.2特性和优势
◼ 基于 32.768kHz 的晶振,提供年、月、日、星期、时、分和秒计时
◼ Century flag
◼ 时钟工作电压: 1.0 – 5.5 V(室温)
◼ 低备用电流;典型值为 0.25 μA(VDD = 3.0 V, Tamb =25 °C)
◼ 400 kHz 双线 I2C 总线接口(VDD = 1.8 – 5.5 V)
◼ 可编程时钟输出(32.768 kHz、 1.024 kHz、 32 Hz 和 1Hz)
◼ 报警和定时器功能
◼ 集成晶振电容器
◼ 内部上电复位(POR)
◼ I2C 总线从机地址:读: A3h;写: A2h
◼ 开漏中断管脚
2.3 引脚说明

| 符号 | 引脚 | 说明 | |
| SO8、 TSSOP8 | HVSON10 | ||
| OSCI | 1 | 1 | 振荡器输入 | 
| OSCO | 2 | 2 | 振荡器输出 | 
| INT | 3 | 4 | 中断输出(开漏;有效 LOW) | 
| VSS | 4 | 5[1] | 接地 | 
| SDA | 5 | 6 | 串行数据输入和输出 | 
| SCL | 6 | 7 | 串行时钟输入 | 
| CLKOUT | 7 | 8 | 时钟输出、开漏 | 
| VDD | 8 | 9 | 电源电压 | 
| n.c. | – | 3, 10 | 未连接;不连接,也不馈通使用 | 
寄存器结构




更多寄存器说明请参见手册,我们主要关心与时间相关的一些寄存器,其它功能未使用。
设置和读取时间流程

读/写操作期间,计时电路(内存位置 02h 至 08h)会被封锁。
这可以防止
 在传送条件下,对时钟和日历芯片的错读。
 在一个读周期内,增加时间寄存器
这次读/写访问完成后,时间电路再次开放,且在读访问期间发生的任何被挂起的增加时
间寄存器的请求会得到处理。最多可以存储 1 个请求;因此,必须在 1 秒之内完成所有访
问

使用这种方法之后,必须一次完成读或写访问,也就是说,在一次访问期间,完成秒到年
的设置或读取,这非常重要。不按此方法行事可能导致时间损坏。
例如,如果在一次访问期间设置时间(秒到年),然后在第二次访问期间设置日期,那么
在两次访问期间,时间可能增加。实施读操作时,存在类似的问题。在几次读操作之间可
能会发生转存,从而在一个时刻给出分钟数,在下一个时刻给出小时数。
推荐的读取时间的方法:
1. 发送一个 START 条件和用于写入的从机地址(A2h)。
2. 通过发送 02h,将地址指针设置为 2(VL_seconds)。
3. 发送一个 RESTART 条件,或在 START 之后发送 STOP。
4. 发送用于读取的从机地址(A3h)。
5. 读取 VL_seconds(寄存器)。
6. 读取 Minutes(寄存器)。
7. 读取 Hours(寄存器)。
8. 读取 Days(寄存器)。
9. 读取 Weekdays(寄存器)。
10. 读取 Century_months(寄存器)。
11. 读取 Years(寄存器)。
12. 发送一个 STOP 条件。
3.代码分析
工程层次图如下,

我们主要关注u_seg_deci_top模块及它的子模块。
- u_seg_deci_top,可以看到u_seg_deci_top我们是从数码管模块工程中抠出来的,但是对模块作了改造,其中云掉了BCD模块,因为RTC出来的数字本来就是每一位分开的,所以不需要BCD模块了。
 - u_decimal模块也是之前数码管工程中的。
 - 
pcf8563_ctrl, 控制PCF8563的数据读取。
 - 
i2c_ctrl, IIC Master模块
 
因为其它模块之前已用过,下面我们仅对子模块pcf8563_ctrl,i2c_ctrl进行说明。
I2C 总线的特性
I2C 总线用于在不同 IC 或模块之间进行双向、双线通信。这两条线路分别是串行数据线
(SDA)和串行时钟线(SCL)。这两条线路必须通过一个上拉电阻连接到正电源。只有在总
线空闲时才可以启动数据传输。
位传输
在每个时钟脉冲期间传输一个数据位。 SDA 线路上的数据在时钟脉冲的高电平周期内必
须保持稳定,因为此时数据线如果变化将被解析为一个控制信号

 START 和 STOP 条件
当总线不繁忙时,数据线和时钟线都保持高电平。
当时钟处于高电平时,数据线路从高电平到低电平的转换会被定义为 START 条件——S。
当时钟处于高电平时,数据线路从低电平到高电平的转换会被定义为 STOP 条件——P

系统配置
生成消息的设备是发送器;接收消息的设备是接收器。控制消息的设备是主设备;受主设
备控制的设备是从设备

确认(ACK)
从发送器到接收器,在 START 和 STOP 条件之间传输的数据的字节数是无限的。每个 8
位字节后面都有一个确认周期。
 被寻址的从接收器在接收每个字节后,必须生成确认。
 此外,主接收器必须在接收到从从发送器发出的每个字节后生成一个确认。
 确认设备必须在确认时钟脉冲期间下拉 SDA 线,以便 SDA 线在确认相关时钟脉冲
的高电平周期内稳定保持低电平(必须考虑设置和保持时间)。
 主接收器通过不对从机时钟输出的最后一个字节生成确认,向发送器发送数据结束
的信号。在这种情况下,发送器必须让数据线路保持高电平,以使主机生成 STOP 条
件。
有关 I2C 总线上的确认,请参见

定址
在 I2C 总线上传输任何数据之前,应首先对响应的设备进行寻址。寻址始终在启动程序后
发送第一个字节时进行。
PCF8563 用作从机接收器或从机发送器。因此时钟信号 SCL 只是一个输入信号,而数据
信号 SDA 则是一条双向线路。
为 PCF8563 预留了两个从机地址:
读: A3h (10100011)
写: A2h (10100010)
关于 PCF8563 从机地址的描述,请参见

时钟和日历的读写周期
寄存器地址是一个 4 位的值,它定义接下来要访问哪个寄存器。寄存器地址的前四位不使
用。

| 主机发送至从机接收器(写模式) | 

| 主机在设置寄存器地址后读取(写寄存器地址;读数据) | 

| 主机在首个字节后立即读取从机(读模式) | 
pcf8563_ctrl
模块接口
module  pcf8563_ctrl 
#(
    parameter   TIME_INIT = 48'h20_06_08_08_00_00
)
(
    input   wire            sys_clk     ,   //系统时钟,频率50MHz
    input   wire            i2c_clk     ,   //i2c驱动时钟
    input   wire            sys_rst_n   ,   //复位信号,低有效
    input   wire            i2c_end     ,   //i2c一次读/写操作完成
    input   wire    [7:0]   rd_data     ,   //输出i2c设备读取数据
    input   wire            key_flag    ,   //按键消抖后标志信号
    output  reg             wr_en       ,   //输出写使能信号
    output  reg             rd_en       ,   //输出读使能信号
    output  reg             i2c_start   ,   //输出i2c触发信号
    output  reg     [15:0]  byte_addr   ,   //输出i2c字节地址
    output  reg     [7:0]   wr_data     ,   //输出i2c设备数据
    output  reg     [23:0]  data_out        //输出到数码管显示的bcd码数据
    
);
代码注释已对每个管脚的作用做了一一说明。
localparam   S_WAIT         =   4'd1    ,   //上电等待状态
             INIT_SEC       =   4'd2    ,   //初始化秒
             INIT_MIN       =   4'd3    ,   //初始化分
             INIT_HOUR      =   4'd4    ,   //初始化小时
             INIT_DAY       =   4'd5    ,   //初始化日 
             INIT_MON       =   4'd6    ,   //初始化月 
             INIT_YEAR      =   4'd7    ,   //初始化年 
             RD_SEC         =   4'd8    ,   //读秒
             RD_MIN         =   4'd9    ,   //读分
             RD_HOUR        =   4'd10   ,   //读小时
             RD_DAY         =   4'd11   ,   //读日
             RD_MON         =   4'd12   ,   //读月
             RD_YEAR        =   4'd13   ;   //读年
定义状态机状态。接下来的代码先通过按键来判断是要读取时分秒还是显示年月日,
data_flag为0时显示时分秒,为1时显示年月
//产生数据切换的标志信号
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data_flag   <=  1'b0;
    else    if(key_flag ==  1'b1)
        data_flag   <=  ~data_flag;
    else
        data_flag   <=  data_flag;
//data_flag为0时显示时分秒,为1时显示年月日
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data_out    <=  24'd0;
    else    if(data_flag == 1'b0)
        data_out    <=  {hour,minute,second};
    else
        data_out    <=  {year,month,day};
cnt_wait延时等延计数器,只有在状态机跳转到一个新的状态时计数器归0,其余时候一直计数。
always@(posedge i2c_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt_wait    <=  13'd0;
    else    if((state==S_WAIT && cnt_wait==CNT_WAIT_8MS) || 
              (state==INIT_SEC && i2c_end==1'b1) || (state==INIT_MIN 
              && i2c_end==1'b1) || (state==INIT_HOUR && i2c_end==1'b1)
              || (state==INIT_DAY && i2c_end==1'b1) || (state==INIT_MON
              && i2c_end == 1'b1) || (state==INIT_YEAR && i2c_end==1'b1)
              || (state==RD_SEC && i2c_end==1'b1) || (state==RD_MIN && 
              i2c_end==1'b1) || (state==RD_HOUR && i2c_end==1'b1) || 
              (state==RD_DAY && i2c_end==1'b1) || (state==RD_MON && 
              i2c_end==1'b1) || (state==RD_YEAR && i2c_end==1'b1))
        cnt_wait    <=  13'd0;
    else
        cnt_wait    <=  cnt_wait + 1'b1;
状态机的跳转过程如下:
//状态机状态跳转
always@(posedge i2c_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        state   <=  S_WAIT;
    else    case(state)
    //上电等待8ms后跳转到系统配置状态
        S_WAIT:
            if(cnt_wait == CNT_WAIT_8MS)
                state       <=  INIT_SEC;
            else
                state       <=  S_WAIT;
    //初始化秒状态:初始化秒后(i2c_end == 1'b1),跳转到下一状态
        INIT_SEC:
            if(i2c_end == 1'b1)
                state       <=  INIT_MIN;
            else
                state       <=  INIT_SEC;
    //初始化分状态:初始化分后(i2c_end == 1'b1),跳转到下一状态
        INIT_MIN:
            if(i2c_end == 1'b1)
                state       <=  INIT_HOUR ;
            else
                state       <=  INIT_MIN       ;
    //初始化时状态:初始化时后(i2c_end == 1'b1),跳转到下一状态
        INIT_HOUR:
            if(i2c_end == 1'b1)
                state       <=  INIT_DAY;
            else
                state       <=  INIT_HOUR       ;
    //初始化日状态:初始化日后(i2c_end == 1'b1),跳转到下一状态
        INIT_DAY:
            if(i2c_end == 1'b1)
                state       <=  INIT_MON;
            else
                state       <=  INIT_DAY       ;
    //初始化月状态:初始化月后(i2c_end == 1'b1),跳转到下一状态
        INIT_MON:
            if(i2c_end == 1'b1)
                state       <=  INIT_YEAR;
            else
                state       <=  INIT_MON;
    //初始化年状态:初始化年后(i2c_end == 1'b1),跳转到下一状态
        INIT_YEAR:
            if(i2c_end == 1)
                state   <=  RD_SEC;
            else
                state       <=  INIT_YEAR;
    //读秒状态:读取完秒数据后,跳转到下一状态
        RD_SEC:
            if(i2c_end == 1'b1)
                state       <=  RD_MIN;
            else
                state       <=  RD_SEC;
    //读分状态:读取完分数据后,跳转到下一状态
        RD_MIN:
            if(i2c_end == 1'b1)
                state       <=  RD_HOUR;
            else
                state       <=  RD_MIN;
    //读时状态:读取完小时数据后,跳转到下一状态
        RD_HOUR:
            if(i2c_end == 1'b1)
                state       <=  RD_DAY;
            else
                state       <=  RD_HOUR;
    //读日状态:读取完日数据后,跳转到下一状态
        RD_DAY:
            if(i2c_end == 1'b1)
                state       <=  RD_MON;
            else
                state       <=  RD_DAY;
    //读月状态:读取完月数据后,跳转到下一状态
        RD_MON:
            if(i2c_end == 1'b1)
                state       <=  RD_YEAR;
            else
                state       <=  RD_MON;
    //读年状态:读取完年数据后,跳转回读秒状态开始下一轮数据读取
        RD_YEAR:
            if(i2c_end == 1'b1)
                state       <=  RD_SEC;
            else
                state       <=  RD_YEAR;
        default:
            state       <=  S_WAIT;
    endcase
先上电等待8ms后跳转到系统配置状态,然后分别初始化秒、分、时、日、月、年,初始化完成之后就开始分别读取时间和年月信息,是一个顺序流程。
接下来的代码就是根据状态机所处的状态给i2c_ctrl发送读写数据指令。以初始化秒为例:
wr_en <= 1'b1 ;
i2c_start <= 1'b1 ;
byte_addr <= 16'h02 ;
wr_data <= TIME_INIT[7:0];
在上一个延时完成后开始执行I2C写操作,其中byte_addr即为规格书中的 VL_seconds 寄存器 。

其它状态下在的代码类似,不再赘述。
i2c_ctrl
模块接口如下,模块中的注释详细说明了每个管脚的用途。
module  i2c_ctrl
#(
    parameter   DEVICE_ADDR     =   7'b1010_000     ,   //i2c设备地址
    parameter   SYS_CLK_FREQ    =   26'd50_000_000  ,   //输入系统时钟频率
    parameter   SCL_FREQ        =   18'd250_000         //i2c设备scl时钟频率
)
(
    input   wire            sys_clk     ,   //输入系统时钟,50MHz
    input   wire            sys_rst_n   ,   //输入复位信号,低电平有效
    input   wire            wr_en       ,   //输入写使能信号
    input   wire            rd_en       ,   //输入读使能信号
    input   wire            i2c_start   ,   //输入i2c触发信号
    input   wire            addr_num    ,   //输入i2c字节地址字节数
    input   wire    [15:0]  byte_addr   ,   //输入i2c字节地址
    input   wire    [7:0]   wr_data     ,   //输入i2c设备数据
    output  reg             i2c_clk     ,   //i2c驱动时钟
    output  reg             i2c_end     ,   //i2c一次读/写操作完成
    output  reg     [7:0]   rd_data     ,   //输出i2c设备读取数据
    output  reg             i2c_scl     ,   //输出至i2c设备的串行时钟信号scl
    inout   wire            i2c_sda         //输出至i2c设备的串行数据信号sda
    // output  i2c_sda_out,
    // input   i2c_sda_in,
    // output  i2c_sda_en
);
下面开始进行代码说明。
parameter   IDLE            =   4'd00,  //初始状态
            START_1         =   4'd01,  //开始状态1
            SEND_D_ADDR     =   4'd02,  //设备地址写入状态 + 控制写
            ACK_1           =   4'd03,  //应答状态1
            SEND_B_ADDR_H   =   4'd04,  //字节地址高八位写入状态
            ACK_2           =   4'd05,  //应答状态2
            SEND_B_ADDR_L   =   4'd06,  //字节地址低八位写入状态
            ACK_3           =   4'd07,  //应答状态3
            WR_DATA         =   4'd08,  //写数据状态
            ACK_4           =   4'd09,  //应答状态4
            START_2         =   4'd10,  //开始状态2
            SEND_RD_ADDR    =   4'd11,  //设备地址写入状态 + 控制读
            ACK_5           =   4'd12,  //应答状态5
            RD_DATA         =   4'd13,  //读数据状态
            N_ACK           =   4'd14,  //非应答状态
            STOP            =   4'd15;  //结束状态
上面的代码定义了IIC的操作状态机,注释中也详细说明了状态含义。
接下来我们看主代码。
cnt_clk:
系统时钟计数器,控制生成clk_i2c时钟信号,通过计数器对主时钟进行分频生产IIC的scl。
// cnt_clk:系统时钟计数器,控制生成clk_i2c时钟信号
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt_clk <=  8'd0;
    else    if(cnt_clk == CNT_CLK_MAX - 1'b1)
        cnt_clk <=  8'd0;
    else
        cnt_clk <=  cnt_clk + 1'b1;
// i2c_clk:i2c驱动时钟
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        i2c_clk <=  1'b1;
    else    if(cnt_clk == CNT_CLK_MAX - 1'b1)
        i2c_clk <=  ~i2c_clk;
cnt_i2c_clk_en:
cnt_i2c_clk计数器使能信号,控制对IIC时钟个数进行计数,以确定传输了多少个时钟和数据,
cnt_i2c_clk 用于对时钟SCL计数,cnt_bit用于对数据SDA进行计数。
// cnt_i2c_clk_en:cnt_i2c_clk计数器使能信号
always@(posedge i2c_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt_i2c_clk_en  <=  1'b0;
    else    if((state == STOP) && (cnt_bit == 3'd3) &&(cnt_i2c_clk == 3))
        cnt_i2c_clk_en  <=  1'b0;
    else    if(i2c_start == 1'b1)
        cnt_i2c_clk_en  <=  1'b1;
// cnt_i2c_clk:i2c_clk时钟计数器,控制生成cnt_bit信号
always@(posedge i2c_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt_i2c_clk <=  2'd0;
    else    if(cnt_i2c_clk_en == 1'b1)
        cnt_i2c_clk <=  cnt_i2c_clk + 1'b1;
// cnt_bit:sda比特计数器
always@(posedge i2c_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt_bit <=  3'd0;
    else    if((state == IDLE) || (state == START_1) || (state == START_2)
                || (state == ACK_1) || (state == ACK_2) || (state == ACK_3)
                || (state == ACK_4) || (state == ACK_5) || (state == N_ACK))
        cnt_bit <=  3'd0;
    else    if((cnt_bit == 3'd7) && (cnt_i2c_clk == 2'd3))
        cnt_bit <=  3'd0;
    else    if((cnt_i2c_clk == 2'd3) && (state != IDLE))
        cnt_bit <=  cnt_bit + 1'b1;
state:
状态机跳转,就是顺序执行读写操作,代码比较简单就不贴了。
ack:
ACK应答信号的产生,只在ACK_1,ACK_2,ACK_3,ACK_4,ACK_5状态的时候执行ACK应答,
always@(*)
    case    (state)
        IDLE,START_1,SEND_D_ADDR,SEND_B_ADDR_H,SEND_B_ADDR_L,
        WR_DATA,START_2,SEND_RD_ADDR,RD_DATA,N_ACK:
            ack <=  1'b1;
        ACK_1,ACK_2,ACK_3,ACK_4,ACK_5:
            if(cnt_i2c_clk == 2'd0)
                ack <=  sda_in;
            else
                ack <=  ack;
        default:    ack <=  1'b1;
    endcase
i2c_scl:
产生输出至i2c设备的串行时钟信号scl, 先依据状态判断要不发SCL然后依据cnt_i2c_clk计数器来确定SCL的电平。
always@(*)
    case    (state)
        IDLE:
            i2c_scl <=  1'b1;
        START_1:
            if(cnt_i2c_clk == 2'd3)
                i2c_scl <=  1'b0;
            else
                i2c_scl <=  1'b1;
        SEND_D_ADDR,ACK_1,SEND_B_ADDR_H,ACK_2,SEND_B_ADDR_L,
        ACK_3,WR_DATA,ACK_4,START_2,SEND_RD_ADDR,ACK_5,RD_DATA,N_ACK:
            if((cnt_i2c_clk == 2'd1) || (cnt_i2c_clk == 2'd2))
                i2c_scl <=  1'b1;
            else
                i2c_scl <=  1'b0;
        STOP:
            if((cnt_bit == 3'd0) &&(cnt_i2c_clk == 2'd0))
                i2c_scl <=  1'b0;
            else
                i2c_scl <=  1'b1;
        default:    i2c_scl <=  1'b1;
    endcase
i2c_sda_reg,rd_data_reg
i2c_sda_reg依据不同的状态来决定SDA上要发送的数据是寄存器地址还是寄存器数据,并判断该输出数据的哪一bit, 另外也依据状态将SDA上的数据存入rd_data_reg。
i2c_end
依条件产生一次STOP信号
always@(posedge i2c_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        i2c_end <=  1'b0;
    else    if((state == STOP) && (cnt_bit == 3'd3) &&(cnt_i2c_clk == 3))
        i2c_end <=  1'b1;
    else
        i2c_end <=  1'b0;
I2C SDA的三态处理
assign i2c_sda = (sda_en == 1'b1) ? i2c_sda_reg : 1'bz;
sda_en用于判定SDA当前是输出还是输入,sda_en == 1'b1则在SDA上输出i2c_sda_reg,否则读取SDA上的数据assign sda_in = i2c_sda;。
sda_en 只在下面这几个状态时才为0,即SDA只在下面这几个状态时用作输入管脚,其它时候SDA都是输出管脚。
- RD_DATA
 - ACK_1
 - ACK_2
 - ACK_3
 - ACK_4
 - ACK_5
 





没有回复内容