11. RTC—实时时钟

11.1. 章节导读

RTC(英文全称:Real-time clock,中文名称:实时时钟)是指可以像时钟一样输出实际时间的电子设备,RTC是集成电路,因此也称为时钟芯片。本次实验将使用型号为PCF8563的多功能时钟/日历芯片来进行时间、日期的实时显示。

11.2. 理论学习

11.2.1. PCF8563简介

PCF8563 是飞利浦公司推出的一款工业级内含I2C总线接口功能的具有极低功耗的多功能时钟/日历芯片。PCF8563 的多种报警功能、定时器功能、时钟输出功能以及中断输出功能,能完成各种复杂的定时服务。是一款性价比极高的时钟芯片,它已被广泛用于电表、水表、气表、电话、传真机、便携式仪器以及电池供电的仪器仪表等产品领域。

PCF8563内部功能框图如图 41‑1所示。

RTC002

图 41‑1 PCF8563内部功能框图

如图 41‑1所示PCF8563内有16(00~0F)个8位寄存器:一个可自动增量的地址寄存器,一个内置 32.768KHz的振荡器(带有一个内部集成的电容)一个分频器(用于给实时时钟RTC 提供源时钟)一个可编程时钟输出,一个定时器,一个报警器,一个掉电检测器和一个I2C总线接口

所有16 个寄存器设计为可寻址的 8位并行寄存器,但不是所有位都有用。

  • 前两个寄存器(内存地址 00H和01H):用于控制寄存器和状态寄存器。

  • 内存地址 02H~08H 寄存器:用于时钟计数器(秒到年计数器)。

  • 地址 09H~0CH 寄存器:用于报警寄存器(定义报警条件)。

  • 地址 0DH 寄存器:用于控制CLKOUT 管脚的输出频率。

  • 地址 0EH 和 0FH 寄存器:分别用于定时器控制和定时器。

秒、分钟、小时、日、月、年、分钟报警、小时报警、日报警寄存器都以二进制编码的十进制(BCD)格式编码,星期和星期报警寄存器不以 BCD 格式编码。当一个 RTC 寄存器被读时,所有计数器的内容被锁存,因此,在传送条件下,可以防止对时钟日历芯片的错读。

11.2.2. PCF8563寄存器介绍

本次实验是对时间以及日期进行显示,下面对相关的寄存器进行介绍,其余寄存器大家可参考《PCF8563数据书册》进行了解。

11.2.2.1. 秒寄存器(02h)

表格 41‑1 秒寄存器描述

bit

符号

描述

7

VL

0

保证准确的时钟数据

1

无法保证准确的时钟数据

6:4

0~5

十位数据(BCD码)

3:0

0~9

个位数据(BCD码)

这里需要注意的是,不管是秒数据还是分、时、时、日、月、年数据都是以BCD格式编码的实际数据,编码格式请参阅图 41‑2。

RTC003

图 41‑2 BCD编码图

11.2.2.2. 分寄存器(03h)

表格 41‑2 分寄存器描述

bit

符号

描述

7

不使用

6:4

0~5

十位数据(BCD码)

3:0

0~9

个位数据(BCD码)

11.2.2.3. 时寄存器(04h)

表格 41‑3 时寄存器描述

bit

符号

描述

7:6

不使用

5:4

小时

0~2

十位数据(BCD码)

3:0

0~9

个位数据(BCD码)

11.2.2.4. 日寄存器(05h)

表格 41‑4 日寄存器描述

bit

符号

描述

7:6

不使用

5:4

小时

0~3

十位数据(BCD码)

3:0

0~9

个位数据(BCD码)

11.2.2.5. 月寄存器(07h)

表格 41‑5 月寄存器描述

bit

符号

描述

7

C

0

当前世纪

1

下一世纪

6:5

不使用

4

0~1

十位数据(BCD码)

3:0

0~9

个位数据(BCD码)

11.2.2.6. 年寄存器(08h)

表格 41‑6 年寄存器描述

bit

符号

描述

7:4

0~9

十位数据(BCD码)

3:0

0~9

个位数据(BCD码)

年数据为当前年份的末两位。

11.2.3. PCF8563通信接口

PCF8563是通过I2C接口与FPGA进行通信,如图 41‑3所示:

RTC004

图 41‑3 I2C接口时序图

由上图可以看到:上电到SCL下降沿(起始信号)需等待8ms的时间,也就是我们上电之后需先等待8ms后再开始对PCF8563芯片进行配置。

RTC005

图 41‑4 PCF8563器件地址

如图 41‑4所示,可知PCF8563的器件地址为7’b1010001。

