核心概念:Nios II 嵌入式编程与标准 C 的区别
在开始之前,必须理解以下几点,这是 Nios II C 编程的基石:

(图片来源网络,侵删)
| 特性 | 标准 PC (如 Windows/Linux) | Nios II 嵌入式系统 |
|---|---|---|
| 运行环境 | 操作系统(Windows, Linux)提供丰富的库和抽象。 | 裸机 或 实时操作系统,通常没有标准库。 |
| 标准库 | 完整的 stdio.h, stdlib.h, string.h 等。 |
受限的 Newlib,只提供部分功能,且可能不支持文件 I/O(因为没有文件系统)。 |
| I/O 操作 | printf 输出到控制台,fopen 打开文件。 |
printf 重定向到 UART(串口),fopen 通常不可用,I/O 通过读写特定内存地址(硬件寄存器)完成。 |
| 内存管理 | 动态内存分配 (malloc, free) 是常态。 |
谨慎使用动态内存。malloc 可能导致内存碎片,在嵌入式系统中常被禁用或使用静态内存池。 |
| 启动流程 | 操作系统加载程序并设置环境。 | Bootloader 或 硬件初始化代码 设置时钟、栈、堆,并调用 main 函数。 |
| 调试 | GDB, IDE 调试器。 | JTAG 调试,通过 FPGA 的 JTAG 接口,在硬件或模拟器中单步执行、查看内存/寄存器。 |
开发环境:Nios II Software Build Tools (SBT)
这是官方的、也是推荐的集成开发环境,它不是一个传统的 IDE(如 Visual Studio),而是一套命令行工具和 Eclipse 插件的集合。
1 安装环境
你需要安装以下软件:
- Intel Quartus Prime:用于 FPGA 的设计、综合和配置,其中包含了 Nios II SBT 的核心组件。
- Nios II SBT for Eclipse:这是一个 Eclipse IDE 插件,提供了图形化的项目管理、代码编辑和调试功能,强烈推荐使用它,而不是纯命令行。
安装步骤:
- 安装 Quartus Prime。
- 运行 Nios II SBT 安装包,它会自动检测已安装的 Quartus 并集成。
- 安装完成后,启动 Eclipse,你将能看到 Nios II 的相关菜单和图标。
2 创建一个 Nios II 工程
Nios II 工程不是孤立存在的,它必须与一个 FPGA 工程紧密绑定。

