**这是本文档旧的修订版!**

=====数字万年历设计=====

实验任务

  • 普通列表项目任务:基于 STEP-MAX10M08核心板 和 STEP BaseBoard V3.0底板 完成数字万年历设计并观察调试结果
  • 普通列表项目要求:驱动底板上的实时时钟芯片DS1340Z获取时间信息(年、月、日、周、时、分、秒),显示在8位数码管上,分两页显示,第一页显示年月日周信息,第二页显示时分秒信息,通过旋转编码器调节数字万年历和控制显示,具体控制如下:
    • 万年历有8个状态(常态、调年、调月、调日、调周、调时、调分、调秒)
    • 按动旋转编码器在8个状态中依次循环切换
    • 常态下,转动编码器切换显示页,逆时针转显示第一页,顺时针转显示第二页
    • 调节状态下,旋转调节时间,逆时针转减,顺时针转加
    • 调节状态下,显示被调节选项所在页,同时被调节选项闪烁显示
  • 普通列表项目解析:通过FPGA编程驱动实时时钟芯片DS1340Z,实现时间写入和读出的功能,驱动旋转编码器获取操作信息,设计一个控制模块根据编码器操作信息控制数字万年历的逻辑(包括时间调节,显示控制),最后驱动数码管显示数字万年历信息。

实验目的

前面的章节中我们学习了扫描式数码管模块和旋转编码器模块的工作原理及驱动方法,也对I2C总线协议及相关知识,本实验主要对I2C总线驱动方法加以练习,同时完成数字万年历时间调节和显示控制的逻辑,最终完成数字万年历总体设计。

  • 复习I2C总线工作原理及通信协议
  • 练习I2C接口驱动设计方法,完成实时时钟芯片DS1340Z驱动设计
  • 根据要求完成数字万年历调时和显示控制的逻辑
  • 完成数字万年历设计实现

设计框图

根据前面的实验解析我们可以得知,该设计可以拆分成几个功能模块实现,

  • Encoder:驱动旋转编码器旋转功能实现,获取转动信息
  • Debounce:驱动旋转编码器按键功能实现,实现按键消抖
  • modectrl:根据编码器操作信息控制万年历时间调节和显示控制逻辑 * DS1340Zdriver:驱动实时时钟芯片DS1340Z完成时间写入和读出功能
  • Segment_scan:通过驱动扫描式数码管将万年历信息显示出来。

Top-Down层次设计 模块结构设计

实验原理

DS1340Z模块介绍

从DS1340Z芯片手册可以得到如下信息,DS1340Z芯片典型电路连接如下:

典型电路连接

DS1340Z芯片管脚功能描述如下(SO-8封装):

管脚功能描述

DS1340Z芯片内部结构图如下:

芯片内部结构图

DS1340Z模块连接

STEP BaseBoard V3.0底板上的实时时钟芯片DS1340Z模块电路图如下(上拉电阻未显示):

DS1340Z模块电路

我们的实时时钟芯片为DS1340Z-33,模块电路中有电池座,电池电压范围为1.3V~5.5V,当安装电池后底板掉电不影响实时时钟芯片的运行,重新上电后读取实时时钟数据。

DS1340Z电压范围

实时时钟芯片DS1340Z需要外置32.768KHz的晶体,芯片内部集成起振电阻电容等电路,晶体直接连接即可。

DS1340Z晶振连接

DS1340Z驱动设计

前面实验中我们已经讲述学习过I2C总线驱动的设计,本实验可以上原来的基础上调整,首先来了解DS1340Z时序中的参数要点。

DS1340Z时序图

DS1340Z时序参数

通过DS1340Z时序参数了解,DS1340Z支持I2C通信400KHz快速模式同时兼容100KHz的标准模式,还有两种模式下时序中的各种时间参数,所以通信速度不需要调整。

  • 普通列表项目分频得到400KHz的时钟,程序实现同智能接近系统设计实验。

I2C时序基本单元(启动、停止、发送、接收、发应答、读应答)协议里统一的,所以所以基本单元状态的设计也是不需要调整的。

  • 启动时序状态设计程序实现同智能接近系统设计实验。
  • 发送单元和读应答单元合并,时序状态设计程序实现同智能接近系统设计实验。
  • 接收单元和写应答单元合并,时序状态设计程序实现同智能接近系统设计实验。
  • 停止时序状态设计程序实现同智能接近系统设计实验。

DS1340Z芯片有很多寄存器,用于存储实时时钟的时间信息,例如地址为00H的寄存器中,bit7为晶体使能控制位,低有效,默认使能,bit6~bit0为秒钟数据,且是BCD码的格式(bit6~bit4代表秒钟的十位,bit3~bit0代表秒钟的个位),当需要调整秒钟时间时,对00H寄存器写操作,当读取秒钟时间时, 对00H寄存器读操作。其他寄存器也是一样,详细请参考每个寄存器的功能说明。

