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),而是使用了计数器控制流。
-
空闲状态 (
bit_cnt == 0 ) :- 检测
rxd下降沿(起始位)。 - 一旦检测到,初始化
bit_cnt为DATA_WIDTH + 2(起始位 + 数据位 + 停止位)。 - 初始化定时器
prescale_reg为半个比特周期(为了在起始位的中间采样)。
- 检测
-
定时与采样 (
prescale_reg 倒计时) :prescale_reg递减直到 0。当它为 0 时,表示到达了采样点。
-
位处理逻辑 (基于
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: IDLEbit_cnt > DATA_WIDTH+1: START CHECKbit_cnt > 1: DATA SAMPLINGbit_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,代表半个比特宽度。
效果:
- 检测到下降沿后,等待
4个单位时间(半周期),正好对准起始位的中心进行二次确认。 - 确认后,每次等待
8个单位时间(全周期),从而保证后续所有数据位都在波形的中心位置进行采样。 - 使用移位(
<<)而不是乘法器,硬件实现极其廉价且高速。
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 (分频定时器) 来驱动逻辑。
-
空闲状态 (
bit_cnt == 0 ) :-
s_axis_tready置 1,允许上游发送数据。 -
当握手成功 (
s_axis_tvalid 为高) :- 拉低 Tready:表示正忙,不再接收新数据。
- 发送起始位 :直接令
txd_reg <= 0。 - 加载数据 :将输入数据存入
data_reg。 - 设置计数器 :
bit_cnt设为DATA_WIDTH + 1(数据位 + 停止位)。 - 启动定时器:加载一个完整的波特率周期。
-
-
定时等待 (
prescale_reg > 0 ) :- 单纯倒计时,保持当前
txd状态不变,维持一个比特的宽度。
- 单纯倒计时,保持当前
-
移位与发送 (
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 和填充位):
-
右侧 (
RHS ) :{1'b0, data_reg}。在data_reg的左边补一个 0,构成一个右移后的新值。 -
左侧 (
LHS ) :{data_reg, txd_reg}。这是目标容器。- 目标的高位部分是
data_reg。 - 目标的最低位是
txd_reg。
- 目标的高位部分是
效果解读 :
这相当于同时完成了两件事:
- 更新发送引脚 :
txd_reg获取了data_reg原来的最低位 (LSB) 。这符合 UART LSB First 的标准。 - 右移寄存器 :
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。
能看懂代码,并不代表能写出来这样牛逼的代码,但是可以作为一个储备,很多的这种储备应该在未来能够成为"彼得一机灵"。