nio模式有什么优缺点(II开发全纪录之总结NIOS)

从开始接触Altera(现在应该叫intel PSG了)的NIOS II处理器,到现在,已经有6个年头了。从开始的C语言都不懂,到现在能使用NIOS II开发一些实用的东西,中间的过程也是非常的曲折。最开始的时候,完全是炼狱一般,走一步,十个坑,没人指导,填几天,再走一步,再填一个坑。到了后来对这个东西开始心生敬畏,敬畏不是因为它有多么多么强大,而是在学习和使用它的过程中,让我对CPU架构,单片机系统实现思路和编程方法有了较为底层的认识,也算是一个升华吧,虽然在这个过程中还是常常掉入坑里好久才能爬出来。到了现在,能够指导大家学习和使用NIOS II处理器结合FPGA RTL逻辑实现一些功能,自己也能做一些不大的小东西。这6年,感觉就像是按照指数函数的曲线进步的,最开始很慢,后面越来越快。想想自己能坚持到现在,可真不容易(/偷笑)。

深知大家在自学这门技术的开始半年时间内有多么痛苦。我一早就想出一点NIOS II方面开发的实用性书籍文档,可一直一个人打理着各种事情,实在没有精力。我也深知当下讲解和使用NIOS II的开发已经有些不那么前沿了,毕竟现在嵌入硬核的FPGA应用已经较为成熟了,NIOS II这个处理器处于中间这样一个尴尬的位置,实用性和性价比值得思量。但是,毕竟NIOS II和Xilinx的MicroBlaze处理器设计和开发思路异曲同工,MicroBlaze和Xilinx当前非常受欢迎的Zynq硬核FPGA开发思路和过程很像,NIOS II和Intel(Altera)的SOC FPGA开发过程和思路很像,因此,学习NIOS II处理器,是一条经济轻巧的道路。真正掌握了NIOS II处理器的应用和开发,迁移到Intel SOC FPGA上,也就需要3~5周的时间,换到Xilinx的MicroBlaze或者Zynq平台上也只需要5~8周。所以,这个事情值得一做,毕竟,咱国家每年还有那么多高校学子需要一个合适的切入点来进入SOPC/SOC开发的大门。

当下定决心做这件事的时候,我却犯了难。到底,我该以一种怎样的方式来开启我的系列文档呢?是从CPU架构、指令集开讲,还是直接从LED点灯写起呢?每天在技术交流群里,看到大家学习时候遇到各种问题并却不知道如何解决时,我突然觉得,其实,大家暂时不缺入门的教程,缺的,是继续学习下去的信心。那么信心从何而来?就从解决NIOS II开发中常见的各种问题着手开始吧。如果大家首先就有一份“捉虫子”手册放在旁边,遇到各种问题马上能从手册中找到解决方法或解决思路,那么大家的学习信心必然会与日俱增。所以,我选择我的这一份文档,从教大家捉虫子开始。

唠叨了这么多,下面开始切入正题。

大家在进行NIOS II系统开发时,往往有很多的疑问,第一个疑问就是大家经常提到的,NIOS II CPU运行不稳定,NIOS II 开发bug太多。由于FPGA本身相对于单片机、ARM处理器来说,应用市场要小的多。加上NIOS II仅仅是FPGA应用和开发的一个小的分支,用的就更加的少了。所以,关于NIOS II 非常系统且科学的教程教材也是非常的少,导致大部分人在进行NIOS II的学习和开发的时候,都会遇到各种各样的问题,如elf文件下载失败,CPU运行不起来,程序运行不正确,CPU运行一段时间后停止,调试正常但是烧写到EPCS后无法运行等。那么今天,我就在这里将NIOS II CPU的各种问题做一个总结分析,希望大家通过本总结分析,以后在开发NIOS II相关应用时候,能够手到擒来。

1、elf文件下载失败

当大家将FPGA的编程文件SOF文件下载到FPGA芯片中后,就可以下载nios ii eds中编译好的软件固件,该固件的尾缀为.elf。那么大家在下载的时候,会经常遇到下述问题:

Launching New_configuration has encountered a problem. Downloading ELF Process failed。

nio模式有什么优缺点(II开发全纪录之总结NIOS)(1)

个人目前总结的,出现该问题的原因主要有以下几点,

1)目标板/开发板上的NIOS II CPU系统未能正常工作,例如,未下载对应的SOF文件到FPGA中,或者时钟、复位、存储器(SRAM、SDRAM、DDR2)引脚分配有误,当使用SDRAM存储器作为NIOS II CPU的内存时,还有可能是SDRAM的控制器时钟和接口时钟之间的相位差不合适。

