PotatoPie 4.0 实验教程(13) —— FPGA与PCF8563通讯实现数字时钟-Anlogic-安路社区-FPGA CPLD-ChipDebug

PotatoPie 4.0 实验教程(13) —— FPGA与PCF8563通讯实现数字时钟

本实验主要用于演示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信号我们不需要使用,故可以不接。

20231220191137533-image

1.2实验现象

连好两个模块后,下载程序就可以看到数码管上在显示时间,同时 :在跳跃。 

 

2. 实验原理

FPGA通过IIC读取PCF8563的时间信息,然后将时间信息投送给数码管模块进行显示。

20231220193831738-image

PCF8536的资料只需要关心其规格书。

2.1. 简介

PCF8563 是一款低功耗的 CMOS1实时时钟/日历芯片,支持可编程时钟输出、中断输出 和低压检测。所有地址和数据通过双线双向 I2C 总线串联传输,最高速率: 400 kbps。每 次读写数据字节后,寄存器地址自动累加。

2.2特性和优势

基于 32.768kHz 的晶振,提供年、月、日、星期、时、分和秒计时
Century flag
时钟工作电压: 1.0 – 5.5 V(室温)
低备用电流;典型值为 0.25 μAVDD = 3.0 VTamb =25 °C
400 kHz 双线 I2C 总线接口(VDD = 1.8 – 5.5 V
可编程时钟输出(32.768 kHz1.024 kHz32 Hz 1Hz)
报警和定时器功能
集成晶振电容器
内部上电复位(POR)
I2C 总线从机地址:读: A3h;写: A2h
开漏中断管脚

2.3 引脚说明

20231220194108461-image

符号 引脚 说明
SO8TSSOP8 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. 310 未连接;不连接,也不馈通使用

寄存器结构

20231220194238666-image

20231220194302641-image

20231220194311424-image

20231220194322353-image

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

设置和读取时间流程

20231220194505688-image

/写操作期间,计时电路(内存位置 02h 08h)会被封锁。
这可以防止
在传送条件下,对时钟和日历芯片的错读。
在一个读周期内,增加时间寄存器

这次读/写访问完成后,时间电路再次开放,且在读访问期间发生的任何被挂起的增加时
间寄存器的请求会得到处理。最多可以存储
1 个请求;因此,必须在 1 秒之内完成所有访

20231220194552722-image

使用这种方法之后,必须一次完成读或写访问,也就是说,在一次访问期间,完成秒到年
的设置或读取,这非常重要。不按此方法行事可能导致时间损坏。
例如,如果在一次访问期间设置时间(秒到年),然后在第二次访问期间设置日期,那么
在两次访问期间,时间可能增加。实施读操作时,存在类似的问题。在几次读操作之间可
能会发生转存,从而在一个时刻给出分钟数,在下一个时刻给出小时数。

推荐的读取时间的方法:
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.代码分析

工程层次图如下,

20231220192839249-image

我们主要关注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 线路上的数据在时钟脉冲的高电平周期内必
须保持稳定,因为此时数据线如果变化将被解析为一个控制信号

20231220194652842-image

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

20231220194712544-image

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

20231220194735605-image

确认(ACK)

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

有关 I2C 总线上的确认,请参见

20231220194816738-image

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

20231220194941573-image

时钟和日历的读写周期

寄存器地址是一个 4 位的值,它定义接下来要访问哪个寄存器。寄存器地址的前四位不使
用。

20231220195018325-image

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

20231220195154932-image

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

20231220195214754-image

主机在首个字节后立即读取从机(读模式)

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 寄存器 。

20231220201308776-image

其它状态下在的代码类似,不再赘述。

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
请登录后发表评论

    没有回复内容