第6章 软件设计流程和方法 SOPC技术与应用

更新时间:2024-02-02 06:26:01 阅读量: 教育文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

第1节 Nios II IDE简介

Nios II EDS(Embedded Design Suite——嵌入式开发包)提供了一个统一的开发平台,适用于所有Nios II处理器系统。仅仅通过一台PC机、一片Altera的FPGA以及一根JTAG下载电缆,软件开发人员就能够往Nios II 处理器系统写入程序以及和Nios II处理器系统进行通讯。Nios II处理器的JTAG 调试模块提供了使用JTAG下载线和Nios II处理器通信唯一的、统一的方法。无论是单处理器系统中的处理器,还是复杂多处理器系统中的处理器,对其的访问都是相同的。用户不必去自己建立访问嵌入 式处理器的接口。

Nios II EDS 提供了两种不同的设计流程,包括很多生成Nios II程序的软件工具,包括需要版权的和开源软件工具如,GNU C/C++ 工具集 。Nios II EDS为基于Nios II的系统自动生成板支持包(board support package——BSP)。 Altera的BSP包括Altera硬件抽象层(hardware abstraction layer——HAL),可选的RTOS,设备驱动。BSP提供了C/C++运行环境,使用户避免直接和硬件打交道。

Nios II EDS 的第一种开发流程是用户在集成开发环境Nios II IDE中完成所有的工作,第二种开发流程是在命令行和脚本环境中使用Nios II 软件生成工具,然后将工程导入到IDE中进行调试。本书介绍使用Nios II IDE进行软件设计的流程, Nios II IDE基于开放式的、可扩展Eclipse IDE project工程以及Eclipse C/C++ 开发工具(CDT)工程。

Nios II集成开发环境(IDE)是Nios II系列嵌入式处理器的基本软件开发工具。所有软件开发任务都可以Nios II IDE下完成,包括编辑、编译和调试程序。Nios II IDE为软件开发提供四个主要的功能: 工程管理器 编辑器和编译器 调试器 闪存编程器 6.1.1 工程管理器

The Nios II IDE提供多个工程管理任务,加快嵌入式应用程序的开发进度。

新工程向导——Nios II IDE推出了一个新工程向导,用于自动建立C/C++应用程序工程和系统库工程。采用新工程向导,能够轻松地在Nios II IDE中创建新工程,如下图所示。

Nios II IDE新工程向导

软件工程模板——除了工程创建向导,Nios II IDE还以工程模板的形式提供了软件代码实例,帮助软件工程师尽可能快速地推出可运行的系统。 每个模板包括一系列软件文件和工程设置。通过覆盖工程目录下的代码或者导入工程文件的方式,开发人员能够将他们自己的源代码添加到工程中。在上图中的下半部分分别是可选用的模板和模板的介绍。

软件组件——Nios II IDE使开发人员通过使用软件组件能够快速地定制系统。软件组件(或者称为“系统软件”)为开发人员提供了一个简单的方式来轻松地为特定目标硬件配置他们 的系统。在上图中点击Next,会出现下图所示的系统库的创建/选择窗口,新建工程用到的组件会包含在系统库中。 组件包括:

Nios II运行库(或者称为硬件抽象层(HAL)) 轻量级IP TCP/IP 库

MicroC/OS-II实时操作系统(RTOS) Altera压缩文件系统

系统库工程

6.1.2 编辑器和编译器

Altera's Nios II IDE提供了一个全功能的源代码编辑器和C/C++编译器。包括下面的几部分:

文本编辑器——Nios II IDE文本编辑器是一个成熟的全功能源文件编辑器。这些功能包括:语法高亮显示C/C++,代码辅助/代码协助完成,全面的搜索工具,文件管理,广泛的在线帮助主题和教程,引入辅助,快速定位,自动纠错,内置调试功能。

C/C++编译器——Nios II IDE为GCC编译器提供了一个图形化用户界面,Nios II IDE编译环境使设计Altera的Nios II处理器软件更容易,它提供了一个易用的按钮式流程,同时允许开发人员手工设置高级编译选项。

Nios II IDE编译环境自动地生成一个基于用户特定系统配置(SOPC Builder生成的PTF文件)的makefile。Nios II IDE中编译/链接设置的任何改变都会自动映射到这个自动生成的makefile中。这些设置可包括生成存储器初始化文件(MIF)的选项、闪存内容、仿 真器初始化文件(DAT/HEX)以及profile总结文件的相关选项。 6.1.3 调试器

Nios II IDE包含一个强大的、基于GNU调试器的软件调试器-GDB。该调试器提供了许多基本调试功能,以及一些在低成本处理器开发套件中不会经常用到的高级调试功能。

基本调试功能——Nios II IDE调试器包含如下的基本调试功能: 运行控制

调用堆栈查看

软件断点

反汇编代码查看 调试信息查看 指令集仿真器

高级调试——除了上述基本调试功能之外,Nios II IDE调试器还支持以下高级调试功能:

硬件断点调试ROM或闪存中的代码 数据触发 指令跟踪

Nios II IDE调试器通过JTAG调试模块和目标硬件相连。另外,支持片外跟踪功能便于和第三方跟踪探测工具结合使用,如FS2公司提供的用于Nios II处理器的in-target系统分析仪(ISA-NIOS)。

调试信息查看——调试信息查看使用户可以访问本地变量、寄存器、存储器、断点以及表达式赋值函数。 连接目标——Nios II IDE调试器能够连接多种目标。表6-1列出了Nios II IDE中可用的目标连接。

Nios II IDE调试器目标

6.1.4 闪存编程器

许多使用Nios II处理器的设计都在单板上采用了闪存,可以用来存储FPGA配置数据和/或Nios II编程数据。Nios II IDE提供了一个方便的闪存编程方法。任何连接到FPGA的兼容通用闪存接口(CFI)的闪存器件都可以通过Nios II IDE闪存编程器来烧写。除CFI闪存之外,Nios II IDE闪存编程器能够对连接到FPGA的任何Altera串行配置器件进行编程。

闪存编程器管理多种数据,下表显示了编程到闪存的通用内容类型。

Nios II IDE闪存编程器具有易用的接口,Nios II IDE闪存编程器已做了预先配置,能够用于Nios II开发套件中的所有单板,而且能够轻易地引入到用户硬件中。

除了IDE中的这些工具之外,Nios II EDS还包括如下的部分:

GNU工具系列——Nios II 编译器工具是基于标准的GNU gcc编译器、汇编器、连接器和make工具。

指令集仿真器——Nios II 指令仿真器(ISS)使得用户在目标硬件准备好之前就能开发程序。Nios II IDE使得用户可以基于ISS运行开发的程序,就如同在真正的目标硬件上运行一样简单。

设计实例——Nios_II ED提供了软件实例和硬件设计来展示Nios II处理器和开发环境所具有的卓越的性能。

第2节 软件开发流程

6.2.1 Nios II程序的构成 每个Nios II程序包括一个应用工程,可选的库工程和一个板支持包工程。用户将Nios II程序编译成一个能在Nios II处理上运行的可执行和连接的格式(Executable And Linked Format File—— .elf)的文件。 应用工程

Nios II C/C++ 应用工程包括组成一个可执行的.elf文件的源代码的集合。一个典型的应用的特征是一个源文件包含main ()——主函数。应用工程包括libraries和BSP中被调用的函数的源代码。 库工程

库工程是一个库文件(.a)中的源代码的集合。库文件中通常包含可重用的,通用的函数,这些函数可被多个应用工程所共享。比如,数学函数库。库工程没有main ()函数。 BSP工程

Nios II BSP工程是包含特定系统支持代码的特殊的库。BSP为SOPC Builder

系统的处理器提供定制的软件运行环境。Nios II EDS提供相应的工具可以修改设置以控制BSP的行为。Nios II IDE和Nios II IDE 开发流程文档中使用\library\来指代BSP。 BSP包括如下的组成部分: 硬件抽象层 (HAL) Newlib C 标准库 设备驱动

可选的软件包

可选的实时操作系统 (RTOS) 硬件抽象层 (HAL)

HAL提供一个非线程的,类UNIX的C/C++ 运行环境。HAL可以提供通用的I/O设备,允许用户newlib C 标准库的函数编程来访问硬件,如printf ()。使用HAL可以最小化(或消除)通过直接访问硬件的寄存器来控制外设和与外设通信。 Newlib C 标准库

Newlib是为了嵌入式系统的应用,而对C的标准库进行精简的开源实现。包括一些常用的函数的集合,如printf()、malloc()和open()等。 设备驱动

