疫情未散,在家里开始投入新的一年的学习。去年零零散散研究了一点FPGA,今年打算系统学习一下。
串口通信是单片机、FPGA学习逃不过的一课,也是很好的FPGA入门实验。
对于一个无校验位的串口数据包,包含如下几个部分
起始位[0] | Data[1] | Data[2] | Data[3] | Data[4] | Data[5] | Data[6] | Data[7] | 停止位[1] |
---|
当串口数据发送开始,TX拉低表示一个起始位,紧接着从低到高 依次发送8位数据位,数据发送完毕以后释放TX,表示数据结束。
由于笔者的FPGA开发板使用外部50MHz晶振提供时钟,因此 f c l k = 50 M H z f_{clk} = 50MHz fclk=50MHz,通讯实验使用的波特率位115200bps,这意味着每一秒钟需要发送115200个数据位。
不同于单片机程序设计,FPGA做不到直接延时,需要使用累加计数控制每一位发送的时间,波特率技术器的值可以用下式计算
c o u n t b a u d = f c l k b a u d count_{baud}=\frac{f_{clk}}{baud} countbaud=baudfclk
式中 c o u n t b a u d count_{baud} countbaud就是累加值。
累加的方式体现了分频的思想,高速运行的逻辑门不可能“停下来”等那么几个毫秒。
1.使能信号上升沿的提取
reg en1, en2;
wire tx_en;
assign tx_en = en1 & (~en2);
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
en1<=1'b0;
en2<=1'b0;
end
else begin //打两拍,提取上升沿
en1<=en;
en2<=en1;
end
end
这里使能信号采用了打两拍的方式,连续的非阻塞赋值使得en1、en2保持了跳变沿前后的值,因此对于上升沿检测,只需要
e n 1 & e n 2 ‾ \;en1\;\ \& \;\overline{en2} en1 &en2
反之检测下降沿,只需要
e n 2 & e n 1 ‾ \;en2\;\ \& \;\overline{en1} en2 &en1
为什么要使用边沿信号而非电平信号?
边沿信号可以避免数据装载与发送速率不同步带来的数据发送异常。
2.开始标志位
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
tx_start <= 1'b0;
end
else begin
if(tx_en) begin
tx_data <= data;//装载数据
tx_start <= 1'b1;//开始
end
else begin
if(data_count == 5'd9 && (count == baud_count)) begin //最后一位停止位
tx_start <= 1'b0;
end
else begin
tx_start <= tx_start;
end
end
end
end
当检测到使能信号上升沿以后,发送标志位tx_en被置位,同时数据装入八位寄存器tx_data。当数据传输至最后一位(停止位)时,发送标志位复位。
data_count == 5'd9 && (count == baud_count)
这句话的使用能够保证发送标志位在整个数据传输期间都为高电平。
3.波特率计数器
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
count <= 16'b0;
end
else begin
if(tx_start) begin
if(count < baud_count) begin
count <= count + 1'b1;
end
else begin
count <= 16'b0;
end
end
else begin
count <= 16'b0;
end
end
end
波特率计数器其实就是一个分频器,用分频代替C语言中的Delay。
4.数据计数器
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
data_count <= 5'b0;
end
else begin
if(tx_start) begin
if(count == baud_count - 1) begin
if(data_count < 5'd9) begin
data_count <= data_count + 1'b1;
end
else begin
data_count <= 5'b0;
end
end
else begin
data_count <= data_count;
end
end
else begin
data_count <= 5'b0;
end
end
end
数据计数器记录了发送数据的数量。
5.并行转串行
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
tx <= 1'b1;
end
else begin
if(tx_start) begin
case(data_count)
5'd0:tx <= 1'b0;
5'd1:tx <= tx_data[0];
5'd2:tx <= tx_data[1];
5'd3:tx <= tx_data[2];
5'd4:tx <= tx_data[3];
5'd5:tx <= tx_data[4];
5'd6:tx <= tx_data[5];
5'd7:tx <= tx_data[6];
5'd8:tx <= tx_data[7];
5'd9:tx <= 1'b1;
endcase
end
else begin
tx <= 1'b1;
end
end
end
这是串口发送中最重要的一部分。
为什么数据计数器不在这段程序中完成?
对于每一个always块而言他们都是并行执行的,而always块中的程序则是顺序执行的,对于每一个并行执行的代码而言只需要各司其职,在合适的时候产生合适的电平信号。将多个不同的功能都在同一个always块中完成无疑增加了逻辑复杂度,使得代码难以编写。
module tx_test(
input clk,
input nrst,
output reg [7:0] data,
output reg en
);
reg [3:0] data_count;
reg [19:0] div;
parameter d0 = "H";
parameter d1 = "e";
parameter d2 = "l";
parameter d3 = "l";
parameter d4 = "o";
parameter d5 = "\n";
parameter div_count = 20'd100_0000;
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
div <= 20'b0;
end
else begin
if(div<div_count) begin
div <= div + 20'b1;
end
else begin
div <= 20'b0;
end
end
end
always@(posedge clk or negedge nrst) begin
if(!nrst) begin
data_count <= 4'b0;
end
else begin
if(div == div_count - 1'b1) begin
if(data_count < 4'd5) data_count <= data_count + 4'b1;
else data_count <= 4'b0;
end
else begin
data_count <= data_count;
end
end
end
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
en <= 1'b0;
end
else begin
case(div)
20'd0:en <= 1'b1;
div_count - 20'b1:en <= 1'b0;
default:en <= en;
endcase
end
end
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
data <= 8'b0;
end
else begin
case(data_count)
4'd0:data <= d0;
4'd1:data <= d1;
4'd2:data <= d2;
4'd3:data <= d3;
4'd4:data <= d4;
4'd5:data <= d5;
default:data <= 8'b0;
endcase
end
end
endmodule
这部分程序几乎与发送部分采用了同样的逻辑,代码比较类似。