FPGA教程系列-Vivado AXI串口程序解析

FPGA教程系列-Vivado AXI串口程序解析

这是一部分,需要的是跳出思维的框架,学习的是思维,而不是简单的代码。如果只需要代码,网上一大堆,好的坏的都有。

先附上代码

接收模块:

verilog 复制代码
`timescale 1ns / 1ps

/*
 * AXI4-Stream UART
 */
module uart_rx #
(
    parameter DATA_WIDTH = 8
)
(
    input  wire                   clk,
    input  wire                   rst,

    /*
     * AXI output
     */
    output wire [DATA_WIDTH-1:0]  m_axis_tdata,
    output wire                   m_axis_tvalid,
    input  wire                   m_axis_tready,

    /*
     * UART interface
     */
    input  wire                   rxd,

    /*
     * Status
     */
    output wire                   busy,
    output wire                   overrun_error,
    output wire                   frame_error,

    /*
     * Configuration
     */
    input  wire [15:0]            prescale

);

reg [DATA_WIDTH-1:0] m_axis_tdata_reg = 0;
reg m_axis_tvalid_reg = 0;

reg rxd_reg = 1;

reg busy_reg = 0;
reg overrun_error_reg = 0;
reg frame_error_reg = 0;

reg [DATA_WIDTH-1:0] data_reg = 0;
reg [18:0] prescale_reg = 0;
reg [3:0] bit_cnt = 0;

assign m_axis_tdata = m_axis_tdata_reg;
assign m_axis_tvalid = m_axis_tvalid_reg;

assign busy = busy_reg;
assign overrun_error = overrun_error_reg;
assign frame_error = frame_error_reg;

always @(posedge clk) begin
    if (rst) begin
        m_axis_tdata_reg <= 0;
        m_axis_tvalid_reg <= 0;
        rxd_reg <= 1;
        prescale_reg <= 0;
        bit_cnt <= 0;
        busy_reg <= 0;
        overrun_error_reg <= 0;
        frame_error_reg <= 0;
    end else begin
        rxd_reg <= rxd;
        overrun_error_reg <= 0;
        frame_error_reg <= 0;

        if (m_axis_tvalid && m_axis_tready) begin
            m_axis_tvalid_reg <= 0;
        end

        if (prescale_reg > 0) begin
            prescale_reg <= prescale_reg - 1;
        end else if (bit_cnt > 0) begin
            if (bit_cnt > DATA_WIDTH+1) begin
                if (!rxd_reg) begin
                    bit_cnt <= bit_cnt - 1;
                    prescale_reg <= (prescale << 3)-1;
                end else begin
                    bit_cnt <= 0;
                    prescale_reg <= 0;
                end
            end else if (bit_cnt > 1) begin
                bit_cnt <= bit_cnt - 1;
                prescale_reg <= (prescale << 3)-1;
                data_reg <= {rxd_reg, data_reg[DATA_WIDTH-1:1]};
            end else if (bit_cnt == 1) begin
                bit_cnt <= bit_cnt - 1;
                if (rxd_reg) begin
                    m_axis_tdata_reg <= data_reg;
                    m_axis_tvalid_reg <= 1;
                    overrun_error_reg <= m_axis_tvalid_reg;
                end else begin
                    frame_error_reg <= 1;
                end
            end
        end else begin
            busy_reg <= 0;
            if (!rxd_reg) begin
                prescale_reg <= (prescale << 2)-2;
                bit_cnt <= DATA_WIDTH+2;
                data_reg <= 0;
                busy_reg <= 1;
            end
        end
    end
end

endmodule

1. 功能概述

该模块的作用是将异步串行数据(UART RXD)接收并转换为 FPGA 内部通用的 AXI4-Stream 总线流数据。输入 :UART 串行线 rxd​,配置分频值 prescale​。输出 :AXI-Stream 接口 (tdata​, tvalid​, tready),以及状态标志(忙、溢出错误、帧错误)。

参数DATA_WIDTH(默认8位),支持非标准位宽。

2. 代码逻辑流程

代码并没有使用显式的状态机(FSM with case​ state),而是使用了计数器控制流

  1. 空闲状态 (bit_cnt == 0)

    • 检测 rxd 下降沿(起始位)。
    • 一旦检测到,初始化 bit_cntDATA_WIDTH + 2(起始位 + 数据位 + 停止位)。
    • 初始化定时器 prescale_reg 为半个比特周期(为了在起始位的中间采样)。
  2. 定时与采样 (prescale_reg倒计时)

    • prescale_reg 递减直到 0。当它为 0 时,表示到达了采样点。
  3. 位处理逻辑 (基于 bit_cnt的值)

    • **起始位确认 (** > DATA_WIDTH+1 ) :在半周期结束后再次检查 rxd 是否为低。如果是,确认是有效起始位,设置定时器为一个完整周期,准备采样数据位。
    • **数据位采样 (** > 1 ) :每隔一个完整周期采样一次,将 rxd 移位存入 data_reg
    • **停止位校验 (** == 1 ) :采样停止位。如果为高(有效),输出数据到 AXI 接口;如果为低,产生帧错误。

3. 值得学习的地方

A. 隐式状态机 (Implicit FSM)

通常情况下会定义 IDLE​, START​, DATA​, STOP​ 等状态。但这个模块没有状态变量

巧妙点 :利用 bit_cnt 一个变量同时充当了"状态指示器"和"剩余位数计数器"。

  • bit_cnt == 0: IDLE
  • bit_cnt > DATA_WIDTH+1: START CHECK
  • bit_cnt > 1: DATA SAMPLING
  • bit_cnt == 1: STOP CHECK

优势:节省寄存器资源,逻辑由简单的比较器构成,代码极其紧凑。

B. 精妙的过采样与时钟分频算法
verilog 复制代码
// 起始位检测后(空闲 -> 起始):加载半个周期
prescale_reg <= (prescale << 2)-2;

// 后续位采样:加载一个完整周期
prescale_reg <= (prescale << 3)-1;

设计思路 :这里假设输入的 prescale 值等于 ClockFreq/(BaudRate×8)ClockFreq / (BaudRate \times 8)ClockFreq/(BaudRate×8)。也就是说,基准时间单位是 1/8 个比特周期

巧妙点

  • (prescale << 3) 等于 prescale * 8,代表一个完整的比特宽度
  • (prescale << 2) 等于 prescale * 4,代表半个比特宽度

效果

  1. 检测到下降沿后,等待 4 个单位时间(半周期),正好对准起始位的中心进行二次确认。
  2. 确认后,每次等待 8 个单位时间(全周期),从而保证后续所有数据位都在波形的中心位置进行采样。
  3. 使用移位(<<)而不是乘法器,硬件实现极其廉价且高速
C. AXI4-Stream 握手集成的简洁性

代码在处理 UART 时序的同时,完美集成了 AXI 流控:

verilog 复制代码
if (m_axis_tvalid && m_axis_tready) begin
    m_axis_tvalid_reg <= 0;
end
// ... 在停止位逻辑中 ...
if (rxd_reg) begin
    m_axis_tdata_reg <= data_reg;
    m_axis_tvalid_reg <= 1; // 拉高 Valid
    overrun_error_reg <= m_axis_tvalid_reg; // 如果上一个数据还没被取走(Valid仍为1),则报错
end

巧妙点overrun_error​ 的检测非常简单粗暴且有效------如果我要写入新数据时,旧数据的 valid​ 还是 1(意味着下游没来得及 ready),那就是溢出。

D. 移位寄存器的使用

data_reg <= {rxd_reg, data_reg[DATA_WIDTH-1:1]};

这是一个标准的LSB First(低位先发)移位逻辑。不需要由索引(index)来寻址保存数据,每次只管往里塞,最后剩下的就是排列好的数据。

发射模块:

verilog 复制代码
`timescale 1ns / 1ps