解决方法:重新下载sof;检查核对引脚分配;查看SDRAM参数配置和时钟配置。

2)创建NIOS II软件工程时候,选择的sopcinfo文件与对应的FPGA工程不一致。这一点我在每次公开课的时候都会强调, NIOS II 软件开发需要两个工程,一个板级支持包(bsp)和一个应用工程。每次创建NIOS II BSP工程的时候都需要选择一个sopcinfo文件,该文件就是实际我们在Qsys中搭建的希望使用的NIOS II系统的描述文件。NIOS II软件开发环境根据该信息文件创建对应的硬件信息头文件“system.h”,但是,NIOS II开发软件有一个比较不好的地方就是每次选择sopcinfo文件的时候,都会记录上一次选择sopcinfo文件的路径并直接自动定位到该路径,所以大家如果一旦粗心,一点击浏览文件,发现一个sopcinfo文件就直接选择的话,往往就会选择到上一次的工程的文件,而不是本次新的工程的文件。这样我们创建软件工程时使用的sopcinfo文件就还是上一个工程的,而我们下载sof时又是下载的新的工程的,所以就出现了下载的sof文件与elf文件不是基于同一个工程的问题,导致无法下载成功。因此为了避免出现这个问题,新建工程时请时刻记住选择正确的sopcinfo文件。该问题一个更加奇妙的现象就是当前一个工程和这次的新工程两者之间差别不大的时候,elf甚至可以正确下载,NIOS II也能运行起来,但是就是现象与预期不一致,这一点可能往往也是让很多人误以为NIOS II不稳定的原因之一。如果你想知道你当前的工程的bsp文件是否正确,非常简单,打开bsp工程下的settings.bsp文件,查看第9行就可以啦。

解决方法:新建工程选择sopcinfo文件时务必选择正确的sopcinfo文件。

nio模式有什么优缺点(II开发全纪录之总结NIOS)(2)

3)1)NIOS II的启动地址错误。

NIOS II 是一个CPU,其运行过程是受程序指令控制的,而程序有不同的存放位置。例如,程序可以直接存放在RAM中,然后CPU复位后直接从硬件定义的RAM中程序存放的初始位置开始执行。该种方式常见于我们在进行软件编写调试的过程中,这个过程,我们可能需要经常进行程序的调试,所以直接使用仿真器(USB Blaster)在线将程序下载到CPU的RAM中运行或者debug。另一种情况就是项目发布的时候,我们做一个项目或产品,当产品功能都调试通过之后,需要将程序烧写在板卡上,这样板卡在上电之后,不需要PC端下载程序,就可以自动从非易失性存储器(FLASH、EEPROM)中加载程序并运行。此时我们需要CPU设置从非易失性存储器中开始启动。那么如果我们设置了NIOS II的启动地址为FLASH(EPCS),而我们又下载了定义从RAM中启动的程序,那么程序会被下载到RAM中,CPU启动时候会去FLASH中读取程序,由于我们的程序并没有下载到FLASH中去,因此CPU无法读取到正确的程序,就会无法正常运行,然后报此错误。同理,如果我们设置CPU从RAM中启动,而我们又将程序烧写在了FLASH中,那么CPU上电运行后,由于RAM中没有下载正确的程序,因此也无法运行。这一点实际上是我们上面提到的另一个现象,即调试正常但是烧写到EPCS后无法运行。

好了,饶了这么多口舌,该说大家最关心的问题了,怎么设置CPU的复位地址呢?其实有两个地方需要设置,而90%以上的人只知道一个地方,那就是QSYS中选择CPU的复位地址。当我们的系统中FLASH使用EPCS芯片剩余容量,那么如果我们要定义CPU硬件上从FLASH中启动,就需要定义CPU 的Reset Vector为EPCS,如果我们要定义CPU硬件上从RAM(onchip_ram/SDRAM/DDR2)中启动,那么就选择Reset Vector为ram。当然,这没什么问题,但是我们任然可以在一开始就定义CPU 的Reset Vector为EPCS,然后在调试的时候,却从RAM中运行。为什么可以呢,这就是我说的90%人不知道的第二个地方,该设置在NIOS II 软件开发环境中。

nio模式有什么优缺点(II开发全纪录之总结NIOS)(3)

