AXI协议(六) AXI-Stream接入实例及小总结
在这节中,你将可能看到:
- 一个普通的摄像头接口介绍
- 摄像头数据转AXI-Stream的接入实例
- 关于AXI4协议暂时性的总结
文章目录
文前声明
- 本文所举例来自黑金的ov5640摄像头驱动代码,开发板是zynq系列的,本文仅用于学习交流,知识分享,如有侵权通知后会下架(到时我再自己写一个吧)核心转换ip、流程如下:
本文也不会放硬件的原理图和PCB,会自己画图取而代之
普通摄像头的硬件模块
这里的"普通"是除mipi或者其他专用设计以外的摄像头。以ov5640为例,一般会包含两个部分:
- 通过IIC进行摄像头的各种参数配置
- 像素流+行同步和列同步信号
如图所示,iic配置接口在ZYNQ的PS端,即ARM端做配置,数据流在PL端,即FPGA做接收。由于本文主要讲PL端的AXI-Stream转换,所以我们默认IIC部分是已经实现好了,使用行扫描模式。
这个摄像头在配置的时候有很多参数可以设置,在这里我们就不做介绍了,主力于把Axi-Stream搞出来。
需要注意的是,在硬件上,ov5640是10位的,但是在本模块中只用了高8位做数据传输。
基本思路和问题
在接着往下做的时候,我们先厘清一下问题:
- 摄像头传进的数据的时钟域与AXI总线的时钟域不同
- 像素格式和硬件上的“data”线往往是不匹配的,比如我们可能接收的是RGB888,假设数据线位宽为8,这需要三个时钟才能传完一个像素,此时的行列同步信号时序需要提前规定好,不然容易空行。
实现思路
而对于实现方式来说,对一个行扫描的摄像头,我们可以以行为axi-stream的last信号,列同步也就是帧同步信号因后级的VDMA需要放在tuser[0]中,可见于手册:
但是需要注意,这里的行列同步信号是需要通过处理的,正如上面的问题2而言,若我们直接将行列同步信号往外送,就会出现空行和空帧情况。
问题解决思路
-
对于时钟域问题解决不当会有“亚稳态问题”,主流解决方法有两种:
- “打两拍”
- 异步fifo、ram
这里刚好和我们上一篇中讨论的“Default value signaling”有关系,由于各家定义的不同,我们可以省略很多axis的信号线,其中对于tready信号则讨论了反压的问题,所以这里我们选择异步fifo,让流水线出现反压时,fifo可以先局部地缓存一下数据
-
像素格式和硬件上的“data”线不匹配问题可以根据手册按照自己想要的像素格式和硬件数据线做一个转换,比如在本设计中像素格式是RGB565,而接收数据线只有8bit,所以就需要做一个8转16的模块,同时处理边界指示信号问题。
所以具体解决思路为:
代码解析
这里大家从代码上就知道,问题的关键点在于怎么拿fifo的数据位,empty和full跟AXI-Stream协议握手做映射
完整代码建议大家支持黑金的zynq开发板后获取,或者去他们的官方论坛获取,此处不放出链接
我们假设同步信号按下面的格式进来:
所以整体的代码实现上有这三点:
- 摄像头像素格式的对齐cmos8->16
- 将同步位和像素数据转成axis的"初步格式"
- 加入异步fifo,完成异步时钟域处理
像素格式对齐
这里主要对应cmos_8_16bit模块
这里主要是将两个pdata_i合并在一起就完事了,de_i是有效位,所以我们可以根据de_i做一个二分频就可以得到输出的像素时钟。所以像素输出和有效位de_o控制如下(为简便展示,对源代码有修改):
// 输出
always@(posedge pclk or posedge rst)
begin
if(rst) begin
de_o <= 1'b0;
pdata_o <= 16'd0;
end
else if(de_i && x_cnt[0])begin
de_o <= 1'b1;
pdata_o <= {pdata_i_d0,pdata_i};
end
else begin
de_o <= 1'b0;
pdata_o <= 16'd0;
end
end
其中,x_cnt[0]为de_i的2分频:
always@(posedge pclk or posedge rst)
begin
if(rst)
x_cnt <= 12'd0;
else if(de_i)
x_cnt <= x_cnt + 12'd1;
else
x_cnt <= 12'd0;
end
但是像素的有效位做二分频后,就不能再用有效位的下降沿做axis_tlast了,所以我们需要拿原来的有效位给个延迟做行同步信号:
always@(posedge pclk or posedge rst)
begin
if(rst)
hblank <= 1'b0;
else
hblank <= de_i;
end
至此,像素处理结束,因为代码上逻辑层次比较清晰,这里给出例化代码,不然下面代码比较难讲:
cmos_8_16bit cmos_8_16bit_m0(
.rst(~cmos_aresetn),
.pclk(cmos_pclk),
.pdata_i(cmos_d_d0),
.de_i(cmos_href_d0),
.pdata_o(cmos_d_16bit),
.hblank(cmos_hblank),
.de_o(cmos_href_16bit)
);
将同步位和像素数据转成axis的"初步格式"
这里主要处理的是同步位,因为像素数据是直接接fifo的,逻辑上比较直接。
这里不解释各个信号的延时,因为后面要检测hblank的下降沿和16bit的数据要等一拍才能出来,导致几乎所有的信号都要delay,这个具体时序大家自己仿真对一下吧🦾。
我们直接看代码中的对应关系(slave端,逻辑关系见上图):
assign s_axis_tready = ~full; // fifo的满信号
wire s_axis_tvalid = cmos_href_16bit_d1 & cmos_hblank_d1 & s_axis_tready;
wire[15:0] s_axis_tdata = cmos_d_16bit_d1;
// 时序逻辑部分 axis_tlast
always@(posedge cmos_pclk)
begin
if(cmos_aresetn == 1'b0)
s_axis_tlast <= 1'b0;
else
s_axis_tlast <= cmos_hblank_d0 & ~cmos_hblank;
end
// 时序逻辑部分 axis_tuser
always@(posedge cmos_pclk)
begin
if(cmos_aresetn == 1'b0)
s_axis_tuser <= 1'b0;
else if(cmos_vsync_d1 == 1'b1 && cmos_vsync_d0 == 1'b0)
s_axis_tuser <= 1'b1;
else if(s_axis_tuser == 1'b1 && s_axis_tvalid == 1'b1)
s_axis_tuser <= 1'b0;
end
可以看出:
- ready和valid信号主要在判断fifo的状态和**'像素数据’的有效**,和AXI协议上握手过程是一样的,唯一有疑惑的就在于在AXI协议中,说明valid信号不能根据ready而置位,否则可能会引起互锁。不过与后级fifo连接中可以得知,这里的与s_axis_tready可以看作是axi握手成功的标志。
- 同步信号基本就是原传进来的cmos_hblank和cmos_vsync,通过检测他们的下降沿得到同步信号。
加入异步fifo
再看一次框图,所以在具体的fifo连线中,是这样的:
- 在从端(输入端):
.wr_clk (cmos_pclk),
.wr_en (s_axis_tvalid & fifo_ready),
.din ({s_axis_tdata,s_axis_tlast,s_axis_tuser}),
.full (full),
其中,fifo_ready是fifo这个ip核所需要的复位时间,这里不展开。所以可以看见wr_en是只受s_axis_tvalid影响的,所以我们可以将这一部分重构成:
// 旧版
assign s_axis_tready = ~full; // fifo的满信号
wire s_axis_tvalid = cmos_href_16bit_d1 & cmos_hblank_d1 & s_axis_tready;
.wr_en (s_axis_tvalid & fifo_ready),
// 新版
assign s_axis_tready = ~full; // fifo的满信号
assign s_axis_tvalid = cmos_href_16bit_d1 & cmos_hblank_d1;
.wr_en (s_axis_tvalid & s_axis_tready & fifo_ready),
这样大家可能就能理解我上面说的"这里的与s_axis_tready可以看作是axi握手成功的标志"。
- 在主端(输出):
.rd_clk (m_axis_video_aclk),
.rd_en (m_axis_video_tready & ~empty & fifo_ready_maxis),
.dout ({m_axis_video_tdata,m_axis_video_tlast,m_axis_video_tuser}),
.empty (empty),
其中,fifo_ready_maxis是在axi时钟下fifo的复位完成标志位,这里异步时钟处理用的是打拍,也不展开。
此时我们只需要加上主端数据输出的握手设计,整个ip就做完了:
assign m_axis_video_tkeep = 2'b11;
assign m_axis_video_tvalid = ~empty & m_axis_video_tready;
可以看到,他这里的valid又跟ready沾一起了,我们这里就不管了,毕竟不知道外部的ready信号怎么来。
- 观察可知,tlast和tuser走的是数据通路。
加点魔法
对这个我们自己做出来的AXIS,这个时候看回框图:
实际上我们可以用xilinx的AXI4-Stream Subset Converter转换成"更为标准和完整的流数据"
我们看看黑金的实例工程的设计:
可以看到,这里我们再把我们之前接出来得到RGB565改成了RGB888,输出的位宽也变成了24。这里可以回想在AXI协议中,做这种非标准位宽传输就会涉及到非对齐传输(Unaligned Transfer)问题。所以后级在VDMA中,也需要打开这个选项,因为我们后级存进ddr的是64位的。
AXI4简要总结
这个协议写到这里其实要迎来一个“小结束“了,因为再介绍下去就是:
- 缓存结构的设计
- 和内存之间的交互
- 互联机制的设计
- ……
这些都需要对别的领域先进行一定的介绍,而前几篇下来我们已经可以利用AXI做好一个endpoint IP的设计了,这个系列就暂且放放了。
总的来说,我们主要介绍了AXI4的这么几个东西:
- AXI协议的通道和握手机制,并以此讲了AXI-Lite和代码解析:
- 介绍了AXI的burst机制和窄传输问题,并以此讲了AXI-FULL和代码解析:
- 介绍了用于高速数据流的AXI-Stream,在此基础上整了摄像头接入代码解析:
主线上的介绍其实就上面这些东西,希望大家能有较好的"入门"体验。
小结
大家好,鸽了许久,甚是想念!
《让我们来猜猜下一个系列会是什么》
也欢迎关注我的个人公众号,小何的芯像石头: