首页资源分类FPGA/CPLD > NiosII的奇幻漂流-v2.0

NiosII的奇幻漂流-v2.0

已有 445122个资源

下载专区

上传者其他资源

    文档信息举报收藏

    标    签:NiosII编程

    分    享:

    文档简介

    FPGA 和 Nios II 在快速不断地发展着,学习者的能力也在不断提高。

    无论是 FPGA 开发板本身硬件还是提供的资料,只有不断更新,才能适应不断变化的

    开发环境、开发需求和开发者能力的提高。当 Nios II 开发发展到 Qsys+Nios II SBT

    的时代时,如果教程还停留在 SOPC Builder+Nios II IDE 时代,显然太过 OUT。经

    典的 FPGA 开发板,不仅要历史悠久,更要与时俱进

    文档预览

    NiosII 的奇幻漂流 byVITO Ver 2.0 老虎社区 www.tigerbd.cn 处心积虑地让 FPGA 好学好用 NiosII 的奇幻漂流 V2.0 工具版本:Windows7 + QuartusII 12.0 + Qsys + NiosIISBT 12.0 作者:VITO E-Mail:vito943@qq.com 老虎社区 版权所有 http://www.tigerbd.cn All Right Reserved 欢迎转载,转载请保留版权信息 2014.8 目录 目录 第1章 1.1 1.2 1.3 1.4 1.5 第2章 2.1 2.2 2.3 2.4 2.5 2.6 2.7 第3章 3.1 3.2 3.3 3.4 3.5 第4章 开发前的准备 ..................................................................................................... 1 写在前面的话 ..................................................................................................... 1 TIGER BOARD T22 图片 ........................................................................................ 2 TIGER BOARD T22 简介 ........................................................................................ 5 Quartus II 的安装与激活 ..................................................................................... 8 学习 Nios II 的技术条件 ...................................................................................... 9 开始 Qsys 设计.................................................................................................. 10 概述 .................................................................................................................. 11 创建 Quartus II 工程.......................................................................................... 13 添加 PLL ............................................................................................................ 18 创建 Qsys 模块.................................................................................................. 25 管脚分配........................................................................................................... 44 Quartus II 工程配置........................................................................................... 46 总结 .................................................................................................................. 51 Nios II 软件开发流程......................................................................................... 52 概述 .................................................................................................................. 53 创建 Nios II 工程 ............................................................................................... 54 编译与运行 ....................................................................................................... 60 烧写 Nios II 程序到 Flash 存储器....................................................................... 67 总结 .................................................................................................................. 71 PIO 应用 1——控制 LED ................................................................................... 72 目录 4.1 4.2 4.3 4.4 第5章 5.1 5.2 5.3 5.4 5.5 第6章 6.1 6.2 6.3 6.4 第7章 7.1 7.2 7.3 7.4 第8章 8.1 8.2 8.3 8.4 8.5 8.6 概述 .................................................................................................................. 73 硬件实现........................................................................................................... 74 软件实现........................................................................................................... 80 总结 .................................................................................................................. 88 PIO 应用 2——外部中断 .................................................................................. 89 概述 .................................................................................................................. 90 硬件实现........................................................................................................... 91 软件实现........................................................................................................... 95 断点调试........................................................................................................... 99 总结 ................................................................................................................ 102 经典的 RS232 串口.......................................................................................... 103 概述 ................................................................................................................ 104 硬件实现......................................................................................................... 105 软件实现......................................................................................................... 107 总结 ................................................................................................................ 115 DS1302 应用——RTC ...................................................................................... 119 概述 ................................................................................................................ 120 硬件实现......................................................................................................... 121 软件实现......................................................................................................... 122 总结 ................................................................................................................ 129 Interval Timer 应用——定时器、System Clock 与 Timestamp ........................ 130 概述 ................................................................................................................ 131 硬件实现......................................................................................................... 132 软件实现——定时器 ..................................................................................... 134 软件实现——System Clock............................................................................. 139 软件实现——Timestamp................................................................................ 143 总结 ................................................................................................................ 145 目录 第9章 9.1 9.2 9.3 9.4 第 10 章 10.1 10.2 10.3 第 11 章 11.1 11.2 11.3 11.4 第 12 章 12.1 12.2 12.3 12.4 第 13 章 13.1 13.2 13.3 13.4 第 14 章 14.1 14.2 I2C 总线应用——EEPROM.............................................................................. 146 概述 ................................................................................................................ 147 硬件实现......................................................................................................... 148 软件实现......................................................................................................... 149 总结 ................................................................................................................ 157 SDRAM ............................................................................................................ 159 概述 ................................................................................................................ 160 软件实现......................................................................................................... 161 总结 ................................................................................................................ 164 Flash ................................................................................................................ 165 概述 ................................................................................................................ 166 软件实现——Simple Flash Access................................................................... 168 软件实现——Fine-Grained Flash Access ......................................................... 172 总结 ................................................................................................................ 177 神奇的直接存储器访问——DMA .................................................................. 178 概述 ................................................................................................................ 179 硬件实现......................................................................................................... 180 软件实现......................................................................................................... 182 总结 ................................................................................................................ 186 PS/2 接口键盘................................................................................................. 187 概述 ................................................................................................................ 188 硬件实现......................................................................................................... 189 软件实现......................................................................................................... 190 总结 ................................................................................................................ 198 定制自己的 IP——数码管控制器................................................................... 199 概述 ................................................................................................................ 200 定制 IP 核模块 ................................................................................................ 203 目录 14.3 14.4 14.5 第 15 章 15.1 15.2 15.3 15.4 第 16 章 16.1 16.2 16.3 16.4 第 17 章 17.1 17.2 17.3 17.4 第 18 章 18.1 18.2 18.3 18.4 18.5 18.6 第 19 章 19.1 硬件实现......................................................................................................... 214 软件实现......................................................................................................... 215 总结 ................................................................................................................ 218 AD 转换和 DA 转换 ......................................................................................... 219 概述 ................................................................................................................ 220 软件实现——A/D........................................................................................... 222 软件实现——D/A ........................................................................................... 230 总结 ................................................................................................................ 235 TFT 彩色液晶屏应用 1——显示..................................................................... 236 概述 ................................................................................................................ 237 硬件实现......................................................................................................... 238 软件实现......................................................................................................... 241 总结 ................................................................................................................ 250 TFT 彩色液晶屏应用 2——触摸控制 ............................................................. 251 概述 ................................................................................................................ 252 硬件实现......................................................................................................... 254 软件实现......................................................................................................... 259 总结 ................................................................................................................ 285 Nios II 使用 USB——CH376 芯片 ..................................................................... 286 概述 ................................................................................................................ 287 硬件实现......................................................................................................... 288 软件实现——USB DEVICE ................................................................................ 290 软件实现——USB HOST................................................................................... 299 软件实现——SD HOST ..................................................................................... 311 总结 ................................................................................................................ 313 Nios II 使用以太网——LAN.............................................................................. 323 概述 ................................................................................................................ 324 目录 19.2 19.3 19.4 第 20 章 20.1 20.2 20.3 20.4 20.5 硬件实现......................................................................................................... 325 软件实现......................................................................................................... 327 总结 ................................................................................................................ 359 附录 ................................................................................................................ 366 关于 NIOS II 中操作 PIO 接口异常问题的解决 ................................................ 367 四步搞定 NiosII 工程文件夹目录路径改变 ..................................................... 371 NiosII/Eclipse 中遇到“Launching ... has encountered a problem”的解决方案376 Avalon 总线的地址对齐:Dynamic Addressing 和 Native Addressing............... 377 【转】Quartus II 常见警告解释及措施........................................................... 382 开发前的准备 第1章 开发前的准备 本章主要介绍了 TIGER BOARD T22 开发板的外观功能和 Quartus II 软件的安装。通过本 章,你能学到: (1)了解 TIGER BOARD T22 开发板。 (2)Quartus II 软件的安装与激活。 (3)学习 Nios II 的技术准备 本章分为五个部分: 一、写在前面的话 二、TIGER BOARD T22 图片 三、TIGER BOARD T22 开发板简介 四、Quartus II 的安装与激活 五、学习 Nios II 的技术准备 1.1 写在前面的话 从本章开始,《Nios II 的奇幻漂流》教程就与大家正式见面了。如今国内学习 FPGA 和 Nios II 开发的人越来越多,FPGA 开发板市场也越来越大,各品牌和价位的 FPGA 开发板充斥着各 个电子市场和 X 宝。无论是囊中羞涩的在读同学,还是已经工作的硬件工程师,挑选一块好的 FPGA 开发板对 FPGA 和 Nios II 的学习开发都至关重要。那么,什么样的 FPGA 开发板算是 1 开发前的准备 一块好的开发板呢?依 VITO 看来,称得上一个“好”字的 FPGA 开发板要满足以下几个条件: 1、 硬件做工好。与纯粹的软件开发不同,FPGA 和 Nios II 开发除了需要 PC 机,还要依 托一些硬件设备,比如开发板。一块好的 FPGA 开发板,硬件做工好是前提,要保证 开发板的硬件设计合理,各个模块功能正常、运行稳定。试想如果一块开发板的硬件 存在着这样那样的问题,开发者在费劲力气调试程序代码后发现问题出现在硬件上, 对开发板的设计生产者会是多深的怨念。 2、 学习资料丰富,特别要提供教程。如果说硬件做工是一块好 FPGA 开发板的前提条件, 丰富的学习资料无疑是其核心。资料对于硬件开发的重要性不言而喻,一块 FPGA 开 发板,即使做工再好,如果没有丰富的学习资料作支撑,拿到手里的只是一块冷冰冰 的板子,那也无法称之为一块好的 FPGA 开发板。丰富的原理图、Datasheet、例程 和使用说明,会大大加速 FPGA 和 Nios II 学习者的学习速度,降低学习难度。此外, 完整的教程更是核心中的核心,有一本好的教程,指引着学习者从零开始,一步步地 完成每一个实现,解决每一个问题,是学习者的福音。 3、 不断保持更新。FPGA 和 Nios II 在快速不断地发展着,学习者的能力也在不断提高。 无论是 FPGA 开发板本身硬件还是提供的资料,只有不断更新,才能适应不断变化的 开发环境、开发需求和开发者能力的提高。当 Nios II 开发发展到 Qsys+Nios II SBT 的时代时,如果教程还停留在 SOPC Builder+Nios II IDE 时代,显然太过 OUT。经 典的 FPGA 开发板,不仅要历史悠久,更要与时俱进。 以上面三个条件为基本原则,云虎科技历时一年,精心打造了 TIGER BOARD T22 FPGA 开发板,希望 TIGER BOARD T22 开发板和 TIGER BOARD 系列其它产品,能成为 大家 FPGA 和 Nios II 学习开发必不可少的利器! 1.2TIGER BOARD T22 图片 下面通过几张图片,给大家全方位无死角地展示 TIGER BOARD T22 开发板,摄影和美工 2 开发前的准备 由 Jerry 操办。 首先是 45 度角俯视图,有机玻璃的采用可以有效地防静电和保护开发板。 再来张正面图,可以清晰地看到开发板的各芯片模块布局。 3 开发前的准备 接下来是一张侧面图,可以看到 T22 的接口。 4 开发前的准备 1.3TIGER BOARD T22 简介 这一节主要介绍 TIGER BOARD T22 的功能特点,本着原汁原味的原则,本节内容基本来 自于 Jerry 编写的《T22 硬件手册》。 1、Tigerboard T22 FPGA 开发平台的特点及其用户群 云虎科技推出的这款 Tigerboard T22 FPGA 开发平台,基于 TCA-4CE22 核心板,并板载 丰富的常用外设,提供配套开源示例工程。主要针对学习 FPGA 开发的各位同学和 FPGA 项目 开发工程师。 Tigerboard T22 配套的 TCA-4CE22 核心板,采用一片 Altera 公司的高性价比 FPGA— CycloneIV EP4CE22F17,并配有 SDRAM 和 SRAM 等存储器。核心板尺寸小巧,设计精细, 具有丰富的外部接口,可以方便地运用到用户自己的项目开发中去。 2、元件分布及其对应功能 Tigerboard T22 FPGA 开发平台上提供有以下主要元件:  TCA-4CE22 核心板  FPGA:Altera CycloneIV EP4CE22F17,最高 60 万等效门  SDRAM:256Mb(4M x 16Bit x 4 Banks)  SRAM:4Mb (256K x16Bit)  FLASH:64Mb 串行 FLASH  多达 75 个双向 IO 扩展口和 12 个单向(输入)/时钟口,2.0mm 和 2.54mm 两种连 接器 5  板载 50MHz 晶振,预留 1 个板载晶振位置  重配置按键、复位按键  4 个由 FPGA 控制的 LED  JTAG、AS 接口  320*240 TFT 真彩显示屏(带触摸)  USB Host 模式,U 盘读写  USB Client 模式,PC 机控制开发板  SD 卡文件读写  10M 以太网接口  8 位 AD 转换、DA 转换  可变频率蜂鸣器  DS1302 实时时钟  6 个独立按键  4 位拨动开关  8 位 8 段共阳数码管  AT24C02 256Bytes EEPROM  RS-232 串行接口  VGA 接口  PS2 键盘、鼠标接口 3、Tigerboard T22 FPGA 开发平台功能模块图 下图展示了 Tigerboard T22 FPGA 各功能模块框图。 开发前的准备 6 开发前的准备 USB Client USB Host SD卡 插座 以太网 UART串口 PS2接口 VGA接口 SRAM LED 核心板 FPGA TFT彩色 触摸屏 8位数码管 SDRAM 实时时 钟 AS JTAG AD转换 DA转换 EEPROM 蜂鸣器 拨动开关 独立按键 4、Tigerboard T22 FPGA 开发平台功能模块图 Tigerboard T22 FPGA 开发平台在出厂时,配置芯片中已经预置了一个测试程序。通过运 行这个预置的程序,开发平台将可以演示大部分功能。同时,还能够验证开发平台硬件的完好 性。 在给 Tigerboard T22 FPGA 开发平台上电之前,请按照如下步骤操作: 第 1 步: 使用 10pin 扁平排线连接 Tigerboard T22 FPGA 的 JTAG 口和 USB-Blaster 下 载器 第 2 步: 使 用 USB 电 缆 连 接 USB-Blaster 下 载 器 和 电 脑 。 如 果 这 是 您 第 一 次 连 接 USB-Blaster 和电脑,电脑将会提示您安装 USB-Blaster 的驱动程序。安装方法,请参考 “USB-Blaster 驱动安装说明.pdf” 第 3 步: 给 Tigerboard T22 FPGA 开发平台连接 5V 电源适配器 第 4 步: 打开电源开关 如果一切正常,您将观察到如下现象: 1、开机点亮所有 LED 和 DPY。 2、 初始化 TFT。 7 开发前的准备 3、进行 RTC 测试、EEPROM 测试、Flash 测试、SDRAM 测试和 USB 测试,读取 ADC 值,并将所有结果显示在 TFT 上。(测试成功显示 XXX OK!否则显示 XXX Failure) 4、 进入一个 while(1)循环: { (1)执行检测程序,检测以下模块 { (a)触摸屏幕进入画图程序。 (b)按下 K1 键进入 DA 测试程序。 (c)按下 K4 键进入屏幕校准程序。 (d)按下 K2 键刷红色。 (e)按下 K3 键刷蓝色。 (f)按下 K5 键刷绿色。 (g)按下 K6 键刷黄色。 } (2)如果检测程序中某项有效,执行完检测程序后执行显示程序。 } 1.4Quartus II 的安装与激活 进行 NiosII 开发,Quartus II 的安装必不可少。关于 Quartus II 的安装与激活,请参考 TIGER BOARD 资料中的《QuartusII 的奇幻漂流-Chap01-手把手教你安装 QuartusII》,也 可以到老虎社区下载:http://www.tigerbd.cn/thread-5402-1-1.html。 另外需要特别说明的是,如果你是在 Windows 7 环境下进行 Nios II 开发,最好使用 Win7 的 Administrator 管理员账户(即“超级管理员”,点击查看设置方法)进行开发,如 果不想更改账户,至少要保证你的账户是管理员账户,且 Nios II 软件要以管理员身份运行, 这样才能保证不会出现兼容性问题。 8 1.5学习 Nios II 的技术条件 开发前的准备 作为准备学习 Nios II 的一名开发者,你一定想知道:学习 Nios II 需要有哪些技术条件? 我是否已经具备这些条件?在 VITO 看来,学习 Nios II 的门槛相对来说还是比较低的,在技术 层面需要下面三个条件: 1、具备一定的 C 语言基础。 2、有嵌入式开发经验,单片机、ARM 皆可。 3、有一定的 FPGA 开发经验,掌握基于 Quartus II 的 Altera FPGA 开发流程,并具备 一定的 Verilog 语言基础。 只要具备了上述三个条件,你就完全可以投入到 Nios II 的学习中来了。当然,也不能说 不具备这三个条件就不能学习 Nios II。Nios II 本质上来说与单片机、ARM 等类似,也属于嵌 入式系统,而嵌入式开发主要基于 C 语言,所以 C 语言基础是必不可少的;嵌入式开发经验自 然有助于 Nios II 学习,如果没有相关经验直接学习 Nios II 也未尝不可。至于条件三,奇幻漂 流会给大家详细地介绍基于 Quartus II 的开发流程,就算你没有 FPGA 开发经验,大家也可以 跟着教程从头学起。下面,我们就正式开始 Nios II 的学习。 9 开始 Qsys 设计 第2章 开始 Qsys 设计 本文通过建立包含 Qsys 模块的 Quartus II 工程,完成硬件工程的建立。通过本章,你能 学到: (1)Quartus II 的基本操作和工程创建。 (2)Qsys 的基本操作和工程创建。 本文分为七个部分: 一、概述 二、创建 Quartus II 工程 三、添加 PLL 四、创建 Qsys 工程 五、管脚分配 六、Quartus II 工程配置 七、总结 10 2.1 概述 开始 Qsys 设计 如果你玩过单片机,或者 ARM 等嵌入式的开发,那么对嵌入式软件的开发流程应该会有 一定的认识。可是现在你要面对的是 Nios II,so something is different! 区别在哪里?你跟着走完这一章后,就会比较有体会了!这里,先简单介绍一下。 无论你之前玩过的是单片机,或是 ARM,都有一个共同特点:一旦你选择好了一款芯片, 那么芯片的“硬件”部分,就是确定了的。注意,这里的“硬件”,是指芯片内部,而不是 PCB 级别的硬件。例如,你没法去改变一款单片机的 IO 数量,也没法给某款 ARM 加一个串口或者 SPI 口之类的。而 Nios II 及其 SOPC,由于它是基于硬件可编程的 FPGA,因此,硬件非但是 可以变的,而且必须由我们搭建! 硬件怎么搭建?是不是很复杂?确实有一定的难度,但是别怕,Altera 公司的一些工具软 件,能帮我们很大的忙。这一章,我们就是要来讲讲怎么来搭建这个“硬件”环境。 这一章我们先完成 Quartus II 工程和 Qsys 模块的创建。在创建的过程中,我们首先运行 Quartus II,建立一个 Quartus II 工程,并在里面添加 PLL 等工程必备的模块;然后运行 Qsys 软件,在 Qsys 中创建一个包含 NiosII CPU 的 qsys 模块(也就是原来 SOPC Builder 中的 sopc 工程),再将其添加到 Quartus II 工程中;最后完成对 Quartus II 工程的配置。 在 NiosII 的开发过程中,我们必然离不开三个开发环境:QuartusII、Qsys 和 NiosII SBT。  QuartusII 学过 FPGA 的同学们必然对 QuartusII 不陌生了,它是 Altera 基于自身 FPGA/CPLD 产品 推出的开发软件,如果你有一定的 FPGA 基础,通过 QuartusII 建立工程自然就问题不大了; 当然,即使你没有学习过 FPGA 也没有关系,因为 QuartusII 的工程建立也就是一个固定步骤, 我们在这章会手把手的教你,而且我们学习的重点是 NiosII,QuartusII 工程只是为开发 NiosII 工程 提 供了 一 个 平台 环 境。 QuartusII 软 件也 在 不断 地 更 新中 , 我们 在 教程 中 使用 的是 QuartusII 12.0。 11 开始 Qsys 设计  Qsys Qsys 是下一代 SOPC Builder 工具,可以理解为是 SOPC Builder 的升级版。SOPC Builder 是 QuartusII 中用来建立,开发,维护系统的平台,这个系统就是以 NiosII CPU 为核心的嵌 入式系统。我们知道,要从事嵌入式开发,需要有同时具备硬件环境和软件代码,SOPC Builder 就是用来创建硬件环境的,它不同于 ARM、单片机等开发板的固定硬件环境,而是可以根据 自己的需求来搭建硬件环境,这也这是 NiosII 开发的独到和创新之处。Qsys 作为 SOPC Buider 的升级版,与 SOPC Builder 相比,提高了性能,增强了设计重用功能,能更迅速的进行验证, 但是说白了就是换了个包装,它该是干嘛的就还是干嘛的。  Nios II SBT 最后就是 NiosII SBT 了,在 QuartusII12.0 中,NiosII SBT 的全名是 Nios II Software Build Tools for Eclipse。我们在完成了 QuartusII 工程和 Qsys 模块的建立后,接下来就要进行软件 代码的编写了。NiosII SBT 就是一个软件代码的开发环境,供 NiosII 的开发者们进行软件代码 的编写、编译和仿真运行。先说到这,关于 Nios II SBT,我们将在下一章再详细介绍。 说了这么多,其实就是想让大家对这三种软件有个大概了解,尤其是原来没有接触过这些 软件的同学。接下来我们正式开工,来完成硬件工程的创建! 12 2.2创建 Quartus II 工程 首先打开 QuartusII 12.0 软件,如下图所示, 开始 Qsys 设计 选择菜单栏的 File ->New Project Wizard,点击 Next,得到下图。图中框 1 内为 QuartusII 工程的存放路径,框 2 内为工程名,框 3 内为工程顶层模块的名字,默认与工程名相同。我们 在框 1 内选择上图中的路径(这个可以根据需求自己选择,只要路径不包含中文字符和空格即 可),在框 2 和框 3 内输入 nios_prj,完成后点击 Next。 13 开始 Qsys 设计 点击 Next 后得到下图,在工程中选择添加文件,我们先不添加,点击 Next。 然后来到下图所示的选择 FPGA 器件型号界面,TIGER BOARD T22 采用的是 Cyclone IV 系列的 EP4CE22F17C8,在框 1 的 Device Family 栏选择 CycloneIV E,在 2 框中的 Available 14 devices 栏中选择 EP4CE22F17C8,完成后点击 Next。 开始 Qsys 设计 接下来为 EDA 工具设置,我们不用修改,再点击 Next,得到下图。图中为我们即将创建 完成的工程的一个概要(Summary),可以看到我们刚才设置的工程目录、工程名、器件等信 息,我们点击 Finish,工程就创建完毕了。 15 开始 Qsys 设计 接下来我们在已经建好的工程中添加设计文件,在 QuartusII 主页面中点击 File->New, 如下图所示。选择 Design Files 中的 BlockDiagram/Schematic File,点击 OK。 点击 OK 后生成了一个 Block1.bdf 文件,这就是原理图格式的文件,也是整个工程的顶层 文件,如下图。至此,Quartus II 工程的创建就完成了,但此时工程里没有任何内容,也无法 16 实现任何功能,后面需要做的工作还有很多。 开始 Qsys 设计 小贴士 Tips:顶层文件的向左走 OR 向右走——原理图还是硬件描述语言? 这相当程度上是个人喜好问题。原理图的最大优点是直观,最大缺点是可移植性差(要 移植到别的品牌的 FPGA,或者 ASIC),不过 Qsys 只有 Altera 支持,所以这个可移植 性差的问题似乎也不是问题。当然,如果你已经很熟练硬件描述语言,对文本更偏爱而 不喜欢原理图点来点去的操作方式,那就用硬件描述语言吧☺。 17 2.3添加 PLL 开始 Qsys 设计 接下来添加一个 Altera 的 IP 核:PLL,也就是锁相环。 小贴士 Tips:关于 IP IP 的全称是 Intellectual Property,即知识产权。IP 核是一种已经设计完成并封装好、可 供直接使用的能实现某些特定功能的模块。简单来说,IP 核就是一个我们可以直接拿来 使用的现成模块。 在 FPGA 开发中,PLL 主要用来得到开发者希望的时钟信号,它输入硬件电路上的时钟源 (通常为板上晶振),可以改变输入时钟的频率、占空比等参数,从而得到期望 的时钟信号。 TIGER BOARD 开发板上的晶振频率为 50MHz,我们希望的时钟频率同样为 50MHz,本来是 无需 PLL 的,但一来为了介绍下 PLL 的使用方法,二来经过 PLL 后的时钟在抖动(Jitter)方 面更好一些,我们就用 PLL 来生成一个仍为 50MHz 的时钟。 关于 jitter 和 skew,可以参考这个帖子: http://www.tigerbd.cn/thread-2-1-1.html 在 Quartus II 菜单栏中选择 Tools->MegaWizard Plug-In Manager,得到下图,保持 默认选项接点击 Next。 18 开始 Qsys 设计 点击 Next 后得到下图所示的 IP 核选择界面,在左边的 IP 核列表中选择 PLL,位置如框 1 中所示;框 2 为我们选定的语言类型,即 Verilog;在框 3 的位置输入名称,我们把它命名为 pll(名称随意,自己舒服就行),完成后点击 Next。 点击 Next 后得到下图,我们在框 1 中设置输入时钟的频率,即 50MHz,保持其它选项 不变,点击 Next。 19 开始 Qsys 设计 接下来得到下图,我们把框 1 和框 2 中的对勾去掉,目的是去掉 PLL 的 arese(t 异步复位) 和 locked(锁定)管脚,可以看到,框 3 中的 PLL 模块原理图发生了变化,管脚减少了两个, 自然就是 areset 和 locked 了。 小贴士 Tips:PLL 上的 locked 管脚 关于 locked 这个输出,是表明 PLL 已经锁定成功了,时钟系统工作正常。这里为了简 略,所以把它省略了。实际项目中,特别是当时钟源的质量不一定很好的时候,建议把 这个 locked 引脚引出到 PCB,并点亮一个 LED 灯,这样时钟系统是否正常,可以一目 了然。 20 开始 Qsys 设计 连续点击 Next,直到下图,框 1 为当前所在位置,这个是用来配置 c0 这一输出时钟的。 我们选择框 2,即直接输入频率模式,在框 3 中输入 50,右侧 Actual Settings 显示的是真实 的频率,可以看到其值为 50MHz。 21 开始 Qsys 设计 小贴士 Tips:PLL 上的 locked 管脚 关于 locked 这个输出,是表明 PLL 已经锁定成功了,时钟系统工作正常。这里为了简 略,所以把它省略了。实际项目中,特别是当时钟源的质量不一定很好的时候,建议把 这个 locked 引脚引出到 PCB,并点亮一个 LED 灯,这样时钟系统是否正常,可以一目 了然。 连续点击 Next,直到下图,框 1 为当前所在位置,框 2 是要生成的文件列表,除了前两项必 须要生成的,我们再勾选上 bsf 文件即可,剩下的都不需要,点击 Finish,PLL 就生成了。 回到 Quartus II 的主界面,我们双击 Block1.bdf 文件的空白处,得到下图。 22 开始 Qsys 设计 在框 1 中选中 pll,点击 OK,就可以把 pll 放到 bdf 文件中了,如下图。 我们把文件先保存下,在菜单栏选择 File->Save 或者直接 Ctrl+S(笔者 Vito 的习惯就是 不断经常 Ctrl+S,以免一死机成千古恨。Jerry 注☺ ),得到下图,因为刚才新建的 bdf 文件是 整个工程的顶层文件,我们把它命名为 nios_prj.bdf,大家注意下红框内的选项,它的意思是 把该文件添加到工程中,我们自然需要添加,所以默认勾选上。 23 开始 Qsys 设计 24 2.4创建 Qsys 模块 开始 Qsys 设计 接下来我们利用 Qsys 软件来创建一个 Qsys 模块。Qsys 模块其实也是一个 IP,只不过这 个 IP 的功能更强,它以包含其他 IP 和多个可配置的项目。Qsys 模块在 FPGA 中的结构如下图 所示,它与 PLL、FPGA 中其它模块在设计上是并列关系。但 Qsys 这个模块又有其特殊性,它 包含了以 Nios II CPU 为核心的多个 IP 核模块,建立完成后,你就拥有了一个“片上可编程系 统”:通常以 Nios II CPU 为核心,包含存储单元和各类接口(有点像是一个单片机的感觉), 并且可以对其编程。Qsys 创建了硬件,而下一章的 NiosII 则是针对这个 Qsys 模块进行软件 的设计。 在 Quartus II 主界面的菜单栏中选择 Tools->Qsys,得到下图,可以看到,此时 Qsys 模块 中只有一个 clk_0,即时钟,左侧边栏 Library 里面就是在 Qsys 中可以添加的现成 IP 核(一 眼看去种类还是挺多的),我们就在 Library 库中选择和添加自己需要的 IP 核。 25 开始 Qsys 设计 我们先保存下工程,在菜单栏选择 File->Save 或 Ctrl+S(要养成经常保存的好习惯!)都 可,我们把它命名为 kernel,如下图所示。 我们双击 clk_0,得到下图,可以看到系统默认的时钟频率就是 50MHz,所以无需修改频率, 其它配置也无需改动,我们直接 Finish。 26 开始 Qsys 设计 接 着 来 添 加 Nios II CPU , 它 也 是 整 个 Qsys 模 块 的 核 心 。 在 左 侧 边 栏 选 择 Library->Embedded Processors->Nios II Processor,双击,弹出下图所示的 Nios II CPU 配置对话框。框 1 是 CPU 类型的选择,有三种 CPU 可供选择:Nios II/e(经济型)、Nios II/s (标准型)和 Nios II/f(快速型)。 三种 CPU 从性能上来说 Nios II/e 最差,Nios II/s 居中,NiosII/f 最好;从资源消耗上来 说,Nios II/e 消耗最少,Nios II/s 居中,NiosII/f 消耗最多。大家可以根据自己的需求来选择 合适的 CPU,我们这追求资源消耗与性能的平衡,选择 Nios II/s。 如果你选择 Nios II/f,要小心 Cache 问题,它可能会导致 Nios II 操作外部接口异常, 关于这个问题详见附录 1《关于 NIOS II 中操作 PIO 接口异常问题的解决》。 框 2 和框 3 是 Reset Vector 与 Exception Vector 的选项,我们先不管它,待添加完其它 IP 核后再设置。Nios II Processor 中的其它选项无需修改,我们直接点击 Finish,完成 Nios II CPU 核的添加。 27 开始 Qsys 设计 然 后 是 添 加 SDRAM 控 制 器 , 它 用 来 控 制 FPGA 的 片 外 SDRAM 。 我 们 选 择 Library->Memories and Memory Controllers->External Memory Interfaces->SDRAM Interfaces->SDRAM Controller,双击,得到下图,因为 TIGER BOARD 上的外设 SDRAM 是 256Mb(4M x 16Bit x 4 Banks)的,所以设置如下:框 1 的 Presets 为 Custom,框 2 的 Data width 设为 16;框 3 的 Architecture 中 Chip select 为 1,Banks 为 4;框 4 的 Address widths 中 Row 为 13,Column 为 9。其它设置无需修改,设置完成后,点击 Finish,生成 SDRAM Controller。 28 开始 Qsys 设计 然后添加 Flash 控制器,与 SDRAM 控制器类似,它是用来控制 FPGA 的片外 Flash 的。 TIGER BOARD 的 Flash 选用的是 EPCS64,所以我们选择 Library->Memories and Memory Controllers->External Memory Interfaces->Flash Interfaces->EPCS Serial Flash Controller,得到下图,此 IP 核无需修改设置,直接点击 Finish 即可。 29 开始 Qsys 设计 接着添加 System ID。在 Altera 的《Embedded Peripherals IP User Guide》手册中写 到,System ID 是用来校验的一个只读器件,可以防止系统出现异常,这种异常一般指的是 Nios II SBT 的软件工程与 Qsys 工程不匹配。对于它我们无需过多了解,只要知道它是用来校 验 的 就 可 以 了 。 选 择 Library->Peripherals ->Debug and Performance->System ID Peripheral,双击得到下图。同样无需修改设置选项,点击 Finish,搞定。 30 开始 Qsys 设计 最后是添加 JTAG UART,JTAG UART 是实现 PC 机与 Qsys 模块通信功能的接口,有了 它的存在,无需另外添加串口就能实现 PC 机与 Nios II CPU 的串口通信。 选择 Library->Interface Protocols ->Serial->JTAG UART,双击,得到下图。同样无需 任何修改,点击 Finish 搞定。 到此为止,Qsys 工程的基本 IP 核就添加完毕了,如下图所示。但任务远没有完成,底下 的 erros 和 warnings 提醒着我们事还没完。我们需要对 Qsys 工程做一系列工作,才能生成 一个可用、规范的 Qsys 工程。 31 开始 Qsys 设计 首先是改名,每个生成的 IP 核的名字都挂着个 0,看着就别扭。VITO 的个人命名习惯是: 把 IP 核名字用大写字母表示,太长的缩短点,如果有多个相同 IP 核就在名字后面加上从 1 开 始的后缀。当然,这个跟个人习惯有关,没什么标准,自己看着舒服就行。 Qsys 工程的 IP 核改名方式有两种,点击相应名字后,一是鼠标右键->Rename,二是按 F2,二者效果相同。改名生的效果如下图,个人感觉看着舒服多了。 然后是连线。在上图中,Connection 一栏中的线还都未连接,我们需要把它正确的连起 来。把光标放到 Connection 下面的区域,得到下图。 32 开始 Qsys 设计 可以看到,图中出线了密密麻麻的线和点,点有空心和实心两种,空心代表未连接,实心 代表已连接。这里要提到 Qsys 与原来的 SOPC Builder 的不同之处,SOPC Builder 中大部分 的连线已经给开发者连好,需要手动连接的很少,而 Qsys 正好相反,这个 VITO 也不了解是 为什么,给人感觉是一种退步啊。不过也无所谓,自己动手丰衣足食,我们把有连接关系的线 连上,方法是光标放到对应的空心上,单击即可,连接完成后得到下图。 33 开始 Qsys 设计 这时我们再回过头配置 Nios II CPU 中的 Reset Vector 和 Exception Vector。首先我们 来了解下这两个概念,Reset Vector(复位向量)是 CPU 复位后启动时指向的存储器类型和地 址偏移量,存储器一般为掉电不可擦除型,如 Flash; Exception Vector 是 CPU 异常情况时 指向的存储器类型和地址偏移量,存储器可为掉电即擦除型,如 SDRAM。Vector 的计算方法 为 Vector=Base+offset,即最后的复位向量等于存储器的基地址加上偏移地址。 在我们的工程中,把 Reset Vector 指向 EPCS,Exception Vector 指向 SDRAM。双击 CPU,得到下图,我们只需按下图配置框 1 和框 2 中的 memory 即可,offset 不必修改,点 击 Finish 搞定。 接下来我们进行分配地址和锁地址。在 Qsys 工程中,有 Base 栏和 End 栏,它显示的是 IP 核模块的起始地址(基地址)和结束地址,如下图框 1 所示。我们发现很多模块的地址是重 复的,这违背了地址唯一性原则,自然是不允许的,框 2 中的 Error 提示也正是因为此。 34 开始 Qsys 设计 我们需要对地址进行重新分配。方法有两种,自动分配和手动分配。手动分配的优点是能 给模块分配自己希望的地址,但一般没必要,而且手动分配太麻烦。我们这里采用自动分配, 方法是在菜单栏选择 System->Assign Base Addresses,得到下图,可以看到,框 1 中的地 址没有重复的了,框 2 中的 Error 也没了。 35 开始 Qsys 设计 我们接下来锁定 EPCS 和 SDRAM 的地址,在工程中如果添加了新的 IP 核模块需要重新分 配地址后,原来模块的基地址是可能会变的,出于习惯考虑,我们把 EPCS 和 SDRAM 的基地 址锁定上,让它们不再变化(当然不锁定也是木有影响的)。方法是点击相应模块基地址前的小 锁(这时小锁是开着的),小锁就锁上了,模块的基地址也就固定了。修改完得到下图。 事情还没完,我们还得手动连接上中断,这也是 Qsys 与 SOPC Builder 的区别,原来 SOPC Builder 中带有中断的模块与 CPU 之间是自动连接的,到了 Qsys 又得自己动手了。方法与前 面模块间连线类似,我们把光标放到 IRQ 栏,看到了熟悉的空心,还是老方法,点击空心连上, 得到下图。 因为中断号并没有重复,所以我们无需再分配中断优先级。如有分配需要,方法与分配地 36 开始 Qsys 设计 址一样:菜单栏 System->Assign Interrupt Numbers 自动分配,点击中断号手动修改。 最后我们设置下端口引出,这依旧是是 Qsys 给找的“麻烦”,如果某个模块有要引出 Qsys 模块的输入输出端口,我们必须要设置下引出,这在 SOPC Builder 中是不需要的。以 SDRAM 为例,红框里面有 Click to export 字样,点击红框,我们把它命名为 sdram 即可。 我们把光标放到 Connection 栏,大家可以看到 wire 前面的连线发生了变化,如下图所示, 这代表端口成功引出了。 工程中需要端口引出的还有 EPCS,我们把它按照同样方法引出,得到下图 37 开始 Qsys 设计 可以看到,Message 里面的 Error 和 Warning 都没有了,我们的 Qsys 工程也创建完毕, 我们选择主页面上边栏的 Genration,即下图框 1,得到下图:框 2 是仿真选项,因为我们不 进行仿真所以无需修改;框 3 是综合选项,我们选择生成 HDL 文件和 BSF 文件(其实只用到 了 BSF 文件);框 4 是输出路径选择,我们按照图中配置。 最后我们点击框 5 的 Genrate,就开始漫长的编译过程了,我们可以趁机休息下,时间长 38 开始 Qsys 设计 短一般取决于电脑的配置。当出现 Genrate Completed 的时候,如下图,编译就完成了,点 击 Close,Qsys 工程的创建就结束了。 小贴士 Tips:软件偶尔抽抽疯 有时候,Generate 过程中会出现莫名其妙的错误,可能是软件 bug,也可能是电脑系统 的原因。如果一时摸不着头脑,那就保存一下 Qsys 和 QuartusII 工程,重启一下电脑再 试试吧(囧) 我们回到 Quartus II 的主界面,首先我们要把生成的 Qsys 模块添加到 Quartus II 工程中, 否则在后面编译时会报错。选择 Project->Add/Remove Files in Project,关于 Qsys 模块的 添加,方法有两个: 方法一是添加 Qsys 模块的.qsys 文件。如下图所示,点击框 1,找到 kernel.qsys 文件, 点击框 2 的 Add 即可,添加完后点击 OK 回到主界面。该方法优点是只需添加 Qsys 模块的.qsys 文件即可,后面无论 Qsys 模块中再发生任何改动(在 Qsys 中删除或添加 IP 核模块),无需关 心;缺点是每次编译 Quartus II 工程都会同时 Generate Qsys 模块,造成编译速度较慢(尤 其是 Qsys 模块较复杂的时候),因此也会造成即使 Qsys 模块没有改动,只要编译 Quartus II 工程,Nios II 工程就需要更新 BSP,这个问题会在下一章提到。 39 开始 Qsys 设计 方法二是添加 Qsys 模块的.v 文件。依然打开上图所示的添加文件的对话框,点击框 1, 在工程中找到 Qsys 模块的.v 文件,其存放路径在可见 Qsys 软件的 Generation 栏,如下图红 框所示,一般位于 Qsys 模块名/synthesis 文件夹下,。 如下图所示,将 kernel.v 和 submodules 里面的众多.v 文件全部添加到工程中。添加完 后点击 OK 回到主界面。该方法优点是 Quartus II 编译时无需再编译 Qsys 模块,节省时间, 且 Nios II 工程也无需重新生成 BSP;缺点是 Qsys 模块有改动时(添加或删除 IP 核)需要同 时添加或删除相应的.v 文件,过程繁杂。 40 开始 Qsys 设计 两种添加方法各有优缺点,大家可以根据实际情况自选选择,VITO 推荐使用第二种。 接下来我们给 Nios II 核和 PLL 生成管脚。首先双击 bdf 文件的空白处,选择我们用 Qsys 工程创建的名为 kernel 的 Nios II 核,如下图,点击 OK,将其放到 bdf 文件中。 我们给图中的 PLL 和 kernel 生成管脚。选中 PLL,点击右键,选择 Generate Pins for Symbol Ports,如下图红框。 41 开始 Qsys 设计 我们看到,PLL 的输入输出端口都被生成了管脚,如下图。按照同样的方法,我们给 Nios II 核也生成管脚。 我们再添加一个 SDRAM 的时钟管脚,双击空白处,在下图框 1 的位置找到 input,或者 通过框 2 直接搜索,找到后添加到 bdf 文件中。 42 开始 Qsys 设计 之后我们建立 PLL 与 Nios II 核的连接关系,并将 SDRAM 的时钟管脚与 PLL 的输出连接, 删除掉多余管脚,连线,修改管脚名称,得到下图。 43 2.5 管脚分配 开始 Qsys 设计 接下来进行管脚分配操作,管脚分配一般有两种方式,第一种是在 Quartus II 主菜单中选 择 Assignments->Pin Planner,进行管脚分配,这种方法较为直观但是对于较多管脚来讲比 较慢;第二种是用 TCL 脚本文件进行管脚分配。 我们采用 TCL 脚本的方式来分配管脚,首先我们用文件编辑器编写 TCL 脚本文件。以上图 的 clk_in 为例,TIGER BOARD 上时钟是通过 PIN_E15 输入给 clk_in 的,那么分配 clk_in 管 脚的 TCL 脚本的格式如下图, 我们将其它管脚也按照这样的格式编写,编写完成后保存成 pin.tcl 文件,然后导入进来。 导入的方法很简单,Quartus II 菜单栏下选择 Tools->TCL Scripts,得到下图 我们选中 pin.tcl 文件,点击 Run,弹出下图的框,表示导入成功。 这时我们发现 bdf 文件中有些变化,如下图红框中的内容,它代表已经得到分配的管脚。 44 开始 Qsys 设计 小贴士 Tips:关于 SDRAM 时钟的相位偏移 有的同学到这可能犯嘀咕,因为在一些教程中 SDRAM 的输入时钟需要设置一个与系统 时钟(也就是 kernel 的输入时钟)的相位偏移,而本教程中直接使用了系统时钟,其实 两者都没错,当系统时钟的频率较高时,SDRAM 的时钟需要设置一个相位偏移才能确 保正常,比如 100MHz 时 SDRAM 的时钟要偏移-75deg(设置生成 PLL IP 核的 Code Phase Shift 选项),因为在本教程里面系统时钟只有 50MHz(这个已经够用啦),所 以无需特意设置相位偏移。 45 2.6Quartus II 工程配置 开始 Qsys 设计 最后我们对 Quartus II 工程做一些配置。选中 Entity 中有黄色三角那一栏,右键单击,如 下如下图所示。 在弹出的 Device 页面中,选择 Device and Pin Options,如下图红框所示。 我们先来对配置芯片做配置。在弹出的 Device and Pin Options 页面选择侧边栏的 46 开始 Qsys 设计 Configuration,如下图框 1,在框 2 中勾选上 Use configuration device,因为 TIGER BOARD 采用的是 EPCS64 芯片,我们选择 EPCS64。 再选择侧边栏的 Dual-Purpose Pins,将 Name 里面所有管脚的 Value 都改为 Used as regular I/O,点击 OK,配置就完成了。 到此为止,我们就完成了整个 Quartus II 工程和 Qsys 工程的创建。回到 Quartus II 主界 47 开始 Qsys 设计 面,保存下文件,我们开始编译 Quartus II 工程,方法是选择菜单栏 Processing->Start Compilation,或者直接点击界面上的紫色三角形,如下图红框所示。 我们继续等待漫长的编译过程,直到弹出下图的框,表示编译完成。 小贴士 Tips:莫追求“完美”——关于编译报告中的 warnings Quartus II 工程在编译完成后会出现一些 warnings,开始有些同学可能觉得很不爽,想 把 warnings 通通消灭掉,其实这是没有必要也很困难的。一般来说,在软件开发中, errors 和 warnings 是都不允许的;在硬件开发中,errors 同样不允许,但是 warnings 有时却是不可避免的。我们可以看看详细的 warnings 信息,检查下哪些是真正的设计 问题把它改正,至于别的就可以不用管啦。 编译完成后会生成 Compilation Report,如下图所示,我们主要看两个指标,一是框 1 内的逻辑资源消耗,可以看到一共消耗了 14%,二是框 2 内的存储资源消耗,一共消耗了 9%。 这两个指标也是 FPGA 最受关注的两个。 48 开始 Qsys 设计 我们最后介绍下如何下载设计文件(也可以称为固件)。在工程编译完成后,会生成 sof 和 pof 两种格式的设计文件,前者是掉电丢失的,通过 JTAG 口下载;后者是掉电不丢失的, 通过 AS 口下载。TIGER BOARD 同时配备有 JTAG 口和 AS 口,我们在调试时一般用 JTAG 口 下载 sof 文件,而要固化程序时则通过 AS 口下载 pof 文件。下载线一般采用 USB Blaster。 下 载 文 件 的 方 法 很 简 单 , 以下 载 sof 为 例 , 先 插 上 USB Blaster 线 , 选 择 菜 单 栏 Tools->Programmer 或者直接点击下图中框 1 内的标志,就会弹出下图中的 Programmer 对话框,先通过框 2 的 Add File 添加 sof 文件,再通过框 3 设置下载硬件(这里是 USB Blaster), 4 中的 Model 选择 JTAG,再单击 Start 就可以了,框 5 中的 Progress 会从 0 增加直到 100%, 到 100%时代表下载成功。 49 开始 Qsys 设计 至此整个包含 Qsys 模块的 Quartus II 工程就创建完毕了,我们也完成了硬件实现部分的 工作,接下来就可以在此基础上进行 Nios II 的开发了。后我们对 Quartus II 工程做一些配置。 50 2.7 总结 开始 Qsys 设计 在本章中,我们首先创建了一个 Quartus II 工程,其中顶层模块设计采用的是 bdf 文件的 原理图描述方式,这种方式更为直观,尤其适用于顶层较简单的工程。 然后通过 Altera 自带的 IP 核创建了一个 PLL,因为我们给 Nios II CPU 的系统时钟为 50MHz,属于低频率,所以不用去为 SDRAM 设置一个移相的时钟,直接使用系统时钟即可。 接着又创建了一个 Qsys 模块,原来使用过 SOPC Builder 的同学要领会 Qsys 与 SOPC Builder 使用的一些区别,包括手动连线、手动端口引出、手动中断设置等,没使用过 SOPC Builder 的同学照着操作就 OK 了。 在管脚分配时,我们采用的是导入 TCL 脚本文件的方式,这种方式尤其适合大量管脚的分 配,要注意 TCL 脚本的格式。 最后我们对 Quartus II 工程做了一些配置,并介绍了编译工程和下载程序的方法。 通过这章的学习,希望你能学会 Quartus II 和 Qsys 软件的基本操作和工程创建,为后面 学习 Nios II 扫清障碍。因本人水平有限,文中有任何错误请联系我,大家在交流中进步!邮 箱:vito943@qq.com 51 Nios II 软件开发流程 第3章 Nios II 软件开发流程 本本文通过建立第一个 Nios II 工程——Hello,来让大家初步了解 Nios II 的开发流程。 通过本章,你能学到: (1)Nios II SBT 的基本操作和工程创建、编译流程。 (2)烧写含有 Nios II 工程的固件(sof+elf)到 Flash 存储器 本文分为四个部分: 一、概述 二、创建 Nios II 工程 三、编译与运行 四、烧写 Nios II 程序到 Flash 存储器 五、总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 52 3.1 概述 Nios II 软件开发流程 上一章我们完成了含有 Qsys 模块的 Quartus II 工程的建立,但事情可远远没完,我们只 是搭建好了硬件平台,软件开发可还在后头呢。光搭台不唱戏那台子不也就白搭了吗?别急, 接下来这一章我们就开始唱第一出戏:创建第一个 Nios II 工程——Hello。 软件开发都得需要开发环境,我们的开环境就是 Nios II SBT,在 Quartus II 12.0 中,Nios II SBT 的全名是 Nios II Software Build Tools for Eclipse,我们如果接触过较早的 Nios II 开 发,应该还听说过 Nios II IDE 这个开发环境,其实 Nios II SBT 本质上就是 Nios II IDE 的升 级版。 我们先看看 Altera 官网对 Nios II IDE 的介绍: Nios II IDE 是 Nios II 系列嵌入式处理器的基本软件开发工具。所有软件开发任务都可以 Nios II IDE 下完成,包括编辑、编译和调试程序。Nios II IDE 提供了一个统一的开发平台,用 于所有 Nios II 处理器系统。仅仅通过一台 PC 机、一片 Altera 的 FPGA 以及一根 JTAG 下载 电缆,软件开发人员就能够往 Nios II 处理器系统写入程序以及和 Nios II 处理器系统进行通讯。 Nios II IDE 为软件开发提供四个主要的功能:  工程管理器  编辑器和编译器  调试器  闪存编程器 Nios II IDE 基于开放式的、可扩展 Eclipse IDE project 工程以及 Eclipse C/C++开发工 具(CDT)工程。 何为升级版?加量不加价,货真价实嘛。作为升级版,Nios II SBT 的功能与 Nios II IDE 是一样的,两者的区别在于:Nios II IDE 能兼容老的 SOPC Builder 与新的 Qsys 工程,而 Nios II SBT 适用于新的 Qsys 工程(不对啊,量减了!)。下面就来介绍下如何用 Nios II SBT 创建和开发工程。 53 3.2创建 Nios II 工程 Nios II 软件开发流程 首先打开 Nios II SBT 软件,可以在桌面通过快捷方式打开或者在 Quartus II 主界面选择 Tools->Nios II Software Build Tools for Eclipse 打开。 第一次打开会有下图提示,即指定一个 Workspace(工作空间),我们指定创建的 Quartus II 工程所在路径即可,如下图所示,如果想将其设为默认的 Workspace 而不想每次打开 Nios II SBT 都弹出下图,勾选红框。完成上述操作后点击 OK。 点击 OK 后,我们得到下图所示的 Nios II SBT 主界面,看得出它是一个很标准的 SBT 界 面。首先创建一个 Nios II 工程,在主界面选择菜单栏的 New->File->Nios II Application and BSP from Template,即红框中选项。 54 Nios II 软件开发流程 我们得到下图所示的工程创建导向,框 1 是我们需要导入的 SOPC 模块文件所在路径,这 里要说明的是,Nios II 工程的是建立在某个 Qsys 模块(即 SOPC 模块)上的,可以理解为有 什么样的硬件工程,才能有配套的软件工程,对于老版的 SOPC Builder 建立的工程,我们要 导入其生成的.ptf 文件,对于 Qsys,我们则要导入其生成的.sopcinfo 文件,这也是 SOPC Builder 与 Qsys 的区别,我们选择上一章建立的 kernel.sopcinfo 文件所在路径;框 2 是工程 名,命名为 hello;框 3 是工程采用的模板,我们选用经典的 Hello World 模块。 55 Nios II 软件开发流程 点击 Next,得到下图。这里要说一下 Nios II SBT 中引入的一个新的概念:BSP 工程。 小贴士 Tips:何为 BSP? BSP 的全称是 board support package,即板级支持包,学过嵌入式的同学对它肯定不陌 生,它是介于主板硬件和操作系统中驱动层程序之间的一层,一般认为它属于操作系统 一部分,主要是实现对操作系统的支持,为上层的驱动程序提供访问硬件设备寄存器的 函数包,使之能够更好的运行于硬件主板。 Nios II SBT 中的 BSP 工程与 Nios II IDE 中的 systemlib 在功能上是一样的,我们生成了 一个 Qsys 工程后,硬件就固定了,BSP 工程中包含了访问该硬件的函数包,这样软件就能正 56 Nios II 软件开发流程 常的在硬件上运行了。在 Nios II SBT 中,每个 Nios II 工程(我们称之为原工程)都同时有一 个 BSP 工程。BSP 工程的名和路径都默认不修改,我们点击 Finish 完成工程创建。 我们得到下图的界面,我们把 Nios II SBT 主界面分成三个区,分别为目录区、编辑区和 状态区。目录区中显示整个工程的目录,包括原工程和 BSP 工程,原工程中我们只要关注.c 文 件和.h 文件即可,当前原工程中只有一个 hello_world.c 文件;BSP 工程我们主要关注 system.h 文件,里面包含了 Qsys 工程的硬件信息,对我们非常有用,后面的章节中会详细说明。编辑 区用来进行代码的编辑。状态区用来显示工程编译或调试运行的状态信息,比如当前打开的 Console 显示工程目前状态。 57 Nios II 软件开发流程 这里需要说明一下,在 Nios II 工程建立的时候,它被存放在了一个路径下。如果我们改 变它的路径,会出现一系列错误警告,使我们不得不“被迫”记住 Nios II 工程的路径,不能 改变,很不方便,关于这个问题的分析及解决方案可以参考附录文章《四步搞定 NiosII 工程文 件夹目录路径改变》。 在原来的 Nios II IDE 中,我们对工程配置是在 System Library Properties 中,在 Nios II SBT 中,我们是在 BSP Editor 中配置,打开 BSP Editor 的方式如下图所示,右键单击 bsp 工 程,选择 Nios II->BSP Editor 即可,也可以在菜单栏中选择 Nios II->BSP Editor。 58 Nios II 软件开发流程 BSP Editor 的界面如下图,里面,包含了工程的各种配置信息,我们先不用对工程作 配置,退出即可。 59 3.3 编译与运行 Nios II 软件开发流程 接下来我们来编译工程,首先要说一下 Nios II SBT 在编译方式上的改变。在 Nios II IDE 中,每个 Nios II 工程只有原工程,而没有 BSP 工程,要编译该工程的话只需要在目录区选 中原工程,右键单击,选择 Build Project 即可。我们按照这种方式在 Nios II SBT 中编译, 如下图红框所示。 编译的过程中会弹出下图所示的进度条,点击 Run in Background 可以把进度条放在后 台去运行。首次编译因为要生成一些新的工程文件所以速度较慢,后续的修改程序后的编译则 会快很多。 60 Nios II 软件开发流程 编译完 成后状 态区 Console 一 栏会显 示下 图所示 内容 ,我们 可以 看到 hello build complete 字样,表示原工程编译完成。 在状态区选择 Problem,如下图,可以看到没有错误和警告,一切顺利。 这时我们对比下目录区的文件列表,可以发现与编译前相比,原工程多了一些文件, 而 BSP 工程没有任何变化。 61 Nios II 软件开发流程 这就是引入 BSP 工程之后的一个问题。在 Nios II SBT 中,编译工程不仅包括编译原工程, 还包括编译 BSP 工程。如果我们按照上面的方式编译的话,只会编译了原工程,而 BSP 工程 并没有被编译。我们需要在目录区选中 BSP 工程,右键单击,选择 Build Project,来编译 BSP 工程。下图是两个工程都未被编译、只编译原工程后、两个工程都被编译后的文件列表对比, 大家可以好好看下区别。 62 Nios II 软件开发流程 当然,要想图省事一次编译原工程和 BSP 工程也可以,在菜单栏选择 Project->Build All 或者快捷键 Ctrl+B,可以实现两个工程(原工程+BSP 工程)一次性编译,如下图红框所示。 编译完成后,我们就要运行工程了,hello_world 这个例程很简单,就是打印出来一句 “Hello from Nios II!”。需要特别注意的是,在运行 Nios II 前,要确保最新的 Quartus II 工程 sof 文件已经下载到板子中。运行的方法很简单,选中原工程,右键单击选择 Run As, 得到下图。Run As 下面有 4 个选择,分别是 Lauterbach ISS、Local C/C++ Application、 Nios II Hardware 和 Nios II Modelsim,我们主要关注第 Lauterbach ISS 和 Nios II Hardware 63 Nios II 软件开发流程 两种运行方式,其余先不用管。 Lauterbach ISS 方式其实就是 Nios II IDE 中的 Nios II Instruction Set Simulator 方式, 也就是在 PC 端进行软件仿真运行,不过 Nios II SBT 中并没有自带软件仿真器,还需要去官网 下载,并且这种方式只对不涉及到具体硬件的工程有效,比如本工程;如果工程中涉及到了硬 件操作,比如下节中的通过 PIO 控制 LED,这种方式就无能为力了。所以推荐采用 Nios II Hardware 方式去运行工程,当然,前提是你手头有一块开发板哈。 64 Nios II 软件开发流程 关于开发板 走到这一步,你会需要一块开发板。因此对 TIGER BOARD 而言,是一个绝好的插播广 告的机会。如果你还没有开发板,那推荐你购买一款 TIGERBOARD 的开发板,因为后 续教程都会以 TIGER BOARD 开发板为基础,你跟着走会顺一些。不过,作为一个业界 良心的团队,必须实事求是地告诉你,也不是必须要用 TIGER BOARD 开发板。如果你 已经有了类似的 Altera FPGA 开发板,你大可利用好手头的资源,并且对本教程的内容 灵活运用,以不变应万变。愿能祝你一臂之力! Nios II Hardware 即通过硬件在线运行,在选择这种方式之前,要确保你的开发板已经上 电,USB Blaster 与电脑连接并且驱动安装完毕,而且 FPGA 已经下载了上一章生成的 sof 文 件。这时,我们选择 Run As Nios II Hardware,如果是第一次运行工程,有可能会出现下面 的对话框,这是 Run Configuration 对话框,它提示我们 Target Connection 有问题,它其 实是提示没有检测到 USB Blaster。 65 Nios II 软件开发流程 我们翻到 Target Connection 栏,看到 Processor 和 Byte Stream Devices 一栏都是空 的,这就是问题所在。我们点击下图框 1 的 Refresh Connection,可以看到框 2 中的 Processor 和 Byte Stream Devices 出现了 USB Blaster 的信息。点击右下角的 Run,运行工程。 Nios II SBT 会通过 JTAG 电缆(也就是 USB-Blaster)将程序下载到 Qsys 的 RAM 中, 并且运行程序。 我们观察状态区,得到下图,可以看到状态区多了一个 Nios II Console 栏,这个栏里面 是记录硬件在线调试运行时的一些状态,我们看到出现了“Hello from Nios II!”,工程运行成 功。值得注意的是,这里的 printf 语句的输出,是通过 JTAG UART 实现的,而 Nios II Console 正是 JTAG UART 输入、输出的场所(还记得 JTAG UART 吗?忘记的话,请看上一章 Qsys 的 建立)。 在运行过程中,有可能会遇到“Launching ... has encountered a problem”的问 题,其中省略号一般为 Nios II 的工程名,关于该问题的解决方法,可以参考附录文章 《NiosII/Eclipse 中遇到“Launching ... has encountered a problem”的解决方案》。 66 Nios II 软件开发流程 3.4烧写 Nios II 程序到 Flash 存储器 接下来介绍下 Nios II 程序的下载。上一节,虽然实现了 Nios II 程序的运行,但是还无法 实现上电自动启动,原因是程序并没有被烧入非易失存储器(比如 Flash 芯片),导致开发板掉 电后程序丢失。这里来介绍下如何下载 Nios II 程序到开发板到 Flash 存储器中。 要想在 FPGA 中运行 Nios II 程序,需要同时将 Quartus II 工程的 sof 文件和 Nios II 工程的 elf 文件烧写到 Flash 中。有了 sof 文件,FPGA 才能按照设计生成含 Qsys 模块的硬 件电路,为我们搭好了台;有了 elf 文件,才能运行 Nios II 工程中编写的程序,在台上唱戏。 二者缺一不可,本节标题的烧写 Nios II 程序实际上指的是同时烧写 sof 文件和 elf 文件。 小贴士 Tips:烧写前定要“铺好路” 这里需要特别说明的是,在烧写前,要保证 FPGA 中下载了最新的 sof 文件,否则会在 下面介绍的烧写界面的信息中提示 SystemID 和 Timestamp 不匹配,这还是软硬件要匹 配的问题。虽然 sof 文件和 elf 文件是被烧写到 Flash 中,但它是借助 JTAG 和 FPGA 来 实现的,数据通过 JTAG 传输到 FPGA,再由 FPGA 传输到 Flash 中,如果 FPGA 没有下 载最新的 sof 文件,这条路就等于没有铺好,数据自然无法传输到 Flash 中。 下面来介绍烧写方法,FPGA 中下载好最新的 sof 文件后,在 Nios II SBT 主界面选择 Nios II->Flash Programmer,如下图。 67 Nios II 软件开发流程 我们得到了 Nios II Flash Programmer 对话框。首先在对话框菜单栏选择 File->New, 得到下图所示的 New Flash Programmer Setting File,即新建一个 Flash Programmer Setting 文件,这是能成功下载程序的前提。我们选择获得系统信息的方式:通过 BSP 文件还 是通过 sopc 文件,两者没什么区别,效果相同。我们选择通过 BSP 文件,如框 1 所示,找到 BSP 文件所在路径,如框 2 所示,点击 OK。 我们得到下图,框 1 是要烧写到 Flash 的文件所在工程的一些信息,包括 BSP 文件名称和 路径、SOPC 文件名称和路径、CPU 名称和硬件连接状态(指 USB Blaster,因为是通过它来 下载),如果硬件连接信息有问题,可以点击 Connections 去刷新下,方法与前面运行时刷新 类似,就不再详叙。 68 Nios II 软件开发流程 我们把要烧写的文件通过上图框 2 中的 Add 来添加,点击 Add,得到下图,这里我们要 特别注意红框内的文件类型,可以是所有文件、sof 文件或者 elf 文件,我们先添加 elf 文件, 所以选择 elf 文件(当然选择所有文件也可),找到 elf 文件(一般在原工程的目录下),点击 Select。 如下图红框所示,elf 文件已经被添加到了烧写列表中,因为要同时烧写 sof 文件,按同样 69 的方法添加即可。点击 Start,开始烧写。 Nios II 软件开发流程 当底部的 Process 栏中出现如下信息时,说明烧写结束,这样 sof 文件和 elf 文件就被成 功的烧写进 Flash 中了。另外这里简单提一下,因为 sof 文件和 elf 文件会从 Flash 的 0 地址 开始烧写入 Flash 中,如果在 Nios II 程序中包含了对 Flash 的读写操作,一定要注意在程序中 给 Flash 的读写地址设置足够大的地址偏置(offset),防止覆盖掉烧写的文件数据。 70 3.5 总结 Nios II 软件开发流程 继上章创建好 Quartus II 工程和 Qsys 工程,完成硬件准备后,我们在本章开始着手于教 程的核心——Nios II 了。本章主要介绍了如何用 Nios II SBT 完成 Nios II 工程的创建,并成 功运行了第一个 Nios II 程序也是在代码界人气最高的例程——Hello。 首先是 Nios II 工程的创建,这里仍然要提及软件升级之后的改变,告别了 Nios II IDE, 我们迎来了 Nios II SBT,虽然是换汤不换药,新瓶装旧酒,但是我们必须要了解和掌握这些改 变,才能以不变应万变。BSP 工程的引入将硬件的系统信息从原工程独立出来,使 Nios II 的 开发更像专业的嵌入式开发。 在编译时,要注意 BSP 工程的独立性,要分别编译。运行时遇到 Target Connection 问 题不要慌,Refresh Connections 一下就搞定。当看到状态区的“Hello from Nios II!”时, 我们的努力终于看到了回报! 最后介绍了 Nios II 程序的下载,这里仍然是 IDE 进化成 SBT 后的变化,引入了 Flash Programmer Setting 文件的概念。要想实现在 FPGA 上运行 Nios II 程序,sof 和 elf 一个都 不能少。 本章过后,关于软件操作的内容基本已经讲完,后面就可以加足马力,直奔 Nios II 开发 了。因本人水平有限,文中有任何错误请联系我,大家在交流中进步!邮箱:vito943@qq.com 71 PIO 应用 1——控制 LED 第4章 PIO 应用 1——控制 LED 本章主要介绍 PIO 的应用,并让它控制 LED 的亮灭。通过一个流水灯程序的实现,来体 会 Nios II 中用 PIO 操作外围设备,使大家对 Nios II 有更深的了解。通过本章的学习,你能学 到: (1)在 Nios 中利用 PIO 控制外围设备。 (2)用结构体指针方式来控制硬件寄存器。 本章分为四个部分: 一、概述 二、硬件实现 三、软件实现 四、总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 72 4.1 概述 PIO 应用 1——控制 LED 前面又搭台又唱戏的,挺热闹,从这里开始,我们要实打实地来实现一些功能了! 学过嵌入式开发的同学一定忘不了经典的流水灯(又称跑马灯)程序,当我们第一次编写 完流水灯程序并运行,在开发板上看到 LED 如流水般闪烁起来的时候,相信大家当时的兴奋程 度远高于日后完成一个大项目时。流水灯这个经典的例程带领着无数学子迈出了成为嵌入式大 牛的第一步。 从本章起,我们就要真正的“用”开发板了,不单单地要通过电脑去完成工程创建、程序编 写,更要观察开发板现象。本章,从最简单的 PIO 开始。 小贴士 Tips:关于 PIO PIO,全称为 Parallel Input/Output,即并行输入/输出,它属于 Qsys 中的一个常用的 IP 核。Nios II CPU 可以通过 PIO 与外围设备连接,通过改变 PIO 的值等方式来实现对外 设的控制。PIO 可以配置为输入、输出、双向等模式,还可以连接外部中断源来触发 Nios II CPU 中断。 关于中断的内容,我们在下一章再做介绍,本章主要通过 PIO 实现对 LED 的控制。 73 4.2 硬件实现 PIO 应用 1——控制 LED 首先我们来添加 PIO。TIGER BOARD 开发板上配有 4 个 LED,所以我们需要添加一个 4 位的 PIO 来控制 LED。打开已经建好的 Quartus II 工程和 Qsys 工程,我们先在 Qsys 工程中 添加 PIO 软核。 建立 Qsys 的步骤在之前的章节中,请到这里下载 http://www.tigerbd.cn/forum-39-1.html 打开 Qsys 软件后,会得到下图,我们需要导入已经创建好的 Qsys 工程,选中 kernel.qsys 打开即可。 先在左侧边栏的 Component Library 中找到 PIO 软核,双击得到下图,红框中是 Basic Settings,我们将 PIO 位宽设为 4,方向设为输出,初始值设为 0 即可,其它设置如 Output Registere、Edge capture register 等配置无需更改,点击 Finish 完成。 74 PIO 应用 1——控制 LED 将添加的 PIO 改名为 LED,如下图,我们首先将 LED 与其它模块相连,将下图红框中的 3 个空心圆连接上即可。 这里要提一下各个 IP 核模块的连线问题,因为 Qsys 中的连线需要开发者自己搞定,如何 正确连线就非常重要了。一般来说,在 Qsys 工程中,CLK 模块、CPU 模块、ROM 模块和 RAM 模块的连线形式是固定的,Qsys 建立的章节已经给出了正确的连接方法。对于其它 IP 核模块, 一般需要连接时钟输入、复位输入和 Avalon Memory Mapped Slave 对应的端口,分别将其 与 CLK 模块的 clk、clk_reset 和 CPU 模块的 data_master 相连即可,正如 PIO 的连接。 75 PIO 应用 1——控制 LED 连接完成后我们再进行自动分配地址和端口引出工作,具体操作不再详叙,参见 Qsys 建 立的章节。一切顺利的话我们会得到下图,这时可以看到界面底部的 Message 中已经没有了 Errors 和 Warnings。至此就完成了 PIO 在 Qsys 工程中的添加(是不是感觉轻松加愉快?)。 接下我们保存一下,就可以编译工程了,依旧是漫长的等待。出现下图中的 Genrate Completed 时,就大功告成了。 76 PIO 应用 1——控制 LED 回到 Quartus II 主界面,打开顶层文件 nios_prj.bdf,我们更新 kernel 器件,将 PIO 引 出并给它分配管脚。选中 bdf 原理图中的 kernel 器件,右键单击,选择 Update Symbol or Block,更新器件,如下图红框所示。 弹出下图窗口,我们选择更新选中的器件,点击 OK。 77 PIO 应用 1——控制 LED 这时我们发现 kernel 器件有了变化,多出了 led_export 端口,如下图红框所示,是不是 感觉很神奇?其实这是因为更新器件后,kernel 器件会导入新生成的 Qsys 工程的信息,自然 也就有我们刚刚生成的 PIO 模块啦。 我们给新生成的端口生成管脚,方法依旧,选中 kernel,点击右键,选择 Generate Pins for Symbol Ports,得到下图,可以看到 led_export 的管脚已经生成了。这里需要说明的是, 在更新器件或者给新端口生成管脚的过程中如果图中的线乱了,大家整理下即可,只要自己看 着顺眼就行啦。 78 PIO 应用 1——控制 LED 最后再给 led_export 分配管脚,我们仍然用 TCL 脚本的方式分配管脚。在原 TCL 脚本文 件中添加上这 4 个管脚,导入即可,具体过程就不详叙了,不明白的同学可以参考 Qsys 建立的 章节。 上面的工作完成后,就可以编译工程了,继续等待吧,直到编译成功的好消息,至此,硬 件设计就完成了。 79 4.3 软件实现 PIO 应用 1——控制 LED 接接下来我们通过 Nios II SBT 来实现软件部分。新建一个工程,命名为 led,具体过程不 再详叙。建好后得到下图。 我们再来关注下上一章中提到的 system.h 文件,它位于 BSP 工程中,上一章中提到 system.h 文件包含了 Qsys 工程的硬件信息,我们打开文件,通过查找 PIO 模块的信息,了解 下 system.h 文件。直接在文件中搜索 LED(我们在 Qsys 中把 PIO 命名为 LED),得到下图中 的代码部分,可以看到,system.h 文件包含的 LED 这个 PIO 的所有硬件信息,包括模块类型, 基地址、中断信息、数据宽度等等,在 Nios II SBT 中,我们正是通过这些信息实现对硬件的 控制。在本章中,我们要用到 LED_BASE,即基地址。 80 PIO 应用 1——控制 LED 接下来我们在目录区原工程下新建三个文件夹,分别命名为 main、driver 和 inc,从本章 开始,都采用这三个文件夹的结构放置原工程中的程序文件,其中 main 来放置主函数所在的.c 格式源文件,driver 来放置其它.c 格式源文件,inc 放置.h 格式的头文件,这样的文件放置方 式显得整齐而有条理,易于寻找。当然,目录的结构并不唯一,没有最好只有最适合,以自己 舒服为最高标准。 新建文件夹方法为选中目录区的原工程,右键单击,选择 New->Folder,如下图所示。 81 PIO 应用 1——控制 LED 弹出下图的 New Folder 对话框后,框 1 是指定新建文件夹的上级文件夹,因为我们刚才 是在原工程上作的操作所以默认就是原工程 led,框 2 是新建文件夹的名字,我们命名为 main。 小贴士 Tips:面对 Refreshing Makefiles,waitng 即可 点击 Finish 之后会弹出下图所示的对话框,这个在 Nios II SBT 中会经常出现,当工程的 一些设置改变时,它就会弹出刷新 makefiles,无需大惊小怪,慢慢等待,任期自生自 灭即可。 82 PIO 应用 1——控制 LED 这样 main 文件夹就建好了,我们按照这个方法再把 driver 和 inc 文件夹建好,把 hello_world.c 文件移到 main 文件夹中,直接选中文件拖到文件夹中就可以了,这种方法最省 事也最直接。我们把 hello_word.c 改名为 main.c,方法是选中文件,右键单击,选择 Rename, 如下图所示。 我们在弹出的新对话框中把文件名改为 main.c,就 OK 了。 接下来再在 inc 文件夹中新建一个.h 头文件,方法与建文件夹相同,选中 inc 文件夹,右 键单击,选择 New->Header File,弹出下图,需要注意的是红框中的文件模板,我们选择 C 83 头文件模板即可。 PIO 应用 1——控制 LED 至此整个工程的目录结构就已经搭建完毕了,一切顺利的话,目录区应该是下图的文件结 构。 接下来正式开始软件程序的编写了。打开 sopc.h 文件,得到下图,里面空空如也,只有 一个宏定义。需要注意的是,在.h 头文件中这种格式的宏定义是很有必要的,当多个.c 源文件 include 同一个头文件时,它可以有效地防止重复定义,具体原理大家可以去理解下,这里就 84 PIO 应用 1——控制 LED 不多说了。 #ifndef SOPC_H_ #define SOPC_H_ #endif /* SOPC_H_ */ 接下来我们编写 sopc.h 文件的程序,编写完的程序如下图。 #ifndef SOPC_H_ #define SOPC_H_ #include "system.h" //1 #define _LED //2 typedef struct { unsigned long int DATA; unsigned long int DIRECTION; unsigned long int INTERRUPT_MASK; //3 unsigned long int EDGE_CAPTURE; }PIO_STR; #ifdef _LED #define LED ((PIO_STR *) LED_BASE) //4 #endif /*_LED*/ #endif /*SOPC_H_*/ 我们在 sopc.h 中主要加入了 4 部分程序。框 1 是引用 system.h 头文件,因为在程序中要 调用 system.h 中的宏定义信息,所以我们要引入下 system.h。框 3 是这里要重点说明的,它 定义了一个结构体,这个结构体的格式可不是大笔一挥随意写的,它是由 PIO 的硬件寄存器表 决定的。在 Altera 的《Embedded Peripherals IP User Guide》手册中,定义了 PIO 的寄存 器表如下图所示。 85 PIO 应用 1——控制 LED 小贴士 Tips:关于硬件寄存器 在 Nios II 中乃至所有嵌入式系统中,CPU 控制某个硬件模块一般是通过控制该模块寄 存器值的方式实现的,听着有些绕,其实道理很简单。比如 PIO 输出值是 0,我们要把 它改成 1,那么找到控制 PIO 输出值的寄存器,如下图中框 5 所示,该寄存器值 1 代表 输出 1,值 0 代表输出 0,我们把寄存器的值改成 1 就 OK 了。 在 PIO 中共有 6 个寄存器来控制 PIO 的各个状态,其中 data(数据值,框 1)、 direction (方向,框 2)、 interruptmask(中断使能,框 3)、edgecapture(中断发现,框 4)是常 用的 4 个,Offset 代表对应寄存器地址与 PIO 基地址的偏移量,上图框 3 的结构体格式正是 根据 PIO 的寄存器表来设计的,我们定义一个该结构体类型的结构体指针变量 LED 来指向 PIO 的基地址 LED_BASE(正如上图框 4 的操作,上图框 2 是与框 4 搭配的一种固定操作,无实际 意义),则结构体指针的成员变量正好能指向对应的寄存器,从而能通过结构体赋值实现控制寄 存器。 虽然 Altera 中提供了现成的 API 函数可供直接用来实现对硬件模块的控制,但是直接寄存 器赋值的方式效率更高,更重要的是它能帮助我们了解硬件的底层结构,培养硬件的思维方式, 所以推荐大家尽量用直接寄存器赋值的方式。 完成 sopc.h 的编写后,我们再来完成 main.c 的编写。在编写之前,先简单介绍下 LED 实现流水灯的原理。TIGER BOARD 上有 4 个 LED,位于核心板部分,LED 的负极全部接地, 正级的值通过 FPGA 的 4 个管脚给出,正级为 1 时 LED 点亮,为 0 时 LED 熄灭,在本工程中, 4 位 PIO 的值就是 4 个 FPGA 管脚的输出值,我们要实现 4 个 LED 从低到高的顺序依次点亮, 往复循环,如流水一般,也就是经典的流水灯了。编写完的 main.c 文件如下图所示。 #include //1 #include "../inc/sopc.h" int main(void) { int i; 86 PIO 应用 1——控制 LED while(1) { for(i=0;i<4;i++) { LED->DATA = 1 << i; //2 usleep(100000); } } return 0; } 上图的程序中,框 1 引用了两个头文件,分别是 unistd.h 和 sopc.h,unistd.h 是 C 语言 的常用头文件,不必多说;因为文件中要用到 sopc.h 中的宏定义,也需要引入上 sopc.h。 框 2 是在主函数中实现一个 while(1)死循环,使里面的 for 循环不断地执行,我们重点关 注下 LED->DATA = 1 <DATA 是 结 构 体 指 针 指 向 的 地 址 的 内 容 , 也 就 是 LED_BASE 地 址 的 寄 存 器 内 容 , 我 们 通 过 改 变 LED->DATA 的值,来改变寄存器的值,从而改变 PIO 的输出值来控制 LED 的点亮熄灭;通过 移位运算,使 PIO 的位输出总有一位为高,使 LED 总有一个点亮。usleep 是一个直接可用的 延时函数,它能提供大约 1 微秒的延时,里面的参数值是延时的微秒数,我们每次延时大约 0.1 秒。 至此整个 LED 程序就编写完毕了,怎么样,是不是很简单?接下来依旧是编译工程,编译 完成后就可以运行或下载,去开发板上观察现象了。 87 4.4 总结 PIO 应用 1——控制 LED 本章是第一个真正“用”到开发板的 Nios II 工程,我们通过使用 Altera 提供的 PIO IP 核 模块,来对开发板上的 LED 进行控制,实现经典的流水灯。最后运行开发板,我们会看到开发 板上的 4 个 LED 依次点亮,循环往复。 我们首先在硬件工程中添加了 PIO 模块,要注意 PIO 的参数配置;重点介绍了 IP 核模块 的连线原则,防止在连线环节出错。 在软件实现部分,重点要掌握通过结构体指针来控制硬件模块的方法,这也是本章乃至整 个教程的重点内容,今后这种方法我们还会无数次使用,希望大家越用越熟练,用出自己的心 得哈。当然调用 Altera 的 API 方式也并非不可取,有些时候采用结构体指针的方式过于复杂, 而调用 API 则很简单,比如说对 Flash 的操作,这时我们自然采用后者了。 PIO 还可以连接外部中断源来实现对 Nios II CPU 的中断操作,这部分内容我们留在下一 章。因本人水平有限,文中有任何错误请联系我,大家在交流中进步!邮箱:vito943@qq.com 88 PIO 应用 2——外部中断 第5章 PIO 应用 2——外部中断 本章主要介绍 PIO 的应用——外部中断,通过用 PIO 连接的按键来触发 Nios II CPU 的外 部中断,来点亮 LED;同时介绍了 Nios II 中断点调试的使用方法。通过本章的学习,你能学 到: (1)在 Nios II 中利用 PIO 实现外部中断。 (2)按键的原理和使用方法。 (3)在 Nios II 中进行断点调试。 本章分为五个部分: 一、概述 二、硬件实现 三、软件实现 四、断点调试 五、实验总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 89 5.1 概述 PIO 应用 2——外部中断 上一章我们用 PIO 点亮了 LED,终于看到开发板的现象了。不过话说回来,如果 PIO 只 能用来点个 LED,那也忒小看它了。接下来,我们来说说 PIO 的另一个应用:用 PIO 实现外 部中断。 小贴士 Tips:关于中断 简单来说,中断指 CPU 在执行 A 任务时,发生了某个事件(程序自身或外界)使 CPU 暂停对 A 任务的处理,保留现场,去执行 B 任务;B 任务完成后,回到 A 的现场,继 续执行 A 任务。中断有优先级之分,优先级高的中断可以打断优先级低的中断,同级别 中断按先后顺序执行。在嵌入式系统中,中断一般分为定时中断和外部中断,定时中断 由程序定时产生,外部中断则由外设产生。 有道是:有人的地方就有江湖,有 CPU 的地方就有中断,Nios II CPU 自然也少不了中断 嘛。这一章我们就利用 PIO 来实现对 Nios II CPU 的外部中断,方法为:Nios II CPU 通过 PIO 连接某个按键,按键按下后触发中断,CPU 响应外部中断改变 LED 的亮灭(LED 自然也是通 过另一个 PIO 来控制的),下面就来具体介绍下实现。 90 5.2 硬件实现 PIO 应用 2——外部中断 打开 Quartus II 和 Qsys 软件,我们在上一章工程的基础上再增加一个 PIO(当然,不嫌 麻烦重新再建一个工程也可以哈)。先切换到 Qsys 软件,导入 kernel.qsys 文件后,在左侧边 栏的 Library 找到 PIO 模块,双击配置 PIO,如下图,框 1 是 PIO 宽度、方向和输出初始值设 置,我们把宽度设为 1,方向设为输入,因为是输入,所以输出初始值就不用管啦。框 3 是中 断选项,我们勾选上 Generate IRQ(生成中断),中断类型(IRQ Type)有两种:电平触发中 断和边沿触发中断,我们选择电平触发中断。框 2 中是边沿触发中断寄存器的一些设置,如果 要设置边沿触发中断,则需要勾选上 Synchronously capture(同步捕获),再选择是捕获上 升沿(RISING)还是下降沿(FALLING),因为我们在框 3 中选择的为电平触发中断,所以框 2 无需设置。 点击 Finish,完成 PIO 配置,我们将得到的新 PIO 改名为 KEY,并进行连线、端口引出 91 PIO 应用 2——外部中断 和中断连接工作,具体步骤就不讲啦,还没掌握的同学可以回顾下前几章哈。我们得到下图, 要注意红框中的地址和中断号:前面已经讲过,模块的地址区间(基地址和结束地址之间的部 分)不能与其它模块的地址区间重合;对于中断号,EPCS 已经设为 0,JTAG_UART 设为 1, 其它模块按照顺序依次增加即可(在对中断优先级无特殊要求的情况下),事实上这次 Qsys 还 真“人性化”了一回,我们连接上中断后它自动分配了 2,不容易啊! 当然,为了稳妥我们还是给 Qsys 自动分配下地址和中断号(别忘了保存!),得到下图。 最后自然是编译了,点击 Generate,等待编译完成,休息一下吧。 编译完成后我们回到 Quartus II 主界面,先介绍下按键的原理。在 TIGER BOARD 上面配 有 6 个按键,如下图所示,按键的一端连接 FPGA 的某个管脚,并通过上拉电阻接高电平;另 一端接地。当该 FPGA 管脚作为输入时,按键松开时输入高电平,按下时则输入低电平。 92 PIO 应用 2——外部中断 打顶层文件,我们先更新 kernel 模块,得到新生成的 key。因为 Nios II CPU 只对高电平 敏感,所以要给 key 加一个非门作电平转换,添加方法与加入 kernel 模块、输入输出管脚类 似,如下图所示。 接下来再添加输入管脚、连线,分配管脚,最终得到下图,大家注意下红框中的配置。 93 PIO 应用 2——外部中断 保存下文件,我们开始编译 Quartus II 工程,编译成功后,硬件实现就 OK 了。 94 5.3 软件实现 PIO 应用 2——外部中断 接接下来进行软件工程的实现。新建一个 Nios II 工程,命名为 int,在编写程序前先把三 文件夹结构(main、inc 和 driver)搭建好,如果大家觉得在 Nios II 主界面新建文件夹和文 件太慢,这里介绍一个快捷点的方法:在我的电脑下找到原工程(即 int)所在的文件夹,在 文件夹内新建上面的三个文件夹(别忘了将 hello_world.c 改名并移到 main 文件夹中),回到 Nios II 中界面,在目录区右键单击选择 Refresh 即可,怎么样,这样会快点吧。 我们先打开 BSP 工程中的 system.h 文件,看看新添加的名为 KEY 的 PIO 的情况,得到 下图,可以看到,KEY 的基地址(框 1)、宽度(框 4)、方向(框 5)等信息与我们在 Qsys 中 设置的一致,同时,框 2、框 3 和框 6 中关于中断的设置也与预期一致,这正是 system.h 的 伟大之处啊。 接下来我们来完成 sopc.h 文件的编写。我们在 sopc.h 中添加上 KEY 的定义,如下图框 1、 95 PIO 应用 2——外部中断 框 2 所示,方法与上一章添加 LED 类似,不再详述。 #ifndef SOPC_H_ #define SOPC_H_ #include"system.h" #define _LED #define _KEY//1 typedef struct { unsigned long int DATA; unsigned long int DIRECTION; unsigned long int INTERRUPT_MASK; unsigned long int EDGE_CAPTURE; }PIO_STR; #ifdef _LED #define LED #endif/*_LED*/ ((PIO_STR *) LED_BASE) #ifdef _KEY #define KEY #endif/*_KEY*/ ((PIO_STR *) KEY_BASE)//2 #endif/*SOPC_H_*/ 我们在 main.c 中主要完成中断初始化函数和中断服务函数 (ISR,Interrupt Service Routine)的编写。 要实现中断功能,首先要注册中断。在 Nios II SBT 中,中断注册函数的原型为 int alt_ic_isr_register (alt_u32 ic_id, alt_u32 irq, alt_isr_func isr, void*isr_context, void*flags) ic_id 为中断控制器标号,定义于 system.h 之中,在本例中即 KEY_IRQ_INTERRUPT_CONTROLLER_ID。 irq 为设备的硬件中断号,也在 system.h 中定义,即 KEY_IRQ。 isr 为调用的中断服务函数。 isr_context 指向与设备驱动实例相关的数据结构体,一般设为 NULL。 96 flags 保留未用,设为 0 即可。 PIO 应用 2——外部中断 小贴士 Tips:不一样!中断注册函数的变化 接触过较早版本的 Nios II 软件的同学可能会纳闷,这个中断注册函数跟原来的不一样 啊!事实正是如此,Nios II 9.1 版本后的中断注册函数发生的变化,函数名、参数都有 改变,但是功能上并没什么区别。无论你是否接触过原来的中断注册函数,既然已经改 版,大家就顺应历史潮流吧☺。 然后是中断服务函数了,中断服务函数是指触发中断后要执行的函数(也就是概述中的 B 任务),其原型为 void handle_button_interrupts(void* isr_context); handle_button_interrupts 为中断服务函数名称,不固定;isr_context 为给该函数提供 的输入参数。 介绍完这两个函数,我们开始编写程序,完成后得到下图。 框 2 是中断初始化函数,包括中断全能和中断注册:KEY_INTERRUPT_MASK 是 PIO 的 中断使能寄存器,置 1 时中断使能有效,否则无效,我们把它置 1,使中断使能有效;然后按 照上面介绍的格式编写中断注册函数 alt_ic_isr_register,如果初始化成功,函数返回 1,否则 返回 0。 框 1 是中断服务函数,一旦外部中断被触发,中断服务函数就会将 LED 置 1,即点亮 LED。 框 3 是 main 函数。在 main 函数中,首先将 LED 置 0,再判断中断注册是否成功,如果 成功,打印“register successfully!”,否则打印“Error: register failure!”。 #include"../inc/sopc.h" #include"system.h" #include"sys/alt_irq.h" #include #include void ISR_key(void * isr_context)//1 { 97 PIO 应用 2——外部中断 LED->DATA = 1; } int init_key(void) { KEY->INTERRUPT_MASK = 1; return alt_ic_isr_register(KEY_IRQ_INTERRUPT_CONTROLLER_ID, KEY_IRQ,//2 ISR_key, NULL, 0x0); } int main() { LED->DATA = 0; if(!init_key()) { printf("register successfully!\n");//3 } else { printf("Error: register failure!\n"); } return 0; } 整个软件实现就这么多, 怎么样,外部中断也不过如此吧? 98 5.4 断点调试 PIO 应用 2——外部中断 既然讲到了中断,那就顺便介绍下断点调试(DEBUG)。前面我们进行实验验证的时候, 都是采用的直接运行的方式,其实很多时候为方便程序调试,我们也需要在程序中设置一些断 点,让程序执行到断点的时候能暂停下来,这时就需要我们的 DEBUG 来帮忙了。 在进行 DEBUG 前,与运行相同,要确保最新的 Quartus II 工程 sof 文件已经下载到板子 中。打开 Nios II 主界面,选中原工程,右键单击,选择 Debug As->Nios II Hardware,如 下图红框。 首次打开会弹出下图,它提示我们是否切换到调试界面,我们既然要调试自然要切换了, 点击 Yes,也可同时勾选上记住选项省得下次再选。 99 PIO 应用 2——外部中断 我们得到下图,这就是 DEBUG 的主界面。按照惯例,VITO 把它分为目录区、数据区、编 辑区和状态区。 目录区主要列出了一些可供 DEBUG 的函数目录以及一些 DEBUG 操作;数据区提供了一 些可供观察分析的数据,包括变量值、Memory 值等; 编辑区可进行程序编辑和设置断点; 状态区用来观察状态。 在中断中服务函数中设置一个断点,方法是选择设置断点的某行位置,双击即可,如下图 所示。 100 PIO 应用 2——外部中断 断点设置完成后选择目录区的绿色箭头(Resume)开始运行程序,如下图红框所示。 运行后我们发现状态区已经出现了“register successfully!”,表明程序已经开始运行,但 是因为没有触发中断,故没有停在断点,我们这时按下按键触发中断,则程序停在了断点处, 如下图所示。 此时单击 Resume 键程序会继续运行,我们再次触发中断则会继续停留在断点处。断点调 试的基本步骤就是这样了,至于添加观察变量、单步调试之类的功能,这里就不介绍了,有兴 趣的同学可以研究下如何实现哈。 101 5.5 总结 PIO 应用 2——外部中断 本章利用 PIO 模块、按键和 LED,实现了 PIO 的外部中断应用。程序运行后,在状态区 会得到注册成功的提示,如下图所示。 我们按下键盘,就会触发中断,点亮 LED。 小贴士 Tips: 需要说明的是,程序运行中 LED 只会被点亮,而且点亮后就不再熄灭,不会出现按一次 按键 LED 亮灭变换一次的现象,因为本章的中断服务函数执行的是将 LED 点亮而不是 变换亮灭。事实上,因为本章的 PIO 配置的是电平触发中断,按键按下后会一直触发中 断,松开时执行中断服务函数的次数并不确定,所以即使中断服务函数中执行的是 LED 变换亮灭,松开按键后的亮灭也不确定。本章的主要目的就是让大家领会中断的用法。 要想实现按键变换亮灭的现象,可以采用边沿触发中断并加入按按键消抖动。 在硬件实现中,加入了名为 KEY 的 PIO 模块,要注意生成模块时的设置,然后介绍了按 键的工作原理。 在软件实现中,介绍了中断注册函数的原型(用过 Nios II 9.1 之前的中断注册函数的同学 别忘了函数已经升级了哈),最后实现了通过触发中断来点亮 LED 的功能。程序并不复杂,大 家要好好领会中断初始化函数(中断使能+中断注册函数)和中断服务函数的用法。 至此本章的内容就结束了,关于 PIO 的应用也告一段落了。因本人水平有限,文中有任何 错误请联系我,大家在交流中进步!邮箱:vito943@qq.com 102 PIO 应用 2——外部中断 第6章 经典的 RS232 串口 本章主要介绍在 Nios II 中实现开发板与 PC 机之间的串口通信。通过本章,你能学到: (1)在 Nios II 中实现串口通信。 (2)串口调试助手的使用。 本章分为四个部分: 一、概述 二、硬件实现 三、软件实现 四、实验总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 103 6.1 概述 PIO 应用 2——外部中断 对大部分学习嵌入式的同学来讲,如果“Hello,World”是自己编写的第一个程序,流水 灯是看到的第一个硬件现象,那么同样,串口通信实验也必然是实现的第一个通信实验了。串 口的经典不必多说,时至今日,在许多硬件设计时仍然以 RS232 串口进行数据通信,个人认为, “Hello,World”、流水灯和串口,堪称嵌入式开发的三大入门经典! 本章通过在 Nios II 中添加 RS232 串口控制器,来实现开发板与 PC 机的双向串口通信: 开发板通过串口向 PC 机发送数据,PC 机接收数据;PC 机通过串口向开发板发送数据,开发 板接收数据。闲话少说,下面就来介绍下具体的实现。 104 硬件实现 6.2 硬件实现 首 先 在 Qsys 中 添 加 串 口 控 制 模 块 , 位 置 在 左 侧 边 栏 的 Library->Interface Protocols->Serial->UART(RS-232 Serial Port),点击得到下图的模块设置对话框。框 1 是基 本设置,包括数据位、停止位等,我们保持原样,无需修改。框 2 是波特率设置,单位是 bps, 我们保持默认值 115200(这是传统串口最高的传输速率,如果不需要这么高,可以采用 9600bps,这样稳定性会比较好);Baud error 指的是波特率的误差,这个与时钟有关,因为 模块还未生成和连接时钟,所以此时默认值为 0;需要说明的是 Fixed Baud rate 选项,如果 勾选上,那么波特率就固定下来,不能在程序中修改,反之则能修改,我们不勾选。 点击 Finish,依然是对模块进行改名(改为 UART)、连线、连中断、端口引出、分配地址 105 和中断操作,完成后得到下图。 硬件实现 此时我们双击 UART 打开模块设置,因为此时 UART 模块已经连接上了时钟,在波特率设 置中可以看到 Baud error 已经变成了 0.01,怎么样,它的确是与时钟有关吧? 添加完成后,保存,编译 Qsys,老规矩:休息中等待完成(一台配置低的电脑至少可以 让开发者多休息会 Jerry 注☺)。 编译完成后回到 Quartus II 主界面,更新下 kernel 模块,得到添加的 UART 模块引出的 两个 IO 端口:uart_txd 和 uart_rxd,给这两个 IO 端口生成管脚、分配 FPGA 管脚,完成后 得到下图。 uart_txd 和 uart_rxd 分别是串口的发送、接收信号。这组信号在时序上,已经是标准的 串口信号了。然而,我们知道,FPGA 的默认电平是 3.3v LVTTL 的,而 RS-232 串口的电平并 非如此,这里需要一个电平转换。具体转换是通过 TIGER BOARD 上的 MAX3232 芯片完成的。 保存并编译 Quartus II 工程,编译成功后硬件实现部分就完成了,下面我们来完成软件部 分的实现。 106 软件实现 6.3 软件实现 打开 Nios II SBT,新建一个 Nios II 工程命名为 uart,建好后先把目录区整理成三文件夹 结构。 首先打开 system.h,我们在里面找到新添加的 UART 的相关信息,如下图所示,可以看 到 UART 的波特率(UART_BAUD)、数据位长(UART_DATA_BITS)等信息与我们设置的一 致。 /* * UART configuration * */ #define ALT_MODULE_CLASS_UART altera_avalon_uart #define UART_BASE 0x1000 #define UART_BAUD 115200 #define UART_DATA_BITS 8 #define UART_FIXED_BAUD 0 #define UART_FREQ 50000000u #define UART_IRQ 3 #define UART_IRQ_INTERRUPT_CONTROLLER_ID 0 #define UART_NAME "/dev/UART" #define UART_PARITY 'N' #define UART_SIM_CHAR_STREAM "" #define UART_SIM_TRUE_BAUD 0 #define UART_SPAN 32 #define UART_STOP_BITS 1 #define UART_SYNC_REG_DEPTH 2 #define UART_TYPE "altera_avalon_uart" #define UART_USE_CTS_RTS 0 #define UART_USE_EOP_REGISTER 0 然后在 inc 文件夹中新建两个.h 头文件,分别命名为 sopc.h 和 uart.h。 我们先编写 sopc.h,编写后的 sopc.h 文件如下图所示。 #ifndef SOPC_H_ #define SOPC_H_ #include "system.h" #define _UART //1 107 软件实现 typedef struct { union{ struct{ volatile unsigned long int RECEIVE_DATA volatile unsigned long int NC }BITS; volatile unsigned long int WORD; }RXDATA; :8; :24; union{ struct{ volatile unsigned long int TRANSMIT_DATA :8; volatile unsigned long int NC :24; //2 }BITS; volatile unsigned long int WORD; }TXDATA; union{ struct{ volatile unsigned long int PE volatile unsigned long int FE volatile unsigned long int BRK volatile unsigned long int ROE volatile unsigned long int TOE volatile unsigned long int TMT volatile unsigned long int TRDY volatile unsigned long int RRDY volatile unsigned long int E volatile unsigned long int NC volatile unsigned long int DCTS volatile unsigned long int CTS volatile unsigned long int EOP volatile unsigned long int NC1 } BITS; volatile unsigned long int WORD; }STATUS; :1; :1; :1; :1; :1; :1; :1; :1; :1; :1; :1; :1; :1; :19; union{ struct{ volatile unsigned long int IPE :1; volatile unsigned long int IFE :1; volatile unsigned long int IBRK :1; volatile unsigned long int IROE :1; volatile unsigned long int ITOE :1; volatile unsigned long int ITMT :1; volatile unsigned long int ITRDY :1; volatile unsigned long int IRRDY :1; volatile unsigned long int IE :1; volatile unsigned long int TRBK :1; volatile unsigned long int IDCTS :1; volatile unsigned long int RTS :1; volatile unsigned long int IEOP :1; volatile unsigned long int NC :19; //2 }BITS; 108 软件实现 volatile unsigned long int WORD; }CONTROL; union{ struct{ volatile unsigned long int BAUD_RATE_DIVISOR :16; volatile unsigned long int NC :16; }BITS; volatile unsigned int WORD; }DIVISOR; }UART_ST; #ifdef _UART #define UART ((UART_ST *) UART_BASE) //3 #endif /*_UART */ #endif /*SOPC_H_*/ 框 2 长长的结构体定义可不是随便定义的,它根据 Altera 公司《Embedded Peripherals IP User Guide》手册中 UART IP 核部分的 UART 模块寄存器地址映射来编写,以达到利用结 构体控制 IP 核寄存器的目的,如下图所示,这个结构体与 PIO 应用里面定义的结构体本质上 是一样的,只是形式上复杂些。 框 1 和框 3 不用多说,它将定义的结构体指向了 UART 模块的基地址。 然后编写 uart.h,完成后得到下图。 #ifndef UART_H_ #define UART_H_ #include "../inc/sopc.h" #define BUFFER_SIZE 200 //1 109 软件实现 typedef struct{ unsigned int receive_flag; unsigned int receive_count; unsigned char receive_buffer[BUFFER_SIZE]; int (* send_byte)(unsigned char data); void (* send_string)(unsigned int len, unsigned char *str); //2 int (* init)(void); unsigned int (* baudrate)(unsigned int baudrate); }UART_T; extern UART_T uart; #endif /*UART_H_*/ 框 1 是一个宏定义,BUFFER_SIZE 代表缓存的尺寸。框 2 这个自定义结构体需要重点说 明下,它代表了一种模仿面向对象编程的一种思想,我们把与 UART 有关的所有变量、函数全 部封装在这个结构体中,当我们需要对 UART 做一些操作时,只需要通过结构体接口即可,无 需知道结构体里面的具体内容。 接下来编写驱动文件,在 driver 文件夹新建一个.c 文件命名为 uart.c,完成编写后得到下 图。 //----------------include-----------------// #include "sys/alt_irq.h" #include #include #include "../inc/uart.h" #include "../inc/sopc.h" //----------------函数声明-----------------// static int uart_send_byte(unsigned char data); static void uart_send_string(unsigned int len, unsigned char *str); static int uart_init(void); static void uart_ISR(void *context); static int set_baudrate(unsigned int baudrate); //----------------结构体初始化-----------------// UART_T uart={ .receive_flag=0, .receive_count=0, .send_byte=uart_send_byte, 110 软件实现 .send_string=uart_send_string, .init=uart_init, .baudrate=set_baudrate }; /* * === FUNCTION ======================================================= * Name: uart_send_byte * Description: 发送一个Byte * ====================================================================== */ int uart_send_byte(unsigned char data) { UART->TXDATA.BITS.TRANSMIT_DATA = data; while(!UART->STATUS.BITS.TRDY); return 0; } /* * === FUNCTION ======================================================= * Name: uart_send_string * Description: 串口发送函数 * ====================================================================== */ void uart_send_string(unsigned int len, unsigned char *str) { while(len--) { uart_send_byte(*str++); } } /* * === FUNCTION ======================================================= * Name: uart_init * Description: 串口初始化 * ====================================================================== 111 软件实现 */ int uart_init(void) { //将波特率设置为115200 set_baudrate(115200); //将控制寄存器IRRDY置1,在接收准备好后使能中断 UART->CONTROL.BITS.IRRDY=1; //将状态寄存器全部清零 UART->STATUS.WORD=0; //为UART注册中断 alt_ic_isr_register(UART_IRQ_INTERRUPT_CONTROLLER_ID,UART_IRQ,uart_ISR,NU LL,0x0); return 0; } /* * === FUNCTION ======================================================= * Name: uart_ISR * Description: 串口中断服务函数 * ====================================================================== */ static void uart_ISR(void *context) { //等待状态寄存器RRDY置1,当RRDY为1时表示接收的数据已经传输到RXDATA中 while(!(UART->STATUS.BITS.RRDY)); uart.receive_buffer[uart.receive_count++] = UART->RXDATA.BITS.RECEIVE_DATA; //接收的数据以换行符'\n'为结束标志,所以PC端的发送数据最后必须添加上'\n' if(uart.receive_buffer[uart.receive_count-1]=='\n'){ uart.receive_buffer[uart.receive_count]='\0'; uart.receive_count=0; uart.receive_flag=1; 112 软件实现 } } /* * === FUNCTION ======================================================= * Name: set_baudrate * Description: 设置波特率 * ====================================================================== */ int set_baudrate(unsigned int baudrate) { UART->DIVISOR.WORD=(unsigned int)(ALT_CPU_FREQ/baudrate+0.5); return 0; } 驱动文件的内容较多,主要是对串口通信用需要的一些函数的定义,包括 uart_send_byte() (发送字节函数)、uart_send_string()(串口发送函数)、uart_ISR()(串口中断服务函数)、 uart_init()(串口初始化函数)和 set_baudrate()(设置波特率函数):uart_send_byte()用来 实现将 1 个字节从开发板发送到 PC 机;uart_send_string()通过调用发送字节函数,用来将字 符串数据从开发板发送到 PC 机端;uart_ISR()用来接收从 PC 机传输过来的数据;uart_init() 用来初始化串口模块;set_baudrate()用来设置串口的波特率。我们拥有了这 5 个函数,就能 拥有了串口通信的资本了。 小贴士 Tips:波特率的设置原理 在生成串口控制模块时没有固定波特率的前提下,可以以软件的方式重新设置波特率, 它采用向串口控制模块的 DIVISOR 寄存器赋值的方法来设置波特率,公式依据于 Altera 公司的《Embedded Peripherals IP User Guide》,如下图所示。 113 软件实现 最后来编写 main.c,完成后得到下图,先是 include 一些必要的头文件,然后是主函数部 分:先定义了一个字符串 buffer,然后对串口初始化;接下来是一个 while(1)循环,在循环中, 每隔 500ms 不断地检测串口接收的标志位 uart.receiver_flag,如果无效,说明串口没接收到 数据,那么通过串口发送函数向 PC 机发送 buffer 中的内容;如果有效,说明串口接收到了数 据,则修改 buffer 中的内容为收到的数据,这样使开发板发送到 PC 机的数据总是 PC 机上次 发送给开发板的数据。 #include "system.h" #include "sys/alt_irq.h" #include #include #include #include "../inc/uart.h" #include "../inc/sopc.h" int main(void) { unsigned char buffer[50]="Hello FPGA!\n"; uart.init(); while(1) { if(uart.receive_flag) { memset(buffer,0,50);// clear buffer strcpy(buffer,uart.receive_buffer);//copy buffer uart.receive_flag = 0;//clear flags } uart.send_string(sizeof(buffer),buffer); usleep(500000); } return 0; } 至此软件实现就完成了,软件部分的程序比较复杂,大家要好好理解吸收。 114 6.4 总结 DS1302 应用——RTC 完成软硬件的开发后,我们需要在串口调试助手的帮助下完成串口通信实验。串口调试助 手是串口调试的必备软件,网上版本很多,功能相似,大家随便下载一个就可以。 打开串口调试助手,得到下图(界面根据版本不同而不同,但是组成都基本相似),我们把 软件界面分为串口设置区、接收区、发送区、其它设置区。 串口设置区可以设置串口的一些基本参数,包括波特率、数据位长等,这个要保持与开发 板端的一致,串口号可以在电脑的设备管理器中查询到;接收区用来显示 PC 机接收到的数据; 发送区用来输入发送数据,然后发送到开发板端;其它设置区是一些其它功能的设置,这里保 持默认。 我们设置好串口调试助手后,点击串口设置区的打开键,运行 Nios II 工程,一切顺利的 话,就可以接到开发板上发送过来的“Hello FPGA!”了,如下图所示。 115 DS1302 应用——RTC 我们在发送框内输入“Hello TIGER BOARD!”,要注意输入完成后要点击回车键以生成绿 色的发送按键,得到下图,可以看到,开发板发送过来的数据已经变成了“ Hello TIGER BOARD!”,怎么样,神奇吧?如果得到这些现象,说明我们的串口通信成功了。 116 DS1302 应用——RTC 实验进行到这里,有的同学可能会发现输入“Hello TIGER BOARD!”后串口调试助手接 收的数据并没有改变,这很有可能是发送数据缺少换行符’\n’导致的。软件实现一节已经讲 到,Nios II CPU 接收串口数据以’\n’作为数据结束的标志,当串口调试助手发送的数据不 含’\n’时,CPU 无法判断数据结束,所以也不会通过串口返回接收到的数据。 为了验证这一猜想,将其它设置区的按十六进制发送选项勾选上,如下图所示,这时待发 送的数据都转变成了十六进制 ASCII 码格式,可以看到最后为’!’的 ASCII 码 21 而非‘\n’ 的 ASCII 码 0A。 这时将换行符的 ASCII 码加上,如下图所示,再点击发送,可以看到调试助手收到的正是 发送的数据,表明功能正常了。 也可以在输入完数据(输入时不勾选按十六进制发送)后直接单击回车键再发送,调试助 手会自动添加回车键和换行符的 ASCII 码,这时再勾选上按十六进制发送,可以看到添加了 0D 和 0A 两个 ASCII 码,如下图所示。 117 DS1302 应用——RTC 当然,有的 调试助手 可能会自 动添加换 行符,这 个问题就 不存在了 。总之, 当本实验 的 串 口调试助手发送数据功能不正常时,要先检查下发送的数据中是否包含了 Nios II CPU 判断数 据结束的标志——’\n’。 本章通过添加串口控制模块,实现了 Nios II CPU 与 PC 机的串口通信。 在硬件实现部分,我们在 Qsys 中添加了串口控制模块,要注意参数的设置;然后将 uart_txd 和 uart_rxd 两根线引出 FPGA 连接外设。 在软件实现部分,我们编写了 sopc.h 和 uart.h 两个头文件,仍然使用了通过结构体给硬 件寄存器赋值的方式,编写了驱动文件 uart.c,定义了串口通信需要的几个函数,最后编写了 main.c,实现了串口通信。 串口调试助手的使用大家务必掌握,它是调试串口的必备神器。当发送数据不正常时,要 检查下是否发送了换行符’\n’。 希望大家能好好掌握这一章的内容,单是串口本身的重要性已经不用多言,更何况在今后 的某些实验中,我们可能还会借助串口通信来进行实验。这一章的内容就到这里,因本人水平 有限,文中有任何错误请联系我,大家在交流中进步!邮箱:vito943@qq.com 118 DS1302 应用——RTC 第7章 DS1302 应用——RTC 本章主要介绍了在 Nios II 中利用实时时钟芯片 DS1302 实现 RTC 功能。通过本章,你能 学到: (1)在 Nios II 中利用 DS1302 实现 RTC 功能。 本章分为四个部分: 一、概述 二、硬件实现 三、软件实现 四、总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 119 概述 7.1 概述 RTC(Real Time Clock),中文译作实时时钟,通俗点讲就是在硬件电路上实现一个实时的时 钟,提供年月日时分秒信息,而且掉电后仍然能继续工作(当然得有后备电池啦!)。要在开发 板上实现 RTC,需要实时时钟芯片的帮助,先来介绍下 TIGER BOARD 上自带的实时时钟芯片— —DS1302。 DS1302 是由美国 DALLAS 公司推出的具有涓细电流充电能力的低功耗实时时钟芯片。 DS1302 在实时显示时间中的应用广泛,它可以对年、月、日、周、日、时、分、秒进行计时, 且具有闰年补偿等多种功能。 RTC 模块的典型原理图如下图,U2 是 DS1302 芯片,它的 X1、X2 管脚连接了一个晶振,用 来提供频率源;CPU 则主要通过 SCLK、DATA 和 RST 来控制 DS1302,实现 RTC 功能;此外,U4 是 DS1302 的备用电源,在硬件电路掉电时可以为 DS1302 供电,在 TIGER BOARD 上保留了电池 接口,连接 CR1220 电池即可,本章不会涉及到备用电源的使用,感兴趣的同学可以自己研究 下哈。 120 硬件实现 7.2 硬件实现 上面的原理图告诉我们,CPU 通过三个管脚就能实现对 DS1302 的控制,那我们就先在 Qsys 中添加三个 1 位宽度的 PIO:RTC_SCLK、RTC_DATA 和 RTC_nRST,其中 RTC_SCLK 和 RTC_nRST 方向设置为输出;RTC_DATA 方向设为双向(Bidir),因为 CPU 与 DS1302 的数据 传输是双向的。其它设置不变,生成后改名、连线、端口引出、分配地址,得到下图。 然后保存并编译 Qsys,等待编译成功后打开 Quartus II,更新 kernel 来得到新添加的三 个 PIO 的 IO 端口,并生成管脚、分配管脚,完成后得到下图,可以看到,rtc_sclk 和 rtc_nrst 都是输出管脚,而 rtc_data 则是双向管脚。 然后编译 Quartus II 工程,完成硬件实现。硬件部分的实现非常简单,只是添加了三个 PIO,其实我们建好基本的 Qsys 模块并操作熟练了之后,硬件实现部分就是分分钟搞定的事, Nios II 的开发工作主要还是软件实现:这不正是“搭台为辅,唱戏为主”的道理吗? 121 总结 7.3 软件实现 接下来我们来完成软件实现。打开 Nios II SBT,新建一个名为 rtc 的工程,建好后先在目 录区把结构整理成三文件夹结构。 在 inc 文件夹中新建两个.h 头文件:sopc.h 和 ds1302.h。 我们先编写 sopc.h,完成后得到下图,里面没什么新鲜内容,就是定义了控制三个 PIO 寄存器的结构体。 #ifndef SOPC_H_ #define SOPC_H_ #include "system.h" #define _RTC typedef struct { unsigned long int DATA; unsigned long int DIRECTION; unsigned long int INTERRUPT_MASK; unsigned long int EDGE_CAPTURE; }PIO_STR; #ifdef _RTC #define RTC_SCLK #define RTC_DATA #define RTC_RST #endif /* _RTC */ ((PIO_STR *) RTC_SCLK_BASE) ((PIO_STR *) RTC_DATA_BASE) ((PIO_STR *) RTC_NRST_BASE) #endif /*SOPC_H_*/ 然后编写 ds1302.h,完成后得到下图,主要是做了两个宏定义,目的是使用时方便些; 然后定义了一个结构体,里面包含了控制 DS1302 的所有函数,这个与上一章串口定义的结构 体相似,也不是新鲜货哈。 #ifndef DS1302_H_ #define DS1302_H_ #include "../inc/sopc.h" #define RTC_DATA_OUT #define RTC_DATA_IN RTC_DATA->DIRECTION = 1 RTC_DATA->DIRECTION = 0 122 总结 typedef struct{ void (* set_time)(unsigned char *ti); void (* get_time)(char * ti); }DS1302_STR; extern DS1302_STR ds1302; #endif /*DS1302_H_*/ 接下来在 driver 文件夹中新建一个.c 源文件 ds1302.c,我们来编写驱动程序,完成后得 到下图。 #include "../inc/ds1302.h" static void delay(unsigned int dly); static void write_1byte_to_ds1302(unsigned char da); static unsigned char read_1byte_from_ds1302(void); static void write_data_to_ds1302(unsigned char addr, unsigned char da); static unsigned char read_data_from_ds1302(unsigned char addr); void set_time(unsigned char *ti); void get_time(char *ti); DS1302_STR ds1302={ .set_time = set_time, .get_time = get_time }; /* * === FUNCTION ======================================================= * Name: delay * Description: 延时函数 * ====================================================================== */ void delay(unsigned int dly) { for(;dly>0;dly--); } /* * === FUNCTION ======================================================= * Name: write_1byte_to_ds1302 * Description: 向ds1302写入1 byte数据 * ====================================================================== */ void write_1byte_to_ds1302(unsigned char da) { unsigned int i; RTC_DATA_OUT; for(i=8; i>0; i--){ 123 总结 if((da&0x01)!= 0) RTC_DATA->DATA = 1; else RTC_DATA->DATA = 0; delay(10); RTC_SCLK->DATA = 1; delay(20); RTC_SCLK->DATA = 0; delay(10); da >>= 1; //相当于汇编中的 RRC } } /* * === FUNCTION ======================================================= * Name: read_1byte_from_ds1302 * Description: 从ds1302读取1 byte数据 * ====================================================================== */ unsigned char read_1byte_from_ds1302(void) { unsigned char i; unsigned char da = 0; RTC_DATA_IN; for(i=8; i>0; i--){ delay(10); da >>= 1; //相当于汇编中的 RRC if(RTC_DATA->DATA !=0 ) da += 0x80; RTC_SCLK->DATA = 1; delay(20); RTC_SCLK->DATA = 0; delay(10); } RTC_DATA_OUT; return(da); } /* * === FUNCTION ======================================================= * Name: write_data_to_ds1302 * Description: 向ds1302写入数据 * ====================================================================== */ void write_data_to_ds1302(unsigned char addr, unsigned char da) { RTC_DATA_OUT; 124 总结 RTC_RST->DATA = 0;//复位,低电平有效 RTC_SCLK->DATA = 0; delay(40); RTC_RST->DATA = 1; write_1byte_to_ds1302(addr); // 地址,命令 write_1byte_to_ds1302(da); // 写1Byte数据 RTC_SCLK->DATA = 1; RTC_RST->DATA = 0; delay(40); } /* * === FUNCTION ======================================================= * Name: read_data_from_ds1302 * Description: 从ds1302读取数据 * ====================================================================== */ unsigned char read_data_from_ds1302(unsigned char addr) { unsigned char da; RTC_RST->DATA = 0; RTC_SCLK->DATA = 0; delay(40); RTC_RST->DATA = 1; write_1byte_to_ds1302(addr); da = read_1byte_from_ds1302(); RTC_SCLK->DATA = 1; RTC_RST->DATA = 0; delay(40); return(da); } /* * === FUNCTION ======================================================= * Name: set_time * Description: 设置时间 * ====================================================================== */ void set_time(unsigned char *ti) { unsigned char i; 125 总结 unsigned char addr = 0x80; write_data_to_ds1302(0x8e,0x00); // 控制命令,WP=0,写操作 for(i =7;i>0;i--){ write_data_to_ds1302(addr,*ti); // 秒 分 时 日 月 星期 年 ti++; addr +=2; } write_data_to_ds1302(0x8e,0x80); // 控制命令,WP=1,写保护 } /* * === FUNCTION ======================================================= * Name: get_time * Description: 获取时间 ,读取的时间为BCD码,需要转换成十进制 * ====================================================================== */ void get_time(char *ti) { unsigned char i; unsigned char addr = 0x81; char time; for (i=0;i<7;i++){ time=read_data_from_ds1302(addr);//读取的时间为BCD码 ti[i] = time/16*10+time%16;//格式为: 秒 分 时 日 月 星期 年 addr += 2; } } 在驱动程序中,write_1byte_to_ds1302()和 read_1byte_from_ds1302()是两个最基本的 函数,它们的功能分别是向 ds1302 写入 1byte 数据和从 ds1302 读取 1 byte 数据,写和读 的时序来自 DALLAS 公司的官方手册,如下图所示。 126 总结 write_data_to_ds1302()、read_data_from_ds1302()两个函数通过调用上述两个基本函 数(write_1byte_to_ds1302()和 read_1byte_from_ds1302())来实现 Nios II CPU 与 DS1302 之间的数据读写。set_time()函数功能为给 DS1302 设置初始化时间,get_time()函数功能为 从 DS1302 获取实时时间。 小贴士 Tips:DS1302“不寻常”的数据格式——8421BCD 这里有一点需要注意,DS1302 输入和输出的数据都是 8421BCD 码格式,所以在程序中 要对输入 DS1302 前的数据和从 DS1302 输出的数据做相应的转换。在本程序中,在将 数据输入 DS1302 时,我们直接输入数据的 16 进制格式即可(比如设置年为 14,则直 接输入 0x14);对于从 DS1302 输出的数据,则通过 get_time 函数中的转换方式来进 行数据转换。 最后依然是 main.c 文件的编写,完成后得到下图。 #include #include #include #include #include "../inc/ds1302.h" 127 总结 //格式为: 秒 分 时 日 月 星期 年 unsigned char time[7] = {0x00,0x55,0x19,0x26,0x05,0x22,0x14}; /* * === FUNCTION ======================================================= * Name: main * Description: 主函数 * ====================================================================== */ int main() { ds1302.set_time(time); //设置实时时钟的时间 while(1){ ds1302.get_time(time); //采集时间 printf("20%d-%d-%d %d:%d:%d\n", time[6],time[4],time[3],time[2],time[1],time[0]); usleep(1000000); } return 0; } 在 main.c 中,首先定义了一个 char 型的数组,把初值设为当前时间(2014 年 5 月 26 日 19 点 55 分 0 秒)。在 main 函数中,首先通过 set_time()函数把当前时间传递给 DS1302 芯片,芯片得到时间后以此作为初始时间开始计时;在 while(1)循环中每隔大约 1 秒通过 get_time 函数从 DS1302 芯片获取实时时间,然后打印出来。当然这里也可以通过串口把实 时时间传递到 PC 机的调试助手上显示出来,顺便可以复习下上一章的串口内容哈。 最后提出一点,实际使用的时候,一般不会在 main 函数里进行时间设置。因为这样每次 上电都会新设置一次时间,并不是预期的效果。一般会专门用一个函数,只在需要重新设置时 间的时候,调用它。 软件实现的内容就这么多,到这里就全部完成了。 128 7.4 总结 DS1302 应用——RTC 完成软硬件的开发后,我们运行 Nios II,如果大家采用的是打印显示时间的方式,会得到 下图的现象,怎么样,RTC 还是挺准的吧? 本章介绍了在 Nios II 中利用 DS1302 芯片实现 RTC 功能。 在硬件实现部分,添加了三个 PIO(RTC_SCLK、RTC_nRST 和 RTC_DATA)来控制 DS1302 芯片,要注意 RTC_DATA 要设置成双向。 在软件实现部分,编写了 sopc.h、ds1302.h、ds1302.c 和 main.c 四个文件,实现了 RTC 功能:先向 DS1302 设置初始时间,然后每隔固定时间从 DS1302 芯片处获得实时时间并显示。 这里要注意两点:一是 DS1302 的数据采用的是 8421BCD 码,有兴趣的同学可以了解下这种 编码;二是根据芯片手册实现 DS1302 芯片读写数据的时序,大家不仅要掌握 DS1302 的读写 时序,更要掌握这种把时序图变为程序的思维,这在嵌入式开发中非常重要! RTC 的内容就全部讲完了,但时间应用才刚刚开始,下一章我们来说说介绍下 Qsys 自带 的计时模块——Interval Timer。这一章的内容就到这里,因本人水平有限,文中有任何错误 请联系我,大家在交流中进步!邮箱:vito943@qq.com 129 Interval Timer 应用——定时器、System Clock 与 Timestamp 第8章 Interval Timer 应用——定时 器、System Clock 与 Timestamp 本章主要介绍在 Nios II 中利用 Interval Timer 模块实现定时器、System Clock 和 Timestamp 功能,通过本章,你能学到 (1)Interval Timer 模块的生成和使用。 (2)在 Nios II 中实现定时器功能。 (3)在 Nios II 中实现 System Clock 功能。 (4)在 Nios II 中实现 Timestamp 功能。 本章分为六个部分: 一、概述 二、硬件实现 三、软件实现——定时器 四、软件实现——System Clock 五、软件实现——Timestamp 六、总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 130 概述 8.1 概述 在上一章我们利用 Nios II CPU 和 DS1302 实现了 RTC 功能。有的同学可能会思考: DS1302 不就是因为接了个晶振就能精确计时了么,Nios II CPU 本身就有晶振(系统时钟), 难道自己就不能计时?答案是能的,毕竟求人不如求己嘛,没了 DS1302,咱 Nios II 照样能计 时。实际上,DS1302 的意义更多体现在系统断电后,依靠它的备用电池,照样可以计时。在 不掉电的情况下,Nios II 利用自带的 IP 核模块能实现定时器、System Clock 和 Timestamp, 而这个模块就是 Interval Timer。 先介绍下 Interval Timer 模块,中文译为间隔计时器,它是 Nios II 中用来实现计时功能 的 IP 核模块,有了它,我们就可以在 Nios II 中实现精确计时,定时器、System Clock 和 Timestamp 都是基于 Interval Timer 来实现的,下面分别来介绍下这个三个概念和本章要实 现的功能。 定时器是利用 Interval Timer 的计时功能,设置计时周期,每隔计时周期触发一次中断来 实现某些功能,每个 Nios II 工程可以有多个定时器,它们可以配合实现较复杂的功能。在本 章中,我们通过两个定时器来实现流水灯闪烁频率的变化。 System Clock,中文译为系统时钟,其实它的功能与定时器类似,也是每隔计时周期触发 一次中断来实现某些功能,不过每个 Nios II 只能有一个 System Clock,我们可以指定所有 Interval Timer 中的一个为 System Clock。在本章中,我们利用 System Clock 实现流水灯。 Timestamp,中文译为时间戳,我们可以把它理解为一个在后台运行的时钟,每调用一次 就得到当前的时间,(就好像在这个时间上盖了一个戳,是不是很形象),每个 Nios II 也只能 有一个 Timestamp。在本章中,我们利用 Timestamp 来测量函数的执行时间。 131 硬件实现 8.2 硬件实现 首先在 Qsys 添加 Interval Timer 模块。打开 Qsys 软件,在左侧边栏的 Library 中找到 Interval Timer 模块,双击得到下图。我们来设置 Interval Timer:框 1 是设置周期,单位为 ms,周期设为 500,即 500ms,这个其实没有关系,因为后面通过软件可以重新设置周期, 所以只是一个初始值;框 2 是设置计数器大小,有 32 和 64 可选,我们选择 32 即可;框 3 是 预设方式选择,我们选择 Full-featured,即全功能,设置好后点击 Finish。 按照同样的设置再生成一个 Interval Timer 模块,然后将两个模块分别命名为 TIMER1 和 TIMER2,最后是连线、分配地址和中断(注:因为 Timer 没有要引出 Qsys 的 IO 端口,故无 需端口引出),TIMER1 的中断优先级要高于 TIMER2,完成后得到下图。 132 硬件实现 需要说明的是,定时器功能的实现需要还需要 4 个 LED,如果你的工程中已经添加了控制 LED 的 PIO,那就 OK,如果没有添加则依照第 4 章的方法添加上控制 LED 的 PIO,方法就不 讲了。 保存并编译 Qsys,Generate 完成后,我们在 Quartus II 无需做其它改动,直接编译工程 即可,编译成功后硬件实现就完成了,怎么样,够简单吧? 133 8.3 软件实现——定时器 软件实现——定时器 接下来完成软件程序的编写。打开 Nios II SBT,新建一个名为 timer 的工程,建好后先在 目录区把结构整理成三文件夹结构。 首先打开 BSP 工程中的 system.h 文件,看看新添加的模块的描述。下图是 TIMER1 的描 述,包括基地址、时钟频率、中断号、周期等信息,看看与我们设置的是否一致,TIMER2 的 描述与 TIMER1 类似。 /* * TIMER1 configuration * */ #define ALT_MODULE_CLASS_TIMER1 altera_avalon_timer #define TIMER1_ALWAYS_RUN 0 #define TIMER1_BASE 0x1020 #define TIMER1_COUNTER_SIZE 32 #define TIMER1_FIXED_PERIOD 0 #define TIMER1_FREQ 50000000u #define TIMER1_IRQ 4 #define TIMER1_IRQ_INTERRUPT_CONTROLLER_ID 0 #define TIMER1_LOAD_VALUE 24999999ull #define TIMER1_MULT 0.0010 #define TIMER1_NAME "/dev/TIMER1" #define TIMER1_PERIOD 500 #define TIMER1_PERIOD_UNITS "ms" #define TIMER1_RESET_OUTPUT 0 #define TIMER1_SNAPSHOT 1 #define TIMER1_SPAN 32 #define TIMER1_TICKS_PER_SEC 2u #define TIMER1_TIMEOUT_PULSE_OUTPUT 0 #define TIMER1_TYPE "altera_avalon_timer" 在 inc 文件夹中新建一个 sopc.h 文件,先对它进行编写,完成后得到下图,因为我们对 TIMER1 和 TIMER2 的操作是利用 Altera 提供的 API 函数实现的,所以 sopc.h 很简单,就是 对 LED 的一个结构体定义。 #ifndef SOPC_H_ #define SOPC_H_ #include "system.h" 134 软件实现——定时器 #define _LED typedef struct { unsigned long int DATA; unsigned long int DIRECTION; unsigned long int INTERRUPT_MASK; unsigned long int EDGE_CAPTURE; }PIO_STR; #ifdef _LED #define LED #endif /*_LED*/ ((PIO_STR *) LED_BASE) #endif /*SOPC_H_*/ 然后编写 main.c 文件,完成后得到下图。 #include #include #include #include #include "system.h" #include "altera_avalon_timer_regs.h" #include "alt_types.h" //1 #include "sys/alt_irq.h" #include "../inc/sopc.h" static void timer_init(void); //初始化中断 int i = 0,j = 0,flag; //定时器的周期 alt_u32 timer_prd[4] = {5000000, 10000000, 50000000, 100000000}; //2 /* * === FUNCTION ======================================================= * Name: main * Description: 主函数 * ====================================================================== */ int main(void) { //初始化Timer timer_init(); while(1); return 0; } /* 135 软件实现——定时器 * === FUNCTION ======================================================= * Name: ISR_timer1 * Description: 控制流水灯按一定频率闪烁 * ====================================================================== */ static void ISR_timer1(void *context) { //控制流水灯闪烁,一共四个LED LED->DATA = 1<> 16); //重新启动定时器 IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER1_BASE, 0x07); //闪烁频率先高后低然后又变高 if(j == 0) flag = 0; if(j == 3) flag = 1; if(flag == 0){ j++; } else{ j--; } //清除中断标志位 IOWR_ALTERA_AVALON_TIMER_STATUS(TIMER2_BASE, 0); } 136 软件实现——定时器 /* * === FUNCTION ======================================================= * Name: timer_init * Description: 初始化中断 * ====================================================================== */ void timer_init(void) //初始化中断 { //清除Timer1中断标志寄存器 IOWR_ALTERA_AVALON_TIMER_STATUS(TIMER1_BASE, 0x00); //设置Timer1周期 IOWR_ALTERA_AVALON_TIMER_PERIODL(TIMER1_BASE,80000000); IOWR_ALTERA_AVALON_TIMER_PERIODH(TIMER1_BASE, 80000000 >> 16); //允许Timer1中断 IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER1_BASE, 0x07); //注册Timer1中断 alt_ic_isr_register(TIMER1_IRQ_INTERRUPT_CONTROLLER_ID,TIMER1_IRQ, ISR_timer1,(void *)TIMER1_BASE,0x0); //清除Timer2中断标志寄存器 IOWR_ALTERA_AVALON_TIMER_STATUS(TIMER2_BASE, 0x00); //设置Timer2周期 IOWR_ALTERA_AVALON_TIMER_PERIODL(TIMER2_BASE,400000000); IOWR_ALTERA_AVALON_TIMER_PERIODH(TIMER2_BASE, 400000000 >> 16); //允许Timer2中断 IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER2_BASE, 0x07); //注册Timer2中断 alt_ic_isr_register(TIMER2_IRQ_INTERRUPT_CONTROLLER_ID,TIMER2_IRQ, ISR_timer2,(void *)TIMER2_BASE,0x0); } 在解释主要的函数功能前,首先关注下红框中的内容。框 1 是引用的一些头文件,这里有 两个我们头一次遇到的“生面孔”:altera_avalon_timer_regs.h 和 alt_types.h。 前面我们讲过,对 Qsys 生成的 IP 核模块控制有两种方式:一是前面一直使用的构建结构 体来改变寄存器值的方式,二就是利用 Nios II 提供的 API 函数。我们之所以能使用现成的 API 函数来对控制硬件模块,是因为 Nios II 集成了一个 HAL(Hardware Abstraction Layer,硬 件抽象层)系统,HAL 系统将对硬件模块的控制抽象成了 API 函数,由很多个头文件组成, altera_avalon_timer_regs.h 就是其中的一个,它定义了一些控制 Interval Timer 模块的 API 函数,引用它后,我们在主函数中就可以直接使用 API 函数来控制模块了,下图是 Interval Timer 作为 32bit Timer 时的寄存器表,如果想对 status 赋值为 0x00,直接采用框 3 中的方 式即可。 137 软件实现——定时器 alt_types.h 也属于 HAL 系统,但它并不是定义某个模块的 API 函数,而主要是一些常用 变量类型的类型别名,框 2 中的 alt_u32 其实就是 unsigned long,我们可以在 alt_types 中 找到定义,如下图所示。 在 main.c 中,除 main 函数外主要定义了三个函数:timer_init、ISR_timer1 和 ISR_timer2。 timer_init 函数对 TIMER1 和 TIMER2 两个 Interval Timer 进行了初始化,主要是设置好中断, 设置定时器周期,周期的单位是时钟(cycle),初始化完成后,每隔预设的周期触发中断,执 行 ISR_timer1 和 ISR_timer2 两个中断函数,这其实就是我们在单片机中学习过的定时中断。 ISR_timer1 函数的功能是产生流水灯,ISR_timer2 函数的功能是改变 TIMER1 的周期,两个 定时器配合,让流水灯的闪烁频率由慢到快,再由快到慢。 138 软件实现——System Clock 8.4 软件实现——System Clock 下面来实现 System Clock 功能。 硬件部分没什么要添加的,有刚才实现定时器的两个 Interval Timer 模块就完全够用了(事 实上一个就够了☺)。新建一个 Nios II 工程命名为 sys_clock,建好后依然先在目录区把结构 整理成三文件夹结构。 我们先来设置系统时钟,在目录区选中原工程,右键单击,选择 Nios II->BSP Editor,如 下图红框所示。 在 得 到 的 BSP Editor 界 面 中 , 选 择 上 边 栏 的 main , 然 后 在 左 侧 边 栏 选 择 139 软件实现——System Clock Settings->Common->hal->sys_clk_timer,如下图框 1 所示,我们把 TIMER1 设为 System Clock,如框 2 所示,点击 Generate,System Clock 就设置完成了,怎么样,是不是 so easy? 接下来完成程序的编写。首先在 inc 文件夹中完成 sopc.h 文件的编写,内定与上一节定时 器一样,就不再写了。 最后编写 main.c 文件,完成后得到下图。 #include #include #include "system.h" #include "altera_avalon_pio_regs.h" #include "alt_types.h" #include "sys/alt_irq.h" #include "../inc/sopc.h" #include "sys/alt_alarm.h" //1 alt_u32 my_alarm_callback(void *context); //2 unsigned int i = 0; unsigned int alarm_flag; #define INTEVAL_TICK 2 //3 #define DEBUG 140 软件实现——System Clock /* * === FUNCTION ======================================================= * Name: main * Description: 主函数 * ====================================================================== */ int main(void) { alt_alarm alarm; printf("hello world!\n"); if(alt_alarm_start(&alarm,INTEVAL_TICK,my_alarm_callback,NULL) < 0) //4 { #ifdef DEBUG printf("Error: No system clock available\n"); #endif exit(0); } else { #ifdef DEBUG printf("Success: System clock available\n"); #endif } while(1) { if(alarm_flag != 0) { LED->DATA = 1<Common->hal->timestamp_timer,如下图框 1 所 示,我们既然已经把 TIMER1 设为了 System Clock,那就干脆把 TIMER2 设为 Timestamp, 如框 2 所示,点击 Generate,设置就 OK 了。 本节我们只需要编写 main.c 文件,编写完成后得到下图。 #include #include "sys/alt_timestamp.h" #include "alt_types.h" 143 软件实现——Timestamp int func1(void) { int i = 100; while(i--); return 0; } int func2(void) { int i = 200; while(i--); return 0; } /* * === FUNCTION ======================================================= * Name: main * Description: 主函数 * ====================================================================== */ int main(void) { unsigned int time1; unsigned int time2; unsigned int time3; if(alt_timestamp_start() < 0) { printf("Timestamp Failed!\n"); exit(-1); } time1 = alt_timestamp(); func1(); time2 = alt_timestamp(); func2(); time3 = alt_timestamp(); printf("func1 needs %d\n",(time2 - time1)); printf("func2 needs %d\n",(time3 - time2)); return 0; } 整个程序内容比较简单,先是调用 alt_timestamp_start()来启动 Timestamp,开始计时。 alt_timestamp 函数的返回值就是当前的计时值,我们在 func1()、func2()函数的执行前后调 用了三次 alt_timestamp 函数,得到了三个计时值,再相减就能得到 func1()、func2()的执行 时长。 144 8.6 总结 DS1302 应用——RTC 在定时器实验中,我们得到的实验现象为:LED 按流水灯方式闪烁,且闪烁频率由快到慢, 再由慢到快,往复循环;在 System Clock 实验中,实验现象为:闪烁频率固定为 1s 的流水灯; 在 Timestamp 实验中,实验现象为:在 Nios II 主界面的状态区会打印 fun1()函数和 fun2() 函数的时间,如下图所示。 本章首先在硬件实现部分添加了两个 Interval Timer 模块:TIMER1 和 TIMER2,要注意 设置的参数。 在软件实现部分,分别实现了定时器、System Clock 和 Timestamp 功能,这里需要注意 Altera HAL 系统定义的 API 函数的调用方法,我们在前边讲过,这种方法与自己定义结构体赋 值寄存器的方法效果是一样的,我的建议是大家最好是两种方法都能掌握,再根据实际情况决 定用哪个(自然是哪个方便用哪个);还有 Nios II 工程中 System Clock 和 Timestamp 的设 置方法,同一个工程中,System Clock 和 Timestamp 不能是同一个 Interval Timer。 其实无论是定时器、System Clock 还是 Timestamp,都是 Interval Timer 模块的应用, 有了 Interval Timer,能实现的功能多了去了(得和时间有关啊,你让它控制个 TFT 液晶屏它 做不到),大家有时间的话可以研究下它的其它功能。这一章的内容就到这里,因本人水平有限, 文中有任何错误请联系我,大家在交流中进步!邮箱:vito943@qq.com 145 DS1302 应用——RTC 第9章 I2C 总线应用——EEPROM 本章主要介绍在 Nios II 中利用 I2C 总线实现对 EEPROM 存储器——AT24C02 的读写, 通过本章,你能学到: (1)在 Nios II 中利用 I2C 总线读写 EEPROM。 本章分为四个部分: 一、概述 二、硬件实现 三、软件实现 四、实验总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 146 概述 9.1 概述 在 Nios II 开发中,总免不了和总线打交道,CPU 与外设(模块和芯片)的数据通信,往 往都是通过总线来完成的。总线一般分为并行总线和串行总线,并行总线的代表是后面章节会 讲到的 Avalon 总线(其实我们一直在用它☺),它的优点是传输速度快,缺点是会占用过多的 IO 口,抗干扰性能差;串行总线包括 I2C 总线、SPI 总线等等,它与并行总线恰恰相反,优点 是占用 IO 口较少,抗干扰性能强,缺点是传输速率较慢。并行总线与串行总线各有优缺点,“鱼 与熊掌不可兼得”。一般来讲,芯片内部之间的通信采用并行总线较多,而芯片与外部的数据通 过更多的采用串行总线,比如本章的 I2C 总线。 在介绍具体的实现前,先介绍下 I2C 和 EEPROM 这两个概念。 I2C(Inter-Integrated Circuit)总线是由 PHILIPS 公司开发的两线式(时钟线 SCL 和 数据线 SDA)串行总线,用于连接微控制器及其外围设备。是微电子通信控制领域广泛采用的 一种总线标准。因为在 Nios II 中没有 I2C 总线接口控制模块,我们使用两个 PIO 来模拟 I2C 总线的 SCL、SDA 来实现 I2C 的读写时序。 EEPROM (Electrically Erasable Programmable Read-Only Memory)是一种掉电后数 据不丢失的存储芯片。 EEPROM 的存储内容可反复擦除,重复读写,对 EEPROM 的读写通 过 I2C 总线完成。 TIGER BOARD 上采用了型号为 AT24C02 的 EEPROM 存储器芯片,它的容量为 2Kbit (256x8),在这一章,我们就利用 PIO 模拟的 I2C 总线,来完成对它的读写。 147 硬件实现 9.2 硬件实现 因为我们采用 PIO 来模拟 I2C 总线协议,所以先在 Qsys 中添加两个 PIO 模块,第一个设 置位数为 1,方向为输出,其它不变,命名为 SCL,即 I2C 总线的时钟线;另一个位数也为 1, 方向为双向(Bidir),其它不变,命名为 SDA,即 I2C 总线的数据线。最后再将 PIO 模块连线、 端口引出、分配地址,得到下图。 保存后编译 Qsys,完成后打开 Quartus II,在顶层 bdf 文件中更新 kernel 得到新添加的 PIO:SDA 和 SCL。将这两个 IO 端口生成管脚、分配管脚,完成后得到下图,它们将与 TIGER BOARD 上的 AT24C02 芯片的 SCL 和 SDA 管脚相连。 上面工作完成后,保存并编译 Quartus II 工程,硬件实现部分就搞定了。 148 软件实现 9.3 软件实现 新建一个 Nios II 工程,命名为 eeprom,将目录区整理成 main、inc 和 driver 三文件夹 结构,得到下图。 在 inc 文件夹中新建一个.h 头文件命名为 iic.h,编写完成后得到下图。 #ifndef IIC_H_ #define IIC_H_ #define OUT 1 #define IN 0 typedef struct{ void (* write_byte)(unsigned short addr, unsigned char dat); unsigned char (* read_byte)(unsigned short addr); }IIC; extern IIC iic; #endif /*IIC_H_*/ 里面没有什么新鲜的东西,首先是两个宏定义,然后定义了一个包含 I2C 读写函数的结构 体,把对 I2C 的所有操作都封装在该结构体中。 然后在 driver 文件夹中新建一个.c 源文件命名为 iic.c,编写完成后得到下图。 #include #include 149 软件实现 #include #include "system.h" #include "altera_avalon_pio_regs.h" #include "alt_types.h" #include "../inc/iic.h" static alt_u8 read_byte(alt_u16 addr); static void write_byte(alt_u16 addr, alt_u8 dat); IIC iic ={ .write_byte = write_byte, .read_byte = read_byte }; /* * === FUNCTION =================================================== * Name: start * Description: IIC启动 * ================================================================= */ static void start(void) { IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, OUT); IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, 1); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, 0); usleep(5); } /* * === FUNCTION =================================================== * Name: uart_send_byte * Description: IIC停止 * ================================================================== */ static void stop(void) { IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, OUT); IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, 0); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(5); IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, 1); usleep(10); } /* * === FUNCTION =================================================== * Name: ack * Description: IIC应答 * ================================================================= */ 150 软件实现 static void ack(void) { alt_u8 tmp; IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, IN); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(5); tmp = IORD_ALTERA_AVALON_PIO_DATA(SDA_BASE); usleep(5); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); usleep(10); while(tmp); } /* * === FUNCTION =================================================== * Name: iic_write * Description: IIC写一个字节 * ================================================================= */ void iic_write(alt_u8 dat) { alt_u8 i, tmp; IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, OUT); for(i=0; i<8; i++){ IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); usleep(5); tmp = (dat & 0x80) ? 1 : 0; dat <<= 1; IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, tmp); usleep(5); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(10); } } /* * === FUNCTION =================================================== * Name: read * Description: IIC读一个字节 * ================================================================== */ static alt_u8 iic_read(void) { alt_u8 i, dat = 0; IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, IN); for(i=0; i<8; i++){ IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); 151 软件实现 usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(5); dat <<= 1; dat |= IORD_ALTERA_AVALON_PIO_DATA(SDA_BASE); usleep(5); } usleep(5); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); return dat; } /* * === FUNCTION =================================================== * Name: write_byte * Description: 向EEPROM写一个字节 * ================================================================= */ static void write_byte(alt_u16 addr, alt_u8 dat) { alt_u8 cmd; cmd = (0xa0 | (addr >> 7)) & 0xfe; start(); iic_write(cmd); ack(); iic_write(addr); ack(); iic_write(dat); ack(); stop(); } /* * === FUNCTION =================================================== * Name: read_byte * Description: 从EEPROM读一个字节 * ================================================================= */ static alt_u8 read_byte(alt_u16 addr) { alt_u8 cmd, dat; cmd = (0xa0 | (addr >> 7)) & 0xfe; start(); iic_write(cmd); ack(); iic_write(addr); ack(); 152 软件实现 cmd |= 0x01; start(); iic_write(cmd); ack(); dat = iic_read(); stop(); return dat; } 在解释 iic.c 文件中程序的具体含义前,首先讲解下 I2C 总线协议的时序。 我们把 I2C 总线的基本信号分为三类:启动和停止信号、数据传输信号和应答信号,无论 是读时序还是写时序,都离不开这三类信号。  启动和停止信号 启动和停止信号的时序如上图所示,当 SCL 为高电平时,SDA 由高电平变为低电平的下 降沿被认为是启动信号;当 SCL 为高电平时,SDA 由低电平变为高电平的上升沿被认为是停 止信号,iic.c 文件中的 start()函数和 stop()函数就是根据上图的时序编写的 I2C 的启动函数和 停止函数。  数据传输信号 数据传输信号的时序如上图所示,在 SCL 为高电平时,SDA 上的数据是稳定的,这时 SDA 153 软件实现 上的数据就是要传送的数据。SDA 上数据的改变必须在 SCL 的低电平期间完成。在 iic.c 文件 中的 iic_write()函数和 iic_read()函数就使用上图的数据传输时序完成 1byte 数据的写和读操作。  应答信号 I2C 总线上的所有数据都是以 8 位字节传送的,发送器每发送一个字节,就在时钟脉冲 9 期间释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答;应答 信号为高电平时,规定为非应答。在 iic.c 文件中 ack()函数就是等待有效应答的函数。 介绍完这三类基本信号,我们就可以介绍 I2C 的写操作和读操作了,实际上,写操作和读 操作都是基于这三类信号完成的,有了这三类信号,我们就能像搭积木一样,把时序组合起来。 iic.c 文件中的 write_byte()函数和 read_byte()函数就是完成一个 byte 写操作和操作的函数, 写操作的时序为:启动->写命令(写)->应答->写地址->应答->写数据->应答->停止,读操 作的时序为:启动->写命令(写)->应答->写地址->应答->启动->写命令(读)->应答-> 读数据->非应答->停止,这里需要注意下命令字节的格式为 1010xxxR(=1)/W(=0),其中 1010 为固定位,xxx 三位的值取决于 EEPROM 的器件地址线(E2~E0,有的手册里是 A2~A0)的 电平值,在 TIGER BOARD 中我们把 E2~E0 接地,如下图所示,即器件地址为 000,所以对 器件发送写命令时命令值为 10100000,发送读命令时值为 10100001。 154 软件实现 介绍完 I2C 的时序,iic.c 文件里的函数也就基本上介绍完了,还有一点需要说明的是,这 里对 PIO 的控制采用了调用 API 函数的方法,这与自己构建结构体对寄存器赋值是等效的,比 如说 IOWR_ALTERA_AVALON_PIO_DATA(I2C_SCL_BASE, 0)与 I2C_SCL->DATA = 0,大家 哪个用着顺手就用哪个。 最后编写 main.c 文件,完成后得到下图。 #include #include "../inc/iic.h" #include #include "alt_types.h" alt_u8 write_buffer[256], read_buffer[256]; /* * === FUNCTION =================================================== * Name: main * Description: 主函数 * ================================================================= */ int cnt_wr,cnt_wr2; int main() { alt_u16 i, err; alt_u8 dat; printf("\nWriting data to EEPROM!\n"); //写入256btye的数据,前128个数字为0到127,后128个数据为1 for(i=0; i<256; i++) { if(i<128) dat = i; else dat = 1; cnt_wr++; 155 软件实现 iic.write_byte(i, dat); cnt_wr2++; write_buffer[i] = dat; printf("0x%02x ", dat); usleep(10000); } printf("\nReading data from EEPROM!\n"); //将256byte数据读出来并打印 for(i=0; i<256; i++) { read_buffer[i] = iic.read_byte(i); printf("0x%02x ", read_buffer[i]); usleep(1000); } err = 0; printf("\nVerifing data!\n"); //对比数据是否相同,如果有不同,说明读写过程有错误 for(i=0; i<256; i++){ if(read_buffer[i] != write_buffer[i]) err ++; } if(err == 0) printf("\nData write and read successfully!\n"); else printf("\nData write and read failed!--%d errors\n", err); return 0; } 在 main.c 文件中,我们在 main 函数中实现了如下功能:首先向 EEPROM 中写入了 256 个 byte 的数据,其中前 128 个值为 0 到 127,后 128 个值为 1;然后将写入的 256 个数 据读出并打印出来;最后对比写入的数据与读出的数据,以此检验读写是否成功。 156 9.4 总结 DS1302 应用——RTC 完成软硬件开发后,运行 Nios II 工程,在状态区得到下图所示的打印结果,可以看到, 程序先是打印了 256 个要写入 EEPROM 的数据,然后打印了从 EEPROM 读出的 256 个数据, 最后对比完全一致,说明读写成功,打印出了“Data write and read successfully”。 本章主要介绍了在 Nios II 中利用 I2C 总线实现对 EEPROM 存储器——AT24C02 的读写, 因为 Nios II 中没有直接可用的 I2C 接口控制模块,我们就利用 PIO 模拟 I2C 时序来完成对 EEPROM 的读写。 硬件实现部分没什么好说的,就是添加了两个 PIO:SCL 和 SDA。 在软件实现部分,重点在 iic.c 中 I2C 总线各种基本信号时序的函数实现:启动信号、停止 信号、数据传输信号和应答信号,有了这些函数,我们再根据 I2C 读写操作的时序来“组装“读 写函数,从而编写完整的读写操作函数。这里再强调下时序和将时序转换为程序思维的重要性, 得时序者得天下啊! 利用 I2C 读写 EEPROM 的内容就到这里,当然,作为经典总线,I2C 的应用也相当广泛, 157 DS1302 应用——RTC 后面的章节还会接触到它,有了本章的基础,大家到时候看看会不会轻松些!这一章的内容就 到这里,因本人水平有限,文中有任何错误请联系我,大家在交流中进步!邮箱: vito943@qq.com 158 DS1302 应用——RTC 第10章 SDRAM 本章 主要 介绍 在 Nios II 中利 用 SDRAM 控制 模块 实现 对 SDRAM 存 储芯 片 — — K4M561633G 的读写,通过本章,你能学到 (1)SDRAM 控制模块的使用。 (2)在 Nios II 中读写 SDRAM 芯片。 本章分为三个部分: 一、概述 二、软件实现 三、总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 159 概述 10.1 概述 这一章我们继续跟存储器打交道,来实现对 SDRAM 芯片——K4M561633G 的读写操作。 小贴士 Tips:存储器的经典分类——RAM 和 ROM 在存储器界,对存储器有个经典分类方法——RAM 和 ROM。RAM(Random Access Memory,随机存储器)的特点是掉电数据丢失,只能短时间存储数据,但是读写速度 快,典型的 RAM 包括 SDRAM、SRAM 等;ROM(Read Only Memory)的特点是掉 电数据不会丢失,可以长时间存储数据,读写速度较慢,需要说明的是原来 ROM 的概 念仅限于只能读不能随意修改的存储器,但是现在只要是掉电不丢失的存储器都可以称 为 ROM,包括 EEPROM、Flash 等。 每个 CPU 一般都会配有相应的 RAM 和 ROM,RAM 用来执行程序,ROM 用来存储数据, 它们可以是片内存储,也可以是片外存储。在建立 Qsys 模块时我们把 SDRAM 芯片作为 CPU 的 RAM,Flash 芯片作为 CPU 的 ROM,对它们的读写控制是通过相应的控制器来实现的,前 面虽然没有讲具体的读写控制,其实 CPU 无时无刻不在对 SDRAM 和 Flash 进行读写控制, 接下来我们就“零距离”感受下利用 SDRAM 控制模块读写 SDRAM 芯片——K4M561633G。 160 软件实现 10.2 软件实现 这一章的“特色”是没有硬件实现,因为我们在 Qsys 模块建立的时候早已经生成了 SDRAM 控制模块,所有硬件实现的工作都搞定了,得嘞,前人栽树后人乘凉,直接软件实现搞起。 不仅如此,软件的实现也非常简单,简单到连.h 头文件都无需编写,直接编写 main.c 文 件即可,编写完成后得到下图。 #include #include "system.h" #include "string.h" //SDRAM地址 unsigned short * ram = (unsigned short *)(SDRAM_0_BASE+0x10000); /* * === FUNCTION =================================================== * Name: main * Description: 主函数 * ================================================================= */ int main(void) { int i; memset(ram,0,100); //向ram中写数据,当ram写完以后,ram的地址已经变为(SDRAM_BASE+0x10000+200) for(i=0;i<100;i++) { *(ram++) = i; } //逆向读取ram中的数据 for(i=0;i<100;i++) { printf("%d\n",*(--ram)); } return 0; } 在讲解程序内容前,需要说明一点,因为我们在 Qsys 模块中,通过 SDRAM 控制器将 CPU 与 SDRAM 相连,并把 SDRAM 设置为 CPU 的内存,所以对 SDRAM 进行读写操作就变得非 161 软件实现 常简单,与读写内存一样。 在 main.c 中,首先定义了一个 short 型的指针 ram 指向 SDRAM 的基地址+0x10000, 之后我们改变或读取指针指向的地址(SDRAM 基地址+偏移地址)的值,就改变了 SDRAM 相应地址(偏移地址/2)的值。在主函数中,我们通过 memset 函数将从 ram 指向地址开始 的 100 个地址的值全部清 0,再通过一个 for 循环向从 ram 指向地址开始的 100 个地址的赋 相应的值,最后再将这 100 个值逆向读取打印出来,这样就完成了 SDRAM 的读写操作。可以 看出,通过 SDRAM 控制器的使用,对 SDRAM 的读写操作变得非常简单,简直不费吹灰之力 啊。 之所以对 SDRAM 的读写要偏移 0x10000,是因为 CPU 程序的运行占用了从 SDRAM 基 地址开始的部分内存,如果我们不做偏移直接从基地址开始读写,则很有可能破坏程序正常运 行,0x10000 这个值并不固定,只要别占用程序运行的内存就可以了。 小贴士 Tips:memset 函数 memset 函数是 C 语言中的一个常用函数,在 string.h 文件中声明。它的函数原型是 memset(void *s, int ch, size_t n),功能是将 s 所指向的某一块内存中的前 n 个字节的内 容全部设置为 ch 指定的 ASCII 值,块的大小由第三个参数指定。 162 软件实现 小贴士 Tips:捋一捋“混乱”的地址关系 在本章里,ram 指针指向的 CPU 内存地址为 SDRAM 基地址+0x10000 当 ram 加 1 时, 指向的内存地址变为了 SDRAM 基地址+0x10002,之所以加了 2,是因为在 Nios II CPU 中,每 8 位数据对应一个内存地址,而 ram 是 short 型指针,short 型变量长度为 16, 所以指针每加 1 指向的内存地址加 2,但是因为我们用的 SDRAM 是 16 位的,所以 SDRAM 的真实地址只加了 1。所以关系就是:开始时,ram 指向的内存地址为 SDRAM 基地址+0x10000,对应的 SDRAM 真实地址为 0x8000;ram 加 1 后,指向的内存地址 为 SDRAM 基地址+0x10002,对应的 SDRAM 真实地址为 0x8001,如下图所示。 ram ram+1 Address Mapping CPU SDRAM BASE+0x10000 BASE+0x10001 0x8000 BASE+0x10002 BASE+0x10003 0x8001 BASE+0x10004 … … 163 10.3 总结 DS1302 应用——RTC 运行 Nios II 工程,在状态区会打印出来 99 到 0 这 100 个值,现象挺简单,就不截图了。 本章主要介绍了在 Nios II 中利用 SDRAM 控制模块来实现对 SDRAM 芯片—— K4M561633G 的读写操作,内容不多,但是也有许多需要注意的地方。 在软件实现部分,通过指针操作完成了对 SDRAM 的读写操作。通过 SDRAM 控制器的使 用,对 SDRAM 的读写操作直接用指针访问内存方式即可,非常简单。这里有三点需要注意: 一是为了不影响程序正常运行,对 SDRAM 的读写操作要在基地址的基础上偏移一段地址进行; 二是 memset 函数的用法;三是 ram 指向的地址、SDRAM 的真实地址的关系。 SDRAM 的内容就到这里,下一章我们再搞定掉电非易失的存储器——Flash 的读写。这 一章的内容就到这里,因本人水平有限,文中有任何错误请联系我,大家在交流中进步!邮箱: vito943@qq.com 164 Flash 第11章 Flash 本章主要介绍在 Nios II 中利用 EPCS 控制模块实现对 Flash 存储器芯片——EPCS64 的读 写,通过本章,你能学到 (1)EPCS 控制模块的使用。 (2)在 Nios II 中用 Simple Flash Access 方式读写 Flash。 (3)在 Nios II 中用 Fine-Grained Flash Access 方式读写 Flash。 本章分为四个部分: 一、概述 二、软件实现——Simple Flash Access 三、软件实现——Fine-Grained Flash Access 四、实验总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 165 概述 11.1 概述 上一章介绍的 SDRAM 属于掉电易失的 RAM,如果我们想把程序或数据长时间存储起来, RAM 就无能为力了(总不能一直不断电吧~囧),这时候,ROM 存储器就派上用场了。这一 章介绍常用的一种 ROM——Flash。 Flash 存储器兼具 RAM 和 ROM 的优点,它能在掉电非易失的前提下,保持较高的读写速 度,因此在嵌入式开发中广泛使用。Flash 的种类很多,在 Nios II 中,Qsys 提供了 CFI Flash 和 EPCS Flash 两种类型的 Flash 控制模块,只要是这两种类型的 Flash 芯片,我们就能通过模 块轻松地实现读写,免去了繁琐的驱动程序。 每个 Altera FPGA 都要配备一个配置芯片,这个配置芯片就是 EPCSX 芯片,我们知道, FPGA 内部的存储器是掉电易失的 SRAM,如果想要把 FPGA 程序或 Nios II 程序永久存储, 就得需要 EPCS 芯片了。TIGER BOARD 开发板上配备的是 EPCS64,在本章,我们利用 EPCS 控制模块实现对 EPCS64 芯片的读写操作。 Altera HAL 系统提供了两种 Flash 的访问方式,分别是 Simple Flash Access(简单 Flash 访问)和 Fine-Grained Flash Access(精细 Flash 访问)。 Simple Flash Access 包含了 alt_flash_open_dev(),alt_write_flash(),alt_read_flash(), 和 alt_flash_close_dev()四个函数,作用分别是打开 Flash、写 Flash、读 Flash 和关闭 Flash, 它的操作比较简单,但是存在一个问题:Altera 的《Nios II Software Developer ’s Handbook》 手册称之为 Block Corruption(块腐化)。 EPCS Flash 存储器是由若干个 Block(块)组成的,如果采用简单 Flash 访问方式写 Flash, 在对某地址写数据之前会把对应地址所在的 Block 擦除(全部置 1),这样容易导致此前存储的 数据丢失,从而造成 Block Corruption,有些同学不明白这个道理,以为只要多次写入的数据 地址不重合即可,却发现写入新数据后原来的数据莫名地消失(本人血的教训!%>_<%)。 为了避免 Block Corruption,Altera 提供了 Fine-Grained Flash Access,它包含了 alt_get_flash_info(),alt_erase_flash_block()和 alt_write_flash_block()三个函数,分别为获 166 概述 取 Flash 信息,擦除 Flash Block 和写 Flash Block,通过获取 Flash 信息函数,我们能了解到 Flash 的基本信息:包括 Block 的容量、数目、起始地址等,擦除 Flash Block 函数和写 Flash Block 函数能对指定 Block 的某些地址做擦除和写操作而不改变其它地址的数据,有了这三个 函数,我们就能把数据存储到指定的 Block 而避免 Block Corruption。 167 软件实现——Simple Flash Access 11.2 软件实现——Simple Flash Access 与上一章一样,我们的 EPCS 控制模块在建立 Qsys 模块时早已添加进去,所以硬件部分 没什么要做的,直接软件实现搞起。 这一节我们先来实现 Simple Flash Access,新建一个 Nios II 工程,命名为 flash_sfa, 先打开 system.h,找到 EPCS 控制模块,如下图所示,是 EPCS 控制模块的信息,要注意红框 中的 EPCS_NAME,我们一会得用到它。 /* * EPCS configuration * */ #define ALT_MODULE_CLASS_EPCS altera_avalon_epcs_flash_controller #define EPCS_BASE 0x4001800 #define EPCS_IRQ 0 #define EPCS_IRQ_INTERRUPT_CONTROLLER_ID 0 #define EPCS_NAME "/dev/EPCS" #define EPCS_REGISTER_OFFSET 1024 #define EPCS_SPAN 2048 #define EPCS_TYPE "altera_avalon_epcs_flash_controller" 在编写程序之前,先介绍下几个要用到的函数。  alt_flash_open_dev() 函数的功能为打开 Flash,原型为 alt_flash_fd* alt_flash_open_dev(const char* name) name 是 Flash 的名字,就是上面提到的 EPCS_NAME,值在 system.h 中可以查到,返 回值的类型是 alt_flash_fd 类型的句柄,该类型在 alt_flash.h 文件中声明:返回值为 0 表示打 开成功,否则不成功。  alt_write_flash() 168 软件实现——Simple Flash Access 函数的功能为写 Flash,原型为 int alt_write_flash(alt_flash_fd* fd, /*Flash的句柄*/ int offset, /*flash地址偏移*/ const void* src_addr, /*待写数据地址*/ int length) /*写入长度*/ 函数的返回值为 int 型,返回 0 代表写成功,否则不成功。  alt_read_flash() 函数的功能为读 Flash,原型为 int alt_read_flash(alt_flash_fd* fd, /*Flash的句柄*/ int offset, /*flash地址偏移*/ void* dest_addr, /*读数据存储地址*/ int length) /*读长度*/ 函数的返回值为 int 型,返回 0 代表写成功,否则不成功。  alt_flash_close_dev() 函数的功能为关闭 Flash,原型为 void alt_flash_close_dev(alt_flash_fd* fd)/* Flash的句柄*/ 接下来编写程序,我们只需编写 main.c 文件即可,编写完成后得到下图。 #include #include #include "sys/alt_flash.h" #define BUF_SIZE 100 #define FLASH_OFFSET 0x50000 /* * === FUNCTION =================================================== * Name: main * Description: 主函数 * ================================================================= */ int main(void) { alt_flash_fd *fd; int i; int ret_code; 169 软件实现——Simple Flash Access char source[BUF_SIZE]; char dest[BUF_SIZE]; for(i=0;i<100;i++) { source[i] = i; } //打开FLASH设备 fd = alt_flash_open_dev("/dev/EPCS"); if(fd == NULL) { printf("Can't open flash device\n"); exit(-1); } else { printf("Open Flash Device Successfully.\n"); } //向FLASH中写数据,成功后返回值为0 ret_code = alt_write_flash(fd,FLASH_OFFSET,source,BUF_SIZE); if(ret_code != 0) { printf("Can't write flash device\n"); exit(-1); } else { printf("Write Flash Device Successfully.\n"); } //读FLASH中的数据,成功后返回值为0 ret_code = alt_read_flash(fd,FLASH_OFFSET,dest,BUF_SIZE); if(ret_code != 0) { printf("Can't read flash device\n"); exit(-1); } else { printf("Read Flash Device Successfully.\n"); } //打印读取的数据 for(i=0;i<100;i++) { printf("%d\n",dest[i]); } 170 软件实现——Simple Flash Access alt_flash_close_dev(fd); return 0; } alt_flash.h 中包含了 Flash 控制模块的 API 函数,所以要引用它。在 main 函数中,先通 过 for 函数给 source 数组赋初值;然后调用 alt_flash_open_dev()函数打开 EPCS,然后调用 alt_write_flash()函数来写 EPCS,将 source 数组的数据写入 EPCS 中;再调用 alt_read_flash() 函 数 来 读 刚 刚 写 的 一 段 地 址 , 将 读 出 的 数 据 存 入 dest 数 据 , 并 打 印 出 来 ; 最 后 调 用 alt_flash_close_dev 关闭 Flash,整个过程就完成了,这就是 Simple Flash Access 的整个过 程。可以看出,由于我们并不了解 EPCS 的 Block 的信息,很容易发生 Block Corruption。 171 软件实现——Fine-Grained Flash Access 11.3 软件实现——Fine-Grained Flash Access 接下来完成 Fine-Grained Flash Access,新建 Nios II 工程,命名为 flash_fgfa。 在编写程序之前,先讲一下 Fine-Grained Flash Access 访问的原理。在 Flash 中,只要 对某个 Block 进行写操作,必然要先把该 Block 擦除,再将数据写到对应地址,这样就容易导 致 Block Corruption。有一种方法是:如果对某个 Block 的某些地址进行写操作而不改变该 Block 其它地址的数据,可以先把 Block 里已有的数据复制出到一个 Buffer(缓存)中,再在 Buffer 中改变这些地址的数据,最后将 Buffer 数据再写回到 Block 中,Fine-Grained Flash Access 的 alt_erase_flash_block()函数和 alt_write_flash_block()函数就是在函数中自动完成 了上述方法,从而能实现擦除和写入 Block 中某些地址的数据而 不改变其它地址数 据的 Fine-Grained Flash Access 功能。 先介绍下实现 Fine-Grained Flash Access 要用到的几个函数。  alt_get_flash_info() 函数的功能为获取 Flash 信息,原型为 int alt_get_flash_info(alt_flash_fd* fd, /*Flash的句柄*/ flash_region** info, /*Flash信息*/ int* number_of_regions) /*Region数量*/ 其中 flash_region 为在 alt_flash_types.h 中声明的结构体,其原型为 typedef struct flash_region { int int int int offset; region_size; number_of_blocks; block_size; }flash_region; 里面包含了地址偏移、Region 容量、Block 数量和 Block 容量等信息,Region 是高于 Block 172 软件实现——Fine-Grained Flash Access 的一种结构,简单来说,一个 Flash 存储器由一个或多个 Region 组成,一个 Region 由一个 或多个 Block 组成。 alt_get_flash_info()函数返回值为 0 表示获取成功,否则不成功。  alt_erase_flash_block() 函数功能为擦除某个 Block 中某些地址的数据而不改变其它地址的数据,原型为 int alt_erase_flash_block(alt_flash_fd* fd, /*Flash的句柄*/ int offset, /*擦除块的地址偏移*/ int length) /*擦除长度*/ 函数返回值为 0 表示获取成功,否则不成功。  alt_write_flash_block() 函数的功能为写 Flash 的某个 Block 中某些地址的数据而不改变其它地址的数据,原型为 int alt_write_flash_block(alt_flash_fd* fd, /*Flash的句柄*/ int block_offset, /*Block地址偏移*/ int data_offset, /*写入地址偏移*/ const void *data, /*写入数据源地址*/ int length) /*写入长度*/ 这里需要说明的是 block_offset 与 data_offset 的关系,block_offset 是要写入的 Block 的地址偏移,data_offset 是写入地址偏移,比如我们要写入到第六个 Block,它的起始地址为 0x50000,就把 block_offset 设为 0x50000,但是我们并不从这个 Block 的起始地址开始写, 而是跳过 0x100 个地址,那就把 data_offset 设为 0x50100。函数返回值为 0 表示写入成功, 否则不成功。 下面直接编写 main.c 文件,完成后得到下图。 #include #include "sys/alt_flash.h" #include "sys/alt_flash_types.h" #include "system.h" #include "stdio.h" #define BUF_SIZE 100 int main (void) { 173 软件实现——Fine-Grained Flash Access flash_region* regions; alt_flash_fd* fd; int number_of_regions; int ret_code; char source[BUF_SIZE]; char dest[200]; int i; for(i=0;i<100;i++) { source[i] = i; } fd = alt_flash_open_dev("/dev/EPCS"); if(!fd) { printf("Can't open flash device\n"); exit(-1); } else { printf("Open Flash Device Successfully.\n"); } /*获取Flash信息*/ ret_code = alt_get_flash_info(fd, ®ions, &number_of_regions); /*打印出Flash每个Block的信息*/ for(i=0;ioffset, (regions+i)->region_size+(regions+i)->offset, (regions+i)->number_of_blocks, (regions+i)->block_size); /* 擦除第六个Block的内容 */ ret_code = alt_erase_flash_block(fd, regions->offset+(regions->block_size)*5, if(ret_code != 0) regions->block_size); { printf("Can't Erase flash device\n"); exit(-1); } else { printf("Erase Flash Device Successfully.\n"); } /*把source数组的数据写入第六个Block,从该Block的第0x100个地址开始*/ ret_code = alt_write_flash_block (fd, regions->offset+(regions->block_size)*5, 174 软件实现——Fine-Grained Flash Access regions->offset+(regions->block_size)*5+0x100, source, BUF_SIZE); if(ret_code != 0) { printf("Can't write flash device\n"); exit(-1); } else { printf("Write Flash Device Successfully.\n"); } /*把source数组的数据写入第六个Block,从该Block的第0x100+100个地址开始*/ ret_code = alt_write_flash_block (fd, regions->offset+(regions->block_size)*5, regions->offset+(regions->block_size)*5+0x100+100, source, BUF_SIZE); if(ret_code != 0) { printf("Can't write flash device\n"); exit(-1); } else { printf("Write Flash Device Successfully.\n"); } /*读出第六个Block,从该Block的第0x100个地址开始把前200个地址的数据到dest数组*/ ret_code = alt_read_flash(fd, regions->offset+(regions->block_size)*5+0x100, dest, 200); if(ret_code != 0) { printf("Can't read flash device\n"); exit(-1); } else { printf("Read Flash Device Successfully.\n"); } for(i=0;i<200;i++) { printf("%d\n",dest[i]); } return 0; } 在 主 函 数 中 , 开 始 依 然 是 通 过 alt_flash_open_dev() 函 数 打 开 Flash , 然 后 通 过 alt_get_flash_info()函数得到 EPCS Flash 的信息,并打印出来。我们得到 EPCS64 的信息如 175 软件实现——Fine-Grained Flash Access 下,可以看出,EPCS64 只包含一个 Region,共 128 个 Block,每个 Block 的大小都是 0x10000。 然后用 alt_erase_flash_block()函数擦除第六个 Block,再用 alt_write_flash_block()给第 六个 Block 写数据,从第六个 Block 的偏移 0x100 地址开始,把 source 数据的 100 个数据写 进来;接着再用 alt_write_flash_block()给第六个 Block 写数据,这次是从第六个 Block 的偏 移 0x100+100 地 址 开 始 , 把 source 数 据 的 100 个 数 据 写 进 来 ; 最 后 再 通 过 alt_read_flash_block()函数读出刚才写入的 200 个地址的数据存储到 dest 数组中,打印出来。 需要 注意 的 是, alt_erase_flash_block() 和 alt_write_flash_block() 函 数只 能针 对 某 个 Block 操作,跨 Block 的操作是无效的,比如想擦除两个 Block 或者给两个 Block 写数据,要 分别对这两个 Block 操作,这样对于容量远大于一个 Block 的数据来说,操作变得复杂,而且 这两个函数的工作原理也决定了其速度慢于 SFA 中的函数。 小贴士 Tips:访问 Flash 的向左走向右走——SFA or FGFA? Altera 提供的两种访问 Flash 的方法可谓“各有千秋”,Simple Flash Access 操作简单, 写速度也较快,但是容易出现 Block Corruption 是硬伤;Fine-Grained Flash Acess 能有 效避免 Block Corruption,但是只能对单个 Block 操作,对于大容量数据写入操作复杂 而且速度慢。其实,我们只要通过 alt_get_flash_info()函数了解了 Flash 的信息,那么完 全可以用 SFA 中的 alt_write_flash()函数来写入数据,只需要把握好写入地址和数据长 度,不将新数据写入已经使用的 Block 即可。我个人的访问 Flash 建议方法是:先通过 alt_get_flash_info()函数获取 Flash 信息,再通过 alt_write_flash()写数据。访问 Flash 即 不向左也不向右,咱们走中间! 176 11.4 总结 DS1302 应用——RTC 在 Simple Flash Access 实验中,状态区会打印出来操作成功的几个字符串,最后打印出 0 到 99 的值;在 Fine-Grained Flash Access 实验中,状态区会打印出来操作成功的几个字符 串,最后打印两遍 0 到 99,可以看出,第一次写入的 100 个数据并没有因为第二次的写入而 丢失,Fine-Grained Flash Access 的使用有效地避免了 Block Corruption。 本章主要介绍了 EPCS Flash 芯片——EPCS64 的读写操作,并分别以 Simple Flash Access 和 Fine-Grained Flash Access 两种方式实现了对 EPCS64 的访问;在 Simple Flash Access 中,因为不了解 Flash 的信息,容易出现 Block Corruption,导致原有数据遭到破坏; Fine-Grained Flash Access 利用 alt_get_flash_info()函数可以获取 Flash 的信息,再结合 alt_erase_flash_block()和 alt_write_flash_block()函数能有效避免 Block Corruption,但对 于大容量数据写入操作复杂,所以较合理的访问 Flash 方式为:先通过 alt_get_flash_info()函 数获取 Flash 信息,再通过 alt_write_flash()写数据。 无论是哪种访问方式,只要涉及到写某个 Block,那么该 Block 的原有数据一定会被清除 掉,这是由 Flash 的本质决定的,Fine-Grained Flash Access 方式只是利用了 Buffer 来中 转数据,并没有违背 Flash 的本质。这一章的内容就到这里,因本人水平有限,文中有任何错 误请联系我,大家在交流中进步!邮箱: vito943@qq.com 177 DMA 第12章 神奇的直接存储器访问—— DMA 本章主要介绍在 Nios II 实现 DMA 传输,通过本章,你能学到 (1)DMA 控制模块的生成和使用。 (2)用 DMA 传输方式读写 SDRAM。 本章分为四个部分: 一、概述 二、硬件实现 三、软件实现 四、总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 178 概述 12.1 概述 前几章讲述了各种各样的存储器,它们的读写方式各不相同,但有一点是一致的:读写存 储器要通过 CPU 来进行,也就是说,在对存储器做读写操作时,CPU 一直被占用而不能进行 别的工作。如果某个程序中存储器的读写非常频繁且耗时很长,那 CPU 就甭干别的了,只能 专门“伺候”存储器了。有没有一种方法,能让读写存储器不占用 CPU 呢?答案是肯定的, 那就是我们要介绍的 DMA(Direct Memory Access,直接存储器访问)。 DMA 是指一种高速的数据传输操作,它可以在外部设备和存储器之间读写数据而不占用 CPU,整个过程是在 DMA 控制器的控制下完成的。CPU 除了在数据传输开始和结束时做一些 简单处理外,在传输过程中不被占用,可以并行进行其它工作,这样就大大提高的 CPU 的工 作效率。下图的例子说明了 DMA 传输方式对 CPU 运行效率的影响:图中上面采用普通方式读 写存储器,下图采用 DMA 读写存储器,可以看出,DMA 的使用大大减少了时间消耗,提高 了 CPU 运行效率。 CPU 运算 1 普通方式读写存储器 CPU 运算 2 时间 CPU 运算 1 CPU 运算 2 DMA 读写存储器 Qsys 中有现成的 DMA 控制模块,下面我们就利用 DMA 控制模块,完成 SDRAM 的读 写。 179 硬件实现 12.2 硬件实现 首先在 Qsys 中添加 DMA 控制模块,在左侧边栏选择 Library->Bridges ->DMA->DMA Controller,双击,得到下图,因为 SDRAM 的数据宽度为 16bits,在模块设置中把 DMA 寄 存器的宽度改为 16,如下图红框所示,其它不变,点击 Finish 生成模块。 将 DMA 模块改名为 DMA,下面给 DMA 连线,DMA 的连线比较特殊,与其它普通模块 只连 clk、reset 和 s1 三个端口不 同,DMA 除了要 将这三个端 口连线外, 还要将 连上 read_master 和 write_master 端口。read_master 端口连接到 DMA 控制器读取的存储器的 Avalon 总线端口, write_master 端口连接到 DMA 控制器写入的存储器的 Avalon 总线端 180 硬件实现 口,因为我们的 DMA 读写都是对 SDRAM 进行的,将 read_master 商品和 write_master 端 口连至 SDRAM 控制器模块的 s1 端口(即 Avalon 总线端口)。此外,DMA 操作要涉及到中 断,所以要给 DMA 控制模块添加中断,最后分配地址,得到下图。 完成上述操作后,保存并编译,然后再直接编译 Quartus II 工程即可。 181 软件实现 12.3 软件实现 接下来完成软件实现。新建一个 Nios II 工程,命名为 dma,我们先打开 system.h 文件, 看看 DMA 控制模块的描述,如下图所示,里面包含了 DMA 模块的硬件配置信息。 /* * DMA configuration * */ #define ALT_MODULE_CLASS_DMA altera_avalon_dma #define DMA_ALLOW_BYTE_TRANSACTIONS 1 #define DMA_ALLOW_DOUBLEWORD_TRANSACTIONS 1 #define DMA_ALLOW_HW_TRANSACTIONS 1 #define DMA_ALLOW_QUADWORD_TRANSACTIONS 1 #define DMA_ALLOW_WORD_TRANSACTIONS 1 #define DMA_BASE 0x1060 #define DMA_IRQ 6 #define DMA_IRQ_INTERRUPT_CONTROLLER_ID 0 #define DMA_LENGTHWIDTH 16 #define DMA_MAX_BURST_SIZE 128 #define DMA_NAME "/dev/DMA" #define DMA_SPAN 32 #define DMA_TYPE "altera_avalon_dma" 接下来完成程序编写,虽然 Altera 的 HAL 系统提供了现成的 DMA 传输函数,但我们依 然采用直接给 DMA 寄存器赋值的方法来实现 DMA 传输(不过给寄存器赋值采用的是 API 函 数而非构建结构体,偷个小懒),只需编写 main.c 即可,编写完成后得到下图。 #include #include #include #include #include "system.h" #include "alt_types.h" #include "altera_avalon_dma_regs.h" #define DAT_LEN #define SRC_ADDR #define DST_ADDR 100 (SDRAM_BASE + 0x10000) (SDRAM_BASE + 0x20000) static void DMA_Init(void); //初始化DMA unsigned int dma_end_flag = 0; 182 软件实现 /* * === FUNCTION =================================================== * Name: main * Description: 通过DMA,将SDRAM中一个地址中的数据传输到另一个地址 * ================================================================= */ int main(void) { int i; unsigned short * ram_src = (unsigned short *)SRC_ADDR; unsigned short * ram_dst = (unsigned short *)DST_ADDR; for(i=0; i #include #include #include "system.h" #include "alt_types.h" #define OUT 1 #define IN 0 #define KEY_UP 0 #define KEY_DOWN 1 typedef struct _KEYBOARD_STRUCT { alt_u16 mask; alt_u8 value; alt_u8 up_value; alt_u8 state; alt_8 *desc; } KEYBOARD_STRUCT; extern void ps2_restart(void); extern alt_u8 ps2_input(void); extern alt_8 ps2_getch(void); extern void ps2_command(alt_u8 cmd); #endif 在 driver 文件夹中新建一个.c 源文件命名为 kb.c,编写完成后得到下图。 #include "../inc/kb.h" #include "altera_avalon_pio_regs.h" 190 #define OUT 1 #define IN 0 KEYBOARD_STRUCT key1_lut[]= { 0x16, 0x31, 0x21, 0, 0, // { 1 } { ! } 0x1e, 0x32, 0x40, 0, 0, // { 2 } { @ } 0x26, 0x33, 0x23, 0, 0, // { 3 } { # } 0x25, 0x34, 0x24, 0, 0, // { 4 } { $ } 0x2e, 0x35, 0x25, 0, 0, // { 5 } { % } 0x36, 0x36, 0x5e, 0, 0, // { 6 } { ^ } 0x3d, 0x37, 0x26, 0, 0, // { 7 } { & } 0x3e, 0x38, 0x2a, 0, 0, // { 8 } { * } 0x46, 0x39, 0x28, 0, 0, // { 9 } { ( } 0x45, 0x30, 0x29, 0, 0, // { 0 } { ) } 0x1c, 0x61, 0x41, 0, 0, // { a } { A } 0x32, 0x62, 0x42, 0, 0, // { b } { B } 0x21, 0x63, 0x43, 0, 0, // { c } { C } 0x23, 0x64, 0x44, 0, 0, // { d } { D } 0x24, 0x65, 0x45, 0, 0, // { e } { E } 0x2b, 0x66, 0x46, 0, 0, // { f } { F } 0x34, 0x67, 0x47, 0, 0, // { g } { G } 0x33, 0x68, 0x48, 0, 0, // { h } { H } 0x43, 0x69, 0x49, 0, 0, // { i } { I } 0x3b, 0x6a, 0x4a, 0, 0, // { j } { J } 0x42, 0x6b, 0x4b, 0, 0, // { k } { K } 0x4b, 0x6c, 0x4c, 0, 0, // { l } { L } 0x3a, 0x6d, 0x4d, 0, 0, // { m } { M } 0x31, 0x6e, 0x4e, 0, 0, // { n } { N } 0x44, 0x6f, 0x4f, 0, 0, // { o } { O } 0x4d, 0x70, 0x50, 0, 0, // { p } { P } 0x15, 0x71, 0x51, 0, 0, // { q } { Q } 0x2d, 0x72, 0x52, 0, 0, // { r } { R } 0x1b, 0x73, 0x53, 0, 0, // { s } { S } 0x2c, 0x74, 0x54, 0, 0, // { t } { T } 0x3c, 0x75, 0x55, 0, 0, // { u } { U } 0x2a, 0x76, 0x56, 0, 0, // { v } { V } 0x1d, 0x77, 0x57, 0, 0, // { w } { W } 0x22, 0x78, 0x58, 0, 0, // { x } { X } 0x35, 0x79, 0x59, 0, 0, // { y } { Y } 0x1a, 0x7a, 0x5a, 0, 0, // { z } { Z } 0x0e, 0x60, 0x7e, 0, 0, // { ` } { ~ } 0x4e, 0x2d, 0x5f, 0, 0, // { - } { _ } 0x55, 0x3d, 0x2b, 0, 0, // { = } { + } 0x5d, 0x5c, 0x7c, 0, 0, // { \ } { | } 0x54, 0x5b, 0x7b, 0, 0, // { [ } { { } 0x5b, 0x5d, 0x7d, 0, 0, // { ] } { } } 0x4c, 0x3b, 0x3a, 0, 0, // { ; } { : } 0x52, 0x27, 0x22, 0, 0, // { ' } { " } 0x41, 0x2c, 0x3c, 0, 0, // { , } { < } 0x49, 0x2e, 0x3e, 0, 0, // { . } { > } 0x4a, 0x2f, 0x3f, 0, 0, // { / } { ? } 0x5a, 0x0a, 0, 0, 0, // u{ enter } 0x29, 0x20, 0, 0, 0, // { space } 0x05, 0, 0, 0, "F1", 0x06, 0, 0, 0, "F2", 191 软件实现 0x04, 0, 0, 0, "F3", 0x0c, 0, 0, 0, "F4", 0x03, 0, 0, 0, "F5", 0x0b, 0, 0, 0, "F6", 0x83, 0, 0, 0, "F7", 0x0a, 0, 0, 0, "F8", 0x01, 0, 0, 0, "F9", 0x09, 0, 0, 0, "F10", 0x78, 0, 0, 0, "F11", 0x07, 0, 0, 0, "F12", 0x66, 0, 0, 0, "BKSP", 0x0d, 0, 0, 0, "TAB", 0x14, 0, 0, 0, "L_CTRL", 0xe01f, 0, 0, 0, "L_GUI", 0x11, 0, 0, 0, "L_ALT", 0xe014, 0, 0, 0, "R_CTRL", 0xe027, 0, 0, 0, "R_GUI", 0xe011, 0, 0, 0, "R_ALT", 0xe02f, 0, 0, 0, "APPS", 0x76, 0, 0, 0, "ESC", 0xe070, 0, 0, 0, "Ins", 0xe06c, 0, 0, 0, "Home", 0xe07d, 0, 0, 0, "PgUP", 0xe071, 0, 0, 0, "Del", 0xe069, 0, 0, 0, "End", 0xe07a, 0, 0, 0, "PgDn", 0xe075, 0, 0, 0, "U_ARROW", 0xe06b, 0, 0, 0, "L_ARROW", 0xe072, 0, 0, 0, "D_ARROW", 0xe074, 0, 0, 0, "R_ARROW", 0xe04a, 0, 0, 0, "KP_/", 0x7c, 0, 0, 0, "KP_*", 0x7b, 0, 0, 0, "KP_-", 0x79, 0, 0, 0, "KP_+", 0xe05a, 0, 0, 0, "KP_EN", 0x71, 0, 0, 0, "KP_.", 0x70, 0, 0, 0, "KP_0", 0x69, 0, 0, 0, "KP_1", 0x72, 0, 0, 0, "KP_2", 0x7a, 0, 0, 0, "KP_3", 0x6b, 0, 0, 0, "KP_4", 0x73, 0, 0, 0, "KP_5", 0x74, 0, 0, 0, "KP_6", 0x6c, 0, 0, 0, "KP_7", 0x75, 0, 0, 0, "KP_8", 0x7d, 0, 0, 0, "KP_9", 0xe037, 0, 0, 0, "Power", 0xe03f, 0, 0, 0, "Sleep", 0xe05e, 0, 0, 0, "Wake", 0, 0, 0, 0, 0, }; #define KEY_LSHIFT_MASK #define KEY_RSHIFT_MASK #define KEY_CAPS_MASK #define KEY_NUM_MASK (0x12) (0x59) (0x58) (0x77) 192 软件实现 软件实现 #define KEY_LSHIFT_INDEX (0) #define KEY_RSHIFT_INDEX (1) #define KEY_CAPS_INDEX (2) #define KEY_NUM_INDEX (3) KEYBOARD_STRUCT keys_lut[]= { KEY_LSHIFT_MASK, 0, 0, 0, 0, KEY_RSHIFT_MASK, 0, 0, 0, 0, KEY_CAPS_MASK, 0, 0, 0, 0, KEY_NUM_MASK, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; //L_shift //R_shift //caps //num /* * === FUNCTION =================================================== * Name: ps2_restart * Description: 复位函数 * ================================================================= */ void ps2_restart(void) { //clk and data direction is output IOWR_ALTERA_AVALON_PIO_DIRECTION(PS2_CLK_BASE, OUT); IOWR_ALTERA_AVALON_PIO_DIRECTION(PS2_DAT_BASE, OUT); //drive clk low and data high IOWR_ALTERA_AVALON_PIO_DATA(PS2_CLK_BASE, 0); IOWR_ALTERA_AVALON_PIO_DATA(PS2_DAT_BASE, 1); usleep(200); } /* * === FUNCTION =================================================== * Name: ps2_input * Description: 获取键盘输入数据 * ================================================================= */ alt_u8 ps2_input(void) { alt_u16 i, dat; IOWR_ALTERA_AVALON_PIO_DIRECTION(PS2_CLK_BASE, IN); IOWR_ALTERA_AVALON_PIO_DIRECTION(PS2_DAT_BASE, IN); usleep(1); for(i=0; i<11; i++) { //Wait for the device to bring Clock low. while(IORD_ALTERA_AVALON_PIO_DATA(PS2_CLK_BASE) & 0x01); //Wait for the device to bring Clock High. while(!(IORD_ALTERA_AVALON_PIO_DATA(PS2_CLK_BASE) & 0x01)); dat >>= 1; if(IORD_ALTERA_AVALON_PIO_DATA(PS2_DAT_BASE)) dat |= 0x200; 193 软件实现 } return dat & 0xff; } /* * === FUNCTION =================================================== * Name: ps2_getch * Description: 获取键值 * ================================================================= */ alt_8 ps2_getch(void) { alt_u16 i; alt_u8 status, ps2_dat; alt_8 mchar; ps2_dat = ps2_input(); if(ps2_dat == 0xf0) //断码 { ps2_input(); return 0xff; } else //通码 { for(i=0; i<50; i++) if(ps2_dat == key1_lut[i].mask) mchar = key1_lut[i].value; return mchar; } } /* * === FUNCTION =================================================== * Name: ps2_command * Description: 初始化键盘 * ================================================================= */ void ps2_command(alt_u8 cmd) { alt_u16 i, j; //clk and data direction is output IOWR_ALTERA_AVALON_PIO_DIRECTION(PS2_CLK_BASE, OUT); IOWR_ALTERA_AVALON_PIO_DIRECTION(PS2_DAT_BASE, OUT); //drive clk and data low IOWR_ALTERA_AVALON_PIO_DATA(PS2_CLK_BASE, 0); IOWR_ALTERA_AVALON_PIO_DATA(PS2_DAT_BASE, 0); usleep(200); //Release the Clock line. IOWR_ALTERA_AVALON_PIO_DIRECTION(PS2_CLK_BASE, IN); usleep(10); //Wait for the device to bring the Clock line low. while(IORD_ALTERA_AVALON_PIO_DATA(PS2_CLK_BASE) & 0x01); 194 软件实现 for(i=0,j=0; i<8; i++) { //Set/reset the Data line to send the first data bit. if(cmd & 0x01) { IOWR_ALTERA_AVALON_PIO_DATA(PS2_DAT_BASE, 1); j++; } else IOWR_ALTERA_AVALON_PIO_DATA(PS2_DAT_BASE, 0);; cmd >>= 1; //Wait for the device to bring Clock high. while(!(IORD_ALTERA_AVALON_PIO_DATA(PS2_CLK_BASE) & 0x01)); usleep(20); //Wait for the device to bring Clock low. while(IORD_ALTERA_AVALON_PIO_DATA(PS2_CLK_BASE) & 0x01); usleep(20); } if(!(j&0x01)) IOWR_ALTERA_AVALON_PIO_DATA(PS2_DAT_BASE, 1); else IOWR_ALTERA_AVALON_PIO_DATA(PS2_DAT_BASE, 0); //Send parity bit usleep(20); //Wait for the device to bring Clock high. while(!(IORD_ALTERA_AVALON_PIO_DATA(PS2_CLK_BASE) & 0x01)); usleep(20); //Wait for the device to bring Clock low. while(IORD_ALTERA_AVALON_PIO_DATA(PS2_CLK_BASE) & 0x01); IOWR_ALTERA_AVALON_PIO_DATA(PS2_DAT_BASE, 1); //Send stop bit usleep(20); //Wait for the device to bring Clock high. while(!(IORD_ALTERA_AVALON_PIO_DATA(PS2_CLK_BASE) & 0x01)); usleep(20); //Wait for the device to bring Clock low. while(IORD_ALTERA_AVALON_PIO_DATA(PS2_CLK_BASE) & 0x01); //Release the Data line. IOWR_ALTERA_AVALON_PIO_DIRECTION(PS2_DAT_BASE, IN);; usleep(20); //Wait for the device to bring Data low. while(IORD_ALTERA_AVALON_PIO_DATA(PS2_DAT_BASE) & 0x01); //Wait for the device to bring Clock low. while(IORD_ALTERA_AVALON_PIO_DATA(PS2_CLK_BASE) & 0x01); usleep(20); //Wait for the device to bring Clock high. while(!(IORD_ALTERA_AVALON_PIO_DATA(PS2_CLK_BASE) & 0x01)); //Wait for the device to bring Data high. while(!(IORD_ALTERA_AVALON_PIO_DATA(PS2_DAT_BASE) & 0x01)); 195 软件实现 } kb.c 中 主 要 定 义 了 四 个 函 数 : ps2_command() 、 ps2_restart() 、 ps2_input() 和 ps2_getch()。ps2_command()函数的功能为初始化 PS/2 接口键盘,通过发送命令 0xff,完 成对 PS/2 接口键盘的初始化;ps2_restart()函数的功能为复位,将 PS2_DAT 和 PS2_CLK 这 两个 PIO 的方向设为输出,值分别拉高和拉低,实现复位功能,为接下来接收数据做准备; ps2_input()函数的功能为获取键盘的输入数据;ps2_getch()函数的功能为先通过 ps2_input() 获取键盘的输入数据,再将获取的数据译码得到键值。 这里再简单介绍下 Nios II 控制 PS/2 键盘数据接收和译码的原理(即 ps2_getch()函数)。 先调用 ps2_input()函数获取键盘的输入数据,然后判断接收数据是断码还是通码:断码是指 松开键盘时键盘发送的数据码,通码是指按下键盘时键盘发送的数据。对于通码,我们要查存 储在结构体中的通码值表,将数据译为对应的键值作为返回值;对于断码,则直接返回 0xff。 需要注意的是,我们在 ps2_getch()函数只去判断结构体中前 50 个值。 PS/2 接口的具体原理和时序就不详细介绍了,感兴趣的同学可以查阅一下相关资料,我们 在 TIGER BOARD 的资料中也给大家提供了 PS/2 接口的资料。 最后完成 main.c 文件的编写,完成后得到下图。 #include "../inc/kb.h" alt_u8 key; int main(void) { ps2_command(0xff); //初始化键盘 while(1) { usleep(100); ps2_restart(); //复位 key = ps2_getch(); //读取键值 if(key < 0x80) printf("%c", key); } return 0; } //打印键值 196 软件实现 在 main 函数中,首先调用 ps2_command()函数初始化键盘,然后进入 while(1)循环, 在循环中,先调用 ps2_restart()函数复位,然后调用 ps2_getch()函数读取键值,如果键值小 于 0x80,说明是通码,则将键值打印出来。 197 13.4 总结 DS1302 应用——RTC 完成软件编写后,先把 PS/2 接口的键盘连到 TIGER BOARD 上,运行 Nios II 工程,我们 按下键盘上的值(当然必须属于结构体表前 50 个之中),相应的键值就会在状态区打印出来, 比如我们从 a 开始按字母区第二排的值,效果如下图。 本章主要介绍了在 Nios II 中控制 PS/2 接口键盘。因为 Qsys 没有现成的 PS/2 接口控制 模块,所以我们采用 PIO 模拟 PS/2 接口的 PS2_DAT 和 PS2_CLK 来实现功能。在硬件实现中, 添加了两个 PIO:PS2_DAT 和 PS2_CLK,要注意它们都是双向的。在软件实现中,通过几个 函数实现了在状态区打印出 PS/2 接口键盘的按下键的键值的功能,这里并没有详细介绍 PS/2 接口的原理和具体时序,有感兴趣的同学可以查阅相关资料,加深对 PS/2 接口的理解。这一 章的内容就到这里,因本人水平有限,文中有任何错误请联系我,大家在交流中进步!邮箱: vito943@qq.com 198 定制自己的 IP 第14章 定制自己的 IP——数码管控 制器 本章主要介绍在 Nios II 中根据需求定制基于 Avalon 总线的 IP 核模块,并使用定制的模 块实现对数码管的控制。通过本章,你能学到 (1)在 Nios II 中定制基于 Avalon 总线的 IP 核模块。 (2)在 Nios II 中控制数码管。 本章分为五个部分: 一、概述 二、定制 IP 核模块 三、硬件实现 四、软件实现 五、总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 199 概述 14.1 概述 在学习本章之前,我们再深入理解下 Nios II 的含义。什么是 Nios II? Nios II 本质上也 是嵌入式系统,与传统的单片机、ARM 等嵌入式系统不同的是,它能根据需求自己搭建芯片 内部的硬件,即 IP 核模块。  定制 IP 核模块 Altera 提供了许多模块可以直接使用,如 PIO、RS232 串口、Flash 控制模块等,当我们 想实现某些功能而没有现成模块可用的时候,就需要自己定制模块了。在 Qsys 中,IP 核模块 被称为 component,即元件。要创建生成一个 IP 核模块,需要自己根据功能用硬件描述语言 编写文件(这里用 Verilog),然后再做一些配置,就能封装成一个 component 了。  Avalon 总线 Avalon 这个词在 Nios II 的学习中会反复出现,它其实是一种 Altera 定义的总线协议,包 括 Avalon Streaming Interface (Avalon-ST) 、 Avalon Memory Mapped Interface (Avalon-MM)、Avalon Tri-State Conduit Interface (Avalon-TC)等。在 Nios II 中,CPU 通 过 Avalon 总线(一般为 Avalon-MM)来控制 IP 核模块,如下图所示:在基于 Avalon 总线 的通信中,CPU 是 Master,IP 核模块(如图中的 Flash Controller、UART 等)是 Slave。 200 概述 对于自己定制的 IP 核模块,我们不仅要在 Verilog 文件中实现其基本功能,同时也要编写 符合 Avalon 总线协议的读写时序(即上图中高亮的 Avalon-MM Slave),让 CPU 能够顺利地 控制定制的 IP 核模块。Avalon-MM Slave 的具体结构如下图所示。  数码管 数码管在嵌入式开发和日常应用中都很常见,相比较 LED,它能提供更多的信息量,可以 201 概述 用来计时、显示数据等。以常用的 8 段数据管为例,其原理图如下。每一位数码管分成 A、B、…、 Dp 共 8 段,每段由一个 LED 组成,通过控制每段 LED 的电平(即段码)来决定该段的亮灭, 从而组成不同的字符数据。 小贴士 Tips:数码管的分类 按连接方式来分,数码管分为共阴极和共阳极两种:LED 的阳极一起连接到电源正极的 称为共阳数码管,LED 的阴极一起连接到电源负极的称为共阴数码管。 按驱动方式来分,数码管分为静态式和扫描式两种:静态驱动是指每个数码管的每一个 段码都由一个 CPU 的 I/O 端口进行驱动,优点是编程简单,亮度高,缺点是占用太多 I/O 资源。动态驱动是将所有数码管的 8 个段码"a,b,c,d,e,f,g,dp"的同名端连在一起,另 外为每个数码管的公共极 COM 增加位选控制电路,位选由各自独立的 I/O 线控制,通 过分时轮流控制各个数码管的的 COM 端,就使各个数码管轮流受控显示,这就是动态 驱动。在轮流显示过程中,每位数码管的点亮时间为 1~2ms,由于人的视觉暂留现象 及发光二极管的余辉效应,尽管实际上各位数码管并非同时点亮,但只要扫描的速度足 够快,给人的印象就是一组稳定的显示数据,不会有闪烁感,动态显示的效果和静态显 示是一样的,能够节省大量的 I/O 资源,而且功耗更低,因此使用更为广泛。 在本章中,我们要定制一个符合 Avalon-MM Slave 规范的 IP 核模块,CPU 通过该模块 控制数码管,实现数据的扫描显示功能。可以看到,这是一个集 IP 核模块定制、Avalon 总线 和数码管的综合应用,还是有点挑战性的,下面我们就来搞定它。 202 14.2 定制 IP 核模块 定制 IP 核模块 首先完成 IP 核的定制。TIGER BOARD 上采用的是下图所示的 8 位 8 段数码管,它采用共 阳极的连接方式和动态显示的驱动方式。DSEL0~DSEL7 为位选端,值由一个三八译码器 74HC138D 输出,用来进行数码管位选;DIGa~DIGdp 为段码端,用来控制选中位数码管的 数 据 显 示 。 我 们 通 过 控 制 控 制 三 八 译 码 器 的 输 入 端 DSEL0_IN~DSEL2_IN 和 段 选 端 DIGa~DIGdp,实现对数码管的控制。 在编写定制 IP 核模块的硬件描述语言文件之前,先介绍下要用到的接口信号,如下图所示, 它包含了时钟、复位、片选、字节使能、读写使能、地址线、数据线等等一系列信号,Interface 为信号的接口类型,Signal Type 为信号类型,Width 为信号宽度,Direction 为信号方向。可 以看到,里面大部分信号是 avalon_slave_0 类型,它们都是 Avalon 总线的标准信号。 coe_dsel_138 和 coe_led_num_dig 是输出到三八译码器和数码管的信号,不属于 Avalon 总 线。需要说明的是,信号的命名采用了 Name 中的规范命名格式,这样在生成 IP 核模块时容 易被系统识别出接口类型和信号类型。 203 定制 IP 核模块 接下来完成硬件描述语言文件的编写,在 Quartus II 中新建一个.v 文件命名为 dpy.v,编 写完成后得到下图。 /*----------------------------------------------------------------Function:dpy refreash, avalon mm interface ----------------------------------------------------------------*/ module dpy( input input input input input [3:0] input input [31 :0] input output [31 :0] csi_clk, csi_reset_n, avs_chipselect_n, avs_address, avs_byteenable_n, avs_write_n, avs_writedata, avs_read_n, avs_readdata, output [2:0] coe_dsel_138, output [7:0] coe_led_num_dig); //段码寄存器,存储数码管的段码 reg [7:0] r_dig0,r_dig1,r_dig2,r_dig3,r_dig4,r_dig5,r_dig6,r_dig7; //Avalon总线写时序,给寄存器赋值 always@(posedge csi_clk or negedge csi_reset_n) if(~csi_reset_n) begin r_dig0 <= 7'b0; r_dig1 <= 7'b0; r_dig2 <= 7'b0; r_dig3 <= 7'b0; end else if( ~avs_chipselect_n & ~avs_write_n & avs_address == 1'b0 ) begin if (~avs_byteenable_n[0]) r_dig0 <= avs_writedata[7:0]; if (~avs_byteenable_n[1]) r_dig1 <= avs_writedata[15:8]; if (~avs_byteenable_n[2]) r_dig2 <= avs_writedata[23:16]; if (~avs_byteenable_n[3]) 204 定制 IP 核模块 r_dig3 <= avs_writedata[31:24]; end always@(posedge csi_clk or negedge csi_reset_n) if(~csi_reset_n) begin r_dig4 <= 7'b0; r_dig5 <= 7'b0; r_dig6 <= 7'b0; r_dig7 <= 7'b0; end else if( ~avs_chipselect_n & ~avs_write_n & avs_address == 1'b1 ) begin if (~avs_byteenable_n[0]) r_dig4 <= avs_writedata[7:0]; if (~avs_byteenable_n[1]) r_dig5 <= avs_writedata[15:8]; if (~avs_byteenable_n[2]) r_dig6 <= avs_writedata[23:16]; if (~avs_byteenable_n[3]) r_dig7 <= avs_writedata[31:24]; end //------------------------------------------ //Avalon总线读时序,读寄存器值 reg [31:0] readdata; always@( * ) if(~avs_read_n & ~avs_chipselect_n) case(avs_address) 1'b0: readdata = {r_dig3,r_dig2,r_dig1,r_dig0}; 1'b1: readdata = {r_dig7,r_dig6,r_dig5,r_dig4}; endcase assign avs_readdata = readdata; //-------------------------------------------- //数码管动态扫描逻辑,实现8位数码管的动态显示 reg[23:0] counter; always@(posedge csi_clk or negedge csi_reset_n) if(~csi_reset_n) counter <= 24'b0; else counter <= counter + 24'b1; assign coe_dsel_138 = counter[19:17]; reg [7:0] led_num_dig; always@(coe_dsel_138) begin case(coe_dsel_138) 4'h0 : led_num_dig = r_dig7; 205 定制 IP 核模块 4'h1 : led_num_dig = r_dig6; 4'h2 : led_num_dig = r_dig5; 4'h3 : led_num_dig = r_dig4; 4'h4 : led_num_dig = r_dig3; 4'h5 : led_num_dig = r_dig2; 4'h6 : led_num_dig = r_dig1; 4'h7 : led_num_dig = r_dig0; endcase end assign coe_led_num_dig = led_num_dig; endmodule dpy.v 文件实现的功能是接收 CPU 通过 Avalon 总线传输的数据(数码管段码值),并通 过一个数码管的动态扫描逻辑,实现将 8 位数码管的段码值动态显示在数码管上。将编写好的 dpy.v 文件保存,在 Quartus II 工程中新建一个名为 my_ip 的文件夹,在 my_ip 中新建名为 dpy 的文件夹,我们把 dpy.v 放在 dpy 文件夹中。 完成 dpy.v 的编写后,我们来生成 IP 核模块。打开 Qsys 软件,在菜单栏中选择 File->New component,如下图所示。 点击后得到下图,框 1 和框 2 是 IP 核的模块名及显示名,都改为 dpy_controller,框 3 为 模块分组,我们把模块放到 MyIP 组下(此时 Library 中没有 MyIP 组,系统会自动生成),其 它设置不变,点击 Next。 206 定制 IP 核模块 然后得到下图的 About Files 对话框,把刚才编写完成的 dpy.v 文件添加到 Synthesis Files 中,如框 1 所示,然后点击框 2 中的 Analyze Synthesis Files,系统会分析添加的文件,分析 完成后把框 3 的 Top-level module(顶层模块)设为 dpy,其它设置不变,点击 Next。 207 定制 IP 核模块 得到 About Parameters 对话框,这里没什么需要设置的,点击 Next,得到 About Signals 对话框,对模块信号的参数做设置,包括接口类型、信号类型、信号宽度和信号方向,因为我 们在编写 dpy.v 时端口信号的命名采用了规范的格式,系统能识别信号从而设置成正确参数, 所以无需再设置(辛苦得到回报)。如果系统未设置成正确参数,按照下图设置即可,完成后点 击 Next。 208 定制 IP 核模块 最后是 About Interface 对话框,我们注意到对话框底部的状态区有一个 Error,指 avalon_slave_0 接口没有关联复位键,先除掉这个 Error。在对话框中找到 avalon_slave_0, 把 Associated Reset 设置为 clock_reset,如下图所示,Error 就消失了。 About Interface 对话框是对接口的一些设置,包含接口名称、关联时钟、关联复位、时 序等参数设置,我们先不用修改,点击 Finish,弹出下图的对话框,得到下图,可以看到,IP 核的参数是以 tcl 脚本文件的形式存在的,选择 Yes,Save,完成 IP 核的创建。 209 定制 IP 核模块 这时再查看侧边栏的 Library,就可以看到我们自己定制生成的 IP 核模块 dpy_cotroller 了。 如果要对生成的模块进行修改,选中模块右键单击,选择 Edit 进行修改即可。 我们选中已经生成的 dpy_controller 模块,右键单击,选择 Edit,在弹出的对话框中选择 210 定制 IP 核模块 上侧边栏的 Interfaces 选项,这里来简单说一下地址对齐问题。在 avalon_slave_0 接口的参 数中有一项 Deprecated 选项(它位于 Write Waveforms 选项后面,在 Qsys 中只有生成模 块后再 Edit 模块才会出现,而且内容是隐藏的需要点击 Deprecated 左边的三角打开,真是太 隐蔽了~囧)。里面有一个 Slave addressing 选项,即 Slave 的地址对齐:Native 是静态对齐, Dynamic 是动态对齐。 地址对齐指的是 Avalon 总线的 Master 与 Slave 的地址对齐,当 Master 和 Slave 的数据 线宽度不匹配时就会存在地址对齐问题。一般而言 Master 总线的宽度固定为 32bit(寻址结 构上由 4 个连续地址的 8bit 数据拼成,组成一个 word),Slave 则不一定,有可能是 32bit, 也有可能是 8bit、16bit、64bit,当 Master 与 Slave 数据宽度不等的时候,静态地址对齐和 动态地址的处理方式是不同的。因为我们创建的模块数据宽度为 32bit,所以不存在地址对齐 问题,在这里选择较稳妥的 Native。 如果你想了解更多关于地址对齐的问题,可以参考教程附录的《Avalon 总线的地址对齐: Dynamic Addressing 和 Native Addressing》。 再来观察一下 Avalon 总线的读写时序图,如下图所示,可以看到,Avalon 总线的读写时 序是非常简单的并行读写时序。 211 定制 IP 核模块 最后还需要添加一下 IP 搜索路径,在菜单栏选择 Tools->Options,如下图所示。 得到下图,我们把 dpy.v 所在路径添加进去,点击 Finish 即可。添加 IP 搜索路径的目的 在于让 Qsys 记住 dpy.v 的路径,避免再次打开 Qsys 时模块无效。 212 定制 IP 核模块 至此,IP 核模块的定制工作就全部完成了。 213 硬件实现 14.3 硬件实现 完成 IP 核模块定制后,继续完成硬件实现。在 Qsys 中选择刚刚生成的 dpy_controller 模块,得到下图,直接点击 Finish 完成添加。 将添加的模块改名为 DPY,然后连线、端口引出、分配地址,完成后得到下图。 保存并编译,完成后打开 Quartus II,在顶层 bdf 文件中更新 kernel 得到新添加的 IO 端 口,dpy_dsel_138 和 dpy_led_num_dig。将它们生成管脚、分配管脚,完成后得到下图,它 们将分别与 TIGER BOARD 上 74HC138D 的输入端和数码管的段码端相连。 上述工作完成后编译 Quartus II 工程,编译成功后硬件实现的工作就搞定了。 214 软件实现 14.4 软件实现 最后完成软件实现。新建一个 Nios II 工程,命名为 dpy,首先打开 system.h 文件,查看 一下关于 dpy 的描述,如下图所示,包含了 dpy 模块的基地址等信息。 /* * DPY configuration * */ #define ALT_MODULE_CLASS_DPY dpy_controller #define DPY_BASE 0x1120 #define DPY_IRQ -1 #define DPY_IRQ_INTERRUPT_CONTROLLER_ID -1 #define DPY_NAME "/dev/DPY" #define DPY_SPAN 8 #define DPY_TYPE "dpy_controller" 下面完成程序的编写,只需编写 main.c 文件即可,编写完成后得到下图。 #include #include "system.h" /* * === FUNCTION ==================================== * Name: dpy_decode * Description: 数码管译码函数,将0~9译成数码管段码值 * =================================================== */ unsigned char dpy_decode(unsigned char number) { switch(number) { case 0x0:return 0x03; case 0x1:return 0x9F; case 0x2:return 0x25; case 0x3:return 0x0D; case 0x4:return 0x99; case 0x5:return 0x49; case 0x6:return 0x41; case 0x7:return 0x1F; case 0x8:return 0x01; case 0x9:return 0x09; default:return 0xff; } return 0xff; } 215 软件实现 /* * === FUNCTION ==================================== * Name: dpy_display * Description: 数码管显示函数 * =================================================== */ void dpy_display(unsigned int number) { int *dpy_num = (int*)DPY_BASE; int i; unsigned int j=10000000; unsigned char display[8]; for (i=7;i>=0;i--) { display[i]= number / j; number -= j * display[i]; j /= 10; } *(dpy_num+1) = (dpy_decode(display[0]) << 24) +(dpy_decode(display[1]) << 16) +(dpy_decode(display[2]) << 8) + dpy_decode(display[3]); *dpy_num = (dpy_decode(display[4]) << 24) +(dpy_decode(display[5]) << 16) +(dpy_decode(display[6]) << 8) + dpy_decode(display[7]); } /* * === FUNCTION ==================================== * Name: main * Description: 主函数,从0开始计数并将值显示到数码管上 * =================================================== */ int main() { printf("Hello from Nios II!\n"); int i=0,j=10000; while(1) { dpy_display(i++); while(j--); j=1000000; } return 0; } 整个 main.c 的内容并不复杂,主要定义了两个函数:dpy_decode()和 dpy_display()。 dpy_decode()是数码管译码函数,功能是将 0~9 译码成数码管的段码。dpy_display()是数码 管显示函 数, 它将参 数 number 的值 从高到 低拆 分成数 码管 8 位 对应的 数值, 并 通过 216 软件实现 dpy_decode()函数将每个数值译码成段码值,再将段码值通过 Avalon 总线传输到 DPY 模块 中,最终经由 DPY 模块的 dpy_dsel_138 和 dpy_led_num_dig 端口输出给 74HC138D 和数 码管的段码端,将期望数值以动态方式显示在数码管上。 在 main 函数中,通过调用上述两个函数,最终实现数码管显示一个从 0 开始的计数器。 217 14.5 总结 DS1302 应用——RTC 完成软件编写后,运行 Nios II 工程,TIGER BOARD 上的数码管会从 0 开始计数。 本章主要介绍了在 Nios II 中根据需求定制基于 Avalon 总线的 IP 核模块,并使用定制的 模块实现对数码管的控制,最终用数码管实现了一个从 0 开始的计数器。在定制 IP 核模块部分, 首先编写了硬件描述语言文件 dpy.v,并基于它在 Qsys 中定制生成了 IP 核模块 dpy_controller, 模块的功能是将存储的 8 位段码值动态显示在数码管上。在硬件实现部分,将新生成的 dpy_controller 模块添加到了工程中。最后在软件实现部分,通过几个函数实现了从 0 开始计 数,并将计数值拆分成 8 位,译成段码值后通过 Avalon 总线传输给 DPY 模块的功能,再结合 DPY 模块,最终实现了数码管计数器。 本章重点需要大家掌握 IP 核模块的定制,在 Nios II 研发中,Qsys 提供的 IP 核模块毕竟 是有限的,免不了需要我们自己根据需求来定制模块。这一章的内容就到这里,因本人水平有 限,文中有任何错误请联系我,大家在交流中进步!邮箱:vito943@qq.com 218 PCF8591 应用 第15章 AD 转换和 DA 转换 本章主要介绍在 Nios II 中利用 PCF8591 芯片实现 A/D 和 D/A 功能,通过本章,你能学 到: (1)在 Nios II 中利用 PCF8591 实现 A/D 和 D/A。 本章分为四个部分: 一、概述 二、软件实现——A/D 三、软件实现——D/A 四、总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 219 概述 15.1 概述 我们所处的真实世界是模拟信号组成的世界,无论是温度、声音还是电磁波,都是连续的 模拟信号。而硬件设计发展到现在,早已经是数字信号主宰的时代:无论核心是 FPGA、ARM 还是 DSP,它们处理的都是离散的数字信号。显然,要让这些芯片来处理模拟信号,必然需要 相应的模拟/数字转换器,即 A/D;同样,要把处理完的数字信号还原成模拟信号,也必然需 要相应的数字/模拟转换器,即 D/A。 TIGER BOARD 上配有集 A/D 和 D/A 功能于一身的 PCF8591 芯片,它是 Philips 公司生 产的 8 位 A/D、D/A 转换器。它具有 4 个模拟输入、一个输出和一个串行 I2C 总线接口。3 个 地址引脚 A0、A1 和 A2 用于编程硬件地址,允许将最多 8 个器件连接至 I2C 总线而不需要额 外硬件。器件的地址、控制和数据通过两线双向 I2C 总线传输。 TIGER BOARD 上 PCF8591 及周边原理图如下图所示。其中 AIN0~AIN3 为 A/D 转换器 的 4 个模拟输入通道,分别连接了两个 SMA 接头和两个电位器(滑动变阻器);AOUT 为 D/A 转换器的模拟输出;A0~A2 为硬件地址;SCL、SDA 为 I2C 总线的时钟线和数据线,与 CPU 相连,用来配置芯片和传输数据(即数字信号)。 在本章中,我们让 PCF8591 接收通道 2 的模拟信号(保持 P5 的 5、6 管脚连通), 220 概述 经过 A/D 转换后将数字信号通过 I2C 总线传输到 Nios II CPU 中,CPU 再将数据显示到数码 管上;将数字信号从 CPU 通过 I2C 总线传输给 PCF8591,经过 D/A 转换后将模拟信号从 AOUT 输出,以 LED 灯 D2 的亮灭表示模拟信号的强弱(保持 P7 的 2、3 管脚连通)。可以看出,PCF8591 的使用离不开 I2C 这位“老朋友”,经过前面 EEPROM 的实现,相信大家对 I2C 已经得心应手 了,下面就正式开工。 221 15.2 软件实现——A/D 软件实现——A/D 在 EEPROM 一章中已经提到, Nios II 中没有 I2C 总线接口控制模块,而是使用两个 PIO 来模拟 I2C 总线的 SCL、SDA 来实现 I2C 的读写时序。这一章依然如此,我们直接使用 EEPROM 中已经生成的两个 PIO:SCL、SDA 来连接 PCF8591 的 I2C 总线,硬件部分无需修改,直接 软件实现搞起,我们先来实现 A/D 功能。 打开 Nios II SBT,新建一个工程命名为 ad,将目录区整理为三文件夹结构。在 inc 文件 夹中新建一个.h 头文件 sopc.h,编写完成后得到下图。 #ifndef SOPC_H_ #define SOPC_H_ /*------------------------------------------------------- * Define *------------------------------------------------------*/ #define OUT 1 #define IN 0 #define AddWr 0x90 //写数据地址 #define AddRd 0x91 //读数据地址 /*------------------------------------------------------* Extern *------------------------------------------------------*/ extern unsigned char ReadADC(unsigned char Chl); extern void dpy_display(unsigned int number); #endif /*SOPC_H_*/ sopc.h 中没什么特别的内容,就不细讲了。在 driver 文件夹中新建两个.c 源文件 dpy.c 和 ad.c,先完成 dpy.c 的编写,得到下图。 #include #include "system.h" #include "../inc/sopc.h" /* * === FUNCTION ==================================== * Name: dpy_decode * Description: 数码管译码函数,将0~9译成数码管段码值 * =================================================== 222 软件实现——A/D */ unsigned char dpy_decode(unsigned char number) { switch(number) { case 0x0:return 0x03; case 0x1:return 0x9F; case 0x2:return 0x25; case 0x3:return 0x0D; case 0x4:return 0x99; case 0x5:return 0x49; case 0x6:return 0x41; case 0x7:return 0x1F; case 0x8:return 0x01; case 0x9:return 0x09; default:return 0xff; } return 0xff; } /* * === FUNCTION ==================================== * Name: dpy_display * Description: 数码管显示函数 * =================================================== */ void dpy_display(unsigned int number) { int *dpy_num = (int*)DPY_BASE; int i; unsigned int j=10000000; unsigned char display[8]; for (i=7;i>=0;i--) { display[i]= number / j; number -= j * display[i]; j /= 10; } *(dpy_num+1) = (dpy_decode(display[0]) << 24) +(dpy_decode(display[1]) << 16) +(dpy_decode(display[2]) << 8) + dpy_decode(display[3]); *dpy_num = (dpy_decode(display[4]) << 24) +(dpy_decode(display[5]) << 16) +(dpy_decode(display[6]) << 8) + dpy_decode(display[7]); } dpy.c 基于上一章的数码管的例程修改,具体的原理上一章已经详细介绍,这里就不再赘 述了。下面完成 ad.c 的编写,得到下图。 #include #include 223 #include "altera_avalon_pio_regs.h" #include "system.h" #include "../inc/sopc.h" /*-----------------------------------------------启动IIC总线 ------------------------------------------------*/ void Start(void) { IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, OUT); IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, 1); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, 0); usleep(5); } /*-----------------------------------------------停止IIC总线 ------------------------------------------------*/ void Stop(void) { IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, OUT); IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, 0); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(5); IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, 1); usleep(10); } /*-----------------------------------------------应答IIC总线 ------------------------------------------------*/ void Ack(void) { alt_u8 tmp; IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, IN); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(5); tmp = IORD_ALTERA_AVALON_PIO_DATA(SDA_BASE); usleep(5); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); usleep(10); while(tmp); } /*------------------------------------------------ 224 软件实现——A/D 软件实现——A/D 发送一个字节 ------------------------------------------------*/ void Send(unsigned char Data) { alt_u8 i, tmp; IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, OUT); for(i=0; i<8; i++) { IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); usleep(5); tmp = (Data & 0x80) ? 1 : 0; Data <<= 1; IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, tmp); usleep(5); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(10); } } /*-----------------------------------------------读入一个字节并返回 ------------------------------------------------*/ unsigned char Read(void) { alt_u8 i, dat = 0; IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, IN); for(i=0; i<8; i++) { IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(5); dat <<= 1; dat |= IORD_ALTERA_AVALON_PIO_DATA(SDA_BASE); usleep(5); } usleep(5); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); return dat; } /*-----------------------------------------------读取AD模数转换的值,有返回值 ------------------------------------------------*/ 225 软件实现——A/D unsigned char ReadADC(unsigned char Chl) { unsigned char Data; Start(); //写入芯片地址 Send(AddWr); Ack(); Send(0x40|Chl);//写入选择的通道,本程序只用单端输入,差分部分需要自行添加 //Chl的值分别为0、1、2、3,分别代表1-4通道 Ack(); Start(); Send(AddRd); //读入地址 Ack(); Data=Read(); //读数据 Stop(); return Data; //返回值 } ad.c 文件通过几个函数实现了读取芯片指定通道的 A/D 转换值,关于 I2C 的函数就不再 介绍了,有不太明白的同学可以回顾下 EEPROM 一章(前面树栽的多后面真是好乘凉!),可 以看出,Nios II 的学习越到后面综合性越强,往往是一个工程包含多个知识点,而其中的许多 知识点是前面已经学过的,所以前面打好基础后面就会越学越顺手,闲话就说这么多,下面来 讲解下读 ADC 函数 ReadADC()。 在讲解函数之前,首先介绍下 PCF8591 的时序。CPU 控制 PCF8591 是通过 I2C 总线实 现的,对于 D/A 转换,要通过 I2C 的写时序将数字信号写入芯片中,时序如下图上所示;对于 A/D 转换,我们要通过 I2C 的读时序读取转换后的数字信号,时序如下图下所示,当然,在读 之前也要写入 CONTROL BYTE,配置好芯片。 226 软件实现——A/D 综上可知,实现读一个 A/D 转换后数据的时序为:启动->写器件地址(写)->应答->写 控制字->应答->启动->写器件地址(读)->应答->读数据->非应答->停止。 其中,器件地址的取值如下图所示,因为 PCF8591 的 A0~A2 接地,所以写操作取值为 10010000,读操作取值为 10010001。 控制字用来对芯片的工作状态进行配置,取值如下图所示:Bit7、Bit3 固定为 0,Bit6 为 模拟输出使能标志,置 1 有效;Bit5、Bit4 为模拟输出方式的配置,根据值不同配置为图中的 四种方式;Bit2 为自动增量标志,置 1 有效;Bit1 和 Bit0 为 A/D 通道选择。 227 软件实现——A/D 在 ReadADC()中,首先写入芯片地址(写),即 0x90(10010000),然后写入控制字; 再写入芯片地址(读),最后读取数据,完成一次 A/D 转换数据的读取。函数的参数为读取的 A/D 数据的通道号。 最后在 main 文件夹中完成 main.c 文件的编写,编写完成后得到下图。 /*----------------------------------------------此程序通过IIC协议对PCF8591芯片操作,读取电位器的电压,在数码管上显示,范围为0~255 228 软件实现——A/D ------------------------------------------------*/ #include #include #include "altera_avalon_pio_regs.h" #include "system.h" #include "../inc/sopc.h" /*------------------------------------------------ 主程序 ------------------------------------------------*/ int main() { while(1) { dpy_display(ReadADC(2));//读通道2电压并在数码管上显示 usleep(1000000); } return 0; } main.c 非常简单,在主函数的 while(1)循环中调用 ReadADC 函数读取通道 2 的值,并显 示在数码管上,时间间隔约为 1s。至此整个 A/D 转换功能就实现了,下一节继续搞定 D/A。 229 15.3 软件实现——D/A 软件实现——D/A 接下来实现 D/A 功能,有了上面的 A/D 实现的基础,D/A 的实现就轻松多了。在概述中 已经讲到,D/A 实现的原理是将芯片的 D/A 输出管脚 AOUT 与 LED 的负极相连,控制 AOUT 的输出值在 0~3.3v 之间循环变化,则 LED 就会循环亮灭。 在 Nios II 新建一个工程命名为 da,将目录区整理为三文件夹结构。在 inc 文件夹中新建.h 文件 sopc.h,编写完成后得到下图。 #ifndef SOPC_H_ #define SOPC_H_ /*----------------------------------------------------- * Define *----------------------------------------------------*/ #define OUT 1 #define IN 0 #define AddWr 0x90 //写数据地址 #define AddRd 0x91 //读数据地址 /*----------------------------------------------------* Extern *----------------------------------------------------*/ extern void DAC(unsigned char Data); #endif /*SOPC_H_*/ sopc.h 的内容与 A/D 实验类似,只是声明的函数不同,就不再详述了。 接下来在 driver 文件夹中新建一个.c 源文件命名为 da.c,完成编写后得到下图。 #include #include #include "altera_avalon_pio_regs.h" #include "system.h" #include "../inc/sopc.h" /*-----------------------------------------------启动IIC总线 ------------------------------------------------*/ void Start(void) { IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, OUT); 230 IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, 1); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, 0); usleep(5); } /*-----------------------------------------------停止IIC总线 ------------------------------------------------*/ void Stop(void) { IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, OUT); IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, 0); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(5); IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, 1); usleep(10); } /*-----------------------------------------------应答IIC总线 ------------------------------------------------*/ void Ack(void) { alt_u8 tmp; IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, IN); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(5); tmp = IORD_ALTERA_AVALON_PIO_DATA(SDA_BASE); usleep(5); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); usleep(10); while(tmp); } /*-----------------------------------------------发送一个字节 ------------------------------------------------*/ void Send(unsigned char Data) { alt_u8 i, tmp; IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, OUT); for(i=0; i<8; i++) 231 软件实现——D/A 软件实现——D/A { IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); usleep(5); tmp = (Data & 0x80) ? 1 : 0; Data <<= 1; IOWR_ALTERA_AVALON_PIO_DATA(SDA_BASE, tmp); usleep(5); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(10); } } /*-----------------------------------------------读入一个字节并返回 ------------------------------------------------*/ unsigned char Read(void) { alt_u8 i, dat = 0; IOWR_ALTERA_AVALON_PIO_DIRECTION(SDA_BASE, IN); for(i=0; i<8; i++) { IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(5); dat <<= 1; dat |= IORD_ALTERA_AVALON_PIO_DATA(SDA_BASE); usleep(5); } usleep(5); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 1); usleep(10); IOWR_ALTERA_AVALON_PIO_DATA(SCL_BASE, 0); return dat; } /*-----------------------------------------------写入DA数模转换值 ------------------------------------------------*/ void DAC(unsigned char Data) { Start(); Send(AddWr); //写入芯片地址 Ack(); Send(0x40); //写入控制位,使能DAC输出 Ack(); 232 软件实现——D/A Send(Data); //写数据 Ack(); Stop(); } 要完成 D/A 功能需要我们通过 I2C 总线写入待转换的数字信号数据,所以需要完成 I2C 的写时序。根据 I2C 协议及上节的 PCF8591 时序,可知写数据的时序为:启动->写器件地址 (写)->响应->写控制字->响应->写数据->停止。DAC()函数正是基于此时序实现的 D/A 转 换函数,它的功能就是完成一次写入数字信号数据值以供 D/A 转换。这里写入芯片的控制字为 0x40,有不明白的同学可以看一下上节对控制字的介绍。 最后在 main 文件夹中完成 main.c 的编写,得到下图。 /*----------------------------------------------内容:此程序通过IIC协议对PCF8591芯片操作,输出模拟量,用LED亮度渐变指示 ------------------------------------------------*/ #include #include #include "altera_avalon_pio_regs.h" #include "system.h" #include "../inc/sopc.h" /*------------------------------------------------ 主程序 ------------------------------------------------*/ int main() { unsigned char num; //DA数模输出变量 while(1) { DAC(num); //DA输出,可以用LED模拟电压变化 num++; //累加,到256后溢出变为0,往复循环。显示在LED上亮度逐渐变化 usleep(20000); } //延时用于清晰看出变化 return 0; } 在 main.c 中,通过 main 函数实现了 D/A 转换功能。在 main 函数中编写了 while(1)循 环,在循环中,首先调用 DAC()函数将 num 的数值作为待转换的数字信号数据传输给 PCF8591, 芯片接收数据后完成 D/A 转换,并通过 AOUT 输出给 LED 的负极。num 的值不断累加,在 233 软件实现——D/A 0~255 之间循环,为了能清晰地看到 LED 的变化,间隔设为 20ms。到此 D/A 转换功能的实 现就完成了。 234 15.4 总结 DS1302 应用——RTC 在 A/D 实验中,运行 Nios II 工程后,转动 TIGER BOARD 上面的电位器 R47,可以观察 到,数码管上显示的数值也会随之变化,转到连接 3.3v 端时,数值为 255 或接近 255,转到 GND 端时,数值为 0 或接近 0。在 D/A 实验中,运行 Nios II 工程后,D2 这颗 LED 会循环亮 灭。 本章主要介绍了在 Nios II 中利用 PCF8591 芯片实现 A/D 和 D/A 功能,因为 A/D 与 D/A 的实现都是基于已经添加的 I2C 总线,硬件部分无需修改。在 A/D 的软件实现中,主要是通过 I2C 读数据时序完成了对芯片的配置和转换后数字信号数据的接收,并在数码管上显示;在 D/A 软件实现中,主要是通过 I2C 写数据时序完成了对芯片的配置和写入待转换的数字信号数据。 其实可以看出,完成 A/D 和 D/A 的关键依然是时序,掌握了 I2C 的时序,再熟读芯片手册了 解相应的寄存器(地址寄存器、控制字寄存器),一切自然豁然开朗,在这里我依然不厌其烦甚 至有些啰嗦地向大家说明时序的重要性,因为时序真的很重要!本章实现的只是 PCF8591 芯 片的一小部分功能,感兴趣的同学可以利用 TIGER BOARD 研究下该芯片的其它功能。这一章 的内容就到这里,因本人水平有限,文中有任何错误请联系我,大家在交流中进步!邮箱: vito943@qq.com 235 TFT 彩色液晶屏应用 第16章 TFT 彩色液晶屏应用 1—— 显示 本章主要介绍在 Nios II 中利用 TFT 液晶模块(S95417-AAA)实现 TFT 彩色液晶屏的显 示功能,通过本章,你能学到 (1)在 Nios II 中利用 TFT 彩色液晶屏模块实现显示功能。 本章分为四个部分: 一、概述 二、硬件实现 三、软件实现 四、实验总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 236 概述 16.1 概述 数据的输出显示永远伴随在嵌入式开发中, 它为我们提供了一双“眼睛”,可以窥探到程 序在芯片中运行的状态。数据的输出显示方式众多,从最原始的 LED、数码管,到这一章要介 绍的“高端“方式——TFT 液晶屏,它在嵌入式开发中的重要性不言而喻。 许多嵌入式教程以 1602 液晶屏作为介绍液晶屏的例程,然而信息社会发展到现在,手机 屏幕从蓝色背光的 1.5 寸黑白屏,早已发展成尺寸超过 5 寸的高分辨率触摸屏,这时再以只能 显示 16x2 个字符的黑白屏作为例程来介绍液晶屏,显然太过 out。为了紧跟时代潮流,在本 章及下一章中,我们就以型号为 S95417-AAA 的 TFT 彩色液晶屏模块和型号为 XPT2046 的 触摸控制芯片为例,来分别介绍 TFT 的显示功能和触摸控制功能的实现。 S95417-AAA 是基于 ILI9325 液晶驱动芯片的液晶模块。它的尺寸为 2.4 寸,色彩为 262K, 分辨率为 240x320,背光模式为 WHITE LED。它能独立完成数据、图像的彩色显示功能,并 通过连接触摸控制芯片,实现触摸控制功能。在本章中,我们先单独利用 S95417-AAA 模块 实现彩色显示功能。 237 16.2 硬件实现 TIGER BOARD 上 TFT 模块的原理图如下图所示。 硬件实现 TFT 模块的核心为驱动芯片 ILI9325,模块的管脚大部分都自芯片引出,所以我们控制模 块管脚就等同于控制 ILI9325 芯片本身。 TFT 共有 18 位数据线,可以工作在 18 位数据模式、16 位数据模式、9 位数据模式(高 9 位)和 8 位数据模式(高 8 位)四种模式下,我们这里采用的是 8 位数据模式,即只通过 TFT 模块的高 8 位数据线传输数据,如上图所示。 TFT 模块与 FPGA 连接的管脚(上图标红色管脚)数量很多,在 Nios II 中,要添加相应 的 IO 端口控制这些管脚。下面以表格的形式列出了控制这些管脚的 IO 端口在 Nios II 中的实 现形式及功能。 238 硬件实现 管脚 实现形式 方向 初始值 功能 TFT_RST PIO TFT_BK PIO TFT_RD PIO TFT_WR PIO TFT_RS PIO TFT_CS PIO DB0~DB7 PIO 输出 0 输出 1 输出 0 输出 0 输出 0 输出 0 双向 0 复位管脚,低电平有效 背光使能管脚,低电平开启 TFT 背光 读使能 写使能 数据/命名切换 片选信号,低电平有效 数据线 可以看到,在 Nios II 中,控制这些管脚的 IO 端口的实现形式都为 PIO。 照着上面表格,我们打开 Qsys,先添加 PIO。PIO 的方向、初始值按照表中设置即可,除 了 DB 的位宽为 8,其余都为 1。 将添加的 PIO 改名、连线、端口引出、分配地址,完成后得到下图。 完成上述操作后,保存并点击 Genrate 编译 Qsys。 然后打开 Quartus II,在顶层 bdf 文件中更新 kernel 得到新添加的众多 IO 端口,将它们 生成管脚、分配管脚,完成后得到下图。 239 硬件实现 这一章涉及到的管脚数量众多,大家要格外细心,不要出错。完成后编译 Quartus II 工程, 硬件实现部分就完成了。 240 软件实现 16.3 软件实现 下面来完成软件实现。在 Nios II 中新建一个工程命名为 tft_display,将目录区整理成三 文件夹结构。在 inc 文件夹中新建一个.h 头文件 sopc.h,编写完成后得到下图。 #ifndef SOPC_H #define SOPC_H /*-------------------------------------------------------------* Include *--------------------------------------------------------------*/ #include "system.h" #define #define #define u8 unsigned char u16 unsigned int u32 unsigned long /****定义LCD的尺寸****/ #define LCD_W 240 #define LCD_H 320 #define _LCD #define LCD_DATA_OUT #define LCD_DATA_IN LCD_DataPortH->DIRECTION = 0xFF LCD_DataPortH->DIRECTION = 0 typedef struct { unsigned long int DATA; unsigned long int DIRECTION; unsigned long int INTERRUPT_MASK; unsigned long int EDGE_CAPTURE; }PIO_STR; /*----------------------------------------------------------* Peripheral declaration *-----------------------------------------------------------*/ #ifdef _LCD //高8位数据口,8位模式下只使用高8位 #define LCD_DataPortH ((PIO_STR *) DB_BASE) #define LCD_RS #define LCD_WR ((PIO_STR *) TFT_RS_BASE) ((PIO_STR *) TFT_WR_BASE) 241 //数据/命令切换 //写控制 软件实现 #define LCD_RD #define LCD_CS #define LCD_REST #define LCD_BK #endif /* _LCD */ ((PIO_STR *) TFT_RD_BASE) ((PIO_STR *) TFT_CS_BASE) ((PIO_STR *) TFT_RST_BASE) ((PIO_STR *) TFT_BK_BASE) //读控制 //片选 //复位 //背光 extern u16 BACK_COLOR, POINT_COLOR; //背景色,画笔色 void Lcd_Init(void); void LCD_Clear(u16 Color); void Address_set(unsigned int x1,unsigned int y1,unsigned int x2,unsigned int y2); void LCD_WR_DATA8(char VH,char VL); //发送数据-8位参数 void LCD_WR_DATA(int da); void LCD_WR_REG(int da); void delayms(int count); //画笔颜色 #define WHITE #define BLACK #define BLUE #define BRED #define GRED #define GBLUE #define RED #define MAGENTA #define GREEN #define CYAN #define YELLOW #define BROWN #define BRRED #define GRAY //GUI颜色 0xFFFF 0x0000 0x001F 0XF81F 0XFFE0 0X07FF 0xF800 0xF81F 0x07E0 0x7FFF 0xFFE0 0XBC40 //棕色 0XFC07 //棕红色 0X8430 //灰色 #define DARKBLUE #define LIGHTBLUE #define GRAYBLUE //以上三色为PANEL的颜色 0X01CF //深蓝色 0X7D7C //浅蓝色 0X5458 //灰蓝色 #define LIGHTGREEN #define LGRAY 0X841F //浅绿色 0XC618 //浅灰色(PANNEL),窗体背景色 #define LGRAYBLUE 0XA651 //浅灰蓝色(中间层颜色) 242 软件实现 #define LBBLUE 0X2B12 //浅棕蓝色(选择条目的反色) #endif /*SOPC_H_*/ sopc.h 文件的内容虽然较多,但是没什么新鲜地方,主要定义了 PIO 结构体便于直接控 制寄存器,并声明了一些函数,最后给一些颜色的值作了宏定义,一会儿我们会通过向模块写 入颜色对应的数值来实现刷屏。 然后在 driver 文件夹中新建.c 文件,命名为 LCD.c,完成编写后得到下图。 #include #include #include "../inc/sopc.h" #include "system.h" u16 BACK_COLOR, POINT_COLOR; //背景色,画笔色 /* * === FUNCTION ========================================== * Name: delayms * Description: 延时函数 * ========================================================= */ void delayms(int count) // /* X1ms */ { int i,j; for(i=0;iDATA=VH; LCD_WR->DATA=0; LCD_WR->DATA=1; LCD_DataPortH->DATA=VL; LCD_WR->DATA=0; LCD_WR->DATA=1; } /* * === FUNCTION ========================================== * Name: LCD_WR_DATA8 243 软件实现 * Description: 发送数据-8位参数 * ========================================================= */ void LCD_WR_DATA8(char VH,char VL) //发送数据-8位参数 { LCD_RS->DATA=1; LCD_Writ_Bus(VH,VL); } /* * === FUNCTION ========================================== * Name: LCD_WR_DATA * Description: 发送数据-16位参数 * ========================================================= */ void LCD_WR_DATA(int da) { LCD_RS->DATA=1; LCD_Writ_Bus(da>>8,da); } /* * === FUNCTION ========================================== * Name: LCD_WR_REG * Description: 写寄存器地址函数 * ========================================================= */ void LCD_WR_REG(int da) { LCD_RS->DATA=0; LCD_Writ_Bus(da>>8,da); } /* * === FUNCTION ========================================== * Name: LCD_WR_REG_DATA * Description: 先写寄存器地址,再写数据 * ========================================================= */ void LCD_WR_REG_DATA(int reg,int da) { LCD_WR_REG(reg); LCD_WR_DATA(da); } /* * === FUNCTION ========================================== * Name: Address_set * Description: 设置填充区域地址 * ========================================================= */ void Address_set(unsigned int x1,unsigned int y1,unsigned int x2,unsigned int y2) 244 软件实现 { LCD_WR_REG(0x0020);LCD_WR_DATA(x1); LCD_WR_REG(0x0021);LCD_WR_DATA(y1); LCD_WR_REG(0x0050);LCD_WR_DATA(x1); LCD_WR_REG(0x0051);LCD_WR_DATA(x2); LCD_WR_REG(0x0052);LCD_WR_DATA(y1); LCD_WR_REG(0x0053);LCD_WR_DATA(y2); LCD_WR_REG(0x0022); } /* * ========================================================= * Name: Lcd_Init * Description: TFT模块初始化函数 * ========================================================= */ void Lcd_Init(void) { LCD_CS->DATA =1; //片选无效 LCD_BK->DATA = 0; //点亮背光 LCD_RD->DATA=1; LCD_WR->DATA=1; //TFT复位 LCD_REST->DATA=1; delayms(20); LCD_REST->DATA=0; delayms(20); LCD_REST->DATA=1; delayms(5); LCD_CS->DATA =0; //打开片选使能 LCD_WR_REG_DATA(0x0001,0x0100); LCD_WR_REG_DATA(0x0002,0x0700); LCD_WR_REG_DATA(0x0003,0x1030); LCD_WR_REG_DATA(0x0004,0x0000); LCD_WR_REG_DATA(0x0008,0x0207); LCD_WR_REG_DATA(0x0009,0x0000); LCD_WR_REG_DATA(0x000A,0x0000); LCD_WR_REG_DATA(0x000C,0x0000); LCD_WR_REG_DATA(0x000D,0x0000); LCD_WR_REG_DATA(0x000F,0x0000); //power on sequence VGHVGL LCD_WR_REG_DATA(0x0010,0x0000); LCD_WR_REG_DATA(0x0011,0x0007); LCD_WR_REG_DATA(0x0012,0x0000); LCD_WR_REG_DATA(0x0013,0x0000); 245 软件实现 //vgh LCD_WR_REG_DATA(0x0010,0x1290); LCD_WR_REG_DATA(0x0011,0x0227); //vregiout LCD_WR_REG_DATA(0x0012,0x001d); //vom amplitude LCD_WR_REG_DATA(0x0013,0x1500); //vom H LCD_WR_REG_DATA(0x0029,0x0018); LCD_WR_REG_DATA(0x002B,0x000D); //gamma LCD_WR_REG_DATA(0x0030,0x0004); LCD_WR_REG_DATA(0x0031,0x0307); LCD_WR_REG_DATA(0x0032,0x0002); LCD_WR_REG_DATA(0x0035,0x0206); LCD_WR_REG_DATA(0x0036,0x0408); LCD_WR_REG_DATA(0x0037,0x0507); LCD_WR_REG_DATA(0x0038,0x0204); LCD_WR_REG_DATA(0x0039,0x0707); LCD_WR_REG_DATA(0x003C,0x0405); LCD_WR_REG_DATA(0x003D,0x0F02); //ram LCD_WR_REG_DATA(0x0050,0x0000); LCD_WR_REG_DATA(0x0051,0x00EF); LCD_WR_REG_DATA(0x0052,0x0000); LCD_WR_REG_DATA(0x0053,0x013F); LCD_WR_REG_DATA(0x0060,0xA700); LCD_WR_REG_DATA(0x0061,0x0001); LCD_WR_REG_DATA(0x006A,0x0000); LCD_WR_REG_DATA(0x0080,0x0000); LCD_WR_REG_DATA(0x0081,0x0000); LCD_WR_REG_DATA(0x0082,0x0000); LCD_WR_REG_DATA(0x0083,0x0000); LCD_WR_REG_DATA(0x0084,0x0000); LCD_WR_REG_DATA(0x0085,0x0000); LCD_WR_REG_DATA(0x0090,0x0010); LCD_WR_REG_DATA(0x0092,0x0600); LCD_WR_REG_DATA(0x0093,0x0003); LCD_WR_REG_DATA(0x0095,0x0110); LCD_WR_REG_DATA(0x0097,0x0000); LCD_WR_REG_DATA(0x0098,0x0000); LCD_WR_REG_DATA(0x0007,0x0133); } /* * ========================================================= * Name: LCD_Clear 246 软件实现 * Description: 清屏函数,Color为要清屏的填充色 * ========================================================= */ void LCD_Clear(u16 Color) { u8 VH,VL; u16 i,j; VH=Color>>8; VL=Color; Address_set(0,0,LCD_W-1,LCD_H-1); for(i=0;i #include #include "../inc/sopc.h" /* TFT液晶模块显示例程,功能为实现TFT液晶模块红、绿、蓝三色刷屏 */ 248 软件实现 int main(void) { LCD_DATA_OUT; //数据输出 Lcd_Init(); //tft初始化 while(1) { LCD_Clear(RED); delayms(3000); LCD_Clear(GREEN); delayms(3000); LCD_Clear(BLUE); delayms(3000); } return 0; } 有了上面几个文件的铺垫,main.c 文件显得很是简单。在 main 函数中,首先将 DB 的方 向设置为输出(别忘了 DB 是个双向的 PIO!),然后调用 Lcd_Init()函数完成 TFT 模块的初始 化,最后在一个 while(1)循环中,完成红、绿、蓝三色清屏,间隔为 3 秒钟。当然,还有很多 狂拽酷炫的颜色可以选,大家可以尝试下。 249 16.4 总结 DS1302 应用——RTC 完成软件实现后,运行 Nios II 工程,可以看到 TIGER BOARD 上的 TFT 屏幕不停地进行 红、绿、蓝清屏,颜色间隔约为 3 秒钟。 本章主要介绍在 Nios II 中利用 TFT 液晶模块(S95417-AAA)实现 TFT 液晶屏的显示功 能,在硬件实现部分,添加了若干 PIO 用来控制 TFT 模块的相应管脚,要注意我们的 TFT 模 块选择的是 8 位数据模式,而且数据线 DB 是双向的(即写又读)。在软件实现部分,介绍了 TFT 模块的核心——驱动芯片 ILI9325 的写时序(包括写控制寄存器时序和写 GRAM 时序), 我们通过对控制寄存器和 GRAM 的赋值来实现 TFT 模块的显示功能,最终实现了 TFT 模块的 三色清屏功能。这一章的内容较多,理解起来稍有难度,大家要多看 ILI9325 的 Datasheet, 结合程序来理解 TFT 模块的工作原理。这一章的内容就到这里,因本人水平有限,文中有任何 错误请联系我,大家在交流中进步!邮箱:vito943@qq.com 250 TFT 彩色液晶屏应用 第17章 TFT 彩色液晶屏应用 2—— 触摸控制 本章主要介绍在 Nios II 中利用 TFT 液晶模块(S95417-AAA)和触摸控制芯片 XPT2046 实现 TFT 液晶屏的触摸控制功能,通过本章,你能学到 (1)在 Nios II 中利用 TFT 液晶模块和 XPT2046 实现触摸控制功能。 (2)Nios II 中 SPI 总线的使用。 本章分为四个部分: 一、概述 二、硬件实现 三、软件实现 四、实验总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 251 概述 17.1 概述 接着上一章的内容,我们来实现 TFT 模块的触摸控制功能,要实现触摸控制,光有 TFT 模 块是不行的,还得结合触摸控制芯片——XPT2046。 XPT2046 是一款 4 导线制触摸屏控制器,内含 12 位分辨率 125KHz 转换速率逐步逼近型 A/D 转换器。XPT2046 能通过执行两次 A/D 转换查出被按的屏幕位置, 除此之外,还可以测 量加在触摸屏上的压力。 要介绍 XPT2046,自然少不了 SPI 总线。SPI 全称为 Serial Peripheral Interface,即串 行外设接口,是 Motorola 推出的一种串行接口总线,一般由 CLK(时钟线)、CS(片选)、 MOSI(主输出从输入)和 MISO(主输入从输出)4 根线组成,不仅本章中要利用 SPI 总线实 现 CPU 与 XPT2046 的数据通信,后面章节的 USB、LAN 都离不开它,掌握 SPI 总线的重要 性不言而喻。在 Qsys 中,有现成的 SPI 总线模块可用,我们直接采用现成模块即可。下面就 来完成 TFT 模块的触摸控制。 小贴士 Tips:一条总线多处用——SPI 总线复用 复用性是总线的一个基本特征,TIGER BOARD 上的 SPI 总线采用了复用方式,即 SPI 总线的 SPI_SCK、SPI_MISO 和 SPI_MOSI 三根线与三个不同的器件相连(TFT、USB 和 LAN),每个器件单独有一条片选线(CS),如下图所示。这样做采用了一个 Master (即 CPU)控制多个 Slave 的结构,当我们想通过 SPI 总线与某个器件进行通信时,将 其它器件的片选信号保持无效即可。复用结构大大减少了资源的消耗。 252 概述 253 硬件实现 17.2 硬件实现 XPT2046 在 TIGER BOARD 上的原理图如下图所示:其中 X+、Y+、X-、Y-与 TFT 模块 连接,接收来自 TFT 模块的坐标信息;红色管脚与 CPU 连接,包括 SPI 总线的四根线(SPI_SCK、 TCH_CS、SPI_MOSI、SPI_MISO)、忙碌信号(TCH_BUSY)和中断信号(TCH_PEN)。 因为 SPI 总线有现成的模块可用,在 Qsys 中,我们用 SPI 总线模块实现 SPI 总线控制, 而 TCH_BUSY 和 TCH_PEN 用 PIO 来实现,如下表所示。这里需要注意以下几点:1、SPI 总 线模块虽然包含片选信号,但采用另外添加 PIO 模拟片选信号的方式更为方便灵活,所以这里 和后面的 USB、LAN 的 SPI 总线的片选信号都采用另外添加 PIO 模拟片选信号的方式。2、 上一节的 Tips 里面已经提到,在 SPI 总线复用方式下,要与某个器件进行通信时,要保证其 它器件的片选信号无效,所以我们先把 USB 模块的 SPI 片选信号(USB_nCS)和 LAN 模块 的 SPI 片选信号(LAN_nCS)添加进来并置 1(无效),防止其它器件干扰总线通信。3、TFT_KEY1 和 TFT_KEY2 为两个按键的输入,用来辅助实现触摸控制的某些功能。 管脚 实现形式 方向 初始值 功能 SPI_SCK SPI_MOSI SPI_MISO SPI 总线模块 SPI 总线模块 SPI 总线模块 SPI 总线时钟信号 SPI 总线主设备输出从设备输入数据线 SPI 总线主设备输入从设备输出数据线 254 硬件实现 TCH_nCS PIO TCH_BUSY PIO TCH_PEN PIO USB_nCS PIO LAN_nCS PIO TFT_KEY1 PIO TFT_KEY2 PIO 输出 1 输入 输入 输出 1 输出 1 输入 输入 SPI 总线 TFT 模块片选信号 忙碌信号 中断信号 SPI 总线 USB 模块片选信号 SPI 总线 LAN 模块片选信号 TFT 模块按键 1 TFT 模块按键 2 打开 Qsys,先添加 SPI 总线。在 Qsys 左侧边栏选择 Library->Interface Protocols->SPI, 点击得到下图,我们来设置 SPI 总线模块的参数。 红框里面的内容我们需要重点了解:Type 为 SPI 总线的模式,有 Master(主)和 Slave (从)两种,我们自然选择 Master;Number of selected signals 是片选信号的个数,因为 一个 Master 可以驱动多个 Slave,每个 Slave 对应一个片选信号,所以片选信号的个数不定, 我们并没有采用这 SPI 总线自带的片选信号,所以这里的片选信号个数就无关紧要了,我们随 意选择 1 即可;SPI clock rate 为 SPI 时钟的速率,我们设置为 500kHz;Actual clock rate 为真实时钟速率,这个与系统时钟有关,在 SPI 未连接系统时钟前为 0;Target delay 和 Actual delay 为延时,这里不做修改。 其它设置保持不变,点击 Finish 完成添加。 255 硬件实现 然后添加表中的 PIO,方向与初始值按照表中值设置,位宽都为 1,其中 TCH_PEN 要设 置为电平触发的中断,其它不变。 将添加的 IP 核模块改名、连线、端口引出、分配地址,对 TCH_PEN 和 SPI 总线模块还要 分配中断(注意!SPI 总线是要分配中断的!),完成后得到下图。 256 硬件实现 这时再双击 SPI 模块进入模块设置,可以看到 Actual clock rate 已经变为了 500000Hz, 这是我们给 SPI 模块连接了系统时钟的缘故。 完成上述操作后保存并点击 Genrate 编译 Qsys。然后打开 Quartus II,在顶层 bdf 文件 中更新 kernel 得到新添加的 IO 端口,将它们生成管脚、分配管脚,完成后得到下图。可以看 到 SPI 总线模块的 spi_SS_n 并没有被使用。 257 硬件实现 完成后编译 Quartus II 工程,硬件实现部分就完成了。 258 软件实现 17.3 软件实现 下面来完成软件实现。打开 Nios II,新建一个工程命名为 tft_touch,将目录区整理成三 文件夹结构。这一章需要编写的程序文件较多,下图为目录区的文件及功能。 在 TFT 上实现触摸控制功能,自然要保证 TFT 的显示功能正常,所以这一章的软件实现是 在上一章 TFT 显示实现的基础上修改而成的,当然,因为增加了触摸控制,需要驱动 XPT2046 芯片,内容也变得较为复杂。里面涉及到的函数较多,考虑到篇幅和必要性就不一一介绍了, 这里重点介绍 SPI 总线模块的使用等关键部分,其余函数会尽量以注释形式介绍。 在 inc 文件夹中新建三个.h 头文件,命名为 sopc.h、font.h 和 sys.h,先编写 sopc.h,完 成后得到下图。 #ifndef SOPC_H #define SOPC_H /*----------------------------------------------------* Include *-----------------------------------------------------*/ #include "system.h" #include "../inc/sys.h" //定义LCD的尺寸 #define LCD_W 240 #define LCD_H 320 #define LCD_DATA_OUT LCD_DataPortH->DIRECTION = 0xFF 259 #define LCD_DATA_IN LCD_DataPortH->DIRECTION = 0 /* touch panel interface define */ #define CMD_RDX 0xD0 //触摸IC读坐标积存器 #define CMD_RDY 0x90 #define _LCD #define _TCH /*----------------------SPI--------------------------*/ typedef struct{ volatile unsigned long int RXDATA; volatile unsigned long int TXDATA; union{ struct{ volatile unsigned long int NC volatile unsigned long int ROE volatile unsigned long int TOE volatile unsigned long int TMT volatile unsigned long int TRDY volatile unsigned long int RRDY volatile unsigned long int E volatile unsigned long int NC1 }BITS; volatile unsigned long int WORD; }STATUS; :3; :1; :1; :1; :1; :1; :1; :23; union{ struct{ volatile unsigned long int NC volatile unsigned long int IROE volatile unsigned long int ITOE volatile unsigned long int NC1 volatile unsigned long int ITRDY volatile unsigned long int IRRDY volatile unsigned long int IE volatile unsigned long int NC2 volatile unsigned long int SSO }BITS; volatile unsigned long int CONTROL; }CONTROL; :3; :1; :1; :1; :1; :1; :1; :1; :21; unsigned long int RESERVED0; unsigned long int SLAVE_SELECT; }SPI_ST; typedef struct { unsigned long int DATA; unsigned long int DIRECTION; unsigned long int INTERRUPT_MASK; 260 软件实现 软件实现 unsigned long int EDGE_CAPTURE; }PIO_STR; /*------------------------------------------------------* Peripheral declaration *-------------------------------------------------------*/ #ifdef _LCD //#define Bus_16 //16位数据模式,如果使用8位模式,请注释此语句,如果使用16位模 式,请打开此句,修改8位模式之前,请确认你手里的模块是否是8位总线接口 //IO连接 #define LCD_DataPortH ((PIO_STR *) DB_BASE) //高8位数据口,8位模式下 只使用高8位 //#define _LCD_DataPortL //低8位数据口 ,8位模式下 低8位可以不接线,请确认P0口已经上拉10K电阻,不宜太小,最小4.7K,推荐10K. #define LCD_RS ((PIO_STR *) TFT_RS_BASE) //数据/命令切换 #define LCD_WR ((PIO_STR *) TFT_WR_BASE) //写控制 #define LCD_RD ((PIO_STR *) TFT_RD_BASE) //读控制 #define LCD_CS ((PIO_STR *) TFT_CS_BASE) //片选 #define LCD_REST ((PIO_STR *) TFT_RST_BASE) //复位 #define LCD_BK ((PIO_STR *) TFT_BK_BASE) //背光 #define TFT_KEY1 ((PIO_STR *) TFT_KEY1_BASE) //背光 #define TFT_KEY2 #endif /* _LCD */ ((PIO_STR *) TFT_KEY2_BASE) //背光 #ifdef _TCH #define TCH_nCS #define SPI_TCH #define TCH_PEN #define TCH_DCLK #define TCH_DOUT #define TCH_DIN #endif /* _TCH */ ((PIO_STR *) TCH_NCS_BASE) //SPI片选控制线 ((SPI_ST *) SPI_BASE) //SPI总线 ((PIO_STR *) TCH_PEN_BASE) //检测触摸屏响应信号 ((PIO_STR *) SPI_SCLK_BASE) //检测触摸屏响应信号 ((PIO_STR *) MISO_BASE) //检测触摸屏响应信号 ((PIO_STR *) MOSI_BASE) //检测触摸屏响应信号 extern u16 BACK_COLOR, POINT_COLOR; //背景色,画笔色 void Lcd_Init(void); void LCD_Clear(u16 Color); void Address_set(unsigned int x1,unsigned int y1,unsigned int x2,unsigned int y2); void LCD_WR_DATA8(char VH,char VL); //发送数据-8位参数 void LCD_WR_DATA(int da); void LCD_WR_REG(int da); 261 软件实现 void LCD_DrawPoint(u16 x,u16 y);//画点 void LCD_DrawPoint_big(u16 x,u16 y);//画一个大点 u16 LCD_ReadPoint(u16 x,u16 y); //读点 void Draw_Circle(u16 x0,u16 y0,u8 r); void LCD_DrawLine(u16 x1, u16 y1, u16 x2, u16 y2); void LCD_Fill(u16 xsta,u16 ysta,u16 xend,u16 yend,u16 color); void LCD_ShowChar(u16 x,u16 y,u8 num,u8 mode);//显示一个字符 void LCD_ShowNum(u16 x,u16 y,u32 num,u8 len);//显示数字 void LCD_Show2Num(u16 x,u16 y,u16 num,u8 len);//显示2个数字 void LCD_ShowString(u16 x,u16 y,const u8 *p); //显示一个字符串,16字体 void showhanzi(unsigned int x,unsigned int y,unsigned char index); void light(void); struct tp_pix_ { u16 x; u16 y; }; struct tp_pixu32_ { u32 x; u32 y; }; extern struct tp_pix_ tp_pixad,tp_pixlcd; //当前触控坐标的AD值,前触控坐标的像 素值 extern u16 vx,vy; //比例因子,此值除以1000之后表示多少个AD值代表一个像素点 extern u16 chx,chy;//默认像素点坐标为0时的AD起始值 u8 tpstate(void); void spistar(void); void Touch_Adjust(void); void point(void); //绘图函数 u16 ReadFromCharFrom7843(); //SPI 读数据 //画笔颜色 #define WHITE #define BLACK #define BLUE #define BRED #define GRED #define GBLUE #define RED #define MAGENTA 0xFFFF 0x0000 0x001F 0XF81F 0XFFE0 0X07FF 0xF800 0xF81F 262 软件实现 #define GREEN #define CYAN #define YELLOW #define BROWN #define BRRED #define GRAY //GUI颜色 0x07E0 0x7FFF 0xFFE0 0XBC40 //棕色 0XFC07 //棕红色 0X8430 //灰色 #define DARKBLUE #define LIGHTBLUE #define GRAYBLUE //以上三色为PANEL的颜色 0X01CF //深蓝色 0X7D7C //浅蓝色 0X5458 //灰蓝色 #define LIGHTGREEN #define LGRAY 0X841F //浅绿色 0XC618 //浅灰色(PANNEL),窗体背景色 #define LGRAYBLUE #define LBBLUE 0XA651 //浅灰蓝色(中间层颜色) 0X2B12 //浅棕蓝色(选择条目的反色) #endif /*SOPC_H_*/ 文件基于上一章的 sopc.h 文件修改而成,增加了实现触摸功能的一些宏定义和函数声明。 需要注意的是 SPI_ST 结构体,它是为了控制 SPI 总线模块的寄存器而定义的结构体,与模块 的寄存器表一一对应,如下图所示。 然后编写 font.h,编写完成后得到下图。 #ifndef __FONT_H #define __FONT_H extern unsigned char image[3200]; //changed extern unsigned char hanzi[3200]; //changed extern unsigned char asc2_1608[1520]; #endif 263 软件实现 font.h 中声明了几个字符数组,用来存储程序中用到的图像、汉字和 ASCII 码字符等显示 内容对应的数据,其实无论是上一章显示的刷屏颜色,还是本章需要显示的图像、汉字和 ASCII 码字符,显示原理都如上章介绍:在屏幕的某些区域地址刷上某些颜色的点。只是相比较把屏 幕刷成同一颜色,后面几种的实现略微复杂,需要编写相应的函数,将显示内容对应的字符数 据写入到 ILI9325 芯片中。图像和汉字字符数据的生成分别需要借助 Image2Lcd 和多国文字 点阵字库生成器这两个小软件,我们在开发板的资料中给大家提供了这两个软件,感兴趣的同 学可以研究下软件的使用方法,生成不同图像和汉字的字符数据。 最后编写 sys.h,编写完成后得到下图。 #ifndef __SYS_H #define __SYS_H #define #define #define u8 unsigned char u16 unsigned int u32 unsigned long void delayms(int count); #endif sys.h 文件主要声明了延时函数 delayms()。 在 driver 文件夹中新建四个.c 源文件,命名为 LCD.c、touch.c、font.c 和 sys.c,先编写 LCD.c,完成后得到下图。 #include #include #include "../inc/sys.h" #include "../inc/sopc.h" #include "../inc/font.h" #include "system.h" u16 BACK_COLOR, POINT_COLOR; //背景色,画笔色 #ifdef Bus_16 //条件编译-16位数据模式 void LCD_Writ_Bus(char VH,char VL) { LCD_DataPortH->DATA=VH; LCD_DataPortH->DATA=VL; LCD_WR->DATA=0; LCD_WR->DATA=1; } #else //条件编译-8位数据模式 //并行数据写入函数 void LCD_Writ_Bus(char VH,char VL) //并行数据写入函数 264 { LCD_DataPortH->DATA=VH; LCD_WR->DATA=0; LCD_WR->DATA=1; LCD_DataPortH->DATA=VL; LCD_WR->DATA=0; LCD_WR->DATA=1; } #endif unsigned int LCD_Read_Bus(void) { unsigned int VA,VH,VL; //并行数据读出函数 VH = LCD_DataPortH->DATA; LCD_RD->DATA=0; LCD_RD->DATA=1; VL = LCD_DataPortH->DATA; LCD_RD->DATA=0; LCD_RD->DATA=1; VA = (VH<<8) + VL; return(VA); } void LCD_WR_DATA8(char VH,char VL) //发送数据-8位参数 { LCD_RS->DATA=1; LCD_Writ_Bus(VH,VL); } void LCD_WR_DATA(int da) { LCD_RS->DATA=1; LCD_Writ_Bus(da>>8,da); } void LCD_WR_REG(int da) { LCD_RS->DATA=0; LCD_Writ_Bus(da>>8,da); } void LCD_WR_REG_DATA(int reg,int da) { LCD_WR_REG(reg); LCD_WR_DATA(da); } unsigned int LCD_RD_DATA() { LCD_RS->DATA=1; return(LCD_Read_Bus()); } unsigned int LCD_RD_REG() { LCD_RS->DATA=0; return(LCD_Read_Bus()); 265 软件实现 软件实现 } unsigned LCD_RD_REG_DATA(int reg) { LCD_WR_REG(reg); LCD_DATA_IN; //数据输入 return(LCD_RD_DATA()); LCD_DATA_OUT; //数据输出 } void Address_set(unsigned int x1,unsigned int y1,unsigned int x2,unsigned int y2) { LCD_WR_REG(0x0020);LCD_WR_DATA8(x1>>8,x1); //设置X坐标位置 LCD_WR_REG(0x0021);LCD_WR_DATA8(y1>>8,y1); //设置Y坐标位置 LCD_WR_REG(0x0050);LCD_WR_DATA8(x1>>8,x1); //开始X LCD_WR_REG(0x0052);LCD_WR_DATA8(y1>>8,y1); //开始Y LCD_WR_REG(0x0051);LCD_WR_DATA8(x2>>8,x2); //结束X LCD_WR_REG(0x0053);LCD_WR_DATA8(y2>>8,y2); //结束Y LCD_WR_REG(0x0022); } void Lcd_Init(void) { LCD_CS->DATA =1; LCD_BK->DATA = 0; //点亮背光 //调用一次这些函数,免得编译的时候提示警告 { LCD_WR_REG_DATA(0,0); LCD_ShowString(0,0," "); LCD_ShowNum(0,0,0,0); LCD_Show2Num(0,0,0,0); LCD_DrawPoint_big(0,0); Draw_Circle(0,0,0); } LCD_REST->DATA=1; delayms(20); LCD_REST->DATA=0; delayms(20); LCD_REST->DATA=1; LCD_CS->DATA=1; LCD_RD->DATA=1; LCD_WR->DATA=1; delayms(5); LCD_CS->DATA =0; //打开片选使能 //printf("%X",LCD_RD_REG_DATA(0x00)); 266 LCD_WR_REG_DATA(0x0001,0x0100); LCD_WR_REG_DATA(0x0002,0x0700); LCD_WR_REG_DATA(0x0003,0x1030); LCD_WR_REG_DATA(0x0004,0x0000); LCD_WR_REG_DATA(0x0008,0x0207); LCD_WR_REG_DATA(0x0009,0x0000); LCD_WR_REG_DATA(0x000A,0x0000); LCD_WR_REG_DATA(0x000C,0x0000); LCD_WR_REG_DATA(0x000D,0x0000); LCD_WR_REG_DATA(0x000F,0x0000); //power on sequence VGHVGL LCD_WR_REG_DATA(0x0010,0x0000); LCD_WR_REG_DATA(0x0011,0x0007); LCD_WR_REG_DATA(0x0012,0x0000); LCD_WR_REG_DATA(0x0013,0x0000); //vgh LCD_WR_REG_DATA(0x0010,0x1290); LCD_WR_REG_DATA(0x0011,0x0227); //delayms(100); //vregiout LCD_WR_REG_DATA(0x0012,0x001d); //0x001b //delayms(100); //vom amplitude LCD_WR_REG_DATA(0x0013,0x1500); //delayms(100); //vom H LCD_WR_REG_DATA(0x0029,0x0018); LCD_WR_REG_DATA(0x002B,0x000D); //gamma LCD_WR_REG_DATA(0x0030,0x0004); LCD_WR_REG_DATA(0x0031,0x0307); LCD_WR_REG_DATA(0x0032,0x0002);// 0006 LCD_WR_REG_DATA(0x0035,0x0206); LCD_WR_REG_DATA(0x0036,0x0408); LCD_WR_REG_DATA(0x0037,0x0507); LCD_WR_REG_DATA(0x0038,0x0204);//0200 LCD_WR_REG_DATA(0x0039,0x0707); LCD_WR_REG_DATA(0x003C,0x0405);// 0504 LCD_WR_REG_DATA(0x003D,0x0F02); //ram LCD_WR_REG_DATA(0x0050,0x0000); LCD_WR_REG_DATA(0x0051,0x00EF); LCD_WR_REG_DATA(0x0052,0x0000); LCD_WR_REG_DATA(0x0053,0x013F); LCD_WR_REG_DATA(0x0060,0xA700); LCD_WR_REG_DATA(0x0061,0x0001); LCD_WR_REG_DATA(0x006A,0x0000); // LCD_WR_REG_DATA(0x0080,0x0000); LCD_WR_REG_DATA(0x0081,0x0000); LCD_WR_REG_DATA(0x0082,0x0000); LCD_WR_REG_DATA(0x0083,0x0000); LCD_WR_REG_DATA(0x0084,0x0000); LCD_WR_REG_DATA(0x0085,0x0000); 267 软件实现 软件实现 // LCD_WR_REG_DATA(0x0090,0x0010); LCD_WR_REG_DATA(0x0092,0x0600); LCD_WR_REG_DATA(0x0093,0x0003); LCD_WR_REG_DATA(0x0095,0x0110); LCD_WR_REG_DATA(0x0097,0x0000); LCD_WR_REG_DATA(0x0098,0x0000); LCD_WR_REG_DATA(0x0007,0x0133); } //清屏函数 //Color:要清屏的填充色 void LCD_Clear(u16 Color) { u8 VH,VL; u16 i,j; VH=Color>>8; VL=Color; Address_set(0,0,LCD_W-1,LCD_H-1); for(i=0;i0)incx=1; //设置单步方向 else if(delta_x==0)incx=0;//垂直线 else {incx=-1;delta_x=-delta_x;} if(delta_y>0)incy=1; else if(delta_y==0)incy=0;//水平线 269 软件实现 else{incy=-1;delta_y=-delta_y;} if( delta_x>delta_y)distance=delta_x; //选取基本增量坐标轴 else distance=delta_y; for(t=0;t<=distance+1;t++ )//画线输出 { LCD_DrawPoint(uRow,uCol);//画点 xerr+=delta_x ; yerr+=delta_y ; if(xerr>distance) { xerr-=distance; uRow+=incx; } if(yerr>distance) { yerr-=distance; uCol+=incy; } } } //在指定位置显示一个字符 //num:要显示的字符:" "--->"~" //mode:叠加方式(1)还是非叠加方式(0) //在指定位置显示一个字符 //num:要显示的字符:" "--->"~" //mode:叠加方式(1)还是非叠加方式(0) void LCD_ShowChar(u16 x,u16 y,u8 num,u8 mode) { u8 temp; u8 pos,t; u16 x0=x; u16 colortemp=POINT_COLOR; if(x>LCD_W-16||y>LCD_H-16)return; //设置窗口 num=num-' ';//得到偏移后的值 Address_set(x,y,x+8-1,y+16-1); //设置光标位置 if(!mode) //非叠加方式 { for(pos=0;pos<16;pos++) { temp=asc2_1608[(u16)num*16+pos]; //调用1608字体 for(t=0;t<8;t++) { if(temp&0x01)POINT_COLOR=colortemp; 270 软件实现 else POINT_COLOR=BACK_COLOR; LCD_WR_DATA(POINT_COLOR); temp>>=1; x++; } x=x0; y++; } }else//叠加方式 { for(pos=0;pos<16;pos++) { temp=asc2_1608[(u16)num*16+pos]; //调用1608字体 for(t=0;t<8;t++) { if(temp&0x01)LCD_DrawPoint(x+t,y+pos);//画一个点 temp>>=1; } } } POINT_COLOR=colortemp; } //m^n函数 u32 mypow(u8 m,u8 n) { u32 result=1; while(n--)result*=m; return result; } //显示2个数字 //x,y :起点坐标 //len :数字的位数 //color:颜色 //num:数值(0~4294967295); void LCD_ShowNum(u16 x,u16 y,u32 num,u8 len) { u8 t,temp; u8 enshow=0; num=(u16)num; for(t=0;tLCD_W-16){x=0;y+=16;} if(y>LCD_H-16){y=x=0;LCD_Clear(RED);} LCD_ShowChar(x,y,*p,0); x+=8; p++; } } LCD.c 文件根据上一章的 LCD.c 文件改写而成,除了 LCD 初始化函数、设置填充区域地址 函数、清屏函数等函数外,新增加了一些绘图显示函数,包括汉字显示函数、画点函数、画线 函数等等,这些函数为 TFT 显示一些复杂图案内容奠定了基础,函数虽然功能各异,但正如上 面所讲,在屏幕的某些区域地址刷上某些颜色的点的基本原理是一样的。 然后编写 touch.c 文件,编写完成后得到下图。 #include #include #include "../inc/sys.h" #include "../inc/sopc.h" #include "../inc/font.h" #include "system.h" int data_rd; //***因触摸屏批次不同等原因,默认的校准参数值可能会引起触摸识别不准,建议校准后再使用, 272 软件实现 不建议使用固定的默认校准参数 u16 vx=13581,vy=10879; //比例因子,此值除以1000之后表示多少个AD值代表一个像素点 u16 chx=3676,chy=3886;//默认像素点坐标为0时的AD起始值 //***因触摸屏批次不同等原因,默认的校准参数值可能会引起触摸识别不准,建议校准后再使用, 不建议使用固定的默认校准参数 struct tp_pix_ tp_pixad,tp_pixlcd; //当前触控坐标的AD值,前触控坐标的像素值 u8 tpstate(void) { return TCH_PEN->DATA; } //********************************************************** void WriteCharTo7843(unsigned char num) //SPI写数据 { unsigned char count=0; SPI_TCH->TXDATA = num; while(!(SPI_TCH->STATUS.BITS.TMT)); } //********************************************************** u16 ReadFromCharFrom7843() //SPI 读数据 { u8 count=0; u16 Num=0; u16 num1=0,num2=0; SPI_TCH->TXDATA = 0x00; //0x00 is random number ,to enable clock while(!(SPI_TCH->STATUS.BITS.TMT)); num1 = SPI_TCH->RXDATA; SPI_TCH->TXDATA = 0x00; //0x00 is random number ,to enable clock while(!(SPI_TCH->STATUS.BITS.TMT)); num2 = SPI_TCH->RXDATA; Num = ((num1&0x7f)<<5)+(num2>>3); return(Num); } //从7846/7843/XPT2046/UH7843/UH7846读取adc值 0x90=y 0xd0-x u16 ADS_Read_AD(unsigned char CMD) { u16 l; TCH_nCS->DATA=0; WriteCharTo7843(CMD); //送控制字即用差分方式读X坐标 详细请见有关资料 273 l=ReadFromCharFrom7843(); TCH_nCS->DATA=1; return l; } //读取一个坐标值 //连续读取READ_TIMES次数据,对这些数据升序排列, //然后去掉最低和最高LOST_VAL个数,取平均值 #define READ_TIMES 15 //读取次数 #define LOST_VAL 5 //丢弃值 u16 ADS_Read_XY(u8 xy) { u16 i, j; u16 buf[READ_TIMES]; u16 sum=0; u16 temp; for(i=0;ibuf[j])//升序排列 { temp=buf[i]; buf[i]=buf[j]; buf[j]=temp; } } } sum=0; for(i=LOST_VAL;i>1; *y=(y1+y2)>>1; return 1; }else return 0; } //精确读取一次坐标,校准的时候用的 u8 Read_TP_Once(void) { u8 re=0; u16 x1,y1; while(re==0) { while(!Read_ADS2(&tp_pixad.x,&tp_pixad.y)); delayms(10); while(!Read_ADS2(&x1,&y1)); if(tp_pixad.x==x1&&tp_pixad.y==y1) { re=1; } } return re; } ////////////////////////////////////////////////// //与LCD部分有关的函数 //画一个触摸点 //用来校准用的 void Drow_Touch_Point(u16 x,u16 y) { LCD_DrawLine(x-12,y,x+13,y);//横线 LCD_DrawLine(x,y-12,x,y+13);//竖线 275 软件实现 LCD_DrawPoint(x+1,y+1); LCD_DrawPoint(x-1,y+1); LCD_DrawPoint(x+1,y-1); LCD_DrawPoint(x-1,y-1); // Draw_Circle(x,y,6);//画中心圈 } //转换结果 //根据触摸屏的校准参数来决定转换后的结果,保存在X0,Y0中 u8 Convert_Pos(void) { u8 l=0; if(Read_ADS2(&tp_pixad.x,&tp_pixad.y)) { l=1; tp_pixlcd.x=tp_pixad.x>chx?((u32)tp_pixad.x-(u32)chx)*1000/vx:((u32)c hx-(u32)tp_pixad.x)*1000/vx; tp_pixlcd.y=tp_pixad.y>chy?((u32)tp_pixad.y-(u32)chy)*1000/vy:((u32)c hy-(u32)tp_pixad.y)*1000/vy; } return l; } //触摸屏校准代码 //得到四个校准参数 #define tp_pianyi 50 //校准坐标偏移量 #define tp_xiaozhun 1000 void Touch_Adjust(void) { //校准精度 float vx1,vx2,vy1,vy2; //比例因子,此值除以1000之后表示多少个AD值代表一个像素 点 u16 chx1,chx2,chy1,chy2;//默认像素点坐标为0时的AD起始值 u16 lx,ly; struct tp_pixu32_ p[4]; u8 cnt=0; cnt=0; POINT_COLOR=BLUE; BACK_COLOR =WHITE; LCD_Clear(WHITE);//清屏 POINT_COLOR=RED;//红色 LCD_Clear(WHITE);//清屏 Drow_Touch_Point(tp_pianyi,tp_pianyi);//画点1 while(1) { if(TCH_PEN->DATA==0)//按键按下了 { if(Read_TP_Once())//得到单次按键值 276 软件实现 { p[cnt].x=tp_pixad.x; p[cnt].y=tp_pixad.y; cnt++; } switch(cnt) { case 1: LCD_Clear(WHITE);//清屏 while(!TCH_PEN->DATA) //等待松手 { } Drow_Touch_Point(LCD_W-tp_pianyi-1,tp_pianyi);//画点2 break; case 2: LCD_Clear(WHITE);//清屏 while(!TCH_PEN->DATA) //等待松手 { } Drow_Touch_Point(tp_pianyi,LCD_H-tp_pianyi-1);//画点3 break; case 3: LCD_Clear(WHITE);//清屏 while(!TCH_PEN->DATA) //等待松手 { } Drow_Touch_Point(LCD_W-tp_pianyi-1,LCD_H-tp_pianyi-1);//画点4 break; case 4: //全部四个点已经得到 LCD_Clear(WHITE);//清屏 while(!TCH_PEN->DATA) //等待松手 { } vx1=p[1].x>p[0].x?(p[1].x-p[0].x+1)*1000/(LCD_W-tp_pianyi-tp_pianyi): (p[0].x-p[1].x-1)*1000/(LCD_W-tp_pianyi-tp_pianyi); chx1=p[1].x>p[0].x?p[0].x-(vx1*tp_pianyi)/1000:p[0].x+(vx1*tp_pianyi) /1000; vy1=p[2].y>p[0].y?(p[2].y-p[0].y-1)*1000/(LCD_H-tp_pianyi-tp_pianyi): (p[0].y-p[2].y-1)*1000/(LCD_H-tp_pianyi-tp_pianyi); chy1=p[2].y>p[0].y?p[0].y-(vy1*tp_pianyi)/1000:p[0].y+(vy1*tp_pianyi) /1000; vx2=p[3].x>p[2].x?(p[3].x-p[2].x+1)*1000/(LCD_W-tp_pianyi-tp_pianyi): 277 软件实现 (p[2].x-p[3].x-1)*1000/(LCD_W-tp_pianyi-tp_pianyi); chx2=p[3].x>p[2].x?p[2].x-(vx2*tp_pianyi)/1000:p[2].x+(vx2*tp_pianyi) /1000; vy2=p[3].y>p[1].y?(p[3].y-p[1].y-1)*1000/(LCD_H-tp_pianyi-tp_pianyi): (p[1].y-p[3].y-1)*1000/(LCD_H-tp_pianyi-tp_pianyi); chy2=p[3].y>p[1].y?p[1].y-(vy2*tp_pianyi)/1000:p[1].y+(vy2*tp_pianyi) /1000; if((vx1>vx2&&vx1>vx2+tp_xiaozhun)||(vx1vy2&&vy1>vy2+tp_xiaozhun)||(vy1DATA==0) { t=0; if(Convert_Pos()) //得到坐标值 { // LCD_ShowString(10,250,"X:");LCD_ShowNum(30,250,(u32)tp_pixad.x,6); //LCD_ShowString(180,250,"Y:");LCD_ShowNum(200,250,(u32)tp_pixad.y,6) ; LCD_ShowString(10,250,"X:");LCD_ShowNum(30,250,tp_pixad.x,4); 279 软件实现 LCD_ShowString(180,250,"Y:");LCD_ShowNum(200,250,tp_pixad.y,4); LCD_DrawPoint_big(tp_pixlcd.x,tp_pixlcd.y); } } else { t++; if(t>65000) { return; } } } } touch.c 中定义了一些实现触摸控制功能的函数,触摸功能的实现原理为:XPT2046 通过 X+、Y+、X-、Y-四根线接收来自 TFT 模块的模拟信号并转换为数字坐标信息;CPU 通过 SPI 总线与 XPT2046 进行数据通信,读取坐标信息,根据坐标信息来进行某些操作(如在触摸点 显示某些图案),从而实现了触摸控制功能。 整个过程中,CPU 与 XPT2046 的 SPI 总线通信尤为重要。SPI 总线通信的时序如下图所 示(12bit 转换模式)。 下面分别介绍下 XPT2046 的 SPI 读写时序。在 CPU 与 XPT2046 的数据通信中,CPU 首 先通过 SPI 总线向 XPT2046 写入控制字,控制字长度为 1byte,结构如下图所示:MSB 的 S 为启动位,置高代表启动数据通信;BIT6~BIT4 的 A2~A0 为通道选择位,BIT2 的 SER/nDFR 为单端/差分参考选择位,这 4 个 BIT 决定了 XPT2046 的输入;BIT3 的 MODE 为 12bit/8bit 转换模式选择,高为 8bit,低为 12bit,它决定了 SPI 总线下一个读取数据的位数;BIT1~BIT0 280 的 PD1~PD0 为节电模式选择。 软件实现 得到上面控制字的结构后,结合 XPT2046 Datasheet 对每个控制位的具体介绍,可以知 道,如果设置为差分方式和 12bit 转换模式,读取 X+坐标,需要写入的控制字为 0xD0 (11010000),如果读取 Y+坐标,需要写入的控制字为 0x90(10010000)。 WriteCharTo7843()为 SPI 总线的写函数,功能是向 XPT2046 写入 1byte 的数据(控制 字),在这个函数中,可以看到,Nios II 自带的 SPI 模块的写数据实现程序语句为 SPI_TCH->TXDATA = num; while(!(SPI_TCH->STATUS.BITS.TMT)); TXDATA 为 SPI 模块存储发送数据的寄存器,将新数据写入其中后,SPI 模块会自动将数 据通过 SPI 总线发送出去,STATUS.BITS.TMT 为传输状态位,为 1 表示数据正在传输,为 0 表示数据传输完成,所以在这里通过一个 while()循环等待数据传输的完成。 ReadFromCharFrom7843()为 SPI 总线读函数,SPI 模块读数据的实现方式为:先向 SPI 写一个无效数据(一般为 0x00),目的是产生 SPI 时序(关键是时钟线会产生 8 个周期的时钟 信号),此时 Slave 设备(这里即 XPT2046) 通过 MISO 将数据传输到 Master(这里即 Nios II CPU)的 RXDATA 寄存器,传输完成后再读取寄存器即可获得读数据。这里需要注意的是, 我们选择的是 12bit 转换模式,所以需要写入两个无效数据从而产生 16 个周期的时钟信号, 结合上图的时序,我们需要的数据为 2~13 个周期的时钟传输的数据,所以在函数中通过 Num = ((num1&0x7f)<<5)+(num2>>3); 这条语句将两次得到的读数据拼接成了最终想得到的数据。 touch.c 文件的核心其实就是 SPI 读写函数,后面的屏幕校准等函数都离不开 SPI 读写的 支持,这里就不再详细介绍了。 然后编写 font.c 文件,里面存放的是显示的图片、汉字和 ASCII 码的字符数据,内容较长, 会在本意总结一节以附录的形式给出下载链接,这里就不列出了。 最后编写 sys.c 文件,完成后得到下图。 281 软件实现 #include #include #include "../inc/sys.h" void delayms(int count) // /* X1ms */ { int i,j; for(i=0;i #include #include "../inc/sys.h" #include "../inc/font.h" #include "../inc/sopc.h" /***************************************************************** * 测试程序功能 * * 1、开机显示相关信息和40*40图片若干。 * 2、之后可以触摸显示屏任何位置进入触摸绘图程序, * 测试板按键1(K4)进入触摸校准程序, * 测试板按键2(K5)开始刷红绿兰3色。 * * 修改LCD数据脚连接IO在lcd.h文件中 * 修改触摸数据脚连接IO在touch.h文件中 * 40*40图片数据和95个ASCCII字符数据和测试汉字取模数据在font.c文件中 * *****************************************************************/ u8 ref=0;//刷新显示 u8 jiance() //检测触摸和按键 { if (tpstate()==0) //如果触摸按下,则进入绘图程序 { LCD_Clear(WHITE); //清屏 BACK_COLOR=WHITE; POINT_COLOR=RED; point(); return 1; } if(TFT_KEY1->DATA==0) //如果按键1按下,进入校准程序 { 282 软件实现 Touch_Adjust();//校准 return 1; } if(TFT_KEY2->DATA==0) //三色刷屏 { LCD_Clear(RED); delayms(3000); LCD_Clear(GREEN); delayms(3000); LCD_Clear(BLUE); delayms(3000); return 1; } return 0; } void xianshi()//显示信息 { u16 lx,ly; BACK_COLOR=WHITE; POINT_COLOR=RED; showhanzi(10,0,0); //云 showhanzi(45,0,1); //虎 showhanzi(75,0,2); //科 showhanzi(105,0,3); //技 LCD_ShowString(10,35,"TIGER BOARD"); LCD_ShowString(10,55,"LCD_W:"); LCD_ShowNum(70,55,LCD_W,3); LCD_ShowString(110,55,"LCD_H:");LCD_ShowNum(160,55,LCD_H,3); lx=10;ly=75; LCD_ShowString(lx,ly,"VX:");lx+=40;LCD_ShowNum(lx,ly,vx,5); lx+=60; LCD_ShowString(lx,ly,"Vy:");lx+=40;LCD_ShowNum(lx,ly,vy,5); lx=10;ly=95; LCD_ShowString(lx,ly,"CHX:");lx+=40;LCD_ShowNum(lx,ly,chx,5); lx+=60; LCD_ShowString(lx,ly,"CHY:");lx+=40;LCD_ShowNum(lx,ly,chy,5); } void showimage() //显示40*40图片 { int i,j,k; LCD_Clear(WHITE); //清屏 xianshi(); //显示信息 for(k=3;k<8;k++) { for(j=0;j<6;j++) { Address_set(40*j,40*k,40*j+39,40*k+39); for(i=0;i<1600;i++) { 283 //坐标设置 软件实现 if(jiance()) //检测触摸和按键 { ref=1; return; } LCD_WR_DATA8(image[i*2+1],image[i*2]); //发送颜色数据为提高速 度高8位低8位分别传递 } } } ref=0; } int main(void) { LCD_DATA_OUT; //数据输出 Lcd_Init(); //tft初始化 LCD_Clear(WHITE); //清屏 BACK_COLOR=BLACK;;POINT_COLOR=WHITE; showimage(); //显示40*40图片 while(1) { if(jiance()) //检测触摸和按键 { showimage(); //显示40*40图片 } if(ref) { showimage(); //显示40*40图片 } } return 0; } main.c 通过调用前面文件定义的函数,实现了一个复杂的触摸控制检测功能: 开机显示相关信息和 40*40 图片若干;之后可以触摸显示屏任意位置进入触摸绘图程序, 按下开发板按键 1(K4)进入触摸校准程序,测试板按键 2(K5)开始刷红绿兰 3 色。整个软件实 现就完成了。 284 17.4 总结 DS1302 应用——RTC 完成软件实现后,运行 Nios II 工程,会显示一些文字图片信息,之后触摸显示屏的任意 位置会进入触摸绘图程序,,按下开发板按键 1(K4)进入触摸校准程序,测试板按键 2(K5)开始 刷红绿兰 3 色。 本章主要介绍在 Nios II 中利用 TFT 液晶模块(S95417-AAA)和触摸控制芯片 XPT2046 实现 TFT 液晶屏的触摸控制功能。在硬件实现部分,添加了控制 XPT2046 触摸芯片的一些 PIO 和一些实现功能所需的 PIO,同时添加了 SPI 总线模块,需要注意的是这里并没有采用 SPI 模 块自带的片选信号线,而是另外添加 PIO 模拟片选信号的方式,更为方便灵活;同时添加上开 发板上复用 SPI 总线器件的片选信号并将其置为无效,以保证 SPI 总线的正常使用。在软件实 现部分,在上一章 TFT 显示功能软件实现的基础上进行了修改,增加了触摸功能和复杂图案显 示功能,触摸功能实现的核心是 SPI 读写函数的编写,读写函数依据 XPT2046 的 Datasheet 编写,掌握时序的重要性依然不言而喻。这一章的内容就到这里,因本人水平有限,文中有任 何错误请联系我,大家在交流中进步!邮箱:vito943@qq.com 附录: font.c 文件下载链接 285 NiosII 使用 USB 第18章 Nios II 使用 USB—— CH376 芯片 本章主要介绍在 Nios II 中利用文件管理控制芯片 CH376 实现 USB DEVICE、USB HOST 和 SD HOST,通过本章,你能学到 (1)在 Nios II 中实现 USB DEVICE。 (2)在 Nios II 中实现 USB HOST。 (3)在 Nios II 中实现 SD HOST。 本章分为六个部分: 一、概述 二、硬件实现 三、软件实现——USB DEVICE 四、软件实现——USB HOST 五、软件实现——SD HOST 六、总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 286 18.1 概述 NiosII 使用 USB U 盘和 SD 卡是生活中常见的不能再常见的存储工具,学习 Nios II 自然少不了掌握它们的 控制方法。要实现对 U 盘和 SD 卡的读写,就得用到专业的文件管理控制芯片——CH376,有 了它,我们就能利用 Nios II 来读写 U 盘或者 SD 卡中的文件了。 CH376 内置了 USB 通信协议的基本固件,内置了处理 Mass-Storage 海量存储设备的专 用通讯协议的固件,内置了 SD 卡的通讯接口固件,内置了 FAT16 和 FAT32 以及 FAT12 文件 系统的管理固件,支持常用的 USB 存储设备(包括 U 盘/USB 硬盘/USB 闪存盘/USB 读卡器) 和 SD 卡(包括标准容量 SD 卡和高容量 HC-SD 卡以及协议兼容的 MMC 卡和 TF 卡)。 因为 CH376 内置 USB 通信协议,所以要实现 USB 通信,无需再另外编写通信协议,CPU 直接通过 SPI 总线与 CH376 进行数据通信即可,至于剩下的复杂工作,包括 USB 通信协议实 现、文件系统管理等,CH376 自然会搞定。 CH376 支持 USB DEVICE(USB 设备方式)、USB HOST(USB 主机方式)和 SD HOST (SD 卡主机方式):在 USB DEVICE 下,我们能实现 TIGER BOARD 与 PC 机的数据通信(当 然得连接 USB 线);在 USB HOST 下,我们可以通过 CH376 芯片来实现 TIGER BOARD 上 U 盘的读写操作;同样在 SD HOST 下,我们可以通过 CH376 芯片来实现 TIGER BOARD 上 SD 卡的读写操作。下面我们就分别来搞定 CH376 的这三种工作模式。 287 18.2 硬件实现 NiosII 使用 USB CH376 的应用框图如下图所示。CPU 可以通过并行总线、SPI 总线和串口三种通信方式实 现对 CH376 的控制,在 TIGER BOARD 上我们采用了 SPI 总线方式;此外,CH376 与 PC 机、 U 盘等设备通过 USB 总线来实现数据通信,与 SD 卡通过 SPI 总线(与 CPU 的 SPI 总线不是 同一个)来实现数据通信。 我们进一步来看 CH376 的原理图,图中 J6 为连接 PC 机的 USB 线接口,用于 USB DEVICE 下与 PC 机通信;J3 为连接 U 盘的接口,用于 USB HOST 下与 U 盘通信;P6 为 SD 卡的接口, 用于 SD HOST 下与 SD 卡通信。红色管脚为 CH376 与 CPU 的连接管脚,除了 4 根 SPI 总线, 还有一个中断信号 USB_nINT。因为 TIGER BOARD 上的 SPI 总线采用的复用方式,我们在 TFT 一章已经添加了 SPI 总线模块和单独的片选线 USB_nCS,这里只需要添加一个 PIO 连接 USB_nINT 即可。 288 NiosII 使用 USB 打开 Qsys,在工程里面添加一个 PIO,将方向设置为输入,位宽为 1,并设置电平中断触 发有效,添加完成后改名、连线、端口引出、分配地址、分配中断,完成后得到下图。 完成上述操作后保存并点击 Genrate 编译 Qsys。然后打开 Quartus II,在顶层 bdf 文件 中更新 kernel 得到新添加的 IO 端口,将它生成管脚、分配管脚,完成后得到下图。不要忘了 Nios II CPU 只对高电平敏感,而 CH376 的中断管脚产生的是低电平中断,所以要给 usb_nint 加一个非门作电平转换。 完成后编译 Quartus II 工程,硬件实现部分就完成了。 289 NiosII 使用 USB 18.3 软件实现——USB DEVICE 下面先完成 USB DEVICE 的软件实现。打开 Nios II,新建一个工程,命名为 usb_device。 将目录区整理成三文件夹结构。 在编写程序之前,首先介绍下本章 USB DEVICE 的实现原理。 要实现 USB DEVICE,PC 端需要安装好 CH376 的驱动,并有一个相应的上位机软件,USB DEVICE 整个过程如下: 1、初始化 CH376,包括将工作模式设为 USB DEVICE,并开启中断。 2、CPU 每隔一段时间就向 CH376 的缓冲区发送数据,上位机上有相应的接收控制按键, 点击按键上位机就会接收缓冲区的数据,同时 CH376 的 USB_nINT 管脚电平变低,触发 CPU 中断,在中断函数中释放缓冲区。 3、当上位机写数据时,先将数据发送到 CH376 的缓冲区,然后 CH376 的 USB_nINT 管 脚电平变低,触发 CPU 中断,CPU 读 CH376 的缓冲区数据并释放缓冲区。 以上就是 USB DEVICE 的工作原理 ,可以看出,整个过程包含两个关键因素:中断和 SPI 总线。这里再详细介绍下 CPU 与 CH376 的数据通信原理。CPU 与 CH376 通过 SPI 总线进行 数据通信,CH376 芯片内部包含多个命令寄存器,要实现对 CH376 的控制,就要通过 SPI 总 线对命令寄存器进行读写操作,读写时序如下图所示:要实现写操作,先写入命令地址,再写 入数据;要实现读操作,先写入命令地址,再读出数据。 290 NiosII 使用 USB 依据上面的原理,我们来完成软件实现。在 inc 文件夹中新建一个.h 头文件命名为 usb.h, 编写完成后得到下图。 #ifndef __usb_h__ #define __usb_h__ /*---------------------------------------------------------------* Include *---------------------------------------------------------------*/ #include "system.h" /*---------------------------------------------------------------* Define *---------------------------------------------------------------*/ //common #define USB_HOST 0X06 #define USB_DEVICE 0x02 #define USB_DISABLE 0X00 #define RESET_ALL 0X05 #define CHECK_EXIST 0X06 #define SET_USB_ID 0X12 #define SET_USB_MODE 0X15 #define GET_STATUS 0X22 #define UNLOCK_USB 0X23 #define RD_USB_DATA 0X28 #define WR_USB_DATA5 0X2A #define WR_USB_DATA7 0X2B #define GET_IC_VER 0X01 #define ENTER_SLEEP 0X03 #define CHK_SUSPEND 0X0B #define RD_USB_DATA0 0X27 291 NiosII 使用 USB #define RET_SUCCESS 0X51 #define RET_ABORT 0X5B #define INT_EP2_OUT 0x02 #define INT_EP2_IN 0x0a //host #define DISK_READ 0X54 #define DISK_RD_GO 0X55 #define DISK_READY 0X59 #define DISK_INIT 0X51 //status #define USB_INT_CONNECT 0x15 #define USB_INT_DISCONNECT 0X16 #define USB_INT_SUCCESS 0X14 #define USB_INT_DISK_READ 0X1D #define PIO_USB_INT *(volatile unsigned long int *)USB_INT_BASE //spi #define _SPI_USB typedef struct{ volatile unsigned long int RXDATA; volatile unsigned long int TXDATA; union{ struct{ volatile unsigned long int NC volatile unsigned long int ROE volatile unsigned long int TOE volatile unsigned long int TMT volatile unsigned long int TRDY volatile unsigned long int RRDY volatile unsigned long int E volatile unsigned long int NC1 }BITS; volatile unsigned long int WORD; }STATUS; :3; :1; :1; :1; :1; :1; :1; :23; union{ struct{ volatile unsigned long int NC volatile unsigned long int IROE volatile unsigned long int ITOE volatile unsigned long int NC1 volatile unsigned long int ITRDY volatile unsigned long int IRRDY volatile unsigned long int IE volatile unsigned long int NC2 volatile unsigned long int SSO }BITS; 292 :3; :1; :1; :1; :1; :1; :1; :1; :21; NiosII 使用 USB volatile unsigned long int CONTROL; }CONTROL; unsigned long int RESERVED0; unsigned long int SLAVE_SELECT; }SPI_ST; typedef struct { unsigned long int DATA; unsigned long int DIRECTION; unsigned long int INTERRUPT_MASK; unsigned long int EDGE_CAPTURE; }PIO_STR; #ifdef _SPI_USB #define SPI_USB #define USB_CS #endif ((SPI_ST *) SPI_BASE) ((PIO_STR *) USB_NCS_BASE) #define VID 0X0FFE #define PID 0X1000 /*----------------------------------------------------------------* Struct *----------------------------------------------------------------*/ typedef struct{ char receive_buffer[200]; int send_ok_flag; int receive_ok_flag; }USB_T; /*----------------------------------------------------------------* Extern *----------------------------------------------------------------*/ extern USB_T usb; extern int initialize_usb(void); extern int set_usb_mode(unsigned char); extern int send_string_to_usb(char *str,int str_len); extern void write_command_to_usb(unsigned char command); extern void write_data_to_usb(unsigned char data); #endif //__usb_h__ usb.h 文件的内容很长,但细细一看都似曾相识,无非是定义结构体、声明函数,需要注 意的是 common 部分,底下是一些宏定义,这些宏定义是 CH376 芯片的指令,稍后我们要利 用这些指令来实现功能。 然后在 driver 文件夹中新建一个.c 文件夹命名为 usb.c,编写完成后得到下图。 293 NiosII 使用 USB /*------------------------------------------------------------------* Include *------------------------------------------------------------------*/ #include "../inc/usb.h" #include "altera_avalon_pio_regs.h" #include "sys/alt_irq.h" #include #include /*------------------------------------------------------------------* Function *------------------------------------------------------------------*/ void write_command_to_usb(unsigned char command); void write_data_to_usb(unsigned char data); unsigned char read_data_from_usb(void); void delay(void); /*------------------------------------------------------------------* Variable *------------------------------------------------------------------*/ USB_T usb; /* * === FUNCTION =================================================== * Name: irq_usb * Description: USB中断函数 * ================================================================== */ void irq_usb(void *context) { unsigned int i; unsigned char interrupt_status,data_len; // static int times=0; printf("int!"); write_command_to_usb(GET_STATUS); interrupt_status=read_data_from_usb(); switch(interrupt_status){ //Device case INT_EP2_OUT: write_command_to_usb(RD_USB_DATA); data_len=read_data_from_usb(); for(i=0;iDATA = 1; usleep(5); USB_CS->DATA = 0; // spi write data SPI_USB->TXDATA = command; while(!(SPI_USB->STATUS.BITS.TMT)); usleep(20); } /* * === FUNCTION ================================================== * Name: write_data_to_usb * Description: 向USB芯片写数据 * ================================================================= */ void write_data_to_usb(unsigned char data) { SPI_USB->TXDATA = data; while(!(SPI_USB->STATUS.BITS.TMT)); usleep(20); 296 NiosII 使用 USB } /* * === * FUNCTION ================================================== Name: read_data_from_usb * Description: 从USB芯片读数据 * ================================================================= */ unsigned char read_data_from_usb(void) { unsigned char data=0; //向USB芯片写0x00,目的是产生读数据时序,更新SPI模块的RXDATA寄存器 SPI_USB->TXDATA = 0x00; while(!(SPI_USB->STATUS.BITS.TMT)); data = SPI_USB->RXDATA; return data; } usb.c 是 USB 的底层驱动文件,中包含了实现 USB DEVICE 功能的函数,包括中断函数、 写命令函数、写数据函数、读数据函数等等。函数的详细内容就不再介绍了,CH376 各个命令 寄存器的含义可以参考 Datasheet。 最后完成 main.c 文件的编写,得到下图。 /*----------------------------------------------------------------* Include *----------------------------------------------------------------*/ #include #include #include "../inc/usb.h" /* * === FUNCTION =============================================== * Name: main * Description: * ============================================================== */ int main() { unsigned char tmp[] = "Hello USB!\n"; initialize_usb(); while(1){ if(usb.receive_ok_flag){ printf("%s\n",usb.receive_buffer); usb.receive_ok_flag = 0; } send_string_to_usb(tmp,sizeof(tmp)); usleep(100000); 297 NiosII 使用 USB } return 0; } 在 main 函数中,首先调用 USB 初始化函数对 CH376 初始化。然后在一个 while(1)循环 中:不停地检测 usb.receive_ok_flag 是否有效,有效表明上位机发送过来了数据,将数据打 印 并 将 usb.receive_ok_flag 置 无 效 ; 同 时 不 断 地 将 tmp 数 据 中 的 字 符 串 通 过 send_string_to_usb()函数发送到 CH376 的缓冲区,以备上位机读取数据。 在本节的开始已经讲到,要实现 USB DEVICE,PC 端要安装 CH376 的驱动并有上位机软 件,在实验总结一节我们会介绍 CH376 驱动和上位机软件。 298 NiosII 使用 USB 18.4 软件实现——USB HOST 完成了 USB DEVICE,接下来搞定 USB HOST 的软件实现,USB HOST 实现的是对 TIGER BOARD 上 U 盘的读写和文件管理操作。 打开 Nios II,新建一个工程命名为 usb_host,将目录区整理成三文件夹结构。这一节我 们要创建和编写的文件较多,如下图所示。 USB HOST 的软件实现基于南京沁恒公司提供的 CH376 的相关例程,原例程适用于单片 机环境,为了能在 Nios II CPU 中正常运行,需要我们进行一些修改。本工程的文件虽然多, 但有一些文件是沁恒公司提供的通用程序,无需修改,所以会方便一些。 在 inc 文件夹中新建五个.h 头文件,命名为 sopc.h、hal.h、host.h、file_sys.h 和 ch376inc.h, 先编写 sopc.h,完成后得到下图。 #ifndef SOPC_H_ #define SOPC_H_ /*-------------------------------------------------------------------* Include *--------------------------------------------------------------------*/ #include "system.h" /*-------------------------------------------------------------------* Define *--------------------------------------------------------------------*/ #define _SPI_USB #define _LED 299 NiosII 使用 USB /*-------------------------------------------------------------------* Peripheral registers structures *--------------------------------------------------------------------*/ typedef struct{ volatile unsigned long int RXDATA; volatile unsigned long int TXDATA; union{ struct{ volatile unsigned long int NC volatile unsigned long int ROE volatile unsigned long int TOE volatile unsigned long int TMT volatile unsigned long int TRDY volatile unsigned long int RRDY volatile unsigned long int E volatile unsigned long int NC1 }BITS; volatile unsigned long int WORD; }STATUS; :3; :1; :1; :1; :1; :1; :1; :23; union{ struct{ volatile unsigned long int NC volatile unsigned long int IROE volatile unsigned long int ITOE volatile unsigned long int NC1 volatile unsigned long int ITRDY volatile unsigned long int IRRDY volatile unsigned long int IE volatile unsigned long int NC2 volatile unsigned long int SSO }BITS; volatile unsigned long int CONTROL; }CONTROL; :3; :1; :1; :1; :1; :1; :1; :1; :21; unsigned long int RESERVED0; unsigned long int SLAVE_SELECT; }SPI_ST; typedef struct { unsigned long int DATA; unsigned long int DIRECTION; unsigned long int INTERRUPT_MASK; unsigned long int EDGE_CAPTURE; }PIO_STR; 300 NiosII 使用 USB /*----------------------------------------------------------------- * Peripheral declaration *-----------------------------------------------------------------*/ #ifdef _SPI_USB #define SPI_USB ((SPI_ST *) SPI_BASE) #define USB_CS ((PIO_STR *) USB_NCS_BASE) #endif /*_SPI_USB*/ #ifdef _LED #define LED #endif /* _LED */ ((PIO_STR *) LED_BASE) #endif /*SOPC_H_*/ sopc.h 文件主要定义了一些结构体,指向 IP 核模块的基地址,没什么可说的。 然后编写 hal.h 文件,完成后得到下图。 #ifndef __usb_h__ #define __usb_h__ /*----------------------------------------------------------------------* Include *---------------------------------------------------------------------*/ #include "system.h" /*----------------------------------------------------------------------* Define *---------------------------------------------------------------------*/ //common #define USB_HOST #define SD_HOST #define USB_DEVICE #define USB_DISABLE 0X06 0X03 0x02 0X00 #define RESET_ALL 0X05 #define CHECK_EXIST 0X06 #define SET_USB_ID 0X12 #define SET_USB_MODE 0X15 #define GET_STATUS 0X22 #define UNLOCK_USB 0X23 #define RD_USB_DATA 0X28 #define WR_USB_DATA5 0X2A #define WR_USB_DATA7 0X2B #define GET_IC_VER 0X01 #define ENTER_SLEEP 0X03 #define CHK_SUSPEND 0X0B #define RD_USB_DATA0 0X27 #define RET_SUCCESS 0X51 #define RET_ABORT 0X5B #define INT_EP2_OUT #define INT_EP2_IN 0x02 0x0a 301 NiosII 使用 USB //host #define DISK_READ #define DISK_RD_GO 0X54 0X55 #define DISK_READY 0X59 #define DISK_INIT 0X51 //status #define USB_INT_CONNECT 0x15 /*----------------------------------------------------------------------* Include *---------------------------------------------------------------------*/ #define CMD_RET_SUCCESS 0x51 /* 命令操作成功 */ #define CMD_RET_ABORT 0x5F /* 命令操作失败 */ /*----------------------------------------------------------------------* Define *---------------------------------------------------------------------*/ //usb #define PIO_USB_NINT *(volatile unsigned long int *)USB_NINT_BASE #define VID 0X0FFE #define PID 0X1000 /*----------------------------------------------------------------------* Struct *---------------------------------------------------------------------*/ typedef struct{ char receive_buffer[200]; int send_ok_flag; int receive_ok_flag; }USB_T; /*----------------------------------------------------------------------* Extern *---------------------------------------------------------------------*/ extern USB_T usb; extern void xWriteCH376Cmd(unsigned char command); extern void xWriteCH376Data(unsigned char data); extern unsigned char xReadCH376Data(void); extern unsigned int mInitCH376Host(void); extern unsigned char Query376Interrupt(void); #endif //__usb_h__ hal.h 文件为 USB 驱动程序头文件,对一些常用的数据做了宏定义,并声明了一些函数, hal.h 文件为 hal.c 文件的实现做了准备。 302 NiosII 使用 USB 然后编写 host.h,完成后得到下图。 #ifndef HOST_H_ #define HOST_H_ extern void host(void); #endif /*HOST_H_*/ host.h 文件声明了要在 host.c 中实现的 host()函数,这个文件够简单吧?(要是所有程 序都这么简单多好~囧) 我们还需要编写两个.h 文件:file_sys.h 和 ch376inc.h。file_sys.h 是文件系统层头文件, 对文件系统常用子程序作了声明;ch376inc.h 对 CH376 芯片的命令作了宏定义。这两个文件 直接用南京沁恒公司提供的现成文件即可,无需修改。因为它们的内容较长,这里就不列出了, 在本章最后的附录中会提供下载链接。 接下来在 driver 文件夹中新建三个.c 源文件:hal.c、host.c 和 file_sys.c。hal.c 先编写 hal.c,完成后得到下图。 /*------------------------------------------------------------------* Include *-----------------------------------------------------------------*/ #include "../inc/hal.h" #include "../inc/sopc.h" #include "altera_avalon_pio_regs.h" #include "altera_avalon_spi.h" #include "sys/alt_irq.h" #include #include #include "../inc/ch376inc.h" #include "system.h" /*------------------------------------------------------------------* Function *-----------------------------------------------------------------*/ void xWriteCH376Cmd(unsigned char command); void xWriteCH376Data(unsigned char data); unsigned char xReadCH376Data(void); unsigned int mInitCH376Host(void); void delay(void); int set_usb_mode(unsigned char type); unsigned char Query376Interrupt(void); void irq_usb(void * context,unsigned int id); /*------------------------------------------------------------------* Struct *-----------------------------------------------------------------*/ USB_T usb; 303 NiosII 使用 USB /* * === FUNCTION =================================================== * Name: set_usb_mode * Description: 设置USB模式 * ================================================================== */ int set_usb_mode(unsigned char type) { unsigned int res; xWriteCH376Cmd(SET_USB_MODE); xWriteCH376Data(type); res = xReadCH376Data(); USB_CS->DATA = 1; if(res == CMD_RET_SUCCESS) { printf("init finish!"); return 0; } else { printf("init failure!"); return -1; } } /* * === FUNCTION ==================================================== * Name: xWriteCH376Cmd * Description: 写命令 * =================================================================== */ void xWriteCH376Cmd(unsigned char command) { USB_CS->DATA = 1; usleep(5); USB_CS->DATA = 0; SPI_USB->TXDATA = command; while(!(SPI_USB->STATUS.BITS.TMT)); } /* * === FUNCTION =================================================== * Name: xWriteCH376Data * Description: 写数据 * ================================================================== */ void xWriteCH376Data(unsigned char data) { USB_CS->DATA = 0; 304 NiosII 使用 USB SPI_USB->TXDATA = data; while(!(SPI_USB->STATUS.BITS.TMT)); } /* * === FUNCTION =================================================== * Name: xReadCH376Data * Description: 读数据 * ================================================================== */ unsigned char xReadCH376Data(void) { unsigned char data=0; USB_CS->DATA = 0; SPI_USB->TXDATA = 0x00; //0x00 is random number ,to enable clock while(!(SPI_USB->STATUS.BITS.TMT)); data = SPI_USB->RXDATA; return data; } /* * === FUNCTION =================================================== * Name: mInitCH376Host * Description: CH376初始化函数 * ================================================================== */ unsigned int mInitCH376Host(void) { usb.receive_ok_flag=0; usb.send_ok_flag=0; //enable the io interrupt IOWR_ALTERA_AVALON_PIO_IRQ_MASK(USB_NINT_BASE,0); IOWR_ALTERA_AVALON_PIO_EDGE_CAP(USB_NINT_BASE,0); set_usb_mode(USB_HOST); return USB_INT_SUCCESS; } /* * === FUNCTION ================================================== * Name: Query376Interrupt * Description: 中断查询函数 * ================================================================= */ unsigned char Query376Interrupt( void ) { return(PIO_USB_NINT?TRUE:FALSE); } 305 NiosII 使用 USB hal.c 文件是 USB 的驱动程序文件,在 USB DEVICE 一节已经编写了 USB 的驱动程序文 件,细心的同学会发现 hal.c 的内容与 USB DEVICE 中的驱动程序文件相似,无非还是中断函 数、写命令函数、写数据函数、读数据函数等。这里有两处与 USB DEVICE 驱动程序的区别 要说明:一是在 USB 初始化函数中,我们设置的 USB 模式为 USB_HOST(0x06),而 USB DEVICE 一节设置的为 USB_DEVICE(0x02);二是在程序中,虽然有中断函数,但并不是采 用的外部中断触发的方式,而采用查询中断管脚电平的方式,如 Query376Interrupt()函数所 示。 然后编写 host.c,完成后得到下图。 /* CH376 主机文件系统接口 */ /* Nios II的U盘文件读写示例程序 */ /* 本程序演示字节读写,文件枚举, 用于将U盘中的/C51/CH376HFT.C文件中的前200个字符显示 出来, 如果找不到原文件CH376HFT.C, 那么该程序将显示C51子目录下所有以CH376开头的文件名, 如果找不到C51子目录, 那么该程序将显示根目录下的所有文件名 */ #include "../inc/ch376inc.h" #include "../inc/hal.h" #include "../inc/file_sys.h" #include #include #include #include "../inc/sopc.h" UINT8 buf[64]; void host(void) { UINT8 i, s; UINT8 TotalCount; UINT16 RealCount; P_FAT_DIR_INFO pDir; s = mInitCH376Host( ); /* 初始化CH376 */ /* 其它电路初始化 */ while ( 1 ) { printf( "Wait Udisk/SD\n" ); while ( CH376DiskConnect( ) != USB_INT_SUCCESS ) 306 NiosII 使用 USB { /* 检查U盘是否连接,等待U盘插入,对于SD卡,可以由单片机直接查询SD卡座的插拔 状态引脚 */ usleep( 1000*100 ); /* 没必要频繁查询 */ printf("USB FAILURE\n"); } /* 对于检测到USB设备的,最多等待100*50mS,主要针对有些MP3太慢,对于检测到USB设 备并且连接DISK_MOUNTED的,最多等待5*50mS,主要针对DiskReady不过的 */ for ( i = 0; i < 100; i ++ ) { /* 最长等待时间,100*50mS */ usleep( 1000*50 ); printf( "Ready ?\n" ); s = CH376DiskMount( ); /* 初始化磁盘并测试磁盘是否就绪 */ if ( s == USB_INT_SUCCESS ) break; /* 准备好 */ else if ( s == ERR_DISK_DISCON ) break; /* 检测到断开,重新检测并计 时 */ if ( CH376GetDiskStatus( ) >= DEF_DISK_MOUNTED && i >= 5 ) break; /* 有的U盘总是返回未准备好,不过可以忽略,只要其建立连接MOUNTED且尝试5*50mS */ } if ( s == ERR_DISK_DISCON ) { /* 检测到断开,重新检测并计时 */ printf( "Device gone\n" ); continue; } if ( CH376GetDiskStatus( ) < DEF_DISK_MOUNTED ) { /* 未知USB设备,例如USB键盘、打印机等 */ printf( "Unknown device\n" ); goto UnknownUsbDevice; } i = CH376ReadBlock( buf ); /* 如果需要,可以读取数据块 CH376_CMD_DATA.DiskMountInq,返回长度 */ if ( i == sizeof( INQUIRY_DATA ) ) { /* U盘的厂商和产品信息 */ buf[ i ] = 0; printf( "UdiskInfo: %s\n", ((P_INQUIRY_DATA)buf) -> VendorIdStr ); } /* 读取原文件 */ printf( "Open\n" ); strcpy( buf, "\\C51\\CH376HFT.C" ); /* 源文件名,多级目录下的文件名和路径 名必须复制到RAM中再处理,而根目录或者当前目录下的文件名可以在RAM或者ROM中 */ 307 NiosII 使用 USB printf("buf:%s\n",buf); s = CH376FileOpenPath( buf ); /* 打开文件,该文件在C51子目录下 */ if ( s == ERR_MISS_DIR || s == ERR_MISS_FILE ) { /* 没有找到目录或者 没有找到文件 */ /* 列出文件,完整枚举可以参考EXAM13全盘枚举 */ if ( s == ERR_MISS_DIR ) strcpy( buf, "\\*" ); /* C51子目录不存在则列 出根目录下的文件 */ else strcpy( buf, "\\C51\\CH376*" ); /* CH376HFT.C文件不存在则列出\C51 子目录下的以CH376开头的文件 */ printf( "List file %s\n", buf ); s = CH376FileOpenPath( buf ); /* 枚举多级目录下的文件或者目录,输入缓冲区 必须在RAM中 */ while ( s == USB_INT_DISK_READ ) { /* 枚举到匹配的文件 */ CH376ReadBlock( buf ); /* 读取枚举到的文件的FAT_DIR_INFO结 构,返回长度总是sizeof( FAT_DIR_INFO ) */ pDir = (P_FAT_DIR_INFO)buf; /* 当前文件目录信息 */ if ( pDir -> DIR_Name[0] != '.' ) { /* 不是本级或者上级目录名则继续,否则必须丢弃不处理 */ if ( pDir -> DIR_Name[0] == 0x05 ) pDir -> DIR_Name[0] = 0xE5; /* 特殊字符替换 */ pDir -> DIR_Attr = 0; /* 强制文件名字符串结束以便打印输出 */ printf( "*** EnumName: %s\n", pDir -> DIR_Name ); /* 打印名称, 原始8+3格式,未整理成含小数点分隔符 */ } xWriteCH376Cmd( CMD0H_FILE_ENUM_GO ); */ /* 继续枚举文件和目录 s = Wait376Interrupt( ); } } else { /* 找到文件或者出错 */ TotalCount = 200; /* 准备读取总长度 */ printf( "从文件中读出的前%d个字符是:\n",(UINT16)TotalCount ); while ( TotalCount ) { /* 如果文件比较大,一次读不完,可以再调用CH376ByteRead继续读取,文件指针自 动向后移动 */ 308 NiosII 使用 USB if ( TotalCount > sizeof(buf) ) i = sizeof(buf); /* 剩余 数据较多,限制单次读写的长度不能超过缓冲区大小 */ else i = TotalCount; /* 最后剩余的字节数 */ s = CH376ByteRead( buf, i, &RealCount ); /* 以字节为单位读 取数据块,单次读写的长度不能超过缓冲区大小,第二次调用时接着刚才的向后读 */ 的字符数 */ TotalCount -= (UINT8)RealCount; /* 计数,减去当前实际已经读出 for ( s=0; s!=RealCount; s++ ) printf( "%c", buf[s] ); /* 显示读出的字符 */ //printf("%s",buf); if ( RealCount < i ) { /* 实际读出的字符数少于要求读出的字符数,说明已经到文件的结尾 */ printf( "\n" ); printf( "文件已经结束\n" ); break; } } printf( "Close\n" ); s = CH376FileClose( FALSE ); /* 关闭文件 */ } UnknownUsbDevice: printf( "Take out\n" ); while ( CH376DiskConnect( ) == USB_INT_SUCCESS ) { /* 检查U盘是否连接,等待U盘拔出 */ usleep( 1000*100 ); } usleep( 1000*100 ); } } host.c 中通过 host()函数实现了 USB HOST 的功能,具体功能为:演示字节读写,文件枚 举, 用于将 U 盘中的/C51/CH376HFT.C 文件中的前 200 个字符显示出来,如果找不到原文件 CH376HFT.C, 那么该程序将显示 C51 子目录下所有以 CH376 开头的文件名,如果找不到 C51 子目录, 那么该程序将显示根目录下的所有文件名。文件内容较长,大家要结合注释慢慢吸收 理解。 file_sys.c 文件为文件系统层文件,提供了文件系统常用子程序和命令打包,我们同样直接 309 NiosII 使用 USB 用南京沁恒公司提供的现成文件即可,无需修改,也会在总结一节的附录中提供下载链接。 最后完成 main.c 文件编写,得到下图。 #include #include "../inc/hal.h" #include #include "../inc/sopc.h" #include "../inc/file_sys.h" #include "../inc/host.h" int main() { printf("Hello from Nios II!\n"); host(); return 0; } 有了前面程序文件的铺垫,main.c 基本上是“坐享其成”,就是在 main 函数中调用了 host() 函数实现了 USB HOST 功能。至此,USB HOST 的功能就全部实现了。 310 NiosII 使用 USB 18.5 软件实现——SD HOST 最后来实现 SD HOST。上面的 USB HOST 程序长,难度大,掌握起来要费些功夫,读写 个 U 盘真心不易啊!轮到 SD HOST 实现时,有一个坏消息一个好消息:坏消息是 SD HOST 的程序也很长( );好消息是程序虽然长,却与 USB HOST 基本一致,只需改几个地方即 可( )。只要前面 USB HOST 掌握了,搞定 SD HOST 就是分分钟的事! 我们只需改动两个文件,一个是 hal.c,把文件中的 CH376 初始化函数中设置 USB 模式 由 USB HOST(0X06)改为 SD HOST(0X03),如下图红框所示。 …… /* * === * FUNCTION =================================================== Name: mInitCH376Host * Description: CH376初始化函数 * ================================================================== */ unsigned int mInitCH376Host(void) { usb.receive_ok_flag=0; usb.send_ok_flag=0; //enable the io interrupt IOWR_ALTERA_AVALON_PIO_IRQ_MASK(USB_NINT_BASE,0); IOWR_ALTERA_AVALON_PIO_EDGE_CAP(USB_NINT_BASE,0); set_usb_mode(SD_HOST); return USB_INT_SUCCESS; } …… 另一个是 host.c 文件,主要有两种改动:把红框 1 的插拨检测程序注释掉,因为在 CH376 中,此命令不支持 SD 卡(参见 CH376 芯片 Datasheet);把红框 2 的插拨检测程序注释掉, 理由同 1,此外,把 usleep 的延时增长到 30s,避免 CPU 频繁执行 host 功能。 /* CH376 主机文件系统接口 */ /* Nios II的SD卡文件读写示例程序 */ /*注意:TIGER BOARD上未连接SD卡插拔检测管脚,运行程序前请确保SD已经插在开发板上!!! 311 NiosII 使用 USB */ /* 本程序演示字节读写,文件枚举, 用于将U盘中的/C51/CH376HFT.C文件中的前200个字符显示 出来, 如果找不到原文件CH376HFT.C, 那么该程序将显示C51子目录下所有以CH376开头的文件名, 如果找不到C51子目录, 那么该程序将显示根目录下的所有文件名 */ …… while ( 1 ) { //printf( "Wait Udisk/SD\n" ); //while ( CH376DiskConnect( ) != USB_INT_SUCCESS ) //1 //{ // /* 检查U盘是否连接,等待U盘插入,对于SD卡,可以由单片机直接查询SD卡座的插 拔状态引脚 */ // usleep( 1000*100 ); /* 没必要频繁查询 */ // printf("USB FAILURE\n"); // } …… UnknownUsbDevice: printf( "Take out\n" ); //while ( CH376DiskConnect( ) == USB_INT_SUCCESS ) //{ // /* 检查U盘是否连接,等待U盘拔出 */ // usleep( 1000*100 ); //2 //} usleep( 1000*1000*30); } } …… SD HOST 软件实现就完成了,3 处改动搞定一切! 312 18.6 总结 DS1302 应用——RTC 这一章的内容较多,而且除了硬件实现和软件实现,其它 X 因素(驱动、上位机、U 盘、 SD 卡)对实验的成功与否都有影响。为了保障大家能顺利完成各个功能的实现,下面将三个 功能的调试过程简单介绍下。  USB DEVICE 调试 USB DEVICE 功能的实现需要 PC 机的配合,在 USB DEVICE 模式下实现 TIGER BOARD 与 PC 机的通信。要想正常实现功能,PC 机需要安装 CH376 驱动和 USB 调试助手。 首先是 CH376 驱动的安装。CH376 是南京沁恒的芯片,我们可以去南京沁恒的官网下载 相应驱动安装包,下载地址为:http://www.wch.cn/downloads.php?name=pro&proid=66。 当然,我们在 TIGER BOARD 的资料包中也给大家准备好了 CH376 驱动安装包,可直接下载 使用。 CH376 驱动安装包如下图所示(不要被名字蒙蔽,该驱动适用于 CH372/CH375/CH376), 驱动的安装有两种方式:预安装与直接安装。 预安装的步骤为:双击 CH376 驱动安装包,得到下图。 313 DS1302 应用——RTC 点击安装键进行预安装,完成后会弹出下图所示对话框,表示预安装成功。 然后用 USB 线连接 TIGER BOARD 开发板 USB CLIENT 接口和 PC 机,运行 Nios II 工程, 桌面的右下方会弹出下图所示窗口,提示驱动安装成功。 我们打开电脑的设备管理器,在外部接口中看到了 USB CH376 设备,表明驱动已经安装 成功。 直接安装在步骤上与预安装略有不同:先连接好 USB 线运行 Nios II 工程,然后再双击 CH376 驱动安装包,点击安装键进行直接安装,完成后会弹出下图的窗口,可以看到提示从驱 动预安装成功变为了驱动安装成功。 预安装与直接安装的效果是一样的,之所以存在两种安装方式,是因为我们在 Nios II 工 程中对 CH376 进行了初始化,将其设置为 USB DEVICE 模式,只有在这种模式下 USB 设备才 能被 PC 机识别。如果在运行 Nios II 工程之前选择安装驱动,只是预先将驱动文件准备好(即 314 DS1302 应用——RTC 预安装),等到 Nios II 工程运行,USB 初始化成功后 PC 机识别了 USB 设备,才会正式安装驱 动;如果先运行 Nios II 工程,再选择安装程序,因为 PC 机已经识别了 USB 设备,则会直接 安装驱动。 以上的驱动安装是在 WIN7 环境下进行的,XP 可能略有不同,大家需要注意下。 然后是 USB 调试助手的安装,USB 调试助手是一个帮助调试 USB 通信的小软件,功能类 似于前面介绍的串口调试助手。这里我们采用南京沁恒官网提供的 USB 调试助手软件,下载地 址为:http://www.wch.cn/downloads.php?name=pro&proid=9,当然 TIGER BOARD 的 资料包中也准备了该软件,可直接使用。 南京沁恒的 USB 调试助手为 exe 文件,无需安装,可直接使用。双击软件,得到下图所 示的界面。我们需要关注的区域为设备操作、端点 2 下传和端点 2 上传,端点 1 上传无需关注。 设备操作区会显示当前 USB 设备的状态,包括设备信息、设备状态等。可以看到在设备状 态框显示 USB 设备已拨出,表示设备还未连接上。 端点 2 下传区为数据发送区,我们在数据栏中输出要发送的字符数据,需要注意的是 USB 调试助手只能发送字符的 ASCII 码,而且相邻字符的 ASCII 码之间要连续不能出现空格,同时 长度栏要设置好发送数据的长度。 端点 2 上传区为数据接收区,负责接收从 USB 设备发送过来的数据,格式同样为 ASCII 码,与端点 2 下传区不同的是,传输过来的字符 ASCII 码之间被加了空格方便大家查看。 315 DS1302 应用——RTC 准备好驱动和 USB 调试助手后,就可以正式开始调试 USB DEVICE 功能了。这里驱动采 用预安装方式,点击 CH376 驱动进行预安装,成功后用 USB 线连接 TIGER BOARD 的 USB CLIENT 接口和 PC 机,运行 Nios II 工程,等待驱动安装成功。此时 USB 调试助手的设备状态 栏会显示检测到有设备插入,此时点击设备操作区的打开设备按键,设备信息栏会显示 USB 设备的相关信息,如下图所示。 先进行数据接收测试,点击端点 2 上传区的上传键,在数据栏会显示接收到的数据,如下 图所示。 316 DS1302 应用——RTC Up 显示接收到的数据长度,为 12。查阅 ASCII 码对应的字符,如下图所示,对应的为“Hello USB!\n”字符串。 而 Nios II 程序中发送的正是该字符串,如下图所示,表示数据接收成功。 然后是数据发送测试,发送的内容和对应的 ASCII 码如下图所示。 在端点 2 下传区的数据栏输出 ASCII 码(注意没有空格!),在长度栏输入 12,如下图所 示。 点击下传键进行数据发送,在 Nios II 的状态区可以看到 TIGER BOARD 接收到的“TIGER BOARD”字符,表示数据发送成功。以上就是 USB DEVICE 的整个调试过程。 317 DS1302 应用——RTC 小贴士 Tips:字符串的结束标志——‘\0’ 细心的同学可能会发现,在数据接收调试中,USB 调试助手接收的数据包含了字符’\0’(即 NULL)的 ASCII 码 00,而 Nios II 发送的字符串数组中并没有包含它。其实在 C 语言 中,’\0’是字符串的结束标志,虽然字符串中没有该字符,但系统会自动在字符串结尾 添加,所以会接收到数据的最后一个 ASCII 码为 00。当然也可以手动添加,正如在数 据发送调试中做的那样(因为不确定 USB 调试助手是否会自动添加’\0’,这样做比较稳 妥)。  USB HOST 调试 在 USB HOST 调试中,我们需要准备一个 U 盘,并且要确保 U 盘的文件系统是 CH376 支持的 FAT32、FAT16 或 FAT12。如何查看 U 盘当前的文件系统呢?把 U 盘插到电脑上,右 键单击可移动磁盘,选择属性,在常规菜单栏中可以看到文件系统信息,如下图所示。 如果 U 盘当前用的是 NTFS,就只能格式化一下重新选择文件系统了,如下图(格式化前 318 勿忘备份!)。 DS1302 应用——RTC 我使用的是 Sandisk Cruzer 系列的 4G U 盘,按照程序要求,先在 U 盘根目录中创建一 个文件夹命名为 C51,然后在 C51 文件夹中新建一个.c 文件命名为 CH376HFT.c,如下图所示。 在 CH376HFT.c 中写入一些内容,不少于 200 字,这里我写入了一长段全 a,如下图(眼 不要花掉哈)。 319 DS1302 应用——RTC 准备工作就绪,我们先不插入 U 盘,直接在 TIGER BOARD 上运行 USB HOST 程序,这 时候在状态区会不断地打印 USB FAILURE,表示没检测到设备,这就对了嘛,本来就没插上 U 盘,检测出来才怪。 然后把 U 盘插上,这时 CH376 芯片旁边的指示灯会亮起来,表示 USB 正在连接,此时状 态区会打印出以下信息,显示的 U 盘信息与读出的文件内容与预期一致,表明实验成功了。 大家也可以试试其它的功能:如果找不到原文件 CH376HFT.C, 那么该程序将显示 C51 子 目录下所有以 CH376 开头的文件名,如果找不到 C51 子目录, 那么该程序将显示根目录下的 320 DS1302 应用——RTC 所有文件名。看看是否能得到预期效果。  SD HOST 调试 在 SD HOST 软件实现部分已经提到:因为在 CH376 中,检测设备连接的命令不支持 SD 卡,并且在 TIGER BOARD 上,SD 卡能用来检测插拨的引脚 3(下图的 VSS)并未连接到 FPGA 上而是直接接地,所以我们的实验是假设 SD 卡已经插好的。 我使用的是 ADATA 的 2G SD 卡,使用前同样确保 SD 卡的文件系统为 CH376 支持。这 次换个玩法,在 SD 卡中新建三个 txt 文件,分别命名为 CH376A.txt、CH376B.txt 和 CH376C.txt,如下图所示。 准备工作完毕,先把 SD 卡插到 TIGER BOARD 上(勿忘!),然后运行 SD HOST 工程, 状态区打印出来下图所示的信息,可以看到 SD 卡的厂商信息未被识别(估计只能识别 U 盘), 列出了我们新建的三个文件,表明实验成功。 321 DS1302 应用——RTC 上面就是三个功能的调试过程,希望能对大家实现 USB 的三个功能有所帮助。 本章硬件实现部分没有新鲜内容,添加的仍然是已经被用“烂”的 PIO 和上一章使用过的 SPI 总线,要注意 SPI 总线的片选线是另添加 PIO 模拟而没有用 SPI 模块自带片选信号。在软 件实现中,我们借助了芯片厂商的驱动程序,通过对芯片的了解,实现了 USB DEVICE、USB HOST 和 SD HOST 三个功能软件编程。本章的内容多而复杂,堪称奇幻漂流以来最难的一章, 唯一能与之匹敌的也就下一章的 LAN 了。没办法,想要从高手变为大神,这点苦难是必经过 程,经历过这些,才能真正的成长!这一章的内容就到这里,因本人水平有限,文中有任何错 误请联系我,大家在交流中进步!邮箱:vito943@qq.com 附录: file_sys.h、ch376inc.h、file_sys.c 源代码下载链接 322 在 NiosII 中使用以太网 第19章 Nios II 使用以太网—— LAN 本章主要介绍在 Nios II 中利用 ENC28J60 芯片实现 LAN 功能,通过本章,你能学到 (1)在 Nios II 中利用 ENC28J60 实现 LAN。 (2)TCP&UDP 测试工具的使用。 本章分为四个部分: 一、概述 二、硬件实现 三、软件实现 四、实验总结 更多章节,请访问 http://www.tigerbd.cn/forum-39-1.html 323 概述 19.1 概述 这个时代是属于互联网的时代,网络已经深入到每个人的生活,如同空气、水、食物般不 可或缺。虽然软件工程师在虚拟世界中大放光彩,风头正盛,但各位学习硬件的同学无需担心, 无论互联网再怎么发展,永远是建立在硬件基础之上的,就像我们今天要实现的 LAN(Local Area Network,局域网)功能,没有 ENC28J60 这样的网络芯片、没有 Nios II 这样的 CPU、 没有嵌入式工程师编写的底层驱动,哪里来的局域网? TIGER BOARD 上配有网络芯片 ENC28J60,它是 Microchip 公司推出的带 SPI 接口的独 立以太网控制器,我们利用 Nios II CPU 来驱动它,实现 LAN 功能:在 TCP/IP 协议和 UDP 协议下实现 TIGER BOARD 与 PC 机的通信,下面就来搞定 LAN。 小贴士 Tips:说说区别——局域网与以太网 局域网和以太网是两个经常被提起的概念,可它们的含义并不相同。局域网是指在某一 区域内由多台计算机互联成的计算机组,一般是方圆几千米以内,它与广域网(WAN)、 城域网(MAN)等概念并列,大家可以理解为一种组网规模。而以太网(Ethernet)是 一种局域网的实现技术,也是最为通用的技术,此外还有令牌环网、FDDI 网和无线局 域网等局域网技术。可以看出,局域网与以太网是一种包含关系,以太网必然属于局域 网,所以尽管 ENC28J60 是以太网控制芯片,我们把它实现的功能称为更为熟悉的 LAN 是没有任何问题的。 324 硬件实现 19.2 硬件实现 首先来看看 ENJ28C60 芯片的原理图,如下图所示。标红的管脚为芯片与 FPGA 相连的管 脚,CPU 通过控制这些管脚来实现功能。其中 SPI 总线的 4 根线已经添加完成了(见 TFT 应 用 2),剩下的 LAN_nINT(中断输出)、LAN_nWOL(中断唤醒)和 LAN_nRST(复位)三 个引脚,我通过 PIO 来控制它们。 打开 Qsys,在工程中添加三个 PIO 模块,两个设为输入(对应 LAN_nINT 和 LAN_nWOL), 一个设为输出(对应 LAN_nRST),位宽都为 1,其它设置不变,添加完成后改名、连线、端口 引出、分配地址,完成后得到下图。 325 硬件实现 完成上述操作后保存并点击 Genrate 编译 Qsys。然后打开 Quartus II,在顶层 bdf 文件 中更新 kernel 得到新添加的 IO 端口,将它生成管脚、分配管脚,完成后得到下图。 完成后编译 Quartus II 工程,硬件实现部分就完成了。 326 软件实现 19.3 软件实现 下面来完成软件实现。LAN 的软件实现比较复杂,功能为实现开发板与 PC 机的 TCP/IP 协议和 UDP 协议的以太网通信。由于程序内容较多,程序就不详细解释了,这里主要以注释 的形式供大家理解。 首先打开 Nios II,新建一个工程命名为 lan,将目录区整理成三文件夹结构。先来预览下 我们要编写的程序文件列表及对应功能,如下图所示。 在 inc 文件夹中新建五个.h 头文件分别命名为 sopc.h、enc28j60.h、ip_arp_udp_tcp.h、 net.h 和 simple_server.h,分别编写。 sopc.h 文件为 SOPC 头文件,编写完成后得到下图。 #include "system.h" #include "sys/alt_irq.h" #include #include #include #include #ifndef SOPC_H_ #define SOPC_H_ #define _LED #define _LAN_NRST #define _LAN 327 typedef struct{ unsigned long int DATA; unsigned long int DIRECTION; unsigned long int INTERRUPT_MASK; unsigned long int EDGE_CAPTURE; }PIO_STR; typedef struct{ volatile unsigned long int RXDATA; volatile unsigned long int TXDATA; union{ struct{ volatile unsigned long int NC :3; volatile unsigned long int ROE :1; volatile unsigned long int TOE :1; volatile unsigned long int TMT :1; volatile unsigned long int TRDY :1; volatile unsigned long int RRDY :1; volatile unsigned long int E :1; volatile unsigned long int NC1 :23; }BITS; volatile unsigned long int WORD; }STATUS; union{ struct{ volatile unsigned long int NC :3; volatile unsigned long int IROE :1; volatile unsigned long int ITOE :1; volatile unsigned long int NC1 :1; volatile unsigned long int ITRDY :1; volatile unsigned long int IRRDY :1; volatile unsigned long int IE :1; volatile unsigned long int NC2 :1; volatile unsigned long int SSO :21; }BITS; volatile unsigned long int CONTROL; }CONTROL; unsigned long int RESERVED0; unsigned long int SLAVE_SELECT; }SPI_STR; #ifdef _LED #define LED ((PIO_STR *)LAN_LED_BASE) #endif #ifdef _LAN_NRST #define LAN_NRST ((PIO_STR *)LAN_NRST_BASE) #endif #ifdef _LAN #define LAN #define LAN_CS #define LAN_NINT ((SPI_STR *)SPI_BASE) ((PIO_STR *) LAN_NCS_BASE) ((PIO_STR *) LAN_NINT_BASE) 328 软件实现 软件实现 #endif #endif /*SOPC_H_*/ enc28j60.h 为 ENC28J60 芯片的驱动程序头文件,主要定义了 ENC28J60 驱动程序中用 到的一些参数和函数,编写完成后得到下图。 #include "../inc/sopc.h" #ifndef ENC28J60_H_ #define ENC28J60_H_ // ENC28J60 Control Registers // Control register definitions are a combination of address, // bank number, and Ethernet/MAC/PHY indicator bits. // - Register address (bits 0-4) // - Bank number (bits 5-6) // - MAC/PHY indicator (bit 7) #define ADDR_MASK 0x1F #define BANK_MASK #define SPRD_MASK 0x60 0x80 // All-bank registers #define EIE 0x1B #define EIR 0x1C #define ESTAT 0x1D #define ECON2 #define ECON1 0x1E 0x1F // Bank 0 registers #define ERDPTL #define ERDPTH #define EWRPTL #define EWRPTH #define ETXSTL #define ETXSTH #define ETXNDL #define ETXNDH #define ERXSTL #define ERXSTH #define ERXNDL #define ERXNDH (0x00|0x00) (0x01|0x00) (0x02|0x00) (0x03|0x00) (0x04|0x00) (0x05|0x00) (0x06|0x00) (0x07|0x00) (0x08|0x00) (0x09|0x00) (0x0A|0x00) (0x0B|0x00) //ERXWRPTH:ERXWRPTL 寄存器定义硬件向FIFO 中 //的哪个位置写入其接收到的字节。 指针是只读的,在成 //功接收到一个数据包后,硬件会自动更新指针。 指针可 //用于判断FIFO 内剩余空间的大小。 #define ERXRDPTL #define ERXRDPTH (0x0C|0x00) (0x0D|0x00) #define ERXWRPTL (0x0E|0x00) #define ERXWRPTH #define EDMASTL (0x0F|0x00) (0x10|0x00) #define EDMASTH (0x11|0x00) #define EDMANDL #define EDMANDH (0x12|0x00) (0x13|0x00) 329 #define EDMADSTL #define EDMADSTH #define EDMACSL #define EDMACSH // Bank 1 registers #define EHT0 #define EHT1 #define EHT2 #define EHT3 #define EHT4 #define EHT5 #define EHT6 #define EHT7 #define EPMM0 #define EPMM1 #define EPMM2 #define EPMM3 #define EPMM4 #define EPMM5 #define EPMM6 #define EPMM7 #define EPMCSL #define EPMCSH #define EPMOL #define EPMOH #define EWOLIE #define EWOLIR #define ERXFCON #define EPKTCNT // Bank 2 registers #define MACON1 #define MACON2 #define MACON3 #define MACON4 #define MABBIPG #define MAIPGL #define MAIPGH #define MACLCON1 #define MACLCON2 #define MAMXFLL #define MAMXFLH #define MAPHSUP #define MICON #define MICMD #define MIREGADR #define MIWRL #define MIWRH #define MIRDL #define MIRDH // Bank 3 registers #define MAADR1 #define MAADR0 #define MAADR3 #define MAADR2 #define MAADR5 #define MAADR4 (0x14|0x00) (0x15|0x00) (0x16|0x00) (0x17|0x00) (0x00|0x20) (0x01|0x20) (0x02|0x20) (0x03|0x20) (0x04|0x20) (0x05|0x20) (0x06|0x20) (0x07|0x20) (0x08|0x20) (0x09|0x20) (0x0A|0x20) (0x0B|0x20) (0x0C|0x20) (0x0D|0x20) (0x0E|0x20) (0x0F|0x20) (0x10|0x20) (0x11|0x20) (0x14|0x20) (0x15|0x20) (0x16|0x20) (0x17|0x20) (0x18|0x20) (0x19|0x20) (0x00|0x40|0x80) (0x01|0x40|0x80) (0x02|0x40|0x80) (0x03|0x40|0x80) (0x04|0x40|0x80) (0x06|0x40|0x80) (0x07|0x40|0x80) (0x08|0x40|0x80) (0x09|0x40|0x80) (0x0A|0x40|0x80) (0x0B|0x40|0x80) (0x0D|0x40|0x80) (0x11|0x40|0x80) (0x12|0x40|0x80) (0x14|0x40|0x80) (0x16|0x40|0x80) (0x17|0x40|0x80) (0x18|0x40|0x80) (0x19|0x40|0x80) (0x00|0x60|0x80) (0x01|0x60|0x80) (0x02|0x60|0x80) (0x03|0x60|0x80) (0x04|0x60|0x80) (0x05|0x60|0x80) 330 软件实现 #define EBSTSD #define EBSTCON #define EBSTCSL #define EBSTCSH #define MISTAT #define EREVID #define ECOCON #define EFLOCON #define EPAUSL #define EPAUSH // PHY registers #define PHCON1 #define PHSTAT1 #define PHHID1 #define PHHID2 #define PHCON2 #define PHSTAT2 #define PHIE #define PHIR #define PHLCON (0x06|0x60) (0x07|0x60) (0x08|0x60) (0x09|0x60) (0x0A|0x60|0x80) (0x12|0x60) (0x15|0x60) (0x17|0x60) (0x18|0x60) (0x19|0x60) 0x00 0x01 0x02 0x03 0x10 0x11 0x12 0x13 0x14 // ENC28J60 ERXFCON Register Bit Definitions #define ERXFCON_UCEN 0x80 #define ERXFCON_ANDOR 0x40 #define ERXFCON_CRCEN 0x20 #define ERXFCON_PMEN 0x10 #define ERXFCON_MPEN 0x08 #define ERXFCON_HTEN 0x04 #define ERXFCON_MCEN 0x02 #define ERXFCON_BCEN 0x01 // ENC28J60 EIE Register Bit Definitions #define EIE_INTIE 0x80 #define EIE_PKTIE 0x40 #define EIE_DMAIE 0x20 #define EIE_LINKIE 0x10 #define EIE_TXIE 0x08 #define EIE_WOLIE 0x04 #define EIE_TXERIE 0x02 #define EIE_RXERIE 0x01 // ENC28J60 EIR Register Bit Definitions #define EIR_PKTIF 0x40 #define EIR_DMAIF 0x20 #define EIR_LINKIF 0x10 #define EIR_TXIF 0x08 #define EIR_WOLIF 0x04 #define EIR_TXERIF 0x02 #define EIR_RXERIF 0x01 // ENC28J60 ESTAT Register Bit Definitions #define ESTAT_INT 0x80 #define ESTAT_LATECOL 0x10 #define ESTAT_RXBUSY 0x04 #define ESTAT_TXABRT 0x02 #define ESTAT_CLKRDY 0x01 // ENC28J60 ECON2 Register Bit Definitions #define ECON2_AUTOINC 0x80 #define ECON2_PKTDEC 0x40 331 软件实现 #define ECON2_PWRSV 0x20 #define ECON2_VRPS 0x08 // ENC28J60 ECON1 Register Bit Definitions #define ECON1_TXRST 0x80 #define ECON1_RXRST 0x40 #define ECON1_DMAST 0x20 #define ECON1_CSUMEN 0x10 #define ECON1_TXRTS 0x08 #define ECON1_RXEN 0x04 #define ECON1_BSEL1 0x02 #define ECON1_BSEL0 0x01 // ENC28J60 MACON1 Register Bit Definitions #define MACON1_LOOPBK 0x10 #define MACON1_TXPAUS 0x08 #define MACON1_RXPAUS 0x04 #define MACON1_PASSALL 0x02 #define MACON1_MARXEN 0x01 // ENC28J60 MACON2 Register Bit Definitions #define MACON2_MARST 0x80 #define MACON2_RNDRST 0x40 #define MACON2_MARXRST 0x08 #define MACON2_RFUNRST 0x04 #define MACON2_MATXRST 0x02 #define MACON2_TFUNRST 0x01 // ENC28J60 MACON3 Register Bit Definitions #define MACON3_PADCFG2 0x80 #define MACON3_PADCFG1 0x40 #define MACON3_PADCFG0 0x20 #define MACON3_TXCRCEN 0x10 #define MACON3_PHDRLEN 0x08 #define MACON3_HFRMLEN 0x04 #define MACON3_FRMLNEN 0x02 #define MACON3_FULDPX 0x01 // ENC28J60 MICMD Register Bit Definitions #define MICMD_MIISCAN 0x02 #define MICMD_MIIRD 0x01 // ENC28J60 MISTAT Register Bit Definitions #define MISTAT_NVALID 0x04 #define MISTAT_SCAN 0x02 #define MISTAT_BUSY 0x01 // ENC28J60 PHY PHCON1 Register Bit Definitions #define PHCON1_PRST 0x8000 #define PHCON1_PLOOPBK 0x4000 #define PHCON1_PPWRSV 0x0800 #define PHCON1_PDPXMD 0x0100 // ENC28J60 PHY PHSTAT1 Register Bit Definitions #define PHSTAT1_PFDPX 0x1000 #define PHSTAT1_PHDPX 0x0800 #define PHSTAT1_LLSTAT 0x0004 #define PHSTAT1_JBSTAT 0x0002 // ENC28J60 PHY PHCON2 Register Bit Definitions #define PHCON2_FRCLINK 0x4000 #define PHCON2_TXDIS 0x2000 #define PHCON2_JABBER 0x0400 #define PHCON2_HDLDIS 0x0100 332 软件实现 软件实现 // ENC28J60 Packet Control Byte Bit Definitions #define PKTCTRL_PHUGEEN 0x08 #define PKTCTRL_PPADEN 0x04 #define PKTCTRL_PCRCEN 0x02 #define PKTCTRL_POVERRIDE 0x01 // SPI operation codes #define ENC28J60_READ_CTRL_REG #define ENC28J60_READ_BUF_MEM #define ENC28J60_WRITE_CTRL_REG #define ENC28J60_WRITE_BUF_MEM #define ENC28J60_BIT_FIELD_SET #define ENC28J60_BIT_FIELD_CLR #define ENC28J60_SOFT_RESET 0x00 0x3A 0x40 0x7A 0x80 0xA0 0xFF // The RXSTART_INIT should be zero. See Rev. B4 Silicon Errata // buffer boundaries applied to internal 8K ram // the entire available packet buffer space is allocated // // start with recbuf at 0/ #define RXSTART_INIT 0x0 // receive buffer end #define RXSTOP_INIT (0x1FFF-0x0600-1) // start TX buffer at 0x1FFF-0x0600, pace for one full ethernet frame (~1500 bytes) #define TXSTART_INIT (0x1FFF-0x0600) // stp TX buffer at end of mem #define TXSTOP_INIT 0x1FFF // // max frame length which the conroller will accept: #define MAX_FRAMELEN 1500 // (note: maximum ethernet frame length would be 1518) //#define MAX_FRAMELEN 600 #define #define ENC28J60_CSH() ENC28J60_CSL() LAN_CS->DATA = 1 LAN_CS->DATA = 0 //SPI1初始化 //void ENC28J60_Init(void); unsigned char enc28j60ReadOp(unsigned char op, unsigned char address); void enc28j60WriteOp(unsigned char op, unsigned char address, unsigned char data); void enc28j60ReadBuffer(unsigned int len, unsigned char* data); void enc28j60WriteBuffer(unsigned int len, unsigned char* data); void enc28j60SetBank(unsigned char address); unsigned char enc28j60Read(unsigned char address); void enc28j60Write(unsigned char address, unsigned char data); void enc28j60PhyWrite(unsigned char address, unsigned int data); void enc28j60clkout(unsigned char clk); void enc28j60Init(unsigned char* macaddr); unsigned char enc28j60getrev(void); void enc28j60PacketSend(unsigned int len, unsigned char* packet); unsigned int enc28j60PacketReceive(unsigned int maxlen, unsigned char* packet); 333 软件实现 //SPI1读写一字节数据 //INT8U ENC28J60_ReadWrite(INT8U writedat); #endif ip_arp_udp_tcp.h 为通信协议头文件,主要声明了通信协议文件中定义的函数,编写完成 生得到下图。 #ifndef IP_ARP_UDP_TCP_H_ #define IP_ARP_UDP_TCP_H_ // you must call this function once before you use any of the other functions: extern void init_ip_arp_udp_tcp(unsigned char *mymac,unsigned char *myip,unsigned char wwwp); // extern unsigned char eth_type_is_arp_and_my_ip(unsigned char *buf,unsigned int len); extern unsigned char eth_type_is_ip_and_my_ip(unsigned char *buf,unsigned int len); extern void make_arp_answer_from_request(unsigned char *buf); extern void make_echo_reply_from_request(unsigned char *buf,unsigned int len); extern void make_udp_reply_from_request(unsigned char *buf,char *data,unsigned int datalen,unsigned int port); extern void make_tcp_synack_from_syn(unsigned char *buf); extern void init_len_info(unsigned char *buf); extern unsigned int get_tcp_data_pointer(void); extern unsigned int fill_tcp_data_p(unsigned char *buf,unsigned int pos, const unsigned char *progmem_s); extern unsigned int fill_tcp_data(unsigned char *buf,unsigned int pos, const char *s); extern void make_tcp_ack_from_any(unsigned char *buf); extern void make_tcp_ack_with_data(unsigned char *buf,unsigned int dlen); #endif /* IP_ARP_UDP_TCP_H */ //@} net.h 定义了一些网络参数,也是实现以太网通信的所必须的,编写完成后得到下图。 //@{ #ifndef NET_H_ #define NET_H_ // ******* ETH ******* #define ETH_HEADER_LEN 14 // values of certain bytes: #define ETHTYPE_ARP_H_V 0x08 #define ETHTYPE_ARP_L_V 0x06 #define ETHTYPE_IP_H_V 0x08 #define ETHTYPE_IP_L_V 0x00 // byte positions in the ethernet frame: 334 // // Ethernet type field (2bytes): #define ETH_TYPE_H_P 12 #define ETH_TYPE_L_P 13 // #define ETH_DST_MAC 0 #define ETH_SRC_MAC 6 // ******* ARP ******* #define ETH_ARP_OPCODE_REPLY_H_V 0x0 #define ETH_ARP_OPCODE_REPLY_L_V 0x02 // #define ETHTYPE_ARP_L_V 0x06 // arp.dst.ip #define ETH_ARP_DST_IP_P 0x26 // arp.opcode #define ETH_ARP_OPCODE_H_P 0x14 #define ETH_ARP_OPCODE_L_P 0x15 // arp.src.mac #define ETH_ARP_SRC_MAC_P 0x16 #define ETH_ARP_SRC_IP_P 0x1c #define ETH_ARP_DST_MAC_P 0x20 #define ETH_ARP_DST_IP_P 0x26 // ******* IP ******* #define IP_HEADER_LEN 20 // ip.src #define IP_SRC_P 0x1a #define IP_DST_P 0x1e #define IP_HEADER_LEN_VER_P 0xe #define IP_CHECKSUM_P 0x18 #define IP_TTL_P 0x16 #define IP_FLAGS_P 0x14 #define IP_P 0xe #define IP_TOTLEN_H_P 0x10 #define IP_TOTLEN_L_P 0x11 #define IP_PROTO_P 0x17 #define IP_PROTO_ICMP_V 1 #define IP_PROTO_TCP_V 6 // 17=0x11 #define IP_PROTO_UDP_V 17 // ******* ICMP ******* #define ICMP_TYPE_ECHOREPLY_V 0 #define ICMP_TYPE_ECHOREQUEST_V 8 // #define ICMP_TYPE_P 0x22 #define ICMP_CHECKSUM_P 0x24 // ******* UDP ******* #define UDP_HEADER_LEN 8 // #define UDP_SRC_PORT_H_P 0x22 335 软件实现 软件实现 #define UDP_SRC_PORT_L_P 0x23 #define UDP_DST_PORT_H_P 0x24 #define UDP_DST_PORT_L_P 0x25 // #define UDP_LEN_H_P 0x26 #define UDP_LEN_L_P 0x27 #define UDP_CHECKSUM_H_P 0x28 #define UDP_CHECKSUM_L_P 0x29 #define UDP_DATA_P 0x2a // ******* TCP ******* #define TCP_SRC_PORT_H_P 0x22 #define TCP_SRC_PORT_L_P 0x23 #define TCP_DST_PORT_H_P 0x24 #define TCP_DST_PORT_L_P 0x25 // the tcp seq number is 4 bytes 0x26-0x29 #define TCP_SEQ_H_P 0x26 #define TCP_SEQACK_H_P 0x2a // flags: SYN=2 #define TCP_FLAGS_P 0x2f #define TCP_FLAGS_SYN_V 2 #define TCP_FLAGS_FIN_V 1 #define TCP_FLAGS_PUSH_V 8 #define TCP_FLAGS_SYNACK_V 0x12 #define TCP_FLAGS_ACK_V 0x10 #define TCP_FLAGS_PSHACK_V 0x18 // plain len without the options: #define TCP_HEADER_LEN_PLAIN 20 #define TCP_HEADER_LEN_P 0x2e #define TCP_CHECKSUM_H_P 0x32 #define TCP_CHECKSUM_L_P 0x33 #define TCP_OPTIONS_P 0x36 // #endif //@} simple_server.h 为以太网通信程序头文件,就声明了以太网通信函数 simple_server(), 灰常简单,编写完成后得到下图。 #ifndef _TCPIP_H #define _TCPIP_H #include "ENC28J60.h" int simple_server(void); #endif /*SIMPLE_SERVER_H_*/ 在 driver 文件夹中新建三个.c 源文件分别命名为 enc28j60.c、ip_arp_udp_tcp.c 和 simple_server.c,分别编写。 enc28j60.c 为 ENC28J60 芯片的驱动程序,编写完成后得到下图。 #include "../inc/enc28j60.h" 336 软件实现 static unsigned char Enc28j60Bank; static unsigned int NextPacketPtr; unsigned char enc28j60ReadOp(unsigned char op, unsigned char address) { unsigned char dat = 0; ENC28J60_CSL(); LAN->TXDATA = op | (address & ADDR_MASK); while(!(LAN->STATUS.BITS.TMT)); LAN->TXDATA = 0x00; //0x00 is random number ,to enable clock while(!(LAN->STATUS.BITS.TMT)); if(address&0x80){ LAN->TXDATA = 0x00; while(!(LAN->STATUS.BITS.TMT)); //The first byte that MAC and MII registers read is invalid,so they need to read twice } dat = LAN->RXDATA; // release CS ENC28J60_CSH(); return dat; } void enc28j60WriteOp(unsigned char op, unsigned char address, unsigned char data) { ENC28J60_CSL(); LAN->TXDATA = (op | (address & ADDR_MASK)); // write command while(!(LAN->STATUS.BITS.TMT)); LAN->TXDATA = data; // write data while(!(LAN->STATUS.BITS.TMT)); ENC28J60_CSH(); } void enc28j60ReadBuffer(unsigned int len, unsigned char* data) { ENC28J60_CSL(); LAN->TXDATA = ENC28J60_READ_BUF_MEM; while(!(LAN->STATUS.BITS.TMT)); while(len--){ LAN->TXDATA = 0x00; while(!(LAN->STATUS.BITS.TMT)); while(!(LAN->STATUS.BITS.RRDY)); *data++=LAN->RXDATA; } 337 软件实现 *data='\0'; ENC28J60_CSH(); } void enc28j60WriteBuffer(unsigned int len, unsigned char* data) { ENC28J60_CSL(); // issue write command LAN->TXDATA = ENC28J60_WRITE_BUF_MEM; while(!(LAN->STATUS.BITS.TMT)); while(len--){ LAN->TXDATA = *data++; while(!(LAN->STATUS.BITS.TMT)); } ENC28J60_CSH(); } void enc28j60SetBank(unsigned char address) { // set the bank (if needed) if((address & BANK_MASK) != Enc28j60Bank) { // set the bank enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, ECON1, (ECON1_BSEL1|ECON1_BSEL0)); enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, (address & BANK_MASK)>>5); Enc28j60Bank = (address & BANK_MASK); } } unsigned char enc28j60Read(unsigned char address) { // set the bank enc28j60SetBank(address); // do the read return enc28j60ReadOp(ENC28J60_READ_CTRL_REG, address); } void enc28j60Write(unsigned char address, unsigned char data) { // set the bank enc28j60SetBank(address); // do the write enc28j60WriteOp(ENC28J60_WRITE_CTRL_REG, address, data); } void enc28j60PhyWrite(unsigned char address, unsigned int data) { // set the PHY register address enc28j60Write(MIREGADR, address); // write the PHY data enc28j60Write(MIWRL, data); 338 软件实现 enc28j60Write(MIWRH, data>>8); // wait until the PHY write completes while(enc28j60Read(MISTAT) & MISTAT_BUSY); } void enc28j60clkout(unsigned char clk) { //setup clkout: 2 is 12.5MHz: enc28j60Write(ECOCON, clk & 0x7); } void enc28j60Init(unsigned char* macaddr) { /*将ENC28J60的SPI NSS信号置高*/ ENC28J60_CSH(); /*软件复位ENC28J60*/ enc28j60WriteOp(ENC28J60_SOFT_RESET, 0, ENC28J60_SOFT_RESET); //设置接收缓冲区地址 8K字节容量 NextPacketPtr = RXSTART_INIT; //接收缓冲器由一个硬件管理的循环FIFO 缓冲器构成。寄存器对ERXSTH:ERXSTL 和 ERXNDH:ERXNDL 作为指针,定义 //缓冲器的容量和其在存储器中的位置。ERXST和ERXND指向的字节均包含在FIFO缓冲器内。 当从以太网接口接收数据 //字节时,这些字节被顺序写入接收缓冲器。 但是当写入由ERXND 指向的存储单元后,硬件会 自动将接收的下一 //字节写入由ERXST 指向的存储单元。 因此接收硬件将不会写入FIFO 以外的单元。 enc28j60Write(ERXSTL, RXSTART_INIT&0xFF); enc28j60Write(ERXSTH, RXSTART_INIT>>8); // set receive pointer address //ERXWRPTH:ERXWRPTL 寄存器定义硬件向FIFO 中的哪个位置写入其接收到的字节。 指针是 只读的,在成 //功接收到一个数据包后,硬件会自动更新指针。 指针可用于判断FIFO 内剩余空间的大小 8K-1500。 enc28j60Write(ERXRDPTL, RXSTART_INIT&0xFF); enc28j60Write(ERXRDPTH, RXSTART_INIT>>8); // RX end enc28j60Write(ERXNDL, RXSTOP_INIT&0xFF); enc28j60Write(ERXNDH, RXSTOP_INIT>>8); // TX start 1500 enc28j60Write(ETXSTL, TXSTART_INIT&0xFF); enc28j60Write(ETXSTH, TXSTART_INIT>>8); // TX end enc28j60Write(ETXNDL, TXSTOP_INIT&0xFF); 339 软件实现 enc28j60Write(ETXNDH, TXSTOP_INIT>>8); // do bank 1 stuff, packet filter: // For broadcast packets we allow only ARP packtets // All other packets should be unicast only for our mac (MAADR) // // The pattern to match on is therefore // Type ETH.DST // ARP BROADCAST // 06 08 -- ff ff ff ff ff ff -> ip checksum for theses bytes=f7f9 // in binary these poitions are:11 0000 0011 1111 // This is hex 303F->EPMM0=0x3f,EPMM1=0x30 //接收过滤器 //UCEN:单播过滤器使能位 // 当ANDOR = 1 时: // 1= 目标地址与本地MAC 地址不匹配的数据包将被丢弃 // 0= 禁止过滤器 // 当ANDOR = 0 时: // 1= 目标地址与本地MAC 地址匹配的数据包会被接受 // 0 = 禁止过滤器 //CRCEN:后过滤器CRC 校验使能位 // 1 = 所有CRC 无效的数据包都将被丢弃 // 0 = 不考虑CRC 是否有效 // PMEN:格式匹配过滤器使能位 // 当ANDOR = 1 时: // 1 = 数据包必须符合格式匹配条件,否则将被丢弃 // 0 = 禁止过滤器 // 当ANDOR = 0 时: // 1 = 符合格式匹配条件的数据包将被接受 // 0 = 禁止过滤器 enc28j60Write(ERXFCON, ERXFCON_UCEN|ERXFCON_CRCEN|ERXFCON_PMEN); enc28j60Write(EPMM0, 0x3f); enc28j60Write(EPMM1, 0x30); enc28j60Write(EPMCSL, 0xf9); enc28j60Write(EPMCSH, 0xf7); // do bank 2 stuff // enable MAC receive //bit 0 MARXEN:MAC 接收使能位 // 1= 允许MAC 接收数据包 // 0 = 禁止数据包接收 //bit 3 TXPAUS:暂停控制帧发送使能位 // 1= 允许MAC 发送暂停控制帧(用于全双工模式下的流量控制) 340 软件实现 //0 = 禁止暂停帧发送 //bit 2 RXPAUS:暂停控制帧接收使能位 // 1 = 当接收到暂停控制帧时,禁止发送(正常操作) // 0 = 忽略接收到的暂停控制帧 enc28j60Write(MACON1, MACON1_MARXEN|MACON1_TXPAUS|MACON1_RXPAUS); // bring MAC out of reset //将MACON2 中的MARST 位清零,使MAC 退出复位状态。 enc28j60Write(MACON2, 0x00); // enable automatic padding to 60bytes and CRC operations //bit 7-5 PADCFG2:PACDFG0:自动填充和CRC 配置位 //111 = 用0 填充所有短帧至64 字节长,并追加一个有效的CRC //110 = 不自动填充短帧 //101 = MAC 自动检测具有8100h 类型字段的VLAN 协议帧,并自动填充到64 字节长。 如果不 //是VLAN 帧,则填充至60 字节长。填充后还要追加一个有效的CRC //100 = 不自动填充短帧 //011 = 用0 填充所有短帧至64 字节长,并追加一个有效的CRC //010 = 不自动填充短帧 //001 = 用0 填充所有短帧至60 字节长,并追加一个有效的CRC //000 = 不自动填充短帧 //bit 4 TXCRCEN:发送CRC 使能位 // 1= 不管PADCFG如何,MAC都会在发送帧的末尾追加一个有效的CRC。 如果PADCFG规 定要 //追加有效的CRC,则必须将TXCRCEN 置1。 // 0 = MAC不会追加CRC。 检查最后4 个字节,如果不是有效的CRC 则报告给发送状态 向量。 //bit 0 FULDPX:MAC 全双工使能位 // 1= MAC工作在全双工模式下。 PHCON1.PDPXMD 位必须置1。 // 0 = MAC工作在半双工模式下。 PHCON1.PDPXMD 位必须清零。 enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, MACON3, MACON3_PADCFG0|MACON3_TXCRCEN|MACON3_FRMLNEN|MACON3_FULDPX); // set inter-frame gap (non-back-to-back) //配置非背对背包间间隔寄存器的低字节MAIPGL。 大多数应用使用12h 编程该寄存器。 //如果使用半双工模式,应编程非背对背包间间隔寄存器的高字节MAIPGH。 大多数应用使用 0Ch //编程该寄存器。 enc28j60Write(MAIPGL, 0x12); enc28j60Write(MAIPGH, 0x0C); // set inter-frame gap (back-to-back) //配置背对背包间间隔寄存器MABBIPG。当使用全双工模式时,大多数应用使用15h 编程该寄 341 软件实现 存 //器,而使用半双工模式时则使用12h 进行编程。 enc28j60Write(MABBIPG, 0x15); // Set the maximum packet size which the controller will accept // Do not send packets longer than MAX_FRAMELEN: // 最大帧长度 1500 enc28j60Write(MAMXFLL, MAX_FRAMELEN&0xFF); enc28j60Write(MAMXFLH, MAX_FRAMELEN>>8); // write MAC address // NOTE: MAC address in ENC28J60 is byte-backward enc28j60Write(MAADR5, macaddr[0]); enc28j60Write(MAADR4, macaddr[1]); enc28j60Write(MAADR3, macaddr[2]); enc28j60Write(MAADR2, macaddr[3]); enc28j60Write(MAADR1, macaddr[4]); enc28j60Write(MAADR0, macaddr[5]); //配置PHY为全双工 LEDB为拉电流 enc28j60PhyWrite(PHCON1, PHCON1_PDPXMD); // no loopback of transmitted frames 禁止环回 //HDLDIS:PHY 半双工环回禁止位 //当PHCON1.PDPXMD = 1 或PHCON1.PLOOPBK = 1 时: //此位可被忽略。 //当PHCON1.PDPXMD = 0 且PHCON1.PLOOPBK = 0 时: // 1 = 要发送的数据仅通过双绞线接口发出 // 0 = 要发送的数据会环回到MAC 并通过双绞线接口发出 enc28j60PhyWrite(PHCON2, PHCON2_HDLDIS); // switch to bank 0 //ECON1 寄存器 //寄存器3-1 所示为ECON1 寄存器,它用于控制 //ENC28J60 的主要功能。 ECON1 中包含接收使能、发 //送请求、DMA 控制和存储区选择位。 enc28j60SetBank(ECON1); // enable interrutps //EIE: 以太网中断允许寄存器 //bit 7 INTIE: 全局INT 中断允许位 // 1 = 允许中断事件驱动INT 引脚 // 0 = 禁止所有INT 引脚的活动(引脚始终被驱动为高电平) //bit 6 PKTIE: 接收数据包待处理中断允许位 // 1 = 允许接收数据包待处理中断 342 软件实现 // 0 = 禁止接收数据包待处理中断 enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, EIE, EIE_INTIE|EIE_PKTIE); // enable packet reception //bit 2 RXEN:接收使能位 // 1 = 通过当前过滤器的数据包将被写入接收缓冲器 //0 = 忽略所有接收的数据包 enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_RXEN); //指示灯状态:0x476 is PHLCON LEDA(绿)=links status, LEDB(红)=receive/transmit //enc28j60PhyWrite(PHLCON,0x7a4); //PHLCON:PHY 模块LED 控制寄存器 enc28j60PhyWrite(PHLCON,0x0476); enc28j60clkout(2); // change clkout from 6.25MHz to 12.5MHz } // read the revision of the chip: unsigned char enc28j60getrev(void) { //在EREVID 内也存储了版本信息。 EREVID 是一个只读控 //制寄存器,包含一个5 位标识符,用来标识器件特定硅片 //的版本号 return(enc28j60Read(EREVID)); } void enc28j60PacketSend(unsigned int len, unsigned char* packet) { // Set the write pointer to start of transmit buffer area enc28j60Write(EWRPTL, TXSTART_INIT&0xFF); enc28j60Write(EWRPTH, TXSTART_INIT>>8); // Set the TXND pointer to correspond to the packet size given enc28j60Write(ETXNDL, (TXSTART_INIT+len)&0xFF); enc28j60Write(ETXNDH, (TXSTART_INIT+len)>>8); // write per-packet control byte (0x00 means use macon3 settings) enc28j60WriteOp(ENC28J60_WRITE_BUF_MEM, 0, 0x00); // copy the packet into the transmit buffer enc28j60WriteBuffer(len, packet); // send the contents of the transmit buffer onto the network enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_TXRTS); // Reset the transmit logic problem. See Rev. B4 Silicon Errata point 12. if( (enc28j60Read(EIR) & EIR_TXERIF) ) { enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_TXRTS); 343 软件实现 } } // Gets a packet from the network receive buffer, if one is available. // The packet will by headed by an ethernet header. // maxlen The maximum acceptable length of a retrieved packet. // packet Pointer where packet data should be stored. // Returns: Packet length in bytes if a packet was retrieved, zero otherwise. unsigned int enc28j60PacketReceive(unsigned int maxlen, unsigned char* packet) { unsigned int rxstat; unsigned int len; // check if a packet has been received and buffered // The above does not work. See Rev. B4 Silicon Errata point 6. if( enc28j60Read(EPKTCNT) ==0 ) //收到的以太网数据包长度 { return(0); } // Set the read pointer to the start of the received packet 指针 enc28j60Write(ERDPTL, (NextPacketPtr)); enc28j60Write(ERDPTH, (NextPacketPtr)>>8); 缓冲器读 // read the next packet pointer NextPacketPtr = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0); NextPacketPtr |= enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0)<<8; // read the packet length (see datasheet page 43) len = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0); len |= enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0)<<8; len-=4; //remove the CRC count // read the receive status (see datasheet page 43) rxstat = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0); rxstat |= enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0)<<8; // limit retrieve length if (len>maxlen-1) { len=maxlen-1; } // check CRC and symbol errors (see datasheet page 44, table 7-3): // The ERXFCON.CRCEN is set by default. Normally we should not // need to check this. if ((rxstat & 0x80)==0) { // invalid len=0; } 344 软件实现 else { // copy the packet from the receive buffer enc28j60ReadBuffer(len, packet); } // Move the RX read pointer to the start of the next received packet // This frees the memory we just read out enc28j60Write(ERXRDPTL, (NextPacketPtr)); enc28j60Write(ERXRDPTH, (NextPacketPtr)>>8); // decrement the packet counter indicate we are done with this packet enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON2, ECON2_PKTDEC); return(len); } ip_arp_udp_tcp.c 为通信协议文件,在该文件中实现了 TCP/IP 通信协议和 UDP 通信协议, 编写完成后得到下图。 //#include #include "../inc/net.h" #include "../inc/ip_arp_udp_tcp.h" #include "../inc/enc28j60.h" #define pgm_read_byte(ptr) ((char)*(ptr)) //#define unsigned char unsigned char //#define unsigned int unisgned int static unsigned char wwwport=80; static unsigned char macaddr[6]; static unsigned char ipaddr[4]; static unsigned int info_hdr_len=0; static unsigned int info_data_len=0; static unsigned char seqnum=0xa; // my initial tcp sequence number // The Ip checksum is calculated over the ip header only starting // with the header length field and a total length of 20 bytes // unitl ip.dst // You must set the IP checksum field to zero before you start // the calculation. // len for ip is 20. // // For UDP/TCP we do not make up the required pseudo header. Instead we // use the ip.src and ip.dst fields of the real packet: // The udp checksum calculation starts with the ip.src field // Ip.src=4bytes,Ip.dst=4 bytes,Udp header=8bytes + data length=16+len // In other words the len here is 8 + length over which you actually // want to calculate the checksum. // You must set the checksum field to zero before you start // the calculation. // len for udp is: 8 + 8 + data length // len for tcp is: 4+4 + 20 + option len + data length 345 软件实现 // // For more information on how this algorithm works see: // http://www.netfor2.com/checksum.html // http://www.msc.uky.edu/ken/cs471/notes/chap3.htm // The RFC has also a C code example: http://www.faqs.org/rfcs/rfc1071.html unsigned int checksum(unsigned char *buf, unsigned int len,unsigned char type){ // type 0=ip // 1=udp // 2=tcp unsigned long sum = 0; //if(type==0){ // // do not add anything //} if(type==1){ sum+=IP_PROTO_UDP_V; // protocol udp // the length here is the length of udp (data+header len) // =length given to this function - (IP.scr+IP.dst length) sum+=len-8; // = real tcp len } if(type==2){ sum+=IP_PROTO_TCP_V; // the length here is the length of tcp (data+header len) // =length given to this function - (IP.scr+IP.dst length) sum+=len-8; // = real tcp len } // build the sum of 16bit words while(len >1){ sum += 0xFFFF & (*buf<<8|*(buf+1)); buf+=2; len-=2; } // if there is a byte left then add it (padded with zero) if (len){ sum += (0xFF & *buf)<<8; } // now calculate the sum over the bytes in the sum // until the result is only 16bit long while (sum>>16){ sum = (sum & 0xFFFF)+(sum >> 16); } // build 1's complement: return( (unsigned int) sum ^ 0xFFFF); } // you must call this function once before you use any of the other functions: void init_ip_arp_udp_tcp(unsigned char *mymac,unsigned char *myip,unsigned char wwwp){ unsigned char i=0; wwwport=wwwp; while(i<4){ ipaddr[i]=myip[i]; i++; } 346 软件实现 i=0; while(i<6){ macaddr[i]=mymac[i]; i++; } } /*--------------------------------------------------------------------------------当收到目的IP为本机IP的ARP包时,返回值为1,否则返回0 ----------------------------------------------------------------------------------*/ unsigned char eth_type_is_arp_and_my_ip(unsigned char *buf,unsigned int len){ unsigned char i=0; //包长度不够,直接返回 if (len<41) return(0); //如果类型不是ARP包,直接返回 if(buf[ETH_TYPE_H_P] != ETHTYPE_ARP_H_V || buf[ETH_TYPE_L_P] != ETHTYPE_ARP_L_V) return(0); //如果ARP包的IP地址与本机IP不一致,直接返回 while(i<4){ if(buf[ETH_ARP_DST_IP_P+i] != ipaddr[i]) return(0); i++; } printf("\n\r收到主机[%d.%d.%d.%d]发送的ARP包 ",buf[ETH_ARP_SRC_IP_P],buf[ETH_ARP_SRC_IP_P+1],buf[ETH_ARP_SRC_IP_P+2],b uf[ETH_ARP_SRC_IP_P+3]); return(1); } unsigned char eth_type_is_ip_and_my_ip(unsigned char *buf,unsigned int len){ unsigned char i=0; //包长度不够,直接返回 if (len<42) return(0); //如果包类型不是IP包,直接返回 if(buf[ETH_TYPE_H_P]!=ETHTYPE_IP_H_V || buf[ETH_TYPE_L_P]!=ETHTYPE_IP_L_V) return(0); //如果长度参数不正确,直接返回 if (buf[IP_HEADER_LEN_VER_P]!=0x45) // must be IP V4 and 20 byte header return(0); //如果IP包的IP地址与本机IP不一致,直接返回 while(i<4){ 347 软件实现 if(buf[IP_DST_P+i]!=ipaddr[i]) return(0); i++; } return(1); } // make a return eth header from a received eth packet void make_eth(unsigned char *buf){ unsigned char i=0; //填写包的目的MAC地址,以及源MAC地址 while(i<6){ buf[ETH_DST_MAC +i]=buf[ETH_SRC_MAC +i]; buf[ETH_SRC_MAC +i]=macaddr[i]; i++; } } void fill_ip_hdr_checksum(unsigned char *buf){ unsigned int ck; // clear the 2 byte checksum buf[IP_CHECKSUM_P]=0; buf[IP_CHECKSUM_P+1]=0; buf[IP_FLAGS_P]=0x40; // don't fragment buf[IP_FLAGS_P+1]=0; // fragement offset buf[IP_TTL_P]=64; // ttl // calculate the checksum: ck=checksum(&buf[IP_P], IP_HEADER_LEN,0); buf[IP_CHECKSUM_P]=ck>>8; buf[IP_CHECKSUM_P+1]=ck& 0xff; } // make a return ip header from a received ip packet void make_ip(unsigned char *buf){ unsigned char i=0; while(i<4){ buf[IP_DST_P+i]=buf[IP_SRC_P+i]; buf[IP_SRC_P+i]=ipaddr[i]; i++; } fill_ip_hdr_checksum(buf); } // make a return tcp header from a received tcp packet // rel_ack_num is how much we must step the seq number received from the // other side. We do not send more than 255 bytes of text (=data) in the tcp packet. // If mss=1 then mss is included in the options list // // After calling this function you can fill in the first data byte at TCP_OPTIONS_P+4 // If cp_seq=0 then an initial sequence number is used (should be use in synack) // otherwise it is copied from the packet we received void make_tcphead(unsigned char *buf,unsigned int rel_ack_num,unsigned char mss,unsigned char cp_seq){ 348 软件实现 unsigned char i=0; unsigned char tseq; while(i<2){ buf[TCP_DST_PORT_H_P+i]=buf[TCP_SRC_PORT_H_P+i]; buf[TCP_SRC_PORT_H_P+i]=0; // clear source port i++; } // set source port (http): buf[TCP_SRC_PORT_L_P]=wwwport; i=4; // sequence numbers: // add the rel ack num to SEQACK while(i>0){ rel_ack_num=buf[TCP_SEQ_H_P+i-1]+rel_ack_num; tseq=buf[TCP_SEQACK_H_P+i-1]; buf[TCP_SEQACK_H_P+i-1]=0xff&rel_ack_num; if (cp_seq){ // copy the acknum sent to us into the sequence number buf[TCP_SEQ_H_P+i-1]=tseq; }else{ buf[TCP_SEQ_H_P+i-1]= 0; // some preset vallue } rel_ack_num=rel_ack_num>>8; i--; } if (cp_seq==0){ // put inital seq number buf[TCP_SEQ_H_P+0]= 0; buf[TCP_SEQ_H_P+1]= 0; // we step only the second byte, this allows us to send packts // with 255 bytes or 512 (if we step the initial seqnum by 2) buf[TCP_SEQ_H_P+2]= seqnum; buf[TCP_SEQ_H_P+3]= 0; // step the inititial seq num by something we will not use // during this tcp session: seqnum+=2; } // zero the checksum buf[TCP_CHECKSUM_H_P]=0; buf[TCP_CHECKSUM_L_P]=0; // The tcp header length is only a 4 bit field (the upper 4 bits). // It is calculated in units of 4 bytes. // E.g 24 bytes: 24/4=6 => 0x60=header len field //buf[TCP_HEADER_LEN_P]=(((TCP_HEADER_LEN_PLAIN+4)/4)) <<4; // 0x60 if (mss){ // the only option we set is MSS to 1408: // 1408 in hex is 0x580 buf[TCP_OPTIONS_P]=2; buf[TCP_OPTIONS_P+1]=4; buf[TCP_OPTIONS_P+2]=0x05; buf[TCP_OPTIONS_P+3]=0x80; // 24 bytes: buf[TCP_HEADER_LEN_P]=0x60; }else{ // no options: 349 软件实现 // 20 bytes: buf[TCP_HEADER_LEN_P]=0x50; } } void make_arp_answer_from_request(unsigned char *buf){ unsigned char i=0; //填写包的目的MAC地址以及源MAC地址 make_eth(buf); //填写ARP响应包的类型 buf[ETH_ARP_OPCODE_H_P]=ETH_ARP_OPCODE_REPLY_H_V; buf[ETH_ARP_OPCODE_L_P]=ETH_ARP_OPCODE_REPLY_L_V; //arp 响应 //填写ARP包的目的MAC地址以及源MAC地址 while(i<6){ buf[ETH_ARP_DST_MAC_P+i]=buf[ETH_ARP_SRC_MAC_P+i]; buf[ETH_ARP_SRC_MAC_P+i]=macaddr[i]; i++; } //填写ARP包的目的IP地址以及源IP地址 i=0; while(i<4){ buf[ETH_ARP_DST_IP_P+i]=buf[ETH_ARP_SRC_IP_P+i]; buf[ETH_ARP_SRC_IP_P+i]=ipaddr[i]; i++; } printf("\n\rTIGER BOARD[%d.%d.%d.%d]发送ARP响应 ",ipaddr[0],ipaddr[1],ipaddr[2],ipaddr[3]); //发送ARP相应包 enc28j60PacketSend(42,buf); } void make_echo_reply_from_request(unsigned char *buf,unsigned { //填写包的目的MAC地址以及源MAC地址 make_eth(buf); //填写包的目的IP地址以及源IP地址 make_ip(buf); int len) //填写ICMP相应包类型 buf[ICMP_TYPE_P]=ICMP_TYPE_ECHOREPLY_V; //////回送应答 ///////////////////////////////////////////////////////////////////////// /// // we changed only the icmp.type field from request(=8) to reply(=0). 350 软件实现 // we can therefore easily correct the checksum: if (buf[ICMP_CHECKSUM_P] > (0xff-0x08)) { buf[ICMP_CHECKSUM_P+1]++; } buf[ICMP_CHECKSUM_P]+=0x08; printf("\n\rTIGER BOARD[%d.%d.%d.%d]发送ICMP包响应 ",ipaddr[0],ipaddr[1],ipaddr[2],ipaddr[3]); //发送ICMP响应包 enc28j60PacketSend(len,buf); } // you can send a max of 220 bytes of data void make_udp_reply_from_request(unsigned char *buf,char *data,unsigned int datalen,unsigned int port) { unsigned int i=0; unsigned int ck; make_eth(buf); // total length field in the IP header must be set: i= IP_HEADER_LEN+UDP_HEADER_LEN+datalen; buf[IP_TOTLEN_H_P]=i>>8; buf[IP_TOTLEN_L_P]=i; make_ip(buf); buf[UDP_DST_PORT_H_P]=port>>8; buf[UDP_DST_PORT_L_P]=port & 0xff; // source port does not matter and is what the sender used. // calculte the udp length: buf[UDP_LEN_H_P]=datalen>>8; buf[UDP_LEN_L_P]=UDP_HEADER_LEN+datalen; // zero the checksum buf[UDP_CHECKSUM_H_P]=0; buf[UDP_CHECKSUM_L_P]=0; // copy the data: while(i>8; buf[UDP_CHECKSUM_L_P]=ck& 0xff; //add unsigned char pt; pt = buf[UDP_DST_PORT_H_P]; buf[UDP_DST_PORT_H_P] = buf[UDP_SRC_PORT_H_P]; buf[UDP_SRC_PORT_H_P]=pt; pt = buf[UDP_DST_PORT_L_P]; 351 buf[UDP_DST_PORT_L_P] = buf[UDP_SRC_PORT_L_P]; buf[UDP_SRC_PORT_L_P]=pt; //add 软件实现 enc28j60PacketSend(UDP_HEADER_LEN+IP_HEADER_LEN+ETH_HEADER_LEN+datalen,bu f); printf("\n\rTIGER BOARD[%d.%d.%d.%d]发送udp包响应 ",ipaddr[0],ipaddr[1],ipaddr[2],ipaddr[3]); } void make_tcp_synack_from_syn(unsigned char *buf) { unsigned int ck; //填写包的目的MAC地址以及源MAC地址 make_eth(buf); //计算包的长度 // total length field in the IP header must be set: 20 bytes IP + 24 bytes (20tcp+4tcp options) buf[IP_TOTLEN_H_P]=0; buf[IP_TOTLEN_L_P]=IP_HEADER_LEN+TCP_HEADER_LEN_PLAIN+4; //填写包的目的IP地址以及源IP地址 make_ip(buf); buf[TCP_FLAGS_P]=TCP_FLAGS_SYNACK_V; make_tcphead(buf,1,1,0); // calculate the checksum, len=8 (start from ip.src) + TCP_HEADER_LEN_PLAIN + 4 (one option: mss) ck=checksum(&buf[IP_SRC_P], 8+TCP_HEADER_LEN_PLAIN+4,2); buf[TCP_CHECKSUM_H_P]=ck>>8; buf[TCP_CHECKSUM_L_P]=ck& 0xff; // add 4 for option mss: printf("\n\rTIGER BOARD[%d.%d.%d.%d]发送SYN包响应 ",ipaddr[0],ipaddr[1],ipaddr[2],ipaddr[3]); enc28j60PacketSend(IP_HEADER_LEN+TCP_HEADER_LEN_PLAIN+4+ETH_HEADER_LEN,bu f); } // get a pointer to the start of tcp data in buf // Returns 0 if there is no data // You must call init_len_info once before calling this function unsigned int get_tcp_data_pointer(void) { if (info_data_len) { return((unsigned int)TCP_SRC_PORT_H_P+info_hdr_len); } 352 软件实现 else { return(0); } } // do some basic length calculations and store the result in static varibales void init_len_info(unsigned char *buf) { info_data_len=(buf[IP_TOTLEN_H_P]<<8)|(buf[IP_TOTLEN_L_P]&0xff); info_data_len-=IP_HEADER_LEN; info_hdr_len=(buf[TCP_HEADER_LEN_P]>>4)*4; // generate len in bytes; info_data_len-=info_hdr_len; if (info_data_len<=0) { info_data_len=0; } } // fill in tcp data at position pos. pos=0 means start of // tcp data. Returns the position at which the string after // this string could be filled. unsigned int fill_tcp_data_p(unsigned char *buf,unsigned int pos, const unsigned char *progmem_s) { char c; // fill in tcp data at position pos // // with no options the data starts after the checksum + 2 more bytes (urgent ptr) while ((c = pgm_read_byte(progmem_s++))) { buf[TCP_CHECKSUM_L_P+3+pos]=c; pos++; } return(pos); } // fill in tcp data at position pos. pos=0 means start of // tcp data. Returns the position at which the string after // this string could be filled. unsigned int fill_tcp_data(unsigned char *buf,unsigned int pos, const char *s) { // fill in tcp data at position pos // // with no options the data starts after the checksum + 2 more bytes (urgent ptr) while (*s) { buf[TCP_CHECKSUM_L_P+3+pos]=*s; pos++; s++; } return(pos); } 353 软件实现 // Make just an ack packet with no tcp data inside // This will modify the eth/ip/tcp header void make_tcp_ack_from_any(unsigned char *buf) { unsigned int j; make_eth(buf); // fill the header: buf[TCP_FLAGS_P]=TCP_FLAGS_ACK_V; if (info_data_len==0) { // if there is no data then we must still acknoledge one packet make_tcphead(buf,1,0,1); // no options } else { make_tcphead(buf,info_data_len,0,1); // no options } // total length field in the IP header must be set: // 20 bytes IP + 20 bytes tcp (when no options) j=IP_HEADER_LEN+TCP_HEADER_LEN_PLAIN; buf[IP_TOTLEN_H_P]=j>>8; buf[IP_TOTLEN_L_P]=j& 0xff; make_ip(buf); // calculate the checksum, len=8 (start from ip.src) + TCP_HEADER_LEN_PLAIN + data len j=checksum(&buf[IP_SRC_P], 8+TCP_HEADER_LEN_PLAIN,2); buf[TCP_CHECKSUM_H_P]=j>>8; buf[TCP_CHECKSUM_L_P]=j& 0xff; printf("\n\rTIGER BOARD[%d.%d.%d.%d]发送ACK包响应 ",ipaddr[0],ipaddr[1],ipaddr[2],ipaddr[3]); enc28j60PacketSend(IP_HEADER_LEN+TCP_HEADER_LEN_PLAIN+ETH_HEADER_LEN,buf) ; } // you must have called init_len_info at some time before calling this function // dlen is the amount of tcp data (http data) we send in this packet // You can use this function only immediately after make_tcp_ack_from_any // This is because this function will NOT modify the eth/ip/tcp header except for // length and checksum void make_tcp_ack_with_data(unsigned char *buf,unsigned int dlen) { unsigned int j; // fill the header: // This code requires that we send only one data packet // because we keep no state information. We must therefore set // the fin here: // buf[TCP_FLAGS_P]=TCP_FLAGS_ACK_V|TCP_FLAGS_PUSH_V|TCP_FLAGS_FIN_V; // total length field in the IP header must be set: // 20 bytes IP + 20 bytes tcp (when no options) + len of data 354 软件实现 j=IP_HEADER_LEN+TCP_HEADER_LEN_PLAIN+dlen; buf[IP_TOTLEN_H_P]=j>>8; buf[IP_TOTLEN_L_P]=j& 0xff; fill_ip_hdr_checksum(buf); // zero the checksum buf[TCP_CHECKSUM_H_P]=0; buf[TCP_CHECKSUM_L_P]=0; // calculate the checksum, len=8 (start from ip.src) + TCP_HEADER_LEN_PLAIN + data len j=checksum(&buf[IP_SRC_P], 8+TCP_HEADER_LEN_PLAIN+dlen,2); buf[TCP_CHECKSUM_H_P]=j>>8; buf[TCP_CHECKSUM_L_P]=j& 0xff; enc28j60PacketSend(IP_HEADER_LEN+TCP_HEADER_LEN_PLAIN+dlen+ETH_HEADER_LEN ,buf); } /* end of ip_arp_udp.c */ simple_server.c 文 件 为 以 太 网 通 信 程 序 , 在 文 件 中 编 写 完 成 了 以 太 网 通 信 函 数 simple_server(),编写完成后得到下图。 #include #include "../inc/enc28j60.h" #include "../inc/ip_arp_udp_tcp.h" #include "../inc/net.h" #include "../inc/simple_server.h" #include "unistd.h" #include "altera_avalon_pio_regs.h" #define PSTR(s) s static unsigned char myip[4] = {192,168,1,15}; //static unsigned char myip[4] = {10,14,112,127}; extern unsigned char mymac[6]; static unsigned int mywwwport =80; static unsigned int myudpport =1200; // listen port for udp //static unsigned int mytcpport =6000; // how did I get the mac addr? Translate the first 3 numbers into ascii is: TUX #define BUFFER_SIZE 1500//400 static unsigned char buf[BUFFER_SIZE+1]; int simple_server(void) { unsigned int plen,dat_p,i1=0,payloadlen=0; unsigned char *buf1 = 0; init_ip_arp_udp_tcp(mymac,myip,mywwwport); printf("\n\rTIGER BOARD MAC地 355 软件实现 址:0x%x,0x%x,0x%x,0x%x,0x%x,0x%x",mymac[0],mymac[1],mymac[2],mymac[3],mym ac[4],mymac[5]); printf("\n\rTIGER BOARD IP地 址:%d.%d.%d.%d",myip[0],myip[1],myip[2],myip[3]); printf("\n\rTIGER BOARD 端口号:%d\n\r\n\r",mywwwport); //init the ethernet/ip layer: while(1) { IOWR_ALTERA_AVALON_PIO_DATA(LED_BASE,1); //判断是否有接收到有效的包 plen = enc28j60PacketReceive(BUFFER_SIZE, buf); //如果收到有效的包,plen将为非0值。 if(plen==0) { continue; //没有收到有效的包就退出重新检测 } if(eth_type_is_arp_and_my_ip(buf,plen)) { make_arp_answer_from_request(buf); continue; } //判断是否接收到目的地址为本机IP的合法的IP包 if(eth_type_is_ip_and_my_ip(buf,plen)==0) { continue; } if (buf[IP_PROTO_P]==IP_PROTO_TCP_V&&buf[TCP_DST_PORT_H_P]==0&&buf[TCP_DST_P ORT_L_P]==mywwwport) { printf("\n\rTIGER BOARD接收到TCP包,端口为80。"); if (buf[TCP_FLAGS_P] & TCP_FLAGS_SYN_V) { printf("包类型为SYN"); make_tcp_synack_from_syn(buf); continue; } if (buf[TCP_FLAGS_P] & TCP_FLAGS_ACK_V) { printf("包类型为ACK"); init_len_info(buf); dat_p=get_tcp_data_pointer(); if (dat_p==0) { if (buf[TCP_FLAGS_P] & TCP_FLAGS_FIN_V) 356 软件实现 { make_tcp_ack_from_any(buf);/*发送响应*/ } // 发送一个没有数据的ACK响应,等待下一个包 continue; } plen=fill_tcp_data(buf,0,PSTR("TIGER BOARD ")); make_tcp_ack_from_any(buf); // send ack make_tcp_ack_with_data(buf,plen); // send data continue; } } //UDP包,监听1200端口的UDP包 if (buf[IP_PROTO_P]==IP_PROTO_UDP_V&&buf[UDP_DST_PORT_H_P]==4&&buf[UDP_DST_P ORT_L_P]==0xb0) { payloadlen= buf[UDP_LEN_H_P]; payloadlen=payloadlen<<8; payloadlen=(payloadlen+buf[UDP_LEN_L_P])-UDP_HEADER_LEN; //ANSWER buf1 = malloc(payloadlen); if(buf1 == NULL) { printf("malloc buffer error!\n"); continue; } for(i1=0; i1DATA = 1; delayms(100); enc28j60Init(mymac); simple_server(); return 0; } 在 main 函数中,首先将芯片复位管脚拉高,结束复位状态;然后调用 enc28j60Init()函 数初始化 ENC28J60 芯片;最后调用 simple_server()函数实现了以太网通信功能。 整个软件实现就完成了,怎么样?代码够长吧。LAN 的软件实现涉及到网络通信的知识, 建议大家可以了解下网络通信的相关知识,结合芯片 Datasheet,加深对代码了解。 358 总结 19.4 总结 LAN 功能的实现除了 TIGER BOARD,还需要 TCP&UDP 测试工具的帮助。TCP&UDP 测试工具用于开发网络通讯程序时,在服务器或客户端测试 TCP/UDP 通讯连接和测试数据的 接收和发送情况。大家可以把它理解为串口调试助手:一个测试串口通信,一个测试以太网通 信。下面结合 TCP&UDP 测试工具的使用方法,来介绍下 LAN 功能的调试过程。 首先准备一个 TCP&UDP 测试工具,我们在 TIGER BOARD 的资料包中给大家准备了测试 可用的 TCP&UDP 测试工具,当然也可以去网上下载(注意网上某些版本不好用),软件的安 装十分简单,这里就不介绍了。 首先用一根 RJ45 网线(也就是我们平时用的普通网线)连接 PC 机与 TIGER BOARD 的 网口,运行 Nios II 工程,在状态区打印出如下信息。 状态区打印出了我们在程序中设置的 TIGER BOARD 的 MAC 地址、IP 地址和端口号,这 些信息尤其是 IP 地址和端口号对我们很有帮助。 接下来设置下 PC 机上的 IP 地址,我们需要手动设置 PC 机上的 IP 地址,使 PC 机与 TIGER BOARD 处在同一网段,这也是实现 LAN 的前提条件。由上图可知 TIGER BOARD 上的 IP 地 址为 192.168.1.15,我们把 PC 机上的 IP 地址设置为 192.168.1.x 即可,这里设置为 192.168.1.2, 配置如下,点击确定即可。 359 总结 这时打开 TCP/UPD 测试工具,得到下图界面。 此时软件界面空空如也,因为我们还未创建连接。选中客户端模式,右键单击,选择创建 连接,得到下图的连接配置对话框。我们先来实现 TCP 通信,设置参数如下:类型选择 TCP, 目标 IP 选择 192.168.1.15,端口选择 80,本机端口选择随机端口,其它设置不变,点击创建 来创建连接。 360 总结 得到下图的界面,这时就丰富多了,我们把界面分为属性区、控制区、发送区和接收区四 个区域,属性区显示连接的基本属性,并且可以右键选择连接进行删除连接、创建新连接等操 作;控制区显示了当前连接的参数状态,并可以进行连接、清空等控制操作;发送区 用来输出 发送数据;接收区用来显示接收到的数据。 点击控制区的连接按键,正式进行连接。可以看到属性区 IP 地址前蓝色圆球变成了绿色三 角,表示连接成功。 361 总结 TCP 通信实现的功能为:PC 机发送任意字符,TIGER BOARD 收到字符后会将“TIGER BOARD”字符返回给 PC 机。我们在发送区随意输出字符,这里输入“Hello Nios”,接收区 会收到“TIGER BOARD”的字符,如下图所示,表示 TCP 通信成功。可以看到在控制区的计 数值显示发送了 10 个字符,收到 12 个字符。 这时在 Nios II 软件界面的状态区,打印出了一些信息,如下图所示。 362 总结 上述就是 TCP 通信的调试过程。下面继续完成 UDP 通信调试。先删除 TCP 连接,右键单 击属性区的 IP 地址,选择删除连接,如下图所示,连接就被删除了。 依照上面的方法再创建 UDP 连接,参数设置如下图所示:类型选择 UDP,目标 IP 选择 192.168.1.15,端口选择 1200,本机端口选择随机端口,其它设置不变,点击创建来创建连 接。 363 总结 点击控制区的连接按键正式连接,同样可以看到属性区的 IP 地址前出现了绿色三角,表示 连接成功。UDP 通信实现的功能为:PC 机发送任意字符,TIGER BOARD 收到字符后会将相 同的字符返回给 PC 机。在发送区同样输入“Hello Nios”,点击发送,可以看到接收区显示了 同样的“Hello Nios”字符,如下图所示,表示 UDP 通信成功。可以看到在控制区的计数值 显示发送了 10 个字符,收到 10 个字符。 以上就是 LAN 功能的调试过程,希望对大家实现功能有所帮助。 在硬件实现部分,我们添加了 LAN_nINT、LAN_nWOL 和 LAN_nRST 三个 PIO,细心的 同学可能会发现 LAN_nINT 这个 PIO 在添加时并没有给它设置中断,其实本章的以太网通信 并没有使用到 LAN_nINT 和 LAN_nWOL(囧),因为采用的不是中断模式,大家有兴趣可以 研究下中断模式下的以太网通信。软件实现部分的代码长且难以理解,希望大家能结合芯片的 Datasheet 和网络通信的相关知识,慢慢消化吸收。在实验调试时,要掌握 PC 机 IP 地址的设 置方法和 TCP/UDP 测试工具的使用,结合教程中的调试过程一步步调试,LAN 功能的实现也 就问题不大了。这一章的内容就到这里,因本人水平有限,文中有任何错误请联系我,大家在 364 总结 交流中进步!邮箱:vito943@qq.com 365 总结 第20章 附录 附录中收录了 VITO 原创和转载的一些关于 Nios II 和 FPGA 开发的文章,有些是对于前面 的教程中一些难点的更为详细的讲解,有些是开发调试过程中问题的总结,希望这些文章能给 各位开发者些许引导和启发。 366 总结 20.1 关于 NIOS II 中操作 PIO 接口异 常问题的解决 在使用 NIOS II 时常常会遇到一个奇怪的问题:在建立 SOPC 工程后,用 NIOS II 进行调试 时,CPU 对外部的接口控制往往会与程序设计不符,这里的接口包括 PIO、外部存储器控制接 口等等。 举个简单的例子,在程序中用一个 PIO 控制一个 LED 灯匀速闪烁,这个程序的实现并不复 杂,但是当写好后调试运行时,却发现 LED 灯并未像最初设想的那样闪烁,其亮灭没有任何规 律。在检查了 SOPC 工程中 PIO 设置和程序头文件中关于 PIO 的设置确定没有问题后,就会 陷入困境:如此简单的程序,问题究竟在哪里? 其实,这一切都是 DATA CACHE(数据高速缓存)造成的。 在建立 SOPC 工程选择 CPU 时,有三种类型的 CPU 可选,分别是 NIOS II/e、NIOS II/s、 NIOS II/f,它们对应的 economy(经济型)、standard(标准型)和 fast(快速型)。似乎除 了性能和资源占用外,看不出三种 CPU 的其它区别,如下图所示。 367 总结 其实问题在于 Caches and Memory Interfaces 这步骤里。如下图画圈处所示,若选择 fast 型的 CPU,Data Master 部分就会打开;反之,若选择 economy 或 standard,此部分是灰色 不可用的。 那 Data Cache 到底是什么呢,芯片手册上是这样介绍的(引自《n2cpu_nii5v1.pdf》的 2-12 页):Cache memory resides on-chip as an integral part of the Nios II processor 368 总结 core. The cache memories can improve the average memory access time for Nios II processor systems that use slow off-chip memory such as SDRAM for program and data storage. 简单的说,Data Cache 是像电脑的一级缓存二级缓存一样的东东,就是为了提高系统速 度的。可是有个这个高速缓冲区后,我们的代码执行时间却变得不可确定了,降低了程序的实 时性。我们发出的数据,放到了高速缓冲区里而没有及时的去执行才导致代码不执行,没有显 示 效 果 。 想 要 了 解 Cache memory 相 关 内 容 的 可 以 参 考 http://www.altera.com/literature/hb/nios2/n2sw_nii52007.pdf 既然知道原因了,那我们该如何解决呢。方法有两个: 第一种就是在建立 NIOS II/f 的时候,将 Data Cache 设置为 0,就是说关闭 Data Cache。 如下图所示,这样就不存在 Data Cache 了,这个办法非常彻底,是斩草除根式的做法。 第 二 种 是 在 开 启 了 Data Cache 前 提 下 , 通 过 软 件 来 解 决 。 http://www.altera.com/literature/hb/nios2/n2sw_nii52007.pdf 这里面有所介绍,就是通 过 31-bit Cache bypass。什么意思呢,很简单,NIOS II 将寄存器的第 31 位作为了 Cache 开 启与否的控制位,如果此位为 1,则 Cache 关闭不启用,否则就开启。一般情况我们是不会注 意到这个的,所以才会出现无法控制 LED 的情况。我们把最简单的程序修改一下,如下所示 369 总结 #include #include"system.h" //这个地方要注意了,多了(1<<31),这就是bypass Cache,关闭Cache #define LED *(volatileunsignedlong *)(LED_BASE | (1 << 31) intmain(void) { while(1) { LED = 1; usleep(100000); LED = 0; usleep(100000); } return 0; } 这时,LED 就会按程序设计的那样正常闪烁。 两种方法的选择因人而异。第一种方法更为彻底,关闭 Cache 后一劳永逸,但也彻底失去 了缓存对系统性能的提升。第二种方法在每次 SOPC 工程有改动后都要手动重新修改,但缓存 在其它方面的作用并没有去掉。 370 总结 20.2 四步搞定 NiosII 工程文件夹目 录路径改变 在 NiosII 的开发过程中,路径改变会带来一系列问题:比如当我们在 PC 上某个路径下新 建一个工程 Nios_Prj(含 QuartusII 工程、Qsys 模块和 NiosII 工程)后,假设其路径为 C:\Nios_Prj,如果将工程路径改为 D:\Nios_Prj,如果不做任何设置修改,会因为出现的一系 列问题而使 NiosII 工程异常。其实,我们只需要简单的四步设置操作,就能使 NiosII 工程正 常运行。 一、修改 Workspace 路径 首先是 Workspace 路径的改变,一般来讲,我们把工程所在文件夹设置为 Workspace, 因为工 程的 路径 已变 ,所 以要 相应 调 整 Workspace 的路 径, 如下 图所 示, 把路 径 改为 D:\Nios_Prj。 点击 OK 后,在 NiosII SBT 软件中导入已经存在的工程,方法不再详述,完成后得到下图。 需要注意的是如果目录区已经存在工程,需要删除后再另行导入,因为此时工程因路径改变已 经无效。 371 总结 二、修改 setting.bsp 文件中的路径 此时如果直接 Build 工程,状态区会出现如下图所示的 warning。 warning 提示 sopcinfo 文件无法找到,原因自然是路径改变了,我们需要重新设置 sopcinfo 文件的路径,在 bsp 工程下找到 setting.bsp 文件,如下图所示。 双击进入文件,可以看到红框中的 SOPC 文件路径和 BSP 生成路径都还在原来路径位置, 这是问题的根源。 我们将他们改为当前路径,完成后得到下图。 372 总结 三、重新 Generate BSP 工程 最后右键单击 BSP 工程,选择 Nios II->Generate BSP,如下图所示。 完成后再 Build 工程即可。 四、进行 Run Configuration 需要说明的是,此时如果直接运行,会弹出下图所示的对话框。 373 总结 这依然是拜路径改变所赐,软件去找原路径中的 ELF 文件了,右键单击 t22_test 主工程, 进行 Run Configuration,路径如下图所示。 其实在这个过程中,软件就会去更新当前工程所在路径的 ELF 文件,如下图红框所示。 374 总结 所以无需做任何修改,直接点击 Run,工程就可以正常运行了。 以上就是搞定 NiosII 路径改变的四步操作,完成后路径改变对 NiosII 工程的影响就消除 了。可以看出,之所以路径改变会对 NiosII 工程有较多影响,原因在于 NiosII 工程的某些部 分存储了路径信息需要更新。当然,作为一种更为稳妥的方法,当我们改变工程路径时,倒不 如干脆新建一个 NiosII 工程,把源码添加进入再 Build 和 Run,就会直接避免路径问题了。 375 总结 20.3 NiosII/Eclipse 中 遇 到 “Launching ... has encountered a problem”的解决方案 在使用 NiosII/Eclipse 对工程进行编译时,有时会遇到“Launching New Configuration has encountered a problem”的错误提示,其中 New Configuration 为工程名,名字因工 程而异。对于这种问题,一般存在硬件原因和软件原因两种可能。 硬件原因可能有两方面,解决方案分别为: 一、确认 Nios II 的 RAM 存储器(如 SDRAM)焊接正常,管脚全部分配且分配正确。 二、尝试重启下开发板或者重新下载下 FPGA 的 sof 文件。 软件原因的解决步骤为 1、选择菜单栏中的 Project -> Properties -> Run/Debug Settings: 2、选择列表中的 "Launching New Configuration Nios II hardware configuration" 3、点击右侧的 Delete 4、点击下方的 OK,问题解决 大家可以分别尝试,上述解决方案适用于绝大多数情况。 376 总结 20.4 Avalon 总线的地址对齐: Dynamic Addressing 和 Native Addressing 首先需要声明的是,Native Addressing 和 Dynamic Addresing 这两种寻址方式并存可 选的情况只存在于 SOPC Builder 中,在 Qsys 中,已经取消了对 Native Addressing 的支持, 虽 然 仍 保 留 了 该 选 项 , 实 际 上 只 是 把 Native Addressing 当 作 32bit 的 Dynamic Addressing 来处理。不管怎样,先介绍下 Avalon 总线的两种地址对齐:Native Addressing 和 Dynamic Addressing。 在 Nios II 开发中经常需要自己定制 IP 核,而 NiosII CPU 与定制的 IP 核之间的通信一般 通过 Avalon-MM(Avalon Memory-Mapped)接口总线来进行,它是最常用的一种 Avalon 总线。在基于这种结构的总线通信中,CPU 是 Master,定制的 IP 核是 Slave。Avalon 总线 是一种并行总线,一般来讲,Master 的总线宽度固定为 32bit,而 Slave 则因为定制需求的不 同位数也不尽相同,如果 Slave 的总线宽度不为 32bit,那么 Master 对 Slave 的寻址必然会遇 到地址对齐(Address Alignment)问题。 这里首先要提一个 System Interconnect Fabric(系统互联结构)的概念,它是用来连接 Avalon-MM Master 和 Avalon-MM Slave 的一种结构,是两者之间的桥梁和中转站。虽然 NiosII 是 32bit 架构的处理器,但为了兼容不同的 CPU 与外设,System Interconnect Fabric 采用的是字节(Byte)寻址的方式,每个 32bit 数据为一个 word,每个 word 包含 4 个基地 址,如下图所示。 377 总结 为了保证 Master 与 Slave 的地址对齐,System Interconnect Fabric 采用了两种不同的 寻址方式可选:Dynamic Addressing 和 Native Addressing,下面分别来介绍这两种寻址方 式的工作原理。 Dynamic Addressing 的本质是将 Slave 的地址空间连续映射到 Master 的地址空间上。 如果 Master 的数据位宽大于 Slave,则 Master 的单个地址空间的数据会映射到 Slave 的多个 地址空间;如果 Master 的数据位宽小于 Slave,则 Slave 的单个个地址空间的数据会映射到 Master 的多个地址空间。这里的单个地址空间指的是单个数据总线位宽的数据所占用的地址 空间,比如 Nios II 处理作为 Master 时总线数据位宽为 32bit,那么 32bit 数据(4bytes 数据) 占用的地址就是单个地址空间。Dynamic Addressing 模式下 Slave 的宽度只能为 8*2^n,即 8,16,32 等数据(最高 1024)。下图为 Master 为 32bit,Slave 分别为 8bit、16bit 和 64bit 时的地址空间的映射关系(OFFSET 代表 Slave 的地址空间偏置)。 378 总结 举例来说明 Dynamic Addressing 的工作原理:比如 Master 数据位宽为 32bit,而 Slave 为 16bit,则 Master 执行一次 32bit 读操作,Interconnect 会在 Slave 端读两次 16bit 数据 再把 32bit 数据返回给 Master;Master 执行一次 32bit 写操作,Interconnect 会在 Slave 端 写两次 16bit 数据。整个过程是通过一个有限状态机来实现,具体的过程无需开发者关心。 需 要 注 意 的 是 , Dynamic Addressing 方 式 的 实 现需 要 byteenable 信 号 的 支 持。 byteenable 信号采用了 one hot(独热)编码方式,总宽度由 Slave 的数据宽度决定, byteenable 的每一位信号对应一个 byte 宽度的数据。比如 Slave 数据宽度为 16bit, byteenable 为 2 位;Slave 数据宽度为 64bit,byteenable 为 8 位。 而在 Native Addressing 方式下,Slave 的单个地址空间会映射到 Master 的单个地址 空间,而无需关心 Slave 的具体数据宽度。无论 Master 的数据宽度大于还是小于 Slave,每 个 Master 的单个地址空间只映射一个 Slave 地址空间的数据。如果 Master 的数据宽度大于 Slave,那么 Master 在读 Slave 时会将缺少的高位填充 0,写 Slave 时会抛弃掉多出的高位; 如果 Master 的数据宽度小于 Slave,则操作相反。可以看出,因为在 Native Addressing 方 式下,Master 的单个地址空间永远对应着 Slave 的单个地址空间,所以无需 byteenable 信号; 379 总结 同时,若 Slave 数据宽度大于 Master,则采用 Native Addressing 方式必然会造成数据丢失。 下图为 Master 为 32bit,Slave 为 16bit 时,Dynamic Addressing 和 Native Addressing 两种方式的地址映射关系(OFFSET 代表 Slave 的地址空间偏置)。 仍以 Master 为 32bit,Slave 为 16bit 的例子来说明 Native Addressing 方式下的读写过 程。Master 执行一次 32bit 读操作,Interconnect 会读一次 Slave 的 16bit 数据,作为低 16bit, 然后将高 16bit 补 0 后传给 Master;Master 执行一次 32bit 写操作,Interconnect 会抛弃高 16bit 数据,将低 16bit 数据直接传给 Slave。 以上就是 Dynamic Addressing 和 NativeAddressing 的工作原理。在 Qsys 中,Slave Addressing 选项位于创建 IP 核模块对话框的 Interface 上侧边栏,下图红框所在的位置。 380 总结 远 在 SOPC Builder 时 代 , Altera 在 官 方 的 Datasheet 中 就 推 荐 使 用 Dynamic Addressing 方式,理由是该种方式更为灵活。在 Qsys 时代,Altera 做的更绝,虽然上图的选 项中保留了 Native Addressing 选项,实际上在选择它后只是把它当作一个 32bit 宽度的 Dynamic Addressing 来处理,仍会产生 byteenable 信号。 381 总结 20.5 【转】Quartus II 常见警告解释 及措施 1、Warning (10227): Verilog HDL PortDeclaration warning at PRESS_MODELE.v(29): data type declaration for"iR" declares packed dimensions but the port declaration declarationdoes not. 解释: 2、Warning: PLL"DE2_TV:inst1|Sdram_Control_4Port:u6|Sdram_PLL:sdram_pll1|altpll:altpll_compon ent|pll"output port clk[0] feeds output pin "DRAM1_CLK" via non-dedicatedrouting -jitter performance depends on switching rate of other designelements. Use PLL dedicated clock outputs to ensure jitter performance 解释:PLL 的输出用在了非专属的 PLL_OUT 措施:设计电路板的时候最好将 PLL_OUT 用在相关的时钟信号上,如果没有使用,则这个警 告不理会也可。 3、Warning: Using design file cpu.v, which isnot specified as a design file for the current project, but containsdefinitions for 25 design units and 25 entities in project 解释:模块不是在本项目生成的,而是直接 copy 了别的项目的原理图和源程序生成的,不是 用 QUARTUS 将文件添加进本项目 措施:无须理会,不影响使用 4、Warning (10240): Verilog HDL AlwaysConstruct warning at I2C_V_Config.v(153): inferring latch(es) for variable"LUT_DATA", which holds its previous value in one or more pathsthrough the always construct 382 总结 解释:信号被综合成了 latch,锁存器的 EN 和数据输入端口存在一个竞争的问题 措施:将计数器从里面抽出来 5、Warning: 12 hierarchies have connectivitywarnings - see the Connectivity Checks report folder 解释:实例化的时候,有一些端口没用,让没用的端口的位置空着, 措施:不用理会 6、Warning: Synthesized away the followingnode(s) 解释:以下节点被综合优化掉 措施:不用理会 7、Warning:Found xx output pins withoutoutput pin load capacitance assignment 解释:没有给输出管教指定负载电容 措施:该功能用于估算 TCO 和功耗,可以不理会,也可以在 Assignment Editor 中为相应的 输出管脚指定负载电容,以消除警告 8、Warning: The following nodes have bothtri-state and non-tri-state drivers 解释:该用三态逻辑驱动的信号,被用非三态逻辑驱动了 措施:在子信息中定位到警告所在,改用三态逻辑驱动 9、Warning: LatchDE2_TV:inst1|I2C_V_Config:I2C_AV_Config|LUT_DATA[8] has unsafe behavior Warning: Ports D and ENA on the latch are fed by the samesignal DE2_TV:inst1|I2C_V_Config:I2C_AV_Config|LUT_INDEX[4] 解释:产生了 latch 措施:用时序代替组合电路,或者是用完备的 if/else,和 case 语句 10、Warning: TRI or OPNDRN buffers permanentlyenabled 解释:输出要加三态控制 11、Warning: Output pins are stuck at VCC orGND 解释:这几个输出管脚直接接地了 措施:如果这符合你的设计要求这种警告可以不管 383 总结 12、Warning (15400):WYSIWYG primitive"DE2_TV:inst1|Sdram_Control_4Port:u6|Sdram_WR_FIFO:write_fifo2|dcfifo:dcfi fo_component|dcfifo_21m1:auto_generated|altsyncram_1l81:fifo_ram|altsyncram_drg 1:altsyncram5|ram_block6a15"has a port clk1 that is stuck at GND 解释:这里是采用的 SDRAM 的读写方式为 1 入 2 出的模式,将 fifo2 的输入信号给接 GND 了 措施:不用理会。 另外:如果出现跟 RAM 相关的 WYSIWYG primitive 错误或者是警告,则是 RAM 的输入端信 号不通导致。 13、Warning: Design contains 2 input pin(s) that do not drive logic 解释:有 2 个输入没有驱动任何逻辑,也就是说,只定义了 2 个输入管脚,但在逻辑中并没有 使用这 2 个输入信号 措施:将这 2 个输入管脚的定义去掉即可 14、Warning: At least one of the filters hadsome problems and could not be matched. 解释: 措施: 15、Warning: Node: XXX was determined to be aclock but was found without an associated clock assignment. 解释及措施: (1). 这个信号是不是你期望的时钟信号?还是被综合器误将普通信号综合成了时钟信号?有没 有在代码中用过这个信号的上升沿/下降沿? (2). 如果是期望的时钟信号,那么是否有可能调整管脚位置约束到专用时钟管脚?如果不行的 话,这条时钟线上的延时会比较大。但是整个布局布线还是可以进行下去的。 16、Warning: PLL"DE2_TV:inst1|Sdram_Control_4Port:u6|Sdram_PLL:sdram_pll1|altpll:altpll_compon ent|pll"is in normal or source synchronous mode with output clock"compensate_clock" set to clk[0] that is not fully compensated becauseit feeds an 384 总结 output pin -- only PLLs in zero delay buffer mode can fullycompensate output pins 解释: 措施: 17、Warning: PLL"DE2_TV:inst1|Sdram_Control_4Port:u6|Sdram_PLL:sdram_pll1|altpll:altpll_compon ent|pll"output port clk[0] feeds output pin "DRAM1_CLK" via non-dedicatedrouting -jitter performance depends on switching rate of other designelements. Use PLL dedicated clock outputs to ensure jitter performance 解释:这是说没有使用 FPGA 专用的 PLL 输出引脚 措施:同 2 18、Warning: Ignored locations or regionassignments to the following nodes Warning: Node "FIELD" is assigned to location orregion, but does not exist in design 解释:有些引脚做了分配,但是在设计中没有使用 措施:可以不用理会 19:Warning: Following 1 pins have no outputenable or a GND or VCC output enable later changes to this connectivity maychange fitting results 解释:下面有 1 个管脚没有输出使能,或者仅仅是 GND,VCC 使能 措施:给其配置一个使能即可 20、Warning: Following 4 pins have nothing,GND, or VCC driving datain port -changes to this connectivity may change fitting results 解释:同 11 措施:同 11 21、Warning: The Reserve All Unused Pinssetting has not been specified, and will default to 'As output driving ground'. 解释:所有没有用到的管脚都直接接 GND 措施:可以不用理会,也可以在 Assignments 里做修改 385

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