我又双叒叕写了个电子钟

这篇文章基本上是SoPC的课程设计报告,要求是使用SoPC EDA实验板开发一个电子钟,使用OLED显示时间,并支持按键修改时间。本文包括SoPC系统硬件消抖器的开发,定时中断与硬件中断的实现等内容。最终实现效果如下图所示 电子钟显示效果

课程EDA实验平台介绍

EDA,SoPC概述

作为可编程逻辑应用的核心方向,EDA和SoPC是目前电子设计的两个热门领域。EDA是电子设计自动化(Electronic Design Automation)的缩写。EDA技术就是以计算机为工具,设计者在EDA软件平台上,用硬件描述语言HDL完成设计文件,然后由计算机自动地完成逻辑编译、化简、分割、综合、优化、布局、布线和仿真,直至对于特定目标芯片的适配编译、逻辑映射和编程下载等工作,最后通过这些芯片演示出所需要的设计结果。
而SoC(System on Chip)一般来说被称为系统级芯片,也称为片上系统。它是一个有专用目标的集成电路,包含完整系统并有嵌入软件的全部内容。而SoPC(System on a Programmable chip)技术则可以使设计人员充分利用FPGA的逻辑单元以及植入FPGA内部的储存模块,并使用FPGA制造厂商提供的软核处理器,设计出可灵活裁剪、扩充、可升级的嵌入式处理系统。

EDA实验板功能

EDA实验板采用了Cyclone II 2C35 FPGA。它具有672个引脚,33,216 个逻辑单元 。FPGA引脚连接各个功能模块,方便对各个模块的控制和通信。对于简单的实验,在EDA实验板上开发了四个拨动开关,四个按键,四个LED灯,旋转编码器。对于用于比较系统的实验,EDA实验板提供了SDRAM、FLASH、128*64点阵OLED显示屏、日历时钟芯片。 对于需要处理各种通信的实验,EDA实验板提供了各种标准化的接口方案,如RS-232、RS-485、PS/2。对于较大较高级的设计项目,EDA实验板提供了USB2.0主从设备、10/100M以太网、红外端口、SD记忆卡、CAN总线。对于娱乐需求,EDA实验板还提供了音频编解码器,8位AD/DA,FM芯片。最后,其它扩展变可通过EDA实验板上的两个40针的扩展槽连接。
## EDA实验板硬件说明 1. FPGA - Cyclone II EP2C35F484C8 FPGA - EPCS16串行配置芯片 2. I/O设备 - RS-232、RS-485、PS/2 - 10/100以太网、USB主从控制器 - IrDA 、CAN - FM、音频解码芯片、8-BIT ADC/DAC 3. 开关、LED、显示、时钟 - 实时日历时钟芯片 - OLED显示屏 4. 储存器 - 8-Mbyte Flash - 32-Mbyte SDRAM \(\times\) 2 - SD Card

EDA实验板硬件设计如下图所示: EDA实验板硬件设计示意图

电子钟设计要求和系统硬件资源

电子钟设计要求

基于课程EDA实验平台,开展基于SoPC技术的电子钟设计:

  • 在OLED显示屏上显示日期和时间,走时准确;
  • 可区分闰年,各月天数无错误;
  • 配置3个按键,用以调整时间;
  • 调整时间时闪烁当前调整的位;

系统硬件资源

电子钟所需的硬件资源可分为外围器件和SoPC硬件系统模块两个部分,下面分别叙述:

系统所需外围硬件

外围器件 作用
按键 电子钟设置功能键
OLED屏幕 电子钟显示屏幕
FLASH 储存软、硬件程序
SDRAM 程序运行内存

SoPC硬件系统模块

SoPC硬件系统模块包括以下部分:Nios II CPU、定时器、按键PIO、OLED接口、AVALON三态桥、外部SDRAM接口,外部Flash接口、EPCS串行Flash控制器、JTAG UART。

系统硬件设计

这部分主要设计基于FPGA的SoPC整体硬件系统,主要包括PIO接口模块,存储模块、配置模块等。外围接口电路主要包括系统的OLED屏幕接口电路,储存系统接口电路,晶振电路、按键电路等部分。该系统的整体单元结构如下图所示: 电子钟硬件系统整体结构框图

键盘接口电路设计

在本例中键盘接口电路比较简单,并未通过串电容的方式来进行硬件消抖。之后将详细介绍基于FPGA设计D触发器消抖电路的方法,EDA实验板上的基础键盘电路如下所示: SoPC系统与键盘接口电路图

OLED接口电路设计

