模块输入输出端口
module uart_byte_rx(
clk,//50M时钟
rst_n,//复位信号
baud_set,//波特率设置信号
rs232_rx,//串口数据接受线
data_byte,//接收到的数据
rx_done//接受完成信号
);
时钟默认时50M,可以修改,波特率设置是选择了几个常见的串口通信速率,通过选择器选择
代码逻辑
//同步寄存器,消除亚稳态
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
s0_rs232_rx <= 1'b0;
s1_rs232_rx <= 1'b0;
end
else begin
s0_rs232_rx <= rs232_rx;
s1_rs232_rx <= s0_rs232_rx;
end
串口的输入信号rs232_rx相对于 FPGA 内部信号来说是一个异步信号(rs232_rx的状态
不依赖于时钟 Clk),如不进行处理直接将其输入使用,容易出现时序违例导致亚稳态。因此
这里就需要先将信号同步到 FPGA 的时钟域内才可以供后续模块使用,常见的同步方法即使
用两级触发器,也就是使用触发器对信号打两拍的方式进行与系统时钟进行同步,参考电路
即下图 所示(参考按键输入的)。其中 key_in(想象成rs232_rx) 为按键输入, key_in_sb (想象成s1_rs232_rx)为同步后的信号。
//数据寄存器
//将同步后的数据在延时两拍,用来判断rx_232的下降沿(下降沿是uart通信的起始信号)
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
tmp0_rs232_rx <= 1'b0;
tmp1_rs232_rx <= 1'b0;
end
else begin
tmp0_rs232_rx <= s1_rs232_rx;
tmp1_rs232_rx <= tmp0_rs232_rx;
end
//生成rx_232的下降沿
assign nedege = !tmp0_rs232_rx & tmp1_rs232_rx;
说一下为什么这个可以判断下降沿,边沿检测其原理就是利用寄存器在时钟信号的控制下,输入状态即为下一时刻输出状态这一特性进行比较判断,原理如下图
其检测过程,可以假设 data_in 从 0 变 1,也就是上升沿:
1. 第一个时钟到来, 第一个寄存器 regA_data 的输出为 0;
2. 第二个时钟沿到来后第一个寄存器输出为 1,第二个寄存器输出此时为 0,这样对
两个寄存器输出进行相关组合逻辑运算则可检测出上升沿;
3. 同理 data_in 从 1 变为 0,也就是下降沿:
4. 第一个时钟到来, 第一个寄存器 regA_data 的输出为 1;
5. 第二个时钟沿到来后第一个寄存器输出为 0,第二个寄存器输出此时为 1。
assign neg_out = !data_in & regA_data;
assign pos_out = data_in & !regA_data;
//波特率选择
always@(posedge clk or negedge rst_n)
if(!rst_n)
bps_DR <= 16'd324;
else begin
case(baud_set)
0:bps_DR <= 16'd324;//9600
1:bps_DR <= 16'd162;//
2:bps_DR <= 16'd80;
3:bps_DR <= 16'd53;
4:bps_DR <= 16'd26;//115200
default:bps_DR <= 16'd324;
endcase
end
波特率生成电路,即通过一个选择器,根据输入的选择确定相应的波特率
波特率计算取下:举例9600 bps
9600bps的意义是一秒钟传输9600bit,因此传输一个bit(码元速率)的时间为1S除以9600,考虑到fpga是数字电路,并且输入查询的时钟为50M(50000000),因此传输1bit需要计数50000000除以9600=5208,
然而上面选择器对应的输出并不是5208,而是324,下面说明为什么这样设计
在实际工业应用中,现场往往有非常强的电磁干扰,只采样一次就作为该数据的电平状态是不可靠的。 很有可能恰好采集到被干扰的信号而导致结果出错,因此这里提出以下改进型的单 bit 数据接收方式示意图,使用多次采样求概率的方式进行状态判定,如下图所示。
将每一位数据再平均分成了 16 小段。对于 Bit_x 这一位数据,考虑到数据在刚刚发生变化和即将发生变化的这一时期,数据极有可能不稳定的(用深灰色标出的两段),在这两个时间段采集数据,很有可能得到错误的结果,因此判定这两段时间的电平无效,采集时直接忽略。而中间这一时间段(用浅灰色标出),数据本身是比较稳定的,一般都代表了正确的结果。也就是前面提到的中间测量方式,但是也不排除该段数据受强电磁干扰而出现错误的电平脉冲。因此对这一段电平,进行多次采样,并求高低电平发生的概率, 6 次采集结果中,取出现次数多的电平作为采样结果。例如,采样 6 次的结果分别为 1/1/1/1/0/1/,则取电平结果为 1,若为 0/0/1/0/0/0,,则取电平结果为 0,当 6 次采样结果中 1 和 0 各占一半(各 3 次),则可判断当前通信线路环境非常恶劣,数据不具有可靠性,不进行处理。
从上面知晓,我们需要对每一bit再进行细分,细分成16分,因此9600bps对应的16细分为5208除以16=324
//波特率分频计数器(上面是分频系数)
always@(posedge clk or negedge rst_n)
if(!rst_n)
div_cnt <= 16'd0;
else if(uart_state)begin
if(div_cnt == bps_DR)
div_cnt <= 16'd0;
else
div_cnt <= div_cnt + 1'b1;
end
else
div_cnt <= 16'd0;
// 波特率时钟生成
always@(posedge clk or negedge rst_n)
if(!rst_n)
bps_clk <= 1'b0;
else if(div_cnt == 16'd1)
bps_clk <= 1'b1;
else
bps_clk <= 1'b0;
//波特率时钟计数器
always@(posedge clk or negedge rst_n)
if(!rst_n)
bps_cnt <= 8'd0;
else if(bps_cnt == 8'd159 | (bps_cnt == 8'd12 && (START_BIT > 2)))
bps_cnt <= 8'd0;
else if(bps_clk)
bps_cnt <= bps_cnt + 1'b1;
else
bps_cnt <= bps_cnt;
上面代码就是根据前面的原理生成的计数器和分频时钟,bps_cnt是以每一个码元的16分之1进行加1的,用于后面进行数据判别
//生成串口接受完成信号,告之后续模块已经接受完成数据
always@(posedge clk or negedge rst_n)
if(!rst_n)
rx_done <= 1'b0;
else if(bps_cnt == 8'd159)
rx_done <= 1'b1;
else
rx_done <= 1'b0;
rx_done是串口接收完成信号,告知后面模块电路串口接收到数据了
//生成最后接受的数据(1 byte)
always@(posedge clk or negedge rst_n)
if(!rst_n)
data_byte <= 8'd0;
else if(bps_cnt == 8'd159)begin
data_byte[0] <= r_data_byte[0][2];
data_byte[1] <= r_data_byte[1][2];
data_byte[2] <= r_data_byte[2][2];
data_byte[3] <= r_data_byte[3][2];
data_byte[4] <= r_data_byte[4][2];
data_byte[5] <= r_data_byte[5][2];
data_byte[6] <= r_data_byte[6][2];
data_byte[7] <= r_data_byte[7][2];
end
生成串口数据,在这里是将一个数组的最高位赋值给接收的字节,
讲一下为什么有个数组,
前面我们说过,每一个码元被分成了16分,我们对中间的六分进行采样,如果一位码元仅仅使用一位的话,我们就不能累积六次的结果,从而降低电磁的干扰,前面说我们在六次中采中同一电平次数大于4次,即认为正确采样,因此需要一个计数器(计数器最大值大于6)来统计采样的次数,这显然不可能是1bit可以做到的,因此设置了3bit的计数器,因为有8个码元,因此创建了8个3bit数组(3bit最大值为7>6),
//串口接受过程,
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
START_BIT = 3'd0;
r_data_byte[0] <= 3'd0;
r_data_byte[1] <= 3'd0;
r_data_byte[2] <= 3'd0;
r_data_byte[3] <= 3'd0;
r_data_byte[4] <= 3'd0;
r_data_byte[5] <= 3'd0;
r_data_byte[6] <= 3'd0;
r_data_byte[7] <= 3'd0;
STOP_BIT = 3'd0;
end
else if(bps_clk)begin
case(bps_cnt)
0:begin
START_BIT = 3'd0;
r_data_byte[0] <= 3'd0;
r_data_byte[1] <= 3'd0;
r_data_byte[2] <= 3'd0;
r_data_byte[3] <= 3'd0;
r_data_byte[4] <= 3'd0;
r_data_byte[5] <= 3'd0;
r_data_byte[6] <= 3'd0;
r_data_byte[7] <= 3'd0;
STOP_BIT = 3'd0;
end
6,7,8,9,10,11:START_BIT <= START_BIT + s1_rs232_rx;
22,23,24,25,26,27:r_data_byte[0] <= r_data_byte[0] + s1_rs232_rx;
38,39,40,41,42,43:r_data_byte[1] <= r_data_byte[1] + s1_rs232_rx;
54,55,56,57,58,59:r_data_byte[2] <= r_data_byte[2] + s1_rs232_rx;
70,71,72,73,74,75:r_data_byte[3] <= r_data_byte[3] + s1_rs232_rx;
86,87,88,89,90,91:r_data_byte[4] <= r_data_byte[4] + s1_rs232_rx;
102,103,104,105,106,107:r_data_byte[5] <= r_data_byte[5] + s1_rs232_rx;
118,119,120,121,122,123:r_data_byte[6] <= r_data_byte[6] + s1_rs232_rx;
134,135,136,137,138,139:r_data_byte[7] <= r_data_byte[7] + s1_rs232_rx;
150,151,152,153,154,155:STOP_BIT <= STOP_BIT + s1_rs232_rx;
default:
begin
START_BIT = START_BIT;
r_data_byte[0] <= r_data_byte[0];
r_data_byte[1] <= r_data_byte[1];
r_data_byte[2] <= r_data_byte[2];
r_data_byte[3] <= r_data_byte[3];
r_data_byte[4] <= r_data_byte[4];
r_data_byte[5] <= r_data_byte[5];
r_data_byte[6] <= r_data_byte[6];
r_data_byte[7] <= r_data_byte[7];
STOP_BIT = STOP_BIT;
end
endcase
end
上面是核心的串口接收部分,需要说一下的是第二段赋值的地方,这个时候由于每个bit采样了6六次,所以才会出来6,7,8,9,10,11;22,23,24,25,26,27成组的数据(这些都是每个码元中间六段bps_cnt的计数值),
r_data_byte[0] <= r_data_byte[0] + s1_rs232_rx;
r_data_byte[0]这是一个数组,在verilog中这个表示寄存器的第0位(r_data_byte定义成寄存器的情况下)或者是第一个数组(r_data_byte定义成数组的情况下),在六次计数中,分别加上rx数据线上的数据,即可统计六次中采样电平数
假设六次中有5次高电平,这个数组就会加成5,假设有六次,这个数组就会加成6,而不用判断rx电平高低,因为判断为1时,需要的是加1,判断为0时需要假的是0,
说一下为什么统计电平为什么不需要两个数组,因为在六次采样中,不是1就是0,因此两种电平的采样总数为6,因此只需要采样高电平的次数
//串口接受状态,为高时表明串口正在进行接受工作,为低时表示空闲,可以告知其他模块在空闲时间发送数据
always@(posedge clk or negedge rst_n)
if(!rst_n)
uart_state <= 1'b0;
else if(nedege)
uart_state <= 1'b1;
else if(rx_done || (bps_cnt == 8'd12 && (START_BIT > 2)))
uart_state <= 1'b0;
else
uart_state <= uart_state;
上面用来表示串口工作状态,为1是表示串口正在工作,为0时空闲