笔者在CSDN的第一篇万字长文,请多多支持。
本文是笔者的公众号 IC设计者笔记 文章的转载。很多优质原创内容都会第一时间发布在公众号,欢迎关注公众号,一起交流学习。公众号后台回复“ZYNQ图像传输”即可免费下载包括Vivado工程、Python源码以及说明文档等文件。
前言
前段时间接到老板匆忙打电话,大概内容是:之前师兄流片的CMOS图像传感器马上要提交结题报告,需要帮忙用ZYNQ系列FPGA将图像传感器的数据实时传输到PC,并且通过上位机拍照。由于时间紧急,要求两三天内完成。当时自己心想:“FPGA开发+ARM程序编写+PC端上位机开发” 两三天完成,还包括调试。。。Are you kiding me ? 自己直言,两三天不现实,经过一番讨论,最终答应尽量在一周内完成。
自己之前过FPGA和PC通信的设计,只是当时数据量不大,直接用的串口。图像的实时传输,串口显然无法胜任。可供选择的只有USB和网口。自己没有接触USB接口开发,但接触过UDP通信,而且ZYNQ的教程有网络通信实验,故选择网口传输数据。至于PC端上位机部分,最方便的肯定是Matlab和Python,尤其是涉及到图像处理。正好自己最近学习Python,感觉开发起来效率挺高,所以决定“人生苦短,我选Python”。
下面自己分三部分讲解具体的实现过程:
FPGA 部分
自己接手的时候,情况是这样:FPGA端的CMOS sensor 图像控制部分Verilog代码师兄已经写好,自己的任务是写一个数据处理模块将CMOS sensor 输出的数据读取出来,然后将控制部分和数据处理部分封装成AXI接口的IP接到ZYNQ的PS (ARM) 端。FPGA和ARM通信的AXI接口类型常用的有两种:AXI-lite和AXI-steam。AXI-lite主要通过寄存器实现ARM和FPGA之间的数据交互,带宽较低,适合数据量不大的情形。AXI-steam则属于高带宽类型,一般都和DMA配合使用,实现大量数据的交互。相比而言,AXI-lite要简单很多,但要评估是否满足要求。显然AXI-steam和DMA更适合当前的应用,但经过认真考虑,还是采用了保守方案——AXI-lite总线。原因如下:1.师兄做的Image sensor像素不高,只有128x128。虽然是16个通道并行输出,但每个通道输出一个Byte需要16 clk,而且时钟频率只有10Mhz。如果AXI-lite总线用100Mhz时钟,且数据线宽为32Bit,这就意味着只需要在160个总线时钟内读取4次,就可以把16 Byte的数据全部从FGPA中读出。而且ARM的频率是666Mhz,用查询方式读取,应该不会出现数据丢失问题。2.自己没有用过DMA和AXI-steam使用经验,中途一旦出现问题,可能就要拖一两天时间(这才是自己最担心的),后面有时间了再去学习用这两种方法实现。*
具体实现步骤如下:
1)创建自定义IP
vivado工程创建过程我就不写了,下面主要介绍如何将AXI-lite总线与自己设计的目标电路连接起来并并封装成一个IP。
任何总线都是通过 ‘读’ 和‘ 写’ 两种操作实现CPU和硬件外设的通信。通信的数据主要包括控制信号,状态信号以及需要读写的数据,这些信息的传递都是通过中间的寄存器实现。
我们在用vivado 新建 AXI-lite总线的IP时,可以选择中间寄存器的个数,这里我设置了20个,接着Vivado会自动生成 AXI-lite总线的代码,自己需要做的就是将这些信号和目标电路连接起来。
2)CPU写操作
我们首先来看一下刚刚设置的这20个寄存器,这些寄存器就是用来存放CPU写入的数据。每一个寄存器都有唯一的32位地址,而且这些寄存器的地址是根据其编号从低到高连续累加的,内部地址是(0~19)。当CPU向某一个地址写入数据时,数据就会被存放至与该地址对应的寄存器中。目标电路所需要的输入信号(控制信号和配置参数等)就是通过这些寄存器传递。
下面就是目标电路的顶层输入信号,其中启动信号和测试模式的开关由 slv_reg0
的最低两个Bit实现,而switch0-5
是控制image sensor 的开关信号,由slv_reg0
的 2-7 bit 控制。
另外还有两个控制曝光时间长度的信号,它们分别由 slv_reg1
和 slv_reg3
控制。 这样我们就实现了输入信号的连接。
3)CPU 读操作
读操作就是将Image Sensor的图像输出读出来,先看一下目标电路的顶层输出信号,输出信号主要包括数据和相应的标志信号。CPU 查询方式读出数据的原理是:循环读取标志信号,当标志信号为 1 时即说明当前有数据输出,这时再读取数据。
CPU读取数据和状态信号的关键部分如下,十六进制 0-13即内部的20个偏移地址,当CPU读取某一个地址时,相应信号的值就会传给CPU。
值得注意的是,上面的CPU读信号中并没有目标硬件电路的标志信号,因为硬件标志信号一般都是持续一个时钟周期,而CPU查询一次往往需要多个时钟周期,如果将标志信号直接传给总线,CPU是无法正常读出的。因此需要设计一个简单的电路来锁存标志信号,同时CPU读完以后信号还要自动释放。
以标志信号 word_en
为例,该电路实现方法如下,software_flag
即锁存的word_en
信号:
代码讲解如下:word_en
上升沿到来后将software_flag
置 1,而software_flag
清除的条件是 总线处于读使能且总线地址位的内部偏移地址为 4。因为 word_en
是数据 word1~4
的标志信号,而读操作的地址 4 即对应 word1
的数据。CPU开始读 word1
则表明 CPU 已经检测到了 word_en
有效。当然也可以将 software_flag
清除的条件改为 总线处于读使能且总线地址位的内部偏移地址为 0,因为读操作的地址 0 即对应 software_flag
,这样software_flag
持续的时间会比较短。
ARM裸机程序部分
ZYNQ芯片内部有两个ARM Cortex-A9 CPU,主频为766MHz, 虽然可以运行Linux,但是需要为Image Sensor控制电路编写驱动程序,为了节省时间,直接运行裸机程序。ARM处理器主要负责配置CMOS sensor的工作状态,同时将数据从FPGA中读出来并与上位机通信实现图像实时传输。
1) TCP 通信
网络传输采用的TCP协议,由于我们采用的裸机程序,实现TCP通信最合适的方法就是使用light-weight IP stack (lwIP)小型开源TCP/IP协议栈,Xilinx SDK中提供有Demo工程,如下图
本文中的代码是在 LWIP Demo工程的基础上进行改写。TCP协议中通信的双方分为Server和Client,Client只要知道了Server的IP和端口号就可以建立连接。根据上图可知,LWIP的demo工程中FPGA作为Server,监听端口号为 Port 7,程序实现的功能是将收到的数据自动回传给client。
Demo工程建立好以后对LWIP进行以下配置,以提高传输效率。
使用Demo工程最大的好处就是我们不需要懂TCP协议的具体通信过程,只需要修改数据的接收和发送函数即可。
Demo工程中负责发送和接收数据的函数在echo.c中,函数内容如下:
从上图中的代码可以看到:
64行接收数据,71发送数据,而发送的内容为 p->payload
所指向的内存区,即接收的数据存放在 p->payload
所指的地址处,数据长度为 p->len
。读懂了函数的功能就可以根据自己的需要进行修改了。
首先将数据发送部分替换成自己写的函数 recv_process(tpcb, p)
,该函数负责处理上位机的指令并发送图像数据。
我们所定义的上位机的指令以0x55
开头,第二个字节即命令,命令分为4种:状态查询,启动图像采集,关闭图像采集,以及工作模式设置。
要想控制硬件,首先需要定义一个结构体,该结构体中的变量与硬件电路中的寄存器一一对应,如下图:
因为ZYNQ内部采用的32位AXI总线,因此结构体内所有变量都是32位。这些变量的顺序与FPGA控制电路的寄存器相对应。当我们向这些变量中写入数据时,数据会自动传递到如下寄存器
而当读取这些变量的值时我们得到的是硬件电路中如下信号的值
结构体定义好以后,最重要的一步就是定义结构体指针,并将指针指向 FPGA控制电路的基地址(XPAR_IMAGE_CTL_0_S00_AXI_BASEADDR
),这样就可以对硬件进行读写了。同时还需要定义两个BUF,用于保存从Sensor中读取的图像数据。
硬件控制电路的驱动函数如下。该函数的核心是用软件读写Image sensor 的控制电路的寄存器,从而实现图像数据的读入,同时控制传感器的工作模式。函数内容如下图:
该函数的具体流程是:设置曝光时间、工作模式、启动Sensor、通过查询方式读取图像数据。
因为我们定义的ImageBuf是8位数组,而从总线中读到的数据是32位。一般的操作需要四次才能将32位的数据写入4个8位的变量中,而通过如下方式,可以一次将 32位数据写入 4个变量
*(int *)(ImageBuf[1]+pixel_num) = a0;
下面简单介绍一下图像传输函数 trans_image_data(tpcb)
。
该函数首先调用图像驱动函数读取数据。图像数据读完以后,调用 tcp_write()
将数据写入到 TCP 缓存,最后调用 tcp_output()
将数发送出去。
正常情况下,写入TCP缓存前需要先判断剩余空间 pcb->snd_buf
大小。由于我们在TCP BSP 参数配置时,将 tcp_snd_buf
设置为最大值 65535,而图像数据小于此值,故此处没有判断剩余空间,直接将数据全部写入。
到此ARM裸机程序编写完成。梳理一下,包括三个部分:上位机命令接收和处理、Image Sensor 控制及数据读取、数据发送。
Python上位机开发
Python上位机是自己用两天的时间做的一个简单demo。
下面我对上位机程序进行简单整理。
上位机功能可分为四个方面:1. TCP通信;2.OpenCV图像实时显示及拍照;3.GUI界面;4.将所拍照片保存为CSV文件。
需要使用的库包括:numpy
, cv2
, socket
, tkinter
,pandas
1) TCP 通信
Python实现TCP通信和UDP通信一样简单,只需要简单调用几个函数即可,具体步骤如下(Python为客户端):
- 创建socket套接字
soc=socket.socket()
- 建立连接
soc.connect((server_ip_addr, port))
server_ip_addr为 server端的IP地址,port为server监听的端口号
- 发送数据
soc.send(send_data)
send_data 为我们需要发送的数据。注意:发送的数据必须为bytes 类型,如果是其他数据类型,需要用 bytes()函数转换。
- 接收数据
soc=socket.socket()
其中buffer_size 为一次最多接收数据的个数,tcp_dat 为接收到的数据。接收数据有两点需要注意:
- 如果server端一次发送的数据量较大,网网关会自动将数据分成多批发送,如果我们只调用一次 recv(buffer_size),会导致数据接收不完整 (自己有实际踩坑经历)。
- 接收到的数据类型为 bytes 类型,需要转换成 自己所需的类型。
完整数据接收及类型转换可通过如下方法实现(当然,此方法的前提是我们知道每张图像的大小,即上位机一次发送了多少数据):
int_dat 保存接收到的int型数据,当接收到我们设定的数据量时停止接收。
2)OpenCV 图像实时显示
因为我们采用的opencv库,图像实时显示非常简单。
首先将接收到的图像数据转换成 numpy 整数类型,由于数据是一维的,需要进行图像重构变成128x128的二维数组。然后调用 cv2.resize( )函数对图片大小进行缩放,下图中的 img_test1 即为调整大小后的二维图像矩阵。最后调用cv2.imshow( )即可显示图像。
拍照通过 cv2.waitKey(1) & 0xFF == ord(‘c’) 实现,当键盘按下‘c’ 键时即停止图像采集和显示,从而实现拍照功能。
3)GUI 界面
GUI界面采用 tkinter 库实现。首先我们需要添加所需的控件(比如:输入框、显示框、按键等),代码如下:
该程序的具体步骤包括:
创建主窗口,并设置窗口title和大小。
self.root = tkinter.Tk()
创建一个输入框,并设置宽度。该输入框用于设置server端IP
tkinter.Entry(self.root, width = 50);
创建一个显示框,用于显示提示信息,并设置宽度。
tkinter.Listbox(self.root);
创建三个按钮,分别是Connect (建立以太网连接), Start(启动图像传输) 和 Save (保存图像数据)。
tkinter.Button(self.root, command = self.connect_fun, text = "connect");
注意该函数有三个参数,第二个参数为按键被点击时所调用的函数,第三个参数为按键上显示的名称。
GUI界面所需的控件添加完成后需要对它们进行布局,有两种方法可以实现
place()
和 pack()
, place()
需要指定起始坐标以及长度和宽度(坐标原点为GUI界面左上角),pack()函数则可以自动调整布局。一般我们需要两种方法结合使用。
如果我们全部使用pack()
自动布局,代码如下
布局效果如下图,按键大小根据文本长度自动调整的,好像不太美观。哈哈哈。。。
如果我们对三个按键如下调整,效果就好多了:
布局完成后,还有最重要的一步,调用 tkinter.mainloop()
函数,这样运行程序是GUI界面才会出现。
4)保存数据
将数据保存至 .csv 文件可以用 pandas 库实现,
保存数据前我们可以用python自动创建文件夹并用当前时间作为文件夹名称,具体代码如下:
数据保存只需要两个函数即可实现:
save = Pd.DataFrame(self.img_buf[0]); # 转换数据格式, 此处 self.img_buf[0] 为需要保存的数据,此处为128x128的二维数组。
save.to_csv(file_name) # 保存数据
结束语
如果本文对你有帮助,请多多点赞支持,让作者有动力创作更多优质内容。最后,欢迎关注微信公众号IC设计者笔记,很多技术干货内容会第一时间在公众号发布。公众号后台回复“ZYNQ图像传输”即可免费下载包括Vivado工程、Python源码以及说明文档等文件,下载成功后记得回来给作者点赞。