RTC006

图 41‑5 PCF8563写时序

如图 41‑5为PCF8563写时序,即用该时序对各寄存器写入初始值。其中S:表示I2C起始信号;R/W:读写控制位(0表示写,1表示读);A:表示应答信号;P:表示I2C停止信号。

RTC007

图 41‑6 PCF8563读时序

如图 41‑6为PCF8563的读时序,即用该时序读出各寄存器里的时间、日期值。其中S:表示I2C起始信号;R/W:读写控制位(0表示写,1表示读);A:表示应答信号;P:表示I2C停止信号。

读写数据都是通过i2C总线去读写的,详细的I2C控制时序方法大家可参考前面《基于I2C协议的 EEPROM驱动控制》章节进行学习。

通过以上的学习我们可大致描述一下驱动PCF8563的流程:通过I2C总线对秒、分、使、日、月、年寄存器先写入初始值,初始值写完后再对秒、分、使、日、月、年寄存器进行读取。

11.3. 实战演练

11.3.1. 实验目标

实验目标:通过数码管显示实时时间,并通过按键切换显示年月日。

11.3.2. 硬件资源

本次实验我们需使用到开发板上的按键、数码管资源,同时还需使用开发板上的板载PCF8563实时时钟芯片。其中按键和数码管硬件资源在前面章节已有讲解,这里就不再过多描述。开发板上的实时时钟芯片如图 41‑7所示。

RTC008

图 41‑7 PCF8563实时时钟芯片

其原理图如图 41‑8所示,从该图中可以看到该传感器是通过I2C总线与FPGA芯片进行通信的。

RTC009

图 41‑8 环境光传感器原理图

11.3.3. 程序设计

11.3.3.1. 整体说明

根据实验目标可知,要实现这个功能,数码管显示模块是必不可少的,同时我们是用i2c总线对其控制的,所以还需要一个i2c配置模块以及对PCF8563传感器的控制模块,最后还需要一个按键消抖模块实现按键切换显示功能。该工程的整体框图如图 41‑9所示:

RTC010

图 41‑9 实时时钟显示整体框图

通过上图可以看到,该工程共分五个模块。各模块简介见表格 41‑7。

表格 41‑7 rtc工程模块简介

模块名称

功能描述

pcf8563_ctrl

pcf8563控制模块

i2c_ctrl

iic驱动模块

key_filter

按键消抖模块

seg_595_bcd

数码管bcd码显示模块

rtc

工程顶层模块

通过模块框图为大家讲述该工程的工作流程:通过pcf8563_ctrl模块和i2c_ctrl模块控制PCF8563进行实时的时钟和日期输出到seg_595_bcd模块进行驱动显示,同时按下按键时控制显示数据的切换。

下面分模块为大家讲解。

11.3.3.2. 按键消抖模块

在前面按键消抖章节已经对按键消抖模块进行了详细的讲解,在此就不再进行说明,直接调用这个模块即可。

11.3.3.3. i2c驱动模块

在《基于I2C协议的 EEPROM驱动控制》章节已经详细地讲解了驱动方法,在此就不再叙述,直接调用这个模块即可。

11.3.3.4. pcf8563控制模块

该模块通过i2c驱动模块对pcf8563进行初始值设置以及数据读取。

模块框图

RTC011

图 41‑10 pcf8563控制模块

这个模块的作用就是配置时钟、日期的初始值,配置完后进行读取。模块各个信号的简介,如表格 41‑8所示。

表格 41‑8 pcf8563控制模块输入输出信号描述

信号

位宽

类型

功能描述

sys_clk

1bit

input

系统时钟,50MHz

i2c_clk

1bit

input

i2c驱动时钟,1Mhz

sys_rst_n

1bit

input

复位信号,低有效

i2c_end

1bit

input

i2c一次读/写操作完成

rd_data

8bit

input

i2c设备读取的数据

key_flag

1bit

input

按键消抖后标志信号

wr_en

1bit

output

写数据使能信号

rd_en

1bit

output

读数据使能信号

i2c_start

1bit

output

i2c触发信号

byte_addr

16bit

output

输入i2c字节地址

wr_data

8bit

output

输入i2c设备数据

data_otu

24bit

output

输出数码管显示数据

通过对pcf8563的理论学习我们可以知道,该模块适合用状态机去设置时钟的初始值以及读取时钟数据。

状态机

通过该模块的状态跳转图来进一步了解pcf8563的工作过程,如图 41‑11所示。

RTC012

图 41‑11 pcf8563控制模块状态跳转图

表格 41‑9 pcf控制模块状态机状态描述

状态名称

状态描述