我们选择一个bsp工程,打开bsp editor页面,在第一个选项卡main中,找到下面的Advanced选项,然后右侧相关内容中有个hal.linker选项,在该选项中有5个可选项,其中第一个叫做allow_code_at_reset。这个选项是什么意思呢,就是允许代码存放在复位向量处,好了我们回头来想一下,上面我们说过,我们可以选择CPU的Reset Vector为EPCS,而这里又使能了allow_code_at_reset选项,所以程序编译的时候就会将启动程序部分编译在EPCS所在的地址段。这就是真真切切的将复位地址设置在了EPCS中。此种模式下我们无法在线调试的,如果强行调试一定会出现上述报错。若有人反驳说他的工程此种情况下也能下载成功,那一定是因为你之前已经将EPCS中烧写过一次本程序了,所以你的CPU能够启动,是因为设定的启动地址EPCS中有能够支持CPU启动的程序,但能启动不代表其他功能也能正常运行哦。一旦选择该选项,下面的enable_alt_load也要一并选上,选上之后CPU就能够正常从EPCS中加载了。部分用户在烧写SOF和ELF到EPCS中进行固化时不成功,原因也与没有勾选这两个选项有关。

那么如果我们希望先在RAM中调试怎么办呢,想必大家已经想到了,不使能allow_code_at_reset和enable_alt_load就可以了。对的,当我们希望在RAM中调试的时候,将这两个勾选项取消,那么程序编译就会默认将启动代码编译到RAM中,然后就可以从RAM中启动并调试啦。

解决方法:调试时不勾选allow_code_at_reset和enable_alt_load。烧写时务必勾选这两个选项。

nio模式有什么优缺点(II开发全纪录之总结NIOS)(4)

1、CPU运行一段时间后停止

很多用户在调试或者运行NIOS II CPU程序的时候,可能最开始的时候程序能够正确的运行,但是过一会儿后CPU却停下来了,例如本来做的一个LED流水灯,结果流水了一两分钟后却不再动了。这里是不是又是NIOS II CPU运行不稳定的一个佐证呢?实际上,引发这个问题的原因,个人总结主要有两点:

第一点,系统使用了JTAG UART作为调试串口打印数据。用户在自己的软件代码中写了有printf的代码,例如每分钟打印一次“Hello”,同时让流水灯闪烁。但是用户可能在开发板运行的过程中,突然拔掉或者断开了USB Blaster下载器,或者关闭了nios ii软件开发工具。这就导致JTAG UART与PC间的链接断开了。由于JTAG UART是基于JTAG协议的一个模拟串口,实际上我们使用的jtag uart发送数据的时候,是先将数据写在jtag uart的fifo中,然后pc端的nios ii开发软件定时通过jtag协议读取fifo中的数据,以此模拟jtag uart的发送的。如果我们切断了nios ii开发软件与jtag uart之间的连接,或者关闭了nios ii eds软件,使之不再去读取fifo中的数据,那么jtag uart的fifo中的数据就会越积越多。当fifo满之后,数据就再也写不进去了。而jtag uart的软件驱动中,使用的是阻塞的方式发送数据的,即只有当所有的数据都写入fifo之后,程序才会执行下一步。可是,现在fifo中的数据一直是满的,没有被读走,那么软件就只能一直等在这个地方,等待fifo变成非满。然后,就导致整个CPU的运行被阻断在了这个地方。所以NIOS看起来就像是停止运行了。有的是拔掉下载器之后还能运行一会儿,这是因为jtag uart默认是64字节的fifo,刚刚拔掉下载器,fifo还没满,程序运行过程中间隔的写入数据到fifo中,过了一会写fifo满了,然后CPU就停下来了。

第二点,用户C水平不过关(不在少数),写的代码指针使用不合理,出现了指针越界行为,把正常的数据给损毁了,导致CPU的运行数据在运行过程中被损毁,然后就无法继续运行下去了。

上述原因主要是用户通过反复分析,总是找不出程序问题的时候考虑的几点。至于本身程序就没写对导致的,那就还是乖乖回去深造C语言吧。合理利用debug,帮助查找程序中存在的问题。

2、程序运行不正确

有很多用户在进行NIOS II开发的时候经常遇见程序下载到目标板后,运行不正确的情况,例如,中断不响应,printf无法打印,期望的功能无法实现,这样的问题一般分为两类。

a、下载的sof与elf不属于同一个工程,这个原因其实在我们第1个问题的第二种可能原因的时候已经讲到,即由于前后两个quartus的工程差别不大,或者说最新编辑的工程是在前一个工程的基础上增删修改部分功能后得到,两个QSYS系统中相同外设的地址都是一样,这样,当我们创建nios ii软件工程的时候,选择了上一个工程的sopcinfo文件,而下载了新的工程的sof文件,就会出现,之前的功能是OK的,但是新增或者修改的功能总是无法实现。解决问题的方法就不说了,选择正确的sopcinfo文件就行。