DS1340Z寄存器布局

本实验涉及DS1340Z的写寄存器和读寄存器操作,查看手册给出的操作时序流程。

芯片支持连续写寄存器操作(寄存器地址自加1),时序流程如下:

DS1340Z连续写操作

根据连续写寄存器时序流程,其设计程序实现如下:

<code verilog> 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

<\code>

芯片支持连续读寄存器操作(寄存器地址自加1),时序流程如下:

DS1340Z连续读操作

根据连续写寄存器时序流程,其设计程序实现如下:

<code verilog> 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

<\code>

上面两段程序就是对于DS1340Z芯片的两种操作,调时间和读时间,对于万年历来说因为有电池供电,实时时钟一直都处于工作状态,当给FPGA上电时只需要读时间即可,只有遇到时间不对的时候才需要调时间,所以DS1340Z驱动模块平时都在循环读取时间,所以如果将调时间和读时间的时序操作融合到同一个状态下时,对于cntmain要加以控制,cntmain初值为12,且运行轨迹在12~32之间,控制程序调整如下:

<code verilog> if(cntmain >= 6'd32) 对MAIN中的子状态执行控制cntmain if(setflag)cntmain ⇐ 6'd0; 当setflag被置位时才会执行时间写入操作 else cntmain ⇐ 6'd12; 否则只执行时间读取操作 else cntmain ⇐ cnt_main + 1'b1;
<\code> 上面setflag为时间调整标志位,只有按动编码器在调时间模式时需要用到写时间数据的操作流程,可以根据按键脉冲置位setflag并自锁,每次完成写入操作后再将set_flag复位。程序实现如下: <code verilog> reg setflag; always@(posedge clk or negedge rstn) begin if(!rstn) setflag ⇐ 1'b0; else if(cntmain==5'd11) setflag ⇐ 1'b0;
完成写入时间操作复位set
flag

  else if(key_set) set_flag <= 1'b1;          //按键脉冲控制set_flag置位
  else set_flag <= set_flag;

end <\code>

模块端口如下:

<code verilog> module DS1340Zdriver ( input clk, rstn, 系统时钟和复位 input keyset, 按动脉冲输入 input [7:0] adjhour, adjmin, adjsec, 时分秒调整输入 input [7:0] adjyear, adjmon, adjday, adjweek, 年份调整输入 output i2cscl, I2C总线SCL inout i2csda, I2C总线SDA

output [7:0] rtchour, rtcmin, rtcsec, 实时时钟输出 output [7:0] rtcyear, rtcmon, rtcday, rtcweek 实时年份输出 ); <\code> 到这里就完成了万年历中DS1340Z模块的驱动设计,宏观上讲,该模块的功能可以这样描述: * 正常模式下循环读取时间信息,并把时间数据输出 * 由旋转编码器按动脉冲信号keyset触发进行一次写操作,用于调节时间

  • 每次写操作调节时间的时间数据由其他模块提供

万年历控制模块实现

控制模块包含多个功能的设计:模式控制、调时控制、显示控制,可以细化成多个模块实现,本实验例程中就写在了一个模块下,我们会针对这三个功能分别讲解其实现方法及原理。

模式控制

项目要求设计成8个模式(常态、调年、调月、调日、调周、调时、调分、调秒),对8个状态编码,常态—0、调秒—1、调分—2、调时—3、调周—4、调日—5、调月—6、调年—7,通过按动旋转编码器切换,按照常识调时间从大到小调节,先调节年份最后调秒钟,所以我们这8个状态的状态机跳转顺序是固定的(0→7→6→5→4→3→2→1→0),依次循环跳转,程序实现如下:

<code verilog> 时钟运行状态控制 always@(posedge clk or negedge rstn ) if(!rstn) state ⇐ 3'd0; else if(O_pulse) 按键脉冲控制时钟运行状态的跳变,

      if(state) state <= state - 3'd1;
      else state <= 3'd7;
  else state <= state;

<\code>

调时控制

调时控制在不同的调节模式对不同时间进行调整,我们分别以常态模式和调秒模式为例进行分析。

万年历时间调节要以当时的时间为基础,常态模式下不需要调整任何时间,但是可以将实时时钟读出的时间数据赋给调节变量,这样等跳转到调节模式时对调节变量的控制就是以当时的时间为基础了,程序实现如下:

<code verilog> 3'd0: 正常模式 begin if(Opulse)begin 在常态下按动编码器将当前实时时间赋值给调节寄存器 adjsec ⇐ rtcsec; adjmin ⇐ rtcmin; adjhour ⇐ rtchour; adjweek ⇐ rtcweek; adjday ⇐ rtcday; adjmon ⇐ rtcmon; adjyear ⇐ rtcyear; end end <\code> 调秒模式与其他调节模式操作一样,不同的是调节的规则不同,例如秒和分的调节范围为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。对秒钟数据进行调节,程序实现如下: <code verilog> 3'd1: 调秒模式 begin if(Lpulse) 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

