首页资源分类FPGA/CPLDAltera > 6《HELLO FPGA》- 软核演练篇

6《HELLO FPGA》- 软核演练篇

已有 445185个资源

下载专区

文档信息举报收藏

标    签:《HELLOFPGA》

分    享:

文档简介

6《HELLO FPGA》- 软核演练篇

文档预览

封面 前言 为什么要学软核演练篇:当学习完了前面的篇章之后,我们先回想一下,我们能够采用硬件 描述语言(Verilog)所能实现的最复杂功能的逻辑电路是什么?也许你会回答,应该是那个示波 器吧,在示波器的代码中,我们其实只是完成了几个并不复杂的操作,但却动用了上千行的代码 与若干个复杂的状态机,去完成那些简单的循序选择操作。而这些操作仅仅是构成一个我们前面 所述的复杂系统的最最基本的元素。设想一下:如果单纯采用硬件描述语言去实现类似于计算机 系统一样庞大的工程,那么其想对应的工作量是难以想象的,即使是一个从事 FPGA 行业多年 工程师,也会望而生畏。Qsys 就是为了解决上述问题而诞生的,Qsys 系统集成工具会自动生 成互联逻辑,连接知识产权(IP)功能和子系统,从而显著节省了开发时间,减轻了 FPGA 设计工 作量。Qsys 是下一代 SOPC Builder 工具,与 SOPC Builder 相比,Qsys 提高了性能,增强了 设计重用功能,更迅速的进行验证。 软核演练篇包含了哪些内容:该篇以什么是软核、什么是 Qsys、如何构建一个 Qsys 系统 为切入点,在该基础上,我们进一步介绍了 Nios II 处理器的体系结构、Qsys 丰富多彩的内置 IP,以及 Avalon 总线接口规范,然后我们又以 Avalon 总线接口规范为基础,进一步定制了开 发板所有外设的 IP 核。最后,我们又以系统 uC/OS-II 和 uCGUI 为例进行了应用开发的介绍。 本篇不同于传统的傻瓜式教程,将理论和实践相结合,不仅仅讲述了怎样做,更进一步讲述了为 什么要这样做。 目录 第一章 Qsys,What,How,Why .............................................................................................. 1 §1.1 什么是 Qsys? .................................................................................................................3 §1.2 如何构建一个 Qsys 系统?.............................................................................................8 §1.3 为什么要采用 Qsys? ................................................................................................. 11 第二章 建立你的第一个 Qsys 系统 ............................................................................................ 17 §2.1 Qsys 的开发流程 ..........................................................................................................19 §2.2 手把手教你构建一个 Qsys 系统 ................................................................................ 20 2.2.1 使用 Quartus II 创建新工程..................................................................................21 2.2.2 使用 Qsys 软件创建 Qsys 系统 ......................................................................... 28 2.2.3 集成 Qsys 系统到 Quartus II 工程中 ................................................................. 48 2.2.4 使用 Nios II SBT Eclipse 建立用户程序..............................................................61 §2.3 让你的 Qsys 系统上电自启动吧 ................................................................................ 72 第三章 剥去神秘的外衣——Qsys 到底是如何运行的 ...............................................................79 §3.1 Nios II 硬件框架结构的深入剖析..................................................................................81 3.1.1 寄存器文件 ........................................................................................................... 82 3.1.2 算术逻辑单元 ....................................................................................................... 84 3.1.3 复位信号 .............................................................................................................. 85 3.1.4 异常和中断........................................................................................................... 85 3.1.5 存储器和 I/O 结构 ................................................................................................ 92 3.1.6 JTAG 调试模块 ...................................................................................................100 3.1.7 Nios II 处理器性能 ............................................................................................... 101 §3.2 Nios II 软件框架结构的深入剖析 ...............................................................................102 3.2.1 system.h 系统描述文件 ......................................................................................103 3.2.2 硬件抽象层的构成 ..............................................................................................105 3.2.3 类型定义 .............................................................................................................106 3.2.4 通用设备模型 .....................................................................................................106 3.2.5 异常处理 .............................................................................................................107 §3.3 Nios II 用户程序引导过程深入剖析 ...........................................................................109 3.3.1 什么是 Bootloader? ..........................................................................................109 3.3.2 用户程序引导过程 .............................................................................................. 114 第四章 Qsys 丰富多彩的内置 IP 核.......................................................................................... 117 §4.1 PIO IP 核 .....................................................................................................................120 4.1.1 PIO IP 核的综述................................................................................................... 121 4.1.2 PIO IP 核的寄存器描述 ....................................................................................... 121 4.1.3 PIO IP 核的配置选项...........................................................................................122 4.1.4 PIO IP 核的软件编程...........................................................................................124 4.1.5 PIO IP 核的应用实例——流水............................................................................124 4.1.6 PIO IP 核的应用实例——按键............................................................................127 4.1.7 PIO IP 核的应用实例——中断............................................................................130 §4.2 SDRAM IP 核 .............................................................................................................134 4.2.1 SDRAM IP 核的综述...........................................................................................134 4.2.2 SDRAM IP 核的配置选项...................................................................................136 4.2.3 SDRAM IP 核的软件编程...................................................................................138 4.2.4 SDRAM IP 核的应用实例...................................................................................138 §4.3 EPCS IP 核.................................................................................................................142 4.3.1 EPCS IP 核的综述 ..............................................................................................142 4.3.2 EPCS IP 核的配置选项 ......................................................................................143 4.3.3 EPCS IP 核的软件编程 ......................................................................................144 4.3.4 EPCS IP 核的应用实例 ......................................................................................144 §4.4 Interval Timer IP 核 ....................................................................................................149 4.4.1 Timer IP 核的综述 ...............................................................................................149 4.4.2 Timer IP 核的寄存器描述 ...................................................................................150 4.4.3 Timer IP 核的配置选项 ....................................................................................... 151 4.4.4 Timer IP 核的软件编程 .......................................................................................153 4.4.5 Timer IP 核的应用实例——系统时钟服务.........................................................155 4.4.6 Timer IP 核的应用实例——标准中断服务.........................................................159 4.4.7 Timer IP 核的应用实例——时间标记服务.........................................................163 4.4.8 Timer IP 核的应用实例——看门狗 ....................................................................167 §4.5 System ID IP 核 ......................................................................................................... 171 4.5.1 System ID IP 核的综述 ....................................................................................... 171 4.5.2 System ID IP 核的寄存器描述 ........................................................................... 171 4.5.3 System ID IP 核的配置选项 ............................................................................... 171 4.5.4 System ID IP 核的软件编程 ...............................................................................172 4.5.5 System ID IP 核的应用实例 ...............................................................................172 §4.6 DMA IP 核 ..................................................................................................................176 4.6.1 DMA IP 核的综述 ................................................................................................176 4.6.2 DMA IP 核的寄存器描述 ....................................................................................177 4.6.3 DMA IP 核的配置选项 ........................................................................................179 4.6.4 DMA IP 核的软件编程 ........................................................................................180 4.6.5 DMA IP 核的应用实例 ........................................................................................ 181 §4.7 JTAG_UART IP 核 ....................................................................................................187 4.7.1 JTAG_UART IP 核的综述 ..................................................................................187 4.7.2 JTAG_UART IP 核的寄存器描述 ......................................................................187 4.7.3 JTAG_UART IP 核的配置选项 ..........................................................................189 4.7.4 JTAG_UART IP 核的软件编程 ..........................................................................190 4.7.5 JTAG_UART IP 核的应用实例——C 函数 .......................................................190 4.7.6 JTAG_UART IP 核的应用实例——API 函数....................................................193 §4.8 UART IP 核 ................................................................................................................196 4.8.1 UART IP 核的综述 ..............................................................................................196 4.8.2 UART IP 核的寄存器描述 ..................................................................................197 4.8.3 UART IP 核的配置选项 ..................................................................................... 200 4.8.4 UART IP 核的软件编程 ..................................................................................... 203 4.8.5 UART IP 核的应用实例——C 函数 .................................................................. 203 4.8.6 UART IP 核的应用实例——API 函数 ............................................................... 207 4.8.7 UART IP 核的应用实例——中断 ....................................................................... 211 §4.9 SPI IP 核 .....................................................................................................................215 4.9.1 SPI IP 核的综述...................................................................................................216 4.9.2 SPI IP 核的寄存器描述 .......................................................................................217 4.9.3 SPI IP 核的配置选项...........................................................................................219 4.9.4 SPI IP 核的软件编程.......................................................................................... 222 4.9.5 SD 卡的综述 ...................................................................................................... 222 4.9.6 SPI IP 核的应用实例.......................................................................................... 228 第五章 Qsys 的精髓——定制你专属的高性能 IP 核 .............................................................. 237 §5.1 Avalon 接口规范 ........................................................................................................ 239 5.1.1 Avalon Clock....................................................................................................... 240 5.1.2 Avalon Reset...................................................................................................... 242 5.1.3 Avalon-MM ........................................................................................................ 243 5.1.4 Avalon Interrupt ..................................................................................................251 5.1.5 Avalon-ST.......................................................................................................... 253 5.1.6 Avalon-TC ......................................................................................................... 259 5.1.7 Avalon Conduit .................................................................................................. 260 §5.2 LED 外设 ....................................................................................................................261 5.2.1 LED IP 核的定制 .................................................................................................261 5.2.2 LED IP 核的应用 ................................................................................................ 277 §5.3 数码管外设.................................................................................................................281 5.3.1 数码管 IP 核的定制.............................................................................................281 5.3.2 数码管 IP 核的应用 ........................................................................................... 299 5.3.3 数码管 PIO 的应用 ............................................................................................ 302 §5.4 TLC5615 DA 外设......................................................................................................310 5.4.1 DA PIO 的应用 ....................................................................................................310 5.4.2 DA IP 核的定制 ...................................................................................................317 5.4.3 DA IP 核的应用 .................................................................................................. 328 §5.5 VGA 外设................................................................................................................... 333 5.5.1 VGA PIO 的应用 ................................................................................................ 333 5.5.2 VGA IP 核的定制 ............................................................................................... 339 5.5.3 VGA IP 核的应用 ............................................................................................... 357 §5.6 蜂鸣器外设................................................................................................................ 360 5.6.1 蜂鸣器 IP 核的定制............................................................................................ 360 5.6.2 蜂鸣器 IP 核的应用 ........................................................................................... 375 §5.7 红外外设 ................................................................................................................... 377 5.7.1 红外 IP 核的定制 ............................................................................................... 377 5.7.2 红外 IP 核的应用 ............................................................................................... 389 §5.8 TLC549 AD 外设....................................................................................................... 392 5.8.1 AD IP 核的定制 .................................................................................................. 392 5.8.2 AD IP 核的应用 .................................................................................................. 403 §5.9 PS/2 键盘外设 ........................................................................................................... 405 5.9.1 PS/2 键盘 IP 核的定制 ....................................................................................... 405 5.9.2 PS/2 键盘 IP 核的应用....................................................................................... 424 §5.10 PS/2 鼠标外设 ......................................................................................................... 426 5.10.1 PS/2 鼠标 IP 核的定制 ..................................................................................... 426 5.10.2 PS/2 鼠标 IP 核的应用 ..................................................................................... 450 第六章 老爷车也能开出法拉利的感觉——属于 FPGA 的操作系统 ...................................... 453 §6.1 建立你的第一个 uC/OS II 系统 ................................................................................ 455 6.1.1 uC/OS II 的系统综述........................................................................................... 455 6.1.2 手把手教你构建一个 uC/OS II 系统.................................................................. 455 §6.2 uC/OS II 任务管理和时间管理 .................................................................................. 459 6.2.1 uC/OS II 的任务管理 .......................................................................................... 459 6.2.2 uC/OS II 的时间管理.......................................................................................... 462 6.2.3 uC/OS II 任务管理和时间管理的应用 ............................................................... 462 §6.3 uC/OS-II 同步与通信 ................................................................................................ 468 6.3.1 信号量和互斥型信号量...................................................................................... 468 6.3.2 信号量和互斥型信号量的应用 ...........................................................................471 6.3.3 消息邮箱和消息队列 ......................................................................................... 479 6.3.4 消息邮箱和消息队列的应用...............................................................................481 §6.4 uC/GUI 图形界面系统 ............................................................................................... 490 6.4.1 uC/GUI 的系统综述............................................................................................ 490 6.4.2 TFT 液晶屏的综述 ..............................................................................................491 6.4.3 uC/GUI 的系统移植 ........................................................................................... 499 6.4.4 uC/GUI 的系统应用 ........................................................................................... 503 第七章 基于 uC/OS-II 和 uC/GUI 的 UI 系统.......................................................................... 509 §7.1 功能概述..................................................................................................................... 511 §7.2 硬件框架 .................................................................................................................... 511 §7.3 软件工程 ....................................................................................................................512 §7.4 板级调试 ....................................................................................................................518 版权声明 ....................................................................................................................................... 521 Qsys,What,How,Why 第一章 Qsys,What,How,Why §1.1 什么是Qsys? 如果我们想要知道什么是 Qsys,那么我们就必须要对下面这些名词概念有一定的了解,如: ASIC、SOC、SOPC、NIOS……。我们只有弄清楚了、搞明白了这些名词概念,我们才能真正 明白什么是 Qsys,这些名词我们在图 1.1 中已经罗列出来了。 集成电路 专用集成电路 通用集成电路 可编程 全定制 其他 逻辑 电路 SOPC 半定制 DSP芯片 单片机 其他通 用芯片 基于硬核的 SOPC系统 基于软核的 SOPC系统 采用ARM系列CPU 采用Nios II系列CPU 图 1.1 集成电路的分类以及各种专业名词之间的层次关系 在图 1.1 中的这些专业名词概念,想必大家也都或多或少的知道一些,当我们要细究起它们 的准确概念,或者是它们相互关联与区别的时候,我想大家肯定和我们一样头疼不已,因为关于 这些名词概念的资料,它们都是很“官方”的,并不是那么的容易让人理解的。作为读者的你, 在读我们的这本教程时,是不是也会担心我们教材中出现这些情况?这里大家请放心,我们的教 材都是很接地气的,绝对会以最通俗易懂的方式来进行解释这些专业名词概念。下面我们就从大 树的根部——集成电路开始谈起。 说到集成电路,那么我们不得不从 1904 年开始说起,1904 年,世界上第一只电子二极管, 在英国物理学家弗莱明的手下诞生了,这同时也标志着世界从此进入了电子时代。随着科技的发 展与进步,由于电子管体积大、功耗大、发热厉害,所以电子管已经无法满足人们。直至 1947 年,美国贝尔实验室的肖克莱、巴丁和布拉顿组成的研究小组,研制出一种点接触型的锗晶体管。 晶体管的问世,是 20 世纪的一项重大发明,是微电子革命的先声。晶体管出现后,人们就能用 一个小巧的、消耗功率低的电子器件,来代替体积大、功率消耗大的电子管了,不仅仅如此,晶 体管的发明又为集成电路的降生吹响了号角。1958 年,德州仪器公司的杰克•基尔比发明了集成 电路,这一发明不仅革新了我们的工业,更改变了我们生活的世界。大家看图 1.2 所示电路。 4 软核演练篇 §1 图 1.2 集成电路图 图 1.2 中电路板上那些有很多引脚的小黑块就是集成电路,它里面并不复杂,无非是把一个 电路中所需的晶体管、电阻、电容和电感等元件,按照电路的要求把它们连接起来,制作在一小 块或几小块半导体晶片或介质基片上,然后封装在一个黑壳内,成为具有所需电路功能的微型结 构,这种微型结构就是集成电路。简言之,我们去电子市场所买的“芯片”均是指的集成电路。 集成电路按照应用对象的不同,可以分为通用集成电路和专用集成电路。 (1) 通用集成电路:所谓通用集成电路就是我们常说的 51 单片机、DSP(数字信号处理器), 与 ARM 等等,这些芯片中包含了通用微处理器,需要二次开发,才能用于某一用途。通用集成 电路还包括俗称的“电路零件”,就是从各种各样的相对明确的应用当中提取出来的公共部分, 比如:我们在学习数字电路中用到的各种 74 系列数字门电路、A/D、D/A 转换器、电源电路等 等,这些通用集成电路的特点是可以被各种各样的应用系统直接添加进去完成基本的功能的部 分,本质上是分立的。 (2) 专用集成电路:所谓专用集成电路就是我们常说的 ASIC ,是 Application Specific Integrated Circuit 的缩写,它是直接按功能和要求设计出整个系统,并把这个系统做到集成电路 上。ASIC 在批量生产时与通用集成电路相比具有体积更小、功耗更低、可靠性提高、性能提高、 保密性增强、成本降低等优点。我们这里就举个简单的例子,当拆开你的电脑摄像头,你会发现 电路板上往往只有一个芯片,如图 1.3 所示。大家可不要小看这一个芯片,它可以完成 AD 转换, 视频编解码,USB 协议转换等各项功能。我们这里可以设想一下,倘若我们这里利用通用集成 电路去实现上面相同的功能,那么这个摄像头的价格可能要贵上数倍,并且体积也不可能这么小。 简言之,ASIC 是专门为某一项功能开发定制的专用集成芯片,其体积小,成本相对较低,且功 能集成度要比通用集成电路高。 Zircon Opto-Electronic Technology CO.,Ltd. §1 Qsys,What,How,Why 5 图 1.3 摄像头的电路图 前面我们已经说了,ASIC 相比于通用集成电路而言最大的特点在于“定制”。因此,我们可 以根据其定制方法的不同,可以分为全定制、半定制和可编程 ASIC 三类: (1) 全定制 ASIC 是利用集成电路的最基本设计方法(不使用现有库单元),对集成电路中所 有的元器件进行精工细作的设计方法。全定制设计可以实现最小面积,最佳布线布局、最优功耗 速度积,得到最好的电特性。简而言之,全定制就是全部从零开始设计 ASIC。 (2) 半定制使用单元库里的标准逻辑单元,设计时可以从标准逻辑单元库中选择 SSI(门电路)、 MSI(如加法器、比较器等)、数据通路(如 ALU、存储器、总线等)、存储器甚至系统级模块(如乘 法器、微控制器等)和 IP 核,将其组合在一起。这些逻辑单元已经布局完毕,而且设计得较为可 靠,设计者可以较方便地完成系统设计。简而言之,半定制 ASIC 就是部分借鉴之前的设计结果, 部分需要重新设计的 ASIC。 (3) 无论全定制还是半定制的设计方法,均要涉及芯片的布局布线和工艺等底层问题,一旦 电路功能设计完成投片生产后,不可再更改,这使得开发周期较长,成本较高。随着数字电路应 用越来越广泛,产品的生命周期逐渐变短,对于传统 IC 设计而言,系统设计师们更愿意自己设 计专用 ASIC 芯片,而且希望 ASIC 的设计周期尽可能短,最好是在实验室里就能设计出合适的 ASIC 芯片,并且立即投入实际应用之中,因而出现了现场可编程逻辑器件,在可编程逻辑器件 中应用最广泛的当属现场可编程门阵列(FPGA,Field Progarmmable Gate Array)和复杂可编 程逻辑器件(CPLD,Complex Progarmmable Logic Device)。FPGA 和 CPLD 有着结构上的 区别,这在《数字电路篇》中已经提到,这里我们就不再多说。从相同点上来说,FPGA 和 CPLD 都是一块空白的集成电路,它本身不实现任何功能,而是提供很多逻辑门单元,设计者可以用硬 件描述语言设计一个数字系统,并通过 EDA 软件仿真、综合,生成网络表,最终下载到芯片中 对逻辑门单元进行配置使用。因此,设计者不用再关注于芯片内的布局布线和工艺问题,可以把 精力集中在用软件设计电路功能上。简而言之,FPGA 与 CPLD 就是可编程 ASIC。读到这里你 也许会有这么一个疑问,为什么 FPGA 可编程还被称之为 ASIC?在《学习指导篇》里我们已经 了解到,FPGA 其实就是一个“白片”,它是由存放在片内 RAM 中的程序来设置其工作状态的。 加电时,FPGA 芯片将 EEPROM 中数据读入片内编程 RAM 中,配置逻辑门之间的连接,完成 后,FPGA 进入工作状态。掉电后,FPGA 恢复成白片,内部逻辑关系消失,因此,FPGA 能够 反复使用,当需要修改 FPGA 功能时,只需更改 EEPROM 中的程序即可。但是永远牢记,当 http://www.fpga.gs/ 6 软核演练篇 §1 FPGA 进入工作状态时,它就会摇身一变,成为一片为某一项功能开发定制的专用硬件电路,这 与 ARM、DSP、单片机等硬件本身通用,且不可变,仅能够改写软件是截然不同的。 这三种设计方法各有优缺点,具体 ASIC 采用哪种方法,取决 ASIC 的批量大小、生产周期 的长短,产品利润、产品寿命等因素。顺着前面的大树,我们讲完了集成电路,讲完了可编程逻 辑器件,接下来我们就要步入本教材的主角 SOPC 了。 所谓 SOPC 就是片上可编程系统(System on a Programmable Chip)。要了解什么是“片 上可编程系统”前,我们必须弄清楚什么是“系统(System)”。在这里,我们要阐明一个易混 淆的概念:有些学习者认为我们之前所构建的 Verilog 逻辑模块就属于这里的“系统(System)” 范畴。但是这个概念是不正确的。之前我们在《项目实战篇》里所接触到的数字时钟、示波器等; 它们都不能称之为系统,它们只能够被称为逻辑电路!那么到底什么才能称作"系统"呢?我们来 看下面这个图 1.4。 CPU 程序 计数 器 寄存 器 文件 算术 逻辑 单元 总线接口 系统总线 IO桥 主存储器 IO总线 USB控制器 图形适配器 磁盘控制器 扩展接口 鼠标 键盘 显示器 磁盘 图 1.4 一种典型的计算机系统结构图 这就是我们每天都要接触的计算机系统的结构图,它是最典型的一种系统。可见,一个完整 的系统,至少由 CPU、总线、外设控制器等几部分组成。CPU 负责中断分配、地址管理、内存 调度等总的控制任务;外设控制器负责与外部设备连接,控制外设的行为;CPU 和各个外设控 制器之间通过总线交互信息。由此可见,系统的复杂度要比我们之前所遇到的逻辑电路要复杂的 多。那么聪明的你也许会说,系统的组成结构既然如此复杂,那么必定系统的硬件组成也会随之 变的庞大,就比如:我们的计算机,它其实是一个非常庞大的系统,我们需要 CPU、内存、显 卡、主板、硬盘等等才能构成一个完整的计算机。这种说法虽有道理,但并不准确。随着电子技 术的发展,半导体工艺水平的不断进步,使集成电路器件特征尺寸越来越小,芯片集成规模越来 越大,数百万门级电路可以集成在一个芯片上。这使得我们可以把功能复杂的 CPU、总线、外 设控制器等集成到同一个专用集成芯片 ASIC 内,做成一个完整的单芯片电子系统,这种基于单 芯片的电子系统我们将其称为 SOC (System On Chip)。 SOC 从整个系统的角度出发,把处理机制、模型算法、芯片结构、各层次电路,直至器件 Zircon Opto-Electronic Technology CO.,Ltd. §1 Qsys,What,How,Why 7 的设计紧密结合起来,在单个(或少数几个)芯片上完成整个系统的功能。更具体地来说,SOC 集 成了各种功能模块,每一种功能模块都是由硬件描述语言设计程序,然后在芯片内由电路实现的; 每一个模块不是一个单独的 ASIC“器件”,只是利用芯片的一部分资源去实现某种传统的功能。 SOC 所集成的这些功能模块,我们将其称为 IP 核(Intelligence Property Core,知识产权核), 它是 SOC 的核心部件。当需要推出新产品时,SOC 开发人员可以将原来的 IP 核转移到新的系 统上,或者只需更改一小部分电路,就可符合产品所需要的功能要求,这就是对 IP 的重要利用, 可以做最有效率的使用,借以缩短产品的开发周期,降低开发的复杂度。 到这里我想大家已经能够大概猜到什么是片上可编程系统(System on a Programmable Chip)了吧。SOPC 比 SOC 仅仅只多了一个“P”,其代表 Promgrammable,即可编程的意思, 回想前面 FPGA 的定义,我们知道 FPGA 就是 Progarmmable 的 ASIC,因此,SOPC ( System on a Programmable Chip,片上可编程系统)就是以 FPGA 为代表的可编程逻辑器件取代专用集 成电路 ASIC,所实现的一种更加灵活、高效的技术 SOC (System On Chip)解决方案。它不仅 可编程,还具有灵活的设计方式,可裁减、可扩充、可升级,并具备软硬件在系统可编程的功能。 可编程器件内,还具有小容量高速 RAM 资源。由于开发厂商提供了丰富的 IP 核资源可供灵活 选择,用户可以构成各种不同的系统。就如同计算机系统中的核心部件 CPU,在 SOPC 中,系 统的控制中枢被称作 MCU(Micro Control Unit),即微控制器(有时也不加区分的称之为 CPU)。 SOPC 中的 MCU 可以和计算机系统中 CPU 类似,是一块“固化”在芯片内部,用于无法进行 更改的固定电路模块。也就是说,用户得到的硬核仅是产品的功能,而不是产品的设计。这种 MPU 称为硬核,典型的硬核产品是 ARM 公司推出的 ARM 微处理器,许多公司将 ARM 微处理 器和林林总总的外围模块(AD、DA、各种总线、常用的驱动模块)都固化一个芯片里,就形成 ARM 内核的各种芯片(例如,ALTERA 的 Excalibur 系列 FPGA 中就植入了 ARM922T 微处理 器)。另一方面,借助于 FPGA 的硬件可编程本身的特性,我们还可以采用硬件描述语言去生成 一个 CPU,以满足自己所需要的功能。这种 CPU 称为“软核”,典型代表有 Altera 公司的 Nios 软核和 Xilinx 公司的 MicroBlaze 软核。软核与硬核的优缺点是很明显的,如果追求性能的稳定 和应用的简便,应该选择硬核;硬核固然有很多优势,但它在灵活性上还是受到了制约。道理很 简单:一旦你在某个型号的 FPGA 中加入了一个硬核 CPU,硬核 CPU 也就将其“不灵活”的 属性带给了这个平台——CPU 过时了,也就意味着平台过时了。因此,如果想追求更大限度的 灵活性,设计者可以选择在 FPGA 中“写”入一个软核,而不是“固化”一个硬核。综上所述, SOPC 其实就是 SOC 在可编程器件上的实现,SOPC 的核心部件 CPU 根据其生成机理的区别 可分为硬核和软核。 听到这里你是不是有点跃跃欲试的呢?你是否现在已经开始幻想,现在摆在你手边的这块 小小的开发板是否能够完成一个类似计算机一样功能强大的复杂系统呢?答案是肯定的,Altera 已经帮你实现了这个梦想,它就叫做 Qsys。Qsys 是 ALTERA 公司为其 FPGA 上定制实现的 SOPC 框架。而开发 Qsys 所需的软件也称为 Qsys,它是功能强大的基于图形界面的片上系统 定义和定制工具。Qsys 库中包括处理器和大量的 IP 核及外设。可以这么来理解,Qsys 就是 Altera 公司为我们这些菜鸟提供了一个简单易用的工具,借助于它我们就可以很容易的在 Altera 的 FPGA 上构建出一个功能强大的 SOPC 系统,即 Qsys 系统。再告诉你一个好消息,当下正 值学习 Qsys 的大好时机,因为 Altera SOPC 技术前不久刚好经历改朝换代:此前 Altera 公司 http://www.fpga.gs/ 8 软核演练篇 §1 的 SOPC 集成开发环境 SOPC Builder,而 Altera 公司已在现有集成开发环境全面采用 Qsys 取 代之前的 SOPC builder。当然,如果你此前学过 SOPC builder 也不用遗憾,因为采用 SOPC builder 所建立的所有工程都被“安全可靠”地移植到 Qsys 中,实现向下兼容。关于 Qsys 与此 前 SOPC Builder 的区别在这里就不多加以赘述了,有兴趣的读者可以问度娘;或者你就干脆抛 弃 SOPC Builder 长久的包袱,完全从一个崭新的 Qsys 学起。 为了保证本教程的严谨性,最后,我们还是引用一下 Altera 官方网站对 Qsys 的介绍语作为 本小节的总结:Qsys 系统集成工具自动生成互联逻辑,连接 IP 核功能和子系统,从而显著节省 了时间,减轻了 FPGA 设计工作量。Qsys 是下一代的 SOPC Builder 工具,在 FPGA 优化芯片 网络新技术支持下,与 SOPC Builder 相比,提高了性能,增强了设计重用功能,更迅速的进行 验证。 §1.2 如何构建一个Qsys系统? 图 1.5 我们给出了一个典型 Qsys 系统的框架图,首先,我们要清楚 Qsys 是建立在 FPGA 芯片内部的,对照图 1.4 我们可以发现,Qsys 系统可谓“麻雀虽小,五脏俱全”:Nios II 是 Qsys 系统总的调控中心,相当于计算机系统中的 CPU。Qsys 系统采用 Avalon 总线接口实现系统的 可扩展性,各种外设控制器如串口、通用输入输出接口、存储器、定时器等等通过 Avalon 总线 接口接入系统。这些外设控制器以独立模块的形式加入系统,这些独立的模块就是之前我们所说 的 IP 核。 FPGA Qsys System 处理器位置 UART Cache Avalon® Switch Fabric CPU Debug On-Chip ROM GPIO Timer Custom Logic On-Chip RAM SDRAM Controller 图 1.5 一个典型 Qsys 系统的框架图 Nios II CPU 是 Qsys 系统中最为核心的一个 IP 核。这里的 Nios II 后面所紧跟的 II 是指第 二代的意思,相比于前一代,Nios II 从 16 位升级到 32 位,因此性能更高,占用 FPGA 的资源 更少,而与之配套的开发环境更先进,有更多的资源可供用户使用。Nios II 具有完全可定制和重 新配置特性,所实现的产品可满足现在和今后的需求。Nios II 处理器系列包括三种不同等级内核 可供用户配置——快速(Nios II/f)、标准(Nios II/s)和经济型(Nios II/e),每一型号都针对价 格和性能范围进行了优化。这三种不同等级 Nios II 处理器的特性如表 1.1 所示。所有这些内核共 享 32 位指令集体系,与二进制代码 100%兼容。 Zircon Opto-Electronic Technology CO.,Ltd. §1 Qsys,What,How,Why 9 特性 说明 流水线 乘法器 指令缓冲 数据缓冲 定制指令 表 1.1 三种不同等级 Nios II 处理器的特性 Nios II /f (快速) Nios II /s (标准) Nios II /e (经济) 针对最佳性能优化 平衡性能和尺寸 针对逻辑资源占用优化 6级 5级 无 1 周期 3 周期 软件仿真实现 可设置 可设置 无 可设置 无 无 256 256 256 作为 SOPC 系统,Qsys 的外设具有很强的可定制性,开发包含有一套通用外设和接口库。 这些外设和接口库列于表 1.2 之中。利用 Qsys 软件中的用户逻辑接口向导,用户可以生成自己 的定制外设,并将其集成在 Nios II 处理器系统中。此外,用户可以在 Altera FPGA 中,组合实 现现有处理器无法达到的嵌入式处理器配置。 定时器/计数器 用户逻辑接口 外部 SRAM 接口 SDR SDRAM 表 1.2 Qsys 的 开发包有一套通用外设和接口库 外部三态桥接 EPCS 串行闪存 控制器 串行外设接口 (SPI) S8900 10Base- JTAG UARTC 以太网接口 PCI T 接口 片内 ROM 直接存储器通道 (DMA) 紧凑闪存接口 (CFI) 片内 RAM LAN 91C111 10/100 有源串行存储器 接口 LCD 接口 系统 ID UART 并行 I/O PCI DDR SDRAM CAN RNG USB DDR2 SDRAM DES 16550 UART RSA 10/100/1000 I2C Ethernet MAC SHA-1 浮点单元 下面我们来简述一下设计 Qsys 系统的流程以及所需要的软件工具。一般来说设计系统之前 我们首先应该分析系统需求主要包括功能需求和性能需求等,往往需要画出如图 1.6 的草图。在 草图中,需要对系统功能做模块性的划分,并且要决定哪些功能是由硬件描述语言直接实现的, 而剩下的交由 Qsys 系统完成。 http://www.fpga.gs/ 10 软核演练篇 §1 图 1.6 设计 Qsys 系统所画的草图示意图 其次采用 Quartus II 软件(图 1.7 中①所示)建立工程与顶层实体,在 Quartus II 完成硬件 语言直接实现的那部分逻辑电路。然后调用 Qsys 集成开发工具(图 1.7 中②所示)生成一个用 户定制的 SOPC 系统模块,包括 Nios II 处理器以及相应的外设模块;这些模块可以是 Altera 公 司提供的 IP 核、第三方提供的或用户自己定制的 IP 核。将 Qsys 系统集成到 Quartus II 顶层实 体工程中, 并将此与之前所完成的采用硬件语言直接设计的逻辑电路连接起来; 最后分配管脚、 配置工程、编译生成系统的硬件配置文件.sof 和.pof 文件;将配置文件下载到开发板上进行初步 验证; 上述过程完成了硬件部分的开发,随后进入软件开发部分,这里采用的软件是 Nios II SBT for Eclipse (在 Quartus 10.0 之前称为 Nios II IDE),它是 Nios II 系列嵌入式处理器的基本开 发工具(图 1.7 中③所示)。所有软件开发任务都可以 Nios II SBT for Eclipse 下完成,包括编辑、 编译和调试程序。调试无误的程序由 Nios II SBT for Eclipse 生成可执行文件.elf,将其下载到开 发板并运行于 Qsys 中的 NIOS II 处理器上。最终进行软件硬件联合调试,直到软硬件协同工作。 在上面的过程中,用到的三个最重要的软件为 Quartus II、Qsys 与 Nios II SBT for Eclipse。 其中后两者的使用是我们此本教材的核心内容。当然在此过程中我们还会用到其他的软件如 ModelSim,Signaltap 等,关于 Quartus、Modelsim、Signaltap 的使用可以参考《软件工具篇》 的相关部分。 Zircon Opto-Electronic Technology CO.,Ltd. §1 Qsys,What,How,Why 11 FPGA 2 Integration Tool Qsys System 3 处理器位置 UART Cache Avalon® Switch Fabric CPU Debug Nios II SBT for Eclipse On-Chip ROM GPIO Timer Custom Logic On-Chip RAM SDRAM Controller 1 图 1.7 开发 Qsys 系统所需要用到的软件工具 §1.3 为什么要采用Qsys? 为什么要采用 Qsys,也许是每个初学者阅读这本教程时所最关心的问题。这个问题的答案 并不难理解:我们先回想一下,当你还没有步入这本教程时,你能够采用硬件描述语言(Verilog) 所能实现的最复杂功能的逻辑电路是什么?也许你会回答,应该是那个示波器吧,在示波器的代 码中,我们其实只是完成了几个并不复杂的操作,但却动用了上千行的代码与若干个复杂的状态 机,去完成那些简单的循序选择操作。而这些操作仅仅是构成一个我们前面所述的复杂系统的最 最基本的元素。设想一下:如果单纯采用硬件描述语言去实现上一节我们所描述的类似于计算机 系统一样庞大的工程,那么其想对应的工作量是难以想象的。然而在当今的消费电子以及工业控 制领域,往往我们所面临的系统都是十分庞大且完整的。它们通常包括嵌入式处理器、各类通讯 接口、DSP、数字通信模块、存储电路、及普通数字系统等,甚至有的系统还包括了多个(协) 处理器或加速系统。这些复杂如果单纯采用纯硬件描述语言实现,即使是一个从事 FPGA 行业 多年工程师,也会望而生畏。 I/O Flash MCU I/O SDRAM I/O I/O I/O I/O FPGA CPU DSP DSP FPGA Flash SDRAM 图 1.8 Qsys 有助于简化系统、缩小电路板尺寸与成本 那么换种思路:采用其它的通用集成电路芯片如单片机、ARM、DSP 作为主控制器去实现 这样一个完整的系统,那么我们电路板是定会是极其庞大而复杂的,它们往往包括主控制器、数 字信号处理模块、存储器及其控制模块,与外部通信的各种接口及其协议模块,含有 AD/DA 的 模拟前段模块、电源及功耗管理模块,如图 1.8 左图所示。这样一个系统线的相当大复杂,不仅 http://www.fpga.gs/ 12 软核演练篇 §1 调试难度大,而且系统调试起来也相当不方便。 Qsys 就是为了解决上述问题而诞生的,如图 1.8 右图所示。Altera 公司已经帮助大家考虑 到采用硬件描述语言实现一个完整系统的复杂度,因此 Altera 公司动用了大量工程师设计出了 主控制器、数字信号处理模块、存储器及其控制模块,与外部通信的各种接口及其协议模块的硬 件描述语言实现框架,也就是我们前面所说的 IP 核。因为所有的主控制器以及接口模块它们在 实现上是有所相同的,你所要设计的 CPU 和我需要的 CPU 在很大一部上功能是重叠的,因此, Altera 工程师所提供给我们的 IP 核具有很强的定制型,它只是给出了框架,而具体的实现是由 用户所最终定制的。打个比方,我所要的 CPU 也许需要两个定时器、三片存储器和两个串口, 而你所要的 CPU 也许只要一个定时器、一片存储器和十个串口,它们本质上是没有区别的,只 是数量有所差异,采用 Qsys 我们很轻松的将这些 Altera 工程师为我们所设计的 IP 核,也就是 我们所需要实现的各种模块的一个框架具体化,最终构建出属于你自己的嵌入式系统。在此过程 中,你只是简单的点了几下鼠标,也许简简单单的敲了十几行的代码,就完成了原本需要写上几 千行的 Verilog 代码才能实现的功能模块,而在此过程中 Qyss 起了最关键的作用。因此,当你 在跟随着我们的教程,轻点了几次鼠标就完成了一个连你都为之骄傲的庞大工程系统时,不要忘 了抽空看一看 Qsys 为你生成的成千上万行的 Verilog 代码文件,它们凝结了 Altera 工程师们大 量的智慧与心血! 图 1.9 Nios II:世界上最多功能的嵌入式处理器(源自 https://www.altera.com/products/processors/overview.html) Qsys 所 带 来 的 福 音 , 不 仅 仅 限 于 FPGA 学 习 者 。 打 开 Altera 官 网 Nios II 的 (https://www.altera.com/products/processors/overview.html)第一行印入眼前的大字就是: Nios II Processor: The World's Most Versatile Embedded Processor(图 1.9)。Altera 致力于 将其 Nios II 处理器打造全世界最通用的处理器,Nios II 处理器作为 Qsys 核心部件具有很强的 可定制特性和性能,较低的产品和实施成本以及很强的易用性和适应性。当我们采用了包含有 Zircon Opto-Electronic Technology CO.,Ltd. §1 Qsys,What,How,Why 13 Nios II 处理器的 Qsys 系统时,我们完全可以将原本电路板上分立的单片机、DSP 以及其他外 围设备模块纳入 FPGA 芯片内部实现,Nios II 不但可以承担系统主控制器的功能,还可以进行 数据信号处理,并耦合各类外设接口。通过此种架构构成的系统不仅结构上简单明了,而且硬件 复杂度以及电路板尺寸可以得到大大的简化,如图 1.10 所示。这种优势在对于需要多个 CPU 的 复杂系统中愈显明显,我们甚至可以在一片 FPGA 内部构建多个 Nios II 处理器,它们可以是主 CPU,也可以是协处理器,在同一芯片内部协调有序的完成整个复杂系统的各项功能。Qsys 这 种基于可重构 SoC 的设计技术不仅有助于简化设计,降低电路板尺寸,缩短产品研发周期,节 约系统开发成本。还有助于最优化资源配置,也就是说可以针对你的不同项目需要定制出完全属 于你自己的 SOPC 系统与处理器。就好比之前我们所举的例子,你不得不承认在某种情况下, 你有可能要构建一个我们上面提及到的这样一个奇葩的系统,它只有一个 CPU,一个定时器, 一片存储器,但它还需要有十个串口。如果我们采用标准的 51 单片机去完成这项工作,那后果 是不堪设想的:因为一个标准 AT89C51 单片机只支持一个串行接口,这就意味着你最少需要十 个单片机去完成十个串口接口的控制与通信。不仅如此,如何完成这十个单片机之间的协调与控 制,这将会是比串口控制更加头疼的一大问题。而采用 Qsys 我们也许只要一片很廉价的 FPGA, 就能实现十分优雅的实现上述相同的功能。这个例子也许并不是十分恰当,但是却简单直观的表 现了采用 Qsys 系统为电子设计所带来的革命性优势。 … FPGA … 图 1.10 Qsys 可以将十个串口控制器集成到一个 FPGA 芯片中 哦,对了!不要以为 Qsys 只能做十个串口这种小儿科的功能,Altera Qsys 以及其 Nios II 嵌入式处理器,已经在各行各业中起到了非常广泛的应用,如图 1.11 所示。这些知名公司我想大 家都不陌生吧!他们涉及的行业上至航空航天、广播无线电通信,下至汽车交通、科研医疗、工 业计量、消费电子。在他们的产品中或多或少的都会用到 Altera Qsys 以及其 Nios II 嵌入式处 理器的技术。 http://www.fpga.gs/ 14 软核演练篇 §1 图 1.11 应用 Qsys 以及其 Nios II 嵌入式处理器的代表性公司 我们下面仅举两个比较代表性的例子。第一个例子涉足通信领域, Crescendo Networks (科锐先达)公司的 Maestro CN-5000E(图 1.12),它是一个高性能的 Web 应用交付控制器。它 的主要功能之一是从服务器上卸载消耗资源型的任务,释放运算资源用于核心功能,如处理应用 数据。并能够有效面对,Web 门户在内的大量用户同时访问所带来的承重服务压力以及高峰负 载。针对此大数据量,高实时性要求,Crescendo Networks 采用了基于 Qsys(前 SOPC Builder) 的 SOPC 硬件框架,仅采用一片 Startix EP1S80 FPGA 完成了所有的硬件控制功能,其中最核 心的部分就是其在 FPGA 内部同时构建了 8 个 Nios II 软核处理器用于同时协同实现 2 个 20G 高速内存接口交互以及 2 个 10G 芯片到芯片的端口控制。这 8 个 Nios II 处理器还协同完成了完 整的 TCP 协议,以及复杂的存储器管理。 10 Gigabit chip2chip Interface Post/Pre Processing Unit 20 Gigabit Memory Interface Memory Controller Nios II 1 Nios II 2 Nios II 8 Memory Controller 20Gigabit Memory Interface Pre/Post Processing Unit 10 Gigabit chip2chip Interface Stratix EP1S80 图 1.12 Crescendo Networks 公司的 Web 应用交付控制器 Maestro CN-5000E Zircon Opto-Electronic Technology CO.,Ltd. §1 Qsys,What,How,Why 15 相比于传统现有的通用型集成电路处理器以及那些全定制或半定制的 ASIC 而言,采用以 Qsys 为框架的 SOPC 所带来的优势是革命性的。Crescendo Networks 美国分部总裁这样评价 道:"Altera's Nios II processors and Stratix FPGAs are at the very heart of our Maestro platform, delivering the core functionality that enables it to provide unprecedented data center functionality. We have been evaluating the Nios II processor for some time, and even in its pre-released form, it is a very impressive offering, providing 2x the performance in our product and considerable resource savings. ” ( 来 源 : PR newswire 网 站 报 道 http://www.prnewswire.com/news-releases/altera-delivers-worlds-most-versatileembedded-processor-with-Nios-ii-family-74090317.html)在此系统中,Altera Qsys 以及其 Nios II 嵌入式处理器为传输控制协议提供高性能的硬件加速性能,而 Nios II 提供了更加灵活多 样且数量丰富的用户自定义处理器。 我们下面所举的例子更加接地气一些,该产品涉足汽车电子领域:Blaupunkt(蓝宝公司) 的 TravelPilot Rome 便携式卫星导航仪(图 1.13)。除了导航功能以外,还支持 MP3/WMA 播 放以及照片浏览功能。支持语音导航、3D 地图显示、超速提示以及 TMC 交通信息服务等等功 能。该系统仅仅采用了单片 Cyclone 系列的低成本 FPGA,在其上构建了基于一个 Nios II 软核 处理器的 SOPC 系统,由于 FPGA 与 SOPC 系统的简单灵活特性,整个系统的开发周期仅仅 采用了六个月就完成了。此外,单 FPGA 实现了卫星导航、多媒体播放、地图数据管理、语音 导航、交通信息服务等众多功能,大大缩减了系统的成本。 图 1.13 Blaupunkt(蓝宝公司)的 TravelPilot Rome 便携式卫星导航仪 从上述两个例子可以看出,在 FPGA 内部构建基于 Qsys 的 SOPC 以及 Nios II 处理器提供 了众多独特的功能与优势:我们可以自定义最适合我们应用的 CPU 外设与接口;不需要改动电 路设计,就能对整个硬件系统结构重新布局,更新升级;有效缩减产品的开发周期;将多种功能 集成单个芯片实现,降低系统的成本、复杂度和功耗;甚至可以在单片 FPGA 内部构建出多个 CPU 完成复杂的数据处理或通信控制功能。当然所举的两个例子,仅仅是 Qsys 与 Nios II 应用 的冰山一角,据 Altera 官方提供的数据 Nios II 现已成为业界最流行的软核处理器,已经有超过 http://www.fpga.gs/ 16 软核演练篇 §1 15000 家企业,在产品开发中应用到 Qsys(前 SOPC Builder)SOPC 框架与 Nios II 软核处理 器技术。相信在不久的将来,你定会成为一名优秀的工程师,你将会有机会将你所学会的 FPGA 以及 Qsys SOPC 技术实际应用到你的工程项目中去。用你的一次次看你平凡的鼠标点击,看似 单调乏味的程序代码,构建出一个个功能强大具有灵魂的电子作品! Zircon Opto-Electronic Technology CO.,Ltd. 建立你的第一个 Qsys 系统 第二章 建立你的第一个 Qsys 系统 通过上一章节如何构建一个 Qsys 系统的学习,我们知道了构建一个 Qsys 系统需要三个工 具,它们分别是:Quartus II、Qsys 和 Nios SBT for Eclipse。下面我们就使用这三个工具来手 把手教大家创建一个最基本的 Qsys 系统,并在该 Qsys 系统上开发一个简单的应用程序。通过 Qsys 系统创建和应用程序的开发,我们将引导读者学会 Quartus II、Qsys 和 Nios II SBT for Eclipse 三种开发工具的使用和操作。 §2.1 Qsys的开发流程 在开始创建 Qsys 系统前,我们先来介绍一下 Qsys 的开发流程。Qsys 的开发流程主要有 以下三个步骤: (1) 采用 Qsys 集成开发工具定制 SOPC 系统模块,包括 Nios II 处理器以及相应的外设模 块; (2) 采用 Quartus II 软件建立工程与顶层实体,完成硬件语言逻辑电路。 (3) 采用 Nios II SBT for Eclipse 编写与 Qsys 系统相配套软件使其运行于 Nios II 处理器 上。 在上述步骤里,采用 Quartus II、Qsys 中进行的设计我们一般称为硬件设计或硬件开发, 而采用 Nios II SBT for Eclipse 所完成的设计我们称为软件设计或软件开发。对于比较简单的系 统,一个人便可执行所有设计;对于比较复杂的系统,硬件和软件设计可以分开进行。接下来我 们根据图 2.1 所示的开发流程图,分别对硬件开发和软件开发进一步介绍。 处理器库 Qsys 配置Nios II处理器 自定义命令 外设模块库 选择并配置外设.IP IP 模块 硬件开发 • HDL 源文件 • Testbench 编译(分析与综合、布 局布线、时序分析等) 管脚连接分配 • 用户逻辑设计 • 其它的IP模块 • SOPC Builder的 顶层.bdf文件 Quartus II 连接各外设模块 软件开发 *.qip 分配外设地址和中断 生成系统 Nios II SBT for Eclipse *.sopcinfo • C头文件 • 用户库函数 • 外设驱动 硬件配置 文件*.sof 验证调试 可执行代码*.elf JTAG 串口、以太网 编译、连接、调试 Altera FPGA 片上调试 (软件跟踪、硬件断点 SignalTap II) • 用户代码 • 库函数 • 操作系统 (RTOS) GNU Tools 20 软核演练篇 §2 图 2.1 Qsys 系统的开发流程 首先我们介绍的是硬件开发,硬件设计的具体步骤如下: (1) 使用 Quartus II 软件创建一个新工程,选取具体的 Altera FPGA 器件型号;当工程创建 完毕后,在 Quartus II 软件中打开 Qsys 软件,从 Qsys 软件的处理器库和外设模块库中选择合 适的 CPU、存储器以及各外围器件(如片内存储器、PIO、定时器、UART 等 IP 核),并定制和 配置它们的功能;分配外设地址及中断号;设定复位地址;最后生成系统。用户也可以添加用户 自身定制指令逻辑到 Nios II 内核以加速 CPU 性能,或添加用户外设以减轻 CPU 的任务。 (2) 使用 Qsys 软件生成系统后,将其集成到整个 Quartus II 工程中。可以在 Quartus II 工 程中加入 Nios II 系统以外的逻辑,大多数的 SOPC 设计都包括 Nios II 系统以外的逻辑,这也是 SOPC 系统的优势所在。用户可以集成自身定制的硬件模块到 SOPC 设计,或集成从 Altera 或 第 3 方 IP 供应商中得到的其他现成 IP 核模块。 (3) 使用 Quartus II 软件为 Nios II 系统上的各 I/O 分配引脚,另外还要根据要求进行硬件编 译选项或时序约束的设置;最后编译 Quartus II 工程,在编译过程中 Quartus II 将对 Qsys 生成 的系统的 HDL 设计文件进行布局布线,从 HDL 源文件综合生成一个适合目标器件的网表,生 成 FPGA 配置文件(.sof)。 (4) 使用 Quaruts II 中的 Programmer 软件和 Altera 下载器(USB-Blaster),将配置文件 下载到目标板上。当校验完当前硬件设计后,可将新的配置文件下载到目标板上的非易失性存储 器里(如 EPCS 器件)。下载完硬件配置文件后,软件开发者就可以将此目标板作为软件开发的 初期硬件平台进行软件功能的开发验证了。 接下来我们介绍的是软件开发, 软件设计的具体步骤如下: (1) 打开 Nios II SBT for Eclipse 软件,将 Qsys 软件生成的.sopcinfo 文件导入到 Nios II SBT for Eclipse 软件中即可进行用户程序开发。这一步可以在 Qsys 生成系统模块后立即进行,也可 以在整个系统编译完成,并将.sof 文件下载到目标板后进行。利用 Nios II SBT for Eclipse 软件 开发这个过程与传统嵌入式系统的软件开发类似,唯一的不同在于,软件所运行的嵌入式系统是 自己定制的、裁剪过的,因此,可能受硬件的局限会小一些。 (2) 当在 Nios II SBT for Eclipse 软件中完成了用户程序时,便可对用户软件进行编译,如果 编译没有错误,软件便会生成可执行文件*.elf,将其下载到目标板中便可执行程序(下载*.elf 前, 要先给目标板下载.sof 文件,否则将会出现错误提示)。 (3) 修改用户程序直到硬件和软件设计都达到设计要求,利用 Flash 下载工具,将配置文件 *.sof 和可执行文件*.elf 编程到 Flash 中即可完成上电程序自启动。 至此,Qsys 的开发流程我们就讲解完了,读到这里,你是不是已经有些按耐不住了。别急, 在下一小节中。我们将对上述过程进行亲手实践,手把手教你构建一个最基本的 Qsys 系统。 §2.2 手把手教你构建一个Qsys系统 通过前面 Qsys 开发流程的学习,我们知道想要创建一个 Qsys 系统,我们需要使用 Qsys 软件,接下来我们是不是就要打开 Qsys 软件,来创建 Qsys 硬件系统了呢?且慢,且慢,大家 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 21 还记得我们上一章节中的草图吗?在开始动手之前,我们应该根据我们所要实现的任务功能,来 规划出我们的 Qsys 系统。通过规划出的 Qsys 系统,我们才能够知道,我们应该选择什么样的 IP 核,来构建我们的 Qsys 系统。接下来我们就来规划一下我们本次实验的 Qsys 系统,由于我 们本次实验想要实现的功能是,利用按键中断来控制 LED 亮灭。所以,我们就可以根据这个功 能,简单的画出我们的 Qsys 系统框架图,如图 2.2 所示。 JTAG接口 时钟接口 PIO JTAG 指令 PIO Nios II/f Avalon 总线 数据 ROM RAM LED接口 KEY接口 Altera FPGA 图 2.2 利用按键中断控制 LED 亮灭的 Qsys 系统框图 在该图中,我们可以看到,我们创建的 Qsys 系统有 Nios 核、系统时钟、JTAG 接口、片 内 ROM、片内 RAM 和 PIO 接口,PIO 接口上又接有 LED 和按键两个外设。通过前面我们对 系统概念的讲解,我们可以知道,Nios II 是 Qsys 系统总的调控中心,负责中断分配、地址管理、 内存调度等总的控制任务。通用输入输出接口 PIO、存储器 RAM 和 ROM 等,这些外设控制器 负责与外部设备连接,控制外设的行为。Nios II 和各个外设控制器之间的通信是通过 Avalon 总 线。由此可见,该框架图就是一个典型的系统。 接下来我们就可以动手进行操作了,在操作过程中,大家可以一边看着文档,一边跟着我们 的文档一步步自己尝试操作。当然,这个操作过程,我们在视频教程中也有详细的讲解,如果你 不喜欢跟着文档操作,那么你可以跟着我们的视频教程进行操作。 2.2.1 使用 Quartus II 创建新工程 下面我们就来创建一个新工程。在创建工程之前,我们建议大家在硬盘中专门建立一个文件 夹专用于存储自己的 Quartus II 工程,这个工程目录的路径名应该只有字母、数字和下划线,以 字母为首字符,且不要包含中文和其他符号。(想要成为一个优秀的嵌入式工程师,首先要养成 一个良好的习惯)。创建好了我们的专属文件夹后,我们便可以打开 Quartus 软件。在这里我们 另外补充说明一下,Qsys 和 Nios II SBT for Eclipse 这两个软件不需要我们另外安装,我们在 安装 Quartus 软件时,它们会一并安装,如果没有安装该软件的朋友,可以参考我们的《Quartus 软件安装篇》来进行安装。我们这里使用的软件是 Quartus II 13.1 版本,Quartus II 13.1 版本目 前是最稳定的一个版本。 回到我们之前的话题,大家看图 2.3,这个就是 Quartus II 软件操作界面,学习过我们《软 件工具篇》的朋友,想必 Quartus II 这个软件用的很熟了,这里我们就不在去介绍它了,不熟悉 的朋友可以去看我们的《软件工具篇》来学习 Quartus II 软件的使用。 http://www.fpga.gs/ 22 软核演练篇 §2 图 2.3 Quartus II 13.1 软件界面 我们在 Quartus II 软件的菜单栏中找到【File】→【New Project Wizard…】来新建一个工 程。这里要注意不要把【New…】误认为【New Project Wizard…】。新建工程向导说明页面如 图 2.4 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 23 图 2.4 新建工程向导说明页面 在“Introduction”页面中,我们可以了解到在新建工程的过程中要完成哪些工作,这些工作 主要包括以下 5 点: (1) 指定项目目录、名称和顶层实体; (2) 指定项目设计文件; (3) 指定该设计的 Altera 器件系列; (4) 指定用于该项目的其他 EDA 工具; (5) 项目信息报告; 单击下面【Next>】按钮进入图 2.5 所示页面。任何一项设计都是一项工程,必须为此工程 建立一个放置与此工程相关的所有文件的文件夹,此文件夹将被 Quartus II 默认为工作库。通 常,不同的设计项目最好放在不同的文件夹中,而同一工程的所有文件都必须放在同一文件夹中。 http://www.fpga.gs/ 24 软核演练篇 §2 图 2.5 新建工程路径、名称、顶层实体指定页面 在这个页面中,我们可以看到有三个文本框,第一个文本框是让我们填写工程文件夹路径, 第二个文本框是让我们填写工程名,第三个文本框是让我们填写顶层文件的实体名。这里我们设 置的工程路径为 D:/Zircon_Qsys/Qsys_First 文件夹,这个 Zircon_Qsys 文件夹就是我们专门 用来存放 Quartus II 工程的,这个 Qsys_First 文件夹就是我们现在创建的工程文件夹,为了方 便工程的查找使用,我们这里就把工程名和顶层文件的实体名都命名为 Qsys _First。填写好了 以后,我们就可以点击【Next>】进入图 2.6 所示页面。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 25 图 2.6 工程添加文件页面 在这个页面中,我们可以添加已经设计好的文件,由于我们是新工程,并没有文件可以添加, 所以我们就不需要任何操作,直接点击【Next>】,进入图 2.7 所示页面。 http://www.fpga.gs/ 26 软核演练篇 §2 图 2.7 新建工程选择器件页面 在这个页面中,我们需要选择目标器件,在选择目标器件时,我们需要根据实际所用的 FPGA 芯片来进行选择,我们查看开发板主芯片,可以从主芯片上看到,我们使用的是 Cyclon IV E 系 列的“EP4CE10F17C8”。接下来我们就可以在下面的芯片列表中找到 EP4CE10F17C8 这个芯 片,找到这个芯片后,我们通过鼠标左键点击选中这个芯片就可以了。如果我们对这款芯片比较 熟悉,知道这个芯片的封装和引脚数,那么我们就可以通过这边的筛选功能,来加快芯片查找的 速度。大家看,我们还能够在这个窗口中查看到每个芯片器件的内核电压,逻辑资源,用户 I/O 等芯片的一些基本信息。选择完器件后,单击【Next>】进入图 2.8 所示页面。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 27 图 2.8 新建工程选择器件页面 在这个页面中,我们可以看出它是用来选择其他的 EDA 工具的,可以选择综合工具,还可 以选择仿真工具,由于我们本次实验着重介绍 Qsys 这个软件,所以我们在这里就不介绍其他的 EDA 工具了,我们将这里的所有选项全都更改为。接下来我们点击【Next>】进入图 2.9 所示页面。 http://www.fpga.gs/ 28 软核演练篇 §2 图 2.9 新建工程配置信息报告页面 在这个页面中,我们可以看到整个工程的创建信息,这些信息都是我们前面已经设置好的信 息,我们粗略的扫了一下并没有发现错误的设置,下面我们就可以点击【Finish】即可完成工程 的创建。这里需要注意的是,工程建立完毕后,还可以根据设计中的实际情况,通过选择 【Assignments】菜单下的【Device…】或者【Settings…】对工程进行重新设置。 2.2.2 使用 Qsys 软件创建 Qsys 系统 创建好工程后,我们就要使用 Qsys 软件来创建 Qsys 硬件系统了。我们在 Quartus II 菜单 栏中选择【Tools】→【Qsys】选项来启动 Qsys 软件,如图 2.10 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 29 图 2.10 Qsys 软件界面图 大家看,这个就是 Qsys 软件工具界面,在这个界面中,我们可以看到系统默认为我们添加 了一个时钟 IP 核组件,在定制系统前,我们首先就需要指定系统的时钟频率,Qsys 将利用这个 时钟频率来产生时钟分频或波特率等,需要注意的是,所设置的频率要与系统实际运行的时钟频 率相匹配。这里我们双击时钟组件或者右键时钟组件点击【Edit…】选项,就可以修改时钟频率, 如图 2.11 所示。 http://www.fpga.gs/ 30 软核演练篇 §2 图 2.11 修改时钟频率页面 在该页面中,我们可以看到默认的时钟频率是 50MHz,这里我们就将它修改成 100MHz, 修改时钟频率可以提高 Nios II 的运行速度。修改好了以后,我们直接点击【Finish】即可完成修 改。时钟 IP 核配置好了以后,我们就可以根据之前规划好的 Qsys 系统框架图,来一步步添加 我们的 IP 核。首先我们添加是 Nios II IP 核,没有使用过 Qsys 软件的朋友也许会有这么一个疑 问,如何添加 Nios II IP 核呢?其实添加 Nios II IP 核是非常简单的。在图 2.10 所示的 Library 窗 口中我们可以看到 Altera 为我们提供的所有的 IP 核组件,我们可以在 Embedded Processors 中找到 Nios II Processor 内核组件,也可以在上面的搜索栏中输入 Nios 来快速找到 Nios II Processor 内核组件。找到 Nios II Processor 后,我们双击 Nios II Processor,便会弹出 Altera Nios II 配置向导页面,如图 2.12 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 31 图 2.12 Nios II 内核配置向导页面 大家看,这个就是 Nios II IP 核的设置页面,在这个设置页面中,我们可以设置不同等级的 Nios,e 等级的 Nios II 它消耗的逻辑资源最少,但是运行速度最慢,f 等级的 Nios II 它消耗的逻 辑资源最多,但是它的运行速度是最快的,而 s 等级的 Nios II 处于 e 和 f 之间。看到这里,也 许有朋友会问,什么时候用 e 等级?什么时候用 f 等级?,关于 Nios 等级的选择,我们是要根 据应用要求和逻辑资源来决定的。如果你的应用对速度要求比较高,并且逻辑资源也很充足,那 么你可以选择 f 等级的 Nios,否则只能选择低等级的 Nios。我们本次实验对 Nios 等级并没有什 么要求,这三种等级的 Nios 我们都可以选择使用,在这里我们选择的是 Nios II/f。 选择完了 Nios 等级,接下来我们再来看硬件运算操作(Hardware Arithmetic Operation), 在 Nios II/s 和 Nios II/f 标准中想必大家都有看到 Hardware Multiply(硬件乘法器)和 Hardware Divide(硬件除法器),Nios II/s 和 Nios II/f 的 DMIPS 性能主要依赖于硬件乘法选项。这里我们 就说下什么是 DMIPS(MIPS 相信大家还是比较熟悉的,它是 Million Instructions Per Second 的缩写,即计算机每秒钟执行的百万指令数,D 是 Dhrystone 的缩写,它表示在 Dhrystone 这 样一种测试方法下的 MIPS,Dhrystone 是一种整数运算测试程序,主要用于测整数计算能力)。 由于我们选择的是 Nios II/f,所以这里我们选择使用硬件乘法器和硬件除法器。其他选项的设置, 如复位地址和异常地址,我们将在稍后的设置中进行说明,这里我们先不更改默认即可。 下面我们来看图 2.13 所示的 Caches and Memory Interfaces 页面。 http://www.fpga.gs/ 32 软核演练篇 §2 图 2.13 Caches and Memory Interfaces 页面 在图 2.13 页面中我们可以看到 Instruction Master(指令主端口)和 Data Master(数据主 端口),我们可以在这里设置它们各自 Cache 容量的大小和突发传输的方式,需要注意的是, Tightly Couplein Instruction Master Prot(紧耦合指令主端口)和 Tightly Couplein Data Master Prot(紧耦合数据主端口)的设置,该选项用于在 Nios II CPU 核里面构建与 CPU 外部的存储 器紧密耦合的数据端口。通过该端口与存储器交换数据比通过 Avalon 总线块,如果选择了这个 端口,那么必须指定片内存储器,并且手工将端口与存储器相连。关于指令和数据,还有紧耦合 详细内容可参考 3.1.5 节中的介绍。这里我们使用默认的设置即可。 下面我们来看图 2.14 所示的 Advanced Features 页面。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 33 图 2.14 Advanced Features 页面 从图 2.14 中我们可以看到中断控制配置有两种,一种是内部控制器,另一种是外部控制器; 内部控制器只能用于 Nios II/e 和 Nios II/s,而外部控制器通常和影子寄存器配合使用,这种配合 使用可以大大减少中断延迟,这里要注意的是,外部控制器只能用于 Nios II/f。Nios II 处理器可 以选择一个或多个影子寄存器组,它是通用寄存器组的一个完整的备用,用于在 ISR 中维持一 个独立的上下文环境,关于影子寄存器和外部中断的详细内容可参考第 3.1.4 节中的介绍。 下面我们再来看一下检查设置提供的几个选项:illegal instruction(非法指令)、division error (除法错误)、misaligned memory access(非法存储器访问):不一致的指令/数据地址导致出 现非法存储器访问、extra exception information(异常指令)、HardCopy compatible(硬拷贝 兼容)、ECC present(检测错误)。由于上面设置我们几乎用不到,这里我们就不在进一步进行 介绍,直接默认设置即可。 下面我们来看图 2.15 所示的 MMU and MPU SettingsAdvanced Features 页面。 http://www.fpga.gs/ 34 软核演练篇 §2 图 2.15 MMU and MPU Settings 页面 从该图中我们可以看到,这里面主要有两个设置,一个是 MMU,另一是 MPU,所谓 MMU (Memory Management Unit)就是存储器管理单元。所谓 MPU,就是内存保护单元(即:Memory Protection Unit),由于这里我们同样也没有用到 MMU 和 MPU,这里我们也是默认设置,并没 有任何改动,关于 MMU 和 MPU 详细内容可参考 3.1.5 节中的介绍。 下面我们来看图 2.16 所示的 JTAG Debug Module 页面。为了方便调试,为 CPU 加入 JTAG 调试模块,JTAG 调试模块要占用较多的逻辑单元,如果整个系统已经调试完毕了,可以选用 No Debugger,以减少系统占用资源。JTAG 调试模块根据功能的不同,分成了 4 级。具体 JTAG 调试模块内容可参考 3.1.6 节中的介绍。这里我们同样不改变其中的设置,使用默认设置即可。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 35 图 2.16 JTAG Debug Module 页面 至此,我们点击【Finish】来完成 Nios II 处理器的配置,生成一个带有 JTAG 调试接口的 Nios II/f 行 CPU 核,这时,我们会发现主界面窗口中出现了 Nios II 组件,并且还会在 Messages 窗口中出现错误信息,如图 2.17 所示,这些信息我们暂时不必关心,将在后面步骤中对它们进 行处理。 http://www.fpga.gs/ 36 软核演练篇 §2 图 2.17 添加 Nios II 核后的页面 通过前面系统概念的学习我们知道,每个处理器系统至少要有一个存储器用于存储数据和 指令,接下来我们便来添加一个片内存储器,用于存储程序代码以及程序运行空间,我们在 Library 库中搜索直接 Memory 找到 On-Chip Memory(RAM or ROM),双击即可弹出片内存 储器配置向导页面,如图 2.18 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 37 图 2.18 片内存储器 ROM 的配置向导页面 在图 2.18 中的 Memory type 选项区域中选择 ROM,即指定为 ROM 型。FPGA 内部其实 并没有专用的 ROM 硬件资源,实现 ROM 的思想是对 RAM 赋初值,并保持该初值,使其为只 读的。ROM 的内容在对 FPGA 进行配置时,一起写入 FPGA 中。在 Total memory size 文本框 中输入 10240,即指定 10KB 的存储容量。其他的配置我们不做改变,也同样使用默认设置。在 这里需要注意的是:ROM 存储器在上电时可以配置使用 onchip_mem.hex 文件进行初始化。 onchip_mem.hex 在 Quartus II 中可以由用户编辑生成。在这里,由于我们使用 onchip_ROM 来存储用户程序,onchip_mem.hex 将由 Nios II SBT for Eclipse 编译生成,文件的内容即用户 程序。我们点击【Finish】即可完成片内存储器的配置,便可以在 Qsys 界面中看到添加的片内 存储器组件。 下面我们使用同样的方法再添加一个 onchip_RAM,和 onchip_ROM 不同的是,我们在选 择类型时选择 RAM 类型,RAM 的存储大小我们设置 20480bytes,也就是 20KB,RAM 的其他 设置也不做改变,使用默认设置即可,RAM 的配置页面如图 2.19 所示。 http://www.fpga.gs/ 38 软核演练篇 §2 图 2.19 添加片内存储器 RAM 的配置向导页面 下面我们就需要添加 JTAG UART 的组件来实现 PC 和 Nios II 系统间的串行通信。我们需 要在 Library 库中找到 JTAG UART 组件,双击 JTAG UART 组件,将会弹出 JTAG UART 配 置向导页面,如图 2.20 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 39 图 2.20 JTAG UART 配置向导页面 这里我们也可以直接使用默认设置,点击【Finish】完成配置即可,关于 JTAG UART 详细 内容请参考 4.7 节中的介绍。 下面我们需要添加 System ID,这里我们有一点要注意,之前在 SOPC Builder 中 System ID 是自动生成的,但是在 Qsys 里已经不会再自动生成了,我们在 IP 核库中搜索到 System ID 后,双击 System ID,将会弹出 PIO 配置向导页面,如图 2.21 所示。 http://www.fpga.gs/ 40 软核演练篇 §2 图 2.21 System ID 配置向导页面 这里我们使用默认配置,点击【Finish】完成配置即可,关于 System ID 详细内容请参考 4.5 节中的介绍。 下面我们需要添加 PIO,PIO 为 Nios II 处理器系统接收输入信号以及输出信号提供了一种 简易的方法。我们需要在 Library 库中找到 PIO 组件,双击 PIO 组件,将会弹出 PIO 配置向导 页面,如图 2.22 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 41 图 2.22 输出 PIO 的配置向导页面 从该图中可以看出,我们将宽度 8 改为了 1,其余的我们使用默认配置,点击【Finish】完 成配置即可。接下来我们利用同样的方法再来添加一个 PIO,不过,这里要添加的 PIO 是和上 面的 PIO 配置是完全不同的,上面的 PIO 我们用作了输出端口,而下面我们添加的 PIO 是用它 来作为输入端口,并且还要带有中断功能,由于我们的按键悬空时是低电平,按下时是高电平, 所以我们这里选择了边沿上升沿触发方式。关于 PIO 详细内容请参考 4.1 节中的介绍。我们直接 给我 PIO 的配置页面向导图,如图 2.23 所示。 http://www.fpga.gs/ 42 软核演练篇 §2 图 2.23 输入 PIO 的配置向导页面 完成了所有的组件添加以后我们需要对每个组件进行重命名,如图 2.24 所示。这里需要注 意的是,由于命名没有一个规范,可以任意命名,我们尽量给出每个硬件外设的描述名称,因为 在编写后面的用户程序中,我们会用到这些名称。 图 2.24 组件添加完毕后的页面 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 43 接下来我们就需要玩上一玩“连连看”,在之前的 SOPC Builder 版本中,添加完了组件以 后,SOPC Builder 会自动连接添加的组件,而在 Qsys 中,Qsys 是不会自动连线的,这里我们 需要我们手动连线,对于一个初用 Qsys 软件的读者来说,什么时候连接数据端口,什么时候连 接指令端口是一件很头疼的事情,这里我们可以遵循以下规则:数据主端口连接存储器和外设元 件,而指令主端口只连接存储器元件。关于指令和数据总线的详细内容可参考 3.1.5 节中的介绍。 因此,我们可以按照上述规则进行如下连线,如果是存储器类 IP 核,比如 onchip_RAM 和 onchip_ROM 等,我们需要将其 Avalon Memory Mapped Slave 端口连接到 Nios II 处理器核的 data_master 和 instruction_master 端口上,如果是非存储器 IP 核,比如 PIO 外设,或者是 System ID 和 JTAG UART 等,我们只需要将其 Avalon Memory Mapped Slave 端口连接到 Nios II 处理器核的 data_master 端口上即可,而时钟端口和复位端口,我们需要全部连接,各 个组件连接完毕如图 2.25 所示。 图 2.25 端口连接完毕的组件页面 细心的读者会发现,在图 2.25 中不仅仅将线连接完毕,而且还多出了几个小细节,首先我 们会发现在 onchip_rom 的地址上面,原本没有上锁的地址,却上了锁,其次在 Export 这一栏 中出现了一个 pio_led 和 pio_key 的端口,这两个端口的作用是引出 Nios II 系统,如何引出呢, 我们只需要在需要引出的端口上面双击即可,名字也是可以任意修改的。这里有一点需要注意, 需要引出的端口是不需要连线的。最后一点,在 IRQ 一栏中出现了一个 0 和一个 1,这里代表了 我们为 jtag_uart 设置了一个 0 中断,为 pio_key 设置了一个 1 中断,关于中断的详细内容可参 考 3.1.4 节中的介绍。接下来我们需要设置 Nios II 复位和异常地址,这里我们需要双击 Nios II 核,返回 Nios II 配置页面中对复位变量和异常变量进行设置,如图 2.26 所示。 http://www.fpga.gs/ 44 软核演练篇 §2 图 2.26 设置 Nios II 复位地址和异常地址 在这里,本系统上电后,从内部 ROM 开始运行,所以复位地址的 memory 设置为 onchip_ROM,offset 地址为 0x00000000。异常向量表放在内部 RAM 中,所以异常地址的 memory 设置为 onchip_RAM,offset 地址为 0x00000020。这里有一点要注意,Reset Address 和 Exception Address 的 Offset 只有在多处理器的系统中才进行设置,且其值必须是 0x20 的 倍数。如果地址设置违反规则,信息窗口会给出错误提示。对于复位地址和异常地址具体的详细 过程会在 3.3.1 节中介绍。 完成了复位地址和异常地址的设置后,Qsys 系统的设计基本上就要接近尾声了,返回到 Qsys 界面窗口,我们仍能够看到漫天红叉,接下来我们只需要轻轻一点,所有红叉将会消失, 即可完成 Qsys 系统的设计。下面我们点击菜单栏中的【System】→【Assign Base Addresses】, Qsys 系统会自动为我们分配地址 Qsys 系统不仅仅只提供了自动分为基地址,还有自动分配中 断等命令,对于许多系统,包括该设计,Assign Base Addresses 能满足要求。当然,用户可以 自己调整基地址和中断优先级来满足系统的要求,Nios II 处理器内核可寻址 31 位地址范围。用 户必须分配 0x00000000~0x7FFFFFFF 之间的基地址。由于 Qsys 不处理软件操作,所以它不 能做出关于最好的中断分配。因此,我们这里建议用户不采用自动分配中断,而自己来分配中断 优先级。 点击了自动分配地址以后,系统中的红叉统统消失不见了,下面我们就可以通过点击菜单栏 中的【Generate】→【Generate…】选项来生成 Qsys 系统,如图 2.27 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 45 图 2.27 生成 Qsys 系统设置页面 由于我们不涉及仿真,所以我们便可以将 Simulation 和 Testbench System 都设为 None 即可。下面我们点击【Generate】,会提示如图 2.28 所示。 图 2.28 保存 Qsys 系统 我们单击【Save】,会给出保存路径对话框,如图 2.29 所示。这里我们就保存在我们的 Quartus II 工程“Qsys_First”文件夹下,系统可以任意命名,我们这里便命名为“Qsys_system”。 http://www.fpga.gs/ 46 软核演练篇 §2 图 2.29 保存 Qsys 系统路径对话框 单击保存即可进行系统生成。在系统生成过程中,Qsys 会执行一系列操作,Qsys 会为添 加的所有组件生成 Verilog HDL 源文件,并生成每个组件以及连接组件的片内总线结构、仲裁和 中断逻辑。Qsys 会为系统生成 Nios II SBT for Eclipse 软件开发所需的硬件抽象层(HAL)、C 以及汇编头文件。这些头文件定义了存储器映射、中断优先级和每个外设寄存器空间的数据结构。 关于硬件抽象层(HAL)详细内容请参考 3.2 节中的介绍。这样的自动生成过程有助于软件设计 者处理硬件潜在的变化性,如果硬件改变了,Qsys 会自动更新这些头文件。Qsys 也会为系统 中现有的每个外设生成定制的 C 和汇编函数库。如果添加了片内存储器,Qsys 还将为片内 ROM、 RAM 生成其初始化所使用的 HEX 文件。在生成阶段的最后一步,Qsys 创建适合与系统组件的 总线结构,把所有的组件连接在一起。生成过程时间随计算机的性能而不同,电脑配置越高编译 速度就会越快,一般需要几分钟。Generate Completed 中显示系统生成的过程。当系统生成结 束后,会出现图 2.30 所示内容。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 47 图 2.30 Qsys 系统成功生成 我们单击【Close】退出并返回到 Quartus II 软件即可。至此,已经完成 Qsys 系统的创建, 生成系统结束后,要将系统集成到 Quartus II 硬件工程并使用 Nios II SBT for Eclipse 来开发软 件,可以先将系统集成到 Quartus II 中,也可以先使用 Nios II SBT for Eclipse 进行软件开发。 当然,如果是多人合作开发,两者可同时进行。接下面我们就来简单介绍一下 Qsys 系统为我们 生成的若干文件,具体的文件目录结构如图 2.31 所示。 Qsys_First Qsys_system.cmp Qsys_system.html Qsys_system.qsys Qsys_system.bsf Qsys_system.sopcinfo Qsys_system_generation.rpt Qsys_system synthesis Qsys_system.v Qsys_system.qip Qsys_system.regmap Qsys_system.debuginfo submodules 图 2.31 Qsys 系统生成的具体文件目录结构图 下面我们就来简单的介绍一下这些文件: http://www.fpga.gs/ 48 软核演练篇 §2 (1) Qsys_system.cmp:这是一个文本文件,包含本地通用端口定义,可用于 VHDL 设计文件中。 (2) Qsys_system.html:系统生成报告文件,主要包含了组件的连接,内存映射地址 及参数分配等信息。 (3) Qsys_system.qsys:这个文件包含了 Qsys 系统中所有添加的 IP 核、及核连接 和核参数。 (4) Qsys_system.bsf:这是 Qsys 生成的一个顶层的符号文件,用于添加到 Quartus II 工程顶层文件。 (5) Qsys_system.sopcinfo:这个文件包含了完整的 Qsys 系统描述,后续的软件项 目依据此文件自动生相关的软件驱动。 (6) Qsys_system_generation.rpt:Qsys 生成日志文件,主要记录 Qsys 在系统生成 中遇到的问题。 (7) Qsys_system.v:这个文件是描述 Nios II 系统的硬件设计文件。Quartus II 软件 将使用这些 HDL 文件来编译整个 FPGA 设计。 (8) Qsys_system.qip:这是 Qsys 的 IP 核文件,在 Quartus II 工程中,不将这个文 件加入到工程项目中,编译时会报错。 (9) Qsys_system.regmap:如果 IP 系统中包含注册信息,Qsys 生成 regmap 文 件.regmap 文件描述的寄存器映射信息主站和从站接口。这个文件的补充文 件.sopcinfo 提供更详细的信息登记系统。这使得在系统控制台中注册显示视图和 用户可自定义的统计数据。 (10) Qsys_system.debuginfo:包含后代信息。用于传递系统控制台和总线分析仪工具 包的信息关于 Qsys 互联。总线分析工具使用该文件来确定调试组件在 Qsys 互联 在以上这么多的输出文件中,我们主要用到三个文件,它们分别是 Qsys_system.qsys、 Qsys_system.qip 和 Qsys_system.sopcinfo。Qsys_system.qsys 主要用于 Qsys 系统的移植。 Qsys_system.qip 主要用于 Quartus II 硬件工程项目中,而 Qsys_system.sopcinfo 则用于 Nios II SBT for Eclipse 软件工程项目中。 2.2.3 集成 Qsys 系统到 Quartus II 工程中 Qsys 系统建立完毕后,下面我们就需要来建立顶层文件,顶层模块用于将真个工程的各个 模块包含在里面,Quartus II 编译时将这些模块整合在一起。顶层文件相当于传统电路设计中的 电路板(PCB),用于将各种功能的芯片焊接在上面。我们选择菜单栏中的【File】→【New…】 选项,则会弹出图 2.32 所示页面。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 49 图 2.32 新建 Verilog 文件页面 这里需要注意的是,如果你喜欢以原理图方式管理顶层文件,那么你就要选择 Block Diagram/Schematic File 作为顶层文件,如果你喜欢以代码方式管理顶层文件,那么就需要选择 Verilog HDL File 文件来作为顶层文件,两种方法都可以完成 Qsys 系统的创建。这里我们就以 Verilog HDL File 文件为例进行介绍,我们选中 Verilog HDL File 后,点击【OK】即可。接下来 我们便需要在顶层文件中编写代码,如代码 2.1 所示。 代码 2.1 Qsys_First.v 代码 1 module Qsys_First 2( 3 //输入端口 4 CLK_50M,RST_N,KEY_PIO, 5 //输出端口 6 LED_PIO 7 ); 8 9 //--------------------------------------------------------------------------10 //-- 外部端口声明 11 //--------------------------------------------------------------------------- 12 input CLK_50M; 13 input RST_N; 14 input KEY_PIO; 15 output LED_PIO; 16 17 //--------------------------------------------------------------------------- http://www.fpga.gs/ 50 软核演练篇 §2 18 //-- 内部端口声明 19 //--------------------------------------------------------------------------- 20 wire clk_100m; 21 22 //--------------------------------------------------------------------------- 23 //-- 逻辑功能实现 24 //--------------------------------------------------------------------------- 25 PLL PLL_Init 26 ( 27 .inclk0 (CLK_50M ), 28 .c0 (clk_100m ) 29 ); 30 31 //--------------------------------------------------------------------------- 32 33 Qsys_system Qsys_system_Init 34 ( 35 .clk_clk (clk_100m ), // clk.clk 36 .reset_reset_n (RST_N ), // reset.reset_n 37 .pio_led_export (LED_PIO ), //pio_led.export 38 .pio_key_export (KEY_PIO ) //pio_key.export 39 ); 40 41 endmodule 代码编写完毕后,我们就需要进行保存,我们可以点击菜单栏【File】→【Sava】进行保存, 也可以点击快捷栏中的保存图标进行代码保存,单击保存出现如图 2.32 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 51 图 2.33 保存顶层文件路径 在上述代码中,我们主要介绍两点:第一点是 PLL IP 核是如何使用的,这里我们并不给出 详细讲解,只给出创建过程,想要深入了解 PLL IP 核的读者,可以参考《软件工具篇中》的介 绍来进一步学习。第二点是 Qsys_system 代码是如何编写的。首先我们介绍的是 PLL IP 核的 创建。在 Quartus II 软件中,我们单击菜单栏中的【Tools】→【MegaWizard Plug-In Manager】 出现图 2.34 所示页面。 http://www.fpga.gs/ 52 软核演练篇 §2 图 2.34 创建 IP 核向导页面 我们这里直接点击【Next>】,我们可以通过搜索栏进行搜索 ALTPLL IP 核,也可以直接找 到 I/O 文件夹下的 ALTPLL IP 核,这里我们需要给 PLL IP 核命名,这里同样也可以任意命名, 我们这里就命名为 PLL。如图 2.35 所示。 图 2.35 选择创建 PLL 向导页面 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 53 接下来我们点击【Next>】进入 PLL IP 核配置向导页面,如图 2.36 所示。由于我们使用的 开发板晶振时钟为 50MHz,所以我们这里 PLL IP 核的时钟输入需要设置成 50Mhz。 图 2.36 PLL IP 核输入时钟配置向导页面 接下来我们点击【Next>】进入“Inputs/Lock”页面,如图 2.37 所示。由于我们这里用不 到上面的两个功能,所以我们这里直接将勾号取消掉。 图 2.37 PLL IP 核“Inputs/Lock”配置向导页面 http://www.fpga.gs/ 54 软核演练篇 §2 接下来我们一路狂点【Next>】,直到进入“Output Clocks”页面,如图 2.38 所示。由于我 们的 Qsys 系统的时钟频率为 100MHz,我们这里的输出时钟也需要设置成 100MHz,以供给 Qsys 系统使用。 图 2.38 PLL IP 核输出时钟配置向导页面 接下来我们便可以直接点击【Finish】,进入最后的总结页面。如图 2.39 所示。这里我们需 要将 PLL_inst.v 勾选上,PLL_inst.v 文件是 ALTPLL IP 核自动生成的使用模板代码,我们只需 要将模板中的代码复制到顶层文件中修改端口便可以直接使用。 图 2.39 PLL IP 核最后的配置总结页面 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 55 我们点击【Finish】完成 PLL IP 核的创建。Quartus II 软件会自动将我们创建的 PLL IP 核 自从添加到工程项目中,我们可以在工程文件夹 Qsys_First 中找到生成的 PLL_inst.v 文件,这 里我们就不在给出代码内容以做对比。 接下来我们要介绍的是,第二点是 Qsys_system 代码是如何编写的,我们返回到 Qsys 系 统软件中,我们单击菜单栏中的【Generate】→【HDL Example…】选项,弹出图 2.40 所示内 容。我们只需要点击【Copy】,便可以直接复制到顶层文件中进行修改。 图 2.40 Qsys 系统自动生成的 HDL 代码 接下来我们就需要将 Qsys 系统添加到 Quartus II 工程项目中,进行编译,看看有没有出现 一些由于操作失误而引起的小错误。我们可以通过 Quartus 软件菜单栏【Assignment】→ 【Settings】进入图 2.41 所示页面。 http://www.fpga.gs/ 56 软核演练篇 §2 图 2.41 将 Qsys 系统添加到 Quartus 项目工程中 我们点击【…】找到 Qsys_system/synthesis/Qsys_system.qip,这里要注意,一定要记得 点击【Add】后,再去点击【OK】,否则将会出现没有添加成功尴尬场面。添加完毕后,我们会 在 Project Navigator 窗口中看到我们工程中的所有的文件,如图 2.42 所示。 图 2.42 Project Navigator 窗口页面 这时,我们便能够进行编译查错了,我们可以通过 Quartus II 软件菜单栏中的【Processing】 →【Start Compilation】来进行编译,也可以通过快捷栏中的快捷键进行编译,这里的编译速度 也是随着电脑性能的越高而越快。我们稍等片刻后。编译就会完成,出现如图 2.43 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 57 图 2.43 编译完成生成报告窗口页面 接下来我们就需要进行 Quartus II 软件中的扫尾工作,配置 IO,分配管脚,下面我们依次 进行设置。首先我们可以通过 Quartus II 软件菜单栏中的【Assignment】→【Device】,然后我 们在 Device 界面中找到【Device and Pin Options…】进入图 2.44 所示页面配置 IO。 http://www.fpga.gs/ 58 软核演练篇 §2 图 2.44 设置 IO 界面 接下来我们便将进入 Unused Pins 页面,对没有使用引脚的进行设置,按照图 2.45 所示, 将未使用引脚设置为高阻输入,这样上电后 FPGA 的所有不使用引脚都将进入高阻抗状态。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 59 图 2.45 未使用引脚设置界面 最后我们通过 Quartus II 软件菜单栏中的【Assignments】→【Pin Planner】选项进行分配 管脚,如图 2.46 所示。 图 2.46 管脚分配界面 最后我们在进行一次全编译,在编译过程中,可能产生很多警告信息,但这些不会影响设计 结果。成功编译硬件系统后,将产生 Qsys_First.sof 的 FPGA 配置文件输出。下面我们就来讲 解一下将.sof 文件下载到目标 FPGA 器件的步骤。 (1) 通过 USB-Blaster 下载器连接开发板和计算机,接通 A4 开发板电源,如图 2.47 所示; http://www.fpga.gs/ 60 软核演练篇 §2 图 2.47 接通 A4 开发板电源示意图 (2) 在 Quartus II 软件中选择【Tools】→【Programmer】,打开编程器窗口后确保编程器 窗口的 Hardware Setup 栏中硬件已经安装。如果硬件没有安装,可以参考《驱动安装 指导手册》中的介绍。如图 2.48 所示; 图 2.48 USB-Blaster 设备连接图 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 61 (3) 打开配置文件(Qsys_First.sof),确认下载模式,确保 Program/Configure 下的方框 选中,最后点击【Start】按钮,开始使用配置文件对 FPGA 进行配置,Progress 栏显 示配置进度,如图 2.49 所示。 图 2.49 设备连接完成下载图 本节只讲述了将配置文件下载到 FPGA 中,掉电后 FPGA 中的配置数据将丢失,可以将配 置文件写入掉电保持的 EPCS 器件。在上电时使用 EPCS 对 FPGA 进行配置,详细内容见 2.3 节中的介绍。至此,硬件部分设计完成,下面进行基于 Nios II SBT for Eclipse 的软件方面的设 计。 2.2.4 使用 Nios II SBT Eclipse 建立用户程序 我们可以通过 Quartus II 软件菜单栏中的【Toos】→【Nios II SBT for Eclipse】,来启动 Nios II SBT for Eclipse。打开 Nios II SBT for Eclipse 后,首先弹出的是 Workspace Launcher 页面, 如图 2.50 所示。 http://www.fpga.gs/ 62 软核演练篇 §2 图 2.50 设置 Nios II SBT for Eclipse 工作空间 为了方便工程的管理,我们将 Nios II SBT for Eclipse 工程文件放在我们的 Quartus II 工程 项目 Qsys_First 文件夹下的 software 中。 设置好工作空间后,我们点击【OK】进入 Nios II SBT for Eclipse 软件编辑页面中,在该页 面我们通过单击菜单栏中的【File】→【New】→【Nios II Application and BSP from Template】, 来打开新建工程向导,如图 2.51 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 63 图 2.51 打开新建 Eclipse 工程向导 我们单击【…】按钮来选择 Qsys_First 工程文件夹下的 Qsys_system.sopcinfo,即指向当 前硬件设计系统,这时,Nios II SBT for Eclipse 软件会自动识别我们在 Qsys 系统中 CPU 的名 称,记忆力比较好的读者一定还记得我们在 Qsys 软件中为我们的 Nios II CPU 核命名为 nios2_qsys。接下来,我们便给我们 Nios II SBT for Eclipse 软件中的工程起个名称,这里的名 称可以任意命名,我们这里就命名为 Qsys_First。下面我们在来看下向导中的 Project template 窗口,该窗口中是已经设计好的软件工程,我们可以选择其中的一个,把它当作模板来创建自己 的工程。当然也可以选择 Bland Project(空白工程),完全由我们自己写所有的代码。这里我们 就以模板为例进行讲解,我们选择的是 Hello World 模板工程,然后我们只需要在此基础上进行 适当的修改,一般情况下这比空白工程更加容易,也更方便。 设置好工程后,我们便可以直接点击【Finish】来完成工程创建,这时,你会发现,在 Nios http://www.fpga.gs/ 64 软核演练篇 §2 II SBT for Eclipse 软件的左侧 Project Explorer 窗口中出现两个新的工程:Qsys_First 和 Qsys_First_bsp。Qsys_First 是 C/C++应用工程,而 Qsys_First_bsp 是描述 Qsys_system 系 统硬件细节的系统库,关于系统库的详细信息可参考 3.2 节中的介绍。如图 2.52 所示。 图 2.52 完成工程创建后的软件界面 下面我们右键选择左栏中的 Qsys_First 工程,在出现的菜单中的选择【Build Project】进行 工程编译,则会出现如图 2.53 所示错误。 图 2.53 编译错误信息提示 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 65 我们可以从错误信息提示中看到,它告诉我们,你的 onchip_ram 太小拉,我还有 15140bytes 的代码没有地方存放,万万没有想到,一个简单的 Hellow world 代码竟然这么大,20K 的片内 存储器都放不下,疯了,居然还差 15140bytes…先擦擦冷汗,没事,我们有绝招!Nios 减少代 码量的方法有很多,这里我们推荐使用如下方式进行减少代码量。我们右键选择左栏中的 Qsys_First 工程,在出现的菜单中的选择【Nios II】→【BSP Editor…】出现如图 2.54 所示界 面。 图 2.54 Nios II BSP Editor 配置页面 下面我们就来介绍一下这几个选项: (1) enable_c_plus_plus:由于我们这里用的是 C 语言来编写的软件程序,所以这里我们 可以将这个选项去掉。 (2) enable_clean_exit:当该选项选中时,系统库在 main()返回时调用 exit(),调用 exit() 时,首先清 I/O 的缓冲区,然后调用_exit()。当该选项不选中时,系统库仅调用_exit(), 能节省程序空间。对于嵌入式系统程序来说,一般都不从 main()返回,所以该选项也 可以不用选中。 (3) enable_reduced_device_drivers:HAL 为处理器的外设提供了两种驱动库:一种是执 行速度块,但代码量大的版本;另一种是小封装版本。默认情况下,HAL 系统使用是 代码量大的版本,可以通过【enable_reduced_device_drivers】选项来选择小封装版 本,从而减少代码量。 (4) enalbe_small_c_library:完整的 ANSI C 标准库通常不适用于嵌入式系统,HAL 提供 了一系 列经过 裁剪的 新 的 ANSI C 标准 库,占 用非常 小的代 码,我 们可 以通过 http://www.fpga.gs/ 66 软核演练篇 §2 【enalbe_small_c_library】选项来选择新的 ANSI C 标准库。 选择完毕后,我们点击【Generate】,然后点击【Exit】,再一次进行【Build Project】工程 编译,如图 2.55 所示。 图 2.55 编译成功后的信息提示 这里我们就来看看我们的优化成果,现在仅用 5508B 的代码与初始化数据,对于我们的 20KB 的 onchip_RAM 来说是小菜一碟。这里要注意:虽然优化可以有效减少代码量,但有的时 候也会带来意想不到的问题,所以大家请慎用。遇到异常时,先关闭优化,Clean Project 代码 后再次编译,测试一下是否是优化所致。 一切都已就绪,那么我们就先来用 Hello world 工程来测试我们的硬件系统是否正常,在开 始下载 Frist_Qsys.elf 文件之前,我们要确保两点,第一点,开发板已经通过 USB-Blaster 下载 器连接至计算机并打开了开发板电源;第二点,通过 Quartus II 软件中的 Programmer 程序将 我们的硬件系统 Qsys_First.sof 下载至开发板。如果以上两点都已经完成,那么才可以进行我们 的下一步,右键 Qsys_First 工程,在出现的菜单栏中选择【Run As】→【Nios II Hardware】, 出现图 2.56 所示页面。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 67 图 2.56 Run Configurations 配置页面 我们在该页面中选择【Target Connection】标签,在 Target Connection 窗口中单击【Refresh Connections】按钮后,软件便会自动识别我们开发板上的 Qsys 系统,并给出 Qsys 系统的一 些信息。我们点击【Run】,程序便会自动将 Frist_Qsys.elf 文件下载至开发板中自动进行运行。 如图 2.57 所示。 图 2.57 程序运行输出结果窗口 Nios II SBT for Eclipse 中每个工作界面都包括一个或多个窗口,每个窗口都有其特定的功 能。在工作界面中包括的主要窗口有编辑器窗口和一个或多个浏览器。编辑器可以用于打开并编 辑一个工程。浏览器用于对编辑器提供各种支持,可由我们根据需要进行选择。我们可以同时打 开多个编辑器,但在同一时刻只能有一个编辑器处于激活状态。在工作界面上的主菜单和工具条 的各种操作只对处于激活状态的编辑器起作用。在编辑区中的各个标签上是当前被打开的文件 名,带有“*”号的标签表示这个编辑器中的内容还没有被保存。接下来我们只需要更改 hello_world.c 中的代码就可以完成我们的软件设计,我们将 hello_world.c 文件中的代码更改, http://www.fpga.gs/ 68 软核演练篇 §2 如代码 2.2 所示。 代码 2.2 hello_world.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : hello_world.c 3 //-- 描述 : 使用按键中断控制 LED 亮灭 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include //标准的输入输出头文件 8 #include "system.h" //系统头文件 9 #include "altera_avalon_pio_regs.h" //pio 寄存器头文件 10 #include "alt_types.h" //数据类型头文件 11 #include "sys/alt_irq.h" //中断头文件 12 13 alt_u8 LED_Toggle = 0; //LED 状态切换全局变量 14 15 //--------------------------------------------------------------------------- 16 //-- 名称 : KeyDown_interrupts() 17 //-- 功能 : 中断服务子程序 18 //-- 输入参数 : context:一般用于传递中断状态寄存器的值;id:中断号,这里都未使用 19 //-- 输出参数 : 无 20 //--------------------------------------------------------------------------- 21 static void KeyDown_interrupts(void* context, alt_u32 id) 22 { 23 /* 更改 LED 的显示状态 */ 24 LED_Toggle = ~LED_Toggle; 25 /* 清中断捕获寄存器 */ 26 IOWR_ALTERA_AVALON_PIO_EDGE_CAP(PIO_KEY_BASE, 0x1); 27 } 28 29 //--------------------------------------------------------------------------- 30 //-- 名称 : Init_Interrupt() 31 //-- 功能 : 中断初始化 32 //-- 输入参数 : 无 33 //-- 输出参数 : 无 34 //--------------------------------------------------------------------------- 35 void Init_Interrupt(void) 36 { 37 /* 开 KEY 的中断 */ 38 IOWR_ALTERA_AVALON_PIO_IRQ_MASK(PIO_KEY_BASE, 0x1); 39 /* 清边沿捕获寄存器 */ 40 IOWR_ALTERA_AVALON_PIO_EDGE_CAP(PIO_KEY_BASE, 0x1); 41 /* 注册中断服务子程序 */ 42 alt_irq_register(PIO_KEY_IRQ, NULL, KeyDown_interrupts); Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 69 43 } 44 45 //--------------------------------------------------------------------------- 46 //-- 名称 : main() 47 //-- 功能 : 程序入口 48 //-- 输入参数 : 无 49 //-- 输出参数 : 无 50 //--------------------------------------------------------------------------- 51 int main(void) 52 { 53 printf("Code Running..."); 54 Init_Interrupt(); 55 56 while(1) 57 { 58 IOWR_ALTERA_AVALON_PIO_DATA(PIO_LED_BASE, LED_Toggle); 59 } 60 61 return(0); 62 } 由于该程序代码以 C 语言为主,所以我们就不再进一步对 C 语言进行讲解了。这里我们主 要说明两点,第一点是程序中的外设地址是如何来的,这部分的详细内容可以参考第 3.2.1 节中 的介绍。第二点要说明的便是程序中的中断问题,这部分的详细内容可以参考第 3.2.5 节中的介 绍。代码更改完毕后,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_First.sof 下载至我们的 A4 开发板,Qsys_First.sof 下载完成后, 我们还需要在 Eclipse 软件中将 Qsys_First.elf 文件下载至我们的 A4 开发板,Qsys_First.elf 下 载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,我们可以看到 A4 开发板上的 D1 被点亮,如图 2.58 所示。 http://www.fpga.gs/ 70 软核演练篇 §2 图 2.58 A4 开发板运行图 与此同时,我们还可以在 Eclipse 软件的控制台中看到我们的打印信息,如图 2.59 所示。 图 2.59 Eclipse 软件的控制台中打印的信息图 此时,如果我们按下触摸按键 KEY1,那么 A4 开发板上的 D1 就会熄灭,我们再一次按下, D1 就会点亮。至此,我们就完成了整个工程。最后我们再补充说明一点,如果在硬件配置成功 后,开发板上不能正常运行程序,说明我们的程序存在问题,我们可以通过调试来解决。启动调 试程序和启动运行程序类似,我们右键选择左栏中的 Qsys_First 工程,在出现的菜单中的选择 【Debug As】→【Nios II Hardwar】,在出现的对话框中我们点击【Yes】,调试运行后,将会出 现如图 2.60 所示页面。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 71 图 2.60 程序调试界面 在调试页面,我们可以通过选取图 2.61 所示的通用调试控制工具来跟踪程序的运行。当然, 我们还可以在程序运行观察窗口中可以观察程序的运行情况;在局部变量观察窗口中可以观察 变量、寄存器、存储器的值等。 http://www.fpga.gs/ 72 软核演练篇 §2 图 2.61 调试工具菜单栏 这里我们就简单介绍一下常用的几个调试命令: (1) Resume:从当前代码处继续执行; (2) Suspend:暂停运行; (3) Terminate:停止调试; (4) Step Into:单步跟踪时进入子程序; (5) Step Over:单步跟踪时不进入子程序; (6) Step Return:运行并跳转出子程序; (7) Run to Line:运行到光标所在的当前行; 至此,我们已经建立好了我们的第一个 Qsys 系统,但是这里要注意,在这里我们只讲述了 将配置文件下载到 FPGA 中,掉电后 FPGA 中的配置数据将丢失,如果想要程序不丢失,那么 可以将配置文件写入掉电保持的 EPCS 器件中,在上电时使用 EPCS 对 FPGA 进行配置,接下 来我们就来讲解让你的 Qsys 系统上电自启动吧。 §2.3 让你的Qsys系统上电自启动吧 由于前面已经给出了建立工程的详细步骤,这里我们就不在手把手的一步步建立工程,这里 我们会给出几个关键部分,想要让 Qsys 系统上电自启动,我们需要将配置文件写入掉电保持的 ECPS 器件中,因此我们就需要在我们的 Qsys 系统中添加 EPCS 组件。我们这里就直接在第 一个工程上进行修改,我们将修改完毕的 Qsys 系统直接给出,Qsys 系统如图 2.62 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 73 图 2.62 添加了 EPCS 的 Qsys 系统 从图 2.62 中,我们可以看出,我们将 onchip_rom 组件换成了 epcs_flash,这里的 epcs_flash 配置,我们没有改变任何的选项,我们是直接使用的默认配置,关于 EPCS 组件的详细内容请 参考 4.3 节中的介绍。添加好了 EPCS 组件后,我们修改了名称,引出了管脚,并添加了中断, 这里要注意的是,和 onchip_rom 一样,epcs_flash 的地址也是上了锁的。还有一点不要忘记, nios2_qsys 中的 Reset vector memory 需要改成 epcs_flash。完成了 Qsys 系统的创建后,接 下来就要修改 Quartus II 工程中顶层文件的代码,下面我们直接给出修改完成的代码,如代码 2.3 所示。 代码 2.3 Qsys_First.v 代码 1 module Qsys_First 2( 3 //输入端口 4 CLK_50M,RST_N,KEY_PIO,EPCS_DATA0, 5 //输出端口 6 LED_PIO,EPCS_DCLK,EPCS_SCE,EPCS_SDO 7 ); 8 9 //--------------------------------------------------------------------------10 //-- 外部端口声明 11 //--------------------------------------------------------------------------- 12 input CLK_50M; 13 input RST_N; 14 input KEY_PIO; 15 output LED_PIO; 16 input EPCS_DATA0; http://www.fpga.gs/ 74 软核演练篇 §2 17 output EPCS_DCLK; 18 output EPCS_SCE; 19 output EPCS_SDO; 20 //--------------------------------------------------------------------------- 21 //-- 内部端口声明 22 //--------------------------------------------------------------------------- 23 wire clk_100m; 24 25 //--------------------------------------------------------------------------- 26 //-- 逻辑功能实现 27 //--------------------------------------------------------------------------- 28 PLL PLL_Init 29 ( 30 .inclk0 (CLK_50M ), 31 .c0 (clk_100m ) 32 ); 33 34 //--------------------------------------------------------------------------- 35 36 Qsys_system Qsys_system_Init 37 ( 38 .clk_clk (clk_100m ), // clk.clk 39 .reset_reset_n (RST_N ), // reset.reset_n 40 .pio_led_export (LED_PIO ), // pio_led.export 41 .pio_key_export (KEY_PIO ), // pio_key.export 42 .epcs_flash_dclk (EPCS_DCLK ), //epcs_flash.dclk 43 .epcs_flash_sce (EPCS_SCE ), // .sce 44 .epcs_flash_sdo (EPCS_SDO ), // .sdo 45 .epcs_flash_data0 (EPCS_DATA0 ) // .data0 46 ); 47 48 endmodule 从代码中我们可以看出,我们仅增加了 epcs 的四个管脚,接下来就是编译,分配管脚,如 图 2.63 所示,最后我们编译并生成 Qsys_First.sof 并将该文件下载至开发板。 图 2.63 EPCS 的管脚分配图 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 75 接下来便是 Nios II SBT for Eclipse 工程了,由于 Qsys 系统有改动,所以我们必须要在 Nios II BSP Editor 中重新【Generate】一次。我们打开右键 Nios II SBT for Eclipse 软件在工程项目 栏中找到 Qsys_First 工程右键选择【Nios II】→【BSP Editor】,这里我们会发现之前减少代码 量的配置依然没有改变,我们打开 Linker Script 标签如图 2.64 所示。 图 2.64 Linker Script 配置页面 从该图中我们可以看出,由于我们在 Qsys 软件中将 Reset Address 设置成了 epcs_flash, 所以这里的.entry 也变成了 epcs_flash,关于.text、.stack、.rwdata、.rodata、.heap、.bss 详 细内容请参考 3.3.1 节中的介绍。我们再一次点击【Generate】,随后我们将工程重新编译一次, 生成新的 Qsys_First.elf,Qsys_First.elf 文件生成好了以后,我们就可以单击 Nios II SBT for Eclipse 软件菜单栏中的【Nios II】→【Flash Programmer】,将会出现如图 2.65 所示。 http://www.fpga.gs/ 76 软核演练篇 §2 图 2.65 Nios II Flash Programmer 界面 接下来我们点击 Nios II Flash Programmer 菜单栏中的【File】→【New…】,如图 2.66 所 示。 图 2.66 New Flash Programmer Settings File 界面 从图 2.66 中我们可以看到有两种方法,一种方法是利用 settings.bsp 文件,另一种方法是 利用 Qsys_system.sopcinfo 文件,这两种方法都可以,我们这里就以第一种方法为例,单击 【OK】,将会出现如图 2.67 所示界面。 Zircon Opto-Electronic Technology CO.,Ltd. §2 建立你的第一个 Qsys 系统 77 图 2.67 添加 Qsys_First.sof 文件和 Qsys_First.elf 文件 我们单击【Add】,先将 Qsys_First.sof 文件添加进去,然后再将 Qsys_First.elf 文件添加 进去。如果你的提示窗口中有红色的提示信息或者是你的 Start 是灰色的不能够点击,那么有可 能是你没有将 Qsys_First.sof 文件下载至 A4 开发板,只有当我们的 A4 开发板中的硬件信息与 我们软件工程相匹配,我们才可以点击【Start】烧写程序,点击【Start】以后,我们稍等片刻, 程序便下载到 EPCS 中,弹出如图 2.68 所示页面, http://www.fpga.gs/ 78 软核演练篇 §2 图 2.68 程序下载完成图 下载完毕后,我们需要将 A4 开发板电源关闭,然后再将 A4 开发板电源打开,这时我们可 以看到我们的 A4 开发板实现了 Qsys 系统上电自启动,如图 2.69 所示。 图 2.69 A4 开发板上电自启动 Zircon Opto-Electronic Technology CO.,Ltd. 剥去神秘的外衣——Qsys 到底是如何运行的 第三章 剥去神秘的外衣——Qsys 到底是如何运行的 通过上一章建立你的第一个 Qsys 系统的学习,我想各位一定对 Qsys 的恐惧程度打消了大 半了吧!是的,Qsys 就是这么简单易用。你有时只需要轻点几下鼠标,简单敲了几行的代码, 就能够完成了极其庞大的工程。是不是感觉有点像把大象装进冰箱的感觉?①打开冰箱门,②把 大象放进去,③关门大吉,简单利落!且慢且慢,这可不是一个优秀的嵌入式工程师的风格啊! 下面引用一段某国内知名企业对工程师的招聘要求: (1) 对待工作认真负责,一丝不苟,求实求精、勤奋进取,积极主动,追求上进; (2) 对待困难乐于钻研,不畏枯燥,在置疑中追寻,耐得住寂寞; (3) 对知识有强烈的求知欲和责任感,遇到问题不但要知其然而且要究其所以然。 看到了没有,第三条你做到没?如果你不想要知道我们 Qsys 系统,点了几下鼠标、敲了几 行代码,它内部到底是如何运行的话,那么你根本就没有做到,遇到问题不但要知其然而且要究 其所以然。所以,我们想要成为一个优秀的工程师,我们还是要踏踏实实的深入了解一下我们的 Qsys 系统到底是如何运行的。本章里,我们就要带领大家剥去 Qsys 的神秘外衣,告诉大家它 究竟是如何运行的;我们轻点几下鼠标,简单敲了几行的代码后,Qsys 究竟帮我们做了哪些工 作。虽然这部分会有些枯燥,但是请务必坚持下去,原因请参考上面的第二条。 §3.1 Nios II硬件框架结构的深入剖析 Nios II CPU 在线调试内核 Off-chip software trace memory 地址译码器 中断控制器 等待状态发生器 复用器 主机仲裁 时钟域交叉 Avalon主从 端借口 . . . . UART 0 UART n Timer 0 Timer n SPI 0 SPI n PIO 0 PIO n DMA 0 DMA n 存储器接口 存储器接口 动态总线带宽 图 3.1 Qsys 系统结构框图 用户定义接口 用户定义接口 图 3.1 给出了一个 Qsys 系统的结构框图。由 Qsys 系统的结构框图我们可以看出,Qsys 系 统是指包含 Nios II 软核处理器和 UART、SPI、定制外设等外设集合的系统。其中 Nios II 是软 核处理器,可以借助开发工具对其进行配置,然后下载到可编程芯片中。其优势就是可以根据设 计需要选择处理器和外设,并进行灵活配置。图中间的 Avalon 互连模块将系统互连。Nios II 提 供 JTAG 接口以方便用户下载和调试。Qsys 系统还可以通过外部存储器接口和 FPGA 片外的 82 软核演练篇 §3 处理器通信,共享外部处理器。它还支持与高性能的 Avalon ST 设备的互连。除了定制模块需 要客户的开发,其余部分全部由 Qsys 系统集成工具来生成。本节将向大家介绍 Qsys 硬件系统 的架构等相关知识。 Nios II 处理器是整个 Qsys 硬件系统的核心,所以我们先对 Nios II 处理器的结构作深入剖 析,为了让大家建立一个完整的 Nios II 处理器的概念,了解一些 Nios II 处理器的工作细节,这 对进一步深入学习 Qsys 的开发工作是非常有帮助的。下面我们给出 Nios II 处理器的框架结构 图,如图 3.2 所示。 clock reset cpu_resetrequest cpu_resettaken 软件调试接口 JTAG接口 irq[31...0] eic_port_data[44...0] eic_port_valid 地址发生器 & 程序控制器 异常控制器 内部中断 控制器 外部中断 控制器 通用寄存器 控制寄存器 影子寄存器集 指令 Cache 指令区 存储器 保护单元 数据区 存储器 管理单元 转换后备 缓冲区 I/O口 用户逻辑 算术逻辑单元 数据 Cache … … 紧耦合 指令存储器 紧耦合 指令存储器 指令总线 数据总线 紧耦合 指令存储器 紧耦合 指令存储器 必需模块 可选模块 图 3.2 Nios II 处理器的框架结构图 由 Nios II 处理器的框架结构图我们可以看出,Nios II 处理器主要包含以下六个用户可见的 功能模块:1、寄存器文件;2、算术逻辑单元;3、复位信号;4、异常和中断;5、存储器和 I/O 结构;6、JTAG 调试模块。接下来我们就对上面的这六个功能模块分别进行介绍。 3.1.1 寄存器文件 首先我们介绍的是寄存器文件,寄存器文件是 Nios II 处理器内部用来存放数据的一些小型 存储区域,这些小型存储区域可以用来暂时存放参与运算的数据和运算结果。Nios II 寄存器文件 包括 32 个通用寄存器、32 个控制寄存器,以及影子寄存器组。 (1) 通用寄存器 所谓通用寄存器,就是它可以用于多种用途,不是限定好的,它们可用来暂存指令、数据和 地址。Nios II 的 32 个通用寄存器命名和用法如表 3.1 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 83 寄存器 r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 表 3.1 Nios II 的 32 个通用寄存器命名和用法 通用寄存器组 助记符 功能 寄存器 助记符 功能 zero 0x00000000 r16 被调用保存寄存器 at 暂存寄存器 r17 被调用保存寄存器 返回值 r18 被调用保存寄存器 返回值 r19 被调用保存寄存器 参数寄存器 r20 被调用保存寄存器 参数寄存器 r21 被调用保存寄存器 参数寄存器 r22 被调用保存寄存器 参数寄存器 r23 被调用保存寄存器 调用保存寄存器 r24 et 异常暂存寄存器 调用保存寄存器 r25 bt 断点暂存寄存器 调用保存寄存器 r26 gp 全局指针 调用保存寄存器 r27 sp 堆栈指针 调用保存寄存器 r28 fp 帧指针 调用保存寄存器 r29 ea 异常返回地址 调用保存寄存器 r30 ba 断点返回地址 调用保存寄存器 r31 ra 函数返回地址 (2) 控制寄存器 所谓控制寄存器,就是它可以用于控制和确定 Nios II 处理器的操作模式,以及当前执行任 务的特性。Nios II 的 32 个控制寄存器命名和用法如表 3.2 所示。 寄存器 ctl0 ctl1 ctl2 ctl3 ctl4 ctl5 ctl6 ctl7 ctl8 ctl9 ctl10 ctl11 ctl12 ctl13 ctl14 ctl15 表 3.2 Nios II 的 32 个控制寄存器命名和用法 控制寄存器组 名字 功能 寄存器 名字 status 状态寄存器 ctl16 Reserved estatus 异常状态寄存器 ctl17 Reserved bstatus ienable ipending cpuid Reserved exception pteaddr tlbacc tlbmisc eccinj badaddr config mpubase mpuacc 断点状态寄存器 中断允许寄存器 中断发生寄存器 处理器标识寄存器 Reserved 异常寄存器 页表寄存器 TLB 访问寄存器 TLB 余项寄存器 ECC 注入寄存器 异常寄存器 配置寄存器 MPU 基地址寄存器 MPU 入口寄存器 ctl18 ctl19 ctl20 ctl21 ctl22 ctl23 ctl24 ctl25 ctl26 ctl27 ctl28 ctl29 ctl30 ctl31 Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved 功能 Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved Reserved http://www.fpga.gs/ 84 软核演练篇 §3 通过该表我们可以看出,虽然控制寄存器它有 32 个,但是我们只用了 15 个,其余的控制 寄存器都用作了保留。在这 15 个寄存器中,其中 ctl0~ctl5 这 6 个寄存器我们是比较常用的, ctl7~ctl15 这 9 个寄存器我们一般是用不到的,它们主要是用于 MMU 或者 MPU 中。下面我们 就主要介绍一下 ctl0~ctl5 这 6 个寄存器。首先我们介绍的是 ctl0(status 状态寄存器),status 寄存器如图 3.3 所示。 图 3.3 status 状态寄存器 从该图中我们可以看出,status 的第 0 位 PIE 是外设中断允许位:1 表示允许外设中断;0 表示禁止外设中断。第 1 位 U 是反应计算机当前状态:1 表示处于用户模式;0 表示处于管理员 模式。第 2 位 EH 是异常处理程序模式位,如果系统中没有 MMU,EH 将始终为 0.第 3 位 IH 是 中断处理模式位,当 IH 为 1 时,表示 Nios II 处理器需要一个外部中断。第 4 位 IL 是中断级字 段。第 5 位 CRS 是当前执行的寄存器组;第 6 位 PRS 是之前执行的寄存器组。第 7 位 NMI 是 不可屏蔽中断模式位。第 8 位 RSIE 是寄存器设置中断允许位。 简单的介绍完了 status,接下来我们就来介绍 estatus 异常状态寄存器(ctl1),在异常处理 过程中,异常状态寄存器保存状态寄存器的备份。这个寄存器仅定义了 EPIE,EPIE 位是状态寄 存器中 PIE 位的备份。异常管理器可以检查异常状态寄存器,确定异常处理之前处理的状态。在 从中断返回时,异常返回指令使处理器把异常状态寄存器复制到状态寄存器,恢复到异常处理之 前的状态。 在调试断点处理过程中,断点状态寄存器 bstatus(ctl2)保存状态寄存器的备份。这个寄存 器仅定义了 BPIE,BPIE 是 PIE 的备份。当系统产生一个断点时,状态寄存器的值被复制到断 点状态寄存器。用断点状态寄存器可以把状态寄存器恢复到处理断点之前的值。 中断允许寄存器 ienable(ctl3)控制外部硬件中断,中断允许寄存器的每一位对应着一个硬 件中断输入 IRQ0~IRQ31 中的一个中断源。如果对应位为 1,则相应的中断被允许;如果对应位 为 0,则相应的中断被禁止。 中断发生寄存器 ipending(ctl4)表示还没有被处理器接受的中断源,如果中断发生寄存器 的某位为 1,则表示处理器已经接收 IRQn 的中断请求,并且相应的中断允许位被使能。这时一 个只读寄存器,写这个寄存器没有定义。 处理器标识寄存器 cpuid(ctl5)保存一个静态数值,在多处理器系统中,用这个数值作为处 理器的唯一标识。处理器标识寄存器的值是在处理器产生时确定的。写这个寄存器不会影响处理 器标识值。 (3) 影子寄存器组 所谓影子寄存器组,其实就是用来备份通用寄存器和状态寄存器的。影子寄存器组通常会和 外部中断控制器联合使用,关于影子寄存器组的使用我们将会在外部中断控制器中进行讲解。 3.1.2 算术逻辑单元 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 85 算术逻辑单元(ALU),对于 ALU 我相信大家都是有所耳闻的,它的主要功能就是对存储在 通用寄存器中的数据进行操作,ALU 从寄存器中取一个或者两个操作数,经过计算后并将运算 结果存回寄存器中。Nios II 的处理器中的 ALU 支持以下四种运算: (1) 算术运算:加法、减法、乘法和除法; (2) 关系运算:有符号和无符号的等于、不等于、大于等于和小于运算; (3) 逻辑运算:与、或、非和异或; (4) 移位运算:逻辑右移、逻辑左移、算术右移、算术左移、循环右移和循环左移。 我们知道,利用 ALU 实现乘法、除法和可控位数的移位等操作是非常复杂的,需要很多逻 辑资源,因此,在 Nios II 设置中,还可以选择硬件算法操作,通过嵌入式乘法器和嵌入式除法 器来实现。 3.1.3 复位信号 Nios II 处理器支持两个复位信号,其中一个复位信号是全局硬件复位信号 reset,这个复位 信号是一个外部输入信号,高电平有效,用这个信号可以强制处理器核立即进入复位。另一个复 位信号是 cpu_resetrequest,这个复位信号是一个本地复位信号,也是高电平有效,它可以只 让 CPU 复位,而 Nios II 系统中的其他元件不受这个复位信号影响。当 Nios II 处理器检查到这 个复位信号有效后,将执行完流水线中的全部指令,然后进入复位状态,这个复位过程可能需要 数个时钟周期才能完成。当 Nios II 处理器完成复位后,Nios II 处理器将输出 cpu_resettaken 信 号,表明复位完成。这时 Nios II 处理器将保持复位状态到复位信号 cpu_resetrequest 无效为止。 这里要注意的是,当 Nios II 处理器处于 JTAG 调试模式控制下,Nios II 处理器是不会响应复位 信号 cpu_resetrequest 的。 3.1.4 异常和中断 异常和中断我想大家都不会陌生。我们举个简单的例子,当我们在敲击计算机键盘空格的时 候,计算机为什么会在文档中添加一个空格呢?这是因为我们在敲击键盘的一刹那,我们键盘的 硬件控制器会发送一个中断给我们计算机的 CPU,我们计算机的 CPU 会响应这个中断,它先 停下当前的工作,转而去调用中断服务程序(Interrupt Service Routine)来执行我们的这个空格操 作。如果你学习过单片机或者是微机原理,那么你对这个中断过程会非常熟悉。在 Nios II 中也 有类似的中断机制,我们将这种类似的机制统称为异常。异常通常分为以下五种类型,中断只是 其中的一类,如图 3.4 所示。 http://www.fpga.gs/ 86 软核演练篇 §3 Nios II异常 复位异常 断点异常 指令相关 异常 其它异常 中断异常 当程序遇到复位异常时, 将会产生复位异常 当程序遇到断点异常时, 将会产生断点异常 当程序遇到指令相关异常时, 将会产生指令相关异常 其它异常类型是为将来准 备的 内部中断 图 3.4 异常的五种类型 外部中断 从该图中我们可以看出,Nios II 异常主要包括复位异常、断点异常、指令相关异常、其它异 常和中断异常。在这五种类型异常中,最重要的异常就属我们的中断异常了,下面我们就来重点 介绍中断异常,在 Nios II 中,中断异常又分为内部中断和外部中断。首先我们介绍的是内部中 断,Nios II 体系结构支持 32 个内部中断,即 irq0~irq31,IRQ 的优先级由 Qsys 软件决定,如 图 3.5 所示。 图 3.5 在 Qsys 软件中的内部中断 从该图右侧部分我们可以看出,我们连接了 2 个内部中断,一个是 jtag_uart 的 0 中断,一 个是 pio_key 的 1 中断,0 中断的优先级要高于 1 中断,每个中断对应一个独立的中断通道。针 对每个 IRQ 输入,处理器中的 ienable 中断允许寄存器中都有一个相应的中断使能位,处理器能 够通过 ienable 控制器来独立地使能或者禁止每个中断源。处理器也可以通过 status 控制寄存 器的 PIE 为来使能或者禁止所有的中断,如图 3.6 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 87 图 3.6 内部中断的产生图 通过该图我们可以看出,只有具备以下条件,系统才会产生内部中断: (1) Status 控制寄存器中的 PIE 位为 1; (2) 某个中断请求 irqn 有效; (3) 在 ienable 寄存器中,该中断源相应位为 1; 知道了如何产生内部中断以后,下面我们再来看下内部中断的处理流程,当内部中断发生后, Nios II 处理器会依次完成以下工作: (1) 把 status 寄存器内容复制到 estatus 寄存器中,保存当前处理器状态, (2) 清除 status 寄存器的 PIE 位为 0,禁止所有的中断; (3) 把异常返回地址写入 ea 寄存器(r29); (4) 跳转到异常处理地址。 讲完了内部中断,接下来我们再来看下外部中断。外部中断相对于内部中断来说比较抽象, 这是因为我们在之前的工程中只对内部中断进行了配置和使用,并没有对外部中断进行操作说 明。下面我们就来看下内部中断和外部中断究竟有着怎样的区别与不同。首先我们先来看下外部 中断是如何配置的,如图 3.7 所示。 http://www.fpga.gs/ 88 软核演练篇 §3 图 3.7 外部中断的配置图 从该图中我们可以看出,我们可以在【Advanced Features】标签下的【Interrupt Controller】 中进行设置。如果我们想要使用内部中断,那么我们只需要选择【Internal】选项就可以了。如果 我们想要使用外部中断,那么我们就需要选择【External】选项。这里需要我们注意的是,如果 我们选择了【External】选项,那么我们还需要设置相对应的影子寄存器组。也就是说,我们针 对每一个外部中断都会给它分配一个相对应的影子寄存器,用来备份通用寄存器和状态寄存器 的。一旦外部中断来了之后,我们的 CPU 就会切换到影子寄存器组中去运行。运行完后,我们 的 CPU 就会回到之前的寄存器中继续运行,因此能够完成保护现场的工作。最后我们再来补充 说明一下,对于 Nios II/e 和 Nios II/s 这两个等级中,它是不支持外部中断的。只有 Nios II/f 等级 中,它才会支持外部中断。当我们选择使用外部中断控制器以后,会出现如图 3.8 所示页面。 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 89 图 3.8 在 Qsys 软件中的外部中断 在该图中我们可以看到,我们的内部中断连接已经全部消失,同时 nios2_qsys 多出了一个 端口,该端口叫做 interrupt_controller_in,它就是用来连接外部中断的。当然,只使用这一个端 口我们是实现不了外部中断的,我们还需要添加一个 Vectored Interrupt Controller IP 核,该 IP 核的配置如图 3.9 所示。 图 3.9 Vectored Interrupt Controller 配置页面 从该页面中我们可以看到,该 IP 核的配置很简单,只有三个选项,首先第一个选项是用来 http://www.fpga.gs/ 90 软核演练篇 §3 配置外部中断的个数,第二个选项是用来配置每个中断的中断级别的位数。第三个选项就是菊花 链,可以用来连接多个 Vectored Interrupt Controller IP 核,支持更多的外部中断。我们将该 IP 核添加至 Qsys 软件中,如图 3.10 所示。 图 3.10 外部中断在 Qsys 软件中的连接 从该图中我们可以看出,首先我们将 Vectored Interrupt Controller IP 核连接到了我们的 nios2_qsys 上,然后我们就可以在该图的右侧像连接内部中断一样去连接外部中断。至此我们 的外部中断就配置完成了。也许有的朋友会有这么一个疑问,我们的内部中断配置起来那么简单, 我们的外部中断配置起来那么复杂,为什么我们不用内部中断,而去选择用外部中断呢?这主要 有以下四点原因: (1) 你需要一个或多个中断来减少平均响应时间; (2) 你对中断性能有很强的实时性要求; (3) 你需要不可屏蔽中断; (4) 你需要处理超过 32 个中断。 通过以上四点,我们相信大家都能够理解,对于一般的中断我们都可以去使用内部中断去处 理,但是对于一些内部中断没有办法处理的中断,我们必须要使用外部中断去处理。也许有的朋 友还会有这么一个疑问,外部中断为什么要比内部中断要快呢?这主要是因为外部中断用到了 影子寄存器组。我们在之前寄存器文件讲解中,我们只是简单的介绍了一下影子寄存器组,并没 有进一步去讲解它,下面我们就结合外部中断来对影子寄存器组进行一个进一步的介绍。影子寄 存器组,说白了它其实就是之前我们所说的通用寄存器组的一个完整拷贝,它是用来代替我们的 通用寄存器的。当外部中断来了,影子寄存器组就会为这个中断,制造一个完全属于它自己的一 个运行环境,它运行完了以后,再回到原来的通用寄存器中继续运行。我们要知道,程序运行过 程中,我们不仅需要通用寄存器,我们还需要知道程序运行的状态等等,因此,影子寄存器组它 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 91 还包括一个控制寄存器中 ctl0 寄存器的拷贝,我们称这个寄存器为 sstatus,其中 s 就是 shadow, status 就是我们的状态寄存器的意思,如图 3.11 所示。 图 3.11 sstatus 寄存器 从该图中我们可以看到,这个 sstatus 和我们之前的状态寄存器几乎是一样,唯一不同的就 是第 31 位的 SRS,这个 SRS 是 Switched register set bit 的缩写,意思是切换寄存器比特位。 当 SRS 为 1 的时候,就表示我们的寄存器要开始切换到另一组影子寄存器组中了。介绍完了影 子寄存器组,下面我们就来看下外部中断如何与我们的影子寄存器组配合来处理中断流程的,如 图 3.12 所示。 运行中 0 号寄存器组 status CPU 影子寄存器组 ① sstatus CRS PRS SRS …… 影子寄存器组 ② sstatus CRS PRS SRS …… 影子寄存器组 ③ sstatus CRS PRS SRS …… 影子寄存器组 ④ sstatus CRS PRS CSS …… Requested register set RRS (要求得到的寄存器组) 外部中断 控制器 外部中断 控制器 外部中断 控制器 外部中断 控制器 图 3.12 外部中断处理过程图 从该图中我们可以看出,我们这里有四个外部中断,每个外部中断对应一个影子寄存器组, 此时,我们的程序正运行在 0 号寄存器组中,也就是运行在我们 CPU 本身正常工作模式下的寄 存器组中,在我们的 0 号寄存器组中我们还可以看到,它包含着一个 status 状态寄存器。由于 我们影子寄存器组是对通用寄存器和状态寄存器的一个拷贝,所以我们影子寄存器中也包含有 通用寄存器和 sstatus 状态寄存器。下面我们就来分析一下外部中断产生后,这几个寄存器是如 何变化的。比如说,我们的 2 号中断来了,我们的外部中断控制器会产生一个 RRS(Requested register set,即要求得到的寄存器组)信号告诉我们的 CPU,2 号中断来了,此时我们的 CPU 就会将 RRS 与我们的 CRS(Current register set field,即当前执行的寄存器组)做对比,看是 否吻合,如果不吻合,那么 sstatus 中的 SRS(Switched register set bit,即切换寄存器比特位) 就会被打开。SRS 被打开以后,我们的 CPU 就会将 CRS 中的值写入到 PRS(Previous register set field,即之前执行的寄存器组)中,同时还会将 RRS 中的 2 号写入到 CRS 中。CRS 一旦 被写入以后,那么整个运行环境就会立刻切换到 2 号影子寄存器组中,如图 3.13 所示。 http://www.fpga.gs/ 92 软核演练篇 §3 运行中 影子寄存器组 ② 0 号ss寄ta存tu器s 组 CRS status PRS SRS …… CPU 影子寄存器组 ① sstatus CRS PRS SRS …… 0 号寄存器组 status 影子寄存器组 ③ sstatus CRS PRS SRS …… 影子寄存器组 ④ sstatus CRS PRS CSS …… RRS 外部中断 控制器 外部中断 控制器 外部中断 控制器 图 3.13 外部中断处理过程图 外部中断 控制器 从该图中我们可以看出,我们的程序刚开始全部是跑在 0 号寄存器组中的,当 2 号外部中 断来了以后,我们的程序会立刻运行在 2 号影子寄存器组。这样就会完成一个非常快捷的中断响 应过程,从而避免了地址跳转等复杂操作。当 2 号外部中断运行完了以后,我们的程序会回到 0 号寄存器中继续运行。这里我们只是简单的定性的去描述了一下整个过程,如果大家想去了解更 多关于外部中断和影子寄存器组的知识,那么可以去看下 Altera 提供的官方手册。 3.1.5 存储器和 I/O 结构 Nios II 存储器和 I/O 结构非常灵活,这是 Nios II 处理器系统与传统微控制器之间的最显著 的区别。因为 Nios II 处理器系统可配置,存储器和外设随着系统的不同而不同,最终使得存储 器和 I/O 结构随着系统不同发生变化。Nios II 内核可使用下面的一种或多种方式访问存储器和 I/O: (1) 指令主端口:Avalon 主端口,通过 Avalon 系统互连结构连接到指令存储器; (2) 数据主端口:Avalon 主端口,通过 Avalon 系统互连结构连接到数据存储器; (3) 指令高速缓存:Nios II 内核里面的高速缓存; (4) 数据高速缓存:Nios II 内核里面的高速缓存; (5) 紧耦合指令或数据存储器端口:与 Nios II 内核外的快速存储器相连。 说完了 Nios II 内核访问存储器和 I/O 的 5 种方式,下面我们来看下 Nios II 处理器内核的存 储器和 I/O 结构框图,如图 3.14 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 93 Nios II处理器核 紧耦合数据 M 存储器1 程序计 数器 通用寄 存器文 件 指令 总线 选择 逻辑 数据 总线 选择 逻辑 MPU指令区 指令 Cache MMU的地址 转换的缓冲区 数据 Cache 旁路 逻辑 数据 Cache MPU数据区 M 紧耦合数据 存储器 N Avalon系统 M 互连结构 存 S储 器 M 紧耦合数据 M 存储器1 M 紧耦合数据 存储器 N 从 S外 设 S Avalon 从端口 M Avalon 主端口 图 3.14 Nios II 处理器内核的存储器和 I/O 结构图 从该图中我们可以看出,Nios II 处理器属于哈佛结构,支持独立的指令和数据总线。指令和 数据总线都遵循 Avalon 接口规范。数据主端口连接存储器和外设元件,而指令主端口只连接存 储器元件,我们在“连连看”的时候就是遵循的这个规则。由于上面的这个框架图比较复杂,它 包含了所有的端口,为了让大家容易理解学习,我们将上面那个复杂的框图分解成了四部分,1、 指令主端口和数据主端口;2、指令高速缓存和数据高速缓存;3、MMU 和 MPU;4、紧耦合存 储器。下面我们就对这四部分内容分别进行介绍。 首先我们介绍的是第一部分指令主端口和数据主端口,如图 3.15 所示。 Nios II处理器核 程序计 数器 指令 总线 选择 逻辑 Avalon系统 M 互连结构 存 S储 器 通用寄 存器文 件 数据 总线 选择 逻辑 M 从 S外 设 S Avalon从端口 M Avalon主端口 图 3.15 指令主端口和数据主端口的结构图 从该图中我们可以看到,指令总线选择逻辑和数据总线选择逻辑,它们通过 Avalon 系统互 http://www.fpga.gs/ 94 软核演练篇 §3 连结构连接到我们外部设备,包括存储器和外设。对于指令主端口,它只执行一个功能,就是对 处理器将要执行的指令进行取指,并且它不执行任何写操作。对于数据主端口,它执行两个功能, 第一个是当处理器执行装载指令时,从存储器或外设中读数据;第二个是当处理器执行存储指令 时,将数据写入存储器或外设。这时候我们再回想一下,想必大家就能明白为什么数据主端口连 接存储器和外设元件,而指令主端口只连接存储器元件。这是因为我们的外设元件不进行写操作 就无法控制,并且我们的外设元件不包含任何指令,所以我们的外设元件不需要连接指令主端口 了,只要连接数据端口就可以了。当然,由于我们的存储器能够为系统提供指令,所以我们的存 储器需要连接指令端口和数据端口。 介绍完了第一部分指令主端口和数据主端口,下面我们再来介绍第二部分指令高速缓存和 数据高速缓存,如图 3.16 所示。 Nios II处理器核 程序计 数器 指令 总线 选择 逻辑 指令 Cache M Avalon系统 互连结构 存 S储 器 通用寄 存器文 件 数据 数据 Cache Cache 旁路 M 数据 逻辑 总线 选择 逻辑 从 S外 设 S Avalon 从端口 M Avalon 主端口 图 3.16 指令高速缓存和数据高速缓存的结构图 从该图中我们可以看出,Nios II 结构的指令主端口和数据主端口都支持高速缓存(Cache)。 指令主端口使用指令高速缓存,数据主端口使用数据高速缓存。高速缓存使用片内存储器,是 Nios II 处理器内核的重要组成部分。它能够改善使用较慢片存储器(如用来存放程序和数据的 SDRAM)的 Nios II 处理器系统的平均存储器访问时间。指令 Cache 和数据 Cache 不仅仅加速 了存储器的平均访问时间,它在软件上也几乎是透明的,不需要我们写任何代码就可以实现。作 为 Nios II 处理器组成部分的告诉缓存在 Qsys 中是可选的,这取决于用户对系统存储性能以及 FPGA 资源的使用要求。Nios II 处理器内核可以含有数据或指令缓存,也可以两者都有或都没 有,高速缓存大小可由用户配置,如图 3.17 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 95 图 3.17 指令高速缓存和数据高速缓存在 Qsys 软件中的配置 这里需要我们注意的是,指令 Cache 和数据 Cache 虽然可以改善了系统的整体性能,但是 它会使程序的执行时间变得不可预测,对于实时系统来说,代码执行的确定性——装载和存储指 令或数据的时间必须是可预测的,这一点至关重要。 介绍完了第二部分指令高速缓存和数据高速缓存,下面我们再来介绍第三部分 MMU 和 MPU,如图 3.18 所示。 Nios II处理器核 程序计 数器 通用寄 存器文 件 指令 总线 选择 逻辑 MPU指令区 指令 Cache M MMU的地址 转换的缓冲区 Avalon系统 互连结构 存 S储 器 数据 数据 Cache Cache 旁路 M 数据 逻辑 总线 选择 MPU数据区 逻辑 从 S外 设 S Avalon 从端口 M Avalon 主端口 图 3.18 MMU 和 MPU 的结构图 http://www.fpga.gs/ 96 软核演练篇 §3 MMU(Memory Management Unit 即,存储器管理单元),说到 MMU,我们不得不说一下 虚拟内存这个概念,虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连 续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片, 还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。这里我们举个简单的例子,比 如对一个 16MB 的程序和一个内存只有 4MB 的机器,操作系统通过选择,可以决定各个时刻将 哪 4M 的内容保留在内存中,并在需要时在内存和磁盘间交换程序片段,这样就可以把这个 16M 的程序运行在一个只具有 4M 内存机器上了。实现这种虚拟内存系统需要一种机制,就是将虚拟 地址转换为实际的物理地址。这一机制的实现过程通常是由一个操作系统和特定的硬件联合完 成。在 Nios II/f 型处理器中,可选的 MMU 就是用来完成这一工作的特定硬件。当然,仅有硬件 无法实现这一功能的。我们还需要一个能支持虚拟内存的操作系统,常见的系统有 Linux 和 Windows CE 等,常见的不支持虚拟内存的操作系统有 uC/OS、uClinux。 MPU(Memory Protection Unit 即,存储器保护单元) ,现代的操作系统包含保护机制,限 制用户应用程序访问关键的系统资源。例如,一些操作系统将程序的执行分为内核模式(没有限 制)和用户模式(有限制),这个方案的实现也需要特殊硬件的支持了,在 Nios II/f 等级的处理 器中,可选的 MPU 就是用来完成这项工作的。MPU 仅提供存储器保护,它是不支持内存映射 和管理的。在这里我们需要注意的是,由于 Altera HAL 为 MPU 提供了基本的支持,所以,没 有 OS 也是可以使用的,在 Nios II 配置中,MMU 和 MPU 的使用是互斥的,这意味着两者之中 仅有一个可用。它们在 Qsys 软件中的配置如图 3.19 所示。 图 3.19 MMU 和 MPU 在 Qsys 软件中的配置 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 97 介绍完了第三部分 MMU 和 MPU,下面我们再来介绍最后一部分紧耦合存储器,如图 3.20 所示。 Nios II处理器核 紧耦合数据 M 存储器1 程序计 数器 通用寄 存器文 件 指令 总线 选择 逻辑 数据 总线 选择 逻辑 MPU指令区 指令 Cache MMU的地址 转换的缓冲区 数据 Cache 旁路 逻辑 数据 Cache MPU数据区 M 紧耦合数据 存储器 N Avalon系统 M 互连结构 存 S储 器 M 紧耦合数据 M 存储器1 M 紧耦合数据 存储器 N 从 S外 设 S Avalon 从端口 M Avalon 主端口 图 3.20 紧耦合存储器的结构图 为了提高系统的整体性能,Nios II 内核不仅可以集成数据 Cache 和指令 Cache,还可带有 紧耦合存储器接口。紧耦合存储器是一种紧挨着内核的快速 SRAM,它不仅能改善系统性能, 而且保证了装载和存储指令或数据的时间是确定的。紧耦合存储器既能使 Nios II 处理器提高性 能,又能获得可预测的实时响应。紧耦合存储器可向对性能要求严格的应用提供低延迟访问。与 高速缓存相比,使用紧耦合存储器有以下优点: (1) 性能类似于高速缓存; (2) 软件能够保证将关键性能的代码或数据存放在紧耦合存储器中; (3) 代码执行的确定性——装载和存储指令或数据的时间是可预测的。 实际上,紧耦合存储器是 Nios II 处理器内核上的一个独立的主端口,与指令或数据主端口 类似。Nios II 结构指令和数据访问都支持紧耦合存储器。Nios II 内核可以不包含紧耦合存储器, 也可以包含一个或多个紧耦合存储器。每个紧耦合存储器端口直接与具有固定的低延迟的存储 器相连,该存储器在 Nios II 内核的外部,通常使用 FPGA 片内存储器。紧耦合存储器与其它通 过 Avalon 交换结构连接的存储器件一样,占据标准的地址空间。它的地址范围在生成系统时确 定。软件使用常规的装载和存储指令访问紧耦合存储器。从软件的角度来看,访问紧耦合存储器 与访问其他存储器没什么不同。系统在访问指定的代码或数据时,能够使用紧耦合存储器来获得 最高性能。例如,中断频繁的应用能够将异常处理代码放在紧耦合存储器中来降低中断延迟。类 似的,计算密集型的数字信号处理(DSP)应用能够将紧耦合存储器指定为数据缓存区,实现最 快的数据访问。如果应用程序的存储器需求足够小,能够完全在片内实现,可以使用专门针对代 码和数据的紧耦合存储器。如果应用程序较大,那么必须仔细选择放入紧耦合存储器中的内容, 使成本和性能能够实现最佳平衡。 http://www.fpga.gs/ 98 软核演练篇 §3 讲完了什么是紧耦合存储器,下面我们再来看下如何在 Qsys 软件中配置我们的紧耦合存储 器,如图 3.21 所示。 图 3.21 紧耦合存储器在 Qsys 软件中的配置 在该图中我们可以看到,我们可以在【Include tightly coupled instruction master pods】和 【Include tightly coupled data master pods】这两个选项中,进行选择是否添加紧耦合存储器。 当我们都选择 1 以后,我们的 nios2_qsys 会出现如图 3.22 所示页面。 图 3.22 紧耦合存储器在 Qsys 软件中的连接 在该页面中我们可以看到,我们的 nios2_qys 多出了两个端口,一个是 tightly_coupled_data _master_0 接口,一个是 tightly_coupled_instruction_master_0 接口。当然,只使用这两个端 口我们是实现不了的,我们还需要添加两个片上存储器,也就是我们的 onchip_memory IP 核, 如图 3.23 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 99 图 3.23 紧耦合存储器的连接示意图 从该图中我们可以看出,我们添加的两个片上存储器,一个是单端口的名称叫 Tightly Coupled Data Memory。另一个是双端口的,名叫 Tightly Coupled Instruction Memory。其中 双端口的片上存储器,一端接到我们的 Tightly Coupled Instruction Memory Port,另一端则接 到我们的 Avalon Data Master Port,它们最终的连接如图 3.24 所示。 图 3.24 紧耦合存储器在 Qsys 软件中的连接 这里我们需要注意的是,当我们在 Qsys 软件完成了连接以后,我们还需要将 nios2_qsys 中的异常向量设置为 tightly_coupled_instruction_memory。至此,我们的紧耦合存储器就配置 完毕了。 http://www.fpga.gs/ 100 软核演练篇 §3 3.1.6 JTAG 调试模块 Nios II 结构中包含 JTAG 调试模块,提供片上仿真部件,便于 PC 主机对处理器进行远程 控制。PC 上的软件调试工具可以通过与 JTAG 调试模块通信,提供调试和诊断功能。比如说:  把程序下载到存储器中。  启动和停止程序的执行。  设置断点和观察点。  分析寄存器和存储器。  采集实时的执行跟踪数据。 由于我们 Nios II 是一个高度可定制的处理器,所以我们的 JTAG 调试模块也是一个完全可 定制的,如图 3.25 所示。 图 3.25 JTAG 调试模块在 Qsys 软件中的配置 从该图中我们可以看出,我们的 JTAG 调试模块分为 4 个等级,不同等级的 JTAG 调试模 块,它所消耗的逻辑资源是不同的,同时它所带有的功能也是不同的,我们将它们总结如表 3.3 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 101 表 3.3 不同等级的 JTAG 调试模块对比表 从表中我们可以看出,等级越高,消耗的逻辑资源也就越高,但相对的功能也就越强大,在 调试的过程中,如果你只是需要一些简单的调试功能,那么你就可以选择一个低等级的 JTAG 调 试模块。如果你需要一些比较复杂的调试功能,那么就需要选择一个高等级的 JTAG 调试模块。 3.1.7 Nios II 处理器性能 Nios II 处理器有 3 种类型:Nios II/e、Nios II/s 和 Nios II/f。我们将 3 种 Nios II 处理器的功 能、面积和性能总结如表 3.4 所示。关于各处理器的更详细内容请参考 Altera 的官方文档。 表 3.4 3 种 Nios II 处理器的性能对比表 特性 Nios II/e 处理器内核 Nios II/s DMIPS/MHz 0.15 0.74 Nios II/f 1.16 性能 最大 DMIPS 31 127 218 最大工作频率(f ) MAX 大致尺寸(以 LE 为单位) 流水线阶数 外部寻址空间 高速缓存 指令总线 存储器流水线访问 分支预测 紧耦合存储器 高速缓存 数据总线 存储器流水线访问 紧耦合存储器 硬件乘法器 算术逻 辑单元 硬件除法器 移位器 JTAG 调 JTAG 接口、运行控 试模块 制、软件断点支持 200 MHz <700 1 2GB - – - - - – - - - 1 周期每比特 可选 165 MHz 185 MHz <1,400 <1,800 5 6 2GB 2GB 512B 到 64KB 512B 到 64KB 可选 可选 静态 动态 可选 可选 - 512B 到 64KB – – - 可选 3 周期 1 周期 可选 可选 3 周期桶形移位器 1 周期 barrel 移位 器 可选 可选 http://www.fpga.gs/ 102 软核演练篇 §3 硬件断点支持 - 片外跟踪缓冲区支持 - 存储器管理单元 - 存储器保护单元 - 内部中断控制器 是 异常处理 外部中断控制器 否 影子寄存器组 否 用户模式支持 否 定制指令支持 是 ECC 支持 否 可选 可选 - - 是 否 否 否 是 否 可选 可选 可选 可选 是 可选 可选 是 是 是 §3.2 Nios II软件框架结构的深入剖析 回顾第二章的 Qsys 的开发流程,我们知道 Qsys 系统设计包括硬件设计与软件设计两部 分。前一小节中我们对 Qsys 的硬件系统的架构与 Nios II 处理器内核作了较为详细的介绍。在 本节中我们将从硬件转向软件,着重为大家介绍 Nios II 程序框架结构。 在进入具体介绍之前,请大家先回想一下之前直接用 Verilog 硬件描述语言驱动外设的情形: 我们根据外设的数据手册与时序规则,将 FPGA 管脚与外设管脚的连接,通过一系列硬件描述 语言控制管脚电平的高低,从而让 FPGA 实现外设的直接驱动。当这个外设比较容易,如数码 管,LED 时。我们可以直接控制最底层资源,甚至对每个管脚在每个时刻的电平输出了如指掌。 但是如果设计稍显复杂,如设计 SDRAM 的驱动时,往往对底层细节的过多关注就会成为一种 累赘。 试想我们平时在电脑上编写 C 程序,比如在显示器上输出一行字,我们只用一句 printf()即 可完成,至于打印命令怎么传到显示芯片上,哪个芯片管脚怎么变化,又怎么传到显示器上输出, 诸如此类涉及底层硬件的问题,我们没必要关注太多。于是,我们把用 printf()这类高级语言描述 设计逻辑的工作称为软件设计。然而,软件只是一种抽象的看不见摸不着的东西,它的结构接近 于人类思维逻辑。无论软件再怎样构思精妙,只有在硬件上才能体现出实际效果。做计算机开发 应用程序的时候,硬件是现成的,软硬件之间的桥梁早就由操作系统给你搭好了,我们只需专心 完成软件的构思和设计就 OK。显而易见,这种软硬件的分工会给电子设计带来极大的方便,自 然有人把这种分工方式引进 FPGA 设计领域,琢磨着怎么在小小的 FPGA 上也搞个软硬件协同 设计,负责硬件设计的和负责软件的各司其职,最后一整合,效率倍增。 那么再来看看 Qsys 是怎么做的。拿出我们的 Zircon 开发板,上面很多外设接口和与之 连接的芯片,那是硬件,中间有一块大的 CycloneIV 芯片,里面是空的,等着我们编写逻辑下载 到里面运行。而上一章里,我们已经知道 Qsys 系统是用软硬件协同设计来完成。回顾 Qsys 的 开发流程中,我们需要用到的三个工具,它们分别是 Quartus II、Qsys 和 Nios II SBT for Eclipse。 Quartus II、Qsys 中进行的设计完成了硬件的控制与设备的驱动,这些驱动有的是需要我们自行 编写的,有些是可以采用 Qsys 自带的 IP 核可以直接生成的。而基于 Nios II SBT for Eclipse 所 完成的设计我们称为软件设计或软件开发,这种软件开发和我们在 VC 中的设计大部分是极其类 似的,我们希望用行为级的设计语言去操作整个系统,而不想过多关注硬件的细节。说白了,就 叫应用程序开发。那么跟计算机对比一下,我们想一下:在 Qsys 中连接软件和硬件之间的那座 桥到底是什么呢?ALETRA 给了我们问题的答案,如图 3.26 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 103 图 3.26 Nios II SBT for Eclipse 工程结构 从该图中我们可以看出,Application Project 是我们的软件工程,Hardware System 则是我 们的硬件系统,而连接硬件与软件的这座桥叫做 HAL BSP Project,也就是我们所说的硬件抽象 层(HAL)系统库或者是板级支持包(BSP)。硬件抽象层系统库是指在应用程序和系统硬件之 间的一个系统库,HAL 应用程序接口(API)与 ANSI C 标准库综合在一起,可使用类似 C 语言 的库函数来访问硬件设备或文件,如 printf()、fopen()、fwrite()等函数。软件工程师可非常方便 地使用这些系统库来与底层硬件通信,而无须关心底层硬件实现细节。这样在上层应用程序和底 层硬件之间就构成了一个明显的界限,底层驱动程序的修改不会对应用程序造成任何影响。HAL 可以看作是一个支持应用程序开发的软件平台,它提供 API 函数接口,屏蔽硬件访问细节,为了 方便用户编程,用户只要利用 HAL 提供的各种函数就可以编写应用程序。虽然占用了一些额外 的资源,但是大大增加了应用程序的开发速度和可移植性。HAL 系统库在 Nios II SBT for Eclipse 中创建一个新的工程时,由 Nios II SBT for Eclipse 基于用户在 Qsys 中创建的 Nios II 处理器系 统自动生成。用户不用创建或拷贝 HAL 文件,也不用编辑 HAL 中的任何源代码。HAL 设备驱 动配置是和 Qsys 紧密相关的,如果硬件配置有了变化,那么 HAL 设备驱动配置也需要随之更 改,从而避免了由于底层硬件的变化而产生的编程错误。 3.2.1 system.h 系统描述文件 system.h 文件是 HAL 系统库的基础,它提供了关于 Nios II 系统硬件的描述,是硬件和软 件之间的桥梁。在第一次编译 Nios II SBT for Eclipse 过程中,编译工具会根据硬件系统文件 (.sopcinfo 文件)生成一个描述硬件信息的 system.h 文件。system.h 文件是由编译工具根据 Qsys 系统的内容(.sopcinfo 文件)和系统库的配置设置生成的一个头文件,如图 3.27 所示。 http://www.fpga.gs/ 104 软核演练篇 §3 Qsys system system.h system library settings 图 3.27 system.h 文件构成过程 从该图中我们可以看出,system.h 文件的内容主要由 2 个部分构成,一部分是描述系统库 的设置信息;另一部分是给出了每个外围设备的详细信息,其内容有:外围设备的硬件配置、外 设的基地址、中断优先级(如果外设有中断)、外围器件的符号名称。如果用户选择了 uC/OS II 支持,system.h 文件中还将包括 uC/OS II 的配置信息。对于程序员来说,system.h 文件中的系 统库设置信息基本用不到,所需要关心的仅仅是外围设备信息的宏定义及相关用法。接下来,我 们就以第二章的 Qsys_First 工程中的 system.h 文件中 LED_PIO 设备的宏定义描述为例,详细 分析该设备的宏定义及用法,如代码 3.1 所示。其他设备的宏定义与 LED_PIO 设备相似,这里 就不在给出。 代码 3.1 LED_PIO 在 system.h 文件中的宏定义 1 #define LED_PIO_BASE 0x1810 //LED_PIO 设备的寄存器基地址 2 #define LED_PIO_BIT_CLEARING_EDGE_REGISTER 0 //LED_PIO 位清除边沿寄存器 3 #define LED_PIO_BIT_MODIFYING_OUTPUT_REGISTER 0 //LED_PIO 位修改输出寄存器 4 #define LED_PIO_CAPTURE 0 //LED_PIO 的同步捕获设置 5 #define LED_PIO_DATA_WIDTH 8 //LED_PIO 的数据位宽 6 #define LED_PIO_DO_TEST_BENCH_WIRING 0 //LED_PIO 的仿真线设置 7 #define LED_PIO_DRIVEN_SIM_VALUE 0 //LED_PIO 的仿真值 8 #define LED_PIO_EDGE_TYPE "NONE" //LED_PIO 的边沿触发类型 9 #define LED_PIO_FREQ 100000000 //LED_PIO 的时钟频率 10 #define LED_PIO_HAS_IN 0 11 #define LED_PIO_HAS_OUT 1 12 #define LED_PIO_HAS_TRI 0 13 #define LED_PIO_IRQ -1 //LED_PIO 输入端口设置 //LED_PIO 输出端口设置 //LED_PIO 三态设置 //LED_PIO 中断号 14 #define LED_PIO_IRQ_INTERRUPT_CONTROLLER_ID -1 //LED_PIO 中断控制 ID Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 105 15 #define LED_PIO_IRQ_TYPE "NONE" 16 #define LED_PIO_NAME "/dev/led_pio" 17 #define LED_PIO_RESET_VALUE 0 18 #define LED_PIO_SPAN 16 19 #define LED_PIO_TYPE "altera_avalon_pio" //LED_PIO 中断类型 //LED_PIO 设备名称及所在路径 //LED_PIO 的复位值 //LED_PIO 的地址长度 //LED_PIO 的内核类型 3.2.2 硬件抽象层的构成 HAL 系统库为用户提供下列支持: (1) 集成了 ANSI C 标准函数库,允许调用类似 C 标准库函数; (2) 提供访问 NiosII 系统每个设备的驱动程序; (3) 提供 HAL API,用于标准的函数接口如设备访问、中断处理以及 ALARM 等; (4) 提供系统初始化函数,为 main()函数和 C 库函数建立运行时环境。由于这里包含了 Bootload 以及程序重定位等工作,所以 Nios II 开发中没有像 ARM 系统开发中涉及 Bootload 等问题; (5) 提供设备初始化函数,在 main()函数前,分配设备空间并初始化所有的外围设备。 HAL 系统库与 ANSI C 标准库一起构成 HAL 的运行环境。HAL 使用的 C 标准库是在嵌入 式上使用的免许可和版税的 C 语言程序库——Newlib。Newlib 是 C 语言标准库的一种开放源码 的实现。Nios II HAL 的结构如图 3.28 所示。 User Program C Standard Library HAL API Device Driver Device Driver … Device Driver Nios II Processor System Hardware HAL API _exit() close() closedir() fstat() getpid() gettimeofdav() ioctl() isatty() kill() lseek() open() opendir() read() readdir() rewinddir() sbrk() settimeofdav() stat() usleep() wait() write() 图 3.28 Nios II HAL 的结构 从该图中我们可以看出,用户程序架构在硬件抽象层和 C 标准库函数上,这说明用户程序 要访问硬件设备至少有 4 种方法: (1) 调用 C 标准库函数,如 printf()和 fwrite(); (2) 调用硬件抽象层的 API 函数,如 write(); (3) 调用设备驱动程序,如 alt_avalon_uart_write(); (4) 直接访问设备寄存器(如 IORD_ALTERA_AVALON_PIO_DATA(base))。 前两种方法的抽象层度最高,可移植性最好,对用户来说最容易实现,但是需要最多的额外 开销;第三种方法抽象层度较低,有一定的移植性,额外开销较少,但还需要用户考虑对设备写 数据前的其他硬件操作;最后一种方法完全是对硬件的直接访问,需要用户关注设备的每一个硬 http://www.fpga.gs/ 106 软核演练篇 §3 件细节,无额外开销。对于简单的硬件设备,如 I/O 的访问,4 种方法都可以考虑;但是对于复 杂的设备,建议使用前两种来访问。 3.2.3 类型定义 在编程的过程中,我们必须知道数据类型的宽度和精度。ANSI C 数据类型没有非常确切地 定义数据宽度,它们的数据宽度取决于编译器的定义。HAL 使用 alt_types.h 头文件定义了一套 支持 ANSI C 类型的数据类型,如表 3.5 所示。 表 3.5 HAL 数据类型定义 类型 说明 alt_8 有符号 8 位整数 alt_u8 无符号 8 位整数 alt_16 有符号 16 位整数 alt_u16 无符号 16 位整数 alt_32 有符号 32 位整数 alt_u32 无符号 32 位整数 Altera 提供的 GNU 编译器下的 ANSI C 数据类型宽度如表 3.6 所示。 表 3.6 ANSI C 数据类型宽度 类型 说明 char 8bits short 16bits long 32bits int 32bits 3.2.4 通用设备模型 HAL 为嵌入式系统中最常见的外围设备提供了通用的设备模型。各类通用设备模型使用相 同的应用编程接口(API),用户无需考虑底层硬件而利用与该设备模型相一致的 API 编写应用 程序。HAL 支持以下几种通用的设备模型: (1) 字符型设备:发送和接收字符串的外围硬件设备; (2) 文件子系统:提供访问存储在物理设备中的文件操作; (3) DMA 设备:执行大量数据在数据源和目的地之间传输的外围设备。数据源和目的地可 以是存储设备; (4) 定时器设备:对时钟脉冲计数并能产生周期行中断请求的外围设备。 (5) Flash 设备。 (6) 以太网设备:对 Altera 提供的轻量级 IP 协议提供访问的以太网连接。 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 107 上述的通用设备模型,由于具有相同且与硬件细节无关的应用编程接口(API),大大方便了 应用程序开发,增加了应用程序的开发速度和可移植性。HAL 系统库定义了一套函数,用户可 以用来初始化和访问上述类型的设备。例如,可以使用 C 的标准库函数 printf()和 fopen()来访问 字符型设备和文件子系统。对于应用程序开发者来说,不需要写底层驱动就可以和这些类型的设 备进行基本通信。另一方面,通用设备模型也大大方便了驱动程序的开发。各种类型的设备都定 义了一套必须的驱动函数。此外,HAL 函数和应用程序都可以作为用户访问设备,从而提高软 件开发的效率,节省开发时间。HAL 系统库调用驱动程序访问硬件,应用程序不用直接调用驱 动程序访问硬件,而是调用 ANSI C 或 HAL API。从这个意义上说,用户驱动程序其实已经作为 HAL API 的一部分在使用。 Altera 提供了许多在 Nios II 处理器系统中使用的外围设备,且大多数都提供了 HAL 设备驱 动,以便直接通过 HAL API 来访问这些设备。下面列出 Altera 提供的完整 HAL 支持的外围设 备: (1) 字符型设备包括 UART 核、JTAG 核以及 LCD16207 显示控制器; (2) 文件子系统包括只读文档系统; (3) DMA 设备包括 DMA 控制器核; (4) 定时器设备包括定时器核; (5) Flash 存储器设备包括通用 Flash 接口芯片和主动串行配置器件 EPCS 控制器。 (6) 以太网设备包括 LAN9111 以太网 MAC/PHY 控制器并且需要 uC/OS-II 支持。 除了以上所列的 Altera 提供的外围设备外,还有一些第三方提供的外围设备,用户也可以 定制自己的外围设备。所有这些为 Nios II 提供的外围设备都有一个头文件,这个头文件定义了 外围设备底层硬件接口。对于没有提供设备驱动程序的外围设备,用户就只能利用头文件中的宏 定义来访问该外设了。例如 Altera 提供了 PIO 核就不属于上述类型的外设,只能通过其他提供 了头文件的宏定义来操作 PIO。还有一些外设,属于上述外设类型中一类,但由于其特有的硬件 特性,不能完全使用相同的应用编程接口(API)来访问。HAL 系统库为此提供给了 UNIX 类型 的 ioctl()函数。由于 ioctl()依赖于具体的外围设备的硬件特性,所有其具体内容要查看各外设的 文档说明。 3.2.5 异常处理 Nios II 采用典型的、简单的、异常处理方式,使用一个简单的异常处理器来处理所有类型的 中断。通过前面的异常和中断的学习,我们可以知道当跳转到异常处理地址后,处理器开始执行 一段由 HAL 插入的代码,判断中断源和中断优先级,然后再跳转到用户的中断服务子程序(ISR) 中。即异常发送后,异常处理除 ISR 外的所有工作都由 HAL 系统库代码替用户完成。下面我们 给出异常处理过程和中断服务程序间的关系图,如图 3.29 所示。 http://www.fpga.gs/ 108 软核演练篇 §3 异常处理过程 中断服务程序 保护现场 查找中断原因 调用相应的ISR 恢复现场 中断返回 (由HAL提供) isr_name(void *context , int id) { /*用户代码*/ } (由用户编写) 图 3.29 异常处理过程和中断服务程序 从图中我们可以看出,当异常发生后,处理器会依次执行保护现场、查找中断原因、调用相 应的 ISR、恢复现场和中断返回。在 Nios II 中的异常处理中不采用中断向量表,所有异常和中 断都驻留在一个单一存储空间的代码来处理,这个存储空间称为 exception address。在该存储 空间,HAL 系统库会自动插入判断终端元和中断优先级的代码。对于用户,所要做的工作就是 编写中断服务程序。为了方便创建和维护中断服务程序,HAL 系统库提供了中断 API 函数,如 代码 3.2 所示。 代码 3.2 HAL 系统库提供的中断相关函数 1 /* 新版本的中断相关函数 */ 2 int alt_ic_isr_register(alt_u32 ic_id,alt_u32 irq,alt_isr_func isr,void *isr_context,void *flags); //注册中断服务程序 3 int alt_ic_irq_enable (alt_u32 ic_id, alt_u32 irq); //使能单个中断 4 int alt_ic_irq_disable(alt_u32 ic_id, alt_u32 irq); //禁止单个中断 5 alt_irq_disable_all (void) //禁止所有中断 6 alt_irq_enable_all (alt_irq_context context) //使能所有中断 7 alt_irq_cpu_enable_interrupts() //允许嵌套 8 alt_u32 alt_ic_irq_enabled(alt_u32 ic_id, alt_u32 irq); //检测中断的状态 9 alt_irq_enabled (void) //检测中断的状态 10 alt_irq_pending() //返回当前挂起的中断 11 /* 老版本的中断相关函数 */ 12 int alt_irq_register (alt_u32 id, void* context,alt_isr_func handler);//注册中断 服务程序 13 alt_irq_enable(alt_u32 id) 14 alt_irq_disable(alt_u32 id) 15 alt_irq_interruptible(alt_u32 priority) 16 alt_irq_non_interruptible(alt_u32 mask) //使能单个中断 //禁止单个中断 //允许嵌套 //禁止嵌套 上 面 所 给 出 的 中 断 相 关 函 数 源 码 均 可 在 “ C:\altera\13.1\nios2eds\components\alter a_nios2\HAL\inc\sys\alt_irq.h ” 中 找 到 , 我 们 在 使 用 这 些 函 数 时 , 必 须 包 含 该 头 文 件 #include 。 这 里 需 要 我 们 注 意 的 是 : alt_irq_interruptible() 和 alt_irq_no n_interruptible() 必 须 配 对 使 用 : 当 要 实 现 中 断 嵌 套 时 , 先 在 中 断 服 务 程 序 中 调 用 alt_irq_interruptible(),高优先级的中断就可以中断当前中断服务程序,在中断返回时一定要调 用 alt_irq_non_interruptible()关闭中断嵌套,否则会引起中断自锁。使用 HAL API 函数编写中 断服务程序分 2 步: (1) 参照 void isr_name(void *context,alt_u32 id)函数原型编写中断服务子程序。其中 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 109 isr_name 是用户给 ISR 取的函数名;void *context 是指向传递给 ISR 的信息的全局 变量指针(通常是寄存器信息);id 是硬件中断号,在 system.h 中声明。 (2) 调 用 alt_irq_register(alt_u32 id,void *context,void (*isr)(void *,alt_u32)) 或 者 是 alt_ic_isr_register(alt_u32 ic_id, alt_u32 irq, alt_isr_func isr, void *isr_context, void *flags);函数向硬件抽象层登记中断服务子程序。其中 alt_u32 id 和 alt_u32 irq 是硬件 中断号;void *context 和 void *isr_context 是指向传递给 ISR 的信息的全局变量指针 (通常是寄存器信息);void (*isr)(void *,alt_u32)和 alt_isr_func isr 是指向 ISR 的函 数指针;alt_u32 ic_id 是中断控制器标号,*flags 保留未用。 关于中断服务程序编写的例子可以参考 Altera 提供的模板程序“C:\altera\13.1\nios2e ds\examples\software\count_binary\count_binary.c”。中断服务程序主要处理一些高优先级, 实时性比较强的操作,所以不可以在中断服务程序里面进行等待或者其他阻塞性的操作。下面给 出一些编写中断服务程序的注意事项,希望能在应用开发中起到一些帮助作用。 (1) 尽量保持中断服务程序精简(精简以为这快速和可靠); (2) 把无关紧要的事情放在中断服务程序之外处理; (3) 尽量避免调用 C 库函数(如 printf(),因为可能引起阻塞且运行时间无法预知); (4) 尽量避免进行浮点操作。 中断服务程序的调试和一般程序调试有些区别,也复杂,主要是因为中断事件何时发生是无 法预测的。下面给出一些调试中断服务程序的经验。 (1) 在中断服务程序内设置断点。当中断发生后,处理器会在断点处停下,用户可以单步调 试中断服务程序。需要注意的是,在单步调试的过程中,硬件会忽略其他中断。 (2) 使用 sprintf()函数把关键数据写到内存中,然后触发外部分析程序,在中断服务程序中, 不可以调用 printf()函数,但可以调用 sprintf()。 §3.3 Nios II用户程序引导过程深入剖析 3.3.1 什么是 Bootloader? 通过前面章节的学习,我们知道了 Qsys 系统是可以下载到开发板的 Flash 中,并且还能使 开发板在一上电就能够运行我们的程序,有的读者就会认为这一现象很神奇,但好奇心比较强的 读者便会有这么一个疑问,为什么烧写到 Flash 中,程序就能够上电自启动呢?这是一个好问 题,下面我们就来揭开它的神秘面纱,详细了解开发板在上电的一瞬间究竟做了什么。Nios II 的 启动过程要经历两个过程: (1) FPGA 器件本身的配置过程。FPGA 器件在外部配置控制器或自身携带的配置控制器 (EPCS)的控制下配置 FPGA 的内部逻辑。如果内部逻辑中使用了 Nios II,则配置 完成的 FPGA 中包含有 Nios II 软核 CPU。 (2) Nios II 本身的引导过程。一旦 FPGA 配置成功后,Nios II 就被逻辑中的复位电路复位, 然后开始从头执行软件代码。 http://www.fpga.gs/ 110 软核演练篇 §3 这两个过程应该不难理解,前一个可以理解为硬件构建,即把 FPGA 从白片变成一个包含 Qsys 的 SOPC 硬件系统,这部分我们并不陌生了;后一个可以理解为软件复位,让我们所开发 的软件程序运行于 Nios II 的 CPU 上。而在此 Bootloader 起了关键的作用。那么到底什么是 Bootloader 呢?要了解 Bootloader,首先我们要弄清楚几个地址概念,如图 3.30 所示。 图 3.30 Linker Script 配置页面 从该图中我们可以看到,有.bss、.entry、.exceptions、.heap、.rodata、.rwdata、.stack 和.text 这几个地址,下面我们就对它们分别进行介绍: (1) .bss(未初始化变量数据段):未初始化变量,如全局变量定义后,但未初始化的都放 这里; (2) .entry(复位地址):最终软件代码存储区,程序从此处开始启动; (3) .exceptions(异常处理地址):系统异常处理代码存放的地方 (4) .heap(堆):动态分配内存的存储区,从小地址向大地址方向增长; (5) .rodata(只读数据段):用于存放程序中的静态全局变量,所有赋了初值的全局变量都 放在这里; (6) .rwdata(可读写数据段):用于存放程序中可读写变量和指针变量; (7) .stack(栈):函数调用变量与临时数据存储器区(如返回地址,现场信息),从大地址 向小地址方向增长; (8) .text (正文段,只读):代码执行区,也就是就是 Nios II 程序最终运行的地方;一个 程序只有一个副本;只读,防止程序由于意外事故而修改自身指令; Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 111 知道了这些地址的功能,接下来我们再来看下如何设置这些地址。对于.bss、.heap、.roda ta、.rwdata、.stack 和.text 地址,我们可以在 Nios II BSP Editor 中的 Linker Script 配置页面中 直接设置,对于.entry 和.exceptions 这两个地址,我们在 Nios II BSP Editor 中的 Linker Script 配置页面中是不可以设置的,从上面的图片中我们可以看到它们是灰色的。如果我们想要设置这 两个地址,那么我们需要在 Qsys 软件中进行设置,如图 3.31 所示。 图 3.31 复位地址和异常地址设置页面 讲完了这些地址,下面我们举一个简单的例子:如果我们将 Reset Vector memory 和 Exception Vector Memory 都指向 epcs_flash,如图 3.32 所示。 图 3.32 将复位地址和异常地址都指向了 epcs_flash 并且在 Nios II BSP Editor 中 System Library 下的.bss、.heap、.rodata、.rwdata、.stack 和.text 都设为 cfi_flash,如图 3.33 所示。 http://www.fpga.gs/ 112 软核演练篇 §3 图 3.33 Linker Script 配置页面 那么这时候会如何呢?显然,这样设置就表示下载的时候程序代码会固化到 epcs_flash 芯 片上,并且程序也在 epcs_flash 中运行。通过学习《数字电路篇》,我们知道虽然 Flash 具有较 低的价格、较高的容量、掉电数据不丢失等优点,但是 Flash 的运行速度很慢;程序运行在 Flash 中对运行速度有影响,也容易损坏 Flash 芯片。相反,易失性存储器,如 sdram。具有较快的运 行速度,所以通常我们会把程序放到 sdram 中运行。要让程序在 sdram 中运行,那么需要将 Exception Vector 设 置 成 sdram , 并 且 还 将 Nios II BSP Editor 中 System Library 下 的.bss、.heap、.rodata、.rwdata、.stack 和.text 都设为 sdram,如图 3.34 所示和图 3.35 所 示。 图 3.34 设置 Nios II 复位地址和异常地址 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 113 图 3.35 Linker Script 配置页面 从上面的描述我们知道,sdram 掉电之后数据都会丢失,那么问题来了,如何使用 sdram 来存储程序代码呢?不急!办法是有的,下面就听我慢慢道来,我们不是还有个 epcs_flash 吗? 它可不会因为掉电而丢失数据的,因此我们 Reset vector memory 指向了 epcs_flash 中去,在 Linker Script 配置页面中我们依然可以看到 reset 指向 epcs_flash,很奇怪吧,什么也没变啊? 怎么让程序运行在 sdram 中呢?这时候我们需要一位助手,它就是 bootloader,这是一段引导 程序,它是由 Nios II SBT for Eclipse 根据你的设置自动生成。它会把存放在 epcs_flash 中的相 关的程序段搬到它们需要运行的地方,就是我们设置的 sdram 中,如果是 onchip_memory 的 话,程序就搬到 onchip_memory 里面去了。这样的话,不仅实现了程序的掉电存储,而且还让 它在 sdram 中运行了。这里需要我们注意的是,程序在运行前所有的数据都是存放在 EPCS Flash 中。如果你设计的系统 Reset address 与其它地址的设置不相同,比如 Reset address 和 Exception address ,Reset address 和 Linker Script 配置页面中的.text 设成不同的存储器,那 么系统从 Reset address 启动时都会从 Flash 中把相关数据自动下载到相应的地址或者初始化 相应地址,也就是说: (1) 如果 Exception address 和 Reset address 不一样,那么程序从 Reset address 启动 后将把放在 Reset address 处的系统异常处理代码拷贝到 Exception address 。 (2) 如果.text address 和 Reset address 不一样,那么程序从 Reset address 启动后将 把放在 Reset address 处的普通只读程序代码拷贝到 text address。 通过上面的认识,我们就比较清楚了各存储器间可进行哪些搭配了,Reset address 的选择 可选 EPCS Flash 或者 CFI Flash;program memory(.text)可选择 Onchip Memory 、SDRAM 等。 http://www.fpga.gs/ 114 软核演练篇 §3 3.3.2 用户程序引导过程 说了这么多我想大家都已经对 Nios II 的 boot 设置以及 Bootloader 都有所了解了吧,在整 个 boot 过程中,bootloader 作为“代码搬用工”起到了十分关键的作用。如果存在,那么 Bootloader 就是系统加电启动后运行的第一段代码,Bootloader 能够根据用户在 Nios II SBT for Eclipse 中设置的用户程序文件(.elf)连接地址来重新装载程序,然后跳过.elf 文件的连接地址执 行程序。下面我们给出 Bootloader 的执行过程: (1) 完成复位子程序的功能。在从复位地址开始的 32 字节(指令缓存行的尺寸)空间内完 成复位的软件响应。 (2) 加载应用项目程序映像中的代码和数据段(从装载地址 Reset address 加载到程序运 行地址.text)。 (3) 跳转到应用程序入口(通常是_start)。 当 Bootloader 跳转到应用程序入口后,便会用到 Nios II 软件架构为我们准备的启动代码, 它们分别是 Crt0.S 和 alt_main.c 两个 HAL 源文件。Crt0.S 位于“C:\altera\13.1\nios2eds\c omponents\altera_nios2\HAL\src\crt0.s”中,主要完成了以下功能: (1) 初始化指令 Cache,初始化数据 Cache; (2) 设置堆栈指针(Stack Pointer)寄存器; (3) 全局指针(Global Pointer)寄存器; (4) 初始化 bss 段,__bss_start ~ __bss_end 清零; (5) 如果系统中没有 bootloader,则把可读写数据(.rwdata),只读数据(.rodata)和异常向 量表装入 RAM 中; (6) 调用 alt_main()函数,也就是执行 alt_main.c 文件中的内容。 alt_main.c 位于“C:\altera\13.1\nios2eds\components\altera_hal\HAL\src\alt_main.c” 文件中,主要完成了以下功能: (1) 调用 alt_irq_init()初始化中断控制器。 (2) 调用 ALT_OS_INIT()初始化 OS。 (3) 如果使用了操作系统,那么初始化用于控制访问文件描述器列表的信号量。 (4) 使能中断。 (5) 调用 alt_sys_init()初始化添加的 IP 核的设备驱动。 (6) 把标准输入输出设备(stdin, stdout, stderr)映射到合适的设备上 (7) 调用 C++的构造器; (8) 调用 main()函数 (9) 调用 exit()函数。 最后我们将整个用户程序引导过程,总结如图 3.36 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §3 Qsys 到底是如何运行的 115 运行Bootloader 初始化.bss地址段 使能中断 跳转到ctr0.S _start标号处 调用 alt_load函数 调用alt_sys_init() 初始化设备驱动 初始化数据缓存 调用 alt_main函数 把标准输入输出映 射到合适的设备上 初始化堆栈指针 调用alt_irq_init() 初始化中断控制器 调用main函数 初始化 全局变量指针 调用alt_os_init() 初始化OS 图 3.36 整个用户程序引导过程图 调用exit函数 至此,我们第三章剥去神秘的外衣——Qsys 到底是如何运行的就讲解完了。接下来我们讲 解的是第四章 Qsys 丰富多彩的内置 IP 核。 http://www.fpga.gs/ Qsys 丰富多彩的内置 IP 核 第四章 Qsys 丰富多彩的 IP 核 终于熬过了枯燥的第三章,接下来,我们将迎来更加轻松有趣的第四章。为什么这么说呢, 因为我们在第三章节中主要讲解了一些理论层面的知识,所以大家听起来难免会枯燥乏味。而我 们的第四章主要讲解一些应用层面的知识,这些应用知识我们大家看的见、摸的着,能够真枪实 战的去操练,因此,我们的第四章要比第三章更加轻松有趣。在开始进入第四章节之前,我们觉 得有必要要和大家说明一下,我们后续章节的内容安排,说到这内容安排,我们不得不要从我们 开发板上的 UI 系统说起,大家看,如图 4.1 所示。 图 4.1 A4 开发板的 UI 系统主界面 这就是我们的开发板上的 UI 系统,从图中我们可以看出,主页面中有简介、测试、演示、 提示四个功能选项,下面我们就来简单的介绍一下这四个功能选择,简介页面是用来介绍我们整 个 UI 系统相关功能的;测试页面是用来测试我们开发板上的外设,演示页面是演示我们《项目 实战篇》中的工程功能的;提示页面是用来告诉我们一些开发板的使用事项。 如果你已经购买了我们的开发板,那么我相信这些功能选项你都已经比较熟悉了,用过了我 们的这个 UI 系统,你有没有感觉到它很人性化。当你在操作这个 UI 系统的时候,你有没有一种 错觉,你使用的不在是一个开发板,而是一个手机!看到这里,也许你会有这么一个想法,我们 的这个 UI 系统是如何制作的呢?大家不要着急,下面我们就给大家娓娓道来。由于我们的 UI 系 统涉及的知识面比较广泛,所以我们的 UI 系统制作起来比较复杂。我们想要实现 UI 系统,我们 需要掌握以下三个方面的知识: (1) 内置的 IP 核:这部分的内容也就是我们本章节中所讲的的内容。 (2) 自定义 IP 核:这部分的内容我们将会在第五章节中进行详细的讲解。 (3) 操作系统及 UI 界面:这部分的内容我们将会在第六章节中进行详细的讲解。 我们只有掌握了这三个方面的知识,我们才能设计出 UI 系统,大家在学习这三个方面的内 容时,要脚踏实地,一步一个脚印。相信大家都听说过这么一句话:Rome was not built in a day。 接下来我们就来进行第四章,Qsys 丰富多彩的内置 IP 核的内容讲解,我们已经知道,Qsys 能够使我们简单直观的构建一个片上嵌入式系统,不需要拘泥于具体底层逻辑代码的实现。这其 中的关键就在于 Altera 已经帮助大家设计出了许多常用外设接口及其协议模块的硬件描述语言 120 FPGA 软核演练篇 §4 实现框架与软件驱动,也就是我们前面所说的 IP 核。这些常用外设 IP 核不仅仅只有我们使用 PIO 和 JTAG_UART 这些 IP 核,它还包含了大量的其它 IP 核,比如定时器、串行通信接口、 SDRAM 控制器和其他的存储器接口等。这里我们将常用的 IP 核总结如表 4.1 所示。 表 4.1 Qsys 中常用的 IP 核 外围设备 功能说明 PIO 1~32 位并行 I/O 接口(输入、输出和边沿捕捉) SDRAM 提供一个连接片外 SDRAM 芯片的 Avalon 接口 EPCS 实现处理器系统访问 Altera EPCS 串行配置器件 Timer 32 位定时器,能被用作周期性脉冲发生器或系统监视定时器 System ID 实现 Nios II 系统与所指定的硬件系统间的匹配 DMA 在外围设备和存储器之间有效地进行批量数据传送 JTAG UART 实现基于 FPGA 的嵌入式系统与主机之间的串行符号流通信 UART 通用串行接口,波特率、数据位等可调,流量控制信号可选 SPI 3 线主/从串行外设接口 这些 IP 核我们将会在后续章节中一一介绍,这些 IP 核虽然功能不同,但是它们使用起来 的过程是极其类似的。因此,对于每个外设 IP 核的介绍,我们都遵循如下的模式: (1) IP 核的综述; (2) IP 核的寄存器描述; (3) IP 核的配置选项; (4) IP 核的软件编程; (5) IP 核的应用实例; 值得注意的是,由于每个 IP 核的功能不同,所以它们是相对独立的。我们尽可能完整地给 出了每个 IP 的特点、配置以及软件编程,供大家在使用这些外设定制 Nios II 系统时查阅。但由 于篇幅冗长,在阅读过程中,你可以任意跳过里面的部分章节,并不会影响整个知识框架的理解。 这里我们推荐你先精读前三个外设 IP,后面的可以当有需要用到的时候再一一查阅。 此外,学习这部分的建议大家先不求甚解,再做到深入理解。为什么要不求甚解?因为 Altera 为我们提供这些 IP 的目的就是为了让大家更方便快捷地对这些通用外设进行驱动。每个 IP 的作 用就是为了驱动一个特定的外设,即使我们不知道它底层是如何实现的,这也并不妨碍我们用好 它。如果你时间有限,且只是想做到“会用”,那么开始可以不需要花很多的时间把文档里面的 每一行都了解清楚,只需要跟着我们的例程走下去就行了。因为你最想知道的是怎么让它工作起 来,而不是它怎么工作的,最终你一定能够娴熟地使用每一种外设。那么又为什么要再做到深入 理解呢?原因之一,这里先卖个关子,读到下一章你自然就知道了。第二个原因嘛…..因为我们 最终的目标就是做一名优秀的工程师,优秀的工程师怎么能不求甚解呢? §4.1 PIO IP核 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 121 4.1.1 PIO IP 核的综述 首先我们介绍的是 PIO IP 核的综述,PIO IP 核是 Nios II 处理器支持的可编程并行输入/输 出,它是一种带有 Avalon 总线借口的核,提供了一个介于 Avalon 存储器映射的从端口和普通 I/O 端口之间的存储器映射借口。I/O 端口一端连接偏上的用户逻辑,另一端链接 FPGA 外部的 I/O 管脚。PIO IP 核为用户逻辑货外部器件提供了简单的直接 I/O 操作,可以直接完成位操作。 每个 Avalon 接口的 PIO 内核可提供 32 个 I/O 端口且端口数可设置,用户可以添加一个或多个 PIO IP 核。CPU 通过 I/O 寄存器控制 I/O 端口的行为。I/O 可以配置为输入、输出和三态,还可 以用来检测电平事件和边沿事件。 4.1.2 PIO IP 核的寄存器描述 介绍完了 PIO IP 核的综述,接下来我们再来看看 PIO IP 核的寄存器描述,PIO IP 核的寄存 器描述如表 4.2 所示。 表 4.2 PIO IP 核的寄存器描述 偏移量 寄存器名称 操作 (n-1) … 2 1 0 读 返回在输入引脚上出现的值 0 data 写 向 PIO 写入新值 1 direction 读/写 控制每个 I/O 口的输入/输出方向。0:输入;1:输出 2 interruptmask 读/写 使能或禁止每个输入端口的 IRQ。1:中断使能;0:禁止中断 3 edgedcapture 读/写 当边沿事件发生时对应位置 1 4 outset 写 指定输出位为 1 5 outclear 写 指定输出位为 0 从 PIO IP 核寄存器的描述表格中我们可以看出,PIO IP 核有 6 个寄存器,接下来我们就对 这 6 个寄存器分别进行一个介绍: (1) 数据寄存器(Data) 读数据寄存器,返回在输入引脚上出现的值。如果 PIO IP 核硬件配置为“Output Ports Only”, 则读数据寄存器返回未定义的值。写数据寄存器驱动输出口输出写入的值。如果 PIO IP 核硬件 配置为“Input Ports Only”,则写数据寄存器无效。如果 PIO IP 核硬件配置在双向模式下,那么 方向寄存器中对应的位设 1(输出)时,值才输出。 (2) 方向寄存器(Direction) 只有 PIO IP 核工作模式配置为“Bidirectional ports”时,方向寄存器才存在。PIO IP 核工 作模式(输入、输出或双向)在添加 PIO IP 核时指定,且在系统生成后不能改变。在仅为输入 或仅为输出模式下,方向寄存器并不存在,此时,读方向寄存器返回未定义的值,写方向寄存器 无效。方向寄存器控制每个 PIO 口的数据方向。当方向寄存器中的位 n 设为 1 时,端口 n 为输 出模式;0 时,端口 n 为输入模式。复位后,方向寄存器的所有位设置为 0.因此所有双向 I/O 口 配置为输入。 (3) 中断屏蔽寄存器(Interruptmask) http://www.fpga.gs/ 122 FPGA 软核演练篇 §4 当中断屏蔽寄存器的位设为 1 时,使能相对应的 PIO 输入口中断。中断操作取决 PIO IP 核 的硬件配置,只有配置为输入口时才能进行中断操作。中断屏蔽寄存器只有在硬件配置为 “Generate IRQ”时才存在。如果硬件配置未选中“Generate IRQ”,那么读中断屏蔽寄存器返 回未定义的值,写中断屏蔽寄存器无效。复位后,中断屏蔽寄存器所有位为 0,因此禁止所有 PIO 口的中断。 (4) 边沿捕获寄存器(Edgedcapture) 只要在输入口上检测到边沿事件时,边沿捕获寄存器中对应位 n 置 1。Avalon 主控制器可 读边沿捕获寄存器来确定边沿在哪一个 PIO 输入口出现。写任意值到边沿捕获寄存器将使寄存 器所有位清 0.要检测的边沿类型在 PIO IP 核添加时指定。当硬件配置有边沿捕获功能时,边沿 捕获寄存器才存在,否则,读边沿捕获寄存器返回未定义的值,写边沿捕获寄存器无效。 (5) 置位和清零寄存器(outset and outclear) 你可以使用置位和清零寄存器设置输出端口为 1 或 0。比如,当我们想把输出端口的第 6 位 设置成 1 时,我们可以写 0x40 到置位寄存器。当我们想把输出端口的第 3 位设置成 0 时,我们 可以写 0x08 到清零寄存器。 4.1.3 PIO IP 核的配置选项 介绍完了我们的 PIO IP 核的寄存器描述,接下来我们再来看一看 PIO IP 核在 Qsys 中的配 置选项,图 4.2 给出了 PIO IP 核的配置选项图。 图 4.2 PIO IP 核的配置选项图 从 PIO IP 核的配置选项图中我们可以看出,PIO IP 核在 Qsys 有 5 种设置方式,接下来我 们就对这 5 种设置方式分别进行介绍: Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 123 (1) 基础设置(Basic Settings) Basic Settings 允许用户指定 I/O 口的位宽和方向。Width 设置可为 1~32 的任何整数值,如 果 Width 设置为 8,则 PIO 为 8 位宽。Direction 设置有 4 个选项,各选项描述如表 4.3 所示。 表 4.3 Direction 设置的 4 个选项描述 设置 描述 双向(三态)端口 [Bidirectional(tri-state)Ports] 该模式下,每个 PIO 位共用一个设备引脚来驱动和捕获数据。 每个引脚方向可单独选择。若要使 FPGA 的 I/O 引脚为三态,则 把 I/O 设置为输入状态。可选择输入时是否产生中断。 仅为输入端口(Input Ports Only) 在该模式下,PIO 端口仅为输入状态,可选是否产生中断。 仅为输出端口(Output Ports Only) 在该模式下,PIO 端口仅为输出状态,不能产生中断。 输入和输出端口 在该模式下,输入和输出端口总线独立,相当于同时生成“仅为 (Both Input and Output Ports) 输入端口”+“仅为输出端口”。 (2) 输出寄存器(Ouput Register) 这个选项就是设置我们所说的置位寄存器和清零寄存器。在 Direction 为 Input Ports Only 时,输出寄存器是不可以使用的。 (3) 边沿捕获寄存器(Ouput Register) 当我们选中 Input Options 选项时,PIO IP 核便允许我们设定边沿捕获和产生 IRQ。当我们 选择 Output Ports 选项时,边沿捕获和产生 IRQ 选项是不可使用的。当我们 Synchronously capture 选项选中时,PIO IP 核包含边沿捕获寄存器(Edgecapture)。我们必须进一步指定要检 测哪种边沿类型:  Rising Edge 上升沿;  Falling Edge 下降沿;  Any Edge 上升或下降沿。 当指定类型的边沿在输入端口出现时,边沿捕获寄存器对应位置 1。当 Synchronously capture 选项未选中时,边沿捕获寄存器不存在。这里需要注意,若是在 Qsys 中选择了 enable bit-clearing for edge capture register 的话,那么对于 edge capture 就应该是写 1 清中断;若 是没有选择 enable bit-clearing for edge capture register,则是写任意数清中断。 (4) 中断(Interrupt) 当 Generate IRQ 选项选中时,若指定的事件出现在输入端口,则 PIO IP 核申请 IRQ 中断。 用户必须进一步指定 IRQ 事件产生的原因:  Level 只要输入为高电平且中断使能,那么 PIO IP 核产生一个 IRQ。  Edge 只要边沿捕获寄存器中相应位为 1 且中断使能,那么 PIO IP 核产生一个 IRQ。 如果希望低电平时中断,则需在该 I/O 输入引脚前加一个非门(使用片内逻辑最方便)。当 Generate IRQ 选项未选中时,中断屏蔽寄存器不存在。当硬件配置为电平触发方式时,只要高 电平出现并且中断使能,就申请一个中断。当硬件配置为边沿触发方式时,只要捕获到边沿事件 并且中断使能时,就申请一个中断。中断(IRQ)一直保持有效直至禁止中断(中断屏蔽寄存器 相应位清零)或清边沿捕获标志(向边沿捕获寄存器写一个任意值)为止。每个 PIO IP 核的 I/O http://www.fpga.gs/ 124 FPGA 软核演练篇 §4 口共用一个中断号(系统生成时指定),用户需要在中断服务子程序中通过中断掩码的方式来查 明是哪个 I/O 产生了中断。 (5)仿真(Test bench wiring) 当我们需要进行仿真时,我们便可以在这里设置,由于仿真很少使用,所以我们这里就不在 进一步进行讲解。 4.1.4 PIO IP 核的软件编程 介绍完了 PIO IP 核的配置选项,接下来我们再来看看 PIO IP 核的软件编程,PIO IP 核提供 了对硬件进行寄存器级访问的文件 altera_avalon_pio_regs.h。该文件定义了内核的寄存器映射 并提供硬件设备访问宏定义。我们可以通过阅读上述文件来熟悉 PIO IP 核的软件访问方法,我 们这里建议大家不要修改该文件。 4.1.5 PIO IP 核的应用实例——流水 介绍完了 PIO IP 核的软件编程,接下来我们再来看看 PIO IP 核的应用实例,对于应用实例 的讲解,我们这里需要说明的是,我们将会从以下四个方面进行讲解: (1) 实验目的:该方面主要是用来讲解,我们的实验将要完成一个怎样的功能,我们从该实 验中将会学到什么知识。 (2) 硬件框架:该方面主要是用来讲解,如何构建一个 Qsys 系统,以及我们构建该实验都 用到了哪些 IP 核。 (3) 软件工程:该方面主要是用来讲解,如何通过 C 语言程序来完成我们的设计,实现我 们的功能。 (4) 板级调试:该方面主要是用来验证,我们的实验是否正确,以及能否在锆石 A4 开发板 上正常运行。 (1) 实验目的 首先我们讲解的是第一部分实验目的,该实验主要是利用 PIO IP 核来控制 8 个 LED 进行 流水灯显示,在这个实验中,我们将学习到 PIO IP 核在 Qsys 软件中是如何进行配置的,以及 我们如何利用 C 语言程序来通过 PIO 输出数据。 (2) 硬件框架 由于之前我们已经给出了详细的工程建立步骤,那么这里我们就不再给出大量的重复步骤, 大家只需要根据我们给出的关键性步骤,便能创建出可以用于实验的工程。如果此时你对 Qsys 软件创建流程还不是很熟悉,那么你可以参考该篇第 2 章中介绍的内容。下面我们便给出该实验 的硬件框架,如图 4.3 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 125 图 4.3 LED 流水灯实验的硬件框架图 从该图中我们可以看出,我们使用了 Nios、Onchip_Memory、Jtag_Uart 和 PIO 四个 IP 核,对于这四个 IP 核,相信大家都不会陌生,我们在之前工程中都有用过,这里我们就不再给 出它们的详细配置图。下面我们就对它们的配置进行一个简单的讲解,在这四个 IP 核中,我们 Nios 配置使用的是 Nios II/f 等级;onchip_memory 配置使用的是单端口 RAM,RAM 的大小为 30KB。jtag_uart 配置使用的是默认配置;pio 配置使用的是输出端口,数据宽度为 8。这里要注 意的是,由于我们这里没有用到 onchip_rom,只用了一个 onchip_ram,所以读者在设置 Reset Vector 和 Exception Vector 的时候全都选择 onchip_ram 即可。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.1 所示。 代码 4.1 Qsys_Led.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Led.c 3 //-- 描述 : 通过 PIO 直接控制 8 个 Led 产生流水灯效果 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "system.h" //系统头文件 8 #include "unistd.h" //延迟函数头文件 9 #include "alt_types.h" //数据类型头文件 10 #include "altera_avalon_pio_regs.h"//pio 寄存器头文件 11 12 /* 流水灯花样,Led 低电平亮,高电平灭 */ 13 alt_u32 LED_TBL[] = { 14 0xFF, 0x00, 15 0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F, 16 0xFE, 0xFC, 0xF8, 0xF0, 0xE0, 0xC0, 0x80, 0x00, 17 0x00, 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 18 0x7E, 0xBD, 0xDB, 0xE7, 0xE7, 0xDB, 0xBD, 0x7E, 19 0x7E, 0x3C, 0x18, 0x00, 0x00, 0x18, 0x3C, 0x7E // 全部熄灭后,再全部点亮 // 依次逐个点亮 // 依次逐个叠加 // 依次逐个递减 // 两个靠拢后分开 // 从两边叠加后递减 http://www.fpga.gs/ 126 FPGA 软核演练篇 §4 20 }; 21 22 //--------------------------------------------------------------------------- 23 //-- 名称 : main() 24 //-- 功能 : 程序入口 25 //-- 输入参数 : 无 26 //-- 输出参数 : 无 27 //--------------------------------------------------------------------------- 28 int main (void) 29 { 30 alt_u8 i; 31 32 while(1) 33 { 34 for(i=0; i<=41; i++) 35 { 36 //将 LED_TBL 数组中的数值依次赋值给 Led 进行显示 37 IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE, LED_TBL[i]); 38 usleep(500000); //等待 100ms 39 } 40 } 41 return 0; 42 } 由于该代码比较简单,并且我们也给出了详细的注释,所以这里我们就不再一句一句的讲解 代码了,我们下面主要为大家讲解代码中的一些关键部分。 (1) 为什么要包含 system.h 文件?这主要是因为 system.h 头文件是系统硬件信息的宏定 义文件,程序中 LED_PIO_BASE 就是从该文件中获取的。 (2) altera_avalon_pio_regs.h 寄存器头文件一般有什么用?altera_avalon_pio_regs.h 头 文件提供 PIO 内核寄存器访问宏定义,该文件路径为 C:\altera\13.1\ip\altera\sopc_build er_ip\altera_avalon_pio\inc\ altera_avalon_pio_regs.h。程序中对 I/O 口操作的宏定 义都在这个文件中给出。 (3) altera_types.h 头文件的作用是什么?altera_types.h 头文件定义了与 Nios II 相关的数 据类型,它的路径为 C:\altera\13.1\nios-2eds\components\altera_nios2\HAL\inc\al t_types.h。 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Led.sof 下载至我们的 A4 开发板,Qsys_Led.sof 下载完成后, 我们还需要在 Eclipse 软件中将 Qsys_Led.elf 文件下载至我们的 A4 开发板,Qsys_Led.elf 下 载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,我们可以看到 A4 开发板上的 Led 全部熄灭后,再全部点亮,依次逐个点亮,依次逐个叠加,依次逐个递减,两个靠拢后分开,从 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 127 两边叠加后递减等等各种流水花样。由于我们的图片不能显示动态效果,所以我们这里就给出一 个 LED 全部点亮的图片,如图 4.4 所示。 图 4.4 LED 流水灯实验的板级调试图 4.1.6 PIO IP 核的应用实例——按键 (1) 实验目的 介绍完了 PIO 流水实验,接下来我们再来看看 PIO 按键实验,对于 PIO 按键实验,我们将 和前面一样,也是从四个方面进行讲解。首先我们讲解的是第一个方面实验目的。该实验主要是 利用 PIO IP 核读取按键的值,然后将读到的值输出到 LED 上进行显示。在这个实验中,我们将 学习到 PIO IP 核在 Qsys 软件中是如何进行配置,以及我们如何利用 C 语言程序来通过 PIO 读 取数据。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.5 所示。 http://www.fpga.gs/ 128 FPGA 软核演练篇 §4 图 4.5 按键实验的硬件框架图 从该图中我们可以看出,我们的 PIO 按键实验在 PIO 流水实验的基础上多添加了一个 PIO IP 核,该 PIO 的配置我们使用的是输入端口,数据宽度为 8,至于其他的 IP 核配置,我们这里 和 PIO 流水实验中的配置是一样的,没有做任何改动。这里需要注意的是,我们的按键没有连 接内部中断。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.2 所示。 代码 4.2 Qsys_Key.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Key.c 3 //-- 描述 : 按下不同的按键,点亮相对应的 Led。 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "system.h" //系统头文件 8 #include "alt_types.h" //数据类型头文件 9 #include "altera_avalon_pio_regs.h"//pio 寄存器头文件 10 11 //--------------------------------------------------------------------------- 12 //-- 名称 : main() 13 //-- 功能 : 程序入口 14 //-- 输入参数 : 无 15 //-- 输出参数 : 无 16 //--------------------------------------------------------------------------- 17 int main(void) 18 { 19 alt_u32 key_state,led_state; //Key 和 Led 缓存变量 20 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 129 21 while(1) 22 { 23 //读取按键的值,并赋值给 key_state。 24 key_state = IORD_ALTERA_AVALON_PIO_DATA(KEY_PIO_BASE); 25 //注意:Key 悬空为低电平,按下为高电平。Led 低电平亮,高电平灭。 26 //为了保证按下不同按键点亮相对应 Led,因此,我们将按键的值取反后,再赋值给 led_state。 27 led_state = ~key_state; 28 //将 led_state 中的值输出到 Led 上进行显示。 29 IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE, led_state); 30 } 31 32 return(0); 33 } (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Key.sof 下载至我们的 A4 开发板,Qsys_ Key.sof 下载完成后, 我们还需要在 Eclipse 软件中将 Qsys_ Key.elf 文件下载至我们的 A4 开发板,Qsys_ Key.elf 下 载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,我们可以看到 A4 开发板上的 Led 全部熄灭了,当我们按下 KEY1 时,D1 就会被点亮,如图 4.6 所示。 图 4.6 按键实验的板级调试图 从该图中可以看出,我们的按键实验程序是正确的,我们在 A4 开发板上实现了按下不同的 按键,点亮相对应的 Led 功能。 http://www.fpga.gs/ 130 FPGA 软核演练篇 §4 4.1.7 PIO IP 核的应用实例——中断 (1) 实验目的 介绍完了 PIO 按键实验,接下来我们再来看看 PIO 中断实验,该实验主要是利用 PIO IP 核 内部中断来实现控制 LED 灯亮灭。中断信号由按键提供,产生中断后,在中断事件中将读取按 键的值并将值输出到 LED 上进行显示。在这个实验中,我们将学习到 PIO IP 核中断在 Qsys 软 件中是如何进行配置,以及中断服务程序的编写、注册和调试方法。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.7 所示。 图 4.7 中断实验的硬件框架图 从该图中我们可以看出,我们 PIO 中断实验与我们 PIO 按键实验的硬件框架结构是一样的, 没有添加任何组件,唯一不同的是,我们改动了 key_pio 的配置,我们为 key_pio 添加了中断选 项,它的配置如图 4.8 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 131 图 4.8 中断实验的 PIO 配置页面 从该图中可以看出,我们选择的是边沿上升沿触发或边沿下降沿触发中断。这里我们需要注 意的是,我们还选择了 Enalbe bit-clearing for edge capture register 这个选项,如果大家还记 得我们之前讲过的内容,那么大家肯定知道这个选项是用来干什么的,如果我们选中了该选项,, 那么对于 edge capture 就应该是写 1 清中断;如果我们没有选择 enable bit-clearing for edge capture register,则是写任意数清中断。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.3 所示。 代码 4.3 Qsys_Key_Interrupt.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Key_Interrupt.c 3 //-- 描述 : 按下不同的按键,点亮相对应的 Led。 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "system.h" //系统头文件 8 #include "altera_avalon_pio_regs.h" //pio 寄存器头文件 9 #include "sys/alt_irq.h" //中断头文件 10 #include "unistd.h" //延迟头文件 11 12 void IRQ_Init(); 13 void IRQ_Key_Interrupts(); //中断初始化函数 //中断服务子程序 14 http://www.fpga.gs/ 132 FPGA 软核演练篇 §4 15 //--------------------------------------------------------------------------- 16 //-- 名称 : main() 17 //-- 功能 : 程序入口 18 //-- 输入参数 : 无 19 //-- 输出参数 : 无 20 //--------------------------------------------------------------------------- 21 int main() 22 { 23 IRQ_Init(); // 初始化 PIO 中断 24 IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE, 0xFF); //初始化 LED 全灭 25 26 while(1) 27 { 28 usleep(1000); 29 } 30 } 31 32 //--------------------------------------------------------------------------- 33 //-- 名称 : IRQ_Init() 34 //-- 功能 : 中断初始化函数 35 //-- 输入参数 : 无 36 //-- 输出参数 : 无 37 //--------------------------------------------------------------------------- 38 void IRQ_Init() 39 { 40 IOWR_ALTERA_AVALON_PIO_IRQ_MASK(KEY_PIO_BASE, 0xff); // 使能中断 41 IOWR_ALTERA_AVALON_PIO_EDGE_CAP(KEY_PIO_BASE, 0xff); // 清中断边沿捕获寄存器 42 // 注册 ISR 43 alt_ic_isr_register( 44 KEY_PIO_IRQ_INTERRUPT_CONTROLLER_ID,// 中断控制器标号,从 system.h 复制 45 KEY_PIO_IRQ, // 硬件中断号,从 system.h 复制 46 IRQ_Key_Interrupts, // 中断服务子函数 47 0x0, // 指向与设备驱动实例相关的数据结构体 48 0x0); // flags,保留未用 49 } 50 51 //--------------------------------------------------------------------------- 52 //-- 名称 : IRQ_Key_Interrupts() 53 //-- 功能 : 中断服务子程序 54 //-- 输入参数 : 无 55 //-- 输出参数 : 无 57 //--------------------------------------------------------------------------- 58 void IRQ_Key_Interrupts() 59 { Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 133 60 alt_u32 key_state,led_state; //Key 和 Led 缓存变量 61 //读取按键的值,并赋值给 key_state。 62 key_state = IORD_ALTERA_AVALON_PIO_DATA(KEY_PIO_BASE); 63 //注意:Key 悬空为低电平,按下为高电平。Led 低电平亮,高电平灭。 64 //为了保证按下不同的按键点亮相对应 Led,因此,我们将按键的值取反后,再赋值给 led_state。 65 led_state = ~key_state; 66 //将 led_state 中的值输出到 Led 上进行显示。 67 IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE, led_state); 68 IOWR_ALTERA_AVALON_PIO_EDGE_CAP(KEY_PIO_BASE, 0xff); //清中断边沿捕获寄存器 69 } 下面我们就来给大家讲解一下该程序中的关键点。通过前面的学习我们知道,当中断来了以 后,我们的程序会跳转到异常处理地址,这时,Nios II 处理器开始执行一段硬件抽象层(HAL) 插入的代码,并判断中断源和中断优先级,然后再跳转到我们编写的中断服务子程序(ISR)中, 整个过程不需要我们任何操作,我们只需要将中断服务子程序的信息告知给硬件抽象层就可以 了,具体如何将中断服务子程序的信息告知给硬件抽象层,我们需要完成以下两个步骤:  参照 void isr_name(void *context,alt_u32 id)函数原型编写中断服务子程序。其中 isr_name 是用户给 ISR 取的函数名;void *context 是指向传递给 ISR 的信息的全局 变量指针(通常是寄存器信息);id 是硬件中断号,在 system.h 中声明,比如我们这 里的 KEY_PIO_IRQ。  调 用 alt_irq_register(alt_u32 id,void *context,void (*isr)(void *,alt_u32)) 或 者 是 alt_ic_isr_register(alt_u32 ic_id, alt_u32 irq, alt_isr_func isr, void *isr_context, void *flags);函数向硬件抽象层登记中断服务子程序。其中 alt_u32 id 和 alt_u32 irq 是硬件 中断号;void *context 和 void *isr_context 是指向传递给 ISR 的信息的全局变量指针 (通常是寄存器信息);void (*isr)(void *,alt_u32)和 alt_isr_func isr 是指向 ISR 的函 数指针;alt_u32 ic_id 是中断控制器标号,*flags 保留未用。 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软 件 中 将 Qsys_Key_Interrupt.sof 下 载 至 我 们 的 A4 开 发 板 , Qsys_ Key_Interrupt.sof 下载完成后,我们还需要在 Eclipse 软件中将 Qsys_Key_Interrupt.elf 文件下 载至我们的 A4 开发板,Qsys_Key_Interrupt.elf 下载完成以后,我们的 C 程序将会执行在我们 的 A4 开发板上,我们可以看到 A4 开发板上的 Led 全部熄灭了,当我们按下 KEY3 时,D3 就 会被点亮,如图 4.9 所示。 http://www.fpga.gs/ 134 FPGA 软核演练篇 §4 图 4.9 中断实验的板级调试图 从该图中可以看出,我们的中断实验程序是正确的,我们在 A4 开发板上利用中断实现了按 下不同的按键,点亮相对应的 Led 功能。 §4.2 SDRAM IP核 4.2.1 SDRAM IP 核的综述 介绍完了 PIO IP 核,接下来我们就来看看我们的 SDRAM IP 核,同 PIO IP 核一样,首先 我们介绍的是 SDRAM IP 的综述。学过《数字电路篇》朋友肯定都知道,SDRAM 常用于需要 大量易失性存储器且成本要求高的应用系统。虽然 SDRAM 比较便宜,但是 SDRAM 应用需要 实现刷新操作、行列管理、不同延迟和命令序列等逻辑。SDRAM IP 核提供了连接一个或多个 SDRAM 芯片的接口,并处理所有 SDRAM 协议要求。有了 SDRAM IP 核,在 Nios II 系统中使 用 SDRAM 就像使用 SRAM 一样简单。为了让读者能够更易理解,以及有更直观的感受,下面 我们就给出 SDRAM IP 核连接到外部 SDRAM 芯片的一个结构框图,如图 4.10 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 135 时钟源 时钟速 率调整 时钟 控制器 地址、数据 控制 从机接口 到片内 逻辑 等待请求 从机 接口 读数据有效 控制 接口到 逻辑 引脚 图 4.10 SDRAM IP 核连接到外部 SDRAM 芯片的结构框图 我们可以从上面的这个结构框图中看出,这个 SDRAM 控制器,它其实是在我们 Altera FPGA 中产生的,它本身包含有接口引脚、控制逻辑、以及 Avalon 从机接口。接口引脚是用来 连接我们外部 SDRAM 芯片管脚的,这些接口引脚通过 Altera FPGA 上的 I/O 引脚连接到 SDRAM 芯片管脚上。控制逻辑就是用来实现 SDRAM 操作的,比如,SDRAM 初始化,自刷 新,突发读写等。这些全都是控制逻辑来完成的,控制逻辑不需要我们编写,当我们生成 SDRAM IP 核之后,它会自动生成。Avalon 从机接口是用来连接我们的 CPU 的,Avalon 从机接口是 SDRAM IP 核中仅为用户可见的部分。从控制器端口提供一个如 SDRAM 芯片一样大的平滑、 线性存储器空间。当访问从控制器端口时,SDRAM 协议的细节完全透明。Avalon 接口作为一 个简单的存储器接口操作,没有存储器映射的配置寄存器。这里我们需要注意的是:SDRAM 芯 片必须和 Avalone 接口一样以相同的时钟来驱动。我们可以看到图中的片内锁相环(PLL),它 就是用来调整 SDRAM 控制器与 SDRAM 芯片之间的时钟相位差。在较低的时钟频率下,可能 不需要 PLL。在较高的时钟频率下,当信号在引脚上有效时,需要 PLL 来调整 SDRAM 时钟。 PLL 不是包括在 SDRAM 控制器内。如果需要 PLL,设计者必须在生成 Qsys 系统模块以外手 动添加 PLL。Altera FPGA 和 SDRAM 芯片的不同组合将要求不同的 PLL 设置。 还有一点我们需要说明的是 fmax 性能取决于整个硬件设计。Qsys 系统模块的主控制器时 钟驱动 SDRAM 控制器与 SDRAM 芯片。因此,整个系统模块的性能决定 SDRAM 控制器的性 能。例如,为了实现 100MHz 的 fmax 性能,系统模块必须设计为 100MHz 时钟率,且 Quartus II 软件的时序分析必须检验硬件设计是否能够进行 100MHz 的操作。说完了 SDRAM 的综述之 后下面我们就总结给出 SDRAM IP 核的功能特性: (1) SDRAM IP 核具有不同数据宽度(8、16、32 或 64 位)、不同内存容量和多片选择等 设置。 (2) SDRAM IP 核可以全面支持符合 PC100 标准的 SDRAM 芯片。 http://www.fpga.gs/ 136 FPGA 软核演练篇 §4 (3) SDRAM IP 核可选择与其他的片外 Avalon 三态器件共用地址和数据总线,该特性在 I/O 引脚资源紧张的系统中很有用。 4.2.2 SDRAM IP 核的配置选项 我们可以在 Qsys 中使用 SDRAM IP 核的配置向导来指定硬件特性和仿真特性。SDRAM IP 核配置向导有两个选项卡:Memory Profile 和 Timing,如图 4.11 所示。 图 4.11 SDRAM 的配置选项 为了使用方便,Presets 下拉列表提供几个预定义的 SDRAM 配置。如果实际使用的 SDRAM 芯片型号与列表中的一致,可直接选用而不用设置其他选项。选择不同的预配置,SDRAM IP 核 将自动改变 Memory Profile 和 Timing 选项卡上的值来匹配指定的配置。如果实际使用的 SDRAM 芯片与列表中的不相同,则需要设计者根据 SDRAM 芯片数据手册的参数来设置 Memory Profile 和 Timing 标签上的值,改变任何选项卡上的配置设置转变 Preset 值为 custom。 当然我们也可以将我们配置好的 SDRAM 参数添加到预定义的 SDRAM 配置,在今后的使用过 程中我们就直接选择我们添加的预定义的 SDRAM 配置。接下来我们就来简单的介绍一下 Memory Profile 和 Timing。 (1) Memory Profile 选项卡 Memory Profile 选项卡允许设计者指定 SDRAM 的结构,例如地址和数据线宽度、片选信 号的数据和区的数目等。表 4.4 列出 Memory Profile 选项卡可用的设置。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 137 设置 数据宽度 片 结构 选 设置 区 地址 行 宽度 设计 列 控制器共用 dq/dqm/addr I/O 引脚 包括系统测 试台的功能 存储器模块 允许值 8,16,32,64 1,2,4,8 2、4 11,12,13,14 ≥8 且小于 行的值 表 4.4 Memory Profile 选项卡设置 默认值 描述 SDRAM 数据总线宽度。该值确定 dq 总线(数据)和 dqm 32 总线(字节使能)的宽度。具体数值请查阅 SDRAM 数据手 册。 独立芯片的数目在 SDRAM 子系统中选择。通过使用多个片 1 选信号,SDRAM 控制器可组合多个 SDRAM 芯片为一个存 储器子系统。 区的数目。该值确定连接到 SDRAM 的 ba 总线(区地址) 4 宽度。具体数值请查阅 SDRAM 数据手册。 行地址位的数目。该值确定 addr 总线的宽度。具体数值请查 12 阅 SDRAM 数据手册。 列地址位的数目。例如,SDRAM 排列为 4096 行、512(29) 8 列,所以列的值为 9。具体数值请查阅 SDRAM 数据手册。 是、否 当设为 No 时,所有引脚都专用与 SDRAM 芯片。当设为 Yes 否 时,addr、dq 和 dqm 引脚在系统内可与三态桥共享 是、否 当打开选项时,Qsys 创建 SDRAM 芯片的功能仿真模型, 是 该默认的存储器模型加速创建的过程和校验使用 SDRAM 控 制器的系统。 通过 Memory Profile 选项卡上的设置,消息框显示 SDRAM 期望的内存容量,以兆字节、 兆位以及可寻址的字数位单位。将这些期望值与选择的 SDRAM 的实际大小相比较可以检验设 置是否正确。这里我们需要注意的是,我们在 SDRAM 的配置选项图中是看不到共用引脚这个 选项的,这个共用引脚在老的 SOPC 版本是有的,不过在我们新的 Qsys 版本中给去掉了。这 个功能我们只做了解即可。说完了 Memory Profile 选项卡,接下来我们再来看一看 Timing 选项 卡。 (2) Timing 选项卡 Timing 选项卡允许设计者设置 SDRAM 芯片的时序规范。正确值在 SDRAM 芯片数据手册 中提供。表 4.5 列出 Timing 选项卡上可用的设置。 设置 CAS 等待时间 初始化刷新周期 每隔一段时间执行 一个刷新命令 在初始化前、上电 后延时 刷新命令(t_rfc)的 持续时间 允许值 1,2,3 1~8 — — — 表 4.5 Timing 选项卡设置 默认值 描述 3 从读命令到数据输出的等待时间(以时钟周期计算) 复位后,该值指定 SDRAM 控制器将执行多少个刷新周期作 2 为初始化序列的一部分 15.625 us 该值指定 SDRAM 控制器多久刷新一次 SDRAM。典型的 SDRAM 每 64ms 需 要 4096 个 刷 新 命 令 , 通 过 每 64ms/4096=15.625μs 执行一个刷新命令来符合这个要求 100us 从稳定的时钟和电源到 SDRAM 初始化的延时 70 ns 自动刷新周期 http://www.fpga.gs/ 138 FPGA 软核演练篇 §4 预充电命令(t_rp) — 的持续时间 Active 到 Read 或 — Write 延时(t_rcd) 访问时间(t_ac) — 写恢复时间(t_wr, — 无自动预充电) 20 ns 预充电命令周期 20 ns ACTIVE 到 READ 或 WRITE 延时 5.5 ns 14 ns 时钟边沿的访问时间。该值由 CAS 的等待时间决定 如果执行了明确的预充电命令,写恢复。该 SDRAM 控制器 总是执行明确的预充电命令 无论用户输入的精确时序值如何,每个参数实现的实际时序将为 Avalon 时钟的整数倍。对 于每隔一段时间执行一个刷新命令(Issue One Refresh Command Every)的参数,实际时序 将不超出目标值。对于其他所有参数,实际时序将大于或等于目标值。一般情况下,我们使用默 认值就可以了。 4.2.3 SDRAM IP 核的软件编程 当通过 Avalon 接口访问时,SDRAM IP 核操作起来像简单的 SRAM 存储器,没有可配置 的软件设置,没有存储器映射的寄存器。处理器访问 SDRAM IP 核不需要软件驱动程序。 4.2.4 SDRAM IP 核的应用实例 (1) 实验目的 介绍完了 SDRAM IP 核的软件编程,接下来我们再来看看 SDRAM IP 核的应用实例,该实 验主要是将应用程序下载到 SDRAM 中运行,并且将递增的数据通过 LED 进行显示,通过 JTAG_UART 进行输出。在这个实验中,我们将学习到 SDRAM IP 核在 Qsys 软件中是如何进 行配置的,以及我们如何利用 C 语言程序来访问 SDRAM 存储器。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.12 所示。 图 4.12 SDRAM 实验的硬件框架图 从该图中我们可以看出,我们将之前的 onchip_memory IP 核修改成了我们的 SDRAM IP 核,对于 Nios、Jtag_Uart 和 PIO 这三个 IP 核,它们是我们的老朋友了,我们相信大家都能够 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 139 理解了,这里我们就不再对它们进行介绍了,我们主要来介绍一下新添加的 SDRAM IP 核,它 的配置如图 4.13 所示。 图 4.13 SDRAM IP 核的配置页面 从该图中可以看出,我们将 SDRAM 配置成了 16 位,行是 13,列是 9。关于 SDRAM 的配 置,我们需要参考 SDRAM 的数据手册,不同型号的 SDRAM,它的配置也是不同的。由于我们 这里的 SDRAM 型号是 MT48LC16M16A2,所以我们参考了 MT48LC16M16A2 的 datasheet。 这里需要大家注意的是,由于我们将 MT48LC16M16A2 型号的 SDRAM 配置添加到了 Library 中,所以我们只需要选择这个配置即可自动完成所有设置。为了方便大家以后的使用,下面我们 就简单为大家讲解一下如何将自己的 SDRAM 配置添加至 Library 中。当我们配置好 SDRAM 以 后,我们就可以找到 Library 窗口中的【New】按钮并点击,弹出如图 4.14 所示页面。 http://www.fpga.gs/ 140 FPGA 软核演练篇 §4 图 4.14 将 SDRAM 配置添加至 Library 中 在该页面中,我们将 Preset name 和 Preset description 填写好以后,我们就可以点击【Save】 按钮,弹出如图 4.15 所示提示窗口。 图 4.15 路径添加提示窗口 在该提示窗口中我们选择否,这时我们就可以在 Library 中看到我们添加的 SDRAM 配置 了。这时,我们打开该工程文件夹可以看到多出了一个 Micron_MT48LC16M16A2_module.qprs 文件,这个文件就是我们添加的 SDRAM 配置信息,为了便于统一管理,这里我们建议大家将 该文件中的内容复制到 C:\altera\13.1\ip\altera\sopc_builder_ip\altera_avalon_new_sdram_co ntroller\altera_avalon_new_sdram_controller.qprs 文件中,当我们复制完毕以后,我们就可以 将 Micron_MT48LC16M16A2_module.qprs 文件删掉了,然后我们重新打开 Qsys 软件,这时, 我们就可以在 SDRAM 的 Library 中看到我们添加的 Micron_MT48LC16M16A2_module。最后 我们再补充说明一点,SDRAM 为动态存储器对时序要求比较高,由于 FPGA 内部有延迟,所 以 PLL 输出 100Mhz 时钟频率给 SDRAM_SCLK 时,PLL 时钟需要设置相位偏移,相位偏移我 们通常设置为-63deg。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.4 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 141 代码 4.4 Qsys_Sdram.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Sdram.c 3 //-- 描述 : 打印 00-FF,并将 00-FF 的值显示到 Led 上 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include //标准输入输出头文件 8 #include "system.h" //系统头文件 9 #include "alt_types.h" //数据类型头文件 10 #include "unistd.h" //延迟函数头文件 11 #include "altera_avalon_pio_regs.h"//pio 寄存器头文件 12 13 //--------------------------------------------------------------------------- 14 //-- 名称 : main() 15 //-- 功能 : 程序入口 16 //-- 输入参数 : 无 17 //-- 输出参数 : 无 18 //--------------------------------------------------------------------------- 19 int main() 20 { 21 alt_u8 count = 0; //声明变量,并初始化为 0 22 23 while(1) 24 { 25 usleep(100000);//等待 100ms 26 //将 conut 的值打印出来,%x 表示输出十六进制,02 表示不足两位,前面补 0 输出 27 printf("%02x,", count); 28 count++; //等价于 count = count + 1; 29 //将 count 变量的值赋值给 Led 并显示 30 IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE, count); 31 } 32 33 return 0; 34 } (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Sdram.sof 下载至我们的 A4 开发板,Qsys_Sdram.sof 下载完成 后 , 我 们 还 需 要 在 Eclipse 软 件 中 将 Qsys_Sdram.elf 文 件 下 载 至 我 们 的 A4 开 发 板 , Qsys_Sdram.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,我们可以看到 A4 开发板上的 Led 开始有规律的进行闪烁,与此同时,我们还可以在 Eclipse 软件的控制台中 看到我们的打印信息,如图 4.16 所示。 http://www.fpga.gs/ 142 FPGA 软核演练篇 §4 §4.3 EPCS IP核 图 4.16 SDRAM 实验的板级调试图 4.3.1 EPCS IP 核的综述 介绍完了我们的 SDRAM IP 核,接下来我们就来介绍一下我们的 EPCS IP 核。Altera 公司 的 FPGA 芯片大多采用 EPCS 系列存储器作为配置数据的存储空间,带 Avalon 接口的 EPCS IP 核允许 Nios II 系统访问 Altera EPCS 串行配置器件。Altera 提供集成到 Nios II 硬件抽象层 (HAL)系统库的驱动程序,允许用户使用 HAL 应用程序接口(API)来读取和编写 EPCS 器 件。通过 EPCS IP 核,Nios II 系统可以执行以下操作: (1) 在 EPCS 器件中存储程序代码。EPCS 控制器自带 Boot-Loader 代码,因此 Nios II 系 统允许用户在 EPCS 器件中存储程序代码。 (2) 存储非易失性数据,例如串行号、NIC 号和其他需要长久储存的数据。 (3) 管理 FPGA 配置数据。EPCS 可存储 FPGA 的配置数据,并在上电时自动完成对 FPGA 的配置。具有网络接口的嵌入式系统可从网络上接收新的 FPGA 配置数据,并通过 EPCS 控制器将新的配置数据下载到 EPCS 串行配置器件中。 为了让读者能够更易理解,以及有更直观的感受,我们这里同 SDRAM IP 核一样,也给出 EPCS IP 核的结构框架图。如图 4.17 所示。 Altera FPGA EPCS配置器件 通用存储 空间 配置存储 空间 EPCS控制器 Boot-Loader ROM Avalon 总线 Nios II CPU 片内外设 图 4.17 EPCS IP 核的结构框图 我们从 EPCS IP 核结构框图中可以看出,EPCS 器件的存储器被分成了两个独立的区域, 一个是 FPGA 配置存储器,它主要是用于存储 FPGA 配置数据的,另一个是通用存储器,如果 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 143 FPGA 配置数据没有填满整个 EPCS 器件,那么剩下空间可以存储用户非易失性数据。 EPCS 控制器包含一个片上用于存储 Bootloader 程序的 ROM 存储器,当 EPCS 控制器与 Cyclone 和 Cyclone II 器件一起使用时,EPCS 控制器需要 512 字节的 Bootloader 程序的 ROM 存储器。当 EPCS 控制器与 Cyclone III、Cyclone IV、Stratix II 等器件一起使用时,EPCS 控制 器需要 1024 字节(也就是 1KB)的 Bootloader 程序的 ROM 存储器。Nios II 处理器可以设置 成 EPCS 控制器开始引导,在这种情况下,复位 CPU 后首先执行引导 EPCS 控制器的 Bootloader ROM 中的代码,把存储在 EPCS 中通用内存区域的数据复制到指定的 RAM 存储 器,然后把系统控制权转移给存储在 RAM 中的程序。实现这些操作的程序代码无需用户编写, 由 Nios II SBT for Eclipse 软件自动生成。Nios II SBT for Eclipse 软件提供编译产生用于存储在 EPCS 中文件的程序代码,也提供了编程 EPCS 的工具。 EPCS 控制器有一个 Avalon-MM 从接口,这个接口提供访问 Bootloader 代码和控制寄存 器的能力。表 4.6 给出了 EPCS IP 核的寄存器描述。 偏移地址 0x00 … 0xff 0x100 0x101 0x102 0x103 0x104 0x105 0x106 表 4.6 EPCS IP 核的寄存器描述 寄存器名称 操作 31 … 0 Bootloader 代码存储器 读 Bootloader Code 读数据 写数据 状态寄存器 控制寄存器 保留 从设备使能 数据包结束 读 写 读/写 读/写 — 读/写 读/写 未知 未知 未知 未知 未知 未知 未知 从 EPCS IP 核的寄存器描述表格中我们可以看出: (1) 偏移地址 0x00-0xff:这 256 个字是专用的 Bootloader 代码,当 Nios II 系统的复位地 址指向 EPCS 控制器时,处理器便会从 EPCS 控制器基地址开始的 256 个字存储空间 读取 Bootloader 代码,EPCS 控制器包含有一个中断信号,当把程序代码全部加载到 指定的 RAM 中时产生中断请求。 (2) 偏移地址 0x100-0x106:这 7 个字节是 EPCS 控制和数据寄存器,由于 Altera 没有公 布 EPCS IP 核的寄存器用法,要访问 EPCS 器件,用户必须使用 Altera 提供的 HAL 驱动程序,所以本节将不对 EPCS IP 核的寄存器文件进行讲解。 4.3.2 EPCS IP 核的配置选项 说完了 EPCS IP 核的综述,接下来我们就来看看 EPCS IP 核在 Qsys 中的配置选项,EPCS IP 核的配置选项如图 4.18 所示。 http://www.fpga.gs/ 144 FPGA 软核演练篇 §4 图 4.18 EPCS IP 核的配置选项 在该图中我们可以看到,其实我们不需要任何设置,我们只要将 EPCS IP 核添加到 Qsys 软件中就可以使用了。 4.3.3 EPCS IP 核的软件编程 EPCS IP 核提供了定义硬件的底层接口头文件和 HAL 驱动驱动程序文件,文件如下所示: (1) altera_avalon_epcs_flash_controller.h 和 altera_avalon_epcs_flash_controller.c:定 义集成到 HAL 系统库所需的驱动程序的头文件和源文件。 (2) epcs_commands.h 和 epcs_commands.c:通过直接控制 EPCS 设备来进行读/写操 作的头文件和源文件。 我们可以通过阅读上述文件来熟悉 EPCS IP 核的软件访问方法,我们这里建议大家不要修 改该文件。 4.3.4 EPCS IP 核的应用实例 (1) 实验目的 介绍完了 EPCS IP 核的软件编程,接下来我们再来看看 EPCS IP 核的应用实例,通过前面 的学习,我们知道可以通过 Nios II Flash Programmer 对 EPCS 进行操作,在该实验中我们不 再重复讲解这种方法了,我们将介绍另外的一种方法,就是通过调用硬件抽象层提供的 API 函 数对 EPCS 进行读写操作。在这个实验中,我们将学习到 EPCS IP 核在 Qsys 软件中是如何进 行配置的,以及我们如何利用 API 函数来访问 EPCS 存储器。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 145 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.19 所示。 图 4.19 EPCS 实验的硬件框架图 从该图中我们可以看出,我们的 EPCS 实验是在 SDRAM 实验的基础上多添加了一个 EPCS IP 核,该 IP 的配置我们使用的是默认设置,如图 4.20 所示。 图 4.20 EPCS IP 核的配置页面 由于我们添加了 EPCS IP 核,所以我们需要在 Nios II 处理器中将 Reset Vector 设置为 epcs_flash。 http://www.fpga.gs/ 146 FPGA 软核演练篇 §4 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.5 所示。 代码 4.5 Qsys_Epcs.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Epcs.c 3 //-- 描述 : 将 0-255 写入到 EPCS 中,再从 EPCS 中将 0-255 读出。 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include //标准输入与输出头文件 8 #include //延迟函数头文件 9 #include "system.h" //系统头文件 10 #include "alt_types.h" //数据类型头文件 11 #include "sys/alt_flash.h" //flash 相关函数头文件 12 #include "altera_avalon_epcs_flash_controller.h"//EPCS 相关函数头文件 13 14 //--------------------------------------------------------------------------- 15 //-- 名称 : main() 16 //-- 功能 : 程序入口 17 //-- 输入参数 : 无 18 //-- 输出参数 : 无 19 //--------------------------------------------------------------------------- 20 int main() 21 { 22 flash_region* regions; 23 alt_flash_fd* my_epcs; 24 int number_of_regions; 25 int error; 26 int i; 27 alt_u8 data[256]; 28 29 printf("初次给 data 数组赋值 "); 30 for(i=0;i<=255;i++) 31 { 32 data[i] = i; //初始化 data 数组 33 printf("%d,",data[i]); //将 data 数组中的值打印到控制台 34 } 35 printf("\n"); //换行 36 //打开 EPCS_FLASH 器件,获取 EPCS_FLASH 器件句柄 37 if((my_epcs = alt_flash_open_dev(EPCS_FLASH_NAME))== NULL) 38 { 39 printf("Can't open EPCS flash device"); //没有打开 EPCS_FLASH 器件 40 } Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 147 41 else 42 { 43 //成功打开 EPCS_FLASH 器件,并获取 EPCS_FLASH 器件信息 44 error = alt_epcs_flash_get_info(my_epcs, ®ions, &number_of_regions); 45 } 46 47 if(!error) //成功获得 EPCS_FLASH 器件信息 48 { 49 //擦除第 64 块区域 50 alt_epcs_flash_erase_block(my_epcs, regions->offset+0xFC0000); 51 //将 data 数组中的 256 个字节写入到第 64 块区域内 52 alt_epcs_flash_write(my_epcs, regions->offset+0xFC0000, data, 256); 53 printf("将 data 数组中的值写入 EPCS 中,然后我们在将 data 中的值清零"); 54 55 for(i=0;i<256;i++) 56 { 57 data[i] = 0; //清除 data 数组中的值 58 printf("%d,",data[i]); //将 data 数组中的值打印到控制台 59 } 60 printf("\n"); //换行 61 //将第 64 块区域内的 256 个字节读入到 data 数组中 62 alt_epcs_flash_read(my_epcs, regions->offset+0xFC0000, data, 256); 63 printf("从 EPCS 中将数据读出,并保存至 data 数组中,然后在将 data 数组中的值输出显示"); 64 65 for(i=0;i<256;i++) 66 { 67 printf("%d,",data[i]); //将 data 数组中的值打印到控制台 68 } 69 printf("\n"); 70 } 71 else 72 { 73 printf("Can't get EPCS flash device info"); //没有获得 EPCS_FLASH 信息 74 } 75 76 alt_flash_close_dev(my_epcs); //关闭 EPCS_FLASH 器件 77 78 while(1) 79 { 80 usleep(10000); 81 } 82 } 83 下面我们就来给大家讲解一下该程序中的关键点。首先我们给出 EPCS Flash 各个型号的 http://www.fpga.gs/ 148 FPGA 软核演练篇 §4 参数表,如表 4.7 所示。 Details Bytes bits) Number of sectors Bytes per sector Pages per sector Total number of pages Bytes per page 表 4.7 EPCS 型号参数表 EPCS128 EPCS64 EPCS16 16,777,216byt 8,388,608by 2,097,152byt es(128Mbits) tes(64Mbits) es(16Mbits) 64 128 32 262,144bytes (2Mbits) 65,563bytes (512Kbits) 65,563bytes (512Kbits) 1,024 256 256 EPCS4 524,288byt es(4Mbits) 8 65,563byte s (512Kbits) 256 EPCS1 131,072byt es(1Mbits) 4 32,768byte s (256Kbits) 128 65,536 32,768 8,192 2,048 512 256bytes 256 bytes 256 bytes 256 bytes 256 bytes 这里我们用到的器件是 EPCS128,我们可以从表中得出 EPCS128 有 64 个扇区数,每个 扇区有 2Mbits 大小,由于上面的程序用到了第 64 区,这里我们给出第 64 区的开始地址和结束 地址,它们分别是 H’FC0000 和 H’FFFFFF。关于更多的详细扇区地址,大家可以参考 EPCS128 的使用说明手册。 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Epcs.sof 下载至我们的 A4 开发板,Qsys_Epcs.sof 下载完成后, 我们还需要在 Eclipse 软件中将 Qsys_Epcs.elf 文件下载至我们的 A4 开发板,Qsys_Epcs.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可以在 Eclipse 软件 的控制台中看到我们的打印信息,如图 4.21 所示。 图 4.21 EPCS 实验的板级调试图 从该图中可以看出,我们的 EPCS 实验程序是正确的,我们的程序首先给我们的 data 数组 进行了赋值,然后又将 data 数组中的数值打印了出来,因此我们可以在控制台中看到 0~255 这 些数字,我们打印 data 数值这一步骤主要是来验证我们的 data 数组中是否已经写入了数据,当 我们确定了我们的 data 中已经有了数据以后,我们就通过 alt_epcs_flash_write 函数将 data 数 组中的数值写入到了 EPCS 中的第 64 块区域内,写完了以后,我们再将 data 数组中的数值清 零并打印出来,这一步骤主要是来验证我们的 data 数组中是否还存有数据。当我们确定了我们 的 data 中没有数据以后,我们就通过 alt_epcs_flash_read 函数将 EPCS 中的第 64 块区域内 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 149 的数据读取到我们的 data 数组中,读取完成以后,我们再将 data 数组中的数值打印输出,至此 我们就完成了整个实验。 §4.4 Interval Timer IP核 4.4.1 Timer IP 核的综述 介绍完了我们的 EPCS IP 核,接下来我们就来介绍一下我们的 Interval Timer IP 核。定时 器(Timer)可以说是一个非常重要的常用外围设备。它可以作为系统的周期性时钟源(Tick); 也可以作为一个计时器,测定事件发生的时间;还可以对外输出周期性脉冲或作为一条监管系统 正常运行的“看门狗”(Watchdog)。集成在 Qsys 系统中的 Timer IP 核主要具有以下特性: (1) 32 位和 64 位计数器 (2) 控制定时器的启动、停止和复位; (3) 两种计数模式:单次减 1 和连续减 1 计数模式; (4) 减法周期寄存器; (5) 定时器到达 0 时产生中断请求(IRQ); (6) 可选择设定为看门狗定时器,当为看门狗时,定时器计算到达 0 时复位系统; (7) 可选择输出周期性脉冲,在定时器计算到达 0 时输出脉冲; (8) 兼容 16 位和 32 位处理器。 接下来我们给出 Timer IP 核的结构框图,如图 4.22 所示。 Avalon 总线从机 接口到内 核逻辑 数据总线 地址总线 IRQ Reset (看门狗) 寄存器文件 Status Control Periodh Periodl Snaph Snapl 控制 逻辑 计数器 Timeout pulse 图 4.22 Timer IP 核的结构框图 由 Timer IP 核的结构框图我们可以看出,Timer IP 核是由 6 个 16 位寄存器和 1 个控制逻辑 构成。首先我们介绍的是 6 个寄存器,这 6 个寄存器分别是是状态寄存器、控制寄存器、周期 http://www.fpga.gs/ 150 FPGA 软核演练篇 §4 寄存器高、周期寄存器低、捕获寄存器高和捕获寄存器低。Nios II 处理器可以通过 Avalon-MM 总线接口访问这些寄存器,这些寄存器设计为 16 位宽度,可以与 16 位和 32 位处理器兼容,这 些内部寄存器可以由定时器的不同而配置不同的模式,比如,当定时器配置为固定定时周期时, 图中的周期寄存器就不存在了。介绍完了 6 个寄存器,接下来我们再来看一看控制逻辑。我们可 以从图中看到,这个控制逻辑可以输出三种不同的信号,一个是中断信号、一个是复位信号,还 有一个是超时脉冲信号。 4.4.2 Timer IP 核的寄存器描述 介绍完了 Timer IP 核的综述,接下来我们再来说一说 Timer IP 核的寄存器描述。Timer IP 核的寄存器描述如表 4.8 所示。 表 4.8 Timer IP 核的寄存器描述 位描述 偏移量 名称 操作 15 … 4 3 2 1 0 0 status 读/写 保留 RUN TO 1 control 读/写 保留 STOP START CONT ITO 2 periodl 读/写 周期寄存器低(位 15..0) 3 periodh 读/写 周期寄存器高(位 31..16) 4 snapl 读/写 捕获寄存器低(位 15..0) 5 snaph 读/写 捕获寄存器高(位 31..16) 从 Timer IP 核寄存器的描述表格中我们可以看出,Timer IP 核有 6 个寄存器,接下来我们 就对这 6 个寄存器分别进行一个介绍: (1) 状态寄存器(status) 状态寄存器有 2 个定义位,如表 4.9 所列。 位 名称 0 TO 1 RUN 操作 读/清除 读 表 4.9 状态寄存器位 描述 当内部计数器减到 0 时,TO(Timeout)位被置为 1。一旦发生 Timeout 事件,TO 位保持置位状态直到被主控制器清除。向 TO 位写零即可清除 置位状态 当内部计数器运行时,RUN 位为 1;否则该位为 0。对 RUN 位的写操作 无效 (2) 控制寄存器(control) 控制寄存器有 4 个定义位,如表 4.10 所列。 位 名称 0 ITO 1 CONT 操作 读/写 读/写 表 4.10 控制寄存器位 描述 如果 ITO 位为 1,则使能定时器中断;如果 ITO 位为 0,则屏蔽定时器中 断 CONT(连续方式)位决定内部计数器减到 0 时的操作。如果 CONT 位 为 1,则计数器连续运行,直到用 stop 信号将其停止。如果 CONT 位为 0,则计数器在减到 0 后停止。当计数器减到 0 时,不管 CONT 位的值如 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 151 何,都会自动装载 periodl 和 periodh 寄存器中的 32 位计数值 2 START* 写 写 1 到 START 位启动内部计数器运行(减 1 计数),写 0 到起始位无效 3 STOP* 写 1 到停止位停止内部计数器,写 0 到停止位无效。如果定时器硬件配置 写 为“关闭 Start/Stop control bits”,则写停止位无效 (3) 周期寄存器(periodl & periodh) 周期寄存器是由两个 16 位寄存器组成。两个寄存器一起存储定时器初值,其中 periodl 保存 低 16 位,periodh 保存高 16 位。当以下任意一种情况发生时,保存在 periodl 和 periodh 中的 32 位值会装载到内部计数器中:  对 periodh 或 periodl 寄存器进行写操作;  内部计数器减到 0。 定时器的实际周期是 periodl 和 periodh 寄存器的值加 1,因为内部计数器减到 0 时,也需 要一个时钟周期。写 periodh 或 periodl 停止内部计数器,当硬件配置为“关闭 Start/Stop control bits”时除外。如果 Start/Stop control bits 选项关闭,那么写任一寄存器都不会停止计数器。当 硬件配置为“禁止 Writeable period”时,写 periodh 或 periodl 寄存器会使计数器复位为系统生 成时指定的 Timeout Period。这里需要我们注意的是,当我们使用 32 位的计数器时,是只有 periodh 和 periodl 两个寄存器,当我们使用 64 位的计数器时,我们的周期寄存器会由原来的两 个变成四个,即 period0、period1、period2、period3。 (4) 捕获寄存器(snapl & snaph) 捕获寄存器是由两个 16 位寄存器组成,它们分别对应内部计数器的高 16 位和低 16 位,我 们可以通过对 snapl 或 snaph 寄存器的写操作(写任意数据)来获得 32 位内部计数器的当前 值。当对 snapl 或 snaph 执行写操作时,计数器的当前值会被复制到 snapl 和 snaph 中,不管 计数器是否正在处于运行状态,定时器读出操作都会被执行,并且不会改变内部计数器的运行状 态。这里需要我们注意的是,当我们使用 32 位的计数器时,是只有 snapl 和 snaph 两个寄存 器,当我们使用 64 位的计数器时,我们的捕获寄存器会由原来的两个变成四个,即 snapl0、 snapl1、snapl2、snapl3。 4.4.3 Timer IP 核的配置选项 Timer IP 核的配置选项只有一页,如图 4.23 所示。 http://www.fpga.gs/ 152 FPGA 软核演练篇 §4 图 4.23 Timer IP 核的配置选项图 从 Timer IP 核的配置选项图中我们可以看出,Timer IP 核和 SDRAM IP 核一样,为了使用 方便,Presets 下拉列表中也提供几个预定义的 Timer IP 核的配置。选择不同的预配置,Timer IP 核将自动改变 Registers 和 Output Signals 中的选项来匹配指定的配置。 (1) 完整特性(Full-featured):用于产生一个具有可变周期的完整特性的定时器,可以在 处理器控制下启动和停止该定时器。 (2) 简单的周期中断(Simple periodic interrupt):用于仅要求周期性 IRQ 发生器的系统。 固定周期且不能停止定时器,但可以禁止 IRQ。 (3) 看门狗(Watchdog):用于需要看门狗定时器的系统,以便在系统已经停止响应的情况 下复位系统。 介绍完了预定义中的 Timer IP 核的配置,接下来我们就来简单的介绍一下 Timer IP 核中的 4 种设置。 (1) 定时器超时周期(Timeout Period) 定时器超时周期(Timeout Period)是由用户设置 periodl 和 periodh 寄存器的初始值决定 的,这个初始值根据系统输入时钟频率和 Period 中的设定值计算获得。Timeout Period 设置可 以用单位 us、ms、s 或 clocks(时钟周期数)来指定。  period:用于预设硬件生成后的定时器周期,如果不用软件更改的话,那么定时器将按 照这个周期产生 Timeout 事件。  Units:用于指定单位的,可以设置为 us、ms、s 或 clocks。 (2) 定时计数的大小(Timer counter size) Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 153 Counter Size:可以设置 32 位的计数器,也可以设置成 64 位的计数器,默认值为 32 为的 计数器大小。 (3) 寄存器(Registers)  No Start/Stop control bits:当该选项使能时,主控制器可通过写控制寄存器的 START 和 STOP 位来启动和停止定时器。当禁能时,定时器连续运行。当使能 System reset on timeout(Watchdog)选项时,不管 Start/Stop control bits 选项的状态如何,都有 START 位,也就是说一旦启动看门狗,定时器可永远不会停止。  Fixed period:当使能该选项时,主控制器(如 Nios II 处理器)可通过写 periodl 和 periodh 改变向下计数周期。当禁能时,在指定的 Timeout Period 固定向下计数周期, 且 periodl 和 periodh 寄存器不在硬件中存在。  Readable snapshot——当该选项使能时,主控制器可读当前向下计数的值。当禁能时, 计数器的状态仅通过其他指示器可检测,如状态寄存器或 IRQ 信号。在这种情况下, snapl 和 snaph 寄存器不在硬件中存在,且读这些寄存器产生未定义的值。 (4) 输出信号(Output Signals)  Timeout pulse(1 clock wide):当使能该选项时,定时器内核输出信号 timeout_pulse。 只要定时器计数到达 0,该信号脉冲高电平就持续一个时钟周期。当禁能时, timeout_pulse 信号不存在。  System reset on timeout(Watchdog):当使能该选项时,定时器内核的 Avalon 从控 制器端口包括 resetrequest 信号。只要定时器计数到达 0,该信号脉冲高电平就持续 一个时钟周期(使系统复位)。当该选项使能时,内部定时器在复位时停止,要通过写 控制寄存器的 START 位来启动定时器。当选项禁能时,resetrequest 信号不存在。 4.4.4 Timer IP 核的软件编程 Altera 为 Nios II 处理器用户提供硬件抽象层(HAL)系统库驱动程序,允许用户使用 HAL 应 用程序接口(API)函数来访问定时器内核。 (1) HAL 系统库支持 Altera 提供的驱动程序集成到 Nios II 系统的 HAL 系统库中。用户应该尽可能通过 HAL API 访问定时器,而不是访问定时器寄存器。Altera 公司提供两个 HAL 定时器设备模型:系统时钟 定时器和时间标记定时器。 (2) 系统时钟驱动程序 当配置为系统时钟时,定时器使用 Qsys 中的周期设置,在周期模式下持续运行。系统时钟 服务程序作为该定时器中断服务程序的一部分。Nios II 集成开发环境允许用户在系统库属性设 置里面指定哪个定时器设备用作系统时钟定时器,如图 4.24 所示。 http://www.fpga.gs/ 154 FPGA 软核演练篇 §4 图 4.24 在 Nios II BSP Editor 中指定系统时钟定时器和时间标记定时器 (3) 时间标记驱动程序 如果定时器内核符合下列条件,它可用作时间标记设备:  定时器有一个可写的 snapshot 寄存器;  定时器不用作系统时钟。 这里要注意的是:定时器内核的 HAL 驱动程序不支持定时器内核的看门狗复位特性。用户 需要通过对 periodh 和 periodl 寄存器进行写操作来复位看门狗。 (4) 软件文件 Timer IP 核提供了定义硬件的底层接口头文件和 HAL 驱动驱动程序文件,文件如下所示:  Altera_avalon_timer_regs.h——定义内核的寄存器映射并提供硬件设备访问宏定义。  altera_avalon_timer.h 、 altera_avalon_timer_sc.c 、 altera_avalon_tim-er_ts.c 、 altera_avalon_timer_vars.c——实现了 HAL 系统库的定时器设备驱动程序。 我们可以通过阅读上述文件来熟悉 Timer IP 核的软件访问方法,我们这里建议大家不要修 改该文件。 (5) 中断操作 只要内部计数器减到 0 且控制寄存器的 ITO 位设为 1,定时器内核就会产生 IRQ。用户要用 一下的任意一种方式应答 IRQ:  清楚状态寄存器的 TO 位,等待下一个超时事件的发生;  通过将控制寄存器的 ITO 位清零来禁止中断。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 155 4.4.5 Timer IP 核的应用实例——系统时钟服务 (1) 实验目的 介绍完了 Timer IP 核的软件编程,接下来我们再来看看 Timer IP 核的应用实例,该实验主 要是利用定时器的系统时钟服务产生 1s 的周期性事件,并借此控制 8 个 LED 同时进行间隔 1s 的闪烁。在这个实验中,我们将学习到 Timer IP 核在 Qsys 软件中是如何进行配置,以及定时 器的系统时钟服务功能是如何使用的。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.25 所示。 图 4.25 系统时钟服务实验的硬件框架图 从该图中我们可以看出,我们的系统时钟服务实验是在 SDRAM 实验的基础上多添加了一 个 Timer IP 核,该 IP 的配置如图 4.26 所示。 http://www.fpga.gs/ 156 FPGA 软核演练篇 §4 图 4.26 系统时钟服务实验的 Timer 配置页面 从该图中可以看出,定时器的配置很简单,我们将 Timer IP 核配置为了完整特性(Fullfeatured)。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.6 所示。 代码 4.6 Qsys_Systick.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Systick.c 3 //-- 描述 : 利用系统时钟服务产生 1s 的周期性事件,并借此控制 8 个 LED 同时进行闪烁, 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include //标准输入输出头文件 8 #include "system.h" //系统头文件 9 #include "altera_avalon_pio_regs.h" //pio 寄存器头文件 10 #include "alt_types.h" //数据类型头文件 11 #include "sys/alt_alarm.h" //系统时钟服务头文件 12 13 alt_alarm alarm; 14 alt_u8 led = 0xff; //按调用 API 函数规定定义的变量 //初始化 Led 的状态 15 16 //--------------------------------------------------------------------------- 17 //-- 名称 : my_alarm_callback() 18 //-- 功能 : 系统时钟回调函数,在该函数中实现用户功能 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 157 19 //-- 输入参数 : context,系统传给回调函数的参数 20 //-- 输出参数 : 返回下一次的系统时钟服务的周期值 21 //--------------------------------------------------------------------------- 22 alt_u32 my_alarm_callback (void* context) 23 { 24 if(led == 0xff) //如果 led 等于 0xff,也就是说所有的 LED 都熄灭 25 { 26 led = 0x00; //那么 led 将赋值为 0x00,也就是说点亮所有的 LED 27 } 28 else //如果 led 不等于 0xff 29 { 30 led = 0xff; //那么 led 将赋值为 0xff 31 } 32 IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE,led); //将 led 的值输出 33 return alt_ticks_per_second(); //返回下一次的系统时钟服务的周期值 34 } 35 36 //--------------------------------------------------------------------------- 37 //-- 名称 : main() 38 //-- 功能 : 程序入口 39 //-- 输入参数 : 无 40 //-- 输出参数 : 无 41 //--------------------------------------------------------------------------- 42 int main() 43 { 44 IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE,0xff); //让所有的 LED 熄灭 45 46 printf("test alarm...\n"); 47 printf("alt_ticks_per_second() is %ld",alt_ticks_per_second()); 48 49 //启动系统时钟服务 50 if(alt_alarm_start(&alarm,alt_ticks_per_second(),my_alarm_callback,NULL) < 0) 51 { 52 printf ("No system clock available\n"); 53 } 54 55 while(1); //等待时钟事件发生 56 57 return 0; 58 } 下面我们就来给大家讲解一下该程序中的关键点。不常用操作系统的工程师很少碰到 alarm 这个概念,在它们心目中 alarm 实质上就是一个简单的定时器周期中断事件。在操作系统中, alarm 却是一个常用的概念,比如,当一个进程需要等待某个时间发生又不想永远等待下去时, http://www.fpga.gs/ 158 FPGA 软核演练篇 §4 该进程会设置一个超时(timeout)值。当这个超时值到达时,操作系统会向进程发送一个 alarm(警 告信号),提醒进程不要再等待。在 Nios II 中,alarm 也可以同样理解为是打断正在执行的流程, 提醒系统不要等待的信号。 如何初始化系统时钟服务?按照一般思路,我们先初始化定时器,然后再编写定时器中断服 务程序。但是,在 HAL 层上进行应用程序开发却不必如此,因为与硬件细节相关的初始化过程 都由系统替我们完成了。我们只需要在使用某项服务前,开启该项服务即可进行使用。使用 HAL 提供的系统时钟服务大致分 3 个步骤:  调用 alt_alarm_start()开启系统时钟服务,该函数位于:C:\altera\13.1\nios2eds\compo nents\altera_hal\HAL\src\alt_alarm_start.c 文件中。  按照格式要求编写回调函数,该回调函数将实现用户的功能。  调用 alt_alarm_stop()关闭系统时钟服务。该函数位于:C:\altera\13.1\nios2eds\com ponents\altera_hal\HAL\src\alt_tick.c 文件中。 系统时钟服务 API 函数分析:  开启系统时钟服务函数:alt_alarm_start()。函数原型:int alt_alarm_start(alt_alarm* alarm, alt_u32 nticks, alt_u32(*callback)(void* context), void* context)。函数功能: 启动系统时钟服务。输入参数:alarm:一个指向 alt_alarm 结构体类型的指针变量, 用户需要为每个系统时钟服务创建一个 alt_alarm 类型变量。系统由该变量对系统时钟 服务进行维护。例如,用户需要两个系统时钟服务,则需要定义 alt_alarm alarm_1 和 alarm_2。nticks:指示每隔 nticks 执行一次回调函数。如果定时器预设的中断周期为 1ms;则每个 tick 的时间间隔为 1ms。如果用户希望获得 1s 一次的服务,则 nticks 的 输入值为 1s/1m=1000。Callback:用户回调函数指针。Context:传给回调函数的参数, 一般为寄存器的地址,也可以是其他参数。如果不需要传递参数则此处填入 NULL。  用户回调函数:my_alarm_callback()。函数原型:alt_u32 my_alarm_callback(void* context)。输入参数:context:alt_alarm_start()函数传入的参数,函数名可以自己定 义,但是函数原型一定要按上述格式书写。这里要注意的是,不要在回调函数中实现复 杂的功能,因为回调函数实际是定时器中断服务函数的一部分。  停止系统时钟服务函数 alt_alarm_stop()。函数原型:void alt_alarm_stop(alt_alarm* alarm)。输入参数:alarm:一个指向 alt_alarm 结构体类型的指针变量,用户希望停止 哪一个系统时钟服务,即把该时钟服务对应的 alarm 变量传给 alt_alarm_stop()。 最后我们在补充说明一点,为了达到隐藏信息的目的,Altera 公司提供了一个接口函数 alt_ticks_per_second()供用户获得一个设定 alarm 服务周期为 1s 的变量值。 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Systick.sof 下载至我们的 A4 开发板,Qsys_Systick.sof 下载完 成后,我们还需要在 Eclipse 软件中将 Qsys_Systick.elf 文件下载至我们的 A4 开发板, Qsys_Systick.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可 以在 Eclipse 软件的控制台中看到我们的打印信息,如图 4.27 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 159 图 4.27 系统时钟服务实验的控制台打印信息图 这时,我们不需要任何操作,我们的 A4 开发板上的 8 个 LED 便会全部点亮,如图 4.28 所 示。 图 4.28 系统时钟服务实验的板级调试图 8 个 LED 全亮 1s 后,我们会发现 A4 开发板上的 8 个 LED 便会全部熄灭,然后我们再等 待 1s 后,我们又发现 LED 全部点亮了,1s 后,又熄灭,8 个 LED 依此循环显示。 4.4.6 Timer IP 核的应用实例——标准中断服务 (1) 实验目的 介绍完了系统时钟服务实验,接下来我们再来看看标准中断服务实验,该实验主要是利用定 时器的标准中断服务产生 5s 的周期性事件,并借此控制 8 个 LED 同时进行间隔 5s 的闪烁。在 这个实验中,我们将学习到 Timer IP 核在 Qsys 软件中是如何进行配置,以及定时器的标准中 断服务功能是如何使用的。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.29 所示。 http://www.fpga.gs/ 160 FPGA 软核演练篇 §4 图 4.29 标准中断服务实验的硬件框架图 从该图中可以看出,我们标准中断服务实验与我们系统时钟服务实验的硬件框架结构是一 样的,它们的定时器配置也是一样的。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.7 所示。 代码 4.7 Qsys_Timer_Interrupt.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Timer_Interrupt.c 3 //-- 描述 : 利用定时器标准中断服务产生 5s 的周期性事件,并借此控制 8 个 LED 同时进行闪烁 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "system.h" //系统头文件 8 #include "altera_avalon_timer_regs.h" //定时器头文件 9 #include "altera_avalon_pio_regs.h" //PIO 头文件 10 #include "sys/alt_irq.h" //中断头文件 11 #include "unistd.h" //延迟函数头文件 12 #include //标准的输入输出头文件 13 14 alt_u32 i=0; 15 alt_u32 timer_isr_context; //定义全局变量以储存 isr_context 指针 16 void Timer_Initial(void); //定时器中断初始化 17 void Timer_ISR_Interrupt(void* isr_context , alt_u32 id); //定时器中断服务子程序 18 19 //--------------------------------------------------------------------------- 20 //-- 名称 : Timer_Initial() 21 //-- 功能 : 定时器中断初始化 22 //-- 输入参数 : 无 23 //-- 输出参数 : 无 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 161 24 //--------------------------------------------------------------------------- 25 void Timer_Initial(void) 26 { 27 //改写 timer_isr_context 指针以匹配 alt_irq_register()函数原型 28 void* isr_context_ptr = (void*) &timer_isr_context; 29 //设置 PERIOD 寄存器 30 //PERIODH << 16 | PERIODL = 计数器周期因子 * 系统时钟频率因子 - 1 31 //PERIODH << 16 | PERIODL = 5s*100M - 1 = 499999999 = 0x1DCD_64FF 32 IOWR_ALTERA_AVALON_TIMER_PERIODH(TIMER_BASE, 0x1DCD); 33 IOWR_ALTERA_AVALON_TIMER_PERIODL(TIMER_BASE, 0x64FF); 34 35 //设置 CONTROL 寄存器 36 //位数 |3 |2 |1 |0 | 37 //CONTROL | STOP | START | CONT | ITO | 38 //ITO 1,产生 IRO; 0,不产生 IRQ 39 //CONT 1,计数器连续运行直到 STOP 被置一;0,计数到 0 停止 40 //START 1,计数器开始运行; 0,无影响 41 //STOP 1,计数器停止运行; 0,无影响 42 IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER_BASE, 43 ALTERA_AVALON_TIMER_CONTROL_START_MSK | //START = 1 44 ALTERA_AVALON_TIMER_CONTROL_CONT_MSK | //CONT = 1 45 ALTERA_AVALON_TIMER_CONTROL_ITO_MSK); //ITO = 1 46 //注册定时器中断 47 alt_ic_isr_register( 48 TIMER_IRQ_INTERRUPT_CONTROLLER_ID, //中断控制器标号,从 system.h 复制 49 TIMER_IRQ, //硬件中断号,从 system.h 复制 50 Timer_ISR_Interrupt, 51 isr_context_ptr, 52 0x0); //中断服务子函数 //指向与设备驱动实例相关的数据结构体 //flags,保留未用 53 } 54 55 //--------------------------------------------------------------------------- 56 //-- 名称 : Timer_Initial() 57 //-- 功能 : 定时器中断服务子程序 58 //-- 输入参数 : timer_isr_context,用于传递中断状态寄存器的值,id,中断号 59 //-- 输出参数 : 无 60 //--------------------------------------------------------------------------- 61 void Timer_ISR_Interrupt(void* timer_isr_context, alt_u32 id) 62 { 63 //用户中断代码 64 i = 1; 65 //应答中断,将 STATUS 寄存器清零 66 IOWR_ALTERA_AVALON_TIMER_STATUS(TIMER_BASE,~ALTERA_AVALON_TIMER_STATUS_TO_MSK); 67 } http://www.fpga.gs/ 162 FPGA 软核演练篇 §4 68 69 //--------------------------------------------------------------------------- 70 //-- 名称 71 //-- 功能 : main() : 程序入口 72 //-- 输入参数 : 无 73 //-- 输出参数 : 无 74 //--------------------------------------------------------------------------- 75 int main(void) 76 { 77 alt_u32 led_state = 0xff; //初始化 Led 78 Timer_Initial(); //初始化定时器中断 79 80 printf("Welcome To Timer Ip Demo Program... \n"); 81 82 while(1) 83 { 84 if(i == 1) 85 { 86 IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE, led_state);//使 led 全灭 87 led_state = ~led_state; 88 i = 0; 89 } 90 } 91 } 从该代码中可以看出,我们的标准中断服务程序与我们的 PIO 中断程序基本类似,它们中 断的工作原理都是相同的,只是它们提供中断信号的方式不同。由于前面给出了比较详细的讲解, 这里我们就不在赘述了。 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软 件 中 将 Qsys_Timer_Interrupt.sof 下 载 至 我 们 的 A4 开 发 板 , Qsys_Timer_Interrupt.sof 下 载 完 成 后 , 我 们 还 需 要 在 Eclipse 软 件 中 将 Qsys_Timer_Interrupt.elf 文件下载至我们的 A4 开发板,Qsys_Timer_Interrupt.elf 下载完成以 后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可以在 Eclipse 软件的控制台中 看到我们的打印信息,如图 4.30 所示。 图 4.30 标准中断服务实验的控制台打印信息图 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 163 这时,我们不需要任何操作,我们的 A4 开发板上的 8 个 LED 便会全部点亮,如图 4.31 所 示。 图 4.31 标准中断服务实验的板级调试图 8 个 LED 全亮 5s 后,我们会发现 A4 开发板上的 8 个 LED 便会全部熄灭,然后我们再等 待 5s 后,我们又发现 LED 全部点亮了,5s 后,又熄灭,8 个 LED 依此循环显示。 4.4.7 Timer IP 核的应用实例——时间标记服务 (1) 实验目的 介绍完了标准中断服务实验,接下来我们再来看看时间标记服务实验,该实验主要是利用定 时器的时间标记服务来测量两个函数的运行时间。在这个实验中,我们将学习到 Timer IP 核在 Qsys 软件中是如何进行配置,以及定时器的时间标记服务功能是如何使用的。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.32 所示。 http://www.fpga.gs/ 164 FPGA 软核演练篇 §4 图 4.32 时间标记服务实验的硬件框架图 从该图中可以看出,我们的时间标记服务实验在标准中断服务实验的基础上多添加了一个 Timer IP 核,该 IP 核的配置与我们之前的配置是一样的,也是完整特性(Full-featured)。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.8 所示。 代码 4.8 Qsys_Timestamp.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Timestamp.c 3 //-- 描述 : 使用时间标记服务来测两个函数的运行时间。 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include 8 #include "sys/alt_timestamp.h" //时间标记服务头文件 9 #include "alt_types.h" 10 11 //--------------------------------------------------------------------------- 12 //-- 名称 : func1() 13 //-- 功能 : 延时函数 1 14 //-- 输入参数 : 无 15 //-- 输出参数 : 无 16 //--------------------------------------------------------------------------- 17 void func1(void) //用于测试的两个函数 18 { 19 int cnt = 10000; 20 while(cnt--); 21 } 22 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 165 23 //--------------------------------------------------------------------------- 24 //-- 名称 : func2() 25 //-- 功能 : 延时函数 2 26 //-- 输入参数 : 无 27 //-- 输出参数 : 无 28 //--------------------------------------------------------------------------- 29 void func2(void) 30 { 31 int cnt = 20000; 32 while(cnt--); 33 } 34 35 //--------------------------------------------------------------------------- 36 //-- 名称 : main() 37 //-- 功能 : 程序入口 38 //-- 输入参数 : 无 39 //-- 输出参数 : 无 40 //--------------------------------------------------------------------------- 41 int main (void) 42 { 43 alt_u32 time1,time2,time3; 44 if (alt_timestamp_start() < 0) //开启时间标记服务 45 { 46 printf("Can't Start Timestamp...\n"); 47 } 48 time1 = alt_timestamp(); //测量时间点 1 49 func1(); 50 time2 = alt_timestamp(); //测量时间点 2 51 func2(); 52 time3 = alt_timestamp(); //测量时间点 3 53 54 printf("func1 needs %u\n",(unsigned int)(time2-time1)); 55 printf("func2 needs %u\n",(unsigned int)(time3-time2)); 56 printf("the freq of timer is %u\n",(unsigned int)alt_timestamp_freq()); 57 58 return 0; 59 } 下面我们就来给大家讲解一下该程序中的关键点。使用时间标记服务的工作分两步:第一步 调用 alt_timestamp_start()开启时间标记服务,第二步调用 alt_timestamp()测量用户感兴趣的 时间点。这两个函数位于:C:\altera\13.1\ip\altera\sopc_builder_ip\altera_avalon_timer\HAL\sr c\altera_avalon_timer_ts.c 文件中 Nios II 的定时器有两个快照寄存器 snapl 和 snaph。主控制器可通过对 snapl 或 snaph 寄 存器的写操作(忽略写数据)请求当前 32 位内部计数器的快照。当执行写操作时,计数器的值 http://www.fpga.gs/ 166 FPGA 软核演练篇 §4 复制到 snapl 和 snaph 中,且不会改变计数器的运行状况。snapl 保存计数器的低 16 位,snaph 保存计数器的高 16 位。 时间标记服务 API 函数分析:  alt_timestamp_start()。函数原型:int alt_timestamp_start(void); 函数功能:启动时间 标记服务;输入参数:无。返回值:小于 0,则失败;大于或等于 0,则成功。  alt_timestamp()。函数原型:alt_u32 alt_timestamp(void);函数功能:返回某时刻的 时间值;输入参数:无;返回值:某时刻的时间值。  alt_timestamp_freq()。调用该函数可以获知系统工作频率。 这里我们需要注意的是,我们一定要记得指定 timestamp_timer,如图 4.33 所示。 图 4.33 指定 Timestamp Timer (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Timestamp.sof 下载至我们的 A4 开发板,Qsys_Timestamp.sof 下载完成后,我们还需要在 Eclipse 软件中将 Qsys_Timestamp.elf 文件下载至我们的 A4 开发 板,Qsys_Timestamp.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时, 我们可以在 Eclipse 软件的控制台中看到我们的打印信息,如图 4.34 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 167 图 4.34 时间标记服务实验的控制台打印信息图 从该图中我们可以看出,fun2 需要的时间是 fun1 的两倍,这是和我们预设的情况一致的, 调用 alt_timestamp_freq()获得的系统工作频率是 100MHz,这也是与我们实际频率一致的。 4.4.8 Timer IP 核的应用实例——看门狗 (1) 实验目的 介绍完了时间标记服务实验,接下来我们再来看看看门狗实验,该实验主要是利用定时器的 看门狗功能实现 8 个 LED 同时闪烁效果。在这个实验中,我们将学习到 Timer IP 核在 Qsys 软 件中是如何进行配置,以及定时器的看门狗功能是如何使用的。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.29 所示。 图 4.35 看门狗实验的硬件框架图 从该图中可以看出,我们的看门狗实验在标准中断服务实验的基础上多添加了一个 EPCS IP 核,这是因为我们的看门狗实验直接下载是看不到效果的,我们需要使用 Eclipse 软件中的 Flash programmer 功能,把我们的程序烧写到外部 EPCS Flash 中运行才能够看到效果。这里 我们还需要注意的是,我们的 Timer IP 核不在是之前的完整特性(Full-featured)配置,而修改 成了看门狗(Watchdog)配置。 http://www.fpga.gs/ 168 FPGA 软核演练篇 §4 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.9 所示。 代码 4.9 Qsys_Watchdog.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Watchdog.c 3 //-- 描述 : 首先初始化 WDT,对 8 个 LED 闪烁控制,然后喂狗处理;最后进入死循环,等待复位 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include //标准输入输出头文件 8 #include "system.h" //系统头文件 9 #include "altera_avalon_timer_regs.h" //定时器头文件 10 #include "altera_avalon_pio_regs.h" //PIO 头文件 11 #include "alt_types.h" //数据类型头文件 12 #include "unistd.h" //延时函数头文件 13 14 //--------------------------------------------------------------------------- 15 //-- 名称 : WdtFeed() 16 //-- 功能 : 看门狗喂狗操作,向看门狗寄存器写入任意值即可完成看门狗复位 17 //-- 输入参数 : 无 18 //-- 输出参数 : 无 19 //--------------------------------------------------------------------------- 20 void WdtFeed(void) 21 { 22 IOWR_ALTERA_AVALON_TIMER_PERIODL(TIMER_BASE, 0x1234); 23 } 24 25 //--------------------------------------------------------------------------- 26 //-- 名称 : DelayNS() 27 //-- 功能 : 软件延时,具有喂狗功能。 28 //-- 输入参数 : dly 延时参数,值越大,延时越久 29 //-- 输出参数 : 无 30 //--------------------------------------------------------------------------- 31 void DelayNS(alt_u32 dly) 32 { 33 alt_u32 i; 34 for(; dly>0; dly--) 35 { 36 for(i=0; i<1000; i++) 37 WdtFeed(); 38 } 39 40 } Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 169 41 42 //--------------------------------------------------------------------------- 43 //-- 名称 44 //-- 功能 : InitWDT() : 初始化 WDT,需要注意的是,看门狗定时器一旦开启,无法关闭。 45 //-- 输入参数 : 无 46 //-- 输出参数 : 无 47 //--------------------------------------------------------------------------- 48 void InitWDT(void) 49 { 50 /* 该宏开启 WATCHDOG */ 51 IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER_BASE, 52 ALTERA_AVALON_TIMER_CONTROL_START_MSK); 53 } 54 55 //--------------------------------------------------------------------------- 56 //-- 名称 57 //-- 功能 : main() : 程序入口 58 //-- 输入参数 : 无 59 //-- 输出参数 : 无 60 //--------------------------------------------------------------------------- 61 int main(void) 62 { 63 alt_u8 i; 64 InitWDT(); // 初始化看门狗 65 WdtFeed(); // 进行喂狗操作 66 for(i=0; i<8; i++) 67 { 68 IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE,0xff); //熄灭 LED 69 DelayNS(1000); //延时操作 70 IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE,0x00); //点亮 LED 71 DelayNS(1000); //延时操作 72 } 73 while(1) 74 { 75 ;//等待 WDT 复位 76 } 77 return(0); 78 } 下面我们就来给大家讲解一下该程序中的关键点。看门狗的操作分两步:第一步是开启看门 狗。需要注意的是,一旦开启看门狗,将不能通过软件停止;第二步是“喂狗”。  开 启 看 门 狗 由 如 下 宏 定 义 实 现 : IOWR_ALTERA_AVALON_TIMER_CONTRO L(WATCHDOG_BASE,ALTERA_AVALON_TIMER_CONTROL_START_MSK); 实 http://www.fpga.gs/ 170 FPGA 软核演练篇 §4 现  喂狗操 作由 如下 宏定 义实 现: IOWR_ALTERA_AVALON_TGIMER_PERIODL(W ATCHDOG_BASE,0x1234); 向定时器的周期寄存器写一个任意的数即实现“喂狗” 操作。  宏名:IOWR_ALTERA_AVALON_TIMER_CONTROL(base,data);功能:向定时器 控制寄存器写一个数据;输入参数:base:定时器的基地址,例如,WATCHDOG_BASE; data:向控制寄存器写的数据。  宏名:IOWR_ALTERA_AVALON_TIMER_PERIODL(base,data)。功能:向定时器周 期寄存器写一个数据;输入参数:base:定时器的基地址;data:向周期寄存器写的 数据,例如,0x1234(该数据可以是任意值)。 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,这里需要我们 注意的是,如果你是直接将 Qsys_Timer_Interrupt.sof 文件和 Qsys_Timer_Interrupt.elf 文件下 载到我们的 A4 开发板上,那么你会看到我们的 A4 开发板上的 8 个 LED,刚开始会进行亮灭闪 烁,但是闪烁几次以后,我们的 A4 开发板上的 8 个 LED 便会常亮不在闪烁,这是因为我们的 程序进入了死循环,等待我们看门狗进行复位。这里我们使用的是 Eclipse 软件中的 Flash programmer 功能进行下载,如图 4.36 所示。 图 4.36 看门狗实验的程序下载图 从 该 图 中 可 以 看 出 , 我 们 首 先 将 Qsys_Watchdog.sof 文 件 添 加 进 入 , 然 后 又 将 Qsys_Watchdog.elf 文件添加了进去,两个文件添加好以后,我们就可以点击 Start 开始进行下 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 171 载了,下载完成以后,我们需要将 A4 开发板电源关闭,然后再打开,打开 A4 开发板电源以后 我们会发现 A4 开发板上的 8 个 LED 循环开始进行亮灭闪烁。至此,我们的 Timer IP 核就讲解 完了。 §4.5 System ID IP核 4.5.1 System ID IP 核的综述 介绍完了我们的 EPCS IP 核,接下来我们就来介绍一下我们的 System ID IP 核。System ID IP 核是一个具有 Avalon 接口的简单只读设备。Qsys 生成 Nios II 系统时,将为每个 Nios II 系统生成一个标识符。该标识符会被写入 System ID 寄存器中,供编译器和用户识别所运行的 程序是否与目标系统匹配。当程序运行在与之不匹配的系统上时,会产生不可预测的后果。 4.5.2 System ID IP 核的寄存器描述 System ID IP 核包含两个 32 位寄存器,System ID IP 核的寄存器描述如表 4.11 所示。 偏移量 0 1 寄存器名称 id timestamp 表 4.11 System ID IP 核的寄存器描述 操作 位描述(31~0) 读 Qsys 系统 ID 读 Qsys 生成时间 从 System ID IP 核寄存器的描述表格中我们可以看出,System ID IP 核只有 2 个寄存器, 接下来我们就对这 2 个寄存器分别进行介绍: (1) Id:这个 id 值类似于校验和,不同外设配置的系统产生不同的 id 值。 (2) Timestamp:基于系统生成时间的 32 位值,该值等于从 1970 年 1 月 1 日到生成 Qsys 系统时的间隔时间,以秒计算单位。 4.5.3 System ID IP 核的配置选项 说完了 System ID IP 核的综述,接下来我们就来看看 System ID IP 核在 Qsys 中的配置选 项,System IDIP 核的配置选项如图 4.37 所示。 http://www.fpga.gs/ 172 FPGA 软核演练篇 §4 图 4.37 System ID IP 核的配置选项 在该图中我们可以看到,System ID IP 核的配置很简单,它只有一个 32 bit System ID 可以 配置,这里我们就不做更改使用默认配置。 4.5.4 System ID IP 核的软件编程 System ID IP 核提供了定义硬件的底层接口头文件和 HAL 驱动驱动程序文件,文件如下所 示: (1) alt_avalon_sysid_regs.h:定义硬件接口寄存器; (2) alt_avalon_sysid.c,alt_avalon_sysid.h:定义访问 System ID IP 核的头文件和函数。 在 HAL 系统头文件中包含一个函数 alt_avalon_sysid_test(),该函数返回一个值来指示软 件期望的系统 ID 是否匹配 System ID 核。该程序位于:C:\altera\13.1\ip\altera\sopc_builder_ip\al tera_avalon_sysid\HAL\src\altera_avalon_sysid.c 文件中。这里要注意的是,alt_avalon_sysi d_test()函数是之前版本使用的函数,而现在的版本中使用的是 alt_avalon_sysid_qsys_test() 函数,该函数位于:C:\altera\13.1\ip\altera\sopc_builder_ip\altera_avalon_sysid_qsys\HAL\sr c\altera_avalon_sysid_qsys.c 文件中,这两个函数所实现的功能是一样的。 4.5.5 System ID IP 核的应用实例 (1) 实验目的 介绍完了 System ID IP 核的软件编程,接下来我们再来看看 System ID IP 核的应用实例, 该实验主要是通过硬件抽象层提供的 API 函数来访问获得系统信息。在这个实验中,我们将学 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 173 习到:System ID IP 核在 Qsys 软件中是如何进行配置的,以及 Sysmtem ID 的意义和作用。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.38 所示。 图 4.38 System ID 实验的硬件框架图 从该图中我们可以看出,我们的 System ID 实验是在 SDRAM 实验的基础上多添加了一个 System ID IP 核,该 IP 的配置我们使用的是默认设置,如图 4.39 所示。 图 4.39 System ID IP 核的配置页面 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.10 所 http://www.fpga.gs/ 174 FPGA 软核演练篇 §4 示。 代码 4.10 Qsys_SystemID.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_SystemID.c 3 //-- 描述 : 通过 alt_avalon_sysid_qsys_test()函数检测系统是否匹配 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include //标准的输入输出头文件 8 #include "system.h" //系统头文件 9 #include "alt_types.h" //数据类型头文件 10 #include "altera_avalon_sysid_qsys.h" //system ID 头文件 11 12 //--------------------------------------------------------------------------- 13 //-- 名称 : main() 14 //-- 功能 : 程序入口 15 //-- 输入参数 : 无 16 //-- 输出参数 : 无 17 //--------------------------------------------------------------------------- 18 int main() 19 { 20 printf("test sysid...\n"); 21 switch (alt_avalon_sysid_qsys_test()) /* 检查系统 ID */ 22 { 23 case 0 : {printf("0 if the hardware and software appear to be in sync"); break;} 24 case 1 : {printf("1 if software appears to be older than hardware.Halt....\n"); break;} 25 case -1 : {printf("-1 if hardware appears to be older than software!Halt...\n"); break;} 26 default : {printf("Can't judge the situation...\n"); break;} 27 } 28 return 0; 29 } 下面我们就来给大家讲解一下该程序中的关键点。Altera 公司为 Nios II 处理器用户提供定 义 系 统 ID 内 核 寄 存 器 的 HAL 系 统 库 头 文 件 。 同 时 也 提 供 了 一 个 访 问 程 序 alt_avalon_sysid_qsys_test(),该程序返回一个值来只是软件期望的系统 ID 是否与其内核匹配。 原型:alt_32 alt_avalon_sysid_test(void) 线性安全:否 头文件:< altera_avalon_sysid_qsys.h > 描述:如果存储在硬件寄存器中的值与软件期望的值匹配,则返回 0。如果硬件时间标记大 于软件时间标记,则返回 1.如果软件时间标记大于硬件时间标记,则返回-1。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 175 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_SystemID.sof 下载至我们的 A4 开发板,Qsys_ SystemID.sof 下 载完成后,我们还需要在 Eclipse 软件中将 Qsys_ SystemID.elf 文件下载至我们的 A4 开发板, Qsys_ SystemID.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我 们可以在 Eclipse 软件的控制台中看到我们的打印信息,如图 4.40 所示。 图 4.40 System ID 实验的板级调试图 从该图中可以看出,我们控制台中返回的是 0,这表示我们存储在硬件寄存器中的值与软件 期望的值匹配,我们 System ID 实验程序是可以正确运行在 A4 开发板上。最后我们在补充说明 的是,查看寄存器中的值与软件期望的值是否匹配,我们不仅仅只有这一种方法,其实我们在下 载程序的过程中,Eclipse 软件已经提供了该功能,如图 4.41 所示。 图 4.41 Run Configurations 配置页面 在该页面中我们可以看到,system ID 和 timestamp 这两个值也同样是匹配的。 http://www.fpga.gs/ 176 FPGA 软核演练篇 §4 §4.6 DMA IP核 4.6.1 DMA IP 核的综述 介绍完了我们的 System ID IP 核,接下来我们就来介绍一下我们的 DMA IP 核。DMA(直 接存储访问,Direct Memory Access)是指外部设备不通过 CPU 而直接与系统内存交换数据的 接口技术。在实际应用中,需要在两个存储器之间或者外设与存储器之间频繁地进行数据存储操 作,这些操作如果通过 CPU 来进行,势必会消耗大量的 CPU 时间。虽然利用中断进行数据传 送,可以大大提高 CPU 的利用率,但是采用中断传送有它的缺点,难以处理高速 I/O 设备,以 及批量交换数据的情况。在这种情形下,我们只能采用 DMA 才能解决效率和速度问题。DMA 在 外设与内存间直接进行数据交换,而不通过 CPU,这样数据传送的速度就取决于存储器和外设 的工作速度。存储器之间或者外设与存储器之间的数据存储操作并不需要任何的算术逻辑运算, 因此完全可以不需要 CPU 的干预。DMA 的出现,把 CPU 从繁重的数据传送工作中解放出来, 使得 CPU 更专注于数据处理工作。在 DMA 数据传输方式下,DMA 控制器接管了总线的控制 权,并以中断的方式向 CPU 报告传送操作的结束。 带 Avalon 接口的直接存储器存取控制器(DMA 控制器)替代 Avalon 主控制器执行储存器 对储存器或者储存器与 IO 设备间的批量数据传输。当 DMA 控制器执行数据传输任务时,主控 制器可自由执行其它并行的任务。DMA 控制器能够以尽可能高的效率进行传输,能够以源或目 的所允许的最大的速度进行数据的读写。DMA 控制器能够提供带有数据流控制的 Avalon 总线 传输,能够通过数据流控制来同一个慢速的外设进行数据传输,如 UART,其最大传输速度是由 外设限定的。DMA 控制器常常提供一个直接的存储器模式的数据传输,从一个源地址空间到一 个目标地址空间,源地址和目标地址或者是一个 Avalon 总线存储器映射的从设备,或者是存储 器中的一个地址空间。DMA 控制器用来连接外围设备,并带有数据流控制,它允许数据的传输 长度是固定的或可变的。DMA 控制器可以在一个 DMA 传输结束时,产生一个中断请求。下面 我们给出 DMA IP 核的结构框图,如图 4.42 所示。 Avalon 主控制器 端口 Addr Data Control 控制 端口 IRQ 寄存器文件 状态寄存器 源地址寄存器 目的地址寄存器 数据长度寄存器 控制寄存器 主控制器 读端口 主控制器 写端口 独立的 Avalon 主控制器 端口 图 4.42 DMA IP 核的结构框图 从该图中我们可以看出,DMA 控制器拥有两个存储器映射的主模式的端口(主控制器读端 口和主控制写端口),还有一个存储器映射的从模式端口控制 DMA 传输。一个典型的 DMA 传 输过程如下所示: Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 177 (1) 一个 CPU 向控制端口写入控制命令使 DMA 控制器准备好传输。 (2) CPU 使能 DMA 控制器。DMA 控制器开始没有 CPU 干预的数据传输。DMA 主端口 从读地址读取数据,它可能是一个存储器或一个外部的设备。主写端口向目标地址写入 数据,它也可能是一个存储器或一个外部设备。一个深度较低的 FIFO 缓冲读写端口中 的数据。 (3) DMA 传输在指定字节传输完毕后结束,或者用一个包结束传输信号来结束 DMA 传输, 它可由发送或接收端发出。在传输结束后,DMA 控制器产生一个中断请求,前提是 CPU 配置了这种模式。 (4) 在传输过程中或传输结束后,CPU 来决定传输是否正在进行中,或者可以通过检测 DMA 状态寄存器来判断传输是否结束。 4.6.2 DMA IP 核的寄存器描述 介绍完了 DMA IP 核的综述,接下来我们再来说一说 DMA IP 核的寄存器描述。DMA IP 核 的寄存器描述如表 4.12 所示。 表 4.12 DMA IP 核的寄存器描述 偏移量 寄存器名称 0 Status 1 Readaddre ss 2 Writeaddre ss 3 Length 4 一 5 一 6 Control 7 一 操作 读/写 读/写 读/写 读/写 一 一 读/写 一 9 8 7 6 5 4 3 2 1 0 LEN WEOP REO P BUSY DONE 读取数据的起始地址 WCO N RCO N LEEN 数据写入的起始地址 DMA传输长度 保留 保留 WEE N REEN I_E N GO 保留 WOR D HW BYTE 从 DMA IP 核寄存器的描述表格中我们可以看出,DMA IP 核共有 8 个寄存器,其中 3 个是 为将来新增功能保留的,5 个为当前有效的寄存器,接下来我们就对这 5 个寄存器分别进行介 绍: (1) 状态寄存器(status) 状态寄存器由单独的位组成,每个位表示了 DMA 控制器内部的不同状态。状态寄存器能够 在任何时候被读取。读取不会改变它的值。状态寄存器位定义如表 4.13 所示。 位编号 0 1 2 3 4 位名称 DONE BUSY REOP WEOP LEN 操作 读/清除 读 读 读 读 表 4.13 状态寄存器位 描述 DMA 传输完成。当检测到数据包结束信号或指定长度的数据传输完成 时,DONE 位置为 1.写 0 到状态寄存器使 DONE 位清零 当 DMA 传输正在进行时,BUSY 位为 1 当传输完成事件由读数据包结束信号引起时,REOP 位为 1 当传输完成事件由写数据包结束信号引起时,WEOP 位为 1 当长度寄存器递减为 0(指定长度数据传输完成)时,LEN 位置为 1 (2) 读地址寄存器(Readaddress) http://www.fpga.gs/ 178 FPGA 软核演练篇 §4 读地址寄存器指定 DMA 传输的数据源起始地址。读地址寄存器的宽度在系统生成时确定, 决定了源数据区的寻址范围。 (3) 写地址寄存器(Writeaddress) 写地址寄存器指定 DMA 传输的目标数据区的起始地址。写地址寄存器的宽度在系统生成同 时确定,决定了目标数据区的寻址范围。 (4) 长度寄存器(Length) 长度寄存器指定从读端口传输到写端口的字节数。如果是字传输,则长度寄存器的值必须乘 以 4;若为半字传输,则乘以 2。主控制器写端口每写一个字节数据时,长度寄存器减 1。当长 度减到 0 时,LEN 位置位。长度寄存器不能减到小于 0.长度寄存器的宽度在系统生成时确定。 (5) 控制寄存器(Control) 控制寄存器的各位控制 DMA 的内部操作。控制寄存器的值能够在任意时刻被读出。控制寄 存器的各位决定了 DMA 的传输,它能影响传输的结束和中断请求。控制寄存器位定义如表 4.14 所示。 表 4.14 控制寄存器位 位编号 位名称 操作 描述 0 BYTE 读/写 指定传输方式为:字节 1 HW 读/写 指定传输方式为:半字(16 位) 2 WORD 读/写 指定传输方式为:字(32 位) 使能 DMA 传输。当 GO 位设置为 0 时,阻止 DMA 进行传输。当 GO 3 GO 读/写 位为 1 且长度寄存器非 0 时,开始 DMA 传输 使能中断请求 IRQ。当 I_EN 位为 1 时,若状态寄存器的 DONE 位为 4 I_EN 读/写 1,那么 DMA 控制器产生一个 IRQ。当 I_EN 位为 0 时,IRQ 被禁止 终止读数据传输。当 REEN 位设为 1 时,读设备可以通过拉高读数据包 5 REEN 读/写 结束信号结束 DMA 传输 终止写数据传输。当 WEEN 位设为 1 时,写设备可以通过拉高写数据 6 WEEN 读/写 包结束信号结束 DMA 传输 长度寄存器到达 0 时结束传输。当 LEEN 位为 1 时,若长度寄存器到达 0,则结束 DMA 传输;当该位为 0 时,长度到 0 时;传输不结束,此 7 LEEN 读/写 时 DMA 操作必须由数据包结束信号终止,该信号来自读或写主控制器 端口 读地址固定,当 RCON 为 0 时,读地址在每次数据传输后增加,这是 8 RCON 读/写 DMA 控制器读存储器的地址处理机制。当 RCON 为 1 时,读地址不增 加。这时 DMA 控制器读外设的地址处理机制 写地址固定,类似于 RCON 位,当 WCON 为 0 时,在每次数据传输后 9 WCON 读/写 写地址增加;当 WCON 为 1 时,写地址不增加 DOUBLE 10 读/写 指定双字传输 WORD QUAD 11 读/写 指定四字传输 WORD DMA 传输的数据宽度用字节(BYTE)、半字(HW)、字(WORD)、双字(DOUBLEWORD) Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 179 和四字(QUADWORD)位指定,这些位只能有一位设为 1。如果超过一位被设置,则 DMA 控 制器操作未定义。传输数据的宽度由两个读/写传输设备中传输宽度窄的一方确定。例如,读 16 位 Flash 存储器的数据到 32 位片内存储器时,传输数据宽度必须设为半字。 4.6.3 DMA IP 核的配置选项 介绍完了我们的 DMA IP 核的寄存器描述,接下来我们再来看一看 DMA IP 核在 Qsys 中的 配置选项,图 4.43 给出了 DMA IP 核的配置选项图。 图 4.43 DMA IP 核的配置选项图 从 DMA IP 核的配置选项图中我们可以看出,DMA IP 核有两个标签 DMA Parameters 和 Advanced 可以设置,首先我们先来看下第一个标签 DMA Parameters 中的内容: (1) 传输宽度(Transfer Size) 这个选项设定了 DMA 传输单元的最小宽度。允许的范围为 1~32,长度寄存器决定了在一 次 DMA 传输中能够传输的最大数据单元个数。默认情况下长度寄存器的宽度足够满足主模式的 端口对从模式的外围器件进行读写。 (2) 突发传输(Burst Transactions) 这个选项定义 DMA IP 核是否支持突发传输,如果定义了突发传输,那么用户还需要指定每 次最多突发传输的数据量。 (3) FIFO 深度(FIFO Depth) 这个选项用于数据传输的 FIFO 缓冲区深度。Altera 公司建议这里设置的 FIFO 缓冲区深度 至少要能够达到最大读延迟的两倍。如果深度设置的太小,那么就会降低传输吞吐量。 (4) FIFO 实现(FIFO Implementation) http://www.fpga.gs/ 180 FPGA 软核演练篇 §4 这个选项控制缓冲 FIFO 的实现,如果我们选择 Construct FIFO from Registers 选项,那 么 FIFO 就使用一个存储器作为一个存储位,这在 DMA 控制器数据宽度值较大时,会占用比较 多的寄存器资源。如果我们不选择,那么 FIFO 将会由嵌入式存储器模块构成。 说完第一个标签 DMA Parameters 中的内容,下面我们再来看第二标签 Advanced 中的内 容,如图 4.44 所示。 图 4.44 DMA IP 核的配置选项图 在该页面中可以看出,我们可以选择 DMA 硬件控制器所支持的数据传输宽度,byte 字节、 halfword 半字(2 字节)、word 字(4 字节)、doubleword 双子(8 字节)、quardword 四字(16 字节)。禁止不必要的传输宽度可减少 DMA 控制器内核所消耗的片内逻辑资源的数量。例如, 如果一个系统有 16 位和 32 位的存储器,但 DMA 仅执行 16 位的数据传输,那么可禁止 32 位 传输模式来节省逻辑资源。 4.6.4 DMA IP 核的软件编程 介绍完了 DMA IP 核的配置选项,接下来我们再来看看 DMA IP 核的软件编程,Altera 提供 HAL DMA 设备驱动程序,该驱动程序把 DMA 传输抽象成了两种数据通道:发送通道和数据接 收通道。当存储器从外设接收数据时,使用数据接收通道;当存储器向外设发送数据时,使用数 据发送通道;当存储器间传输数据时既要使用数据发送通道又要使用接收通道时。 (1) ioctl()操作 用户可以通过 ioctl()操作控制 DMA 控制器的硬件相关部分。HAL 层中定义了 2 个 ioctl()函 数 分 别 用 于 辅 助 接 收 通 道 驱 动 程 序 和 发 送 通 道 驱 动 程 序 : alt_dma_rxchan_ioctl() 和 alt_dma_txchan_ioctl()。表 4.15 列出 ioctl()操作可用的选项,这些选项对于发送和接收通道都 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 181 有效。 表 4.15 alt_dma_rxchan_ioctl()和 alt_dma_txchan_ioctl()的操作 I/O 请求 意义 ALT_DMA_SET_MODE_8 8 位数据传输模式。arg 参数的值被忽略 ALT_DMA_SET_MODE_16 16 位数据传输模式。arg 参数的值被忽略 ALT_DMA_SET_MODE_32 32 位数据传输模式。arg 参数的值被忽略 ALT_DMA_SET_MODE_64 64 位数据传输模式。arg 参数的值被忽略 ALT_DMA_SET_MODE_128 128 位数据传输模式。arg 参数的值被忽略 ALT_DMA_RX_ONLY_ON 设置 DMA 接收通道为数据流模式。在这种模式下,数据从固定的地址 中连续读如。arg 参数指定读取数据的地址。 ALT_DMA_RX_ONLY_OFF 关闭接收通道的数据流模式。arg 的值被忽略 ALT_DMA_TX_ONLY_ON 设置 DMA 发送通道为数据流模式。在这种模式下,数据连续写入固定 地址。arg 参数指定写入数据的地址。 ALT_DMA_TX_ONLY_OFF 关闭发送通道的数据流模式。arg 的值被忽略 (2) 软件文件 DMA IP 核提供了定义硬件的底层接口头文件和 HAL 驱动驱动程序文件,文件如下所示:  altera_avalon_dma_reg.h:这个文件定义 DMA 内核的寄存器映射,提供底层硬件访 问宏定义。  altera_avalon_dma.h 和 altera_avalon_dma.c:这个文件实现了 HAL 系统库的 DMA 控制器设备驱动程序。 我们可以通过阅读上述文件来熟悉 DMA IP 核的软件访问方法,我们这里建议大家不要修 改该文件。 4.6.5 DMA IP 核的应用实例 (1) 实验目的 介绍完了 DMA IP 核的软件编程,接下来我们再来看看 DMA IP 核的应用实例,该实验主要 利用 DMA IP 核来实现 SDRAM 内部不同地址之间的 DMA 数据传输。在这个实验中,我们将学 习到 DMA IP 核在 Qsys 软件中是如何进行配置的,以及 DMA IP 核是如何进行使用的。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.45 所示。 http://www.fpga.gs/ 182 FPGA 软核演练篇 §4 图 4.45 DMA 实验的硬件框架图 从该图中我们可以看出,我们的 DMA 实验是在 System ID 实验的基础上多添加了一个 DAM IP 核,该 IP 核的配置我们这里使用的是默认配置,如图 4.46 所示。 图 4.46 DMA IP 核的配置页面 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.11 所 示。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 183 代码 4.11 Qsys_Dma.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Dma.c 3 //-- 描述 : 用于实现 SDRAM 内部不同位置的 DMA 数据传输 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include 8 #include 9 #include 10 #include "system.h" 11 #include "sys/alt_dma.h" 12 #include "altera_avalon_dma.h" 13 14 //--------------------------------------------------------------------------- 15 //-- 名称 : main() 16 //-- 功能 : 程序入口 17 //-- 输入参数 : 无 18 //-- 输出参数 : 无 19 //--------------------------------------------------------------------------- 20 int main() 21 { 22 int i; 23 char *send_Address = (void*)0x03000000; //定义发送的地址 24 char *receive_Address = (void*)0x03002000; //定义接收的地址 25 26 //程序从 0x03000000 地址开始,连续写入 512 个字节的数据,写入的数据为 0-511 27 for(i=0;i<512;i++) 28 { 29 *(send_Address+i) = 0x0+i; 30 } 31 //程序从 0x03002000 地址开始,连续写入 512 个字节的数据,写入的数据为 0xff 32 memset(receive_Address,0xff,512); 33 34 alt_dma_txchan tx; 35 tx = alt_dma_txchan_open(DMA_NAME); //打开发送通道 36 if(tx != NULL) //通道成功打开 37 { 38 printf("Dma transition start.\n"); 39 //设置 DMA 传输的数据位宽为 8 位 40 alt_avalon_dma_tx_ioctl(tx,ALT_DMA_SET_MODE_8,NULL); 41 //提交 DMA 发送请求,并指定发送数据的位置(sdram)以及传输数据量 42 if( alt_avalon_dma_send(tx,send_Address, 512, NULL, NULL) < 0 ) 43 { http://www.fpga.gs/ 184 FPGA 软核演练篇 §4 44 printf ("Error: failed to post transition request\n"); 45 } 46 alt_dma_txchan_close(tx); //关闭 DMA 发送信道 47 usleep(1000000); //等待 1s 48 } 49 50 alt_dma_rxchan rx; 51 rx = alt_dma_rxchan_open(DMA_NAME); //打开接收通道 52 if(rx != NULL) //通道成功打开 53 { 54 printf("Dma receive start.\n"); 55 //设置 DMA 传输的数据位宽为 8 位 56 alt_dma_rxchan_ioctl(rx,ALT_DMA_SET_MODE_8,NULL); 57 //提交 DMA 接收请求,并指定接收数据的位置(sdram)以及传输数据量 58 if( alt_dma_rxchan_prepare(rx, receive_Address, 512, NULL, NULL) < 0 ) 59 { 60 printf ("Error: failed to post receive request\n"); 61 } 62 alt_dma_rxchan_close(rx); //关闭 DMA 接收信道 63 usleep(1000000); //等待 1s 64 } 65 66 printf("transition and receive successsful\n"); 67 return 0; 68 } 下面我们就来给大家讲解一下该程序中的关键点。Nios II 中的 DMA 传输有以下三种形式:  存储器到存储器:这种情况下我们需要同时打开发送通道和接收通道,源地址和目标 地址都是自增的,代码如下所示: 1 //打开发送通道 2 tx = alt_dma_txchan_open(DAM_NAME); 3 //tx_buf 是源地址、传输数据块长度是 length 4 dma_res = alt_dma_txchan_send(tx, tx_buf, length, NULL, NULL); 5 //打开接收通道 6 rx = alt_dma_rxchan_open(DAM_NAME); 7 //rx_buf 是目标地址、传输数据块长度是 length、dma_done()是 DMA 完成后被调用的回调函数 8 dma_res = alt_dma_rxchan_prepare(rx, rx_buf, length, dma_done, NULL);  存储器到外设:这种情况下只要打开发送通道,源地址是自增的,目标地址是固定的, 代码如下所示: 1 // 打开发送通道 2 tx = alt_dma_txchan_open(DMA_NAME); 3 // dst_addr 是目标地址 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 185 4 alt_dma_txchan_ioctl(tx, ALT_DMA_TX_ONLY_ON, (void *)dst_addr); 5 // tx_buf 是源地址 6 dma_res = alt_dma_txchan_send(tx, tx_buf, length, dma_done, NULL);  外设到存储器:这种情况下只要打开接收通道,源地址是固定的,目标地址是自增的, 代码如下所示: 1 // 打开接收通道 2 rx = alt_dma_rxchan_open(DMA_NAME); 3 // source_addr 是源地址 4 alt_dma_rxchan_ioctl(rx, ALT_DMA_RX_ONLY_ON, (void *)source_addr); 5 // rx_buf 是目标地址 6 dma_res = alt_dma_rxchan_prepare(rx, rx_buf, length, dma_done, NULL); 我们还可以通过 alt_dma_txchan_ioctl 和 alt_dma_rxchan_ioctl 这两个函数设置每次发送 和接收的字节数。 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Dma.sof 下载至我们的 A4 开发板,Qsys_Dma.sof 下载完成后, 我们还需要在 Eclipse 软件中将 Qsys_Dma.elf 文件下载至我们的 A4 开发板,Qsys_Dma.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可以在 Eclipse 软件 的控制台中看到我们的打印信息,如图 4.47 所示。 图 4.47 DMA 实验的板级调试图 由于 DMA 传输是在 SDRAM 内部进行操作的,所以我们在 A4 开发板上是看不到任何效果 的,为了让大家能够看出 DMA 在 SDRAM 内部进行了操作,我们这里给出调试过程。首先,我 们初始化 0x03000000 和 0x03002000 内存地址段中的内容,初始化后的 0x03000000 地址中 的内容如图 4.48 所示。 http://www.fpga.gs/ 186 FPGA 软核演练篇 §4 图 4.48 0x03000000 初始化完成图 初始化后的 0x03002000 地址中的内容如图 4.49 所示。 图 4.49 0x03002000 初始化完成图 经过 DMA 传输后的 0x03002000 地址中的内容如图 4.50 所示。 图 4.50 DMA 完成传输后的图 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 187 至此,我们的 DMA IP 核就讲解完了。 §4.7 JTAG_UART IP核 4.7.1 JTAG_UART IP 核的综述 介绍完了我们的 DMA IP 核,接下来我们就来介绍一下我们的 JTAG_UART IP 核。带 Avalon 接口的 JTAG_UART 设备实现 PC 和 Nios II 系统间的串行通信。在许多设计中,JTAG_UART 常取代 RS-232 通信设备,用于字符的输入和输出。JTAG UART 核为用户提供了简单的 AvalonMM 接口映射,简化了 JTAG 接口的复杂性。Nios II 处理器通过读、写 Avalon-MM 接口映射寄 存器实现与 JTAG_UART 核的数据交换。JTAG_UART 的系统框图如图 4.51 所示。 接口 Altera FPGA JTAG UART 内核 从设备接口 寄存器组 数据寄存器 控制寄存器 读FIFO 写FIFO 集线器 接口 控制器 集线器 使用 接口 的其他节点 Altera FPGA内置特性 由Quartus II 自动生成 图 4.51 JTAG_UART 的结构框图 从该图中我们可以看出,JTAG_UART 内核通过 Avalon 从控制器接口连接到 Avalon 总线。 JTAG_UART 核包含 2 个 32 位寄存器,即数据寄存器和控制寄存器。Nios II 处理器可以通过这 两个寄存器与 JTAG_UART 进行数据传输。为了提高 JTAG 连接的传输带宽,JTAG_UART 核 还提供了双 FIFO 结构,即读 FIFO 和写 FIFO,FIFO 的存储深度是可以通过编程进行配置的。 FIFO 可以用 FPGA 内部的存储器块资源实现,也可以用寄存器资源实现。 4.7.2 JTAG_UART IP 核的寄存器描述 介绍完了 JTAG_UART IP 核的综述,接下来我们再来说一说 JTAG_UART IP 核的寄存器 描述。JTAG_UART IP 核的寄存器描述如表 4.16 所示。 http://www.fpga.gs/ 188 FPGA 软核演练篇 §4 表 4.16 JTAG_UART 内核寄存器映射 偏移量 寄存器 名称 操作 31…16 位描述 15 14 … 10 9 8 7…2 1 0 0 data 读/写 RAVAIL RVLID 保留 DATA 1 control 读/写 WSPACE 保留 AC WI RI 保留 W R 从 JTAG_UART IP 核寄存器的描述表格中我们可以看出,JTAG_UART 核共有 2 个 32 位 寄存器,Nios II 处理器可以通过这两个寄存器与 JTAG_UART 进行通信。下面我们就对这 2 个 32 寄存器分别进行介绍: (1) 数据寄存器(Data) 软件通过数据寄存器访问读/写 FIFO。表 4.17 描述数据寄存器每个位的功能。 位 0~7 15 16~32 名称 DATA RVALID RAVAIL 操作 读/写 读 读 表 4.17 数据寄存器位 描述 传输到或来自 JTAG 内核的值。写操作时,DATA 字段是被写入写 FIFO 的字符。读操作时,DATA 字段是从读 FIFO 中读取的字符 指示 DATA 字段是否有效。如果 RVALID=1,那么 DATA 字段有效, 否则 DATA 未定义 在读 FIFO 中剩余的字符数(读以后) 读数据寄存器获得 DATA 字段和 RAVAIL 字段的数据。数据寄存器中仅有 DATA 字段是可 以写的,在执行写数据寄存器的操作时,如果 FIFO 处于满的状态,那么字符将被丢弃。 (2) 控制寄存器(Control) Nios II 处理器可以通过读/写控制寄存器实现对 JTAG_UART 核的各种控制。读控制寄存器 返回读/写 FIFO 的状态,写控制寄存器可以使能禁止中断或者清零 AC 位,表 4.18 为控制寄存 器各位的意义。 位 0 1 8 9 10 16~32 名称 RE WE RI WI AC WSPACE 操作 读/写 读/写 读 读 读/清除 读 表 4.18 控制寄存器位 描述 读中断的中断使能位 写中断的中断使能位 表示读中断正在等待 表示写中断正在等待 表示自从 AC 位清零后就有 JTAG 活动。写 1 到 AC 将其清零 在写 FIFO 中可用的空间数 RE 和 WE 位分别使能读/写 FIFO 的中断。WI 和 RI 位表示中断源的状态,受中断使能位 (WE 和 RE)的限制。软件可通过检查 RI 和 WI 来确定在什么条件下会产生 IRQ。AC 为表示 主控制器 PC 上的应用程序已通过 JTAG 接口查询 JTAG_UART 内核。一旦置位,AC 位保持 置位直到被清零。写 1 到 AC 位可将其清零。软件可检查 AC 位来确定与 PC 的连接是否存在。 如果连接不存在,软件可选择忽略 JTAG 数据流。当主控制器 PC 没有数据传输时,它可选择每 秒查询 JTAG_UART 内核一次。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 189 4.7.3 JTAG_UART IP 核的配置选项 介绍完了我们的 JTAG_UART IP 核的寄存器描述,接下来我们再来看一看 JTAG_UART IP 核在 Qsys 中的配置选项,图 4.52 给出了 JTAG_UART IP 核的配置选项图。 图 4.52 JTAG_UART IP 核的配置选项图 从 JTAG_UART IP 核的配置选项图中我们可以看出,JTAG_UART IP 核有三个选项可以 设置,下面我们就对这三个选项分别进行介绍: (1) 写 FIFO(Write FIFO) 写 FIFO 缓冲区使数据流从 Avalon 接口到主机。各设置的意义为:  Buffer Depth:写缓冲区 FIFO 的深度可以设置为 8~32768 字节。深度越深,消耗越 多的片上存储器资源。深度为 64 时具有最佳的性能,这可以满足大多数应用。这里需 要我们注意的是,深度值必须为 2 的 n 次方。例如可以是 8、16、32 等值,但不能是 15、18、50 等类似的数值。  IRQ Threshold:写 IRQ 阈值控制内核如何提交其 IRQ 来响应 FIFO 清空。在 JTAG 清 空写 FIFO 中的数据时,若 FIFO 中剩余的字节数达到该阈值,则 JTAG_UART 核产 生中断请求 IRQ 信号。为了得到最有效的传输带宽,处理器应该防止 FIFO 完全清空 才触发中断。8 通常是一个最佳的 IRQ 阈值。  Construct using registers instead of memory blocks:如果打开该选项,那么可以节 省 FPGA 内部的存储器资源。存储一个字节数据大约消耗 11 个逻辑单元(LE)。 (2) 读 FIFO(Read FIFO) http://www.fpga.gs/ 190 FPGA 软核演练篇 §4  Buffer Depth:读缓冲区 FIFO 的深度可以设置为 8~32768 字节。深度越深,消耗越 多的片上存储器资源。深度为 64 时具有最佳的性能,这可以满足大多数应用。这里需 要我们注意的是,深度值必须为 2 的 n 次方。例如可以是 8、16、32 等值,但不能是 15、18、50 等类似的数值。  IRQ Threshold:读 IRQ 阈值控制内核如何提交其 IRQ 来响应 FIFO 填满。在 JTAG 填 满 FIFO 中的数据时,若 FIFO 中剩余的字节数达到该阈值,则 JTAG_UART 核产生 中断请求 IRQ 信号。为了得到最有效的传输带宽,处理器应该防止 FIFO 完全填满才 触发中断。8 通常是一个最佳的 IRQ 阈值。  Construct using registers instead of memory blocks:如果打开该选项,那么可以节 省 FPGA 内部的存储器资源。存储一个字节数据大约消耗 11 个逻辑单元(LE)。 (3) 允许多个连接(Allow multiple connections) 如果选择该选项,那么就可以连接多个 JTAG 外设,一般我们用不到。 4.7.4 JTAG_UART IP 核的软件编程 介绍完了 JTAG_UART IP 核的配置选项,接下来我们再来看看 JTAG_UART IP 核的软件 编程,JTAG_UART IP 核提供了定义硬件的底层接口头文件和 HAL 驱动驱动程序文件,文件如 下所示: (1) altera_avalon_jtag_uart_regs.h:定义内核的额寄存器映射,提供宏定义来访问底层 硬件。该文件中的符号仅由设备驱动程序函数使用。 (2) altera_avalon_jtag_uart.h,altera_avalon_jtag_uart.c:实现 HAL 系统库设备驱动程 序。 我们可以通过阅读上述文件来熟悉 JTAG_UART IP 核的软件访问方法,我们这里建议大家 不要修改该文件。 4.7.5 JTAG_UART IP 核的应用实例——C 函数 (1) 实验目的 介绍完了 JTAG_UART IP 核的软件编程,接下来我们再来看看 JTAG_UART IP 核的应用 实例,该实验主要利用 ANSI C 标准函数从 JTAG_UART 中读入字符,如果检测到符合要求的 字符,则输出提示信息。在这个实验中,我们将学习 printf()、scanf()、fopen()和 fwrite()等函数 的用法,以及掌握使用 ANSI C 标准函数访问 JTAG_UART 的方法。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.53 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 191 图 4.53 JTAG_UART C 函数的硬件框架图 从该图中我们可以看出,我们 JTAG_UART 实验的硬件框架与 System ID 实验的硬件框架 是一样的,我们没有添加任何 IP 核,也没有更改任何配置。我们这里的 JTAG_UART 使用的是 默认配置。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.12 所 示。 代码 4.12 Qsys_Jtag_Uart_C.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Jtag_Uart_C.c 3 //-- 描述 : 如果发现字符 t,则打印信息到控制台;如果发现字符 v,则退出程序;其它字符忽略 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include 8 #include 9 #include "system.h" 10 #include "alt_types.h" 11 12 //--------------------------------------------------------------------------- 13 //-- 名称 : main() 14 //-- 功能 : 程序入口 15 //-- 输入参数 : 无 16 //-- 输出参数 : 无 17 //--------------------------------------------------------------------------- 18 int main (void) 19 { 20 FILE* fp; http://www.fpga.gs/ 192 FPGA 软核演练篇 §4 21 char prompt = 0; 22 char* msg = "Discover the character 't'.\n"; 23 24 printf("Please write character and press Enter... \n"); 25 26 fp = fopen(JTAG_UART_NAME, "r+"); //以可读写方式打开设备文件 27 if(fp) //成功打开设备文件 28 { 29 while(prompt != 'v') //循环直至接收到 'v' 30 { 31 prompt = getc(fp); //从 JTAG UART 中获取字符 32 if(prompt == 't') 33 { //如果字符为 't' 则打印 msg 中的信息 34 fwrite(msg, strlen (msg), 1, fp); 35 } 36 if(ferror(fp)) 37 { 38 clearerr(fp); //检查错误并清除错误 39 } 40 } 41 fprintf(fp, "Closing the JTAG UART file handle.\n"); 42 fclose(fp); //关闭设备文件 43 } 44 else 45 { 46 printf("Fail to open file...\n"); //设备文件打开失败 47 } 48 return 0; 49 } 下面我们就来给大家讲解一下该程序中的关键点。在 ANSI C 库函数的支持下,我们既可把 JTAG_UART 设备当作标准输入/输出设备使用,也可当作文件操作。其实质是通过 ANSI C 库 函数调用 JTAG_UART 设备驱动函数访问硬件设备。在其他处理器的集成开发环境中,若开发 工具没有提供这项支持,则不能这样访问硬件设备。ferror(fp)用于检查错误是否在 JTAG_UART 连接上出现,例如 JTAG 连接断开。如果驱动程序检测到 JTAG 连接被断开,则报导错误(EIO) 并丢弃数据以便后续传输。如果这种错误已经出现,C 函数库锁存该错误信息直至使用 clearerr() 函数将其清除。 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软 件 中 将 Qsys_Jtag_Uart_C.sof 下 载 至 我 们 的 A4 开 发 板 , Qsys_Jtag_Uart_C.sof 下载完成后,我们还需要在 Eclipse 软件中将 Qsys_Jtag_Uart_C.elf 文 件下载至我们的 A4 开发板,Qsys_Jtag_Uart_C.elf 下载完成以后,我们的 C 程序将会执行在 我们的 A4 开发板上,此时,我们可以在 Eclipse 软件的控制台中看到我们的打印信息,如图 4.54 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 193 所示。 图 4.54 JTAG_UART C 函数的控制台打印信息图 这时,我们在 Eclipse 软件的控制台中输入一串带有 t 字符的字符串,按下回车,然后我们 再输入一串带有 v 字符的字符串,按下回车,控制台则会提示如图 4.55 所示内容。 图 4.55 JTAG_UART C 函数的板级调试图 4.7.6 JTAG_UART IP 核的应用实例——API 函数 (1) 实验目的 介绍完了 JTAG_UART C 函数实验,接下来我们再来看看 JTAG_UART API 函数实验,该 实验主要利用 HAL API 函数从 JTAG_UART 中读入和输出字符。在这个实例中,我们将学习 open()、close()、read()、write()和 lseek()等函数的用法,以及熟悉并掌握使用 HAL API 函数 访问 JTAG_UART 设备的方法。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.56 所示。 http://www.fpga.gs/ 194 FPGA 软核演练篇 §4 图 4.56 JTAG_UART API 函数的硬件框架图 从 该 图 中 我 们 可 以 看 出 , 我 们 的 JTAG_UART API 函 数 实 验 的 硬 件 框 架 与 我 们 JTAG_UART C 函数的硬件框架是一样的。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.13 所 示。 代码 4.13 Qsys_Jtag_Uart_API.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Jtag_Uart_API.c 3 //-- 描述 : 用 HAL API 函数来访问 JTAG UART;读入一串字符再输出。 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include 8 #include 9 #include 10 #include "system.h" 11 #include "unistd.h" 12 13 //--------------------------------------------------------------------------- 14 //-- 名称 : main() 15 //-- 功能 : 程序入口 16 //-- 输入参数 : 无 17 //-- 输出参数 : 无 18 //--------------------------------------------------------------------------- 19 int main(void) 20 { 21 int fd,count; Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 195 22 char buf[100]; 23 char *info = " too! "; 24 char *msg = "Please write the following characters:Nice to meet you \n"; 25 26 fd = open(JTAG_UART_NAME, O_RDWR,0666); //以可读写方式打开设备文件 27 if (fd < 0) 28 { 29 printf("Fail to open file...\n "); //设备文件打开失败 30 } 31 else 32 { 33 write(fd,msg,strlen(msg)); //将 msg 中的数据,写到控制台中 34 count = read(fd,buf,16); //将控制台中的数据,读到 buf 中 35 write(fd,buf,count); //将 buf 中的数据,写到控制台中 36 write(fd,info,strlen(info)); //将 info 中的数据,写到控制台 37 close(fd); //关闭设备 38 } 39 return 0; 40 } 下面我们就来给大家讲解一下该程序中的关键点。Nios II 的 HAL 层从宏观上把硬件抽象成 3 种类型:字符设备、块设备和网络设备。对于字符设备和块设备,HAL 层提供标准的 I/O 接口 函数来访问,如图 4.57 所示。 应用 程序 ANSI C 库函数 open() close() read() write() ioctl() Zip文件 系统 非块设备 驱动程序 块设备驱 动程序 设备A 设备B 设备C 设备D 图 4.57 HAL 层 I/O 系统与设备驱动程序 从该图中我们可以看出,JTAG_UART 设备既可以通过 ANSI C 库函数访问,又可以通过 HAL API 函数访问。 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软 件 中 将 Qsys_Jtag_Uart_API.sof 下 载 至 我 们 的 A4 开 发 板 , Qsys_Jtag_Uart_API.sof 下载完成后,我们还需要在 Eclipse 软件中将 Qsys_Jtag_Uart_API.elf 文件下载至我们的 A4 开发板,Qsys_Jtag_Uart_API.elf 下载完成以后,我们的 C 程序将会执 行在我们的 A4 开发板上,此时,我们可以在 Eclipse 软件的控制台中看到我们的打印信息,如 图 4.58 所示。 http://www.fpga.gs/ 196 FPGA 软核演练篇 §4 图 4.58 JTAG_UART API 函数的控制台打印信息图 这时,我们按照上面的提示在 Eclipse 软件的控制台中输入 Nice to meet you,按下回车, 控制台则会弹出如图 4.59 所示内容。 图 4.59 JTAG_UART API 函数的板级调试图 §4.8 UART IP核 4.8.1 UART IP 核的综述 介绍完了我们的 JTAG_UART IP 核,接下来我们就来介绍一下我们的 UART IP 核。具有 Avalon 接口的通用异步收发器/发送器内核(UART 内核)为 Altera FPGA 上的嵌入式系统和外 部设备提供了串行字符流的通信方式。UART 核符合 RS-232 数据通讯协议,并支持可调整的 波特率。用户可配置奇偶校验位、停止位和数据位,以及可选的 RTS/CTS 流控制信号。图 4.60 为 UART 内核的结构框图。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 197 clk 波特率除数 uart clk Addr Data 接收寄存器 状态寄存器 移位寄存器 RXD CTS 总线接口 IRQ 发送寄存器 移位寄存器 TXD endofpacket 控制寄存器 RTS dataavailable readfordata 数据包结束符 接口 图 4.60 UART IP 核的结构框图 从该图中我们可以看出,UART 核提供了 Avalon-MM 从接口连接到内部寄存器,我们可以 通过 Avalon-MM 从接口访问 UART 核包含的 6 个 16 位寄存器,即控制寄存器、状态寄存器、 接收寄存器、发送寄存器、除数寄存器和数据包结束符寄存器。Nios II 处理器可以通过 AvalonMM 接口访问寄存器实现对 UART 核的控制,通过串行接口传输数据。UART 核提供了高有效 的 IRQ 信号,当接收一个新的数据或者当 UART 准备好传输下一个字符时,输出 IRQ 信号。 UART 发送器由发送寄存器和发送移位寄存器组成,发送寄存器和发送移位寄存器提供双重缓 冲,主控制器可以在前一个字符正在移出时将新数据值写入发送寄存器。UART 接收器是由接收 寄存器和接收移位寄存器组成,接收寄存器和接收移位寄存器为数据接收提供双重缓冲能力,接 收寄存器在后续字符正在移入接收移位寄存器时可以保持前面的接收字符。UART 核可以实现 RS-232 协议的发送和接收,UART 核通过 TXD 和 RXD 端发送和接收串行数据。大部分 Altera 公司的 FPGA 系列上的 I/O 引脚步符合 RS-232 的电平标准,设计不能把符合 RS-232 电平标 准的信号直接与 FPGA 的引脚相连,否则可能会损害器件。为了满足 RS-232 信号电平要求, 需要在 FPGA I/O 脚和对应的 RS-232 外部输入输出接口之间连接一个外部电平转换电路,使 两者实现电平匹配。 4.8.2 UART IP 核的寄存器描述 介绍完了 UART IP 核的综述,接下来我们再来说一说 UART IP 核的寄存器描述。UART IP 核的寄存器描述如表 4.19 所示。 偏移量 0 1 2 3 4 5 寄存器名称 Rxdata Txdata Status Control Divisor Endofpacket 操作 读 写 读/写 读/写 读/写 读/写 表 4.19 UART 内核寄存器映射 描述/寄存器位 15~13 12 11 10 9 8 7 6 5 EOP CTS DCTS E RRDY TRDY TMT IEOP RTS IDCTS TRBK IE IRRDY ITRDY ITMT 波特率除数 4 3 接收数据 2 10 发送数据 TOE ROE BRK FE PE ITOE IROE IBRK IFE IPE 数据包结束符值 从 UART IP 核寄存器的描述表格中我们可以看出,UART 核共有 6 个位寄存器,下面我们 http://www.fpga.gs/ 198 FPGA 软核演练篇 §4 就对这 6 个寄存器分别进行介绍: (1) 接收数据寄存器(Rxdata) 接收数据寄存器(Rxdata)用于存储 UART 核的接收数据,串行数据由 RXD 引脚输入, 完全接收后,状态寄存器中 RRDY 位被置 1。当 Nios II 处理器从接收数据寄存器读取数据后, 状态寄存器 RRDY 位清零。若 RRDY 位为 1 时,又有新的字符输入,则会发生溢出错误,状态 寄存器的 ROE 位被置 1。不管前一个字符是否被读出,新接收到的字符总是自动保存到接收数 据寄存器。Nios II 处理器对接收数据寄存器写操作无效。 (2) 发送数据寄存器(Txdata) Avalon 主控制器写要发送的字符到发送数据寄存器(Txdata)。当将字符写入发送寄存器时, 状态位 TRDY 位为 0;当将字符从发送数据寄存器传输到发送移位寄存器时,TRDY 位被置为 1。如果在 TRDY 位为 0 时,将字符写入发送数据寄存器,结果是未定义的。读发送数据寄存器 返回未定义的值。 (3) 状态寄存器 (Status) 状态寄存器(Status)的状态位反应 UART 内核状态情况。每个状态位与控制寄存器中的 对应中断使能位相联系。状态寄存器可以在任何时候读取。读不改变任何位的值。写 0 到状态寄 存器清零 DCTS、E、TOE、ROE、BRK、FE 和 PE 位。状态寄存器的状态位在表 4.20 中列 出。 位 名称 0 PE 1 FE 2 BRK 3 ROE 4 TOE 5 TMT 操作 读/清除 读/清除 读/清除 读/清除 读/清除 读 表 4.20 状态寄存器位 描述 Parity Error。当读取的校验位有不期望的逻辑值时,发生校验错误。当内 核接收到带有不正确校验位的字符时,PE 位设为 1。PE 位为 1 时,需要 通过写状态寄存器将其清零。当 PE 位置 1 时,从接收数据寄存器中读取 数据将产生未定义值。如果 Parity 硬件选项不被使能,UART 内核不执行 奇偶校验,并且读 PE 位总是 0 Framing Error。当接收器没能检测到正确的停止位时,发生帧错误。帧错 误发生时,FE 位设为 1,需要通过写状态寄存器将其清零。当 FE 位置位 时,读接收数据寄存器将返回未定义的值 Break Detect。当 RXD 引脚连续保持低电平(逻辑 0)的时间大于一个完 整字符的时间(数据位,加上起始、停止和校验位)时,接收逻辑检测到间 断。当检测到间断时,BRK 位设为 1。BRK 位为 1 时,需要通过写状态寄 存器将其清零 Receive Overrun Error。当新接收到的字符在读取前一个字符前(RRDY 位是 1)传输到接收数据寄存器时,接收溢出错误发生。此时,ROE 位置 1,且接收数据寄存器内前一个接收字符被新的接收字符改写。BRK 位为 1 时,需要通过写状态寄存器将其清零 Transmit Overrun Error。当新字符在前一个字符传输到移位寄存器前写发 送数据寄存器(TRDY 位是 0),发送溢出错误发生。此时 TOE 位置 1。 TOE 位为 1 时,需要通过写状态寄存器将其清零 Transmit Empty。TMT 位指示发送移位寄存器的当前状态。当发生移位寄 存器正将字符从 TXD 引脚移出时,TMT 设为 0。当发生移位寄存器空闲时 (没有字符发送),TMT 位为 1。Avalon 主控制器可以通过检查 TMT 位 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 199 6 TRDY 7 RRDY 8 E 10 DCTS 11 CTS 12 EOP 读 读 读/清除 读/清除 读 读 以确定发送是否完成(并在串行链接的另一端接收) Transmit Ready。TRDY 位指示发生数据寄存器的当前状态。当发送数据 寄存器空时,它准备接收新字符且 TRDY 位为 1。当发送数据寄存器满时, TRDY 为 0。Avalon 主控制器在将新数据写入发送数据寄存器前,必须等 待 TRDY 位变为 1 Receive Character Ready。RRDY 位指示接收数据寄存器的当前状态。当 接收数据寄存器空时,RRDY 为 0。当新接收的值传输到接收数据寄存器 时,RRDY 位为 1。读接收数据寄存器将 RRDY 位清零。Avalon 主控制器 在读接收数据寄存器前,必须等待 RRDY 位变为 1 Exception。E 位指示错误情况的发生。E 位时 TOE、ROE、BRK,FE 和 PE 位的逻辑或。E 位与在控制寄存器中对应的中断使能(IE)位共同为允 许/禁止所有错误中断提供更便利的方法。状态寄存器写入操作将 E 位设为 0 Change in Clear to Send signal。只要在 CTS_N 输入端口上检测到逻辑 电平跳变时(采样与 Avalon 时钟同步),DCTS 位设为 1。该位通过 CTS_N 上的上升沿和下降沿跳变设置。DCTS 位为 1 时保持不变,知道写状态寄 存器将其清零。如果 Flow Control 硬件选项没有被使能,那么 DCTS 位总 是读为 0。 Clear to Send(CTS)signal。CTS 位反映 CTS_N 输入的瞬时值(采样 与 Avalon 时钟同步)。CTS_N 引脚为负逻辑输入,因此当 CTS_N 输入 为 0 电平时,CTS 位为 1。CTS_N 输入对发送或接收逻辑没影响。CTS_N 输入仅影响 CTS 和 DCTS 位状态,且当控制寄存器的 idcts 位使能时,有 可能产生中断。如果 Flow Control 硬件选项没有被使能,那么 CTS 位总是 读 0。 End of Packet。以下事件发生时,EOP 位置 1: ① 向发送数据寄存器写入 EOP 字符 ② 从接收数据寄存器读取 EOP 字符 EOP 字符由数据包结束符寄存器的内容确定。EOP 位为 1 时保持不变, 直到写入状态寄存器将其清零。 如果不使能 Include End of Packet Register 硬件选项,那么 EOP 位总是 读为 0。 (4) 控制寄存器 (Control) 控制寄存器(Control)由控制 UART 内核操作的控制位组成。控制寄存器的值可在任何时 候读取。控制寄存器的每一位使能状态寄存器中对应位的中断。当状态位及其对应的中断使能位 为 1 时,UART 内核产生一个中断。例如,状态寄存器的 PE 位是第 0 位,控制寄存器对应的 IPE 位也是第 0 位,当 PE 和 IPE 位都为 1 时产生中断。控制寄存器位在表 4.21 中列出。 http://www.fpga.gs/ 200 FPGA 软核演练篇 §4 位 名称 操作 表 4.21 控制寄存器位 描述 0 IPE 读/写 校验错误中断使能 1 IFE 读/写 帧错误中断使能 2 IBRK 读/写 间断检测中断使能 3 IROE 读/写 接收溢出错误中断使能 4 ITOE 读/写 发送溢出错误中断使能 5 ITMT 读/写 发送移位寄存器空中断使能 6 ITRDY 读/写 发送准备好中断使能 7 IRRDY 读/写 接收真棒好中断使能 8 IE 读/写 错误中断使能 发送间断。TRBK 位允许 Avalon 主控制器通过 TXD 输出发送间断字符。当 9 TRBK 读/写 TRBK 位设为 1 时,TXD 信号强制为 0。TRBK 位会干扰任何进行中的发送。 Avalon 主控制器必须在适当的间断周期后将 TRBK 位设回 0 10 IDCTS 读/写 CTS 信号改变中断使能 请求发送(RTS)信号。RTS 位直接连接到 RTS_N 输出。Avalon 主控制器 可在任何时候写 RTS 位。RTS 位的值仅影响 RTS_N 输出;它对发生器或接 11 RTS 读/写 收器逻辑没有影响。由于 RTS_N 输出为负逻辑电平,因此当 RTS 位为 1 时, RTS_N 输出逻辑低电平 0。如果 Flow Control 硬件选项没有被使能,那么 RTS 位总是读 0,且写操作没有影响。 12 IEOP 读/写 数据包结束符中断使能 (5) 除数寄存器 (Divisor) 除数寄存器(Divisor)的值用于生成波特率时钟。有效的波特率用下面的公式计算: 波特率=(时钟频率)/(divisor + 1) 除数寄存器是一种可选的硬件特性。如果不使能 Baud Rate Can Be Changed By Software 硬件选项,那么除数寄存器不存在。此时除数寄存器写入操作无效,且读除数寄存器返回未定义 的值。 (6) 数据包结束符寄存器 (Endofpacket) 数据包结束字符由数据包结束符寄存器(Endofpacket)的值确定,以便可变长度的 DMA 传输。复位后,默认值为 0,这是 ASCII 空值字符(\0)。数据包结束符寄存器时一种可选的硬 件特性。如果不使能 Include end-of-packet register 硬件选项,那么数据包结束符寄存器不存 在。此时,数据包结束符寄存器写入操作无效,且读该寄存器返回未定义的值。 4.8.3 UART IP 核的配置选项 介绍完了我们的 UART IP 核的寄存器描述,接下来我们再来看一看 UART IP 核在 Qsys 中 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 201 的配置选项,图 4.61 给出了 UART IP 核的配置选项图。 图 4.61 UART IP 核的配置选项图 从 UART IP 核的配置选项图中我们可以看出,UART IP 核有两个选项可以设置,一个是 Basic settings,另一个是 Baud rate。下面我们就对这两个选项分别进行介绍: (1) 基础设置(Basic Settings) 我们可以根据需要设置串行数据格式,即对数据位(Data Bits)、奇偶校验(Partity)和停 止位(Stop Bits)进行设置,数据位设置见表 4.22 所示。 设置 数据位 (Data Bits) 停止位 (Stop Bits) 奇偶校验 (Partity) 允许值 7、8、9 1、2 None、 Even、Odd 表 4.22 数据位设置 描述 该设置确定发送寄存器、接收寄存器和数据包结束符寄存器的宽度 该设置确定内核在每个字符后发送 1 个还是 2 个停止位。内核总是在 第一个停止位终止接收传输,并忽略所有后续的停止位 该设置确定 UART 是否发送带奇偶校验的字符,和它是否期望接收 到的字符有奇偶校验。 当奇偶(Parity)设为 None 时,发送逻辑发送的数据不包含校验位,且接收逻辑假设输入 的数据也不包含校验位。状态寄存器的 PE(校验错误)位不执行,且总是读 0。当 Parity 设为 Odd 或 Even 时,发送逻辑计算并插入所需的校验位到输出的 TXD 位流,且接收逻辑校验输入 RXD 位流中的校验位。如果接收器发现校验结果不正确,则状态寄存器的 PE 位设为 1.当 Parity 位 Even 时,字符中有偶数个 1 位则校验位 1;否则校验位为 0。同样,当 Parity 位 Odd 时,若 字符中有奇数个 1 位则校验位为 1。 http://www.fpga.gs/ 202 FPGA 软核演练篇 §4 Synchronizer Stages 选项是用于指定帧起始位,一般我们使用默认设置就可以了。 Include CTS/RTS 为流控制选项。当选项被选中时,UART 硬件包括以下硬件:  CTS_N:输入端口;  RTS_N:输出端口;  CTS 位:清楚发送状态位;  DCTS 位:清楚发送变化状态位;  RTS 位:发送请求控制位;  IDCTS 位:中断使能控制位; 基于这些流控制硬件,Avalon 主控制器可检测 CTS 和发送 RTS 流控制信号。CTS 输入和 RTS 输出端口直接连接到状态和控制寄存器的位,并且对内核的任何其他部分没有直接影响。 相当 Incule CTS/RTS 选项设置关闭时,UART 内核不包括上述硬件。 Include end-of-packet 为流数据控制,UART 内核的 Avalon 接口可执行流 Avalon 传输。 这允许 Avalon 主控制器当且仅当 UART 内核可接收新字符时写数据,UART 内核有可用的数 据时读数据。UART 内核也可包括数据包结束寄存器。当该设置打开时,UART 内核包括:  7/8/9 位数据包结束符寄存器在地址偏移量 5.数据宽度由 Data Bits 设置确定。  EOP 位在状态寄存器中。  IEOP 位在控制寄存器中。  数据包结束符(EOP)检测允许 UART 内核终止有数据流处理器的 Avalon 主控制器 的流数据 DMA 传输,EOP 检测可与 DMA 控制器一起使用。 当数据包结束符寄存器被禁止时,UART 核步包括上面列举的资源。写数据包结束符寄存器 没有影响,且读操作产生未定义的值。 (2) 波特率设置(Baud Rate) UART 内核可执行 RS-232 标准中的任意波特率。波特率可用以下任意一种方法配置:  固定的波特率——波特率在系统生成时固定且不能通过 Avalon 从控制器端口改变。  可变的波特率——基于除数寄存器中保持的时钟分频值,波特率可变。主控制器通过向 除数寄存器写入新值来改变波特率。 Nios II 系统复位后,UART 的波特率被设置为默认波特率。设置波特率时可以选择 Qsys 系 统提供标准的预设置值(例如 9600bps、57600bps、115200bps),也允许用户输入任何非标准 波特率。为了实现所需要的波特率,通常根据波特率计算时钟分频系数。波特率与分频系数的关 系如下: 除数=int((时钟频率)/(波特率)+ 0.5 ) 波特率=(时钟频率)/(除数 + 1 ) 当不选择 Fixed baud rate 时,在 UARTR 的硬件中生成一个 16 位分频寄存器,软件可以 通过向分频寄存器写入新值来改变波特率。当选择 Fixed baud rate 时,UART 硬件中不再包括 分频寄存器,UART 硬件使用预设的波特率分频系数,且这个值不能在系统生成后改变。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 203 4.8.4 UART IP 核的软件编程 介绍完了 UART IP 核的配置选项,接下来我们再来看看 UART IP 核的软件编程,UART IP 核提供了定义硬件的底层接口头文件和 HAL 驱动驱动程序文件,文件如下所示: (1) altera_avalon_uart_regs.h:定义内核的寄存器映射并提供硬件设备访问宏定义。 (2) altera_avalon_uart.h,altera_avalon_uart.c:实现了 HAL 系统库的 UART 内核设备 驱动程序。 我们可以通过阅读上述文件来熟悉 UART IP 核的软件访问方法,我们这里建议大家不要修 改该文件。 4.8.5 UART IP 核的应用实例——C 函数 (1) 实验目的 介绍完了 UART IP 核的软件编程,接下来我们再来看看 UART IP 核的应用实例,该实验主 要利用 ANSI C 标准函数从 UART 中读入字符,如果检测到符合要求的字符,则输出提示信息。 在这个实验中,我们将学习 printf()、scanf()、fopen()和 fwrite()等函数的用法,以及掌握使用 ANSI C 标准函数访问 UART 的方法。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.62 所示。 图 4.62 UART C 函数的硬件框架图 从该图中我们可以看出,我们 UART 实验的硬件框架在我们的 JTAG_UART 实验的硬件框 架基础上多添加了一个 UART IP 核,该 IP 核的配置,如图 4.63 所示。 http://www.fpga.gs/ 204 FPGA 软核演练篇 §4 图 4.63 UART IP 核的配置图 从该图中可以看出,我们的 UART IP 核使用的是默认配置。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.14 所 示。 代码 4.14 Qsys_Uart_C.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Uart_C.c 3 //-- 描述 : 如果发现字符 t,则打印信息到控制台;如果发现字符 v,则退出程序;其它字符忽略 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include 8 #include 9 #include "system.h" 10 11 //--------------------------------------------------------------------------- 12 //-- 名称 : main() 13 //-- 功能 : 程序入口 14 //-- 输入参数 : 无 15 //-- 输出参数 : 无 16 //--------------------------------------------------------------------------- 17 int main () 18 { Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 205 19 FILE* fp; 20 char prompt = 0; 21 char* msg = "Discover the character 't'.\n"; 22 23 printf("Please write character and press Enter... \n"); 24 25 fp = fopen(UART_NAME, "r+");//以可读写方式打开设备文件 26 if(fp) //成功打开设备文件 27 { 28 while(prompt != 'v') //循环直至接收到 'v' 29 { 30 prompt = getc(fp); //从 UART 中获取字符 31 if(prompt == 't') 32 { //如果字符为 't' 则打印 msg 中的信息 33 fwrite(msg, strlen (msg), 1, fp); 34 } 35 if(ferror(fp)) 36 { 37 clearerr(fp); //检查错误并清除错误 38 } 39 } 40 fprintf(fp, "Closing the UART file handle.\n"); 41 fclose(fp); //关闭设备文件 42 } 43 else 44 { 45 printf("Fail to open file...\n"); //设备文件打开失败 46 } 47 return 0; 48 } 这里需要我们注意的是,重定向 I/O:当标准输入/输出设备面临多个可选硬件时,需要将 I/O 数据流重定向。当为标准输入/输出设备指定硬件后,HAL 系统库会提供重定向代码,把标准输 入/输出设备的数据流转到指定设备上。如图 4.64 所示。 http://www.fpga.gs/ 206 FPGA 软核演练篇 §4 图 4.64 指定标准输入/输出设备 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Uart_C.sof 下载至我们的 A4 开发板,Qsys_Uart_C.sof 下载完 成后,我们还需要在 Eclipse 软件中将 Qsys_Uart_C.elf 文件下载至我们的 A4 开发板,Qsys_ Uart_C.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,我们需要注意的是, 由于我们将 stderr、stdin 和 stdout 指向了 uart,所以我们在 Eclipse 软件的控制台中是收不到 任何信息的,这时,我们需要使用串口调试助手来接收我们 uart 的打印信息,这里我们使用的 SecureCRT 软件,如图 4.65 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 207 图 4.65 UART C 函数的板级调试图 4.8.6 UART IP 核的应用实例——API 函数 (1) 实验目的 介绍完了 UART C 函数实验,接下来我们再来看看 UART API 函数实验,该实验主要利用 HAL API 函数从 UART 中读入和输出字符。在这个实例中,我们将学习 open()、close()、read()、 write()和 lseek()等函数的用法,以及熟悉并掌握使用 HAL API 函数访问 UART 设备的方法。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.66 所示。 http://www.fpga.gs/ 208 FPGA 软核演练篇 §4 图 4.66 UART API 函数的硬件框架图 从该图中我们可以看出,我们的 UART 中断实验的硬件框架与我们 UART API 函数的硬件 框架是一样的。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.15 所 示。 代码 4.15 Qsys_Uart_API.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Uart_API.c 3 //-- 描述 : 用 HAL API 函数来访问 UART;读入一串字符再输出。 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include 8 #include 9 #include 10 #include "system.h" 11 #include "unistd.h" 12 13 //--------------------------------------------------------------------------- 14 //-- 名称 : main() 15 //-- 功能 : 程序入口 16 //-- 输入参数 : 无 17 //-- 输出参数 : 无 18 //--------------------------------------------------------------------------- 19 int main(void) Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 209 20 { 21 int fd,len,count; 22 char *ptr; 23 char buf[100]; 24 char *info = " too! "; 25 char *msg = "Please write the following characters:Nice to meet you\n"; 26 27 ptr = buf; //指定缓冲区 28 count = 16; //从控制台中读入 16 个字符 29 len = count; //读入数据的总长度 30 31 fd = open(UART_NAME, O_RDWR,0666); //以可读写方式打开设备文件 32 if (fd < 0) 33 { 34 printf("Fail to open file...\n "); //设备文件打开失败 35 } 36 else 37 { 38 write(fd,msg,strlen(msg)); //将 msg 中的数据,写到控制台中 39 while(len) 40 { 41 count = read(fd,ptr,count); //将控制台中的数据读到缓冲区(即:buf 中) 42 ptr = ptr + count; //每读到一个字符,ptr 指针便会加 1 43 len = len - count; //每读到一个字符,len 总长度便会减 1 44 count = len; 45 } 46 write(fd,buf,16); //将 buf 中的数据,写到控制台中 47 write(fd,info,strlen(info)); //将 info 中的数据,写到控制台中 48 close(fd); //关闭设备 49 } 50 return 0; 51 } 下面我们就来给大家讲解一下该程序中的关键点。在默认情况下,UART 设备驱动程序采用 中断方式与硬件交互。用户调用 open 函数读取的是 UART 缓冲区的内容。如果缓冲区的内容 少于用户期望值,open 函数便会退出。所以每次调用 open 函数读完数据后,了解读到了几个 字符是一个很好的习惯。 最后我们再来补充说明一下 HAL 提供的设备驱动问题,HAL 系统库为同一设备提供了两种 硬件驱动程序,一种是以查询方式实现的硬件驱动程序,另一种是以中断方式实现的驱动程序。 查询方式实现的硬件驱动程序会不断轮询硬件直到硬件有响应才返回。中断方式仅操作数据缓 冲区,剩下的任务交与最底层的中断服务程序去执行。例如,中断方式的接收数据仅依赖于接收 数据缓冲区是否有数据,收到数据(缓冲区内数据较少)或收到指定个数的数据(缓冲区内数据 较多)时即返回。因此,如果库函数使用查询式硬件驱动程序,则具有较强的阻塞性。当硬件没 http://www.fpga.gs/ 210 FPGA 软核演练篇 §4 有响应时,会使整个应用程序停滞,使用中断式硬件驱动程序,阻塞较小。如果有操作系统提供 超时服务,那么无论硬件有无响应,整个应用程序都不会被阻塞。在默认情况下,库函数会自动 链接中断式硬件驱动程序,如果在 Nios II BSP Editor 中选中了 Enable Reduced device driver 选项,就会链接查询式硬件驱动程序,如图 4.67 所示。 图 4.67 链接查询式硬件驱动程序 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Uart_API.sof 下载至我们的 A4 开发板,Qsys_Uart_API.sof 下 载完成后,我们还需要在 Eclipse 软件中将 Qsys_Uart_API.elf 文件下载至我们的 A4 开发板, Qsys_Uart_API.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时我们同 UART C 函数实验一样也是需要使用串口调试助手来接收我们 uart 的打印信息,这里我们使用 的 SecureCRT 软件,如图 4.68 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 211 图 4.68 UART API 函数的板级调试图 4.8.7 UART IP 核的应用实例——中断 (1) 实验目的 介绍完了 UART API 函数实验,接下来我们再来看看 UART 中断实验,该实验主要利用 UART IP 核内部中断来实现数据的传输,并控制 LED 亮灭。在这个实验中,我们将学习到 UART IP 核中断在 Qsys 软件中是如何进行配置,以及 UART 中断服务程序的编写、注册和调试方法。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.69 所示。 http://www.fpga.gs/ 212 FPGA 软核演练篇 §4 图 4.69 UART 中断实验的硬件框架图 从该图中我们可以看出,我们的 UART API 函数实验的硬件框架与我们 UART C 函数的硬 件框架是一样的。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.16 所 示。 代码 4.16 Qsys_Uart_Interrupt.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Uart_Interrupt.c 3 //-- 描述 : 利用 UART IP 核内部中断来实现数据的传输,并控制 LED 亮灭 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "stdio.h" 8 #include "unistd.h" 9 #include "system.h" 10 #include "alt_types.h" 11 #include "altera_avalon_uart_regs.h" 12 #include "sys\alt_irq.h" 13 #include "priv/alt_legacy_irq.h" 14 #include "altera_avalon_pio_regs.h" 15 16 alt_u8 temp_data = 0; 17 alt_u8 rxdata = 0; 18 alt_u8 start_flag = 0; //用于判断点亮 Led //用于接收串口数据 //标志位 19 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 213 20 //--------------------------------------------------------------------------- 21 //-- 名称 : uart_isr() 22 //-- 功能 : 串口中断服务子程序 23 //-- 输入参数 : 无 24 //-- 输出参数 : 无 25 //--------------------------------------------------------------------------- 26 void uart_isr(void * context,alt_u32 id) 27 { 28 //读取串口数据 29 rxdata = IORD_ALTERA_AVALON_UART_RXDATA(UART_BASE); 30 //判断字符是否在 0~8 之间 31 if(rxdata >= 0x30 && rxdata <= 0x38) 32 { 33 temp_data = rxdata - 0x30; //将字符型转换为整数型 34 start_flag = 1; //标志位 35 } 36 } 37 38 //--------------------------------------------------------------------------- 39 //-- 名称 : uart_init() 40 //-- 功能 : 串口中断初始化 41 //-- 输入参数 : 无 42 //-- 输出参数 : 无 43 //--------------------------------------------------------------------------- 44 void uart_init() 45 { 46 //延时等待设备空闲 47 usleep(100000); 48 //清除状态寄存器 49 IOWR_ALTERA_AVALON_UART_STATUS(UART_BASE,0); 50 //使能接受准备好中断 51 IOWR_ALTERA_AVALON_UART_CONTROL(UART_BASE,0x80); 52 //注册 UART 中断服务函数 53 alt_irq_register(UART_IRQ,NULL,uart_isr); 54 } 55 56 //--------------------------------------------------------------------------- 57 //-- 名称 : main() 58 //-- 功能 : 程序入口 59 //-- 输入参数 : 无 60 //-- 输出参数 : 无 61 //--------------------------------------------------------------------------- 62 int main() 63 { http://www.fpga.gs/ 214 FPGA 软核演练篇 §4 64 printf("Please input 0 - 8 to control the LEDs:"); 65 IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE,0xff); //初始化 Led,全灭 66 uart_init(); //初始化串口 67 while(1) 68 { 69 if(start_flag == 1) 70 { 71 start_flag = 0; 72 switch(temp_data) 73 { 74 case 0:{IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE,0xFF);break;} //全灭 75 case 1:{IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE,0xFE);break;} //点亮 1 个 76 case 2:{IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE,0xFC);break;} //点亮 2 个 77 case 3:{IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE,0xF8);break;} //点亮 3 个 78 case 4:{IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE,0xF0);break;} //点亮 4 个 79 case 5:{IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE,0xE0);break;} //点亮 5 个 80 case 6:{IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE,0xC0);break;} //点亮 6 个 81 case 7:{IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE,0x80);break;} //点亮 7 个 82 case 8:{IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE,0x00);break;} //点亮 8 个 83 default:{break;} 84 } 85 } 86 } 87 return 0; 88 } (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软 件 中 将 Qsys_Uart_Interrupt.sof 下 载 至 我 们 的 A4 开 发 板 , Qsys_Uart_Interrupt.sof 下载完成后,我们还需要在 Eclipse 软件中将 Qsys_Uart_Interrupt.elf 文件下载至我们的 A4 开发板,Qsys_Uart_Interrupt.elf 下载完成以后,我们的 C 程序将会执行 在我们的 A4 开发板上,此时我们同 UART C 函数实验一样也是需要使用串口调试助手来接收 我们 uart 的打印信息,这里我们使用的 secureCRT 软件,我们在 SecureCRT 软件中输入 3, 然后按下回车,如图 4.70 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 215 图 4.70 SecureCRT 软件界面 此时,我们的 A4 开发板上的 D1、D2 和 D3 同时亮起,如图 4.71 所示。 §4.9 SPI IP核 图 4.71 UART 中断实验的板级调试图 http://www.fpga.gs/ 216 FPGA 软核演练篇 §4 4.9.1 SPI IP 核的综述 介绍完了我们的 UART IP 核,接下来我们就来介绍一下我们的 SPI IP 核。SPI 是嵌入式系 统常用的标准串行数据传输接口,通常用于各种片外数字传感器、A/D 和 D/A 转换接口、存储 器以及各种控制器件的控制。带 Avalon 接口的 SPI 核符合 SPI 协议并通过 Avalon 从接口接到 Avalon 总线上。SPI 核可执行主控制器或从控制器协议。当配置为主控制器时,SPI 核可控制多 达 32 个独立的 SPI 从控制器。接收和发送寄存器的宽度在 1~16 位之间配置。较长的传输长度 (如 24 位传输)可由软件程序支持。SPI 核提供一个中断输出,只要传输结束,该输出就可标 记一个中断。SPI 内核的结构框图,如图 4.72 所示 Avalon 总线 从机接口 CLK Data Addr SPI时钟分频器 接收数据 发送数据 状态寄存器 控制寄存器 IRQ 从机选择* SCLK 移位寄存器 移位寄存器 MISO MOSI ss_n0 ss_n1 … ss_n31 图 4.72 SPI IP 核的结构框图 从该图中我们可以看出,SPI 核提供了 Avalon-MM 从接口连接到内部寄存器,我们可以通 过 Avalon-MM 从接口访问 SPI 核包含的 5 个寄存器,即接收寄存器、发送寄存器、状态寄存 器、控制寄存器和从设备选择寄存器。SPI 内核逻辑与 Avalon 接口提供的时钟输入同步,配置 为主控制器时,SCLK 由 Avalon 时钟分频输出;配置为从控制器时,SPI 内核的接收逻辑与 SCLK 同步。SPI 核的发送逻辑包括发送寄存器和发送移位寄存器,发送寄存器和发送移位寄存 器在数据传输期间提供双缓冲,当前一个数据正在移出移位寄存器时,后一个数据可写入发送寄 存器。SPI 核接收逻辑包括接收寄存器和接收移位寄存器,接收寄存器和接收移位寄存器在数据 接收时提供双缓冲功能,接收寄存器在新数据正在移入接收寄存器时,可以存储前一次的接收数 据。SPI 核还提供了 4 个 SPI 接口信号,分别是 sclk、ss_n、mosi 和 miso,SPI 核可以通过这 4 个接口信号与其他 SPI 接口进行连接,这 4 个接口信号的特性与 SPI 核被配置为主设备还是 从设备有关。在主控制器模式下,SPI 端口的操作如表 4.23 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 217 名称 MOSI MISO SCLK ss_nM 方向 输出 输入 输出 输出 表 4.23 主控制器模式端口配置 描述 输出数据到从控制器 从控制器输入数据 所有从控制器的同步时钟 从控制器选择信号,其中 M 为 0~31 的数 SPI 协议为全双工,每次传输都同时发送和接收数据。主控制器通过 MOSI 发送数据并同时 从 MISO 上接收数据。SPI 内核通过时钟分频器分频 Avalon 系统时钟来产生 SCLK 信号。当 SPI 内核配置为多个从控制器连接时,会为每个从控制器生成一个片选信号。一个 SPI 内核最多 可以连接 32 个从控制器。在传输器件,主控制器通过从机片选信号 ss_n 来指定希望与之交换 数据的从控制器。这里要注意的是:在任何的传输中只能有一个从控制器发送数据,否则在 MISO 上将会有冲突。从控制器设备的数量在系统生成时指定。在从控制器模式下,SPI 端口的操作如 表 4.24 所示。 名称 MOSI MISO SCLK ss_n 表 4.24 从控制器模式端口配置 方向 描述 输出 从主控制器输入数据 输入 输出数据到主控制器 输入 同步时钟 输入 选择信号 在从控制器模式下,SPI 内核由 SPI 主控制器控制。片选信号 ss_n 有效时,从控制器逻辑 开始发送数据并从 MOSI 上输入数据。 4.9.2 SPI IP 核的寄存器描述 介绍完了 SSPI IP 核的综述,接下来我们再来说一说 SPI IP 核的寄存器描述。SPI IP 核的 寄存器描述如表 4.25 所示。 表 4.25 SPI 内核寄存器映射 偏移量 寄存器名称 15~11 10 9 8 7 6 5 4 3 2~0 0 rxdata 接收数据(n-1~0) 1 txdata 发送数据(n-1~0) 2 status E RRDY TRDY TMT TOE ROE 3 control sso IE IRRDY ITRDY ITOE IROE 4 保留 5 slaveselect 从控制器选择屏蔽 从 SPI IP 核寄存器的描述表格中我们可以看出,UART 核共有 5 个位寄存器,下面我们就 http://www.fpga.gs/ 218 FPGA 软核演练篇 §4 对这 5 个寄存器分别进行介绍: (1) 接收数据寄存器(Rxdata) 主控制器从接收数据寄存器(Rxdata)读取接收的数据。当接收移位寄存器接收完一帧数据 时,接收逻辑电路把状态寄存器中的 RRDY 位设置为 1,且把接收到的数据传输到接收数据寄 存器。当 Nios II 系统读取接收数据寄存器时,接收逻辑控制电路自动清除 RRDY 位。Nios II 系 统对接收数据寄存器写操作无效。不管前一个数据是否被读取,新接收的数据总是被传输到接收 数据寄存器。如果在 RRDY 位为 1 的状态下,接收逻辑电路把接收移位寄存器中的数据传输到 接收数据寄存器,则发生接收溢出错误,同时状态寄存器的 ROE 位被置为 1。在这种情况下, Nios II 系统读取接收数据寄存器的结果没有意义。 (2) 发送数据寄存器(Txdata) 主控制器写数据到发送数据寄存器(Txdata)。当状态寄存器的 TRDY 位为 1 时,表示发送 数据寄存器准备好接收需要发送的数据。只要执行写发送数据寄存器操作,TRDY 位就会被清 0。 数据从发送数据寄存器传输到发送移位寄存器后,TRDY 位被发送逻辑电路设置为 1,表明发送 数据寄存器可以接收新的需要发送数据了。如果在 TRDY 位为 0 的状态下主控制器执行对发送 数据寄存器的写操作,则会发生发送溢出错误,此时状态寄存器中的 TOE 为被设置为 1。在这 种情况下,新写入到发送寄存器的数据被忽略,并且发送数据寄存器的内容保持不变。 (3) 状态寄存器(Status) 状态寄存器(Status)为用户提供 SPI 核的当前工作状态。主控制器可以在任何时候读取状 态寄存器,而不改变寄存器中任何位的值。对状态寄存器写操作可以清除 ROE、TOE 和 E 状态 位。表 4.26 描述状态寄存器的各位。 表 4.26 状态寄存器位 位 名称 描述 接收溢出错误位。如果在接收数据寄存器满(也就是,当 RRDY 位为 1)时接收到 3 ROE 新数据,则 ROE 位置为 1。此时,新数据覆盖旧数据,写 status 寄存器清零 ROE 位 发送溢出错误位。如果在发送数据寄存器仍为满(也就是,当 TRDY 位为 0)时写 4 TOE 该寄存器,则 TOE 位置为 1。此时,新数据被忽略。写 status 寄存器清零 TOE 位 发送移位寄存器空。发送移位寄存器正在发送数据时,TMT 位设为 0;当移位寄存 5 TMT 器空时,TMT 位设为 1。 6 TRDY 发送准备好位。当发送数据寄存器空时,TRDY 位设为 1。 7 RRDY 接收转变好位。当接收数据寄存器满时,RRDY 位设为 1。 错误位。E 位是 TOE 和 ROE 位的逻辑或,这为编程者检测错误条件提供了方便, 8 E 写 status 寄存器清零 E 位。 (4) 控制寄存器(Control) 控制寄存器主要通过控制位使能或禁止 SPI 内核的中断事件。主控制器可以在任何时候读 控制寄存器而不会改变其值。这些中断使能位分别对应状态寄存器中对应的状态位。用户可以通 过软件改变控制位 IROE、ITOE、ITRDY、IRRDY 和 IE 的状态,实现对这些状态有效时是否 SPI 核申请中断。控制寄存器中 SSO 位不是一个中断使能控制位,它是 SS_n 信号的控制位, 在任何时候都可以通过软件修改 SSO 位实现对 SS_n 信号的控制。控制寄存器各控制位如表 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 219 4.27 所示。 状态位 3 4 6 7 8 10 名称 IROE ITOE ITRDY IRRDY IE SSO 表 4.27 控制寄存器位 描述 设置 IROE 为 1 使能接收溢出错误的中断 设置 ITOE 为 1 使能发送溢出错误的中断 设置 ITRDY 为 1 使能发送准备条件的中断 设置 IRRDY 为 1 使能接收准备条件的中断 设置 IE 为 1 使能任何错误条件的中断 不管是否正在进行串行移位操作,设置 SSO 为 1 以强制 SPI 内核驱动其 ss_n 输 出。从控制器选择寄存器控制哪个 ss_n 输出有效。 复位后,控制寄存器的所有位都设为 0。所有中断都禁止,且复位后 SS_n 信号无效。 (5) 从设备选择寄存器(Slaveselect) 从控制器选择寄存器用于选择哪个 SS_n 信号有效。在串行移位操作器件,SPI 主控制器仅 选择从控制器选择寄存器中指定的从控制器设备。从控制器选择寄存器仅在 SPI 内核配置为主 控制器模式时存在。从控制器选择寄存器中每一位对应一个片选信号 SS_n 输出。例如,要使能 与从控制器设备 3 的通信。则将从控制器选择寄存器的位 3 设置为 1。主控制器可同时设置从控 制器选择寄存器的多个位,使 SPI 主控制器在执行操作时同时选择多个从控制器设备。例如,要 使能与从控制器设备 1、5 和 6 的通信,寄存器的位 1、5 和 6 置位。然而,必须小心操作以避 免多个从控制器在 MISO 输出上的信号冲突。复位时,从控制器选择寄存器位 0 置为 1 并清除 其他所有位。因此,复位后自动选择从控制器设备 0。 4.9.3 SPI IP 核的配置选项 介绍完了我们的 SPI IP 核的寄存器描述,接下来我们再来看一看 SPI IP 核在 Qsys 中的配 置选项,图 4.73 给出了 SPI IP 核的配置选项图。 http://www.fpga.gs/ 220 FPGA 软核演练篇 §4 图 4.73 SPI IP 核的配置选项图 从 SPI IP 核的配置选项图中我们可以看出,SPI IP 核有四个选项可以设置,下面我们就对 这四个选项分别进行介绍: (1) 主控制器/从控制器(Master/Slave) 用户可以选择主控制器模式或从控制器模式来确定 SPI 核的工作模式。当选择主控制器工 作模式时,可用下列选择:Number of select(SS_n)signals (one for each slave)、SPI clock (SCLK)rate 和 Specify delay。  Number of select(SS_n)signals (one for each slave):该设置指定 SPI 主控制器 将连接的从控制器数量,可接收的范围为 1~32。SPI 主控制器内核为每个从控制器提 供唯一的 SS_n 信号。  SPI clock(SCLK)rate:该设置确定在主控制器和从控制器之间的 SCLK 信号。目标 时钟频率可指定用 Hz、KHz 或 MHz 单位。SPI 主控制器内核使用 Avalon 系统时钟和 时钟分频器来生成 SCLK。SCLK 的实际频率可能不完全匹配所需的目标时钟率。可达 到的时钟值为: / [2,4,6,8,…]。可达到的实际频率将 不会大于指定的目标值。例如,如果系统时钟频率为 50MHz,目标值为 25MHz,那么 时钟分频为 2,实际的 SCLK 频率恰恰达到 25MHz。然而,如果目标频率为 24MHz, 那么时钟分频为 4,实际的 SCLK 频率变为 12.5MHz。  Specify delay:打开该选项使 SPI 主控制器在拉高 SS_n 信号和移动数据的第一位之 间添加时间延时。某些特定的 SPI 从控制器设备需要该延时。如果延时选项打开,设 计者必须用 ns、us 或 ms 单位指定延时时间。可实现的实际延时是所需的目标延时四 舍五入后接近 SCLK 半周期的倍数,如公式:p=1 / 2 × ,实际延时 = Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 221 取整(<目标延时> / P)×P。 (2) 数据寄存器(Data Register) 数据寄存器设置影响 SPI 内核中数据寄存器的大小和操作。数据寄存器设置详解如下:  数据宽度(Width):指定接收数据寄存器、发送数据寄存器,以及接收和发送移位寄存 器的宽度,其值为 1~16 可选。  移位方向(Shift direction):确定数据移入(先为 MSB 或先为 LSB)和移出移位寄存 器的方向。 (3) 时序(Timing) 时序设置影响 SS_n、SCLK、MOSI 和 MISO 信号间的时序关系。在这部分的讨论中,MOSI 和 MISO 信号一般指 data,时序设置详解如下:  时钟极性(Clk Polarity):可为 0 或 1。当时钟极性设为 0 时,SCLK 的空闲状态为低 电平;当时钟极性设为 1 时,SCLK 的空闲状态为高电平。  时钟相位(Clk Phase):可为 0 或 1。当时钟相位为 0 时,在 SCLK 的上升沿锁存数 据,在 SCLK 的下降沿输出数据;当时钟相位为 1 时,在 SCLK 的上升沿输出数据, 在 SCLK 的下降沿改变数据。 图 4.74~图 4.77 所示为在所有可能的时钟极性和时钟相位情况下信号的操作。 SS_n SCLK DATA_OUT SS_n MSB LSB 图 4.74 时钟极性=0,时钟相位=0 SCLK DATA_OUT SS_n MSB LSB 图 4.75 时钟极性=0,时钟相位=1 SCLK DATA_OUT MSB LSB 图 4.76 时钟极性=1,时钟相位=0 http://www.fpga.gs/ 222 FPGA 软核演练篇 §4 SS_n SCLK DATA_OUT MSB LSB 图 4.77 时钟极性=1,时钟相位=1 (4) 同步阶段(Synchronizer Stages) 该选项主要是用来增加系统的容错性,一般我们用不到,不需要选择该选项。 4.9.4 SPI IP 核的软件编程 Altera 提供一个访问 SPI 的函数 alt_avalon_spi_command(),该函数为配置成主控制器的 SPI 内核提供通用访问。原型:int alt_avalon_spi_command(alt_u32 base,alt_u32 slave,alt_u32 write_length,const alt_u8* write_data,alt_u32 read_length,alt_u8* read_da-ta,alt_u32 flags)。 线程安全:否。在 ISR 中可用:否。需要包含的文件:#include 。返回 值:已读入 read_data 缓冲区中的字节数。输入参数:  base:为 SPI 内核的基地址;  slave:选择 SPI 从器件,例:输入 0 表示选通连接在 SS_0 上的 SPI 从器件;  write_length:欲写入的数据长度;  write_data:指向欲写入数据的缓冲区;  read_length:欲写入数据的缓冲区;  read_data:指向欲读出数据的缓冲区;  flags:ALT_AVALON_SPI_COMMAND_MERGE(altera_avalon_spi.h 中定义)或 者 0。ALT_AVALON_SPI_COMMAND_MERGE 表示访问完 SPI 从器件后不释放 SS_n 信号,如果需要对同一 SPI 从器件进行连续访问,则不释放 SS_n 信号可以提 高访问速度,这就是该标志字面上的意思“把(多个)SPI 访问命令合并到一起”。输 入参数 0 表示在每次访问后释放 SS_n 信号。 4.9.5 SD 卡的综述 介绍完了 SPI IP 核的软件编程,接下来我们再来看看 SD 卡的综述,由于我们的应用实例 是利用 SPI IP 核驱动 SD 卡来实现读写操作,所以在开始讲解 SPI IP 核的应用实例之前,我们 先给大家普及一下 SD 卡的一些基本知识。这里我们主要从三个方面对 SD 卡进行介绍,第一个 方面是 SD 卡的基本概述,第二个方面是 SD 卡的命令,第三个方面是 SD 卡的时序。 (1) SD 卡的基本概述 首先我们讲解是第一方面 SD 卡的基本概述。SD 卡的全称(Secure Digital Memory Card), 由松下、东芝和 SanDiSK 于 1999 年 8 月共同退出,并成立了 SDA(SD Association,SD 协 会),共同来推广 SD 标准。SD 卡协会对 SD 卡的速度用 Class 等级来标识,目前主要有 Class 2、 Class 4 和 Class 6 三种。主要以写入速度来区别不同的 Class,Class 2 表示写入速度大于 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 223 或等于 2MB/s,Class 4 表示写入速度大于或等于 4MB/s,Class 6 表示写入速度大于或等于 6MB/s。SD 卡的数据传输和物理规范由 MMC 发展而来,其尺寸和 MMC 相近,表面积大概和 一张邮票差不多。SD 卡具备串行和随机存取能力,可通过优化速度的串行接口访问,数据传输 可靠。因此 Secure Digital 这个命名就有“ 安全数码” 的意思。 SD 卡通过 9 Pin 接口与相应 读写设备连接,由于采用 NAND 闪存介质而不易损坏,读写数据、格式化都比较方便,被 MP3、 数码相机、游戏机等多种设备广泛采用。虽然 SD 卡推广很迅速,被各种数码设备广泛采用。但 对于日益小型化、轻量化的手机来说,体积还是有点大了。因此,SanDiSK 首先在 2003 年公布 了 miniSD 标准,miniSD 卡面积比 SD 卡几乎缩小了一半,厚度也有所降低,比 SD 卡更轻,尽 管 miniSD 卡有 11pin,但在电气性能上 miniSD 和 SD 是完全一样的,操作电压都是 2.7V 到 3.6V,miniSD 可通过转接卡转为标准 SD 卡使用。只是,miniSD 卡不具备 SD 所具有的写保护 和终止保护功能。miniSD 是专门针对手机存储的产品。miniSD 卡推出至今,被各类手机大量采 用,成为应用最为广泛的手机存储卡之一。miniSD 卡虽然得到了广泛的采用,但由于手机越来 越小,性能越来越高,内部留给存储卡的空间也越来越少。同时,一些别的领域也需要体积更小 的存储卡产品。面对这样的情况,SandiSK 又开始兴风作浪,开发出了 T-Flash 微型存储卡, 后来更名为 TransFlash 卡。SD 卡协会在 2005 年 3 月 14 日以 TransFlash 卡标准为基础公布 MicroSD 的格式,并于 2005 年 7 月 13 日批准了 MicroSD 最终的规格,并被摩托罗拉率先应用 在手机产品上。因为这样的关系,至今 microSD 也被人们叫为 T-Flash 卡或 TF 卡。MicroSD 卡的针脚改为 8pin,电气性能仍然和 SD 卡兼容,工作电压依然是 2.7V 到 3.6V。 MicroSD 卡 也是可以通过转接卡可作为标准 SD 卡使用。我们 A4 开发板上所采用的就是 MicroSD 卡,如 图 4.78 所示。 图 4.78 SD 卡 下面我们就来看下 MicroSD 卡的接口管脚,如表 4.28 所示。 表 4.28 MicroSD 卡的接口管脚表 引脚 SD 模式 名称 描述 SPI 模式 名称 描述 1 DATA2 数据线 2 RSV — 2 CD/DATA3 卡检测/数据线 3 CS 片选 3 CMD 命令 DI 数据输入 4 VDD 电源 VDD 电源 5 CLK 时钟 SCLK 时钟 http://www.fpga.gs/ 224 FPGA 软核演练篇 §4 6 VSS 7 DATA0 8 DATA1 电源地 数据线 0 数据线 1 VSS DO RSV 电源地 数据输出 — 从该表中我们可以看出,MicroSD 卡它有两种模式,一种是 SD 模式,另一种是 SPI 模式。 我们 A4 开发板上使用的是 SPI 模式,这八个管脚在我们 A4 开发板上的连接,如图 4.79 所示。 图 4.79 A4 开发板上 MicroSD 卡的连接原理图 从该图中我们可以看出,MicroSD 卡的 SPI 模式连接非常简单,接上电源和地,再接上 CS、 SCLK、SDDI 和 SDDO 就可以了,其他管脚我们可以不做连接。这里需要我们注意的是,SD 卡的电源电压和操作电压都位 2.7~3.6V,并且还要将 CS、SCLK、SDDI 和 SDDO 用 10~100K 的电阻上拉。最后我们再来补充说一点,标准的 SD 卡的容量范围从几 M 字节到 2G 字节。SDHC (SD High Capacity)卡是 SD 卡标准的扩展,支持的存储容量在 4G 到 32G 字节之间。也就 是说,超过 2G 的 SD 都叫 SDHC,因为早期的 SD 使用的是 FAT16 文件系统,并不支持大容 量,而 SDHC 升级为 FAT32,才支持 2G 以上的大容量。SDHC 卡与 SD 卡有相同的物理形状 和外形规格,但是在协议上略有不同,SD 卡所采用的是 SD1.0 协议,而 SDHC 卡所采用的是 SD2.0 协议,从 MMC 到 SD1.0 再到 SD2.0 协议是向上兼容的。也就是说 SD2.0 可以兼容 SD1.0 协议,SD1.0 可以兼容 MMC 卡协议。 (2) SD 卡的命令 简单的介绍完了 SD 卡的基本概念,接下来我们再来看看 SD 卡的命令。在 SPI 模式中, SD 卡为从设备,SPI 控制器为主设备,SPI 控制器发送命令帧给 SD 卡,然后 SD 卡在一定时 间内处理这个命令,最后 SD 卡再回应一个响应帧,其过程如图 4.80 所示。这里要注意的是: 尽管 SPI 标准允许全双工操作,但是 SD 卡不用此功能。SD 卡协议是基于半双工操作的,它不 可同时进行接收数据和发送数据两个操作。 1个字节 4个字节 1个字节 0~8个字节 1个字节 到SD卡 0 1 命令索引 来自SD卡 参数字段 命令帧 CRC 1 0 response 命令响 应时间 响应帧 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 225 图 4.80 SD 接收命令及回应一个响应帧的基本时序图 从图中我们可以看出,它总共分为三个部分,命令帧、命令响应时间和响应帧。下面我们就 对这三个部分分别进行介绍:  命令帧:一个命令帧包含 6 个字节,第一个字节是命令,以 01 开始,然后跟着 6 位命 令索引。接下来的 4 个字节是参数字段,可以容纳多达 32 位的信息。最后一个字节是 由一个 7 位的 CRC(循环冗余校验)校验码和一个停止位组成。CRC 码的值可以手 动计算和添加。不过,在 SPI 模式中,CRC 校验码是被忽略的,可以都置 1 或置 0。 尽管 CRC 效验码被忽略,但是最后的字节也需要包括在内,构成 6 个字节的帧格式。  命令响应时间:是为 SD 卡提供时间来处理命令。在这段时间间隔内,主设备连续产生 sclk 时钟,响应时间在 0 到 8 个字节之间。  响应帧:命令响应时间结束后,SD 卡回应一个响应帧,响应帧格式有多种,最常见的 是 R1 响应和 R7 响应。R1 响应格式包含一个字节。最高有效位总是 0,7 个低有效位 指出了各种条件和错误状态,如位 0:处于空闲状态;位 1:擦除复位;位 2:不合法 的命令;位 3:命令 CRC 校验错误;位 4:擦除序列错误;位 5:地址错误;位 6 参 数错误。R7 响应格式包含 5 个字节,第一个字节与 R1 格式相同,其余 4 个字节返回 特定的状态信息。大多数命令期望的响应是 0x00,表示没有错误发生。 6 位的命令索引字段即定义了 64 个命令,在 SD 卡文档中用 CMDxx 表示,其中 xx 表示索 引数。SD 卡标准通过定义一套附加的特殊应用命令,进一步扩展了命令个数。特殊应用命令是 一系列双命令,其中特殊命令跟在 CMD55 命令之后。这些命令用 ACMDxx 表示。下面我们给 一些常用命令,如表 4.29 所示。 表 4.29 SD 卡常用命令 命令索引 CMD0 CMD8 CMD17 CMD24 CMD55 ACMD41 参数字段 [31:0]填充比特 [31:12]保留 [11:8]支持电压 [7:0]检验码 [31:0]地址 [31:0]地址 [31:0]填充比特 [31]:保留 [30]:HCS [29:0]保留 响应帧 R1 R7 描述 复位 SD 卡,进入空闲状态 发送 SD 卡接口条件,保留位应设置为 0 R1 读一个扇区 R1 写一个扇区 R1 开启一个特殊应用命令 R1 发送主机容量支持信息,初始化 SD 卡。保留位应 设置为 0 下面我们就来简单的介绍一下这些常用命令:  CMD0:用于复位。发送 CMD0 命令,SD 卡应回应 0x01 的 R1 格式的响应,在这个 响应帧中,位 0 被置位,表示卡处于空闲状态中,注意,CMD0 命令必须和一个固定 的 CRC 值一起被传输,这个值为 0x95。 http://www.fpga.gs/ 226 FPGA 软核演练篇 §4  CMD8:包括一个 32 位的参数,[31:12]:保留,该字段应该设置为 0。[11:8]:供电电 压。该字段应该设置为 0x01,电压范围是 2.7V 到 3.6V。[7:0]:8 位的检测码,该字 段应该设置为 0xaa。因此,该命令完整的参数字段是 0x000001aa,这里需要注意的 是,该命令需要固定的 CRC 校验码,它的值是 0x87。当我们发送 CMD8 命令后,SD 卡回应 0x01 的 R1 格式响应和 0x000001aa 的 R7 格式响应。如果 CMD8 命令被拒 绝,那么这个卡就是一个 MMC 卡或者更旧版本的 SD 卡。  CMD17:用于从 SD 卡中读取单个数据块。32 位的参数指出了数据的开始位置。对于 标准容量的 SD 卡,该位置被表示为字节地址,对于大容量的 SDHC 卡,该位置被表 示为扇区地址。  CMD24:用于向 SD 卡中写单个数据库。32 位的参数给出了数据的开始位置,类似于 命令 CMD17。  ACMD41:[30]:HCS(Host Capacity Support),表示此设备是否支持高容量的 SDHC 卡,其他 31 位被保留,且应该设置为 0。因此这个参数应该是 0x40000000。 (3) SD 卡的时序 说完了 SD 卡的命令,最后我们再来讲讲 SD 卡的复位,初始化和读写时序。首先我们讲解 的是 SD 卡的复位,如图 4.81 所示。 Reset clk 74+clocks 8X4 8X 8 (64clocks) 8 (multiple) 8 8 NCR cs (CMD)IN (DATA)OUT 40h (CMD0) 95h (CRC) 01h 图 4.81 SD 卡的复位时序图 从图中我们可以看出,想要复位 SD 卡,首先我们需要拉高 CS,发送至少 74 个 clk 周期来 使 SD 卡达到正常工作电压和进行同步,然后我们再将 CS 拉低,并发送命令 CMD0 给 SD 卡, SD 卡接收到命令后,将会在一定时间内处理该命令,处理完成后,SD 卡便会回应 0x01 的 R1 格式响应帧。最后我们再将 CS 拉高,发送 8 个时钟完成 SD 卡复位。复位成功后,SD 卡就进 入了 SPI 模式,接下我们应该进行初始化。如图 4.82 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Init clk cs Qsys 丰富多彩的内置 IP 核 8X4 8X 8X 8 (64clocks) 8 (multiple) 8 (multiple)8 8X4 (64clocks) 8 8X (multiple) 8 8 NCR NWR NCR 227 (CMD)IN (DATA)OUT 77h FFh (CMD55) (CRC) 69h (ACMD41) FFh (CRC) 01h 00h 图 4.82 SD 卡的初始化时序图 这里我们需要说明的是,由于我们使用的是 8G 的 SDHC 卡,所以我们首先必须发送 CMD8 命令,以验证这个卡的接口在主设备提供的电压范围内可以操作。验证了工作电压范围之后,我 们可以发送 CMD55+ACMD41 命令来进行 SD 卡初始化,SD 卡初始化过程可能需要几百毫秒。 完成之后,SD 卡会退出空闲模式,意味着 R1 响应的值从 0x01 变成了 0x00。需要注意的是: 我们应该反复发送 CMD55+ACMD41 命令,并检测 R1 响应,直到空闲状态位被清零。成功初 始化后,我们就可以对 SD 卡进行读写操作了。这里需要我们注意的是,SRAM 和 SDRAM 的 数据存取是以字节或字为单位进行的,而 SD 卡数据存取是以扇区(512 个字节称为一个扇区) 为单位的。首先我们介绍的是读操作,如图 4.83 所示。 Read clk cs 8X4 8 (64clocks) 8 8X (multiple) 8 8X (multiple) 8 NCR NAC 8x2 8 (CMD)IN (DATA)OUT 51h (CMD17) FFh (CRC) 00h 512 Bytes Read Data FEh (Start Byte) 图 4.83 SD 卡的读操作时序图 ORC (2 Bytes) Don’t Care 从图中我们可以看出,首先我们需要发送 CMD17 命令给 SD 卡,SD 卡收到命令后会响应 0x00。然后我们连续读直到读到开始字节 0xFE,开始读 512 个字节,读完了 512 个字节还需要 再读两个 CRC 字节。这里需要我们注意的是,CMD17 命令的参数为要读区域的开始地址,因 为考虑到 SD 卡的读写要求地址对齐,所以一般我们都将地址转为扇区,并以扇区为单位进行读 写,比如读扇区 0 参数就是 0,读扇区 1 参数就为 1<<9(即地址 512),读扇区 2 就为 2<<9(即 地址 1024),依此类推。说完了读操作,最后我们再来看下写操作,如图 4.84 所示。 http://www.fpga.gs/ 228 FPGA 软核演练篇 §4 Write clk cs 8X4 8X 8X 8 (64clocks) 8 (multiple) 8 (multiple)8 NCR NWR 512+2 8X 8 (multiple) 8 (CMD)IN (DATA)OUT 58h FFh (CMD24) (CRC) 512 Bytes Write Data FEh (Start Byte) CRC (2 Bytes) Don’t Care 00h 图 4.84 SD 卡的写操作时序图 Busy xxx0_0101b 从该图中我们可以看出,首先我们需要发送 CMD18 命令给 SD 卡,SD 卡收到命令后会响 应 0x00。然后我们发送若干时钟,发送写扇区开始字节 0xFE,发送 512 个字节数据,再发送 2 字节 CRC(可以均为 0xff),最后连续读直到读到 xxx00101 表示数据写入成功,当然这还没结 束,我们还需要继续读,直到读到 0xff 表示写操作完成。 4.9.6 SPI IP 核的应用实例 (1) 实验目的 介绍完了 SD 卡的综述,接下来我们再来看看 SPI IP 核的应用实例,该实验主要利用 SPI IP 核驱动 SD 卡来实现读写实验,在这个实验中,我们要了解 SPI 使用方法和学习 SD 的读写 操作方法。 (2) 硬件框架 讲完了实验目的,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 4.85 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 229 图 4.85 SPI 实验的硬件框架图 从该图中我们可以看出,我们的 SPI 实验是在 System ID 实验的基础上多添加了一个 SPI IP 核,该 IP 核的配置,如图 4.86 所示。 图 4.86 SPI IP 核的配置页面 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 4.17 所 示。 代码 4.17 Qsys_Spi.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Spi.c 3 //-- 描述 : 利用 SPI 读写 SD 卡 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "system.h" //系统头文件 8 #include //标准的输入输出头文件 9 #include //延时函数头文件 10 #include "alt_types.h" //数据类型头文件 11 #include "altera_avalon_spi_regs.h"//spi 寄存器头文件 12 #include "altera_avalon_spi.h" //spi 底层驱动头文件 13 14 alt_u8 SDReadBlock_Data[512]; //(读)扇区缓冲数组,512 字节数据 http://www.fpga.gs/ 230 FPGA 软核演练篇 §4 15 alt_u8 SDWriteBlock_Data[512]; //(写)扇区缓冲数组,512 字节数据 16 17 //--------------------------------------------------------------------------- 18 //-- 名称 : Spi_SDWriteByte() 19 //-- 功能 : 往 Spi 中写数据函数 20 //-- 输入参数 : txdata:需要发送的数据 21 //-- 输出参数 : 无 22 //--------------------------------------------------------------------------- 23 void Spi_SDWriteByte(alt_u8 txdata) 24 { 25 //往 Spi 中写一个字节 26 alt_avalon_spi_command(SPI_BASE, 0, 1, &txdata, 0, NULL, 0); 27 } 28 29 //--------------------------------------------------------------------------- 30 //-- 名称 31 //-- 功能 : Spi_SDReadByte() : 从 Spi 中读数据函数 32 //-- 输入参数 : 无 33 //-- 输出参数 : readbuf:从 Spi 中读取出来的数据 34 //--------------------------------------------------------------------------- 35 alt_u8 Spi_SDReadByte() 36 { 37 alt_u8 readbuf; 38 //从 Spi 中读一个字节 39 alt_avalon_spi_command(SPI_BASE, 0, 0, NULL, 1, &readbuf, 0); 40 return(readbuf); 41 } 42 43 //--------------------------------------------------------------------------- 44 //-- 名称 : Spi_SDReadByte() 45 //-- 功能 : 往 SD 卡中写命令函数 46 //-- 输入参数 : cmd:Byte1 命令;arg:Byte2~Byte5 命令;crc:Byte6 命令 47 //-- 输出参数 : r1:响应变量 48 //--------------------------------------------------------------------------- 49 alt_u8 Spi_SDSendCmd(alt_u8 cmd,alt_u32 arg,alt_u8 crc) 50 { 51 alt_u8 r1; //响应变量 52 alt_u8 time = 0; //超时变量 53 54 //SD 卡的命令格式如下,6 字节共 48 位,传输时最高位(MSB)先传输 55 Spi_SDWriteByte(cmd | 0x40); //写 Byte1 56 Spi_SDWriteByte(arg>>24); //写 Byte2 57 Spi_SDWriteByte(arg>>16); //写 Byte3 58 Spi_SDWriteByte(arg>>8); //写 Byte4 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 231 59 Spi_SDWriteByte(arg); //写 Byte5 60 Spi_SDWriteByte(crc); //写 Byte6 61 62 //写入命令后,附加 8 个填充时钟,等待 SD 卡回应 63 do{ 64 65 r1 = Spi_SDReadByte(); //读数据 66 time++; 67 if(time > 254) return 1; //超时退出返回 1 68 69 }while(r1 == 0xff); 70 71 return r1; //命令写入成功,返回响应变量 72 } 73 74 //--------------------------------------------------------------------------- 75 //-- 名称 : Spi_SDReset() 76 //-- 功能 : SD 卡复位函数 77 //-- 输入参数 : 无 78 //-- 输出参数 : 0:成功;1:失败 79 //--------------------------------------------------------------------------- 80 alt_u8 Spi_SDReset(void) 81 { 82 alt_u8 i; //循环变量 83 alt_u8 r1; //响应变量 84 alt_u8 time = 0; //超时变量 85 86 //发送至少 74 个 clk 周期来使 SD 卡达到正常工作电压和进行同步 87 for(i = 0;i < 16;i ++) 88 Spi_SDWriteByte(0xff); 89 90 //发送 CMD0,需要收到回应 0x01 表示成功 91 do{ 92 93 r1 = Spi_SDSendCmd(0,0,0x95); //发送 CMD0 命令 94 time++; 95 if(time > 254) return 1; //超时退出返回 1 96 97 }while(r1 != 0x01); //等待返回 0x01 98 99 return 0; //复位成功,则返回 0 100 } 101 102 //--------------------------------------------------------------------------- http://www.fpga.gs/ 232 FPGA 软核演练篇 §4 103 //-- 名称 : Spi_SDInit() 104 //-- 功能 : SD 卡初始化函数 105 //-- 输入参数 : 无 106 //-- 输出参数 : 0:成功;1:失败 107 //--------------------------------------------------------------------------- 108 alt_u8 Spi_SDInit(void) 109 { 110 alt_u8 r1; //响应变量 111 alt_u8 time = 0; //超时变量 112 alt_u32 r7 = 0; //响应变量 113 114 //发送 CMD8 检测接口条件,若 r1 返回 0x01,r7 返回 0x000001aa,则表示检测成功 115 do{ 116 117 r1 = Spi_SDSendCmd(8,0x000001aa,0x87); //发送 CMD8 命令 118 r7 += Spi_SDReadByte(); //读取响应 0x00 119 r7 <<= 8; 120 r7 += Spi_SDReadByte(); //读取响应 0x00 121 r7 <<= 8; 122 r7 += Spi_SDReadByte(); //读取响应 0x01 123 r7 <<= 8; 124 r7 += Spi_SDReadByte(); //读取响应 0xaa 125 time++; 126 if(time > 254) return 1; //超时退出返回 1 127 128 }while((r1 != 0x01) && (r7 != 0x000001aa)); //r1 返回 0x01,r7 返回 0x000001aa 129 130 time = 0; 131 132 //此处省略发送 CMD58 命令 133 134 //发送 CMD55+ACMD41,收到 0x00 表示成功 135 do{ 136 137 r1 = Spi_SDSendCmd(55,0,0xff); //发送 CMD55 命令 138 if(r1 == 0x01) r1 = Spi_SDSendCmd(41,0x40000000,0xff); //发送 ACMD41 命令 139 time++; 140 if(time > 254) return 1; //超时退出返回 1 141 142 }while(r1 != 0x00); //等待返回 0x00 143 144 //此处省略发送 CMD58 命令 145 146 return 0; //初始化成功,则返回 0 Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 233 147 } 148 149 //--------------------------------------------------------------------------- 150 //-- 名称 : Spi_SDReadBlock() 151 //-- 功能 : 读取 SD 卡一个扇区数据 152 //-- 输入参数 : address:扇区地址 153 //-- 输出参数 : 0:成功;1:失败 154 //--------------------------------------------------------------------------- 155 alt_u8 Spi_SDReadBlock(alt_u32 address) 156 { 157 alt_u8 r1; //响应变量 158 alt_u32 i = 0; //循环变量 159 160 //发送 CMD17 命令,收到 0x00 表示成功 161 r1 = Spi_SDSendCmd(17,address,0xff); //发送 CMD17 命令 162 if(r1 != 0x00) return 1; 163 164 //连续读直到读到开始字节 0xFE 165 while (Spi_SDReadByte()!= 0xfe); 166 167 //读取一个扇区 512 字节数据 168 for(i = 0; i < 512; i++) 169 SDReadBlock_Data[i] = Spi_SDReadByte(); 170 171 //读取两个字节 CRC 校验 172 Spi_SDReadByte(); 173 Spi_SDReadByte(); 174 175 return 0; //读取成功,则返回 0 176 } 177 178 //--------------------------------------------------------------------------- 179 //-- 名称 : Spi_SDWriteBlock() 180 //-- 功能 : 写入 SD 卡一个扇区数据 181 //-- 输入参数 : sector:扇区地址;buffer:写入 SD 卡的数据首地址 182 //-- 输出参数 : 0:成功;1:失败 183 //--------------------------------------------------------------------------- 184 alt_u8 Spi_SDWriteBlock(alt_u32 sector, alt_u8* buffer) 185 { 186 alt_u8 r1; //响应变量 187 alt_u32 i; //循环变量 188 189 //发送 CMD24 命令,收到 0x00 表示成功 190 r1 = Spi_SDSendCmd(24, sector<<9, 0xff); //发送 CMD24 命令 http://www.fpga.gs/ 234 FPGA 软核演练篇 §4 191 if(r1 != 0x00) return 1; //写入失败,返回 1 192 193 //发送若干时钟 194 for(i =0; i < 5; i++) 195 Spi_SDWriteByte(0xff); //送 8 个时钟周期脉冲 196 197 //发送写扇区开始字节 0xFE 198 Spi_SDWriteByte(0xfe); 199 200 //发送 512 个字节数据 201 for(i = 0; i < 512; i++) 202 Spi_SDWriteByte(*buffer++); 203 204 //发送 2 字节 CRC 校验 205 Spi_SDWriteByte(0xff); 206 Spi_SDWriteByte(0xff); 207 208 //连续读直到读到 XXX00101 表示数据写入成功 209 r1 = Spi_SDReadByte(); 210 if((r1 & 0x1f) != 0x05) return 1; //写入失败,返回 1 211 212 //继续读进行忙碌检测,当读到 0xff 表示写操作完成 213 while(!Spi_SDReadByte()); 214 215 return 0; 216 } 217 218 //--------------------------------------------------------------------------- 219 //-- 名称 220 //-- 功能 : main() : 程序入口 221 //-- 输入参数 : 无 222 //-- 输出参数 : 无 223 //--------------------------------------------------------------------------- 224 int main(void) 225 { 226 alt_u32 i; 227 228 if(Spi_SDReset()) //SD 卡复位 229 printf("SD Reset Failed!"); 230 else 231 printf("SD Reset Succeed!"); 232 233 if(Spi_SDInit()) //SD 卡初始化 234 printf("SD Inint Failed!"); Zircon Opto-Electronic Technology CO.,Ltd. §4 Qsys 丰富多彩的内置 IP 核 235 235 else 236 printf("SD Inint Succeed!"); 237 238 for(i = 0; i < 512; i++) //初始化写入数据 239 SDWriteBlock_Data[i] = i; 240 241 if(Spi_SDWriteBlock(0, SDWriteBlock_Data)) //写一个扇区 242 printf("SD Write Failed!"); 243 else 244 printf("SD Write Succeed!"); 245 246 Spi_SDReset();Spi_SDInit(); //SD 卡复位并初始化 247 248 if(Spi_SDReadBlock(0)) //读一个扇区 249 printf("SD Read Failed!"); 250 else 251 printf("SD Read Succeed!\n"); 252 253 for(i = 0; i < 16; i++) //读取 16 个字节数据 254 printf("0x%.2x,",SDReadBlock_Data[i]); 255 256 return 0; 257 } 从该代码中我们可以看出,使用 Altera 公司提供的 API 访问程序 alt_avalon_spi_command() 对从机进行读/写。只要在 Qsys 中设置好 SPI 工作模式,然后给 SPI 访问程序 alt_avalon_spi_co mmand()输入正确的参数,即可获得用户希望的效果。但这里我们需要注意两点:  alt_avalon_spi_command()仅对主机模式有效,在从机模式接收数据需要直接访问 SPI 数据寄存器。  SPI 内核不匹配 HAL 支持的通用设备模型种类,因此它不能通过 HAL API 或 ANSI C 标准库访问。 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Spi.sof 下载至我们的 A4 开发板,Qsys_Spi.sof 下载完成后,我 们还需要在 Eclipse 软件中将 Qsys_Spi.elf 文件下载至我们的 A4 开发板,Qsys_Spi.elf 下载完 成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可以在 Eclipse 软件的控制 台中看到我们的打印信息,如图 4.87 所示。 http://www.fpga.gs/ 236 FPGA 软核演练篇 §4 图 4.87 SPI 实验的板级调试图 Zircon Opto-Electronic Technology CO.,Ltd. Qsys 的精髓——定制你专属的高性能 IP 核 第五章 Qsys 的精髓——定制你专属的高性能 IP 核 在上一章中,我们对 Qsys 所内置的各种各样的 IP 核作了一个较完整的学习,这些 IP 核既 有简单的 IO 操控,也有较为复杂的 SDRAM 控制器。通过这些 IP 我们可以很容易的驱动板子 上的按键、LED 灯、串口、SD 卡以及 SDRAM 存储器等等。但是当你再仔细观察我们的开发 板,你会发现我们的开发板上,还有很多用户外设,如数码管、蜂鸣器、AD、0044A、VGA 等, 这些外设 Altera 并没有在 Qsys 中内置它们的 IP 核。那么我们该如何驱动它们呢? 如果之前你学习过单片机的话,这个问题貌似并不难回答。作为 Qsys 中 CPU 的 Nios II 处 理器,本身上就可以被看作强大的“超级单片机”:它不仅在操作上跟单片机类似,还比单片机 通用灵活的多,最大的福音是:每一个 FPGA 的 IO 都可以被定义为 PIO 元件,这样我们就可以 通过类似于单片机的 IO 操作,去驱动我们任何想驱动的器件如蜂鸣器、数码管等,即使 Altera 并没有为这些器件提供现成的 IP 核。这当然是一个聪明的好主意,我相信很多初学者在没有阅 读我们这本教程之前已经把这种实现方式练就的“炉火纯青”了。但即使这样,请你安静下耐心 的看一看,Altera 为我们所提供的另一种方案:自定义用户 IP。那么什么叫自定义用户 IP 呢? 简言之,就是我们自己来设计类似与上一章 Altera 为我们提供的 IP。在上一章中我们知道,在 Qsys 系统中,Nios II 处理器是系统的主控制器,即一个 Master。这个 Master 可以连接很多的 Slave,也就是我们常说的外设(上一章中所学的所有 IP 都属于 Salver)。更浅显易懂的说,Qsys 系统就像是一个大家族,里面的大管家就是 Nios II 处理器,它有很多服侍它的仆人,但是大家 族就是大家族,规矩特别多,仆人服侍主人必须按规矩办事,而这一套规矩其实就被称作 Avalon 接口规范。因此,在定制我们的自定义外设之前,我们要首先明确我们所制定永远是一个 Nios II 处理器的 Slave,你想让它服侍主人,必须弄清楚 Qsys 这个大家族所固有的一些规矩,也就是 Avalon 接口规范,只有你按规矩做事,Nios II 处理器才能真正接纳你。所以在紧接着的下一小 节中,我们就开始介绍 Avalon 规范。这里提醒大家的是,以下过程(5.1)也许会有些枯燥,因 为大家族规矩多。但是相信我在此过程中千万不要放弃,当你不步入豪门时,我相信你定会大开 眼界。好,现在请你先洗把脸、弄清头脑,我们开始了! §5.1 Avalon接口规范 在 Altera FPGA 中 Avalon 允许你简单地连接组建来简化系统设计,Avalon 接口适用于高 速数据流,读写寄存器,存储器,以及控制片外设备。这些标准接口在 Qsys 中有效地设计到组 件中。你可以在你定制的组件中使用这些标准化的接口来增强你设计的兼容性。在 Avalon 接口 规范中,定义了以下七个接口: (1) Avalon Clock Interface:发送和接受时钟的接口。所有 Avalon 接口都是同步的。 (2) Avalon Reset Interface:提供联合复位的接口。 (3) Avalon Memory Mapped Interface (Avalon-MM):基于地址读写典型的主从连接关系 的接口。 (4) Avalon Interrupt Interface:允许组件到信号事件到其他组件的接口。 240 软核演练篇 §5 (5) Avalon Streaming Interface (Avalon-ST):支持单向数据流,包括复用流,数据包, DSP 数据。 (6) Avalon Tri-State Conduit Interface (Avalon-TC):支持连接到片外外围设备的接口。 多重外围设备可以通过信号复用(多路传输)来分享引脚,减少使用 FPGA 引脚数和 PCB 上的导线。 (7) Avalon Conduit Interface:适应不能适合任何其余 Avalon 类型的个别信号或信号组的 接口类型。你可以连接在 Qsys 系统里面的 conduit 接口,或者你可以输出它们,以连 接到设计中的其他模块,或者 FPGA 的引脚。 一个信号组件可以包括任意数量的接口,也可以包括多种实例化的相同接口类型。下面我们 就对七个接口分别进行介绍。 5.1.1 Avalon Clock 首先我们介绍的是 Avalon Clock 接口,Avalon Clock 接口定义了一个时钟或多个时钟用于 一个组件。组件可以有时钟输入,时钟输出,或两者都有。例如,相位锁相环(PLL),它一个时 钟输入和多个时钟输出,如图 5.1 所示。 reset ref_clk PLL Core Megafunction Reset Sink Clock Sink Clock Source Clock Source CClolock SSoouurce Clock Output Interface1 Clock Output Interface2 Clock Output Interface_n 图 5.1 PLL 的时钟输入与输出 从该图中我们可以看出,Clock Sink 是输入信号,Clock Source 是输出信号,下面我们就 对这两个信号分别进行介绍: 首先我们介绍的是 Clock Sink 输入信号。Clock Sink 为其他接口和内部逻辑提供一个同步 时钟。Clock Sink 信号类型,如表 5.1 所示。 信号 clk 宽度 1 方向 Input 表 5.1 Clock Sink 信号类型 必选 描述 Yes 一个时钟信号,为内部逻辑和其他接口提供同步 看完了 Clock Sink 信号类型,我们再来看下 Clock Sink 信号属性,如图 5.2 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 241 图 5.2 Clock Sink 的信号属性 从该图中我们可以看出,Clock Sink 有 1 个属性,Clock rate,该属性默认值是 0,表示表 示频率,如果为 0,那么 Clock rate 无效,如果不为 0 并且连接的时钟源不是指定的频率,那么 Qsys 则发出警告,一般我们这里采用默认设置就可以了。说完了 Clock Sink,接着我们再来看 下 Clock Source,Clock Source 类型,如表 5.2 所示。 信号 clk 宽度 1 表 5.2 Clock Source 信号类型 方向 必选 Output Yes 描述 一个输出时钟信号 看完了 Clock Source 信号类型,我们再来看下 Clock Source 信号属性,如图 5.3 所示。 图 5.3 Clock Source 的信号属性 从该图中我们可以看出,Clock Source 有 3 个属性,下面我们就对这 3 个属性分别进行介 绍: (1) Associated direct clock:如果存在,直接驱动这一时钟输出的时钟输入名字。 (2) Clock rate:在被驱动时,时钟输出的 Hz 中表示频率。 (3) Clock rate know:表示时钟频率是否已知,如果已知,这个信息在系统中可用于自定 http://www.fpga.gs/ 242 软核演练篇 §5 义其他组件。 5.1.2 Avalon Reset 说完了 Avalon Clock,接下来我们再来看看 Avalon Reset,Reset Sink 信号类型,如表 5.3 所示。 信号 reset,reset_n reset_req 宽度 1 1 方向 Input Input 表 5.3 Reset Sink 信号类型 必选 描述 Yes 复位信号,reset 高电平有效;reset_n 低电平有效 Optional 复位请求信号 看完了 Reset Sink 信号类型,我们再来看下 Reset Sink 信号属性,如图 5.4 所示。 图 5.4 Reset Sink 的信号属性 从该图中我们可以看出,Reset Sink 有 2 个属性,下面我们就对这 2 个属性分别进行介绍: (1) Associated clock:同步到该接口的时钟名称,由于我们 Associated Clock 设置的是 clock,所以这里我们显示的是 clock。 (2) Synchronous edges:表示 reset 输入需要的同步类型,None:没有需要的同步; Deassert:reset 有效时是异步,reset 无效时是同步;Both:reset 有效和无效都是同 步的。 说完了 Reset Sink,接着我们再来看下 Reset Source,Reset Source 信号类型,如表 5.4 所示。 信号 reset,reset_n reset_req 宽度 1 1 表 5.4 Reset Source 信号类型 方向 必选 描述 Output Yes 复位信号,reset 高电平有效;reset_n 低电平有效 Output Optional 复位请求信号 看完了 Reset Source 信号类型,我们再来看下 Reset Source 信号属性,如图 5.5 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 243 图 5.5 Reset Source 的信号属性 从该图中我们可以看出,Clock Source 有 4 个属性,下面我们就对这 4 个属性分别进行介 绍: (1) Associated clock:同步到该接口的时钟名称,由于我们 Associated Clock 设置的是 clock,所以这里我们显示的是 clock。 (2) Associated direct reset:通过一对一的连接直接驱动这个 reset source 的 reset 输入 的名字。 (3) Associated reset sinks:指定最后将引起 reset source 有效 reset 的 reset 输入 (4) Synchronous edges:表示 reset 输入需要的同步类型,None:没有需要的同步; Deassert:reset 有效时是异步,reset 无效时是同步;Both:reset 有效和无效都是同 步的。 5.1.3 Avalon-MM 说完了 Avalon Reset,接下来我们再来看看 Avalon-MM,Avalon-MM 接口是一种交换式 总线,具有良好的数据交换特性和很高的总线带宽。由于 Avalon-MM 接口是针对 Qsys 设计的, 所以 Avalon-MM 接口具有结构简单,采用全同步时序,以及可以灵活地配置等特点,其运行时 钟、总线位宽、各个接口位宽以及各个外设之间的互联特性等都可以灵活地配置。Avalon-MM 总线的传输方式是一种主从式的传输方式,即由一个主控端外设发起并控制传输过程,而从属端 外设响应经由总线模块发来的信号完成整个传输。 Avalon-MM 接口与共享式的总线接口是有本质区别的,共享式总线只有一个总线数据通路, 可以看做是铁路轨道,在任何时刻只由一个主控端占有总线模块,其他主控端必须等待该主控端 放弃总线后才能获取总线控制权进行数据传输,也就是说,当一个火车在轨道上行驶的时候,就 http://www.fpga.gs/ 244 软核演练篇 §5 不可以有另一个火车同时使用轨道,否则就会发生事故。交换式总线与此不同,可以将它看作是 高速公路,它在任意的一个主控端和一个从属端之间都可以有一条数据通路,只要访问的从属端 不同,多个主控端可以同时进行数据传输。也就是说,你开一个车从你家到别人家里。另一个人 也可以从他家到另外一个人家里。并不是说,你用了高速公路,就不允许别人用了。这种接口方 式,不会因为总线被占据而延误传输事件。当然,如果当你和另一个人都需要去同一个人家里的 时候,那么我们就需要做一些仲裁了,否则,就要撞车。Avalon-MM 总线模块内部的数据通路 连接结构,如图 5.6 所示。 Connected to Nios CPU Connected to DMA Controller System Interconnect Fabric Arbitrator to Flash Arbitrator to SDRAM Arbitrator to SRAM 图 5.6 Avalon-MM 总线模块内部结构示意图 从图中我们可以看出,总线的每个 Slave(从)端都具有一个仲裁单元,如果有多个 Master (主)端都连接同一个 Slave 端传输时,那么就会由这个仲裁模块来决定 Master 端获取 Slave 端传输控制权。知道了什么是 Avalon-MM 接口后,我们在来看看 Avalon-MM 接口信号类型, Avalon-MM 接口信号类型,如表 5.5 所示。 信号类型 address byteenable byteenable_n debugaccess read read_n readdata response write,write_n writedata 表 5.5 Avalon-MM 信号类型 宽度 方向 描述 基本信号 1~64 主→从 读写操作地址 2,4,8,16,32,64,128 主→从 字节使能信号 1 主→从 允许正常写保护的内存被写入 1 主→从 读信号 8,16, 32,64,128, 256,512,1024 2 1 8,16, 32, 64,128, 从→主 读出去的数据 从→主 主→从 主→从 响应信号 写信号 写进来的数据 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 245 lock waitrequest waitrequest_n readdatavalid readdatavalid _ n burstcount Beginbursttransfer 256,512,1024 1 1 等待信号 主→从 从→主 信号锁 表示无法接受新的读写操作 流水处理信号 1 从→主 返回数据有效信号 1 ~11 1 突发处理信号 主→从 互连→从 显示需要突发的数据数量 突发处理开始信号 看完了 Avalon-MM 信号类型,我们再来看下 Avalon-MM 信号属性,如图 5.6 所示。 图 5.6 Avalon-MM 信号属性 从该图中我们可以看出,Avalon-MM 信号属性有很多,下面我们就对它们分别进行介绍: (1) Address units:为地址指定单元。字节或字寻址都是有效的。符号典型地为一个字节。 http://www.fpga.gs/ 246 软核演练篇 §5 (2) Associated clock:同步到该接口的时钟名称,由于我们 Associated Clock 设置的是 clock,所以我们这里显示的是 clock。 (3) Associated reset:同步到该接口的复位名称,由于我们的 Associated Reset 设置的 是 reset,所以我们这里显示的是 reset。 (4) Bits per symbol:定义每个符号位数。 (5) Burstcount units:这个属性为 burstcount 信号指定单位。对于 symbols,在突发中, burstcount 值可以看作 symbols 的数量(字节数)。对于 words,在突发中,burstcount 值可看作 word 的数量。 (6) explicit address span:明确的地址范围。 (7) Setup:指定 address 和 data 有效时与 read 或 write 有效时之间的时间。如果我们设 置的是 2,那么该时序图,如图 5.7 所示。 图 5.7 Setup 等于 2 的时序图 从该图中我们可以看出,当 address 和 writedata 有效时,write 等待了两个 2 时钟周期才 有效。 (8) Read wait:给不用 waitrequest 信号的接口。readWaitTime 表示从设备接收读命令之 前 的 周 期 数 或 纳 秒 数 , 这 个 时 间 就 像 从 设 备 readWaitTime 个 周 期 或 纳 秒 后 使 waitrequest 信号有效。如果我们设置的是 2,那么该时序图,如图 5.8 所示。 图 5.8 Read wait 等于 2 的时序图 从该图中我们可以看出,当 read 和 address 有效后,readdata 等待了 2 个时钟周期才返 回有效数据 D0。 (9) Write wait:给不使用 waitrequest 信号的接口。writeWaitTime 表示从设备接受写之前 的 周 期 数 或 纳 秒 数 。 这 个 时 间 就 像 从 设 备 writeWaitTime 个 周 期 或 纳 秒 后 使 waitrequest 信号有效,如果我们设置的是 2,那么该时序图,如图 5.9 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 247 图 5.9 Write wait 等于 2 的时序图 从该图中我们可以看出,当 write、address 有效后,writedata 等待了 2 个时钟周期才将接 收有效数据 D0。 (10) Hold wait:指定在 write 的失效时与 address 和 data 失效时之间的时间(仅应用于写), 如果我们设置的是 2,那么该时序图,如图 5.10 所示。 图 5.10 Hold wait 等于 2 的时序图 从该图中我们可以看出,当 write 失效后,address 和 writedata 等待了 2 个时钟周期才失 效。 (11) Timing units:为 Setup,Hold Time , Write Wait 和 Read Wait 指定单位。给同步设 备使用周期,异步设备使用纳秒。 (12) Read latency:对固定时延的 Avalon-MM slaves 读时延。不要在包含 readdatavalid 信号的接口上使用。如果我们设置为 2,那么该时序图,如图 5.11 所示。 图 5.11 Read latency 等于 2 的时序图 从该图中我们可以看出,当 read 和 address 有效时,readdata 等待了两个时钟周期才返 回有效数据 D0。 (13) Maximum pending read transactions:从设备:这个参数是从设备在等候未读的队列 最大数;主设备:这个参数是主设备生成的未解决的读事件数量。 (14) Burst on burst boundaries only:若为真,Burst 开始传递到这个接口在 bytes 中数倍 http://www.fpga.gs/ 248 软核演练篇 §5 burst 大小的地址。 (15) Linewrap bursts:有些存储设备执行限制的 burst 而不是增加的 burst。当限制的 burst 到达 burst 边界时,地址将限制回到以前的 burst 边界。只需要低阶位地址计数。例如, 限制的 burst 到达地址 0xC 时,burst 边界每 32 个字节穿过一个 32 位的接口,写入 下列地址:0xC、0x10、0x14、0x18、0x1C、0x0、0x4、0x8。 一个 Avalon-MM 外设可以包含任意的信号类型,这取决于它与外设逻辑接口的需求,但外 设的每个信号都要指定一个有效的 Avalon-MM 信号类型,以确定该信号的作用。Avalon-MM 信号类型可以分为从端口和主端口信号两类,这取决于 Avalon-MM 端口是主端口还是从端口。 对于某些信号类型,主端口和从端口中可能都包含,但由于端口类型不同、这些信号的行为可能 有所不同。每个单独的主端口或从端口使用的信号类型由外设的设计决定。Avalon-MM 主端口 或从端口的每个信号都准确地对应与一种 Avalon-MM 信号类型。对于每种信号类型,AvalonMM 端口都只能具有一个信号类型。Avalon-MM 接口信号可配置,对于特定的 Avalon-MM 外 设,并不是所有 Avalon-MM 信号都必须用到,外设设计者可以根据需要只使用必须的信号类型, 从而降低系统的复杂性。例如,一个只用于输出的 16 位的通用 I/O 外设,如图 5.12 所示。 Avalon-MM Interface (Avalon-MM Slave Interface) Avalon-MM Peripheral writedata[15..0] D pio_out[15..0] Q Application - Specific Interface write CLK_EN clk 图 5.12 只用于输出的 16 位通用 I/O 外设 从该图中我们可以看出,这个简单的 Avalon-MM 外设只写信号和写数据信号,没有用到读 信号和读数据信号。说完了 Avalon-MM 信号类型,下面我们再来看下 Avalon-MM 传输时序, Avalon-MM 的传输定义为外设(peripheral)与 Avalon-MM 总线模块间的数据传输,分为 Mster 端传输和 Slave 端传输两类,每类传输又分为基本(fundamental)传输、流水线(pipelined) 传输、突发(burst)等等传输。所有的 Avalon-MM 传输都基于基本传输,其他传输形式都是在 该传输模式下加以改进或增加某些特性以适应不同需要。一个 Master 端传输和一个对应的 Slave 端传输即可完成两个外设通过总线模块进行的一次数据传输,但 Master 端传输与 Slave 端传输的模式并不要求一致,两端传输模式可以随意搭配。同种类型的 Master 端传输与 Slave 端传输在时序上基本是一致的,其区别仅在于 Master 端传输是由 Master 端外设驱动总线模块, 而 Slave 端传输是由总线模块驱动 Slave 端外设。下面我们就以 Avalon-MM Slave 端写基本传 输时序为例进行详细的讲解,如图 5.13 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 249 图 5.13 Avalon-MM Slave 端写基本传输时序图 这个时序图可以说已经很简单的,相信大家都能够看的懂,为了大家能够进一步理解和记忆, 我们就不再给大家一步步解读时序了,我们将换一个轻松加愉快的气氛给大家进行讲解,上面的 时序图其实是这么一种情况。Avalon-MM 是一个比较会使唤人的千金大小姐,她总是喜欢使唤 我们去帮她做事情,每当 write 这个信号有效后,MM 就会向我们提出要求,要我们去指定的抽 屉里面放点东西,当然,MM 的命令我们是要执行的,我们需要在 1 天内把东西放进指定的抽屉 内才算完成 MM 的任务,如果 1 天内没有完成任务,那么 MM 就要翻脸了。说完了 Avalon-MM Slave 端写基本传输时序,下面我们再来看下 Avalon-MM Slave 端固定延时的写基本传输时序 图,如图 5.14 所示。 图 5.14 Avalon-MM Slave 端固定延时的写基本传输时序图 该图表示,MM 给我们的地址太过遥远,1 天内我们根据完成不了,因此我们通过设置 Write wait 来延长了 2 天时间供我们完成任务。说完了 Avalon-MM Slave 固定延迟的写基本传输时 序,下面我们再来看下 Avalon-MM Slave 流水线读传输时序,如图 5.15 所示。 图 5.15 Avalon-MM Slave 流水线读传输时序图 这种情况只适用于拿东西,不适合放东西。这个 MM 是一个典型的女汉子,她没耐心等你 把东西拿来才发出下一个要求。她会一直发要求,虽然她并不知道可能会花多少时间去拿来。所 以她可以每个时钟都发出拿东西的请求,告诉你地址,你就不断的去拿。由于可能回来的时间是 http://www.fpga.gs/ 250 软核演练篇 §5 不同的,所以你需要提醒一下她东西来了,所以当我们把东西拿来的时候,我们同时给她一个 readdatavalid 的牌子。这样她就知道她要的东西来了。在这里我们需要一个前提,就是东西是 一样一样去拿的,换句话说就是后发出的请求和回来的东西的顺序一定是相同的。否则后到的地 址,先拿来东西,MM 就要暴走打人了。当然,这样效率会高很多,但是我们不能宠着她,我们 需要有一点反抗精神。如图 5.16 所示。 图 5.16 可变延迟的 Avalon-MM Slave 流水线读传输时序图 从该图中我们可以看出,当我们很忙的时候,或者想罢工的时候,我们就毫不留情的给她一 个 Waitrequest 的牌子说,等着,我现在没空。在这段时候,对她的要求不予理睬,让她眼巴巴 的 等 着 。 我 们 需 要 注 意 的 是 , 在 流 水 线 模 式 中 我 们 需 要 设 置 : Maximum pending read transactions,它是对模块的一个附加说明。说明这个模块最大能接收的流水量,也就是说最大 可以容忍的 MM 的要求,超过这个要求的话,那只好说对不起了。 说完了 Avalon-MM Slave 可变延迟的流水线读传输时序图,下面我们再来看下 AvalonMM Slave 写突发传输时序,如图 5.17 所示。 图 5.17 Avalon-MM Slave 写突发时序图 这是一个比较内向的 Avalon-MM,她不喜欢不厌其烦的告诉每次操作的抽屉位置,她只是 告诉你第一次的位置,和希望放多少东西就好了。在突发写时,会有一个 beginbursttransfer 命 令,随着这个命令,她会告诉你地址,数据,要求突发的数量(burstcount)。Waitrequest 对 beginbursttransfer 无效,她不管你是不是给她那个牌子,她会告诉你她需要多少的信息。但是 地址,burstcount,写信号以及写的东西,必须要保持到 waitrequest 撤销的时候。在第一个数 据以后的传递中,她不再需要告诉你地址和数量,只要告诉你放( write)和要放什么东西 (writedata)就好了,你就应该很自觉自愿的从命。最后我们在给出 Avalon-MM Slave 读突发 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 251 传输时序,如图 5.18 所示。 图 5.18 Avalon-MM Slave 读突发时序图 从该图中我们可以看出,读突发与写突发基本相似,拿东西的指令也是由 beginbursttransfer 发出的。依然告诉你读的初始地址。但是在读的时候,不需要等到数据返回,可以继续发起下一 次操作。而拿回来东西的时候,还需要给一个 readdatavalid 的牌子告诉一下。 5.1.4 Avalon Interrupt 说完了 Avalon-MM,接下来我们再来看看 Avalon Interrupt,Interrupt Sender 信号类型, 如表 5.6 所示。 信号类型 irq,irq_n 表 5.6 Interrupt Sender 信号类型 宽度 方向 必选 描述 1 Output Yes 中断请求。当从器件需要接收时使 irq 有效 看完了 Interrupt Sender 信号类型,我们再来看下 Interrupt Sender 信号属性,如图 5.19 所 示。 http://www.fpga.gs/ 252 软核演练篇 §5 图 5.19 Interrupt Sender 信号属性 从该图中我们可以看出,Interrupt Sender 有 3 个属性可以设置,下面我们就对这 3 个属性 分别进行介绍: (1) Associated addressable interface:提供访问寄存器来接收中断的 Avalon-MM 从接 口的名称。 (2) Associated Clock:同步到该接口的时钟名称,由于我们 Associated Clock 设置的是 clock,所以这里我们显示的是 clock。 (3) Associated Reset:同步到该接口的复位名称,由于我们 Associated Reset 设置的是 reset,所以这里我们显示的是 reset。 说完了 Interrupt Sender,接着我们再来看下 Interrupt Receiver,Interrupt Receiver 信号 类型,如表 5.7 所示。 信号类型 irq 宽度 1~32 表 5.7 Interrupt Receiver 信号类型 方向 必选 描述 irq 是 n 位的矢量,每个位直接对应一个 IRQ 发送 Input Yes 器,没有优先级的内在呈现 看完了 Interrupt Receiver 信号类型,我们再来看下 Interrupt Receiver 信号属性,如图 5.20 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 253 图 5.20 Interrupt Receiver 信号属性 从该图中我们可以看出,Interrupt Sender 有 4 个属性可以设置,下面我们就对这 4 个属性 分别进行介绍: (1) Interrupt scheme:每个中断发送器接口使它的 irq 信号有效,以请求接收。 (2) Associated addressable interface:Avalon-MM 主接口的名字用于接收在这个接口上 接收的中断 (3) Associated Clock:同步到该接口的时钟名称,由于我们 Associated Clock 设置的是 clock,所以这里我们显示的是 clock。 (4) Associated Reset:同步到该接口的复位名称,由于我们 Associated Reset 设置的是 reset,所以这里我们显示的是 reset。 最后我们再补充说明一点,Avalon-MM 主器件在优先级 1 中断之前接收优先级 0 中断,如 图 5.21 所示。 clk Individual Requests int0 int1 1 2 图 5.21 优先级编码的中断时序图 从图中我们可以看出,Interrupt Receiver 接口在时序 1 接收 int0,在时序 2 接收 int1。 5.1.5 Avalon-ST 说完了 Avalon Interrupt,接下来我们再来看看 Avalon-ST,Avalon-ST 是一种单向点对点 的高速接口,主要针对的是高速数据流的传输。由于其同样是针对 Qsys 开放而设计的,因此也 具有灵活的可配置性。Avalon-ST 接口与 Avalon-MM 接口的区别非常明显,Avalon-MM 接口 用于 Qsys 控制流传输或者简单的数据流传输,而 Avalon-ST 接口则用于 Qsys 设计中高速数 http://www.fpga.gs/ 254 软核演练篇 §5 据流的传输,更多的适用于一些传递速度要求比较高,没有地址需求对的应用方面,比如说一些 DSP,包处理方面的应用,FIR,FFT 什么的,更多的是对数据的一种传递。Avalon-ST 接口可 以被用户用来作为各组件的数据通信接口,该接口具有高带宽、低延时和非双向的特点。典型应 用场合包括点对点的传输、多通道的传输、包传输,以及自动的接口调整等。Avalon-ST 接口信 号可以被描述成传统的数据流接口,即支持传输单个数据流而不需关心通道数和数据包的大小 范围。接口也支持更加复杂的协议,包括突发传输,和数据包在多个通道间交错传输等功能。 知道了什么是 Avalon-ST 接口后,我们在来看看 Avalon-ST 接口信号类型,Avalon-ST 接 口信号类型,如表 5.8 所示。 信号类型 channel data error ready valid empty endofpacket startofpacket 宽度 0-128 1-4096 1-256 1 1 1-8 1 1 表 5.8 Avalon-ST 信号类型 方向 描述 源→目标 定 义 了 本 周 期 发 送 数 据 的 通 道 数 。 如 果 一 个 接 口 支 持 channle 信号,则必须定义 maxChannel 参数。 源→目标 数据信号从源端口发送到目标端口,一般的数据包从通过 data 信号发送。data 信号的内容和格式将在后面的参数中定 义。 源→目标 二进制位组合的形式,用来标记本周期正在传输的数据中的 错误。error 中的单个位对应的错误由模块的 errorDescriptor 属性定义。 目标→源 高时,表明目标端口可以接收数据。目标端口在周期拉高 ready 信号,表示周期为准备完成的周 期。在此期间,源端口可以使 valid 有效并传输数据。源端口 在没有 ready 信号输入时不能被反馈。同样的,目标端口在 没有 ready 信号输出时不能发送反馈。 源→目标 valid 信号置高表示源端到目的端的信号是有效的。在 vaild 刚被置高后的准备周期,目的端采集数据总线和其他源端到 目的端的信号,而在其他周期时候这些信号将会被忽略。 源→目标 数据包发送结束的周期指示本数据包中的空符号的数目。在 一次传输中如果只有一个符号,empty 信号是无用的。如果 endofpacket 没有置高,本信号无效。 源→目标 源端口置高表明数据包的结束 源→目标 源端口置高表明数据包的开始 这里我们需要注意的是,在 Avalon-ST 总线的源端口和目标端口的每个接口信号对应一个 Avalon-ST 的信号类型。Avalon-ST 的接口可能只包含一个实例模块的信号。所有的 AvalonST 的信号类型在源端口和目标端口的含义都是相同的。看完了 Avalon-ST 信号类型,我们再来 看下 Avalon-ST 信号属性,如图 5.22 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 255 图 5.22 Avalon-ST 信号属性 从该图中我们可以看出,Avalon-ST 信号属性有很多,下面我们就对它们分别进行介绍: (1) Data bits per symbol:在每个有效周期上传递的符号数。 (2) Associated Clock:同步到该接口的时钟名称,由于我们 Associated Clock 设置的是 clock,所以我们这里显示的是 clock。 (3) Associated Reset:同步到该接口的复位名称,由于我们的 Associated Reset 设置的 是 reset,所以我们这里显示的是 reset。 (4) Error descriptor:描述 error 和 error 信号的每个位有关的字的列表。列表的长度必须 与在 error 信号中的位数相同。列表中的第一个字应用于最高的位。例如,“ crc , overflow "意思是 error 的位 1 表示 CRC 错误。位 0 表示溢出错误。 (5) First Symbol In High-Order Bits:若为真,第一级符号驱动到数据接口的最高有效位。 最高位符号在本规范中标注为 D0。当这个属性设为假时,第一个符号出现在低位上。 D0 出现在 data[7:0]。 (6) Maximum channel:数据接口支持的最大通道数。 (7) Ready latency:定义 ready 信号有效和失效之间的关系,以及认定对数据传递有效的 周期。Ready latency 为每个接口分别定义。 说完了 Avalon-ST 信号类型,下面我们再来看下 Avalon-ST 传输时序,Avalon-ST 传输 分为基本传输、包传输以及元件指定传输等三种传输方式,其中基本传输是其他传输的基础,通 过增加若干控制信号,包传输可以允许更高级别的传输控制,而元件指定的传输则可以按照组件 自身的需求来自定义传输的过程。Avalon-ST 听起来很高端,感觉上会很难,其实它很简单,首 先我们给出基本传输连接示意图,如图 5.23 所示。 http://www.fpga.gs/ 256 软核演练篇 §5 Source Source_data Source_valid Sink Sink_data Sink_valid 图 5.23 Avalon-ST 基本传输连接示意图 从该图中我们可以看出,一个接口的两个部分。我们有一个 Source 和一个 Sink 部分, Source 是源,数据从这个接口发出,然后由 Sink 来接受。数据就是从 data 这个信号发出来的, 而 valid 信号表示当前这是一个有效信号。下面我们给出该基本传输的时序图,如图 5.24 所示。 图 5.24 Avalon-ST 基本传输时序图 从该图中我们可以看出,这个时序很简单,当 Source_valid 有效时,Source 端就会将 Source_data 中的数据发送给 Sink 端,当 Sink_valid 有效时,Sink 端就会将 Source 端发送的 Source_data 数据读取到 Sink_data 中。当然 Sink 并不是什么时候都可以接受数据的,所以它 可以通过 ready 信号告诉 Source,我有没有准备好接受数据,如图 5.25 所示。 Source Source_data Source_valid Source_ready Sink Sink_data Sink_valid Sink_ready 图 5.25 带有 ready 信号的基本传输连接示意图 从该图中我们可以看出,这样的效果就是可以一直把信息传递到前面,然一切操作可以停止 下来。这里面有一个非常重要的参数 ReadyLatency,这代表说,在这个 ready 信号有效后几个 时钟 Sink 可以接受数据。我们分两种情况来看: (1) ReadyLatency 等于 0 的时候,在 ready 为高的同时 Sink 就可以开始接受数据。即便 在 ready 为低的时候,Source 依然可以把 valid 设为高,但是这个时候,Sink 是不接 受数据的,只有当 ready 和 valid 同时为高的时候,Sink 才接受数据。如果我们设置的 是 0,那么该时序图,如图 5.26 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 257 图 5.26 带有 ready 信号,Ready latency=0 的基本传输时序图 从该图我们可以看出,只有当 ready 和 valid 信号同时有效的时候,Sink 才会接收 Source 发送的数据。 (2) ReadyLatency 不是 0 的时候。代表 ready 为高以后的几个时钟才能接受数据。这个 状况下,Source 必须严格控制 valid 信号。在 ready 信号为低,以及为高的前几个时 钟 内 ( readlatency ) 都 不 得 为 高 。 而 在 ready 信 号 由 高 变 低 的 几 个 时 钟 内 (readlatency),valid 信号还是可以为高的。 这样,Sink 端口就只需要监视 valid 信号就 好了,控制上会更简单一些。如果我们设置的是 2,那么该时序图,如图 5.27 所示。 图 5.27 带有 ready 信号,Ready latency=2 的基本传输时序图 从该图中我们可以看出,当 ready 信号有效时,Valid 信号等待了 2 个时钟周期才有效,此 时 Sink 才会接收 Source 发送的数据。说完了 Avalon-ST 的基本传输,下面我们再来看看 Avalon-ST 的包传输,在很多应用中,我们都会用到包这个概念,在 Avalon-ST 中,我们用 SOP 和 EOP 来指示一个包。SOP 就是 Start of Packet,EOP 就是 End of Packet,其连接示意图, 如图 5.28 所示。 Source Source_data Source_valid Source_ready Source_SOP Source_EOP Sink Sink_data Sink_valid Sink_ready Sink_SOP Sink_EOP 图 5.28 Avalon-ST 的包传输连接示意图 从该图中我们可以看出,包传输是在我们基本传输的基础上多添加了两个信号,SOP 表示 http://www.fpga.gs/ 258 软核演练篇 §5 数据包的开始,EOP 表示数据包的结束,其对应的时序图,如图 5.29 所示。 图 5.29 Avalon-ST 的包传输时序图 从该图中我们可以看出,startofpacket 在所有的支持包传输接口中都是必须的,以确定包头 数据传输的时钟周期。该信号的值仅在 valid 信号拉高时有意义。endofpacket 在所有的支持包 传输接口中都是必须的,以确定包尾数据传输的时钟周期。该信号的值仅在 valid 信号拉高时有 意义。startofpacket 和 endofpacket 信号可能在同一时钟周期拉高。当两次包传输之间没有空 闲周期时,startofpacket 信号可以紧跟前一次包传输的 endofpacket 信号。为了把故事讲完整 了,把其他信号也加进来,如图 5.30 所示。 图 5.30 Avalon-ST 的完整时序图 从该图中我们可以看出,该时序图比包传输多出了 3 个信号,empty 为可选信号。表明最后 一个数据包发送时,数据线上有效符号的数目。目的端口仅在 endofpacket 信号为高时检测 empty 信号值。空符号总是数据流的最后几个符号,即在低位数据线上传输。当接口 data 信号 宽度超过一个符号的宽度时,有效的数据宽度是变化的,这样的接口是必须使用 empty 信号。 channel 是源端口发向目标端口的可选信号,表明本次数据属于哪个通道。对于一个给定的端口, channel 的意义取决于应用程序:一些应用程序使用 channel 作为端口号,而其他应用程序使用 channel 作为页编号或者时间码或时间编号。源端口可能在一个活动周期内改变 channel 的值。 接口如果使用 channel 信号,则必须定义 Maximum channe 参数,以确定最大的通道数。如果 组件在运行时接口改变了 channel 值,Maximum channel 是接口支持的 channel 信号的最大值。 error 信号的每一个比特位对应一个可能的错误状态。error 为 0 时表明本周期的数据没有错误。 这个信号可以灵活运用,并不是一定要用来传递错误信息的。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 259 5.1.6 Avalon-TC 说完了 Avalon-ST,接下来我们再来看看 Avalon-TC,Avalon-TC 是点到点接口,为驱动 外围组件的片上控制器设计。这个接口允许通过多重三态设备共享数据,地址和控制引脚。在有 多重外围存储设备的系统中共享节约引脚。Avalon-TC 接口用两步来约束一般的 Avalon 导管接 口: (1) Avalon-TC 需要 request 和 grant 信号。当多重三态导管主器件(TCM)请求使用共 享总线,这些信号使能总线仲裁。 (2) 必须使用后缀附加到信号的角色来定义信号的引脚类型。这三个后缀是_out ,_in , 和_outen。匹配的前缀确认信号共用相同的 I/O 引脚。 下面我们给出 Avalon-TC 主从接口的典型使用和信号命名,如图 5.31 所示。 Altera FPGA TristateConduitMaster TristateConduitSlave Avalon-MMSlave Avalon-MM Master Controller for 2MByte x32SSRAM clock TristateConduit PinSharer chipselect_out addr_out_[20:0] dataout_outen dataout_out[31:0] data_reinad[3_1o:0u]t write_out request grant Arb Controller for8MByte x16Flash request grant addr_out_[22:0] dataout_outen dataout_out[15:0] data_in[15:0] read_out write_out chipselect_out irq_in TristateConduit Bridge request grant chipselect_out addr_out data_outen data_out data_in read_out write_out chipselect_out irq_in 图 5.31 Avalon-TC 主从接口的典型使用和信号命名 下面我们给出 Avalon-TC 信号类型,如表 5.9 所示。我们需要注意的是,所有 Avalon-TC 信号都用于主从器件,并且对主从器件都有相同的意义。 http://www.fpga.gs/ 260 软核演练篇 §5 信号类型 request grant _in _out _outen 宽度 1 1 1~1,024 1~1,024 1 表 5.9 Avalon-TC 信号类型 方向 需求 Description request 的意义取决于 grant 信号的状态。当 request 有效,grant 失效时,request 是在当前周期请求访 问。当 request 有效,grant 有效时,request 是在 下一周期请求访问。因此,request 将在访问的最后 一个周期失效。Request 在总线访问的最后一个周 主→从 Yes 期失效。在随后的传递的最后一个周期立即再次有 效。如果没有其它主器件请求访问,这个协议合理的 使再仲裁和连续总线访问。一旦有效,request 必须 保持到 grant 有效。因此,最短的总线访问是 2 个 周期。 当 grant 有效时,表示三态导管主器件被允许访问 执行处理。grant 在响应 request 信号中有效。保持 从→主 Yes 有效直到 1 个周期之后 request 失效。在没有主器 件请求时,Avalon-TC 接口的设计不允许一个默认 的 Avalon-TC 主器件被 granted。 从→主 No 一个逻辑三态信号的输入信号。 主→从 No 一个逻辑三态信号的输出信号。 主→从 No 为一个逻辑三态信号的输出使能。 图 5.32 为一个 Avalon-TC 时序图,下面我们根据时序图来解释对三态导管引脚分配器的 仲裁时序,注意一个设备在 grant 周期能驱动和接收有效数据。 1 clk request grant data_out[31:0] 23 4 5 6 7 8 9 10 11 12 13 14 15 16 17 b c d e f 10 11 12 13 14 15 16 17 图 5.32 Avalon-TC 时序图 从该图中我们可以看出,当 clk 为 1 时,Avalon-TC 主端口 grant 有效,从器件在周期 1 和 2 中驱动有效数据。当 clk 为 4 时,Avalon-TC 主端口 grant 有效,从器件在周期 5~8 中驱动有 效数据。当 clk 为 8 时,Avalon-TC 主端口 grant 有效,从器件在周期 10~16 中驱动有效数据。 当 clk 为 3,4 和 9 时,是不包含有效数据的。 5.1.7 Avalon Conduit 说完了 Avalon-TC,接下来我们再来看看 Avalon Conduit,Avalon Conduit 信号类型,如 表 5.3 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 261 信号 表 5.10 Avalon Conduit 信号类型 宽度 方向 描述 一个导管接口由任意宽度的一个或更多输入,输出或双向信 In,Out,or 号组成。导管可以有用户指定的任意角色。你可以在 Qsys 系 bidirectional 统内连接兼容的导管接口,提供角色,匹配的宽度和相反的方 向。 看完了 Avalon Conduit 信号类型,我们再来看下 Avalon Conduit 信号属性,如图 5.33 所 示。 图 5.33 Reset Sink 的信号属性 从该图中我们可以看出,Reset Sink 有 2 个属性,下面我们就对这 2 个属性分别进行介绍: (1) Associated Clock:同步到该接口的时钟名称,由于我们 Associated Clock 设置的是 clock,所以这里我们显示的是 clock。 (2) Associated Reset:同步到该接口的复位名称,由于我们 Associated Reset 设置的是 reset,所以这里我们显示的是 reset。 §5.2 LED外设 5.2.1 LED IP 核的定制 学完了 Avalon 总线,接下来我们就可以为我们的外设定制 IP 核了,在我们 A4 开发板所有 的外设中,最简单的外设要属我们的 LED 外设了,为了能够让大家更好的理解 IP 核的制作流 程,我们就以最简单的 LED 外设为例,。由于我们是初次制作 IP 核,对 IP 核的制作步骤并不了 解,所以,在开始制作 IP 核之前,我们先来讲一讲 IP 核的制作流程,一个典型 IP 核的制作流 程主要分为以下六个步骤: (1) 规划 IP 核的硬件功能; http://www.fpga.gs/ 262 软核演练篇 §5 (2) 定义一个恰当的 Avalon 接口; (3) 使用硬件描述语言描述硬件逻辑; (4) 使用 IP 核编辑器封装硬件逻辑,完成 IP 核定制; (5) 编写用于描述寄存器的 C 头文件和 IP 核的驱动 C 文件; (6) 让 Nios SBT for Eclipse 自动抓取 IP 核的 HAL 知道了 IP 核的制作流程,接下来我们可以跟着制作流程一步步往下走,手把手教你定制最 基本的 LED IP 核。 (1) 规划 IP 核的硬件功能 首先我们的第一步规划 IP 核的硬件功能,如何规划 LED IP 核的硬件功能呢?如果是凭空 想象,也许会很难,不过我们可以依葫芦画瓢呀,如果你还记得我们之前讲过的 PIO IP 核,那 么你也许会眼前一亮,我们知道 PIO IP 核,它有数据寄存器、方向寄存器、中断屏蔽寄存器、 边沿捕获寄存器等等,这里我们也可以装模作样的给我们的 LED IP 核规划两个寄存器,一个是 控制寄存器,一个是数据寄存器。下面我们将两个寄存器列出,如表 5.11 所示。 偏移量 00 01 寄存器名称 控制寄存器 数据寄存器 表 5.11 LED IP 核寄存器映射 操作 31 … 位描述 8 7 写 保留 写 保留 … 0 EN DATA 从表中我们可以看出,控制寄存器用了 1 位,数据寄存器用了 8 位,下面我们就对这两个寄 存器分别进行介绍:  控制寄存器:是用来控制我们的 LED 外设使能的,当我们控制寄存器 EN 为 0 时,我 们的 LED IP 核使能关闭,不管我们的数据寄存器接收到怎样的数据,我们的 LED 都 不会点亮,当我们的控制寄存器 EN 为 1 时,我们的 LED IP 核使能打开,我们的 LED 将会随数据寄存器中接收的值而改变。  数据寄存器:是用来控制我们 LED 外设亮灭的,我们 LED 外设总共有 8 个,而我们 数据寄存器也刚好定义了 8 位,因此每一位代表着一个 LED。0 表示亮,1 表示灭。 (2) 定义一个恰当的 Avalon 接口 规划完 IP 核的硬件功能,接下来我们就可以进入第二步,就是定义一个恰当的 Avalon 接 口。如何定义一个恰当的 Avalon 接口?这将会用到我们前面所讲解的 Avalon 总线规范知识了, 在 Avalon 总线规范讲解中我们知道,Avalon 总线规范定义了七个接口,它们分别是:Clock、 Reset、Avalon-MM、Avalon-ST、Interrupt、Avalon-TC 和 Conduit。在我们的 LED IP 核中 我们将会用到其中的四个接口,它们分别是 Clock、Reset、Avalon-MM 和 Conduit,如表 5.12 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 263 信号名 clock reset address write writedata led 表 5.12 LED IP 核的接口信号 Avalon 信号类型 宽度 Clock 1 Reset 1 Avalon-MM 2 Avalon-MM 1 Avalon-MM 32 Conduit 8 方向 Input Input Input Input Input Output 从表中我们可以看出,clock 就是为我们的 LED IP 核时钟信号,reset 就是为我们的 LED IP 核复位信号,address 是我们 LED IP 核的地址信号,write 是我们 LED IP 核的写使能信号, writedata 是我们 LED IP 核的写数据信号,led 是用来连接我们 LED 外设管脚的。它们的时序 如图 5.34 所示。 图 5.34 LED IP 核的信号接口时序图 从该图中我们可以看出,当时钟信号 clk 为上升沿,且写使能 write 为高时,地址信号 address 和写数据信号 writedata 才有效。 (3) 使用硬件描述语言描述硬件逻辑 定义完了 Avalon 接口,接下来我们就可以进入第三步,使用硬件描述语言描述硬件逻辑。 在开始描述 LED IP 核的硬件逻辑之前,我们先来看下一个典型的 IP 核的硬件逻辑。一个典型 的 IP 核的硬件逻辑由以下三个功能模块组成:  接口文件:作为顶层模块,定义总线接口信号;  寄存器文件:完成该 IP 核与外部信号进行通信,有了寄存器文件,用户就可以通过 Avalon 接口采用基地址+地址偏移量的方式来访问组件内部各寄存器。  硬件逻辑文件:实现 IP 核的硬件功能; 知道了这三个功能模块后,接下来我们就来编写这个三个功能模块,首先我们需要在 C:\altera\13.1\ip 路径下新建一个文件夹用来存放我们的 IP 核文件,这里我们创建的文件夹名字 是 zircon_ip,然后我们在 zircon_ip 文件夹中创建了一个新文件夹,这里我们取名为 zircon_led。 接下来我们就需要往 zircon_led 文件夹中添加内容,首先我们添加的是 HDL 文件,它们分别是 zircon_led.v(Avalon 接口文件也可以称为顶层文件)、zircon_led_logic.v(硬件逻辑文件)、 zircon_led_register.v(寄存器文件)。创建这三个文件有很多方法,这里我们推荐使用 Quartus 来创建,我们可以创建一个以 zircon_led 为顶层文件的 Quartus 工程,在 Quartus 工程中我们 可以编写代码并编译检查代码中的错误,代码通过编译后将.v 文件复制到 zircon_ip\zircon_led 文件夹中即可。首先我们给出的是顶层文件 zircon_led.v,该文件如代码 5.1 所示。 http://www.fpga.gs/ 264 软核演练篇 §5 代码 5.1 zircon_led.v 代码 1 module zircon_led 2( 3 //时钟复位 4 csi_clk,rsi_reset_n, 5 //Avalon-MM 从端口 6 avs_address,avs_write,avs_writedata, 7 //外设管脚输出 8 coe_led 9 ); 10 11 input 12 input 13 input 14 input 15 input 16 output [ 1:0] [31:0] [ 7:0] csi_clk; rsi_reset_n; avs_address; avs_write; avs_writedata; coe_led; //系统时钟 //系统复位 //Avalon 地址总线 //Avalon 写请求信 //Avalon 写数据总线 //led 接口管脚 17 18 wire 19 wire [ 7:0] led_data; led_control; //led 数据寄存器 //led 控制寄存器 20 21 //硬件逻辑文件 22 zircon_led_logic zircon_led_logic_init 23 ( 24 .csi_clk 25 .rsi_reset_n 26 .led_data 27 .led_control 28 .coe_led 29 ); (csi_clk (rsi_reset_n (led_data (led_control (coe_led ), //系统时钟 ), //系统复位 ), //led 数据寄存器 ), //led 控制寄存器 ) //led 接口管脚 30 31 //寄存器文件 32 zircon_led_register zircon_led_register_init 33 ( 34 .csi_clk 35 .rsi_reset_n 36 .avs_address 37 .avs_write 38 .avs_writedata 39 .led_data 40 .led_control 41 ); (csi_clk (rsi_reset_n (avs_address (avs_write (avs_writedata (led_data (led_control ), //系统时钟 ), //系统复位 ), //Avalon 地址总线 ), //Avalon 写请求信 ), //Avalon 写数据总线 ), //led 数据寄存器 ) //led 控制寄存器 42 43 endmodule Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 265 从该代码中我们可以看出,该文件主要是作为顶层模块,用于连接硬件逻辑文件和寄存器文 件,代码中没有编写任何的逻辑功能。下面我们给出的是寄存器文件 zircon_led_register.v,该 文件如代码 5.2 所示。 代码 5.2 zircon_led_register.v 代码 1 module zircon_led_register 2( 3 //时钟复位 4 csi_clk,rsi_reset_n, 5 //Avalon-MM 从端口 6 avs_address,avs_write,avs_writedata, 7 //用户逻辑输入与输出 8 led_data,led_control 9 ); 10 11 input 12 input 13 input 14 input 15 input 16 output reg 17 output reg [ 1:0] [31:0] [ 7:0] csi_clk; rsi_reset_n; avs_address; avs_write; avs_writedata; led_data; led_control; //系统时钟 //系统复位 //Avalon 地址总线 //Avalon 写请求信 //Avalon 写数据总线 //led 数据寄存器 //led 控制寄存器 18 19 reg led_control_r; 20 reg [ 7:0] led_data_r; 21 22 //时序电路,用来给 led 控制寄存器进行赋值 23 always @ (posedge csi_clk or negedge rsi_reset_n) 24 begin 25 if(!rsi_reset_n) 26 led_control <= 1'b0; //判断复位 //初始化 led 控制寄存器 27 else 28 led_control <= led_control_r; 29 end //用来给 led 控制寄存器赋值 30 31 //组合电路,用来给地址偏移量等于 0 的 led 控制寄存器写数据 32 always @ (*) 33 begin 34 if((avs_write) && (avs_address == 2'b00))//判断写使能和地址偏移量 35 led_control_r = avs_writedata[0];//如果条件成立,那么将值赋值给 led 控制寄存器 36 else 37 led_control_r = led_control; //否则,将保持不变 38 end 39 http://www.fpga.gs/ 266 软核演练篇 §5 40 //时序电路,用来给 led 数据寄存器进行赋值 41 always @ (posedge csi_clk or negedge rsi_reset_n) 42 begin 43 if(!rsi_reset_n) //判断复位 44 led_data <= 8'hff; //初始化 led 数据寄存器 45 else 46 led_data <= led_data_r; //用来给 led 数据寄存器赋值 47 end 48 49 //组合电路,用来给地址偏移量等于 1 的 led 数据寄存器写数据 50 always @ (*) 51 begin 52 if((avs_write) && (avs_address == 2'b01)) //判断写使能和地址偏移量 53 led_data_r = avs_writedata[7:0]; //如果条件成立,那么将值赋值给 led 数据寄存器 54 else 55 led_data_r = led_data; //否则,将保持不变 56 end 57 58 endmodule 从该代码中我们可以看出,Avalon 地址总线(address)为 2 位,address 为 00 表示控制 寄存器,用来使能和关闭 LED 输出。Address 为 01 表示数据寄存器,用来向 LED 写入新数据。 address 为 10 和 11 位保留。Avalon 数据总线信号在 write 信号的控制下,向寄存器写入控制位 和数据位,当向控制寄存器写入数据时,其中[31:1]为保留数据,[0]位中的数据用来赋值给控制 寄存器。当向数据寄存器写入数据时,其中[31:8]为保留数据,[7:0]中的数据用来赋值给数据寄 存器。下面我们给出的是硬件逻辑文件 zircon_led_logic.v,该文件如代码 5.3 所示。 代码 5.3 zircon_led_logic.v 代码 1 module zircon_led_logic 2( 3 //时钟复位 4 csi_clk,rsi_reset_n, 5 //用户逻辑输入与输出 6 led_data,led_control, 7 //外设管脚输出 8 coe_led 9 ); 10 11 input 12 input 13 input 14 input 15 output reg [ 7:0] [ 7:0] csi_clk; //系统时钟 rsi_reset_n; //系统复位 led_data; //led 数据寄存器 led_control; //led 控制寄存器 coe_led; //led 接口管脚 16 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 267 17 reg [ 7:0] coe_led_r; //led 接口管脚的寄存器 18 19 //时序电路,用来给 led 接口管脚进行赋值 20 always @(posedge csi_clk or negedge rsi_reset_n ) 21 begin 22 if(!rsi_reset_n) //判断复位 23 coe_led <= 8'hff; //初始化 led 接口管脚为 0xff,LED 全灭 24 else 25 coe_led <= coe_led_r; //用来给 led 接口管脚赋值 26 end 27 28 //组合电路,用来实现 led 硬件逻辑功能 29 always @ (*) 30 begin 31 if(led_control) //判断 led 控制寄存器 32 coe_led_r = led_data; //如果 led 控制寄存器使能打开,允许接收 led 数据寄存器数据 33 else 34 coe_led_r = coe_led; //否则,将保持不变 35 end 36 37 endmodule 从代码中我们可以看出,该代码很简单,硬件逻辑文件实现了 IP 核的硬件功能,接收来自 Avalon 总线的数据,向 LED 写入接收到的新数据。 (4) 使用 IP 核编辑器封装硬件逻辑,完成 IP 核定制 当我们创建好了描述 LED IP 核的三个模块后,我们就可以使用 Qsys 中的组件编辑器将它 们封装成一个 IP 核了。我们打开 Qsys 软件,选择 Qsys 软件菜单栏中的【File】→【New Component…】将会出现如图 5.35 所示页面。 http://www.fpga.gs/ 268 软核演练篇 §5 图 5.35 Component Editor 页面图 在组件编辑器页面中我们给创建的 IP 核填写基本信息,填写完毕后,我们便可以点击【Next】 进入下一个页面,如图 5.36 所示。 图 5.36 添加完硬件文件后的窗口 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 269 在该图中,我们可以点击【+】按钮打开添加文件对话框,将路径指向 zircon_led 设计文件 夹所在的目录,最后选中三个.v 文件添加即可。添加完硬件文件后,Synthesis Files 栏中可看到 刚添加的 3 个文件。Top-level File 默认是 zircon_led.v,如果不是,请改正。下面我们便可以 点击【Analyze Synthesis Files】按钮来分析这三个文件,分析完毕后将会出现如图 5.37 所示 页面。 图 5.37 硬件描述文件分析完毕结果图 从该图中我们可以看到,我们的代码没有错误,如果代码中存在错误,这里分析就不会通过。 我们点击【Close】即可,这时我们会发现最下面的提示窗口中出现了一个错误,这个错误我们 会在后面中将它解决掉,这里我们就先让它存活一会。接下来我们继续点击【Next】进入下一个 页面,如图 5.38 所示。 http://www.fpga.gs/ 270 软核演练篇 §5 图 5.38 参数页面图 参数标签能够使你指定用于在 Qsys 系统中配置组件的实例的参数。这里就举个例子进行说 明:比如我们写了一个 4 位加法器 IP 核,那么我们想要修改成 16 位加法器 IP 核那么就得修改 硬件描述文件代码,那么这时,我们可以在加法器 IP 核的硬件描述文件中定义一个参数,想要 实现几位的加法器只需要修改这个参数即可实现。当我们定义了参数后,我们在创建 IP 核时, 参数标签页面就会出现这个参数,我们在使用添加这个加法器 IP 核组件时,这个参数的值是可 以直接进行修改的,不要在重新添加 IP 核组件。接下来我们点击【Next】进入下一个页面,如 图 5.39 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 271 图 5.39 设置信号页面图 在该图中我们可以看到在硬件描述文件中的 Avalon 信号管脚在这里都显示了出来,这里我 们会发现我们的系统竟然自动识别了我们想要的信号接口,之所以系统会自动识别是因为我们 这里遵守了一个规定,下面我们给出自动信号识别的接口类型前缀,如表 5.13 所示。 表 5.13 自动信号识别的接口类型前缀表 接口前缀 接口类型 asi Avalon-ST 接收器(输入) aso Avalon-ST 源(输出) avm Avalon-MM 主端口 avs Avalon-MM 从端口 axm AXI 主端口 axs AXI 从端口 coe 导管 csi 时钟接收器(输入) cso 时钟源(输出) inr 中断接收器 ins 中断发送器 ncm Nios II 定制指令主端口 ncs Nios II 定制指令从端口 rsi 复位接收器(输入) rso 复位源(输出) tcm Avalon-TC 主端口 tcs Avalon-TC 从端口 http://www.fpga.gs/ 272 软核演练篇 §5 如果使用组件编辑器之前创建组件的顶层 HDL 文件,那么组件编辑器基于源 HDL 文件中 的信号名称识别接口和信号类型。这一自动识别功能消除手动在组件编辑器中分配每个接口和 信号类型的任务。要实现这一自动识别功能,必须使用以上名称约定创建信号名称。看到这里, 读者是不是一下就能够明白了为什么我们在 Verilog 中使用的名字都那么古怪。接下来我们点击 【Next】进入下一个页面,如图 5.40 所示。 图 5.40 设置接口页面图 从该图中我们在 avalon slave 一栏中修改了 Name 和 Associated 两个选项,这时我们便能 够发现错误消失了。为了名字好看统一严谨(典型的强迫症患者),下面我们也一并将 conduit end 中的 Name 和 Associated 修改掉。下面我们便可以点击【Finish】按钮,出现如图 5.41 所 示页面。 图 5.41 保存设置并创建 zircon_led_hw.tcl 文件 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 273 这 时 我 们 可 以 看 到 Qsys 软 件 将 我 们 创 建 的 zircon_led_hw.tcl 文 件 保 存 到 了 D : \Zircon_Qsys\Qsys_Led_Ip 路径下面。之所以保存在这里,是因为我们在创建 IP 核之前已经 创建了 Quartus 工程和 Qsys 系统,如果你没有创建 Quartus 工程直接在 Qsys 软件中创建 IP 核,默认路径将会是 C:\altera\13.1,这里需要注意的是,如果默认路径是 C:\altera\13.1,那 么创建出来的 zircon_led_hw.tcl 文件我们还需要修改其中的路径,否则我们在使用时会出现错 误。这里读者可以自行尝试。我们点击【Yes,Save】按钮就完成 IP 核的定制,这里需要我们 注 意 的 是 , 为 了 方 便 IP 的 管 理 , 我 们 需 要 将 zircon_led_hw.tcl 文 件 复 制 到 C:\altera\13.1\ip\zircon_ip\zircon_led 中。 (5) 编写用于描述寄存器的 C 头文件和 IP 核的驱动 C 文件 完成 IP 核定制,我们就可以在 Qsys 软件的 IP 核库中看到我们添加的 IP 核了,也就是说, 我们已经可以使用该 IP 核了,不过,由于我们没有给该 IP 核添加寄存器头文件和底层驱动文 件,我们使用起来是极其不方便的,为了以后的使用方便,下面我们就需要给该 IP 核添加寄存 器头文件和底层驱动文件,首先我们需要做的是在\zircon_ip\zircon_led 文件夹中创建两个新文 件夹,一个为 HAL 文件夹,另一个为 inc 文件夹。如图 5.42 所示。 图 5.42 zircon_led IP 核文件夹 我们需要在\zircon_ip\zircon_led\inc 文件夹下创建一个 zircon_led_regs.h 的 C 文件,我还 需 要 在 \zircon_ip\zircon_led\HAL\inc 文 件 夹 创 建 一 个 zircon_led.h , 再 \zircon_ip\zircon_led\HAL\src 文件夹创建一个 zircon_led.c。(HAL 文件夹下的 inc 和 src 文件 夹也是需要创建的),创建好了文件以后,接下来我们要分别对这三个 C 文件进行编写代码,在 编 写 代 码 时 我 们 可 以 参 考 altera 为 我 们 已 经 提 供 好 的 IP 核 , 比 如 , 可 以 参 考 altera_avalon_pio_regs.h 文件来写我们的 zircon_led_regs.h 文件,这里我们直接给出已经编 写好的代码,首先我们给出的是 zircon_led_regs.h 文件的代码,如代码 5.4 所示。 代码 5.4 zircon_led_regs.h 代码 1 #ifndef __ZIRCON_LED_REGS_H__ 2 #define __ZIRCON_LED_REGS_H__ 3 4 #include 5 6 #define IOWR_ZIRCON_LED_CONTROL(base,data) IOWR(base,0,data) http://www.fpga.gs/ 274 软核演练篇 §5 7 #define IOWR_ZIRCON_LED_DATA(base,data) 8 9 #endif IOWR(base,1,data) 从该代码中我们可以看出,我们定义了两个宏定义,一个是读控制寄存器,另一个是读数据 寄存器。接下来我们给出的是 zircon_led.h 文件的代码,如代码 5.5 所示。 代码 5.5 zircon_led.h 代码 1 #ifndef __ZIRCON_LED_H__ 2 #define __ZIRCON_LED_H__ 3 4 #include "system.h" 5 #include "alt_types.h" 6 #include "zircon_led_regs.h" 7 8 #ifdef __cplusplus 9 extern "C" 10 { 11 #endif /* __cplusplus */ 12 13 void zircon_led_allclose(); 14 void zircon_led_allopen(); 15 void zircon_led_demo1(); 16 void zircon_led_demo2(alt_u32 length ,alt_u32* led_data); 17 18 /* Macros used by alt_sys_init */ 19 #define ZIRCON_LED_INSTANCE(name, dev) alt_u32 led_controller_addr = name##_BASE 20 #define ZIRCON_LED_INIT(name, dev) while(0) 21 22 #ifdef __cplusplus 23 } 24 #endif /* __cplusplus */ 25 26 #endif /* __ZIRCON_LED_H__ */ 从该代码中我们可以看出,我们声明了 4 个函数,这 4 个函数究竟实现了什么功能,我们 需要在 zircon_led.c 文件中查看。这里我们需要注意的是,##是不常见的一个预处理标示符,它 的意思是##前面的字符只是占个位置,在预处理过程中会被实际的字符串替代。比如说我们在 Qsys 软件中给添加的 LED IP 核命名为 zircon_led,那么这里 name##_BASE 就会被替代为 ZIRCON_LED_BASE,而 ZIRCON_LED_BASE 刚好就是 system.h 文件中定义的 UART 设 备基地址,这个值就是用于初始化对应的 BASE 变量。最后我们给出的是 zircon_led.c 文件的 代码,如代码 5.6 所示。 1 #include "unistd.h" 代码 5.6 zircon_led.c 代码 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 275 2 #include "zircon_led.h" 3 #include "priv/alt_busy_sleep.h" 4 #include "alt_types.h" 5 6 extern alt_u32 led_controller_addr; 7 8 void zircon_led_allclose() 9{ 10 IOWR_ZIRCON_LED_CONTROL(led_controller_addr,1); 11 IOWR_ZIRCON_LED_DATA(led_controller_addr,0xff); 12 } 13 14 void zircon_led_allopen() 15 { 16 IOWR_ZIRCON_LED_CONTROL(led_controller_addr,1); 17 IOWR_ZIRCON_LED_DATA(led_controller_addr,0x00); 18 } 19 20 void zircon_led_demo1() 21 { 22 alt_u8 i; 23 24 for(i = 0; i <= 254; i++) 25 { 26 IOWR_ZIRCON_LED_CONTROL(led_controller_addr,1); 27 IOWR_ZIRCON_LED_DATA(led_controller_addr,~i); 28 usleep(50000); 29 } 30 31 } 32 33 void zircon_led_demo2(alt_u32 length ,alt_u32* led_data) 34 { 35 alt_u8 i; 36 37 for(i = 0; i < length;i++) 38 { 39 IOWR_ZIRCON_LED_CONTROL(led_controller_addr,1); 40 IOWR_ZIRCON_LED_DATA(led_controller_addr, ~led_data[i]); 41 usleep(100000); 42 } 43 } 下面我们就来简单的讲解一下该代码,代码第 1 至 4 行是声明所需要的头文件,第 6 行是 http://www.fpga.gs/ 276 软核演练篇 §5 用来帮助我们减少不必要的麻烦,该变量对应的就是我们的 LED IP 核的基地址,由于我们在 zircon_led.h 代码中进行了宏定义,所以我们这里只需要读写该变量就可以对 LED IP 核的基地 址进行操作。当然,我们这里也可以直接用 ZIRCON_LED_BASE,不过这样我们会面对一个问 题,如果我们在 Qsys 软件中将 LED IP 核命名为 ZIRCON_LED,那么我们的程序就可以正常 运行,如果我们在 Qsys 软件中将 LED IP 核命名为了其他名称,那么我们这里的程序就找不到 ZIRCON_LED_BASE 这个名称。经过我们的宏定义之后,不管我们 Qsys 软件中命名为什么名 称,都不会影响我们读写 led_controller_addr。第 8 至 12 行是用来实现 LED 全灭功能,第 14 至 18 是用来实现 LED 全亮功能。第 20 至 31 是用来实现 LED 演示 DEMO1 闪烁功能,第 33 至 43 是用来实现 LED 演示 DEMO2 花样流水功能。 (6) 让 Nios SBT for Eclipse 自动抓取 IP 核的 HAL 编写完了寄存器头文件和驱动底层头文件,也就到了 LED IP 核定制的最后一步,创建_sw.tcl 文件,_sw.tcl 文件同_hw.tcl 文件一样都是利用 Tcl 语言编写的,由于_sw.tcl 内容比较少,并且 Tcl 语言也是比较容易看懂的,所以我们完全没有必要再专门去学习 Tcl 语言,当然,如果有对 Tcl 语言感兴趣的朋友可以另外查找关于 Tcl 语言资料进行学习。这里我们就不再进一步介绍了。 下面我们直接给出已经编写好的 zircon_led_sw.tcl 代码,如代码 5.7 所示。 代码 5.7 zircon_led_sw.tcl 代码 1# 2 # zircon_led_driver.tcl 3# 4 5 # Create a new driver 6 create_driver zircon_led 7 8 # Associate it with some hardware known as "zircon_led" 9 set_sw_property hw_class_name zircon_led 10 11 # The version of this driver 12 set_sw_property version 13.1 13 14 # This driver may be incompatible with versions of hardware less 15 # than specified below. Updates to hardware and device drivers 16 # rendering the driver incompatible with older versions of 17 # hardware are noted with this property assignment. 18 # 19 set_sw_property min_compatible_hw_version 1.0 20 21 # Initialize the driver in alt_sys_init() 22 set_sw_property auto_initialize true 23 24 # Interrupt properties: 25 # This peripheral has an IRQ output but the driver doesn't currently Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 277 26 # have any interrupt service routine. To ensure that the BSP tools 27 # do not otherwise limit the BSP functionality for users of the 28 # Nios II enhanced interrupt port, these settings advertise 29 # compliance with both legacy and enhanced interrupt APIs,and to state 30 # that any driver ISR supports preemption. If an interrupt handler 31 # is added to this driver, these must be re-examined for validity. 32 33 # 34 # Source file listings... 35 # 36 add_sw_property c_source HAL/src/zircon_led.c 37 38 # Include files 39 add_sw_property include_source HAL/inc/zircon_led.h 40 add_sw_property include_source inc/zircon_led_regs.h 41 42 # This driver supports HAL & UCOSII BSP (OS) types 43 add_sw_property supported_bsp_type HAL 44 add_sw_property supported_bsp_type UCOSII 45 46 47 # End of file 这里我们需要说明的是,我们参考的是 altera_avalon_pio_sw.tcl 文件中的代码来编写的, 在该代码中,比较重要的也就两处,第一处是第 22 行代码,该代码是用来将我们的 LED IP 核 添加至 alt_sys_init()函数中进行自动初始化。第二处是第 33 至 40 行,该代码是用来关联我们 编写的寄存器头文件和底层驱动文件。 5.2.2 LED IP 核的应用 完成了 LED IP 核的定制,接下来我们再来看看 LED IP 核的应用,对于 LED IP 核的应用, 我们将会从四个方面进行讲解,分别是功能概述、硬件框架、软件工程和板级调试。 (1) 功能概述 在该工程中,我们主要是利用 LED IP 核实现了一个演示程序,我们通过输入不同的数字来 完成不同的 LED 功能,如果我们输入数字 1,那么 LED 将会全部点亮。如果我们输入数字 2, 那么 LED 将会全部熄灭。如果我们输入数字 3,那么就会执行 DEMO1 程序,LED 就会进行花 样闪烁。如果我们输入数字 4,那么就会执行 DEMO2 程序,LED 就会进行花样流水。如果我 们输入数字 5,那么就会完成演示关闭程序。 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该工程的硬件框架,如图 5.43 所示。 http://www.fpga.gs/ 278 软核演练篇 §5 图 5.43 LED IP 核的硬件框架图 从该图中我们可以看出,Nios II、Sdram、System ID 和 JTAG_UART 都是我们之前学过 的,相信大家都已经很熟悉了,我们这里就不再进行介绍了。对于 zircon_led 的使用,也是很简 单,因为它没有任何需要我们设置的,我们只需要在 Qsys 软件的 IP 核库中找到该 IP,双击添 加就可以和其他 IP 核一样使用了。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 5.8 所示。 代码 5.8 Qsys_Led_Ip.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Led_Ip.c 3 //-- 描述 : 通过 Led_Ip 核来控制 8 个 Led 产生各种效果 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include 8 #include 9 #include "zircon_led.h" 10 #include "priv/alt_busy_sleep.h" 11 12 /* 流水灯花样,Led 低电平亮,高电平灭*/ 13 alt_u32 LED_TBL[42] = { 14 0xFF, 0x00, 15 0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F, 16 0xFE, 0xFC, 0xF8, 0xF0, 0xE0, 0xC0, 0x80, 0x00, 17 0x00, 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 18 0x7E, 0xBD, 0xDB, 0xE7, 0xE7, 0xDB, 0xBD, 0x7E, 19 0x7E, 0x3C, 0x18, 0x00, 0x00, 0x18, 0x3C, 0x7E // 全部熄灭后,再全部点亮 // 依次逐个点亮 // 依次逐个叠加 // 依次逐个递减 // 两个靠拢后分开 // 从两边叠加后递减 20 }; Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 279 21 22 //--------------------------------------------------------------------------- 23 //-- 名称 24 //-- 功能 : main() : 程序入口 25 //-- 输入参数 : 无 26 //-- 输出参数 : 无 27 //--------------------------------------------------------------------------- 28 int main() 29 { 30 FILE* fp; 31 char prompt = 0; 32 33 printf("Welcome to led ip demo program... \n"); 34 printf("1. open all led... \n"); 35 printf("2. close all led...\n"); 36 printf("3. demo1 ...\n"); 37 printf("4. demo2...\n"); 38 printf("5. exit program\n"); 39 fp = fopen(JTAG_UART_NAME, "r+"); //以可读写方式打开设备文件 40 if(fp) //成功打开设备文件 41 { 42 while(prompt != '5') //循环直至接收到 '5' 43 { 44 fprintf(fp, "Please write one number and press Enter... \n"); 45 prompt = getc(fp); //从 JTAG UART 中获取字符 46 switch(prompt) 47 { 48 case '1': {zircon_led_allopen(); break;} 49 case '2': {zircon_led_allclose(); break;} 50 case '3': {zircon_led_demo1(); break;} 51 case '4': {zircon_led_demo2(42,LED_TBL); break;} 52 default : {break;} 53 } 54 } 55 fprintf(fp, "Closing the JTAG UART file handle.\n"); 56 fclose(fp); //关闭设备文件 57 } 58 else 59 { 60 printf("Fail to open file...\n"); //设备文件打开失败 61 } 62 return 0; 63 } 由于代码比较简单,并且我们也给出了注释,所以我们就不再对代码进行详细的讲解了。下 http://www.fpga.gs/ 280 软核演练篇 §5 面我们来看下图 5.44 所示。 图 5.44 LED IP 核的软件工程图 从该图中我们可以看出,我们的 zircon_led_regs.h、zircon_led.h 和 zircon_led.c 这三个文 件都已经添加到了板级支持包中。我们还可以从该图中的 alt_sys_init.c 文件中看到,我们的 zircon_led 也都进行了初始化。我们可以发现,这些步骤我们一个都没做,Eclipse 软件自动帮 助 我 们 完 成 了 所 有 的 操 作 , 之 所 以 Eclipse 软 件 会 自 动 完 成 , 这 主 要 是 因 为 我 们 的 zironc_led_sw.tcl 文件。 (4) 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Led_Ip.sof 下载至我们的 A4 开发板,Qsys_Led_Ip.sof 下载完成 后 , 我 们 还 需 要 在 Eclipse 软 件 中 将 Qsys_Led_Ip.elf 文 件 下 载 至 我 们 的 A4 开 发 板 , Qsys_Led_Ip.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可 以在 Eclipse 软件的控制台中看到我们的打印信息,如图 5.45 所示。 图 5.45 LED IP 核的控制台打印信息图 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 281 这时,我们使用键盘输入数字 1 以后按下回车,我们可以看到我们的 A4 开发板 LED 全部 亮起,如图 5.46 所示。 图 5.46 LED IP 核的板级调试图 当然,我们还可以按下数字 2、3、4,我们的 A4 开发板都会执行相对应的 LED 显示效果。 这里我们就不给大家一一进行展示了,大家可以自行下载至 A4 开发板上逐一验证。至此,我们 的 LED IP 核的应用就讲解完了。看到这里,相信很多朋友都会长出一口气,然后发自内心深处 的说一句:“终于结束了,定制自己的专属 IP 核的过程真是太复杂了。”我们这里需要说明的是, 虽然定制自己的专属 IP 核过程复杂,但是定制自己的专属 IP 核还是能够给我们带来很多好处 的,当然,由于我们这里的 LED 外设比较简单,所以并不能展现出定制自己专属 IP 核的优势。 §5.3 数码管外设 5.3.1 数码管 IP 核的定制 (1) 规划 IP 核的硬件功能 通过我们 LED IP 核的定制,相信大家对 IP 核定制的六个步骤有了一定的了解,接下来我 们就趁热打铁,为我们的数码管外设定制 IP 核。定制数码管 IP 核与定制 LED IP 核是一样,首 先我们需要为数码管外设规划 IP 核硬件功能。首先我们给出第一种寄存器规划方案,具体方案 如表 5.14 所示。 偏移量 00 01 寄存器名称 数据寄存器 控制寄存器 表 5.14 数码管 IP 核寄存器映射 位描述 操作 5 4 3 2 1 0 写 保留 DATA 写 EN5 EN4 EN3 EN2 EN1 EN0 http://www.fpga.gs/ 282 软核演练篇 §5 从该表中我们可以看出,第一种寄存器的规划方案与我们之前定制 LED IP 核是很相似的, 它们都是由两个寄存器构成,一个是数据寄存器,一个是控制寄存器。下面我们简单的介绍下这 两个寄存器:  数据寄存器:是用来控制我们数码管外设显示的数字,通常我们利用数码管显示 0-9, 加上 A-F,那么总共就是 16 个数字,所以我们这里的数据寄存器定义了 4 位,4 位刚 好能够表示 16 个数值。  控制寄存器:是用来控制我们数码管外设亮灭的,因为我们的 A4 开发板上总共有 6 个 数码管,所以我们的控制寄存器也刚好定义了 6 位,每一位代表一个数码管,0 表示 灭,1 表示亮。 通过我们的介绍,相信大家都能够理解我们的第一种寄存器规划方案,接下来我们在来看看 我们的第二种寄存器规划方案,如表 5.15 所示。 表 5.15 数码管 IP 核寄存器映射 偏移量 寄存器名称 位描述 操作 31 … 4 3 … 0 00 数据寄存器 0 写 保留 DATA 01 数据寄存器 1 写 保留 DATA 02 数据寄存器 2 写 保留 DATA 03 数据寄存器 3 写 保留 DATA 04 数据寄存器 4 写 保留 DATA 05 数据寄存器 5 写 保留 DATA 第二种寄存器规划方案与我们的第一种大大的不同,如果我们从第一种寄存器规划方案的 角度来看待第二种寄存器规划方案,我们会觉的第二种寄存器规划方案有点非主流,我们通过表 格可以看出,我们的第二种寄存器规划方案没有控制寄存器,它只有数据寄存器,并且它还是拥 有着 6 个数据寄存器,下面我们简单的介绍一下这 6 个数据寄存器,数据寄存器 0:是用来控制 我们 A4 开发板上的第一个数码管,我们可以看到这个数据寄存器 0 它有 4 位,它通过这 4 位 可以向我们的数码管发送 0-F,这 16 个数字;数据寄存器 1:是用来控制我们 A4 开发板上的第 二个数码管,功能与我们的数据寄存器 0 是相同的,以此类推,我们数据寄存器 5 就是用来控 制我们 A4 开发板上的第六个数码管。至此,我们的两种寄存器规划方案就介绍完了,在这里我 们需要给大家说明一下:既然我们这里讲解的是定制自己的专属 IP 核,那么我们就任性一回, 在接下来的数码管 IP 核讲解中,我们将会使用第二种规划方案。当然第一种方案也是可行的, 有兴趣的朋友可以自己尝试制作。这里我们需要说明的是,不管是第一种方法还是第二种方法, 或者是其他的方法,只要符合我们的 IP 核的定制需求,我们就可以想怎么定制就怎么定制。Qsys 就是这么好用,IP 核定制就是这么随意。 (2) 定义一个恰当的 Avalon 接口 规划完了数码管 IP 核的硬件功能,如果我们没有记错的话,那么便会进入第二步也就是: 给我们的数码管外设定义一个恰当的 Avalon 接口。下面我们给出数码管所用的接口信号,如表 5.16 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 283 信号名 clock reset address write writedata segled_cs segled_data 表 5.16 数码管 IP 核的接口信号 Avalon 信号类型 宽度 Clock 1 Reset 1 Avalon-MM 2 Avalon-MM 1 Avalon-MM 32 Conduit 6 Conduit 8 方向 Input Input Input Input Input Output Output 从表格中我们可以看出,数码管 IP 核所用到的信号和我们 LED IP 核所用到的信号基本上 是一致的,唯一不同的就是我们的数码管 IP 核比我们的 LED IP 核多出几个管脚。我们 LED IP 核只有 8 个管脚,而我们数码管 IP 核不仅有 8 个数据管脚,还有 6 个使能管脚。说完了数码管 IP 核的信号接口,接下来我们再来看看我们的 Avalon 接口时序,如图 5.47 所示。 图 5.47 数码管 IP 核的 Avalon 接口时序图 其实我们这里不看也是知道的。因为它和我们的 LED IP 核使用的接口是一样的,并且我们 的数码管外设也是只有输入信号,没有输出信号的,所以我们可以断定它的时序图和我们的 LED IP 核的时序图肯定是一模一样的。 (3) 使用硬件描述语言描述硬件逻辑 为我们的数码管定义好了 Avalon 接口,接下里我们就开始进行第三步:使用硬件描述语言 描述硬件逻辑。通过前面的学习,我们知道在这一步中,我们需要使用三个.v 文件,它们分别是 顶层接口文件,寄存器文件以及硬件逻辑文件。当然,这三个.v 文件我们可以合并成一个文件, 但是,合并以后.v 文件会给我们后面 IP 核的维护、IP 核的升级带来诸多不便。这里我们就给大 家讲一个故事,还记得我在刚学 C 语言的时候,花费了九牛二虎之力终于完成了一个百行的数 字时钟代码。写好以后,那是少不了要和同学嘚瑟一下,嘚瑟完以后,第二天,我那个同学就来 找我了,然后对我说:“你也太水了吧,写一个数字时钟竟然用了一百多行,我一行代码就完成 了数字时钟的功能。”当时,我就震惊了,我打开了他的代码一看,我就笑了,他的所有代码都 写在了一行里,整个代码没有一个换行符。可以想象,如果以后他要在用到这个代码,那么看代 码所花费的时间要比重新写一个花费的时间还要多。跑题了跑题了,废话不多说,我们直接给出 顶层文件 zircon_segled.v 的代码,如代码 5.9 所示。 代码 5.9 zircon_segled.v 代码 1 module zircon_segled 2( 3 //时钟复位 4 csi_clk,rsi_reset_n, http://www.fpga.gs/ 284 软核演练篇 §5 5 //Avalon-MM 从端口 6 avs_address,avs_write,avs_writedata, 7 //外设管脚输出 8 coe_seg_cs,coe_seg_data 9 ); 10 11 input csi_clk; //系统时钟 12 input 13 input 14 input 15 input [ 2:0] [31:0] rsi_reset_n; avs_address; avs_write; avs_writedata; //系统复位 //Avalon 地址总线 //Avalon 写请求信 //Avalon 写数据总线 16 output [ 7:0] coe_seg_data; //数码管数据管脚 17 output [ 5:0] coe_seg_cs; //数码管数据使能管脚 18 19 wire 20 wire 21 wire [ 3:0] [ 3:0] [ 3:0] seg_data1; seg_data2; seg_data3; //数码管数据寄存器 0 //数码管数据寄存器 1 //数码管数据寄存器 2 22 wire [ 3:0] seg_data4; //数码管数据寄存器 3 23 wire [ 3:0] seg_data5; //数码管数据寄存器 4 24 wire [ 3:0] seg_data6; //数码管数据寄存器 5 25 26 //硬件逻辑文件 27 zircon_segled_logic zircon_segled_logic_init 28 ( 29 .CLK_50M (csi_clk ), //系统时钟 30 .RST_N (rsi_reset_n ), //系统复位 31 .seg_data1 (seg_data1 32 .seg_data2 (seg_data2 33 .seg_data3 (seg_data3 34 .seg_data4 (seg_data4 ), //数码管数据寄存器 0 ), //数码管数据寄存器 1 ), //数码管数据寄存器 2 ), //数码管数据寄存器 3 35 .seg_data5 (seg_data5 ), //数码管数据寄存器 4 36 .seg_data6 (seg_data6 ), //数码管数据寄存器 5 37 .coe_seg_data (coe_seg_data ), //数码管数据管脚 38 .coe_seg_cs (coe_seg_cs ) //数码管数据使能管脚 39 ); 40 41 //寄存器文件 42 zircon_segled_register zircon_segled_register_init 43 ( 44 .csi_clk (csi_clk ), //系统时钟 45 .rsi_reset_n (rsi_reset_n ), //系统复位 46 .avs_address (avs_address ), //Avalon 地址总线 47 .avs_write (avs_write ), //Avalon 写请求信 48 .avs_writedata(avs_writedata ), //Avalon 写数据总线 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 285 49 .seg_data1 50 .seg_data2 51 .seg_data3 52 .seg_data4 53 .seg_data5 54 .seg_data6 55 ); 56 57 endmodule (seg_data1 (seg_data2 (seg_data3 (seg_data4 (seg_data5 (seg_data6 ), //数码管数据寄存器 0 ), //数码管数据寄存器 1 ), //数码管数据寄存器 2 ), //数码管数据寄存器 3 ), //数码管数据寄存器 4 ) //数码管数据寄存器 5 从该代码中我们可以看出,该文件主要是作为顶层模块,用于连接硬件逻辑文件和寄存器文 件,代码中没有编写任何的逻辑功能。下面我们给出的是顶层文件 zircon_segled_register.v, 该文件如代码 5.10 所示。 代码 5.10 zircon_segled_register.v 代码 1 module zircon_segled_register 2 ( 3 //时钟复位 4 csi_clk,rsi_reset_n, 5 //Avalon-MM 从端口 6 avs_address,avs_write,avs_writedata, 7 //用户逻辑输入与输出 8 seg_data1,seg_data2,seg_data3,seg_data4,seg_data5,seg_data6 9 ); 10 11 input 12 input 13 input 14 input 15 input [ 2:0] [31:0] csi_clk; rsi_reset_n; avs_address; avs_write; avs_writedata; //系统时钟 //系统复位 //Avalon 地址总线 //Avalon 写请求信 //Avalon 写数据总线 16 17 output reg [ 3:0] seg_data1; 18 reg [ 3:0] seg_data1_n; 19 output reg [ 3:0] seg_data2; 20 reg [ 3:0] seg_data2_n; 21 output reg [ 3:0] seg_data3; 22 reg [ 3:0] seg_data3_n; 23 output reg [ 3:0] seg_data4; 24 reg [ 3:0] seg_data4_n; 25 output reg [ 3:0] seg_data5; 26 reg [ 3:0] seg_data5_n; 27 output reg [ 3:0] seg_data6; 28 reg [ 3:0] seg_data6_n; //数码管数据寄存器 0 //seg_data1 的下一个状态 //数码管数据寄存器 1 //seg_data2 的下一个状态 //数码管数据寄存器 2 //seg_data3 的下一个状态 //数码管数据寄存器 3 //seg_data4 的下一个状态 //数码管数据寄存器 4 //seg_data5 的下一个状态 //数码管数据寄存器 5 //seg_data6 的下一个状态 29 30 //时序电路,用于给数码管数据寄存器 0 进行赋值的 http://www.fpga.gs/ 286 软核演练篇 §5 31 always @ (posedge csi_clk or negedge rsi_reset_n) 32 begin 33 if(!rsi_reset_n) 34 seg_data1 <= 4'hf; //判断复位 //初始化数码管数据寄存器 0 35 else 36 seg_data1 <= seg_data1_n; //用来给数码管数据寄存器 0 赋值 37 end 38 39 //组合电路,用来给地址偏移量 0,也就是我们的数码管数据寄存器 0 写 4 位的数据 40 always @ (*) 41 begin 42 if((avs_write) && (avs_address == 3'b000)) //判断写使能和地址偏移量 43 seg_data1_n = avs_writedata[3:0]; //如果条件成立,那么将值赋值给数据寄存器 0 44 else 45 seg_data1_n = seg_data1; //否则,将保持不变 46 end 47 48 //时序电路,用于给数码管数据寄存器 1 进行赋值的 49 always @ (posedge csi_clk or negedge rsi_reset_n) 50 begin 51 if(!rsi_reset_n) 52 seg_data2 <= 4'hf; //判断复位 //初始化数码管数据寄存器 1 53 else 54 seg_data2 <= seg_data2_n; //用来给数码管数据寄存器 1 赋值 55 end 56 57 //组合电路,用来给地址偏移量 1,也就是我们的数码管数据寄存器 1 写 4 位的数据 58 always @ (*) 59 begin 60 if((avs_write) && (avs_address == 3'b001)) //判断写使能和地址偏移量 61 seg_data2_n = avs_writedata[3:0]; //如果条件成立,那么将值赋值给数据寄存器 1 62 else 63 seg_data2_n = seg_data2; //否则,将保持不变 64 end 65 66 //时序电路,用于给数码管数据寄存器 2 进行赋值的 67 always @ (posedge csi_clk or negedge rsi_reset_n) 68 begin 69 if(!rsi_reset_n) //判断复位 70 seg_data3 <= 4'hf; //初始化数码管数据寄存器 2 71 else 72 seg_data3 <= seg_data3_n; //用来给数码管数据寄存器 2 赋值 73 end 74 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 287 75 //组合电路,用来给地址偏移量 2,也就是我们的数码管数据寄存器 2 写 4 位的数据 76 always @ (*) 77 begin 78 if((avs_write) && (avs_address == 3'b010)) //判断写使能和地址偏移量 79 seg_data3_n = avs_writedata[3:0]; //如果条件成立,那么将值赋值给数据寄存器 2 80 else 81 seg_data3_n = seg_data3; //否则,将保持不变 82 end 83 84 //时序电路,用于给数码管数据寄存器 3 进行赋值的 85 always @ (posedge csi_clk or negedge rsi_reset_n) 86 begin 87 if(!rsi_reset_n) //判断复位 88 seg_data4 <= 4'hf; //初始化数码管数据寄存器 3 89 else 90 seg_data4 <= seg_data4_n; //用来给数码管数据寄存器 3 赋值 91 end 92 93 //组合电路,用来给地址偏移量 3,也就是我们的数码管数据寄存器 3 写 4 位的数据 94 always @ (*) 95 begin 96 if((avs_write) && (avs_address == 3'b011)) //判断写使能和地址偏移量 97 seg_data4_n = avs_writedata[3:0]; //如果条件成立,那么将值赋值给数据寄存器 3 98 else 99 seg_data4_n = seg_data4; //否则,将保持不变 100 end 101 102 //时序电路,用于给数码管数据寄存器 4 进行赋值的 103 always @ (posedge csi_clk or negedge rsi_reset_n) 104 begin 105 if(!rsi_reset_n) //判断复位 106 seg_data5 <= 4'hf; //初始化数码管数据寄存器 4 107 else 108 seg_data5 <= seg_data5_n; //用来给数码管数据寄存器 4 赋值 109 end 110 111 //组合电路,用来给地址偏移量 4,也就是我们的数码管数据寄存器 4 写 4 位的数据 112 always @ (*) 113 begin 114 if((avs_write) && (avs_address == 3'b100)) //判断写使能和地址偏移量 115 seg_data5_n = avs_writedata[3:0]; //如果条件成立,那么将值赋值给数据寄存器 4 116 else 117 seg_data5_n = seg_data5; //否则,将保持不变 118 end http://www.fpga.gs/ 288 软核演练篇 §5 119 120 //时序电路,用于给数码管数据寄存器 5 进行赋值的 121 always @ (posedge csi_clk or negedge rsi_reset_n) 122 begin 123 if(!rsi_reset_n) //判断复位 124 seg_data6 <= 4'hf; //初始化数码管数据寄存器 5 125 else 126 seg_data6 <= seg_data6_n; //用来给数码管数据寄存器 5 赋值 127 end 128 129 //组合电路,用来给地址偏移量 5,也就是我们的数码管数据寄存器 5 写 4 位的数据 130 always @ (*) 131 begin 132 if((avs_write) && (avs_address == 3'b101)) //判断写使能和地址偏移量 133 seg_data6_n = avs_writedata[3:0]; //如果条件成立,那么将值赋值给数据寄存器 5 134 else 135 seg_data6_n = seg_data6; //否则,将保持不变 136 end 137 138 endmodule 从这个代码中,我们可以看出这个代码的规模还是挺吓人的,其实它就是一个纸老虎,我们 先来看下第一个 always 模块和第二个 always 模块,也就是第 30 至 46 行,这两个 always 模 块一个是时序电路,一个是组合电路,时序电路用来存储,组合电路则用来判断,当写使能到来 时,我们便将地址偏移量 0 上的数据读取到 seg_data1 中。同时,我们在来看第三个 always 模 块和第四个 always 模块,也就是第 48 至 64 行,它同我们前面的一样,也是一个时序,一个组 合,当写使能到来时,我们便将地址偏移量 1 上的数据读到 seg_data2 中。依次同理,整个代 码就是用来读取 6 个数据寄存器的值。 最后我们再来看一看硬件逻辑文件 zircon_segled_logic.v,该文件如代码 5.11 所示。 代码 5.11 zircon_segled_logic.v 代码 1 module zircon_segled_logic 2 ( 3 //输入端口 4 CLK_50M,RST_N, 5 //用户逻辑输入与输出 6 seg_data1,seg_data2,seg_data3,seg_data4,seg_data5,seg_data6, 7 //外设管脚输出端口 8 coe_seg_data,coe_seg_cs 9 ); 10 11 //--------------------------------------------------------------------------12 //-- 外部端口声明 13 //--------------------------------------------------------------------------- Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 289 14 input CLK_50M; //时钟端口 15 input 16 input 17 input [ 3:0] [ 3:0] RST_N; seg_data6; seg_data5; //复位端口 //数码管数据寄存器 5 //数码管数据寄存器 4 18 input [ 3:0] seg_data4; //数码管数据寄存器 3 19 input [ 3:0] seg_data3; //数码管数据寄存器 2 20 input [ 3:0] seg_data2; //数码管数据寄存器 1 21 input [ 3:0] seg_data1; 22 output reg [ 5:0] coe_seg_cs; 23 output reg [ 7:0] coe_seg_data; //数码管数据寄存器 0 //数码管使能端口 //数码管端口 24 25 //--------------------------------------------------------------------------- 26 //-- 内部端口声明 27 //--------------------------------------------------------------------------- 28 reg 29 reg 30 reg [15:0] [15:0] [ 2:0] time_cnt; time_cnt_n; led_cnt; //用来控制数码管闪烁频率的定时计数器 //time_cnt 的下一个状态 //用来控制数码管亮灭及显示数据的显示计数器 31 reg [ 2:0] led_cnt_n; //led_cnt 的下一个状态 32 reg [ 3:0] led_data; //数据转换寄存器 33 34 //--------------------------------------------------------------------------- 35 //-- 逻辑功能实现 36 //--------------------------------------------------------------------------- 37 //时序电路,用来给 time_cnt 寄存器赋值 38 always @ (posedge CLK_50M or negedge RST_N) 39 begin 40 if(!RST_N) 41 time_cnt <= 16'h0; //判断复位 //初始化 time_cnt 值 42 else 43 time_cnt <= time_cnt_n; //用来给 time_cnt 赋值 44 end 45 46 //组合电路,实现 10ms 的定时计数器 47 always @ (*) 48 begin 49 if(time_cnt == 16'd50_000) 50 time_cnt_n = 16'h0; //判断 10ms 时间 //如果到达 10ms,定时计数器将会被清零 51 else 52 time_cnt_n = time_cnt + 27'h1; //如果未到 10ms,定时计数器将会继续累加 53 end 54 55 //时序电路,用来给 led_cnt 寄存器赋值 56 always @ (posedge CLK_50M or negedge RST_N) 57 begin http://www.fpga.gs/ 290 软核演练篇 §5 58 if(!RST_N) //判断复位 59 led_cnt <= 3'b000; //初始化 led_cnt 值 60 else 61 led_cnt <= led_cnt_n; //用来给 led_cnt 赋值 62 end 63 64 //组合电路,判断时间,实现控制显示计数器累加 65 always @ (*) 66 begin 67 if(time_cnt == 16'd50_000) //判断 10ms 时间 68 led_cnt_n = led_cnt + 1'b1; //如果到达 10ms,计数器进行累加 69 else 70 led_cnt_n = led_cnt; //如果未到 10ms,计数器保持不变 71 end 72 73 //组合电路,实现数码管的数字显示,将时钟中的数据转换成显示数据 74 always @ (*) 75 begin 76 case(led_cnt) 77 0 : led_data = seg_data1; //数码管数据寄存器 0 78 1 : led_data = seg_data2; //数码管数据寄存器 1 79 2 : led_data = seg_data3; //数码管数据寄存器 2 80 3 : led_data = seg_data4; //数码管数据寄存器 3 81 4 : led_data = seg_data5; //数码管数据寄存器 4 82 5 : led_data = seg_data6; //数码管数据寄存器 5 83 default: led_data = 4'hF; //给数码管赋值为 F 84 endcase 85 end 86 87 //组合电路,控制数码管的亮灭 88 always @ (*) 89 begin 90 case (led_cnt) 91 0 : coe_seg_cs = 6'b111110; //当计数器为 0 时,数码管 SEG1 显示 92 1 : coe_seg_cs = 6'b111101; //当计数器为 1 时,数码管 SEG2 显示 93 2 : coe_seg_cs = 6'b111011; //当计数器为 2 时,数码管 SEG3 显示 94 3 : coe_seg_cs = 6'b110111; //当计数器为 3 时,数码管 SEG4 显示 95 4 : coe_seg_cs = 6'b101111; //当计数器为 4 时,数码管 SEG5 显示 96 5 : coe_seg_cs = 6'b011111; //当计数器为 5 时,数码管 SEG6 显示 97 default: coe_seg_cs = 6'b111111; //熄灭所有数码管 98 endcase 99 end 100 101 //组合电路,控制数码管小数点的亮灭 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 291 102 always @ (*) 103 begin 104 case (led_cnt) 105 0 : coe_seg_data[7] = 1'b0; //点亮数码管 SEG1 的小数点 106 1 : coe_seg_data[7] = 1'b0; //点亮数码管 SEG2 的小数点 107 2 : coe_seg_data[7] = 1'b0; //点亮数码管 SEG3 的小数点 108 3 : coe_seg_data[7] = 1'b0; //点亮数码管 SEG4 的小数点 109 4 : coe_seg_data[7] = 1'b0; //点亮数码管 SEG5 的小数点 110 5 : coe_seg_data[7] = 1'b0; //点亮数码管 SEG6 的小数点 111 default: coe_seg_data[7] = 1'b0; //熄灭所有数码管的小数点 112 endcase 113 end 114 115 //组合电路,实现数码管的显示 116 always @ (*) 117 begin 118 case(led_data) 119 0 : coe_seg_data[6:0] = 7'b0111111; //显示数字 "0" 120 1 : coe_seg_data[6:0] = 7'b0000110; //显示数字 "1" 121 2 : coe_seg_data[6:0] = 7'b1011011; //显示数字 "2" 122 3 : coe_seg_data[6:0] = 7'b1001111; //显示数字 "3" 123 4 : coe_seg_data[6:0] = 7'b1100110; //显示数字 "4" 124 5 : coe_seg_data[6:0] = 7'b1101101; //显示数字 "5" 125 6 : coe_seg_data[6:0] = 7'b1111101; //显示数字 "6" 126 7 : coe_seg_data[6:0] = 7'b0000111; //显示数字 "7" 127 8 : coe_seg_data[6:0] = 7'b1111111; //显示数字 "8" 128 9 : coe_seg_data[6:0] = 7'b1101111; //显示数字 "9" 129 10 : coe_seg_data[6:0] = 7'b1110111; //显示数字 "A" 130 11 : coe_seg_data[6:0] = 7'b1111100; //显示数字 "B" 131 12 : coe_seg_data[6:0] = 7'b1011000; //显示数字 "C" 132 13 : coe_seg_data[6:0] = 7'b1011110; //显示数字 "D" 133 14 : coe_seg_data[6:0] = 7'b1111001; //显示数字 "E" 134 15 : coe_seg_data[6:0] = 7'b0000000; //显示数字 "F" 135 default: coe_seg_data[6:0] = 7'b0111111; //显示数字 "0" 136 endcase 137 end 138 139 endmodule 在学习本篇之前,如果你学习过我们的《项目实战篇》,那么你看到这个代码会倍感亲切, 因为这个代码就是我们《项目实战篇》中数码管的 Verilog 代码,这个代码在我们的《项目实战 篇》中经常使用,比如我们《项目实战篇》的数字时钟这个工程,其中用来控制数码管显示的代 码就是这个代码。这个代码可以说是非常简单的,如果你看不懂这个代码,那么你的这个 Verilog 代码功力不过关呀,要回头好好学习学习 Verilog 这部分知识。下面我们就来简单的介绍一下上 http://www.fpga.gs/ 292 软核演练篇 §5 面的这个代码。第 1 至 32 行是代码的端口声明,第 38 至 53 行是用来生成一个 10ms 的定时 器。第 56 至 71 行是用来生成一个模 8 的加法计数器,这个模 8 的加法计数器每 10ms 钟就自 动加 1。我们将这个模 8 的加法器计数器用来定时选通每个数码管,在选通过程中,我们将需要 显示的数据进行译码,然后送到数码管的数据线上进行显示输出。6 个数码管看起来像是同时显 示了不同的数据,实际上每个固定的时间只有一个数码管点亮,由于人的视觉暂留现象及发光二 极管的余辉效应,尽管实际上各位数码管并非同时点亮,但只要扫描的速度足够快,给人的印象 就是一组稳定的显示数据,不会有闪烁感。这里需要我们注意的是:由于我们只有数据寄存器, 并没有控制寄存器,那么我们想要让我们的数码管灭掉怎么实现呢?大家可以看这么一段代码, 第 134 行的代码, 在这里我们将显示 F 改成了不显示。也就是说,我们向数据寄存器中写 1, 数码管则显示 1,我们向数据寄存器中写 10,数码管则显示 A,如果没有修改之前,我们向数据 寄存器中写 15,那么数码管则显示 F,现在修改了之后,我们向数据寄存器中写 15,数码管则 不会在显示 F 了,而是熄灭数码管。因为我们的数据寄存器只用了 4 位,所以我们这里只能牺 牲 0-F 这 16 个数字之间的一个数字。当然,我们完全可以通过增加数据寄存器的位数来修改, 不过一般我们使用数码管只用 0-9,而 A-F 不常用,所以我们就可以通过修改 F 的值来使数码 管熄灭。 (4) 使用 IP 核编辑器封装硬件逻辑,完成 IP 核定制 为我们的数码管 IP 核编写好了硬件逻辑文件后,接下来我们就需要使用 IP 核编辑器封装硬 件逻辑文件,由于这部分内容过于累赘,并且与我们之前的 LED IP 核的操作过程基本一致,所 以我们这里就不在进一步讲解,我们将跳过该步骤,直接进入下一个步骤,来介绍数码管 IP 核 的寄存器头文件和底层驱动文件。 (5) 编写用于描述寄存器的 C 头文件和 IP 核的驱动 C 文件 首先我们给出的是数码管 IP 核的寄存器头文件 zricon_avalon_segled_regs.h,如代码 5.12 所示。 代码 5.12 zircon_avalon_segled_regs.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_segled_regs.h 3 //-- Describe : segled IP core register header file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_SEGLED_REGS_H__ 8 #define __ZIRCON_AVALON_SEGLED_REGS_H__ 9 10 #include 11 12 //Data register 0 13 #define IOWR_ZIRCON_AVALON_SEGLED_DATA0(base,data) IOWR(base, 0, data) 14 //Data register 1 15 #define IOWR_ZIRCON_AVALON_SEGLED_DATA1(base,data) IOWR(base, 1, data) 16 //Data register 2 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 293 17 #define IOWR_ZIRCON_AVALON_SEGLED_DATA2(base,data) 18 //Data register 3 19 #define IOWR_ZIRCON_AVALON_SEGLED_DATA3(base,data) 20 //Data register 4 21 #define IOWR_ZIRCON_AVALON_SEGLED_DATA4(base,data) 22 //Data register 5 23 #define IOWR_ZIRCON_AVALON_SEGLED_DATA5(base,data) 24 25 #endif /* __ZIRCON_AVALON_SEGLED_REGS_H__ */ IOWR(base, 2, data) IOWR(base, 3, data) IOWR(base, 4, data) IOWR(base, 5, data) 从这个代码中我们可以看出,我们的数码管 IP 核的寄存器文件和我们的 LED IP 核一样简 短,精炼。这也主要是因为我们的数码管 IP 核和我们的 LED IP 太简单了,没有给我们发挥的 空间。这六句代码分别对应的每个数据寄存器,每个数据寄存器又分别对应我们每个数码管,因 此,我们只要使用这个六个语句就可以直接操控我们的数码管了。这里我们需要说明的是,为了 防止我们在 Eclipse 软件中打开该文件出现中文乱码,所以我们这里使用的是英文注释,这些英 文都比较简单,相信大家一看就能明白。看完了我们的数码管寄存器头文件,我们接下来在来看 一看我们的数码管 IP 核的底层驱动头文件 zircon_avalon_segled.h,如代码 5.13 所示。 代码 5.13 zircon_avalon_segled.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_segled.h 3 //-- Describe : segled IP core driver header file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_SEGLED_H__ 8 #define __ZIRCON_AVALON_SEGLED_H__ 9 10 #include "system.h" 11 #include "alt_types.h" 12 #include "zircon_avalon_segled_regs.h" 13 14 #ifdef __cplusplus 15 extern "C" 16 { 17 #endif /* __cplusplus */ 18 19 //--------------------------------------------------------------------------- 20 //-- Name : zircon_avalon_segled_allclose 21 //-- Function : Close all segled 22 //-- Input parameters : no 23 //-- Output parameters: no 24 //--------------------------------------------------------------------------- 25 void zircon_avalon_segled_allclose(); http://www.fpga.gs/ 294 软核演练篇 §5 26 27 //--------------------------------------------------------------------------- 28 //-- Name : zircon_avalon_segled_allopen 29 //-- Function : Open all segled and 8 digital display 30 //-- Input parameters : no 31 //-- Output parameters: no 32 //--------------------------------------------------------------------------- 33 void zircon_avalon_segled_allopen(); 34 35 //--------------------------------------------------------------------------- 36 //-- Name : zircon_avalon_segled_demo1 37 //-- Function : From left to right,in order to open and display 0, 38 //-- 1,2,3,4,5 In the next, from right to left, turn off the segled 39 //-- Input parameters : no 40 //-- Output parameters: no 41 //--------------------------------------------------------------------------- 42 void zircon_avalon_segled_demo1(); 43 44 //--------------------------------------------------------------------------- 45 //-- Name : zircon_avalon_segled_demo2 46 //-- Function : every segled display random 0-E 47 //-- Input parameters : no 48 //-- Output parameters: no 49 //--------------------------------------------------------------------------- 50 void zircon_avalon_segled_demo2(); 51 52 //--------------------------------------------------------------------------- 53 //-- Name : zircon_avalon_segled_demo3 54 //-- Function : segled to achieve a stopwatch function 55 //-- Input parameters : no 56 //-- Output parameters: no 57 //--------------------------------------------------------------------------- 58 void zircon_avalon_segled_demo3(); 59 60 /* Macros used by alt_sys_init */ 61 #define ZIRCON_AVALON_SEGLED_INSTANCE(name, dev) alt_u32 segled_controller_addr = name##_BASE 62 #define ZIRCON_AVALON_SEGLED_INIT(name, dev) while(0) 63 64 #ifdef __cplusplus 65 } 66 #endif /* __cplusplus */ 67 68 #endif /* __ZIRCON_AVALON_SEGLED_H__ */ Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 295 这个代码同样也很简单,我们可以从这个代码中看到有 5 个函数,这 5 个函数实现了数码 管各种功能。下面我们就来进一步分析一下这 5 个函数,看下这 5 个函数究竟实现了什么功能, 下面我们给出底层驱动文件 zircon_avalon_segled.c,该文件如代码 5.14 所示。 代码 5.14 zircon_avalon_segled.c 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_segled.c 3 //-- Describe : segled IP core driver C file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "unistd.h" 8 #include "alt_types.h" 9 #include "stdlib.h" 10 #include "zircon_avalon_segled.h" 11 12 extern alt_u32 segled_controller_addr; 13 14 //--------------------------------------------------------------------------- 15 //-- Name : zircon_avalon_segled_allclose 16 //-- Function : Close all segled 17 //-- Input parameters : no 18 //-- Output parameters: no 19 //--------------------------------------------------------------------------- 20 void zircon_avalon_segled_allclose() 21 { 22 23 IOWR_ZIRCON_AVALON_SEGLED_DATA0(segled_controller_addr,15); 24 IOWR_ZIRCON_AVALON_SEGLED_DATA1(segled_controller_addr,15); 25 IOWR_ZIRCON_AVALON_SEGLED_DATA2(segled_controller_addr,15); 26 IOWR_ZIRCON_AVALON_SEGLED_DATA3(segled_controller_addr,15); 27 IOWR_ZIRCON_AVALON_SEGLED_DATA4(segled_controller_addr,15); 28 IOWR_ZIRCON_AVALON_SEGLED_DATA5(segled_controller_addr,15); 29 30 } 31 32 //--------------------------------------------------------------------------- 33 //-- Name : zircon_avalon_segled_allopen 34 //-- Function : Open all segled and 8 digital display 35 //-- Input parameters : no 36 //-- Output parameters: no 37 //--------------------------------------------------------------------------- 38 void zircon_avalon_segled_allopen() 39 { http://www.fpga.gs/ 296 软核演练篇 §5 40 41 IOWR_ZIRCON_AVALON_SEGLED_DATA0(segled_controller_addr,8); 42 IOWR_ZIRCON_AVALON_SEGLED_DATA1(segled_controller_addr,8); 43 IOWR_ZIRCON_AVALON_SEGLED_DATA2(segled_controller_addr,8); 44 IOWR_ZIRCON_AVALON_SEGLED_DATA3(segled_controller_addr,8); 45 IOWR_ZIRCON_AVALON_SEGLED_DATA4(segled_controller_addr,8); 46 IOWR_ZIRCON_AVALON_SEGLED_DATA5(segled_controller_addr,8); 47 48 } 49 50 //--------------------------------------------------------------------------- 51 //-- Name : zircon_avalon_segled_demo1 52 //-- Function : From left to right,in order to open and display 0, 53 //-- 1,2,3,4,5 In the next, from right to left, turn off the segled 54 //-- Input parameters : no 55 //-- Output parameters: no 56 //--------------------------------------------------------------------------- 57 void zircon_avalon_segled_demo1() 58 { 59 60 IOWR_ZIRCON_AVALON_SEGLED_DATA0(segled_controller_addr,0); 61 usleep(200000); 62 IOWR_ZIRCON_AVALON_SEGLED_DATA1(segled_controller_addr,1); 63 usleep(200000); 64 IOWR_ZIRCON_AVALON_SEGLED_DATA2(segled_controller_addr,2); 65 usleep(200000); 66 IOWR_ZIRCON_AVALON_SEGLED_DATA3(segled_controller_addr,3); 67 usleep(200000); 68 IOWR_ZIRCON_AVALON_SEGLED_DATA4(segled_controller_addr,4); 69 usleep(200000); 70 IOWR_ZIRCON_AVALON_SEGLED_DATA5(segled_controller_addr,5); 71 usleep(200000); 72 IOWR_ZIRCON_AVALON_SEGLED_DATA5(segled_controller_addr,15); 73 usleep(200000); 74 IOWR_ZIRCON_AVALON_SEGLED_DATA4(segled_controller_addr,15); 75 usleep(200000); 76 IOWR_ZIRCON_AVALON_SEGLED_DATA3(segled_controller_addr,15); 77 usleep(200000); 78 IOWR_ZIRCON_AVALON_SEGLED_DATA2(segled_controller_addr,15); 79 usleep(200000); 80 IOWR_ZIRCON_AVALON_SEGLED_DATA1(segled_controller_addr,15); 81 usleep(200000); 82 IOWR_ZIRCON_AVALON_SEGLED_DATA0(segled_controller_addr,15); 83 usleep(200000); Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 297 84 85 } 86 87 //--------------------------------------------------------------------------- 88 //-- Name : zircon_avalon_segled_demo2 89 //-- Function : every segled display random 0-E 90 //-- Input parameters : no 91 //-- Output parameters: no 92 //--------------------------------------------------------------------------- 93 void zircon_avalon_segled_demo2() 94 { 95 int i,k1=0,k2=0,k3=0,k4=0,k5=0,k6=0; 96 97 for(i = 0 ; i < 50; i++) 98 { 99 k1 = rand()%15; 100 IOWR_ZIRCON_AVALON_SEGLED_DATA0(segled_controller_addr,k1); 101 usleep(10000); 102 k2 = rand()%15; 103 IOWR_ZIRCON_AVALON_SEGLED_DATA1(segled_controller_addr,k2); 104 usleep(10000); 105 k3 = rand()%15; 106 IOWR_ZIRCON_AVALON_SEGLED_DATA2(segled_controller_addr,k3); 107 usleep(10000); 108 k4 = rand()%15; 109 IOWR_ZIRCON_AVALON_SEGLED_DATA3(segled_controller_addr,k4); 110 usleep(10000); 111 k5 = rand()%15; 112 IOWR_ZIRCON_AVALON_SEGLED_DATA4(segled_controller_addr,k5); 113 usleep(10000); 114 k6 = rand()%15; 115 IOWR_ZIRCON_AVALON_SEGLED_DATA5(segled_controller_addr,k6); 116 usleep(10000); 117 } 118 119 zircon_avalon_segled_allclose(); 120 } 121 122 //--------------------------------------------------------------------------- 123 //-- Name : zircon_avalon_segled_demo3 124 //-- Function : segled to achieve a stopwatch function 125 //-- Input parameters : no 126 //-- Output parameters: no 127 //--------------------------------------------------------------------------- http://www.fpga.gs/ 298 软核演练篇 §5 128 void zircon_avalon_segled_demo3() 129 { 130 int i,k1=0,k2=0,k3=0,k4=0,k5=0,k6=0; 131 132 while(1) 133 { 134 k1++; 135 if(k1 == 10) {k2++; k1 = 0;} 136 if(k2 == 10) {k3++; k2 = 0;} 137 if(k3 == 10) {k4++; k3 = 0;} 138 if(k4 == 10) 139 { 140 k4 = 0; 141 zircon_avalon_segled_allclose(); 142 break; 143 } 144 145 IOWR_ZIRCON_AVALON_SEGLED_DATA5(segled_controller_addr,k1); 146 IOWR_ZIRCON_AVALON_SEGLED_DATA4(segled_controller_addr,k2); 147 IOWR_ZIRCON_AVALON_SEGLED_DATA3(segled_controller_addr,k3); 148 IOWR_ZIRCON_AVALON_SEGLED_DATA2(segled_controller_addr,k4); 149 IOWR_ZIRCON_AVALON_SEGLED_DATA1(segled_controller_addr,15); 150 IOWR_ZIRCON_AVALON_SEGLED_DATA0(segled_controller_addr,15); 151 usleep(1000); 152 153 } 154 155 } 下面我们就来简单的讲解一下该 5 个函数的功能,首先我们来看第一个函数第 14 至 30 行, 其实我们从第一个函数的名称中或者是注释中,就能看出它所实现的功能,就是熄灭我们所有的 数码管。我们在来看下它如何实现关闭数码管的,从代码中我们可以看出,就如我们前面所说, 我们这里发送的是数字 15,本来是数码管是显示 F 的,但是被我们改成了熄灭。接下来我们再 来看一下第二个函数第 32 至 48 行,这第二个函数同第一个函数恰恰相反,它是打开我们的所 有数码管,并让我们的数码管显示数字 8。接着我们再来看第三个函数第 51 至 85 行,第三个函 数实现的功能是:从左到右,依次打开数码管,并显示 0、1、2、3、4、5。随后,再从右到左, 依次关闭数码管。第四个函数第 88 至 120 行,实现的功能是让每个数码管都随机显示 0-E。第 五个函数第 122 至 155 行,实现的功能是秒表。 (6) 让 Nios SBT for Eclipse 自动抓取 IP 核的 HAL 完成了数码管 IP 核的驱动文件的编写,接下来也就是我们制作 IP 的最后一步,让 Nios SBT for Eclipse 自动抓取 IP 核的 HAL。这部分内容我们可以参考 zircon_led_sw.tcl 来编写我们的 zircon_avalon_segled_sw.tcl。由于该代码比较简单,并且该代码中的内容与我们的 LED IP 核 zircon_led_sw.tcl 文件中的代码基本一致,所以我们就不再对 zircon_avalon_segled_sw.tcl 进 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 299 一步进行讲解了。 5.3.2 数码管 IP 核的应用 (1) 功能概述 完成了数码管 IP 核的定制,接下来我们再来看看数码管 IP 核的应用,首先我们讲解的是第 一部分功能概述,在该工程中,我们主要是利用数码管 IP 核实现了一个演示程序,我们通过输 入不同的数字来完成不同的数码管功能,如果我们输入数字 1,那么数码管将会全部点亮并显示 数字 8。如果我们输入数字 2,那么数码管将会全部熄灭。如果我们输入数字 3,那么就会执行 DEMO1 程序,数码管从左到右,依次打开,并显示 0、1、2、3、4、5。随后,再从右到左,数 码管依次关闭。如果我们输入数字 4,那么就会执行 DEMO2 程序,每个数码管都随机显示 0E。如果我们输入数字 5,那么就会执行 DEMO3 程序,将会实现秒表功能。如果我们输入数字 6,那么就会完成演示并关闭程序。 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该工程的硬件框架,如图 5.48 所示。 图 5.48 数码管 IP 核的硬件框架图 从该图中我们可以看出,我们的数码管 IP 核的硬件框架图与我们的 LED IP 核的硬件框架 图基本上是一致的,唯一不同的是我们将 LED IP 核换成了我们的数码管 IP 核。对于数码管 IP 核,它的使用和我们的 LED IP 核是一样的,我们不需要任何配置,只需要添加数码管 IP 核就 可以正常使用。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 5.15 所 示。 代码 5.15 Qsys_Segled_Ip.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Segled_Ip.c 3 //-- 描述 : 用于测试我们的数码管 IP 核 http://www.fpga.gs/ 300 软核演练篇 §5 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include //标准输入输出头文件 8 #include "zircon_avalon_segled.h" //数码管 IP 核的驱动头文件 9 10 //--------------------------------------------------------------------------- 11 //-- 名称 : main() 12 //-- 功能 : 程序入口 13 //-- 输入参数 : 无 14 //-- 输出参数 : 无 15 //--------------------------------------------------------------------------- 16 int main() 17 { 18 FILE* fp; 19 char prompt = 0; 20 21 printf("Welcome to segled ip demo program... \n"); 22 printf("1. open all segled... \n"); 23 printf("2. close all segled...\n"); 24 printf("3. segled demo1 ...\n"); 25 printf("4. segled demo2...\n"); 26 printf("5. segled demo3...\n"); 27 printf("6. exit program\n"); 28 fp = fopen(JTAG_UART_NAME, "r+"); //以可读写方式打开设备文件 29 if(fp) //成功打开设备文件 30 { 31 while(prompt != '6') //循环直至接收到 '6' 32 { 33 fprintf(fp, "Please write one number and press Enter... \n"); 34 prompt = getc(fp); //从 JTAG UART 中获取字符 35 switch(prompt) 36 { 37 case '1': {zircon_avalon_segled_allopen(); break;} 38 case '2': {zircon_avalon_segled_allclose(); break;} 39 case '3': {zircon_avalon_segled_demo1(); break;} 40 case '4': {zircon_avalon_segled_demo2(); break;} 41 case '5': {zircon_avalon_segled_demo3(); break;} 42 default : {break;} 43 } 44 } 45 fprintf(fp, "Closing the JTAG UART file handle.\n"); 46 fclose(fp); //关闭设备文件 47 } Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 301 48 else 49 { 50 printf("Fail to open file...\n"); //设备文件打开失败 51 } 52 return 0; 53 } (4) 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Segled_Ip.sof 下载至我们的 A4 开发板,Qsys_Segled_Ip.sof 下 载完成后,我们还需要在 Eclipse 软件中将 Qsys_Segled_Ip.elf 文件下载至我们的 A4 开发板, Qsys_Segled_Ip.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们 可以在 Eclipse 软件的控制台中看到我们的打印信息,如图 5.49 所示。 图 5.49 数码管 IP 核的控制台打印信息图 这时,我们使用键盘输入数字 1 以后按下回车,我们可以看到我们的 A4 开发板数码管全部 亮起,并显示数字 8,如图 5.50 所示。 图 5.50 数码管 IP 核的板级调试图 http://www.fpga.gs/ 302 软核演练篇 §5 当然,我们还可以按下数字 2、3、4、5,我们的 A4 开发板都会执行相对应的数码管显示 效果。这里我们就不给大家一一进行展示了,大家可以自行下载至 A4 开发板上逐一验证。至此, 我们的数码管 IP 核的应用就讲解完了。学到这里,相信有不少朋友开始抱怨了,我们前面一直 说 Qsys 的精髓就是定制我们自己的专属 IP 核,经过 LED 和数码管这两个外设 IP 核的定制, 我们折腾来折腾去,功夫花费了不少,但然并卵呢?一点实质性的效果都没有看到呀。难道我们 数码管 IP 核实现的这些程序,我们 PIO 实现不了吗?为了能够让大家对比学习。下面我们就化 身为一个单片机高手,利用单片机的方法,也就是我们所说的使用 PIO IP 核来控制我们的数码 管,并且利用 PIO IP 核来实现,我们数码管 IP 核所实现的功能。 5.3.3 数码管 PIO 的应用 (1) 功能概述 我们主要是利用 PIO IP 核实现一个演示程序,这个演示程序和我们之前数码管 IP 核实现的 演示程序功能一致。我们通过输入不同的数字来完成不同的数码管功能,如果我们输入数字 1, 那么数码管将会全部点亮并显示数字 8。如果我们输入数字 2,那么数码管将会全部熄灭。如果 我们输入数字 3,那么就会执行 DEMO1 程序,数码管从左到右,依次打开,并显示 0、1、2、 3、4、5。随后,再从右到左,数码管依次关闭。如果我们输入数字 4,那么就会执行 DEMO2 程序,每个数码管都随机显示 0-E。如果我们输入数字 5,那么就会执行 DEMO3 程序,将会实 现秒表功能。如果我们输入数字 6,那么就会完成演示并关闭程序。 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该工程的硬件框架,如图 5.51 所示。 图 5.51 数码管 PIO 的硬件框架图 从该图中我们可以看出,我们数码管 PIO 的硬件框架图与我们的数码管 IP 核的硬件框架图 基本上是一致的,唯一不同的是我们将数码管 IP 核换成了我们的 PIO IP 核。这里我们使用了两 个 PIO IP 核,一个 PIO IP 核用于数码管的数据管脚,另一个 PIO IP 核用于数码管的使能管脚。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 303 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 5.16 所 示。 代码 5.16 Qsys_Segled.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Segled.c 3 //-- 描述 : 使用 PIO IP 核来测试我们的数码管 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "system.h" //系统头文件 8 #include "stdlib.h" //rand 随机函数的头文件 9 #include //标准输入输出头文件 10 #include "unistd.h" //usleep 延时函数的头文件 11 #include "altera_avalon_pio_regs.h"//PIO IP 核的寄存器头文件 12 13 #define NUM0 0x3f 14 #define NUM1 0x06 15 #define NUM2 0x5b 16 #define NUM3 0x4f 17 #define NUM4 0x66 18 #define NUM5 0x6d 19 #define NUM6 0x7d 20 #define NUM7 0x07 21 #define NUM8 0x7f 22 #define NUM9 0x6f 23 #define NUMA 0x77 24 #define NUMB 0x7C 25 #define NUMC 0x39 26 #define NUMD 0x5E 27 #define NUME 0x79 28 #define NUMF 0x00 //数码管显示 0 //数码管显示 1 //数码管显示 2 //数码管显示 3 //数码管显示 4 //数码管显示 5 //数码管显示 6 //数码管显示 7 //数码管显示 8 //数码管显示 9 //数码管显示 A //数码管显示 B //数码管显示 C //数码管显示 D //数码管显示 E //熄灭数码管 29 30 #define SEGLED_OPEN_CS1 0x3E //使能数码管 1 31 #define SEGLED_OPEN_CS2 0x3D //使能数码管 2 32 #define SEGLED_OPEN_CS3 0x3B //使能数码管 3 33 #define SEGLED_OPEN_CS4 0x37 //使能数码管 4 34 #define SEGLED_OPEN_CS5 0x2F //使能数码管 5 35 #define SEGLED_OPEN_CS6 0x1F //使能数码管 6 36 #define SEGLED_ALL_OPEN_CS 0x00 //使能所有的数码管 37 #define SEGLED_ALL_CLOSE_CS 0xff //关闭所有的数码管 38 39 //--------------------------------------------------------------------------- http://www.fpga.gs/ 304 软核演练篇 §5 40 //-- 名称 : zircon_avalon_segled_allclose() 41 //-- 功能 : 关闭所有的数码管 42 //-- 输入参数 : 无 43 //-- 输出参数 : 无 44 //--------------------------------------------------------------------------- 45 void zircon_avalon_segled_allclose() 46 { 47 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_ALL_CLOSE_CS); 48 } 49 50 //--------------------------------------------------------------------------- 51 //-- 名称 : zircon_avalon_segled_allopen() 52 //-- 功能 : 打开所有的数码管,并显示数字 8 53 //-- 输入参数 : 无 54 //-- 输出参数 : 无 55 //--------------------------------------------------------------------------- 56 void zircon_avalon_segled_allopen() 57 { 58 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_ALL_OPEN_CS); 59 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,NUM8); 60 } 61 62 //--------------------------------------------------------------------------- 63 //-- 名称 : zircon_segled_dynamic() 64 //-- 功能 : 数码管的动态驱动,用于 zircon_avalon_segled_demo1 函数 65 //-- 输入参数 : data1、data2、data3、data4、data5、data6 数码管显示的数值 66 //-- 输出参数 : 无 67 //--------------------------------------------------------------------------- 68 void zircon_segled_dynamic(alt_u8 data1,alt_u8 data2,alt_u8 data3,alt_u8 data4,alt_u8 data5,alt_u8 data6) 69 { 70 int i; 71 72 for(i = 0; i < 50 ; i++) 73 { 74 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS1); 75 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data1); 76 usleep(1000); 77 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS2); 78 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data2); 79 usleep(1000); 80 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS3); 81 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data3); 82 usleep(1000); Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 305 83 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS4); 84 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data4); 85 usleep(1000); 86 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS5); 87 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data5); 88 usleep(1000); 89 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS6); 90 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data6); 91 usleep(1000); 92 } 93 } 94 95 //--------------------------------------------------------------------------- 96 //-- 名称 : zircon_segled_dynamic2() 97 //-- 功能 : 数码管的动态驱动,用于 zircon_avalon_segled_demo2 函数 98 //-- 输入参数 : data1、data2、data3、data4、data5、data6 数码管显示的数值 99 //-- 输出参数 : 无 100 //--------------------------------------------------------------------------- 101 void zircon_segled_dynamic2(alt_u8 data1,alt_u8 data2,alt_u8 data3,alt_u8 data4,alt_u8 data5,alt_u8 data6) 102 { 103 int i; 104 105 for(i = 0; i < 50 ; i++) 106 { 107 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS1); 108 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data1); 109 usleep(100); 110 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS2); 111 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data2); 112 usleep(100); 113 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS3); 114 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data3); 115 usleep(100); 116 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS4); 117 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data4); 118 usleep(100); 119 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS5); 120 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data5); 121 usleep(100); 122 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS6); 123 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data6); 124 usleep(100); 125 } http://www.fpga.gs/ 306 软核演练篇 §5 126 } 127 128 //--------------------------------------------------------------------------- 129 //-- 名称 : zircon_segled_dynamic3() 130 //-- 功能 : 数码管的动态驱动,用于 zircon_avalon_segled_demo3 函数 131 //-- 输入参数 : data1、data2、data3、data4、data5、data6 数码管显示的数值 132 //-- 输出参数 : 无 133 //--------------------------------------------------------------------------- 134 void zircon_segled_dynamic3(alt_u8 data1,alt_u8 data2,alt_u8 data3,alt_u8 data4,alt_u8 data5,alt_u8 data6) 135 { 136 137 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS1); 138 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data1); 139 usleep(150); 140 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS2); 141 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data2); 142 usleep(150); 143 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS3); 144 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data3); 145 usleep(150); 146 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS4); 147 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data4); 148 usleep(150); 149 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS5); 150 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data5); 151 usleep(150); 152 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_CS_BASE,SEGLED_OPEN_CS6); 153 IOWR_ALTERA_AVALON_PIO_DATA(SEGLED_DATA_BASE,data6); 154 usleep(150); 155 156 } 157 158 //--------------------------------------------------------------------------- 159 //-- 名称 : zircon_avalon_segled_demo1() 160 //-- 功能 : 从左到右,依次打开数码管并显示 0、1、2、3、4、5,再从右到左依次关闭数码管 161 //-- 输入参数 : 无 162 //-- 输出参数 : 无 163 //--------------------------------------------------------------------------- 164 void zircon_avalon_segled_demo1() 165 { 166 167 zircon_segled_dynamic(NUM0,NUMF,NUMF,NUMF,NUMF,NUMF); 168 zircon_segled_dynamic(NUM0,NUM1,NUMF,NUMF,NUMF,NUMF); Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 307 169 zircon_segled_dynamic(NUM0,NUM1,NUM2,NUMF,NUMF,NUMF); 170 zircon_segled_dynamic(NUM0,NUM1,NUM2,NUM3,NUMF,NUMF); 171 zircon_segled_dynamic(NUM0,NUM1,NUM2,NUM3,NUM4,NUMF); 172 zircon_segled_dynamic(NUM0,NUM1,NUM2,NUM3,NUM4,NUM5); 173 zircon_segled_dynamic(NUM0,NUM1,NUM2,NUM3,NUM4,NUMF); 174 zircon_segled_dynamic(NUM0,NUM1,NUM2,NUM3,NUMF,NUMF); 175 zircon_segled_dynamic(NUM0,NUM1,NUM2,NUMF,NUMF,NUMF); 176 zircon_segled_dynamic(NUM0,NUM1,NUMF,NUMF,NUMF,NUMF); 177 zircon_segled_dynamic(NUM0,NUMF,NUMF,NUMF,NUMF,NUMF); 178 179 } 180 181 //--------------------------------------------------------------------------- 182 //-- 名称 : zircon_avalon_segled_demo2() 183 //-- 功能 : 让每一个数码管都随机显示 0-E 184 //-- 输入参数 : 无 185 //-- 输出参数 : 无 186 //--------------------------------------------------------------------------- 187 void zircon_avalon_segled_demo2() 188 { 189 190 int display[15] = {NUM0,NUM1,NUM2,NUM3,NUM4,NUM5,NUM6,NUM7, NUM8,NUM9,NUMA,NUMB,NUMC,NUMD,NUME}; 191 int i,k1,k2,k3,k4,k5,k6; 192 193 for(i = 0;i < 50; i++) 194 { 195 k1 = rand()%15;k2 = rand()%15; 196 k3 = rand()%15;k4 = rand()%15; 197 k5 = rand()%15;k6 = rand()%15; 198 zircon_segled_dynamic2(display[k1],display[k2],display[k3], display[k4],display[k5],display[k6]); 199 } 200 201 zircon_avalon_segled_allclose(); 202 } 203 204 //--------------------------------------------------------------------------- 205 //-- 名称 : zircon_avalon_segled_demo3() 206 //-- 功能 : 用数码管实现秒表的功能 207 //-- 输入参数 : 无 208 //-- 输出参数 : 无 209 //--------------------------------------------------------------------------- 210 void zircon_avalon_segled_demo3() http://www.fpga.gs/ 308 软核演练篇 §5 211 { 212 int display[15] = {NUM0,NUM1,NUM2,NUM3,NUM4,NUM5,NUM6,NUM7, NUM8,NUM9,NUMA,NUMB,NUMC,NUMD,NUME}; 213 int k1=0,k2=0,k3=0,k4=0; 214 215 while(1) 216 { 217 k1++; 218 if(k1 == 10) {k2++; k1 = 0;} 219 if(k2 == 10) {k3++; k2 = 0;} 220 if(k3 == 10) {k4++; k3 = 0;} 221 if(k4 == 10) 222 { 223 k4 = 0; 224 zircon_avalon_segled_allclose(); 225 break; 226 } 227 zircon_segled_dynamic3(NUMF,NUMF,display[k4], display[k3],display[k2],display[k1]); 228 usleep(100); 229 } 230 231 } 232 233 //--------------------------------------------------------------------------- 234 //-- 名称 : main() 235 //-- 功能 : 程序入口 236 //-- 输入参数 : 无 237 //-- 输出参数 : 无 238 //--------------------------------------------------------------------------- 239 int main() 240 { 241 FILE* fp; 242 char prompt = 0; 243 244 printf("Welcome to segled ip demo program... \n"); 245 printf("1. open all segled... \n"); 246 printf("2. close all segled...\n"); 247 printf("3. segled demo1 ...\n"); 248 printf("4. segled demo2...\n"); 249 printf("5. segled demo3...\n"); 250 printf("6. exit program\n"); 251 fp = fopen(JTAG_UART_NAME, "r+"); //以可读写方式打开设备文件 252 if(fp) //成功打开设备文件 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 309 253 { 254 while(prompt != '6') //循环直至接收到 '5' 255 { 256 fprintf(fp, "Please write one number and press Enter... \n"); 257 prompt = getc(fp); //从 JTAG UART 中获取字符 258 switch(prompt) 259 { 260 case '1': {zircon_avalon_segled_allopen(); break;} 261 case '2': {zircon_avalon_segled_allclose(); break;} 262 case '3': {zircon_avalon_segled_demo1(); break;} 263 case '4': {zircon_avalon_segled_demo2(); break;} 264 case '5': {zircon_avalon_segled_demo3(); break;} 265 default : {break;} 266 } 267 } 268 fprintf(fp, "Closing the JTAG UART file handle.\n"); 269 fclose(fp); //关闭设备文件 270 } 271 else 272 { 273 printf("Fail to open file...\n"); //设备文件打开失败 274 } 275 return 0; 276 277 } 从该代码中我们可以看出,我们的函数全都是通过 PIO 寄存器头文件中的写操作来完成, 我们的主程序中的代码和我们的数码管 IP 核中的主程序基本上一致,唯一不同的是我们将需要 执行的函数全都替换成我们上面所编写的函数,而不在是数码管 IP 核中的函数。只要是个单片 机高手,我相信分分钟钟就能搞定该程序。 (4) 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Segled.sof 下载至我们的 A4 开发板,Qsys_Segled.sof 下载完 成后,我们还需要在 Eclipse 软件中将 Qsys_Segled.elf 文件下载至我们的 A4 开发板, Qsys_Segled.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可 以在 Eclipse 软件的控制台中看到我们的打印信息,如图 5.52 所示。 http://www.fpga.gs/ 310 软核演练篇 §5 图 5.52 数码管 IP 核的控制台打印信息图 这时,我们使用键盘输入数字 1 以后按下回车,我们可以看到我们的 A4 开发板数码管全部 亮起,并显示数字 8,如图 5.53 所示。 图 5.53 数码管 IP 核的板级调试图 当然,我们还可以按下数字 2、3、4、5,我们的 A4 开发板都会执行相对应的数码管显示 效果。至此,我们的数码管 PIO 的应用就讲解完了。通过该工程我们可以看出,我们使用数码 管 PIO 的方式也是可以实现和我们使用数码管 IP 核的方式一样的功能,并且我们使用数码管 PIO 的方式更方便简单。我们的 Qsys_Segled.c 文件中的代码看似变多了,复杂了,其实它和 我 们 的 Qsys_Segled_Ip.c 代 码 基 本 上 是 一 致 的 , 只 不 过 我 们 是 将 所 有 的 程 序 都 写 在 了 Qsys_Segled.c 文件中。此时此刻,我们应该自我怀疑一下,我使用自定义 IP 核的方式是否是 值得的?我们是不是一定要对一些外设去定制 IP 核呢?从该工程的结果我们可以看出,我们使 用自定义 IP 核的方式是有点浪费时间的,我们完全可以使用 PIO 的方式来替代自定义 IP 核的 方式,通过使用 PIO 的方式和使用自定义 IP 核的方式对比我们可以知道,我们使用 PIO 的方式 是多么的轻松、多么容易,我们只需要添加 PIO IP 核,编写与其相对应的操作就可以完成外设 控制了,我们完全可以省去定制 IP 核的六个步骤。在此时,也许大家会庆幸还好我们只是学了 两个外设 LED 和数码管,在接下来的 TLC5615 DA 外设学习中,我们将通过更简单的 PIO 方 式来实现它,因为我们毕竟是要去用我们这个外设,至于自定义 IP 核的方式我们在 TLC5615 DA 外设中再去斟酌一下,它到底有没有任何好的作用。 §5.4 TLC5615 DA外设 5.4.1 DA PIO 的应用 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 311 (1) 功能概述 由于我们在数码管的 IP 核的定制过程中,我们发现使用 PIO 也一样能够实现我们 IP 核所 实现的功能,所以我们就偷懒偷到底,接下来我们就使用 PIO 的方式来实现 TLC5615 DA 外设 的控制。在该工程中,我们主要是利用 TLC5615 DA 外设生成一个正弦波。我们先将正弦波的 数据保存在数组中,然后我们通过读取该数组中的数据送给我们的 DA 模块进行数模转换,转换 完成的模拟信号我们直接通过 A4 开发板上的 SMA 头进行了输出。这里需要我们注意的是,我 们在 A4 开发板上是看不到任何效果的,我们需要使用示波器测量我们的 DA 输出管脚,然后才 能看到输出的正弦波形。 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该工程的硬件框架,如图 5.54 所示。 图 5.54 DA PIO 的硬件框架图 从该图中我们可以看出,该硬件框架图很简单,我们添加了 3 个 PIO IP 核,这 3 个 PIO IP 核我们都配置成了 1 位的输出,它们分别对应 DA 外设的时钟管脚、DA 外设的使能管脚和 DA 外设的数据管脚。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 5.17 所 示。 代码 5.17 Qsys_Tlc5615.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Tlc5615.c 3 //-- 描述 : 使用 PIO IP 核来测试我们的 TLC5615 DA 外设生成正弦波形 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. http://www.fpga.gs/ 312 软核演练篇 §5 6 //--------------------------------------------------------------------------- 7 #include 8 #include "system.h" 9 #include "unistd.h" 10 #include "alt_types.h" 11 #include "altera_avalon_pio_regs.h" 12 13 /*正弦波的数据*/ 14 alt_u16 sine_wave[32] = { 15 0x800,0x980,0xB00,0xC60,0xDA0,0xEA0,0xF50,0xFD0, 16 0xFF0,0xFD0,0xF50,0xEA0,0xDA0,0xC60,0xB00,0x980, 17 0x7F0,0x670,0x4F0,0x390,0x250,0x150,0x0A0,0x020, 18 0x000,0x020,0x0A0,0x150,0x250,0x390,0x4F0,0x670 19 }; 20 21 //--------------------------------------------------------------------------- 22 //-- 名称 : zircon_delay() 23 //-- 功能 : 软件延时 24 //-- 输入参数 : data,延时参数,其值越大,延时越久 25 //-- 输出参数 : 无 26 //--------------------------------------------------------------------------- 27 void zircon_delay(alt_u16 data) 28 { 29 int i; 30 for(i = 0; i < data; i++) 31 ; 32 } 33 34 //--------------------------------------------------------------------------- 35 //-- 名称 : zircon_tlc5615() 36 //-- 功能 : TLC5615 的驱动函数 37 //-- 输入参数 : data,发送的正弦数据 38 //-- 输出参数 : 无 39 //--------------------------------------------------------------------------- 40 void zircon_tlc5615_send(alt_u16 data) 41 { 42 alt_u8 i; 43 44 IOWR_ALTERA_AVALON_PIO_DATA(DA_CLK_BASE,0); //将 DA 的时钟信号拉低 45 IOWR_ALTERA_AVALON_PIO_DATA(DA_CS_BASE,0); //将 DA 的片选信号拉低 46 47 for(i = 0; i < 12; i++) 48 { 49 if((data & 0x800) == 0) Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 313 50 { 51 IOWR_ALTERA_AVALON_PIO_DATA(DA_DIN_BASE,0); //将 DA 的数据信号拉低 52 zircon_delay(20); 53 } 54 else 55 { 56 IOWR_ALTERA_AVALON_PIO_DATA(DA_DIN_BASE,1); //将 DA 的数据信号拉高 57 zircon_delay(20); 58 } 59 60 IOWR_ALTERA_AVALON_PIO_DATA(DA_CLK_BASE,1); //将 DA 的时钟信号拉高 61 data = data << 1; //左移一位数据 62 IOWR_ALTERA_AVALON_PIO_DATA(DA_CLK_BASE,0); //将 DA 的时钟信号拉低 63 } 64 65 IOWR_ALTERA_AVALON_PIO_DATA(DA_CS_BASE,1); 66 } 67 68 //--------------------------------------------------------------------------- 69 //-- 名称 : main() 70 //-- 功能 : 程序入口 71 //-- 输入参数 : 无 72 //-- 输出参数 : 无 73 //--------------------------------------------------------------------------- 74 int main() 75 { 76 alt_u8 i; 77 78 printf("Sine_wave Send Start...\n"); 79 80 while(1) 81 { 82 for(i = 0; i < 32; i++) 83 { 84 zircon_tlc5615_send(sine_wave[i]); //DA 外设发送正弦波数据 85 zircon_delay(500); 86 } 87 } 88 89 return 0; 90 } 从该代码中我们可以看出,代码很简单,也很少,我们轻轻松松就实现了 DA 外设的控制, 节省了我们大量的时间,如果我们使用的是 IP 核的方式来实现 DA 外设的控制,那么我们现在 http://www.fpga.gs/ 314 软核演练篇 §5 连 IP 核都还没定制完成呢。这里需要注意的是,如果你对 TLC5615 DA 外设的时序控制还不理 解,或者说还不知道如何控制 TLC5615 DA 外设,那么你可以参考我们的《项目实战篇》,我们 的《项目实战篇》中有对 TLC5615 DA 外设详细的讲解。 (4) 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Tlc5615.sof 下载至我们的 A4 开发板,Qsys_Tlc5615.sof 下载完 成后,我们还需要在 Eclipse 软件中将 Qsys_Tlc5615.elf 文件下载至我们的 A4 开发板, Qsys_Tlc5615.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可 以在 Eclipse 软件的控制台中看到我们的打印信息,如图 5.55 所示。 图 5.55 DA PIO 的控制台打印信息图 这时,我们使用数字示波器来测量我们的 DA 输出端口,我们可以看到示波器上显示如图 5.56 所示。 图 5.56 DA PIO 的板级调试图 从该图中我们可以看出,这个波形还是很完美的,完全就是一个正弦波,我们再来看下它的 频率,我们可以从示波器中看到它的频率是 347Hz。从我们测量出来的 DA 频率来看,我们的 频率有点低,如果我们想要用这种频率的 DA 来当作我们的毕业设计,这完全是不达标的,我们 通过度娘搜索了某某某校的一个有关 DA 外设的毕业设计,该毕设的要求如下: Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 315  首先电路能够输出正弦波、方波和三角波等多个波形。  输出信号频率要求可调,频率范围 1K~10KHz。 看到第一个要求后,我们可以知道,第一条要求很简单,我们很容易就能实现,我们只需要 再声明一个数组,里面放入方波或者三角波的数据,我们就能够实现多个波形了,这个不是难题。 看到第二个要求后,我们心里就有点不安了,因为我们的输出频率只有 347Hz,连 1KHz 都没有 达到,如何实现 10K 呀,这也太说不过去了吧。接下来我们就来重点研究研究我们的代码,看下 究竟什么原因致使它的频率这么低,我们怎么样才能够将我们的波形提高到 10Khz。从我们的代 码中,我们可以知道,如果程序想要变快,那么我们就需要减少延迟,首先我们将第 85 行 zircon_delay(500)代码给注释掉,然后我们再次编译重新下载程序,这时我们发现示波器上的频 率发生了变化,如图 5.57 所示。 图 5.57 DA PIO 的板级调试图 从该图中我们可以看出,我们的波形变密了,我们可以看到频率由 347Hz 变成了 898Hz, 虽然示波器的频率增加了,但是还远远不够快,我们干脆把所有的延迟函数都去掉。我们将程序 中的所有延迟都给注释掉以后,我们再次编译重新下载程序,这时我们发现示波器上的频率又发 生了变化,如图 5.58 所示。 http://www.fpga.gs/ 316 软核演练篇 §5 图 5.58 DA PIO 的板级调试图 从该图中我们可以看出,我们正弦波的频率由 898Hz 变成了 5KHz,减少延迟虽然是有效 果,但是效果不理想,还是没有达到我们想要的 10Khz,我们程序中的延迟都注释掉了,已经是 最快了,不能在快了,在快就要出事了。难道我们的 TLC5615 最高只能跑到 5.85Khz,下面我 们就来看看 TLC5615 的数据手册,来计算一下它的最快频率,我们可以在 DA 的数据手册中找 到如图 5.59 所示。 图 5.59 DA 串行时钟和更新速率 通过该图我们可以看出,f(SCLK)max 可以通过 1/tw(CH)+ tw(CL)这个公式进行计算的,这 个公式中的 tw(CH)和 tw(CL)就是我们时序图中 SCLK 高电平和低电平持续的时间,通过查找 TLC5615 的芯片手册我们可以知道它们都是 25ns。因此,f(SCLK)max 计算得出最大约为 14MHz。我们知道 TLC5615 它有两种工作方式,第一种是 12 位,第二种是 16 位,我们这里就 按照手册中的 16 位来进行计算,因此我们就可以通过第二个公式计算出 TLC5615 转换一次需 要 16x(25+25)+20 = 820ns,然后我们再代入第一个公式可以计算出 f(SCLK)max = 1/820ns = 1.21MHz,当然这 1.21Mhz 只是理想情况下的,根据上图中我们还可以知道,However,DAC setting time to 10 bit of 12.5us limits the updata rate to 80Khz for full-scale input setp transitions, 这什么意思,它就是我们最后虽然可以已 1.21MHz 的速率去更新这个 DA,但是对于 DA 来说, Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 317 信号从一个高变成一个低的陡变过程,它需要一定的建立时间,也就是上面所说的 12.5us,这 个 12.5us 是非常长的,理想情况下它从一个电压跳到另一个电压需要 80KHz。实际上严格计算 的话,它只能达到 75KHz 左右,也就是 1/(820ns+12.5us)。如果你学过《信号与系统》或者是 《数字信号处理》的话,那么你就知道想要生成一个完整周期信号,至少需要 fmax 的两倍,这 意味着我们想要生成一个正弦波,它能够看见,至少需要 2 个点,这样的话我们的 TLC5615 的 极限速度也就是 75KHz/2=37.5KHz。最后我们发现,并不是我们的器件达不到 10K,而是我们 程序送出去的速度不够。我们虽然功能实现了,能够输出一个比较完美的正弦波了,但是我们性 能达不到要求,我们最多的频率只能达到 5.85KHz,这距离我们的毕业要求 10KHz,还是远远 不够的,我们通过这种 PIO 的方式是没有办法完成的,这时候我们发现,使用 PIO 的方式似乎 有了一点点局限性。为了能够解决这个问题,我们只能去尝试使用自定义 IP 核的方式来控制我 们的 TLC5615 DA 外设,下面我们就来看一看自定义 IP 核的方式究竟能不能解决我们 PIO 没 有解决的问题。 5.4.2 DA IP 核的定制 (1) 规划 IP 核的硬件功能 通过前面的学习我们知道,定制 IP 核的第一步,就是规划 IP 核的硬件功能,这里我们直接 给出 DA 外设寄存器的具体规划方案,如表 5.17 所示。 偏移量 00 寄存器名称 数据寄存器 表 5.17 DA IP 核寄存器映射 位描述 操作 31 … 8 7 … 0 写 保留 DATA 看到这里你是不是有点吃惊,我们这里给出的寄存器规划方案是不是太简单了,按照我们的 想象,DA 外设要比 LED 和数码管外设复杂的多,它的寄存器的具体规划方案应该要比 LED 和 数码管更复杂才对。其实 DA 并没有大家想象中的那么复杂,它也是很简单的。这个 8 位的数据 寄存器就是用来发送我们的 DA 数据的。 (2) 定义一个恰当的 Avalon 接口 规划完 DA IP 核的硬件功能,接下来我们就来给 DA 定义 Avalon 接口。我们将信号总结如 表 5.18 所示。 信号名 clock reset address write writedata tlc5615_clk tlc5615_cs tlc5615_data 表 5.18 DA IP 核的接口信号 Avalon 信号类型 宽度 Clock 1 Reset 1 Avalon-MM 2 Avalon-MM 1 Avalon-MM 32 Conduit 1 Conduit 1 Conduit 1 方向 Input Input Input Input Input Output Output Output http://www.fpga.gs/ 318 软核演练篇 §5 前面这几个信号,相信大家已经都很熟悉了,后面这几个信号都是 DA 的管脚。由于我们的 Avalon 信号没有变化,我们可以知道 Avalon 的时序还是和之前一样的,这里我们就不在进一步 介绍了。 (3) 使用硬件描述语言描述硬件逻辑 定义完了 Avalon 接口,接下来我们就来看看我们的三个.v 文件,首先我们给出的是顶层接 口文件 zircon_avalon_tlc5615.v,如代码 5.18 所示。 代码 5.18 zircon_avalon_tlc5615.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_tlc5615.v 3 //-- 描述 : TLC5615 DA IP 核的顶层文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_tlc5615 8( 9 //时钟复位 10 csi_clk,rsi_reset_n, 11 //Avalon-MM 从端口 12 avs_address,avs_write,avs_writedata, 13 //外设管脚输出 14 coe_tlc5615_cs,coe_tlc5615_data,coe_tlc5615_clk 15 ); 16 17 input 18 input 19 input 20 input 21 input [31:0] csi_clk; rsi_reset_n; avs_address; avs_write; avs_writedata; //系统时钟 //系统复位 //Avalon 地址总线 //Avalon 写请求信号 //Avalon 写数据总线 22 23 output 24 output 25 output coe_tlc5615_cs; coe_tlc5615_data; coe_tlc5615_clk; //tlc5615 的片选信号 //tlc5615 的数据信号 //tlc5615 的时钟信号 26 27 wire 28 wire [ 7:0] da_start; da_in; //tlc5615 发送开始位 //tlc5615 发送的 8 位数据 29 30 zircon_avalon_tlc5615_logic zircon_avalon_tlc5615_logic_init 31 ( 32 .CLK_50M 33 .RST_N 34 .DA_CLK 35 .DA_DIN 36 .DA_CS (csi_clk ), (rsi_reset_n ), (coe_tlc5615_clk ), (coe_tlc5615_data ), (coe_tlc5615_cs ), //系统时钟 //系统复位 //tlc5615 的时钟信号 //tlc5615 的数据信号 //tlc5615 的片选信号 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 319 37 .DA_DATA ({da_in,2'b00} ), //tlc5615 发送的 8 位数据 38 .send_start (da_start ) //tlc5615 发送开始位 39 ); 40 41 zircon_avalon_tlc5615_register zircon_avalon_tlc5615_register_init 42 ( 43 .csi_clk (csi_clk ), //系统时钟 44 .rsi_reset_n (rsi_reset_n ), //系统复位 45 .avs_address (avs_address ), //Avalon 地址总线 46 .avs_write (avs_write ), //Avalon 写请求信号 47 .avs_writedata (avs_writedata ), //Avalon 写数据总线 48 .da_in (da_in ), //tlc5615 发送的 8 位数据 49 .da_start (da_start ) //tlc5615 发送开始位 50 ); 51 52 endmodule 接下来我们给出的是寄存器文件 zircon_avalon_tlc5615_register.v,如代码 5.19 所示。 代码 5.19 zircon_avalon_tlc5615_register.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_tlc5615_register.v 3 //-- 描述 : TLC5615 DA IP 核的寄存器文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_tlc5615_register 8( 9 //时钟复位 10 csi_clk,rsi_reset_n, 11 //Avalon-MM 从端口 12 avs_address,avs_write,avs_writedata, 13 //用户逻辑输入与输出 14 da_in,da_start 15 ); 16 17 input 18 input 19 input 20 input 21 input [31:0] csi_clk; rsi_reset_n; avs_address; avs_write; avs_writedata; //系统时钟 //系统复位 //Avalon 地址总线 //Avalon 写请求信 //Avalon 写数据总线 22 23 output reg 24 reg 25 output reg [ 7:0] [ 7:0] da_in; da_in_n; da_start; //tlc5615 发送的 8 位数据 //da_in 的下一个状态 //tlc5615 发送开始位 http://www.fpga.gs/ 320 软核演练篇 §5 26 reg da_start_n; //da_start 的下一个状态 27 28 //时序电路,用于给数据寄存器进行赋值的 29 always @ (posedge csi_clk or negedge rsi_reset_n) 30 begin 31 if(!rsi_reset_n) //判断复位 32 da_in <= 8'h00; //初始化数据寄存器 33 else 34 da_in <= da_in_n; //用来给数据寄存器赋值 35 end 36 37 //组合电路,用来给地址偏移量 0,也就是我们的数据寄存器写 8 位的数据 38 always @ (*) 39 begin 40 if((avs_write) && (avs_address == 1'b0)) //判断写使能和地址偏移量 41 da_in_n = avs_writedata[7:0]; //如果条件成立,那么将写数据中的值赋值给数据寄存器 42 else 43 da_in_n = da_in; //否则,将保持不变 44 end 45 46 //时序电路,用于给 tlc5615 发送开始位进行赋值的 47 always @ (posedge csi_clk or negedge rsi_reset_n) 48 begin 49 if(!rsi_reset_n) //判断复位 50 da_start <= 1'b0; //初始化 tlc5615 发送开始位 51 else 52 da_start <= da_start_n; //用来给 tlc5615 发送开始位赋值 53 end 54 55 //组合电路,用来判断 tlc5615 发送开始位 56 always @ (*) 57 begin 58 if((avs_write) && (avs_address == 1'b0)) //判断写使能和地址偏移量 59 da_start_n = 1'b1; //如果条件成立,那么将 tlc5615 发送开始位置 1 60 else 61 da_start_n = 1'b0; //否则,将置 0 62 end 63 64 endmodule 最后我们给出的是硬件逻辑 zircon_avalon_tlc5615_logic.v,如代码 5.20 所示。 代码 5.20 zircon_avalon_tlc5615_logic.v 代码 1 //--------------------------------------------------------------------------2 //-- 文件名 : zircon_avalon_tlc5615_logic.v Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 321 3 //-- 描述 : TLC5615 DA IP 核的硬件逻辑文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_tlc5615_logic 8 ( 9 //输入端口 10 CLK_50M,RST_N, 11 //TLC5615 输出管脚 12 DA_CLK,DA_DIN,DA_CS, 13 //用户逻辑输入与输出 14 DA_DATA,send_start,send_finish 15 ); 16 17 //--------------------------------------------------------------------------- 18 //-- 外部端口声明 19 //--------------------------------------------------------------------------- 20 21 input CLK_50M; //系统时钟 22 input RST_N; //系统复位 23 output reg DA_CLK; 24 output DA_DIN; 25 output reg DA_CS; 26 input [ 9:0] DA_DATA; //DA 时钟端口 //DA 数据输出端口 //DA 片选端口 //DA 数据的输入 27 input send_start; //DA 工作开始标识 28 output send_finish; //DA 工作完成标识 29 30 //--------------------------------------------------------------------------- 31 //-- 内部端口声明 32 //--------------------------------------------------------------------------- 33 reg [ 3:0] FSM_CS; //状态机的当前状态 34 reg [ 3:0] FSM_NS; //状态机的下一个状态 35 reg [ 3:0] bit_cnt; //用来记录数据发送个数的计数器 36 reg 37 reg 38 reg 39 reg [ 3:0] [11:0] [11:0] [ 3:0] bit_cnt_n; shift_reg; shift_reg_n; time_cnt; //bit_cnt 的下一个状态 //移位寄存器,将最高位数据移给 DA_DIN //shift_reg 的下一个状态 //用于记录时钟个数的计数器 40 reg [ 3:0] time_cnt_n; //time_cnt 的下一个状态 41 reg DA_CLK_N; //DA_CLK 的下一个状态 42 reg DA_CS_N; //DA_CS 的下一个状态 43 44 parameter 45 parameter FSM_IDLE = 4'h0; FSM_READY = 4'h1; //状态机的空闲状态 //状态机的准备状态,将 CS 拉低 46 parameter FSM_SEND = 4'h2; //状态机的发送状态,发送 12 个数据 http://www.fpga.gs/ 322 软核演练篇 §5 47 parameter FSM_FINISH= 4'h4; //状态的完成状态,将 CS 拉高 48 49 //--------------------------------------------------------------------------- 50 //-- 逻辑功能 51 //--------------------------------------------------------------------------- 52 //时序电路,用来给 time_cnt 寄存器赋值 53 always @ (posedge CLK_50M or negedge RST_N) 54 begin 55 if(!RST_N) 56 time_cnt <= 4'h0; //判断复位 //初始化 time_cnt 值 57 else 58 time_cnt <= time_cnt_n; //用来给 time_cnt 赋值 59 end 60 61 //组合电路,用于记录时钟个数的计数器 62 always @ (*) 63 begin 64 if(FSM_CS != FSM_NS) //判断状态机的当前状态 65 time_cnt_n = 4'h0; //如果当前的状态不等于下一个状态,计数器就清零 66 else if(DA_CLK != DA_CLK_N) //判断时钟的当前状态 67 time_cnt_n = 4'h0; //如果当前的时钟不等于下一个时钟状态,计数器清零 68 else 69 time_cnt_n = time_cnt + 4'h1;//否则计数器就加 1 70 end 71 72 //时序电路,用来给 bit_cnt 寄存器赋值 73 always @ (posedge CLK_50M or negedge RST_N) 74 begin 75 if(!RST_N) 76 bit_cnt <= 4'h0; //判断复位 //初始化 bit_cnt 值 77 else 78 bit_cnt <= bit_cnt_n; //用来给 bit_cnt 赋值 79 end 80 81 //组合电路,用来记录数据发送个数的计数器 82 always @ (*) 83 begin 84 if(FSM_CS == FSM_FINISH) //判断状态机的当前状态 85 bit_cnt_n = 4'h0; //如果当前的状态不等于完成状态,bit_cnt_n 就置 0 86 else if(DA_CLK && (!DA_CLK_N))//判断时钟的当前状态 87 bit_cnt_n = bit_cnt + 4'h1;//如果当前时钟等于下一个时钟取非的状态,bit_cnt_n 就加 1 88 else 89 bit_cnt_n = bit_cnt; //否则保持不变 90 end Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 323 91 92 //时序电路,用来给 shift_reg 寄存器赋值 93 always @ (posedge CLK_50M or negedge RST_N) 94 begin 95 if(!RST_N) //判断复位 96 shift_reg <= 12'h0; //初始化 shift_reg 值 97 else 98 shift_reg <= shift_reg_n;//用来给 shift_reg 赋值 99 end 100 101 //组合电路,移位寄存器,将 DA_DATA 的数据依次移给 DA_DIN 102 always @ (*) 103 begin 104 if(send_start) //判断开始标识 105 shift_reg_n = {DA_DATA,2'h0};//如果标志为 1,则将 DA_DATA 的数据赋值给移位寄存器 106 else if(DA_CLK && (time_cnt == 4'h0))//判断 DA_CLK 的状态 107 shift_reg_n = {shift_reg[10:0] , 1'h0};//如果 DA_CLK 为高,移位寄存器开始移位 108 else 109 shift_reg_n = shift_reg; //否则保持不变 110 end 111 112 //--------------------------------------------------------------------------- 113 //-- 状态机 114 //--------------------------------------------------------------------------- 115 116 //时序电路,用来给 FSM_CS 寄存器赋值 117 always @ (posedge CLK_50M or negedge RST_N) 118 begin 119 if(!RST_N) 120 FSM_CS <= FSM_IDLE; //判断复位 //初始化 FSM_CS 值 121 else 122 FSM_CS <= FSM_NS; //用来给 FSM_CS 赋值 123 end 124 125 //组合电路,用来实现状态机 126 always @ (*) 127 begin 128 case(FSM_CS) //判断状态机的当前状态 129 FSM_IDLE: 130 if(send_start) //判断开始标识 131 FSM_NS = FSM_READY; //如果标识符为 1,则进入准备状态 132 else 133 FSM_NS = FSM_CS; //否则保持原状态不变 134 http://www.fpga.gs/ 324 软核演练篇 §5 135 FSM_READY: 136 if(time_cnt == 4'h1) //等待两个时钟 137 FSM_NS = FSM_SEND; //两个时钟到了便进入发送状态 138 else 139 FSM_NS = FSM_CS; //否则保持原状态不变 140 141 FSM_SEND: 142 if((bit_cnt == 4'hC)&&(!DA_CLK))//发送数据 12 个 143 FSM_NS = FSM_FINISH; //发送完成进入完成状态 144 else 145 FSM_NS = FSM_CS; //否则保持原状态不变 146 147 FSM_FINISH: 148 if(time_cnt == 4'h2) //等待三个时钟 149 FSM_NS = FSM_IDLE; //完成一次数据转换,进入下一次转换 150 else 151 FSM_NS = FSM_CS; //否则保持原状态不变 152 153 default:FSM_NS = FSM_IDLE; 154 endcase 155 end 156 157 //时序电路,用来给 DA_CS 寄存器赋值 158 always @ (posedge CLK_50M or negedge RST_N) 159 begin 160 if(!RST_N) //判断复位 161 DA_CS <= 1'h1; //初始化 DA_CS 值 162 else 163 DA_CS <= DA_CS_N; //用来给 DA_CS 赋值 164 end 165 166 //组合电路,用来生成 DA 的片选波形 167 always @ (*) 168 begin 169 if(FSM_CS == FSM_READY) //判断状态机的当前状态 170 DA_CS_N = 1'h0; //如果当前的状态等于准备状态,DA_CS_N 就置 0 171 else if((FSM_CS == FSM_FINISH) && (time_cnt == 4'h1))//判断状态机的当前状态 172 DA_CS_N = 1'h1; //如果当前的状态等于完成状态并且时钟计数器等于 1,DA_CS_N 就置 1 173 else 174 DA_CS_N = DA_CS; //否则保持不变 175 end 176 177 //时序电路,用来给 DA_CLK 寄存器赋值 178 always @ (posedge CLK_50M or negedge RST_N) Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 325 179 begin 180 if(!RST_N) 181 DA_CLK <= 1'h0; //判断复位 //初始化 DA_CLK 值 182 else 183 DA_CLK <= DA_CLK_N; //用来给 DA_CLK 赋值 184 end 185 186 //组合电路,用来生成 DA 的时钟波形 187 always @ (*) 188 begin 189 if((FSM_CS == FSM_SEND) && (!DA_CLK) && (time_cnt == 4'h1))//判断当前状态 190 DA_CLK_N = 1'h1; //如果符合上述条件,每两个时钟就会生成一个高电平的 DA_CLK 191 else if((FSM_CS == FSM_SEND) && (DA_CLK) && (time_cnt == 4'h1))//判断当前状态 192 DA_CLK_N = 1'h0; //如果符合上述条件,每两个时钟就会生成一个低电平的 DA_CLK 193 else 194 DA_CLK_N = DA_CLK; //否则保持不变 195 end 196 197 assign DA_DIN = shift_reg[11]; //将移位寄存器的最高位赋值给 DA_DIN 198 assign send_finish = (FSM_CS == FSM_IDLE); //标识发送完成 199 200 endmodule 从上面的三个代码中,我们可以看出,第一个顶层接口文件和第二个寄存器文件都是很简单 的,我们很容易就能看懂,第三个硬件逻辑代码还是挺多的,不过,当你看到这段代码的开头, 我相信你已经明白了这段代码,因为这段代码同样也是出自我们的《项目实战篇》。如果你对这 段代码还不是很熟悉,或者不太理解,可以观看我们的《项目实战篇》,这里我们就不在进一步 介绍了。 (4) 编写用于描述寄存器的 C 头文件和 IP 核的驱动 C 文件 写完了我们的硬件逻辑代码,我们就可以使用 IP 核编辑器来封装我们的硬件逻辑,完成 IP 核的定制了,对于后面的几个步骤,我相信大家都了如指掌了,我们就不在一一进行介绍了,我 们这里主要介绍一下 DA IP 核的寄存器的头文件和 DA IP 核的驱动文件。首先我们给出的是 DA IP 核的寄存器头文件 zircon_avalon_tlc5615_regs.h,如代码 5.21 所示。 代码 5.21 zircon_avalon_tlc5615_regs.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_tlc5615_regs.h 3 //-- Describe : DA IP core register header file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_TLC5615_REGS_H__ 8 #define __ZIRCON_AVALON_TLC5615_REGS_H__ 9 http://www.fpga.gs/ 326 软核演练篇 §5 10 #include 11 12 //Data register 13 #define IOWR_AVALON_ZIRCON_TLC5615_DATA(base,data) 14 15 #endif /* __ZIRCON_AVALON_TLC5615_REGS_H__ */ IOWR(base, 0, data) 从该代码中我们可以看出,它是用来定义数据寄存器的宏定义,接下来我们给出的是底层驱 动头文件 zircon_avalon_tlc5615.h,该文件如代码 5.22 所示。 代码 5.22 zircon_avalon_tlc5615.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_tlc5615.h 3 //-- Describe : DA IP core driver header file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_TLC5615_H__ 8 #define __ZIRCON_AVALON_TLC5615_H__ 9 10 #include "system.h" 11 #include "alt_types.h" 12 #include "zircon_avalon_tlc5615_regs.h" 13 14 #ifdef __cplusplus 15 extern "C" 16 { 17 #endif /* __cplusplus */ 18 19 //--------------------------------------------------------------------------- 20 //-- Name : zircon_avalon_tlc5615_send 21 //-- Function : tlc5615 data transmission function 22 //-- Input parameters : length: the length of the data transmission, 23 //-- wave_data: send data to the first address, 24 //-- delay_data: delay time 25 //-- Output parameters: no 26 //--------------------------------------------------------------------------- 27 void zircon_avalon_tlc5615_send(alt_u16 length, alt_u16* wave_data, alt_u16 delay_data); 28 29 /* Macros used by alt_sys_init */ 30 #define ZIRCON_AVALON_TLC5615_INSTANCE(name, dev) alt_u32 tlc5615_controller_addr = name##_BASE 31 #define ZIRCON_AVALON_TLC5615_INIT(name, dev) while(0) 32 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 327 33 #ifdef __cplusplus 34 } 35 #endif /* __cplusplus */ 36 37 #endif /* __ZIRCON_AVALON_TLC5615_H__ */ 从该代码中我们可以看出,它是用来声明 TLC5615 DA 外设的数据传输函数。最后我们给 出的是底层驱动文件 zircon_avalon_tlc5615.c,该文件如代码 5.23 所示。 代码 5.23 zircon_avalon_tlc5615.c 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_tlc5615.c 3 //-- Describe : DA IP core driver C file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "alt_types.h" 8 #include "zircon_avalon_tlc5615.h" 9 10 extern alt_u32 tlc5615_controller_addr; 11 12 //--------------------------------------------------------------------------- 13 //-- Name : zircon_avalon_tlc5615_delay 14 //-- Function : software delays 15 //-- Input parameters : data: the delay parameters, the greater the value, 16 //-- the longer the delay 17 //-- Output parameters: no 18 //--------------------------------------------------------------------------- 19 void zircon_avalon_tlc5615_delay(alt_u16 data) 20 { 21 int i; 22 for(i = 0; i < data; i++) 23 ; 24 } 25 26 //--------------------------------------------------------------------------- 27 //-- Name : zircon_avalon_tlc5615_send 28 //-- Function : tlc5615 data transmission function 29 //-- Input parameters : length: the length of the data transmission, 30 //-- wave_data: send data to the first address, 31 //-- delay_data: delay time 32 //-- Output parameters: no 33 //--------------------------------------------------------------------------- 34 void zircon_avalon_tlc5615_send(alt_u16 length, alt_u16* wave_data, alt_u16 delay_data) http://www.fpga.gs/ 328 软核演练篇 §5 35 { 36 alt_32 j,i = 0; 37 38 for(j = 0; j < 200000; j++) 39 { 40 while(i < length) 41 { 42 IOWR_AVALON_ZIRCON_TLC5615_DATA(tlc5615_controller_addr,wave_data[i++]); 43 zircon_avalon_tlc5615_delay(delay_data); 44 } 45 i = 0; 46 } 47 48 } 从代码中我们可以看出,它是用来实现 TLC5615 DA 外设的数据传输函数,该函数有三个 参数,第一个参数是用来传递数组的长度大小,第二个参数是用来传递数组的首地址,第三个参 数是用来传递延迟时间。 5.4.3 DA IP 核的应用 (1) 功能概述 完成了 DA IP 核的定制,接下来我们再来看看 DA IP 核的应用,首先我们讲解的是第一部 分功能概述,在该工程中,我们主要是利用 DA IP 核实现了一个演示程序,我们通过输入不同的 数字来生成不同的频率的不同波形,如果我们输入数字 1,那么 DA 将生成 30KHz 的正弦波, 如果我们输入数字 2,那么 DA 将生成 20KHz 的三角波,如果我们输入 3,那么 DA 将生成 10KHz 的矩形波,如果我们输入数字 4,那么就会完成演示并关闭程序。这里需要我们注意的 是,我们在 A4 开发板上是看不到任何效果的,我们需要使用示波器测量我们的 DA 输出管脚, 我们才能看到 DA 输出的波形。 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该工程的硬件框架,如图 5.60 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 329 图 5.60 DA IP 核的硬件框架图 从该图中我们可以看出,除了我们常用几个基本 IP 核以外,我们这里只添加了一个 TLC5615 的 IP 核,该 IP 核的使用是非常简单的,我们不需要任何配置,只需要将它添加至 Qsys 软件中 就可以正常使用了。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 5.24 所 示。 代码 5.24 Qsys_Tlc5615_Ip.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Tlc5615_Ip.c 3 //-- 描述 : 使用 DA IP 核来测试我们的 TLC5615 DA 外设生成不同频率的正弦、矩形、三角波形 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include 8 #include "system.h" 9 #include "zircon_avalon_tlc5615.h" 10 11 /*正弦波的数据*/ 12 alt_u16 sine_wave[32]= 13 { 14 0x80,0x98,0xB0,0xC6,0xDA,0xEA,0xF5,0xFD, 15 0xFF,0xFD,0xF5,0xEA,0xDA,0xC6,0xB0,0x98, 16 0x7F,0x67,0x4F,0x39,0x25,0x15,0x0A,0x02, 17 0x00,0x02,0x0A,0x15,0x25,0x39,0x4F,0x67 18 }; 19 http://www.fpga.gs/ 330 软核演练篇 §5 20 /*矩形波的数据*/ 21 alt_u16 square_wave[32]= 22 { 23 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 24 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 25 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 26 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF 27 }; 28 29 /*三角波的数据*/ 30 alt_u16 triangle_wave[32]= 31 { 32 0x00,0x10,0x20,0x30,0x40,0x50,0x60,0x70, 33 0x80,0x8F,0x9F,0xAF,0xBF,0xCF,0xDF,0xEF, 34 0xFF,0xEF,0xDF,0xCF,0xBF,0xAF,0x9F,0x8F, 35 0x80,0x70,0x60,0x50,0x40,0x30,0x20,0x10 36 }; 37 38 //--------------------------------------------------------------------------- 39 //-- 名称 : main() 40 //-- 功能 : 程序入口 41 //-- 输入参数 : 无 42 //-- 输出参数 : 无 43 //--------------------------------------------------------------------------- 44 int main() 45 { 46 FILE* fp; 47 char prompt = 0; 48 49 printf("Welcome to DA ip demo program... \n"); 50 printf("1. sine wave 30kHz ... \n"); 51 printf("2. triangle wave 20kHz...\n"); 52 printf("3. square wave 10kHz...\n"); 53 printf("4. exit program\n"); 54 fp = fopen(JTAG_UART_NAME, "r+"); //以可读写方式打开设备文件 55 if(fp) //成功打开设备文件 56 { 57 while(prompt != '4') //循环直至接收到 '4' 58 { 59 fprintf(fp, "Please write one number and press Enter... \n"); 60 prompt = getc(fp); //从 JTAG UART 中获取字符 61 switch(prompt) 62 { 63 case '1': {zircon_avalon_tlc5615_send(32, sine_wave, 4); break;} Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 331 64 case '2': {zircon_avalon_tlc5615_send(32, triangle_wave, 8); break;} 65 case '3': {zircon_avalon_tlc5615_send(32, square_wave, 22); break;} 66 default : {break;} 67 } 68 } 69 fprintf(fp, "Closing the JTAG UART file handle.\n"); 70 fclose(fp); //关闭设备文件 71 } 72 else 73 { 74 printf("Fail to open file...\n"); //设备文件打开失败 75 } 76 return 0; 77 } (4) 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Tlc5615_Ip.sof 下载至我们的 A4 开发板,Qsys_Tlc5615_Ip.sof 下载完成后,我们还需要在 Eclipse 软件中将 Qsys_Tlc5615_Ip.elf 文件下载至我们的 A4 开发 板,Qsys_Tlc5615_Ip.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时, 我们可以在 Eclipse 软件的控制台中看到我们的打印信息,如图 5.61 所示。 图 5.61 DA IP 核的控制台打印信息图 这时,我们使用键盘输入数字 1 以后按下回车,我们可以通过示波器看到 DA 生成的 30KHz 的正弦波,如图 5.62 所示。 http://www.fpga.gs/ 332 软核演练篇 §5 图 5.62 DA IP 核的板级调试图 当然,我们还可以按下数字 2、3,我们的 DA 也都会生成相对应的波形。这里我们就不给 大家一一进行展示了,大家可以自行下载至 A4 开发板上逐一验证。至此,我们的 DA IP 核的应 用就讲解完了。通过该工程我们可以看出,我们使用自定义 IP 核的方式是可以解决我们在 PIO 中所解决不了的问题。我们使用 PIO 方式生成的正弦波最快只能达到 5~6KHz,而我们使用自 定义 IP 核的方式可以使我们 DA 的转换效率大大提升,我们甚至可以产生 30~40KHz 的一个漂 亮的正弦波。通过我们自定义 IP 核的方式我们完完全全可以发挥出了 DA 外设的功能。因此, 我们最后得到的结论就是,我们的自定义 IP 核方式是要比我们的 PIO 方式强一些的。 此时此刻,相信大家也能够明白我们为什么要煞费苦心去给大家讲这个 PIO 方式了吧,其 实这全都是我们设计好的,我们早就知道了使用自定义 IP 核的方式要比 PIO 的方式快。为什么 使用自定义 IP 核的方式会快呢?这主要是因为最终控制我们 DA 外设的整个时序是用的我们 Verilog 程序,也就是说,其实我们是用一个纯硬件逻辑的方式去驱动整个 DA 的,所以 DA 可 以达到一个非常高的运行效率。而在我们 PIO 的方式中,为什么它的效率比较低下呢?这主要 是因为在 Eclipse 软件中,每一句程序都是通过 PIO IP 核去实现的,在整个实现 Verilog 硬件工 程中,它需要 PIO 来回的翻转,这样会导致整个时序的操作会浪费大量的时间,即使我们在整 个 Eclipse 工程中,尽可能的减少我们的延时,但是我们要知道程序代码每一行执行的话,它都 会去消耗我们的这个时间,而这部分时间我们没有很好的办法去减少。 我们使用自定义 IP 核的方式去控制 DA 和我们直接使用 PIO 的方式去控制 DA 最大的不同 就是,你到底是在用一个硬件,还是在用一个软件的区别。虽然我们这个 IP 核的方式要写 Verilog 代码,要写 C 函数、要写 Tcl 脚本文件等等,但是这些操作却让程序的运行效率得到了提升。尽 管我们使用 PIO 的方式看似很简单,写起来也很容易,我们在 Eclipse 中直接去翻转 PIO 模拟 出 DA 的时钟,模拟出 DA 的数据,但是这样的操作达不到我们所说的高效率。因为我们用的是 FPGA,而不是一个低速的单片机,这也就是我们前面为什么要说,Qsys 精髓就是定制你自己 的专属 IP 核。对于 PIO 方式,我们倒不是说不可取,比如说它在操作我们的 LED 和数码管的 时候,它是完全可以胜任的,这是因为我们的 LED 和数码管外设相对来说速度比较低,通过采 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 333 用我们的 IP 核方式和采用 PIO 的方式,你是感觉不到有任何的差异的,而对于操控 DA 外设这 些高速的外设时,使用 IP 核的方式和采用 PIO 的方式,它们是有着截然不同的性能差异的。 讲到这里,相信大家也能够体会到我们的苦衷了,但是,有的朋友他是一个单片机高手,对 单片机很有经验,他肯定会不服气了,也许会说在这个工程中,虽然我们的 PIO 的方式在速率 上有所欠缺,但是我们功能已经完成了,我们完全可以使用,我们是不是能够认可 PIO 的操作 方式呢?当然了,到了这个节骨眼上,我们已经没有办法来说明哪种方式好,哪种方式坏,这两 种方法太具有争议性了,喜欢偷懒的就可以使用 PIO 的方式去实现,喜欢折腾的就可以使用 IP 核的方式去实现,对于 DA 外设来说,我们通过 PIO 的方式的确可以用,我们通过自定义 IP 核 的方式也是可以用,显然,在功能实现上二者都是完成了我们的功能,所以,我们这里暂时不去 说使用 PIO 的方式和使用自定义 IP 核的方式,究竟谁好,谁坏,它们还会有哪些优缺点,我们 能做的,那就是在接下来的 VGA 外设中,我们继续使用这两种方法去实现我们的 VGA 外设。 对于谁好谁坏,我们将在下面的讲解中给大家揭晓。 §5.5 VGA外设 5.5.1 VGA PIO 的应用 (1) 功能概述 首先我们使用的是 VGA PIO 的方式去控制我们的 VGA 外设,在该工程中,我们主要是利 用 VGA 外设显示分辨率为 800*600 的白色。 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该工程的硬件框架,如图 5.63 所示。 图 5.63 VGA PIO 的硬件框架图 http://www.fpga.gs/ 334 软核演练篇 §5 从该图中我们可以看出,该硬件框架图很简单,我们添加了 3 个 PIO IP 核,这 3 个 PIO IP 核我们都配置成了输出,我们将它们分别命名为 vga_vsync、vga_hsync 以及 vga_data,其中 1 位的 vga_hsync 对应的是 VGA 外设的水平同步管脚,1 位的 vga_vsync 对应的是 VGA 外设 的垂直同步管脚,8 位的 vga_data 对应的是 VGA 外设的数据管脚。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 5.25 所 示。 代码 5.25 Qsys_Vga.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Vga.c 3 //-- 描述 : 使用 PIO IP 核来测试我们的 VGA 外设 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include 8 #include "system.h" 9 #include "alt_types.h" 10 #include "altera_avalon_pio_regs.h" 11 12 //水平扫描,通常我们称它为行时序 13 #define HSYNC_A 128 // 128 14 #define HSYNC_B 216 // 128 + 88 15 #define HSYNC_C 1016 // 128 + 88 + 800 16 #define HSYNC_D 1056 // 128 + 88 + 800 + 40 17 18 //垂直扫描,通常我们称它为场时序或帧时序 19 #define VSYNC_O 4 // 4 20 #define VSYNC_P 27 // 4 + 23 21 #define VSYNC_Q 627 // 4 + 23 + 600 22 #define VSYNC_R 628 // 4 + 23 + 600 + 1 23 24 //--------------------------------------------------------------------------- 25 //-- 名称 : zircon_vga_display() 26 //-- 功能 : VGA 的清屏函数 27 //-- 输入参数 : data,指定颜色 28 //-- 输出参数 : 无 29 //--------------------------------------------------------------------------- 30 void zircon_vga_display(alt_u8 vga_data) 31 { 32 alt_u32 hsync,vsync; 33 34 for(vsync = 0; vsync HSYNC_B && hsync VSYNC_P && vsync < VSYNC_Q)) 50 IOWR_ALTERA_AVALON_PIO_DATA(VGA_DATA_BASE,vga_data);//写数据 51 } 52 } 53 } 54 55 //--------------------------------------------------------------------------- 56 //-- 名称 : main() 57 //-- 功能 : 程序入口 58 //-- 输入参数 : 无 59 //-- 输出参数 : 无 60 //--------------------------------------------------------------------------- 61 int main() 62 { 63 printf("Vga Test Program Starting...\n"); 64 65 while(1) 66 { 67 zircon_vga_display(0xff); //清屏,颜色为白色。 68 } 69 70 return 0; 71 } 这里我们建议大家先不要看代码,先去板级调试结果。 (4) 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Vga.sof 下载至我们的 A4 开发板,Qsys_Vga.sof 下载完成后, 我们还需要在 Eclipse 软件中将 Qsys_Vga.elf 文件下载至我们的 A4 开发板,Qsys_Vga.elf 下 载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可以在 Eclipse 软件的 http://www.fpga.gs/ 336 软核演练篇 §5 控制台中看到我们的打印信息,如图 5.64 所示。 图 5.64 DA PIO 的控制台打印信息图 这时,我们使用 VGA 显示器来看下我们的屏幕是不是显示白色,咦,屏幕竟然不显示,难 道是程序出了问题?我们先用示波器测量一下 VGA 管脚是否有数据存在。我们先用示波器测量 VGA 的 HSYNC 管脚,也就是我们的水平同步端口,如图 5.65 所示。 图 5.65 VGA 的水平同步管脚的波形 从该图中我们可以看出,波形的轮廓大概是对的,但是波形的频率有点不正常,太低了。经 过我们的分析与调试,我们断定是 PIO 的传输速率不够导致的。下面我们就来测试一下我们的 PIO 速度,看它最快翻转能达到多少。由于工程比较简单,我们在工程中只添加了一个 PIO IP 核,所以我们这里就不再进一步进行讲解,我们直接给出 Eclipse 工程中的测试程序,如代码 5.26 所示。 代码 5.26 Qsys_Test_Pio.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Test_Pio.c 3 //-- 描述 : 用来测试我们的 PIO IP 核的最快翻转速率,需要使用示波器测量 GPIO2 第 40 管脚 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 337 7 #include 8 #include "system.h" 9 #include "unistd.h" 10 #include "altera_avalon_pio_regs.h" 11 12 //--------------------------------------------------------------------------- 13 //-- 名称 : main() 14 //-- 功能 : 程序入口 15 //-- 输入参数 : 无 16 //-- 输出参数 : 无 17 //--------------------------------------------------------------------------- 18 int main() 19 { 20 int i; 21 printf("Starting Test!\n"); 22 23 while(1) 24 { 25 IOWR_ALTERA_AVALON_PIO_DATA(TEST_PIO_BASE,0); 26 for(i = 0; i < 1; i++); 27 IOWR_ALTERA_AVALON_PIO_DATA(TEST_PIO_BASE,1); 28 for(i = 0; i < 1; i++); 29 } 30 31 return 0; 32 } 从该代码中我们可以看出,我们先写 PIO 端口 0,然后等待一个空循环,然后在写 PIO 端 口 1,然后又等待一个空循环,直至循环上面整个过程,我们使用示波器测量 GPIO2 第 40 管 脚,如图 5.66 所示。 http://www.fpga.gs/ 338 软核演练篇 §5 图 5.66 示波器测量 PIO 加 for 循环的最快翻转频率 从该图中我们可以看出,我们添加两个 for 循环,我们的程序最快能跑到 2.08MHz。下面我 们将 for 循环给去掉之后,看下程序能跑多快。如图 5.67 所示。 图 5.67 示波器测量 PIO 不加 for 循环的最快翻转频率 从该图中我们可以看出,这个波形比刚才更密了,这个波形的形状也开始走形了。即使我们 Qsys 系统,它什么都不做,只让它操作一个 PIO,从 0 赋 1,从 1 赋 0,它的速度也才只有 8.33MHz,它和我们 VGA 理想的像素时钟 40MHz 还差太远了,我们使用 PIO 的方式去控制我 们的 VGA 外设,虽然它理论上可行,但是实际上它是走不通的,通过该工程我们可以最终确定, 想要使用 PIO IP 核的方式来实现 VGA 的控制,是不可能的。 在某些情况下,我们使用 PIO 的方式,它是可行的,比如我们的低速外设 LED 和数码管, 它能够很好的控制并完美实现其功能。在某些情况下,它却是可用的,不是最优的,比如说我们 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 339 的 TLC5615 DA 外设。在某些情况下,它甚至是完全不可能的、行不通的,比如说我们的 VGA 外设。在这之前,我们一直都感觉 PIO 还挺好的,它简单易用,就像一个单片机一样对我们 FPGA 进行操作,对于一些简单的外设来说,方便快捷,还能达到我们指定的功能,但是现在从 VGA 外设来看,使用 PIO 的方式它导致我们的整个程序无法工作,它的能力不足以完成这项任务, 这就是一个比较关键的问题了,它不在是完成的一个好和不好的问题,而是行和不行的问题。我 们使用 PIO 的方式只能来处理一些相对比较简单的外设,当碰到速度比较高的一些外设的时候, 我们的 PIO 也是心有余而力不足。为了实现我们的 VGA 控制,下面我们只能使用 VGA IP 核的 方式来实现了。 5.5.2 VGA IP 核的定制 (1) 规划 IP 核的硬件功能 定制 IP 核的第一步,就是规划我们的 VGA IP 核的硬件功能,下面我们给出 VGA 外设寄存 器的规划方案,如表 5.19 所示。 表 5.19 VGA IP 核寄存器映射 位描述 偏移量 寄存器名称 操作 31 … 8 7 … 0 00 数据寄存器 写 Data Address 01 控制寄存器 写 保留 EN 从表中我们可以看出,数据寄存器用了 32 位,控制寄存器用了 1 位,下面我们就对这两个 寄存器分别进行介绍:  数据寄存器:这里的数据寄存器和我们前面几个外设中的数据寄存器大大的不同,这里 的数据寄存器是用来发送 VGA 数据缓冲区地址的。我们将会在 SDRAM 中为 VGA 开 辟一个显示缓冲区,也就是一个数组,这个数组的首地址我们将会利用该数据寄存器进 行传输。  控制寄存器:这里的控制寄存器就比较简单了,它和我们前面几个外设中的控制寄存器 功能是一样的,它控制着 VGA 外设的是否工作,当控制寄存器 EN 为 0 时,我们的 VGA 外设停止工作。当控制寄存器 EN 为 1 时,我们的 VGA 外设将开始工作。 (2) 定义一个恰当的 Avalon 接口 规划完 VGA IP 核的硬件功能,接下来我们就来给 VGA 定义 Avalon 接口。我们将信号总 结如表 5.20 所示。 信号名 clock reset address write writedata 表 5.20 VGA IP 核的接口信号 Avalon 信号类型 宽度 Clock 1 Reset 1 Avalon-MM 从 2 Avalon-MM 从 1 Avalon-MM 从 32 方向 Input Input Input Input Input http://www.fpga.gs/ 340 软核演练篇 §5 address Avalon-MM 主 32 byteenalbe Avalon-MM 主 1 read Avalon-MM 主 1 readdata Avalon-MM 主 8 waitrequest Avalon-MM 主 1 readdatavalid Avalon-MM 主 1 vga_clk Conduit 1 vga_hsync Conduit 1 vga_vsync Conduit 1 vga_rgb Conduit 8 Output Output Output Input Input Input Input Output Output Output 在上面的表格中,我们可以看到,我们 VGA 外设和我们前面的几个外设有点不一样了,之 前我们只用到了 Avalon-MM 从端口,现在我们可以看到,我们的 VGA 外设还用到了 AvalonMM 主端口,对于 Avalon-MM 从端口和 vga 的管脚,我相信大家还是比较容易理解的,这里还 需要我们注意的是,vga_clk 时钟信号,这个时钟信号是用来给我们的 VGA 硬件逻辑文件提供 时钟的。接下来我们就来重点介绍一下多出的这几个新端口:  address 信号:是主端口的地址信号,不管主端口的数据宽度是多少,主端口地址都表 示字节地址。根据主端口的数据宽度,它只能提供与字边界对齐的地址。例如,一个 32 位主端口只能提供与 4 字节边界对齐的地址(如 0x00、0x04、0x08、0x0C 等),在 此情况下,Avalon 交换结构忽略 address 的低两位,为了将数据写入一个字范围内的 一个特定的字节,主端口必须使用 byteenable。  byteenable 信号:是一个矢量信号,每一条信号线对应 writedata 的一个字节段。在写 传输时,大于 8 位宽度的主端口能设置 byteenable 信号来指定写入哪个字节段,在读 传输时,主端口必须设置所有的 byteenable 信号线有效。  read 信号:是来自主端口的一位输出信号,用来表示何时主端口将开始一次新的写传 输。  readdata 信号:用于传递与传输相关的数据,它们的宽度必须是 8、16、32、64 或 128 位,如果主端口同时支持 readdata 和 writedata,那么这两个信号的宽度必须相等。  waitrequest 信号:是主端口输入信号,表示 Avalon 交换结构还未准备好处理传输, 所有主端口需要遵循这样一条黄金规则:服从 waitrequest 信号,在传输开始时,主端 口发出适当的信号启动传输,然后须等待知道 Avalon 交换结构使 waitrequest 失效, avalon 交换结构在步与主端口进行传输时使 waitrequest 信号失效。  readdatavalid 信号:用于具有延迟的流水线读传输。表示来自 Avalon 交换结构的有效 数据出现在 readdata 信号线上,如果主机被延迟,则必须使用该信号。 介绍完了 Avalon 主端口,接下来我们再来介绍一下 Avalon 主端口的功能:说到 Avalon 主 端口的功能,我们不得不再一次提及一下我们之前说的数据寄存器,我们在 SDRAM 中为 VGA 开辟的一个显示缓冲区,这个缓冲区的首地址我们通过该寄存器传输给我们的 Avalon 主端口, 我们的 Avalon 主端口便会通过该首地址向 Avalon 索要数据,这时我们就可以通过 Eclipse 软 件代码向该缓冲区内写数据了,Avalon 主端口从缓冲区内接收到数据以后,它并没有立刻将数 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 341 据传输到我们的 VGA 数据线上,让 VGA 进行显示,而是将数据传输到了一个 FIFO 中。当 FIFO 中存入了数据之后,我们的 VGA 时序控制器便通过读取 FIFO 中的数据,来让我们的 VGA 进 行显示。看到这里,也许有的朋友会有这么一个疑问,为什么不直接传输到 VGA 数据线上?这 样不是节省了一步吗?这主要是因为,我们的 Avalon 总线是工作在 100MHz 的时钟下,而我们 的 VGA 显示模块是工作在 40MHz 的时钟下,它们的时钟不同步,导致我们不能直接进行数据 的传输,我们只有解决了这一跨时钟域问题,我们才能完成不同时钟频率下的数据传输。因此, 我们这里采用了 FIFO 来解决这一问题,我们知道 FIFO 可以设置成双端口读取模式,即独立的 写时钟与独立的读时钟,所以,这一特性便能很好的解决 2 个不同的时钟下进行数据的读写操 作。 介绍完了 Avalon 接口信号,接下来我们再来看一看我们的 Avalon 时序,Avalon 从端口的 时序,即使我们不说,相信大家也都知道它是什么样的了,这里我们就直接来介绍一下我们的 Avalon 主端口的时序,如图 5.68 所示。 图 5.68 VGA IP 核的 Avalon 主端口时序图 下面我们就来简单的介绍一下这个时序:  当 clk 第一个时钟到来时, Avalon 交换结构发出了 waitrequest 使能信号,表示此时 Avalon 交换结构处于还未准备好处理传输的状态,这时,虽然我们 read、address 和 byteenalbe 已经使能,但是我们的 Avalon 交换结构并不捕获它们。  当 clk 第二个信号到来时,Avalon 交换机构使 waitrequest 信号失效,也就是表示此时 Avalon 交换结构已经准备好处理传输的状态,这时,Avalon 交换结构捕获 read、 address 和 byteenalbe 信号来发起一次读传输。此时由于,readdatavalid 信号失效, 主端口并没有捕获到 readdata 信号(D0 数据被挂起)。  当 clk 第三个信号到来时,由于 read、address 和 byteenalbe 三个信号仍然有效,所 以 Avalon 交换结构继续捕获新地址(D1 数据被挂起),此时,readdatavalid 有效,所 以主端口捕获到了有效的 readdata 信号(也就是我们第一次挂起的 D0 数据)。  当 clk 第四个信号到来时,Avalon 交换结构发出了 waitrequest 使能信号,read 和 readdatavalid 都失效,因此 Avalon 交换结构没有捕获地址,也没有捕获数据。(D1 数 据仍然被挂起)。  当 clk 第五个信号到来时,由于 waitrequest 信号失效,read 和 readdatavalid 有效, 此时 Avalon 交换结构捕获新地址将 D2 数据挂起,主端口捕获被挂起的 D1 数据。  当 clk 第六个信号到来时,Avalon 交换结构发出了 waitrequest 使能信号,read 失效, http://www.fpga.gs/ 342 软核演练篇 §5 但是 readdatavalid 有效,此时,主端口捕获被挂起的 D1 数据,至此完成整个时序。 (3) 使用硬件描述语言描述硬件逻辑 定义完了 Avalon 接口,接下来我们就到了第三步使用硬件描述语言描述硬件逻辑,在讲解 硬件逻辑之前,我们先给大家理清思路,先来讲一讲我们设计 VGA IP 核的整个思路:首先我们 在 SDRAM 存储器中开辟一段存储空间,用来存放屏幕显示的数据,我们这里就称它为显示缓 冲区,然后我们通过设计适当的硬件逻辑来建立显示缓冲区与屏幕图像像素之间一一对应的关 系,最后我们在配合 VGA 所需要的行时序和帧时序,将显示缓冲区中的数据不断地输送给显示 器,以此完成屏幕的显示。说完了 VGA IP 核的整个思路,我们再给大家理一下整个工作流程: 首先我们在 Eclipse 软件通过两个寄存器来向我们的 VGA IP 核发送显示缓冲区的首地址和开始 信号。我们的主端口根据显示缓冲区首地址完成主端口的读操作,从显示缓冲区中读取数据,然 后我们将读出的数据发送给我们的缓冲区 FIFO 模块,与此同时,我们的时序模块根据 VGA 的 时序规范生成行、帧同步信号。最后,我们的 VGA 显示器通过我们时序模块生成的行和帧同步 信号,以及我们缓冲 FIFO 模块中的 VGA 显示数据,来完成 VGA 的显示。我们根据这个设计 思路与工作流程,分别写出了与其对应的 4 个.v 文件,它们分别是顶层文件 zircon_avalon_vga.v、 FIFO 缓冲模块 zircon_avalon_vga_fifo.v、硬件逻辑文件 zircon_avalon_vga_logic.v 和寄存器 文件 zircon_avalon_vga_register.v。首先我们来看下顶层文件 zircon_avalon_vga.v 中的代码, 如代码 5.27 所示。 代码 5.27 zircon_avalon_vga.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_vga.v 3 //-- 描述 : Vga Ip 核的顶层文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_vga 8( 9 //时钟和复位端口 10 csi_clk,rsi_reset_n, 11 //Avalon 从端口 12 avs_address,avs_write,avs_writedata, 13 //Avalon 主端口 14 avm_address,avm_byteenable,avm_read,avm_readdata, 15 avm_waitrequest,avm_readdatavalid, 16 //外设管脚输出端口 17 coe_vga_clk,coe_vga_hsync,coe_vga_vsync,coe_vga_rgb 18 ); 19 20 input 21 input csi_clk; rsi_reset_n; //系统时钟 //系统复位 22 23 input [ 1:0] avs_address; //Avalon 从端口地址总线 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 24 input avs_write; //Avalon 从端口写请求信号 25 input [31:0] avs_writedata; //Avalon 从端口写数据总线 26 27 output [31:0] avm_address; //Avalon 主端口地址总线 28 output avm_byteenable; //Avalon 主端口字节使能信号 29 output avm_read; //Avalon 主端口读请求信号 30 input [ 7:0] avm_readdata; //Avalon 主端口读数据总线 31 input 32 input avm_waitrequest; avm_readdatavalid; //Avalon 主端口等待信号 //Avalon 主端口读数据有效信号 33 34 input coe_vga_clk; //vga 的系统时钟 35 output coe_vga_hsync; //vga 的行同步信号 36 output coe_vga_vsync; //vga 的帧同步信号 37 output [ 7:0] coe_vga_rgb; //vga 的数据信号 38 39 wire 40 wire vga_data_en; vga_frame_start; //vga 的数据使能信号 //vga 的每一帧的开始位 41 42 zircon_avalon_vga_logic zircon_avalon_vga_logic_init 43 ( 44 .CLK_40M 45 .RST_N 46 .vga_data_en 47 .HSYNC (coe_vga_clk (rsi_reset_n (vga_data_en (coe_vga_hsync ), //vga 的系统时钟 ), //系统复位 ), //vga 的数据使能信号 ), //vga 的行同步信号 48 .VSYNC (coe_vga_vsync ), //vga 的帧同步信号 49 .vga_frame_start (vga_frame_start ) //vga 的每一帧的开始位 50 ); 51 52 zircon_avalon_vga_register zircon_avalon_vga_register_init 53 ( 54 .csi_clk (csi_clk ), //系统时钟 55 .rsi_reset_n (rsi_reset_n ), //系统复位 56 .avs_address (avs_address ), //Avalon 从端口地址总线 57 .avs_write 58 .avs_writedata (avs_write (avs_writedata ), //Avalon 从端口写请求信号 ), //Avalon 从端口写数据总线 59 60 .avm_address (avm_address ), //Avalon 主端口地址总线 61 .avm_byteenable (avm_byteenable ), //Avalon 主端口字节使能信号 62 .avm_read (avm_read ), //Avalon 主端口读请求信号 63 .avm_readdata (avm_readdata ), //Avalon 主端口读数据总线 64 .avm_waitrequest (avm_waitrequest ), //Avalon 主端口等待信号 65 .avm_readdatavalid (avm_readdatavalid), //Avalon 主端口读数据有效信号 66 67 .coe_vga_clk (coe_vga_clk ), //vga 的系统时钟 http://www.fpga.gs/ 343 344 软核演练篇 §5 68 .coe_vga_rgb 69 .vga_data_en 70 .vga_frame_start 71 ); 72 73 endmodule (coe_vga_rgb ), (vga_data_en ), (vga_frame_start ) //vga 的数据信号 //vga 的数据使能信号 //vga 的每一帧的开始位 从该代码中我们可以看出,顶层文件包含了我们所涉及的全部的接口,我们这里可以看到有 Avalon 主端口 Avalon 从端口,以及我们的 VGA 管脚。下面我们在来看下 FIFO 缓冲模块文件 zircon_avalon_vga_fifo.v,这个文件不是我们编写的,这个模块是由我们的 IP 核直接生成的, 如果你对 FIFO 的使用还不太了解,那么可以参考我们的《软件工具篇》,我们《软件工具篇》 中有对 FIFO IP 核进行详细的讲解。说完了 FIFO 缓冲模块,接下来我们再来看下寄存器文件 zircon_avalon_vga_register.v 中的代码,如代码 5.28 所示。 代码 5.28 zircon_avalon_vga_register.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_vga_register.v 3 //-- 描述 : Vga Ip 核的寄存器文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_vga_register 8 ( 9 //时钟和复位端口 10 csi_clk,rsi_reset_n, 11 //Avalon 从端口 12 avs_address,avs_write,avs_writedata, 13 //Avalon 主端口 14 avm_address,avm_byteenable,avm_read,avm_readdata, 15 avm_waitrequest,avm_readdatavalid, 16 //外设管脚输出端口 17 coe_vga_clk,coe_vga_rgb,vga_data_en,vga_frame_start 18 ); 19 20 21 input 22 input csi_clk; rsi_reset_n; //系统时钟 //系统复位 23 24 input 25 input 26 input [ 1:0] [31:0] avs_address; avs_write; avs_writedata; //Avalon 从端口地址总线 //Avalon 从端口写请求信号 //Avalon 从端口写数据总线 27 28 output [31:0] avm_address; 29 output avm_byteenable; //Avalon 主端口地址总线 //Avalon 主端口字节使能信号 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 345 30 output avm_read; //Avalon 主端口读请求信号 31 input 32 input 33 input [ 7:0] avm_readdata; avm_waitrequest; avm_readdatavalid; //Avalon 主端口读数据总线 //Avalon 主端口等待信号 //Avalon 主端口读数据有效信号 34 35 input coe_vga_clk; //vga 的系统时钟 36 input vga_data_en; //vga 的数据使能信号 37 output [ 7:0] coe_vga_rgb; 38 input vga_frame_start; //vga 的数据信号 //vga 的每一帧的开始位 39 40 reg vga_start; //vga 的开始位 41 reg vga_start_n; //vga_start 的下一个状态 42 reg [31:0] vga_buffer_address; //vga 的缓冲区首地址 43 reg [31:0] vga_buffer_address_n; //vga_buffer_address 的下一个状态 44 45 reg 46 reg [18:0] [18:0] buffer_address_count; buffer_address_count_n; //数据地址计数器 //buffer_address_count 的下一个状态 47 reg vga_read; //读数据信号 48 reg vga_read_n; //vga_read 的下一个状态 49 50 wire 51 wire 52 wire [ 7:0] [16:0] fifo_data; fifo_count; fifo_clear; //从 FIFO 中读取出来的数据 //显示当前 FIFO 中数据存量 //FIFO 的清除位 53 54 //----------------Avalon Slave Start------------------------------- 55 56 //时序电路,用于给数据寄存器进行赋值的 57 always @ (posedge csi_clk or negedge rsi_reset_n) 58 begin 59 if(!rsi_reset_n) //判断复位 60 vga_buffer_address <= 1'b0; //初始化数据寄存器 61 else 62 vga_buffer_address <= vga_buffer_address_n; //用来给数据寄存器赋值 63 end 64 65 //组合电路,用来给地址偏移量 0,也就是我们的数据寄存器写 32 位的数据 66 always @ (*) 67 begin 68 if((avs_write) && (avs_address == 2'b00)) //判断写使能和地址偏移量 69 vga_buffer_address_n = avs_writedata; //如果条件成立,那么将值赋值给数据寄存器 70 else 71 vga_buffer_address_n = vga_buffer_address; //否则,将保持不变 72 end 73 http://www.fpga.gs/ 346 软核演练篇 §5 74 //时序电路,用于给控制寄存器进行赋值的 75 always @ (posedge csi_clk or negedge rsi_reset_n) 76 begin 77 if(!rsi_reset_n) //判断复位 78 vga_start <= 1'b0; //初始化控制寄存器 79 else 80 vga_start <= vga_start_n; //用来给控制寄存器赋值 81 end 82 83 //组合电路,用来给地址偏移量 1,也就是我们的控制寄存器写 1 位的数据 84 always @ (*) 85 begin 86 if((avs_write) && (avs_address == 2'b01)) //判断写使能和地址偏移量 87 vga_start_n = avs_writedata[0]; //如果条件成立,那么将写数据中的值赋值给控制寄存器 88 else 89 vga_start_n = vga_start; //否则,将保持不变 90 end 91 92 //----------------Avalon Master Start----------------------------- 93 94 //时序电路,用于给数据地址计数器进行赋值的 95 always @ (posedge csi_clk or negedge rsi_reset_n) 96 begin 97 if (!rsi_reset_n) 98 buffer_address_count <= 19'h0; 99 else 100 buffer_address_count <= buffer_address_count_n; 101 end 102 103 //组合电路,根据 Avalon 主端口时序,用来给 Avalon 交换结构发送地址信号总线的 104 always @ (*) 105 begin 106 if(!vga_start) 107 buffer_address_count_n = 19'h0; 108 else if((vga_start) && (!avm_waitrequest) && (fifo_clear)) 109 buffer_address_count_n = 19'h0; 110 else if((vga_start) && (!avm_waitrequest) && (!fifo_clear) && (buffer_address_count < 19'd480000) && (vga_read)) 111 buffer_address_count_n = buffer_address_count + 1'b1; 112 else 113 buffer_address_count_n = buffer_address_count; 114 end 115 116 //时序电路,用于给读数据信号进行赋值的 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 347 117 always @ (posedge csi_clk or negedge rsi_reset_n) 118 begin 119 if(!rsi_reset_n) 120 vga_read <= 1'b0; 121 else 122 vga_read <= vga_read_n; 123 end 124 125 //组合电路,根据 Avalon 主端口时序,用来给 Avalon 交换结构发送读请求信号的 126 always @ (*) 127 begin 128 if(!vga_start) 129 vga_read_n = 1'b0; 130 else if((vga_start) && (!avm_waitrequest) && (fifo_clear)) 131 vga_read_n = 1'b0; 132 else if((vga_start) && (!avm_waitrequest) && (!fifo_clear) && (fifo_count < 500) && (buffer_address_count < 19'd480000)) 133 vga_read_n = 1'b1; 134 else if((vga_start) && (!avm_waitrequest) && (!fifo_clear) && (fifo_count >= 1500)) 135 vga_read_n = 1'b0; 136 else 137 vga_read_n = vga_read; 138 139 end 140 141 //FIFO 缓冲模块 142 zircon_avalon_vga_fifo zircon_avalon_vga_fifo_init 143 ( 144 .aclr (fifo_clear ), //FIFO 的清除位 145 .data (avm_readdata ), //FIFO 写数据 146 .rdclk (~coe_vga_clk ), //FIFO 读时钟 147 .rdreq (vga_data_en ), //FIFO 读请求 148 .wrclk (csi_clk ), //FIFO 写时钟 149 .wrreq (avm_readdatavalid), //FIFO 写请求 150 .q (fifo_data ), //FIFO 读数据 151 .wrusedw (fifo_count ) //显示当前 FIFO 中数据存量 152 ); 153 154 assign avm_byteenable = 1'b1; //Avalon 主端口字节使能信号 155 assign avm_address = vga_buffer_address + buffer_address_count; //Avalon 主端口地 址总线 156 assign avm_read = vga_read; //Avalon 主端口读请求信号 157 assign fifo_clear = vga_frame_start; //vga 的每一帧的开始位 http://www.fpga.gs/ 348 软核演练篇 §5 158 assign coe_vga_rgb = vga_data_en ? fifo_data[7:0] : 8'b0; //从 FIFO 中读取出来的数 据传输给 vga 159 160 endmodule 该寄存器文件可以说是我们整个 IP 的精华所在,它实现了 Avalon 从端口的功能,还实现了 Avalon 主端口的功能,同时还实现了 FIFO 缓冲区。从该代码中我们可以看出,从端口接收到 缓冲区的首地址,然后将缓冲区首地址赋值给 Avalon 主端口的地址总线上。Avalon 主端口便可 以根据这个缓冲区首地址依次读取 SDRAM 中的缓冲数据,读到的缓冲数据,我们依次写入到 FIFO 缓冲模块中,与此同时,我们的时序模块根据 VGA 的时序规范生成 vga_data_en 信号, 当它使能时就意味我们的 VGA 需要显示的数据,因此,我们将 VGA 的时钟,以及 vga_data_en 连接到 FIFO 的读时钟与读请求上,这样我们的 FIFO 便能送出数据给我们的 coe_vga_rgb。说 完了寄存器文件,接下来我们在来看下硬件逻辑文件 zircon_avalon_vga_logic.v 中的代码,如 代码 5.29 所示。 代码 5.29 zircon_avalon_vga_logic.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_vga_logic.v 3 //-- 描述 : Vga Ip 核的硬件逻辑文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 8 //--------------------------------------------------------------------------- 9 //-- VGA 800*600@60 10 //-- VGA_DATA[7:0] red2,red1,red0,green2,green1,green0,blue1,blue0 11 //-- VGA CLOCK 40MHz. 12 //--------------------------------------------------------------------------- 13 14 //--------------------------------------------------------------------------- 15 //-- Horizonal timing information 16 //-- Sync pluse 128 a 17 //-- back porch 88 b 18 //-- active 800 c 19 //-- front porch 40 d 20 //-- All line 1056 e 21 //-- Vertical timing information 22 //-- sync pluse 4 o 23 //-- back porch 23 p 24 //-- active time 600 q 25 //-- front porch 1 r 26 //-- All lines 628 s 27 //--------------------------------------------------------------------------- 28 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 349 29 //--------------------------------------------------------------------------- 30 //-- Horizonal timing information 31 `define HSYNC_A 16'd128 // 128 32 `define HSYNC_B 16'd216 // 128 + 88 33 `define HSYNC_C 16'd1016 // 128 + 88 + 800 34 `define HSYNC_D 16'd1056 // 128 + 88 + 800 + 40 35 //-- Vertical timing information 36 `define VSYNC_O 16'd4 // 4 37 `define VSYNC_P 16'd27 // 4 + 23 38 `define VSYNC_Q 16'd627 // 4 + 23 + 600 39 `define VSYNC_R 16'd628 // 4 + 23 + 600 + 1 40 //--------------------------------------------------------------------------- 41 42 module zircon_avalon_vga_logic 43 ( 44 //输入端口 45 CLK_40M,RST_N, 46 //输出端口 47 VSYNC,HSYNC, 48 //用户逻辑输入与输出 49 vga_data_en,vga_frame_start 50 ); 51 52 //--------------------------------------------------------------------------- 53 //-- 外部端口声明 54 //--------------------------------------------------------------------------- 55 input 56 input 57 output 58 output CLK_40M; RST_N; VSYNC; HSYNC; //时钟的端口,开发板用的 50M 晶振 //复位的端口,低电平复位 //VGA 垂直同步端口 //VGA 水平同步端口 59 output vga_data_en; //数据使能端口 60 output vga_frame_start; //每一帧的开始位 61 //--------------------------------------------------------------------------- 62 //-- 内部端口声明 63 //--------------------------------------------------------------------------- 64 reg 65 reg [15:0] [15:0] hsync_cnt; hsync_cnt_n; //水平扫描计数器 //hsync_cnt 的下一个状态 66 reg [15:0] vsync_cnt; //垂直扫描计数器 67 reg [15:0] vsync_cnt_n; //vsync_cnt 的下一个状态 68 reg 69 reg 70 reg 71 reg VSYNC; VSYNC_N; HSYNC; HSYNC_N; //垂直同步端口 //VSYNC 的下一个状态 //水平同步端口 //HSYNC 的下一个状态 72 reg vga_data_en; //RGB 传输使能信号 http://www.fpga.gs/ 350 软核演练篇 §5 73 reg vga_data_en_n; //vga_data_en 的下一个状态 74 reg 75 reg vga_frame_start; vga_frame_start_n; //每一帧的开始位 //vga_frame_start 的下一个状态 76 77 //时序电路,用来给 hsync_cnt 寄存器赋值 78 always @ (posedge CLK_40M or negedge RST_N) 79 begin 80 if(!RST_N) 81 hsync_cnt <= 16'b0; //判断复位 //初始化 hsync_cnt 值 82 else 83 hsync_cnt <= hsync_cnt_n; //用来给 hsync_cnt 赋值 84 end 85 86 //组合电路,水平扫描 87 always @ (*) 88 begin 89 if(hsync_cnt == `HSYNC_D) //判断水平扫描时序 90 hsync_cnt_n = 16'b0; //如果水平扫描完毕,计数器将会被清零 91 else 92 hsync_cnt_n = hsync_cnt + 1'b1; //如果水平没有扫描完毕,计数器继续累加 93 end 94 95 //时序电路,用来给 vsync_cnt 寄存器赋值 96 always @ (posedge CLK_40M or negedge RST_N) 97 begin 98 if(!RST_N) //判断复位 99 vsync_cnt <= 16'b0; //给行扫描赋值 100 else 101 vsync_cnt <= vsync_cnt_n; //给行扫描赋值 102 end 103 104 //组合电路,垂直扫描 105 always @ (*) 106 begin 107 if(vsync_cnt == `VSYNC_R) 108 vsync_cnt_n = 16'b0; 109 else if(hsync_cnt == `HSYNC_D) //判断垂直扫描时序 //如果垂直扫描完毕,计数器将会被清零 //判断水平扫描时序 110 vsync_cnt_n = vsync_cnt + 1'b1; //如果水平扫描完毕,计数器继续累加 111 else 112 vsync_cnt_n = vsync_cnt; //否则,计数器将保持不变 113 end 114 115 //时序电路,用来给 HSYNC 寄存器赋值 116 always @ (posedge CLK_40M or negedge RST_N) Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 117 begin 118 if(!RST_N) 119 HSYNC <= 1'b0; //判断复位 //初始化 HSYNC 值 120 else 121 HSYNC <= HSYNC_N; //用来给 HSYNC 赋值 122 end 123 124 //组合电路,将 HSYNC_A 区域置 0,HSYNC_B+HSYNC_C+HSYNC_D 置 1 125 always @ (*) 126 begin 127 if(hsync_cnt < `HSYNC_A) //判断水平扫描时序 128 HSYNC_N = 1'b0; //如果在 HSYNC_A 区域,那么置 0 129 else 130 HSYNC_N = 1'b1; //如果不在 HSYNC_A 区域,那么置 1 131 end 132 133 //时序电路,用来给 VSYNC 寄存器赋值 134 always @ (posedge CLK_40M or negedge RST_N) 135 begin 136 if(!RST_N) //判断复位 137 VSYNC <= 1'b0; //初始化 VSYNC 值 138 else 139 VSYNC <= VSYNC_N; //用来给 VSYNC 赋值 140 end 141 142 //组合电路,将 VSYNC_A 区域置 0,VSYNC_P+VSYNC_Q+VSYNC_R 置 1 143 always @ (*) 144 begin 145 if(vsync_cnt < `VSYNC_O) 146 VSYNC_N = 1'b0; //判断水平扫描时序 //如果在 VSYNC_O 区域,那么置 0 147 else 148 VSYNC_N = 1'b1; //如果不在 VSYNC_O 区域,那么置 1 149 end 150 151 //时序电路,用来给 vga_data_en 寄存器赋值 152 always @ (posedge CLK_40M or negedge RST_N) 153 begin 154 if(!RST_N) //判断复位 155 vga_data_en <= 1'b0; //初始化 vga_data_en 值 156 else 157 vga_data_en <= vga_data_en_n; //用来给 vga_data_en 赋值 158 end 159 160 //组合电路,判断显示有效区(列像素>216&&列像素<1017&&行像素>27&&行像素<627) http://www.fpga.gs/ 351 352 软核演练篇 §5 161 always @ (*) 162 begin 163 if((hsync_cnt >= `HSYNC_B && hsync_cnt <`HSYNC_C) && 164 (vsync_cnt >= `VSYNC_P && vsync_cnt < `VSYNC_Q)) 165 vga_data_en_n = 1'b1; //如果在显示区域就给使能数据信号置 1 166 else 167 vga_data_en_n = 1'b0; //如果不在显示区域就给使能数据信号置 0 168 end 169 170 //时序电路,用来给 vga_frame_start 寄存器赋值 171 always @ (posedge CLK_40M or negedge RST_N) 172 begin 173 if(!RST_N) //判断复位 174 vga_frame_start <= 1'b0; //初始化 vga_frame_start 值 175 else 176 vga_frame_start <= vga_frame_start_n; //用来给 vga_frame_start 赋值 177 end 178 179 //组合电路,用来标识每一帧的开始位 180 always @ (*) 181 begin 182 if(vsync_cnt == 0) //判断帧时序是否刚开始 183 vga_frame_start_n = 1'b1; //如果帧时序刚开始,就将 vga_frame_start_n 置 1 184 else 185 vga_frame_start_n = 1'b0; //如果帧时序没有开始,就将 vga_frame_start_n 置 0 186 end 187 188 endmodule 这个硬件逻辑文件中的代码,我们也是从《项目实战篇》中拿出来的,不过这里需要我们注 意的是,我们在原代码基础上添加了两个 always 模块,也就是我们代码中的第 170 至 189 行, 该代码主要用来标识每一帧的起始位,该起始位主要是用来清除我们的 FIFO 模块中的数据。我 们只要搞懂了 VGA 的时序,这段代码也是很容易理解的,如果不太理解的朋友,可以参考我们 的《项目实战篇》中的内容进一步学习。 (4) 编写用于描述寄存器的 C 头文件和 IP 核的驱动 C 文件 写完了我们的硬件逻辑代码,我们就可以使用 IP 核编辑器来封装我们的硬件逻辑,完成 IP 核的定制了,对于后面的几个步骤,我相信大家都了如指掌了,这里我们同前面一样也是一笔带 过,我们这里主要介绍一下 VGA IP 核的寄存器的头文件和 VGA IP 核的驱动文件。首先我们给 出的是 VGA IP 核的寄存器头文件 zircon_avalon_vga_regs.h,该文件如代码 5.30 所示。 代码 5.30 zircon_avalon_vga_regs.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_vga_regs.h 3 //-- Describe : Vga IP core register header file Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 353 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_VGA_REGS_H__ 8 #define __ZIRCON_AVALON_VGA_REGS_H__ 9 10 #include 11 12 //Data register 0 13 #define IOWR_ZIRCON_AVALON_VGA_DATA(base,data) IOWR(base, 0, data) 14 //Control register 1 15 #define IOWR_ZIRCON_AVALON_VGA_CONTROL(base,data) IOWR(base, 1, data) 16 17 18 #endif /* __ZIRCON_AVALON_VGA_REGS_H__ */ 从该代码中我们可以看出,它是用来定义数据寄存器和控制寄存器的宏定义,接下来我们给 出的是底层驱动头文件 zircon_avalon_vga.h,该文件如代码 5.31 所示。 代码 5.31 zircon_avalon_vga.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_vga.h 3 //-- Describe : Vga IP core driver header file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_VGA_H__ 8 #define __ZIRCON_AVALON_VGA_H__ 9 10 #include "system.h" 11 #include "alt_types.h" 12 #include "zircon_avalon_vga_regs.h" 13 14 #ifdef __cplusplus 15 extern "C" 16 { 17 #endif /* __cplusplus */ 18 19 20 #define COLOR_BLACK 0X00 21 #define COLOR_BLUE 0X03 22 #define COLOR_GREEN 0X1D 23 #define COLOR_CYAN 0X1F 24 #define COLOR_RED 0XE0 25 #define COLOR_FUCHSINE 0XE3 http://www.fpga.gs/ 354 软核演练篇 §5 26 #define COLOR_YELLOW 0xFD 27 #define COLOR_WHITE 0XFF 28 29 #define VGA_WIDTH 800 30 #define VGA_HEIGHT 600 31 #define VGA_BUFFER_SIZE (VGA_WIDTH * VGA_HEIGHT) 32 alt_u8 vga_buffer[VGA_BUFFER_SIZE]; 33 34 //--------------------------------------------------------------------------- 35 //-- Name : zircon_avalon_vga_DrawPoint 36 //-- Function : Draw point at the specified location 37 //-- Input parameters : x:horizontal direction;y:vertical direction 38 //-- color:specified color 39 //-- Output parameters: no 40 //--------------------------------------------------------------------------- 41 void zircon_avalon_vga_DrawPoint(alt_u16 x, alt_u16 y, alt_u16 color); 42 43 //--------------------------------------------------------------------------- 44 //-- Name : zircon_avalon_vga_init 45 //-- Function : vga init 46 //-- Input parameters : no 47 //-- Output parameters: no 48 //--------------------------------------------------------------------------- 49 void zircon_avalon_vga_init(); 50 51 //--------------------------------------------------------------------------- 52 //-- Name : zircon_avalon_vga_ColorBar 53 //-- Function : display Four color ColorBar 54 //-- Input parameters : no 55 //-- Output parameters: no 56 //--------------------------------------------------------------------------- 57 void zircon_avalon_vga_ColorBar(); 58 59 //--------------------------------------------------------------------------- 60 //-- Name : zircon_avalon_vga_ClearScreen 61 //-- Function : Clear Screen 62 //-- Input parameters : color:specified color 63 //-- Output parameters: no 64 //--------------------------------------------------------------------------- 65 void zircon_avalon_vga_ClearScreen(alt_u16 color); 66 67 //--------------------------------------------------------------------------- 68 //-- Name : zircon_avalon_vga_DisplayPic 69 //-- Function : Display Andy_Warhol picture Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 355 70 //-- Input parameters : no 71 //-- Output parameters: no 72 //--------------------------------------------------------------------------73 void zircon_avalon_vga_DisplayPic(); 74 75 /* Macros used by alt_sys_init */ 76 #define ZIRCON_AVALON_VGA_INSTANCE(name, dev) alt_u32 vga_controller_addr = name##_BASE 77 #define ZIRCON_AVALON_VGA_INIT(name, dev) while(0) 78 79 80 #ifdef __cplusplus 81 } 82 #endif /* __cplusplus */ 83 84 #endif /* __ZIRCON_AVALON_VGA_H__ */ 从该代码中可以看出,我们实现了 5 个函数,这 5 个函数实现了 VGA 的各种显示功能。下 面我们就来进一步分析一下这 5 个函数,看下这 5 个函数究竟实现了什么功能,下面我们给出 底层驱动文件 zircon_avalon_vga.c,该文件如代码 5.32 所示。 代码 5.32 zircon_avalon_vga.c 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_vga.c 3 //-- Describe : vga IP core driver C file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "unistd.h" 8 #include "alt_types.h" 9 #include "zircon_avalon_vga.h" 10 #include "Andy_Warhol.h" //安迪沃霍尔图片 11 12 extern alt_u32 vga_controller_addr; 13 14 //--------------------------------------------------------------------------- 15 //-- Name : zircon_avalon_vga_init 16 //-- Function : vga init 17 //-- Input parameters : no 18 //-- Output parameters: no 19 //--------------------------------------------------------------------------- 20 void zircon_avalon_vga_init() 21 { 22 IOWR_ZIRCON_AVALON_VGA_CONTROL(vga_controller_addr, 0); 23 IOWR_ZIRCON_AVALON_VGA_DATA(vga_controller_addr,(alt_u32)vga_buffer); http://www.fpga.gs/ 356 软核演练篇 §5 24 IOWR_ZIRCON_AVALON_VGA_CONTROL(vga_controller_addr, 1); 25 } 26 27 //--------------------------------------------------------------------------- 28 //-- Name : zircon_avalon_vga_DrawPoint 29 //-- Function : Draw point at the specified location 30 //-- Input parameters : x:horizontal direction;y:vertical direction 31 //-- color:specified color 32 //-- Output parameters: no 33 //--------------------------------------------------------------------------- 34 void zircon_avalon_vga_DrawPoint(alt_u16 x, alt_u16 y, alt_u16 color) 35 { 36 IOWR_8DIRECT((alt_u32 )vga_buffer, (y * VGA_WIDTH) + x, color); 37 } 38 39 //--------------------------------------------------------------------------- 40 //-- Name : zircon_avalon_vga_ClearScreen 41 //-- Function : Clear Screen 42 //-- Input parameters : color:specified color 43 //-- Output parameters: no 44 //--------------------------------------------------------------------------- 45 void zircon_avalon_vga_ClearScreen(alt_u16 color) 46 { 47 int x, y; 48 for (y = 0;y < VGA_HEIGHT;y ++) 49 { 50 for (x = 0;x < VGA_WIDTH;x ++) 51 { 52 zircon_avalon_vga_DrawPoint(x, y, color); 53 } 54 } 55 } 56 57 //--------------------------------------------------------------------------- 58 //-- Name : zircon_avalon_vga_ColorBar 59 //-- Function : display Four color ColorBar 60 //-- Input parameters : no 61 //-- Output parameters: no 62 //--------------------------------------------------------------------------- 63 void zircon_avalon_vga_ColorBar() 64 { 65 int x, y; 66 for (y = 0;y < VGA_HEIGHT;y ++) 67 { Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 357 68 for (x = 0;x < VGA_WIDTH;x ++) 69 { 70 if (x < 200) zircon_avalon_vga_DrawPoint(x, y, COLOR_RED); 71 else if (x < 400) zircon_avalon_vga_DrawPoint(x, y, COLOR_BLUE); 72 else if (x < 600) zircon_avalon_vga_DrawPoint(x, y, COLOR_GREEN); 73 else zircon_avalon_vga_DrawPoint(x, y, COLOR_FUCHSINE); 74 } 75 } 76 } 77 78 //--------------------------------------------------------------------------- 79 //-- Name : zircon_avalon_vga_DisplayPic 80 //-- Function : Display Andy_Warhol picture 81 //-- Input parameters : no 82 //-- Output parameters: no 83 //--------------------------------------------------------------------------- 84 void zircon_avalon_vga_DisplayPic() 85 { 86 int x, y; 87 for (y = 0;y < VGA_HEIGHT;y ++) 88 { 89 for (x = 0;x < VGA_WIDTH;x ++) 90 { 91 zircon_avalon_vga_DrawPoint(x, y, Andy_Warhol[(y * VGA_WIDTH) + x]); 92 } 93 } 94 } 下面我们就来简单的讲解一下这 5 个函数的功能,首先我们来看第一个函数第 14 至 25 行, 它主要是用来初始化 VGA 的,第 22 行代码是让我们的 VGA 停止工作,第 23 行代码是用来发 送我们的缓冲区首地址。第 24 行代码就是让我们的 VGA 开始工作。接下来我们再来看一下第 二个函数第 27 至 37 行,它主要是用来在我们的 VGA 屏幕上画点的,该函数第一个参数就是我 们的开辟的缓冲区的首地址,第二个参数就是地址偏移量,第三个参数就是写入的数据。第三个 函数第 39 至 55 行,它主要是用来清屏的,该函数利用了我们第二个画点函数,它将我们整个 屏幕上的点全部都写成了一个颜色,以此达到清屏功能。第四个函数第 57 至 76 行,它主要是 用来显示彩条的,该函数也是利用了我们的画点函数,我们将不同的区域画上不同的颜色,因此 实现了彩条功能。第五个函数第 78 至 94 行,它主要是用来显示图片的,它通用也是利用了我 们的画点函数,我们将.bmp 格式的图片转成了 C 数组,然后我们只需要将 C 数组中的数据依次 显示到屏幕上,便能完成整个图片的显示。 5.5.3 VGA IP 核的应用 (1) 功能概述 http://www.fpga.gs/ 358 软核演练篇 §5 完成了 VGA IP 核的定制,接下来我们再来看看 VGA IP 核的应用,首先我们讲解的是第一 部分功能概述,在该工程中,我们主要是利用 VGA IP 核实现了一个演示程序,我们通过输入不 同的数字 VGA 来显示不同的效果,如果我们输入数字 1,那么 VGA 将会被清屏,并显示绿色。 如果我们输入数字 2,那么 VGA 将会显示一个四种不同颜色的彩条,这四种颜色分别是红、黄、 蓝、绿。如果我们输入数字 3,那么 VGA 将会显示安迪沃霍尔所画的一幅图片。如果我们输入 数字 4,那么就会完成演示并关闭程序。 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该工程的硬件框架,如图 5.69 所示。 图 5.69 VGA IP 核的硬件框架图 从该图中我们可以看出,除了我们常用几个基本 IP 核以外,我们这里只添加了一个 VGA 的 IP 核,该 IP 核的使用是非常简单的,我们不需要任何配置,只需要将它添加至 Qsys 软件中就 可以正常使用了。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 5.33 所 示。 代码 5.33 Qsys_Vga_Ip.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Vga_Ip.c 3 //-- 描述 : 使用 VGA IP 核来测试我们的 VGA 外设显示各种图案 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 8 #include //标准的输入输出头文件 9 #include "zircon_avalon_vga.h" //vga ip 核的的头文件 10 11 //--------------------------------------------------------------------------- Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 359 12 //-- 名称 : main() 13 //-- 功能 : 程序入口 14 //-- 输入参数 : 无 15 //-- 输出参数 : 无 16 //--------------------------------------------------------------------------- 17 int main() 18 { 19 FILE* fp; 20 char prompt = 0; 21 zircon_avalon_vga_init(); 22 printf("Welcome to VGA ip demo program... \n"); 23 printf("1. ClearScreen ... \n"); 24 printf("2. ColorBar...\n"); 25 printf("3. DisplayPic...\n"); 26 printf("4. exit program\n"); 27 28 fp = fopen(JTAG_UART_NAME, "r+"); //以可读写方式打开设备文件 29 if(fp) //成功打开设备文件 30 { 31 while(prompt != '4') //循环直至接收到 '4' 32 { 33 fprintf(fp, "Please write one number and press Enter... \n"); 34 prompt = getc(fp); //从 JTAG UART 中获取字符 35 switch(prompt) 36 { 37 case '1': {zircon_avalon_vga_ClearScreen(COLOR_GREEN); break;} 38 case '2': {zircon_avalon_vga_ColorBar(); break;} 39 case '3': {zircon_avalon_vga_DisplayPic(); break;} 40 default : {break;} 41 } 42 } 43 fprintf(fp, "Closing the JTAG UART file handle.\n"); 44 fclose(fp); //关闭设备文件 45 } 46 else 47 { 48 printf("Fail to open file...\n"); //设备文件打开失败 49 } 50 return 0; 51 } (4) 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Vga_Ip.sof 下载至我们的 A4 开发板,Qsys_Vga_Ip.sof 下载完 http://www.fpga.gs/ 360 软核演练篇 §5 成后,我们还需要在 Eclipse 软件中将 Qsys_Vga_Ip.elf 文件下载至我们的 A4 开发板, Qsys_Vga_Ip.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可 以在 Eclipse 软件的控制台中看到我们的打印信息,如图 5.70 所示。 图 5.70 VGA IP 核的控制台打印信息图 这时,我们使用键盘输入数字 3 以后按下回车,我们的 VGA 显示器显示如图 5.71 所示。 图 5.71 VGA IP 核的板级调试图 当然,我们还可以按下数字 1、2,我们的 VGA 也都会显示相对应的效果。这里我们就不给 大家一一进行展示了,大家可以自行下载至 A4 开发板上逐一验证。至此,我们的 VGA IP 核的 应用就讲解完了。 §5.6 蜂鸣器外设 5.6.1 蜂鸣器 IP 核的定制 (1) 规划 IP 核的硬件功能 通过我们对 VGA 外设的讲解,相信大家对 PIO 和 IP 核这两种方法都有了一个更深的了解, 通过对这两种方法的学习,我们认清了 PIO 的不足,知道了 IP 核的强大,对于 PIO 和 IP 核这 两种方式的好与坏,想必此时我们已经无需多言了,大家都是有目共睹了。为了避免重蹈覆辙, 在后面外设的讲解中,我们决定放弃 PIO 的方法,全部都采用 IP 核的方式来实现。下面我们就 来为我们的蜂鸣器外设定制 IP 核。定制 IP 核的第一步,就是为我们的蜂鸣器外设规划 IP 核的 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 361 硬件功能,下面我们给出蜂鸣器外设寄存器的规划方案,如表 5.21 所示。 表 5.21 蜂鸣器 IP 核寄存器映射 位描述 偏移量 寄存器名称 操作 31 … 0 00 周期设定寄存器 写 DATA 01 占空比设定寄存器 写 DATA 10 控制寄存器 写 保留 EN 11 保留 — — 从表中我们可以看出,周期设定寄存器用了 32 位,占空比设定寄存器用了 32 位,控制寄 存器用了 1 位,下面我们就对这 3 个寄存器分别进行介绍:  周期设定寄存器:用来设定蜂鸣器(PWM)输出周期的时钟数。  占空比设定寄存器:用来设定一个周期内蜂鸣器(PWM)输出低电平的时钟数。  控制寄存器:使能和关闭蜂鸣器(PWM)输出,为 1 时使能蜂鸣器(PWM)输出,为 0 时关闭蜂鸣器(PWM)输出。 (2) 定义一个恰当的 Avalon 接口 规划完蜂鸣器 IP 核的硬件功能,接下来我们就来给蜂鸣器定义 Avalon 接口。我们将信号总 结如表 5.22 所示。 信号名 clock reset address write writedata buzzer 表 5.22 蜂鸣器 IP 核的接口信号 Avalon 信号类型 宽度 Clock 1 Reset 1 Avalon-MM 2 Avalon-MM 1 Avalon-MM 32 Conduit 1 方向 Input Input Input Input Input Output 这几个信号想必我们不说,大家也已经知道了,clock 是我们蜂鸣器 IP 核的时钟信号,reset 是为我们蜂鸣器 IP 核的复位信号,address 是我们蜂鸣器 IP 核的地址信号,write 是我们蜂鸣 器 IP 核的写使能信号,writedata 是我们蜂鸣器 IP 核的写数据信号,buzzer 就是用来连接我们 蜂鸣器外设管脚的。我们对这几个信号讲过的次数已经多的我都数不清了,想必大家耳朵里也都 听说老茧了。 (3) 使用硬件描述语言描述硬件逻辑 定义完了 Avalon 接口,接下来我们就到了第三步使用硬件描述语言描述硬件逻辑,首先我 们来看下顶层文件 zircon_avalon_buzzer.v,该文件如代码 5.34 所示。 代码 5.34 zircon_avalon_buzzer.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_buzzer.v 3 //-- 描述 : 蜂鸣器 IP 核的顶层文件 http://www.fpga.gs/ 362 软核演练篇 §5 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_buzzer 8( 9 //时钟和复位端口 10 csi_clk,rsi_reset_n, 11 //Avalon 从端口 12 avs_address,avs_write,avs_writedata, 13 //外设管脚输出端口 14 coe_buzzer 15 ); 16 17 //Avalon_Slave_PWM Avalon I/O 18 input 19 input 20 input [ 1:0] csi_clk; rsi_reset_n; avs_address; //系统时钟 //系统复位 //Avalon 地址总线 21 input avs_write; //Avalon 写请求信号 22 input [31:0] avs_writedata; //Avalon 写数据总线 23 24 output coe_buzzer; //蜂鸣器输出信号 25 26 wire 27 wire [31:0] [31:0] pwm_clock_divide; pwm_duty_cycle; //周期设定寄存器 //占空比设定寄存器 28 wire pwm_enable; //控制寄存器 29 30 //PWM Instance 31 zircon_avalon_buzzer_logic zircon_avalon_buzzer_logic 32 ( 33 .csi_clk (csi_clk ), //系统时钟 34 .rsi_reset_n (rsi_reset_n ), //系统复位 35 .pwm_enable (pwm_enable ), //控制寄存器 36 .pwm_clock_divide (pwm_clock_divide ), //周期设定寄存器 37 .pwm_duty_cycle (pwm_duty_cycle ), //占空比设定寄存器 38 .coe_buzzer (coe_buzzer ) //蜂鸣器输出信号 39 ); 40 41 zircon_avalon_buzzer_register zircon_avalon_buzzer_register 42 ( 43 .csi_clk 44 .rsi_reset_n 45 .avs_address 46 .avs_write (csi_clk (rsi_reset_n (avs_address (avs_write ), //系统时钟 ), //系统复位 ), //Avalon 地址总线 ), //Avalon 写请求信号 47 .avs_writedata (avs_writedata ), //Avalon 写数据总线 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 363 48 .pwm_clock_divide (pwm_clock_divide ), //周期设定寄存器 49 .pwm_duty_cycle (pwm_duty_cycle ), //占空比设定寄存器 50 .pwm_enable (pwm_enable ) //控制寄存器 51 ); 52 53 endmodule 我们可以看到这个顶层模块文件就是用来连接我们寄存器文件和硬件逻辑文件的。它本身 没有实现任何的逻辑功能,看完了顶层文件,接下来我们再来看下寄存器文件 zircon_avalon_buzzer_register.v,该文件如代码 5.35 所示。 代码 5.35 zircon_avalon_buzzer_register.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_buzzer_register.v 3 //-- 描述 : 蜂鸣器 IP 核的寄存器文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_buzzer_register 8( 9 //时钟和复位端口 10 csi_clk,rsi_reset_n, 11 //Avalon 从端口 12 avs_address,avs_write,avs_writedata, 13 //用户逻辑输入与输出 14 pwm_clock_divide,pwm_duty_cycle,pwm_enable 15 ); 16 17 18 input 19 input 20 input 21 input 22 input 23 output reg 24 reg 25 output reg 26 reg 27 output reg 28 reg [ 1:0] [31:0] [31:0] [31:0] [31:0] [31:0] csi_clk; rsi_reset_n; avs_address; avs_write; avs_writedata; pwm_clock_divide; pwm_clock_divide_n; pwm_duty_cycle; pwm_duty_cycle_n; pwm_enable; pwm_enable_n; //系统时钟 //系统复位 //Avalon 地址总线 //Avalon 写请求信号 //Avalon 写数据总线 //周期设定寄存器 //pwm_clock_divide 的下一个状态 //占空比设定寄存器 //pwm_duty_cycle 的下一个状态 //控制寄存器 //pwm_enalbe 的下一个状态 29 30 //时序电路,用于给周期设定寄存器进行赋值的 31 always @ (posedge csi_clk or negedge rsi_reset_n) 32 begin 33 if(!rsi_reset_n) //判断复位 http://www.fpga.gs/ 364 软核演练篇 §5 34 pwm_clock_divide <= 1'b0; //初始化周期设定寄存器 35 else 36 pwm_clock_divide <= pwm_clock_divide_n;//用来给周期设定寄存器赋值 37 end 38 39 //组合电路,用来给地址偏移量 0,也就是我们的周期设定寄存器写 32 位的数据 40 always @ (*) 41 begin 42 if((avs_write) && (avs_address == 2'b00))//判断写使能和地址偏移量 43 pwm_clock_divide_n = avs_writedata; //如果条件成立,那么将值赋值给周期设定寄存器 44 else 45 pwm_clock_divide_n = pwm_clock_divide; //否则,将保持不变 46 end 47 48 //时序电路,用于给占空比设定寄存器进行赋值的 49 always @ (posedge csi_clk or negedge rsi_reset_n) 50 begin 51 if(!rsi_reset_n) //判断复位 52 pwm_duty_cycle <= 1'b0; //初始化占空比设定寄存器 53 else 54 pwm_duty_cycle <= pwm_duty_cycle_n; //用来给占空比设定寄存器赋值 55 end 56 57 //组合电路,用来给地址偏移量 1,也就是我们的占空比设定寄存器写 32 位的数据 58 always @ (*) 59 begin 60 if((avs_write) && (avs_address == 2'b01))//判断写使能和地址偏移量 61 pwm_duty_cycle_n = avs_writedata; //如果条件成立,那么将值赋值给占空比设定寄存器 62 else 63 pwm_duty_cycle_n = pwm_duty_cycle; //否则,将保持不变 64 end 65 66 //时序电路,用于给控制寄存器进行赋值的 67 always @ (posedge csi_clk or negedge rsi_reset_n) 68 begin 69 if(!rsi_reset_n) 70 pwm_enable <= 1'b0; //判断复位 //初始化控制寄存器 71 else 72 pwm_enable <= pwm_enable_n; //用来给控制寄存器赋值 73 end 74 75 //组合电路,用来给地址偏移量 2,也就是我们的控制寄存器写 1 位的数据 76 always @ (*) 77 begin Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 365 78 if((avs_write) && (avs_address == 2'b10))//判断写使能和地址偏移量 79 pwm_enable_n = avs_writedata[0]; //如果条件成立,那么将值赋值给控制寄存器 80 else 81 pwm_enable_n = pwm_enable; //否则,将保持不变 82 end 83 84 endmodule 我们可以看到这个寄存器文件就是用来接收我们规划的三个寄存器数据的。Avalon 地址总 线 00 对应着我们的周期设定寄存器,Avalon 地址总线 01 对应着我们的占空比设定寄存器, Avalon 地址总线 10 对应着我们的控制寄存器。当 write 信号到来时,我们将从 wirtedata 中读 取出我们相对应的寄存器数据。最后我们再来看下我们的硬件逻辑文件 zircon_avalon_buzzer_logic.v,该文件如代码 5.36 所示。 代码 5.36 zircon_avalon_buzzer_logic.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_buzzer_logic.v 3 //-- 描述 : 蜂鸣器 IP 核的硬件逻辑文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_buzzer_logic 8( 9 //时钟和复位端口 10 csi_clk,rsi_reset_n, 11 //用户逻辑输入与输出 12 pwm_enable,pwm_clock_divide,pwm_duty_cycle, 13 //外设管脚输出端口 14 coe_buzzer 15 ); 16 17 //Inputs 18 input 19 input 20 input 21 input 22 input 23 output reg 24 reg 25 reg 26 reg [31:0] [31:0] [31:0] [31:0] csi_clk; rsi_reset_n; pwm_clock_divide; pwm_duty_cycle; pwm_enable; coe_buzzer; coe_buzzer_n; counter; counter_n; //系统时钟 //系统复位 //周期设定寄存器 //占空比设定寄存器 //控制寄存器 //蜂鸣器输出信号 //coe_buzzer 的下一个状态 //计数器 //counter 的下一个状态 27 28 //时序电路,用于给计数器进行赋值的 29 always @ (posedge csi_clk or negedge rsi_reset_n) http://www.fpga.gs/ 366 软核演练篇 §5 30 begin 31 if(!rsi_reset_n) 32 counter <= 1'b0; //判断复位 //初始化计数器 33 else 34 counter <= counter_n; //用来给计数器赋值 35 end 36 37 //组合电路,计数器根据周期设定寄存器进行计数 38 always @ (*) 39 begin 40 if((pwm_enable) && (counter >= pwm_clock_divide)) 41 counter_n = 1'b0; //如果条件成立,那么将计数器清零 42 else 43 counter_n = counter + 1; //否则,计数器加 1 44 end 45 46 //时序电路,用于给蜂鸣器输出信号进行赋值的 47 always @ (posedge csi_clk or negedge rsi_reset_n) 48 begin 49 if(!rsi_reset_n) //判断复位 50 coe_buzzer <= 1'b0; //初始化蜂鸣器输出信号 51 else 52 coe_buzzer <= coe_buzzer_n; //用来给蜂鸣器输出信号赋值 53 end 54 55 //组合电路,蜂鸣器根据占空比设定寄存器来输出高低电平 56 always @ (*) 57 begin 58 if((pwm_enable) && (counter <= pwm_duty_cycle)) 59 coe_buzzer_n = 1'b1; //如果条件成立,那么将写蜂鸣器输出信号置 1 60 else 61 coe_buzzer_n = 1'b0; //否则,将蜂鸣器输出信号置 0 62 end 63 64 endmodule 这里我们需要说明的是,这个硬件逻辑文件在我们的《项目实战篇》中是找不到的,我们可 以看到这个硬件逻辑文件代码也是很简单的,该代码主要实现了两个功能,第一个功能就是根据 控制寄存器和周期设定寄存器来实现计数器的一个计数。第二个功能就是根据控制寄存器和占 空比设定寄存器来输出高低电平。 (4) 编写用于描述寄存器的 C 头文件和 IP 核的驱动 C 文件 写完了我们的硬件逻辑代码,我们就可以使用 IP 核编辑器来封装我们的硬件逻辑,完成 IP 核的定制了,对于后面的几个步骤,这里我们同前面一样也是一笔带过,我们这里主要介绍一下 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 367 蜂鸣器 IP 核的寄存器的头文件和蜂鸣器 IP 核的驱动文件。首先我们给出的是蜂鸣器 IP 核的寄 存器头文件 zircon_avalon_buzzer_regs.h,该文件如代码 5.37 所示。 代码 5.37 zircon_avalon_buzzer_regs.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_buzzer_regs.h 3 //-- Describe : buzzer IP core register header file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_BUZZER_REGS_H__ 8 #define __ZIRCON_AVALON_BUZZER_REGS_H__ 9 10 #include 11 //Cycle setting register 12 #define IOWR_ZIRCON_BUZZER_CLOCK_DIVIDER(base, data) IOWR(base, 0, data) 13 //Duty Cycle Value Register 14 #define IOWR_ZIRCON_BUZZER_DUTY_CYCLE(base, data) IOWR(base, 1, data) 15 //Control Register 16 #define IOWR_ZIRCON_BUZZER_ENABLE(base, data) IOWR(base, 2, data) 17 18 19 #endif /* __ZIRCON_AVALON_BUZZER_REGS_H__ */ 从代码中我们可以看到我们定义的三个寄存器一个也不少,一个也不多。接下来我们再来看 下我们的蜂鸣器 IP 核底层驱动头文件 zircon_avalon_buzzer.h,该文件如代码 5.38 所示。 代码 5.38 zircon_avalon_buzzer.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_buzzer.h 3 //-- Describe : buzzer IP core driver header file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_BUZZER_H__ 8 #define __ZIRCON_AVALON_BUZZER_H__ 9 10 #ifdef __cplusplus 11 extern "C" 12 { 13 #endif /* __cplusplus */ 14 15 #include "zircon_avalon_buzzer_regs.h" 16 #include "alt_types.h" 17 #include "system.h" http://www.fpga.gs/ 368 软核演练篇 §5 18 19 // rhythm: quarter note as a meter 20 #define RHYTHM 36 21 #define _1 RHYTHM*4 //note 22 #define _1d RHYTHM*6 //dotted note 23 #define _2 RHYTHM*2 //halfnote 24 #define _2d RHYTHM*3 //dotted halfnote 25 #define _4 RHYTHM*1 //quarter note 26 #define _4d RHYTHM*3/2 //dotted quarter note 27 #define _8 RHYTHM*1/2 //eighth note 28 #define _8d RHYTHM*3/4 //dotted eighth note 29 #define _16 RHYTHM*1/4 //sixteenth note 30 #define _16d RHYTHM*3/8 //dotted sixteenth note 31 #define _32 RHYTHM*1/8 //thirty-second note 32 //bass 33 #define _1DO (ALT_CPU_FREQ/131) 34 #define _1DOr (ALT_CPU_FREQ/139) 35 #define _1RE (ALT_CPU_FREQ/147) 36 #define _1REr (ALT_CPU_FREQ/155) 37 #define _1MI (ALT_CPU_FREQ/165) 38 #define _1FA (ALT_CPU_FREQ/175) 39 #define _1FAr (ALT_CPU_FREQ/185) 40 #define _1SOL (ALT_CPU_FREQ/196) 41 #define _1SOLr (ALT_CPU_FREQ/207) 42 #define _1LA (ALT_CPU_FREQ/220) 43 #define _1LAr (ALT_CPU_FREQ/233) 44 #define _1SI (ALT_CPU_FREQ/247) 45 //Alto 46 #define _DO (ALT_CPU_FREQ/262) 47 #define _DOr (ALT_CPU_FREQ/277) 48 #define _RE (ALT_CPU_FREQ/294) 49 #define _REr (ALT_CPU_FREQ/311) 50 #define _MI (ALT_CPU_FREQ/330) 51 #define _FA (ALT_CPU_FREQ/349) 52 #define _FAr (ALT_CPU_FREQ/370) 53 #define _SOL (ALT_CPU_FREQ/392) 54 #define _SOLr (ALT_CPU_FREQ/416) 55 #define _LA (ALT_CPU_FREQ/440) 56 #define _LAr (ALT_CPU_FREQ/466) 57 #define _SI (ALT_CPU_FREQ/492) 58 //treble 59 #define _DO1 (ALT_CPU_FREQ/523) 60 #define _DO1r (ALT_CPU_FREQ/554) 61 #define _RE1 (ALT_CPU_FREQ/579) Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 369 62 #define _RE1r (ALT_CPU_FREQ/740) 63 #define _MI1 (ALT_CPU_FREQ/651) 64 #define _FA1 (ALT_CPU_FREQ/695) 65 #define _FA1r (ALT_CPU_FREQ/740) 66 #define _SOL1 (ALT_CPU_FREQ/784) 67 #define _SOL1r (ALT_CPU_FREQ/830) 68 #define _LA1 (ALT_CPU_FREQ/880) 69 #define _LAR1r (ALT_CPU_FREQ/932) 70 #define _SI1 (ALT_CPU_FREQ/983) 71 72 #define SONG_SIZE 150 73 #define MUTE(TONE) (TONE)>>2 //Bass to 25% duty cycle 74 #define LOUD(TONE) (TONE)>>1 //treble to 50% duty cycle 75 76 77 //--------------------------------------------------------------------------- 78 //-- Name : zircon_buzzer_enable() 79 //-- Function : buzzer open 80 //-- Input parameters : no 81 //-- Output parameters: no 82 //--------------------------------------------------------------------------- 83 void zircon_buzzer_enable(); 84 85 //--------------------------------------------------------------------------- 86 //-- Name : zircon_buzzer_disable() 87 //-- Function : buzzer close 88 //-- Input parameters : no 89 //-- Output parameters: no 90 //--------------------------------------------------------------------------- 91 void zircon_buzzer_disable(); 92 93 //--------------------------------------------------------------------------- 94 //-- Name : zircon_buzzer_sound() 95 //-- Function : buzzer Play sound 96 //-- Input parameters : clock_divider: Cycle setting,duty_cycle: Duty cycle setting 97 //-- Output parameters: no 98 //--------------------------------------------------------------------------- 99 void zircon_buzzer_sound(alt_u32 clock_divider, alt_u32 duty_cycle); 100 101 //--------------------------------------------------------------------------- 102 //-- Name : zircon_buzzer_demo1() 103 //-- Function : buzzer Play Da Chang Jin music 104 //-- Input parameters : no http://www.fpga.gs/ 370 软核演练篇 §5 105 //-- Output parameters: no 106 //--------------------------------------------------------------------------107 void zircon_buzzer_demo1(); 108 109 /* 110 * Macros used by alt_sys_init() 111 */ 112 #define ZIRCON_AVALON_BUZZER_INSTANCE(name, device) alt_u32 buzzer_controller_addr = name##_BASE 113 #define ZIRCON_AVALON_BUZZER_INIT(name, device) while (0) 114 115 #ifdef __cplusplus 116 } 117 #endif /* __cplusplus */ 118 119 #endif /* __ZIRCON_AVALON_BUZZER_H__ */ 从该代码中可以看出,我们实现了 4 个函数,下面我们就来进一步分析看下这 4 个函数究 竟实现了什么功能,下面我们给出底层驱动文件 zircon_avalon_buzzer.c,该文件如代码 5.39 所示。 代码 5.39 zircon_avalon_buzzer.c 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_buzzer.c 3 //-- Describe : buzzer IP core driver C file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "zircon_avalon_buzzer.h" 8 #include "alt_types.h" 9 #include "unistd.h" 10 #include "priv/alt_busy_sleep.h" 11 12 extern alt_u32 buzzer_controller_addr; 13 14 15 //First column tone, 16 //Second columns rhythm 17 //Third column pitch (high and low) 18 int dachangjin[SONG_SIZE][3] = { 19 {_LA, _4, LOUD(_LA)}, //2 20 {_SI, _4, MUTE(_SI)}, //3 21 {_SI, _4, MUTE(_SI)}, //3 22 {_SI, _4d,LOUD(_SI)}, //3. 23 {_LA, _8, MUTE(_LA)}, //2_ Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 24 {_SOL,_4, MUTE(_SOL)},//1 25 {_MI, _4, LOUD(_MI)}, //.6 26 {_SOL,_4, MUTE(_SOL)},//1 27 {_SOL,_8d, MUTE(_SOL)},//1 28 {_LA, _32,MUTE(_LA)}, //2__ 29 {_SOL,_2d,MUTE(_SOL)},//1-- 30 31 {_LA, _4, LOUD(_LA)}, //2 32 {_SI, _4, MUTE(_SI)}, //3 33 {_SI, _4, MUTE(_SI)}, //3 34 {_SI, _4d,LOUD(_SI)}, //3. 35 {_RE1,_8, MUTE(_RE1)},//5 36 {_SI, _4, MUTE(_SI)}, //3 37 {_SI, _4, MUTE(_SI)}, //3 38 {_LA, _4, MUTE(_LA)}, //2 39 {_SI, _4, MUTE(_SI)}, //3 40 {_SI, _2d,MUTE(_SI)}, //3-- 41 42 {_RE1,_4, LOUD(_RE1)},//5 43 {_MI1,_4, MUTE(_MI1)},//6 44 {_MI1,_4, MUTE(_MI1)},//6 45 {_MI1,_4d,LOUD(_MI1)},//6 46 {_RE1,_8, MUTE(_RE1)},//5 47 {_SI, _4, MUTE(_SI)}, //3 48 {_SI, _4, LOUD(_SI)}, //3 49 {_RE1,_4, MUTE(_RE1)},//5 50 {_MI1,_8, MUTE(_MI1)},//6 51 {_RE1,_32,MUTE(_RE1)},//5 52 {_MI1,_32,MUTE(_MI1)},//6 53 {_RE1,_2d,MUTE(_RE1)},//5 54 55 {_LA, _4, LOUD(_LA)}, //2 56 {_SI, _4, MUTE(_SI)}, //3 57 {_SI, _4, MUTE(_SI)}, //3 58 {_LA, _4d,LOUD(_LA)}, //3. 59 {_SI, _8, MUTE(_SI)}, //2 60 {_SI, _4, MUTE(_SI)}, //3 61 {_LA, _4, LOUD(_LA)}, //2 62 {_SI, _4, MUTE(_SI)}, //3 63 {_MI, _4, LOUD(_MI)}, //.6 64 {_SOL,_16,MUTE(_SOL)},//1 65 {_MI, _2d,MUTE(_MI)}, //.6 66 {_MI, _4,0}, //stop 67 {_LA, _4, LOUD(_LA)}, //2 http://www.fpga.gs/ 371 372 软核演练篇 §5 68 {_SI, _4, MUTE(_SI)}, //3 69 {_SI, _4, MUTE(_SI)}, //3 70 {_SI, _4d,LOUD(_SI)}, //3. 71 {_LA, _8, MUTE(_LA)}, //2_ 72 {_SOL,_4, MUTE(_SOL)},//1 73 {_MI, _4, LOUD(_MI)}, //.6 74 {_SOL,_4, MUTE(_SOL)},//1 75 {_SOL,_8d,MUTE(_SOL)},//1 76 {_LA, _32,MUTE(_LA)}, //2__ 77 {_SOL,_2d,MUTE(_SOL)},//1-- 78 79 {_LA, _4, LOUD(_LA)}, //2 80 {_SI, _4, MUTE(_SI)}, //3 81 {_SI, _4, MUTE(_SI)}, //3 82 {_SI, _4d,LOUD(_SI)}, //3. 83 {_RE1,_8, MUTE(_RE1)},//5 84 {_SI, _4, MUTE(_SI)}, //3 85 {_SI, _4, MUTE(_SI)}, //3 86 {_LA, _4, MUTE(_LA)}, //2 87 {_SI, _4, MUTE(_SI)}, //3 88 {_SI, _2d,MUTE(_SI)}, //3-- 89 90 {_RE1,_4, LOUD(_RE1)},//5 91 {_MI1,_4, MUTE(_MI1)},//6 92 {_MI1,_4, MUTE(_MI1)},//6 93 {_MI1,_4d,LOUD(_MI1)},//6 94 {_RE1,_8, MUTE(_RE1)},//5 95 {_SI, _4, MUTE(_SI)}, //3 96 {_SI, _4, LOUD(_SI)}, //3 97 {_RE1,_4, MUTE(_RE1)},//5 98 {_MI1,_4, MUTE(_MI1)},//6 99 {_RE1,_2d,MUTE(_RE1)},//5 100 101 {_LA, _4, LOUD(_LA)}, //2 102 {_SI, _4, MUTE(_SI)}, //3 103 {_SI, _4, MUTE(_SI)}, //3 104 {_LA, _4d,LOUD(_LA)}, //3. 105 {_SI, _8, MUTE(_SI)}, //2 106 {_SI, _4, MUTE(_SI)}, //3 107 {_LA, _4, LOUD(_LA)}, //2 108 {_SI, _4, MUTE(_SI)}, //3 109 {_MI, _4, LOUD(_MI)}, //.6 110 {_SOL,_16,MUTE(_SOL)},//1 111 {_MI, _2d,MUTE(_MI)}, //.6-- Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 373 112 113 {_LA, _16,LOUD(_LA)}, //2 114 {_LA, _16,LOUD(_LA)}, //2 115 {_LA, _16d,LOUD(_LA)},//2 116 {_SOL,_8, MUTE(_SOL)},//1 117 {_MI, _4, MUTE(_MI)}, //.6 118 {_LA, _16,LOUD(_LA)}, //2 119 {_LA, _16,LOUD(_LA)}, //2 120 {_LA, _16d,LOUD(_LA)},//2 121 {_SOL,_8, MUTE(_SOL)},//1 122 {_MI, _4, MUTE(_MI)}, //.6 123 {_LA, _4, LOUD(_LA)}, //2 124 {_SI, _4, MUTE(_SI)}, //3 125 {_SOL,_4, MUTE(_SOL)},//1 126 {_LA, _4d,LOUD(_LA)}, //2 127 {_SI, _8, MUTE(_SI)}, //3 128 {_RE1,_4, MUTE(_RE1)},//5 129 {_MI1,_16,LOUD(_MI1)},//6 130 {_MI1,_16,LOUD(_MI1)},//6 131 {_MI1,_16d,LOUD(_MI1)},//6 132 {_RE1,_8, MUTE(_RE1)},//5 133 {_SI, _4, MUTE(_SI)}, //3 134 {_LA, _16,LOUD(_LA)}, //2 135 {_LA, _16,LOUD(_LA)}, //2 136 {_LA, _16d, LOUD(_LA)},//2 137 {_SOL,_8, MUTE(_SOL)},//1 138 {_MI, _4, MUTE(_MI)}, //.6 139 {_MI, _4, LOUD(_MI)}, //.6 140 {_RE, _4, MUTE(_RE)}, //.5 141 {_MI, _4, MUTE(_MI)}, //.6 142 {_MI, _2d,MUTE(_MI)}, //.6 143 {_MI, _4,0}, //stop 144 {_MI, _4,0}, //stop 145 }; 146 147 //--------------------------------------------------------------------------- 148 //-- Name : zircon_buzzer_sound() 149 //-- Function : buzzer Play sound 150 //-- Input parameters : clock_divider: Cycle setting,duty_cycle: Duty cycle setting 151 //-- Output parameters: no 152 //--------------------------------------------------------------------------- 153 void zircon_buzzer_sound(alt_u32 clock_divider, alt_u32 duty_cycle) 154 { http://www.fpga.gs/ 374 软核演练篇 §5 155 IOWR_ZIRCON_BUZZER_CLOCK_DIVIDER(buzzer_controller_addr, clock_divider - 1); 156 IOWR_ZIRCON_BUZZER_DUTY_CYCLE(buzzer_controller_addr, duty_cycle); 157 } 158 159 //--------------------------------------------------------------------------- 160 //-- Name : zircon_buzzer_enable() 161 //-- Function : buzzer open 162 //-- Input parameters : no 163 //-- Output parameters: no 164 //--------------------------------------------------------------------------- 165 void zircon_buzzer_enable() 166 { 167 IOWR_ZIRCON_BUZZER_ENABLE(buzzer_controller_addr, 1); 168 } 169 170 //--------------------------------------------------------------------------- 171 //-- Name : zircon_buzzer_disable() 172 //-- Function : buzzer close 173 //-- Input parameters : no 174 //-- Output parameters: no 175 //--------------------------------------------------------------------------- 176 void zircon_buzzer_disable() 177 { 178 IOWR_ZIRCON_BUZZER_ENABLE(buzzer_controller_addr, 0); 179 } 180 181 //--------------------------------------------------------------------------- 182 //-- Name : zircon_buzzer_delay() 183 //-- Function : Software delay 184 //-- Input parameters : delaydata:Delay time 185 //-- Output parameters: no 186 //--------------------------------------------------------------------------- 187 void zircon_buzzer_delay(alt_32 delaydata) 188 { 189 while(delaydata--) 190 { 191 usleep(1000); 192 } 193 } 194 195 //--------------------------------------------------------------------------- 196 //-- Name : zircon_buzzer_demo1() 197 //-- Function : buzzer Play Da Chang Jin music 198 //-- Input parameters : no Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 375 199 //-- Output parameters: no 200 //--------------------------------------------------------------------------- 201 void zircon_buzzer_demo1() 202 { 203 int i; 204 205 for(i=0; i 8 #include "zircon_avalon_buzzer.h" 9 10 //--------------------------------------------------------------------------- 11 //-- 名称 : main() 12 //-- 功能 : 程序入口 13 //-- 输入参数 : 无 14 //-- 输出参数 : 无 15 //--------------------------------------------------------------------------- 16 int main () 17 { 18 printf("Welcome to Buzzer Ip demo program..."); 19 zircon_buzzer_demo1(); 20 return 0; 21 } Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 377 (4) 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Buzzer_Ip.sof 下载至我们的 A4 开发板,Qsys_Buzzer_Ip.sof 下 载完成后,我们还需要在 Eclipse 软件中将 Qsys_Buzzer_Ip.elf 文件下载至我们的 A4 开发板, Qsys_Buzzer_Ip.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我 们可以在 Eclipse 软件的控制台中看到我们的打印信息,如图 5.73 所示。 图 5.73 蜂鸣器 IP 核的控制台打印信息图 这时,我们不需要任何操作,我们的 A4 开发板上的蜂鸣器便会响起“嘀哒哒,滴滴哒……”。 至此,我们的蜂鸣器 IP 核的应用就讲解完了。 §5.7 红外外设 5.7.1 红外 IP 核的定制 (1) 规划 IP 核的硬件功能 完成了蜂鸣器 IP 核的定制,接下来我们在为我们的红外外设定制 IP 核,定制 IP 核的第一 步,就是为我们的红外外设规划 IP 核的硬件功能,下面我们给出红外寄存器的规划方案,如表 5.23 所示。 偏移量 00 表 5.23 红外 IP 核寄存器映射 位描述 寄存器名称 操作 31 … 8 7 … 0 数据寄存器 读 保留 DATA 通过上面的寄存器规划方案我们可以看出,之前我们的数据寄存器都是写操作,而这里,我 们的数据寄存器是读操作,也就是说,之前我们都是往数据寄存器中写数据,而现在我们则是需 要从数据寄存器中读数据。 (2) 定义一个恰当的 Avalon 接口 规划完蜂鸣器 IP 核的硬件功能,接下来我们就来给蜂鸣器定义 Avalon 接口。我们将信号总 结如表 5.24 所示。 信号名 clock reset 表 5.24 红外 IP 核的接口信号 Avalon 信号类型 宽度 Clock 1 Reset 1 方向 Input Input http://www.fpga.gs/ 378 软核演练篇 §5 address Avalon-MM 1 read Avalon-MM 1 readdata Avalon-MM 32 irq Interrupt 1 clk Conduit 1 ir_data Conduit 1 Input Input Output Output Input Input 从上面的信号接口我们可以看出,表格中除了我们的几个熟悉面孔之外,又多出了几个新面 孔,下面我们就来简单的介绍一下,首先我们介绍的是 read 和 readdata,read 就是我们的读 请求信号,readdata 就是我们的读数据,这两个信号和我们的 write 和 wirtedata 的使用方法可 以说是基本相同的。接下来我们要介绍的就是 irq,也就是我们的中断信号。有了这个信号,我 们的 IP 核就能够使用中断功能了。这里还需要我们注意的是,clk 时钟信号,这个时钟信号是用 来给我们的红外硬件逻辑文件提供时钟的。说完了我们的信号,接下来我们再来看看我们的 Avalon 总线的时序,如图 5.74 所示。 图 5.74 红外 IP 核的信号接口时序图 从这个时序图中我们可以看出,这个读时序也是很简单的,它和我们的写时序也是基本相同 的。从这个时序中我们可以看到,我们的数据延迟了一个时钟,这是因为我们选择的默认配置, 在 IP 核的编辑器中,默认配置 read wait 是 1,而 write wait 是 0。说完了,我们的读时序,接 下来我们再来看下我们的中断时序,如图 5.75 所示。 图 5.75 红外 IP 核的中断信号时序图 从时序图中我们可以看出,这个中断时序比我们读时序还要简单,当中断来了,我们的 irq 就会置高响应中断,当我们响应完成中断之后,irq 就会变低,等待下一个中断的信号的到来。 (3) 使用硬件描述语言描述硬件逻辑 定义完了 Avalon 接口,接下来我们就到了第三步使用硬件描述语言描述硬件逻辑,首先我 们来看下顶层文件 zircon_avalon_ir.v,该文件如代码 5.41 所示。 代码 5.41 zircon_avalon_ir.v 代码 1 //--------------------------------------------------------------------------2 //-- 文件名 : zircon_avalon_ir.v Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 379 3 //-- 描述 : 红外 IP 核的顶层文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_ir 8( 9 //时钟复位 10 csi_clk,rsi_reset_n, 11 //Avalon-MM 从端口 12 avs_address,avs_read,avs_readdata,ins_irq, 13 //外设管脚输出 14 coe_ir_data,coe_clk 15 ); 16 17 input 18 input 19 input csi_clk; rsi_reset_n; avs_address; //系统时钟 //系统复位 //Avalon 地址总线 20 input avs_read; //Avalon 读请求信号 21 output [31:0] avs_readdata; //Avalon 读数据总线 22 output ins_irq; //Avalon 中断信号 23 input 24 input coe_clk; coe_ir_data; //红外硬件逻辑文件系统时钟 //红外的数据管脚 25 26 wire [ 7:0] o_ir_data; //从红外数据管脚中读出的数据 27 28 zircon_avalon_ir_logic zircon_avalon_ir_logic_init 29 ( 30 .CLK_50M 31 .RST_N 32 .IR_DATA (coe_clk (rsi_reset_n (coe_ir_data ), //红外硬件逻辑文件系统时钟 ), //系统复位 ), //红外的数据管脚 33 .o_ir_data (o_ir_data ), //从红外数据管脚中读出的数据 34 .irq (ins_irq ) //Avalon 中断信号 35 ); 36 37 zircon_avalon_ir_register zircon_avalon_ir_register_init 38 ( 39 .csi_clk (csi_clk ), //系统时钟 40 .rsi_reset_n (rsi_reset_n ), //系统复位 41 .avs_address (avs_address ), //Avalon 地址总线 42 .avs_read (avs_read ), //Avalon 读请求信号 43 .avs_readdata (avs_readdata ), //Avalon 读数据总线 44 .o_ir_data (o_ir_data ) //从红外数据管脚中读出的数据 45 ); 46 http://www.fpga.gs/ 380 软核演练篇 §5 47 endmodule 我们可以看到这个顶层模块文件就是用来连接我们寄存器文件和硬件逻辑文件的。它本身 没有实现任何的逻辑功能,看完了顶层文件,接下来我们再来看下寄存器文件 zircon_avalon_ir_register.v,该文件如代码 5.42 所示。 代码 5.42 zircon_avalon_ir_register.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_ir_register.v 3 //-- 描述 : 红外 IP 核的寄存器文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_ir_register 8( 9 //时钟复位 10 csi_clk,rsi_reset_n, 11 //Avalon-MM 从端口 12 avs_address,avs_read,avs_readdata, 13 //用户逻辑输入与输出 14 o_ir_data 15 ); 16 17 input 18 input 19 input 20 input 21 output 22 input [31:0] [ 7:0] csi_clk; rsi_reset_n; avs_address; avs_read; avs_readdata; o_ir_data; //系统时钟 //系统复位 //Avalon 地址总线 //Avalon 读请求信号 //Avalon 读数据总线 //从红外数据管脚中读出的数据 23 24 reg 25 reg [ 7:0] [ 7:0] data_reg; data_reg_n; //用来将红外数据赋值给 Avalon 读数据总线 //data_reg 的下一个状态 26 27 //时序电路,用于给 data_reg 赋值的 28 always @ (posedge csi_clk or negedge rsi_reset_n) 29 begin 30 if(!rsi_reset_n) 31 data_reg <= 8'h00; //判断复位 //初始化 data_reg 32 else 34 data_reg <= data_reg_n; 35 end //用来给 data_reg 赋值的 36 37 //组合电路,用来给地址偏移量 0,也就是我们的数据寄存器读 8 位的数据 38 always @ (*) 39 begin Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 381 40 if((avs_read) && (avs_address == 1'b0)) //判断读请求和读地址 41 data_reg_n = o_ir_data; //如果条件成立,将红外数据赋值给 data_reg_n 42 else 43 data_reg_n = data_reg; //否则,将保持不变 44 end 45 46 assign avs_readdata = {24'h0,data_reg};//将红外数据赋值给 Avalon 读数据总线 47 48 endmodule 我们可以看到寄存器文件很简单,我们只需要判断读请求和地址,等待数据被读到数据寄存 器中。看完了我们的寄存器文件,最后我们再来看下我们的硬件逻辑文件 zircon_avalon_ir_logic.v,该文件如代码 5.43 所示。 代码 5.43 zircon_avalon_ir_logic.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_ir_logic.v 3 //-- 描述 : 红外 IP 核的硬件逻辑文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 8 /*Timing control. 9 `define HEAD_HIGH 24'h6_DDD0 // 9.000ms @ 50MHz, standard is 24'h6_DDD0. 10 `define HEAD_LOW 24'h3_6EE8 // 4.500ms @ 50MHz, standard is 24'h3_6EE8 11 `define BIT_0_HIGH 24'h6D60 // 0.560ms @ 50MHz, standard is 24'h6D60 12 `define BIT_0_LOW 24'h6E5A // 0.565ms @ 50MHz, standard is 24'h6E5A 13 14 `define BIT_1_HIGH 24'h6D60 // 0.560ms @ 50MHz, standard is 24'h6D60 15 `define BIT_1_LOW 24'h1_4A14 // 1.685ms @ 50MHz, standard is 24'h1_4A14 16 17 `define REP_HEAD_HIGH 24'h6_DDD0 // 9.000ms @ 50MHz, standard is 24'h6_DDD0. 18 `define REP_HEAD_LOW 24'h1_B774 // 2.250ms @ 50MHz, standard is 24'h1_B774 19 `define REP_BIT_HIGH 24'h6D60 // 0.560ms @ 50MHz, standard is 24'h6D60 20 `define REP_BIT_LOW 24'hF4240 // 20.00ms @ 50MHz, standard is 24'hF4240 */ 21 22 //high_time 为红外引导码 9ms 低电平,(high_time[23:14] == `HEAD_HIGH) 23 //24'h6_DDD0 = (0110_11)01_1101_1101_0000 的[23:14]是(01_1011) = 10'h1B 24 //1B+14 个 0=6C000 X 20ns = 8.85ms 25 `define HEAD_HIGH 10'h1B //(0110_11)01_1101_1101_0000 约 8.85ms 26 `define HEAD_LOW 10'hD //(0011_01)10_1110_1110_1000 约 4.26ms 27 `define BIT_0_HIGH 10'h1 //(0000_01)10_1101_0110_0000 约 0.33ms 28 `define BIT_0_LOW 10'h1 //(0000_01)10_1110_0101_1010 约 0.33ms 29 `define BIT_0_LOW2 10'h2 //(0000_10)00_0000_0000_0000 约 0.66ms 30 `define BIT_1_HIGH 10'h1 //(0000_01)10_1101_0110_0000 约 0.33ms http://www.fpga.gs/ 382 软核演练篇 §5 31 `define BIT_1_LOW 10'h5 //(0001_01)00_1010_0001_0100 约 1.64ms 32 `define BIT_1_LOW2 10'h4 //(0001_00)00_0000_0000_0000 约 1.31ms 33 `define REP_HEAD_HIGH 10'h1B //(0110_11)01_1101_1101_0000 约 8.85ms 34 `define REP_HEAD_LOW 10'h7 //(0001_10)11_0111_0111_0100 约 1.97ms 35 `define REP_BIT_HIGH 10'h1 //(0000_01)10_1101_0110_0000 约 0.33ms 36 `define REP_BIT_LOW 10'h37 //(1111_01)00_0010_0100_0000 约 19.99ms 37 38 module zircon_avalon_ir_logic 39 ( 40 //输入端口 41 CLK_50M,RST_N,IR_DATA, 42 //输出端口 43 o_ir_data,irq 44 ); 45 46 //--------------------------------------------------------------------------- 47 //-- 外部端口声明 48 //--------------------------------------------------------------------------- 49 input CLK_50M; //系统时钟 50 input RST_N; //系统复位 51 input 52 output [ 7:0] 53 output reg 54 reg IR_DATA; o_ir_data; irq; irq_n; //红外输入管脚 //从红外读出的数据 //中断信号 //irq 的下一个状态 55 //--------------------------------------------------------------------------- 56 //-- 内部端口声明 57 //--------------------------------------------------------------------------- 58 reg 59 reg 60 reg [ 3:0] [ 3:0] [23:0] ir_fsm_cs; ir_fsm_ns; time_cnt; //状态机的当前状态 //状态机的下一个状态 //计时器 61 reg [23:0] time_cnt_n; //time_cnt 的下一个状态 62 reg [23:0] low_time; //低电平计时器(实际是高电平) 63 reg [23:0] low_time_n; //low_time 的下一个状态 64 reg 65 reg 66 reg 67 reg [23:0] [23:0] [ 7:0] [ 7:0] high_time; high_time_n; bit_cnt; bit_cnt_n; //高电平计时器(实际是低电平) //high_time 的下一个状态 //用来记录 8 位串行红外数据组成一个字节 //bit_cnt 的下一个状态 68 reg [ 1:0] detect_edge; //检测边沿寄存器 69 wire [ 1:0] detect_edge_n; //detect_edge 的下一个状态 70 reg 71 reg 72 reg 73 reg [31:0] [31:0] [31:0] [31:0] ir_data; ir_data_n; ir_data_reg; ir_data_reg_n; //从红外读出的数据 //ir_data 的下一个状态 //红外数据的缓存寄存器 //ir_data_reg 的下一个状态 74 reg posedge_reg; //检测上升沿 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 383 75 wire posedge_reg_n; //posedge_reg 的下一个状态 76 wire 77 wire 78 wire head_code; bit_0_code; bit_1_code; //红外引导码 //逻辑 0(实际逻辑 1) //逻辑 1(实际逻辑 0) 79 wire rep_head_code; //重复引导码 80 wire rep_bit_code; //重复码 81 82 parameter 83 parameter 84 parameter 85 parameter FSM_IDLE FSM_DATA FSM_DATA_END FSM_REP_BIT = 4'h0; //空闲状态 = 4'h1; //串行数据接收状态 = 4'h2; //数据接收完成状态 = 4'h3; //处理重复码状态 86 parameter FSM_REP_BIT_END = 4'h4; //重复码处理完成状态 87 88 //时序电路,用来给 detect_edge 寄存器赋值 89 always @ (posedge CLK_50M or negedge RST_N) 90 begin 91 if(!RST_N) //判断复位 92 detect_edge <= 2'h0; //初始化 detect_edge 值 93 else 94 detect_edge <= detect_edge_n; //用来给 detect_edge 赋值 95 end 96 97 //组合电路,检测上升沿 98 assign detect_edge_n = {detect_edge[0] , {~IR_DATA}};//将红外信号取反并接收 99 100 //时序电路,用来给 posedge_reg 寄存器赋值 101 always @ (posedge CLK_50M or negedge RST_N) 102 begin 103 if(!RST_N) 104 posedge_reg <= 1'h0; //判断复位 //初始化 posedge_reg 值 105 else 106 posedge_reg <= posedge_reg_n; //用来给 posedge_reg 赋值 107 end 108 109 //组合电路,判断上升沿,如果 detect_edge 等于 01,posedge_reg_n 就置 1 110 assign posedge_reg_n = (detect_edge == 2'b01) ? 1'b1 : 1'b0; 111 112 //时序电路,用来给 time_cnt 寄存器赋值 113 always @ (posedge CLK_50M or negedge RST_N) 114 begin 115 if(!RST_N) 116 time_cnt <= 24'h0; //判断复位 //初始化 time_cnt 值 117 else 118 time_cnt <= time_cnt_n; //用来给 time_cnt 赋值 http://www.fpga.gs/ 384 软核演练篇 §5 119 end 120 121 //组合电路,计数器用于记录高电平或者低电平的脉冲宽度 122 always @ (*) 123 begin 124 if(detect_edge[0] != detect_edge[1])//判断电平变化 125 time_cnt_n = 24'h0; //如果红外信号发生变化,time_cnt_n 就从 0 开始计数 126 else 127 time_cnt_n = time_cnt + 24'h1; //否则,time_cnt 就加 1 128 end 129 130 //时序电路,用来给 high_time 寄存器赋值 131 always @ (posedge CLK_50M or negedge RST_N) 132 begin 133 if(!RST_N) 134 high_time <= 24'h0; //判断复位 //初始化 high_time 值 135 else 136 high_time <= high_time_n; //用来给 high_time 赋值 137 end 138 139 //组合电路,实际记录的是 IR_DATA 上的低电平宽度,因为上面对 IR_DATA 做了一次取反操作 140 always @ (*) 141 begin 142 if(detect_edge == 2'b10) //判断下降沿 143 high_time_n = time_cnt; //如果判断为下降沿,则开始计数 144 else 145 high_time_n = high_time; //否则保持不变 146 end 147 148 //时序电路,用来给 low_time 寄存器赋值 149 always @ (posedge CLK_50M or negedge RST_N) 150 begin 151 if(!RST_N) //判断复位 152 low_time <= 24'h0; //初始化 low_time 值 153 else 154 low_time <= low_time_n; //用来给 low_time 赋值 155 end 156 157 //组合电路,实际记录的是 IR_DATA 上的高电平宽度,因为上面对 IR_DATA 做了一次取反操作 158 always @ (*) 159 begin 160 if(IR_DATA) 161 low_time_n = time_cnt; //判断高电平 //如果判断为高电平,则开始计数 162 else Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 385 163 low_time_n = low_time; //当 IR_DATA 变成 0 时就保持不变 164 end 165 166 //低电平至少 8.85ms,高电平至少 4.26ms,就被认为是引导码,head_code 就为 1 167 assign head_code = (high_time[23:14] == `HEAD_HIGH) && (low_time[23:14] == `HEAD_LOW) && posedge_reg; 168 //低电平至少 0.33ms,高电平至少 0.33ms 或者 0.66ms,被认为是逻辑"0",bit_0_code 就为 1 169 assign bit_0_code = (high_time[23:14] == `BIT_0_HIGH) && ((low_time[23:14] == `BIT_0_LOW) || (low_time[23:14] == `BIT_0_LOW2)) && posedge_reg; 170 //低电平至少 0.33ms,高电平至少 1.31ms 或者 1.66ms,被认为是逻辑"1",bit_1_code 就为 0 171 assign bit_1_code = (high_time[23:14] == `BIT_1_HIGH) && ((low_time[23:14] == `BIT_1_LOW) || (low_time[23:14] == `BIT_1_LOW2)) && posedge_reg; 172 //重复引导码 173 assign rep_head_code = (high_time[23:14] == `REP_HEAD_HIGH) && (low_time[23:14] == `REP_HEAD_LOW) && posedge_reg; 174 //重复码 175 assign rep_bit_code = (high_time[23:14] == `REP_BIT_HIGH) && (low_time[23:14] == `REP_BIT_LOW) && posedge_reg; 176 177 //时序电路,用来给 bit_cnt 赋值的 178 always @ (posedge CLK_50M or negedge RST_N) 179 begin 180 if(!RST_N) 181 bit_cnt <= 8'h0; //判断复位 //初始化 bit_cnt 182 else 183 bit_cnt <= bit_cnt_n; //用来给 bit_cnt 赋值 184 end 185 186 //组合电路,用来记录 8 位串行红外数据组成一个字节 187 always @ (*) 188 begin 189 if(ir_fsm_cs != FSM_DATA) //判断状态机当前状态是否在接收数据状态 190 bit_cnt_n = 8'h0; //如果不等于,bit_cnt_n 则清零 191 else if((ir_fsm_cs == FSM_DATA) && posedge_reg)//判断条件 192 bit_cnt_n = bit_cnt + 8'h1; //如果条件成立,则记录 8 位串行红外数据 193 else 194 bit_cnt_n = bit_cnt; //否则保持不变 195 end 196 197 //时序电路,用来给 ir_fsm_cs 赋值的 198 always @ (posedge CLK_50M or negedge RST_N) 199 begin 200 if(!RST_N) //判断复位 201 ir_fsm_cs <= FSM_IDLE; //初始化 ir_fsm_cs 的值 http://www.fpga.gs/ 386 软核演练篇 §5 202 else 203 ir_fsm_cs <= ir_fsm_ns; //用来给 ir_fsm_cs 赋值 204 end 205 206 //组合电路,状态机的控制核心 207 always @ (*) 208 begin 209 case(ir_fsm_cs) //判断当前的状态 210 211 FSM_IDLE: 212 if(head_code) //收到引导码后 213 ir_fsm_ns = FSM_DATA; //进入串行接收状态 214 else if(rep_head_code) //收到重复码后 215 ir_fsm_ns = FSM_REP_BIT; //进入处理重复码状态 216 else 217 ir_fsm_ns = ir_fsm_cs; //否则保持不变 218 219 FSM_DATA: 220 if(bit_cnt == 8'h20) //接收 4 个字节(地址码,地址反码,命令码,命令反码) 221 ir_fsm_ns = FSM_DATA_END; //接收完毕后,进入数据完成状态 222 else if(rep_head_code || head_code || rep_bit_code) //判断重复码 223 ir_fsm_ns = FSM_IDLE; //进入空闲状态 224 else 225 ir_fsm_ns = ir_fsm_cs; //否则保持不变 226 227 FSM_DATA_END: 228 ir_fsm_ns = FSM_IDLE; //进入空闲状态 229 230 FSM_REP_BIT: 231 if(rep_bit_code) //判断重复码 232 ir_fsm_ns = FSM_REP_BIT_END; //进入重复码处理完成状态 233 else if(rep_head_code || head_code || bit_0_code ||bit_1_code)//判断重复码 234 ir_fsm_ns = FSM_IDLE; //进入空闲状态 235 else 236 ir_fsm_ns = ir_fsm_cs; //否则保持不变 237 238 FSM_REP_BIT_END: 239 ir_fsm_ns = FSM_IDLE; //进入空闲状态 240 241 default:ir_fsm_ns = FSM_IDLE; //进入空闲状态 242 endcase 243 end 244 245 //时序电路,用来给 ir_data_reg 赋值的 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 246 always @ (posedge CLK_50M or negedge RST_N) 247 begin 248 if(!RST_N) 249 ir_data_reg <= 32'h0; //判断复位 //初始化 ir_data_reg 250 else 251 ir_data_reg <= ir_data_reg_n; //用来给 ir_data_reg 赋值的 252 end 253 254 //组合电路,记录接收到的串行码 32bit,每接收一位,判断是 0 还是 1 后移位保存。 255 always @ (*) 256 begin 257 if(ir_fsm_cs == FSM_IDLE) //判断状态机的状态 258 ir_data_reg_n = 32'hFFFF; 259 else if((ir_fsm_cs == FSM_DATA) && (bit_1_code)) 260 ir_data_reg_n = {ir_data_reg[30:0] , 1'h1}; 261 else if((ir_fsm_cs == FSM_DATA) && (bit_0_code)) 262 ir_data_reg_n = {ir_data_reg[30:0] , 1'h0}; 263 else 264 ir_data_reg_n = ir_data_reg; //否则保持不变 265 end 266 267 //时序电路,用来给 ir_data 赋值的 268 always @ (posedge CLK_50M or negedge RST_N) 269 begin 270 if(!RST_N) //判断复位 271 ir_data <= 32'h0; //初始化 ir_data 272 else 273 ir_data <= ir_data_n; //用来给 ir_data 赋值 274 end 275 276 //组合电路,解码完成的状态就可以读取值了 277 always @ (*) 278 begin 279 if(ir_fsm_ns == FSM_DATA_END) //判断状态机的状态 280 ir_data_n = ir_data_reg; //解码完成的状态就可以读取值了 281 else 282 ir_data_n = ir_data; //否则保持不变 283 end 284 285 //时序电路,用来给中断信号赋值的 286 always @ (posedge CLK_50M or negedge RST_N) 287 begin 288 if(!RST_N) //判断复位 289 irq <= 1'b0; //初始化 irq http://www.fpga.gs/ 387 388 软核演练篇 §5 290 else 291 irq <= irq_n; //用来给 irq 赋值 292 end 293 294 //组合电路,状态机进入串行接收状态触发中断 295 always @ (*) 296 begin 297 if(ir_fsm_ns == FSM_DATA) 298 irq_n = 1'b1; //判断状态机的状态 //状态机进入串行接收状态触发中断 299 else 300 irq_n = 1'b0; //否则清除中断 301 end 302 303 assign o_ir_data = {ir_data[8],ir_data[9],ir_data[10],ir_data[11], ir_data[12],ir_data[13],ir_data[14],ir_data[15]}; 304 305 endmodule 从我们的硬件逻辑文件可以看出,我们的这个硬件逻辑文件代码那是相当的惊人,如果你在 这之前学习过我们的《项目实战篇》,那么你对这个代码就不会害怕了,这个代码也是从我们的 《项目实战篇》中拿取出来的,我们在原基础上增加了一个中断功能,我们从硬件逻辑文件中可 以看到,只要程序进入数据接收状态,这时我们定义的中断信号便会被置 1,中断便会触发。紧 接着我们便可以响应中断了。在看这个代码之前,我们推荐你先看红外的 NEC 编码协议,只有 理解了这个编码协议,你才能进一步看懂代码。由于这部分代码在我们的《项目实战篇》中有详 细的讲解,在这里我们就不在进一步进行讲解。 (4) 编写用于描述寄存器的 C 头文件和 IP 核的驱动 C 文件 写完了我们的硬件逻辑代码,我们就可以使用 IP 核编辑器来封装我们的硬件逻辑,完成 IP 核的定制了,对于后面的几个步骤,这里我们同前面一样也是一笔带过,我们这里主要介绍一下 红 外 IP 核 的 寄 存 器 头 文 件 , 下 面 我 们 给 出 的 是 红 外 IP 核 的 寄 存 器 头 文 件 zircon_avalon_ir_regs.h,该文件如代码 5.44 所示。 代码 5.44 zircon_avalon_ir_regs.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_ir_regs.h 3 //-- Describe : ir IP core register header file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_IR_REGS_H__ 8 #define __ZIRCON_AVALON_IR_REGS_H__ 9 10 #include 11 //Data register Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 389 12 #define IORD_ZIRCON_AVALON_IR_DATA(base) 13 14 #endif /* __ZIRCON_AVALON_IR_REGS_H__ */ IORD(base, 0) 从代码中我们可以看出,代码非常简单,并且只有一句代码,就是用来读取我们的数据寄存 器中的值。这里需要我们注意的是,由于我们的红外 IP 核使用起来比较简单,所以我们就没有 给出与其相对应的底层驱动头文件和底层驱动文件。最后我们补充说明一点,我们添加好了 IP 核并生成了 zircon_avalon_ir_hw.tcl 文件以后,我们需要修改 zircon_avalon_ir_hw.tcl 文件中 的一段代码,代码修改如下: 1# 2 # connection point interrupt_sender 3# 4 add_interface interrupt_sender interrupt end 5 set_interface_property interrupt_sender associatedAddressablePoint "avalon_slave" 6 set_interface_property interrupt_sender associatedClock clock 7 set_interface_property interrupt_sender associatedReset reset 8 set_interface_property interrupt_sender ENABLED true 9 set_interface_property interrupt_sender EXPORT_OF "" 10 set_interface_property interrupt_sender PORT_NAME_MAP "" 11 set_interface_property interrupt_sender CMSIS_SVD_VARIABLES "" 12 set_interface_property interrupt_sender SVD_ADDRESS_GROUP "" 13 14 add_interface_port interrupt_sender ins_irq irq Output 1 没有修改之前的代码,我们的第 5 行里是没有"avalon_slave"的,这个"avalon_slave"是我 们后面添加上来的,理论上应该由我们的 Qsys 软件帮我们自动添加,也许 Qsys 软件出现了 BUG 吧,它并没有帮我们添加中断,我们只好自己动手添加。如果这里没有添加你就使用该 IP 核,那么你在 Eclipse 软件中是找不到该 IP 核的中断。 5.7.2 红外 IP 核的应用 (1) 功能概述 完成了红外 IP 核的定制,接下来我们再来看看红外 IP 核的应用,首先我们讲解的是第一部 分功能概述,在该工程中,我们主要是使用红外 IP 核来测试我们的红外外设,将接收到的红外 遥控器的值打印到控制台中。 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该工程的硬件框架,如图 5.76 所示。 http://www.fpga.gs/ 390 软核演练篇 §5 图 5.76 红外 IP 核的硬件框架图 从该图中我们可以看出,除了我们常用几个基本 IP 核以外,我们这里只添加了一个红外的 IP 核,该 IP 核的使用是非常简单的,我们不需要任何配置,只需要将它添加至 Qsys 软件中就 可以正常使用了。这里需要我们注意的是,由于我们红外 IP 核带有中断功能,所以我们使用的 时候需要连接内部中断。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 5.45 所 示。 代码 5.45 Qsys_Ir_Ip.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Ir_Ip.c 3 //-- 描述 : 使用 Ir IP 核来测试我们的红外,将接收到的红外遥控器的值打印到控制台中 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include //标准的输入输出 8 #include "unistd.h" //延迟函数头文件 9 #include "sys/alt_irq.h" //中断函数头文件 10 #include "zircon_avalon_ir_regs.h" //红外寄存器头文件 11 #include "system.h" //系统头文件 12 13 int KeyboardData = 0; 14 int keydone = 0; //读取红外遥控器按下的按键值 //信号量:通知外部中断事件发生 15 16 //--------------------------------------------------------------------------- 17 //-- 名称 : Ir_Interrupt_IRQ() 18 //-- 功能 : 中断服务子程序 19 //-- 输入参数 : 无 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 391 20 //-- 输出参数 : 无 21 //--------------------------------------------------------------------------- 22 static void Ir_Interrupt_IRQ(void* context, alt_u32 id) 23 { 24 keydone++; 25 /* 读数据并清中断 */ 26 KeyboardData = IORD_ZIRCON_AVALON_IR_DATA(ZIRCON_AVALON_IR_BASE); 27 } 28 29 //--------------------------------------------------------------------------- 30 //-- 名称 : Ir_Interrupt_Init() 31 //-- 功能 : 红外中断初始化 32 //-- 输入参数 : 无 33 //-- 输出参数 : 无 34 //--------------------------------------------------------------------------- 35 void Ir_Interrupt_Init( void) 36 { 37 /* 注册中断服务子程序 */ 38 alt_irq_register(ZIRCON_AVALON_IR_IRQ, NULL, Ir_Interrupt_IRQ); 39 } 40 41 //--------------------------------------------------------------------------- 42 //-- 名称 43 //-- 功能 : main() : 程序入口 44 //-- 输入参数 : 无 45 //-- 输出参数 : 无 46 //--------------------------------------------------------------------------- 47 int main() 48 { 49 Ir_Interrupt_Init(); //红外中断初始化 50 51 printf("Welcome to Ir Ip demo program... \n"); 52 53 while(1) 54 { 55 if(keydone != 0) 56 { 57 printf("key=%d," ,KeyboardData); 58 keydone = 0; 59 } 60 usleep(100); 61 } 62 return 0; 63 } http://www.fpga.gs/ 392 软核演练篇 §5 (4) 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Ir_Ip.sof 下载至我们的 A4 开发板,Qsys_Ir_Ip.sof 下载完成后, 我们还需要在 Eclipse 软件中将 Qsys_Ir_Ip.elf 文件下载至我们的 A4 开发板,Qsys_Ir_Ip.elf 下 载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可以在 Eclipse 软件的 控制台中看到我们的打印信息,如图 5.77 所示。 图 5.77 红外 IP 核的控制台打印信息图 这时,我们使用红外遥控器按下 7 键,我们的控制台中便会打印出 7 的值,如图 5.78 所示。 图 5.78 红外 IP 核的板级调试图 当然,我们还可以按下红外遥控器上的其他键,我们的控制台都会显示出与其相对应的值。 这里我们就不给大家一一进行展示了,大家可以自行下载至 A4 开发板上逐一验证。至此,我们 的红外 IP 核的应用就讲解完了。 §5.8 TLC549 AD外设 5.8.1 AD IP 核的定制 (1) 规划 IP 核的硬件功能 完成了红外 IP 核的定制,接下来我们在为我们的 TLC549 AD 外设定制 IP 核,定制 IP 核 的第一步,就是为我们的 AD 外设规划 IP 核的硬件功能,下面我们给出 AD 寄存器的规划方案, 如表 5.25 所示。 偏移量 00 寄存器名称 数据寄存器 表 5.25 AD IP 核寄存器映射 位描述 操作 31 … 8 7 … 0 读 保留 DATA Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 393 从该表中可以看出,我们的 AD 外设和红外外设的寄存器规划方案是一样的,它也是只有一 个数据寄存器,并且该数据寄存器也用了低 8 位。 (2) 定义一个恰当的 Avalon 接口 规划完 AD IP 核的硬件功能,接下来我们就来给 AD 定义 Avalon 接口。我们将信号总结如 表 5.26 所示。 信号名 clock reset address read readdata ad_data ad_cs ad_clk ad_sysclk 表 5.26 AD IP 核的接口信号 Avalon 信号类型 宽度 Clock 1 Reset 1 Avalon-MM 1 Avalon-MM 1 Avalon-MM 32 Conduit 1 Conduit 1 Conduit 1 Conduit 1 方向 Input Input Input Input Output Input Output Output Input 从这个表格中我们可以看出,我们的 AD 外设也是使用的读请求和读数据,不过我们的 AD 外设没有使用中断,我们这里使用的是轮询方式。我们还可以从表格中看到我们的 AD 它是有四 个管脚信号,它们分别是数据管脚、片选管脚,AD 时钟管脚,AD 模块的系统时钟管脚,这里 要注意的是,这两个时钟大家不要搞混淆了。 (3) 使用硬件描述语言描述硬件逻辑 定义完了 Avalon 接口,接下来我们就到了第三步使用硬件描述语言描述硬件逻辑,首先我 们来看下顶层文件 zircon_avalon_tlc549.v,该文件如代码 5.46 所示。 代码 5.46 zircon_avalon_tlc549.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_tlc549.v 3 //-- 描述 : TLC549 AD IP 核的顶层文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_tlc549 8( 9 //时钟复位 10 csi_clk,rsi_reset_n, 11 //Avalon-MM 从端口 12 avs_address,avs_read,avs_readdata, 13 //外设管脚输出 14 coe_ad_data,coe_ad_cs,coe_ad_clk,coe_ad_sysclk 15 ); 16 http://www.fpga.gs/ 394 软核演练篇 §5 17 input csi_clk; //系统时钟 18 input 19 input 20 input rsi_reset_n; avs_address; avs_read; //系统复位 //Avalon 地址总线 //Avalon 读请求信号 21 output [31:0] avs_readdata; //Avalon 读数据总线 22 input coe_ad_sysclk; //AD 模块的系统时钟 23 input coe_ad_data; //AD 数据端口 24 output 25 output coe_ad_cs; coe_ad_clk; //AD 片选端口 //AD 时钟端口 26 27 wire [ 7:0] data_out; //从 AD 中读出的数据 28 29 //硬件逻辑文件 30 zircon_avalon_tlc549_logic zircon_avalon_tlc549_logic_init 31 ( 32 .CLK_50M 33 .RST_N (coe_ad_sysclk (rsi_reset_n ), //AD 模块的系统时钟 ), //系统复位 34 .data_out (data_out ), //从 AD 中读出的数据 35 .coe_ad_cs (coe_ad_cs ), //AD 片选端口 36 .coe_ad_clk (coe_ad_clk ), //AD 时钟端口 37 .coe_ad_data (coe_ad_data ) //AD 数据端口 38 ); 39 40 //寄存器文件 41 zircon_avalon_tlc549_register zircon_avalon_tlc549_register_init 42 ( 43 .csi_clk (csi_clk 44 .rsi_reset_n (rsi_reset_n 45 .avs_address (avs_address 46 .avs_read (avs_read ), //系统时钟 ), //系统复位 ), //Avalon 地址总线 ), //Avalon 读请求信号 47 .avs_readdata (avs_readdata ), //Avalon 读数据总线 48 .data_out (data_out ) //从 AD 中读出的数据 49 ); 50 51 endmodule 我们可以看到这个顶层模块文件就是用来连接我们寄存器文件和硬件逻辑文件的。它本身 没有实现任何的逻辑功能,看完了顶层文件,接下来我们再来看下寄存器文件 zircon_avalon_tlc549_register.v,该文件如代码 5.47 所示。 代码 5.47 zircon_avalon_tlc549_register.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_tlc549_register.v 3 //-- 描述 : TLC549 AD IP 核的寄存器文件 4 //-- 修订历史 : 2014-1-1 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 395 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_tlc549_register 8( 9 //时钟复位 10 csi_clk,rsi_reset_n, 11 //Avalon-MM 从端口 12 avs_address,avs_read,avs_readdata, 13 //用户逻辑输入与输出 14 data_out, 15 ); 16 17 input csi_clk; //系统时钟 18 input 19 input 20 input 21 output [31:0] rsi_reset_n; avs_address; avs_read; avs_readdata; //系统复位 //Avalon 地址总线 //Avalon 读请求信号 //Avalon 读数据总线 22 23 input [ 7:0] data_out; //从 AD 中读出的数据 24 reg [ 7:0] data_reg; //用来将 AD 数据赋值给 Avalon 读数据总线 25 reg [ 7:0] data_reg_n; //data_reg 的下一个状态 26 27 //时序电路,用来给数据寄存器赋值 28 always @ (posedge csi_clk or negedge rsi_reset_n) 29 begin 30 if(!rsi_reset_n) //判断复位 31 data_reg <= 8'h00; //初始化数据寄存器 32 else 33 data_reg <= data_reg_n; //用来给数据寄存器赋值 34 end 35 36 //组合电路,用来给地址偏移量 0,也就是我们的数据寄存器读 8 位的数据 37 always @ (*) 38 begin 39 if((avs_read) && (avs_address == 1'b0))//判断读请求和读地址 40 data_reg_n = data_out; //如果条件成立,将 AD 数据赋值给 data_reg_n 41 else 42 data_reg_n = data_reg; //否则,将保持不变 43 end 44 45 assign avs_readdata = {24'h0,data_reg};//将 AD 数据赋值给 Avalon 读数据总线 46 47 endmodule 从该代码中可以看出,这个代码和我们的红外代码是相同的,我们只需要判断读请求和地址, http://www.fpga.gs/ 396 软核演练篇 §5 等待数据被读到数据寄存器中。看完了我们的寄存器文件,最后我们再来看下我们的硬件逻辑文 件 zircon_avalon_tlc549_logic.v,该文件如代码 5.48 所示。 代码 5.48 zircon_avalon_tlc549_logic.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_tlc549_logic.v 3 //-- 描述 : TLC549 AD IP 核的硬件逻辑文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 8 `define AD_CLK_TIME_HALF 10'd22 //909ns / 2 = 454.5ns 45 / 2 = 22 9 `define AD_CLK_TIME 10'd45 //1.1M, 909ns,909 / (1 / 50M) = 45 =0x2D 10 11 module zircon_avalon_tlc549_logic 12 ( 13 //Input 14 CLK_50M,RST_N, 15 //Output 16 coe_ad_cs,coe_ad_clk,coe_ad_data,data_out 17 ); 18 19 //--------------------------------------------------------------------------20 //-- 外部端口声明 21 //--------------------------------------------------------------------------- 22 input CLK_50M; //时钟的端口,开发板用的 50M 晶振 23 input RST_N; //复位的端口,低电平复位 24 input coe_ad_data; //AD 数据端口 25 output coe_ad_cs; //AD 片选端口 26 output coe_ad_clk; //AD 时钟端口,最大不超过 1.1MHz 27 output [ 7:0] data_out; 28 29 //--------------------------------------------------------------------------30 //-- 内部端口声明 31 //--------------------------------------------------------------------------- 32 reg coe_ad_cs; //AD 片选信号端口 33 reg coe_ad_cs_n; //coe_ad_cs 的下一个状态 34 reg coe_ad_clk; //AD 时钟,最大不超过 1.1MHz 35 reg coe_ad_clk_n; //AD_CLK 的下一个状态 36 reg [ 2:0] ad_fsm_cs; //状态机的当前状态 37 reg [ 2:0] ad_fsm_ns; //状态机的下一个状态 38 reg [ 5:0] time_cnt; //用于记录一个时钟所用时间的定时器 39 reg [ 5:0] time_cnt_n; //time_cnt 的下一个状态 40 reg [ 5:0] bit_cnt; //用来记录时钟周期个数的计数器 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 41 reg [ 5:0] bit_cnt_n; //bit_cnt 的下一个状态 42 reg 43 reg 44 reg [ 7:0] [ 7:0] [ 7:0] data_out; data_out_n; ad_data_reg; //用来保存稳定的 AD 数据 //data_out 的下一个状态 //用于保存数据的移位寄存器 45 reg [ 7:0] ad_data_reg_n; //ad_data_reg_n 的下一个状态 46 47 parameter FSM_IDLE = 3'h0; //状态机的初始状态; 48 parameter FSM_READY = 3'h1; //满足 CS 有效时的第一个 1.4us 的延时状态 49 parameter FSM_DATA = 3'h2; //读取 8 个数据状态 50 parameter FSM_WAIT_CONV = 3'h3; //等待转换状态,等待 17us; 51 parameter FSM_END = 3'h4; //结束的状态 52 53 //时序电路,用来给 ad_fsm_cs 寄存器赋值 54 always @ (posedge CLK_50M or negedge RST_N) 55 begin 56 if(!RST_N) 57 ad_fsm_cs <= 1'b0; //判断复位 //初始化 ad_fsm_cs 值 58 else 59 ad_fsm_cs <= ad_fsm_ns; //用来给 ad_fsm_ns 赋值 60 end 61 62 //组合电路,用来实现状态机 63 always @ (*) 64 begin 65 case(ad_fsm_cs) //判断状态机的当前状态 66 FSM_IDLE: 67 //3 x 0.909us = 2.727us 用于初始化延时 68 if((bit_cnt == 6'd2 ) && (time_cnt == `AD_CLK_TIME)) 69 ad_fsm_ns = FSM_READY; //如果空闲状态完成就进入延时状态 70 else 71 ad_fsm_ns = ad_fsm_cs; //否则保持原状态不变 72 FSM_READY: 73 //2 x 0.909us = 1.818us 用于延迟 1.4us 74 if((bit_cnt == 6'd1 ) && (time_cnt == `AD_CLK_TIME)) 75 ad_fsm_ns = FSM_DATA; //如果延时状态完成就进入读取数据状态 76 else 77 ad_fsm_ns = ad_fsm_cs; //否则保持原状态不变 78 FSM_DATA: 79 //读取数据 8 位,1~8 个时钟脉冲 80 if((bit_cnt == 6'd8 ) && (time_cnt == `AD_CLK_TIME)) 81 ad_fsm_ns = FSM_WAIT_CONV;//如果读取数据状态完成就进入等待状态 82 else 83 ad_fsm_ns = ad_fsm_cs; //否则保持原状态不变 84 FSM_WAIT_CONV: http://www.fpga.gs/ 397 398 软核演练篇 §5 85 //19 x 0.909us = 17.271us 用于延迟 17us 86 if((bit_cnt == 6'd18) && (time_cnt == `AD_CLK_TIME)) 87 ad_fsm_ns = FSM_END; //如果等待状态完成就进入读取状态 88 else 89 ad_fsm_ns = ad_fsm_cs; //否则保持原状态不变 90 FSM_END: 91 ad_fsm_ns = FSM_READY; //完成一次数据转换,进入下一次转换 92 default:ad_fsm_ns = FSM_IDLE; 93 endcase 94 end 95 96 //时序电路,用来给 time_cnt 寄存器赋值 97 always @ (posedge CLK_50M or negedge RST_N) 98 begin 99 if(!RST_N) 100 time_cnt <= 6'h0; //判断复位 //初始化 time_cnt 值 101 else 102 time_cnt <= time_cnt_n; //用来给 time_cnt 赋值 103 end 104 105 //组合电路,实现 0.909us 的定时计数器 106 always @ (*) 107 begin 108 if(time_cnt == `AD_CLK_TIME) //判断 0.909us 时间 109 time_cnt_n = 6'h0; //如果到达 0.909us,定时器清零 110 else 111 time_cnt_n = time_cnt + 6'h1; //如果未到 0.909us,定时器继续加 1 112 end 113 114 //时序电路,用来给 bit_cnt 寄存器赋值 115 always @ (posedge CLK_50M or negedge RST_N) 116 begin 117 if(!RST_N) //判断复位 118 bit_cnt <= 6'h0; //初始化 bit_cnt 值 119 else 120 bit_cnt <= bit_cnt_n; //用来给 bit_cnt 赋值 121 end 122 123 //组合电路,用来记录时钟周期个数的计数器 124 always @ (*) 125 begin 126 if(ad_fsm_cs != ad_fsm_ns) 127 bit_cnt_n = 6'h0; //判断状态机的当前状态 //如果当前的状态不等于下一个状态,计时器就清零 128 else if(time_cnt == `AD_CLK_TIME_HALF)//判断 0.4545us 时间 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 399 129 bit_cnt_n = bit_cnt + 6'h1; //如果到达 0.4545us,计数器就加 1 130 else 131 bit_cnt_n = bit_cnt; //否则计数器保持不变 132 end 133 134 //时序电路,用来给 AD_CLK 寄存器赋值 135 always @ (posedge CLK_50M or negedge RST_N) 136 begin 137 if(!RST_N) 138 coe_ad_clk <= 1'h0; //判断复位 //初始化 AD_CLK 值 139 else 140 coe_ad_clk <= coe_ad_clk_n; //用来给 AD_CLK 赋值 141 end 142 143 //组合电路,用来生成 AD 的时钟波形 144 always @ (*) 145 begin 146 if(ad_fsm_cs != FSM_DATA) //判断状态机的当前状态 147 coe_ad_clk_n = 1'h0; //如果当前的状态不等于读取数据状态,AD_CLK_N 就置 0 148 else if(time_cnt == `AD_CLK_TIME_HALF)//判断 0.4545us 时间 149 coe_ad_clk_n = 1'h1; //如果到达 0.4545us,ADC_CLK_N 就置 1 150 else if(time_cnt == `AD_CLK_TIME)//判断 0.909us 时间 151 coe_ad_clk_n = 1'h0; //如果到达 0.909us,AD_CLK_N 就置 0 152 else 153 coe_ad_clk_n = coe_ad_clk; //否则保持不变 154 end 155 156 //时序电路,用来给 coe_ad_cs 寄存器赋值 157 always @ (posedge CLK_50M or negedge RST_N) 158 begin 159 if(!RST_N) //判断复位 160 coe_ad_cs <= 1'h0; //初始化 coe_ad_cs 值 161 else 162 coe_ad_cs <= coe_ad_cs_n; //用来给 coe_ad_cs 赋值 163 end 164 165 //组合电路,用来生成 AD 的片选波形 166 always @ (*) 167 begin 168 if((ad_fsm_cs == FSM_DATA) || (ad_fsm_cs == FSM_READY))//判断状态机的当前状态 169 coe_ad_cs_n = 1'h0;//如果条件符合,coe_ad_cs_n 就置 0 170 else 171 coe_ad_cs_n = 1'h1;//如果条件不符合,coe_ad_cs_n 就置 1 172 end http://www.fpga.gs/ 400 软核演练篇 §5 173 174 //时序电路,用来给 ad_data_reg 寄存器赋值 175 always @ (posedge CLK_50M or negedge RST_N) 176 begin 177 if(!RST_N) //判断复位 178 ad_data_reg <= 8'h0; //初始化 ad_data_reg 值 179 else 180 ad_data_reg <= ad_data_reg_n; //用来给 ad_data_reg 赋值 181 end 182 183 //组合电路,将 AD 线上的数据保存到移位寄存器中 184 always @(*) 185 begin 186 if((ad_fsm_cs == FSM_DATA) && (!coe_ad_clk) && (coe_ad_clk_n))//判断条件 187 ad_data_reg_n = {ad_data_reg[6:0],coe_ad_data};//将数据存入移位寄存器,高位优先 188 else 189 ad_data_reg_n = ad_data_reg; //否则保持不变 190 end 191 192 //时序电路,用来给 data_out 寄存器赋值 193 always @ (posedge CLK_50M or negedge RST_N) 194 begin 195 if(!RST_N) 196 data_out <= 8'h0; //判断复位 //初始化 data_out 值 197 else 198 data_out <= data_out_n; //用来给 data_out 赋值 199 end 200 201 //组合电路,将移位寄存器中的数据存入 data_out 中,可用于输出 202 always @ (*) 203 begin 204 if(ad_fsm_cs == FSM_END) //判断复位 205 data_out_n = ad_data_reg; //初始化 data_out 值 206 else 207 data_out_n = data_out; //用来给 data_out 赋值 208 end 209 210 endmodule 这个代码也是我们的《项目实战篇》中的代码,这个代码与我们的红外代码相比较来说简单 一些,由于这部分代码在我们的《项目实战篇》中有详细的讲解,这里我们就不在分析讲解了。 (4) 编写用于描述寄存器的 C 头文件和 IP 核的驱动 C 文件 写完了我们的硬件逻辑代码,我们就可以使用 IP 核编辑器来封装我们的硬件逻辑,完成 IP 核的定制了,对于后面的几个步骤,这里我们同前面一样也是一笔带过,我们这里主要介绍一下 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 401 AD IP 核的寄存器头文件和 AD IP 核的底层驱动文件,首先我们给出的是 AD IP 核的寄存器头 文件 zircon_avalon_tlc549_regs.h,该文件如代码 5.49 所示。 代码 5.49 zircon_avalon_tlc549_regs.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_tlc549_regs.h 3 //-- Describe : tlc549 IP core register header file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_TLC549_REGS_H__ 8 #define __ZIRCON_AVALON_TLC549_REGS_H__ 9 10 #include 11 12 //Data register 13 #define IORD_ZIRCON_AVALON_TLC549_DATA(base) IORD(base, 0) 14 15 #endif /* __ZIRCON_AVALON_TLC549_REGS_H__ */ 从该代码中我们可以看出,它是用来定义数据寄存器的宏定义,接下来我们给出的是底层驱 动头文件 zircon_avalon_tlc549.h,该文件如代码 5.50 所示。 代码 5.50 zircon_avalon_tlc549.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_tlc549.h 3 //-- Describe : tlc549 IP core driver header file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_TLC549_H__ 8 #define __ZIRCON_AVALON_TLC549_H__ 9 10 #include "system.h" 11 #include "alt_types.h" 12 #include "zircon_avalon_tlc549_regs.h" 13 14 #ifdef __cplusplus 15 extern "C" 16 { 17 #endif /* __cplusplus */ 18 19 //--------------------------------------------------------------------------- 20 //-- Name : zircon_avalon_tlc549_read() 21 //-- Function : Read the value of AD http://www.fpga.gs/ 402 软核演练篇 §5 22 //-- Input parameters : no 23 //-- Output parameters: no 24 //--------------------------------------------------------------------------25 void zircon_avalon_tlc549_read(); 26 27 /* Macros used by alt_sys_init */ 28 #define ZIRCON_AVALON_TLC549_INSTANCE(name, dev) alt_u32 tlc549_controller_addr = name##_BASE 29 #define ZIRCON_AVALON_TLC549_INIT(name, dev) while(0) 30 31 #ifdef __cplusplus 32 } 33 #endif /* __cplusplus */ 34 35 #endif /* __ZIRCON_AVALON_TLC549_H__ */ 我们可以看到该文件里面只有一个函数,这个函数就是用来读取我们的 AD 数值并转换电压 值的。最后我们给出的是底层驱动文件 zircon_avalon_tlc549.c,该文件如代码 5.51 所示。 代码 5.51 zircon_avalon_tlc549.c 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_tlc549.c 3 //-- Describe : tlc549 IP core driver C file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "unistd.h" 8 #include 9 #include "alt_types.h" 10 #include "zircon_avalon_tlc549_regs.h" 11 12 extern alt_u32 tlc549_controller_addr; 13 14 //--------------------------------------------------------------------------- 15 //-- Name : zircon_avalon_tlc549_read() 16 //-- Function : Read the value of AD 17 //-- Input parameters : no 18 //-- Output parameters: no 19 //--------------------------------------------------------------------------- 20 void zircon_avalon_tlc549_read() 21 { 22 int ad_data; 23 float ad_value; 24 25 ad_data = IORD_ZIRCON_AVALON_TLC549_DATA(tlc549_controller_addr); Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 403 26 27 printf("data = %d ,",ad_data); 28 29 ad_value = ad_data; 30 ad_value = ad_value / 255 * 5; 31 32 printf("Value = %2.1fV .",ad_value); 33 34 usleep(1000000); 35 36 } 从代码中我们可以看出,该函数用到了寄存器头文件中定义的读数据寄存器,我们将读到的 数据赋值给 ad_data,接着我们打印输出这个 ad_data,然后我们通过电压转换公式将 ad_data 的值转换成电压,然后我们在将电压值输出。 5.8.2 AD IP 核的应用 (1) 功能概述 完成了 AD IP 核的定制,接下来我们再来看看 AD IP 核的应用,首先我们讲解的是第一部 分功能概述,在该工程中,我们主要是使用 AD IP 核来测试我们的 AD 外设,并将接收到的模拟 信号转换成数字信号和电压值打印到我们的控制台中。 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该工程的硬件框架,如图 5.79 所示。 图 5.79 AD IP 核的硬件框架图 从该图中我们可以看出,除了我们常用几个基本 IP 核以外,我们这里只添加了一个 AD 的 IP 核,该 IP 核的使用是非常简单的,我们不需要任何配置,只需要将它添加至 Qsys 软件中就 可以正常使用了。 http://www.fpga.gs/ 404 软核演练篇 §5 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 5.52 所 示。 代码 5.52 Qsys_Tlc549_Ip.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_tlc549_Ip.c 3 //-- 描述 : 使用 tlc549 IP 核来测试我们的 AD 外设,并将读取的 AD 值并显示在控制台中 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include 8 #include "zircon_avalon_tlc549.h" 9 10 //--------------------------------------------------------------------------- 11 //-- 名称 : main() 12 //-- 功能 : 程序入口 13 //-- 输入参数 : 无 14 //-- 输出参数 : 无 15 //--------------------------------------------------------------------------- 16 int main() 17 { 18 printf("Welcome To Tlc549 Ip Demo Program... \n"); 19 20 while(1) 21 { 22 zircon_avalon_tlc549_read(); 23 } 24 25 return 0; 26 } (4) 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Tlc549_Ip.sof 下载至我们的 A4 开发板,Qsys_Tlc549_Ip.sof 下 载完成后,我们还需要在 Eclipse 软件中将 Qsys_Tlc549_Ip.elf 文件下载至我们的 A4 开发板, Qsys_Tlc549_Ip.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们 可以在 Eclipse 软件的控制台中看到我们的打印信息,如图 5.80 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 405 图 5.80 AD IP 核的控制台打印信息图 由于我们这里没有给 AD 输入电压值,所以我们可以看到 AD 输出的是 0,并且电压值也是 0,下面我们给 AD 接入 3.3V 电压,然后我们再来看下控制台,如图 5.81 所示。 图 5.81 AD IP 核的板级调试图 从该图中我们可以看出,我们的 AD 的值为 170,电压值为 3.3V,由此可以说明我们的程 序是正确,当然,我们还可以给 AD 接入其他的电压值,这里我们就不给大家一一进行展示了, 大家可以自行下载至 A4 开发板上逐一验证。至此,我们的 AD IP 核的应用就讲解完了。 §5.9 PS/2键盘外设 5.9.1 PS/2 键盘 IP 核的定制 (1) 规划 IP 核的硬件功能 完成了 AD IP 核的定制,接下来我们在为我们的 PS/2 键盘外设定制 IP 核,老规矩,定制 IP 核的第一步,就是为我们的 PS/2 键盘外设规划 IP 核的硬件功能,下面我们给出 PS/2 键盘 寄存器的规划方案,如表 5.27 所示。 偏移量 00 寄存器名称 数据寄存器 表 5.27 PS/2 键盘 IP 核寄存器映射 操作 31 … 10 位描述 9 8 读 保留 DATA3 DATA2 7…0 DATA1 从上面的表格中我们可以看到,PS/2 键盘 IP 核只有一个数据寄存器,不过这个数据寄存器 有着三个功能:  DATA1:用来传输读取到的 8 位键盘值。  DATA2:断码标志位,通过查看第二套键盘编码,我们可以知道所有的按键断码中都 含有 0xF0,因此我们便可以通过判断断码 0xF0 来确认,我们是否已经松开按键还是 http://www.fpga.gs/ 406 软核演练篇 §5 继续保持按下状态。如果有 0xF0,则表示按键已经松开,如果没有 0xF0,则表示按键 没有松开仍保持按下状态。  DATA3:SHIFT 键状态标志位,当 SHIFT 键按下时该标志位置 1;松开时标志位清零。 当然了,我们也可以通过三个偏移量来完成上述功能,方法不是统一固定的,只要大家理解 了,实现起来都是很容易的。 (2) 定义一个恰当的 Avalon 接口 规划完 PS/2 键盘 IP 核的硬件功能,接下来我们就来给 PS/2 键盘定义 Avalon 接口。我们 将信号总结如表 5.28 所示。 信号名 clock reset address read readdata irq ps2_clk ps2_data 表 5.28 PS/2 键盘 IP 核的接口信号 Avalon 信号类型 宽度 Clock 1 Reset 1 Avalon-MM 1 Avalon-MM 1 Avalon-MM 32 Interrupt 1 Conduit 1 Conduit 1 方向 Input Input Input Input Output Output Input Input 下面我们就来简单的介绍一下上面的接口信号:Clock 是时钟信号;Reset 是复位信号; address 是 Avalon 从端口的地址总线;read 是 Avalon 从端口的读请求;readdata 是 Avalon 从端口的写请求;irq 是 Avalon 中断信号;ps2_clk 和 ps2_data 是 PS/2 接口的时钟信号和数 据信号。 (3) 使用硬件描述语言描述硬件逻辑 定义完了 Avalon 接口,接下来我们就到了第三步使用硬件描述语言描述硬件逻辑,首先我 们来看下顶层文件 zircon_avalon_ps2_keyboard.v,该文件如代码 5.53 所示。 代码 5.53 zircon_avalon_ps2_keyboard.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_ps2_keyboard.v 3 //-- 描述 : PS/2 键盘 IP 核的顶层文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_ps2_keyboard 8( 9 //时钟复位 10 csi_clk,rsi_reset_n, 11 //Avalon-MM 从端口 12 avs_address,avs_read,avs_readdata,ins_interrupt, 13 //外设管脚输出 14 coe_ps2_clk,coe_ps2_data Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 407 15 ); 16 17 input 18 input csi_clk; rsi_reset_n; //系统时钟 //系统复位 19 input avs_address; //Avalon 地址总线 20 input avs_read; //Avalon 读请求信号 21 output [31:0] avs_readdata; //Avalon 读数据总线 22 output 23 input 24 input ins_interrupt; coe_ps2_clk; coe_ps2_data; //Avalon 中断信号 //ps/2 的时钟信号 //ps/2 的数据信号 25 26 wire continued_press; //持续按下按键标志位 27 wire shift_key_on; //shift 键状态标志位 28 wire 29 wire [ 7:0] ascii_output; read_address; //从 PS/2 中读出的 ASICC 数据 //Avalon 读请求和数据总线的使能标志位 30 31 //硬件逻辑文件 32 zircon_avalon_ps2_keyboard_logic zircon_avalon_ps2_keyboard_logic_init 33 ( 34 .clock (csi_clk ), //系统时钟 35 .reset (rsi_reset_n ), //系统复位 36 .rx_read (read_address ), //Avalon 读请求和数据总线的使能标志位 37 .continued_press (continued_press ), //持续按下按键标志位 38 .shift_key_on (shift_key_on ), //shift 键状态标志位 39 .ascii_output (ascii_output ), //从 PS/2 中读出的 ASICC 数据 40 .interrupt (ins_interrupt ), //Avalon 中断信号 41 .ps2_clk_in 42 .ps2_data_in (coe_ps2_clk (coe_ps2_data ), //ps/2 的时钟信号 ) //ps/2 的数据信号 43 44 ); 45 46 //寄存器文件 47 zircon_avalon_ps2_keyboard_register zircon_avalon_ps2_keyboard_register_init 48 ( 49 .csi_clk 50 .rsi_reset_n 51 .avs_address (csi_clk (rsi_reset_n (avs_address ), //系统时钟 ), //系统复位 ), //Avalon 地址总线 52 .avs_read (avs_read ), //Avalon 读请求信号 53 .avs_readdata (avs_readdata ), //Avalon 读数据总线 54 .read_address (read_address ), //Avalon 读请求和数据总线的使能标志位 55 .continued_press (continued_press ), //持续按下按键标志位 56 .ascii_output (ascii_output ), //从 PS/2 中读出的 ASICC 数据 57 .shift_key_on (shift_key_on ) //shift 键状态标志位 58 ); http://www.fpga.gs/ 408 软核演练篇 §5 59 60 endmodule 我们可以看到这个顶层模块文件就是用来连接我们寄存器文件和硬件逻辑文件的。它本身 没有实现任何的逻辑功能,看完了顶层文件,接下来我们再来看下寄存器文件 zircon_avalon_ps2_keyboard_register.v,该文件如代码 5.54 所示。 代码 5.54 zircon_avalon_ps2_keyboard_register.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_ps2_keyboard_register.v 3 //-- 描述 : PS/2 键盘 IP 核的寄存器文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_ps2_keyboard_register 8( 9 //时钟复位 10 csi_clk,rsi_reset_n, 11 //Avalon-MM 从端口 12 avs_address,avs_read,avs_readdata, 13 //用户逻辑输入与输出 14 shift_key_on,continued_press,ascii_output,read_address 15 ); 16 17 input 18 input 19 input 20 input 21 output reg 22 input 23 input 24 input 25 output [31:0] [ 7:0] csi_clk; rsi_reset_n; avs_address; avs_read; avs_readdata; ascii_output; shift_key_on; continued_press; read_address; //系统时钟 //系统复位 //Avalon 地址总线 //Avalon 读请求信号 //Avalon 读数据总线 //从 PS/2 中读出的 ASICC 数据 //shift 键状态标志位 //持续按下按键标志位 //Avalon 读请求和数据总线的使能标志位 26 27 reg [31:0] avs_readdata_n; //avs_readdata 的下一个状态 28 29 //时序电路,用来给数据寄存器赋值 30 always @ (posedge csi_clk or negedge rsi_reset_n) 31 begin 32 if(!rsi_reset_n) 33 avs_readdata <= 32'h00; //判断复位 //初始化数据寄存器 34 else 35 avs_readdata <= avs_readdata_n; //用来给数据寄存器赋值 36 end 37 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 409 38 //组合电路,用来给地址偏移量 0,也就是我们的数据寄存器读 10 位的数据 39 always @ (*) 40 begin 41 if((avs_read) && (avs_address == 1'b0)) //判断读请求和读地址 42 avs_readdata_n = {22'b0,shift_key_on,continued_press,ascii_output}; 43 else 44 avs_readdata_n = 32'h0; //否则,将保持不变 45 end 46 47 assign read_address = avs_read && (avs_address == 1'b0); 48 49 endmodule 从代码中我们可以看出,这里有三个变量分别写到了 Avalon 数据总线中,这三个变量功能 我们已经在硬件逻辑文件中实现,我们只需要在 Eclipse 工程中将该数据读取出来并判断即可。 看完了我们的寄存器文件,最后我们再来看下我们的硬件逻辑文件 zircon_avalon_ps2_keyboard_logic.v,该文件如代码 5.55 所示。 代码 5.55 zircon_avalon_ps2_keyboard_logic.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_ps2_keyboard_logic.v 3 //-- 描述 : PS/2 键盘 IP 核的硬件逻辑文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_ps2_keyboard_logic 8 ( 9 //时钟复位 10 clock,reset, 11 //外设管脚输出 12 ps2_clk_in,ps2_data_in, 13 //用户逻辑输入与输出 14 continued_press,shift_key_on,ascii_output,interrupt,rx_read 15 ); 16 17 input 18 input 19 input 20 input 21 output 22 output 23 output [7:0] 24 output 25 input clock; reset; ps2_clk_in; ps2_data_in; continued_press; shift_key_on; ascii_output; interrupt; rx_read; //系统时钟 //系统复位 //PS/2 时钟线,输入口 //PS/2 数据线,输入口 //持续按下按键标志位 //shift 键状态标志 //从 PS/2 中读出的 ASICC 数据 //avalon 中断信号 //Avalon 读请求和数据总线的使能标志位 http://www.fpga.gs/ 410 软核演练篇 §5 26 27 reg 28 reg 29 reg [7:0] continued_press; continued_press_n; ascii_output; //持续按下按键标志位 //continued_press 的下一个状态 //从 PS/2 中读出的 ASICC 数据 30 reg [7:0] ascii_output_n; //ascii_out 的下一个状态 31 reg interrupt; //avalon 中断信号 32 reg interrupt_n; //interrupt 的下一个状态 33 reg 34 reg 35 reg 36 reg [ 1:0] [ 1:0] sync_ps2_clk; sync_ps2_data; fsm_cs; fsm_ns; //同步 PS/2 时钟信号 //同步 PS/2 数据信号 //状态机的当前状态 //状态机的下一个状态 37 reg [ 3:0] bit_count; //移位计数器 38 reg [ 3:0] bit_count_n; //bit_count 的下一个状态 39 reg 40 reg 41 reg 42 reg [14:0] [14:0] time_cnt_400us; time_cnt_400us_n; time_cnt_400us_done; time_cnt_400us_done_n; //400us 计数器 //time_cnt_400us 的下一个状态 //400us 计数器完成标识位 //time_cnt_400us_done 的下一个状态 43 reg [10:0] read_data; //移位寄存器,用于接收 ps2 数据 44 reg [10:0] read_data_n; //read_data 的下一个状态 45 reg hold_released; //保持原先的值 46 reg 47 reg 48 reg 49 reg hold_released_n; left_shift_key; left_shift_key_n; right_shift_key; //hold_released 的下一个状态 //左 SHIFT 键标志 //left_shift_key 的下一个状态 //右 SHIFT 键标志 50 reg right_shift_key_n; //right_shift_key 的下一个状态 51 reg [ 6:0] ascii; //ASCII 码 52 53 wire 54 wire 55 wire read_data_done; //接收一帧数据完成标识位 output_strobe; //收到键盘发送过来的一帧数据置位,且 0xF0 和 SHIFT 键值 released; //断码标志 56 wire [ 8:0] shift_key_plus_code; //包含 shift 键状态的扫描码 57 58 //状态机的状态参数表 59 parameter 60 61 62 FSM_CLK_LOW = 2'd0, FSM_CLK_HIGH = 2'd1, FSM_FALLING_EDGE = 2'd2, FSM_RISING_EDGE = 2'd3; //读时钟低电平 //读时钟高电平 //读时钟下降沿标志 //读时钟上升沿标志 63 64 //同步 PS/2 的输入时钟 65 always @ (posedge clock or negedge reset) 66 begin 67 if(!reset) 68 sync_ps2_clk <= 1'b0; 69 else Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 70 sync_ps2_clk <= ps2_clk_in; 71 end 72 73 //同步 PS/2 的数据信号 74 always @ (posedge clock or negedge reset) 75 begin 76 if(!reset) 77 sync_ps2_data <= 1'b0; 78 else 79 sync_ps2_data <= ps2_data_in; 80 end 81 82 //时序电路,用来给 fsm_cs 赋值的 83 always @ (posedge clock or negedge reset) 84 begin 85 if(!reset) 86 fsm_cs <= FSM_CLK_HIGH; 87 else 88 fsm_cs <= fsm_ns; 89 end 90 91 //组合电路,从 PS/2 键盘读数据到主机的状态机 92 always @ (*) 93 begin 94 case (fsm_cs) 95 FSM_CLK_LOW: //读状态时钟低电平 96 begin 97 if(sync_ps2_clk) 98 fsm_ns = FSM_RISING_EDGE; 99 else 100 fsm_ns = FSM_CLK_LOW; 101 end 102 103 FSM_CLK_HIGH: //读状态时钟高电平 104 begin 105 if(!sync_ps2_clk) 106 fsm_ns = FSM_FALLING_EDGE; 107 else 108 fsm_ns = FSM_CLK_HIGH; 109 end 110 111 FSM_FALLING_EDGE: //读状态时钟下降沿标志 112 begin 113 fsm_ns = FSM_CLK_LOW; http://www.fpga.gs/ 411 412 软核演练篇 §5 114 end 115 116 FSM_RISING_EDGE: //读状态时钟上升沿标志 117 begin 118 fsm_ns = FSM_CLK_HIGH; 119 end 120 121 default: fsm_ns = FSM_CLK_HIGH; 122 endcase 123 end 124 125 //时序电路,用来给 time_cnt_400us 赋值的 126 always @ (posedge clock or negedge reset) 127 begin 128 if(!reset) 129 time_cnt_400us <= 1'b0; 130 else 131 time_cnt_400us <= time_cnt_400us_n; 132 end 133 134 //组合电路,400us 定时器计数器 135 always @ (*) 136 begin 137 if(!((fsm_cs == FSM_CLK_HIGH) || (fsm_cs == FSM_CLK_LOW))) 138 time_cnt_400us_n = 1'b0; 139 else if(!time_cnt_400us_done) 140 time_cnt_400us_n = time_cnt_400us + 1; 141 else 142 time_cnt_400us_n = time_cnt_400us; 143 end 144 145 //时序电路,用来给 time_cnt_400us_done 赋值的 146 always @ (posedge clock or negedge reset) 147 begin 148 if(!reset) 149 time_cnt_400us_done <= 1'b0; 150 else 151 time_cnt_400us_done <= time_cnt_400us_done_n; 152 end 153 154 //组合电路,400us 计时完成标识位 155 always @ (*) 156 begin 157 if(time_cnt_400us == 15'd19199) Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 413 158 time_cnt_400us_done_n = 1'b1; 159 else 160 time_cnt_400us_done_n = 1'b0; 161 end 162 163 //时序电路,给 bit_count 赋值 164 always @ (posedge clock or negedge reset) 165 begin 166 if(!reset) 167 bit_count <= 4'd0; 168 else 169 bit_count <= bit_count_n; 170 end 171 172 //组合电路,移位计数器 173 always @ (*) 174 begin 175 if(read_data_done) 176 bit_count_n = 4'd0; 177 else if(time_cnt_400us_done && (fsm_cs == FSM_CLK_HIGH) && (sync_ps2_clk)) 178 bit_count_n = 4'd0; 179 else if(fsm_cs == FSM_FALLING_EDGE) 180 bit_count_n = bit_count + 4'd1; 181 else 182 bit_count_n = bit_count; 183 end 184 185 //时序电路,用来给 read_data 赋值的 186 always @ (posedge clock or negedge reset) 187 begin 188 if(!reset) 189 read_data <= 11'd0; 190 else 191 read_data <= read_data_n; 192 end 193 194 //组合电路,串行数据移位寄存器,用于接收 ps2 的值 195 always @ (*) 196 begin 197 if(fsm_cs == FSM_FALLING_EDGE) 198 read_data_n = {sync_ps2_data,read_data[10:1]}; 199 else 200 read_data_n = read_data; 201 end http://www.fpga.gs/ 414 软核演练篇 §5 202 203 //接收一帧数据完成标识位 204 assign read_data_done = (bit_count == 4'd11); 205 206 //输出断码标志 207 assign released = (read_data[8:1] == 16'hF0) && read_data_done; 208 209 //时序电路,用来给 hold_released 赋值的 210 always @ (posedge clock or negedge reset) 211 begin 212 if(!reset) 213 hold_released <= 1'b0; 214 else 215 hold_released <= hold_released_n; 216 end 217 218 //组合电路,输出键码为断码时置相应标志位 219 always @(*) 220 begin 221 if(read_data_done && (!released)) 222 hold_released_n = 1'b0; 223 else if(read_data_done && released) 224 hold_released_n = 1'b1; 225 end 226 227 //时序电路,用来给 left_shift_key 赋值的 228 always @ (posedge clock or negedge reset) 229 begin 230 if(!reset) 231 left_shift_key <= 1'b0; 232 else 233 left_shift_key <= left_shift_key_n; 234 end 235 236 //组合电路,shift 按键检测,左 shift 键 237 always @ (*) 238 begin 239 if((read_data[8:1] == 16'h12) && read_data_done && ~hold_released) 240 left_shift_key_n = 1'b1; 241 else if ((read_data[8:1] == 16'h12) && read_data_done && hold_released) 242 left_shift_key_n = 1'b0; 243 else 244 left_shift_key_n = left_shift_key; 245 end Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 415 246 247 //时序电路,用来给 right_shift_key 赋值的 248 always @ (posedge clock or negedge reset) 249 begin 250 if(!reset) 251 right_shift_key <= 1'b0; 252 else 253 right_shift_key <= right_shift_key_n; 254 end 255 256 //组合电路,shift 按键检测,右 shift 键 257 always @ (*) 258 begin 259 if((read_data[8:1] == 16'h59) && read_data_done && ~hold_released) 260 right_shift_key_n = 1'b1; 261 else if((read_data[8:1] == 16'h59) && read_data_done && hold_released) 262 right_shift_key_n = 1'b0; 263 else 264 right_shift_key_n = right_shift_key; 265 end 266 267 //输出 shift 状态标志,输出 1:shift 键有按住,输出 0:shift 键无按住 268 assign shift_key_on = left_shift_key || right_shift_key; 269 270 //收到键盘发送过来的键码,不是 0xF0(断码前都有 0xF0)不是 shift 的键值 271 assign output_strobe = (read_data_done && !(released) 272 && ( ( (read_data[8:1] != 16'h59) 273 && (read_data[8:1] != 16'h12 ) ) )); 274 275 //时序电路,用来给 interrupt 赋值的 276 always @ (posedge clock or negedge reset) 277 begin 278 if(!reset) 279 interrupt <= 1'b0; 280 else 281 interrupt <= interrupt_n; 282 end 283 284 //产生中断信号 interrupt = 1 时有数据输出,interrupt_n = 0 时无数据输出 285 //组合电路,读取数据时(rx_read = 1) interrupt = 0. 286 always @ (*) 287 begin 288 if(rx_read) 289 interrupt_n = 1'b0; http://www.fpga.gs/ 416 软核演练篇 §5 290 else if(output_strobe) 291 interrupt_n = 1'b1; 292 else 293 interrupt_n = interrupt; 294 end 295 296 //时序电路,用来给 continued_press 赋值的 297 always @ (posedge clock or negedge reset) 298 begin 299 if(!reset) 300 continued_press <= 1'b0; 301 else 302 continued_press <= continued_press_n; 303 end 304 305 //组合电路,输出持续按下按键标志位 306 always @ (*) 307 begin 308 if(output_strobe) 309 continued_press_n = hold_released; 310 else 311 continued_press_n = continued_press; 312 end 313 314 //时序电路,用来给 ascii_output 赋值的 315 always @ (posedge clock or negedge reset) 316 begin 317 if(!reset) 318 ascii_output <= 1'b0; 319 else 320 ascii_output <= ascii_output_n; 321 end 322 323 //组合电路,输出 ASICC 数据 324 always @ (*) 325 begin 326 if(output_strobe) 327 ascii_output_n = {1'b0,ascii}; //载入之前保存的状态值 328 else 329 ascii_output_n = ascii_output; 330 end 331 332 //这部分将键盘的扫描码转找成 ASCII 码,这里只列了一部分,如果还要 333 //增加更多的键码,可以找到相关的扫描键码增加到下面 CASE 语句中。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 334 assign shift_key_plus_code = {shift_key_on,read_data[8:1]}; 335 336 //表中 9'hXXX 最高位为 1 表示有 SHIFT 键按下 337 always @(shift_key_plus_code) 338 begin 339 casez (shift_key_plus_code) 340 9'h?66 : ascii <= 7'h08; // 删除键 "backspace"key 341 9'h?0d : ascii <= 7'h09; // Tab 键 342 9'h?5a : ascii <= 7'h0d; // 回车键 "enter"key 343 9'h?76 : ascii <= 7'h1b; // Escape "esc"key 344 9'h?29 : ascii <= 7'h20; // 空格键"Space"key 345 9'h116 : ascii <= 7'h21; // ! 346 9'h152 : ascii <= 7'h22; // " 347 9'h126 : ascii <= 7'h23; // # 348 9'h125 : ascii <= 7'h24; // $ 349 9'h12e : ascii <= 7'h25; // % 350 9'h13d : ascii <= 7'h26; // & 351 9'h052 : ascii <= 7'h27; // ' 352 9'h146 : ascii <= 7'h28; // ( 353 9'h145 : ascii <= 7'h29; // ) 354 9'h13e : ascii <= 7'h2a; // * 355 9'h155 : ascii <= 7'h2b; // + 356 9'h041 : ascii <= 7'h2c; // , 357 9'h04e : ascii <= 7'h2d; // - 358 9'h049 : ascii <= 7'h2e; // . 359 9'h04a : ascii <= 7'h2f; // / 360 9'h045 : ascii <= 7'h30; // 0 361 9'h016 : ascii <= 7'h31; // 1 362 9'h01e : ascii <= 7'h32; // 2 363 9'h026 : ascii <= 7'h33; // 3 364 9'h025 : ascii <= 7'h34; // 4 365 9'h02e : ascii <= 7'h35; // 5 366 9'h036 : ascii <= 7'h36; // 6 367 9'h03d : ascii <= 7'h37; // 7 368 9'h03e : ascii <= 7'h38; // 8 369 9'h046 : ascii <= 7'h39; // 9 370 9'h14c : ascii <= 7'h3a; // : 371 9'h04c : ascii <= 7'h3b; // ; 372 9'h141 : ascii <= 7'h3c; // < 373 9'h055 : ascii <= 7'h3d; // = 374 9'h149 : ascii <= 7'h3e; // > 375 9'h14a : ascii <= 7'h3f; // ? 376 9'h11e : ascii <= 7'h40; // @ 377 9'h11c : ascii <= 7'h41; // A http://www.fpga.gs/ 417 418 软核演练篇 §5 378 9'h132 : ascii <= 7'h42; // B 379 9'h121 : ascii <= 7'h43; // C 380 9'h123 : ascii <= 7'h44; // D 381 9'h124 : ascii <= 7'h45; // E 382 9'h12b : ascii <= 7'h46; // F 383 9'h134 : ascii <= 7'h47; // G 384 9'h133 : ascii <= 7'h48; // H 385 9'h143 : ascii <= 7'h49; // I 386 9'h13b : ascii <= 7'h4a; // J 387 9'h142 : ascii <= 7'h4b; // K 388 9'h14b : ascii <= 7'h4c; // L 389 9'h13a : ascii <= 7'h4d; // M 390 9'h131 : ascii <= 7'h4e; // N 391 9'h144 : ascii <= 7'h4f; // O 392 9'h14d : ascii <= 7'h50; // P 393 9'h115 : ascii <= 7'h51; // Q 394 9'h12d : ascii <= 7'h52; // R 395 9'h11b : ascii <= 7'h53; // S 396 9'h12c : ascii <= 7'h54; // T 397 9'h13c : ascii <= 7'h55; // U 398 9'h12a : ascii <= 7'h56; // V 399 9'h11d : ascii <= 7'h57; // W 400 9'h122 : ascii <= 7'h58; // X 401 9'h135 : ascii <= 7'h59; // Y 402 9'h11a : ascii <= 7'h5a; // Z 403 9'h054 : ascii <= 7'h5b; // [ 404 9'h05d : ascii <= 7'h5c; // '\' 405 9'h05b : ascii <= 7'h5d; // ] 406 9'h136 : ascii <= 7'h5e; // ^ 407 9'h14e : ascii <= 7'h5f; // _ 408 9'h00e : ascii <= 7'h60; // ` 409 9'h01c : ascii <= 7'h61; // a 410 9'h032 : ascii <= 7'h62; // b 411 9'h021 : ascii <= 7'h63; // c 412 9'h023 : ascii <= 7'h64; // d 413 9'h024 : ascii <= 7'h65; // e 414 9'h02b : ascii <= 7'h66; // f 415 9'h034 : ascii <= 7'h67; // g 416 9'h033 : ascii <= 7'h68; // h 417 9'h043 : ascii <= 7'h69; // i 418 9'h03b : ascii <= 7'h6a; // j 419 9'h042 : ascii <= 7'h6b; // k 420 9'h04b : ascii <= 7'h6c; // l 421 9'h03a : ascii <= 7'h6d; // m Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 419 422 9'h031 : ascii <= 7'h6e; // n 423 9'h044 : ascii <= 7'h6f; // o 424 9'h04d : ascii <= 7'h70; // p 425 9'h015 : ascii <= 7'h71; // q 426 9'h02d : ascii <= 7'h72; // r 427 9'h01b : ascii <= 7'h73; // s 428 9'h02c : ascii <= 7'h74; // t 429 9'h03c : ascii <= 7'h75; // u 430 9'h02a : ascii <= 7'h76; // v 431 9'h01d : ascii <= 7'h77; // w 432 9'h022 : ascii <= 7'h78; // x 433 9'h035 : ascii <= 7'h79; // y 434 9'h01a : ascii <= 7'h7a; // z 435 9'h154 : ascii <= 7'h7b; // { 436 9'h15d : ascii <= 7'h7c; // | 437 9'h15b : ascii <= 7'h7d; // } 438 9'h10e : ascii <= 7'h7e; // ~ 439 9'h?71 : ascii <= 7'h7f; // Delete 键或小键盘的 DEL 键 440 default: ascii <= 7'h2e; // '.' 代表其它没列出的字符 441 endcase 442 end 443 444 endmodule 从该代码中我们可以看出,这个代码应该是我们目前见过的最多行的代码了,在学习该代码 前,我们建议大家先搞懂 PS/2 的时序,如果大家对 PS/2 时序或者是对键盘编码不太了解,那 么可以参考我们的《项目实战篇》,我们《项目实战篇》中给出了详细的讲解。这个代码看起来 复杂,其实很简单,我们的代码主要是根据状态机来判断 PS/2 时钟管脚的上升沿还是下降沿, 当下降沿到来时,我们便将接收 PS/2 数据管脚上的值放入到移位寄存器中,然后我们在根据接 收到的值来判断按键有没有松开,有没有按下 shift 等状态。只要你理解了 PS/2 的时序,学习起 该程序来是很轻松的,因为该程序就是围绕着 PS/2 时序来编写的。 (4) 编写用于描述寄存器的 C 头文件和 IP 核的驱动 C 文件 写完了我们的硬件逻辑代码,我们就可以使用 IP 核编辑器来封装我们的硬件逻辑,完成 IP 核的定制了,对于后面的几个步骤,这里我们同前面一样也是一笔带过,我们这里主要介绍一下 PS/2 键盘 IP 核的寄存器头文件和 PS/2 键盘 IP 核的底层驱动文件,首先我们给出的是 PS/2 键 盘 IP 核的寄存器头文件 zircon_avalon_ps2_keyboard_regs.h,该文件如代码 5.56 所示。 代码 5.56 zircon_avalon_ps2_keyboard_regs.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_ps2_keyboard_regs.h 3 //-- Describe : ps2_keyboard IP core register header file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. http://www.fpga.gs/ 420 软核演练篇 §5 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_PS2_KEYBOARD_REGS_H__ 8 #define __ZIRCON_AVALON_PS2_KEYBOARD_REGS_H__ 9 10 #include 11 12 //Data register 13 #define IORD_ZIRCON_AVALON_PS2_KEYBOARD_DATA(base) IORD(base, 0) 14 15 #endif /* __ZIRCON_AVALON_PS2_KEYBOARD_REGS_H__ */ 从代码中我们可以看出,代码非常简单,并且只有一句代码,就是用来读取我们的数据寄存 器 中 的 值 。 接 下 来 我 们 再 来 看 看 PS/2 键 盘 IP 核 的 底 层 驱 动 头 文 件 zircon_avalon_ps2_keyboard.h,该文件如代码 5.57 所示。 代码 5.57 zircon_avalon_ps2_keyboard.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_ps2_keyboard.h 3 //-- Describe : ps2_keyboard IP core driver header file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_PS2_KEYBOARD_H__ 8 #define __ZIRCON_AVALON_PS2_KEYBOARD_H__ 9 10 #include "zircon_avalon_ps2_keyboard_regs.h" 11 #include "alt_types.h" 12 #include "system.h" 13 14 #ifdef __cplusplus 15 extern "C" 16 { 17 #endif /* __cplusplus */ 18 19 //--------------------------------------------------------------------------- 20 //-- Name : ReadKeyboardData() 21 //-- Function : Read Keyboard Data 22 //-- Input parameters : no 23 //-- Output parameters: Keyboard Data 24 //--------------------------------------------------------------------------- 25 alt_u32 ReadKeyboardData(void); 26 27 //--------------------------------------------------------------------------- 28 //-- Name : KeyboardIRQ() 29 //-- Function : Interrupt service routine Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 421 30 //-- Input parameters : no 31 //-- Output parameters: no 32 //--------------------------------------------------------------------------- 33 void KeyboardIRQ(void* context, alt_u32 id); 34 35 //--------------------------------------------------------------------------- 36 //-- Name : KeyboardInit() 37 //-- Function : Keyboard Interrupt Init 38 //-- Input parameters : no 39 //-- Output parameters: no 40 //--------------------------------------------------------------------------- 41 void KeyboardInit(void); 42 43 //--------------------------------------------------------------------------- 44 //-- Name : KeyboarDemo() 45 //-- Function : Keyboard program demo 46 //-- Input parameters : no 47 //-- Output parameters: no 48 //--------------------------------------------------------------------------- 49 void KeyboardDemo(void); 50 51 /* 52 * Macros used by alt_sys_init() 53 */ 54 #define ZIRCON_AVALON_PS2_KEYBOARD_INSTANCE(name, dev) alt_u32 ps2keyboard_addr = name##_BASE 55 #define ZIRCON_AVALON_PS2_KEYBOARD_INIT(name, dev) while (0) 56 57 #ifdef __cplusplus 58 } 59 #endif /* __cplusplus */ 60 61 #endif /* __ZIRCON_AVALON_PS2_KEYBOARD_H__ */ 从该代码中可以看出,我们实现了 4 个函数,下面我们就来进一步分析看下这 4 个函数究 竟实现了什么功能,下面我们给出底层驱动文件 zircon_avalon_ps2_keyboard.c,该文件如代 码 5.58 所示。 代码 5.58 zircon_avalon_ps2_keyboard.c 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_ps2_keyboard.c 3 //-- Describe : ps2_keyboard IP core driver C file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- http://www.fpga.gs/ 422 软核演练篇 §5 7 #include "zircon_avalon_ps2_keyboard.h" 8 #include "alt_types.h" 9 #include "sys/alt_irq.h" 10 #include "system.h" 11 #include 12 13 extern alt_u32 ps2keyboard_addr; 14 15 alt_u32 KeyboardData = 0; //Read from the PS2 key 16 alt_u32 keydone = 0; //notify the external interrupt event occurs 17 18 19 //--------------------------------------------------------------------------- 20 //-- Name : ReadKeyboardData() 21 //-- Function : Read Keyboard Data 22 //-- Input parameters : no 23 //-- Output parameters: Keyboard Data 24 //--------------------------------------------------------------------------- 25 alt_u32 ReadKeyboardData(void) 26 { 27 return(IORD_ZIRCON_AVALON_PS2_KEYBOARD_DATA(ps2keyboard_addr)); 28 } 29 30 //--------------------------------------------------------------------------- 31 //-- Name : KeyboardIRQ() 32 //-- Function : Interrupt service routine 33 //-- Input parameters : no 34 //-- Output parameters: no 35 //--------------------------------------------------------------------------- 36 void KeyboardIRQ(void* context, alt_u32 id) 37 { 38 39 KeyboardData = ReadKeyboardData(); 40 keydone++; 41 } 42 43 //--------------------------------------------------------------------------- 44 //-- Name : KeyboardInit() 45 //-- Function : Keyboard Interrupt Init 46 //-- Input parameters : no 47 //-- Output parameters: no 48 //--------------------------------------------------------------------------- 49 void KeyboardInit(void) 50 { Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 423 51 52 alt_irq_register(ZIRCON_AVALON_PS2_KEYBOARD_IRQ, NULL, KeyboardIRQ); 53 } 54 55 //--------------------------------------------------------------------------- 56 //-- Name : KeyboarDemo() 57 //-- Function : Keyboard program demo 58 //-- Input parameters : no 59 //-- Output parameters: no 60 //--------------------------------------------------------------------------- 61 void KeyboardDemo(void) 62 { 63 alt_u8 keydata; 64 65 while(1) 66 { 67 if(keydone != 0) 68 { 69 keydone--; 70 keydata = KeyboardData & 0x000000ff; 71 72 if((KeyboardData & 0x100)!=0x100) 73 { 74 if( ((keydata >= 0x20) && (keydata < 0x7f)) && (((KeyboardData & 0x200)!=0x200) || (keydata != 0x2e)) ) 75 { 76 printf("%c," ,keydata); 77 } 78 79 } 80 } 81 } 82 } 下面我们就来简单的讲解一下该代码,代码第 19 至 28 行是我们第一个函数,它主要是通 过我们定义的数据寄存器来读取按键的值。第 30 至 41 行是我们第二个函数,它是中断服务子 程序。第 43 至 53 行是我们第三个函数,它是键盘中断初始化。第 55 至 82 行是我们第四个函 数,它主要用来输出我们键盘按下的值,当然这个输入的值是有范围限制的,在该程序中我们还 进行了一个断码标识位和 shift 标识位的判断。这里我们需要注意的是,我们只对 IP 核的基地址 进行了声明,但是没有对中断号进行声明,我们可以在第三个中断初始化函数中看到,我们这里 使用的是 ZIRCON_AVALON_PS2_KEYBOARD_IRQ,如果我们修改了 Qsys 中的命名,这里 也需要更改,不然将会找不到对应的中断号。 http://www.fpga.gs/ 424 软核演练篇 §5 5.9.2 PS/2 键盘 IP 核的应用 (1) 功能概述 完成了 PS/2 键盘 IP 核的定制,接下来我们再来看看 PS/2 键盘 IP 核的应用,首先我们讲 解的是第一部分功能概述,在该工程中,我们主要是使用 PS/2 键盘 IP 核来测试我们的 PS/2 外 设,并将接收到的键盘值打印到我们的控制台中。 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该工程的硬件框架,如图 5.82 所示。 图 5.82 PS/2 键盘 IP 核的硬件框架图 从该图中我们可以看出,除了我们常用几个基本 IP 核以外,我们这里只添加了一个 PS/2 键 盘的 IP 核,该 IP 核的使用是非常简单的,我们不需要任何配置,只需要将它添加至 Qsys 软件 中就可以正常使用了。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 5.59 所 示。 代码 5.59 Qsys_Ps2_Keyboard_Ip.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Ps2_Keyboard_Ip.c 3 //-- 描述 : 使用 Ps2_Keyboard IP 核来测试我们的 ps/2 外设,并将键盘的值打印到控制台 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "stdio.h" 8 #include "zircon_avalon_ps2_keyboard.h" 9 10 //--------------------------------------------------------------------------- 11 //-- 名称 : main() 12 //-- 功能 : 程序入口 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 425 13 //-- 输入参数 : 无 14 //-- 输出参数 : 无 15 //--------------------------------------------------------------------------- 16 int main(void) 17 { 18 KeyboardInit(); //注册键盘中断服务子程序 19 20 printf("Welcome to Ps/2 Keyboard ip demo program... \n"); 21 printf("You can enter the following characters: \n"); 22 printf("Numb (0-9) and character (a - z)... \n"); 23 printf("Shift + (0 - 9) and Shift + (a - z)... \n"); 24 printf("Of course, you can also press and hold to send a character in a row... \n"); 25 KeyboardDemo(); 26 27 return 0; 28 } (4) 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软 件 中 将 Qsys_Ps2_Keyboard_Ip.sof 下 载 至 我 们 的 A4 开 发 板 , Qsys_Ps2_Keyboard_Ip.sof 下 载 完 成 后 , 我 们 还 需 要 在 Eclipse 软 件 中 将 Qsys_Ps2_Keyboard_Ip.elf 文件下载至我们的 A4 开发板,Qsys_Ps2_Keyboard_Ip.elf 下载 完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可以在 Eclipse 软件的控 制台中看到我们的打印信息,如图 5.83 所示。 图 5.83 PS/2 键盘 IP 核的控制台打印信息图 这时,我们使用 PS/2 键盘输入 A、4、g、s,我们可以看到控制台打印出 A,4,g,s,如图 5.84 所示。 http://www.fpga.gs/ 426 软核演练篇 §5 图 5.84 PS/2 键盘 IP 核的板级调试图 当然,我们还可以输入其他的字母、数字和字符,这里我们就不给大家一一进行展示了,大 家可以自行下载至 A4 开发板上逐一验证。至此,我们的 PS/2 键盘 IP 核的应用就讲解完了。 §5.10 PS/2鼠标外设 5.10.1 PS/2 鼠标 IP 核的定制 (1) 规划 IP 核的硬件功能 完成了 PS/2 键盘核的定制,接下来我们在为我们的 PS/2 鼠标外设定制 IP 核,老规矩,定 制 IP 核的第一步,就是为我们的 PS/2 鼠标外设规划 IP 核的硬件功能,下面我们给出 PS/2 鼠 标寄存器的规划方案,如表 5.29 所示。 偏移量 寄存器名 表 5.29 PS/2 鼠标 IP 核寄存器映射 操作 … 20 位描述 19 18 17 … 9 8 … 0 00 数据寄存器 读 保留 DATA5 DATA4 DATA3 DATA2 DATA1 从上面的表格中我们可以看到,PS/2 鼠标 IP 核只有一个数据寄存器,下面我们就来简单的 介绍一下这个数据寄存器  DATA1:X 轴增量,为 9 位二进制补码,最高位为符位,鼠标左移,位移增量为负; 右移,位移增量为正。  DATA2:Y 轴增量,为 9 位二进制补码,最高位为符位。鼠标上移,位移增量为负; 下移,位移增量为正。  DATA3:高电平表示鼠标中键按下,低电平表示没按下。  DATA4:高电平表示鼠标右键按下,低电平表示没按下。  DATA5:高电平表示鼠标左键按下,低电平表示没按下。 对于滚轮滑动功能,以及有四、五按键的鼠标功能键,我们这里就不考虑了。 (2) 定义一个恰当的 Avalon 接口 规划完 PS/2 鼠标 IP 核的硬件功能,接下来我们就来给 PS/2 鼠标定义 Avalon 接口。我们 将信号总结如表 5.30 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 427 信号名 clock reset address read readdata irq ps2_clk ps2_data 表 5.30 PS/2 鼠标 IP 核的接口信号 Avalon 信号类型 宽度 Clock 1 Reset 1 Avalon-MM 1 Avalon-MM 1 Avalon-MM 32 Interrupt 1 Conduit 1 Conduit 1 方向 Input Input Input Input Output Output bidir bidir 从该表中我们可以看出,PS/2 鼠标 IP 核和 PS/2 键盘 IP 核的 Avalon 接口是一样的,它们 唯一不同的是 PS/2 的管脚类型,我们前面 PS/2 键盘由于没有用到写功能,所以我们将它声明 成了输入端口,在 PS/2 鼠标 IP 核中,我们既用到了写也用到了读,因此,我们需要将 PS/2 管 脚声明成双向三态端口类型(bidir)。 (3) 使用硬件描述语言描述硬件逻辑 定义完了 Avalon 接口,接下来我们就到了第三步使用硬件描述语言描述硬件逻辑,首先我 们来看下顶层文件 zircon_avalon_ps2_mouse.v,该文件如代码 5.60 所示。 代码 5.60 zircon_avalon_ps2_mouse.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_ps2_mouse.v 3 //-- 描述 : PS/2 鼠标 IP 核的顶层文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_ps2_mouse 8( 9 //时钟复位 10 csi_clk,rsi_reset_n, 11 //Avalon-MM 从端口 12 avs_address,avs_read,avs_readdata,ins_interrupt, 13 //外设管脚输出 14 coe_ps2_clk,coe_ps2_data 15 ); 16 17 input 18 input 19 input 20 input 21 output 22 output 23 inout 24 inout [31:0] csi_clk; rsi_reset_n; avs_address; avs_read; avs_readdata; ins_interrupt; coe_ps2_clk; coe_ps2_data; //系统时钟 //系统复位 //Avalon 地址总线 //Avalon 读请求信号 //Avalon 读数据总线 //Avalon 中断信号 //PS/2 的时钟信号 //PS/2 的数据信号 http://www.fpga.gs/ 428 软核演练篇 §5 25 26 wire 27 wire 28 wire 29 wire 30 wire [ 8:0] [ 8:0] left_button; right_button; middle_button; x_increment; y_increment; //鼠标左键标志位 //鼠标右键标志位 //鼠标中键标志位 //X 轴增量 //Y 轴增量 31 32 wire 33 wire 34 wire 35 wire 36 wire 37 wire ps2_clk_in; ps2_clk_out; ps2_clk_dir; ps2_data_in; ps2_data_out; ps2_data_dir; //PS/2 时钟线,输入口 //PS/2 时钟线,输出口 //PS/2 时钟方向控制,高电平为输出,低电平为输入 //PS/2 数据线,输入口 //PS/2 数据线,输出口 //PS/2 数据方向控制,高电平为输出,低电平为输入 38 39 40 zircon_avalon_ps2_mouse_logic zircon_avalon_ps2_mouse_logic_init 41 ( 42 .clock 43 .reset_n 44 .avs_read 45 .ins_interrupt 46 .x_increment 47 .y_increment 48 .left_button 49 .right_button 50 .middle_button 51 .ps2_clk_in 52 .ps2_clk_out 53 .ps2_clk_dir 54 .ps2_data_in 55 .ps2_data_out 56 .ps2_data_dir (csi_clk (rsi_reset_n (avs_read (ins_interrupt (x_increment (y_increment (left_button (right_button (middle_button (ps2_clk_in (ps2_clk_out (ps2_clk_dir (ps2_data_in (ps2_data_out (ps2_data_dir ), //系统时钟 ), //系统复位 ), //Avalon 读请求信号 ), //Avalon 中断信号 ), //X 轴增量 ), //Y 轴增量 ), //鼠标左键标志位 ), //鼠标右键标志位 ), //鼠标中键标志位 ), //PS/2 时钟线,输入口 ), //PS/2 时钟线,输出口 ), //PS/2 时钟方向控制,高电平输出,低电平输入 ), //PS/2 数据线,输入口 ), //PS/2 数据线,输出口 ) //PS/2 数据方向控制,高电平输出,低电平输入 57 ); 58 59 60 zircon_avalon_ps2_mouse_register zircon_avalon_ps2_mouse_register_init 61 ( 62 .csi_clk 63 .rsi_reset_n 64 .avs_address 65 .avs_read 66 .avs_readdata 67 .x_increment 68 .y_increment (csi_clk (rsi_reset_n (avs_address (avs_read (avs_readdata (x_increment (y_increment ), //系统时钟 ), //系统复位 ), //Avalon 地址总线 ), //Avalon 读请求信号 ), //Avalon 读数据总线 ), //X 轴增量 ), //Y 轴增量 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 429 69 .left_button (left_button ), //鼠标左键标志位 70 .right_button 71 .middle_button (right_button (middle_button ), //鼠标右键标志位 ) //鼠标中键标志位 72 ); 73 74 assign coe_ps2_clk = (ps2_clk_dir) ? ps2_clk_out : 1'bz; 75 assign coe_ps2_data = (ps2_data_dir) ? ps2_data_out : 1'bz; 76 assign ps2_clk_in = coe_ps2_clk; 77 assign ps2_data_in = coe_ps2_data; 78 79 endmodule 我们可以看到这个顶层模块文件就是用来连接我们寄存器文件和硬件逻辑文件的。这里需 要我们注意的是,代码第 74 行是一种典型的 inout 端口使用方法,当 inout 端口作为输入口使 用时,一定要把它置为高阻态,让 ps2_clk_dir 为 0 即可。当 inout 端口作为输出口使用时,则 需要让 ps2_clk_dir 为 1,对 ps2_clk_out 赋值就可以了。第 75 行同理。看完了顶层文件,接下 来我们再来看下寄存器文件 zircon_avalon_ps2_mouse_register.v,该文件如代码 5.61 所示。 代码 5.61 zircon_avalon_ps2_mouse_register.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_ps2_mouse_register.v 3 //-- 描述 : PS/2 鼠标 IP 核的寄存器文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_ps2_mouse_register 8( 9 //时钟复位 10 csi_clk,rsi_reset_n, 11 //Avalon-MM 从端口 12 avs_address,avs_read,avs_readdata, 13 //用户逻辑输入与输出 14 left_button,right_button,middle_button,x_increment,y_increment 15 ); 16 17 input 18 input 19 input 20 input 21 output reg 22 input 23 input 24 input 25 input [31:0] [ 8:0] csi_clk; rsi_reset_n; avs_address; avs_read; avs_readdata; left_button; right_button; middle_button; x_increment; //系统时钟 //系统复位 //Avalon 地址总线 //Avalon 读请求信号 //Avalon 读数据总线 //鼠标左键标志位 //鼠标右键标志位 //鼠标中键标志位 //X 轴增量 http://www.fpga.gs/ 430 软核演练篇 §5 26 input [ 8:0] y_increment; //Y 轴增量 27 28 reg [31:0] avs_readdata_n; //avs_address 的下一个状态 29 30 //时序电路,用来给数据寄存器赋值 31 always @ (posedge csi_clk or negedge rsi_reset_n) 32 begin 33 if(!rsi_reset_n) 34 avs_readdata <= 32'h00; //判断复位 //初始化数据寄存器 35 else 36 avs_readdata <= avs_readdata_n; //用来给数据寄存器赋值 37 end 38 39 //组合电路,用来给地址偏移量 0,也就是我们的数据寄存器读 21 位的数据 40 always @ (*) 41 begin 42 if((avs_read) && (avs_address == 1'b0)) //判断读请求和读地址 43 avs_readdata_n = {11'b0,left_button,right_button,middle_button, y_increment,x_increment}; //如果条件成立,将数据赋值给 avs_readdata_n 44 else 45 avs_readdata_n = 32'h0; //否则,将保持不变 46 end 47 48 endmodule 从代码中我们可以看出,这里有五个变量分别写到了 Avalon 数据总线中,这五个变量就是 对应着我们之前声明的左键、右键、中键、X 轴,以及 Y 轴,这五个变量功能我们已经在硬件逻 辑文件中实现,我们只需要在 Eclipse 工程中将该数据读取出来并判断即可。看完了我们的寄存 器文件,最后我们再来看下我们的硬件逻辑文件 zircon_avalon_ps2_mouse_logic.v,该文件如 代码 5.62 所示。 代码 5.62 zircon_avalon_ps2_mouse_logic.v 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : zircon_avalon_ps2_mouse_logic.v 3 //-- 描述 : PS/2 鼠标 IP 核的硬件逻辑文件 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 module zircon_avalon_ps2_mouse_logic 8 ( 9 //时钟复位 10 clock,reset_n, 11 //Avalon-MM 从端口 12 avs_read,ins_interrupt, Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 431 13 //外设管脚输出 14 ps2_clk_in,ps2_data_in,ps2_clk_out,ps2_data_out,ps2_clk_dir,ps2_data_dir, 15 //用户逻辑输入与输出 16 left_button,right_button,middle_button,x_increment,y_increment 17 18 ); 19 20 input 21 input 22 input 23 output reg clock; reset_n; avs_read; ins_interrupt; //系统时钟 //系统复位 //Avalon 读请求信号 //Avalon 中断信号 24 reg ins_interrupt_n; //ins_interrupt 的下一个状态 25 input ps2_clk_in; //PS/2 时钟线,输入口 26 input 27 output reg 28 reg 29 output reg ps2_data_in; ps2_clk_out; ps2_clk_out_n; ps2_data_out; //PS/2 数据线,输入口 //PS/2 时钟线,输出口 //ps2_clk_out 的下一个状态 //PS/2 数据线,输出口 30 reg ps2_data_out_n; //ps2_data_out 的下一个状态 31 output reg ps2_clk_dir; //PS/2 时钟方向控制,高电平为输出,低电平为输入 32 reg ps2_clk_dir_n; //ps2_clk_dir 的下一个状态 33 output reg 34 reg 35 output reg 36 reg ps2_data_dir; //PS/2 数据方向控制,高电平为输出,低电平为输入 ps2_data_dir_n; //ps2_data_dir 的下一个状态 left_button; //鼠标左键标志位 left_button_n; //left_button 的下一个状态 37 output reg right_button; //鼠标右键标志位 38 reg right_button_n; //right_button 的下一个状态 39 output reg middle_button; 40 reg middle_button_n; 41 output reg [ 8:0] x_increment; 42 reg [ 8:0] x_increment_n; //鼠标中键标志位 //middle_button 的下一个状态 //X 轴增量 //x_increment 的下一个状态 43 output reg [ 8:0] y_increment; //Y 轴增量 44 reg [ 8:0] y_increment_n; //y_increment 的下一个状态 45 46 reg 47 reg 48 reg 49 reg [32:0] [32:0] [ 2:0] [ 2:0] read_data; read_data_n; fsm_cs1; fsm_ns1; //移位寄存器 //read_data 的下一个状态 //状态机 1 的当前状态 //状态机 1 的下一个状态 50 reg [ 3:0] fsm_cs2; //状态机 2 的当前状态 51 reg [ 3:0] fsm_ns2; //状态机 2 的下一个状态 52 reg 53 reg 54 reg 55 reg [ 5:0] [ 5:0] [14:0] [14:0] bit_count; //发送接收数据是的位计数器 bit_count_n; //bit_conut 的下一个状态 watchdog_time_cnt; //看门狗计时器 watchdog_time_cnt_n; //watchdog_time_cnt 的下一个状态 56 reg [ 7:0] time_cnt_5us; //5us 计时器 http://www.fpga.gs/ 432 软核演练篇 §5 57 reg [ 7:0] time_cnt_5us_n; //time_cnt_5us 的下一个状态 58 reg 59 reg 60 reg sync_ps2_clk; sync_ps2_data; sync_clk; //同步后的 PS2 时钟 //同步后的 PS2 数据 //滤波后的 PS2 时钟 61 reg sync_clk_n; //sync_clk 的下一个状态 62 reg rising_edge; //PS2 时钟上升沿标志 63 reg rising_edge_n; //rising_edge 的下一个状态 64 reg 65 reg 66 reg 67 reg falling_edge; falling_edge_n; data_ready; data_ready_n; //PS2 时钟下降沿标志 //falling_edge 的下一个状态 //数据已经正确收到 //data_ready 的下一个状态 68 69 wire watchdog_time_cnt_done; //看门狗状态指示 70 wire 71 wire time_cnt_5us_done; packet_good; //用于延时缓冲 //数据包正确 72 73 //状态机 1 的参数表 74 parameter FSM_CLK_HIGH = 3'b000, //时钟高电平 75 FSM_FALLING_EDGE = 3'b001, //时钟下降沿标志 76 FSM_FALLING_WAIT = 3'b011, //时钟下降沿等待 77 FSM_CLK_LOW = 3'b010, //时钟低电平 78 FSM_RISING_EDGE = 3'b110, //时钟上升沿标志 79 FSM_RISING_WAIT = 3'b100; //时钟上升沿等待 80 81 //状态机 2 的参数表 82 parameter FSM_RESET = 4'b0000, //复位后,发送命令到 PS2 鼠标 83 FSM_WAIT = 4'b0001, //等待状态 84 FSM_GATHER = 4'b0011, //判断数据是否收完 85 FSM_VERIFY = 4'b0010, //检验数据包是否正确 86 FSM_USE = 4'b0110, //数据包正确 87 FSM_HOLD_CLK_LOW = 4'b0111, //拉低时钟线 400US,准备发送命令 88 FSM_DATA_LOW_1 = 4'b0101, //发送起始位"0",d[0],d[1] 89 FSM_DATA_HIGH_1 = 4'b0100, //发送位 d[2] 90 FSM_DATA_LOW_2 = 4'b1100, //发送位 d[3] 91 FSM_DATA_HIGH_2 = 4'b1101, //发送位 d[4],d[5],d[6],d[7] 92 FSM_DATA_LOW_3 = 4'b1001, //发送奇偶校验位 93 FSM_DATA_HIGH_3 = 4'b1011, //停止位"1",应答处理 94 FSM_AWAIT_RESPONSE= 4'b1010; //等待回复状态 95 96 //同步 PS/2 的输入时钟 97 always @ (posedge clock or negedge reset_n) 98 begin 99 if(!reset_n) 100 sync_ps2_clk <= 1'b0; Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 101 else 102 sync_ps2_clk <= ps2_clk_in; 103 end 104 105 //同步 PS/2 的数据信号 106 always @ (posedge clock or negedge reset_n) 107 begin 108 if(!reset_n) 109 sync_ps2_data <= 1'b0; 110 else 111 sync_ps2_data <= ps2_data_in; 112 end 113 114 //时序电路,用来给 fsm_cs1 赋值的 115 always @ (posedge clock or negedge reset_n) 116 begin 117 if(!reset_n == 1'b1) 118 fsm_cs1 <= FSM_CLK_HIGH; 119 else 120 fsm_cs1 <= fsm_ns1; 121 end 122 123 //组合电路,同步 PS2 时钟,在 PS2 的时钟边沿产生边沿标志 124 always @ (*) 125 begin 126 case(fsm_cs1) 127 128 FSM_CLK_HIGH: //时钟高电平 129 begin 130 131 if(~sync_ps2_clk) 132 fsm_ns1 <= FSM_FALLING_EDGE; 133 else 134 fsm_ns1 <= FSM_CLK_HIGH; 135 end 136 137 FSM_FALLING_EDGE: //时钟下降沿 138 begin 139 fsm_ns1 <= FSM_FALLING_WAIT; 140 end 141 142 FSM_FALLING_WAIT: //等待 5US 延时防止毛刺干扰 143 begin 144 if(time_cnt_5us_done) http://www.fpga.gs/ 433 434 软核演练篇 §5 145 fsm_ns1 <= FSM_CLK_LOW; 146 else 147 fsm_ns1 <= FSM_FALLING_WAIT; 148 end 149 150 FSM_CLK_LOW: //时钟低电平 151 begin 152 if(sync_ps2_clk) 153 fsm_ns1 <= FSM_RISING_EDGE; 154 else 155 fsm_ns1 <= FSM_CLK_LOW; 156 end 157 158 FSM_RISING_EDGE: //时钟上升沿 159 begin 160 fsm_ns1 <= FSM_RISING_WAIT; 161 end 162 163 FSM_RISING_WAIT: //等待 5US 延时防止毛刺干扰 164 begin 165 if(time_cnt_5us_done) 166 fsm_ns1 <= FSM_CLK_HIGH; 167 else 168 fsm_ns1 <= FSM_RISING_WAIT; 169 end 170 171 default : fsm_ns1 <= FSM_CLK_HIGH; 172 endcase 173 end 174 175 //时序电路,用来给 falling_edge 赋值的 176 always @ (posedge clock or negedge reset_n) 177 begin 178 if(!reset_n) 179 falling_edge <= 1'b0; 180 else 181 falling_edge <= falling_edge_n; 182 end 183 184 //组合电路,产生下降沿 185 always @ (*) 186 begin 187 if(fsm_cs1 == FSM_FALLING_EDGE) 188 falling_edge_n = 1'b1; Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 189 else 190 falling_edge_n = 1'b0; 191 end 192 193 //时序电路,用来给 rising_edge 赋值的 194 always @ (posedge clock or negedge reset_n) 195 begin 196 if(!reset_n) 197 rising_edge <= 1'b0; 198 else 199 rising_edge <= rising_edge_n; 200 end 201 202 //组合电路,产生上升沿 203 always @ (*) 204 begin 205 if(fsm_cs1 == FSM_RISING_EDGE) 206 rising_edge_n = 1'b1; 207 else 208 rising_edge_n = 1'b0; 209 end 210 211 //时序电路,用来给 sync_clk 赋值的 212 always @ (posedge clock or negedge reset_n) 213 begin 214 if(!reset_n) 215 sync_clk <= 1'b0; 216 else 217 sync_clk <= sync_clk_n; 218 end 219 220 //组合电路,经过状态机同步后的时钟 221 always @ (*) 222 begin 223 if((fsm_cs1 == FSM_CLK_HIGH) || (fsm_cs1 == FSM_RISING_WAIT)) 224 sync_clk_n = 1'b1; 225 else 226 sync_clk_n = 1'b0; 227 end 228 229 //时序电路,用来给 fsm_cs2 赋值的 230 always @ (posedge clock or negedge reset_n) 231 begin 232 if(!reset_n == 1'b1) http://www.fpga.gs/ 435 436 软核演练篇 §5 233 fsm_cs2 <= FSM_RESET; 234 else 235 fsm_cs2 <= fsm_ns2; 236 end 237 238 //组合电路,初始化 PS2 鼠标,接收 PS2 鼠标数据包 239 always @ (*) 240 begin 241 case(fsm_cs2) 242 243 FSM_RESET: //复位后,发送命令到 PS2 鼠标 244 begin 245 fsm_ns2 = FSM_HOLD_CLK_LOW; 246 end 247 248 FSM_WAIT: 249 begin 250 if(falling_edge) 251 fsm_ns2 = FSM_GATHER; 252 else 253 fsm_ns2 = FSM_WAIT; 254 end 255 256 FSM_GATHER: //判断数据是否收完 257 begin 258 if(watchdog_time_cnt_done && (bit_count == 6'd33)) 259 fsm_ns2 = FSM_VERIFY; 260 else if(watchdog_time_cnt_done && (bit_count < 6'd33)) 261 fsm_ns2 = FSM_HOLD_CLK_LOW; 262 else 263 fsm_ns2 = FSM_GATHER; 264 end 265 266 FSM_VERIFY: //检验数据包是否正确 267 begin 268 if(packet_good) 269 fsm_ns2 = FSM_USE; 270 else 271 fsm_ns2 = FSM_WAIT; 272 end 273 274 FSM_USE: //数据包正确 275 begin 276 fsm_ns2 = FSM_WAIT; Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 277 end 278 279 //复位或出错时进入以下状态,主机向 PS2 发送复位指令 0xFF,PS2 鼠标收到 280 //复位指令时应答 0xFA,大约 360mS 后,鼠标完成自检,自检成功发送 0xAA, 281 //不成功发送 0xFC;然后发送 ID 号 0x00.之后进入 Stream 模式 Stream 模式. 282 //此时 PS2 鼠标默认属性为:采样速率 100 采样点/秒;分辨率 4 个计数值/毫米; 283 //缩放比例 1:1;数据报告被禁止,为简化程序,不修改默认属性。 284 //最后发送 0xF4 命令使能数据报告,等 PS2 鼠标应答 0xFA.致此,初使化 PS2 鼠标完毕. 285 //为简化程序,发送复位指令部分省略(复位由鼠标上电复位完成),只发送使能数据报告指令。 286 287 FSM_HOLD_CLK_LOW: //拉低时钟线 400US,准备发送命令 288 begin 289 if(watchdog_time_cnt_done && ~sync_clk) 290 fsm_ns2 = FSM_DATA_LOW_1; 291 else 292 fsm_ns2 = FSM_HOLD_CLK_LOW; 293 end 294 295 FSM_DATA_LOW_1: //发送起始位"0",d[0],d[1] 296 begin 297 if(falling_edge && (bit_count == 6'd2)) 298 fsm_ns2 = FSM_DATA_HIGH_1; 299 else 300 fsm_ns2 = FSM_DATA_LOW_1; 301 end 302 303 FSM_DATA_HIGH_1: //发送位 d[2] 304 begin 305 if(falling_edge) 306 fsm_ns2 = FSM_DATA_LOW_2; 307 else 308 fsm_ns2 = FSM_DATA_HIGH_1; 309 end 310 311 FSM_DATA_LOW_2: //发送位 d[3] 312 begin 313 if(falling_edge) 314 fsm_ns2 = FSM_DATA_HIGH_2; 315 else 316 fsm_ns2 = FSM_DATA_LOW_2; 317 end 318 319 FSM_DATA_HIGH_2: //发送位 d[4],d[5],d[6],d[7] 320 begin http://www.fpga.gs/ 437 438 软核演练篇 §5 321 if(falling_edge && (bit_count == 6'd8)) 322 fsm_ns2 = FSM_DATA_LOW_3; 323 else 324 fsm_ns2 = FSM_DATA_HIGH_2; 325 end 326 327 FSM_DATA_LOW_3: //发送奇偶校验位 328 begin 329 if(falling_edge) 330 fsm_ns2 = FSM_DATA_HIGH_3; 331 else 332 fsm_ns2 = FSM_DATA_LOW_3; 333 end 334 335 FSM_DATA_HIGH_3: //停止位"1",应答处理 336 begin 337 if(falling_edge && sync_ps2_data) 338 fsm_ns2 = FSM_HOLD_CLK_LOW; //有错误产生,重新复位 339 else if(falling_edge && ~sync_ps2_data) 340 fsm_ns2 = FSM_AWAIT_RESPONSE; 341 else 342 fsm_ns2 = FSM_DATA_HIGH_3; 343 end 344 345 //等待 SP2 鼠标回应:这里不对回应做处理,如果指令没有被鼠标正确收到, 346 //鼠标会回应(0xFC)要求重新发送指令,也可能回应(0xFE)表示收到的数 347 //据出错。如果正确收到会回应(0xFA)。 348 //注:如果鼠标的回应时间超过 400us,bit_count 将被复位,这时应收到 349 //的位数应该是 11。但一般鼠标的回应时间较短,所以 bit_count 没被复, 350 //这时的位数值应该是 22。 351 352 FSM_AWAIT_RESPONSE : //等待回复并完成操作 353 begin 354 if(bit_count == 6'd22) 355 fsm_ns2 = FSM_VERIFY; 356 else 357 fsm_ns2 = FSM_AWAIT_RESPONSE; 358 end 359 360 default: fsm_ns2 = FSM_WAIT; 361 endcase 362 end 363 364 //时序电路,用来给 data_ready 赋值的 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 365 always @ (posedge clock or negedge reset_n) 366 begin 367 if(!reset_n) 368 data_ready <= 1'b0; 369 else 370 data_ready <= data_ready_n; 371 end 372 373 //组合电路,判断数据是否已经正确收到 374 always @ (*) 375 begin 376 if(fsm_cs2 == FSM_USE) 377 data_ready_n = 1'b1; 378 else 379 data_ready_n = 1'b0; 380 end 381 382 //时序电路,用来给 ps2_data_dir 赋值的 383 always @ (posedge clock or negedge reset_n) 384 begin 385 if(!reset_n) 386 ps2_data_dir <= 1'b0; 387 else 388 ps2_data_dir <= ps2_data_dir_n; 389 end 390 391 //组合电路,PS/2 数据方向控制,高电平为输出,低电平为输入 392 always @ (*) 393 begin 394 if(fsm_cs2 == FSM_DATA_LOW_1 || fsm_cs2 == FSM_DATA_HIGH_1 || 395 fsm_cs2 == FSM_DATA_LOW_2 || fsm_cs2 == FSM_DATA_HIGH_2 || 396 fsm_cs2 == FSM_DATA_LOW_3 ) 397 ps2_data_dir_n = 1'b1; 398 else 399 ps2_data_dir_n = 1'b0; 400 end 401 402 //时序电路,用来给 ps2_clk_dir 赋值的 403 always @ (posedge clock or negedge reset_n) 404 begin 405 if(!reset_n) 406 ps2_clk_dir <= 1'b0; 407 else 408 ps2_clk_dir <= ps2_clk_dir_n; http://www.fpga.gs/ 439 440 软核演练篇 §5 409 end 410 411 //组合电路,PS/2 时钟方向控制,高电平为输出,低电平为输入 412 always @ (*) 413 begin 414 if(fsm_cs2 == FSM_HOLD_CLK_LOW) 415 ps2_clk_dir_n = 1'b1; 416 else 417 ps2_clk_dir_n = 1'b0; 418 end 419 420 //时序电路,用来给 ps2_clk_out 赋值的 421 always @ (posedge clock or negedge reset_n) 422 begin 423 if(!reset_n) 424 ps2_clk_out <= 1'b1; 425 else 426 ps2_clk_out <= ps2_clk_out_n; 427 end 428 429 //组合电路,生成 PS/2 时钟信号,并输出 430 always @ (*) 431 begin 432 if(fsm_cs2 == FSM_HOLD_CLK_LOW) 433 ps2_clk_out_n = 1'b0; 434 else 435 ps2_clk_out_n = 1'b1; 436 end 437 438 //时序电路,用来给 ps2_data_out 赋值的 439 always @ (posedge clock or negedge reset_n) 440 begin 441 if(!reset_n) 442 ps2_data_out <= 1'b1; 443 else 444 ps2_data_out <= ps2_data_out_n; 445 end 446 447 //组合电路,生成 PS/2 数据信号,并输出 448 always @ (*) 449 begin 450 if(fsm_cs2 == FSM_DATA_LOW_1 || fsm_cs2 == FSM_DATA_LOW_2 || fsm_cs2 == FSM_DATA_LOW_3) 451 ps2_data_out_n = 1'b0; Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 452 else 453 ps2_data_out_n = 1'b1; 454 end 455 456 //时序电路,用来给 bit_count 赋值的 457 always @ (posedge clock or negedge reset_n) 458 begin 459 if(!reset_n) 460 bit_count <= 1'b0; 461 else 462 bit_count <= bit_count_n; 463 end 464 465 //组合电路,移位计数器,用在 PS2 时钟的下降沿加计数 466 always @ (*) 467 begin 468 if(falling_edge) 469 bit_count_n = bit_count + 6'd1; 470 else if(watchdog_time_cnt_done) 471 bit_count_n = 6'd0; 472 else 473 bit_count_n = bit_count; 474 end 475 476 //时序电路,用来给 read_data 赋值的 477 always @ (posedge clock or negedge reset_n) 478 begin 479 if(!reset_n) 480 read_data <= 1'b0; 481 else 482 read_data <= read_data_n; 483 end 484 485 486 //组合电路,移位寄存器,接收 PS2 发送到来的数据,在时钟的下降沿锁存数据, 487 always @ (*) 488 begin 489 if(falling_edge) 490 read_data_n = {sync_ps2_data,read_data[32:1]}; 491 else 492 read_data_n = read_data; 493 end 494 495 //时序电路,用来给 watchdog_time_cnt 赋值的 http://www.fpga.gs/ 441 442 软核演练篇 §5 496 always @ (posedge clock or negedge reset_n) 497 begin 498 if(!reset_n) 499 watchdog_time_cnt <= 1'b0; 500 else 501 watchdog_time_cnt <= watchdog_time_cnt_n; 502 end 503 504 //组合电路,看门狗计时器,发送数据时抑制时钟线标志,接收数据包后状态指示。 505 always @ (*) 506 begin 507 if(rising_edge || falling_edge) 508 watchdog_time_cnt_n = 1'b0; 509 else if(!watchdog_time_cnt_done) 510 watchdog_time_cnt_n = watchdog_time_cnt + 1; 511 else 512 watchdog_time_cnt_n = watchdog_time_cnt; 513 end 514 515 //组合电路,PS/2 无时钟脉冲超过 400us,watchdog_time_cnt_done 置位 516 assign watchdog_time_cnt_done = (watchdog_time_cnt == 15'd19199); 517 518 //时序电路,用来给 time_cnt_5us 赋值的 519 always @ (posedge clock or negedge reset_n) 520 begin 521 if(!reset_n) 522 time_cnt_5us <= 1'b0; 523 else 524 time_cnt_5us <= time_cnt_5us_n; 525 end 526 527 //组合电路,缓冲时间 5US 计数器 528 always @ (*) 529 begin 530 if(falling_edge || rising_edge ) 531 time_cnt_5us_n = 0; 532 else 533 time_cnt_5us_n = time_cnt_5us + 1; 534 end 535 536 //组合电路,超过 5us,time_cnt_5us_done 置位 537 assign time_cnt_5us_done = (time_cnt_5us == 8'd239); 538 539 //验证收到的数据包数据是否有效、正确 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 443 540 assign packet_good = ((read_data[0] == 1'b0) && (read_data[10] == 1'b1) && 541 (read_data[11] == 1'b0) && (read_data[21] == 1'b1) && 542 (read_data[22] == 1'b0) && (read_data[32] == 1'b1) && 543 (read_data[9] == ~^read_data[8:1]) && 544 (read_data[20] == ~^read_data[19:12]) && 545 (read_data[31] == ~^read_data[30:23]) ); 546 547 //时序电路,用来给 left_button 赋值的 548 always @ (posedge clock or negedge reset_n) 549 begin 550 if(!reset_n) 551 left_button <= 1'b0; 552 else 553 left_button <= left_button_n; 554 end 555 556 //组合电路,读取鼠标左键标志位数据 557 always @ (*) 558 begin 559 if(data_ready) 560 left_button_n = read_data[1]; 561 else 562 left_button_n = left_button; 563 end 564 565 //时序电路,用来给 right_button 赋值的 566 always @ (posedge clock or negedge reset_n) 567 begin 568 if(!reset_n) 569 right_button <= 1'b0; 570 else 571 right_button <= right_button_n; 572 end 573 574 //组合电路,读取鼠标右键标志位数据 575 always @ (*) 576 begin 577 if(data_ready) 578 right_button_n = read_data[2]; 579 else 580 right_button_n = right_button; 581 end 582 583 //时序电路,用来给 middle_button 赋值的 http://www.fpga.gs/ 444 软核演练篇 §5 584 always @ (posedge clock or negedge reset_n) 585 begin 586 if(!reset_n) 587 middle_button <= 1'b0; 588 else 589 middle_button <= middle_button_n; 590 end 591 592 //组合电路,读取鼠标中键标志位数据 593 always @ (*) 594 begin 595 if(data_ready) 596 middle_button_n = read_data[3]; 597 else 598 middle_button_n = middle_button; 599 end 600 601 //时序电路,用来给 x_increment 赋值的 602 always @ (posedge clock or negedge reset_n) 603 begin 604 if(!reset_n) 605 x_increment <= 1'b0; 606 else 607 x_increment <= x_increment_n; 608 end 609 610 //组合电路,读取 X 轴增量数据 611 always @ (*) 612 begin 613 if(data_ready) 614 x_increment_n = {read_data[5],read_data[19:12]}; 615 else 616 x_increment_n = x_increment; 617 end 618 619 //时序电路,用来给 y_increment 赋值的 620 always @ (posedge clock or negedge reset_n) 621 begin 622 if(!reset_n) 623 y_increment <= 1'b0; 624 else 625 y_increment <= y_increment_n; 626 end 627 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 445 628 //组合电路,读取 Y 轴增量数据 629 always @ (*) 630 begin 631 if(data_ready) 632 y_increment_n = {read_data[6],read_data[30:23]}; 633 else 634 y_increment_n = y_increment; 635 end 636 637 //时序电路,用来给 ins_interrupt 赋值的 638 always @ (posedge clock or negedge reset_n) 639 begin 640 if(!reset_n) 641 ins_interrupt <= 1'b0; 642 else 643 ins_interrupt <= ins_interrupt_n; 644 end 645 646 //组合电路,生成中断信号 647 always @ (*) 648 begin 649 if(avs_read) 650 ins_interrupt_n = 1'b0; 651 else if(data_ready) 652 ins_interrupt_n = 1'b1; 653 else 654 ins_interrupt_n = ins_interrupt; 655 end 656 657 endmodule 从该代码我们可以看出,该代码和我们的键盘代码一样,也是很多,在键盘代码中,我们只 用到了外设发送数据到主机的 PS/2 时序图,在鼠标代码中,我们不仅用到了外设发送数据到主 机的 PS/2 时序图,我们还用到了主机发送数据到外设的 PS/2 时序图。在学习该代码前,我们 同样建议大家先搞懂 PS/2 的时序,如果大家对 PS/2 时序或者是对鼠标编码不太了解,那么可 以参考我们的《项目实战篇》,我们《项目实战篇》中给出了详细的讲解。下面我们就来简单的 介绍一下该代码,该代码前半部分,也就是第一个状态机,它和我们的键盘代码很类似,都是用 来判断上升沿和下降沿,当下降沿到来时,我们便接收 PS/2 数据管脚上的值放入到移位寄存器 中。该代码后半部分,也就是第二个状态机,该状态机它有两个功能,第一个功能是用来初始化 我们的鼠标,第二个功能就是用来检验接收的数据是否正确。虽然代码比较多,但是代码功能很 简单,我们只要搞懂了 PS/2 的时序,就很容易能够看懂该代码了。 (4) 编写用于描述寄存器的 C 头文件和 IP 核的驱动 C 文件 写完了我们的硬件逻辑代码,我们就可以使用 IP 核编辑器来封装我们的硬件逻辑,完成 IP http://www.fpga.gs/ 446 软核演练篇 §5 核的定制了,对于后面的几个步骤,这里我们同前面一样也是一笔带过,我们这里主要介绍一下 PS/2 鼠标 IP 核的寄存器头文件和 PS/2 鼠标 IP 核的底层驱动文件,首先我们给出的是 PS/2 鼠 标 IP 核的寄存器头文件 zircon_avalon_ps2_mouse_regs.h,该文件如代码 5.63 所示。 代码 5.63 zircon_avalon_ps2_mouse_regs.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_ps2_mouse_regs.h 3 //-- Describe : ps2_mouse IP core register header file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_PS2_MOUSE_REGS_H__ 8 #define __ZIRCON_AVALON_PS2_MOUSE_REGS_H__ 9 10 #include 11 12 //Data Register( 13 #define IORD_ZIRCON_AVALON_PS2_MOUSE_DATA(base) IORD(base, 0) 14 15 16 #endif /* __ZIRCON_AVALON_PS2_MOUSE_REGS_H__ */ 从代码中我们可以看出,代码非常简单,并且只有一句代码,就是用来读取我们的数据寄存 器中的值。接下来我们再来看看 PS/2 鼠标 IP 核的底层驱动头文件 zircon_avalon_ps2_mouse.h, 该文件如代码 5.64 所示。 代码 5.64 zircon_avalon_ps2_mouse.h 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_ps2_mouse.h 3 //-- Describe : ps2_mouse IP core driver header file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #ifndef __ZIRCON_AVALON_PS2_MOUSE_H__ 8 #define __ZIRCON_AVALON_PS2_MOUSE_H__ 9 10 #include "zircon_avalon_ps2_mouse_regs.h" 11 #include "alt_types.h" 12 #include "system.h" 13 14 #ifdef __cplusplus 15 extern "C" 16 { 17 #endif /* __cplusplus */ 18 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 447 19 //--------------------------------------------------------------------------- 20 //-- Name : ReadMouseData() 21 //-- Function : Read Mouse Data 22 //-- Input parameters : no 23 //-- Output parameters: Mouse Data 24 //--------------------------------------------------------------------------- 25 alt_u32 ReadMouseData(void); 26 27 //--------------------------------------------------------------------------- 28 //-- Name : MouseInit() 29 //-- Function : Interrupt service routine 30 //-- Input parameters : no 31 //-- Output parameters: no 32 //--------------------------------------------------------------------------- 33 void MouseIRQ(void* context, alt_u32 id); 34 35 //--------------------------------------------------------------------------- 36 //-- Name : MouseInit() 37 //-- Function : Mouse Interrupt Init 38 //-- Input parameters : no 39 //-- Output parameters: no 40 //--------------------------------------------------------------------------- 41 void MouseInit(void); 42 43 //--------------------------------------------------------------------------- 44 //-- Name : MouseDemo() 45 //-- Function : Mouse program demo 46 //-- Input parameters : no 47 //-- Output parameters: no 48 //--------------------------------------------------------------------------- 49 void MouseDemo(void); 50 51 /* 52 * Macros used by alt_sys_init() 53 */ 54 55 #define ZIRCON_AVALON_PS2_MOUSE_INSTANCE(name, dev) \ 56 alt_u32 ps2mouse_addr = name##_BASE 57 58 #define ZIRCON_AVALON_PS2_MOUSE_INIT(name, dev) while(0) 59 #ifdef __cplusplus 60 } 61 #endif /* __cplusplus */ 62 http://www.fpga.gs/ 448 软核演练篇 §5 63 #endif /* __ZIRCON_AVALON_PS2_MOUSE_H__ */ 从该代码中可以看出,我们实现了 4 个函数,下面我们就来进一步分析看下这 4 个函数究 竟实现了什么功能,下面我们给出底层驱动文件 zircon_avalon_ps2_mouse.c,该文件如代码 5.65 所示。 代码 5.65 zircon_avalon_ps2_mouse.c 代码 1 //--------------------------------------------------------------------------- 2 //-- Name : zircon_avalon_ps2_mouse.c 3 //-- Describe : ps2_mouse IP core driver C file 4 //-- Revision : 2014-1-1 5 //-- Company : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "zircon_avalon_ps2_mouse.h" 8 #include "alt_types.h" 9 #include "sys/alt_irq.h" 10 #include "system.h" 11 #include 12 13 extern alt_u32 ps2mouse_addr; 14 15 alt_u32 MouseDone; //notify the external interrupt event occurs 16 alt_u32 MouseData; //Read from the PS2 key 17 18 alt_16 x = 0; 19 alt_16 y = 0; 20 21 struct { 22 signed x:9, 23 y:9; 24 }inc={0,0}; 25 26 //--------------------------------------------------------------------------- 27 //-- Name : ReadMouseData() 28 //-- Function : Read Mouse Data 29 //-- Input parameters : no 30 //-- Output parameters: Mouse Data 31 //--------------------------------------------------------------------------- 32 alt_u32 ReadMouseData(void) 33 { 34 return(IORD_ZIRCON_AVALON_PS2_MOUSE_DATA(ps2mouse_addr)); 35 } 36 37 //--------------------------------------------------------------------------- 38 //-- Name : MouseInit() Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 449 39 //-- Function : Interrupt service routine 40 //-- Input parameters : no 41 //-- Output parameters: no 42 //--------------------------------------------------------------------------- 43 void MouseIRQ(void* context, alt_u32 id) 44 { 45 MouseData = ReadMouseData(); 46 MouseDone++; 47 } 48 49 //--------------------------------------------------------------------------- 50 //-- Name : MouseInit() 51 //-- Function : Mouse Interrupt Init 52 //-- Input parameters : no 53 //-- Output parameters: no 54 //--------------------------------------------------------------------------- 55 void MouseInit(void) 56 { 57 alt_irq_register(ZIRCON_AVALON_PS2_MOUSE_IRQ, NULL, MouseIRQ); 58 } 59 60 //--------------------------------------------------------------------------- 61 //-- Name : MouseDemo() 62 //-- Function : Mouse program demo 63 //-- Input parameters : no 64 //-- Output parameters: no 65 //--------------------------------------------------------------------------- 66 void MouseDemo(void) 67 { 68 while(1) 69 { 70 if(MouseDone != 0) 71 { 72 MouseDone--; 73 if((MouseData & 0x00100000) == 0x00100000) //Mouse left 74 { 75 printf("left press\n"); 76 } 77 if((MouseData & 0x00080000) == 0x00080000) //Mouse right 78 { 79 printf("right press\n"); 80 } 81 if((MouseData & 0x00040000) == 0x00040000) //Mouse middle 82 { http://www.fpga.gs/ 450 软核演练篇 §5 83 printf("middle press\n"); 84 } 85 86 inc.x = (MouseData & 0x1ff); //Gets the mouse x value 87 x += inc.x; 88 inc.y = ((MouseData>>9) & 0x1ff); //Gets the mouse y value 89 y -= inc.y; 90 if(x < -320) x = -320; 91 else if(x > 319) x = 319; 92 if(y < -240) y = -240; 93 else if(y > 239) y = 239; 94 95 printf("X:%d,Y:%d,\n",x,y); 96 97 } 98 } 99 } 下面我们就来简单的讲解一下该代码,代码第 26 至 35 行是我们第一个函数,它主要是通 过我们定义的数据寄存器来读取鼠标的值。第 37 至 47 行是我们第二个函数,它是中断服务子 程序。第 49 至 58 行是我们第三个函数,它是鼠标中断初始化。第 60 至 99 行是我们第四个函 数,它主要用来输出我们鼠标的移动值,以及判断我们左键、中键和右键是否按下。这里我们需 要注意的是,我们只对 IP 核的基地址进行了声明,但是没有对中断号进行声明,我们可以在第 三个中断初始化函数中看到,我们这里使用的是 ZIRCON_AVALON_PS2_MOUSE_IRQ,如 果我们修改了 Qsys 中的命名,这里也需要更改,不然将会找不到对应的中断号。 5.10.2 PS/2 鼠标 IP 核的应用 (1) 功能概述 完成了 PS/2 鼠标 IP 核的定制,接下来我们再来看看 PS/2 鼠标 IP 核的应用,首先我们讲 解的是第一部分功能概述,在该工程中,我们主要是使用 PS/2 鼠标 IP 核来测试我们的 PS/2 外 设,并将接收到的鼠标值打印到我们的控制台中。 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该工程的硬件框架,如图 5.85 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §5 定制你专属的高性能 IP 核 451 图 5.85 PS/2 鼠标 IP 核的硬件框架图 从该图中我们可以看出,除了我们常用几个基本 IP 核以外,我们这里只添加了一个 PS/2 鼠 标的 IP 核,该 IP 核的使用是非常简单的,我们不需要任何配置,只需要将它添加至 Qsys 软件 中就可以正常使用了。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 5.66 所 示。 代码 5.66 Qsys_Ps2_Mouse_Ip.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Ps2_Mouse_Ip.c 3 //-- 描述 : 使用 Ps2_Mouse IP 核来测试我们的 Ps/2 外设 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include "zircon_avalon_ps2_mouse.h" 8 #include "stdio.h" 9 10 //--------------------------------------------------------------------------- 11 //-- 名称 : main() 12 //-- 功能 : 程序入口 13 //-- 输入参数 : 无 14 //-- 输出参数 : 无 15 //--------------------------------------------------------------------------- 16 int main(void) 17 { 18 MouseInit(); //注册鼠标中断服务子程序 19 20 printf("Welcome to Ps/2 Mouse ip demo program... \n"); http://www.fpga.gs/ 452 软核演练篇 §5 21 MouseDemo(); 22 23 return 0; 24 } (4) 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软 件 中 将 Qsys_Ps2_Mouse_Ip.sof 下 载 至 我 们 的 A4 开 发 板 , Qsys_Ps2_Mouse_Ip.sof 下 载 完 成 后 , 我 们 还 需 要 在 Eclipse 软 件 中 将 Qsys_Ps2_Mouse_Ip.elf 文件下载至我们的 A4 开发板,Qsys_Ps2_Mouse_Ip.elf 下载完成以 后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可以在 Eclipse 软件的控制台中 看到我们的打印信息,如图 5.86 所示。 图 5.86 PS/2 鼠标 IP 核的控制台打印信息图 这时,我们移动 PS/2 鼠标,我们可以看到控制台打印出,如图 5.87 所示内容。 图 5.87 PS/2 鼠标 IP 核的板级调试图 当然,我们按下鼠标的左键、中键和右键,控制台也同样会打印出与其相对应的信息,这里 我们就不给大家一一进行展示了,大家可以自行下载至 A4 开发板上逐一验证。至此,我们的 PS/2 键盘 IP 核的应用就讲解完了。 Zircon Opto-Electronic Technology CO.,Ltd. 老爷车也能开出法拉利的感觉——属于 FPGA 的操 作系统 第七章 老爷车也能开除法拉利的感觉——属于 FPGA 的操作系统 本章节部分内容参考并引用了——任哲. 嵌入式实时操作系统 uC/OS-II 原理及应用[M]. 第 2 版,北京:北京航空航天大学出版社,2009. §6.1 建立你的第一个uC/OS II系统 6.1.1 uC/OS II 的系统综述 uC/OS II(Micro Control Operation System Two)是由 Jean J.Labrosse 于 1992 年编写 的一个多任务实时操作系统。所谓实时操作系统就是:如果操作系统能使计算机系统及时响应外 部事件的请求,并能及时控制所有实时设备与实时任务协调运行,且能在一个规定的时间内完成 对事件的处理,那么这种操作系统就是一个实时操作系统,也就是我们通常说所说的 RTOS (Real Time Operation System)。实时系统有两个基本要求:第一,实时系统的计算必须产生 正确的结果。第二,实时系统的计算机必须在预定的时间内完成。按照时间正确的程序来划分, 实时操作系统又可以分为硬实时操作系统和软实时操作系统两种。如果要求系统必须在极严格 的时间内完成任务,那么这样的系统就叫做硬实时操作系统。相对来说,如果系统完成任务的截 止时间要求不是十分严格,那么这种系统就叫做软实时系统。 早期的时候,这个系统叫做 uC/OS,后来经过近 10 年的应用和修改,在 1999 年 Jean J.Labrosse 推出了 uC/OS II,并在 2000 年得到了美国联邦航空管理局对用于商用飞机的、符 合 RTCA DO-178B 标准的认证,从而证明 uC/OS II 具有足够的稳定性和安全性。uC/OS II 是 用 C 语言和汇编语言来编写的,其中绝大部分代码是用 C 语言编写的,只有极少部分与处理器 密切相关的部分代码是用汇编语言编写的,所以用户只要做很少的工作就可以把它移植到各种 8 位、16 位和 32 位嵌入式处理器上。uC/OS II 可以简单的视为一个多任务调度器,在这个任务调 度器之上完善并添加了多任务操作系统相关的系统服务,如信号量、邮箱等。其主要特点有公开 源代码,代码结构清晰、明了,注释详尽,组织有条理,可移植性好,可剪裁,可固化。执行效 率高、占用空间小、实时性能优良、可扩展性强,最小内核可编译至 2KB 等。uC/OS II 只是一 个实时操作系统内核,它仅仅包含了任务调度,任务管理,时间管理和任务间的通信和同步等基 本功能。没有提供输入输出管理,文件系统,网络等额外的服务。但由于 uC/OS II 良好的可扩 展型和源码开放,这些非必须的功能完全可以由用户自己根据需要分别实现。 6.1.2 手把手教你构建一个 uC/OS II 系统 对于 ARM 等一些其他的微处理器来说,想要搭建 uC/OS II 系统,初学者会很头疼,因为移 植 uC/OS II 过程比较繁琐。但是对于 Nios II 处理器来说,那是再简单不过了,为什么呢?大家 看,如图 6.1 所示,这是 uC/OS II 与 HAL 的关系图。 456 软核演练篇 §6 User Program uC/OS-II API C Standard Library HAL API Device Device Driver Driver … Device Driver Nios II Processor System Hardware 图 6.1 uC/OS-II 与硬件抽象层的关系图 从该图中我们可以看出,Altera 把 uC/OS II 像 C 标准库一样当作 HAL 的一个扩展集移植 到 Nios II 处理器上,与其说 Altera 公司把 uC/OS II 移植到了 Nios II 处理器上,不如说 Altera 公司把 uC/OS II 像外设驱动一样当作一个软件模块,集成到了 Nios II SBT for Eclipse 软件中, 我们不用修改任何文件,也不需要对 uC/OS II 进行任何的配置,我们只需要通过 Eclipse 软件 就能够实现 uC/OS II 系统的搭建。是不是很轻松,是不是很容易,我们还在等什么,下面我们 就开始行动,手把手教你构建一个最基本的 uC/OS II 系统。 (1) 硬件框架 在新建一个 uC/OS II 系统之前,我们需要搭建一个 Qsys 硬件系统平台,以供 uC/OS II 系 统运行,创建 Qsys 硬件系统平台相信大家已经是轻车熟路了,这里我们就不在多说,直接给出 该工程的硬件框架图,如图 6.2 所示。 图 6.2 uC/OS II 的硬件框架图 从该图中我们可以看出,除了我们常用几个基本 IP 核以外,我们还需要添加一个 timer 定 时器,我们将该定时器配置为 Full-featured,以供我们的 uC/OS II 使用。 (2) 软件工程 Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 457 讲完了硬件框架,接下来我们就来讲解软件工程,首先我们需要新建 Nios II BST for Eclipse 工程,在创建 Nios II BST for Eclipse 工程的过程中,我们在选择模板的时候只需要选择 Hello MicroC/OS-II 即可,如图 6.3 所示。 图 6.3 新建 uC/OS II 系统的工程向导图 从该图中我们可以看出,我们没有选择之前的 Hello World 模板,我们而是选择了 Hello MicroC/OS-II 模板,选择好了该模板以后,我们只需要点击【Finish】按钮,来完成工程创建。 创建好工程以后,我们打开 hello_ucosii.c 文件,该文件如代码 6.1 所示。 代码 6.1 hello_ucosii.c 代码 1 #include 2 #include "includes.h" 3 4 /* Definition of Task Stacks */ 5 #define TASK_STACKSIZE 2048 6 OS_STK task1_stk[TASK_STACKSIZE]; 7 OS_STK task2_stk[TASK_STACKSIZE]; 8 9 /* Definition of Task Priorities */ 10 11 #define TASK1_PRIORITY 1 12 #define TASK2_PRIORITY 2 http://www.fpga.gs/ 458 软核演练篇 §6 13 14 /* Prints "Hello World" and sleeps for three seconds */ 15 void task1(void* pdata) 16 { 17 while (1) 18 { 19 printf("Hello from task1\n"); 20 OSTimeDlyHMSM(0, 0, 3, 0); 21 } 22 } 23 /* Prints "Hello World" and sleeps for three seconds */ 24 void task2(void* pdata) 25 { 26 while (1) 27 { 28 printf("Hello from task2\n"); 29 OSTimeDlyHMSM(0, 0, 3, 0); 30 } 31 } 32 /* The main function creates two task and starts multi-tasking */ 33 int main(void) 34 { 35 36 OSTaskCreateExt(task1, 37 NULL, 38 (void *)&task1_stk[TASK_STACKSIZE-1], 39 TASK1_PRIORITY, 40 TASK1_PRIORITY, 41 task1_stk, 42 TASK_STACKSIZE, 43 NULL, 44 0); 45 46 47 OSTaskCreateExt(task2, 48 NULL, 49 (void *)&task2_stk[TASK_STACKSIZE-1], 50 TASK2_PRIORITY, 51 TASK2_PRIORITY, 52 task2_stk, 53 TASK_STACKSIZE, 54 NULL, 55 0); 56 OSStart(); Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 459 57 return 0; 58 } 这就是一个最简单的 uC/OS II 系统代码,我们不需要修改其中的任何代码,我们只需要编 译该程序,并运行该程序就可以实现 uC/OS II 系统。如果我们想要实现其他应用程序,那么只 需要和修改 hello_world 文件一样,修改 hello_ucosii 即可。 (3) 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Ucosii.sof 下载至我们的 A4 开发板,Qsys_Ucosii.sof 下载完成 后 , 我 们 还 需 要 在 Eclipse 软 件 中 将 Qsys_Ucosii.elf 文 件 下 载 至 我 们 的 A4 开 发 板 , Qsys_Ucosii.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可 以在 Eclipse 软件的控制台中看到我们的打印信息,如图 6.4 所示。 图 6.4 uC/OS II 系统的板级调试图 至此,我们最基本的 uC/OS II 系统就搭建完成了,接下来我们就要来进一步了解一下 uC/OS II 的系统服务功能。 §6.2 uC/OS II任务管理和时间管理 6.2.1 uC/OS II 的任务管理 uC/OS II 系统的主要工作就是对任务进行管理和调度,在讲解任务管理和调度之前,我们首 先就要搞清楚什么是任务。在实际生活中,当我们处理一个比较复杂的问题时,我们通过使用的 方法就是分而治之,也就是说,把一个大问题分解成多个相对简单、比较容易解决的小问题,然 后将小问题逐个被解决了,大问题也就随之解决了。同样,在设计一个较为复杂的应用程序时, 也通常会把一个复杂的程序分解成多个相对简单、比较容易运行的小程序,计算机通过运行这些 小程序,最终能够实现大程序想要实现的目的。在 uC/OS II 中,上述小程序所对应的程序实体, 我们就称为任务,任务是由任务控制块、任务堆栈和任务程序代码三个部分组成,如图 6.5 所示。 http://www.fpga.gs/ 460 软核演练篇 §6 任务控制块 前一个任务控制块的指针 后一个任务控制块的指针 指向任务的指针 指向任务堆栈的指针 任务的优先级别 …… 任务 任务堆栈 …… PC …… 任务的代码 void mytask(void *pdata) { for(;;) { …… } } 图 6.5 uC/OS II 的任务组成 下面我们就对这三个部分进行简单的讲解: (1) 任务控制块:是用来记录任务的堆栈指针、任务的当前状态、任务的优先级别等一些与 任务管理有关的属性的表就叫做任务控制块。任务控制块相当于一个任务的身份证,系 统就是通过任务控制块来感知和管理任务的,没有任务控制块的任务不能被系统承认 和管理。uC/OS-II 把系统所有任务的控制块链接为两条链表,并通过它们管理各个任 务。 (2) 任务堆栈:所谓堆栈,就是在存储器中按数据“后进先出”的原则组织的连续存储空间。 为了满足任务切换和响应中断时保存 CPU 寄存器中的内容及任务调用其他函数时的 需要,每个任务都应该配有自己的堆栈。所有 uC/OS-II 任务的任务控制块中都含有一 个指向该任务堆栈的指针。 (3) 任务程序代码:当然,与其他代码一样,任务程序代码就是任务的执行程序。 在 uC/OS II 中,任务还可以划分为用户任务和系统任务,所谓用户任务就是由应用程序设 计者编写的任务;所谓系统任务就是由系统提供的任务。用户任务是为了解决应用问题而编写的; 系统任务是为了应用程序提供某种服务或为系统本身服务的。目前,uC/OS II 最多可以含有 64 个任务。由于 uC/OS II 是按照系统中只有一个 CPU 来设计的,所以在这种系统中,一个具体时 刻只会有一个任务占用 CPU,并处在运行状态,而至于其他的任务,它们不能处在运行状态, 只能处在其他四种状态中。为了方便大家学习,我们将任务的 5 种状态总结,如表 6.1 所示。 任务的状态 睡眠状态 就绪状态 运行状态 表 6.1 任务的 5 种状态 说明 任务只是以代码的形式驻留在程序空间(ROM 或 RAM),还没有交给操作系统管理时 的情况叫做睡眠状态。简单地说,任务在没有被配备任务控制块或被剥夺了任务控制块 时的状态叫做任务的睡眠状态 如果系统为任务配备了任务控制块且在任务就绪表中进行了就绪登记,则任务就具备了 运行的充分条件,这时任务的状态叫做就绪状态 处于就绪状态的任务如果经调度器判断获得了 CPU 的使用权,则任务就进入运行状态 任何时刻只能有一个任务处于运行状态,就绪的任务只有当所有优先级高于本任务的任 务都转为等待状态时,才能进入运行状态。 Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 461 等待状态 中断服务状态 正在运行的任务,需要等待一段时间或需要等待一个事件发生再运行时,该任务就会把 CPU 的使用权让给其他任务而使任务进入等待状态。 一个正在运行的任务一旦响应中断申请就会中止运行而去运行中断服务程序,这时任务 的状态叫做中断服务状态。 在系统的管理下,一个任务可以在 5 个不同的状态之间发生转换,其转换关系如图 6.6 所 示。 等待 状态 任务 OSFlagPost() OSMboxPost() OSMboxPostOpt() OSMutexPost() OSTaskDel() OSQPost() OSQPostFront() OSQPostOpt() OSSemPost() OSTaskResume() OSStart() OSTaskCreate() OSTimeTick() OSIntExit() OSTaskCreateExt() OS_TASK_SW () OSFlagPend() OSMboxPend() OSMutexPend() OSQPend() OSSemPend() OSTaskSuspend() OSTimeDly() OSTimeDlyHMSM() 中断 睡眠 态任 务 就绪 态任 务 OSTaskDel() 任务的CPU使用权被剥夺 OSTaskDel() 运行 态任 务 图 6.6 任务的状态转换关系图 被中 断态 任务 OSIntExit() 说完了任务管理,下面我们再来简单的介绍一下任务调度。在多任务系统中,令 CPU 中止 当前正在运行的任务转而去运行另一个任务的工作叫做任务切换,而按某种规则进行任务切换 的工作就叫做任务调度。uC/OS II 与复杂桌面操作系统不同,uC/OS II 的任务调度策略非常简 单,它只需要检查等待列表中各任务的优先级,谁的优先级高就运行谁,这种简单的任务调度策 略,虽然可以节省嵌入式系统的资源,但同时也会带来一些问题,比如说:如果一个高优先级的 任务一直占用 CPU,那么低优先级的任务将一直等待,因此我们应该合理地分配任务的优先级。 任务的优先级别有 64 个等级,每个等级都可以用一个整数来表示,即 0、1、2、3、4……63。 数字越小,优先级别越高。为了方便使用,系统总是把最低优先级别自动赋给空闲任务,如果应 用程序使用了统计任务,则系统还会把优先级别倒数第 2 低的优先级别自动赋给统计任务。至于 其他的用户任务优先级,我们需要在创建用户任务时显式地给出定义。 所谓空闲任务,就是说,系统极有可能会在某个时间内无用户任务可运行而处于所谓的空闲 状态,为了使 CPU 在没有用户任务可执行时有事可做,uC/OS-II 提供了一个叫做空闲任务的系 统任务。所谓统计任务,就是说该任务每秒计算一次 CPU 在单位时间内被使用的时间,并把计 算结果以百分比的形式存放在变量 OSCPUsage 中,以便其他应用程序来了解 CPU 的利用率。 空闲任务是每个应用程序必须使用的,而统计任务则是应用程序可以根据实际需要来选择使用 的。 最后我们总结给出任务管理函数,如表 6.2 所示,这些函数我们可以在 os_task.c 文件中找 到。 http://www.fpga.gs/ 462 软核演练篇 §6 函数 OSTaskChangePrio() OSTaskCreate() OSTaskCreateExt() OSTaskDel() OSTaskDelReq() OSTaskNameGet() OSTaskNameSet() OSTaskResume() OSTaskStkChk() OSTaskSuspend() OSTaskQuery() OS_TaskStkClr() 表 6.2 任务管理函数 描述 动态改变任务的优先级 创建一个任务 创建一个任务 删除一个任务 请求删除任务 获得一个任务的名称 设置一个任务的名称 恢复一个被挂起的任务 检查一个任务堆栈的使用情况 挂起一个任务 获得任务的任务控制模块的内容 将一个任务的堆栈清零 6.2.2 uC/OS II 的时间管理 介绍完了 uC/OS II 的任务管理,接下来我们在来介绍一下 uC/OS II 的时间管理。由于嵌入 式系统的任务是一个无限循环,并且 uC/OS II 还是一个抢占式内核,所以为了使高优先级别的 任务不至于独占 CPU,可以给其他任务优先级别较低的任务获得 CPU 使用权的机会,uC/OS II 规定:除了空闲任务之外的所有任务必须在任务中合适的位置调用系统提供的任务延时函数。 uC/OS II 提供了两个任务延时函数,OSTimeDly()函数是 uC/OS II 提供的时间服务之一,该函 数使当前任务的运行延时一段时间并进行一次任务调度,以让出 CPU 的使用权,该函数是以系 统时钟节拍为基准的,如果系统时钟节拍延长或缩短了,任务的实际延时时间也跟着延长或缩 短。,为了能使用更为习惯的的方法来使任务延时,uC/OS II 还提供了一个可以用时、分、秒为 参数的任务的延时函数 OSTimeDlyHMSM()。当用户调用该函数后,任务会暂时让出 CPU 的使 用权直到延时时间到达。使用这个延时方式不会造成程序的阻塞。我们这里推荐使用 OSTimeDlyHMSM()函数。 最后我们同样总结给出时间管理函数,如表 6.3 所示,这些函数可以在 os_time.c 中找到。 函数 OSTimeDly() OSTimeDlyHMSM() OSTimeDlyResume() OSTimeGet() OSTimeSet() 表 6.3 时间管理函数 描述 以系统时钟节拍为基准的延迟函数 按时、分、秒延时的延时函数 被 OSTimeDly()或 OSTimeDlyHMSM()延时的任务恢复 获得获得时钟节拍计数器的当前值 从新设置时钟节拍数计数器的当前值 6.2.3 uC/OS II 任务管理和时间管理的应用 (1) 功能概述 介绍完了任务管理和时间管理,接下来我们再来看看任务管理和时间管理的应用,该实验主 要设计 5 个任务,这 5 个任务的名称、任务堆栈大小,以及任务功能,如表 6.4 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 463 任务名称 InitTask() PrintTask1() PrintTask2() PrintTask3() SysInfoTask() 任务栈大小 2048 字节 2048 字节 2048 字节 2048 字节 2048 字节 表 6.4 uC/OS II 的任务列表 功能 初始化其他任务,然后自我删除 每 500ms 打印一次系统时间 每 250ms 打印一次系统时间 每 125ms 打印一次系统时间 打印出系统信息:CPU 利用率 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 6.7 所示。 图 6.7 uC/OS II 的硬件框架图 从该图中我们可以看出,我们这里的 uC/OS II 的硬件框架与我们建立的第一个 UC/OS II 的 硬件框架是一样的。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 6.2 所示。 代码 6.2 Qsys_Ucosii_Task.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Ucosii_Task.c 3 //-- 描述 : 调用 MicroC/OS-II 系统服务,实现任务创建、调度和删除。 4 //-- 调用 MicroC/OS-II 时间服务,实现延时和系统时间获取。 5 //-- 修订历史 : 2014-1-1 6 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 7 //--------------------------------------------------------------------------8 /*步骤 1:声明 MicroC/OS-II 头文件 */ 9 #include 10 #include 11 #include 12 #include "includes.h" http://www.fpga.gs/ 464 软核演练篇 §6 13 14 /*步骤 2:定义任务栈大小 */ 15 #define TASK_STACKSIZE 2048 16 17 /*步骤 3:定义各任务任务栈 */ 18 OS_STK initialize_task_stk[TASK_STACKSIZE]; 19 OS_STK print_task1_stk[TASK_STACKSIZE]; 20 OS_STK print_task2_stk[TASK_STACKSIZE]; 21 OS_STK print_task3_stk[TASK_STACKSIZE]; 22 OS_STK sys_info_task_stk[TASK_STACKSIZE]; 23 24 /*步骤 4:分配各任务优先级 */ 25 #define INITIALIZE_TASK_PRIORITY 6 26 #define PRINT_TASK1_PRIORITY 7 27 #define PRINT_TASK2_PRIORITY 8 28 #define PRINT_TASK3_PRIORITY 9 29 #define SYS_INFO_TASK_PRIORITY 10 30 31 int initCreateTasks(void); 32 33 /*步骤 5:创建任务,并实现任务功能 */ 34 void initialize_task(void* pdata) 35 { 36 /* 创建任务 */ 37 initCreateTasks(); 38 /*完成用户功能后自我删除 */ 39 OSTaskDel(OS_PRIO_SELF); /* 调用系统服务:删除任务 */ 40 while (1); 41 } 42 43 /* 打印任务 1:每.500 毫秒打印一次系统时间系统 */ 44 void PrintTask1(void* pdata) 45 { 46 while(1) 47 { 48 printf("Time infomation from Task1:\n"); 49 printf("System Time is %ld\n",OSTimeGet()); 50 OSTimeDlyHMSM(0, 0, 0, 500); 51 } 52 } 53 54 /* 打印任务 2:每 250 毫秒打印一次系统时间系统 */ 55 void PrintTask2(void* pdata) 56 { Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 57 while(1) 58 { 59 printf("Time infomation from Task2:\n"); 60 printf("System Time is %ld\n",OSTimeGet()); 61 OSTimeDlyHMSM(0, 0, 0, 250); 62 } 63 } 64 65 /* 打印任务 3:每 125 毫秒打印一次系统时间系统 */ 66 void PrintTask3(void* pdata) 67 { 68 while(1) 69 { 70 printf("Time infomation from Task3:\n"); 71 printf("System Time is %ld\n",OSTimeGet()); 72 OSTimeDlyHMSM(0, 0, 0, 125); 73 } 74 } 75 76 /* 系统信息任务:每秒打印一次 CPU 利用率 */ 77 void SysInfoTask(void* pdata) 78 { 79 while(1) 80 { 81 printf("The CPU Usage is %d\n",OSCPUUsage); 82 OSTimeDlyHMSM(0, 0, 1, 0); 83 } 84 } 85 86 /*在任务运行前,需要创建任务并向系统注册 */ 87 int initCreateTasks(void) 88 { 89 OSTaskCreateExt(PrintTask1, 90 NULL, 91 (void *)&print_task1_stk[TASK_STACKSIZE], 92 PRINT_TASK1_PRIORITY, 93 PRINT_TASK1_PRIORITY, 94 print_task1_stk, 95 TASK_STACKSIZE, 96 NULL, 97 0); 98 99 OSTaskCreateExt(PrintTask2, 100 NULL, http://www.fpga.gs/ 465 466 软核演练篇 §6 101 (void *)&print_task2_stk[TASK_STACKSIZE], 102 PRINT_TASK2_PRIORITY, 103 PRINT_TASK2_PRIORITY, 104 print_task2_stk, 105 TASK_STACKSIZE, 106 NULL, 107 0); 108 109 OSTaskCreateExt(PrintTask3, 110 NULL, 111 (void *)&print_task3_stk[TASK_STACKSIZE], 112 PRINT_TASK3_PRIORITY, 113 PRINT_TASK3_PRIORITY, 114 print_task3_stk, 115 TASK_STACKSIZE, 116 NULL, 117 0); 118 119 OSTaskCreateExt(SysInfoTask, 120 NULL, 121 (void *)&sys_info_task_stk[TASK_STACKSIZE], 122 SYS_INFO_TASK_PRIORITY, 123 SYS_INFO_TASK_PRIORITY, 124 sys_info_task_stk, 125 TASK_STACKSIZE, 126 NULL, 127 0); 128 129 return 0; 130 } 131 132 //--------------------------------------------------------------------------- 133 //-- 名称 : main() 134 //-- 功能 : 程序入口 135 //-- 输入参数 : 无 136 //-- 输出参数 : 无 137 //--------------------------------------------------------------------------- 138 int main (int argc, char* argv[], char* envp[]) 139 { 140 /* 创建父任务,由父任务创建其它任务 */ 141 OSTaskCreateExt(initialize_task, 142 NULL, 143 (void *)&initialize_task_stk[TASK_STACKSIZE], 144 INITIALIZE_TASK_PRIORITY, Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 467 145 INITIALIZE_TASK_PRIORITY, 146 initialize_task_stk, 147 TASK_STACKSIZE, 148 NULL, 149 0); 150 151 OSStart(); /*步骤 6:调用 OSStart()启动 MicroC/OS-II */ 152 return 0; 153 } 下面我们就来给大家讲解一下该程序中的关键点。uC/OS II 遇到任务具有相同优先级时会 怎么办?当实时操作系统发现多个任务具有相同优先级时通常会采取以下两种措施:一是向用 户报错;二是对相同优先级的任务进行基于时间片的调度。uC/OS II 采用的是第一种方法,当遇 到多个任务具有相同优先级时会返回一个错误代码函数:alt_ucoosii_check_return_code(re turn_code); 同时解析这个错误代码,并打印出详细信息。 uC/OS-II 应用程序开发步骤有哪些?1、声明 uC/OS II 总头文件;2、定义任务栈大小;3、 定义各任务任务栈;4、分配各任务优先级;5、创建任务,并实现任务功能;6、调用 OSStart() 启动 uC/OS-II。 怎么编写任务函数?任务用户函数必须是无限循环,因为程序执行流由操作系统内核来改 变。当执行完用户代码后最好调用系统服务,主动把 CPU 的任务权让给有需要的任务。uC/OS II 建议用户的任务最好按照以下两种模式来编写: 模式一(普通任务),上述代码中的 PrintTask1()等任务就是参考该模式编写的。 1 void Task(void *pdata) 2{ 3 for(;;) 4 { 5 /*用户代码*/ 6 调用 uC/OS-II 的服务例程之一 7 OSMboxPend(); 8 OSQPend(); 9 OSSemPend(); 10 OSTimeDlyHMSM(); 11 /*用户代码*/ 12 } 13 } 模式二(自删除任务):上述代码中的 InitTask()任务就是参考该模式编写的。 1 void Task(void *pdata) 2{ 3 for(;;) 4 { 5 /*用户代码*/ http://www.fpga.gs/ 468 软核演练篇 §6 6 OSTaskDel(OS_PRIO_SELF); 7 } 8} (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Ucosii_Task.sof 下载至我们的 A4 开发板,Qsys_Ucosii_Task.sof 下载完成后,我们还需要在 Eclipse 软件中将 Qsys_Ucosii_Task.elf 文件下载至我们的 A4 开发 板,Qsys_Ucosii_Task.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此 时,我们可以在 Eclipse 软件的控制台中看到我们的打印信息,如图 6.8 所示。 图 6.8 任务管理和时间管理的板级调试图 §6.3 uC/OS-II同步与通信 6.3.1 信号量和互斥型信号量 通过前面的学习,我们知道嵌入式系统中的各个任务是为同一个大的任务服务的子任务,它 们不可避免地要共同使用一些共享资源,并且在处理一些需要多个任务共同协助来完成的工作 时,还需要相互的支持和限制。因此,对于一个完善的多任务操作系统来说,系统必须具有完备 的同步和通信机制。为了实现各个任务之间的合作和无冲突的运行,在各任务之间必须建立一些 制约关系。其中一种制约关系叫做直接制约关系,另一种制约关系叫做间接制约关系。直接制约 关系源于任务之间的合作。例如,有任务 A 和任务 B 两个任务,它们需要通过访问同一个数据 缓冲区合作完成一项任务,任务 A 负责向缓冲区写入数据,任务 B 负责从缓冲区读取数据。显 然,当任务 A 还没有向缓冲区写入数据时(缓冲区为空时),任务 B 因不能从缓冲区得到有效数 据而应该处于等待状态;只有等任务 A 向缓冲区写入了数据之后,才应该通知任务 B 去取数据。 相反那,当缓冲区的数据还未被任务 B 读取时(缓冲区为满时),任务 A 就不能向缓冲区写入新 的数据而应该处于等待状态;只有等任务 B 自缓冲区读取数据后,才应该通知任务 A 写入数据。 显然,如果这两个任务不能如此协调工作,将势必造成严重的后果。间接制约关系源于对资源的 共享。例如,任务 A 和任务 B 共享一台打印机,如果系统已经把打印机分配给了任务 A,则任 务 B 因不能获得打印机的使用权而应该处于等待状态;只有当任务 A 把打印机释放后,系统才 能环形任务 B 使其获得打印机的使用权。如果这两个任务不这样做,那么也会造成极大的混乱。 由上可知,在多任务合作工作的过程中,操作系统应该解决两个问题:一是各任务间应该具 有一种互斥关系,即对于某个共享资源,如果一个任务正在使用,则其他任务只能等待,等到该 Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 469 任务释放资源后,等待的任务之一才能使用它;二是相关的任务在执行上要有先后次序,一个任 务要等其他伙伴发来通知,或建立了某个条件后才能继续执行,否则只能等待。任务之间的这话 总制约性的合作运行机制叫做任务间的同步。uC/OS-II 使用信号量、消息邮箱和消息队列这些 中间环节来实现任务之间的通信,为了方便起见,这些中间环节都统一被称作“事件”。下面我 们给出两个任务通过事件进行通信的示意图,如图 6.9 所示。 任务1 发送事件 事件 请求事件 任务2 图 6.9 两个任务在使用事件进行通信的示意图 在该图中我们可以看出,任务 1 是发信方,任务 2 是收信方。作为发信方,任务 1 的责任是 把信息发送到事件上,这项操作叫做发送事件。作为收信方,任务 2 的责任是通过读事件操作对 事件进行查询:如果有信息,那么任务 2 就会读取信息;如果没有信号,那么任务 2 就会继续等 待,通常我们把读事件操作叫做请求事件。在 uC/OS II 中,我们把任务发送事件、请求事件以 及其他对事件的操作全都定义成为全局函数,以供应用程序的所有任务来调用。 信号量就是一类事件,使用信号量的最初目的,是为了给共享资源设立一个标志,该标志表 示该共享资源被占用情况。这样,当一个任务在访问共享资源之前,就可以先对这个标志进行查 询,从而在了解资源被占用的情况之后,再来决定自己的行为。观察一下人们日常生活中常用的 一种共享资源:共用电话亭的使用规则,就会发现这种规则很适合在协调某种资源用户关系时使 用。如果一个电话亭只允许一个人进去打电话,俺么电话亭的门上就应该有一个可以变换两种颜 色的牌子(例如,用红色表示有人,用绿色表示无人)。当有人进去时,牌子会变成红色;出来 时,牌子又会变成绿色。这样来打电话的人就可以根据牌子上的颜色是绿色,那么他就可以进去 打电话;如果是红色,那么他只好等待;如果又陆续来了很多人,那么就要排队等待。显然,电 话亭上的这个牌子就是一个表示电话亭是否被占用的标志。由于折后再难过标志特别像交叉路 口上的交通信号灯,所以人们最初给这种标志起的名称就是信号灯,后来因为它含有了量的概念, 所以又叫做信号量。显然,对于上面介绍的红绿标志来说,这是一个二值信号量,而且由于它可 以实现共享资源的独占式占用,所以又被叫做互斥型信号量。如果电话亭可以允许多人打电话, 那么电话亭门前就不应该是那种只有红色和绿色两种颜色状态的牌子,而应该是一个计数器,该 计数器在每进去一个人时会自动减一,而每出去一个人时会自动加 1.如果其初值按电话亭的最 大容量来设置,那么来人只要见到计数器的值大于 0,就可以进去打电话;否则只好等待。这种 技术式的信号叫做信号量。下面我们给出两个任务在使用互斥型信号量进行通信,从而可使这两 个任务无冲突地访问一个共享资源的示意图,如图 6.10 和图 6.11 所示。 http://www.fpga.gs/ 470 软核演练篇 §6 任务1 先请求 信号量 1→0 信号量 后请求 信号量 任务2 共享 资源 图 6.10 任务 1 先获得信号量并使用共享资源,而任务 2 只能等待信号量 从该图中我们可以看出,任务 1 在访问共享资源之前先进行请求信号量的操作,当任务 1 发 现信号量的标志为 1 时,它一方面把信号量的标志由 1 改为 0,另一方面进行共享资源的访问。 如果任务 2 在任务 1 已经获得信号之后来请求信号量,那么由于它获得的标志值是 0,所以任务 2 就只有等待而不能访问共享资源了,由此我们可以得出,这种做法可以有效地防止两个任务同 时访问同一个共享资源所造成的冲突。那么任务 2 何时可以访问共享资源呢?如图 6.11 所示。 任务1 发送信 号量 0→1→0 信号量 请求信 号量 任务2 共享资 源 图 6.11 任务 1 释放信号后,任务 2 方可获得信号量并使用共享资源 从该图中我们可以看出,任务 1 使用完共享资源之后,由任务 1 向信号量发信号使信号量标 志的值由 0 再变为 1 时,任务 2 就有机会访问共享资源了。与任务 1 一样,任务 2 一旦获得了 共享资源的访问权,那么在访问共享资源之前一定要把信号量标志的值由 1 变为 0。 最后我们同样总结给出信号量的函数如表 6.5 所示,这些函数我们可以在 os_sem.c 文件 中找到。互斥信号量的函数如表 6.6 所示。这些函数我们可以在 os_mutex.c 文件中找到。 函数 OSSemAccept() OSSemCreate() OSSemDel() OSSemPend() OSSemPendAbord() OSSemPost() OSSemQuery() OSSemSet() 表 6.5 信号量的函数 描述 检查和此信号量相关的资源是否可用 创建一个信号量 删除一个信号量 等待一个信号量 取消等待 释放一个信号量 查询一个信号量 设置一个信号量的值 Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 471 表 6.6 互斥信号量的函数 函数 描述 OSMutexAccept() 检查互斥信号量相关的资源是否可用 OSMutexCreate() 创建一个互斥信号量 OSMutexDel() 删除一个互斥信号量 OSMutexPend() 等待一个互斥信号量 OSMutexPost() 释放一个互斥信号量 OSMutexQuery() 查询一个互斥信号量 OSMutex_RdyAtPrio () 使一个任务恢复到指定的优先级别 6.3.2 信号量和互斥型信号量的应用 (1) 功能概述 介绍完了信号量和互斥型信号量,接下来我们再来看看信号量和互斥型信号量的应用,该实 验主要设计 6 个任务,这 6 个任务的名称、任务堆栈大小,以及任务功能,如表 6.7 所示。 任务名称 InitTask() WriterTask1() WriterTask2() InputTask() OutputTask() PrintTask() 任务栈大小 2048 字节 2048 字节 2048 字节 2048 字节 2048 字节 2048 字节 表 6.7 uC/OS II 的任务列表 功能 初始化其他任务,然后自我删除 与 WriterTask2()竞争共享内存的使用权,得到使用权后向共享内存 写入字符串”Writer1 get the semaphore” 与 WriterTask1()竞争共享内存的使用权,得到使用权后向共享内存 写入字符串”Writer2 get the semaphore” 查询键盘输入,当有键盘输入事件发生时,向 OutputTask()发送信号 量 等待 InputTask()的信号量,取得信号量后输出”Output Task got the semaphore; Key Down n times” 每 5s 打印一次 WriterTask1()与 WriterTask2()的竞争信息 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 6.12 所示。 http://www.fpga.gs/ 472 软核演练篇 §6 图 6.12 uC/OS II 的硬件框架图 从该图中我们可以看出,我们这里的 uC/OS II 的硬件框架在我们建立的第一个 UC/OS II 的 硬件框架基础上多添加了一个 PIO,该 PIO 主要是给我们的按键使用的,我们这里就爱那个它 配置成了 8 位的输入端口。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 6.3 所示。 代码 6.3 Qsys_Ucosii_Semaphore.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Ucosii_Semaphore.c 3 //-- 描述 : 利用信号量,实现任务同步和资源共享 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include 8 #include 9 #include 10 #include "includes.h" /* 声明 MicroC/OS-II 头文件 */ 11 12 #define TASK_STACKSIZE 2048 /* 定义任务栈大小 */ 13 /* 定义各任务任务栈 */ 14 OS_STK initialize_task_stk[TASK_STACKSIZE]; 15 OS_STK writer_task1_stk[TASK_STACKSIZE]; 16 OS_STK writer_task2_stk[TASK_STACKSIZE]; 17 OS_STK input_task_stk[TASK_STACKSIZE]; 18 OS_STK output_task_stk[TASK_STACKSIZE]; 19 OS_STK print_task_stk[TASK_STACKSIZE]; 20 Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 473 21 /* 分配各任务优先级 */ 22 #define INITIALIZE_TASK_PRIORITY 5 23 #define PRINT_TASK_PRIORITY 6 24 #define WRITER_TASK1_PRIORITY 7 25 #define WRITER_TASK2_PRIORITY 8 26 #define INPUT_TASK_PRIORITY 9 27 #define OUTPUT_TASK_PRIORITY 10 28 29 /* 定义信号量 */ 30 OS_EVENT *shared_resource_sem; 31 OS_EVENT *key_down_sem; /*用作共享资源访问的信号量*/ /*用作任务同步的信号量*/ 32 33 /* 定义全局变量*/ 34 INT32U writer_task1_got_sem = 0; 35 INT32U writer_task2_got_sem = 0; 36 INT32U key_down_num = 0; 37 char shared_memory[40]; /*WriterTask1 获取信号量次数记录*/ /*WriterTask2 获取信号量次数记录*/ /*按键次数记录*/ /*共享内存单元*/ 38 39 /* 外部函数申明*/ 40 extern INT8U KeyPoll(void); /*键状态查询函数,有按键按下返回 1*/ 41 42 /* 局部函数声明 */ 43 int initOSDataStructs(void); 44 int initCreateTasks(void); /* 初始化信号量*/ /* 初始化任务 */ 45 46 /* 每 5 秒打印一次 WriterTask1()和 WriterTask2()的竞争信息 */ 47 void PrintTask(void* pdata) 48 { 49 while (1) 50 { 51 OSTimeDlyHMSM(0, 0, 5, 0); 52 printf("------------------------------------------------------------\n"); 53 printf("From MicroC/OS-II Running on Nios II. Here is the status:\n"); 54 printf("The shared memory is written by: %s\n",&shared_memory[0]); 55 printf("The Number of times writer_task1 acquired the semaphore %lu\n", 56 writer_task1_got_sem); 57 printf("The Number of times writer_task2 acquired the semaphore %lu\n", 58 writer_task2_got_sem); 59 printf("------------------------------------------------------------\n"); 60 printf("\n"); 61 } 62 } 63 64 /* 父任务:初始化其他子任务,然后自我删除 */ http://www.fpga.gs/ 474 软核演练篇 §6 65 void initialize_task(void* pdata) 66 { 67 /* 初始化信号量 */ 68 initOSDataStructs(); 69 /* 创建个子任务 */ 70 initCreateTasks(); 71 /* 完成用户功能后自我删除 */ 72 OSTaskDel(OS_PRIO_SELF); 73 74 while(1); 75 } 76 77 /* Writer 任务 1:当获得共享内存使用权后,向共享内存写入字符*/ 78 void WriterTask1(void* pdata) 79 { 80 INT8U return_code = OS_NO_ERR; /* 系统调用的返回状态 */ 81 while(1) 82 { 83 OSSemPend(shared_resource_sem, 0, &return_code); 84 strcpy(&shared_memory[0],"Writer1 get the semaphore"); 85 writer_task1_got_sem++; 86 OSSemPost(shared_resource_sem); 87 OSTimeDlyHMSM(0, 0, 0, 100); /* 延时 100ms */ 88 } 89 } 90 91 /* Writer 任务 2:当获得共享内存使用权后,向共享内存写入字符*/ 92 void WriterTask2(void* pdata) 93 { 94 INT8U return_code = OS_NO_ERR; /* 系统调用的返回状态 */ 95 while(1) 96 { 97 OSSemPend(shared_resource_sem, 0, &return_code); 98 strcpy(&shared_memory[0],"Writer2 get the semaphore"); 99 writer_task2_got_sem++; 100 OSSemPost(shared_resource_sem); 101 OSTimeDlyHMSM(0, 0, 0, 130); /* 延时 130ms */ 102 } 103 } 104 105 /* 键盘输入任务:当有按键下时通知 output 任务*/ 106 void InputTask(void* pdata) 107 { 108 OS_CPU_SR cpu_sr; Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 109 while(1) 110 { 111 if(KeyPoll() == 1) 112 { /*利用临界区来保护共享变量*/ 113 OS_ENTER_CRITICAL(); 114 key_down_num++; 115 OS_EXIT_CRITICAL(); 116 OSSemPost(key_down_sem); 117 } 118 else 119 OSTimeDlyHMSM(0, 0, 0, 50); /* 延时 50ms */ 120 } 121 } 122 123 /* 输出任务,当获知键盘事件发生时,输出按键次数*/ 124 void OutputTask(void* pdata) 125 { 126 INT8U return_code = OS_NO_ERR; /* 系统调用的返回状态 */ 127 while(1) 128 { 129 OSSemPend(key_down_sem, 0, &return_code); 130 printf("\n"); 131 printf("Output Task got the semaphore\n"); 132 printf("Key down %ld times. \n", key_down_num); 133 } 134 } 135 136 /* 初始化信号量*/ 137 int initOSDataStructs(void) 138 { /* 用作二值信号量时,信号量初始化为 1 */ 139 shared_resource_sem = OSSemCreate(1); 140 /* 用作事件发生标志时,信号量初始化为 0 */ 141 key_down_sem = OSSemCreate(0); 142 return 0; 143 } 144 145 /* 初始化各子任务*/ 146 int initCreateTasks(void) 147 { 148 /* 创建打印任务 */ 149 OSTaskCreateExt(PrintTask, 150 NULL, 151 &print_task_stk[TASK_STACKSIZE], 152 PRINT_TASK_PRIORITY, http://www.fpga.gs/ 475 476 软核演练篇 §6 153 PRINT_TASK_PRIORITY, 154 print_task_stk, 155 TASK_STACKSIZE, 156 NULL, 157 0); 158 159 /* 创建 Writer1 任务 */ 160 OSTaskCreateExt(WriterTask1, 161 NULL, 162 &writer_task1_stk[TASK_STACKSIZE], 163 WRITER_TASK1_PRIORITY, 164 WRITER_TASK1_PRIORITY, 165 writer_task1_stk, 166 TASK_STACKSIZE, 167 NULL, 168 0); 169 170 /* 创建 Writer2 任务 */ 171 OSTaskCreateExt(WriterTask2, 172 NULL, 173 &writer_task2_stk[TASK_STACKSIZE], 174 WRITER_TASK2_PRIORITY, 175 WRITER_TASK2_PRIORITY, 176 writer_task2_stk, 177 TASK_STACKSIZE, 178 NULL, 179 0); 180 181 /* 创建键盘输入任务 */ 182 OSTaskCreateExt(InputTask, 183 NULL, 184 &input_task_stk[TASK_STACKSIZE], 185 INPUT_TASK_PRIORITY, 186 INPUT_TASK_PRIORITY, 187 input_task_stk, 188 TASK_STACKSIZE, 189 NULL, 190 0); 191 192 /* 创建输出任务 */ 193 OSTaskCreateExt(OutputTask, 194 NULL, 195 &output_task_stk[TASK_STACKSIZE], 196 OUTPUT_TASK_PRIORITY, Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 477 197 OUTPUT_TASK_PRIORITY, 198 output_task_stk, 199 TASK_STACKSIZE, 200 NULL, 201 0); 202 203 return 0; 204 } 205 206 //--------------------------------------------------------------------------- 207 //-- 名称 : main() 208 //-- 功能 : 程序入口 209 //-- 输入参数 : 无 //-- 输出参数 : 无 210 //--------------------------------------------------------------------------- 211 int main (int argc, char* argv[], char* envp[]) 212 { 213 /* 创建父任务 */ 214 OSTaskCreateExt(initialize_task, 215 NULL, 216 &initialize_task_stk[TASK_STACKSIZE], 217 INITIALIZE_TASK_PRIORITY, 218 INITIALIZE_TASK_PRIORITY, 219 initialize_task_stk, 220 TASK_STACKSIZE, 221 NULL, 222 0); 223 OSStart(); /* 启动 OS */ 224 225 return 0; 226 } 227 /* This is the end of this file */ 下面我们就来给大家讲解一下该程序中的关键点。信号量的用途是什么?信号量 (Semaphore)是 20 世纪 60 年代中期由 Edgser Dijkstra 发明的,常用于:1、对一个共享资 源(相互排斥)访问的控制;2、表示一个事件的发生;3、让两个任务同步。在本应用中,信号 量 shared_resource_sem 用 于 对 一 个 共 享 资 源 ( 相 互 排 斥 ) 访 问 的 控 制 , 而 信 号 量 key_down_sem 则用于表示一个事件的发生。 如何操控信号量?对于一个信号量可以进行 3 个操作:Initialize(初始化或称为 Create)、 Wait(等待或称 Pend)或 Signal(发信号或称 Post)。一个请求信号量的任务将执行 Wait 等 待。如果该信号量可以使用了(信号量的值大于 0),则信号量的值将递减且任务继续进行。如 果信号量的值为 0,则对该信号量执行 Wait 操作的任务将被放置在等待列表中。一个任务通过 Signal 操作来释放信号量。如果没有任何任务等待使用这个信号量,该信号量的值递增。然而, http://www.fpga.gs/ 478 软核演练篇 §6 如果有任务在等待这个信号量,则其中的一个将准备运行,且该信号量的值不会递增。对一个信 号初始化时,必须提供该信号量的初始值。初始值可以有以下 3 种:  0:当信号量用于表示一个事件发生时,如程序段:key_down_sem = OSSemCreate(0);  1:当信号量用于对一个共享资源访问的控制时,如程序段:shared_resource_sem = OSSemCreate(1);  n:当信号量用于表示允许任务访问 n 个相同的资源时。 如果有多个任务在等待信号量时,信号量应该分配给谁?一般来说,信号量对多个等待任务 的分配采用两种策略:一是分配给等待任务列表中优先级最高的任务;二是给请求信号量的第一 个任务(FIFO)。uC/OS II 采用的是第一种分配策略。 还有其他系统服务能完成信号量的功能吗?在 uC/OS II 中,除了信号量外,还有多种方法 可以保护任务之间的共享数据和提供任务之间的通信。  利用宏 OS_ENTER_CRITICAL()和 OS_EXIT_CRITICAL()来关闭中断和打开中断。 当两个任务或一个任务和一个中断服务子程序共享某些数据时,可以采用这种方法,这 种方法的优点是不会增加调用信号量的系统开销,缺点是降低系统对异步时间的响应 能力。例如,考虑由 InputTask 和 PrintTask 共享的 INT32U 型变量 key_down_num, 当它正在进行加 1 的操作但还未完成加 1 操作时被中断打断,uC/OS-II 开始进行任务 调度。如果此时 PrintTask 任务已处于就绪状态,那么 PrintTask 任务就会获得 CPU 的使用权,输出尚未更新完毕的 key_down_num 的值。为了避免出现这种错误,上述 代码中采用了开关中断的方法来保护共享变量。  利用函数 OSSchedLock()和 OSSchekUnlock()对 uC/OS-II 中的任务调度函数上锁和 开锁。请读者尝试用该种方法来对共享变量 key_down_num 进行保护,并分析此种方 法的优缺点。  邮箱和消息队列。邮箱和消息队列通常用于并发任务间的通信,将在下面进行详述。 这里我们需要注意的是,我们最好将信号量的初始化工作放在 uC/OS II 启动前,这样可以 防止一些莫名奇妙的错误,是一种比较好的编程习惯。 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软 件 中 将 Qsys_Ucosii_Semaphore.sof 下 载 至 我 们 的 A4 开 发 板 , Qsys_Ucosii_Semaphore.sof 下 载 完 成 后 , 我 们 还 需 要 在 Eclipse 软 件 中 将 Qsys_Ucosii_Semaphore.elf 文件下载至我们的 A4 开发板,Qsys_Ucosii_Semaphore.elf 下 载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可以在 Eclipse 软件的 控制台中看到我们的打印信息,如图 6.13 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 479 图 6.13 信号量和互斥信号量的控制台打印信息图 这时,我们按下 A4 开发板上的任意一个按键,我们的控制台则会弹出如图 6.14 所示内容。 图 6.14 信号量和互斥信号量函数的板级调试图 6.3.3 消息邮箱和消息队列 在多任务操作系统中,常常需要在任务与任务之间通过传递一个数据(这种数据叫做“消息”) 的方式来进行通信。为了达到这个目的,可以在内存中创建一个存储空间作为该数据的缓冲区。 如果把这个缓冲区叫做消息缓冲区,那么在任务间传递数据(消息)的一个最简单的方法就是传 递消息缓冲区的指针。因此,用来传递消息缓冲区指针的数据结构就叫做消息邮箱。下面我们给 出两个任务使用消息邮箱进行通信的示意图,如图 6.15 所示。 发送消息 任务1 消息邮箱 请求消息 指针 任务2 消息 缓冲区 图 6.15 两个任务在使用消息邮箱进行通信的示意图 从该图中我们可以看出,任务 1 在向消息邮箱发送消息,任务 2 在从消息邮箱读取消息。读 取消息也叫做请求消息。上面我们谈到的消息邮箱不仅可以用来传递一个消息,而且也可以定义 一个指针数组。让数组的每个元素都存放一个消息缓冲区指针,那么任务就可以通过传递这个指 针数组指针的方法来传递多个消息了。这种可以传递多个消息的数据结构叫做消息队列。下面我 们给出两个任务使用消息队列进行通信的示意图,如图 6.16 所示。 http://www.fpga.gs/ 480 软核演练篇 §6 任务1 发送消 息队列 消息队列 指针 请求消 息队列 任务2 消息缓冲区1 消息缓冲区2 …… 消息缓冲区 指针数组 消息缓冲区n 图 6.16 两个任务在使用消息队列进行通信的示意图 从该图中我们可以看出,任务 1 向消息队列发送消息缓冲区指针数据的指针,这个操作叫做 发送消息队列;任务 2 在从消息队列读取消息缓冲区指针数组的指针,这个操作叫做请求消息队 列。最后我们同样总结给出消息邮箱的函数如表 6.8 所示,这些函数我们可以在 os_mbox.c 文 件中找到。 函数 OSMboxAccept() OSMboxCreate() OSMboxDel() OSMboxPend() OSMboxPendAbort() OSMboxPost() OSMboxPostOpt() OSMboxQuery() 表 6.8 消息邮箱的函数 描述 检查邮箱中是否有消息 创建一个邮箱 删除一个邮箱 等待一个邮箱 取消等待 释放一个邮箱 发送一个消息给一个任务或多个任务 查询一个邮箱 消息队列的函数如表 6.9 所示。这些函数我们可以在 os_q.c 文件中找到。 函数 OSQAccept() OSQCreate() OSQDel() OSQFlush() OSQPend() OSQPendAbort() OSQPost() OSQPostFront() OSQPostOpt() 表 6.9 消息队列的函数 描述 检查一个队列是否有消息可用 创建一个消息队列 删除一个消息队列 清除消息队列中的消息 等待一个消息队列 取消等待 将一个消息送到队列中 将一则消息放到一个消息队列中 发送一则消息给队列 Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 481 OSQQuery() OS_QInit() 查询一个消息队列 初始化消息队列模块 6.3.4 消息邮箱和消息队列的应用 (1) 功能概述 介绍完了消息邮箱和消息队列,接下来我们再来看看消息邮箱和消息队列的应用,该实验主 要设计 8 个任务,这 8 个任务的名称、任务堆栈大小,以及任务功能,如表 6.10 所示。 任务名称 InitTask() MsgSender() MsgReceiver1() MsgReceiver2() MailSend1() MailSend2() MailSend3() PrintTask() 任务栈大小 2048 字节 2048 字节 2048 字节 2048 字节 2048 字节 2048 字节 2048 字节 2048 字节 表 6.10 uC/OS II 的任务列表 功能 初始化其他任务及相关数据结构,然后自行删除 MsgSender()以一定的速率向消息队列发送消息 MsgReceiver1()以一定的速率取消息 MsgReceiver2()以不同于 MsgReceiver1()的速度取消息 MailSend1()以一定的速率从 mailbox1 收取邮件,然后向 maibox2 发 送邮件 MailSend2()以一定的速率从 mailbox2 收取邮件,然后向 maibox3 发 送邮件 MailSend3()以一定的速率从 mailbox3 收取邮件,然后向 maibox1 发 送邮件 每 5s 打印一次使用消息队列进行通信的任务状态信息 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该实验的硬件框架,如图 6.17 所示。 图 6.17 uC/OS II 的硬件框架图 从该图中我们可以看出,我们这里的 uC/OS II 的硬件框架与我们建立的第一个 UC/OS II 的 硬件框架是一样的。 (3) 软件工程 http://www.fpga.gs/ 482 软核演练篇 §6 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 6.4 所示。 代码 6.4 Qsys_Ucosii_Mbox_Queue.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Ucosii_Mbox_Queue.c 3 //-- 描述 : 利用消息队列和邮箱,实现任务间的通讯。 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include 8 #include 9 #include "includes.h" /* 声明 MicroC/OS-II 头文件 */ 10 11 #define WAIT_FOREVER 0 12 #define TASK_STACKSIZE 2048 /* 超时常数,0 表示永远等待 */ /* 定义任务栈大小 */ 13 14 /* 定义各任务任务栈 */ 15 OS_STK initialize_task_stk[TASK_STACKSIZE]; 16 OS_STK msg_sender_stk[TASK_STACKSIZE]; 17 OS_STK msg_receiver1_stk[TASK_STACKSIZE]; 18 OS_STK msg_receiver2_stk[TASK_STACKSIZE]; 19 OS_STK mail_send1_stk[TASK_STACKSIZE]; 20 OS_STK mail_send2_stk[TASK_STACKSIZE]; 21 OS_STK mail_send3_stk[TASK_STACKSIZE]; 22 OS_STK print_task_stk[TASK_STACKSIZE]; 23 24 /* 分配各任务优先级 */ 25 #define INITIALIZE_TASK_PRIORITY 6 26 #define PRINT_TASK_PRIORITY 7 27 #define MSG_SENDER_PRIORITY 8 28 #define MSG_RECEIVER1_PRIORITY 9 29 #define MSG_RECEIVER2_PRIORITY 10 30 #define MAIL_SEND1_PRIORITY 11 31 #define MAIL_SEND2_PRIORITY 12 32 #define MAIL_SEND3_PRIORITY 13 33 34 /* 定义消息队列 */ 35 #define MSG_QUEUE_SIZE 30 36 OS_EVENT *msgqueue; 37 void *msgqueueTbl[MSG_QUEUE_SIZE]; /* 消息队列大小 */ /* 消息队列事件指针 */ /* 队列缓冲区 */ 38 39 /* 定义三个邮箱 */ 40 OS_EVENT *mailbox1; 41 OS_EVENT *mailbox2; 42 OS_EVENT *mailbox3; Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 483 43 44 /* 定义任务状态记录 */ 45 INT32U number_of_messages_sent = 0; /* 发出消息的数量 */ 46 INT32U number_of_messages_received_task1 = 0; /* 接收任务 1 收到消息的数量 */ 47 INT32U number_of_messages_received_task2 = 0; /* 接收任务 2 收到消息的数量 */ 48 49 /* 局部函数声明 */ 50 int initOSDataStructs(void); /* 初始化消息队列和邮箱的数据结构 */ 51 int initCreateTasks(void); /* 初始化任务 */ 52 53 /* 每 5 秒打印一次使用消息队列的进行通讯的任务状态信息. */ 54 void PrintTask(void* pdata) 55 { 56 while (1) 57 { 58 OSTimeDlyHMSM(0, 0, 5, 0); 59 printf("------------------------------------------------------------\n"); 60 printf("The number of messages sent by the MsgSender: %lu\n", 61 number_of_messages_sent); 62 printf("The number of messages received by the MsgReceiver1: %lu\n", 63 number_of_messages_received_task1); 64 printf("The number of messages received by the MsgReceiver2: %lu\n", 65 number_of_messages_received_task2); 66 printf("------------------------------------------------------------\n"); 67 } 68 } 69 70 /* 消息发送任务: 将消息通过消息队列广播给所有等待消息的任务,当队列满时,延时 1s */ 71 void MsgSender(void* pdata) 72 { 73 INT32U msg = 0; /* 欲发出的消息 */ 74 OS_Q_DATA queue_data; /* 用于保存从消息队列的事件控制块中获取的数据信息 */ 75 76 while (1) 77 { 78 OSQQuery(msgqueue, &queue_data); /* 查询本消息队列的信息 */ 79 if(queue_data.OSNMsgs < MSG_QUEUE_SIZE) /* 查询消息队列是否已满 */ 80 { /* 消息队列未满,将消息(msg)通过消息队列(msgqueue)广播给所有等待消息的任务*/ 81 OSQPostOpt(msgqueue, (void *)&msg, OS_POST_OPT_BROADCAST); 82 msg++; /* 产生新消息 */ 83 number_of_messages_sent++; /* 记录发出消息的条数 */ 84 } 85 else 86 { /* 消息队列已满,延时 1s */ http://www.fpga.gs/ 484 软核演练篇 §6 87 OSTimeDlyHMSM(0, 0, 1, 0); 88 } 89 } 90 } 91 92 /* 消息接收任务 1: 每 300ms 接收一次消息队列的消息*/ 93 void MsgReceiver1(void* pdata) 94 { 95 INT8U return_code = OS_NO_ERR; /* 系统调用的返回状态 */ 96 INT32U *msg; /* 存储接收到的消息 */ 97 98 while (1) 99 { /* 到 msgqueue 接收消息,如果消息队列为空,则一直等待 */ 100 msg = (INT32U *)OSQPend(msgqueue, WAIT_FOREVER, &return_code); 101 number_of_messages_received_task1++; /* 接到消息,计数器加 1 */ 102 OSTimeDlyHMSM(0, 0, 0, 300); /* 延时 300ms */ 103 } 104 } 105 106 /* 消息接收任务 1: 每 1s 接收一次消息队列的消息*/ 107 void MsgReceiver2(void* pdata) 108 { 109 INT8U return_code = OS_NO_ERR; /* 系统调用的返回状态 */ 110 INT32U *msg; /* 存储接收到的消息 */ 111 112 while (1) 113 { /* 到 msgqueue 接收消息,如果消息队列为空,则一直等待 */ 114 msg = (INT32U *)OSQPend(msgqueue, WAIT_FOREVER, &return_code); 115 number_of_messages_received_task2++; /* 接到消息,计数器加 1 */ 116 OSTimeDlyHMSM(0, 0, 1, 0); /* 延时 1s */ 117 } 118 } 119 120 /* 邮件发送任务 1: 从 mailbox1 中收取信息,把信息加 1 后发到 mailbox2*/ 121 void MailSend1(void* pdata) 122 { 123 INT8U return_code = OS_NO_ERR; /* 系统调用的返回状态 */ 124 INT32U *mbox1_contents; /* 指向邮件内容的指针 */ 125 126 while (1) 127 { /* 从 mailbox1 接收邮件,如果邮箱为空,则一直等待 */ 128 mbox1_contents = (INT32U *)OSMboxPend(mailbox1,WAIT_FOREVER,&return_code); 129 130 /* 输出邮件内容,并把内容加 1 */ Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 485 131 printf("Task1 received message: %lu in mailbox 1\n",(*mbox1_contents)++); 132 printf("Task1 incremented the message and placed it into mailbox 2\n"); 133 printf("\n"); 134 OSMboxPost(mailbox2,(void *)mbox1_contents); /* 把邮件发送到 mailbox2 */ 135 } 136 } 137 138 /* 邮件发送任务 2: 从 mailbox2 中收取信息,把信息加 1 后发到 mailbox3*/ 139 void MailSend2(void* pdata) 140 { 141 INT8U return_code = OS_NO_ERR; /* 系统调用的返回状态 */ 142 INT32U *mbox2_contents; /* 指向邮件内容的指针 */ 143 144 while(1) 145 { /* 从 mailbox1 接收邮件,如果邮箱为空,则一直等待 */ 146 mbox2_contents = (INT32U *)OSMboxPend(mailbox2,WAIT_FOREVER,&return_code); 147 /* 输出邮件内容,并把内容加 1 */ 148 printf("Task2 received message: %lu in mailbox 2\n", (*mbox2_contents)++); 149 printf("Task2 incremented the message and placed it into mailbox 3\n"); 150 printf("\n"); 151 /* 把邮件发送到 mailbox3 */ 152 OSMboxPost(mailbox3,(void *)mbox2_contents); 153 } 154 } 155 156 /* 邮件发送任务 3: 从 mailbox3 中收取信息,把信息加 1 后发到 mailbox1*/ 157 void MailSend3(void* pdata) 158 { 159 INT8U return_code = OS_NO_ERR; 160 INT32U *mbox3_contents; 161 162 while (1) 163 { 164 mbox3_contents = (INT32U *)OSMboxPend(mailbox3,WAIT_FOREVER,&return_code); 165 166 printf("Task3 received message: %lu in mailbox 3\n", (*mbox3_contents)++); 167 printf("Task3 incremented the message and placed it into mailbox 1\n"); 168 printf("\n"); 169 usleep(3000000); /* 延时 3000000us */ 170 /* 把邮件发送到 mailbox1 */ 171 OSMboxPost(mailbox1,(void *)mbox3_contents); 172 } 173 } 174 http://www.fpga.gs/ 486 软核演练篇 §6 175 /* 初始化各子任务 */ 176 void initialize_task(void* pdata) 177 { 178 INT32U mbox1_contents = 0; 179 /* 初始化消息队列和邮箱的数据结构 */ 180 initOSDataStructs(); 181 182 /* 创建个子任务 */ 183 initCreateTasks(); 184 185 /* 向 mailbox1 发送一封邮件*/ 186 OSMboxPost(mailbox1, (void *)&mbox1_contents); 187 188 OSTaskDel(OS_PRIO_SELF); 189 while (1); 190 } 191 192 //--------------------------------------------------------------------------- 193 //-- 名称 : main() 194 //-- 功能 : 程序入口 195 //-- 输入参数 : 无 196 //-- 输出参数 : 无 197 //--------------------------------------------------------------------------- 198 int main (int argc, char* argv[], char* envp[]) 199 { 200 /* 创建父任务 */ 201 OSTaskCreateExt(initialize_task, 202 NULL, 203 &initialize_task_stk[TASK_STACKSIZE], 204 INITIALIZE_TASK_PRIORITY, 205 INITIALIZE_TASK_PRIORITY, 206 initialize_task_stk, 207 TASK_STACKSIZE, 208 NULL, 209 0); 210 OSStart(); /* 启动 OS */ 211 return 0; 212 } 213 /* This function simply creates the three Mailboxes */ 214 215 int initOSDataStructs(void) 216 { /* 初始化邮箱的数据结构 */ 217 mailbox1 = OSMboxCreate((void *)NULL); 218 mailbox2 = OSMboxCreate((void *)NULL); Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 219 mailbox3 = OSMboxCreate((void *)NULL); 220 /* 初始化消息队列的数据结构 */ 221 msgqueue = OSQCreate(&msgqueueTbl[0], MSG_QUEUE_SIZE); 222 return 0; 223 } 224 225 int initCreateTasks(void) 226 { 227 /* 创建打印任务 */ 228 OSTaskCreateExt(PrintTask, 229 NULL, 230 &print_task_stk[TASK_STACKSIZE], 231 PRINT_TASK_PRIORITY, 232 PRINT_TASK_PRIORITY, 233 print_task_stk, 234 TASK_STACKSIZE, 235 NULL, 236 0); 237 238 /* 创建 MsgSender 任务 */ 239 OSTaskCreateExt(MsgSender, 240 NULL, 241 &msg_sender_stk[TASK_STACKSIZE], 242 MSG_SENDER_PRIORITY, 243 MSG_SENDER_PRIORITY, 244 msg_sender_stk, 245 TASK_STACKSIZE, 246 NULL, 247 0); 248 249 /* 创建 MsgReceiver1 任务 */ 250 OSTaskCreateExt(MsgReceiver1, 251 NULL, 252 &msg_receiver1_stk[TASK_STACKSIZE], 253 MSG_RECEIVER1_PRIORITY, 254 MSG_RECEIVER1_PRIORITY, 255 msg_receiver1_stk, 256 TASK_STACKSIZE, 257 NULL, 258 0); 259 260 /* 创建 MsgReceiver2 任务 */ 261 OSTaskCreateExt(MsgReceiver2, 262 NULL, http://www.fpga.gs/ 487 488 软核演练篇 §6 263 &msg_receiver2_stk[TASK_STACKSIZE], 264 MSG_RECEIVER2_PRIORITY, 265 MSG_RECEIVER2_PRIORITY, 266 msg_receiver2_stk, 267 TASK_STACKSIZE, 268 NULL, 269 0); 270 271 /* 创建 MailSend1 任务 */ 272 OSTaskCreateExt(MailSend1, 273 NULL, 274 &mail_send1_stk[TASK_STACKSIZE], 275 MAIL_SEND1_PRIORITY, 276 MAIL_SEND1_PRIORITY, 277 mail_send1_stk, 278 TASK_STACKSIZE, 279 NULL, 280 0); 281 282 /* 创建 MailSend2 任务 */ 283 OSTaskCreateExt(MailSend2, 284 NULL, 285 &mail_send2_stk[TASK_STACKSIZE], 286 MAIL_SEND2_PRIORITY, 287 MAIL_SEND2_PRIORITY, 288 mail_send2_stk, 289 TASK_STACKSIZE, 290 NULL, 291 0); 292 293 /* 创建 MailSend3 任务 */ 294 OSTaskCreateExt(MailSend3, 295 NULL, 296 &mail_send3_stk[TASK_STACKSIZE], 297 MAIL_SEND3_PRIORITY, 298 MAIL_SEND3_PRIORITY, 299 mail_send3_stk, 300 TASK_STACKSIZE, 301 NULL, 302 0); 303 304 return 0; 305 } 306 /* This is the end of this file */ Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 489 下面我们就来给大家讲解一下该程序中的关键点。为什么要使用消息队列或邮箱?从上面 的两个应用实例我们可以知道,任务间的通信可以通过全局变量或者信号量来完成。全局变量虽 然可以承载通信的内容,但是接收方无法意识到信息的到达,除非发送方向接收方发送一个信号 量,或者接收方不断轮询该全局变量;信号量可以立即即使接收方知道某个事件的发送,但无法 传递具体内容。用信号量进行通信就像我们只拨通别人的手机而不与之通话;用消息队列或者邮 箱进行通信则可达到既拨通别人的手机又与之通话的效果。换句话说,消息队列和邮箱可以及时 传送事件的内容。 邮箱通信的机理是什么?发送方通过内核服务把一封邮件投递到邮箱,内核完成投递任务 后通知等待列表中的接收方收取邮件。在整个投递过程中,内核充当了邮递员的角色。这里的邮 件通常是一个指针,接收方可以用该指针获取邮件内容。 邮箱有些什么基本操作?内核通常提供如下的邮箱服务:  初始化邮箱的内容,邮箱最初可以包含或者不包含邮件。  把邮件发送到邮箱(Post),如果邮箱已满,则返回错误信息(OS_MBOX_FULL)。  以挂起方式接收邮件(Pend),如果邮箱为空,则把取信者挂起;若超过一定时间邮箱 仍为空,则返回超时信息。  以非挂起方式接收邮件(Accept),如果邮箱为空,则返回一个空指针。 有了邮箱为什么还要增加消息队列服务?当希望一次性向某个任务发送多则消息时,邮箱 就优点捉襟见肘了,因为一个邮箱只能装一封信。把多个邮箱集中到一起管理和使用就变成了消 息队列,所以消息队列的操作和邮箱很相似。可以简单地认为,消息队列是邮箱数组。 消息队列有些什么基本操作?消息队列的操作基本和邮箱类似:  初始化消息队列,初始化后消息队列为空。  把消息存入到队列(Post),如果队列已满,则返回错误信息(OS_Q_FULL)。  以挂起方式接收信息(Pend),如果队列为空,则把调用者挂起;若超过一定时间队列 仍为空,则返回超时信息。  以非挂起方式接收信息(Accept),如果队列为空,则返回一个空指针。  发送一则紧急消息(Front),一般来说,消息在队列中的传递顺序是先进先出(FIFO)。 但如果希望发送一则紧急消息,可以调用(OSQPostFront),把消息直接插到队列的 最前端,即后入先出(LIFO)。 当有多个任务同时等待某封邮件时,应该把邮件给谁?如果发送者以一对一的方式发送邮 件,则等待列表中的优先级最高的任务将获取邮件;如果发送者以广播的方式发送邮件,则等待 该邮件的所有任务将获得此邮件。 通信方式之一句话比较:  信号量是最简单并且最快的通信方式,但携带的消息太少。  事件标志组比信号量稍微复杂一点,它较之信号量的优势是允许任务同时等待多个时 间信号或者这些信号相与(AND)或相或(OR)后的结果。  邮箱较之信号量可以携带更多的内容,但是传递速度较慢。 http://www.fpga.gs/ 490 软核演练篇 §6  消息队列传递信息的内容最多,但是执行速度也最慢。 时间与空间是程序设计中的一对永恒不变的矛盾,矛盾的双方相互作用演化出了各种实际 的处理方法。我们应该根据实际情况在时间和空间这对矛盾之间选择一个合理的折中,从而决定 选用哪种实现方法。 (4) 板级调试 讲完了软件工程,接下来我们就将该实验下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软 件 中 将 Qsys_Ucosii_Mbox_Queue.sof 下 载 至 我 们 的 A4 开 发 板 , Qsys_Ucosii_Mbox_Queue.sof 下 载 完 成 后 , 我 们 还 需 要 在 Eclipse 软 件 中 将 Qsys_Ucosii_Mbox_Queue.elf 文件下载至我们的 A4 开发板,Qsys_Ucosii_Mbox_Queue.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可以在 Eclipse 软件 的控制台中看到我们的打印信息,如图 6.18 所示。 图 6.18 消息邮箱和消息队列的控制台打印信息图 §6.4 uC/GUI图形界面系统 6.4.1 uC/GUI 的系统综述 下面我们就来简单的介绍一下 uC/GUI,所谓 GUI 就是 Graphical User Interface 缩写,即: 图形用户接口。uC/GUI 与 uC/OS 出于同一公司,是美国 micrium 公司出品的一款针对嵌入式 系统的优秀图形软件。它可以实现 Windos 风格的图形界面,适用单任务或是多任务系统环境。 它的架构基于模块化设计,由不同的模块中的不同层组成,主要包括:液晶驱动模块,内存设备 模块,窗口系统模块,窗口控件模块,反锯齿模块和触摸屏及外围模块。其主要特性包括丰富图 形库,多窗口、多任务机制,窗口管理及丰富窗口控件类(按钮、检验框、单/多行编辑框、列表 框、进度条、菜单等),多字符集和多字体支持,多种常见图像文件支持,鼠标、触摸屏支持,灵 活自由配制等。uC/GUI 适用于所有的 CPU,因为它 100%由 ANSI-C 语言编写的。它有一个很 好的颜色管理器,允许它处理灰阶。uC/GUI 也提供一个可扩展的 2D 图形库和一个视窗管理器, 在使用一个最小的 RAM 时能支持显示窗口。uC/GUI 的版本有很多,在网上,比较容易找的到 的 uC/GUI 源代码版本有:uC/GUI3.32、uC/GUI3.90 和 uC/GUI3.98。有没有比 uC/GUI3.98 还 高的版本?当然是有的,不过更高的版本不再叫 uC/GUI 了,而叫做 emWin,emWin 发展到 5.0 版本以后已经产生了很大的更新,特别是底层驱动方面,不过这里比较遗憾的是 emWin5.xx 以 后的版本只有库,没有源码。这里我们采用的是 uC/GUI3.90 版本进行移植的。 Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 491 6.4.2 TFT 液晶屏的综述 介绍完了 uC/GUI,接下来我们再来说一说 TFT 液晶屏,TFT(Thin Film Transistor)是指 薄膜晶体管,即每个液晶像素点都是由集成在像素点后面的薄膜晶体管来驱动的,这样不仅提高 了显示屏的响应速度,同时还可以精确控制显示色阶,做到高速度、高亮度、高对比度和高分辨 率,可以说,TFT 液晶屏是目前最好的 LCD 彩色显示设备之一。由于 TFT 液晶屏成本日渐降 低,并且人们越来越渴望拥有用户友好程度更高的图形界面,因此有越来越多的工程师把 TFT 液晶屏设计到他们的产品中去了。TFT 液晶屏外观图,如图 6.19 所示。 图 6.19 TFT 液晶屏外观图 从该图中我们可以看出,TFT 显示屏和驱动 IC 控制器集成在一起,就成了 TFT 显示模块。 TFT 显示屏主要是由:背光源、导光板、偏光板、滤光板、玻璃基板、配向膜、液晶材料、薄膜 晶体管等构成。驱动 IC 控制器主要是由一个通道的源极驱动器、一个通道的栅极驱动器、用于 图形显示的 GRAM,以及供电电路等构成。讲完了 TFT 显示模块的基本概述,下面我们就给大 家讲一下 TFT 显示模块是如何驱动的。在开始讲解如何驱动之前,首先我们要知道对于我们这 个 A4 开发板上所用的 TFT 显示模块,它的驱动 IC 控制器叫 ILI9341,简而言之,就是 9341 驱 动的 TFT 液晶屏。ILI9341 具有 262,144 色,支持并行 8/16 位接口和串行接口,我们 A4 开发 板上使用的 TFT 液晶屏是并行 8 位接口,我们通过 8 位的标准 intel8080 总线进行指令和数据 传输,总线的最高速度可达 8MHz,其原理图如图 6.20 所示。 http://www.fpga.gs/ 492 软核演练篇 §6 图 6.20 A4 开发板上的 TFT 连接原理图 从该图中我们可以看出,ILI9341 的 intel8080 总线有 6 条基本的控制信号线:它们分别是: CS、RESET、RS、WR、RD 和 DBx。CS 用于片选;RESET 用于复位;RS 用于区分数据和 命令;WR 用于写使能,RD 用于读使能,DBx 就是我们的 8 位数据线。 说完了 ILI9341 控制 IC,接下来我们将回到正题,主要来说说 TFT 显示模块是如何驱动的。 驱动 TFT 显示模块,其实是很简单的,我们只要通过 Intel8080 总线向 ILI9341 控制器发送指令 和数据,ILI9341 控制器接收到我们的指令和数据后,便会根据我们的指令来驱动 TFT 液晶屏工 作,因此,我们只要知道了 Intel8080 总线的通信时序,我们就能够发送指令和数据来控制 TFT 液晶屏了,下面我们给出 Intel8080 总线的写时序图,如图 6.21 所示。 图 6.21 Intel8080 总线的写时序图 Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 493 从该时序图中我们可以看出,只有当片选 CS 为低时,ILI9341 控制器才会工作,接收指令 和数据。当 CS 拉低以后,RS 为低,WR 为低、RD 为高,表示数据传输方向为写入,并且写 入的是指令,当 RS 由低拉高以后,我们可以看到 WR 也进行了一次翻转,这表示数据输出方 向为写入,不过这次写入的是不是指令,而是数据,该时序图所对应的代码,如代码 6.5 所示。 代码 6.5 ILI9341.c 代码 1 //ILI9341 写命令 2 void ILI9341_WrCmd(alt_u8 Cmd) 3{ 4 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_RS_BASE,0); 5 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NRD_BASE,1); 6 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NWR_BASE,0); 7 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_DB_BASE,Cmd); 8 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NWR_BASE,1); 9} 10 11 //ILI9341 写数据 12 void ILI9341_WrData(alt_u16 Data) 13 { 14 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_RS_BASE,1); 15 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NRD_BASE,1); 16 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NWR_BASE,0); 17 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_DB_BASE,Data); 18 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NWR_BASE,1); 19 } 20 21 //ILI9341 读数据 22 alt_u16 ILI9341_RdData() 23 { 24 alt_u16 read_data = 0; 25 26 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_RS_BASE,1); 27 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NWR_BASE,1); 28 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NRD_BASE,0); 29 read_data = IORD_ALTERA_AVALON_PIO_DATA(TFT9341_DB_BASE); 30 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NRD_BASE,1); 31 32 return read_data; 33 } 34 35 //ILI9341 写 16 位数据 36 void ILI9341_WrData_16b(alt_u16 data) 37 { 38 ILI9341_WrData(data>>8); http://www.fpga.gs/ 494 软核演练篇 §6 39 ILI9341_WrData(data); 40 } 从该代码中我们可以看出,写命令和写数据完全符合我们的时序图。由于读时序与写时序基 本是一样的,不同的是 WR 始终为高电平,RD 进行翻转,所以我们就不在给出与其相对应的时 序图。这里需要我们注意的是,当我们需要写入 16 位的数据时,由于我们采用的是并口 8 位的 接口,所以我们需要写入两次,第一次写入的是高 8 位数据,第二次写入的是低 8 位数据。 说完了 ILI9341 的通信时序,最后我们再来说下 ILI9341 的控制命令,ILI9341 控制命令可以 说是非常复杂的,为什么这么说的,因为 ILI9341 的控制命令多达 100 多个,如果每个都要搞懂 的话,那么将会花费大量的时间,如果有兴趣的朋友,那么可以自己参考官方手册,当然,官方 手册是英文的,不过,我们庆幸的是网上有位牛人将该文档翻译成了中文,大家可以去网上很容 易就能搜索到并下载。下面我们将结合 ILI9341 初始化代码,对 ILI9341 初始化代码中用到的命 令进行简单的讲解,至于其他的命令我们这里就不再介绍了,ILI9341 初始化代码,如代码 6.6 所示。 代码 6.6 ILI9341.c 代码 1 //ILI9341 初始化 2 void ILI9341_Init(void) 3 { 4 //ILI9341 复位 5 IOWR_ALTERA_AVALON_PIO_DIRECTION(TFT9341_DB_BASE,0xFF); 6 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NRST_BASE,1); 7 ILI9341_DelayMs(50); 8 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NRST_BASE,0); 9 ILI9341_DelayMs(50); 10 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NRST_BASE,1); 11 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NCS_BASE,1); 12 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NRD_BASE,1); 13 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NWR_BASE,1); 14 ILI9341_DelayMs(50); 15 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NCS_BASE,0); 16 17 //ILI9341 开始初始化: 18 ILI9341_WrCmd(0xCF); //功耗控制 B 19 ILI9341_WrData(0x00); //固定值 20 ILI9341_WrData(0xC1); //Power 控制 21 ILI9341_WrData(0X30); //放电路径使能,为 ESD 保护使能高电平 22 23 ILI9341_WrCmd(0xED); //电源序列控制 24 ILI9341_WrData(0x64); //软启动控制 25 ILI9341_WrData(0x03); //电源序列控制 26 ILI9341_WrData(0X12); //电源序列控制 27 ILI9341_WrData(0X81); //DDVHD 增强模式 Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 28 29 ILI9341_WrCmd(0xE8); //驱动时序控制 A 30 ILI9341_WrData(0x85); //栅极驱动器的非重叠时序控制 31 ILI9341_WrData(0x00); //EQ 时序控制 32 ILI9341_WrData(0x79); //预充电时间控制 33 34 ILI9341_WrCmd(0xCB); //功耗控制 A 35 ILI9341_WrData(0x39); //固定值 36 ILI9341_WrData(0x2C); //固定值 37 ILI9341_WrData(0x00); //固定值 38 ILI9341_WrData(0x34); //内核电压控制 39 ILI9341_WrData(0x02); //DDVDH 控制 40 41 ILI9341_WrCmd(0xF7); //泵比控制 42 ILI9341_WrData(0x20); //比率控制 43 44 ILI9341_WrCmd(0xEA); //驱动时序控制 B 45 ILI9341_WrData(0x00); //栅极驱动器的非重叠时序控制 46 ILI9341_WrData(0x00); //EQ 时序控制 47 48 ILI9341_WrCmd(0xC0); //功耗控制 1 49 ILI9341_WrData(0x1D); //设置 GVDD 电平 50 51 ILI9341_WrCmd(0xC1); //功耗控制 2 52 ILI9341_WrData(0x12); //设置用于升压电路的因子 53 54 ILI9341_WrCmd(0xC5); //VCOM 控制 1 55 ILI9341_WrData(0x33); //设置 VCOMH 电压 56 ILI9341_WrData(0x3F); //设置 VCOML 电压 57 58 ILI9341_WrCmd(0xC7); //VCOM 控制 2 59 ILI9341_WrData(0X92); //设置 VCOM 偏移电压 60 61 ILI9341_WrCmd(0x3A); //像素格式设置 62 ILI9341_WrData(0x55); //RGB 图像数据设置像素格式 63 64 ILI9341_WrCmd(0x36); //存储器访问控制 65 ILI9341_WrData(0x08); //定义帧存储器的读写扫描方向 66 67 ILI9341_WrCmd(0xB1); //帧速率控制(在正常模式/全色模式) 68 ILI9341_WrData(0x00); //内部时钟分频设置,00 表示不分频 69 ILI9341_WrData(0x12); //RTNA 设置,用于设置 1H(行)的时间 70 71 ILI9341_WrCmd(0xB6); //显示功能控制 http://www.fpga.gs/ 495 496 软核演练篇 §6 72 ILI9341_WrData(0x0A); //设置在没显示区域的扫描格式,0A 表示间隔扫描 73 ILI9341_WrData(0xA2); //设置源极、栅极驱动器的移动方向和扫描周期 74 75 ILI9341_WrCmd(0x44); //设置波纹效应扫描行 76 ILI9341_WrData(0x02); 77 78 ILI9341_WrCmd(0xF2); //3G 控制 79 ILI9341_WrData(0x00); //关闭 3G 控制 80 81 ILI9341_WrCmd(0x26); //伽马设置 82 ILI9341_WrData(0x01); //选择伽马曲线 1 83 84 ILI9341_WrCmd(0xE0); //正极伽马校准 85 ILI9341_WrData(0x0F); 86 ILI9341_WrData(0x22); 87 ILI9341_WrData(0x1C); 88 ILI9341_WrData(0x1B); 89 ILI9341_WrData(0x08); 90 ILI9341_WrData(0x0F); 91 ILI9341_WrData(0x48); 92 ILI9341_WrData(0XB8); 93 ILI9341_WrData(0x34); 94 ILI9341_WrData(0x05); 95 ILI9341_WrData(0x0C); 96 ILI9341_WrData(0x09); 97 ILI9341_WrData(0x0F); 98 ILI9341_WrData(0x07); 99 ILI9341_WrData(0x00); 100 101 ILI9341_WrCmd(0XE1); //负极伽马校准 102 ILI9341_WrData(0x00); 103 ILI9341_WrData(0x23); 104 ILI9341_WrData(0x24); 105 ILI9341_WrData(0x07); 106 ILI9341_WrData(0x10); 107 ILI9341_WrData(0x07); 108 ILI9341_WrData(0x38); 109 ILI9341_WrData(0x47); 110 ILI9341_WrData(0x4B); 111 ILI9341_WrData(0x0A); 112 ILI9341_WrData(0x13); 113 ILI9341_WrData(0x06); 114 ILI9341_WrData(0x30); 115 ILI9341_WrData(0x38); Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 497 116 ILI9341_WrData(0x0F); 117 ILI9341_WrCmd(0x11); //退出睡眠 118 ILI9341_DelayMs(50); 119 ILI9341_WrCmd(0x29); //打开显示: 120 IOWR_ALTERA_AVALON_PIO_DATA(TFT9341_NCS_BASE,1); 121 } 从代码中我们可以看出,这些命令和参数设置了功耗、电源、时序、像素格式、扫描方式等 等。这里我们需要说明的是,对于 ILI9341 液晶屏初始化函数和驱动函数,TFT 液晶屏厂家一般 都会提供,我们是不需要亲自编写这些函数的,我们只要会用、会修改这些函数就已经足够了, 其实我们是没有必要去看几百页厚的全英文 TFT 液晶屏手册的。 说完了 ILI9341 控制器,我们再来说下触摸控制芯片 ADS7843,ADS7843 是 TI 公司生成 的 4 线电阻触摸屏转换接口芯片,它是一款具有同步串行接口的 12 位取样模数转换器,最高转 换速率为 125KHz,在 125KHz 转换速率和 2.7V 电压下的功耗为 750uw,如果是在关闭模式下, 那么它的功耗将会更低,仅为 0.5uw,因此,ADS7843 以其低功耗和高速率等特性,被广泛应 用在采用电池供电的小型手持设备上。ADS7843 采用 SSOP-16 引脚封装形式,如图 6.22 所 示。 +Vcc 1 X+ 2 Y+ 3 X- 4 Y- 5 GND 6 IN3 7 IN4 8 ADS7843 16 DCLK 15 CS 14 DIN 13 BUSY 12 DOUT 11 PENIRQ 10 +Vcc 9 VREF 图 6.22 ADS7843 的引脚图 下面我们给出这 16 个管脚的功能介绍,如表 6.11 所示。 表 6.11 ADS7843 接口管脚表 管脚 名称 描述 管脚 名称 描述 1 +VCC 供电电源+2.7~+5V 9 VREF A/D 参考电压输入 2 X+ X+位置输入 10 +VCC 供电电源+2.7~+5V 3 Y+ Y+位置输入 11 PENIRQ PEN 中断引脚 4 X- X-位置输入 12 DOUT 串行数据输出 5 Y- Y-位置输入 13 BUSY 忙指示输出 6 GND 接地端 14 DIN 串行数据输入 http://www.fpga.gs/ 498 软核演练篇 §6 7 IN3 8 IN4 辅助输入 1 辅助输入 2 15 CS 16 DCLK 片选信号 外部时钟输入 ADS7843 是能够连续近似记录(SAR)的 A/D 转换器,可通过连接触摸屏 X+触摸信号输 入到 A/D 转换器。同时将 Y+和 Y-的驱动打开,然后将 X+的电压数字化,经过计算便可得到 Y 位置的坐标测量结果,据此也可将 X 方向的坐标计算出来,ADS7843 驱动四线式电阻屏的接口 电路如图 6.23 所示。 图 6.23 A4 开发板上 ADS7843 连接原理图 ADS7843 有差分(Differential)和单端(Single-Ended)两种工作模式,这两种模式对转 换后的精度和可靠性有一些影响,如果将 A/D 转换器配置为读绝对电压单端模式方式,那么驱 动电压的下降将导致转换输入数据的错误,而如果配置为差分模式,则可以避免上述错误。差分 模式还具有以下两个优点:第一个优点是能够在不扩展转换器获取时间的条件下用很长的设置 时间处理触摸屏,即触摸屏电压可以有足够的时间稳定下来。第二个优点是 ADS7843 通过快速 时钟可以进入低功耗模式,从而可以节约能量。因此,我们通常建议使用差分模式。 介绍完了 ADS7843 的基本概述,接下来我们再来看下 ADS7843 的通信协议。如图 6.24 所示。 图 6.24 ADS7843 时序图 从该图中我们可以看出,ADS7843 串行接口的一次完整操作需要 24 个 DCLK 时钟周期, Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 499 前 8 个脉冲用于接收 8 位的命令,8 位的命令如表 6.12 所示。当 ADS7843 接收到前 5 位命令 后,A/D 转换器进入采样阶段。8 位的命令接收完毕后,等待 BUSY 为低电平后,从第 9 个脉 冲开始进入转换阶段,在每个 DCLK 的下降沿输出 12 位采样值,转换结束进入空闲阶段。直到 24 个 DCLK 结束,CS 置高电平,一次测量结束。 表 6.12 ADS7843 的控制字 位 名称 描述 7 S S=1,数据传输开始标志位 6~4 A2~A0 通道选择 3 MODE A/D 转换的精度选择:MODE=1 时精度为 8 位;MODE=0 时精度为 12 位 2 SER/DFR 参考电压输入模式选择:SER/DFR=1 为单端模式;SER/DFR=0 为差分模式 省电模式选择:00 为省电模式允许,在 2 次 A/D 转换之间掉电,且中断允许;01 与 00 1~0 PD1~PD0 相同,只是不允许中断;10 为保留;11 为禁止省电模式 6.4.3 uC/GUI 的系统移植 说完了 TFT 液晶屏的概述,最后我们再来说一说 uC/GUI 的移植,在开始移植我们的 uC/GUI 系统之前,我们建议大家最好先通读一遍《uC/GUI 中文用户手册》,该手册可以在网上下载,当 然,我们也会为大家提供的,《uC/GUI 用户手册》里面详细的介绍了 uC/GUI 的所有 API 函数 及相关例程,并提供了配置说明。特别是从 20 章开始就跟移植有很大的关系。通过阅读《uC/GUI 中文用户手册》,我们可以进一步了解 uC/GUI,加快移植的速度,减少移植的弯路。下面我们就 来手把手教你一步步进行移植。 首先我们新建一个名为 uCGUI 的文件夹,用来存放我们需要的 uCGUI 源码,新建好文件 夹以后,我们将光盘中的 uCGUI3.90 源码复制出来并解压,解压完成以后,我们可以看到该源 码中有三个文件夹分别为:Sample 文件夹、Start 文件夹和 Tool 文件夹。首先,我们将 Start 文 件夹中的 Config 文件夹复制到 uCGUI 中,然后我们将 Start 文件夹下的 GUI 文件夹中的所有 文件夹都复制到 uCGUI 中,最后我们再将 Sample 文件夹下的 GUI_X 和 GUIDemo 这两个文 件夹复制到 uCGUI 中,至此我们就完成了移植第一步,最终我们 uCGUI 文件夹中的内容,如 图 6.25 所示。 http://www.fpga.gs/ 500 软核演练篇 §6 图 6.25 uCGUI 文件夹中的 uCGUI 源码 下面我们简单介绍一下这些文件夹的功能。如表 6.13 所示。 目录 AntiAlias Config ConvertColor ConvertMono Core Font GUI_X GUIDemo JPEG LCDDriver Mendev Wdiet WM 表 6.13 uCGUI 各文件夹说明 内容 抗锯齿支持 配置文件 用于彩色显示的色彩转换的程序 用于 B/W(黑白两色)及灰度显示的色彩转换程序 uC/GUI 内核文件 字体文件 操作系统接口函数及触摸屏支持 uC/GUI 演示程序 图片操作函数 LCD 驱动 存储器件支持 视窗控件库 视窗管理器 完成了第一步后,接下来我们就要进入第二步,修改这些文件夹中文件,首先我们修改的是 Config 文件夹下的 GUIConf.h 文件,该文件修改如代码 6.7 所示。 代码 6.7 GUIConf.h 代码 1 #ifndef GUICONF_H 2 #define GUICONF_H 3 4 #define GUI_OS 5 #define GUI_SUPPORT_TOUCH (1) /* Compile with multitasking support */ (1) /* 触摸屏 */ 6 #define GUI_SUPPORT_UNICODE (1) /* Support mixed ASCII/UNICODE strings */ 7 Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 501 8 #define GUI_DEFAULT_FONT &GUI_Font6x8 /*默认字体*/ 9 //#define GUI_ALLOC_SIZE 12500 10 #define GUI_ALLOC_SIZE 10240 /*动态内存*/ 11 12 #define GUI_WINSUPPORT 1 /* 窗口操作 */ 13 #define GUI_SUPPORT_MEMDEV 1 /* Memory devices */ 14 #define GUI_SUPPORT_AA 0 /* 抗锯齿显示 */ 15 16 #endif /* Avoid multiple inclusion */ 从该代码中我们可以看出,该文件是 GUI 的基本属性配置文件,它有很多开关可以配置, 比如是否支持系统,是否支持触摸等。修改好了该文件以后,接下来我们修改的是 Config 文件 夹下的 LCDConf.h 文件,该文件修改如代码 6.8 所示。 代码 6.8 LCDConf.h 代码 1 #ifndef LCDCONF_H 2 #define LCDCONF_H 3 4 5 #define LCD_XSIZE 6 #define LCD_YSIZE (240) (320) /* 配置 TFT 的水平分辨率 */ /* 配置 TFT 的垂直分辨率 */ 7 8 #define LCD_BITSPERPIXEL (16) /* 每个像素的位数 */ 9 10 #define LCD_CONTROLLER (9341) /* TFT 控制器的名称 */ 11 #define LCD_FIXEDPALETTE (565) /* 调色板格式 */ 12 #define LCD_SWAP_RB (1) /* 红蓝反色交换 */ 13 // #define LCD_SWAP_XY (1) 14 #define LCD_INIT_CONTROLLER() ILI9341_Init(); /* TFT 初始化函数 */ 15 16 #endif /* LCDCONF_H */ 从该代码中我们可以看出,该文件主要是跟 LCD 相关的宏定义,比如 LCD 的分辨率、LCD 初始化入口等,这个文件与硬件直接相关,一般是根据你所使用的 LCD 的类型和所用的 LCD 控 制器的类型来配置。修改好了该文件以后,接下来我们修改的是 Config 文件夹下的 LCDConf.h 文件,该文件修改如代码 6.9 所示。 代码 6.9 GUITouchConf.h 代码 1 #ifndef GUITOUCH_CONF_H 2 #define GUITOUCH_CONF_H 3 4 #define GUI_TOUCH_AD_LEFT 5 #define GUI_TOUCH_AD_RIGHT 6 #define GUI_TOUCH_AD_TOP 7 #define GUI_TOUCH_AD_BOTTOM 3750 300 420 3850 /* 最左侧 AD 转换值 */ /* 最右侧 AD 转换值 */ /* 顶部 AD 转换值 */ /* 底部 AD 转换值 */ http://www.fpga.gs/ 502 软核演练篇 §6 8 #define GUI_TOUCH_SWAP_XY 0 9 #define GUI_TOUCH_MIRROR_X 0 10 #define GUI_TOUCH_MIRROR_Y 1 11 12 #endif /* GUITOUCH_CONF_H */ 从该代码中我们可以看出,该文件是配置触摸屏的参数。如果你在 GUIConf.h 文件的配置 中将触摸支持选项改为了 0,也就是说,你的屏幕不支持触摸或者你不需要实现触摸功能,那么 这里可以不需要修改。至此,我们就完成了移植的第二步。 完成了第二步后,接下来我们就要进入第三步,添加 TFT 底层驱动文件。uC/GUI 自带了很 多驱动,支持很多屏幕,由于我们使用的 ILI9341 的 TFT 屏幕,uCGUI 自带的驱动中并没有该 屏幕的驱动,所以这里需要添加我们 ILI9341 的底层驱动文件。我们打开 uCGUI 文件夹中的 LCDDriver 文件夹,该文件夹中就是用来存放各种屏幕的驱动,由于这些文件中并不包括我们的 驱动,所以我们将它们全部删掉,然后将我们已经修改好的屏幕驱动文件复制进来即可,如图 6.26 所示。 图 6.26 LCDDriver 文件夹中的内容 下面我们就来简单的介绍一下这 3 个文件,ILI9341.c 和 ILI9341.h 是我们 TFT 液晶屏的底 层驱动程序,该文件中主要包含了 ILI9341 写命令函数、ILI9341 写数据函数、ILI9341 读数据函 数以及 ILI9341 初始化函数等。也就是说,即使我们没有 uC/GUI,只要利用里面的函数,也同 样可以驱动 TFT 液晶屏进行工作。ILI9341_ucgui.c 是用来将我们的 ILI9341 的底层驱动连接到 我们的 uc/GUI 系统中,也就是说,uC/GUI 系统中画点、画圆、画线、中英文显示、图标显示等 功能,全都是建立在 ILI9341 的底层驱动的基础上来完成的。至此我们就完成了移植的第三步。 完成了第三步后,接下来我们就要进入第四步,添加触摸驱动文件。我们打开 GUI_X 文件 夹,在 GUI_X 文件夹中我们可以看到有 6 个文件,这里我们将这 6 个文件全部删除,然后将我 们已经修改好的触摸驱动文件复制进来即可,如图 6.27 所示。 图 6.27 GUI_X 文件夹中的内容 下面我们就来简单的介绍一下这 4 个文件,ADS7843.c 和 ADS7843.h 这两个文件是用来 驱动我们 TFT 屏上的触摸功能,GUI_X_Touch.c 文件主要是用来将我们的 ADS7843 底层驱动 连接到我们的 uC/GUI 系统中,其原理和我们的 ILI9341 同理。GUI_X_uCOS.c 文件主要将 uC/OS 中的延时程序同 uC/GUI 挂接实现整合。至此,我们就完成了整个 uC/GUI 的移植。 Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 503 6.4.4 uC/GUI 的系统应用 (1) 功能概述 完成了 uC/GUI 的系统移植,接下来我们再来看看 uC/GUI 的系统应用,首先我们讲解的是 第一部分功能概述,在该工程中,我们主要使用 TFT 液晶屏外设来实现 uC/GUI 的演示 DEMO 程序。 (2) 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该工程的硬件框架,如图 6.28 所示。 图 6.28 uC/GUI 系统的硬件框架图 从该图中可以看出,我们的 uC/GUI 系统看似非常复杂,其实它没有什么东西,除了我们常 用几个基本 IP 核以外,我们这里就只多出一个 Avalon-MM Pipeline Bridge IP 核,至于 tft9341_db、tft9341_nrst……ads7843_dout 这些 IP 核,它们都是我们学过的 PIO IP 核。这里 我们就主要介绍一下 Avalon-MM Pipeline Bridge IP 核,Bridge 的观念是 Quartus II 7.1 之后才 提出的,它是为了解决 Nios II 系统 Fmax 低落的问题,在传统的方法中,Avalon-MM 交换架构 的主端口和从端口都是一个专属通道,虽然这样有更好的并发性,但是它也增加了系统的复杂度, 因此造成系统的 Fmax 达不到最优。如果系统对于慢速、并且没有大量同时并发访问要求的话, 那么使用 Pipeline Bridge 可以降低系统的复杂度,提高系统的 Fmax。想要进一步了解该 IP 核 的朋友,可以参考 Altera 公司给出的官方文档。 (3) 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 6.10 所 示。 代码 6.10 Qsys_Ucosii_Ucgui.c 代码 1 #include 2 #include "gui.h" 3 #include "ADS7843.h" http://www.fpga.gs/ 504 软核演练篇 §6 4 #include "GUIDEMO.h" 5 #include "includes.h" 6 7 /* Definition of Task Stacks */ 8 #define TASK_STACKSIZE 2048 9 OS_STK task1_stk[TASK_STACKSIZE]; 10 OS_STK task2_stk[TASK_STACKSIZE]; 11 12 /* Definition of Task Priorities */ 13 14 #define TASK1_PRIORITY 1 15 #define TASK2_PRIORITY 2 16 17 //Demo 演示程序 18 void task1(void* pdata) 19 { 20 GUI_Init(); 21 while(1) 22 { 23 GUIDEMO_main(); 24 } 25 } 26 27 //触摸检测程序 28 void task2(void* pdata) 29 { 30 ADS7843_Init(); 31 while (1) 32 { 33 OSTimeDlyHMSM(0, 0, 0, 5); 34 GUI_TOUCH_Exec(); 35 } 36 } 37 38 /* The main function creates two task and starts multi-tasking */ 39 int main(void) 40 { 41 42 OSTaskCreateExt(task1, 43 NULL, 44 (void *)&task1_stk[TASK_STACKSIZE-1], 45 TASK1_PRIORITY, 46 TASK1_PRIORITY, 47 task1_stk, Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 505 48 TASK_STACKSIZE, 49 NULL, 50 0); 51 52 53 OSTaskCreateExt(task2, 54 NULL, 55 (void *)&task2_stk[TASK_STACKSIZE-1], 56 TASK2_PRIORITY, 57 TASK2_PRIORITY, 58 task2_stk, 59 TASK_STACKSIZE, 60 NULL, 61 0); 62 OSStart(); 63 return 0; 64 } 从该代码中我们可以看出,代码非常简单,我们是在 uC/OS II 模板基础上进行修改的,首 先我们在任务 task1 中加入了 uC/GUI 系统的 DEMO 运行函数,然后我们又在任务 task2 中加 入了触摸检测程序。看完了我们的 Qsys_Ucosii_Ucgui.c 文件后,下面我们就来介绍一下我们 是如何将移植好的 uCGUI 添加至我们的软件工程中的。首先我们以 Hello MicroC/OS-II 为模板, 创建一个名为 Qsys_Ucosii_Ucgui 的工程,工程创建好了以后,如图 6.29 所示。 图 6.29 没有添加的 Qsys_Ucosii_Ucgui 的工程结构 这里我们有两种方法可以将我们移植好的 uCGUI 文件夹添加至我们的 Qsys_Ucosii_Ucgui 工程中,首先第一种方法是将我们的 uCGUI 文件夹复制到该工程目录下,然后我们刷新该工程, 就可以在 Eclipse 软件中看到 uCGUI 文件夹。第二种方法是直接将我们的 uCGUI 文件夹粘贴至 我们 Eclipse 软件工程中的结构下。以上两种方法都可以将 uCGUI 添加至我们的工程中,添加 http://www.fpga.gs/ 506 软核演练篇 §6 完成以后,如图 6.30 所示。 图 6.30 添加完成的 Qsys_Ucosii_Ucgui 的工程结构 添加完成了以后,我们的工程还是不能够使用该文件夹,这主要是因为我们没有将该文件夹 的路径添加至我们的工程中。我们选中 Qsys_Ucosii_Ucgui 点击右键,在弹出的菜单栏中找到 【Properties】菜单并点击,则会弹出如图 6.31 所示页面。 图 6.31 添加路径至 Qsys_Ucosii_Ucgui 工程中 从该图中可以看出,我们将 uCGUI 文件夹下的所有文件夹都添加了进来。当然添加方法也 Zircon Opto-Electronic Technology CO.,Ltd. §6 属于 FPGA 的操作系统 507 很简单,我们只需要点击【Add】然后找到我们想要添加的文件夹即可。所有路径都添加完成以 后,我们就可以进行编译了,由于 uCGUI 文件夹中包含的内容较多,所以编译起来会很慢。 (4) 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软 件 中 将 Qsys_Ucosii_Ucgui.sof 下 载 至 我 们 的 A4 开 发 板 , Qsys_Ucosii_Ucgui.sof 下载完成后,我们还需要在 Eclipse 软件中将 Qsys_Ucosii_Ucgui.elf 文件下载至我们的 A4 开发板,Qsys_Ucosii_Ucgui.elf 下载完成以后,我们的 C 程序将会执行 在我们的 A4 开发板上,此时,我们可以看到 A4 开发板的 TFT 液晶屏就会运行起 uC/GUI 系统 自带的 DEMO 演示程序,如图 6.32 所示。 图 6.32 uC/GUI 系统的板级调试图 至此,第六章老爷车也能开出法拉利的感觉——属于 FPGA 的操作系统就讲解完了。 http://www.fpga.gs/ 基于 uC/OS-II 和 uC/GUI 的 UI 系统 第七章 基于 uC/OS-II 和 uC/GUI 的 UI 系统 到了这里,我们之前说的三个方面,也就是我们的内置 IP 核,自定义 IP 核、操作系统及 UI 界面,这三个方面的内容就讲解完了,我们 A4 开发板上的外设都已经会使用了,屏幕也可以点 亮了,如果现在,我们想要实现一个漂亮的图形界面系统,那么想必也是轻松加愉快,我们只需 要把屏幕和外设相结合,然后再设计一个非常酷炫的界面,就能够实现和我们开发板开机所自带 的 UI 系统一样的效果了,说是这样说,但是做起来,有的朋友也许就不知道如何下手了,为了 能够让大家有直观的理解,我们就以 A4 开发板的开机测试界面为例,给大家去讲一讲我们到底 是怎样完成这样一个 UI 系统的。 §7.1 功能概述 废话不多说,首先我们先来给大家讲解一下该工程主要实现了哪些功能,该工程主要实现了 如图 7.1 所示内容。 图 7.1 测试板载功能 你没有看错,该工程主要就是设计出这样一个测试板载功能的 UI 界面,对于这个界面相信 大家已经很熟了,它主要是用来测试我们按键、数码管、LED 等和蜂鸣器四个外设的。我们只要 知道了该页面是如何设计的,至于其他页面,我们就可以依葫芦画瓢一一设计出来。 §7.2 硬件框架 讲完了功能概述,接下来我们就来讲解硬件框架,该工程的硬件框架,如图 7.2 所示。 512 软核演练篇 §7 图 7.2 基于 uC/OS II 和 uC/GUI 的 UI 系统硬件框架图 从该图中我们可以看出,我们这里的硬件框架实验是在 uC/GUI 系统的硬件框架基础上多添 加了 3 个 IP 核和 1 个 PIO,其中,3 个 IP 核分别对应着我们的 LED 外设,数码管外设个蜂鸣 器外设,1 个 PIO 对应着我们的按键外设。当然,我们这里也可以不使用 IP 核,和按键一样使 用 PIO 的方式来实现也是可以的,不过,如果我们使用的是 PIO IP 核实现的话,那么我们在软 件编程中将要编写 LED、数码管和蜂鸣器的应用程序。如果我们使用 IP 核的话,那么我们之前 已经编写的 IP 核寄存器头文件和 IP 核底层驱动文件就不需要再编写了,我们只需要将这些 IP 核添加至 Qsys 软件中,这些应用程序会就会自动添加到我们的 BSP 板级支持包中,我们在应 用程序中只需要调用这些函数就可以使用了。现在我们能够发现,之前选择使用 IP 核是多么明 智的选择。此时此刻,即使我们不说,想必大家也是都能明白的,因为这些 IP 核都是我们学过 的。 §7.3 软件工程 讲完了硬件框架,接下来我们就来讲解软件工程,该实验的软件工程代码,如代码 7.1 所示。 代码 7.1 Qsys_Finish.c 代码 1 //--------------------------------------------------------------------------- 2 //-- 文件名 : Qsys_Finish.c 3 //-- 描述 : 基于 uC/OS-II 和 uC/GUI 的 UI 系统 4 //-- 修订历史 : 2014-1-1 5 //-- 作者 : Zircon Opto-Electronic Technology CO.,Ltd. 6 //--------------------------------------------------------------------------- 7 #include 8 #include "GUI.h" Zircon Opto-Electronic Technology CO.,Ltd. §7 基于 UC/OS-II 和 UC/GUI 的 UI 系统 513 9 #include "ZIRCON_UI.h" 10 #include "ADS7843.h" 11 12 //--------------------------------------------------------------------------- 13 //-- 名称 : task1() 14 //-- 功能 : 图形界面程序 15 //-- 输入参数 : 无 16 //-- 输出参数 : 无 17 //--------------------------------------------------------------------------- 18 void task1(void* pdata) 19 { 20 GUI_Init(); 21 ZIRCON_Test01(); 22 } 23 24 //--------------------------------------------------------------------------- 25 //-- 名称 : task2() 26 //-- 功能 : 触摸检测程序 27 //-- 输入参数 : 无 28 //-- 输出参数 : 无 29 //--------------------------------------------------------------------------- 30 void task2(void* pdata) 31 { 32 ADS7843_Init(); 33 while (1) 34 { 35 OSTimeDlyHMSM(0, 0, 0, 5); 36 GUI_TOUCH_Exec(); 37 } 38 } 39 40 //--------------------------------------------------------------------------- 41 //-- 名称 : main() 42 //-- 功能 : 程序入口 43 //-- 输入参数 : 无 44 //-- 输出参数 : 无 45 //--------------------------------------------------------------------------- 46 int main(void) 47 { 48 49 OSTaskCreateExt(task1, 50 NULL, 51 (void *)&task1_stk[TASK_STACKSIZE-1], 52 TASK1_PRIORITY, http://www.fpga.gs/ 514 软核演练篇 §7 53 TASK1_PRIORITY, 54 task1_stk, 55 TASK_STACKSIZE, 56 NULL, 57 0); 58 59 60 OSTaskCreateExt(task2, 61 NULL, 62 (void *)&task2_stk[TASK_STACKSIZE-1], 63 TASK2_PRIORITY, 64 TASK2_PRIORITY, 65 task2_stk, 66 TASK_STACKSIZE, 67 NULL, 68 0); 69 OSStart(); 70 return 0; 71 } 从该代码中我们可以看出,代码第 21 行是最关键的一行,也是我们之前没有见过的。下面 我们就来看下第 21 行代码究竟实现了怎样的功能,该函数代码 7.2 所示。 代码 7.2 ZIRCON_Test01 代码 1 //--------------------------------------------------------------------------- 2 //-- 名称 : ZIRCON_Test01() 3 //-- 功能 : 图形主界面程序 4 //-- 输入参数 :无 5 //-- 输出参数 :无 6 //--------------------------------------------------------------------------- 7 void ZIRCON_Test01() 8 { 9 /* 显示一张图片 ,该代码主要是利用了 ILI9341 底层驱动来实现的 */ 10 ILI9341_DisplayPic(Test_01); 11 /* 在所有窗口上自动使用存储设备 */ 12 WM_SetCreateFlags(WM_CF_MEMDEV); 13 /* 声明 7 个按钮 */ 14 BUTTON_Handle Test_01_BUTTON[7]; 15 /* 创建底部菜单栏左键 */ 16 Test_01_BUTTON[0] = BUTTON_Create(0 , 270, 80 , 50, GUI_ID_BUTTON0, WM_CF_SHOW); 17 BUTTON_SetBitmap(Test_01_BUTTON[0], 0, &bmMenu_Left); 18 /* 创建底部菜单栏中键 */ 19 Test_01_BUTTON[1] = BUTTON_Create(80 , 270, 80 , 50, GUI_ID_BUTTON1, WM_CF_SHOW); 20 BUTTON_SetBitmap(Test_01_BUTTON[1], 0, &bmMenu_Home); 21 /* 创建底部菜单栏右键 */ Zircon Opto-Electronic Technology CO.,Ltd. §7 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 基于 UC/OS-II 和 UC/GUI 的 UI 系统 515 Test_01_BUTTON[2] = BUTTON_Create(160, 270, 80 , 50, GUI_ID_BUTTON2, WM_CF_SHOW); BUTTON_SetBitmap(Test_01_BUTTON[2], 0, &bmMenu_Right); /* 创建按键菜单栏 */ Test_01_BUTTON[3] = BUTTON_Create(0 , 51 , 240, 50, GUI_ID_BUTTON3, WM_CF_SHOW); BUTTON_SetBitmap(Test_01_BUTTON[3], 0, &bmTest_01_Key); /* 创建数码管菜单栏 */ Test_01_BUTTON[4] = BUTTON_Create(0 , 100, 240, 50, GUI_ID_BUTTON4, WM_CF_SHOW); BUTTON_SetBitmap(Test_01_BUTTON[4], 0, &bmTest_01_DigitalLed); /* 创建 LED 菜单栏 */ Test_01_BUTTON[5] = BUTTON_Create(0 , 150, 240, 50, GUI_ID_BUTTON5, WM_CF_SHOW); BUTTON_SetBitmap(Test_01_BUTTON[5], 0, &bmTest_01_Led); /* 创建蜂鸣器菜单栏 */ Test_01_BUTTON[6] = BUTTON_Create(0 , 201, 240, 50, GUI_ID_BUTTON6, WM_CF_SHOW); BUTTON_SetBitmap(Test_01_BUTTON[6], 0, &bmTest_01_Beep); /* 用来去掉按钮的焦点 */ BUTTON_SetFocussable(Test_01_BUTTON[0],0); /* 用来去掉底部菜单栏左键的焦点 */ BUTTON_SetFocussable(Test_01_BUTTON[1],0); /* 用来去掉底部菜单栏中键的焦点 */ BUTTON_SetFocussable(Test_01_BUTTON[2],0); /* 用来去掉底部菜单栏右键的焦点 */ BUTTON_SetFocussable(Test_01_BUTTON[3],0); /* 用来去掉按键菜单栏的焦点 */ BUTTON_SetFocussable(Test_01_BUTTON[4],0); /* 用来去掉数码管菜单栏 的焦点 */ BUTTON_SetFocussable(Test_01_BUTTON[5],0); /* 用来去掉 LED 菜单栏的焦点 */ BUTTON_SetFocussable(Test_01_BUTTON[6],0); /* 用来去掉蜂鸣器菜单栏的焦点 */ /* 判断按钮按下并执行相应的程序 */ switch(GUI_WaitKey()) { /* 如果按下底部菜单栏左键,将执行下列程序 */ case GUI_ID_BUTTON0 : { BUTTON_Delete(Test_01_BUTTON[0]); /* 删掉底部菜单栏左键 */ BUTTON_Delete(Test_01_BUTTON[1]); /* 删掉底部菜单栏中键 */ BUTTON_Delete(Test_01_BUTTON[2]); /* 删掉底部菜单栏右键 */ BUTTON_Delete(Test_01_BUTTON[3]); /* 删掉按键菜单栏 */ BUTTON_Delete(Test_01_BUTTON[4]); /* 删掉数码管菜单栏 */ BUTTON_Delete(Test_01_BUTTON[5]); /* 删掉 LED 菜单栏 */ BUTTON_Delete(Test_01_BUTTON[6]); /* 删掉蜂鸣器菜单栏 */ ZIRCON_Test01();break;} /* 进入测试主页面 */ /* 如果按下底部菜单栏中键,将执行下列程序 */ case GUI_ID_BUTTON1 : { BUTTON_Delete(Test_01_BUTTON[0]); /* 删掉底部菜单栏左键 */ BUTTON_Delete(Test_01_BUTTON[1]); /* 删掉底部菜单栏中键 */ BUTTON_Delete(Test_01_BUTTON[2]); /* 删掉底部菜单栏右键 */ BUTTON_Delete(Test_01_BUTTON[3]); /* 删掉按键菜单栏 */ BUTTON_Delete(Test_01_BUTTON[4]); /* 删掉数码管菜单栏 */ BUTTON_Delete(Test_01_BUTTON[5]); /* 删掉 LED 菜单栏 */ BUTTON_Delete(Test_01_BUTTON[6]); /* 删掉蜂鸣器菜单栏 */ ZIRCON_Test01();break;} /* 进入测试主页面 */ /* 如果按下底部菜单栏右键,将执行下列程序 */ case GUI_ID_BUTTON2 : { BUTTON_Delete(Test_01_BUTTON[0]); /* 删掉底部菜单栏左键 */ http://www.fpga.gs/ 516 软核演练篇 §7 66 BUTTON_Delete(Test_01_BUTTON[1]); /* 删掉底部菜单栏中键 */ 67 BUTTON_Delete(Test_01_BUTTON[2]); /* 删掉底部菜单栏右键 */ 68 BUTTON_Delete(Test_01_BUTTON[3]); /* 删掉按键菜单栏 */ 69 BUTTON_Delete(Test_01_BUTTON[4]); /* 删掉数码管菜单栏 */ 70 BUTTON_Delete(Test_01_BUTTON[5]); /* 删掉 LED 菜单栏 */ 71 BUTTON_Delete(Test_01_BUTTON[6]); /* 删掉蜂鸣器菜单栏 */ 72 ZIRCON_Test01();break;} /* 进入测试主页面 */ 73 /* 如果按下按键菜单栏,将执行下列程序 */ 74 case GUI_ID_BUTTON3 : { BUTTON_Delete(Test_01_BUTTON[0]); /* 删掉底部菜单栏左键 */ 75 BUTTON_Delete(Test_01_BUTTON[1]); /* 删掉底部菜单栏中键 */ 76 BUTTON_Delete(Test_01_BUTTON[2]); /* 删掉底部菜单栏右键 */ 77 BUTTON_Delete(Test_01_BUTTON[3]); /* 删掉按键菜单栏 */ 78 BUTTON_Delete(Test_01_BUTTON[4]); /* 删掉数码管菜单栏 */ 79 BUTTON_Delete(Test_01_BUTTON[5]); /* 删掉 LED 菜单栏 */ 80 BUTTON_Delete(Test_01_BUTTON[6]); /* 删掉蜂鸣器菜单栏 */ 81 Test01_Key_Home();break;} /* 进入按键测试页面 */ 82 /* 如果按下数码管菜单栏,将执行下列程序 */ 83 case GUI_ID_BUTTON4 : { BUTTON_Delete(Test_01_BUTTON[0]); /* 删掉底部菜单栏左键 */ 84 BUTTON_Delete(Test_01_BUTTON[1]); /* 删掉底部菜单栏中键 */ 85 BUTTON_Delete(Test_01_BUTTON[2]); /* 删掉底部菜单栏右键 */ 86 BUTTON_Delete(Test_01_BUTTON[3]); /* 删掉按键菜单栏 */ 87 BUTTON_Delete(Test_01_BUTTON[4]); /* 删掉数码管菜单栏 */ 88 BUTTON_Delete(Test_01_BUTTON[5]); /* 删掉 LED 菜单栏 */ 89 BUTTON_Delete(Test_01_BUTTON[6]); /* 删掉蜂鸣器菜单栏 */ 90 Test01_Segled_Home();break;} /* 进入数码管测试页面 */ 91 /* 如果按下 LED 菜单栏,将执行下列程序 */ 92 case GUI_ID_BUTTON5 : { BUTTON_Delete(Test_01_BUTTON[0]); /* 删掉底部菜单栏左键 */ 93 BUTTON_Delete(Test_01_BUTTON[1]); /* 删掉底部菜单栏中键 */ 94 BUTTON_Delete(Test_01_BUTTON[2]); /* 删掉底部菜单栏右键 */ 95 BUTTON_Delete(Test_01_BUTTON[3]); /* 删掉按键菜单栏 */ 96 BUTTON_Delete(Test_01_BUTTON[4]); /* 删掉数码管菜单栏 */ 97 BUTTON_Delete(Test_01_BUTTON[5]); /* 删掉 LED 菜单栏 */ 98 BUTTON_Delete(Test_01_BUTTON[6]); /* 删掉蜂鸣器菜单栏 */ 99 Test01_Led_Home();break;} /* 进入 LED 测试页面 */ 100 /* 如果按下蜂鸣器菜单栏,将执行下列程序 */ 101 case GUI_ID_BUTTON6 : { BUTTON_Delete(Test_01_BUTTON[0]); /* 删掉底部菜单栏左键 */ 102 BUTTON_Delete(Test_01_BUTTON[1]); /* 删掉底部菜单栏中键 */ 103 BUTTON_Delete(Test_01_BUTTON[2]); /* 删掉底部菜单栏右键 */ 104 BUTTON_Delete(Test_01_BUTTON[3]); /* 删掉按键菜单栏 */ 105 BUTTON_Delete(Test_01_BUTTON[4]); /* 删掉数码管菜单栏 */ 106 BUTTON_Delete(Test_01_BUTTON[5]); /* 删掉 LED 菜单栏 */ 107 BUTTON_Delete(Test_01_BUTTON[6]); /* 删掉蜂鸣器菜单栏 */ 108 Test01_Beep_Home();break;} /* 进入蜂鸣器测试页面 */ 109 } Zircon Opto-Electronic Technology CO.,Ltd. §7 基于 UC/OS-II 和 UC/GUI 的 UI 系统 517 120 } 该代码注释比较详细,代码比较简单,这里我们就不对代码进行介绍了,下面我们主要来讲 解一下该代码所实现的效果,这里我们就以按键菜单为例,如图 7.3 所示。 图 7.3 ZIRCON_Test01 函数实现的效果图 数码管菜单、LED 灯菜单、蜂鸣器菜单,以及底部菜单栏三个按键也都是利用这种方法实 现的,当然了实现这种效果的方法不止一种,我们还使用触摸,判断位置来达到同样的效果。说 完了程序,接下来我们再来说一说,图片是如何转换成数组的。这里我们使用了两种工具,第一 种工具是 Image2Lcd,该工具可以将.bmp 和.jpg 格式的图片直接转换称.c 文件,然后我们再 将.c 文件修改成.h 文件添加至 Eclipse 软件工程中就可以直接使用了,该工具转换的图片我们主 要是利用 ILI9341 底层驱动直接进行显示图片。第二种工具是 uCGUI 自带的位图转换器(uCGUI-BitmapConvert),这个工具我们可以在 uC/GUI 的源码中找到,关于这个工具的使用, uC/GUI 的中文手册第八章中有详细的讲解,我们这里就不在进一步说明了。该工具转换的图片 我们主要用在了按钮上。最后我们再来补充说明两点,第一点就是:如果你的按钮制作出来不是 平面的按钮,而是一个 3D 按钮,那么你需要在 uCGUI 文件夹下的 Widget 中的 BUTTON.c 中 修改如下: 1 /* Support for 3D effects */ 2 #ifndef BUTTON_USE_3D 3 #define BUTTON_USE_3D 0 //将 1 改为 0 关闭 3D 按钮显示 4 #endif 第二点就是:当你进入测试页面以后,如果你发现图片切换会有黑白交替闪烁效果,那么你 需要在 uCGUI 下的 GUI_X 文件夹中的 GUI_X_uCOS.C 中修改如下: 1 void GUI_X_ExecIdle (void) 2{ 3 // OS_X_Delay(1); http://www.fpga.gs/ 518 软核演练篇 §7 4 OSTimeDly(5); 5} §7.4 板级调试 讲完了软件工程,接下来我们就将该工程下载至我们的 A4 开发板进行验证,首先我们需要 在 Quartus II 软件中将 Qsys_Finish.sof 下载至我们的 A4 开发板,Qsys_Finish.sof 下载完成 后 , 我 们 还 需 要 在 Eclipse 软 件 中 将 Qsys_Finish.elf 文 件 下 载 至 我 们 的 A4 开 发 板 , Qsys_Finish.elf 下载完成以后,我们的 C 程序将会执行在我们的 A4 开发板上,此时,我们可 以在 TFT 液晶屏中看到,如图 7.4 所示页面 图 7.4 测试板载功能主页面 这时,我们点击 LED 灯,进入 LED 测试页面,如图 7.5 所示。 图 7.5 LED 测试页面 在该页面中,我们点击全亮按钮,这时我们可以看到 A4 开发板上的 LED 将会全部点亮, 如图 7.6 所示。 Zircon Opto-Electronic Technology CO.,Ltd. §7 基于 UC/OS-II 和 UC/GUI 的 UI 系统 519 图 7.6 UI 系统的板级调试图 当然,其他的测试也都是完全可以实现的,这里我们就不给大家一一进行展示了,大家可以 自行下载至 A4 开发板上逐一验证。至此,我们的软核演练篇中的所有内容就讲解完了。 http://www.fpga.gs/ 版权声明 (1) 南京锆石光电科技有限公司对其发行的或与合作公司共同发行的包括但不限于产品或 服务的全部内容拥有版权等知识产权,受法律保护。 (2) 所有产品及资料内容仅供用户学习使用。 (3) 未经本公司书面许可,任何单位及个人不得以任何方式或理由对上述产品、服务、信息、 材料的任何部分进行复制、修改、抄录或与其它产品捆绑使用、销售。 (4) 凡侵犯本公司版权等知识产权的,本公司必依法追究其法律责任。 声明单位:南京锆石光电科技有限公司

Top_arrow
回到顶部
EEWORLD下载中心所有资源均来自网友分享,如有侵权,请发送举报邮件到客服邮箱bbs_service@eeworld.com.cn 或通过站内短信息或QQ:273568022联系管理员 高进,我们会尽快处理。