工程简介
此工程主要将cmos拍摄的图像通过LVDS发送。cmos选用斯特威sc130gs,外触发模式,2lane mipi接收。DDR缓存,VDMA读写。主控zynq7020,xc7z020clg400
为了验证LVDS发送的数据,使用另一块板子完成接收数据并写入SD卡的操作。
LVDS发送
LVDS简介
LVDS(Low Voltage Differential Signaling,低压差分信号) 是一种用于高速数据传输的电气信号标准。它采用极低的电压摆幅高速差动传输数据,可以实现点对点或一点对多点的连接,具有低功耗、低误码率、低串扰和低辐射等优点,已经被广泛应用于串行高速数据通讯场合当,如高速背板、电缆和板到板数据传输与时钟分配,以及单个PCB内的通信链路。
差分信号
LVDS是差分信号,物理层面有两条线路,在工程中发送的接口信号包括LVDS_CLK(发送时钟)、LVDS_CS(发送数据有效标志)、LVDS_DATA(发送数据)。
LVDS通信为三线差分制,即数据有效标志CS(对应管脚LVDS_CS _OUT+,LVDS_CS_OUT-),时钟信号CLK(对应管脚LVDS_CLK_OUT+,LVDS_CLK_OUT-)及数据信号DATA(对应管脚LVDS_DATA_OUT+,LVDS_DATA_OUT-)。
通信配置如下:
- 时钟信号的占空比为50%;
- 时钟信号的频率为40MHz,即周期为25ns;
- 数据发送时,数据有效信号线置低,其余时段置高;
- 数据跳变发生在CLK上升沿处。LVDS接口数据传输率为40Mbps,字节长度为8bit,有效字节长度为8bit。
差分信号转化使用sn65lvds31pwr芯片,FPGA只需提供三种单端信号即可。
如果没有对应的差分芯片,则可以通过OBUFDS原语完成差分信号转化,并施加相应的管脚约束。
串并转换
LVDS时钟频率为40MHz,数据8bit,那么对应的8位并行数据时钟为5MHz,由于使用VDMA操作DDR并且VDMA读通道最小数据位宽16bit,所以需要使用一个异步FIFO缓存数据。通过FIFO的空满信号控制VDMA读通道的数据流(tready信号)。
从FIFO中获取的8bit数据需要进行串并转换并通过LVDS发送。
最开始使用OSERDES2原语完成串并转换,SDR模式,仿真没有问题,CS信号也是跟着原语的输出做了7个周期的延时。但是最后接收端的工程搞定之后,通过ILA抓到的CS信号却跟DATA信号的延时不一致,并且每次上电延时周期还会改变,找不到问题,最后换了一种方式实现。(如果有大佬阅读到了这里,对此问题有一定想法,可以直接评论交流)
在使用原语出现BUG后,直接使用计数器来完成串并转化,实测低速40MHz的LVDS时钟下,此方法没有一点问题,CS信号与DATA也是一致的。
数据帧发送
在工程中,发送一张图像数据并不是直接一次性将数据全部发送,而是通过特定的帧格式将数据拆分成多个数据帧发送。
具体的帧格式这里就不展示了,无非就是帧头帧尾帧ID帧校验这些。
通过状态机控制数据帧的发送。
- 在初始状态,等待数据写入FIFO,PS端触发一次VDMA读取,FIFO不为空则进入发送帧头状态。
- 在发送帧头状态,将帧头的特定序列从高字节到低字节依次发送,发送完成进入下一状态。
- 在发送帧ID状态,与发送帧头步骤一致,我这是16位的帧ID,需要两个并行数据周期完成发送。
- 在发送数据状态,需先判断FIFO是否为空,只有FIFO不为空,才正常发送数据。如果FIFO为空且当前数据发送没有满足数据帧的数据域字节要求,就需要进入补充数据状态,发送FFH至满足数据域字节需求。
- 在发送校验状态,与发送帧ID基本一致,16位校验和,校验和只在数据域累加。
- 在发送帧尾状态,与发送帧头基本一致。
- 在发送补充数据状态,一直发送特定序列至填满数据域,之后进入发送校验状态。
接下来具体讲解一下,从VDMA到FIFO的数据控制。
VDMA的读通道(M_AXIS_MM2S)是AXI协议,除了数据、数据有效等输出的信号还包含了输入的握手信号(m_axis_mm2s_tready),此信号就是流控的关键,当tready信号为高时,VDMA才会传输数据,在我们LVDS发送时,FIFO中的数据超过了多少就拉低tready信号,低于多少就拉高tready信号,工程中是传输图像数据,那么规定FIFO中的数据超过了2行图像就拉低,直至FIFO中的数据低于单个数据帧数据域的字节数时再拉高,FIFO配置时可以配置这两个信号(prog_full,prog_empty)。
LVDS接收
差分信号
接收端差分信号通过sn65lvds32信号转化为单端信号。
同样的没有芯片可以使用IBUFDS原语将差分信号转化为单端信号。
串并转换
这块其实没什么好说的了,直接贴代码。
module lvds_in (
input lvds_clk ,
input lvds_cs ,
input lvds_data ,
input rst_n ,
output reg [7:0] rx_data ,
output reg rx_dvld
);
reg [2:0] rx_cnt;
reg [2:0] reg_cnt;
reg [7:0] rx_data_reg;
always @(posedge lvds_clk or negedge rst_n) begin
if (!rst_n) begin
rx_cnt <= 0;
end
else if (~lvds_cs) begin
if (rx_cnt == 3'd7) begin
rx_cnt <= 0;
end
else begin
rx_cnt <= rx_cnt + 1;
end
end
end
always @(posedge lvds_clk or negedge rst_n) begin
if (!rst_n) begin
reg_cnt <= 0;
end
else begin
reg_cnt <= rx_cnt;
end
end
always @(posedge lvds_clk or negedge rst_n) begin
if (!rst_n) begin
rx_data_reg <= 0;
end
else if (~lvds_cs) begin
rx_data_reg[rx_cnt] <= lvds_data;
end
end
always @(posedge lvds_clk or negedge rst_n) begin
if (!rst_n) begin
rx_data <= 0;
rx_dvld <= 0;
end
else begin
if (reg_cnt == 3'd7) begin
rx_data <= rx_data_reg;
rx_dvld <= 1;
end
else begin
rx_data <= rx_data;
rx_dvld <= 0;
end
end
end
endmodule
数据帧解析
在串并转换模块输出字节数据后,一般需要对这些数据进行解析,按照发送时的数据帧协议把图像数据输出,这里的实现思路其实跟发送时的基本一样,设计状态机、检测序列等等。在工程中,并没有加上数据帧解析,虽然也写了代码仿真也过了,但是目前是直接将数据帧全部数据都保留下来。
DMA写入DDR
得到8位数据后,需要将数据拼接为16位数据(图像像素为2字节),这里也是用到了异步FIFO,8位写入,16位读出。这里设计了一个FIFO读控制模块,用来输出符合DMA写通道的数据流。
这里的逻辑很简单,当FIFO中数据超过一数据帧字节数时(prog_full拉高)触发一次读,每一次读都读取一帧字节数(包含帧头帧尾这些)。由于写入端是5Mhz的8位数据写入,而读取则是50MHz16位读取,所以不用担心溢出的问题。
需要注意的是FIFO的读使能信号一定是在s_axis_s2mm_tready信号为高时才拉高,否则会丢数据。然后就是每一次触发读的最后一字节时拉高一周期的s_axis_s2mm_tlast信号用以触发中断。
SD卡数据写入
写入sd卡是在PS端完成的,读取DMA数据,写入SD卡,这些都是有很多例程的,稍微修改一下就能直接使用。我也不多做阐述,接下来贴部分代码。
#include "file_op.h"
#include "ff.h"
#include <stdio.h>
FIL recorder_file_fp;
int8_t file_create_and_open(void)
{
FIL fp;
int res;
char file_name_buf[20];
for (uint32_t i=0; i<999; ++i) {
sprintf(file_name_buf, "0:FILE_%03d.dat", (int)i);
res = f_open(&fp,file_name_buf,FA_READ|FA_OPEN_EXISTING);
if (FR_OK != res) {
printf("file_create_and_open, file res: %d\r\n", res);
break;
}
f_close(&fp);
}
res = f_open(&recorder_file_fp,file_name_buf,FA_WRITE|FA_CREATE_ALWAYS);
if (FR_OK != res) {
printf("file open failed, res: %d\r\n", res);
return -1;
}
else return 0;
}
void file_close(void)
{
f_close(&recorder_file_fp);
}
int8_t file_write(uint8_t *buf, uint32_t data_len)
{
uint32_t writen;
f_write(&recorder_file_fp, buf, data_len, (UINT*)&writen);
f_sync(&recorder_file_fp);
return 0;
}
实现
工程也是实现了两块板卡间的LVDS收发通讯,实测下来写入SD卡的数据与发送的数据(加上帧头等)完全一致。