b、NIOS II Cache的影响

经常有人反映说自己写的代码无法正常工作,然后晒出自己的代码来。经过对用户代码的分析可以知道,他们都是参考了网络上流传的比较系统的两份资料的方式,采用直接地址映射,即用指针的方式来直接操作外设里面的寄存器。这样操作在不带Cache的NIOS II版本中能够很好的奏效,例如NIOS II的e版本和s版本,但是在f版本中,为了增强NIOS II处理器的性能,加入了数据cache和指令cache。如果用户使用带cache的CPU,却还是像之前那样,直接使用指针映射寄存器地址的方式,就会出现很多时候,我们希望写入的数据并没有直接写入到外设IP的寄存器中,而是写入到了cache中,即外设IP中并未立即写入我们希望写入的值,也就不会执行相应的操作了。(关于CACHE是个啥东西,这里不做专门讲解,有兴趣的请自行查阅相关资料)。

那么为什么明明是要写入到外设IP寄存器中的数据,却写入到了cache中呢?这是因为,NIOS II e和s版有31位的地址线,f版本有32位地址线,但是这第32位地址线恰恰就是用来选择cache的,当地32位地址线为0的时候,选择cache,当第32位地址线为1的时候,就旁路/屏蔽cache,也就是说,当我们还是继续使用指针映射的方式操作外设寄存器,假设寄存器的地址为0x00000001,那么在带有cache的系统中,该地址实际是映射到了cache中,而真正的寄存器地址应该是0x10000001,因为要想寻址到实际的寄存器地址,必须屏蔽cache,即需要地址的最高位为1。所以,如果我们还是用指针直接操作0x00000001这个地址,当然数据不会传入实际的寄存器中,所以无法生效。那么怎么解决呢,个人观点还是使用Altera 官方提供的HAL库,如io.h这个文件里,提供了很多读写函数:

IORD_32DIRECT(BASE, OFFSET) //从某地址读出32位的数据

IORD_16DIRECT(BASE, OFFSET) //从某地址读出16位的数据

IORD_8DIRECT(BASE, OFFSET) //从某地址读出8位的数据

IOWR_32DIRECT(BASE, OFFSET, DATA) //向某地址写入32位的数据

IOWR_16DIRECT(BASE, OFFSET, DATA) //向某地址写入16位的数据

IOWR_8DIRECT(BASE, OFFSET, DATA) //向某地址写入8位的数据

IORD(BASE, REGNUM) //从某地址按照CPU数据位宽(32)读出数据

IOWR(BASE, REGNUM, DATA) //向某地址按照CPU数据位宽(32)写入的数据

这些函数在使用的时候,会自动屏蔽CACHE,另外,针对每个特定的IP,Altera也都提供了相应的驱动文件,如PIO核,提供的驱动头文件名叫“altera_avalon_pio_regs.h”,在这里面定义了对PIO外设IP进行读写和控制的所有驱动函数,这些函数也都是自动屏蔽了CACHE的。而且,使用这个函数,能够方便的在各种NIOS II 版本的CPU之间移植,而不用担心CACHE的问题。所以,个人强烈推荐使用官方库进行IP核的使用。如果用户执意要坚持自己的观点,使用指针直接映射,那么请在定义指针的时候,将地址最高位置为1,例如, PIO_LED的地址为0x00000001,那么用户定义该地址指针时,请用#define PIO_LED (0x00000001 | 0x80000000),或者#define PIO_LED (PIO_LED_BASE | 0x80000000),其中PIO_LED_BASE在system.h头文件中定义。这样再操作就不会有问题了。

a、自定义驱动与NIOS II BSP工程提供的HAL驱动冲突。

常见现象为外设不受控,如看门狗不受用户程序控制,定时器不受用户程序控制,串口数据发送/接收丢失数据或乱码等。这是因为,当我们的QSYS系统中添加了这些IP外设后,在NIOS II EDS软件中创建模版工程时候,如果选择标准工程,如Hello World模版工程,则会自动添加所有外设的驱动程序,并在alt_main()函数中调用alt_sys_init()函数(位于bsp工程下alt_sys_init.c文件中)中将这些外设初始化,这些初始化就包括了注册外设驱动。因此,当我们在用户程序中再使用直接操作寄存器的方式来控制外设时候,则会出现用户程序和系统已经注册的标准驱动程序相冲突。