每个设备驱动管理一个硬件设备。HAL为SOPC Builder系统中的每一个需要驱动程序的设备实例化一个驱动程序。在Nios II 软件开发环境中,设备驱动具有如下的属性:

一个设备驱动是和一个特定的SOPC Builder设备相关联的。 驱动程序可能有一些设置可以影响驱动程序的编译,这些设置包含在BSP的设置中。

可选的选件包

软件包是用户可以选择加入到BSP工程中,提供附加的功能的源代码。比如Nios II 版本的Nich Stack? TCP/IP协议栈。Nios II IDE和Nios II IDE设计流程文档使用\软件组件\来指代软件包。

在Nios II软件开发环境中,软件包具有如下的典型特性: A软件包和特定的硬件没有关联

软件包有一些设置会影响它的编译,这些设置.包含在BSP的设置中。 在Nios II软件开发环境中,软件包和库工程是不同的,软件包是BSP工程的一部分,不是一个单独的库工程。 可选的实时操作系统(RTOS) C/OS-II?Nios II EDS包含第三方的 C/OS-II基于HAL,实现了一个简单的、well?实时操作系统,用户可以选择加入到BSP中。 documented调度程序。用户可以修改设置,这些设置也是包含在BSP设置中。其它的操作系统可从第三方的软件厂商获得。.

6.2.2 Nios II IDE软件开发步骤

采用Nios II IDE开发流程,用户使用Nios II IDE图形用户界面来创建、修改、编译、运行和调试Nios II程序。IDE创建和管理用户的makefile。如果用户对编译进程和工程设置干预比较少,而且不需要定制的脚本,采用这种流程比较好。 Nios II IDE是基于流行的Eclipse IDE框架以及Eclipse C/C++ 开发工具 (CDT)

插件。Nios II IDE在后台运行其它的工具,对用户屏蔽了底层工具的细节,提供了一个统一的开发环境。 借助工程的创建和配置向导,Nios II IDE使用起来很容易,尤其是对Nios II 的初学者帮助很大。Altera公司提供了Windows和Linux操作系统的Nios II IDE。 下面以电子钟的软件开发为例来介绍软件开发的过程。 1. 新建IDE管理的工程 Nios II IDE提供了新工程的向导,指导用户创建IDE管理的工程。启动Nios II IDE,出现Nios II Nios II C/C++?C/C++的窗口。首先创建Nios II C/C++应用程序,在File菜单中选择new Application,启动创建Nios II C/C++ 应用工程的向导。

为新的Nios II工程命名:这里Nios II的工程名为digi_clock。

选择目标硬件:选择电子钟的硬件的系统的PTF文件,IDE根据该文件来建立系统库。

选择新工程的模板:这里选择为Blank Project。

在该窗口中,用户可以选择是创建一个新的系统库,或是利用已有的系统库,缺省的配置是新建一个系统库。如在图中直接点击Finish,则选择新建一个系统库。

在上图中点击Finish之后,Nios II IDE创建新的工程,IDE也创建了系统库工程 *_syslib。这些工程出现在IDE工作台的Nios II C/C++ 工程视图中。digi_clock为C/C++工程,digi_clock_syslib[SOPC]为系统库工程。

Source File。?New?Source File,用户也可以在选择File菜单?建立C的源文件,首先选中digi_clock工程,然后单击右键,在弹出菜单选择New

进行如上的操作会出现下图的窗口提示用户输入源程序的文件名,本例取名为digi_clock.c,注意一定要加上后缀名,点击Finish完成。用户采用同样的步骤来建立头文件,最后选择Head File即可,将头文件命名为digi_clock.h

2. 编译工程和管理工程

对源程序和头文件编辑完成之后,对工程进行编译,编译的方法,是右键单击digi_clock工程,然后在弹出菜单中选择Build Build?Project,如下图,或者选择Project菜单 Project。编译成功后,在工程下面会出现一个Binaries的目录,其中有一个可执行的文件digi_clock.elf。编译中出现的错误和警 告,IDE会在窗口给出,用户根据系统提供的信息进行修改。

在菜单中,用户还可以对工程一些重要的选项进行设置。有如下的一些设置: Properties——主要是管理工程和硬件以及其它工程的关联。

System Library Properties——管理硬件的特定的设置,比如通信设备,存储器的分配。

Run As——管理程序是在硬件上运行,还是在指令仿真器环境下运行。 Debug As——管理是在硬件上调试程序,还是在指令仿真器环境下。 Properties设置

在上图中点击Properties出现下图的窗口,该窗口的为Info页,显示的该工程的一些信息,不用设置,还有其它的页也无须设置,这里重点介绍C/C++Build页,C/C++ Indexer页。

在上图中点击C/C++ Build,出现下图所示的窗口,在该窗口中,用户关心的选项有Configuration下拉列表框、Tool Settings选项卡。Configuration下拉列表框选择编译工程时采用调试模式(Debug)还是发布(Release)模式。不同的模式对 应不同的编译器设置,优化级别和调试级别都可能不同,用户也可以自己来设置编译器的优化级别和调试级别,如下图中的Tools Settings页中的Nios II Compiler的General栏。选用Release模式能很大程度减小程序空间并提高程序的执行性能。

在上图中点击C/C++ Indexer,出现如下图所示的窗口,在该窗口中,用户在Available Indexers下拉列表框中,可以选择可用的检索器,这里有三个选项,第一个选项是不使用Indexer;第二个选项是Fast C/C++ Indexer;第三个是Full C/C++ Indexer。各种Indexer的特点也给出了说明。借助于检索器,用户可以方便地找到程序文件中的相关信息。

System Library Properties的设置

在工程的弹出菜单中点击System Library Properties,出现下图的窗口

第3 节 运行和调试程序

调试/运行程序之前要把嵌入式系统的硬件系统下载到FPGA中。 程序的调试可以发生在下面的环境: Nios II Hardware——Nios II硬件。

Nios II Instruction Set Simulator——Nios II指令集仿真器。 Nios II程序的运行可以发生在下面的环境: Nios II Hardware——Nios II硬件。

Nios II Instruction Set Simulator——Nios II指令集仿真器。 Nios II ModelSim——在ModelSim软件环境下运行。

Debug As/Run As来实现同样的功能。?想要在哪个环境下调试和运行只要通过

鼠标右键单击Nios II工程,然后在弹出的菜单中选择Debug As/Run As,再选择相应的环境,用户还可以通过选择Run菜单

6.3.1 调试/运行环境设置

Debug?/Run?。如果选择执行Run菜单中的?但是在调试/运行程序之前,必须先对上述的调试/运行环境进行设置。方法是选择Run菜单 Debug?,出现下图的窗口,在窗口中,选中Nios II Hardware,双击或者点击快捷图标New launch configuration,就新建了一个硬件调试配置。对Nios II 指令集仿真器有同样的方法。

对调试环境的设置主要是Target connection和Debugger配置页。配置的首页是Main配置页,用户一般不用修改。选择Target Connection配置页,出现相应的窗口。JTAG Cable:进行JTAG下载电缆的选择。当只有一个下载电缆是,会自动选择,无需设置,当有多个下载电缆时,用户手动进行选择。

JTAG Device:进行连接在JTAG链上的FPGA器件的选择,本例只有一个器件,用户不用选择,当具有多个器件时,用户必须手动进行选择。

Nios II Terminal communication device:进行Nios II系统终端通信设备选择,本例选择jtag_uart。

6.3.2 调试/运行程序

进行完调试和运行环境的设置之后,调试和运行程序只要点击Run菜单,然后选择Run As/Debug As,再选择相应的运行/调试环境,如Nios II Haredware。控制台会显示调试和运行程序的信息。 6.3.3 下载程序到Flash

用户可以将软件文件、FPGA配置文件和数据文件存储到flash存储器中。对flash存储器编程能够使硬件在启动的时从flash装载软件和 FPGA的配置。用户可以使用Nios II IDE flash programmer编程连接到FPGA的flash存储器。 用户好似用flash programmer配置来管理flash存储器的编程。flash

programmer 配置是影响特定的目标硬件的flash编程进程的一组设置。用户可以创建多个flash programmer配置,每一个具有自己的编程参数。如果用户在多个目标板上开发项目时很有帮助。用户也可以建立一个flash programmer配置来编程文件的组合,允许用户一次操作编程两或三个文件。 1. 打开flash programmer,创建flash programmer配置

在Tools菜单中,点击Flash Programmer....,出现Flash Programmer 对话窗口。

在配置列表中右键单击Flash Programmer,然后在弹出菜单中单击New,一个新的flash programmer 配置出现。