<\code>

显示控制

首先来看看一下数码管要显示的效果,8位数码管分两页显示万年历数据,第一页显示年月日周,第二页显示时分秒。

万年历页码规划

我们看到任何一项时间选项都由两位数码管显示,每页最多显示4个时间选项,我们可以使用4位的变量dispen[3:0]控制4个时间选项的点亮或熄灭,dispen[3]控制最左侧两个数码管,disp_en[0]控制最右侧两个数码管,我们分别以常态模式和调秒模式为例进行显示使能控制的分析。

常态模式下,转动编码器控制显示页码,两个页码对应的显示控制,程序实现如下:

<code verilog> 3'd0: 正常模式 if(Lpulse) dispen ⇐ 4'b1111; 逆时针转显示第一页,数码管全亮

  else if(R_pulse) disp_en <= 4'b0111; //顺时针转显示第二页,时分秒亮
  else disp_en <= disp_en;

<\code>

调秒模式下,小时和分钟数码管点亮,秒钟闪烁显示,转动编码器时秒钟强制显示,最后按动旋转编码器切到常态模式时,时分秒数码管都回复显示,程序实现如下:

<code verilog> 3'd1: begin 调秒模式 dispen[3:1] ⇐ 3'b011; 时和分显示 if(Lpulse|Rpulse) dispen[0] ⇐ 1'b1; 转动时强制显示 else if(secpulse) dispen[0] ⇐ ~dispen[0]; 秒钟闪烁显示 else if(Opulse) dispen ⇐ 4'b0111; 返回常态时显示时分秒 else dispen[0] ⇐ disp_en[0]; end <\code> ===系统总体实现=== 前面分析了显示控制,主要对时间选项的点亮还是熄灭做控制,对应到数码管上就转化成数码管位的点亮和熄灭控制。另外还包含显示数据的控制,而这部分设计我们放到顶层模块中实现了,我们来分析一下。 数码管点亮控制 数码管与时间选项是对应关系,每个选项对应两位数码管,程序实现如下: <code verilog> wire [7:0] dataen = 2_disp_en_3,{2{dispen[2]}},{2{dispen[1]}},{2{dispen[0]}}}; 数码管位选控制
wire [7:0] dot
en = {1'b0,dispen[3],1'b0,dispen[2],1'b0,dispen[1],1'b0,dispen[0]};
数码管小数点显示控制 <\code> 数码管内容控制 万年历的显示分两页实现,我们以最右侧两个数码管显示内容为例,这两位数码管在第一页中显示周数据,在第二页中显示秒数据,那么我们怎么控制显示内容呢?分析,万年历8中模式, 1.常态模式下,显示读取的实时时钟数据,具体显示周还是秒再次细化 * disp
en等于4'b1111的时候,对应第一页,显示周数据 * disp_en等于4'b0111的时候,对应第二页,显示秒数据 常态模式下,根据disp_en选择显示周数据还是秒数据,程序实现如下: <code verilog> wire [7:0] datartc0 = dispen[3]? rtcweek:rtcsec;
常态下数码管显示数据 <\code>

2.调节模式下,显示写入的调节时钟数据,具体显示周还是秒再次细化

  • 调年、调月、调日、调周 状态下(state>=3),对应第一页,显示周数据
  • 调时、调分、调秒 状态下(state<3),对应第二页,显示秒数据

调节模式下,根据state选择显示周数据还是秒数据,程序实现如下:

<code verilog> wire [7:0] dataadj0 = state[2]? adjweek:adj_sec; 调节状态下数码管显示数据 <\code> 3.最后根据常态模式还是调节模式控制数码管显示实时时钟数据还是调节时钟数据 根据state选择显示实时时钟数据还是调节时钟数据,程序实现如下: <code verilog> assign {data7,data8} = state? dataadj0:datartc3; 根据状态选择显示常态数据还是调节状态数据 <\code>

综合后的设计框图如下:

RTL设计框图

实验步骤

  1. 双击打开Quartus Prime工具软件;
  2. 新建工程:File → New Project Wizard(工程命名,工程目录选择,设备型号选择,EDA工具选择);
  3. 新建文件:File → New → Verilog HDL File,键入设计代码并保存;
  4. 设计综合:双击Tasks窗口页面下的Analysis & Synthesis对代码进行综合;
  5. 管脚约束:Assignments → Assignment Editor,根据项目需求分配管脚;
  6. 设计编译:双击Tasks窗口页面下的Compile Design对设计进行整体编译并生成配置文件;
  7. 程序烧录:点击Tools → Programmer打开配置工具,Program进行下载;
  8. 观察设计运行结果。

实验现象