(图片来源网络,侵删)
流程:
-
创建/打开一个 Quartus FPGA 工程:这个工程已经包含了你的 Nios II 处理器系统,通常使用 SOPC Builder (旧版) 或 Platform Designer (新版) 来创建这个系统。
- 在 Platform Designer 中,你需要添加:
- Nios II Processor (选择 e.g., Nios II/e, Nios II/s, Nios II/f)
- On-Chip Memory (用作程序和数据存储)
- JTAG UART (用于
printf调试和终端通信) - Interval Timer (用于
usleep,sleep等延时函数) - 其他外设 (e.g., GPIO, SPI, I2C, Ethernet 等)
- 生成系统后,Quartus 会生成一个
.ptf或.sopcinfo文件,这是 SBT 工程的“蓝图”。
- 在 Platform Designer 中,你需要添加:
-
在 Eclipse 中创建 Nios II 应用程序工程:
- 打开 Eclipse,进入
File -> New -> Nios II Application and BSP from Template。 - Select a system:选择你刚刚在 Quartus 中生成的
.sopcinfo文件。 - Select a template:选择一个模板,
Hello World。 - Project name:给你的应用程序工程命名,
my_app。 - BSP (Board Support Package) name:BSP 是连接你的 C 代码和硬件外设的桥梁,会自动生成,名称通常是
my_app_bsp。 - 点击 Finish。
- 打开 Eclipse,进入
-
工程结构:
(图片来源网络,侵删)my_app/:你的 C 源代码目录。main.c:程序入口。
my_app_bsp/:BSP 目录,由 SBT 自动管理,不要手动修改。system.h:最重要的头文件!它定义了所有外设的基地址、中断号等硬件信息。sys/alt_stdio.h:重定向标准 I/O 的头文件。hal/:硬件抽象层库,提供访问外设的 API。
C 语言编程实践
1 Hello World - 标准输出重定向
默认情况下,printf 是不可用的,你需要将其重定向到 JTAG UART。
main.c 示例:
#include <stdio.h> // 包含标准 I/O 头文件
#include "system.h" // 包含硬件定义头文件
#include "alt_printf.h" // 使用 alt_printf 更简单,它直接输出到 JTAG UART
int main()
{
// 方法一:使用 alt_printf (推荐)
// alt_printf 会自动重定向,无需额外设置
alt_printf("Hello from Nios II! (using alt_printf)\n");
// 方法二:使用标准 printf 并手动重定向
// 需要在 main 函数开始时设置
// 设置标准输入输出为 JTAG UART
// setvbuf(stdin, NULL, _IONBF, 0);
// setvbuf(stdout, NULL, _IONBF, 0);
// setvbuf(stderr, NULL, _IONBF, 0);
// printf("Hello from Nios II! (using standard printf)\n");
// 简单延时
for (volatile int i = 0; i < 10000000; i++);
return 0;
}
关键点:
#include "system.h":必须包含,它定义了JTAG_UART_BASE等宏。alt_printf():是 Nios II BSP 提供的轻量级打印函数,直接与 JTAG UART 通信,非常方便。- 编译并运行程序后,打开 Nios II Console (在 Eclipse 的
Window -> Show View -> Nios II Console),就能看到打印信息。
2 硬件访问:直接寄存器操作 vs. HAL API
这是嵌入式编程的核心,假设你在 Platform Designer 中添加了一个名为 my_led_pio 的 PIO (Parallel I/O) 外设。
直接寄存器操作 (高效,但可移植性差)
每个外设都有一组内存映射的寄存器,PIO 通常至少有:
- Data Register:读写数据。
- Direction Register:设置引脚方向(输入/输出)。
#include "system.h"
#include "io.h" // 提供简单的内存读写宏
#define LED_PIO_BASE MY_LED_PIO_BASE // 假设在 system.h 中定义了 MY_LED_PIO_BASE
int main()
{
// 设置所有 LED 引脚为输出
IOWR(LED_PIO_BASE, 1, 0xFFFF); // 参数1:基地址, 参数2:寄存器偏移(0=Data, 1=Direction), 参数3:要写入的值
while (1)
{
// 点亮所有 LED
IOWR(LED_PIO_BASE, 0, 0xFFFF);
for (volatile int i = 0; i < 10000000; i++);
// 熄灭所有 LED
IOWR(LED_PIO_BASE, 0, 0x0000);
for (volatile int i = 0; i < 10000000; i++);
}
return 0;
}
#include "io.h":提供了IOWR(I/O Write) 和IORD(I/O Read) 宏。- 这种方法速度快,但代码与硬件地址紧密耦合。
使用 HAL API (推荐,可移植性好)
BSP 会为每个外设生成一个 C 驱动,你只需要包含相应的头文件并调用 API。
#include <stdio.h>
#include "system.h"
#include "altera_avalon_pio_regs.h" // PIO 的通用驱动头文件
#include "alt_types.h" // 提供数据类型定义
// 假设 PIO 实例名称为 "my_led_pio"
#define LED_PIO_BASE MY_LED_PIO_BASE
int main()
{
// 设置所有引脚为输出
IOWR_ALTERA_AVALON_PIO_DIRECTION(LED_PIO_BASE, 0xFFFF);
while (1)
{
// 点亮所有 LED
IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE, 0xFFFF);
for (volatile int i = 0; i < 10000000; i++);
// 熄灭所有 LED
IOWR_ALTERA_AVALON_PIO_DATA(LED_PIO_BASE, 0x0000);
for (volatile int i = 0; i < 10000000; i++);
}
return 0;
}
#include "altera_avalon_pio_regs.h":这是由 BSP 自动生成的驱动文件。IOWR_ALTERA_AVALON_PIO_DATA和IOWR_ALTERA_AVALON_PIO_DIRECTION是更清晰的 API。- 优点:代码可读性好,与具体硬件地址解耦,如果将来更换 PIO 实例,只需修改
system.h中的基地址定义,代码无需改动。
3 使用中断
中断是高效处理外设事件的关键。
步骤:
- 在硬件层面:在 Platform Designer 中,确保你的外设(如 Interval Timer)被连接到了 Nios II 的中断线上,并记下它的中断 ID。
- 在软件层面: a. 编写中断服务程序。 b. 注册 ISR:告诉 CPU 中断发生时应该调用哪个函数。 c. 使能中断:在 CPU 和外设上使能中断。
示例:使用 Interval Timer 产生周期性中断
#include <stdio.h>
#include "system.h"
#include "altera_avalon_timer_regs.h"
#include "alt_types.h"
#include "alt_irq.h"
// 假设 Timer 实例名为 "my_timer"
#define TIMER_BASE MY_TIMER_BASE
#define TIMER_IRQ MY_TIMER_IRQ // 中断号
volatile int counter = 0;
// 中断服务程序
void timer_isr(void* context)
{
// 清除中断状态,否则会连续进入中断
IOWR_ALTERA_AVALON_TIMER_STATUS(TIMER_BASE, 0);
// 执行中断处理任务
counter++;
alt_printf("ISR called! Counter = %d\n", counter);
}
int main()
{
// 安装中断服务程序
alt_ic_isr_register(
TIMER_IRQ, // 中断号
timer_isr, // ISR 函数指针
NULL, // 传递给 ISR 的上下文
NULL // 用于错误返回,通常设为 NULL
);
// 配置 Timer
// 设置周期 (1秒的时钟频率)
IOWR_ALTERA_AVALON_TIMER_PERIOD_L(TIMER_BASE, 0x00F42400); // ~1 second @ 50MHz
IOWR_ALTERA_AVALON_TIMER_PERIOD_H(TIMER_BASE, 0x00000000);
// 启动 Timer 并使能中断
IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER_BASE, ALTERA_AVALON_TIMER_CONTROL_START_MSK | ALTERA_AVALON_TIMER_CONTROL_ITO_MSK);
// 全局使能中断
alt_irq_enable(TIMER_IRQ);
alt_printf("Main loop started...\n");
while (1)
{
// 主循环可以执行其他任务
// ...
}
return 0;
}
编译、链接与部署
1 编译流程
SBT 使用 make 工具,当你点击 Eclipse 中的 "Build Project" 时,SBT 会执行以下步骤:
- 编译:将 C 文件 (
.c) 编译成目标文件 (.o)。 - 链接:将你的目标文件与 BSP 提供的库文件(如
hal.lib)链接在一起,生成一个可执行文件(在 Nios II 中通常是.elf格式)。 - 生成镜像:将
.elf文件转换成.flash或.ram文件,这些是二进制镜像,可以直接被 FPGA 的 Bootloader 或 EPCS/EPCQ 闪存加载。
2 部署到硬件
- 连接硬件:将开发板(如 DE0-Nano, DE10-Lite)通过 USB Blaster 连接到电脑。
- 配置 FPGA:在 Quartus 中编译整个工程,并使用 "Programmer" 工具将最终生成的
.sof文件烧录到 FPGA 中,这一步会加载你的 Nios II 处理器系统。 - 下载程序:
- 在 Eclipse 中,右键点击 Nios II 应用工程,选择
Run As -> Nios II Hardware。 - SBT 会自动将编译好的程序通过 JTAG 下载到 FPGA 的片上内存中,并启动程序。
- 你可以在 Nios II Console 中查看输出,也可以用逻辑分析仪或示波器观察 GPIO 引脚上的变化。
- 在 Eclipse 中,右键点击 Nios II 应用工程,选择
调试技巧
- JTAG 调试:这是最强大的调试方式,设置断点、单步执行、查看变量、监视内存和寄存器,Eclipse 调试器与 Nios II GDB 服务器无缝集成。
alt_printf:你的“眼睛和耳朵”,合理使用打印语句来跟踪程序流程和变量状态。- 逻辑分析仪:对于硬件信号时序问题(如 SPI, I2C),使用 SignalTap II (Quartus 自带) 或外部逻辑分析仪是必不可少的。
- 检查
system.h:当你不确定某个外设的基地址或中断号时,system.h是你的第一手资料。
Nios II C 语言编程是一个结合了软件编程和硬件设计的领域,其核心流程是:
- 用 Platform Designer 设计硬件系统。
- 用 SBT for Eclipse 创建软件工程,该工程会自动关联硬件设计。
- 在 C 代码中:
- 包含
system.h获取硬件信息。 - 使用 HAL API 或直接寄存器操作访问外设。
- 使用
alt_printf进行调试输出。 - 编写并注册 ISR 来处理中断。
- 包含
- 编译、下载到硬件,并通过 JTAG 调试。
掌握这种方法,你就可以充分利用 FPGA 的灵活性,为你的项目定制一个完美的嵌入式处理器系统。
