- by 杜伟韬 中国传媒大学
- 批评指正敬请写信至 1024670978 AT QQ dot com
引言
和当前主流的单片机系统相比, 基于FPGA的软核处理器显得略微小众和低调, 实际上由于被FPGA系统纷繁复杂的功能所掩盖, 软核处理器隐藏于FPGA的繁复功能背后。 随着深亚微米集成电路制造工艺的快速推进所导致的数字逻辑门单位资源成本的下降, 软核处理器的部署案例几乎和FPGA解决方案拥有相同的数量。
我们在实验课中引入软核处理器作为教学内容的一部分, 有着以下几方面的考虑:
- 首先, 处理器系统是最经典也是最复杂的数字电路之一, 无论同学们日后要设计何种复杂的数字电路, 对处理器系统结构的认识将对同学们大有裨益。
- 其次, 相比于其他的单片机系统, 软核处理器拥有最强的硬件调试可见性
- 可以用FPGA内嵌的逻辑分析仪观察处理器内部总线级别的动作, 这在其他种类的处理器系统中几乎是无法实现的。
- 理解体系结构层面的电路动作和总线模块的之后, 对于同学们快速学习和使用最新的现代处理器结构具有较大帮助,能够快速阅读处理器手册和使用系统库函数。
- 然后, 软核系统的同时具有高度的软件调试可见性, 软核处理器的C环境抽象层代码是由工具链动态生成的, 可以观察其源代码
本实验教程采用了命令行脚本模式的软件编译流程,虽然脚本方式没有图形界面的方式直观, 但是它有着以下的优势。
- 便于工程的复制和搬移, 命令脚本能够较好的解决文件路径的相对关系
- 便于参数的集中修改,命令脚本的参数相对集中, 在配置文件的集中区域可以完成所有的设定
- 执行效率较高,所有必要的调试动作均有专门命令对应, 调试过程中,没有多余动作
什么是软核处理器
在FPGA电路设计领域, 所谓软核处理器, 是指使用FPGA芯片内部的逻辑资源构建实现的处理器, 我们知道FPGA芯片拥有大量的逻辑、存储、I/O 和 计算资源, 这些资源也可以用来构建一个处理器系统并且在其上运行软件。
软核处理器的特点
软核处理器, 构建其的底层单元均来自FPGA芯片内部的可重构逻辑块,因此和传统的已经固化的硬核处理器相比, 有以下特点:
- 灵活的可裁剪性, 能够通过EDA工具控制处理器内部组件的规模和数量。
- 较好的调试可见性, 可以数字电路的调试工具, 把软核处理器作为一个数字电路进行观测和调试
- 与FPGA专用逻辑之间较好的耦合性, 可以使用FPGA内部的互联资源直接耦合至其他专用逻辑
- 峰值频率较低, 软核处理器使用通用逻辑资源构建, 因此其底层的定制优化程度低于专用的硬核处理器, 由于时序性能的限制, 通常来说应用中软核处理器的主频不超过200MHz
下图 是Altera 公司 用于宣传其 NIOS 软核处理器 的结构图, 从中可以看到, 除了浅蓝色的必要组件之外, 其他的组成部分均为可选或可配置调节的。 此特性决定了软核处理器的逻辑规模和处理性能具有高度的弹性, 可以胜任从实现普通的电子表到运行带MMU(内存管理单元)的标准Linux操作系统的不同任务。
NIOS软核处理器结构 (图片来源: Altera)
|
软核处理器的适用场景
通常来说, 在电子系统中使用软核处理器一般出现在以下场景:
- 系统中需要使用FPGA
- 通常我们不会为了使用软核处理器而在系统中添加一颗FPGA芯片
- 但是有时我们在FPGA中使用了软核处理器之后, 可以从系统的电路板上去掉一颗单片机芯片
- 元器件的减少, 对物料管理成本和批量焊接成品率会产生一定帮助
- FPGA内部的应用逻辑模块或与FPGA接口的外部芯片, 存在大量的需要配置的寄存器
- 现代电子系统中的专用处理模块有时会异常复杂
- 例如一个以太网接口控制器 或 某种图像采集/处理的专用电路模块,可能会存在大量的(十几个到上百个)寄存器需要配置。
- 使用C语言编程来控制寄存器比用HDL电路要灵活许多,可以快速修改,编译,下载,测试。
- 需要用FPGA 实现某种低速的、需要频繁修改调试的算法
- 使用HDL语言设计的专用电路的长处是实现简单算法的高速数据流的处理逻辑 。
- 使用高级语言编程的处理器系统则不同, 更加适合实现低速数据流的复杂处理算法。
- 尤其是当该算法需要频繁的 “修改-编译-下载-调试” 的开发场景, 使用处理器更加有开发效率优势。
软核处理器的开发工具链
对于一个运行着软件的处理器系统而言, 从不同的视角进行观察, 其抽象形式也各不相同
- 数字电路设计者视角:处理器系统是一系列导线、逻辑门、触发器、存储器、接口单元的集合。
- 体系结构设计者视角:处理器系统是由两部分构成的,硬件部分和软件部分
- 处理器系统的硬件部分由各种总线单元构成,
- 例如:处理器内核,程序存储器, 数据存储器, 中断控制器, 总线控制器,以及挂载于总线上的各种功能单元。
- 例如 DMA(直接访存控制器),定时器, 外存控制器,通用接口单元(GPIO)等。
- 处理器系统的软件部分有各种系统函数和硬件抽象函数构成。
- 例如,字符打印, 内存拷贝, 通过总线地址访问外设寄存器, 中断服务函数等。
- 体系结构的软硬件之间, 联系两者的最重要纽带是“存储映射和访问地址”
- 所有可以编程控制的硬件单元在软件层次都是一系列访存地址。得益于C语言指针的灵活特性, 软件和硬件可以方便的通过访存地址进行沟通。
- 应用程序开发视角:处理器系统在系统函数和硬件抽象函数的支持下, 完成应用逻辑的执行动作。
上述的开发者的不同视角, 在实际的开发工作中是以工具链中的一系列工具来体现的, 每个视角均有一个工具用于开发。 以Altera的NIOS处理器软核为例。
- 电路逻辑, 用于完成数字电路逻辑的编译合成, Quartus环境
- 体系结构硬件部分, 用于在总线系统中放置各种处理器系统功能单元, SOPC Builder(早期版本)/ QSys(当前版本)
- 体系结构软件部分, 根据系统的总线地址布局, 生成处理器系统的底层硬件驱动函数和 C 编程环境底层代码, BSP Editor
- 应用程序开发, 基于GUI图形环境的 NIOS IDE 和基于命令行环境的 NIOS Shell(一个运行在Cygwin bash 的GNU GCC 编译工具链)
本文参考设计实验
本文提供了一个简单的NIOS系统入门实验参考设计, 该参考设计的硬件结构如下图所示:
参考设计硬件结构
|
- 该参考设计通过一个按键和每组4个共两组LED和用户交互
- 第一组LED,由定时器控制, 每过1秒钟改变一次发光图样
- 第二组LED,由按键触发, 每按下一次按键改变一次发光图样
该参考设计的软件工作流程图如下所示:
参考设计软件流程
|
- 系统启动,硬件抽象层(HAL)代码将C环境系统变量初始化完毕, 跳转到main()函数入口
- main()函数 用户初始化其应用程序变量
- 进入工作循环:入口
- 读取当前工作循环中定时器连接的GPIO数值, 和上次循环中的数值比较
- 若计数值发生变化则更新第一组 LED 发光图样
- 读取当前工作循环中按键计数器连接的GPIO数值, 和上次循环中的数值比较
- 若计数值发生变化则更新第二组 LED 发光图样
- 重新跳转至循环入口
运行参考设计
为节省存储和网络开销, 本实验提供的参考设计资料中的临时编译文件均已全部删除, 下载设计后请按照以下方式重现参考设计项目资料。
启动SOPC Builder工具
|
- 在SOPC Builder 工具界面, 直接点击下方的”Generate “按钮,工具会生成电路逻辑编译所需的Verilog文件。
生成NIOS系统的电路HDL描述文件
|
- 回到Quartus工具, 点击”Processing-Start Compilation“菜单,启动电路编译过程
- 编译完成后,在Quartus工具, 点击 ”Tools - Netlist Viewer - RTL Viewer“观察电路RTL结构图
- 电路RTL如下图所示, 按钮计数器和定时计数器 分别连接至NIOS处理器的两个输入GPIO
顶层电路 RTL 结构
|
硬件逻辑
观察 Quartus 的 RTL Viewer
- 可以见到除了NIOS处理器之外,还有2个RTL 逻辑模块
- Timer 和 Button Counter
- Timer 模块用于每秒 对其计数值加1
- 该模块由1个秒脉冲发生器和1个8比特计数器级联构成
- Button Counter 对按键0的按键次数 计数
- 两者的数值分别被NIOS的GPIO读入
Timer和Button Counter的RTL 结构
|
体系结构-总线单元
回到SOPC Builder 首先观察系统的互联结构,系统包含以下模块:
- cpu_0 ,这是NIOS处理器的一个例化实体
- ONCHIP_RAM,片上RAM, 用于存储NIOS的运行代码和C环境变量数据
- pio 0/1/2/3 4个GPIO 模块例化实体
- pio 0/1 用于输出至LED控制其发光
- pio 2/3 用于读取定时器和按键计数器的数值
- jtag_uart_0 ,使用JTAG调试通道的字符打印模块
- 该模块用于和调试字符终端输入
- 例如printf() 和scanf() 函数
- sysid_0,该模块用于保存一个处理器系统ID
下图是系统的互联结构图, 请注意观察每个模块的总线地址
SOPC 系统互联结构
|
然后双击 cpu 0 模块 ,观察一下CPU地址设定。
- Reset Vector 是复位的启动地址
- Exception Vector 是处理器的代码执行地址
- 注意, 因为本设计把CPU代码保存至片上RAM,则导致两个地址指向同一个存储器
- 当使用了不同的存储方案时
- 例如 代码存储在EPCS存储器中,然后拷贝到SDRAM中执行
- 则Reset Vector指向 EPCS存储器
- Exception Vector 指向 SDRAM
CPU的地址设定
|
最后观察一下 nios_cpu.sopcinfo 文件,使用一个文本编辑器将其打开
- 该文件是一个XML格式的描述文件
- 该文件由SOPC Builder 在Generate 阶段生成
- 该文件用于保存处理器系统的所有参数配置信息
- 软件工具链BSP生成器根据该文件生成系统的底层驱动代码
体系结构-板级支持包(BSP)
- 在Altera的Quartus 应用程序组里, 启动 Nios II 10.1sp1 Command Shell
- 注意:一定要用管理员身份启动,否则会有权限问题
启动Command Shell
|
BSP的C环境和标准终端配置
|
- 观察一下Linker页面的配置情况,从下图中可以看到
- .bss .heap 这一栏是编译器的各种代码段和数据段
- 各种缩写的含义请用搜索引擎检索缩写字母即可
- linker region是用来保存代码段的链接器区域
- 链接器区域又被封装保存到不同的物理存储器中
- 本例中只有一个存储器,即ONCHIP RAM
BSP的Linker 配置
|
- 观察完毕后, 点击界面下方的“Generate” 按钮
- 然后回到bsp目录下使用ls命令,观察一下,bsp editor 生成了若干目录
- 目录中包含着 C 文件和H文件,这些文件都是处理器的C环境需要的库函数文件
BSP Editor的Generate文件和目录
|
请注意一下, 存在一个summary.html 文件,该文件里描述了BSP 的硬件支持库里的软件模块,用于查找各种模块的物理地址
BSP HAL 地址摘要
|
BSP 编译生成libhal_bsp.a 库文件
|
应用软件
和BSP板级支持包类似, 应用软件也是用命令行编译
- 如果当前目录是bsp目录,请切换到app目录
- cd ./app
- ls 一下观察app目录下内容, 包含以下文件
- board_io.c : 读写GPIO的函数原型
- board_io.h : 读写GPIO的函数头文件
- main.c : 主函数文件
- create-this-app : 创建Makefile的脚本
- 运行 create-this-app,使用命令 ./create-this-app
- create-this-app 脚本会生成Makefile,并且执行Make
- 如下图, 编译成功后会出现以下信息,其中最重要的是
- NIOS的目标执行代码:*.elf 文件。 该文件名在 create-this-app 脚本文件中指定。
- 代码和初始化的静态数据尺寸
- 剩余的空闲内存,注意这部分内存用于heap,即malloc( )函数使用,以及stack 即 函数中的临时变量
- 在应用程序中使用malloc和临时变量需要根据空闲内存的尺寸进行预估,防止内存溢出
- 需要注意的是,app里的Makefile 是create-this-app脚本扫描目录中的C代码后生成的
- 如果添加了或删除了C代码文件和头文件, 则需要:
- 先删除Makefile,rm Makefile ,然后重新生成
APP 编译情况报告
|
代码编译成功后,需要下载目标码 elf文件到电路板上的nios处理器中
- 首先在Quartus 工具中 把 FPGA的SOF文件下载到FPGA器件中
- 注意 RST开关,要把复位的拨码开关打到0 档位(本例是把DE0开发板的SW0 拨到下面)
- 如果复位开关在 1 档位, 处理器处于复位状态无法运行
- 使用命令 nios2-download -g prj_nios_sys.elf 下载处理器的软件代码
下载 NIOS的软件执行代码 ELF文件
|
C 代码调试
- 使用文本编辑器打开 代码文件 main.c
- 找到其中的 预编译指令, DEBUG_EN 宏
- DEBUG_EN 宏的 变量值决定是否加入变量打印的代码
- 调试代码使用printf( ) 函数打印调试变量
C程序 的 调试编译开关
|
在使用调试代码时, 有以下的注意事项
- 系统中需要存在Jtag Uart 部件
- 之前我们在BSP Editor 工具中看到过, stdio是被定向到Jtag Uart部件的
- printf() 函数通过Jtag 电缆向开发主机传输数据, 这个电缆的速率较低(几百Kbit/s)
- 因此printf()函数不适合被插入在实时处理的代码段中间,否则会破坏代码的时序
- 另外, 如果 NIOS处理器 执行 printf()函数,一定要在开发主机上启动字符调试终端
- 否则printf( ) 函数无法返回, 处理器会阻塞在printf( ) 函数的调用点
- 即 在NIOS Shell 命令行中,运行, nios2-terminal 命令。
- 启动NIOS调试终端后,会看到处理器每秒钟打印一次GPIO读入的定时器的数值。
- 另外当有按键按下时,通过GPIO读入的计数器数值也会被打印出来
使用字符终端打印变量数据
|
硬件逻辑调试
硬件逻辑部分的调试分为两种
- 一种是系统上电就自行工作的硬件逻辑,不需要处理器的介入
- 另一种是需要和处理器的软件进行配合的硬件逻辑
- 前者来说相对简单,可以按照传统的RTL电路逻辑调试流程进行
- 后者的调试稍微复杂
- 需要在处理器上运行测试软件模块
- 然后在嵌入式逻辑分析仪上根据总线地址进行分析
以本文参考设计为例, 调试处理器系统硬件模块时 , 首先观察处理器系统的RTL结构, 从中可以看到电路逻辑模块的例化层次。 本文系统中, 处理器电路的例化层次如下图,其中可以看到处理器的总线主控单元, 以及 各个挂在总线上的从设备。
RTL View 电路逻辑例化层次
|
从RTL视图上还可以看到处理器内部的总线互联结构, 用于确认主控单元和从设备单元的连接关系。
RTL View 电路逻辑互联关系
|
当需要进行总线层次的电路调试时,可以在嵌入式逻辑分析仪中, 添加 总线主控单元的用于选通从设备的寻址和读写控制信号,以及读写数据。 另外再选择添加需要调试的从设备单元的读写控制信号和数据信号。 例如,下图中,选择了处理器的写使能作为触发信号,用来捕获处理器对GPIO的写操作
Signal TAP 信号追踪 Setup
|
运行SignalTap, 观察捕获的总线数据波形, 分析处理器的读写地址和读写数据的正确性。
Singnal TAP 调试 NIOS处理器视频
获取参考代码
本文提供的参考代码存放于以下位置
- https://github.com/DUWTLAB/Git_nios_small_pio_LAB