例如,对于串口接受,系统驱动已经默认将接受到的数据接收到了其缓存中(系统驱动使用中断方式接收,因此我们看不到直接的过程),可是这些是底层驱动实现的,我们并不知道,当我们再去读串口接收状态寄存器,就没法查到对应的接收成功状态寄存器是否有效,因为接收到数据后该状态已经被系统驱动函数给清零了,所以我们用户程序就出现了无法接收到串口收到的数据的现象。出现数据丢失。当然,这个情况会在接收数据一段时间后消失,用户程序又能读到状态和数据了,这是因为系统驱动函数将读到的数据先暂存在一个fifo数组中。读了一段数据后,fifo中的数据由于没有被用户程序及时取走使用,因此fifo满了,驱动程序就不会再去主动接收串口数据,也就不会去及时清除状态,所以我们用户自己读取状态寄存器,就能读到正确的状态了。

那么怎么解决这个问题呢?很简单,两种方式,第一种是我们手动修改alt_sys_init()函数中的内容,将注册系统驱动函数这部分代码给屏蔽掉,例如默认的,该文件中的内容如下所示:

void alt_sys_init(void)

{

ALTERA_AVALON_UART_INIT ( DEBUG_UART,debug_uart);

ALTERA_AVALON_UART_INIT ( UART_RS232,uart_rs232);

ALTERA_AVALON_UART_INIT ( UART_RS485A,uart_rs485a);

ALTERA_AVALON_UART_INIT ( UART_RS485B,uart_rs485b);

}

我们可以看到,该函数将4个串口默认都初始化了,这个初始化函数中就有注册驱动的功能。因此如果我们对某个外设不希望使用系统提供的驱动,就可以直接将该外设的初始化函数注释掉。例如,我们不希望UART_RS485A和UART_RS485B使用系统提供的默认驱动,就可以将该函数中ALTERA_AVALON_UART_INIT ( UART_RS485A,uart_rs485a);ALTERA_AVALON_UART_INIT ( UART_RS485B,uart_rs485b);两个调用注释掉。修改完成后的该函数如下所示:

void alt_sys_init(void)

{

ALTERA_AVALON_UART_INIT ( DEBUG_UART,debug_uart);

ALTERA_AVALON_UART_INIT ( UART_RS232,uart_rs232);

// ALTERA_AVALON_UART_INIT ( UART_RS485A, uart_rs485a);

// ALTERA_AVALON_UART_INIT ( UART_RS485B, uart_rs485b);

}

这样,这两个外设的驱动由于没有初始化,因此不会工作,这个时候我们就可以使用读写寄存器的方式操作该外设了,如发送一个字节的数据可以写为:

//等待发送寄存器可用

while(!(ALTERA_AVALON_UART_STATUS_TRDY_MSK

&IORD_ALTERA_AVALON_UART_STATUS(UART_RS485A_BASE)))

;

//写入待发送的数据

IOWR_ALTERA_AVALON_UART_TXDATA(UART_RS485A_BASE,0x88);

此种方式在用户执行generate bsp操作后会失效,因为我们改掉的代码会被重新覆盖为默认内容。该种方法用个成语说叫做扬汤止沸,无法根除问题,因此不推荐大家使用。

第二种方式,属于治根,那就是在BSP工程中取消生成对应设备的驱动。由于alt_sys_init.c文件中的内容都是根据bsp设置中的相关选项对应生成的,因此,要想每次重新generate bsp后都不包含不希望初始化的设备代码,可以在bsp中设置不使能生成该设备的驱动,方法为,在BSP Editor中,切换到Drivers选项卡,针对不希望生成驱动的设备,在其后面的Enable栏中,去掉勾项即可。

nio模式有什么优缺点(II开发全纪录之总结NIOS)(5)

当设置完成后,alt_sys_init.c文件会被自动更新,如果我们当前本来就是在alt_sys_init.c文件打开的界面然后去设置bsp的,那么当你设置完成,回到这个文件,系统会提示该文件已经被更改,然后我们选择yes,以更新文件内容,就可以发现,之前的RS485A和RS485B两个设备的驱动初始化代码已经不存在了。

nio模式有什么优缺点(II开发全纪录之总结NIOS)(6)

更新完成后,alt_sys_init函数中原本对RS485A和RS485B两个端口的初始化代码已经不在了:

更新前

nio模式有什么优缺点(II开发全纪录之总结NIOS)(7)

更新后

nio模式有什么优缺点(II开发全纪录之总结NIOS)(8)

再一种可能就是问题2的第一个原因,jtag uart的影响。

暂时总结到这里,NIOS II开发中遇到的问题很多,这里总结了一部分,剩下的问题,会以:开发注意点(二),甚至:开发注意点(三)推出。大家也可以在我的博客中查看之前的一些技术类文章。

最后还是那句话:学无止境,治学严谨,学以致用。

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页