在Name框中为该配置输入一个唯一的、有意义的名字。

2. 指定要编程到flash存储器的文件

在配置列表中,单击一个flash programmer 配置。 点击Main页。

如果要将可执行文件编程到flash存储器,采用如下的步骤: 选中Program software project into flash memory。

在Project 框中指定工程。flash programmer会自动找到用户工程的Nios II ELF Executable和目标硬件。

如果用户不是将可执行文件编程到flash存储器,采取如下的步骤: 选中Program software into flash memory.

删除Project框中的内容。

不选择Program software into flash memory。SOPC Builder System 框可用. 在SOPC Builder System框中指定目标硬件文件。

如果要将FPGA配置编程到flash存储器,采取如下的步骤:

选中Program FPGA configuration data into hardware-image region of flash memory.

在FPGA configuration (.sof)框中指定数据文件. 在Hardware Image 列表中选择FPGA 配置位置。 在Memory列表中,选择flash存储器设备。

在Offset框中,键入flash存储器设备中的配置文件起始位置的偏移量。 如果用户要将数据文件编程到flash存储器,采用如下的步骤: 选中Program a file into flash memory。 在File框指定要编程到flash的文件。 在Memory列表中,选择flash存储器设备。

在Offset框中,输入数据文件的在flash存储器设备中起始位置的偏移量。

指定编程的线缆和目标flash存储器设备

在配置列表中,单击一个flash programmer配置。 单击Target Connection页。

在JTAG cable列表中,选择连接到目标板的JTAG cable。如果用户只有一条电缆,automatic 的设置会自动地确定用户的下载线。如果用户的下载线没有出现在列表中,确认是否正确地安装和连接,然后点击Refresh 将其添加到列表中。 在JTAG device列表中,选择要编程的Nios II 系统。如果用户只有更一个连接到JTAG Cable的Nios II 系统,automatic 的设置自动确定用户的系统。如果用户的Nios II系统没有显示在列表中,确认硬件是否正确地安装和连接, 然后点击Refresh将其添加到列表中。 编程目标板上的flash存储器

在配置列表中,点击一个flash programmer配置。Main和Target Connection页上的设置必须是合法的。 点击右下角的Program Flash开始对flash编程。flash的编程过程需要几分钟,具体的时间取决于下载的数据的大小。 如果用户的工程不是最新的, 在将工程编程到flash存储器之前, Nios II IDE 会自动地编译用户的工程。用户也可以关闭自动编译,在Window 菜单,点击Preferences, 然后展开Run/Debug,点击 Launching,然后去掉Build (if required) before launching的选项。

如果需要的话,flash programmer自动地在编程数据之前添加上boot-loader代码。

第4节 硬件抽象层库(HAL)

6.4.1 HAL 简介

HAL是轻量级(lightweight)的运行环境,提供了简单的设备驱动程序接口,应用程序 使用设备驱动程序接口同底层硬件之间进行通信。HAL应用程序接口(application program interface——API) 同ANSI C 标准库结合在一起。HAL API使得用户可以使用熟悉的C语言的库函数来访问硬件设备或文件,如printf()、fopen()、fwrite()等函数。

HAL作为Nios II处理器系统的设备驱动程序软件包, 为系统中的外设提供了相匹配的接口。SOPC Builder和Nios II 软件开发工具之间的紧密集成,使得特定硬件系统的HAL可以自动产生。当SOPC Builder产生了硬件系统,Nios II IDE或者Nios II 软件生成工具可以生成和硬件配置相匹配的定制的HAL系统库或者板支持包 (BSP)。如果在底层硬件上做了改动,则HAL设备驱动配置会自动地更新,避免了底层硬件的改动产生Bug的可能。

HAL设备驱动抽象使得应用程序和驱动程序之间很明显地区分开来。 驱动抽象促进了应用程序代码的可重用性,应用程序和底层硬件的通信依靠统一的接口函数,底层硬件的改动对应用程序的代码没有影响。而且,HAL标准使得和 已有外设相匹配的新外设的驱动程序编写起来更加简单。

在用户使用Nios II IDE创建新工程的时候,也同时创建了HAL系统库。用户不必创建或 拷贝HAL文件, 也不必编辑任何HAL的源代码。 Nios II IDE会为用户产生和管理HAL系统库。 HAL是基于一个特定的SOPC Builder系统。 SOPC Builder系统即是Nios II 处理器核和外设与 存储器集成在一起的系统(由 SOPC Builder)。在新建工程的时候,用户必须选择相应的硬件系统,见图6-5。

6.4.2 HAL 体系结构

HAL提供下面的服务:

? ? ? ? ?

同newlib ANSI C 标准库集成——提供用户熟悉的C标准库函数。 驱动程序——提供对系统中每个设备的访问。

HAL API——为HAL的服务提供了一个统一的、 标准的接口, 如设备访问、 中断处理等。

系统初始化——在main()执行之前,执行处理器和运行环境的初始化的任务。

设备初始化——在main()执行之前,例化和初始化系统中的每个设备。

图 6-22 显示了基于 HAL 系统的分层结构,从硬件层到用户程序,可以看到 HAL 将硬件层和应用程序层联系起来。

应用程序和驱动程序

嵌入式系统的软件开发分为两部分:应用程序开发和设备驱动程序的开发。应用程序开 发占有更大的比重,包括系统的main() 函数和其它的子程序。应用程序同系统硬件资源的 通信是通过C标准库函数或者HAL API。驱动程序是提供给应用程序开发人员开发应用程序之用的,驱动程序通过底层的硬件访问宏直接和硬件通信。 通用设备模型

HAL为嵌入式系统中的外设种类提供了通用设备模型。 如timers、 Ethernet MAC/PHY 芯 片、 字符型I/O外设。通用设备模型是HAL强大功能的核心。通用设备模型使得用户可以使 用统一的API来编写程序,不用考虑底层的硬件。 通用设备模型的种类:HAL 为下面种类的设备提供模型:

字符型设备 (Character-mode device) ——串行发送和/或接收字符的硬件外设, 如UART。

? 定时器设备(Timer devices)——能够对时钟脉冲计数,并且能够产生周期性的中断请 求的外设。

? 文件子系统(File subsystems)——提供访问存储在物理设备中的文件的一种机制。取 决于内部的实现,文件子系统驱动程序可以直接访问底层的设备,或者使用一个单独的 设备驱动程序。例如,用户可以使用Flash存储设备的HAL API来编写一个Flash的文件子系统来访问Flash。

? ?

以太网设备(Ethernet devices)——对网络协议栈(如Altera的Nios II版本的TCP/IP协议栈)提供以太网连接的访问。

DMA设备(DMA devices)——执行数据源、宿之间大批量数据传输的外

设。原和宿可以是存储器或其它设备,比如,以太网的连接。

Flash存储设备(Flash memory devices)——使用特殊的编程协议进行存储数据的非易失存储设备。

对应用程序开发人员的好处

HAL定义了一套用户用来初始化和访问每类设备的函数。不管设备硬件的底层实现如 何,API都是统一的。例如,要访问字符型的设备和文件子系统,用户可以使用C的标准库 函数,比如printf()和fopen()。对于应用程序的开发人员,不必去为这些种类的外设编写与硬件建立基本的通信的低层的程序。 对驱动程序开发人员的好处

每一个设备模型定义了一套必要的管理特定种类设备的驱动函数。 如果用户正在为一个新外设编写驱动程序,用户只需这套驱动函数。因此,驱动程序的开发任务是预先定义好了的和充分说明了的。另外,用户可以使用已有的 HAL函数和应用来访问设备,这样就节省了软件开发的工作量HAL调用驱动函数来访问硬件。

C标准库——newlib

HAL同ANSI C 标准库集成到一个运行环境。HAL使用的C标准库是针对嵌入式系统应 用的开源版本——newlib,所以能够和HAL和Nios II处理器很好地匹配。Newlib授权不需要 用户发布自己的源代码,也不需要为基于newlib的工程支付版税。 支持的外设

Altera提供很多外设在Nios II处理器的系统中使用, 大多数的Altera外设提供HAL设备驱 动程序,使得用户可以通过HAL API来访问硬件。下面的Altera外设提供完整的HAL支持:

?

字符型设备:

UART核。

JTAG UART核。

LCD 16207 显示控制器。

?

Flash 存储设备:

Flash芯片的通用flash接口。

Altera’s EPCS 串口配置设备控制器。

?

文件子系统:

基于Altera主机的文件系统。 Altera压缩只读文件子系统。

?

定时器设备:

定时器核。

