SYS/BIOS是TI的实时内核,也叫做TI-RTOS,它是RTSC中的一个软件包,可以使DSP的软件设计更加方便。SRIO是适用于嵌入式系统中的高速接口,之前我也写过一些相关的文章,这次做了一些完善。

系统功能

  整个系统实现的功能大概如上图所示。DSP外接的DDR中存放有两张640×512的图片,DSP与FPGA通过一条1x的SRIO链路相连。DSP周期性地向FPGA发送门铃事务包,由FPGA内的MicroBlaze软核解析后,FPGA端发送相应的多个SRIO NREAD事务包,从DSP外接的DDR中读取特定地址区域的图片,然后通过VGA交替显示两张图片。
  DSP采用的是TMS320C6455,FPGA端实例化MicroBlaze软核处理器,由它向SRIO收发控制器发送命令。

SYS/BIOS的使用

  自从开始使用它以来,我觉得大部分情况下,我们都可以在这个实时内核的框架下来完成应用的设计。它的主要优势在于灵活,相较于传统的裸核程序运行在一个死循环里,它能够更快地添加一些功能,只要创建几个新的Task就行。在使用之后发现,它能够更方便地管理中断,更精确地控制延时。

文档查阅

  查询相关文档的方法在之前的文章中也已经介绍过了,在Help->Help Contents里,可以找到所有RTSC的软件包的API的介绍,SYS/BIOS自然也不例外,而且非常详细。

1
2
3
4
5
#include <ti/sysbios/BIOS.h>
#include <ti/sysbios/knl/Task.h>
#include <ti/sysbios/knl/Semaphore.h>
#include <xdc/cfg/global.h>
#include <xdc/runtime/System.h>

  上面这些是一些常用的头文件。"BIOS.h"是最基本的,里面的BIOS_start()就相当于是FreeRTOS里的vTaskStartScheduler()。
  Task和Semaphore是最常用的,必要时还可能会用到Hwi、Cache、Event、Queue、Clock、Timer等API。
  include "global.h"主要是让C/C++源文件能够获取到XDCTools配置文件中创建的句柄,比如Task、Semaphore的句柄等。
  "System.h"主要用来实现打印输出。

UIA的使用

  UIA(Unified Instrumentation Architecture),是一个调试用的工具。我觉得它最棒的功能就是它能够把几个任务之间的切换通过下面这种图的方式表现出来。

  只需要根据“SPRUH43F-System Analyzer User’s Guide”这个文档进行配置就行,这个文档在UIA的安装目录下就有。主要是在XDCTools的配置文件中添加下面这行代码,然后也就有相应的图形界面可以设置。

1
var LoggingSetup = xdc.useModule('ti.uia.sysbios.LoggingSetup');

时间片调度

  SYS/BIOS是没有直接的时间片调度的,也就是说如果有两个相同优先级的Task,那就是哪个Task先创建就先执行哪个。如果想要进行时间片调度,就需要自己添加一个Clock的实例,并周期地执行"Task_yield()",让执行中的Task主动放弃CPU资源。这在SYS/BIOS的User’s Guide里的3.5.6中举了个例子。

精确控制延时

  使用SYS/BIOS能够精确地控制延时。Clock能够用来创建软件中断(Swi)层面的thread。Clock tick是计时的基本单位,包括在等待Semaphore或者Event时,也都是这个统一的单位。默认的一个Clock tick是1ms,这个可以在Clock的配置页面看到。
  而这个准确的计时是由硬件定时器Timer来完成的,每次Timer定时1ms都会自动调用Clock_tick(),使Clock的计数值加一。

  在Task中利用Task_sleep(delay)函数可以使Task休眠delay个Clock tick。在Clock tick是1ms的情况下,Task_sleep(1000)就可以让Task休眠1s。

中断管理

  在使用CSL库的时候,中断的配置是挺头疼的,需要创建好几个全局变量,需要一开始就确定中断的个数。但在SYS/BIOS里,就可以在XDCTools的配置脚本中完成,而且可以直接通过图形化界面配置。

  在C/C++的源文件中我们就只需要写相应的中断服务函数就行了。

关于RTSC工程

添加Section

  在创建了RTSC工程工程之后,不管是有没有用SYS/BIOS,原先的链接脚本文件基本上已经没用了,RTSC工程会自己产生一个链接脚本文件。如果仍然保留原先的链接脚本,很可能就会产生冲突,所以不建议再添加链接脚本。

1
2
Program.sectMap[".image"] = new Program.SectionSpec();
Program.sectMap[".image"].loadSegment = "DDR2";

  如果想要添加一个新的Section,需要在XDCTools的配置脚本中用类似上面的代码将Section分配到Memory中。上面的代码是将叫做".image"的Section放到叫做"DDR2"的Memory中。

打印输出

  打印输出采用的是System_printf(),默认不支持打印输出浮点数,如果想打印输出浮点数,需要在XDCTools的配置脚本中加上下面这行设置。

1
System.extendedFormats = "%f";

保留数据

  我将两幅图的数据定义成数组的形式,并存放到特定的地址空间里。等待FPGA端的SRIO来读这部分数据。

1
2
#pragma DATA_SECTION(nIrDst, ".image");
const Uint8 nIrDst[] = {……};

  然而因为我没有在其它地方对这部分数据进行读写,所以编译器在编译之后就把这部分数据省略了。

  为了解决这个问题,可以在linker的配置中显式的告诉linker这个变量不能省略。

FPGA端设计

  实例化了一个SRIO接收控制器用于接收门铃中断,实例化了一个SRIO收发控制器用于发送NREAD事务包,从DSP读取图像数据,并发送至VGA显示模块中的显存。

Local Memory与固化

  MicroBlaze要实现的功能非常少,只要接收门铃中断,然后配置SRIO收发控制器,发起NREAD事务就行,所以我一开始就想可以把代码都放在Local Memory上,选了8kB的大小,以为足够了。但事实证明,如果要打印很多东西,8kB是不够的,后来我把那些打印的函数都去掉才勉强把代码都放在8kB的空间里。以后应该把Local Memory选大一点,16kB应该差不多,再不行就把代码放DDR上去,那样就需要加一个bootloader的工程。
  因为代码都是存在BRAM中的,所以它可以和bitsteam一起固化,相当于把BRAM初始化为代码。

  右键点击BlockDesign,选择“Associate ELF Files”,再选择Vitis中编译并且调试好的elf文件。重新生成bitstream,再将新生成的bitstream固化即可。

测试结果

  依靠SRIO读确实挺慢的,因为我只用了单条链路,而且一包NREAD最多读256字节,一帧640×512的8bit位深度图像就需要1280包。每次FPGA发出请求,到DSP响应也需要很长的时间。所以如果追求传输速率的话,最好还是DSP主动向FPGA发数据。现在这么做的好处只是能够让DSP可以不干预数据的传输。