【数字IC】从零开始的Verilog UART设计

本文涉及的产品
数据传输服务 DTS,数据迁移 small 3个月
推荐场景:
MySQL数据库上云
数据传输服务 DTS,数据同步 1个月
简介: 【数字IC】从零开始的Verilog UART设计

一、写在前面


上一节中,我们详细讨论了UART的协议内容并从设计组件的角度给出了UART协议中所需要的诸多内容,以供读者参考

这一节中,我们自定义如下标准的UART进行设计,需要注意的是,本篇文章中所涉及的UART仅供初学者学习参考,并没有采用实际工业开发中所涉及到的代码标准和思考,也并未进行综合与后仿。


1.1 协议标准

【数字IC】深入浅出理解UART协议


1.2 数字IC组件代码

【数字IC手撕代码】Verilog边沿检测电路(上升沿,下降沿,双边沿)|题目|原理|设计|仿真

【数字IC手撕代码】Verilog奇偶校验|题目|原理|设计|仿真

【数字IC手撕代码】Verilog奇数分频|题目|原理|设计|仿真

【数字IC手撕代码】Verilog偶数分频|题目|原理|设计|仿真


二、设计要求


UART收/发器固定;

支持5-8位数据位、1/2位停止位、可选1位奇偶校验位;

奇偶校验结果错误的检测能力;

波特率2400、4800、9600可调;


三、模块划分


从top level来看,整个UART的涉及应该包含三个module

首先是baud_generate module


对于波特率生成器而言,我们需要在这个模块中将全局时钟信号100Mhz进行分频,按照约定的波特率如300,1200,2400,9600等要求进行分频处理,以得到所要求的波特率要求。


其次是tx module


对于发送模块而言,我们要从状态机的角度对其进行划分,按起始位,数据位,校验位,停止位进行状态划分,并区分状态转移的条件。


最终是rx module


对于发送模块而言,我们也从状态机的角度对其进行划分,按起始位,数据位,校验位,停止位进行状态划分,并区分状态转移的条件。不过按照《深入浅出理解UART协议》所言,在rx模块中我们需要使用发送频率的十六倍频进行采样,使用多路选择的方法避免数据传输中可能会出现的错误。


四、全局参数


time_frequency = 100_000_000

用以确定全局时钟频率

baud_rate = 9_600

用以确定RX,TX的波特率

data_width = 8

用以确定数据位宽

test = 1

用以确定奇偶校验,其中0为无校验位,1为偶校验,2为奇校验

stop_width = 2

用以确定停止位位宽


五、整体结构


image.png


六、波特率生成器


6.1 设计文件

module baud_generator #(
  parameter clk_rate = 100_000_000, //全局时钟频率
  parameter baud_rate = 9_600 //波特率
)(input clk,
  input rst_n,
  output rx_clk,
  output tx_clk);
  localparam tx_rate = clk_rate / (baud_rate * 2); //发送模块分频系数
  localparam rx_rate = clk_rate / (baud_rate * 2 * 16); //接收模块分频系数
  reg [$clog2(rx_rate)-1:0] rx_count; 
  reg [$clog2(tx_rate)-1:0] tx_count;
  reg rx_clk_reg;
  reg tx_clk_reg;