/*
 * AXI4-Stream UART
 */
module uart_tx #
(
    parameter DATA_WIDTH = 8
)
(
    input  wire                   clk,
    input  wire                   rst,

    /*
     * AXI input
     */
    input  wire [DATA_WIDTH-1:0]  s_axis_tdata,
    input  wire                   s_axis_tvalid,
    output wire                   s_axis_tready,

    /*
     * UART interface
     */
    output wire                   txd,

    /*
     * Status
     */
    output wire                   busy,

    /*
     * Configuration
     */
    input  wire [15:0]            prescale
);

reg s_axis_tready_reg = 0;

reg txd_reg = 1;

reg busy_reg = 0;

reg [DATA_WIDTH:0] data_reg = 0;
reg [18:0] prescale_reg = 0;
reg [3:0] bit_cnt = 0;

assign s_axis_tready = s_axis_tready_reg;
assign txd = txd_reg;

assign busy = busy_reg;

always @(posedge clk) begin
    if (rst) begin
        s_axis_tready_reg <= 0;
        txd_reg <= 1;
        prescale_reg <= 0;
        bit_cnt <= 0;
        busy_reg <= 0;
    end else begin
        if (prescale_reg > 0) begin
            s_axis_tready_reg <= 0;
            prescale_reg <= prescale_reg - 1;
        end else if (bit_cnt == 0) begin
            s_axis_tready_reg <= 1;
            busy_reg <= 0;

            if (s_axis_tvalid) begin
                s_axis_tready_reg <= !s_axis_tready_reg;
                prescale_reg <= (prescale << 3)-1;
                bit_cnt <= DATA_WIDTH+1;
                data_reg <= {1'b1, s_axis_tdata};
                txd_reg <= 0;
                busy_reg <= 1;
            end
        end else begin
            if (bit_cnt > 1) begin
                bit_cnt <= bit_cnt - 1;
                prescale_reg <= (prescale << 3)-1;
                {data_reg, txd_reg} <= {1'b0, data_reg};
            end else if (bit_cnt == 1) begin
                bit_cnt <= bit_cnt - 1;
                prescale_reg <= (prescale << 3);
                txd_reg <= 1;
            end
        end
    end
end

endmodule

1. 功能概述

该模块的作用是将 FPGA 内部的并行数据(通过 AXI-Stream 接口)转换为 UART 串行数据流发送出去。输入 :AXI-Stream 数据 (tdata​, tvalid​),时钟配置 prescale​。输出 :UART 串行线 txd​,AXI 握手信号 tready​,状态 busy

关键特性 :支持任意 DATA_WIDTH,自动处理起始位、数据位和停止位。

2. 代码逻辑流程

和接收模块一样,这里也没有使用 case​ 语句定义状态机,而是完全依赖 bit_cnt(比特计数器)prescale_reg(分频定时器) 来驱动逻辑。

  1. 空闲状态 (bit_cnt == 0)

    • s_axis_tready 置 1,允许上游发送数据。

    • 当握手成功 (s_axis_tvalid为高)

      • 拉低 Tready:表示正忙,不再接收新数据。
      • 发送起始位 :直接令 txd_reg <= 0
      • 加载数据 :将输入数据存入 data_reg
      • 设置计数器bit_cnt 设为 DATA_WIDTH + 1(数据位 + 停止位)。
      • 启动定时器:加载一个完整的波特率周期。
  2. 定时等待 (prescale_reg > 0)

    • 单纯倒计时,保持当前 txd 状态不变,维持一个比特的宽度。
  3. 移位与发送 (bit_cnt控制)

    • 数据位发送 (bit_cnt > 1)

      • 定时器归零后,进入此分支。
      • 利用拼接语法进行移位,将 data_reg 的最低位挤入 txd_reg
    • 停止位发送 (bit_cnt == 1)

      • 数据发送完毕,强制 txd_reg <= 1(停止位电平)。
      • 等待最后一个周期结束后,bit_cnt 归零,回到空闲状态。

3. 值得学习的地方

A. 移位输出逻辑
verilog 复制代码
{data_reg, txd_reg} <= {1'b0, data_reg};

这是一个 64位拼接与赋值 操作(假设 DATA_WIDTH=8,算上 txd 和填充位):

  1. 右侧 (RHS){1'b0, data_reg}​。在 data_reg 的左边补一个 0,构成一个右移后的新值。

  2. 左侧 (LHS){data_reg, txd_reg}。这是目标容器。

    • 目标的高位部分是 data_reg
    • 目标的最低位是 txd_reg

效果解读

这相当于同时完成了两件事:

  1. 更新发送引脚txd_reg 获取了 data_reg 原来的最低位 (LSB) 。这符合 UART LSB First 的标准。
  2. 右移寄存器data_reg 里的内容整体向右移动了一位,高位补 0。

对比传统写法

传统写法通常需要两行:

verilog 复制代码
txd_reg <= data_reg[0];
data_reg <= {1'b0, data_reg[DATA_WIDTH:1]};
B. 数据加载时的"占位符"

在 IDLE 状态加载数据时:

verilog 复制代码
data_reg <= {1'b1, s_axis_tdata};

这里把 s_axis_tdata​ 加载进去,并在最高位拼了一个 1'b1

分析 :虽然代码后来的逻辑中,停止位是强制赋值 txd_reg <= 1​ 的,似乎这个 1'b1 没用到。

目的 :这通常是为了防御性编程或逻辑的一致性。

  • 随着数据不断右移,高位补 0。
  • 如果在某种极端异常情况下,状态机跑飞了或者 bit_cnt 计数错了多移位了一次,txd 得到的也是这个预置的 1(空闲/停止电平),而不是 0(起始/数据电平),从而避免产生总线上的"伪起始位"错误。
C. 停止位长度控制
verilog 复制代码
else if (bit_cnt == 1) begin
    prescale_reg <= (prescale << 3); // 注意这里没有 -1
    txd_reg <= 1;
end
  • 这里 prescale << 3 没有像之前那样 -1
  • 这是为了保证停止位至少有一个完整周期的长度,宁可稍微长一点点(多一个时钟周期),也不能短。因为停止位过短可能导致接收端来不及识别一帧结束,从而产生 Frame Error。

能看懂代码,并不代表能写出来这样牛逼的代码,但是可以作为一个储备,很多的这种储备应该在未来能够成为"彼得一机灵"。

相关推荐
加点油。。。。2 天前
【UAV避障-3D VFH+】
matlab·机器人·无人机·仿真·机器人仿真
anscos5 天前
【无标题】
仿真·软件·marc
Wishell20158 天前
FPGA教程系列- 存储结构与参数化
仿真
Wishell201512 天前
FPGA教程系列-流水线中的一些概念
仿真
Wishell201513 天前
FPGA教程系列-流水线axis_register解读
仿真
Wishell201513 天前
FPGA教程系列-流水线再看
仿真
jacky25715 天前
衍射光波导与阵列光波导技术方案研究
aigc·ar·xr·ai编程·仿真·混合现实·光学设计
Angel Q.16 天前
基于GS(Gaussian Splatting)的机器人Sim2Real2Sim仿真平台有哪些
机器人·仿真·3dgs·高斯泼溅·sim2real2sim
fdtsaid17 天前
Intel 六位专家对 Simics 助力 Shift-Left 的讨论(2018)
qemu·仿真·simulation·simics·intel simics