?

DMA 设备:

DMA 控制器核。

Scatter-gather DMA 控制器核。

?

以太网设备:

三速Ethernet宏核。

LAN91C111 Ethernet MAC/PHY控制器。

LAN91C111和三速Ethernet元件需要μC/OS-II的支持, 第三方的厂商提供的外设此处没 有列出。所有的外设(来自Altera和第三方厂商) 必须提供一个头文件,头文件中定义了外设 的底层硬件接口。由此看来,所有的外设都在一定程度上支持HAL。然而,有些外设可能不提供设备驱动程序。如果没有驱动程序可用,用户只有利用头文件中的 定义来访问硬件,不要使用二进制的地址来访问外设。不可避免地,一些外设具有特殊的硬件特性,不能使用通用的API。为此,HAL提供了UNIX类型的 ioctl( )函数,因为硬件特性和外设相关,ioctl( ) 函数的选项在每个外设的描述文档中说明。一些外设提供专门的访问或不是基于HAL通用设备模型的函数来访问外设。例如,Altera在Nios II处理器系统中提供通用I/O (PIO)核。 PIO外设不属于HAL提供的设备模型中的任何一种,所以只提供了一个头文件和一些专门的访问或函数。想了解对某一个外设的软件支持,参考外设的描述文 档。要获得更详细的信息,参阅Quartus? II Handbook, Volume 5: Embedded Peripheral。 第5节 使用 HAL开发应用程序

本章讨论基于Altera的硬件抽象层系统库开发程序的方法。HAL的API对于第一次接触 Nios II处理器的软件开发人员是很容易理解的。基于HAL的程序,使用ANSI C标准库函数 和运行环境,并且使用HAL API的通用设备模型来访问硬件。HAL API的规范遵照ANSI C 标准库的函数,尽管ANSI C标准库和HAL系统库是相互独立的。ANSI标准库和HAL的紧密集成使得用户开发程序时, 可以不用直接调用HAL系统库的函数。 例如, 可以使用ANSI C 标准库的I/O函数,如printf( )和scanf()来控制字符型的设备。

6.5.1 Nios II IDE 工程结构

基于HAL系统库的软件工程的创建和管理是与Nios III DE紧密联系的。这部分讨论Nios II IDE工程来理解HAL。图6-23为Nios II IDE工程结构,说明了HAL系统库的位置,箭头表示了各个工程之间的依赖关系。

基于HAL的Nios II程序包含两个Nios II IDE工程,用户程序是在用户应用工程中,用户 程序是依赖于独立的系统库工程(HAL system library project)的。 应用工程所有的用户开发的代码,可执行文件最终由该工程编译生成。当用户创建应用

工程时,Nios II IDE创建HAL系统库工程。HAL系统库工程包含所有的用户程序与硬件的接 口的必要信息。生成可执行文件时,所有的和用户的SOPC Builder系统相关的HAL驱动程序 都被加入到系统库工程。HAL设置作为系统库的属性的被保存。系统库工程依赖SOPC Builder系统,SOPC Builder系统由一个.ptf 文件定义,该文件由SOPC Builder生成。Nios II IDE管理HAL系统库并且更新驱动程序的配置以准确地反映系统 硬件的变化。如果SOPC Builder系统发生了改动(即.ptf 文件更新了),Nios II IDE会在下次用户生成和运行应用程序时重新生成HAL系统库。

这种工程之间的依赖结构使得用户程序不用随着硬件改动而进行修改, 用户开发和调试代码时不用担心自己的程序是否和目标硬件相匹配。简而言之,基于HAL系统库的程序总是和目标硬件相同步的。

6.5.2 系统描述文件——system.h

system.h 文件是HAL系统库的基础,system.h文件提供了完整的Nios II系统硬件的软件 描述。它的作用是将硬件和软件设计连接起来。对应用程序开发者来说,不是system.h中所有的信息都是有用的,很少的情况必须要在用

户的C源程序中包 含system.h。虽然如此,system.h 能回答这个最基本的问题:系统中都有什么硬件?

system.h文件描述系统中的每个外设,并提供如下的细节: 外设的硬件配置。 外设的基地址。 中断请求优先级。 外设的符号名。

在软件工程第一次编译的时候,Nios II IDE为HAL系统库工程生成system.h。system.h 内容取决于硬件配置和用户设置的HAL系统库的属性。用户不要编辑system.h。

下面给出的是从system.h文件中截取出来的一部分代码, 是关于定时器的设备的预定义, 其它设备的定义与定时器类似。 #define SYS_CLK_TIMER_NAME \#define SYS_CLK_TIMER_TYPE \#define SYS_CLK_TIMER_BASE 0x00000820 #define SYS_CLK_TIMER_SPAN 32 #define SYS_CLK_TIMER_IRQ 1

#define SYS_CLK_TIMER_ALWAYS_RUN 0 #define SYS_CLK_TIMER_FIXED_PERIOD 0 #define SYS_CLK_TIMER_SNAPSHOT 1 #define SYS_CLK_TIMER_PERIOD 10

#define SYS_CLK_TIMER_PERIOD_UNITS \#define SYS_CLK_TIMER_RESET_OUTPUT 0

#define SYS_CLK_TIMER_TIMEOUT_PULSE_OUTPUT 0 #define SYS_CLK_TIMER_MULT 0.001 #define SYS_CLK_TIMER_FREQ 85000000

#define ALT_MODULE_CLASS_sys_clk_timer altera_avalon_timer

6.5.3 数据宽度和 HAL 类型定义 对于嵌入式处理器,如Nios II处理器,准确的数据的宽度和精度是非常重要的。由于 ANSI C数据类型没有明确地定义数据宽度,HAL使用了一套标准的类型定义,支持ANSI C类型,但是数据宽度取决于编译器的约定。头文件alt_types.h 定义了HAL类型, Table 6-3 是HAL的数据类型定义。表6-4给出了Altera采用的GNU编译器的ANSI C的数据类型。

6.5.4 UNIX 风格的接口 HAL API提供了许多UNIX-风格的函数, UNIX风格的函数使得Nios II的新手程序员不 会感到开发环境的陌生,并且可以很容易地将原有的代码移植到HAL的环境下运行。HAL主要使用这些函数给ANSI C标准库提供系统接口。例如,C标准库调用的执行设备访问的函 数都在stdio.h中进行了定义。下面的列表是可用的UNIX风格的函数的完全列表,其中使用最多的是和FILE I/O相关的函数: _exit( ) close( ) fstat( ) getpid( )

gettimeofday( ) ioctl( ) isatty( )

kill( ) lseek( ) open( ) read( ) sbrk( )

settimeofday( ) stat( ) usleep( ) wait( ) write( )

6.5.5 文件系统

HAL提供支持UNIX风格的文件访问的机制。用户可以在硬件中任何可用的存储设备上建立文件系统。用户能够在基于HAL的文件系统中访问文件,可以使用 newlib C库中的文件I/O 函数,(例如, fopen( ), fclose( )和fread( ))或使用HAL系统库提供的UNIX风格的文件I/O。HAL提供如下的UNIX风格的函数来进行文件的处理: close( ) fstat( ) ioctl( ) isatty( ) lseek( ) open( ) read( ) stat( ) write( )

HAL将一个子文件系统注册为HAL文件系统的一个挂载点(mount point)。要访问一个 挂载点下的文件,则指向该挂载点对应的文件子系统。例如,如果一个只读zip文件子系统(zipfs)被挂载为/mount/zipfs0,zipfs文 件子系统处理对/mount/zipfs0/myfile 进行fopen( )的调 用。

这里没有当前目录的概念,软件访问文件时必须使用绝对路径。HAL文件结构允许用户 通过UNIX风格的路径名来管理字符型的设备。HAL将字符型设备注册为HAL文件系统中的节点。按照约定,system.h中定义设备节点的名字按照 前缀 /dev/加上SOPC Builder中分配给硬件的名字,例如,在system.h 文件中,SOPC Builder中的UART外设uart1的节点名为/dev/uart1。

下面的代码给出了从只读zip文件子系统——ozipfs读字符的操作,这个文件子系统被注 册为HAL文件系统中的一个节点。头文件stdio.hstddef.h和stdlib.h同HAL一起被包含。从文件子系统读字符的例子 #include #include #include #define BUF_SIZE (10)

int main(void) {

FILE* fp;

char buffer[BUF_SIZE];

fp = fopen (“/mount/rozipfs/test”, “r”); if (fp == NULL) {

printf (“Cannot open file.\\n”); exit (1); }

fread (buffer, BUF_SIZE, 1, fp); fclose (fp); return 0; }

