FPGA--UART串口通信
一,串口相关知识
UART 通信 UART 首先将接收到的并行数据转换成串行数据来传输。消息帧从一个低位起始位开始,后面是 7 个或 8 个数据位,一个可用的奇偶位和一个或几个高位停止位。接收器发现开始位时它就知道数据准备发送,并尝试与发送器时钟频率同步。如果选择了奇偶校验,UART 就在数据位后面加上奇偶位。奇偶位可用来帮助错误校验。在接收过程中, UART 从消 息帧中去掉起始位和结束位,对进来的字节进行奇偶校验,并将数据字节从串行转换成并行。UART 传输时序如下图所示 :
串口通讯4根线:Vcc ,Gnd , Tx , Rx;TX-TTL发送端;RX--TTL接收端;
比特率:9600bps 就是每秒中传输9600bit,也就是104us一个逻辑电平;
串行通信的分类:
1、按照数据传送方向,分为:
单工:数据传输只支持数据在一个方向上传输;
半双工:允许数据在两个方向上传输。但是,在某一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信;它不需要独立的接收端和发送端,两者可以合并一起使用一个端口。
全双工:允许数据同时在两个方向上传输。因此,全双工通信是两个单工通信方式的结合,需要独立的接收端和发送端。
2、按照通信方式,分为:
- 同步通信:带时钟同步信号传输。比如:SPI,IIC通信接口。
- 异步通信:不带时钟同步信号。比如:UART(通用异步收发器),单总线。
在同步通讯中,收发设备上方会使用一根信号线传输信号,在时钟信号的驱动下双方进行协调,同步数据。例如,通讯中通常双方会统一规定在时钟信号的上升沿或者下降沿对数据线进行采样。
在异步通讯中不使用时钟信号进行数据同步,它们直接在数据信号中穿插一些用于同步的信号位,或者将主题数据进行打包,以数据帧的格式传输数据。通讯中还需要双方规约好数据的传输速率(也就是波特率)等,以便更好地同步。常用的波特率有4800bps、9600bps、115200bps等。
在同步通讯中,数据信号所传输的内容绝大部分是有效数据,而异步通讯中会则会包含数据帧的各种标识符,所以同步通讯效率高,但是同步通讯双方的时钟允许误差小,稍稍时钟出错就可能导致数据错乱,异步通讯双方的时钟允许误差较大。
常见的串行通信接口:
串口通讯的数据包由发送设备通过自身的TXD接口传输到接收设备的RXD接口,通讯双方的数据包格式要规约一致才能正常收发数据。
奇偶校验位分为奇校验和偶校验两种,是一种简单的数据误码校验方法。奇校验是指每帧数据中,包括数据位和奇偶校验位的全部9个位中1的个数必须为奇数;偶校验是指每帧数据中,包括数据位和奇偶校验位的全部9个位中1的个数必须为偶数。
二,代码相关知识:
1,case语句是分支比较语句,也就是说,本质上就是case后面括号中的值与下面每个分支开头的值作比较,相同则执行。
case(表达式)
表达式1:代码
表达式2:代码
。。。
default:代码
endcase
2,parameter:参数数据类型,其实就是个常量,通常出现在module内部,常被定义为状态机状态、数据位宽和延迟大小,参数的定义是局部的,只在当前模块有效
操作:
parameter N = 8'd5;
parameter P = 4'b0001;
三,程序实现
1,时钟模块:
`timescale 1ns / 1ps /*****************--module name:CLKDIV--********************/ module CLKDIV( clk50,//系统时钟 rst_n,//复位信号 clkout//输出时钟 ); input clk50; input rst_n; output clkout; //寄存器型数据对象的定义 reg clkout; reg [15:0] cnt; //50mhz时钟326分频 always @(posedge clk50 or negedge rst_n) begin if (!rst_n) begin clkout <=1'b0; cnt<=0; end else if(cnt == 16'd162) begin clkout <= 1'b1; cnt <= cnt + 16'd1; end else if(cnt == 16'd325) begin clkout <= 1'b0; cnt <= 16'd0; end else cnt <= cnt + 16'd1; end endmodule
程序对 50Mhz 的系统时钟进行分频, 分频参数 326 计算如下:
这里产生的时钟 clkout 为波特率的 16 倍, 假设数据的波特率为 p,则这里的时钟 clkout的频率为 16*p。以波特率 p 为 9600 为例,系统时钟为 50MHz,则分频系数为50000000/(16*9600) = 325.52,取整为 326。
clkout 时钟取 16 倍波特率的目的为了在 uart 接收的时候对每比特接收的数据有 16 个时钟采样,取中间的采样值,以保证采样不会滑码或误码。
2,串口发送模块:
`timescale 1ns / 1ps module uarttx(clk, rst_n, tx,wrsig,data); input clk;//UART时钟 input rst_n;//系统复位 output tx;//发送数据信号 input wrsig;//发送命令,上升沿有效 input [7:0] data;//需要发送的数据 //寄存器定义 reg idle;//线路状态指示,高为线路忙,低为线路空闲 reg send; reg wrsigbuf; reg wrsigrise; reg presult; reg [7:0] cnt; reg tx; parameter paritymode = 1'b0; //检测上升沿 always @(posedge clk) begin wrsigbuf<=wrsig; wrsigrise<=(~wrsigbuf)&wrsig; end //启动串口发送程序 always @(posedge clk) begin if(wrsigrise&& (~idle))//当发送命令有效且线路为空闲时,启动新的数据发送进程 begin send<=1'b1; end else if(cnt==8'd168)//一帧数据发送结束 begin send<=1'b0; end end //串口发送程序,16个时钟发送一个bit always @(posedge clk or negedge rst_n) begin if(!rst_n) begin tx<=1'b0; idle<=1'b0; cnt<=8'd0; presult<=1'b0; end else if(send==1'b1)begin case(cnt) 8'd0:begin tx<=1'B0; //产生起始位 idle<=1'b1; cnt<=cnt+8'd1; end 8'd16: begin tx <= data[0]; //发送数据 0 位 presult <= data[0]^paritymode; idle <= 1'b1; cnt <= cnt + 8'd1; end 8'd32: begin tx <= data[1]; //发送数据 1 位 presult <= data[1]^presult; idle <= 1'b1; cnt <= cnt + 8'd1; end 8'd48: begin tx <= data[2]; //发送数据 2 位 presult <= data[2]^presult; idle <= 1'b1; cnt <= cnt + 8'd1; end 8'd64: begin tx <= data[3]; //发送数据 3 位 presult <= data[3]^presult; idle <= 1'b1; cnt <= cnt + 8'd1; end 8'd80: begin tx <= data[4]; //发送数据 4 位 presult <= data[4]^presult; idle <= 1'b1; cnt <= cnt + 8'd1; end 8'd96: begin tx <= data[5]; //发送数据 5 位 presult <= data[5]^presult; idle <= 1'b1; cnt <= cnt + 8'd1; end 8'd112: begin tx <= data[6]; //发送数据 6 位 presult <= data[6]^presult; idle <= 1'b1; cnt <= cnt + 8'd1; end 8'd128: begin tx<= data[7]; //发送数据 7 位 presult <= data[7]^presult; idle <= 1'b1; cnt <= cnt + 8'd1; end 8'd144: begin tx <= presult; //发送奇偶校验位 presult <= data[0]^paritymode; idle <= 1'b1; cnt <= cnt + 8'd1; end 8'd160: begin tx <= 1'b1; //发送停止位 idle <= 1'b1; cnt <= cnt + 8'd1; end 8'd168: begin tx <= 1'b1; idle <= 1'b0; //一帧数据发送结束 cnt <= cnt + 8'd1; end default: begin cnt <= cnt + 8'd1; end endcase end else begin tx <= 1'b1; cnt <= 8'd0; idle <= 1'b0; end end endmodule
发送数据过程:空闲状态,线路处于高电位;当收到发送数据指令后,拉低线路一个数据位的时间 T,接着数据按低位到高位依次发送,数据发送完毕后,接着发送奇偶校验位和停止 位(停止位为高电位),一帧数据发送结束。
3,接受模块
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Module name uartrx.v // 说明: 16 个 clock 接收一个 bit, 16 个时钟采样,取中间的采样值 ////////////////////////////////////////////////////////////////////////////////// module uartrx(clk, rst_n, rx, dataout, rdsig, dataerror, frameerror); input clk; //采样时钟 input rst_n; //复位信号 input rx; //UART 数据输入 output dataout; //接收数据输出 output rdsig;//数据接收完成 output dataerror; //数据出错指示 output frameerror; //帧出错指示 reg[7:0] dataout; reg rdsig, dataerror; reg frameerror; reg [7:0] cnt; reg rxbuf, rxfall, receive; parameter paritymode = 1'b0; reg presult, idle; always @(posedge clk) //检测线路的下降沿 begin rxbuf <= rx; rxfall <= rxbuf & (~rx); end /**********************启动串口接收程序********************************/ always @(posedge clk) begin if (rxfall && (~idle)) begin//检测到线路的下降沿并且原先线路为空闲,启动接收数据进程 receive <= 1'b1; end else if(cnt == 8'd168) begin //接收数据完成 receive <= 1'b0; end end /********串口接收程序, 16 个时钟接收一个 bit*********/ always @(posedge clk or negedge rst_n) begin if (!rst_n) begin idle<=1'b0; cnt<=8'd0; rdsig <= 1'b0; frameerror <= 1'b0; dataerror <= 1'b0; presult<=1'b0; end else if(receive == 1'b1) begin case (cnt) 8'd0:begin idle <= 1'b1; cnt <= cnt + 8'd1; rdsig <= 1'b0; end 8'd24:begin //接收第 0 位数据 idle <= 1'b1; dataout[0] <= rx; presult <= paritymode^rx; cnt <= cnt + 8'd1; rdsig <= 1'b0; end 8'd40:begin //接收第 1 位数据 idle <= 1'b1; dataout[1] <= rx; presult <= presult^rx; cnt <= cnt + 8'd1; rdsig <= 1'b0; end 8'd56:begin //接收第 2 位数据 idle <= 1'b1; dataout[2] <= rx; presult <= presult^rx; cnt <= cnt + 8'd1; rdsig <= 1'b0; end 8'd72:begin //接收第 3 位数据 idle <= 1'b1; dataout[3] <= rx; presult <= presult^rx; cnt <= cnt + 8'd1; rdsig <= 1'b0; end 8'd88:begin //接收第 4 位数据 idle <= 1'b1; dataout[4] <= rx; presult <= presult^rx; cnt <= cnt + 8'd1; rdsig <= 1'b0; end 8'd104:begin //接收第 5 位数据 idle <= 1'b1; dataout[5] <= rx; presult <= presult^rx; cnt <= cnt + 8'd1; rdsig <= 1'b0; end 8'd120:begin //接收第 6 位数据 idle <= 1'b1; dataout[6] <= rx; presult <= presult^rx; cnt <= cnt + 8'd1; rdsig <= 1'b0; end 8'd136:begin //接收第 7 位数据 idle <= 1'b1; dataout[7] <= rx; presult <= presult^rx; cnt <= cnt + 8'd1; rdsig <= 1'b1; end 8'd152:begin //接收奇偶校验位 idle <= 1'b1; if(presult == rx) dataerror <= 1'b0; else dataerror <= 1'b1; //如果奇偶校验位不对,表示数据出错 cnt <= cnt + 8'd1; rdsig <= 1'b1; end 8'd168:begin idle <= 1'b1; if(1'b1 == rx) frameerror <= 1'b0; else frameerror <= 1'b1; //如果没有接收到停止位,表示帧出错 cnt <= cnt + 8'd1; rdsig <= 1'b1; end default: begin cnt <= cnt + 8'd1; end endcase end else begin cnt <= 8'd0; idle <= 1'b0; rdsig <= 1'b0; end end endmodule
接收数据过程:空闲状态,线路处于高电位;当检测到线路的下降沿(线路电位由高电位变为低电位)时说明线路有数据传输,按照约定的波特率从低位到高位接收数据,数据接收完毕后,接着接收并比较奇偶校验位是否正确,如果正确则通知后续设备准备接收数据或存入缓存。由于 UART 是异步传输,没有传输同步时钟。为了能保证数据传输的正确性, UART 采用16 倍数据波特率的时钟进行采样。每个数据有 16 个时钟采样,取中间的采样值,以保证采样 不会滑码或误码。一般 UART 一帧的数据位数为 8,这样即使每个数据有一个时钟的误差,接收端也能正确地采样到数据。
4,顶层模块:
`timescale 1ns / 1ps module LED( clk, // 开发板上输入时钟: 50Mhz rst_n, // 开发板上输入复位按键 led,// 输出 LED 灯,用于控制开发板上四个 LED(LED0~LED3) TX, RX ); input clk; input rst_n; output [3:0] led; output TX; input RX; //寄存器型数据对象的定义 reg [31:0] timer; reg [3:0] led; wire uart_clk;//串口使用时钟 wire receiverise_over;//接收完成信号 wire dataerror;//接受数据错误 wire frameerror;//接收帕错误 reg receivebuf,receiverise,send_flag,send; reg [7:0] send_delay; wire [7:0] data; parameter data1=16'h55; //接受完成处理 检测上升沿 always @(posedge uart_clk) begin receivebuf<=receiverise_over; receiverise<=(~receivebuf)&receiverise_over;//接收完成信号上升沿检测 //判断接收是否有问题 if(receiverise==1'b1) send_flag<=1'b1; if((send_flag==1'b1)&&(send_delay<8'd5)) send_delay<=send_delay+1'b1; else if((send_delay>=8'd5)&&(send_delay<8'd10)) begin send<=1'b0; send_delay<=send_delay+1'b1; end else if(send_delay>=8'd10) begin send_flag<=1'b0; send_delay<=1'b0; if((dataerror==1'b0)&&(frameerror==1'b0))//没问题,启动发送 send<=1'b1; end end always @(posedge clk or negedge rst_n) begin if(~rst_n) timer <=0; else if(timer==32'd199_999_999) timer<=0; else timer<=timer+1'b1; end always @(posedge clk or negedge rst_n) begin if(~rst_n) led<=4'b0000; else if(timer==32'd49_999_999) led<=4'b0001; else if(timer==32'd99_999_999) led<=4'b0010; else if(timer==32'd149_999_999) led<=4'b0100; else if(timer==32'd199_999_999) led<=4'b1000; end //例化时钟模块 CLKDIV CLKDIV_LED( .clk50(clk), .rst_n(rst_n), .clkout(uart_clk) ); //例化uartrx模块 uartrx uartrx_LED( .clk(uart_clk), .rst_n(rst_n), .rx(RX), .dataout(data), .rdsig(receiverise_over), .dataerror(dataerror), .frameerror(frameerror) ); //例化uarttx模块 uarttx uarttx_LED( .clk(uart_clk), .rst_n(rst_n), .tx(TX), .wrsig(send), .data(data) ); endmodule
实现接受到数据之后,再将数据发送出去;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?