首先我们得直到串口是怎样进行通信的。
我们可以看到,串口通讯的数据格式是由一位起始位、七个数据位(其中最后一位数据位可以作为检验位来使用。)、一位停止位,在空闲时刻为高电平,当我们使用串口发送时,就可以按照这种时序进行发送,接收时也要按照这种时序进行接收。
什么是波特率?
每秒钟发送的数据个数。
概念很简单,我们需要怎样去计算呢?常见的波特率有 9600 、 19200 、 38400 、 57600 、 115200 。
波特率计算:
9600:1_000_000_000 / 9600;
19200:1_000_000_000 / 19200;
38400:1_000_000_000 / 38400;
57600:1_000_000_000 / 57600;
115200:1_000_000_000 / 115200;
这儿的 1_000_000_000 是代表 1s 因为我的开发板是以 ns 为时间单位的,所以我的波特率计算也是以 ns 为时间单位。
发送思路原理:我们可以通过定义一个计数器,来记录每次发送所需要的的时间,然后用一个状态机来记录发送的状态。
module uart_tx_bottom(
input Clk, //定义输入时钟信号。
input Reset_n, //定义复位信号。
input [7:0]Data, //定义需要发送的八位数据。
input Send_en, //定义发送使能信号,此信号为1时才可以发送数据。
output reg uart_tx, //定义发送信号,reg型是因为需要在always块中给它赋值。
output reg Tx_done //定义发送完成标志信号,以便下次发送数据。
);
parameter Band_Set = 433; //这儿是利用时钟计数发送一位的时间,1_000_000_000 / 115200 / 20 - 1。
// 其中 / 20 是因为我的开发板时钟周期为20ns, - 1 是因为计数器从0开始计数,原本算出需要计数434次,只需要加到433时就是434次了。
reg [3:0]bps_cnt; //数据发送状态,因为发送有起始位,数据位,结束位,用这个来记录数据发送到哪个位置了。
reg [17:0]div_cnt; //一位数据位发送时间计数器,记录发送数据的时间,用于更新下一段数据。
wire bps_clk; //每一位发送数据前后的标志位,具体作用后面会讲到。
always @(posedge Clk or negedge Reset_n) begin //定义一个计数器的always块。
if (!Reset_n)
div_cnt <= 0; //若复位将计数器归0.
else if (Send_en) begin //如果有使能才可以进行自加。
if (Band_Set <= div_cnt) //当中间<=写法错误时,这种写法会让编辑器自动报错,可以减少因为没注意而引发的功能错误,而且使用<=可以对未知的情况进行判断,增加程序的健壮性。
div_cnt <= 0; //当计数器记满时,计数器归0.
else
div_cnt <= div_cnt + 1'b1; //计数器未记满,每个时钟上升沿自加1.
end
else
div_cnt <= 0; //没有使能信号,使计数器归0
end
assign bps_clk = (div_cnt == 1); //当每次计数器记到 1 的时候就让数据开始发送,因为如果div_cnt == Band_Set,在每次发送时还需要等待一个数据位的时间数据才会发送,而这样定义后只需要等待1个时钟周期数据就会发送了。因为bps_clk是wire类型的,所以用assign语句赋值,而不是用always语句赋值。
always @(posedge Clk or negedge Reset_n) begin //定义发送状态块,记录发送的状态。
if (!Reset_n)
bps_cnt <= 0; // 若是复位,状态为0。
else if (Send_en) begin //发送使能,状态才会改变。
if (bps_clk) begin //当有发送的标志时,状态才自增一次。
if (bps_cnt == 11)
bps_cnt <= 0; //当状态计数到第11个状态,状态归0。
else
bps_cnt <= bps_cnt + 1'b1; //状态自增1。
end
end
else
bps_cnt <= 0; //发送不使能,状态归0。
end
always @(posedge Clk or negedge Reset_n) begin //串口发送块。
if (!Reset_n)
uart_tx <= 1; //当复位时,数据线发送高电平,因为空闲状态时数据线为高电平。
else begin
case(bps_cnt) //开始发送数据,以数据状态来决定发送的是哪个数据。
1:uart_tx <= 0; //发送起始位,为什么不是0状态时刻发送0,因为在复位或者空闲时状态为0,而此时需要·uart_tx为高电平,所以在0状态时刻不能为0。
2:uart_tx <= Data[0]; //发送数据低位,数据从低位发送到高位,此下依次发送数据。
3:uart_tx <= Data[1];
4:uart_tx <= Data[2];
5:uart_tx <= Data[3];
6:uart_tx <= Data[4];
7:uart_tx <= Data[5];
8:uart_tx <= Data[6];
9:uart_tx <= Data[7]; //一般来说,不用校验位,直接发送数据最后一位。
default:uart_tx <= 1; //数据位发送完成之后,需要发送停止位,我们可以发现,空闲和停止位都是高电平,所以用default来将这些状态综合。
//细心的可以发现,我们状态定义了11个,这儿9个就结束了,为什么需要定义11个呢?因为在最后一个停止位发送为第10个状态,而停止位需要保留一个数据周期,所以在第10个状态发送停止位后,需要再延后一个状态,来保证停止位的时间,所以是11个状态。
endcase
end
end
always @(posedge Clk or negedge Reset_n) begin //定义发送完成标志块。
if (!Reset_n)
Tx_done <= 0; //复位时,发送完成标志位为0.
else if (10 <= bps_cnt && bps_clk) //当状态为第10或者11个状态时且发送一个数据的标志位置位时才使总发送标志位置1。
Tx_done <= 1;
else
Tx_done <= 0; //其余时刻都将状态标志位置0。
end
endmodule
接收思路原理:因为数据传输是不稳定的,会有波动,所以我们可以将每一个数据接收时间分为 16 份,检测其中中间的 7 份,7 份当中最多的状态就是此时的数据状态。
module uart_rx_bottom(
input Clk, //定义输入时钟信号。
input Reset_n, //定义复位。
input uart_rx, //定义接收口。
output reg [7:0]Data, //定义数据接收位置,将接收的数据输出。
output reg Rx_Done //定义接收完成标志信号。
);
parameter Bps_DR = 1_000_000_000 / 115200 / 16 / 20 - 1; //时钟计数最大值,先算出波特率,时钟周期20,每一份再分16份, - 1 的作用上面已经说了。
reg RX_EN; //定义接收使能信号。
reg [1:0]uart_rx_state; //定义一个两位的寄存器来存储上个时钟和此刻时钟的状态,用于判断上升沿和下降沿。
reg [2:0]sta_bit; //定义接收起始信号的中间七份的寄存器。
reg [2:0]sto_bit; //定义接收结束信号的中间七份的寄存器。
reg [2:0]r_data[7:0]; //定义接收数据信号的中间七份的寄存器,前面[2:0]代表每个数据的宽度,[7:0]代表定义了七个数据。
reg [7:0]bps_cnt; //定义份数计数器,因为每个信号被人为分为16分,起始数据和结束一共有10位,所以bps_cnt需要计数160次。
reg [8:0]div_cnt; //定义每一份时间计数器,记录每一份所需要的时间。
wire nedge_uart_rx; //定义一个寄存器来记录下降沿,因为开始信号是由高电平突变成低电平,是一个下降沿信号。
wire bps_clk_16x; //定义每一份结束的标志信号。
always @(posedge Clk) begin //定义采集前一信号和现在信号always块。
uart_rx_state[0] <= uart_rx; //将uart_rx_state低位采集此时输入数据状态
uart_rx_state[1] <= uart_rx_state[0]; //将uart_rx_state高位采集此时uart_rx_state低位的数据状态,因为使用 <= 赋值,所以此时高位采集的是上个时钟沿的数据接收状态。
end
assign nedge_uart_rx = (uart_rx_state== 2'b10); //将状态赋值进nedge_uart_rx,若是采集到下降沿信号则值为1。
assign bps_clk_16x = (div_cnt == Bps_DR / 2); //当计数器计数到一半时,将接收完成状态置1提醒进入下一个状态。具体作用在后面会提到。
always @(posedge Clk or negedge Reset_n) begin //接收使能块。
if (!Reset_n)
RX_EN <= 0; //复位时将接收使能关闭。
else if (nedge_uart_rx)
RX_EN <= 1; //当就收到下降沿就将使能置位。
else if (Rx_Done || sta_bit >= 4)
RX_EN <= 0; //只有当接收完成标志置1或者是起始位接收错误,后面会解释,只有当sta_bit<4时才认为起始位接收到的是一个低电平信号,否则认为只是接收口受到干扰而产生的低电平波动。
end
always @(posedge Clk or negedge Reset_n) begin //定义一个一份计数器块。
if (!Reset_n)
div_cnt <= 0; //复位时,计数器归0。
else if (RX_EN) begin //只有接收使能时才进行数据接收。
if (Bps_DR <= div_cnt)
div_cnt <= 0; //当计数器记满后归0。
else
div_cnt <= div_cnt + 1'b1; //否则计数器自增1。
end
else
div_cnt <= 0; //接收使能关闭时将计数器归0。
end
always @(posedge Clk or negedge Reset_n) begin //状态计数器块,记录160份中到了哪一份的位置。
if (!Reset_n)
bps_cnt <= 0; //复位时将状态归0。
else if (RX_EN) begin //接收使能时才能自增状态计数器。
if (bps_clk_16x) begin //当进入下一个状态标志位置位时,状态计数器才能自增1。
if (bps_cnt == 160)
bps_cnt <= 0; //当计数器计数到160将状态归0,这儿你们估计会疑惑为什么不是159吧,因为根据实际仿真,我发现,如果定义成159,在最后停止位会少一位。
else
bps_cnt <= bps_cnt + 1'b1; //其他状态自增1。
end
end
else
bps_cnt <= 0; //在接收未使能状态下,将状态归0。
end
always @(posedge Clk or negedge Reset_n) begin //定义一个数据接受块。
if (!Reset_n) begin //复位时将所有数据接收清0。
sta_bit <= 0;
sto_bit <= 0;
r_data[0] <= 0;
r_data[1] <= 0;
r_data[2] <= 0;
r_data[3] <= 0;
r_data[4] <= 0;
r_data[5] <= 0;
r_data[6] <= 0;
r_data[7] <= 0;
end
else begin
case (bps_cnt) //判断此时在哪个状态。
0:begin //0状态时将所有数据清空。
sta_bit <= 0;
sto_bit <= 0;
r_data[0] <= 0;
r_data[1] <= 0;
r_data[2] <= 0;
r_data[3] <= 0;
r_data[4] <= 0;
r_data[5] <= 0;
r_data[6] <= 0;
r_data[7] <= 0;
end
5,6,7,8,9,10,11:sta_bit <= sta_bit + uart_rx; //起始位接收时,读取中间7位数据,后面的依次接收数据位和结束位的中间7份。
21,22,23,24,25,26,27:r_data[0] <= r_data[0] + uart_rx;
37,38,39,40,41,42,43:r_data[1] <= r_data[1] + uart_rx;
53,54,55,56,57,58,59:r_data[2] <= r_data[2] + uart_rx;
69,70,71,72,73,74,75:r_data[3] <= r_data[3] + uart_rx;
85,86,87,88,89,90,91:r_data[4] <= r_data[4] + uart_rx;
101,102,103,104,105,106,107:r_data[5] <= r_data[5] + uart_rx;
117,118,119,120,121,122,123:r_data[6] <= r_data[6] + uart_rx;
133,134,135,136,137,138,139:r_data[7] <= r_data[7] + uart_rx;
149,150,151,152,153,154,155:sto_bit <= sto_bit + uart_rx;
default:; //其他情况没有改动,我们不需要作出什么操作。
endcase
end
end
always @(posedge Clk or negedge Reset_n) begin //数据读出块。
if (!Reset_n)
Data <= 0; //复位时数据位0。
else if (bps_clk_16x && (bps_cnt == 160)) begin //当所有数据全部取出,也就是当状态寄存器计满,并且数据发送置位信号置位时才开始数据读出。
Data[0] <= r_data[0][2]; //为什么要这样做?因为你会发现,当数据小于等于4时,数据的2位,也就是第三位为0,当大于4时,此位为1,所以我们只需要判断此为就可以判断此时的数据状态,用Data[0] <= (r_data[0][2] > 4);会增加它的电路复杂度。
Data[1] <= r_data[1][2];
Data[2] <= r_data[2][2];
Data[3] <= r_data[3][2];
Data[4] <= r_data[4][2];
Data[5] <= r_data[5][2];
Data[6] <= r_data[6][2];
Data[7] <= r_data[7][2];
end
end
always @(posedge Clk or negedge Reset_n) begin //接收完成信号块。
if (!Reset_n)
Rx_Done <= 0; //复位时接收信号清除。
else if (bps_clk_16x && (bps_cnt == 160))
Rx_Done <= 1; //和数据转化同时将数据接收使能。
else
Rx_Done <= 0; //其他情况将使能清空。
end
endmodule