一、串口简介
串口作为常用的三大低速总线(UART、SPI、IIC)之一,在设计众多通信接口和调试时占有重要地位。但UART和SPI、IIC不同的是,它是异步通信接口,异步通信中的接收方并不知道数据什么时候会到达,所以双方收发端都要有各自的时钟,在数据传输过程中是不需要时钟的,发送方发送的时间间隔可以不均匀,接受 方是在数据的起始位和停止位的帮助下实现信息同步的。而SPI、IIC是同步通信接口(后面的章节会做详细介绍),同步通信中双方使用频率一致的时钟,在数据传输过程中时钟伴随着数据一起传输,发送方和接收方使用的时钟都是由主机提供的。
UART通信只有两根信号线,一根是发送数据端口线叫tx(Transmitter),一根是接收数据端口线叫rx(Receiver)。可以实现全双工,即可以同时进行发送数据和接收数据。
RS232通信协议简介
串口的通信协议有很多,如RS232、RS499、RS423、RS422和RS485等接口,但是这里我们就使用最常见的通信协议RS232。
下面是数据帧结构
rx和tx的位宽都是1bit。串口数据的发送与接收是基于帧结构的,即一帧一帧的发送与接收数据。
每一帧数据共10bit,起始位为0,停止位为1,中间包含8bit的数据位。在不发送或者不接收数据的情况下,rx和tx处于空闲状 态,此时rx和tx线都保持高电平,即为空闲状态。
几个重要概念
波特率:在信息传输通道中,携带数据信息的信号单元叫码元(因为串口是1bit进行传输的,所以其码元就是代表一个二进制数),每秒钟通过信号传输的码元数称为码元的传输速率,简称波特率。串口常见的波特率有4800、9600、115200等。
由计算得串口发送或者接收1bit数据的时间为一个波特,即1/9600秒,如果用50MHz(周期为20ns)的系统时钟来计数,需要计数的个数为cnt = (1s * 10^9)ns / 9600bit)ns / 20ns ≈ 5208个系统时钟周期,即每个bit数据之间的间隔要在50MHz的时钟频率下计数5208次。
二、串口接收模块
先设计串口接收模块,该模块的功能是接收通过PC机上的串口调试助手发送的固定波特率的数据,串口接收模块按照串口的协议准确接收串行数据,解析提取有用数据后需将其转化为并行数据,因为并行数据在FPGA内部传输的效率更高,转化为并行数据后同时产生一个数据有效信号标志信号伴随着并行的有效数据一同输出。
采用WPS绘制框图--串口接收模块
- rx:1bit 通过该模块将串行数据转换成8bit并行数据
- 8bit并行数据有效的标志信号 po_flag
- po_data 8bit数据并行输出
波形设计
思路1:减少亚稳态的影响对rx打两拍,两级寄存器
{signal: [
{name: 'clk', wave: 'p.........|............'},
{name: 'rerset', wave: 'lh......|..............'},
{name: 'rx', wave: 'h......lhlhlhlhlh..'},
{name: 'rx_reg1', wave: 'h.......lhlhlhlhlh..'},
{name: 'rx_reg2', wave: 'h........lhlhlhlhlh..'},
],
config: { hscale: 2,
skin: "narrow"
}
}
思路二:采集下降沿
经过两级寄存器后rx_reg2相对稳定,rx_reg1是不稳定的,所以打算再打一拍,rx_reg3和rx_reg2用于采集下降沿
采集好下降沿后让start_negedge维持一个时钟周期
思路三:工作使能信号
为了使在过程中的下降沿不会影响串口的接收,我们设置一个工作使能信号,在work_en信号为高的时候,可以判断出现的start_negedge是不是我们想要的串口一帧的起始位下降沿,从而不影响后续操作
思路四:正式接收数据
正式开始接收一帧数据。我们使用的是9600bps的波特率和PC机进行串口通信,PC机的串口调试助手要将发送数据波特率调整为9600bps。而FPGA内部使用的系统时钟是50MHz,前面也进行过计算,得出1bit需要的时间约为5208个(因为一帧只有10bit,细微的近似计数差 别不会产生数据错误,但是如果计数值差的过大,则会产生接收数据的错误)系统时钟周期,那么我们就需要产生一个能计5208个数的计数器来依次接收10个比特的数据,计数器每计5208个数就接收一个新比特的数据。计数器名为baud_cnt,当work_en信号为高电平的时候就让计数器计数,当计数器计5208个数(从0到5207)或work_en信号为低电平时计数器清零。
思路五:读取数据
我们选择当baud_cnt计数器计数到2603,即中间位置时取数最稳定(其实只要b aud_cnt计数器在计数值不是在0和5207这两个最不稳定的时刻取数都可以,更为准确的是多次取值取概率最大的情况)。
思路六:rx何时拉高
串口的数据是基于帧的,所以每接收完一帧数据rx信号都要被拉高,即恢复到空闲状态重新判断串口帧起始下降沿,以等待下一帧数据的接收,且一帧数据中还包括了起始位和停止位这种无用的数据,而对我们有价值的数据只是中间的8bit数据,也就是说我们需要准确的知道我们此时此刻接收的是第几比特,当接收够10bit数据 后,我们就停止继续接收数据,等rx信号被拉高待恢复到空闲状态后再等待接收下一帧的数据。
bit_cnt用于记录目前接收到第几bit数据
可以利用我们已经产生的bit_flag取数标志信号,对该信号进行计数既可以知道此时我们接收的数据是第几个比 特了。这里我们只让bit_cnt计数器的计数值为8时再清零,虽然bit_cnt计数器的计数值从0计数到8只有9个bit,但这9个bit中已经包含的我们所需要的8bit有用的数据,最后的1bit停止位没有用,可以不用再进行计数了。
思路七:work_en何时拉低
前面讲了work_en什么时候拉高,下面思考什么时候拉低,当接受完第八位数据时:①bit_flag = 1
②bit_cnt = 8;如果只计数到8没有bit_flag = 1的条件的话会错失第8bit有小数据,读取位置偏前
思考八:串转并
串并转换就需要做移位,我们要考虑清楚什么时候开始移位,不能提前也不能推后,否则会将无用的 数据也移位进来,所以我们需要卡准时间。注意移位的条件,要在bit_cnt计数器的计数值为1到8区间内且bit_flag取数标志信号同时为高时才能移位,也就是移动7次即可,接收最后1bit有用数据时就不需要再进行移位了。当移位7次后1bit的串行数据已经变为8 bit的并行数据了,此时产生一个移位完成标志信号rx_flag。
思考九:输出po_data
程序编写
Robei EDA框图
//reg define
reg rx_reg1;
reg rx_reg2;
reg rx_reg3;
reg work_en; //工作使能信号
reg start_negedge;//下降沿检测
reg [12:0]baud_cnt; //比特计数器
reg bit_flag;
reg [3:0]bit_cnt;
reg [7:0]rx_data;
reg rx_flag;
parameter baud_cnt_MAX = 13'd5208;
//三拍寄存器
always@(posedge clk or negedge reset)
if(!reset)begin
rx_reg1 <= 1'b0;
end
else begin
rx_reg1 <= rx;
end
always@(posedge clk or negedge reset)
if(!reset)begin
rx_reg2 <= 1'b0;
end
else begin
rx_reg2 <= rx_reg1;
end
always@(posedge clk or negedge reset)
if(!reset)begin
rx_reg3 <= 1'b0;
end
else begin
rx_reg3 <= rx_reg2;
end
//start_negedge
always@(posedge clk or negedge reset)
if(!reset)begin
start_negedge <= 1'b0;
end
else if((~rx_reg2) && (rx_reg3))
start_negedge <= 1'b1;
else
start_negedge <= 1'b0;
//work_en
always@(posedge clk or negedge reset)
if(!reset)
work_en <= 1'b0;
else if(start_negedge == 1'b1)
work_en <= 1'b1;
else if((bit_cnt == 4'd8) && (bit_flag == 1'b1))
work_en <= 1'b0;
else
work_en <= work_en;
//baud_cnt
always@(posedge clk or negedge reset)
if(!reset)
baud_cnt<= 0;
else if(work_en == 1'b1)begin
if(baud_cnt >= baud_cnt_MAX - 1'b1)
baud_cnt <= 0;
else
baud_cnt <= baud_cnt + 1'b1;
end
else
baud_cnt <= 0;
//bit_flag
always@(posedge clk or negedge reset)
if(!reset)
bit_flag <= 0;
else if(work_en == 1'b1)begin
if(baud_cnt == baud_cnt_MAX/2)
bit_flag <= 1;
else
bit_flag <= 0;
end
else bit_flag <= 0;
//bit_cnt;
always@(posedge clk or negedge reset)
if(!reset)
bit_cnt <= 4'd0;
else if((bit_cnt == 4'd8) && (bit_flag == 1'b1))
bit_cnt <= 4'b0;
else if(bit_flag ==1'b1)
bit_cnt <= bit_cnt + 1'b1;
//rx_data:输入数据进行移位
always@(posedge clk or negedge reset)
if(reset == 1'b0)
rx_data <= 8'b0;
else if((bit_cnt >= 4'd1)&&(bit_cnt <= 4'd8)&&(bit_flag == 1'b1))
rx_data <= {rx_reg3, rx_data[7:1]};
//rx_flag
always@(posedge clk or negedge reset)
if(!reset)
rx_flag <= 1'b0;
else if(bit_cnt == 4'd8 &&(bit_flag == 1'b1))
rx_flag <= 1'b1;
else
rx_flag <= 1'b0;
//po_data
always@(posedge clk or negedge reset)
if(!reset)
po_data <= 8'b0;
else if(rx_flag)
po_data <= rx_data;
//po_flag
always@(posedge clk or negedge reset)
if(!reset)
po_flag <= 1'b0;
else if(rx_flag)
po_flag <= 1'b1;
else
po_flag <= 1'b0;
仿真文件
//初始化系统时钟、全局复位和输入信号
initial begin
clk = 1'b1;
reset <= 1'b0;
rx <= 1'b1;
#20;
reset <= 1'b1;
#1000_000_00;
$finish;
end
//模拟发送8次数据,分别为0~7
initial begin
#200
rx_bit(8'd0); //任务的调用,任务名+括号中要传递进任务的参数
rx_bit(8'd1);
rx_bit(8'd2);
rx_bit(8'd3);
rx_bit(8'd4);
rx_bit(8'd5);
rx_bit(8'd6);
rx_bit(8'd7);
end
//sys_clk:每10ns电平翻转一次,产生一个50MHz的时钟信号
always #10 clk = ~clk;
//定义一个名为rx_bit的任务,每次发送的数据有10位
//data的值分别为0~7由j的值传递进来
//任务以task开头,后面紧跟着的是任务名,调用时使用
task rx_bit(
//传递到任务中的参数,调用任务的时候从外部传进来一个8位的值
input [7:0] data
);
integer i; //定义一个常量
//用for循环产生一帧数据,for括号中最后执行的内容只能写i=i+1
//不可以写成C语言i=i++的形式
for(i=0; i<10; i=i+1) begin
case(i)
0: rx <= 1'b0;
1: rx <= data[0];
2: rx <= data[1];
3: rx <= data[2];
4: rx <= data[3];
5: rx <= data[4];
6: rx <= data[5];
7: rx <= data[6];
8: rx <= data[7];
9: rx <= 1'b1;
endcase
#(5208*20); //每发送1位数据延时5208个时钟周期
end
endtask //任务以endtask结束
仿真文件里Verilog语法学习
task 任务名(
//此处写传递到任务里的参数
input xxx,
output xxx,
inout xxx
);
endtask //任务以endtask结束
仿真波形
经过仿真,慢慢改正,成功完成。
三、串口发送模块
- clk:时钟
- reset:复位信号,低电平有效
- pi_data:并行数据输入
- pi_flag:并行数据输入有效信号
- tx:发送端
波形设计![](https://i-blog.csdnimg.cn/blog_migrate/bb728cde5d41b43b882ea5cc442e2257.png)
波形分析一:由于pi_flag的高电平只维持1个时钟周期,所以我们产生一个work_en工作使能信号
work_en=1 的条件下baud_cnt开始计数
波形分析二:选择发送数据的位置点
baund_cnt = 1
波形分析三:bit_cnt
一帧数据包括起始位、8位有效数据、停止位,需要一个4位的bit_cnt用于记录发了多少
波形分析四:
当bit_cnt计数到9就可以不再计数,因为停止位和空闲状态均为高电平
程序编写
//reg define
reg [12:0] baud_cnt;
reg [3:0] bit_cnt;
reg bit_flag;
reg work_en;
parameter Baud_cnt_Max = 13'd5208;
//工作使能信号
always@(posedge clk or negedge reset)
if(!reset)
work_en <= 1'b0;
else if(pi_flag == 1'b1)
work_en <= 1'b1;
else if(bit_cnt == 4'd9 && bit_flag == 1)
work_en <= 1'b0;
else
work_en <= work_en;
always@(posedge clk or negedge reset)
if(!reset)
baud_cnt <= 13'b0;
else if(baud_cnt >= Baud_cnt_Max)
baud_cnt <= 13'b0;
else if(work_en)
baud_cnt <= baud_cnt + 1'b1;
else
baud_cnt <= 13'b0;
always@(posedge clk or negedge reset)
if(!reset)
bit_flag <= 0;
else if(baud_cnt == 13'd1)
bit_flag <= 1'b1;
else
bit_flag <= 1'b0;
always@(posedge clk or negedge reset)
if(!reset)
bit_cnt <= 4'd0;
else if(bit_flag&&work_en)
bit_cnt <= bit_cnt + 1'b1;
else if((work_en == 1'b0 )||(bit_cnt == 4'd9 && bit_flag ))
bit_cnt <= 4'd0;
else bit_cnt <= bit_cnt;
always@(posedge clk or negedge reset)
if(!reset)
tx <= 1'b1;//空闲状态为高电平
else if(bit_flag)
case(bit_cnt)
0: tx <= 1'b0;
1: tx <= pi_data[0];
2: tx <= pi_data[1];
3: tx <= pi_data[2];
4: tx <= pi_data[3];
5: tx <= pi_data[4];
6: tx <= pi_data[5];
7: tx <= pi_data[6];
8: tx <= pi_data[7];
9: tx <= 1'b1;
default: tx <= 1'b1;
endcase
else tx <= tx;
四、数据回环
写在最后--串口学习(参考)可以应用在蓝牙模块以及一系列串口通信的外设,继续努力学习。
UART那么好用,为什么单片机还需要I2C和SPI?