2)摘自《开拓者FPGA开发指南》关注官方微信号公众号,获取更多资料:正点原子
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-13912-1-1.html
第三十章 实时时钟数码管显示实验
PCF8563是一款多功能时钟/日历芯片。因其功耗低、控制简单、封装小而广泛应用于电表、
水表、传真机、便携式仪器等产品中。本章我们将使用FPGA开发板上的PCF8563器件实现实时
时钟的显示。
本章包括以下几个部分:
30.1 PCF8563简介
30.2 实验任务
30.3 硬件设计
30.4 程序设计
30.5 下载验证
PCF8563简介
PCF8563是PHILIPS公司推出的一款工业级多功能时钟/日历芯片,具有报警功能、定时器
功能、时钟输出功能以及中断输出功能,能完成各种复杂的定时服务。其内部功能模块的框图
如图 30.1.1所示:
图 30.1.1 PCF8563功能框图
PCF8563有16个可寻址的8位寄存器,但不是所有位都有用到。前两个寄存器(内存地址00H、
01H)用作控制寄存器和状态寄存器(CONTROL_STATUS);内存地址02H~08H用作
tiME计时器(秒
~年计时器);地址09H~0CH用于报警(ALARM)寄存器(定义报警条件);地址0DH控制CLKOUT管
脚的输出频率;地址0EH和0FH分别用于定时器控制寄存器和定时器寄存器。
秒、分钟、小时、日、月、年、分钟报警、小时报警、日报警寄存器中的数据编码格式为
BCD,只有星期和星期报警寄存器中的数据不以BCD格式编码。BCD码(Binary-Coded Decimal
)是一种二进制的数字编码形式,用四个二进制位来表示一位十进制数(0~9),能够使二进
制和十进制之间的转换得以快捷的进行。
PCF8563通过I2C接口与FPGA进行
通信。使用该器件时,先通过I2C接口向该器件相应的寄
存器写入初始的时间数据(秒~年),然后通过I2C接口读取相应的寄存器的时间数据。有关I2C
总线协议详细的介绍请大家参考“EEPROM读写实验”。
下面我们对本次实验用到的寄存器做简要的描述和说明,其他寄存器的描述和说明,请大
家参考PCF8563的数据手册。
秒寄存器的的地址为02h,说明如下表所示:
表 30.1.1 秒寄存器描述(地址02h)
当
电源电压低于PCF8563器件的最低供电电压时,VL为“1”,表明内部完整的时钟周期信
号不能被保证,可能导致时钟/日历数据不准确。
BCD编码的秒数值如表 30.1.2所示:
表 30.1.2 秒数值的BCD编码
秒寄存器的地址为03h,说明如下表所示:
表 30.1.3 分钟寄存器描述(地址03h)
小时寄存器的地址为04h,说明如下表所示:
表 30.1.4 小时寄存器描述(地址04h)
天寄存器的地址为05h,说明如下表所示:
表 30.1.5 天寄存器描述(地址05h)
当年计数器的值是闰年时,PCF8563自动给二月增加一个值,使其成为29天。
月/世纪寄存器的地址为07h,说明如下表所示:
表 30.1.6 月/世纪寄存器(地址07h)
表 30.1.7 月份表
年寄存器的地址为08h,说明如下表所示:
表 30.1.8 寄存器(地址08h)
实验任务
本节实验任务是使用开拓者开发板上的PCF8563实时时钟模块通过数码管显示时间,数码
管默认显示年月日,按下KEY2之后显示时分秒,再次按下KEY2之后显示年月日。
硬件设计
开拓者开发板上PCF8563接口部分的原理图如图 30.3.1所示。
图 30.3.1 PCF8563接口原理图
PCF8563作为I2C接口的从器件与EEPROM等模块统一挂接在开拓者开发板上的IIC总线上。
OSCI、OSCO与外部32.768KHz的晶振相连,为芯片提供驱动时钟;SCL和SDA分别是I2C总线的串
行时钟接口和串行数据接口。
本实验中,各端口信号的管脚分配如下表所示:
表 30.3.1 PCF8563实时时钟数码管显示管脚分配
程序设计
根据实验任务,我们可以大致规划出系统的控制流程:FPGA首先通过I2C总线向PCF8563写
入初始时间值,然后读取时间数据,并在按键的控制下将读到的时间数据显示到数码管上。由
此画出系统的功能框图如下所示:
图 30.4.1 PCF8563T实时时钟数码管显示系统框图
由系统框图可知,FPGA部分包括五个模块,顶层模块(rtc)、IIC驱动模块(i2c_dri)、
PCF8563实时时钟模块(pcf8563)、按键消抖模块(key_debounce)以及数码管BCD驱动模块
(seg_bcd_dri)。其中在顶层模块中完成对另外四个模块的例化,并实现各模块控制及数据
信号的交互。
各模块端口及信号连接如图 30.4.2所示:
图 30.4.2 顶层模块原理图
PCF8563实时时钟模块(pcf8563)通过调用IIC驱动模块(i2c_dri)来实现对PCF8563实
时时钟数据的读取;同时根据按键消抖模块(key_debounce)输出的按键数据(key_value)
选择显示时间num(年月日/时分秒),并将其传递给数码管BCD驱动模块(seg_bcd_dri)显示。
顶层模块的代码如下:
1
module rtc
(
2 //system clock
3
input sys_clk
, // 系统时钟
4
input sys_rst_n
, // 系统复位
5
6 //pcf8563 interface
7
output rtc_scl
, // i2c时钟线
8
inout rtc_sda
, // i2c数据线
9
10 //user interface
11
input key0
, // 开关按键
12
output [5
:0
] sel
, // 数码管位选
13
output [7
:0
] seg_led // 数码管段选
14
);
15
16 //parameter define
17
parameter SLAVE_ADDR
= 7'h51
; // 器件地址
18
parameter BIT_CTRL
= 1'b0
; // 字地址位控制参数(16b/8b)
19
parameter CLK_FREQ
= 26'd50_000_000
; // i2c_dri模块的驱动时钟频率(CLK_FREQ)
20
parameter I2C_FREQ
= 18'd250_000
; // I2C的SCL时钟频率
21
parameter POINT
= 6'b010100
; // 控制点亮数码管小数点的位置
22 //初始时间设置,从高到低为年到秒,各占8bit
23
parameter TIME_INI
= 48'h18_05_23_09_30_00
;
24
25 //wire define
26
wire clk
; // I2C操作时钟
27
wire i2c_exec
; // i2c触发控制
28
wire [15
:0
] i2c_addr
; // i2c操作地址
29
wire [ 7
:0
] i2c_data_w
; // i2c写入的数据
30
wire i2c_done
; // i2c操作结束标志
31
wire i2c_rh_wl
; // i2c读写控制
32
wire [ 7
:0
] i2c_data_r
; // i2c读出的数据
33
wire [23
:0
] num
; // 数码管要显示的数据
34
wire key_value
; // 按键消抖后的数据
35
36 //*****************************************************
37 //** main code
38 //*****************************************************
39
40 //例化i2c_dri,调用IIC协议
41 i2c_dri
#(
42
.SLAVE_ADDR
(SLAVE_ADDR
), // slave address从机地址,放此处方便参数传递
43
.CLK_FREQ
(CLK_FREQ
), // i2c_dri模块的驱动时钟频率(CLK_FREQ)
44
.I2C_FREQ
(I2C_FREQ
) // I2C的SCL时钟频率
45
) u_i2c_dri
(
46 //global clock
47
.clk
(sys_clk
), // i2c_dri模块的驱动时钟(CLK_FREQ)
48
.rst_n
(sys_rst_n
), // 复位信号
49 //i2c interface
50
.i2c_exec
(i2c_exec
), // I2C触发执行信号
51
.bit_ctrl
(BIT_CTRL
), // 器件地址位控制(16b/8b)
52
.i2c_rh_wl
(i2c_rh_wl
), // I2C读写控制信号
53
.i2c_addr
(i2c_addr
), // I2C器件内地址
54
.i2c_data_w
(i2c_data_w
), // I2C要写的数据
55
.i2c_data_r
(i2c_data_r
), // I2C读出的数据
56
.i2c_done
(i2c_done
), // I 2C一次操作完成
57
.scl
(rtc_scl
), // I2C的SCL时钟信号
58
.sda
(rtc_sda
), // I2C的SDA信号
59 //user interface
60
.dri_clk
(clk
) // I2C操作时钟
61
);
62
63 //例化PCF8563测量模块
64 pcf8563
#(.TIME_INI
(TIME_INI
)
65
) u_pcf8563
(
66 //system clock
67
.clk
(clk
), // 时钟信号
68
.rst_n
(sys_rst_n
), // 复位信号
69 //i2c interface
70
.i2c_rh_wl
(i2c_rh_wl
), // I2C读写控制信号
71
.i2c_exec
(i2c_exec
), // I2C触发执行信号
72
.i2c_addr
(i2c_addr
), // I2C器件内地址
73
.i2c_data_w
(i2c_data_w
), // I2C要写的数据
74
.i2c_data_r
(i2c_data_r
), // I2C读出的数据
75
.i2c_done
(i2c_done
), // I2C一次操作完成
76 //user interface
77
.key_value
(key_value
), // 按键切换输入
78
.num
(num
) // 数码管要显示的数据
79
);
80
81 //例化数码管驱动模块
82 seg_bcd_dri u_seg_bcd_dri
(
83 //input
84
.clk
(sys_clk
), // 时钟信号
85
.rst_n
(sys_rst_n
), // 复位信号
86
.num
(num
), // 6个数码管要显示的数值
87
.point
(POINT
), // 小数点具体显示的位置,从高到低,高有效
88 //output
89
.sel
(sel
), // 数码管位选
90
.seg_led
(seg_led
) // 数码管段选
91
);
92
93 //例化消抖模块
94 key_debounce u_key_debounce
(
95
.clk
(sys_clk
), //外部50M时钟
96
.rst_n
(sys_rst_n
), //外部复位信号,低有效
97
.key
(key0
), //外部按键输入
98
.key_value
(key_value
), //按键消抖后的数据
99
.key_flag
() //按键数据有效信号
100
);
101
102
endmodule
程序中第23行的TIME_INI参数是PCF8563初始化时的时间数据,可以通过修改此参数值使
PCF8563从不同的时间开始计时,例如从2018年5月23号09:30:00开始计时,需要将该参数值
设置为48’h180523093000。
顶层模块中主要完成对其余模块的例化。其中I2C驱动模块(i2c_dri)程序与“EEPROM读
写实验”章节中的IIC驱动模块(i2c_dri)程序完全相同,有关IIC驱动模块的详细介绍请大
家参考“EEPROM读写实验”。按键消抖模块可参考“按键控制蜂鸣器实验”。
PCF8563实时时钟模块的代码如下所示:
1
module pcf8563
#(
2 // 初始时间设置,从高到低为年到秒,各占8bit
3
parameter TIME_INI
= 48'h18_03_19_09_30_00
)(
4 //system clock 50MHz
5
input clk
, // 时钟信号
6
input rst_n
, // 复位信号
7
8 //i2c interface
9
output reg i2c_rh_wl
, // I2C读写控制信号
10
output reg i2c_exec
, // I2C触发执行信号
11
output reg [15
:0
] i2c_addr
, // I2C器件内地址
12
output reg [ 7
:0
] i2c_data_w
, // I2C要写的数据
13
input [ 7
:0
] i2c_data_r
, // I2C读出的数据
14
input i2c_done
, // I2C一次操作完成
15
16 //user interface
17
input key_value
, // 按键切换输入
18
output [23
:0
] num // 数码管要显示的数据
19
);
20
21 //reg define
22
reg key_dy0
; // 延迟打拍
23
reg key_dy1
; // 延迟打拍
24
reg switch
; // 按键切换显示日期、时间
25
reg [3
:0
] flow_cnt
; // 状态流控制
26
reg [12
:0
] wait_cnt
; // 计数等待
27
28 //PCF8563T的秒、分、时、日、月、年数据
29
reg [7
:0
] sec
; // 秒
30
reg [7
:0
] min
; // 分
31
reg [7
:0
] hour
; // 时
32
reg [7
:0
] day
; // 日
33
reg [7
:0
] mon
; // 月
34
reg [7
:0
] year
; // 年
35
wire [23
:0
] rtc_time
; //时间,从低位到高位依次是秒、分、时,各8bit
36
wire [23
:0
] rtc_date
; //日期,从低位到高位依次是日、月、年,各8bit
37
38 //wire define
39
wire neg_sap
; // 采下降沿得到的信号
40
41 //*****************************************************
42 //** main code
43 //*****************************************************
44
45
assign neg_sap
= (~key_dy0
& key_dy1
); // 按键按下时,得到一个周期的高电平信号
46
assign rtc_time
= {hour
,min
,sec
};
47
assign rtc_date
= {year
,mon
,day
};
48 //通过switch切换时间/日期显示
49
assign num
= switch
? rtc_time
: rtc_date
;
50
51 //打拍(采按键时的下降沿)
52
always @(posedge clk
or negedge rst_n
) begin
53
if(!rst_n
) begin
54 key_dy0
<= 1'b1
;
55 key_dy1
<= 1'b1
;
56
end
57
else begin
58 key_dy0
<= key_value
;
59 key_dy1
<= key_dy0
;
60
end
61
end
62
63 //按键切换
64
always @(posedge clk
or negedge rst_n
) begin
65
if(!rst_n
)
66 switch
<= 1'b0
;
67
else if (neg_sap
)
68 switch
<= ~switch
;
69
end
70
71 //从PCF8563T读出的时间、日期数据
72
always @(posedge clk
or negedge rst_n
) begin
73
if(!rst_n
) begin
74 sec
<= 8'h0
;
75 min
<= 8'h0
;
76 hour
<= 8'h0
;
77 day
<= 8'h0
;
78 mon
<= 8'h0
;
79 year
<= 8'h0
;
80 i2c_exec
<= 1'b0
;
81 i2c_rh_wl
<= 1'b0
;
82 i2c_addr
<= 8'd0
;
83 i2c_data_w
<= 8'd0
;
84 flow_cnt
<= 4'd0
;
85 wait_cnt
<= 13'd0
;
86
end
87
else begin
88 i2c_exec
<= 1'b0
;
89
case(flow_cnt
)
90 //上电初始化
91 4'd0
: begin
92
if(wait_cnt
== 13'd8000
) begin
93 wait_cnt
<= 12'd0
;
94 flow_cnt
<= flow_cnt
+ 1'b1
;
95
end
96
else
97 wait_cnt
<= wait_cnt
+ 1'b1
;
98
end
99 //写读秒
100 4'd1
: begin
101 i2c_exec
<= 1'b1
;
102 i2c_addr
<= 8'h02
;
103 flow_cnt
<= flow_cnt
+ 1'b1
;
104 i2c_data_w
<= TIME_INI
[7
:0
];
105
end
106 4'd2
: begin
107
if(i2c_done
== 1'b1
) begin
108 sec
<= i2c_data_r
[6
:0
];
109 flow_cnt
<= flow_cnt
+ 1'b1
;
110
end
111
end
112 //写读分
113 4'd3
: begin
114 i2c_exec
<= 1'b1
;
115 i2c_addr
<= 8'h03
;
116 flow_cnt
<= flow_cnt
+ 1'b1
;
117 i2c_data_w
<= TIME_INI
[15
:8
];
118
end
119 4'd4
: begin
120
if(i2c_done
== 1'b1
) begin
121 min
<= i2c_data_r
[6
:0
];
122 flow_cnt
<= flow_cnt
+ 1'b1
;
123
end
124
end
125 //写读时
126 4'd5
: begin
127 i2c_exec
<= 1'b1
;
128 i2c_addr
<= 8'h04
;
129 flow_cnt
<= flow_cnt
+ 1'b1
;
130 i2c_data_w
<= TIME_INI
[23
:16
];
131
end
132 4'd6
: begin
133
if(i2c_done
== 1'b1
) begin
134 hour
<= i2c_data_r
[5
:0
];
135 flow_cnt
<= flow_cnt
+ 1'b1
;
136
end
137
end
138 //写读天
139 4'd7
: begin
140 i2c_exec
<= 1'b1
;
141 i2c_addr
<= 8'h05
;
142 flow_cnt
<= flow_cnt
+ 1'b1
;
143 i2c_data_w
<= TIME_INI
[31
:24
];
144
end
145 4'd8
: begin
146
if(i2c_done
== 1'b1
) begin
147 day
<= i2c_data_r
[5
:0
];
148 flow_cnt
<= flow_cnt
+ 1'b1
;
149
end
150
end
151 //写读月
152 4'd9
: begin
153 i2c_exec
<= 1'b1
;
154 i2c_addr
<= 8'h07
;
155 flow_cnt
<= flow_cnt
+ 1'b1
;
156 i2c_data_w
<= TIME_INI
[39
:32
];
157
end
158 4'd10
: begin
159
if(i2c_done
== 1'b1
) begin
160 mon
<= i2c_data_r
[4
:0
];
161 flow_cnt
<= flow_cnt
+ 1'b1
;
162
end
163
end
164 //写读年
165 4'd11
: begin
166 i2c_exec
<= 1'b1
;
167 i2c_addr
<= 8'h08
;
168 flow_cnt
<= flow_cnt
+ 1'b1
;
169 i2c_data_w
<= TIME_INI
[47
:40
];
170
end
171 4'd12
: begin
172
if(i2c_done
== 1'b1
) begin
173 year
<= i2c_data_r
;
174 i2c_rh_wl
<= 1'b1
;
175 flow_cnt
<= 4'd1
;
176
end
177
end
178
default: flow_cnt
<= 4'd0
;
179
endcase
180
end
181
end
182
183
endmodule
程序中第47行的assign语句和第52行的always语句检测按键数据(消抖后)的下降沿,并
输出一个时钟周期的脉冲信号(neg_sap);然后根据neg_sap完成 switch信号的切换,如64~69
行所示;最终在第49行根据switch选择传递给数码管显示模块的时间数据(num)。
图 30.4.3为采集过程中SignalTap抓取的波形图。从图中可以看到当前读到的时间数据为
18年05月23日09:30:02。
图 30.4.3 SignalTap波形图
数码管BCD驱动模块的代码如下所示:
1
module seg_bcd_dri
(
2 //input
3
input clk
, // 时钟信号
4
input rst_n
, // 复位信号
5
input [23
:0
] num
, // 6个数码管要显示的数值
6
input [5
:0
] point
, // 小数点具体显示的位置,从高到低,高有效
7
8 //output
9
output reg [5
:0
] sel
, // 数码管位选
10
output reg [7
:0
] seg_led // 数码管段选
11
);
12
13 //parameter define
14
parameter WIDTH0
= 50_000
;
15
16 //reg define
17
reg [15
:0
] cnt0
; // 1ms计数
18
reg [2
:0
] cnt
; // 切换显示数码管用
19
reg [3
:0
] num1
; // 送给要显示的数码管,要亮的灯
20
reg point1
; // 要显示的小数点
21
22 //*****************************************************
23 //** main code
24 //*****************************************************
25
26 //计数1ms
27
always @(posedge clk
or negedge rst_n
) begin
28
if(rst_n
== 1'b0
)
29 cnt0
<= 15'b0
;
30
else if(cnt0
< WIDTH0
)
31 cnt0
<= cnt0
+ 1'b1
;
32
else
33 cnt0
<= 15'b0
;
34
end
35
36 //计数器,用来计数6个状态(因为有6个灯)
37
always @(posedge clk
or negedge rst_n
) begin
38
if(rst_n
== 1'b0
)
39 cnt
<= 3'b0
;
40
else if(cnt
< 3'd6
) begin
41
if(cnt0
== WIDTH0
)
42 cnt
<= cnt
+ 1'b1
;
43
else
44 cnt
<= cnt
;
45
end
46
else
47 cnt
<= 3'b0
;
48
end
49
50 //6个数码管轮流显示,完成刷新(从右到左)
51
always @(posedge clk
or negedge rst_n
) begin
52
if(rst_n
== 1'b0
) begin
53 sel
<= 6'b000001
;
54 num1
<= 4'b0
;
55
end
56
else begin
57
case (cnt
)
58 3'd0
:begin
59 sel
<= 6'b111110
;
60 num1
<= num
[3
:0
] ;
61 point1
<= point
[0
] ;
62
end
63 3'd1
:begin
64 sel
<= 6'b111101
;
65 num1
<= num
[7
:4
] ;
66 point1
<= point
[1
] ;
67
end
68 3'd2
:begin
69 sel
<= 6'b111011
;
70 num1
<= num
[11
:8
];
71 point1
<= point
[2
] ;
72
end
73 3'd3
:begin
74 sel
<= 6'b110111
;
75 num1
<= num
[15
:12
];
76 point1
<= point
[3
] ;
77
end
78 3'd4
:begin
79 sel
<= 6'b101111
;
80 num1
<= num
[19
:16
];
81 point1
<= point
[4
] ;
82
end
83 3'd5
:begin
84 sel
<= 6'b011111
;
85 num1
<= num
[23
:20
];
86 point1
<= point
[5
] ;
87
end
88
default: begin
89 sel
<= 6'b000000
;
90 num1
<= 4'b0
;
91 point1
<= 1'b1
;
92
end
93
endcase
94
end
95
end
96
97 //数码管显示数据
98
always @ (posedge clk
or negedge rst_n
) begin
99
if(rst_n
== 1'b0
)
100 seg_led
<= 7'b0
;
101
else begin
102
case(num1
)
103 4'd0
: seg_led
<= {~point1
,7'b1000000
};
104 4'd1
: seg_led
<= {~point1
,7'b1111001
};
105 4'd2
: seg_led
<= {~point1
,7'b0100100
};
106 4'd3
: seg_led
<= {~point1
,7'b0110000
};
107 4'd4
: seg_led
<= {~point1
,7'b0011001
};
108 4'd5
: seg_led
<= {~point1
,7'b0010010
};
109 4'd6
: seg_led
<= {~point1
,7'b0000010
};
110 4'd7
: seg_led
<= {~point1
,7'b1111000
};
111 4'd8
: seg_led
<= {~point1
,7'b0000000
};
112 4'd9
: seg_led
<= {~point1
,7'b0010000
};
113
default: seg_led
<= {point1
,7'b1000000
};
114
endcase
115
end
116
end
117
endmodule
由于是BCD编码,从低到高每4位二进制数代表一位十进制数,所以在第57行的case语句块
中,我们只需把num信号相应位的值赋给num1即可。有关数码管显示更详细的解释可参考“动
态数码管显示实验”。
下载验证
首先我们打开PCF8563T实时时钟数码管显示实验工程,在工程所在的路径下打开rtc/par
文件夹,在里面找到“rtc.qpf”并双击打开。注意工程所在的路径名只能由字母、数字以及
下划线组成,不能出现中文、空格以及特殊字符等。工程打开后如图 30.5.1所示。
图 30.5.1 PCF8563T实时时钟数码管显示实验工程
然后将下载器一端连电脑,另一端与开发板上对应端口连接,最后连接电源线并打开电源
开关。
接下来我们下载程序,验证PCF8563的实时时钟数码管显示功能。
工程打开后通过点击工具栏中的“Programmer”图标打开下载界面,通过“Add File”按
钮选择rtc/par/output_files目录下的“rtc.sof”文件。开发板电源打开后,在程序下载界
面点击“Hardware Setup”,在弹出的对话框中选择当前的硬件连接为“USB-Blaster[USB-
0]”。然后点击“Start”将工程编译完成后得到的sof文件下载到开发板中,如图 30.5.2所
示。
图 30.5.2 程序下载界面
下载完成后观察到开发板上数码管显示的值为我们设置的初始年月日值,当按下key2后,
数码管显示时分秒,并且显示时间不停的变化,说明PCF8563实时时钟数码管显示实验程序下
载验证成功。