1、最小的 Nios2 系统前言2003 年 Altera 推出了第一代 32 位 Nios 系统,开创了 FPGA 内构建高性能单片机的先河。随之 2004 年,Nios 系统升级为 Nios2 系统,解决了软硬件开发中一些不方便的问题,软件开发环境从命令行编译转移到 Eclips 的 IDE 集成开放环境。 Nios2 开发环境从 1.0、1.1 到 1.2 逐步升级。后来为了和 QuartusII 软件升级同步,从 QuartusII5.0 版本开始Nios2 的版本号正式和 QuartusII 统一。作者我亲身经历过整个 Nios2 发展历程,深知 Nios2 的不同版本发生的巨大变化。目
2、前网上流行的 Nios2 教程针对的版本相对较老,已经对初学者学习 Nios2 起不到指导作用,并且应广大爱好者的强烈要求,我在此使用 QuartusII 和 Nios2 的 8.0 版本详细叙述 Nios2 的开发流程。构建一个 Nios2 最小系统需要什么构建一个 Nios2 最小系统需要具备以下资源: Nios2 软核处理器 内存 Jtag_uart 调试接口1. Nios2 软核处理器:这就是 Nios2 处理器的核心 CPU,所有的外设都是和这个 CPU 通过 Avalon 总线连接到一起的。2. 内存:编译后的程序代码需要通过下载线下载到该内存中,然后 CPU 的程序指针跳转到内存
3、的首地址开始执行程序。3. Jtag_uart 调试接口:想要用单步调试等调试功能控制程序执行和查看程序变量,那么就需要这个调试接口。开始构建一个再简单不过的 Nios2 工程整个步骤由 2 部分组成,第一部分建立 Nios2 硬件 SOPC 工程,第二部分建立 Nios2 软件工程。1建立 Nios2 硬件 SOPC 工程建立 Nios2 硬件 SOPC 工程就是设计一个软核 CPU 和它的外设,编译成硬件电路放到 FPGA 芯片里面。这时候可以认为 FPGA 就是一个 32 位的单片机了,下面的软件开发都是针对这个单片机的。打开 QuartusII 软件,新建一个工程选择 EP2C8Q20
4、8C8 芯片。打开 ToolsSOPC Builder 菜单,进入 SOPC Builder 界面,新建一个名为 nios32 的 SOPC 文件,语言可以选择 VHDL 或者 VerilogHDL。我们一共要添加 3 个 IP 模块:Nios2 处理器、片上内存和 Jtag_uart调试接口。11 在左边的 IP 导航栏里面双击选择 NiosII Processor,然后选择Nios II/e 型的处理器。这个处理器占用 FPGA 逻辑资源最小。此时CPU 的 Reset Vector 和 Exception Vector 都是不可选的,因为还没有设置内存。在添加了内存后,还要回到这个 CP
5、U 设置里面设置这两项信息。12 在左边的 IP 导航栏里选择 On-Chip Memory(RAM or ROM)使用默认设置就可以了,RAM 类型,32 位数据宽度,4KB 字节容量。13 在左边的 IP 导航栏里选择 jtag_uart使用默认设置就可以了。14 最后建成的 Nios2 系统见下面的连接关系图:Avalon 总线的连接关系图下图:图中实心的圆圈代表连接,空心的圆圈代表不连接。cpu 做为主设备 Onchip_mem 和 jtag_uart 作为从设备,从设备通过数据总线和指令总线连接到主设备上。Onchip_mem 比较特殊,它既连接到 cpu 的指令总线上,又连接到数据
6、总线上。这跟我们设计有关,我们是通过调试接口将程序下载到 Onchip_mem 中,程序然后从 Onchip_mem 开始执行指令。程序中产生的变量同时也是存放在 Onchip_mem 中的。15 新建一个 Block Diagram/Schematic File 作为工程的顶层文件,将刚才制作的 Nios32 的 symbol 添加到 bdf 文件里。添加时钟和复位信号管脚,定义管脚的管脚号。最后编译工程,这样 Nios2 硬件部分的开发就算完成了,可以看到生成的 sof 和 pof 下载文件。2建立 Nios2 软件工程建立 Nios2 软件工程和开发 51 单片机程序差不多,但是具体细节
7、设置的地方有很多差异的地方。21 新建工程打开 Nios2 软件 IDE 开发环境,新建一个 NiosII C/C+ Application的工程。Nios2 开发环境还自带了很多模版例子工程,我们这里选择Hello World Small 工程。和 51 开发不同的是,Nios2 可以自定义很多外设。因此不同的 Nios2 工程添加的外设可能都不同,需要在新建工程的时候指定你所要开发的那个 Nios2 硬件工程。这点很关键,这就是软件工程和硬件工程结合的地方。一般新建一个工程时,系统会同时建立一个和该应用程序对应的库工程。库工程里面编译了指定 Nios2 硬件的外设驱动程序等 api函数。这
8、样就将应用程序和驱动库分离开来,方便用户管理。开发不同的应用程序时,可以共享一个驱动库。22 编译工程建立工程后我们会在导航栏里面看到又两个新建的工程:hello_world_small_0 和 hello_world_small_0_syslib。点击编译按钮直接编译工程,最后查看编译结果。最终生成 hello_world_small_0.elf 二进制文件,程序代码大小为 560个直接,内存剩余 3536 个字节。23 软件在线调试工程工程编译成功后就可以用下载线调试你的程序了。在 Run-Debug 菜单里设置下载线和需要调试的软件工程。Main 页面和 Target Connectio
9、n 页面里的设置完成后可以点击Debug 按钮进行调试。此时 Nios2 的开发环境将刚才编译的二进制代码通过 USB Blaster 下载电缆,下载到 FPGA 片内的 onchip-memory 中,然后将 Nios2 的指令指针指向程序的第一条语句。注意,在调试程序前必须将硬件开发过程中生成的 sof 文件下载到 FPGA 里,否则 FPGA里面是没有 Nios2 的 CPU 软核,程序将无法下载。这时程序会停在 man 函数的第一条语句,点击全速运行按键,我们会在控制台里面看到程序运行的结果。程序在控制台里打出一段字符串,这就是整个程序的运行结果。总结上面我们通过建立一个 Nios2
10、的最小系统,带领大家走过了从硬件到软件整个开发流程,看到了 Nios2 运行的结果。通过这个简单的例子,我们劈开了很多复杂繁琐的设置过程,让 Nios2 清晰的展现在大家面前。Nios2 的开发不是想象中那么神秘,我们只要循序渐进的学习才能真正领会 Nios2 的强大和灵活。上面的例子只是展示了一个可以运行的 Nios2,它的功能非常简单,简单到只能完成打印一行字符串。由于受到 FPGA 片内存储器资源的限制,我们不能构建功能更加强大的代码,因此需要片外扩展 Sdram来存储更大的程序代码。另外,我们的程序是在线下载到内存里面的,断电后程序代码也会消失,我们需要一个外部非易失的存储器如 Fla
11、sh来存储 CPU 的软件代码。下一篇教程,我们将介绍如何扩展外部存储器 Sdram 和 Flash。程序代码:nios2 c 语言编程方法 Nios2 系列教程 2nios2 的 C 语言和 X86 或者单片机 C 语言很相似,上层的标准 C 库函数都是一样的,区别在于与底层硬件相关的各个外设寄存器的结构 不同。如果我们把访问底层硬件寄存器的函数封装起来供上层调用,平台之间的移植就显得很容易了。下面我总结了一些外设的寄存器结构以及用于访问寄存器的函数。1.可编程输入、输出口 PIO/Defined in pio_struct.h/ PIO Peripheral/ PIO Registerst
12、ypedef volatile structint np_piodata; / read/write, up to 32 bitsint np_piodirection; / write/readable, up to 32 bits, 1-output bitint np_piointerruptmask; / write/readable, up to 32 bits, 1-enable interruptint np_pioedgecapture; / read, up to 32 bits, cleared by any write np_pio;#define IOADDR_ALTE
13、RA_AVALON_PIO_DATA(base) _IO_CALC_ADDRESS_NATIVE(base, 0)#define IORD_ALTERA_AVALON_PIO_DATA(base) IORD(base, 0) #define IOWR_ALTERA_AVALON_PIO_DATA(base, data) IOWR(base, 0, data)#define IOADDR_ALTERA_AVALON_PIO_DIRECTION(base) _IO_CALC_ADDRESS_NATIVE(base, 1)#define IORD_ALTERA_AVALON_PIO_DIRECTIO
14、N(base) IORD(base, 1) #define IOWR_ALTERA_AVALON_PIO_DIRECTION(base, data) IOWR(base, 1, data)#define IOADDR_ALTERA_AVALON_PIO_IRQ_MASK(base) _IO_CALC_ADDRESS_NATIVE(base, 2)#define IORD_ALTERA_AVALON_PIO_IRQ_MASK(base) IORD(base, 2) #define IOWR_ALTERA_AVALON_PIO_IRQ_MASK(base, data) IOWR(base, 2,
15、data)#define IOADDR_ALTERA_AVALON_PIO_EDGE_CAP(base) _IO_CALC_ADDRESS_NATIVE(base, 3)#define IORD_ALTERA_AVALON_PIO_EDGE_CAP(base) IORD(base, 3) #define IOWR_ALTERA_AVALON_PIO_EDGE_CAP(base, data) IOWR(base, 3, data)2。可编程定时器/ Timer Peripheral/ Timer Registerstypedef volatile structint np_timerstatus
16、; / read only, 2 bits (any write to clear TO)int np_timercontrol; / write/readable, 4 bitsint np_timerperiodl; / write/readable, 16 bitsint np_timerperiodh; / write/readable, 16 bitsint np_timersnapl; / read only, 16 bitsint np_timersnaph; / read only, 16 bits np_timer;/ Timer Register Bitsenumnp_ti
17、merstatus_run_bit = 1, / timer is runningnp_timerstatus_to_bit = 0, / timer has timed outnp_timercontrol_stop_bit = 3, / stop the timernp_timercontrol_start_bit = 2, / start the timernp_timercontrol_cont_bit = 1, / continous modenp_timercontrol_ito_bit = 0, / enable time out interruptnp_timerstatus_
18、run_mask = (1np_timerperiodl = (nasys_clock_freq_1000) na_timer1-np_timerperiodh = (nasys_clock_freq_1000 16) na_timer1-np_timercontrol =np_timercontrol_start_mask| np_timercontrol_cont_mask| np_timercontrol_ito_mask;running_yet = 1;return milliseconds_count;static void timer_isr_handler(int context
19、)milliseconds_count+;na_timer1-np_timerstatus = 0; / write to clear the IRQ#endif/ end of file3。通用异步串行口/ UART Registerstypedef volatile structint np_uartrxdata; / Read-only, 8-bitint np_uarttxdata; / Write-only, 8-bitint np_uartstatus; / Read-only, 8-bitint np_uartcontrol; / Read/Write, 9-bitint np_
20、uartdivisor; / Read/Write, 16-bit, optionalint np_uartendofpacket; / Read/Write, end-of-packet character np_uart;/ UART Status Register Bitsenumnp_uartstatus_eop_bit = 12,np_uartstatus_cts_bit = 11,np_uartstatus_dcts_bit = 10,np_uartstatus_e_bit = 8,np_uartstatus_rrdy_bit = 7,np_uartstatus_trdy_bit
21、= 6,np_uartstatus_tmt_bit = 5,np_uartstatus_toe_bit = 4,np_uartstatus_roe_bit = 3,np_uartstatus_brk_bit = 2,np_uartstatus_fe_bit = 1,np_uartstatus_pe_bit = 0,np_uartstatus_eop_mask = (1/components/altera_nios2/HAL/inc/sys/alt_irq_entry.h 中有定义。用 HAL 系统库提供的 API 能够很方便的构造和维护中断服务程序。在处理中断前我们必须做两件事情来做好准备:1
22、.编写中断服务子程序。(与我们一般的中断服务程序不同的是它不需要用关键字 interrupt来指明中断服务程序)2.注册中断服务程序。(这就是告诉编译器哪个中断对应哪个中断服务程序)注册中断服务程序的子函数原型为:int alt_irq_register (alt_u32 id,void* context,void (*isr)(void*, alt_u32);id:中断号context:中断服务程序与外界的参数传递*isr:中断服务程序的函数指针用户的中断服务程序要遵循的约束条件一般性的原则:用户的中断服务程序不要调用阻碍中断执行的函数,此外在调用标准 C 类库中使用中断的子函数时要小心谨慎
23、。一个简单的例程这个例程演示了如何使用中断服务程序来读 PIO 口上的按钮状态,通过读 PIO 口的边沿捕获寄存器,后把读到的数据放在一个全局变量里。这个全局变量的地址是作为参数 context 传送到中断服务程序的。#include “system.h“#include “altera_avalon_pio_regs.h“#include “alt_types.h“#include “alt_irq.h“volatile int edge_capture;static void handle_button_interrupts(void* context,alt_u32 id)volatil
24、e int * edge_capture_ptr=(volatile int*)context;*edge_capture_ptr=IORD_ALTERA_AVALON_PIO_EDGE_CAP(BUTTON_PIO_BASE);IOWR_ALTERA_AVALON_PIO_EDGE_CAP(BUTTON_PIO_BASE,0);IOWR_ALTERA_AVALON_PIO_IRQ_MASK(BUTTON_PIO_BASE,0xf);void main(void)alt_u32 *led=LED_PIO_BASE;alt_u16 i;IOWR_ALTERA_AVALON_PIO_IRQ_MAS
25、K(BUTTON_PIO_BASE, 0xf);IOWR_ALTERA_AVALON_PIO_EDGE_CAP(BUTTON_PIO_BASE, 0x0);alt_irq_register(BUTTON_PIO_IRQ,printf(“Interrupt demo routine n“);*(led+1)=3;while(1)if(edge_capture!=0)printf(“interruptedn“);edge_capture=0;return 0;UART 的寄存器:UART 通过 Avalon 总线和一组寄存器文件打交道。 UART 有 6 个 16 位的寄存器。它们是 contro
26、l,status,rxdata,txdata,divisor,endofpacket。UART 的中断:当串口收到一个数据或者为发送一个字节准备好时,它就会产生一个高有效的中断信号。UART 的 DMA 操作:UART 支持流模式传输,因此它可以用来和存储器进行 DMA 操作。UART 的接口:由于大多数 FPGA 芯片不支持 RS-232 的数据接口,因此它需要在片外加一个电平转换芯片来完成电平转换。逻辑0代表数据为 1,逻辑1代表数据为 0。发送逻辑单元:UART 内部的发送逻辑由发送保持寄存器(holding register) 和发送移位寄存器(shifting register)组成
27、。当主设备向发送寄存器(txdata)写数据时,如果移位寄存器发送完上一个数据后,就会把该数据载入。数据就会从最低位开始通过 TXD 引脚从位移寄存器一位一位移出。由于有了保持寄存器和位移寄存器的双缓冲,串口在把数据移出的同时又可以向保持寄存器写入新的数据。主端口还可以实时监控发送 status 寄存器,可以读取发送准备好位(trdy),发送位移寄存器空位(tmt),发送溢出位(toe)。接收逻辑单元:UART 内部的接收逻辑由接收保持寄存器(holding register) 和接收移位寄存器(shifting register)组成。当移位寄存器接收完一个完整的数据后,主设备就可以读取接收
28、保持寄存器(txdata)的数据。由于有了保持寄存器和位移寄存器的双缓冲,串口在把数据移到保持寄存器的同时,又可以向接收位移寄存器移入新的数据了。主端口还可以实时监控接收 status 寄存器,可以读取接收准备好位(rrdy),接收溢出位(roe),停止侦测位(brk),奇偶校验位(pe),帧错误位(fe) 。波特率的产生:波特率可以通过以下两种方式产生:1。在系统生成时设定固定的值。2。设置 16 位的分配寄存器(divisor register) 。当波特率在系统生成时被设成固定值时,系统生成后是不能被改变的。相反,把波特率设成可变的话,在系统生成后,可以通过软件设定分配寄存器的值来改变波
29、特率。下面是波特率和分配因子的计算方法:divisor = int( (clock frequency)/(baud rate) + 0.5 )baud rate = (clock frequency)/(divisor + 1)数据位数的设定:数据的数据位、停止位、奇偶校验位在系统生成时是可以配置的。一旦系统生成后,这些参数是不能被改变的。数据流(DMA) 的控制方式:UART 可以支持流模式的 Avalon 传送方式。这样可以使得 uart 准备好接受下一个字符时主端口才会向它发送数据,或者 uart 接收到数据时主端口才去读它接收的数据。此时可以设置 SOPC Builder 来选择给
30、uart 构造end of packet 寄存器。在加入 end of packet 寄存器后,uar 就会在基地址+5 的位置多了一个寄存器;在 status 寄存器中多一位eop 位;在 control 寄存器中多一位 ieop 位;在 Avalon 总线上多一个 endofpacket 信号用来和支持流模式数据传输的主端口进行接口。如果没有设定该寄存器,则不会加入上述资源。End-of-packet(EOP)侦测可以决定 UART 和支持流模式传输的 Avalon 主端口在什么时候中止流模式数据传输。EOP 侦测可以和 DMA 控制器中一起使用,例如,可以实现将 UART 中接收到的数据
31、自动写入存储器,直到接收到一个特定的字符。这个中止字符的就是被写入到 endofpacket 寄存器的值。软件编程模式:HAL 系统库支持Altera 公司为 Nios II 系统提供了设备驱动,该驱动将 HAL 层的字符型设备驱动集成到了 HAL 系统库中。HAL 用户可以通过熟悉的 HAL API 和 ANSI C 标准库,而不是访问 UART 的寄存器组。用户可以使用 ioctl()请求来控制和 uart 硬件相关的操作。(注:如果在程序中使用 HAL 设备驱动来访问 UART 的话,此时直接对设备的寄存器进行访问会干扰设备驱动的正常运行。)对于 Nios II CPU 用户来说,HAL
32、 系统库的 API 提供了完整的对 UART 的访问函数。Nios II 的程序把 uart 当成一个字符型的设备,发送和接收数据都使用 ANSI C 标准库函数。下面的代码示范了一个最简单的应用,用 printf()打印一段消息到 stdout。在这个例子中 HAL 系统库已经配置了一个串口作为 stdout。-#include int main ()printf(“Hello world.n“);return 0;-下面的代码示范了如何通过 C 标准库从 uart 读字符和向 uart 发送消息。在这个例子中程序把该设备当成和 HAL 文件系统中任何其他的节点一样来处理。-/* A sim
33、ple program that recognizes the characters t and v */#include #include int main ()char* msg = “Detected the character t.n“;FILE* fp;char prompt = 0;fp = fopen (“/dev/uart1“, “r+“); /Open file for reading and writingif (fp)while (prompt != v) / Loop until we receive a v.prompt = getc(fp); / Get a cha
34、racter from the UART.if (prompt = t) / Print a message if character is t.fwrite (msg, strlen (msg), 1, fp);fprintf(fp, “Closing the UART file.n“);fclose (fp);return 0;-驱动实现的两种选择:Fast vs. Small根据不同系统的需要,uart 驱动提供了两种形式:快速模式和小模式。快速模式是默认模式。这两种模式都支持 C 标准库函数和 HAL API。快速模式是一种中断实现方式,这种方式可以使得 CPU 在 UART 未准备好
35、收发数据时做其他的事情。小模式是一种查询方式,这种方式必须一直在等待 uart 准备好以后才能收发数据。使能小模式可以通过两种方式:1。设置 HAL 系统库工程属性,开启 small footprintf(这种方法也会影响其他设备驱动 );2。定义预处理宏 -DALTERA_AVALON_UART_SMALL ,使用这个选项不会影响到其他设备的驱动。ioctl()操作:uart 驱动支持 ioctl()函数,该函数允许程序基于 HAL 层的设备相关操作请求。-请求 含义TIOCEXCL 锁定设备避免被再次访问。对该设备用 open()函数再次访问会失败,直到这个设备的文件描述符被关闭,或者用
36、TIOCNXCL ioctl 请求解锁。在使用该请求时“arg“参数可忽略。TIOCNXCL 对前一次的访问解锁。在使用该请求时“arg“参数可忽略。以下请求只对快速模式有效:TIOCMGET 向 termios 结构填入内容,返回当前的设备配置情况。指向这个结构的指针是作为 ioctl 的“opt“ 参数。TIOCMSET 根据输入 termios 结构的值来配置设备。指向这个结构的指针是作为 ioctl 的“arg“参数。-termios 结构在 Newlib C 标准库里被定义。在/components/altera_hal/HAL/inc/sys/termios.h 文件中有它的定义。
37、软件开发文件: UART 的核还配有以下软件开发文件。这些文件定义了底层硬件的接口,并且提供了 HAL 驱动。应用程序开发者不要去修改这些文件。altera_avalon_uart_regs.h该文件定义了寄存器映射,提供了符号名称来访问底层硬件。这些符号名称只是被设备驱动函数所使用。altera_avalon_uart.h,altera_avalon_uart.c该文件实现了 uart HAL 系统库的设备驱动。另外,UART 还支持第一代 Nios 处理器遗留的 SDK 子程序。UART 的中断行为:UART 会输出一个 IRQ 信号到 Avalon 总线接口,它可以和任何主设备接口,例如,Nios II 处理器。主外设必须读 status 寄存器来决定是哪种类型中断。每一种中断会在 status 和 interrupt-enable 寄存器中有相应的位。当任意一种中断条件满足时,相应的寄存器位就会被置位,直到完全响应中断才会被清除。中断信号输出信号在任何 status 寄存器的某一位被置位且那一位是被中断使能的。而主外设通过清除 status 寄存器来相应这个 IRQ。在系统复位时,所有的中断使能寄存器的位都被置为 0,因此只有当主外设对中断使能寄存器的一位或多位置 1 时,才能产生 IRQ 信号。