LCD接口电路连接采用SSD1305Z型OLED模块,显示的内容为\(128\times64\),它属于点阵型液晶模块,能够显示数字、字母和一些符号。有对比度、背光调节等功能。SoPC系统与LCD接口电路如下图所示: SoPC系统与LCD接口电路图

基于Nios II软核和FPGA的嵌入式SoPC系统配置与调试

Nios 2系统简介

Nios II是一种新型嵌入式处理器。具体来说,它是CPU软核,为Altera公司的FPGA提供了一种嵌入式系统解决方案,几乎可以用在所有Altera公司设计制造的FPGA芯片上。Nios II处理器及其外设模块程序大部分是用硬件描述语言(HDL)或C语言进行编程设计。
Nios II处理器包括Nios II CPU模块、Avalon交换总线,系统外设和片内用户逻辑,系统中的外设,如SDRAM控制器、程序储存器、数据储存器、UART、通用I/O等都是由FPGA内部的逻辑和RAM资源实现的。
Nios II是可以灵活配置的软核处理器,可以灵活地设计或者配置外设接口,满足设计和应用要求,使用嵌入式软核Nios II有以下特点:

  • CPU可以根据需要定制,灵活便捷;
  • 有丰富的外围接口,接口的类型和数量可以灵活定制;
  • 总线的连接不同于传统的CPU,其总线分布在FPGA的内部,因此不存在PCB布线中由于总线拥挤而难以走线;
  • 采用Avalon总线结构,其结构不同于传统意义上的共享式总线,在要连接的每一个主、从端口之间都建立了点到点的连接关系,则不同主、从端口之间可以同时进行通信,提高了Avalon总线的性能和效率。
  • 可以在同一块FPGA内定制多个Nios II软核进行多处理器并行工作,提高处理性能。

基于FPGA的键盘消抖硬件设计

前文已经提到,由于按键本身机械性能并不优异,导致在按键按下或松开时抖动较为严重,很容易出现误操作的情况,影响用户体验。为了避免以上问题,必须对机械按键进行消抖处理。
常见的消抖方法有硬件和软件消抖两种;SoPC系统的优势就在于用户不但可以自由设计软件,还能最大程度上定制自己需要的硬件,减少代码量,提高系统运行效率。消抖即为消除在一段时间按键的反复作用。考虑到D触发器配合合适的时钟频率即可滤去一些在短时间反复跳变的电平,而长时间存在的电平会被保留下来。综上所述,可通过FPGA构造相应的D触发器按键消抖电路。具体做法如下:
首先需要为D触发器电路提供合适的时钟信号。而为Nios II软核提供的是\(50MHz\)时钟,必须对其进行分频。由于大部分人的按键速度都在\(0.1s\)以上,所以选择\(400Hz\)时钟信号,使用四个D触发器串行连接,对一个按键作用的电平连续校验四次,将四次校验的结果输入\(xor\)逻辑,若四次校验的结果相同则认为按键产生了一个可靠动作。
根据以上描述,直接在Block Diagram中放置响应的元件,组成D触发器消抖阵列,并通过VHDL语言设计分频器;D触发器消抖电路如下图所示: 使用D触发器的消抖电路 分频器代码如下:

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
library IEEE;
use IEEE.STD_LOGIC_1164.all;
use IEEE.NUMERIC_STD.all;
entity frqdivide is
port (
clk_50Mhz : in std_logic;
rst : in std_logic;
clk_400Hz : out std_logic);
end frqdivide;
architecture Behavioral of frqdivide is
signal prescaler : unsigned(23 downto 0);
signal clk_400Hz_i : std_logic;
begin
gen_clk : process (clk_50Mhz, rst)
begin
if rst = '1' then
clk_400Hz_i <= '0';
prescaler <= (others => '0');
elsif rising_edge(clk_50Mhz) then
if prescaler = X"186A" then
prescaler <= (others => '0');
clk_400Hz_i <= not clk_400Hz_i;
else
prescaler <= prescaler + "1";
end if;
end if;
end process gen_clk;
clk_400Hz <= clk_400Hz_i;
end Behavioral;

Nios II软核配置

