21. rtc实时时钟¶
RTC(英文全称:Real-time clock,中文名称:实时时钟)是指可以像时钟一样输出实际时间的电子设备,RTC是集成电路,因此也称为时钟芯片。本次实验将使用型号为PCF8563的多功能时钟/日历芯片来进行时间、日期的实时显示。
21.1. 理论学习¶
21.1.1. pcf8563简介¶
PCF8563 是飞利浦公司推出的一款工业级内含I2C总线接口功能的具有极低功耗的多功能时钟/日历芯片。PCF8563的多种报警功能、定时器功能、时钟输出功能以及中断输出功能,能完成各种复杂的定时服务。是一款性价比极高的时钟芯片,它已被广泛用于电表、水表、气表 、电话、传真机 、便携式仪器以及电池供电的仪器仪表等产品领域。
PCF8563内部功能框图如图 50‑1所示。
图 50‑1 PCF8563内部功能框图
如图 50‑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 寄存器被读时,所有计数器的内容被锁存,因此,在传送条件下,可以防止对时钟日历芯片的错读。
21.1.2. PCF8563寄存器介绍¶
本次实验是对时间以及日期进行显示,下面对相关的寄存器进行介绍,其余寄存器大家可参考《PCF8563数据书册》进行了解。
21.1.2.1. 秒寄存器(02h)¶
表格 50‑1 秒寄存器描述
bit |
符号 |
值 |
描述 |
---|---|---|---|
7 |
VL |
0 |
保证准确的时钟数据 |
1 |
无法保证准确的时钟数据 |
||
6:4 |
秒 |
0~5 |
十位数据(BCD码) |
3:0 |
0~9 |
个位数据(BCD码) |
这里需要注意的是,不管是秒数据还是分、时、时、日、月、年数据都是以BCD格式编码的实际数据,编码格式请参阅图 50‑2。
图 50‑2 BCD编码图
21.1.2.5. 月寄存器(07h)¶
表格 50‑5 月寄存器描述
bit |
符号 |
值 |
描述 |
---|---|---|---|
7 |
C |
0 |
当前世纪 |
1 |
下一世纪 |
||
6:5 |
不使用 |
||
4 |
月 |
0~1 |
十位数据(BCD码) |
3:0 |
0~9 |
个位数据(BCD码) |
21.1.2.6. 年寄存器(08h)¶
表格 50‑6 年寄存器描述
bit |
符号 |
值 |
描述 |
---|---|---|---|
7:4 |
年 |
0~9 |
十位数据(BCD码) |
3:0 |
0~9 |
个位数据(BCD码) |
年数据为当前年份的末两位。
21.1.3. PCF8563通信接口¶
PCF8563是通过I2C接口与FPGA进行通信,如图 50‑3所示:
图 50‑3 I2C接口时序图
由上图可以看到:上电到SCL下降沿(起始信号)需等待8ms的时间,也就是我们上电之后需先等待8ms后再开始对PCF8563芯片进行配置。
图 50‑4 PCF8563器件地址
如图 50‑4所示,可知PCF8563的器件地址为7’b1010001。
图 50‑5 PCF8563写时序
如图 50‑5为PCF8563写时序,即用该时序对各寄存器写入初始值。其中S:表示I2C起始信号;R/W:读写控制位(0表示写,1表示读);A:表示应答信号;P:表示I2C停止信号。
图 50‑6 PCF8563读时序
如图 50‑6为PCF8563的读时序,即用该时序读出各寄存器里的时间、日期值。其中S:表示I2C起始信号;R/W:读写控制位(0表示写,1表示读);A:表示应答信号;P:表示I2C停止信号。
读写数据都是通过i2C总线去读写的,详细的I2C控制时序方法大家可参考前面《基于I2C协议的 EEPROM驱动控制》章节进行学习。
通过以上的学习我们可大致描述一下驱动PCF8563的流程:通过I2C总线对秒、分、使、日、月、年寄存器先写入初始值,初始值写完后再对秒、分、使、日、月、年寄存器进行读取。
21.2. 实战演练¶
21.2.1. 实验目标¶
实验目标:通过数码管显示实时时间,并通过按键切换显示年月日。
21.2.2. 硬件资源¶
本次实验我们需使用到开发板上的按键、数码管资源,同时还需使用开发板上的板载PCF8563实时时钟芯片。其中按键和数码管硬件资源在前面章节已有讲解,这里就不再过多描述。开发板上的实时时钟芯片如图 50‑7所示。
图 50‑7 PCF8563实时时钟芯片
其原理图如图 49‑6所示,从该图中可以看到该传感器是通过I2C总线与FPGA芯片进行通信的。
图 50‑8 环境光传感器原理图
21.3. 程序设计¶
21.3.1. 整体说明¶
根据实验目标可知,要实现这个功能,数码管显示模块是必不可少的,同时我们是用i2c总线对其控制的,所以还需要一个i2c配置模块以及对PCF8563传感器的控制模块,最后还需要一个按键消抖模块实现按键切换显示功能。该工程的整体框图如图 50‑9所示:
图 50‑9 实时时钟显示整体框图
通过上图可以看到,该工程共分五个模块。各模块简介见表格 50‑7。
表格 50‑7 rtc工程模块简介
模块名称 |
功能描述 |
---|---|
pcf8563_ctrl |
pcf8563控制模块 |
i2c_ctrl |
iic驱动模块 |
key_filter |
按键消抖模块 |
seg_595_bcd |
数码管bcd码显示模块 |
rtc |
工程顶层模块 |
通过模块框图为大家讲述该工程的工作流程:通过pcf8563_ctrl模块和i2c_ctrl模块控制PCF8563进行实时的时钟和日期输出到seg_595_bcd模块进行驱动显示,同时按下按键时控制显示数据的切换。
下面分模块为大家讲解。
21.3.1.1. 按键消抖模块¶
在前面按键消抖章节已经对按键消抖模块进行了详细的讲解,在此就不再进行说明,直接调用这个模块即可。
21.3.1.2. i2c驱动模块¶
在《基于I2C协议的 EEPROM驱动控制》章节已经详细地讲解了驱动方法,在此就不再叙述,直接调用这个模块即可。
21.3.1.3. pcf8563控制模块¶
该模块通过i2c驱动模块对pcf8563进行初始值设置以及数据读取。
模块框图
图 50‑10 pcf8563控制模块
这个模块的作用就是配置时钟、日期的初始值,配置完后进行读取。模块各个信号的简介,如表格 50‑8所示。
表格 50‑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的工作过程,如图 50‑11所示。
图 50‑11 pcf8563控制模块状态跳转图
表格 50‑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总线写入时钟的初始值来进行设置。对秒、分、时、日、月、年初值设置完成之后,开始对时钟数据读取,因为我们是显示日期和时间,所以我们需要读取 秒、分、时、日、月、年的值。因为是实时时钟,时钟数据是一直变化的,所以我们需要一直不断的读取时钟的值。
下面再通过波形图来看看具体如何去实现对时钟的初始化以及时钟数据的读取。
波形图绘制
图 50‑12 pcf8563控制模块波形图(一)
由于整个控制时序过长,所以在这里将分开为大家讲解。如图 50‑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的初始时间值:年、月、日、时、分、秒由高位到低位排列。如图 50‑12所示,初始化什么值我们就给什么数据。当依次设置完初始时间值后,我们便可以开始读取数据,波形图如图 50‑13所示。
图 50‑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 数据赋为秒数据即可。同理分、时、日、月、年数据也在各读取状态读取完后赋值即可。
当数据读取完成之后,波形图如图 50‑14所示。
图 50‑14 pcf8563控制模块波形图(三)
如上图所示:当一次时钟数据读取完成之后,状态跳回读取秒数据状态(RD_SEC),开始下一次时钟数据的读取。
数据读完之后,我们再看看如何使用按键进行时间数据切换,波形图如图 50‑15所示。
图 50‑15 按键切换数据显示波形图
如上图所示:如果我们只用按键消抖标志信号(key_flag)去控制数据的切换,这并不容易实现。所以这里我们需要用key_flag信号产成一个data_flag信号来控制数据的切换。因为我们按一次按键数据切换一次且变化后的数据在下一个按键到来时才会切换,所以我们需要一个按键信号到来前保持不变的标志信号 。如图data_flag所示,开始时data_flag为低电平,此时输出时分秒的值进行显示;当按下按键时,data_flag信号取反变为高电平,在data_flag为高电平时输出年月日的值进行显示;这就实现了按键切换显示的功能。
代码编写
参照绘制波形图,编写模块代码。模块参考代码,具体见代码清单 50‑1。
代码清单 50‑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 | module pcf8563_ctrl
#(
parameter TIME_INIT = 48'h19_09_07_16_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去采样,将几乎采不到这个标志信号。
21.3.1.4. 数码管BCD码显示模块¶
该模块与“数码管的动态显示”章节的数码管动态显示大致相同,唯一的区别就是:前面章节的动态数码管输入的是十进制数据,而数码管bcd码显示模块输入的是bcd码数据。所以我们将原来的数码管动态显示章节中的二进制转BCD码模块去掉,同时我们输入的BCD码为24位,需让六个数码管都进行显示,故数码管选择显示部 分就可去掉。数码管BCD码显示模块框图如图 50‑16所示。
模块框图
图 50‑16 数码管BCD码显示模块框图
代码编写
数码管bcd码显示代码如代码清单 50‑2所示。
代码清单 50‑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 | 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码显示模块顶层模块代码如代码清单 50‑3
代码清单 50‑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芯片控制模块即可。
21.3.1.5. 顶层模块¶
模块框图
rtc顶层模块主要是对各个子功能模块的实例化,以及对应信号的连接,模块框图如图 50‑17所示。
图 50‑17 rtc顶层模块框图
模块各输入输出信号描述如表格 50‑10所示。
表格 50‑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 |
输出使能,低有效 |
代码编写
顶层代码编写较为容易,无需波形图的绘制。顶层参考代码,具体见代码清单 50‑4。
代码清单 50‑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'h19_09_08_17_15_20;
//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), //复位信号,低有效
.
ta_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即可。
21.3.1.7. SignalTap波形抓取¶
由于该工程的仿真文件不易产生,所以我们可以利用quartus软件自带的SignalTap工具对波形进行抓取。
图 50‑19 SignalTap抓取波形图(一)
我们将时间初始值设为:48’h19_09_09_16_15_20。如图 50‑19所示抓取的是数码管显示的初值与设置的初值是一样。
图 50‑20 SignalTap抓取波形图(二)
如图 50‑20所示:运行一段时间后,我们抓取的波形图显示的值与初始值不同了,这说明实时时钟在实时变化。
图 50‑21 SignalTap抓取波形图(三)
如图 50‑21所示:当按下按键时,数码管显示数据(data_out)从时、分、秒切换到了年、月、日数据,且显示的年、月、日数据与我们设置的也是一致的。
从以上抓取的波形图可以看到我们的设计与我们绘制的波形图是一致的,并且能达到我们的实验要求。
21.4. 上板调试¶
21.4.1. 引脚约束¶
仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 50‑11所示。
表格 50‑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 |
输出使能,低有效 |
下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 50‑22所示。
图 50‑22 管脚分配
21.4.1.1. 结果验证¶
管脚配置完成之后重新进行编译,编译完之后就可以进行下载验证了,在下载之前首先将电源与下载线与开发板连接好,连接好后上电,如图 50‑23所示。
图 50‑23 下载连线图
打开下载界面后,当检测到下载器(USB-Blaster)已连接之后,即可点击“Add File…”添加sof文件,添加好后点击“start”开始下次,随后界面会显示下载成功,如图 50‑24所示。
图 50‑24 下载成功界面
下载成功后即可以开始验证了。刚开始数码管上显示的是实时时钟的时分秒值,初值为我们顶层模块所设置的初值,且该时分秒值会随时间值跳转。当按下按键KEY1时数码管显示的值变为年月日。
21.5. 章末总结¶
该实验的关键在于用i2c总线对RTC芯片进行配置,写入以及读取。大家只要掌握了i2c的控制时序以及RTC数据手册上的关键信息,对RTC芯片控制起来就轻而易举了。
21.6. 拓展训练¶
更改代码,使用FPGA上的按键资源,设计一个可通过FPGA上的按键设置时钟初始值的实时时钟,显示在数码管上。