Zynq-7000 开发总结
近期经手了使用zynq开发的项目,中途遇到了很多问题,大多都是基础性问题,最后也都一一解决了,这里挑选一些印象深刻的坑进行记录,以鉴后者。
软件版本:vivado 2017.4,及其配套的sdk。
芯片型号:xc7z020clg400-2
1、软件使用问题
(1)vivado 生成ps7的系统软核,其中FCLK_CLK0端口设置为100MHz,在使用过程中会自动变为"1e+8Hz"
以上问题,会在sdk中暴露出来。在sdk中,会根据ps7,也就是ZYNQ7 process system这个IP核的配置信息来配置芯片上两片arm核及其外设,正常情况下,时钟频率会宏定义为100000000U
,在C语言其中的U是指无符号整型。由于端口变为了1e+8
,则会在生成软件支持包(bsp)的时候出错,因为会被识别成浮点数,这就与U
相矛盾了。进一步的,跟该时钟相连接的其他模块的时钟端口信息也会变为1e+8
。图中是正常的情况。
解决办法:
- 将模块删除,重新创建IP核,并且祈祷下次综合时,不要变成
1e+8
。一开始我就是这样将就着用的。 - 点开ps软核,Clock Configuration -> PL Fabric Clocks -> FCLK_CLK0 修改为99MHz,保存并退出IP核配置,再保存一下block design,观察到FCLK_CLK0端口从
1e+8Hz
变为了99MHz
,再进入IP核配置,修改回100MHz即可。
(2)SDK中,在软件支持包中分明存在f_close
、f_open
、f_mount
等函数,甚至点击F3都能直接跳转到该函数在ff.h
中的定义,但是仍旧报错,称找不到相关读写sd相关的函数
诸如此类的问题,每次都会浪费很多时间,解决办法就是将自己写的程序单独保存,然后将整个project.sdk
目录删掉,然后在vivado中重新导出hdf文件,再lauch sdk,再新建app,并正确设置bsp,再将自己写的程序拷贝进新建的app中,刷新即可发现报错被解决了。
我也曾尝试单独删除bsp,并且重新生成新的bsp,但是并非每次都能解决问题,进而浪费更多时间。因此,建议遇到找不到文件或者目录的问题,重新生成一次bsp,如不能解决问题,则直接整个重来。
(3)SDK中,找不到xil_printf.h
区别于(2),(2)是找不到bsp的supported libraries中选择的xilffs
相应生成的ff.h
下面的程序,而(3)则是找不到常规的xilinx的库文件。这种情况则是在app右键选择properties->C/C++ General -> Paths and Symbols->GNU C->Include directories
点击添加workspace中bsp的include文件夹,即可。
需要注意,bsp的properties
是没有Paths and Symbols
的。
进一步的,我也遇到过string.h
这样的c语言标准库找不到的情况,这种一般重新编译一下就能解决。
基本上软件问题一般都能通过重新启动软件、重新创建程序来解决问题。
2、PL端程序设计问题
这次主要碰到的问题是IP核打包问题。
这次开发中,由于需要使用一个bram控制程序。ps端可通过Axi Bram Controller
这个IP核控制BRAM,另外如需将PL端的数据写入到BRAM,则需要根据BRAM的控制时序自己开发RTL程序。另外如需PS控制PL端的读写地址、读写长度、读写使能等,则需要将AXI总线的帮助。
学习使用AXI总线协议,是比较复杂繁琐的。在这里简单梳理AXI开发逻辑,详细内容不在此处展开。
通过使用Vivado的IP核打包工具,任意打开一个Vivado,Tools -> Create and Package New IP -> Create AXI4 Peripheral
,即可创建相应的AXI接口。可根据需要增加或者减少AXI的数量。AXI分为Lite、Full、Stream三个版本,其中顾名思义,Stream是流控制版本,是涉及端口信号最少,也最容易上手的,无地址信号,意味着也无法去精确的读写。Lite可以控制寄存器读写,以及配置寄存器数量,适用于这种简单的读写BRAM控制程序,并使用PS传输读写控制信息。
IP打包工具则会生成相应的AXI模版,其中PS到寄存器的部分的通信逻辑已经创建好了,仅需添加自己写的BRAM的状态机即可,当然添加时需要设置一些端口。
此外,IP打包工具还会生成驱动程序,在启动SDK后,就会将其中关于寄存器、以及一些寄存器地址信息写入到xil_parameters.h
中,并且还会生成相应的头文件以及IP相关的库函数,当然自带的只有读寄存函数和写寄存器函数。
通过这种方法,就能建立起PS和PL端最基本的通信,通过读写寄存器进行简单的交流,通过读写BRAM来交换更多的数据。进一步的,可以通过DDR进行更大带宽、更大内容的读写交互。
3、PS端程序设计问题
这部分主要是C语言编程,以及外设中断设计的问题。
(1)zynq中断使用
在嵌入式软件编程中,最常使用的一个技术就是中断,中断可以暂停主函数的运行,并执行中断服务,结束后再回到主函数。
一个经典的应用就是设置按键中断,通过通过设置按键中断,当按下按键触发中断,在中断服务函数中识别是哪个按键被按下,从而在主函数中执行相应的程序,比如打印状态信息、发送一些指令、结束程序等。
启动一个典型的中断程序包括以下步骤:初始化外设和中断控制器,连接中断及中断服务函数到外设控制器,设置中断在中断控制器中的优先级和触发方式,使能中断控制器。此外,为了避免一些异常发生,还需要额外注册中断异常函数(同样也需要提前初始化),并将中断异常使能。
当然,不同的外设的触发中断的事件也不同,比如UART中断,就分为发送类、接收类、异常类,发送fifo空或者满,或者将空或者将满,都可以设置为中断触发的条件,具体根据使用需要来设置UART的中断掩码实现。
比较常用的定时器中断,适用于高精度定时的周期性的应用中。在zynq-7000中的是32位递减计数的定时器。配置定时器,主要有以下步骤:初始化定时器、定时器自检、定时器载入初值、定时器启动。要实现周期性功能,还需要启动定时器的自动重启功能,否则定时器只会倒计时一次。在zynq的定义中,定时器的时钟频率是CPU系统时钟频率的一半,CPU频率默认值为2/3GHz,则定时器频率为1/3GHz,即每隔3ns定时器计数值减1,通过获取当前计数值,则能推算出距离定时器启动隔了多少时间。
一次定时,最多倒计时 2 32 / ( 1 0 9 / 3 ) = 12.885 2^{32}/(10^9/3)=12.885 232/(109/3)=12.885秒,可通过设置定时器的预分频器来提高定时器的计时最大值。比如设置十倍的分频,倒计时时间可提高到128.85秒,同时定时器精度从3ns降低到30ns。在zynq-7000中,定时器最大支持255倍的分频,PrescalerValue是一个8bit的无符号数。当然也可以通过记录重载次数来提高计数最大值。
另一方面,通过设置定时器中断,可以在每次计时器递减到0时触发中断,从而完成周期性任务。其中断设置方式并无任何不同,都是初始化、连接、优先级、使能。
(2)函数循环计数递增,最好每次增加1
图中的BRAM_BYTENUM是常数4,因此每次循环i就会递增4,在数组索引时就会出错。当然可以通过i/4
作为数组索引,但是不推荐这样做。一般能够通过乘法解决的问题,就不要作除法。i递增1,读地址则写成i*4
。
图中这段错误代码放在中断服务函数中,会引发中断异常,并进入Xil_DataAbortHandler()
,这个函数在xil_exception.c
的258到278行,是一个死循环。而且由于vivado sdk在代码静态检查时,未能覆盖到非主函数这部分,因此未曾报错,最后花了很多时间才发现这里出现了数组越界。
(3)在主循环中使用了switch语句,由于case语句占用了break,故无法使用break退出循环
两个解决办法。
- 设置退出标志,在循环中检测退出标志,检测到之后则break退出循环。
- 使用goto语句。直接从switch中退出循环,简介方便,但需谨慎使用。
(4)在c语言中取绝对值,可以使用abs()函数,但它仅支持整型。
在<stdlib.h>
中定义了abs()
函数,可以为int
类型的数据取绝对值。浮点数取绝对值,需要使用<math.h>
中的fabs()
,更加简单方法是自己写一个取绝对值函数。这里就因为直接使用了abs()
对浮点数取绝对值,导致数据类型出错,不过很快就找到原因了。
float fun_fabs(float a, float b)
{
if(a-b>0)
return a-b;
else
return b-a;
}
(5)注意中断触发的时间,中断触发前主函数会持续执行。
如下面的示意代码,read_data打印出来,如果中断触发有所延迟,则首先打印的是0,而非1。
int read_data=0;
int main()
{
while(1){
fun_a();
printf("%d",read_data);
}
}
void fun_a()
{
tri_interrut();
}
void intr_handler(void * CallbackRef)
{
read_data++;
}
此次开发过程中就遇到了这个问题,调整执行顺序之后,read_data
的数据正确了,否则read_data
是上一次的读取的数据。
(6)<sleep.h>
中的sleep(),只能接受整数值。
休眠时间如果输入浮点数则会出错。
(7)XUartPS_Recv()函数的接收时,每次都会少接收一个字节。这个问题debug了将近两三个小时,原因很简单,但较难发现。
现象:发送端一直发送EB 90 00 00
,除了第一次接收正确外,后面每次接收都会少一个字节。
1th recv: EB 90 00 00;
2nd recv: 90 00 00;
3ed recv: 90 00 00;
4th recv: 90 00 00;
......
原因:
-
接收逻辑:超时分包策略,超过指定时间间隔,则视为一次接收结束。
while(1){ //main loop tmp_recv_cnt = XUartPS_Recv(&UART_PS,Read_Buff[Recv_cnt],1); // 若未从rx-fifo中接收到数据,则计数+1 if(tmp_recv_cnt == 0){ tmp_cnt++; }else{ Recv_cnt+=tmp_recv_cnt; tmp_cnt=0; } if (tmp_cnt>100){ read_end(); Recv_cnt=0; } }
-
问题:接收结束之后,UART_PS的值未重置跟初始化一样。
即图中InstancePtr->ReceiveBuffer.RequesteBytes
未置零,当rx-fifo再次有数据写入时,会首先写在InstancePtr->ReceiveBuffer.NextBytePtr
,也就是第一个字节0xEB
写在了Read_Buff[3]中。
- 解决:写入结束时,将
UART_PS.ReceiveBuffer.RequesteBytes=0;
(8)volatile关键字的使用
volatile int global_var;
使用该关键字修饰需要在其他文件或者中断中使用的全局变量,可以防止编译器将其优化为寄存器变量,确保其严格按照程序顺序直接从内存中访问读写。