6.5.6 使用字符型设备

字符型设备串行地发送和接收字符。常见的如通用异步收发器(UART)。字符型设备在 HAL系统库中注册为节点。通常,程序将一个文件描述符同设备名联系起来,则使用在file.h 中定义的ANSI C文件操作对文件进行读写字符。HAL也支持标准输入、标准输出和标准错误的概念,允许程序调用stdio.h I/O函数。标准输入(Standard Input)、标准输出(Standard Output)和标准错误(Standard Error)使用标准输入(stdin)、 标准输出(stdout)和标准错误(stderr)是实现简单控制台I/O最容易的方法。HAL系统库在后台管理stdin、stdout和 stderr,允许用户通过这些通道发送和接收字符,而不是管理文件描述符。例如,系统库将printf()的输出指向标准输出,将perror() 指向标准错误设备。用户可以通过在系统库属性中的设置将每一个通道同一个特定的硬件设备联系起来。 下面的代码显示的是经典的Hello World程序,该程序发送字符到与stdout相关联的设备。 例子: Hello World #include int main () {

printf (“Hello world!”); return 0; }

当使用UNIX风格的API,用户可以使用在unistd.h中定义的文件描述符stdin、stdout和stderr来分别访问标准输入、标准输出和 标准错误的字符I/O流。unistd.h is 在Nios II EDS中作为newlib C库的一部分安装。 对字符型设备的通用访问

访问字符型设备 (除了stdin, stdout, or stderr方法),就像打开和写一个文件一样简单。下 面的代码演示的写一条信息到名为uart1的UART设备。 例子:写字符到UART

#include #include int main (void) {

char* msg = “hello world”; FILE* fp;

fp = fopen (“/dev/uart1”, “w”); if (fp!=NULL) {

fprintf(fp, “%s”,msg); fclose (fp); }

return 0; }

6.5.7 使用文件子系统

HAL的文件子系统通用设备模型允许使用C标准库的文件I/O函数访问存储在存储设备中的数据。例如,Altera的zip只读文件子系统提供对存储在 flash存储器中的文件子系统的只读访问。一个文件按子系统负责管理一个挂载点下面的所有的文件I/O访问。例如,一个文件子系统注册为挂载点 /mnt/rozipfs ,该目录下的所有的文件访问,如

fopen(“/mnt/rozipfs/myfile”, “r”),定向为该文件子系统。如同字符型设备,用户可以在file.h

中定义的使用C文件I/O函数处理文件子系统中的文件。如,fopen()和fread()。 6.5.8 使用定时器设备

定时器设备是可以计数时钟的硬件设备,并可以产生周期性的中断请求。用户可以使用定时器设备提供一些和时间相关的设备,如HAL系统时钟、报警器、和时间 测量。使用定时器设备,Nios II处理器系统在硬件上必须包含一个定时器外设。HAL API提供两种定时器设备的驱动程序:

系统时钟驱动——这种驱动支持报警(alarms),用户在调度程序会用到。

时间标记驱动程序——该驱动程序支持高精度的时间测量。 一个单独的定时器外设可以作为一个系统时钟或是时间标记,但是不能兼顾。HAL特定 的访问定时器设备的API函数在sys/alt_alarm.h和

sys/alt_timestamp.h中定义。.系统时钟驱动程序HAL系统时钟的驱 动程序提供周期的 “心跳节拍”(“heartbeat”),使得系统时钟按照每 一个节拍进行递增。软件可以使用系统时钟设备来在指定的时间执行函数,并且可以获得时序信息。用户通过在Nios II IDE的系统库属性中设置来选择指定的定时器外设作为系统时钟设备。

HAL提供如下的标准的UNIX函数的实现:gettimeofday()、settimeofday()和times()。这些函数返回的时间是基于 HAL系统时钟的。系统库时钟测量时间是以“ticks”为单位的。对于要和软件、硬件都打交道的嵌入式工程师,不要将HAL系统时钟和驱动Nios II 处理器的时钟信号混淆。HAL

系统时钟的滴答”ticks”是要远大于硬件系统时钟的。system.h d定义了时钟的频率。运行时,用户可以调用alt_nticks()函数来获得系统时钟的当前值。该函数返回自从系统复位之后经历的时间。用户能获得系统 时钟速率,通过调用函数alt_ticks_per_second()。HAL定时器驱动程序在创建系统时钟的实例时初始化其频率。标准的UNIX函数 gettimeofday()可以获得当前的时间。用户必须首先通过调用settimeofday()函数来校正时间。而且,用户可以使用times() 函数来或的已经发生的滴答数目信息。这些函数的原型在times.h中。 报警(Alarm)

用户可以注册在指定的时刻执行的函数使用HAL alarm工具。软件程序注册一个报警通过调用函数alt_alarm_start(): int alt_alarm_start (alt_alarm* alarm, alt_u32 nticks,

alt_u32 (*callback) (void* context), void* context);

在经过了n个ticks之后函数callback调用。当调用发生时,输入参数context作为输入变量传递给callback。 alt_alarm_start()初始化输入变量alarm所指向的结构。用户不必去初始化它。 callback函数能够复位alarm。注册的callback函数的返回值是直到下一个callback函数调用的tick的数目。返回值为0,表示 alarm应该停止。用户可以通过调用alt_alarm_stop()手动取消一 个alarm。Alarm的callback函数是在中断的语境(context)中执行的,这强加了函数的限制,用户必须观察何时写alarm callback。 下面的代码片段演示了为周期为1秒的callback注册一个alarm。 例:使用周期的Alarm Callback函数 #include #include

#include “sys/alt_alarm.h” #include “alt_types.h” /*

* The callback function. */

alt_u32 my_alarm_callback (void* context) {

/* This function will be called once/second */ return alt_ticks_per_second(); } ...

/* The alt_alarm must persist for the duration of the alarm. */ static alt_alarm alarm; ...

if (alt_alarm_start (&alarm, alt_ticks_per_second(), my_alarm_callback,

NULL) < 0) {

printf (“No system clock available\\n”); }

时间标记(timestamp)驱动程序 有些时候,用户想测量时间间隔,精度要求比HAL系统时钟提供的要高。HAL使用时 间标记驱动程序提供高分辨率的定时函数。时间标记驱动函数提供单调的增加的计数器,用 户可以采样来获得定时信息。在系统中,HAL只支持一个定时标记驱动程序。用户可以通过在Nios II IDE中通过设置系统库的属性来选择一个指定的硬件定时器外设作为时间标记设备。

如果有时间标记驱动程序,函数alt_timestamp_start()和

alt_timestamp()可用。Altera提供的时间标记驱动程序使 用用户在系统库属性中选择的定时器。调用函数alt_timestamp_start() 启动计数器,接下来调用alt_timestamp(),则返回时间标记计数器的当前值。再次调用

alt_timestamp_start()则复位计数 器为0。当计数器的值达到232– 1时,时间标记驱动的行为没有定义。调用函数alt_timestamp_freq(),用户可以获得时间标记计数器增加的速率。该速率典型值为Nios II处理器运行的硬件频率,通常是每秒钟几百万次。时间标记驱动程序在alt_timestamp.h头文件中定义。 下面的代码片段展示的是使用时间标记设备来测量代码的执行时间。 例:使用时间标记来测量代码的执行时间 #include

#include “sys/alt_timestamp.h” #include “alt_types.h” int main (void) {

alt_u32 time1; alt_u32 time2; alt_u32 time3;

if (alt_timestamp_start() < 0) {

printf (“No timestamp device available\\n”); } else {

time1 = alt_timestamp();

func1(); /* first function to monitor */ time2 = alt_timestamp();

func2(); /* second function to monitor */ time3 = alt_timestamp();

printf (“time in func1 = %u ticks\\n”, (unsigned int) (time2 – time1));

printf (“time in func2 = %u ticks\\n”, (unsigned int) (time3 – time2));

printf (“Number of ticks per second = %u\\n”, (unsigned int)alt_timestamp_freq()); }

return 0; }

6.5.9 使用 flash 设备

HAL提供对非易失性flash存储器设备的通用模型支持。Flash存储器使用特殊的编程协议来存储数据。HAL API提供写数据到flash的函数。例如,用户可以使用这些函数实现基于flash的文件子系统。 HAL API也提供读flash的函数,尽管通常不是必须的。对大多数flash设备,当对其进行 读操作时,程序将flash存储器空间看成简单的存储器,不需要调用特殊的HAL API函数。如果flash设备具有特殊的读数据的协议,如Altera EPCS串行配置设备,用户必须使用HAL API进行读写数据。 下面介绍flash设备模型的HAL API。下面的两个API提供不同的层次flash访问。

