前面的章节中我们学习了扫描式数码管模块和旋转编码器模块的工作原理及驱动方法,也对I2C总线协议及相关知识,本实验主要对I2C总线驱动方法加以练习,同时完成数字万年历时间调节和显示控制的逻辑,最终完成数字万年历总体设计。
根据前面的实验解析我们可以得知,该设计可以拆分成几个功能模块实现,
STEP BaseBoard V3.0底板上的实时时钟芯片DS1340Z模块电路图如下(上拉电阻未显示):
我们的实时时钟芯片为DS1340Z-33,模块电路中有电池座,电池电压范围为1.3V~5.5V,当安装电池后底板掉电不影响实时时钟芯片的运行,重新上电后读取实时时钟数据。
实时时钟芯片DS1340Z需要外置32.768KHz的晶体,芯片内部集成起振电阻电容等电路,晶体直接连接即可。
前面实验中我们已经讲述学习过I2C总线驱动的设计,本实验可以上原来的基础上调整,首先来了解DS1340Z时序中的参数要点。
通过DS1340Z时序参数了解,DS1340Z支持I2C通信400KHz快速模式同时兼容100KHz的标准模式,还有两种模式下时序中的各种时间参数,所以通信速度不需要调整。
I2C时序基本单元(启动、停止、发送、接收、发应答、读应答)协议里统一的,所以所以基本单元状态的设计也是不需要调整的。
DS1340Z芯片有很多寄存器,用于存储实时时钟的时间信息,例如地址为00H的寄存器中,bit7为晶体使能控制位,低有效,默认使能,bit6~bit0为秒钟数据,且是BCD码的格式(bit6~bit4代表秒钟的十位,bit3~bit0代表秒钟的个位),当需要调整秒钟时间时,对00H寄存器写操作,当读取秒钟时间时, 对00H寄存器读操作。其他寄存器也是一样,详细请参考每个寄存器的功能说明。
本实验涉及DS1340Z的写寄存器和读寄存器操作,查看手册给出的操作时序流程。
芯片支持连续写寄存器操作(寄存器地址自加1),时序流程如下:
根据连续写寄存器时序流程,其设计程序实现如下:
MAIN:begin if(cnt_main >= 6'd11) //对MAIN中的子状态执行控制cnt_main cnt_main <= 6'd0; // else cnt_main <= cnt_main + 1'b1; case(cnt_main) 6'd0: begin state <= START; end //I2C通信时序中的START 6'd1: begin data_wr <= 8'hd0; state <= WRITE; end //写地址为8'hd0 6'd2: begin data_wr <= 8'h00; state <= WRITE; end //8'h00,起始寄存器 6'd3: begin data_wr <= adj_sec; state <= WRITE; end //00寄存器地址,写秒 6'd4: begin data_wr <= adj_min; state <= WRITE; end //01寄存器地址,写分 6'd5: begin data_wr <= adj_hour; state <= WRITE; end //02寄存器地址,写时 6'd6: begin data_wr <= adj_week; state <= WRITE; end //03寄存器地址,写周 6'd7: begin data_wr <= adj_day; state <= WRITE; end //04寄存器地址,写日 6'd8: begin data_wr <= adj_mon; state <= WRITE; end //05寄存器地址,写月 6'd9: begin data_wr <= adj_year; state <= WRITE; end //06寄存器地址,写年 6'd10: begin data_wr <= 8'h40; state <= WRITE; end //07寄存器地址,8'h40 6'd11: begin state <= STOP; end //I2C通信时序中的STOP default: state <= IDLE; //如果程序失控,进入IDLE自复位状态 endcase end
芯片支持连续读寄存器操作(寄存器地址自加1),时序流程如下:
根据连续写寄存器时序流程,其设计程序实现如下:
MAIN:begin if(cnt_main >= 6'd32) //对MAIN中的子状态执行控制cnt_main cnt_main <= 6'd12; //否则只执行时间读取操作 else cnt_main <= cnt_main + 1'b1; case(cnt_main) 6'd12: begin state <= START; end //I2C通信时序中的START 6'd13: begin data_wr <= 8'hd0; state <= WRITE; end //写地址为8'hd0 6'd14: begin data_wr <= 8'h00; state <= WRITE; end //8'h00,寄存器初始地址 6'd15: begin state <= START; end //I2C通信时序中的START 6'd16: begin data_wr <= 8'hd1; state <= WRITE; end //读地址为8'hd1 6'd17: begin ack <= ACK; state <= READ; end //读秒 6'd18: begin rtc_sec <= rtc_data_r; end 6'd19: begin ack <= ACK; state <= READ; end //读分 6'd20: begin rtc_min <= rtc_data_r; end 6'd21: begin ack <= ACK; state <= READ; end //读时 6'd22: begin rtc_hour <= rtc_data_r; end 6'd23: begin ack <= ACK; state <= READ; end //读周 6'd24: begin rtc_week <= rtc_data_r; end 6'd25: begin ack <= ACK; state <= READ; end //读日 6'd26: begin rtc_day <= rtc_data_r; end 6'd27: begin ack <= ACK; state <= READ; end //读月 6'd28: begin rtc_mon <= rtc_data_r; end 6'd29: begin ack <= ACK; state <= READ; end //读年 6'd30: begin rtc_year <= rtc_data_r; end 6'd31: begin ack <= NACK; state <= READ; end //控制 6'd32: begin state <= STOP; end //I2C通信时序中的STOP,读取完成标志 default: state <= IDLE; //如果程序失控,进入IDLE自复位状态 endcase end
上面两段程序就是对于DS1340Z芯片的两种操作,调时间和读时间,对于万年历来说因为有电池供电,实时时钟一直都处于工作状态,当给FPGA上电时只需要读时间即可,只有遇到时间不对的时候才需要调时间,所以DS1340Z驱动模块平时都在循环读取时间,所以如果将调时间和读时间的时序操作融合到同一个状态下时,对于cntmain要加以控制,cntmain初值为12,且运行轨迹在12~32之间,控制程序调整如下:
if(cnt_main >= 6'd32) //对MAIN中的子状态执行控制cnt_main if(set_flag)cnt_main <= 6'd0; //当set_flag被置位时才会执行时间写入操作 else cnt_main <= 6'd12; //否则只执行时间读取操作 else cnt_main <= cnt_main + 1'b1;
上面setflag为时间调整标志位,只有按动编码器在调时间模式时需要用到写时间数据的操作流程,可以根据按键脉冲置位setflag并自锁,每次完成写入操作后再将set_flag复位。程序实现如下:
reg set_flag; always@(posedge clk or negedge rst_n) begin if(!rst_n) set_flag <= 1'b0; else if(cnt_main==5'd11) set_flag <= 1'b0; //完成写入时间操作复位set_flag else if(key_set) set_flag <= 1'b1; //按键脉冲控制set_flag置位 else set_flag <= set_flag; end
模块端口如下:
module DS1340Z_driver ( input clk, rst_n, //系统时钟和复位 input key_set, //按动脉冲输入 input [7:0] adj_hour, adj_min, adj_sec, //时分秒调整输入 input [7:0] adj_year, adj_mon, adj_day, adj_week, //年份调整输入 output i2c_scl, //I2C总线SCL inout i2c_sda, //I2C总线SDA output [7:0] rtc_hour, rtc_min, rtc_sec, //实时时钟输出 output [7:0] rtc_year, rtc_mon, rtc_day, rtc_week //实时年份输出 );
到这里就完成了万年历中DS1340Z模块的驱动设计,宏观上讲,该模块的功能可以这样描述:
控制模块包含多个功能的设计:模式控制、调时控制、显示控制,可以细化成多个模块实现,本实验例程中就写在了一个模块下,我们会针对这三个功能分别讲解其实现方法及原理。
模式控制
项目要求设计成8个模式(常态、调年、调月、调日、调周、调时、调分、调秒),对8个状态编码,常态—0、调秒—1、调分—2、调时—3、调周—4、调日—5、调月—6、调年—7,通过按动旋转编码器切换,按照常识调时间从大到小调节,先调节年份最后调秒钟,所以我们这8个状态的状态机跳转顺序是固定的(0→7→6→5→4→3→2→1→0),依次循环跳转,程序实现如下:
//时钟运行状态控制 always@(posedge clk or negedge rst_n ) if(!rst_n) state <= 3'd0; else if(O_pulse) //按键脉冲控制时钟运行状态的跳变, if(state) state <= state - 3'd1; else state <= 3'd7; else state <= state;
调时控制
调时控制在不同的调节模式对不同时间进行调整,我们分别以常态模式和调秒模式为例进行分析。
万年历时间调节要以当时的时间为基础,常态模式下不需要调整任何时间,但是可以将实时时钟读出的时间数据赋给调节变量,这样等跳转到调节模式时对调节变量的控制就是以当时的时间为基础了,程序实现如下:
3'd0: //正常模式 begin if(O_pulse)begin //在常态下按动编码器将当前实时时间赋值给调节寄存器 adj_sec <= rtc_sec; adj_min <= rtc_min; adj_hour <= rtc_hour; adj_week <= rtc_week; adj_day <= rtc_day; adj_mon <= rtc_mon; adj_year <= rtc_year; end end
调秒模式与其他调节模式操作一样,不同的是调节的规则不同,例如秒和分的调节范围为0~59,小时调节范围0~11或0~23,日期调节范围需要考虑年和月的值(1、3、5、7、8、10、12月范围1~31,4、6、9、11月范围1~30,2月平年范围1~28,2月闰年范围1~29),周调节范围1~7,月调节范围1~12,年调节范围0~99。对秒钟数据进行调节,程序实现如下:
3'd1: //调秒模式 begin if(L_pulse) begin //逆时针转 if(adj_sec[3:0]) adj_sec <= adj_sec - 1'h1; else if(adj_sec[7:4]) adj_sec <= {adj_sec[7:4]-1'h1,4'h9}; else adj_sec <= 8'h59; end else if(R_pulse) begin //顺时针转 if(adj_sec[3:0]!=4'h9) adj_sec <= adj_sec + 1'h1; else if(adj_sec[7:4]!=4'h5) adj_sec <= {adj_sec[7:4]+1'h1,4'h0}; else adj_sec <= 8'h00; end else adj_sec <= adj_sec; end
显示控制
首先来看看一下数码管要显示的效果,8位数码管分两页显示万年历数据,第一页显示年月日周,第二页显示时分秒。
我们看到任何一项时间选项都由两位数码管显示,每页最多显示4个时间选项,我们可以使用4位的变量dispen[3:0]控制4个时间选项的点亮或熄灭,dispen[3]控制最左侧两个数码管,disp_en[0]控制最右侧两个数码管,我们分别以常态模式和调秒模式为例进行显示使能控制的分析。
常态模式下,转动编码器控制显示页码,两个页码对应的显示控制,程序实现如下:
3'd0: //正常模式 if(L_pulse) disp_en <= 4'b1111; //逆时针转显示第一页,数码管全亮 else if(R_pulse) disp_en <= 4'b0111; //顺时针转显示第二页,时分秒亮 else disp_en <= disp_en;
调秒模式下,小时和分钟数码管点亮,秒钟闪烁显示,转动编码器时秒钟强制显示,最后按动旋转编码器切到常态模式时,时分秒数码管都回复显示,程序实现如下:
3'd1: begin //调秒模式 disp_en[3:1] <= 3'b011; //时和分显示 if(L_pulse|R_pulse) disp_en[0] <= 1'b1; //转动时强制显示 else if(sec_pulse) disp_en[0] <= ~disp_en[0]; //秒钟闪烁显示 else if(O_pulse) disp_en <= 4'b0111; //返回常态时显示时分秒 else disp_en[0] <= disp_en[0]; end
前面分析了显示控制,主要对时间选项的点亮还是熄灭做控制,对应到数码管上就转化成数码管位的点亮和熄灭控制。另外还包含显示数据的控制,而这部分设计我们放到顶层模块中实现了,我们来分析一下。
数码管点亮控制
数码管与时间选项是对应关系,每个选项对应两位数码管,程序实现如下:
wire [7:0] data_en = {{2{disp_en[3]}},{2{disp_en[2]}},{2{disp_en[1]}},{2{disp_en[0]}}}; //数码管位选控制 wire [7:0] dot_en = {1'b0,disp_en[3],1'b0,disp_en[2],1'b0,disp_en[1],1'b0,disp_en[0]}; //数码管小数点显示控制
数码管内容控制
万年历的显示分两页实现,我们以最右侧两个数码管显示内容为例,这两位数码管在第一页中显示周数据,在第二页中显示秒数据,那么我们怎么控制显示内容呢?分析,万年历8中模式,
1.常态模式下,显示读取的实时时钟数据,具体显示周还是秒再次细化
常态模式下,根据disp_en选择显示周数据还是秒数据,程序实现如下:
wire [7:0] data_rtc0 = disp_en[3]? rtc_week:rtc_sec; //常态下数码管显示数据
2.调节模式下,显示写入的调节时钟数据,具体显示周还是秒再次细化
调节模式下,根据state选择显示周数据还是秒数据,程序实现如下:
wire [7:0] data_adj0 = state[2]? adj_week:adj_sec; //调节状态下数码管显示数据
3.最后根据常态模式还是调节模式控制数码管显示实时时钟数据还是调节时钟数据
根据state选择显示实时时钟数据还是调节时钟数据,程序实现如下:
assign {data_7,data_8} = state? data_adj0:data_rtc3; //根据状态选择显示常态数据还是调节状态数据
综合后的设计框图如下: