先说uart的发送模块的要求吧,驱动时钟是40kHz的
clk_40k
(要求波特率为1k,所以要40分频),复位信号是低电平复位的
rst_n
,输入数据是8位并行的
din
,发送使能信号是
send_start
,输出信号是
bit_out
。
- 该模块接收一个8位输入数据并采用UART协议发送
- UART 采用10位传输协议,即一位起始位,8位数据位,一位终止位
- 发送数据时先发低位数据,最后发高位数据。要求波特率为1K
- 发送第一个数据前发送一个0作为起始标志,发送完最后一个数据后发送1作为结束标志,直到下一个数据周期
让我们先回忆下uart的工作原理,uart是一种异步传输方式。
- 发端比较简单,每当
send_start=1
代表发送开始,先发一个0,再发所有数据,最后发个1等待下一个send_start
。 - 收端相对就会比较麻烦了。因为是异步信号,谁也没法保证时钟对齐。要是比较倒霉,还可能会遇到亚阈值效应。我们先暂且不考虑这些,先考虑如何检测开始信号。考虑到空闲状态是1,起始先要发一个0,这就意味着会有一个下降沿。我们只要先检测下降沿即可,检测到就代表发送开始。然后采样时可以多采几个样本,采样点尽可能靠近中间(因为这时一般比较稳定),然后择多译码。
发端代码
先让我们写比较简单的发端代码吧。
首先是send_start
的检测吧,因为send_start
只会持续一个时钟周期长度,这意味着我们必须用40kHz的信号always模块进行检测,不能乱分频。具体代码如下,我们可以看到,我们用了一个寄存器储存了send_start
信号,每当它为1时,这个寄存器置1,直到400个周期((clk_cnt == 6'd39) && (bit_cnt == 4'd9)
)过去后我们再将其清零。
reg send_en; // 发送时,send_en为1
// send_en信号
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
send_en <= 1'b0;
else if (send_start)
send_en <= 1'b1;
else if ((clk_cnt == 6'd39) && (bit_cnt == 4'd9))
send_en <= 1'b0;
else
send_en <= send_en;
end
然后是40分频,这个很简单。
reg[5:0] clk_cnt; // 分频
// clk_cnt 分频信号
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
clk_cnt <= 6'd0;
else if (send_en) begin
if (clk_cnt == 6'd39)
clk_cnt <= 6'd0;
else
clk_cnt <= clk_cnt + 6'd1;
end
else
clk_cnt <= 6'd0;
end
然后对当前发送bit数计数。bit_cnt代表目前发送到第几个bit了。(clk_cnt == 6'd39) && (bit_cnt==4'd9)
则是代表发完了,此时归0
reg[3:0] bit_cnt; // 记录发到了第几bit
// bit_cnt 分频信号
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
bit_cnt <= 4'd0;
else if (send_en) begin
if ((clk_cnt == 6'd39) && (bit_cnt==4'd9)) // 防止bit_cnt到10
bit_cnt <= 4'd0;
else if (clk_cnt == 6'd39) // 每40周期+1
bit_cnt <= bit_cnt + 4'd1;
else
bit_cnt <= bit_cnt;
end
else
bit_cnt <= 4'd0;
end
最后就是输出部分了。最开始总是先发0,然后发din
,最后发1。注意尽量不要让bit_cnt>=10
,不然可能出bug。
reg[9:0] din_tmp; // 缓存din
assign bit_out = (send_en ? din_tmp[bit_cnt] : 1'b1);
// din_tmp
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
din_tmp <= 10'b11_1111_1110;
else if (send_start)
din_tmp <= {1'b1, din, 1'b0};
else
din_tmp <= din_tmp;
end
uart_tx.v
把以上代码综合起来就是发端的代码了。
`timescale 1ns / 1ns
module uart_tx ( clk_40k, // clock signal, 40kHz
rst_n, // reset signal, active low
din, // the input data which will be sent by the UART module, 8 bit width
send_start, // the start enable signal, active high, the width is one clock period
bit_out // the serial output data
);
input wire clk_40k; // 40kHz的时钟
input wire rst_n; // 低电平复位
input wire[7:0] din; // 8位输入数据
input wire send_start; // 高电平代表发数据
output wire bit_out; // 输出
reg send_en; // 发送时,send_en为1
reg[5:0] clk_cnt; // 分频
reg[3:0] bit_cnt; // 记录发到了第几bit
reg[9:0] din_tmp; // 缓存din
// send_en信号
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
send_en <= 1'b0;
else if (send_start)
send_en <= 1'b1;
else if ((clk_cnt == 6'd39) && (bit_cnt == 4'd9))
send_en <= 1'b0;
else
send_en <= send_en;
end
// clk_cnt 分频信号
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
clk_cnt <= 6'd0;
else if (send_en) begin
if (clk_cnt == 6'd39)
clk_cnt <= 6'd0;
else
clk_cnt <= clk_cnt + 6'd1;
end
else
clk_cnt <= 6'd0;
end
// bit_cnt 分频信号
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
bit_cnt <= 4'd0;
else if (send_en) begin
if ((clk_cnt == 6'd39) && (bit_cnt==4'd9)) // 防止bit_cnt到10
bit_cnt <= 4'd0;
else if (clk_cnt == 6'd39) // 每40周期+1
bit_cnt <= bit_cnt + 4'd1;
else
bit_cnt <= bit_cnt;
end
else
bit_cnt <= 4'd0;
end
assign bit_out = (send_en ? din_tmp[bit_cnt] : 1'b1);
// din_tmp
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
din_tmp <= 10'b11_1111_1110;
else if (send_start)
din_tmp <= {1'b1, din, 1'b0};
else
din_tmp <= din_tmp;
end
endmodule
tb_uart_tx
然后来一个发端的testbench
//~ `New testbench
`timescale 1ns / 1ns
module tb_uart_tx;
// uart_tx Parameters
parameter PERIOD = 10;
// uart_tx Inputs
reg clk_40k = 1 ;
reg rst_n = 1 ;
reg[7:0] din = 8'b00101110 ;
reg send_start = 0 ;
// uart_tx Outputs
wire bit_out ;
initial
begin
forever #(PERIOD/2) clk_40k=~clk_40k;
end
initial
begin
#(PERIOD*2) rst_n = 1;
end
uart_tx u_uart_tx (
.clk_40k ( clk_40k ),
.rst_n ( rst_n ),
.din ( din ),
.send_start ( send_start ),
.bit_out ( bit_out )
);
initial
begin
#(PERIOD*2) rst_n=0;
#(PERIOD*40) rst_n=1;
#(PERIOD*40) send_start=1;
#(PERIOD*1) send_start=0;
#(PERIOD*600);
$finish;
end
endmodule
发端仿真图
收端代码
再让我们写比较难的收端代码吧。
首先我们可以先考虑输出这块,因为可能有亚阈值效应和抖动等,我们通过D触发器的级联消除亚阈值效应。一般来说2个到3个D触发器的级联的效果就很不错了。我们定义5个D触发器的级联为bit_in_dly
,输入从低位进入这个移位寄存器,将最高位bit_in_dly[4]
作为我们实际使用的bit_in
。这样可以增强我们接受的可靠性。
reg[4:0] bit_in_dly;
// 输出bit
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
bit_in_dly <= 5'b11111;
else
bit_in_dly <= {bit_in_dly[3:0], bit_in};
end
然后写bit的采样,为了增强可靠性,我们可以采3次样,择多译码。具体思路是在16时将计数器清零,然后分别在18/20/22处采样。我们假设3次都采样到0,sum_tmp=2'b00
,我们假设3次都采样到1,sum_tmp=2'b11
。假设有1次误码,本来应该3个0变成了2个0、1个1,sum_tmp=2'b01
;或者本来应该3个1变成了2个1、1个0,sum_tmp=2'b10
。
分析所有情况,其实本质上就是取sum_tmp
的最高位sum_tmp[1]
就行了。至于为什么在18/20/22处采样,这是因为我们假设每个bit的中间是比较稳定的。再加上3次采样择多译码,错误概率就很低了。
reg[1:0] sum_tmp;
// sum_tmp
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
sum_tmp <= 2'd0;
else if (clk_cnt == 6'd16)
sum_tmp <= 2'd0;
else if ((clk_cnt == 6'd18) || (clk_cnt == 6'd20) || (clk_cnt == 6'd22))
sum_tmp <= sum_tmp + bit_in_dly[4];
else
sum_tmp <= sum_tmp;
end
我们还需要标记bit接收的开始信号,本质上就是检测下降沿。下降沿其实就是1100这种信号,不过我们在这里偷一个懒,我们只需要检测到0就可以认为是下降沿。
这是为什么呢?因为我们开始一定会发一个0,结束一定是1,要是还不发比特,处于空闲状态依然是1。这意味着我们只需要发现0,这一定意味这不处于空闲状态,并且第一个bit必然是0。我们只需要检测到0后给计数器置1,持续9个多bit的时间((bit_cnt == 4'd9) && (clk_cnt == 6'd30)
相当于9.75个bit),这样就能让结束时必然处于1状态,此时再出现0,必然是一个下降沿。
reg rcv_en;
assign nedge = (bit_in_dly[4:2] == 3'b0);
// rcv_en
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
rcv_en <= 1'b0;
else if (nedge)
rcv_en <= 1'b1;
else if ((bit_cnt == 4'd9) && (clk_cnt == 6'd30))
rcv_en <= 1'b0;
else
rcv_en <= rcv_en;
end
clk_cnt
和bit_cnt
和发端是一致的
reg[5:0] clk_cnt; // 分频
reg[3:0] bit_cnt; // 记录发到了第几bit
// clk_cnt 分频信号
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
clk_cnt <= 6'd0;
else if (rcv_en) begin
if (clk_cnt == 6'd39)
clk_cnt <= 6'd0;
else
clk_cnt <= clk_cnt + 6'd1;
end
else
clk_cnt <= 6'd0;
end
// bit_cnt 分频信号
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
bit_cnt <= 4'd0;
else if (rcv_en) begin
if ((clk_cnt == 6'd39) && (bit_cnt==4'd9)) // 防止bit_cnt到10
bit_cnt <= 4'd0;
else if (clk_cnt == 6'd39) // 每40周期+1
bit_cnt <= bit_cnt + 4'd1;
else
bit_cnt <= bit_cnt;
end
else
bit_cnt <= 4'd0;
end
然后接收信息bit
output reg[7:0] dout; // 输出
// dout
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
dout <= 8'd255;
else if ((clk_cnt == 6'd24) && (bit_cnt >= 4'd1) && (bit_cnt <= 4'd8))
dout[bit_cnt-1] <= sum_tmp[1];
else
dout <= dout;
end
uart_rx.v
合到一起就是uart_rx.v
了。
`timescale 1ns / 1ns
module uart_rx ( clk_40k, // clock signal, 40kHz
rst_n, // reset signal, active low
bit_in, // the input serial bit,
dout_vld, // the output valid signal, active high,the dout is valid when this signal is high.
dout // received data, 8 bit width
);
input wire clk_40k; // 40kHz的时钟
input wire rst_n; // 低电平复位
input wire bit_in; // 串行输入数据
output wire dout_vld; // 高电平代表收数据
output reg[7:0] dout; // 输出
reg[4:0] bit_in_dly;
reg[5:0] clk_cnt; // 分频
reg[3:0] bit_cnt; // 记录发到了第几bit
reg[1:0] sum_tmp;
reg rcv_en;
assign dout_vld = (bit_cnt == 4'd9);
// 输出bit
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
bit_in_dly <= 5'b11111;
else
bit_in_dly <= {bit_in_dly[3:0], bit_in};
end
// sum_tmp
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
sum_tmp <= 2'd0;
else if (clk_cnt == 6'd16)
sum_tmp <= 2'd0;
else if ((clk_cnt == 6'd18) || (clk_cnt == 6'd20) || (clk_cnt == 6'd22))
sum_tmp <= sum_tmp + bit_in_dly[4];
else
sum_tmp <= sum_tmp;
end
// dout
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
dout <= 8'd255;
else if ((clk_cnt == 6'd24) && (bit_cnt >= 4'd1) && (bit_cnt <= 4'd8))
dout[bit_cnt-1] <= sum_tmp[1];
else
dout <= dout;
end
assign nedge = (bit_in_dly[4:2] == 3'b0);
// rcv_en
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
rcv_en <= 1'b0;
else if (nedge)
rcv_en <= 1'b1;
else if ((bit_cnt == 4'd9) && (clk_cnt == 6'd30))
rcv_en <= 1'b0;
else
rcv_en <= rcv_en;
end
// clk_cnt 分频信号
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
clk_cnt <= 6'd0;
else if (rcv_en) begin
if (clk_cnt == 6'd39)
clk_cnt <= 6'd0;
else
clk_cnt <= clk_cnt + 6'd1;
end
else
clk_cnt <= 6'd0;
end
// bit_cnt 分频信号
always @( posedge clk_40k or negedge rst_n ) begin
if (~rst_n)
bit_cnt <= 4'd0;
else if (rcv_en) begin
if ((clk_cnt == 6'd39) && (bit_cnt==4'd9)) // 防止bit_cnt到10
bit_cnt <= 4'd0;
else if (clk_cnt == 6'd39) // 每40周期+1
bit_cnt <= bit_cnt + 4'd1;
else
bit_cnt <= bit_cnt;
end
else
bit_cnt <= 4'd0;
end
endmodule
tb_uart.v
整个的testbench如下
//~ `New testbench
`timescale 1ns / 1ns
module tb_uart;
// uart Parameters
parameter PERIOD = 10;
// uart Inputs
reg clk_40k = 1 ;
reg clk_40k_rx = 0 ;
reg rst_n = 1 ;
reg[7:0] din = 8'b00101110 ;
reg send_start = 0 ;
// uart Outputs
wire bit_out ;
wire bit_in ;
wire dout_vld ;
wire dout ;
initial
begin
forever #(PERIOD/2) clk_40k=~clk_40k;
end
initial
begin
forever #(PERIOD/2) clk_40k_rx=~clk_40k_rx;
end
uart_tx u_uart_tx (
.clk_40k ( clk_40k ),
.rst_n ( rst_n ),
.din ( din ),
.send_start ( send_start ),
.bit_out ( bit_out )
);
uart_rx u_uart_rx (
.clk_40k ( clk_40k_rx ),
.rst_n ( rst_n ),
.bit_in ( bit_out ), // bit_in = bit_out
.dout_vld ( dout_vld ),
.dout ( dout )
);
initial
begin
#(PERIOD*2) rst_n=0;
#(PERIOD*40) rst_n=1;
#(PERIOD*40) send_start=1;
#(PERIOD*1) send_start=0;
#(PERIOD*600);
$finish;
end
endmodule