简单flash访问—函数写缓冲器数据到flash和从flash读数据都是以分区(block)的层次。 在写数据时,如果缓冲器比一个完整的分区小的话,这些函数擦除原来存在于flash中的在新写入的数据之上和之下的数据。 精细flash访问—函数写数据到flash和从flash读数据都是以缓冲器的层次。在写数据到 flash时,如果缓冲器比完整的分区小,这些函数会保留之前的存在于flash之中的在新写 入的数据之上和之下的数据。这个功能通常在管理文件子系统时需要。访问flash设备的API函数定义sys/alt_flash.h。 简单Flash访问

该接口包含函数:alt_flash_open_dev()、alt_write_flash()、

alt_read_flash()和 alt_flash_close_dev()。用户调用alt_flash_open_dev()函数来打开一个flash设备,该函数返回一个 flash设备的文件句柄。该函数需要一个单独的变量,就是flash设备的名字,设备名在system.h中定义。当获得了句柄,用户可以使用 alt_write_flash()函数写数据到flash设备。原型为:

int alt_write_flash(alt_flash_fd* fd, int offset,

const void* src_addr, int length )

调用该函数,写数据到由句柄fd标识的flash设备。驱动程序写数据从flash设备的基地址偏移offset开始。 写入的数据来自src_addr指向的地址, 写入的数据量为length。 alt_read_flash() 函数用来从flash设备读数据。原型为:

int alt_read_flash( alt_flash_fd* fd, int offset,

void* dest_addr, int length )

调用alt_read_flash()从fd标识的flash设备读数据,地址为flash的基地址偏移offset。该函 数写数据到dest_addr指向的地址,数据量为length。对大多数flash设备,用户访问时可以按照标准的存储器进行,不必要使用 alt_read_flash()。函数alt_flash_close_dev()需要一个文件句柄来关闭设备。函数的原型为:void alt_flash_close_dev(alt_flash_fd* fd ) 下面的代码显示简单flashAPI函数的使用,访问的flash设备名为/dev/ext_flash,在 system.h中定义。 例:使用简单Flash API函数 #include

#include

#include “sys/alt_flash.h” #define BUF_SIZE 1024 int main () {

alt_flash_fd* fd; int ret_code;

char source[BUF_SIZE]; char dest[BUF_SIZE];

/* Initialize the source buffer to all 0xAA */ memset(source, 0xAA, BUF_SIZE);

fd = alt_flash_open_dev(“/dev/ext_flash”); if (fd!=NULL) {

ret_code = alt_write_flash(fd, 0, source, BUF_SIZE); if (ret_code==0) {

ret_code = alt_read_flash(fd, 0, dest, BUF_SIZE); if (ret_code==0) { /*

* Success.

* At this point, the flash is all 0xAA and we * should have read that all back into dest */

} }

alt_flash_close_dev(fd); } else {

printf(“Can’t open flash device\\n”); }

return 0; }

擦除分区

通常,flash存储器被分成许多分区(block)。alt_write_flash()函数在写数据之前,可能 需要先擦除一个分区的内容。 这种情况下, 该函数不做任何保留分区中已存在的内容的工作, 这将导致不期望的数据擦除。 如果用户希望保持已有的flash存储器内容, 使用精细flash函数。 精细Flash访问

精细flash访问的函数对写flash的内容提供完全的控制。alt_get_flash_info()、 alt_erase_flash_block()和alt_write_flash_block()。从flash存储器的本质上来说,用户不能擦除 一个分区中单独的一个地址。用户一次必须擦除整个分区。因此要改变一个分区内特定的位置的内容,而不改变分区内其它位置的内容,用户必须先读出分区的整个 内容到缓冲器中, 在其中改变相应的内容, 擦除flash分区, 最后, 将整个分区内容写回到flash存储器。 精细flash 访问函数自动化上述的过程。 alt_get_flash_info()函数获得擦除区域的数目,每个区域内分区的数目,每个分区的大小。函 数原型为:

int alt_get_flash_info( alt_flash_fd* fd, flash_region** info, int* number_of_regions) 如果函数调用成功,返回number_of_regions指向的地址,包含有flash存储器中擦除区域 的数目,*info指向flash_region结构的数组。数组是文件描述符的一部分。

flash_region结构在sys/alt_flash_types.h中定义,其类型定义为: typedef struct flash_region {

int offset; /* Offset of this region from start of the flash */ int region_size; /* Size of this erase region */

int number_of_blocks; /* Number of blocks in this region */ int block_size; /* Size of each block in this erase region */ }flash_region;

调用alt_get_flash_info()获得的信息,用户能够擦除或编程flash中的单个分区。

alt_erase_flash()函数擦除flash中一个单独的分区。其原型为: int alt_erase_flash_block( alt_flash_fd* fd, int offset, int length)

flash存储器由句柄fd标识,分区由相对于flash的起始地址的offset标识,分区的大小通过 length传递。

alt_write_flash_block()函数写flash存储器中一个单独的分区。其原型为:

int alt_write_flash_block( alt_flash_fd* fd, int block_offset, int data_offset, const void *data, int length)

该函数写由句柄fd标识的flash存储器,写数据到相对于flash基地址偏移block_offset处,该函数写由data指向的位置起始length长度的字节。下面的代码演示的是精细flash访问的函数。 例:使用精细Flash访问API函数 #include

#include \#define BUF_SIZE 100 int main (void) {

flash_region* regions; alt_flash_fd* fd;

int number_of_regions; int ret_code;

char write_data[BUF_SIZE];

/* Set write_data to all 0xa */ memset(write_data, 0xA, BUF_SIZE);

fd = alt_flash_open_dev(EXT_FLASH_NAME); if (fd) {

ret_code = alt_get_flash_info(fd, ®ions,

&number_of_regions);

if (number_of_regions && (regions->offset == 0)) {

/* Erase the first block */

ret_code = alt_erase_flash_block(fd, regions->offset,

regions->block_size); if (ret_code) { /*

* Write BUF_SIZE bytes from write_data 100 bytes into * the first block of the flash ret_code = alt_write_flash_block ( fd,

regions->offset,

regions->offset+0x100,

write_data, BUF_SIZE ); } } }

return 0; }

6.5.10 使用 DMA 设备

HAL提供直接存储器访问的设备抽象模型。 这些外设执行从数据源到目的地的大批量的 数据传输。源和目的地可以是存储器或其它的设备,如以太网连接。在HAL DMA设备模型 中,DMA传输属于一下两种分类之一:发送或接收。因此,HAL提供两个设备驱动来实现发送通道和接收通道。发送通道从数据源的缓冲器获得数据,发送数据 到目的设备。接收通道接收数据,并将数据忖道目的缓冲器中。取决于底层的硬件实现,软件可能只能访问两中 端点中的一个。图6-24显示了三种基本的DMA传输。存储器之间的数据拷贝同时包括接收 和发送DMA通道。

访问DMA设备的API在sys/alt_dma.h中定义。DMA设备操作物理存储器的内容,因此, 当读和写数据时,用户必须考虑缓存的交互。 DMA发送通道

DMA发送请求使用DMA发送句柄进行排队。要获得一个句柄, 使用函数 alt_dma_txchan_open()函数。该函数需要一个变量,要使用的设备名字,在system.h中定义。 下面的代码展示的是怎样获得DMA发送设备dma_0的句柄。 例:获得DMA设备的文件句柄

#include

#include “sys/alt_dma.h” int main (void) {

alt_dma_txchan tx;

tx = alt_dma_txchan_open (“/dev/dma_0”);

if (tx == NULL) {

/* Error */ } else {

/* Success */ }

return 0; }

用户可以使用该句柄通过函数alt_dma_txchan_send()来递交一个发送请求。该函数原型 为:

typedef void (alt_txchan_done)(void* handle); int alt_dma_txchan_send (alt_dma_txchan dma, const void* from, alt_u32 length,

alt_txchan_done* done, void* handle);

调用alt_dma_txchan_send()向通道dma递交发送请求。变量length指定要发送的字节数, 变量from 指定源地址。函数在整个DMA传输结束之前返回。返回值指示请求是否成功,并排入队列。负的返回值表示请求失败。当传输结束,函数done被调用,提供通 知。两个附加的函数用来操控DMA发送通道:alt_dma_txchan_space()和 alt_dma_txchan_ioctl()。alt_dma_txchan_space()函数返回还有多少个发送请求可以加入到发 送队列中去。

alt_dma_txchan_ioctl()函数执行发送设备中与设备相关的操控。 如果使用Altera Avalon DMA设备发送到硬件设备(不是存储器到存储器的传输),调用 alt_dma_txchan_ioctl()函数,请求变量设成ALT_DMA_TX_ONLY_ON。 DMA接收通道

DMA 接收通道的工作方式同DMA发送通道相似。软件使用alt_dma_rxchan_open()函数可以获得DMA接收通道的句柄。 用户然后可以使用alt_dma_rxchan_prepare()函数来递交接收请求。alt_dma_rxchan_prepare()函数原型为:

typedef void (alt_rxchan_done)(void* handle, void* data); int alt_dma_rxchan_prepare (alt_dma_rxchan dma, void* data,

alt_u32 length,

alt_rxchan_done* done, void* handle);

void* handle);

