Nios-II软核处理器开发实战:从Qsys搭建到uC/OS-II与emWin移植
简介面向Cyclone IV EP4CE10F17C新起点FPGA开发板的Nios-II工程源码包适合FPGA入门及进阶学习者、嵌入式软核开发者参考。包含15个由浅入深的例程从最简的hello world与PIO读写到中断、UART、定时器、SDRAM、EPCS等常用外设控制再扩展到RGB彩色条、GUI点线绘制与图片显示并引入uC/OS-II操作系统的任务调度、信号量与消息邮箱通信同时附带开发板原理图PDF便于对照硬件设计理解软硬件映射覆盖Qsys图形化配置与Nios-II软硬件协同设计的基本流程。压缩包共约2000个文件以C/H源码、Qsys系统描述、Quartus工程文件qpf/qsf/sof为主另含较多编译过程文件整体约406.64MB例程按数字编号分目录每个工程独立完整可直接作为Nios-II开发模板或课程设计、项目参考。这套例程能减少从零搭建Qsys系统的时间帮助快速验证外设功能与操作系统机制。目前已有487人学习是围绕新起点开发板梳理Nios-II外设与操作系统应用的实用合集。1. 从15个Nios-II例程看Cyclone IV开发板的软硬件协同设计如果你手里有一块EP4CE10F17C的板子却只用来写Verilog点灯那这颗Cyclone IV里有一半的潜力没被释放。Nios-II软核处理器可以在同一片FPGA里生成完整的32位CPU子系统再把UART、SDRAM、EPCS、LCD这些外设由Qsys总纲连接起来——而这个例程包恰好给出了从hello_world到uC/OS-II多任务再到emWin图形界面的完整梯度。对刚接触Nios-II的入门者来说这是一条已经踩平的上手路径对想把软核与硬逻辑结合做图像加速的工程师里面关于Avalon总线、中断优先级、SDRAM时序和EPCS启动的配置也足够当作参考手册来查。2. Qsys系统构建从空白工程到最小Nios-II CPU子系统2.1 为什么用Qsys搭建Nios-II而不是自己写RTLCyclone IV EP4CE10F17C没有硬核ARM想跑C代码只能靠Nios-II这类软核。Nios-II是Altera的32位RISC处理器核用逻辑资源实现不同配置下占用的LE数从几百到几千不等。如果从零开始用Verilog去搭CPU、总线、内存控制器和外设驱动那不是几天能完成的事。Qsys的价值就是把这些IP以Avalon-MM总线为骨架自动连接同时生成地址解码、中断控制器和总线仲裁逻辑让工程师把精力放在系统架构而不是连线细节上。我一般会直接用Tcl脚本生成Qsys系统而不是每次手动在GUI里点。脚本能进版本管理换机器也能一键重建。下面是一个最小Nios-II系统的脚本片段目标器件是EP4CE10F17C8# 创建Qsys系统 nios_min package require -exact qsys 13.0 create_system {nios_min} set_project_property DEVICE {EP4CE10F17C8} set_project_property DEVICE_FAMILY {Cyclone IV E} # 系统时钟 50MHz add_component clk_0 altera_clock_source set_instance_property clk_0 EXPLICIT_CLOCK_RATE {50.0} # Nios-II 处理器核 add_component cpu altera_nios2_gen2 set_instance_property cpu HW_DEBUG_UNIT 1 set_instance_property cpu RESET_VECTOR_MEM {onchip_memory2_0} set_instance_property cpu EXCEPTION_VECTOR_MEM {onchip_memory2_0}RESET_VECTOR_MEM决定CPU上电后的第一条指令从哪取。最小系统放在片上RAM等接入SDRAM或EPCS后再改。HW_DEBUG_UNIT打开JTAG调试单元在Eclipse里就能做断点调试代价只是约1KB逻辑资源建议一直开启。2.2 Avalon总线上的外设IP与地址解码Qsys里每个Avalon-MM外设都必须分配基地址Nios-II通过地址访问它们。例程包中反复出现的几个外设和地址映射如下外设IP常用基地址中断号说明jtag_uart0x010000001调试打印串口sysid0x01000010-系统标识校验硬件版本pio_led0x01000020-8位LED输出uart_00x010000403外接RS232串口timer_00x010000600系统节拍定时器epcs0x01800000-串行配置Flash控制器sdram0x02000000-SDRAM64MB地址空间地址分配的原则是SDRAM这种大块存储独占一段高位地址低速外设按4KB对齐方便Qsys自动生成解码逻辑。中断号按优先级顺序分配0最高。这些地址最终都会写进system.hC代码里直接用宏名不需要手抄地址。2.3 最小C工程驱动PIO让LED按你的节奏闪Qsys生成硬件后在Quartus里例化nios_min模块分配引脚、编译、下载比特流。然后在Nios II SBT for Eclipse中新建工程BSP设置里保持默认的HAL即可。下面是一个点灯程序验证CPU、总线、PIO是否打通#include system.h #include altera_avalon_pio_regs.h #include sys/alt_stdio.h int main(void) { unsigned int led 0x01; volatile unsigned int i; alt_putstr(min Nios-II system started\r\n); while (1) { IOWR_ALTERA_AVALON_PIO_DATA(PIO_LED_BASE, led); led (led 1) | (led 7); for (i 0; i 200000; i) ; // 手动延时 } return 0; }IOWR_ALTERA_AVALON_PIO_DATA是Altera提供的Avalon总线写宏展开后就是一次外设寄存器写操作。PIO_LED_BASE来自system.h对应上表的0x01000020。这段代码让LED做流水灯能跑起来就说明最小系统已经工作。3. 外设驱动背后的细节UART、Timer、SDRAM、EPCS的中断与轮询例程4到7分别演示UART、Timer、SDRAM、EPCS它们恰好覆盖了Nios-II外设的两种驱动方式HAL函数封装和寄存器直接操作。很多工程师只会在Eclipse里调用printf一旦遇到启动地址错误或者SDRAM时序不对就无从下手。这一章直接拆到寄存器级别。3.1 UART从open/read到状态寄存器轮询HAL层用UART直接open(/dev/uart_0, O_RDWR)然后read这套逻辑适合应用工程师。但在写bootloader或者实现串口接收机时我更倾向直接操作状态寄存器不依赖HAL的缓冲区避免中断回调里丢数据。#include system.h #include altera_avalon_uart_regs.h char uart_getchar_nowait(void) { unsigned int status IORD_ALTERA_AVALON_UART_STATUS(UART_0_BASE); if (status ALTERA_AVALON_UART_STATUS_RRDY_MSK) { return (char)IORD_ALTERA_AVALON_UART_RXDATA(UART_0_BASE); } return -1; }RRDY_MSK是接收就绪位数据寄存器被读取后硬件自动清除该位。返回值-1表示当前无数据调用方需要做超时处理。轮询方式在裸机任务里非常实用也很容易移植到uC/OS-II的中断服务函数中。3.2 Timer用alt_alarm注册系统节拍例程5的timer不仅仅是精确延时更是uC/OS-II运行的节拍来源。HAL提供的alt_alarm机制比直接读计时器计数器更优雅可以注册一个周期性回调#include sys/alt_alarm.h #include alt_types.h volatile unsigned int tick_count 0; alt_u32 my_tick(alt_alarm *alarm) { tick_count; return 0; // 返回0表示只运行一次 } void setup_timer(void) { static alt_alarm alarm; alt_alarm_start(alarm, 10, my_tick); }alt_alarm_start的第二个参数是节拍数单位是BSP中定义的系统时钟周期。默认HAL时钟来自timer_0如果系统时钟是50MHz这里的10意味着10个周期后触发回调所以实际间隔为200ns。回调函数运行在中断上下文不能在里面调用printf或任务切换函数。3.3 SDRAM给Nios-II一个可运行代码的大内存EP4CE10F17C片内RAM只有414Kbit跑emWin或者uC/OS-II任务栈都显得不够SDRAM是例程中的常客。例程6把整个Nios-II的代码和数据放到SDRAM这就必须在Qsys里正确配置SDRAM控制器的时序参数。常见的MT48LC16M16A2参数如下参数值说明CAS latency3从列地址到数据输出的周期数tRP20 ns预充电时间tRCD20 ns行有效到列读写的延迟tRC63 ns连续刷新间隔Refresh period64 ms全行刷新周期这些参数在Qsys的SDRAM Controller IP里要换算成时钟周期数。填错的典型症状是上电后第一次打印正常运行几秒后随机死机或者数据异常。遇到这种问题先查刷新周期很多例程直接把刷新周期设成3125个时钟周期64ms/15.625us就完事但不同SDRAM颗粒需要查手册确认。3.4 EPCS与Flash启动把程序固化进配置芯片EPCS是Altera的串行配置FlashNios-II代码可以存储在EPCS中上电后由启动loader搬运到SDRAM继续执行。例程7演示的就是这个流程。烧写命令在Nios II Command Shell中执行elf2flash --inputsram_app.elf --outputepcs_flash.hex \ --base0x01800000 --end0x01FFFFFF --reset0x01800000 nios2-flash-programmer --epcs --base0x01800000 \ --sidp0x01000010 --instance0 --cableUSB-Blaster epcs_flash.hexelf2flash把ELF转换成可烧写镜像--base和--end对应Qsys里EPCS控制器的地址范围--reset必须是Qsys中Nios-II Reset Vector对应的地址。第二个命令通过JTAG写入EPCS--sidp做系统ID校验防止FPGA当前加载的硬件与程序不匹配。最容易错的是--reset写错固化后上电永远跑不起来。4. 从颜色条到emWinGUI与uC/OS-II的工程化整合例程9之后的难度明显提升。颜色条、GUI曲线、图片显示再加上4个uC/OS-II例程把Nios-II从裸机带进了RTOS和图形世界。这一章重点拆解这几个例程的代码组织方式以及移植时最容易踩的坑。4.1 颜色条与LCD帧缓冲一个像素的搬运轨迹例程9的Nios-II colorbar不依赖GUI库直接在SDRAM里开辟一块LCD分辨率大小的帧缓冲按固定的颜色条顺序填充像素。核心逻辑可以概写成#define LCD_WIDTH 480 #define LCD_HEIGHT 272 static unsigned short *frame_buffer (unsigned short *)SDRAM_BASE; void draw_colorbar(void) { for (int y 0; y LCD_HEIGHT; y) { for (int x 0; x LCD_WIDTH; x) { int bar (x * 8) / LCD_WIDTH; frame_buffer[y * LCD_WIDTH x] palette[bar]; } } }palette是8种RGB565颜色值的查询表。帧缓冲地址指向SDRAMLCD控制器通过Avalon总线自动读取这块内存并显示。颜色条的作用是验证三件事LCD时序接口正常、SDRAM作为帧缓冲的带宽够用、CPU写内存后LCD能无冲突读到。如果画面出现横纹多半是SDRAM总线仲裁优先级没设好需要在Qsys中把LCD控制器的主端口优先级调高。4.2 emWin例程里那堆.c文件到底是什么例程10和11的工程目录里有大量ATK_logo.c、GUIDEMO_Automotive.c、FD60x80.c、F32B_1.c、F32B_ASCII.c、SeggerLogoBlue.c这些并不是驱动源码而是emWin开发工具生成的数据文件。FD60x80.c是字号为64x80的字体点阵F32B_1.c是32x32的加粗字体SeggerLogoBlue.c是SEGGER官方logo位图GUIDEMO_Automotive.c是emWin经典的汽车仪表盘演示。它们只是被编译进工程的二进制数据真正让emWin跑起来的是三个配置文件。GUIConf.h决定GUI内存池大小、是否支持OS和触摸LCDConf.c定义屏幕分辨率、颜色格式和帧缓冲地址。在Nios-II上通常需要实现最底层的像素操作// LCDConf.c 片段 void LCD_L0_SetPixelIndex(int x, int y, int color) { frame_buffer[y * LCD_WIDTH x] color; }emWin内部所有绘图函数最终都会调用LCD_L0_SetPixelIndex和LCD_L0_GetPixelIndex。移植的核心不是通读GUI库而是确保这两个函数正确。SDRAM基地址映射到帧缓冲后例程10的曲线、例程11的图片就能直接利用那些数据文件显示了。4.3 uC/OS-II在Nios-II上的移植和任务创建例程12到15使用uC/OS-II与Nios-II HAL组合。uC/OS-II需要三个与CPU相关的文件OS_CPU.H定义数据类型和开关中断宏OS_CPU_A.ASM实现底层调度切换OS_CPU_C.C实现任务钩子。Nios-II没有硬件定时器中断需要把timer_0的中断服务函数挂到OSTimeTick上。任务创建的代码结构如下#include includes.h #define TASK1_STK_SIZE 1024 #define TASK2_STK_SIZE 1024 OS_STK task1_stk[TASK1_STK_SIZE]; OS_STK task2_stk[TASK2_STK_SIZE]; void task1(void *pdata) { (void)pdata; while (1) { printf(task1 run\r\n); OSTimeDlyHMSM(0, 0, 1, 0); // 延时1秒 } } void task2(void *pdata) { (void)pdata; while (1) { printf(task2 run\r\n); OSTimeDlyHMSM(0, 0, 2, 0); } } int main(void) { OSTaskCreate(task1, (void *)0, task1_stk[TASK1_STK_SIZE-1], 4); OSTaskCreate(task2, (void *)0, task2_stk[TASK2_STK_SIZE-1], 5); OSStart(); return 0; }OSTaskCreate的优先级参数数字越小优先级越高。例程12的hello任务优先级最低让其他任务先运行。每个任务栈用OS_STK数组表示元素是32位1024个元素等于4KB这个容量在SDRAM里很宽裕但如果把代码放到片内RAM运行两个栈就可能超出资源。4.4 信号量与消息邮箱两个容易踩坑的同步方式例程14用信号量同步任务例程15用消息邮箱传递数据。一个常见错误是在UART中断服务函数里直接调用OSSemPost但uC/OS-II规定中断里必须包一层统计算法void uart_isr(void *context) { OSIntEnter(); // 通知内核进入中断 OSSemPost(sem_uart); OSIntExit(); // 触发任务调度 }只有加了OSIntEnter和OSIntExituC/OS-II才会在中断结束后重新调度否则高优先级接收任务永远不会被及时唤醒。消息邮箱传递的是指针注意不要传递指向局部变量的地址任务切换后栈内容会被覆盖。例程15中的消息邮箱例子里发送方应该分配静态或动态内存接收方使用完再释放。5. 烧写EPCS与调试让你改的每一行代码都能固化到Flash这一章讲一个很实用的技巧如何让工程从RAM调试模式切到EPCS固化启动模式并且避免最常见的烧写失败。两者需要来回切换我见过很多工程师卡在这一步。5.1 修改BSP设置切换运行介质在Nios II SBT for Eclipse中右键BSP工程进入Properties在Linker Script页面把.text、.rodata、.rwdata段从sdram指向EPCS控制器的内存段同时把系统的Reset Vector也改成EPCS。这一步改写的是链接脚本让代码不经过bootloader直接由CPU从EPCS取指。EPCS访问速度远慢于SDRAM所以这种模式适合最终部署不适合调试。5.2 导出并烧写全流程命令在Nios II Command Shell中按顺序执行# 先关闭占用JTAG链的终端 nios2-terminal --null # 把ELF转成Flash镜像 elf2flash --inputdebug.elf --outputflash_epcs.hex \ --base0x01800000 --end0x01FFFFFF --reset0x01800000 # 通过JTAG写入EPCS nios2-flash-programmer --epcs --base0x01800000 \ --sidp0x01000010 --instance0 flash_epcs.hexnios2-terminal --null用来关闭之前打开的串口终端否则烧写器拿不到JTAG链。重新上电后CPU复位向量指向EPCS起始地址程序从Flash启动。--sidp指定的系统ID地址必须与硬件工程里sysid外设的地址一致否则会报ID mismatch。5.3 固化失败时的快速检查列表检查Qsys中Nios-II的Reset Vector是否指向epcs_flash_controller并与elf2flash的--reset参数一致。固化前先用JTAG加载SOF文件执行nios2-configure-sof -C system.sof确保FPGA当前配置正确。固化完成后如果LED只闪一下就不动多半是代码里调用了printf但终端没连接等待数秒后重新执行nios2-terminal即可看到输出。本文还有配套的精品资源点击获取