// rx_clk分频
always@(posedge clk or negedge rst_n)
  begin
  if(!rst_n)
  begin
    rx_count <= 'b0;
    rx_clk_reg <= 1'b0;
  end
  else if(rx_count == rx_rate - 1'b1)
  begin
    rx_clk_reg <= !rx_clk_reg;
    rx_count <= 'b0;
  end
  else
    rx_count = rx_count + 1'b1;
  end
//tx_clk分频
always@(posedge clk or negedge rst_n)
  begin
  if(!rst_n)
  begin
    tx_count <= 'b0;
    tx_clk_reg <= 1'b0;
  end
  else if(tx_count == tx_rate - 1'b1)
  begin
    tx_clk_reg = !tx_clk_reg;
    tx_count <= 'b0;
  end
  else
    tx_count= tx_count +1'b1;
  end
assign rx_clk = rx_clk_reg;
assign tx_clk = tx_clk_reg;
endmodule


6.2 仿真文件

`timescale 1ns / 1ps
module baud_generator_tb ();
  reg clk;
  reg rst_n;
  wire rx_clk;
  wire tx_clk;
  baud_generator #(10_000_000,9600) u1 (clk,rst_n,rx_clk,tx_clk);
  initial clk = 0;
  always #5 clk = !clk; //生成10ns全局时钟
  initial 
  begin
  rst_n = 1;
  rst_n = 0;
  #45
  rst_n = 1;
  #4000000000;
  $stop;
  end
endmodule


6.3 仿真结果

image.png

根据波形图我们发现,设定为9600波特率的输出,即:用于tx模块与rx模块的分频时钟信号,符合预期。


我们希望tx端的采样频率遵循9.6khz,但是我们这里最终分频输出的是9.599kHz,这会影响最终设计的正确结果吗?大家可以在评论区讨论一波。


七、发送模块


7.1 发射模块状态机跳变

image.png

IDLE:默认态,无数据传输,输出高电平,当enable信号到来时跳转到S1。

S1:起始位,无数据传输,输出低电平,无条件跳转到S2。

S2:数据位,数据传输发生在S2,根据数据输出高低电平,假如有校验位,跳到S3,假如数据传输不设校验位,跳转到S4

S3:校验位,根据要求,输出奇数校验或者偶数校验的值,下一个状态无条件跳转到S4。

S4:停止位,根据要求,输出1个或2个周期的高电平,下一个状态无条件跳转到IDLE。


7.2 设计文件

module tx 
#(parameter data_width = 8,
parameter test = 2,
parameter stop_width = 1)
(
input tx_clk,
input rst_n,
input [data_width-1:0] data_in,
input enable,
output reg tx_out);
localparam IDLE = 3'b000;
localparam S1   = 3'b001;
localparam S2  = 3'b010;
localparam S3   = 3'b011;
localparam S4   = 3'b100;
reg [3:0] state, nstate;
reg [3:0] count_data;
reg [1:0] count_stop;
reg check_bit;
//状态机第一段
always@(posedge tx_clk or negedge rst_n)
  if(!rst_n)
  state <= IDLE;
  else
  state <= nstate;
//状态机第二段
always@(*)
  begin
  case(state)
  IDLE: nstate = (enable ? S1 : IDLE);
  S1:  nstate = S2;
  S2:  nstate = (test == 'b0 && count_data == (data_width - 1)? S4 : count_data == (data_width - 1)? S3 : S2); 
  S3:  nstate =  S4 ;
  S4:  nstate = count_stop == (stop_width - 1) ? IDLE : S4 ;
  default: nstate = IDLE;
  endcase
  end
//状态机的输出
always@(*)
  case(state)
  IDLE : tx_out = 1'b1;
  S1 : tx_out = 1'b0;
  S2 : tx_out = data_in[count_data]; //由低位到高位依次输出
  S3 : tx_out = check_bit;
  S4 : tx_out = 1'b1;
  default : tx_out = 1'b1;
  endcase
//S2状态,输出数据位的计数器
always@(posedge tx_clk or negedge rst_n)
  if(!rst_n)
  count_data <= 4'b0000;
  else if (count_data < data_width && state == S2)
  count_data <= count_data + 1'b1;
  else 
  count_data <= 4'b0000;
//UART的奇偶校验位的生成
always@(posedge tx_clk or negedge rst_n)
  begin
  if(!rst_n)  
  check_bit <= 1'b0;
  else if (state == S2)
  case(test)
  2'b00 : check_bit <= 1'b0;
  2'b01 : check_bit <= !(^data_in);
  2'b10 : check_bit <= (^data_in);
  default:check_bit <= 1'b0;
  endcase
  else check_bit <= check_bit;
  end
//UART停止位计数器
always@(posedge tx_clk or negedge rst_n)
  begin
  if(!rst_n)
    count_stop <= 2'b00;
  else if( state == S4 && count_stop <stop_width)
    count_stop <= count_stop + 1'b1;
  else 
    count_stop <= 2'b00;
  end
endmodule


7.3 仿真文件

`timescale 1ns / 1ps
module tx_tb();
reg clk;
reg rst_n;
reg enable;
reg [7:0] data_in;
wire tx_out;
//端口例化与数据传输
parameter A=8;
parameter B=1;
parameter C=2;
tx #(A,B,C) u1 (clk,rst_n,data_in,enable,tx_out);
//时钟生成
initial clk = 0;
always #5 clk = !clk;
//数据发送task
task write_data;
input [7:0] task_data_in;
begin
@(negedge clk);
enable = 1;
@(negedge clk) ;
data_in = task_data_in;
enable =0;
repeat(12)@(negedge clk);
end
endtask
//正式测试
initial
begin
rst_n=1;
enable = 0;
#15
rst_n=0;
#50
rst_n=1;
#30;
write_data(8'h0a);
write_data(8'h24);
write_data(8'h33);
write_data(8'h14);
end
endmodule


7.4 仿真结果

bb18ca89f0aa42dfabc70d1050950ca0.png

我们这里测试的是8位,奇校验,2位停止位的发送module,可见在S2时,输出位从低位到高位将input的数据逐位发出,校验位和停止位也都符合预期


八、接收模块


8.1 接收模块状态机跳变

image.png

IDLE:默认态,不需要接收数据传输

S1:起始位接收,接收起始位数据传输,无条件跳转到S2。

S2:数据位接收,数据接收发生在S2,根据数据接收情况输出高低电平,假如有校验位,跳到S3,假如数据传输不设校验位,跳转到S4

S3:校验位接收,根据要求,接收奇数校验或者偶数校验的值,并进行检验,下一个状态无条件跳转到S4。

S4:停止位接收,根据要求,接收1个或2个周期的高电平,下一个状态无条件跳转到IDLE。


8.2 设计文件

module rx #(
parameter data_width = 8,
parameter test = 2,
parameter stop_width = 1)
(input rx_clk,
input rst_n,
input data_in,
output [data_width-1:0]rx_out,
output fail);
//状态机定义
localparam IDLE = 3'b000;
localparam S1  = 3'b001;
localparam S2   = 3'b010;
localparam S3  = 3'b011;
localparam S4   = 3'b100;
reg [2:0] state, nstate;
//
reg [3:0] frq_6;
reg data_in_reg;
reg [15:0] filter_reg;
reg filter_out;
reg [data_width-1:0] rx_out_reg;
reg test_reg;
wire start;
//????????????
reg [3:0] count_data;
reg [1:0] count_stop;
//三段式状态机的第一段
always@(posedge rx_clk or negedge rst_n)
  if(!rst_n)
  state <= IDLE;
  else
  state <= nstate;
//三段式状态机的第二段
always@(*)
  begin
  case(state)
  IDLE: nstate = (start ? S1 : IDLE);
  S1:  nstate = (frq_6 == 4'b1111 ? S2 : S1);
  S2:  nstate = (test == 'b0 && frq_6 == 4'b1111 && count_data == data_width - 1) ? S4 : (frq_6 == 4'b1111 && count_data == data_width - 1)? S3 : S2 ;
  S3:  nstate = (frq_6 == 4'b1111 ? S4 : S3);
  S4:  nstate = (frq_6 == 4'b1111 && (count_stop == stop_width - 1)? IDLE : S4);
  default:nstate = IDLE;
  endcase
  end
//下降沿检测电路,输出start信号,激活UART接收
always@(posedge rx_clk or negedge rst_n)
  if(!rst_n)
  data_in_reg <= 1'b0;
  else
  data_in_reg <= data_in;
assign start = data_in_reg & !data_in;
//数据接收个数的计数器
always@(posedge rx_clk or negedge rst_n)
  if(!rst_n || state !== S2) 
  count_data <= 4'b0000;
  else if (count_data == data_width - 1 && frq_6 ==4'b1111)
  count_data <= 4'b0000;
  else if (frq_6 == 4'b1111)
  count_data <= count_data + 1'b1;
  else
  count_data <= count_data;
//停止位接受个数的计数器
always@(posedge rx_clk or negedge rst_n)
  if(!rst_n || state !== S4) 
  count_stop <= 2'b00;
  else if (count_stop == stop_width - 1 && frq_6 ==4'b1111)
  count_stop <= 2'b00;
  else if(frq_6 == 4'b1111)
  count_stop <= count_stop + 1'b1;
  else 
  count_stop <= count_stop; 
//16倍的采样计数器
always@(posedge rx_clk or negedge rst_n)
  if(!rst_n)
  frq_6 <= 4'b0000;
  else if(frq_6 == 4'b1111 && state == IDLE)
  frq_6 <= 4'b0000;
  else if(state == S1 || state == S2 || state == S3 || state == S4)
  frq_6 <= frq_6 + 1'b1;
  else 
  frq_6 <= frq_6;
//16倍频的采样结果存储在filter_reg中
always@(posedge rx_clk or negedge rst_n)
  if(!rst_n)
  filter_reg <= 16'h0000;
  else if(state == S1 || state == S2 || state == S3 || state == S4)
  filter_reg[frq_6] <= data_in;
  else
  filter_reg <= 16'h0000;
//存储后的多路选择,结果输出位filter_out  
always@(posedge rx_clk or negedge rst_n)
  if(!rst_n || state == IDLE)
  filter_out <= 1'b0;
  else if ( frq_6 == 4'b1100)
  filter_out <= (filter_reg[7] & filter_reg[8]) ^ (filter_reg[7] & filter_reg[9]) ^ (filter_reg[8] & filter_reg[9]);
  else
  filter_out <= filter_out;
//S2状态时将数据依次存入寄存器
always@(posedge rx_clk or negedge rst_n)
    if(!rst_n || state == IDLE)
    rx_out_reg <= 'b0;
    else if (state == S2 && frq_6 == 4'b1111)
    rx_out_reg[count_data] <= filter_out;
    else 
    rx_out_reg <= rx_out_reg;
//S3状态时判断校验位是否正确
always@(posedge rx_clk or negedge rst_n)
  if(!rst_n)
  test_reg <= 1'b0;
  else if(state == S3)
  case(test)
    2'b00 : test_reg <= 1'b0;
    2'b01 : test_reg <= !(^rx_out_reg);
    2'b10 : test_reg <= (^rx_out_reg);
    default : test_reg <= 1'b0;
  endcase
  else
    test_reg <= 1'b0;
assign fail = (state == S3 && frq_6 == 4'b1110 && filter_out !== test_reg) ? 1 : 0;
//数据接受完毕,输出传入RX的值
assign rx_out =(state == S4 && count_stop == stop_width - 1) ? rx_out_reg : 'b0 ;
endmodule


8.3 仿真文件

`timescale 1ns / 1ps
module rx_tb();
reg clk;
reg rst_n;
reg data_in;
wire rx_out;
reg test;
parameter A=8;
parameter B=1;
parameter C=2;
rx #(A,B,C) u1 (clk,rst_n,data_in,rx_out);
initial clk = 0;
always #5 clk = !clk;
task receive_data;
input [7:0] A;
begin
repeat(16)@(negedge clk);
  data_in = 0;
  test = ^A;
repeat(8) 
  begin
  repeat(16)@(negedge clk);
  data_in = A[0];
  A = A>>1;
  end
repeat(16)@(negedge clk);
  data_in = test;
repeat(16)@(negedge clk);
  data_in = 1;
repeat(16)@(negedge clk);
  data_in = 1;
#200;
end
endtask
initial
begin
rst_n=1;
data_in = 1;
#15
rst_n=0;
#50
rst_n=1;
#30;
receive_data(8'h34);
receive_data(8'ha8);
receive_data(8'hb4);
end
endmodule


8.4 仿真结果

44aab0659c25478cbb475e817afb3428.png

在仿真中,我们例化RX的时候,位宽选用的八位,校验形式选择的偶校验,停止位选用的为两位

输入数据位宽八位,停止位两位,校验形式为奇校验的数据34,a8,都正确的传入到了输出端,但因校验方式的错误,fail信号拉高一个周期,符合设计标准


九、TOP模块


9.1 设计文件

module uart_top#(
parameter time_frequency = 100_000_000,
parameter baud_rate = 9_600,
parameter data_width = 8,
parameter test = 1,
parameter stop_width = 2
)(
input clk,
input rst_n,
input enable,
input [data_width-1:0] data_in,
output [data_width-1:0] rx_out);
wire rx_clk;
wire tx_clk;
wire tx_out;
baud_generator #(time_frequency,baud_rate)   u1 (.clk(clk),.rst_n(rst_n),.rx_clk(rx_clk),.tx_clk(tx_clk));
tx    #(data_width,test,stop_width) u2(.tx_clk(tx_clk),.rst_n(rst_n),.data_in(data_in),.enable(enable),.tx_out(tx_out));
rx    #(data_width,test,stop_width) u3(.rx_clk(rx_clk),.rst_n(rst_n),.data_in(tx_out),.rx_out(rx_out),.fail());
endmodule


9.2 仿真文件

`timescale 1ns / 1ps
module uart_top_tb();
reg clk;
reg rst_n;
reg enable;
reg [7:0] data_in;
wire [7:0] rx_out;
uart_top  u4(clk,rst_n,enable,data_in,rx_out);
initial clk = 0;
always #5 clk = !clk;
initial
begin
rst_n = 1;
#2000;
rst_n = 0;
enable = 1;
#2000;
rst_n = 1;
data_in = 8'h34;
#20000000;
$stop;
end
endmodule


9.3 仿真结果

image.png

TX输入8’h34之后,经过1.2ms,RX输出相同的数值,设计符合要求


十、本设计与工业级UART的差距


最后再讨论一下本设计与实际工程中的UART的差异性在哪,以供读者补充参考。


仅存在校验位检测,不存在帧格式检测

中断控制缺失

单向TX,RX固定,而非双向RX,TX可选

输入输出缺少FIFO做缓冲

不过本设计仅为学习参考使用,配合【数字IC】深入浅出理解UART协议使读者对于URAT的协议理解和电路实现有基本的认识才是本篇博文的目的所在。


帧格式的检测,与校验位的检测其实大同小异。

双向TX,RX也不过是在单向TX,RX的基础上使用状态机进行更多的状态跳转

FIFO做数据缓冲的功能也不过是在RX的data_in与TX的data_out处例化FIFO,引入更多变量


十一、其他数字IC基础协议解读


11.1 UART协议

【数字IC】深入浅出理解UART

【数字IC】从零开始的Verilog UART设计


11.2 SPI协议

【数字IC】深入浅出理解SPI协议

【数字IC】从零开始的Verilog SPI设计


11.3 I2C协议

【数字IC】深入浅出理解I2C协议


11.4 AXI协议

【AXI】解读AXI协议双向握手机制的原理

【AXI】解读AXI协议中的burst突发传输机制

【AXI】解读AXI协议事务属性(Transaction Attributes)

【AXI】解读AXI协议乱序机制

【AXI】解读AXI协议原子化访问

【AXI】解读AXI协议的额外信号

【AXI】解读AXI协议的低功耗设计

【数字IC】深入浅出理解AXI协议

【数字IC】深入浅出理解AXI-lite协议


相关实践学习
RocketMQ一站式入门使用
从源码编译、部署broker、部署namesrv,使用java客户端首发消息等一站式入门RocketMQ。
Sqoop 企业级大数据迁移方案实战
Sqoop是一个用于在Hadoop和关系数据库服务器之间传输数据的工具。它用于从关系数据库(如MySQL,Oracle)导入数据到Hadoop HDFS,并从Hadoop文件系统导出到关系数据库。 本课程主要讲解了Sqoop的设计思想及原理、部署安装及配置、详细具体的使用方法技巧与实操案例、企业级任务管理等。结合日常工作实践,培养解决实际问题的能力。本课程由黑马程序员提供。
相关文章
|
3天前
|
存储 编译器 索引
Verilog基础【一】
Verilog基础【一】
71 0
|
3天前
|
C语言 开发者
嵌入式系统中的GPIO(通用输入/输出)编程
嵌入式系统中的GPIO(通用输入/输出)编程
29 0
|
3天前
|
存储 人工智能 安全
Verilog基础【二】
Verilog基础【二】
60 1
【数字IC手撕代码】Verilog小数分频|题目|原理|设计|仿真
【数字IC手撕代码】Verilog小数分频|题目|原理|设计|仿真
【数字IC手撕代码】Verilog小数分频|题目|原理|设计|仿真
|
算法 测试技术 异构计算
m基于FPGA的数字下变频verilog设计
m基于FPGA的数字下变频verilog设计
168 0
m基于FPGA的数字下变频verilog设计
Verilog语法入门(二)多bit逻辑门
Verilog HDL是一种硬件描述语言(HDL:Hardware Description Language),以文本形式来描述数字系统硬件的结构和行为的语言,用它可以表示逻辑电路图、逻辑表达式,还可以表示数字逻辑系统所完成的逻辑功能。 Verilog HDL和VHDL是世界上最流行的两种硬件描述语言,都是在20世纪80年代中期开发出来的。前者由Gateway Design Automation公司(该公司于1989年被Cadence公司收购)开发。两种HDL均为IEEE标准。
143 0
|
存储 前端开发 芯片
【数字IC】从零开始的Verilog SPI设计
【数字IC】从零开始的Verilog SPI设计
【数字IC】从零开始的Verilog SPI设计
|
算法 关系型数据库 MySQL
FPGA:Verilog HDL程序的基本结构
FPGA:Verilog HDL程序的基本结构
128 0
FPGA:Verilog HDL程序的基本结构
|
开发者 SoC
【数字IC】深入浅出理解UART协议
【数字IC】深入浅出理解UART协议
【数字IC】深入浅出理解UART协议
【数字IC手撕代码】Verilog半整数分频|题目|原理|设计|仿真
【数字IC手撕代码】Verilog半整数分频|题目|原理|设计|仿真
【数字IC手撕代码】Verilog半整数分频|题目|原理|设计|仿真