FPGA教程系列-Vivado AXI串口仿真测试
其实看完了高速接口,再返回来看串口,有点倒反天罡的意思了,不过还是想重新看一下串口,另外,看下大神是如何编写串口的。
照例放上讲的非常好的原文,https://blog.csdn.net/wuzhikaidetb/article/details/132725767?spm=1001.2014.3001.5501
UART串口简介
串口作为常用的三大低速总线(UART、SPI、IIC)之一,在设计众多通信接口和调试时占有重要地位。 串口(UART)全称通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),主要用于数据间的串行传递,是一种全双工传输模式。它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据。 “异步”两个字即意味着在数据传递的两个模块之间使用的不是同步时钟。实际上在异步串口的传输中是不需要时钟的,而是通过特定的时序来标志传输的开始(起始位--由高到低)和结束(结束位,拉高)。
通信方式在日常的应用中一般分为串行通信(serial communication)和并行通信(parallel communication)。
并行通信:指多比特数据同时通过并行线进行传送,一般以字或字节为 单位并行进行传输。这种传输方式用的通信线多、成本高,故不宜进行远距离通信,因此并行通信一般用 于近距离的通信,通常传输距离小于30米。
串行通信:指数据在一条数据线上,一比特接一比特地按顺序传送 的方式,这一点与并行通信是不同的。这里我们以传输一个字节(8位)数据为例,在并行通信中,一个字 节的数据是在 8 条并行传输线上同时由源地传送到目的地;而在串行通信中,因为数据是在一条传输线上 一位接一位地顺序传送的,所以一个字节的数据要分8次进行传送。如果我们以T为一个时间单位的话, 那么并行通信发送一个字节的数据只需要1T的时间,而串行通信需要8T的时间,由此可以总结出串行通 信的的特点:一是节省传输线,大大降低了使用成本,二是数据传送速度慢,这一点在大位宽的数据传输 上尤为明显。综上可知,串行通信主要应用于长距离、低速率的通信场合。
串行通信一般有 2 种通信方式:同步串行通信(synchronized serial communication)和异步串行通信 (asynchronous serial communication)。
同步串行通信需要通信双方在同一时钟的控制下同步传输数据;异 步串行通信是指具有不规则数据段传送特性的串行数据传输。在常见的通信总线协议中,I2C,SPI 属于同 步通信而UART属于异步通信。同步通信的通信双方必须先建立同步,即双方的时钟要调整到同一个频率, 收发双方不停地发送和接收连续的同步比特流。异步通信在发送字符时,发送端可以在任意时刻开始发送 字符,所以,在UART通信中,数据起始位和停止位是必不可少的。
UART是一种采用异步串行通信方式的通用异步收发传输器(universal asynchronous receiver-transmitter), 它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据。 UART 串口通信需要两根信号线来实现,一根用于串口发送,另外一根负责串口接收,如图所示。 对于 PC来说它的TX要和对于FPGA来说的RX连接,同样PC的RX要和FPGA的TX连接,如果是两 个TX或者两个RX连接那数据就不能正常被发送出去或者接收到。
UART 在发送或接收过程中的一帧数据由 4 部分组成,起始位、数据位、奇偶校验位和停止位,如下 图所示。
起始位:当不传输数据时,UART 数据传输线通常保持高电压电平。若要开始数据传输,发送 UART 会将传输线从高电平拉到低电平并保持1个波特率周期。当接收UART检测到高到低电压跃迁时,便开始 以波特率对应的频率读取数据帧中的位。
数据帧:数据帧包含所传输的实际数据。如果使用奇偶校验位,数据帧长度可以是5位到8位。如果 不使用奇偶校验位,数据帧长度可以是9位。在大多数情况下,数据以最低有效位优先方式发送。
奇偶校验:奇偶性描述数字是偶数还是奇数。通过奇偶校验位,接收UART判断传输期间是否有数据 发生改变。电磁辐射、不一致的波特率或长距离数据传输都可能改变数据位。接收UART读取数据帧后, 将计数值为1的位,检查总数是偶数还是奇数。如果奇偶校验位为0(偶数奇偶校验),则数据帧中的1或 逻辑高位总计应为偶数。如果奇偶校验位为1(奇数奇偶校验),则数据帧中的1或逻辑高位总计应为奇数。 当奇偶校验位与数据匹配时,UART认为传输未出错。但是,如果奇偶校验位为0,而总和为奇数,或者奇 偶校验位为1,而总和为偶数,则UART认为数据帧中的位已改变。
停止位:为了表示数据包结束,发送UART将数据传输线从低电压驱动到高电压并保持1到2位时间。
UART 通信过程中的数据格式及传输速率是可设置的,为了正确的通信,收发双方应约定并遵循同样 的设置。数据位可选择为5、6、7、8位,其中8位数据位是最常用的,在实际应用中一般都选择8位数据 位;校验位可选择奇校验、偶校验或者无校验位;停止位可选择1位(默认),1.5或2位。
串口通信的速 率用波特率表示,它表示每秒传输二进制数据的位数,单位是bps(位/秒),常用的波特率有9600、19200、 38400、57600 以及115200 等。波特率:即每秒传输的位数(bit)。一般选波特率都会有9600,19200,115200等 选项。其实意思就是每秒传输这么多个比特位数(bit)。在信息传输通道中,携带数据信息的信号单元叫作码 元(因为串口是1bit进行传输的,所以其码元就代表一个二进制数),每秒通过信号传输的码元数称为码 元的传输速率,简称“波特率”,常用符号“Baud”表示,其单位为“波特每秒”(Bps)。串口常见的波特率有 4800、9600、115200 等。 通信信道每秒传输的信息量称为位传输速率,简称“比特率”,其单位为“每秒比特数”(bps)。比特率 可由波特率计算得出,公式为比特率=波特率×单个调制状态对应的二进制位数。 如果使用的是115200的波特率,其串口的比特率为115200Bps×1bit = 115200bps,由计算得串口发送或 者接收1bit数据的时间为一个波特,即1/115200s。
在设置好数据格式及传输速率之后,UART 负责完成数据的串并转换,而信号的传输则由外部驱动电 路实现。电信号的传输过程有着不同的电平标准和接口规范,针对异步串行通信的接口标准有RS232、RS422、 RS485 等,它们定义了接口不同的电气特性,如RS-232是单端输入输出,而RS-422/485为差分输入输出等。 RS-232 标准的串口最常见的接口类型为DB9,样式如图所示,工业控制领域中用到的工控机一 般都配备多个串口,很多老式台式机也都配有串口。但是笔记本电脑以及较新一点的台式机都没有串口, 它们一般通过USB转串口线来实现与外部设备的串口通信。
Ps:DB9公头与母头的定义一定要注意!!
AXI串口的实现
普通的串口已经在网上泛滥了,最近发现Alex 大神的一些项目,有串口的通信,而且是用AXI协议的串口,本着向大神学习的念头,站在巨人的肩膀好成事。
文件结构很简单,
三个文件,一个封装,一个发射一个接收。
代码网上都有,但是测试是用的cocotb,暂时没安装,测试也不复杂,所以简单的贴上testbench。另外程序的解读留待以后的。
`timescale 1ns / 1ps module tb_uart; // 参数定义 parameter DATA_WIDTH = 8; parameter CLK_PERIOD = 10; // 100MHz 时钟 // 输入信号 (Reg) reg clk; reg rst; reg [DATA_WIDTH-1:0] s_axis_tdata; reg s_axis_tvalid; reg m_axis_tready; reg [15:0] prescale; // 输出信号 (Wire) wire s_axis_tready; wire [DATA_WIDTH-1:0] m_axis_tdata; wire m_axis_tvalid; wire txd; wire tx_busy; wire rx_busy; wire rx_overrun_error; wire rx_frame_error; // 模拟的回环连接信号 wire rxd; // ---------------------------------------------------------------- // 实例化被测模块 (Unit Under Test - UUT) // 引用源文件 [3], [4], [5] // ---------------------------------------------------------------- uart #( .DATA_WIDTH(DATA_WIDTH) ) uut ( .clk(clk), .rst(rst), // AXI Input (发送端接口) .s_axis_tdata(s_axis_tdata), .s_axis_tvalid(s_axis_tvalid), .s_axis_tready(s_axis_tready), // AXI Output (接收端接口) .m_axis_tdata(m_axis_tdata), .m_axis_tvalid(m_axis_tvalid), .m_axis_tready(m_axis_tready), // UART 物理接口 .rxd(rxd), .txd(txd), // 状态信号 .tx_busy(tx_busy), .rx_busy(rx_busy), .rx_overrun_error(rx_overrun_error), .rx_frame_error(rx_frame_error), // 配置 .prescale(prescale) ); // ---------------------------------------------------------------- // 关键逻辑:硬件回环 (Loopback) // 将发送引脚 TX 连接到接收引脚 RX // ---------------------------------------------------------------- assign rxd = txd; // 时钟生成 initial begin clk = 0; forever #(CLK_PERIOD/2) clk = ~clk; end // ---------------------------------------------------------------- // 测试流程 // ---------------------------------------------------------------- initial begin // 1. 初始化信号 rst = 1; s_axis_tdata = 0; s_axis_tvalid = 0; m_axis_tready = 0; prescale = 16'd10; // 设置较小的分频值以加快仿真速度 [2], [1] // 2. 复位释放 #(CLK_PERIOD * 10); rst = 0; #(CLK_PERIOD * 10); $display("--- Simulation Start ---"); $display("Prescale set to: %d", prescale); // 3. 开启接收端 Ready 信号 (允许接收数据) // 源码 [6] 显示如果不拉高 ready,valid 信号可能无法正确握手清除 m_axis_tready = 1; // 4. 发送测试数据 0x55 (二进制 01010101) send_byte(8'h55); // 5. 等待并检查接收 check_received_byte(8'h55); // 6. 发送测试数据 0xA3 (随机值) send_byte(8'hA3); check_received_byte(8'hA3); // 7. 发送测试数据 0xFF (全1) send_byte(8'hFF); check_received_byte(8'hFF); // 结束仿真 #(CLK_PERIOD * 100); $display("--- Test Passed: All bytes match ---"); $finish; end // ---------------------------------------------------------------- // 任务:发送一个字节 (AXI Stream Master 行为) // 基于 uart_tx 的输入逻辑 [7] // ---------------------------------------------------------------- task send_byte; input [DATA_WIDTH-1:0] data; begin @(posedge clk); s_axis_tdata <= data; s_axis_tvalid <= 1; // 等待 UUT 的 ready 信号 wait(s_axis_tready); @(posedge clk); // 握手完成,拉低 valid s_axis_tvalid <= 0; $display("[TX] Sent data: 0x%h at time %t", data, $time); end endtask // ---------------------------------------------------------------- // 任务:检查接收到的字节 (AXI Stream Slave 行为) // 基于 uart_rx 的输出逻辑 [8], [6] // ---------------------------------------------------------------- task check_received_byte; input [DATA_WIDTH-1:0] expected_data; begin // 等待数据变为 Valid // 注意:由于是串行传输,这里需要等待较长时间 (Bit time * 10) wait(m_axis_tvalid); @(posedge clk); if (m_axis_tdata === expected_data) begin $display("[RX] Received Match: 0x%h at time %t", m_axis_tdata, $time); end else begin $display("[RX] ERROR: Expected 0x%h, Got 0x%h at time %t", expected_data, m_axis_tdata, $time); $stop; // 遇到错误停止 end // 等待 valid 信号被拉低 (由 m_axis_tready 控制的握手完成) wait(!m_axis_tvalid); end endtask endmodule下面来看下仿真:
一共发送了3次,第一次是01010101。这里有个点,串口是低位优先的,所以看时序应该反着来,而01010101看起来跟高位优先差不多,主要看第二组数据10100011.如图所示,就能看出来是低位先行了。
再仔细看下握手。仿真是做了一个回环的设计。
之所以 TX 是从机(Slave),RX 是主机(Master),是因为这里的“主/从”是针对系统内部数据流(AXI Stream 总线)的方向定义的,而不是针对外部 UART 线缆的方向。
需要从数据是谁产生的,流向哪里这个角度来理解:
- 为什么 TX(发送模块)是 Slave?
- UART TX 模块的任务是接收来自系统的数据,然后把它“消化”掉(串行化发送出去)。
- 在 AXI Stream 协议中,接收数据的一方是 Slave(从机),发送数据的一方是 Master(主机)。
- 因此,系统(或 DMA)是 Master,它把数据“喂”给 TX 模块,而 TX 模块作为 Slave 被动接收。
- 为什么 RX(接收模块)是 Master?
- UART RX 模块的任务是产生数据(从外部引脚解串出来的),然后把这些数据推给系统。
- 在 AXI Stream 协议中,发起/输出数据的一方是 Master。
- 因此,RX 模块变成了 Master,它主动告诉系统:“我收到新数据了,给你!”。
为了方便记忆,可以使用打印机和扫描仪的类比:
TX (发送端) 就像打印机:
- 打印机是 Slave。它不会自己写文章,它必须等待电脑(Master)发送文件给它,它才能打印。
RX (接收端) 就像扫描仪:
- 扫描仪是 Master。当你扫描一张照片时,扫描仪主动产生图片数据,并发送给电脑(Slave)保存。