【数字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
用以确定停止位位宽

五、整体结构

在这里插入图片描述

六、波特率生成器

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 仿真结果

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

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

七、发送模块

7.1 发射模块状态机跳变

在这里插入图片描述
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 仿真结果

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

八、接收模块

8.1 接收模块状态机跳变

在这里插入图片描述
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 仿真结果

在这里插入图片描述
在仿真中,我们例化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 仿真结果

在这里插入图片描述
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协议

11.2 SPI协议

11.3 I2C协议

11.4 AXI协议

收藏并关注作者,获取最新的更新动态

  • 58
    点赞
  • 371
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 44
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 44
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张江打工人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值