S_WAIT

上电等待状态

INIT_SEC

初始化秒状态

INIT_MIN

初始化分状态

INIT_HOUR

初始化小时状态

INIT_DAY

初始化日状态

INIT_MON

初始化月状态

INIT_YEAR

初始化年状态

RD_SEC

读取秒数据状态

RD_MIN

读取分数据状态

RD_HOUR

读取小时数据状态

RD_DAY

读取日数据状态

RD_MON

读取月数据状态

RD_YEAR

读取年数据状态

结合状态跳转图,我们讲述一下该模块的工作过程。

上电后先等待一段时间以越过不稳定状态。等待一段时间后就可以开始设置初始化时钟的值(设置时钟初始值)。我们是通过i2c总线对pcf8563进行控制的,也就是通过i2c总线写入时钟的初始值来进行设置。对秒、分、时、日、月、年初值设置完成之后,开始对时钟数据读取,因为我们是显示日期和时间,所以我们需要读取 秒、分、时、日、月、年的值。因为是实时时钟,时钟数据是一直变化的,所以我们需要一直不断的读取时钟的值。

下面再通过波形图来看看具体如何去实现对时钟的初始化以及时钟数据的读取。

波形图绘制

RTC013

图 41‑12 pcf8563控制模块波形图(一)

由于整个控制时序过长,所以在这里将分开为大家讲解。如图 41‑12所示,从理论学习中我们知道上电后需先等一段时间,最少需等待8ms,所以在此我们先等待8ms后再跳转。一般设计中我们都是通过计数器来进行时间计数的,所以此时我们需产生一个计数器cnt_wait。

cnt_wait:除了上电等待状态需要用到计数器外,后面状态我们还需要产生一个时钟的开始信号,这都需要用到计数器,所以状态机每跳到一个新的状态,我们就让计数器归0,其他情况就让计数器一直自加,每来一个i2c_clk时钟加1。这样我们每个状态就都能使用该计数器资源了。

当计数器计到8ms数值时,状态机跳转到初始化秒状态进行初始秒值的写入,所以在该状态状态下,我们需要产生写使能信号(wr_en),i2c触发信号(i2c_start)以及写入的地址和数据。

wr_en:只要是往pcf8563中写入数据,就需要拉高写使能信号,所以在初始化秒、分、时、日、月、年状态都需要拉高写使能信号。当不需要写入时将写使能拉低即可。

i2c_start:i2c触发信号,由i2c驱动模块可知,无论是写还是读,都是先检测到i2c触发信号后才开始写入和读取的。所以每次写入和读取我们都需要产生一个时钟的触发信号,从pcf8563的数据手册可以知道,在一次读写完成之后至少要等待1.3us后才能开始下一次读写,而状态是在初始化秒完成,也就是 写入秒初始值完成(i2c_end=1)后跳转的。所以我们不能在状态开始处产生i2c触发信号,我们需要等待大于1.3us后再产生开始信号,这里我们在读写完成后2us(cnt_wait=1)产生触发信号。也就是触发信号到结束信号直接相隔2us。

byte_addr:在理论部分我们已经知道各个状态的地址,所以这里我们在相应的状态下给其相应的地址,同时给相应的初始化数据,就能对PCF8563初始化成功了。

wr_data:写入的数据,我们可以设置一个我们初始化时间数据:TIME_INIT。由于我们初始化要写入秒、分、时、日、月、年数据,而i2c一次是写入8bit数据,所以我们定义一个48bit的初始时间值:年、月、日、时、分、秒由高位到低位排列。如图 41‑12所示,初始化什么值我们就给什么数据。当依次设置完初始时间值后,我们便可以开始读取数据,波形图如图 41‑13所示。

RTC014

图 41‑13 pcf8563控制模块波形图(二)

如上图所示:当初始化时钟数据写完之后,便可开始读取数据。读取数据和写入数据时序大致相同,需产生读使能信号(rd_en)、i2c触发信号以及寄存器地址。

rd_en:只要是从pcf8563中读取数据,就需要拉高读使能信号,所以在读取秒、分、时、日、月、年数据状态时都需要拉高读使能信号,当不需要读取数据时将读使能信号拉低即可。

i2c触发信号(i2c_start)与初始化数据时的触发信号产生方法相同,在此就不再叙述。

byte_addr:读取的地址与写入的地址一样,在相应的状态下给相应的地址即可。

