????????UART是单片机开发中最常用的串行通信接口之一,今天我用FPGA实现UART通信,感受一下FPGA与单片机之间的区别。
? ? ? ? 本次实验的功能:上位机通过USB转TTL工具向FPGA主板发送数据,主板收到后将数据返回到上位机。
1、UART接收模块?
//串口接收模块
module urat_recv (
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低电平有效
input uart_rxd, //UART接收端口,这里代表的就是硬件RX引脚
output reg uart_done, //接收一帧数据完成标志信号
output reg [7:0] uart_data //接收的数据
);
//parameter可用作在顶层模块中例化底层模块时传递参数的接口,
parameter CLK_FREQ = 50000000; //系统时钟频率
parameter UART_BPS = 9600; //串口波特率
//localparam的作用域仅仅限于当前module,不能作为参数传递的接口。
localparam BPS_CNT=CLK_FREQ/UART_BPS; //为得到指定波特率,需要对系统时钟计数BPS_CNT次
reg uart_rxd_d0;
reg uart_rxd_d1;
reg [15:0] clk_cnt; //系统时钟计数器
reg [3:0] rx_cnt; //接收数据计数器
reg rx_flag; //接收过程标志信号
reg [7:0] rxdata; //接收数据缓存
wire start_flag; //进入接收过程标志
//边沿检测的一个时钟周期脉冲信号
assign start_flag = uart_rxd_d1 & (~uart_rxd_d0);
always @(posedge sys_clk or negedge sys_rst_n)begin//系统时钟的上升沿触发 或者复位信号触发此always块
if(!sys_rst_n) begin
uart_rxd_d0<=1'b0; //复位时清零
uart_rxd_d1<=1'b0; //复位时清零
end
else begin
uart_rxd_d0 <= uart_rxd; //寄存 uart_rxd与实际电路连接,其连接的是uart的RX接口
uart_rxd_d1 <= uart_rxd_d0;
end
end
//当脉冲信号start_flag(边沿检测)到达时,进入接收过程
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
rx_flag=1'b0;
else begin
if(start_flag) //上一个模块控制的变量
rx_flag<=1; //进入接收过程
else if((rx_cnt==4'd9/*9是8个数据尾+1个停止位*/)&&(clk_cnt==BPS_CNT/2/*当接收到第9个bit时,在波特率周期的中间将接收标志置零*/))
//clk_cnt:是波特率计数 rx_cnt:表示当前接收到第几个数据
rx_flag<=0;
else
rx_flag<=rx_flag; //保持原有值
end
end
//进入接收过程后,启动系统时钟计数器与接收数据计数器
always @(posedge sys_clk or negedge sys_rst_n)begin//系统时钟的上升沿触发 或者复位信号触发此always块
if(!sys_rst_n) begin
clk_cnt<=16'b0; //复位时清零
rx_cnt<=4'b0; //复位时清零
end
else if(rx_flag)//上一个模块控制的变量,表示当前处于接收状态
begin
if(clk_cnt<BPS_CNT-1)begin//波特率计数
clk_cnt<=clk_cnt+1;//波特率接着计数
rx_cnt<=rx_cnt;//还没有接收到新的bit,计数保持
end
else begin
clk_cnt<=16'd0;//波特率周期时间到,计数清零
rx_cnt<=rx_cnt+1;
end
end
else begin
clk_cnt<=16'b0; //复位时清零
rx_cnt<=4'b0; //复位时清零
end
end
//将由外部接收到的数据放到uart接收缓存中
always @(posedge sys_clk or negedge sys_rst_n)begin//系统时钟的上升沿触发 或者复位信号触发此always块
if ( !sys_rst_n)
rxdata <= 8'd0; //复位则清空缓存
else if(rx_flag)begin //当前是接收状态
if(clk_cnt==BPS_CNT/2)begin //波特率周期一般的时候
case (rx_cnt) //接收到第几个bit
4'd1 : rxdata[0] <= uart_rxd_d1; //寄存数据位最低位
4'd2 : rxdata[1] <= uart_rxd_d1;
4'd3 : rxdata[2] <= uart_rxd_d1;
4'd4 : rxdata[3] <= uart_rxd_d1;
4'd5 : rxdata[4] <= uart_rxd_d1;
4'd6 : rxdata[5] <= uart_rxd_d1;
4'd7 : rxdata[6] <= uart_rxd_d1;
4'd8 : rxdata[7] <= uart_rxd_d1; //寄存数据位最高位
default:;
endcase
end
else
rx_flag<=rx_flag; //保持
end
else
rxdata<=8'd0;
end
always@(posedge sys_clk or negedge sys_rst_n)begin//系统时钟的上升沿触发 或者复位信号触发此always块
if (!sys_rst_n) begin
uart_data <= 8'd0;
uart_done <= 1'b0;
end
else if(rx_cnt==4'd9)begin //接收数据计数器计数到停止位时
uart_data <= rxdata; //数据由此模块输出
uart_done <= 1'b1; //此模块输出的接收完成标志
end
else begin
uart_data <= 8'd0; //没有接收完成,则清零
uart_done <= 1'b0;
end
end
endmodule
? ? ? ? ?单片机开发时只需要将波特率、数据位、停止位等参数配置给现有的功能模块上即可,但FPGA开发时,每个功能都是硬件层面的东西。例如:
- ????????串口波特率:FPAG开发时,串口波特率是通过系统时钟频率计数得到的,bps周期=系统时钟频率?/ bps,然后再通过计数得到每个bit之间的周期。
- ? ? ? ? 数据位、停止位:在FPGA中就是程序控制上的东西,程序设计时可以任意设计数据位停止位(奇葩做法),但是通信双方的设备要保持一直。
?????????这里程序设计时的一个技巧,在纯软件开发的单片机是体会不到的:
?????????上图中UART接收端其实就是硬件上的信号,表示芯片在硬件电路上检测到了数据线上的数据,对应现实中就是调试软件点了下发数据操作。
? ? ? ? ?上图中的操作就是从硬件上产生一个数据接收的标志,与单片机的串口接收中断类似。
?2、UART发送模块?
//发送模块
module uart_send(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低电平有效
input uart_en, //发送使能信号
input [7:0] uart_din, //待发送数据
output reg uart_txd //UART发送端口
);
parameter CLK_FREQ = 50000000; //系统时钟频率
parameter UART_BPS = 9600; //串口波特率
localparam BPS_CNT = CLK_FREQ/UART_BPS; //为得到指定波特率,对系统时钟计数BPS_CNT次
reg uart_en_d0;
reg uart_en_d1;
reg [15:0] clk_cnt; //系统时钟计数器 计数bps
reg [3:0] tx_cnt; //发送数据计数器bit
reg tx_flag; //发送过程标志信号
reg [7:0] tx_data; //发送数据缓存
wire en_flag;
//捕获uart_en上升沿,得到一个时钟周期的脉冲信号
assign en_flag = (~uart_en_d1) & uart_en_d0;
//对发送使能信号uart_en延迟两个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_en_d0 <= 1'b0;
uart_en_d1 <= 1'b0;
end
else begin
uart_en_d0 <= uart_en;
uart_en_d1 <= uart_en_d0;
end
end
//当脉冲信号en_flag到达时,寄存待发送的数据,并进入发送过程
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
tx_flag <= 1'b0;
tx_data <= 8'd0;
end
else if (en_flag) begin //检测到发送使能上升沿
tx_flag <= 1'b1; //进入发送过程,标志位tx_flag拉高
tx_data <= uart_din; //寄存待发送的数据
end
else
if ((tx_cnt == 4'd9)&&(clk_cnt == BPS_CNT/2))
begin //计数到停止位中间时,停止发送过程
tx_flag <= 1'b0; //发送过程结束,标志位tx_flag拉低
tx_data <= 8'd0;
end
else begin
tx_flag <= tx_flag;
tx_data <= tx_data;
end
end
//进入发送过程后,启动系统时钟计数器与发送数据计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
clk_cnt <= 16'd0;
tx_cnt <= 4'd0;
end
else if (tx_flag) begin //处于发送过程
if (clk_cnt < BPS_CNT - 1) begin
clk_cnt <= clk_cnt + 1'b1;
tx_cnt <= tx_cnt;
end
else begin
clk_cnt <= 16'd0; //对系统时钟计数达一个波特率周期后清零
tx_cnt <= tx_cnt + 1'b1; //此时发送数据计数器加1
end
end
else begin //发送过程结束
clk_cnt <= 16'd0;
tx_cnt <= 4'd0;
end
end
//根据发送数据计数器来给uart发送端口赋值
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
uart_txd <= 1'b1;
else if (tx_flag)
case(tx_cnt)
4'd0: uart_txd <= 1'b0; //起始位
4'd1: uart_txd <= tx_data[0]; //数据位最低位
4'd2: uart_txd <= tx_data[1];
4'd3: uart_txd <= tx_data[2];
4'd4: uart_txd <= tx_data[3];
4'd5: uart_txd <= tx_data[4];
4'd6: uart_txd <= tx_data[5];
4'd7: uart_txd <= tx_data[6];
4'd8: uart_txd <= tx_data[7]; //数据位最高位
4'd9: uart_txd <= 1'b1; //停止位
default: ;
endcase
else
uart_txd <= 1'b1; //空闲时发送端口为高电平
end
endmodule
?????????uart_done: ?接收完成标志,高电平表示接收完成,是信号发出的位置 ?????????uart_TX_en: 发送使能标志,高电平开始接收 ?????????urat_recv_init 实例化时将 ? uart_done ? 与 ?uart_TX_en连接到了一起 ?????????urat_send_init 实例化时又将 uart_TX_en ?与 ?uart_en连接到了一起 ????????这么做实现的功能是:当接收完成时产生一个高电平表示可以发送,发送端检测到这个高电平开始发送数据 ?
? ? ? ? 顶层模块将接收模块 和 发送模块相关信号 连接在一起
串口发送模块同样有一个边沿检测的过程,与接收不同的是这里检测的是一个上升沿
?
3、顶层模块
? ? ? ? FPGA开发中每个项目都有一个顶层代码,来实例化底层的驱动。????????
//顶层模块
module uart_top(
input sys_clk, //外部50M时钟
input sys_rst_n, //外部复位信号,低有效
input uart_rxd, //UART接收端口
output uart_txd //UART发送端口
);
parameter CLK_FREQ=50000000; //定义系统时钟频率
parameter UART_BPS=115200; //定义串口波特率
wire uart_TX_en; //串口发送使能
wire [7:0] uart_TX_data; //串口发送数据缓存
wire clk_1ms ; //1ms定时器
//实例化底层接收
urat_recv #(
.CLK_FREQ (CLK_FREQ), //设置系统时钟频率
.UART_BPS (UART_BPS) //设置串口接收波特率
)//这里是将顶层模块值传递给底层模块
urat_recv_init(
.sys_clk (sys_clk), //系统时钟
.sys_rst_n (sys_rst_n), //复位信号
.uart_rxd (uart_rxd), //UART接收端口
.uart_done (uart_TX_en), //底层接收完成标志 赋值给 顶层 发送 使能标志
.uart_data (uart_TX_data) //底层接收到的数据放到 串口发送数据缓存 中
) ; //底层模块数据 连接到顶层
// uart_done: 接收完成标志,高电平表示接收完成,是信号发出的位置
// uart_TX_en: 发送使能标志,高电平开始接收
// urat_recv_init 实例化时将 uart_done 与 uart_TX_en连接到了一起
// urat_send_init 实例化时又将 uart_TX_en 与 uart_en连接到了一起
// 这么做实现的功能是:当接收完成时产生一个高电平表示可以发送,发送端检测到这个高电平开始发送数据
uart_send #( //串口发送模块
.CLK_FREQ (CLK_FREQ), //设置系统时钟频率
.UART_BPS (UART_BPS)) //设置串口发送波特率
urat_send_init(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.uart_en (uart_TX_en),
.uart_din (uart_TX_data),
.uart_txd (uart_txd)
);
endmodule
????????一般在顶层模块会定义本工程使用的所有硬件资源,然后分配给各个底层模块;
????????各个模块直接的数据传输通过底层实例化来进行连接
5、实验验证?
USB 转TTL工具与电路板相连?
打开调试工具,验证实验结果
?
|