创建Nios II系统模块需要SoPC Builder,它是Quartus II中的一个工具,使用SoPC Builder可以创建一个Nios II系统模块,或者创建多主从设备SoPC模块。

  • 加入CPU核Nios II Processor Nios II CPU有三种不同的类型可以选择,分别是经济型,标准型和快速型,在本设计中,板载的FPGA芯片完全支持快速型Nios II核,资源足够,所以选快速型。

  • 加入定时器Timer 将定时器的组件名称设置为timer0,这个定时器是为了实现时钟的核心功能加入的。在之后的软件设计中,将使用定时器中断,每\(1s\)触发一次。 Altera Nios II核设置页面
  • 加入PIO功能 在电子钟设计中,使用按键来调整时间。由于需要使用按键中断,必须选择Generate IRQ,并选择下降沿中断。这样可以有利于后期软件层面设计的代码简化,即可以方便地在中断服务函数里实现按键功能。
  • 加入外部Flash 在组件中加入Flash Memory Interface(CFI),选择地址线宽度为23,数据线宽度为8,并设置处理器对Flash的读写时序;加入EPCS Serial Flash Controller,可用于CPU对EPCS Flash存储器的读写,可通过此控制器将编译的生成的SOF文件和CPU运行的其他代码存在EPCS器件中,以减少系统的整体硬件组成。
  • 加入用户定制外设 OLED模块为用户自定义外设,需要添加以作为电子钟的显示屏幕。
  • 加入Avalon三态总线桥 Nios II CPU与SDRAM,Flash,和用户自定义组件相连接都需要Avalon三态总线桥。
    配置完成的Nios II CPU内核如下图所示:
    配置完成的Nios II核 在Quartus II中将已经配置好的Nios II核加入设计的顶层设计文件,并连接外设与CPU的接口。 电子钟系统顶层原理图设计

系统软件设计

代码框架

系统的软件可分为两个部分,分别是Nios II EDS辅助构建的板级支持包,指的是使所给操作系统能够适配于所给设备主板的一套特定的支持代码(软件)实现。它通常包含了以基础支持代码来加载操作系统的引导程序(英语:bootloader),以及主板上所有设备的驱动程序},和我编写的电子钟应用层代码。应用层代码分为三块,分别为电子钟时间计算服务;电子钟时间显示服务;电子钟按键功能服务;代码的框架结构如下所示: 电子钟系统代码结构设计 ## 代码数据结构 在电子钟代码设计中对多种变量进行了封装,便于提高代码可读性与规范性,结构化的数据使得数据的引用变得简单。考虑到电子钟设计中主要考虑时间计算与设置问题,于是针对时间计算和时间设置功能构造两个结构体,在time.h文件中定义,两个结构体的定义如下:
时间结构体:由年、月、日、时、分、秒六个变量构成,均为无符号整型;

1
2
3
4
5
6
7
8
9
typedef struct {
unsigned int year;
unsigned int month;
unsigned int day;
unsigned int hour;
unsigned int minute;
unsigned int second;
}clockTypeDef;
clockTypeDef clock;

时间设置结构体:由是否设置时间标志,和设置时间位置两个变量构成,为无符号整型和整型;

1
2
3
4
5
typedef struct {
unsigned int isTimeSet;
int blinkPos;
}setTypeDef;
setTypeDef set;

代码核心功能设计

由于板级支持包的代码由Nios II EDS自动生成,在此不再展开。以下将从电子钟走时实现和时间调整功能两个方面介绍电子钟系统的代码。 ### 走时实现 电子钟实现走时通常有以下两种途径:第一种是通过软件延时实现的,根据CPU的晶振频率计算每秒理论可执行的指令数,使用循环的方法占用CPU资源以达到定时器的效果,这种方法在软件上易于实施,不用配置中断,但是缺点在于耗费CPU资源,计时时间不准。另一种是用过中断实现的,通过配置CPU定时器中断并编写中断服务函数,这样即可实现精准计时的效果。在本设计中采用定时器中断方法实现走时。
电子钟定时中断配置函数如下,在主函数中调用,写寄存器设定周期为1秒,运行,并注册中断。需要注意的是注册中断函数在Nios II EDS 11.0版本中已经由alt_isr_register更改为alt_ic_isr_register,函数结构改变,传入的参数发生了一些变化。

1
2
3
4
5
6
7
8
void initTimer(void) {
IOWR_ALTERA_AVALON_TIMER_STATUS(TIMER0_BASE, 0x00);
IOWR_ALTERA_AVALON_TIMER_PERIODL(TIMER0_BASE,50000000);
IOWR_ALTERA_AVALON_TIMER_PERIODH(TIMER0_BASE, 50000000 >> 16);
IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER0_BASE, 0x07);
alt_ic_isr_register(TIMER0_IRQ_INTERRUPT_CONTROLLER_ID,
TIMER0_IRQ, isrTimer0, NULL,0x0);
}

定时中断的服务函数主要作用是调用时间计算函数,如下所示:

1
2
3
4
5
void isrTimer0() {
timeCalculate();
timeDisplay();
IOWR_ALTERA_AVALON_TIMER_STATUS(TIMER0_BASE, 0x00);
}