当写使能,i2c触发信号和读地址都已经产生之后,i2c总线就能读取pcf8563的数据了。当在读秒数据状态(RD_SEC)且i2c_end=1时说明秒数据已经读取完毕,此时读出的rd_data的值即为秒值,从理论部分我们知道,秒数据是由7位的bcd码组成的,所以在i2c_end=1时将读取的7bit 数据赋为秒数据即可。同理分、时、日、月、年数据也在各读取状态读取完后赋值即可。

当数据读取完成之后,波形图如图 41‑14所示。

RTC015

图 41‑14 pcf8563控制模块波形图(三)

如上图所示:当一次时钟数据读取完成之后,状态跳回读取秒数据状态(RD_SEC),开始下一次时钟数据的读取。

数据读完之后,我们再看看如何使用按键进行时间数据切换,波形图如图 41‑15所示。

RTC016

图 41‑15 按键切换数据显示波形图

如上图所示:如果我们只用按键消抖标志信号(key_flag)去控制数据的切换,这并不容易实现。所以这里我们需要用key_flag信号产成一个data_flag信号来控制数据的切换。因为我们按一次按键数据切换一次且变化后的数据在下一个按键到来时才会切换,所以我们需要一个按键信号到来前保持不变的标志信号 。如图data_flag所示,开始时data_flag为低电平,此时输出时分秒的值进行显示;当按下按键时,data_flag信号取反变为高电平,在data_flag为高电平时输出年月日的值进行显示;这就实现了按键切换显示的功能。

代码编写

参照绘制波形图,编写模块代码。模块参考代码,具体见代码清单 41‑1。

代码清单 41‑1 pcf8563控制模块参考代码(pcf8563_ctrl.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
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码数据

);

////
//\* Parameter and Internal Signal \//
////

//parameter define
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 ; //读年
localparam CNT_WAIT_8MS = 8000 ; //8ms时间计数值

//reg define
reg [7:0] year ; //年数据
reg [7:0] month ; //月数据
reg [7:0] day ; //日数据
reg [7:0] hour ; //小时数据
reg [7:0] minute ; //年数据
reg [7:0] second ; //秒数据
reg data_flag ; //数据切换标志信号
reg [3:0] state ; //状态机状态
reg [12:0] cnt_wait ; //等待计数器

////
//\* Main Code \//
////

//产生数据切换的标志信号
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