调用该函数向dma通道递交一个接收请求, 长度为length比特的数据要被放到data指向的 地址。该函数在DMA传输结束前返回。返回值指示请求是否被排入队列。一个负的返回值 表示请求失败。当传输结束,用户指定的函数

done被调用,变量handle提供通知和接收数据 的指针。两个附加的函数被提供来操控DMA接收通道:alt_dma_rxchan_depth()和 alt_dma_rxchan_ioctl()。 如果用户使用Altera Avalon DMA设备来从硬件(非存储器之间的传输)接收数据,调 用alt_dma_rxchan_ioctl()函数,请求变量设成

ALT_DMA_RX_ONLY_ON。具体使用细节参 看Nios II Software Developer’s Handbook的HAL API 部分的alt_dma_rxchan_ioctl()章节。 alt_dma_rxchan_depth()返回设备可允许的最大的接收请求数。

alt_dma_rxchan_ioctl()执行接 收设备中设备相关的操纵。详细信息参考HAL API章节。 下面的代码演示的一个完整的是递交DMA接收请求以及main()中的直到传输完成的程 序例子。

例子:接收通道的DMA传输 #include #include #include

#include “sys/alt_dma.h” #include “alt_types.h”

/* flag used to indicate the transaction is complete */ volatile int dma_complete = 0;

/* function that is called when the transaction completes */ void dma_done (void* handle, void* data) {

dma_complete = 1; }

int main (void) {

alt_u8 buffer[1024]; alt_dma_rxchan rx;

/* Obtain a handle for the device */

if ((rx = alt_dma_rxchan_open (“/dev/dma_0”)) == NULL) {

printf (“Error: failed to open device\\n”); exit (1); } else {

/* Post the receive request */

if (alt_dma_rxchan_prepare (rx, buffer, 1024, dma_done, NULL) < 0) {

printf (“Error: failed to post receive request\\n”); exit (1);

}

/* Wait for the transaction to complete */ while (!dma_complete);

printf (“Transaction complete\\n”); alt_dma_rxchan_close (rx); }

return 0; }

存储器到存储器的DMA传输

从一个存储器缓冲器拷贝数据到另一个缓冲器,同时包括接收和发送DMA驱动程序。下面的代码演示的是请求队列中,一个接收请求后面跟着一个发送请求的过程。

例:存储器之间拷贝数据 #include #include

#include \#include \

static volatile int rx_done = 0; /*

* Callback function that obtains notification that the data has * been received. */

static void done (void* handle, void* data) {

rx_done++; } /* * */