时间计算函数较长,参见附录完整代码,其效果为每调用一次,时间结构体中的秒递增1,递增至59时清零,同时分钟递增1,之后同理。在此函数中也考虑了不同月份天数不同,闰年二月天数改变等细节问题。
时间显示函数包括了两个模块,分别为闪烁功能和时间的逐位显示。特别地,闪烁功能是基于定时中断实现的,每进入一次中断,闪烁标志都会取反,根据闪烁标志的0,1来确定显示空白字符还是时间。另外通过时间设置功能结构体中的闪烁位置成员计算OLED显示数组中需要被置为空白字符的数组元素,代码如下所示,省略了整数取位显示和变量赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void timeDisplay() {
...
if(set.isTimeSet) {
isBlink = isBlink>0?0:1;
}
else isBlink = 0;
...
if(isBlink) {
if(!set.blinkPos) {
for(i = 0; i < 4; i++)
numDate[i] = 1;
}
else {
numDate[set.blinkPos*3+2] = 1;
numDate[set.blinkPos*3+3] = 1;
if(set.blinkPos > 2) {
numTime[(set.blinkPos-3)*3] = 1;
numTime[(set.blinkPos-3)*3+1] = 1;
}
}
}
Show_String(1,numDate,2,50);
Show_String(1,numTime,4,50);
}

时间调整实现

电子钟的时间调整是通过按键实现的,而按键功能通常也有两种实现方式,第一种为循环查询方式,主要做法是在主函数循环中不断读按键寄存器捕捉电平跳变,捕捉到跳变则调用相应功能函数。这种循环查询方式的优势在于可以方便地设计消抖功能,无需硬件消抖,但缺点仍然是占用CPU资源,从嵌入式开发的角度不够合理。而第二种方式为中断查询方式,通过PIO中断来捕捉边沿,在中断服务函数中部署功能,这种做法节省CPU资源,同时我也为此方式提供了硬件消抖电路。在本设计中采取第二种方式。
按键的中断配置函数与定时中断类似,通过以下代码写PIO中断寄存器,配置中断为边沿触发,注册中断,在本例中使用了三个按键,在硬件资源足够的情况下注册了三个不同的硬件中断:

1
2
3
4
5
6
void initKey() {
IOWR_ALTERA_AVALON_PIO_IRQ_MASK(KEY0_BASE, 0xf);
IOWR_ALTERA_AVALON_PIO_EDGE_CAP(KEY0_BASE, 0x0);
alt_ic_isr_register(KEY0_IRQ_INTERRUPT_CONTROLLER_ID,
KEY0_IRQ,isrKey0,NULL,0x0);
}

按键的中断服务函数有三个,分别针对时间调整加,时间调整减,时间调整开关。时间调整按钮按下后,时间设置结构体中的时间设置标志置位,并将成员闪烁位步进1,直至闪烁位到5,也就是“秒”,将闪烁位置-1以备下次调整时间使用,清时间设置标志,相当于关时间设置功能。核心代码如下:

1
2
3
4
5
6
7
8
9
void isrKey0(void* context, int id) {
set.isTimeSet = 1;
set.blinkPos++;
if(set.blinkPos > 5) {
set.isTimeSet = 0;
set.blinkPos = -1;
}
IOWR_ALTERA_AVALON_PIO_EDGE_CAP(KEY0_BASE, 1);
}

时间调整功能的实现使用了switch语句,通过当前的闪烁位置来决定具体增减哪部分的值,时间调整加示例代码如下,时间调整减代码类似:

1
2
3
4
5
6
7
8
9
10
void isrKey1(void* context, alt_u32 id) {
if(set.isTimeSet == 1) {
switch(set.blinkPos) {
case 0: clock.year++; break;
...
default: break;
}
IOWR_ALTERA_AVALON_PIO_EDGE_CAP(KEY1_BASE, 1);
}
}

需要特别注意的是,在中断服务函数中需要清中断标志位,Altera官方文档说明寄存器写0清除。事实上,按官方文档上的写法并不能有效跳出中断,必须写1清除。 # 总结 ## 最终实现效果 电子钟最终在实验板上的显示效果如下图所示: 电子钟显示效果 由图可见,电子钟从默认的时间-1970年1月1日开始走时,经过五小时的测试误差小于1秒,说明了时钟还是比较准确的。
为何从1970-01-01开始
作为一个使用UNIX和Linux的用户,当然要使用UNIX(POSIX)时间来致敬此伟大的计算机系统。 ## 一个bug 在电子钟上电时会自动进入时间设置功能,这是因为按键中断是用上升沿触发的。也就是说上电时的动作相当于按键从按下到抬起产生了一个上升沿。这个问题可通过在CPU的PIO中断配置中将其更改为下降沿触发解决。