//各状态下的信号赋值
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
wr_en <= 1'b0 ;
rd_en <= 1'b0 ;
i2c_start <= 1'b0 ;
byte_addr <= 16'd0 ;
wr_data <= 8'd0 ;
year <= 8'd0 ;
month <= 8'd0 ;
day <= 8'd0 ;
hour <= 8'd0 ;
minute <= 8'd0 ;
second <= 8'd0 ;
end
else case(state)
S_WAIT: //上电等待状态
begin
wr_en <= 1'b0 ;
rd_en <= 1'b0 ;
i2c_start <= 1'b0 ;
byte_addr <= 16'h0 ;
wr_data <= 8'h00 ;
end
INIT_SEC: //初始化秒
if(cnt_wait == 13'd1)
begin
wr_en <= 1'b1 ;
i2c_start <= 1'b1 ;
byte_addr <= 16'h02 ;
wr_data <= TIME_INIT[7:0];
end
else
begin
wr_en <= 1'b1 ;
i2c_start <= 1'b0 ;
byte_addr <= 16'h02 ;
wr_data <= TIME_INIT[7:0];
end
INIT_MIN: //初始化分
if(cnt_wait == 13'd1)
begin
i2c_start <= 1'b1 ;
byte_addr <= 16'h03 ;
wr_data <= TIME_INIT[15:8];
end
else
begin
i2c_start <= 1'b0 ;
byte_addr <= 16'h03 ;
wr_data <= TIME_INIT[15:8];
end
INIT_HOUR: //初始化小时
if(cnt_wait == 13'd1)
begin
i2c_start <= 1'b1 ;
byte_addr <= 16'h04 ;
wr_data <= TIME_INIT[23:16];
end
else
begin
i2c_start <= 1'b0 ;
byte_addr <= 16'h04 ;
wr_data <= TIME_INIT[23:16];
end
INIT_DAY: //初始化日
if(cnt_wait == 13'd1)
begin
i2c_start <= 1'b1 ;
byte_addr <= 16'h05 ;
wr_data <= TIME_INIT[31:24];
end
else
begin
i2c_start <= 1'b0 ;
byte_addr <= 16'h05 ;
wr_data <= TIME_INIT[31:24];
end
INIT_MON: //初始化月
if(cnt_wait == 13'd1)
begin
i2c_start <= 1'b1 ;
byte_addr <= 16'h07 ;
wr_data <= TIME_INIT[39:32];
end
else
begin
i2c_start <= 1'b0 ;
byte_addr <= 16'h07 ;
wr_data <= TIME_INIT[39:32];
end
INIT_YEAR: //初始化年
if(cnt_wait == 13'd1)
begin
i2c_start <= 1'b1 ;
byte_addr <= 16'h08 ;
wr_data <= TIME_INIT[47:40];
end
else
begin
i2c_start <= 1'b0 ;
byte_addr <= 16'h08 ;
wr_data <= TIME_INIT[47:40];
end
RD_SEC: //读秒
if(cnt_wait == 13'd1)
i2c_start <= 1'b1;
else if(i2c_end == 1'b1)
second <= rd_data[6:0];
else
begin
wr_en <= 1'b0 ;
rd_en <= 1'b1 ;
i2c_start <= 1'b0 ;
byte_addr <= 16'h02 ;
wr_data <= 8'd0 ;
end
RD_MIN: //读分
if(cnt_wait == 13'd1)
i2c_start <= 1'b1;
else if(i2c_end == 1'b1)
minute <= rd_data[6:0];
else
begin
rd_en <= 1'b1 ;
i2c_start <= 1'b0 ;
byte_addr <= 16'h03 ;
end
RD_HOUR: //读时
if(cnt_wait == 13'd1)
i2c_start <= 1'b1;
else if(i2c_end == 1'b1)
hour <= rd_data[5:0];
else
begin
rd_en <= 1'b1 ;
i2c_start <= 1'b0 ;
byte_addr <= 16'h04 ;
end
RD_DAY: //读日
if(cnt_wait == 13'd1)
i2c_start <= 1'b1;
else if(i2c_end == 1'b1)
day <= rd_data[5:0];
else
begin
rd_en <= 1'b1 ;
i2c_start <= 1'b0 ;
byte_addr <= 16'h05 ;
end
RD_MON: //读月
if(cnt_wait == 13'd1)
i2c_start <= 1'b1;
else if(i2c_end == 1'b1)
month <= rd_data[4:0];
else
begin
rd_en <= 1'b1 ;
i2c_start <= 1'b0 ;
byte_addr <= 16'h07 ;
end
RD_YEAR: //读年
if(cnt_wait == 13'd1)
i2c_start <= 1'b1;
else if(i2c_end == 1'b1)
year <= rd_data[7:0];
else
begin
rd_en <= 1'b1 ;
i2c_start <= 1'b0 ;
byte_addr <= 16'h08 ;
end
default:
begin
wr_en <= 1'b0 ;
rd_en <= 1'b0 ;
i2c_start <= 1'b0 ;
byte_addr <= 16'd0 ;
wr_data <= 8'd0 ;
year <= 8'd0 ;
month <= 8'd0 ;
day <= 8'd0 ;
hour <= 8'd0 ;
minute <= 8'd0 ;
second <= 8'd0 ;
end
endcase

endmodule

代码第58行及67行需使用系统时钟(sys_clk,50MHz),而不能使用i2c_clk时钟(1MHz)进行采样,那是因为按键标志信号(key_flag)的产生时钟为系统时钟,如果用i2c_clk去采样,将几乎采不到这个标志信号。

11.3.3.5. 数码管BCD码显示模块

该模块与“数码管的动态显示”章节的数码管动态显示大致相同,唯一的区别就是:前面章节的动态数码管输入的是十进制数据,而数码管bcd码显示模块输入的是bcd码数据。所以我们将原来的数码管动态显示章节中的二进制转BCD码模块去掉,同时我们输入的BCD码为24位,需让六个数码管都进行显示,故数码管选择显示部 分就可去掉。数码管BCD码显示模块框图如图 41‑16所示。

模块框图

RTC017

图 41‑16 数码管BCD码显示模块框图

代码编写

数码管bcd码显示代码如代码清单 41‑2所示。

代码清单 41‑2 数码管bcd码驱动模块参考代码(seg_bcd_disp.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
module seg_bcd_disp
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低有效
input wire [23:0] data_bcd , //数码管要显示的值
input wire [5:0] point , //小数点显示,高电平有效
input wire seg_en , //数码管使能信号,高电平有效
input wire sign , //符号位,高电平显示负号

output reg [5:0] sel , //数码管位选信号
output reg [7:0] seg //数码管段选信号
);

////
//\* Parameter and Internal Signal \//
////

//parameter define
parameter CNT_MAX = 16'd49_999; //数码管刷新时间计数最大值

//reg define
reg [15:0] cnt_1ms ; //1ms计数器
reg flag_1ms ; //1ms标志信号
reg [2:0] cnt_sel ; //数码管位选计数器
reg [5:0] sel_reg ; //位选信号
reg [3:0] data_disp ; //当前数码管显示的数据
reg dot_disp ; //当前数码管显示的小数点

////
//\* Main Code \//
////

//cnt_1ms:1ms循环计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_1ms <= 16'd0;
else if(cnt_1ms == CNT_MAX)
cnt_1ms <= 16'd0;
else
cnt_1ms <= cnt_1ms + 1'b1;

//flag_1ms:1ms标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
flag_1ms <= 1'b0;
else if(cnt_1ms == CNT_MAX - 1'b1)
flag_1ms <= 1'b1;
else
flag_1ms <= 1'b0;

//cnt_sel:从0到5循环数,用于选择当前显示的数码管
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_sel <= 3'd0;
else if((cnt_sel == 3'd5) && (flag_1ms == 1'b1))
cnt_sel <= 3'd0;
else if(flag_1ms == 1'b1)
cnt_sel <= cnt_sel + 1'b1;
else
cnt_sel <= cnt_sel;

//数码管位选信号寄存器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
sel_reg <= 6'b000_000;
else if((cnt_sel == 3'd0) && (flag_1ms == 1'b1))
sel_reg <= 6'b000_001;
else if(flag_1ms == 1'b1)
sel_reg <= sel_reg << 1;
else
sel_reg <= sel_reg;

//控制数码管的位选信号,使六个数码管轮流显示
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_disp <= 4'b0;
else if((seg_en == 1'b1) && (flag_1ms == 1'b1))
case(cnt_sel)
3'd0: data_disp <= data_bcd[3:0] ; //给第1个数码管赋个位值
3'd1: data_disp <= data_bcd[7:4] ; //给第2个数码管赋十位值
3'd2: data_disp <= data_bcd[11:8] ; //给第3个数码管赋百位值
3'd3: data_disp <= data_bcd[15:12]; //给第4个数码管赋千位值
3'd4: data_disp <= data_bcd[19:16]; //给第5个数码管赋万位值
3'd5: data_disp <= data_bcd[23:20]; //给第6个数码管赋十万位值
default:data_disp <= 4'b0 ;
endcase
else
data_disp <= data_disp;

//dot_disp:小数点低电平点亮,需对小数点有效信号取反
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
dot_disp <= 1'b1;
else if(flag_1ms == 1'b1)
dot_disp <= ~point[cnt_sel];
else
dot_disp <= dot_disp;

//控制数码管段选信号,显示数字

always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
seg <= 8'b1111_1111;
else
case(data_disp)
4'd0 : seg <= {dot_disp,7'b100_0000}; //显示数字0
4'd1 : seg <= {dot_disp,7'b111_1001}; //显示数字1
4'd2 : seg <= {dot_disp,7'b010_0100}; //显示数字2
4'd3 : seg <= {dot_disp,7'b011_0000}; //显示数字3
4'd4 : seg <= {dot_disp,7'b001_1001}; //显示数字4
4'd5 : seg <= {dot_disp,7'b001_0010}; //显示数字5
4'd6 : seg <= {dot_disp,7'b000_0010}; //显示数字6
4'd7 : seg <= {dot_disp,7'b111_1000}; //显示数字7
4'd8 : seg <= {dot_disp,7'b000_0000}; //显示数字8
4'd9 : seg <= {dot_disp,7'b001_0000}; //显示数字9
default:seg <= 8'b1100_0000;
endcase

//sel:数码管位选信号赋值
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
sel <= 6'b000_000;
else
sel <= sel_reg;

endmodule

数码管bcd码显示模块顶层模块代码如代码清单 41‑3

代码清单 41‑3 数码管bcd码显示模块顶层模块参考代码(seg_595_bcd.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
module seg_595_bcd
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低有效
input wire [23:0] data_bcd , //数码管要显示的bcd码数值
input wire [5:0] point , //小数点显示,高电平有效
input wire seg_en , //数码管使能信号,高电平有效

output wire stcp , //输出数据存储寄时钟
output wire shcp , //移位寄存器的时钟输入
output wire ds , //串行数据输入
output wire oe //输出使能信号

);

////
//\* Parameter And Internal Signal \//
////

//wire define
wire [5:0] sel; //数码管位选信号
wire [7:0] seg; //数码管段选信号

////
//\* Main Code \//
////

seg_bcd_disp seg_bcd_disp_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n), //复位信号,低有效
.data_bcd (data_bcd ), //数码管要显示的值
.point (point ), //小数点显示,高电平有效
.seg_en (seg_en ), //数码管使能信号,高电平有效

.sel (sel ), //数码管位选信号
.seg (seg ) //数码管段选信号

);

hc595_ctrl hc595_ctrl_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n), //复位信号,低有效
.sel (sel ), //数码管位选信号
.seg (seg ), //数码管段选信号

.stcp (stcp ), //输出数据存储寄时钟
.shcp (shcp ), //移位寄存器的时钟输入
.ds (ds ), //串行数据输入
.oe (oe )

);

endmodule

该模块是对子模块的例化,我们只需要数码管驱动模块例化为数码管bcd码驱动模块即可。至于74HC595芯片的控制模块我们不需要改动,直接调用前面章节使用的74HC595芯片控制模块即可。

11.3.3.6. 顶层模块

模块框图

rtc顶层模块主要是对各个子功能模块的实例化,以及对应信号的连接,模块框图如图 41‑17所示。

RTC018

图 41‑17 rtc顶层模块框图

模块各输入输出信号描述如表格 41‑10所示。

表格 41‑10 rtc顶层模块输入输出信号描述

信号

位宽

类型

功能描述

sys_clk

1bit

input

系统时钟,50MHz

sys_rst_n

1bit

input

复位信号,低电平信号

key_in

1bit

input

按键输入信号

scl

1bit

output

输出至pcf8536的串行时钟信号scl

sda

1bit

output

输出至pcf8536的串行数据信号sda

stcp

1bit

output

存储寄存器时钟

shcp

1bit

output

移位寄存器时钟

ds

1bit

output

串行数据

oe

1bit

output

输出使能,低有效

代码编写

顶层代码编写较为容易,无需波形图的绘制。顶层参考代码,具体见代码清单 41‑4。

代码清单 41‑4 rtc顶层模块参考代码(rtc.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
module rtc
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire key_in , //按键信号

output wire stcp , //输出数据存储寄时钟
output wire shcp , //移位寄存器的时钟输入
output wire ds , //串行数据输入
output wire oe , //输出使能信号
output wire scl , //输出至pcf8536的串行时钟信号scl

inout wire sda //输出至pcf8536的串行数据信号sda

);

////
//\* Parameter And Internal Signal \//
////

//parameter define
//设置实时时钟初始值分别为:年_月_日_时_分_秒
parameter TIME_INIT = 48'h20_06_08_08_00_00;

//wire define
wire i2c_clk ; //i2c驱动时钟
wire i2c_end ; //i2c一次读/写操作完成
wire [7:0] rd_data ; //输出i2c设备读取数据
wire key_flag ; //按键消抖后标志信号
wire wr_en ; //写使能信号
wire rd_en ; //读使能信号
wire i2c_start ; //i2c触发信号
wire [15:0] byte_addr ; //i2c字节地址
wire [7:0] wr_data ; //写入i2c设备数据
wire [23:0] data_out ; //数码管显示数据

////
//\* Instantiation \//
////

//-------------pcf8563_ctrl_inst--------------
pcf8563_ctrl
#(
.TIME_INIT (TIME_INIT)
)
pcf8563_ctrl_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.i2c_clk (i2c_clk ), //i2c驱动时钟
.sys_rst_n (sys_rst_n), //复位信号,低有效
.i2c_end (i2c_end ), //i2c一次读/写操作完成
.rd_data (rd_data ), //输出i2c设备读取数据
.key_flag (key_flag ), //按键消抖后标志信号

.wr_en (wr_en ), //输出写使能信号
.rd_en (rd_en ), //输出读使能信号
.i2c_start (i2c_start), //输出i2c触发信号
.byte_addr (byte_addr), //输出i2c字节地址
.wr_data (wr_data ), //输出i2c设备数据
.data_out (data_out ) //输出到数码管显示的bcd码数据

);

//-------------seg_595_bcd_inst--------------
seg_595_bcd seg_595_bcd_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n), //复位信号,低有效
.data_bcd (data_out ), //数码管要显示的bcd码数值
.point (6'b010100), //小数点显示,高电平有效
.seg_en (1'b1 ), //数码管使能信号,高电平有效

.stcp (stcp ), //输出数据存储寄时钟
.shcp (shcp ), //移位寄存器的时钟输入
.ds (ds ), //串行数据输入
.oe (oe ) //输出使能信号

);

//-------------i2c_ctrl_inst--------------
i2c_ctrl
#(
.DEVICE_ADDR (7'b1010_001 ), //i2c设备地址
.SYS_CLK_FREQ (26'd50_000_000), //输入系统时钟频率
.SCL_FREQ (18'd250_000 ) //i2c设备scl时钟频率
)
i2c_ctrl_inst(
.sys_clk (sys_clk ), //输入系统时钟,50MHz
.sys_rst_n (sys_rst_n), //输入复位信号,低电平有效
.wr_en (wr_en ), //输入写使能信号
.rd_en (rd_en ), //输入读使能信号
.i2c_start (i2c_start), //输入i2c触发信号
.addr_num (1'b0 ), //输入i2c字节地址字节数
.byte_addr (byte_addr), //输入i2c字节地址
.wr_data (wr_data ), //输入i2c设备数据

.i2c_clk (i2c_clk ), //i2c驱动时钟
.i2c_end (i2c_end ), //i2c一次读/写操作完成
.rd_data (rd_data ), //输出i2c设备读取数据

.i2c_scl (scl ), //输出至i2c设备的串行时钟信号scl
.i2c_sda (sda ) //输出至i2c设备的串行数据信号sda
);

//-------------key_fifter_inst--------------
key_filter
#(
.CNT_MAX (24'd999_999)
)
key_filter_inst
(
.sys_clk (sys_clk ), //系统时钟50Mhz
.sys_rst_n (sys_rst_n), //全局复位
.key_in (key_in ), //按键输入信号

.key_flag (key_flag ) //按键消抖后输出信号
);

endmodule

代码23行是对时钟的初始值进行设置;代码第70行是对数码管显示小数点赋值,对左侧第二和第四赋值即可,因为在数码管模块定义的小数点是高电平显示,所以给“1”才能显示;代码第83行是对设备地址进行赋值,输入pcf8563的器件地址即可。代码第93行是对写入字节数赋值,由于pcf8563地址数为一个字节, 所以直接给其赋0即可。

11.3.3.7. RTL视图

编译完成后,我们查看一下RTL视图,仔细看RTL视图展示信息与顶层模块框图是一致的,具体见图 41‑18。

RTC019

图 41‑18 实验工程RTL视图

11.3.3.8. SignalTap波形抓取

由于该工程的仿真文件不易产生,所以我们可以利用quartus软件自带的SignalTap工具对波形进行抓取。

RTC020

图 41‑19 SignalTap抓取波形图(一)

我们将时间初始值设为:48’h19_09_09_16_15_20。如图 41‑19所示抓取的是数码管显示的初值与设置的初值是一样。

RTC021

图 41‑20 SignalTap抓取波形图(二)

如图 41‑20所示:运行一段时间后,我们抓取的波形图显示的值与初始值不同了,这说明实时时钟在实时变化。

RTC022

图 41‑21 SignalTap抓取波形图(三)

如图 41‑21所示:当按下按键时,数码管显示数据(data_out)从时、分、秒切换到了年、月、日数据,且显示的年、月、日数据与我们设置的也是一致的。

从以上抓取的波形图可以看到我们的设计与我们绘制的波形图是一致的,并且能达到我们的实验要求。

11.3.4. 上板调试

11.3.4.1. 引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 41‑11所示。

表格 41‑11 引脚分配表

信号名

信号类型

对应引脚

备注

sys_clk

input

E1

时钟

sys_rst_n

input

M15

复位

key_in

input

M2

按键

scl

input

P15

I2C时钟线

sda

inout

N14

I2C数据线

stcp

output

K9

存储寄存器时钟

shcp

output

B1

移位寄存器时钟

ds

output

R1

串行数据

oe

output

L11

输出使能,低有效

下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 41‑22所示。

RTC023

图 41‑22 管脚分配

11.3.4.2. 结果验证

管脚配置完成之后重新进行编译,编译完之后就可以进行下载验证了,在下载之前首先将电源与下载线与开发板连接好,连接好后上电,如图 41‑23所示。

RTC024

图 41‑23 下载连线图

打开下载界面后,当检测到下载器(USB-Blaster)已连接之后,即可点击“Add File…”添加sof文件,添加好后点击“start”开始下次,随后界面会显示下载成功,如图 41‑24所示。

RTC025

图 41‑24 下载成功界面

下载成功后即可以开始验证了。刚开始数码管上显示的是实时时钟的时分秒值,初值为我们顶层模块所设置的初值,且该时分秒值会随时间值跳转。当按下按键KEY1时数码管显示的值变为年月日。

11.4. 章末总结

该实验的关键在于用i2c总线对RTC芯片进行配置,写入以及读取。大家只要掌握了i2c的控制时序以及RTC数据手册上的关键信息,对RTC芯片控制起来就轻而易举了。

11.5. 拓展训练

更改代码,使用FPGA上的按键资源,设计一个可通过FPGA上的按键设置时钟初始值的实时时钟,显示在数码管上。