int main (int argc, char* argv[], char* envp[]) {

int rc;

alt_dma_txchan txchan; alt_dma_rxchan rxchan;

void* tx_data = (void*) 0x901000; /* pointer to data to send */ void* rx_buffer = (void*) 0x902000; /* pointer to rx buffer */ /* Create the transmit channel */

if ((txchan = alt_dma_txchan_open(\{

printf (\exit (1); }

/* Create the receive channel */

if ((rxchan = alt_dma_rxchan_open(\{

printf (\exit (1); }

/* Post the transmit request */

if ((rc = alt_dma_txchan_send (txchan, tx_data, 128, NULL,

NULL)) < 0) {

printf (\exit (1); }

/* Post the receive request */

if ((rc = alt_dma_rxchan_prepare (rxchan, rx_buffer, 128, done,

NULL)) < 0) {

printf (\exit (1); }

/* wait for transfer to complete */ while (!rx_done);

printf (\return 0; }

6.5.11 启动顺序和入口点

通常用户程序的入口点就是main( )函数。这里还另有一个入口点alt_main( ),用户可以使用它来获得对启动顺序的更大的控制。程序的入口地址是main( )还是alt_main( )的区别就是托管(hosted)的应用和独立

(free-standing)的应用之间的区别。ANSI C标准定义托管应用是调用main( )函数来开始执行的应用,在main( )开始时,一个托管的应用运行环境和系统服务都已准备就绪。在HAL环境中就是这样的,如果是Nios II开发的新手,HAL的托管环境会帮助用户很容易地快速上手,因为用户不必考虑系统中包含什么样的设备以及怎样取初始化它们。HAL户初始化整个系统。 ANSI C标准也提供了一个可选的程序入口,可以避免自动的初始化,而由Nios II 程序 员手动初始化要使用的硬件。alt_main( )提供了一个独立的环境,使用户获得对系统初始

化 的完全控制。独立的环境下,初始化系统的任务由程序员手动完成。例如,在独立的环境下,对printf( ) 的调用不会正确工作,除非alt_main( )首先初始化字符型的设备,并且将stdout重定位到该设备。 基于HAL程序的启动顺序

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

HAL提供的系统的初始化代码按照如下的启动顺序: 刷新指令和数据缓存 配置堆栈指针

配置全局指针寄存器 将BSS段清零

如果系统没有boot loader,将任何运行地址在RAM中的连接器段拷贝到RAM,如读写 数据(.rwdata)、只读数据(rodata)和异常向量表。

调用alt_main( )。 HAL提供一个缺省的alt_main( )函数的实现,该函数执行如下的步骤:

调用ALT_OS_INIT( ) 执行任何必要的操作系统的初始化,如果系统不包含OS调度程 序,这个不起作用。

如果用户的HAL包含操作系统,初始化alt_fd_list_lock信号, 该信号控制对HAL文件系 统的访问。 初始化中断控制器,使能中断。

调用alt_sys_init( )函数,初始化系统中所有设备的驱动程序和软件组件,Nios II IDE为 每一个HAL系统库创建和管理alt_sys_init.c。 重定位C 标准I/O通道(stdin, stdout, and stderr),以使用适当的设备。

使用_do_ctors() 函数调用C++ 构造器。 注册C++ 解构器,以供系统关闭时调用。 调用main( )

调用exit( ) alt_main.c,同Nios II EDS一道安装,提供了缺省的实现,用户可以在

path>/components/altera_hal/HAL/src处找到。 定制启动顺序

通过在用户的Nios II IDE工程中定义alt_main( ),用户可以实现自己定制的启动顺序。 这给了用户对启动顺序完全的控制权。给了用户选择使能HAL服务的能力。如果用户的应用需要alt_main( ) 入口点,用户可以拷贝alt_main的缺省实现,在此基础上根据自己的需要进行修改。

alt_main( )函数调用main( )函数。当main( ) 返回后,缺省的alt_main( )进入一个死循环。 用户的定制alt_main( ) 可以通过调用exit( )而结束。 alt_main( )函数的原型是: alt_main (void)

在生成程序的时候,HAL的所有源文件和包含的文件都位于一个搜索路径。生成系统首 先搜索系统库工程的路径。这使得用户的实现会覆盖缺省的设备驱动和系统代码。例如,用 户可以提供自己的定制的alt_sys_init.c ,把它放到用户的系统工程目录中。 用户的定制文件优先于自动生成的文件。

第6节 异常处理

本节讨论在Nios II处理器体系结构下怎样编写处理异常的程序。 重点放在怎样使用HAL 注册用户定义的中断服务程序(Interrupt Service Routine——ISR)来处理硬件中断请求。

Nios II异常处理是以经典的RISC方式实现的, 即所有的异常类型都由一个异常处理程序 处理。所以,所有的异常(硬件和软件)由位于异常地址(exception address)的程序处理。 Nios II处理器提供如下的异常类型: 硬件中断

软件异常,分成如下三种: 未实现的指令 软件陷阱 其它的异常 6.6.1 异常处理概念

下面列出一些基本的异常处理的概念,还有和其相关的HAL术语:

? ? ? ? ? ? ?

Application context(应用语境)—— 在正常的程序执行过程中Nios II处理器和HAL的 状态,在异常处理程序之外。

ontext switch (语境切换)—— 发生异常时保存Nios II处理器寄存器的过程,在从中 断服务程序返回时进行恢复。

exception (异常)—— 任何的中断正常程序执行的条件或是信号。 exception handler (异常处理程序)—— 完整的软件程序的系统,服务所有的异常, 在必要的情况下将控制权交给ISR。

exception overhead(异常开销)—— 异常处理需要的附加的处理。一个程序的异常开 销是被所有的context switches占用的时间的总和。 hardware interrupt(硬件异常)—— 由硬件设备的信号引起的异常。 implementation-dependent instruction (和处理器实现相关的指令)—— 不被Nios II内核所有实现支持的Nios II处理器指令。例如mul和div指令就是和实现相关的,因为在 Nios II/e 内核中它们不被支持。 interrupt context (中断语境)—— 当异常处理程序执行时Nios II处理器和HAL的状 态。

interrupt request (IRQ:中断请求) —— 来自外设的请求硬件中断的信号。 interrupt service routine (ISR:中断服务程序)—— 处理一个单独的硬件中断的软件 程序。

invalid instruction(无效的指令)——在任何的Nios II处理器的实现中都没有定义的指 令。 software exception(软件异常) —— 由软件条件引起的异常。包括未实现的指令和陷阱指令。

? ? ? ? ?

?

unimplemented instruction(未实现的指令)—— 用户系统的特定的Nios II内核不支持的实现相关的指令。例如,Nios II/e内核中,mul和div是未实现的。

other exception(其它异常)—— 不是硬件中断或陷阱的异常。

?

6.6.2 硬件如何工作

Nios II处理器能够响应软件异常和硬件中断。可支持32个中断源。软件可以对这些中断 信号进行优先级的排序,虽然这些中断信号自身本质上没有优先级的区别。当Nios II处理器 响应一个异常时,做如下的工作: 1. 保存状态寄存器到estatus寄存器。这意味着如果硬件中断被使能了,estatus寄存 器的EPIE 位为有效。 2. 禁止硬件中断。

3. 保存下一个执行地址到ea (r29)寄存器。 4. 将控制权交到Nios II处理器异常地址。

Nios II的异常和中断没有采用中断向量表的处理,所以同一个异常地址接收所有种类的 中断和异常的控制。位于异常地址处的异常处理程序必须决定异常或中断的种类。 软件经常使用中断和外部设备进行通信,当外设发出中断请求IRQ时,会导致处理器的正常的执行出现一个异常。当这样的一个IRQ发生,一个相应的中断服务 程序——ISR必须处理该中断,并且处理完成之后,返回到处理器中断之前的状态。.当使用Nios II IDE创建系统库工程时,IDE包含了所有需要的ISR。用户不必去写HAL ISR,除非用户要和定制的外设通信。作为参考,本节介绍HAL系统库提供的处理硬件中断 程序的框架。用户参考已有的Altera SOPC Builder的组件的中断处理来获得编写HAL ISR的例子。

6.6.2.1HAL 的 ISR API

HAL系统库提供API 来帮助用户简化ISR的创建和维护。API也适用于基于实时操作系 统的程序,因为全部的HAL API对基于RTOS程序都是可用的。HAL API定义了如下的函数来管理硬件中断的处理: alt_irq_register() alt_irq_disable() alt_irq_enable()

alt_irq_disable_all() alt_irq_enable_all() alt_irq_interruptible() alt_irq_non_interruptible() alt_irq_enabled()

使用HAL API来实现ISR需要如下的步骤: 1. 编写处理特定设备中断的ISR。

2. 用户程序必须通过调用alt_irq_register()函数来注册ISR,调用alt_irq_enable_all()函数 来使能中断。

6.6.2.2 编写 ISR

用户编写的ISR必须符合alt_irq_register()函数的原型。 ISR函数的原型必须符合如下的形式:

void isr (void* context, alt_u32 id) context和id参数的定义是和alt_irq_register()函数是相同的。

从HAL异常处理系统的角度来看,ISR最重要的功能是清除相关的外设的中断条件。清除中断条件的步骤是和外设有关的。要获得详细的信息,参考 Quartus? II Handbook, Volume 5: Altera Embedded Peripherals. 当ISR完成中断服务之后,必须返回到HAL异常处理程序。当完成恢复应用程序的现场 之后,HAL异常处理程序发出eret。 受限的环境 ISR在一个受限的环境中运行,许多的HAL API调用是不可用的。例如,访问HAL文件 系统是不允许的。作为一个普遍的规则,当编写用户自己的ISR时,不要包括可能妨碍中断 等待的函数调用。Nios II Software Developer’s Handbook中的HAL API Reference 章节给出了那些在ISR中不可用的API函数。 当在ISR调用ANSI C 标准库函数时,一定要小心。要避免使用C 标准库中的I/O API, 因为调用这些函数会导致系统的死锁,即系统可能会永久地陷入ISR。 特别地,在ISR中不要调用printf(),除非能够确定stdout(标准输出)映射到一个非基于中断的设备驱动程序。否则,printf()可能使 系统死锁,一直等待一个不会发生的中断,因为中断已经被禁止了。

6.6.2.3 注册 ISR

在软件可以使用ISR之前,用户必须先注册该ISR,注册中断服务程序通过调用 alt_irq_register()。alt_irq_register()的原型为: int alt_irq_register (alt_u32 id, void* context,

void (*isr)(void*, alt_u32)); 函数原型具有如下的参数:

?

id 是设备的硬件中断号, 在system.h中定义。 中断优先级和IRQ号成反比, 中断号越低,优先级越高。因此,IRQ0 代表最高的优先级中断,而IRQ31 为最低优先级的中断。

context 是一个用来向ISR传递语境(context)相关信息的指针,可以指向任意ISR相关 的信息。 context的值对HAL是不透明的。 context完全是为了用户定义ISR方便而提供的。

isr 是响应IRQ id而被调用的函数指针。提供给该函数两个参数context指针和id。注册isr 为null指针,导致中断被禁止。

?

?

HAL注册ISR通过存储函数指针isr在一个查找表中。如果注册成功则alt_irq_register()返 回0值,如失败,则返回非零值。如果HAL注册用户注册ISR成功,在alt_irq_register()返回时, 相关的Nios II 中断则被使能。当特定的IRQ发生时,HAL在查找表中查找IRQ,然后指派注册的ISR。 6.6.2.4 使能和禁止 ISR

HAL提供函数alt_irq_disable()、alt_irq_enable()、

alt_irq_disable_all()、 alt_irq_enable_all()和alt_irq_enabled(),允许程序来禁止中断和重新使能中断。 alt_irq_disable()和alt_irq_enable() 运行用户禁止和使能单独的中断。alt_irq_disable_all()禁止所有的中断,并且返回一个context 值。要重新使能中断,调用 alt_irq_enable_all(),传递给函数context参数。这样,中断被返回到调用alt_irq_disable_all() 之前的状态。如果中断被使能,则alt_irq_enabled()返回非零值,程序能够检查中断的状态。禁止中断在尽可能短的时间里完成。中断的最大延 迟随着中断被禁止需要的时间量的增大而增大。

6.6.2.5 C语言例子

下面的代码为按键PIO中断服务程序的例子。本例是基于具有4位的PIO外设的Nios II系 统, 4位的PIO同按键相连。 IRQ当按键被按下即产生。 ISR代码读PIO外设的边沿捕获寄存器,将读到的值存储到一个全局变量。全局变量的地址通过context的指针传递给ISR。 例:按键PIO IRQ的ISR

#include \

#include \#include \

static void handle_button_interrupts(void* context, alt_u32 id) {

/* cast the context pointer to an integer pointer. */ volatile int* edge_capture_ptr = (volatile int*) context; /*

* Read the edge capture register on the button PIO. * Store value. */

*edge_capture_ptr =

IORD_ALTERA_AVALON_PIO_EDGE_CAP(BUTTON_PIO_BASE); /* Write to the edge capture register to reset it. */ IOWR_ALTERA_AVALON_PIO_EDGE_CAP(BUTTON_PIO_BASE, 0); /* reset interrupt capability for the Button PIO. */ IOWR_ALTERA_AVALON_PIO_IRQ_MASK(BUTTON_PIO_BASE, 0xf); }

下面的代码显示的是在主程序中使用HAL注册ISR的例子。 例:使用HAL注册按键PIO ISR #include \

#include \...

/* Declare a global variable to hold the edge capture value. */ volatile int edge_capture; ...

/* Initialize the button_pio. */

本文来源:https://www.bwwdw.com/article/3n2w.html

Top