本实验主要用于演示FPGA读取RTC芯片PCF8563的时间信息。
1.实验说明
1.1引脚说明
set_pin_assignment { scl_rtc } { LOCATION = P98; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { sda_rtc } { LOCATION = P96; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { tm_scl } { LOCATION = P106; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { tm_sda } { LOCATION = P111; 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
没有回复内容