FPGA初步学习之串口发送模块【单字节和字符串的发送】

news2024/11/15 19:25:20

串口相关简介

UART 在发送或接收过程中的一帧数据由4部分组成,起始位、数据位、奇偶校验位和停止位,如图所示。其中,起始位标志着一帧数据的开始,停止位标志着一帧数据的结束,数据位是一帧数据中的有效数据。
请添加图片描述

通常用的串口数据帧格式是:8位数据位,无校验位,1位停止位。
所以一帧数据有10个bit:1bit起始位,8bit数据位,1bit停止位。

关于串口波特率

串口波特率是指串口通信的速率,它表示每秒传输二进制数据的位数,单位是bps(位 /秒),常用的波特率有9600、19200、38400、57600以及115200等。

FPGA如何通过系统时钟来得到串口波特率呢?
已知串口时钟为50Mhz = 50000000hz,生成串口波特率为9600。
那么我们需要在FPGA里构建两个计数器。一个系统时钟计数器用来计数系统时钟周期数,一个串口时钟计数器,用来计数对应串口波特率的时钟周期数。
系统时钟:sys_clk,系统时钟计数值:sys_cnt。
串口时钟计数值:tx_cnt。
当sys_cnt = 50000000 / 9600时,tx_cnt计数一次,相当于串口时钟的一个周期。

数据是怎么发送出去的呢?
由上一个问题的解答已经得到了【串口时钟】这么一个东西,那么发送数据都是在该时钟下进行发送。
按照前面所提到的数据格式【1bit起始位,8bit数据位,1bit停止位】,那么就可以知道,发送一个8位的数据,需要在10个串口时钟周期下,将10bit的数据对应好时序一个个发出去。

数据是怎么接收的呢?
类似于上个问题,只需要按照时序,检测起始位,接收数据,再检测停止位即可。

那么这样是否就能够搭建好串口模块呢?
答案明显是不能的,FPGA并不知道什么时候该发数据,什么时候该收数据,什么时候进入发送状态,什么时候脱离发送状态。这时候还要添加其他信号来打辅助。比如添加发送使能信号来确定什么时候发数据,添加状态信号来表示串口是空闲还是忙碌等一系列的状态。

串口发送子模块的搭建

串口发送子模块相关参数

module uart_send(
    input	      sys_clk,                  //系统时钟
    input         sys_rst_n,                //系统复位,低电平有效
    //由其他模块输入
    input         uart_en,                  //发送使能信号
    input  [7:0]  uart_din,                 //待发送数据
    //输出给其他模块
    output        uart_tx_busy,             //发送忙状态标志
    output  reg   tx_flag,                  //发送过程标志信号
    output  reg [ 7:0] tx_data,             //寄存发送数据
    output  reg [ 3:0] tx_cnt,              //发送数据计数器
    output  reg   uart_txd                  //UART发送端口,即tx引脚
    );
    
    //parameter define
	parameter  CLK_FREQ = 50000000;            //系统时钟频率
	parameter  UART_BPS = 9600;                //串口波特率
	
	//为得到指定波特率,对系统时钟计数BPS_CNT次
	localparam  BPS_CNT  = CLK_FREQ/UART_BPS;   
	
	//reg define
	reg        uart_en_d0; 
	reg        uart_en_d1;  
	reg [15:0] clk_cnt;                        //系统时钟计数器
	
	//wire define
	wire       en_flag;

在该子模块中的参数可分为三个部分:

  1. 系统时钟和复位。
  2. 由其它模块发送来的使能信号和待发送的数据。
  3. 本模块正在执行功能时的标志信号,本模块的数据接口,相关计数器,以及UART串口TX的物理IO口。

根据这些参数,可以大致知道串口发送1个bit数据的流程:

  1. 首先模块通过uart_en收到发送使能信号,则首先拉高tx_flag信号,同时uart_tx_busy会接受tx_flag的值。
  2. 当uart_en拉高后,en_flag会获取一个脉冲,当检测到这个脉冲后,则说明串口进入发送状态。
  3. 进入发送过程后要做三件事:寄存待发送的数据,启动系统时钟计数器,启动发送数据计数器。
  4. 发送数据计数器开始计数,直到计数值等于9时会自动清零,计数器一次计数循环即意味着一个8bit的数据发送完。
  5. 在发送过程中,[7:0]tx_data要将8个bit的数据从低位到高位传给[1:0]uart_txd,首先发送一个低电平作为起始位,然后发送tx_data的8个bit,最后发送一个高电平表示停止位。
  6. 经过这么一个过程,一个8位数据就通过FPGA串口的TX引脚发送出去了。

发送子模块中的逻辑块

获取使能信号脉冲

//捕获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
	//计数到停止位结束时,停止发送过程(并提前1/16个串口时钟)
	else if ((tx_cnt == 4'd9) && (clk_cnt == BPS_CNT - (BPS_CNT/16))) 
	    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)                             
        clk_cnt <= 16'd0;                                  
    else if (tx_flag) begin             //如果处于发送过程
        if (clk_cnt < BPS_CNT - 1)
            clk_cnt <= clk_cnt + 1'b1;
        else
            clk_cnt <= 16'd0;           //系统时钟计数一个波特率周期后清零
    end
    else
        clk_cnt <= 16'd0; 				//发送过程结束
end

//进入发送过程后,启动发送数据计数器
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n)                             
        tx_cnt <= 4'd0;
    else if (tx_flag) begin             //处于发送过程
	    if (clk_cnt == BPS_CNT - 1)	    //对系统时钟计数达一个波特率周期
            tx_cnt <= tx_cnt + 1'b1;	//此时发送数据计数器加1
        else
            tx_cnt <= tx_cnt;       
    end
    else                              
        tx_cnt  <= 4'd0;				//发送过程结束
end

将8bit数据按bit拆分,并按顺序赋值给tx端口

//根据发送数据计数器来给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

发送字符串模块

子模块写好后,可以再做一个顶层模块,用来发送字符串。
发送字符串的思路:
当满足某个条件后,使能串口发送信号,此时串口就开始发送数据了,当在串口发送数据的时候,串口就处在发送忙状态,此时我们就可以更新一次待发送的数据。为什么可以在发送的时候更新待发送数据呢?因为在进入发送过程后,数据还没发送前的时间里,待发送的数据就已经寄存在了子模块里的一个寄存器里了。这样就节约了数据更新的时间了。
相关代码如下:


module uart_loopback_top(
    input           sys_clk,            //外部50M时钟
    input           sys_rst_n,          //外部复位信号,低有效

    output          uart_txd            //UART发送端口
    );

//parameter define
parameter  CLK_FREQ = 50000000;         //定义系统时钟频率
parameter  UART_BPS = 115200;           //定义串口波特率
    
//wire define   
reg [7:0] uart_send_data;               //UART发送数据
wire       uart_tx_busy;                //UART发送忙状态标志
reg        uart_send_en;                //UART发送使能
reg [31:0] data_cnt;

reg send_d0;
reg send_d1;
wire send_en;
reg string_end; //停止字符串发送

reg [ 31:0] Data_Count;                 //字符计数器
parameter [31:0] Data_Len=32'd10;      //字符串的长度
reg [7:0] arry [Data_Len-1:0];          //定义要发送的字符串

//初始化字符串
initial begin
    arry[0] = "H";
    arry[1] = "e";
    arry[2] = "l"; 
    arry[3] = "l"; 
    arry[4] = "o"; 
    arry[5] = "W"; 
    arry[6] = "o"; 
    arry[7] = "r"; 
    arry[8] = "l";
    arry[9] = "d";
end

//使能数据更新位,获得串口数据更新的脉冲
assign send_en = (~send_d1) & send_d0;

//数据更新标志,寄存两次uart_send_en的数据,为构成发送使能脉冲做准备
always @(posedge sys_clk or negedge sys_rst_n) begin
    if (!sys_rst_n) begin
        send_d0 <= 1'b0;
        send_d1 <= 1'b0;
    end                                                      
    else begin                                               
        send_d0 <= uart_send_en;
        send_d1 <= send_d0;                        
    end
end

//串口发送字符串
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n) begin
        uart_send_en <= 1'd0;        //初始化串口发送使能
        data_cnt <= 32'd0;           //初始化数据计数器
        uart_send_data <= arry[0];   //初始化首字符
        string_end <= 1'd0;          //控制字符串发送结束
    end                                                      
    else begin
    //当串口处于空闲状态,且数据计数器小于10的时候
    //条件:data_cnt < Data_Len 不要也可以
    //条件必须满足TX空闲,字符串发送结束标志位0,可在此基础上添加其它条件
		if((~uart_tx_busy) && (data_cnt < Data_Len) && (string_end == 1'b0)) begin  
		//使能串口发送使能寄存器
        uart_send_en <= 1'b1;                           
	    end
        
        //当数据计数器大于等于字符串长度时
        if(data_cnt == Data_Len) begin
            string_end <= 1'b1;
            uart_send_en <= 1'b0;   //失能串口发送使能,停止串口发送数据
            data_cnt <= 32'b0;
        end
        
        //如果数据发送使能脉冲到来
        else if((send_en) && (data_cnt < Data_Len)) begin      
            data_cnt <= data_cnt + 32'b1;      //则数据计数器加一
        end
        
        //如果串口有数据在发送,并且结束位没有拉高
        else if((uart_tx_busy) && (string_end == 1'b0)) begin
            //则失能串口发送使能,为下一个数据的发送做准备
            uart_send_en <= 1'b0;                           
            uart_send_data <= arry[data_cnt];
        end
    end
end



//串口发送模块    
uart_send #(                          
    .CLK_FREQ       (CLK_FREQ),         //设置系统时钟频率
    .UART_BPS       (UART_BPS))         //设置串口发送波特率
u_uart_send(                 
    .sys_clk        (sys_clk),
    .sys_rst_n      (sys_rst_n),
     
    .uart_en        (uart_send_en),
    .uart_din       (uart_send_data),
    .uart_tx_busy   (uart_tx_busy),
    .uart_txd       (uart_txd)
    );
    
//例化ILA IP核
//ila_0 your_instance_name (
//	.clk(sys_clk), // input wire clk
//
//	.probe0(uart_send_en), // input wire [0:0]  probe0  
//	.probe1(data_cnt),     // input wire [7:0]  probe1 
//	.probe2(uart_tx_busy),  // input wire [0:0]  probe2
//	.probe3(send_d0),
//	.probe4(send_d1),
//	.probe5(send_en),
//	.probe6(uart_send_data),
//	.probe7(string_end)
//);
    
endmodule

进一步的改进

如果要发送多个字符串,可以在更新字符串数据时加入状态机,进行不同字符串的转换。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/839434.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【贪心算法】leetcode刷题

贪心算法无固定套路。 核心思想&#xff1a;先找局部最优&#xff0c;再扩展到全局最优。 455.分发饼干 两种思路&#xff1a; 1、从大到小。局部最优就是大饼干喂给胃口大的&#xff0c;充分利用饼干尺寸喂饱一个&#xff0c;全局最优就是喂饱尽可能多的小孩。先遍历的胃口&a…

Win11大小写切换图标关闭方法

大家使用Win11操作系统的时候经常会切换大小写键盘&#xff0c;有些游戏本在游戏过程中需要切换大小写&#xff0c;这个时候电脑的屏幕就会出现大小写切换的图标而影响游戏体验&#xff1b; 那么想要关闭Win11电脑上大小写切换图标&#xff0c;又不知道具体怎么操作&#xff0c…

VS Code search tab

Vs Code search 栏的应用 我发现&#xff0c;在vs code种&#xff0c;上面的搜索框的功能非常多。在最初使用vscode时候&#xff0c;以为这只是一个普通的搜索框。后来&#xff0c;发现它可以用于全局搜索文件&#xff0c;比如使用ctrlshiftp。 后来&#xff0c;我发现&#xf…

lifecycleScope Unresolved reference

描述 导入了lifecycle.lifecycleScope&#xff0c;但是在activity中使用lifecycleScope报错出现Unresolved reference找不到引用。 导包 import androidx.lifecycle.lifecycleScope使用 lifecycleScope.launch(Dispatchers.IO) {...}错误 方案 代码中的activity继承Activ…

SSM(Vue3+ElementPlus+Axios+SSM前后端分离)--功能实现【四】

文章目录 SSM--功能实现实现功能06-修改家居信息需求分析/图解思路分析代码实现注意事项和细节 实现功能07-删除家居信息需求分析/图解思路分析代码实现 实现功能08-分页显示列表需求分析/图解思路分析代码实现完成测试分页显示效果 SSM–功能实现 实现功能06-修改家居信息 需…

39.利用matlab寻找素数(matlab程序)

1.简述 MATLAB嵌套循环允许使用一个循环在另一循环内&#xff0c;下面用一个嵌套循环来把所有从1到100的素数显示出来。 2.代码 %% 学习目标&#xff1a;寻找素数 clear sum5; %求0&#xff5e;100素数之和 ss0; %用来标定是否是素数&#xff0c;0表示不是 p…

MongoDB SQL

Microsoft Windows [版本 6.1.7601] 版权所有 (c) 2009 Microsoft Corporation。保留所有权利。C:\Users\Administrator>cd C:\MongoDB\Server\3.4\binC:\MongoDB\Server\3.4\bin> C:\MongoDB\Server\3.4\bin> C:\MongoDB\Server\3.4\bin>net start MongoDB 请求的…

Mac电脑怎么使用“磁盘工具”修复磁盘

我们可以使用“磁盘工具”的“急救”功能来查找和修复磁盘错误。 “磁盘工具”可以查找和修复与 Mac 磁盘的格式及目录结构有关的错误。使用 Mac 时&#xff0c;错误可能会导致意外行为&#xff0c;而重大错误甚至可能会导致 Mac 彻底无法启动。 继续之前&#xff0c;请确保您…

【C# 基础精讲】C# 开发环境搭建(Visual Studio等)

安装C#开发环境是开始学习和使用C#编程的第一步。目前&#xff0c;最常用的C#开发环境是Microsoft Visual Studio&#xff0c;它是一套强大的集成开发环境&#xff08;IDE&#xff09;&#xff0c;提供了丰富的工具和功能&#xff0c;使开发C#应用程序变得更加便捷。以下是安装…

OSLog与NSLog对比

NSLog: NSLog的文档&#xff0c;第一句话就说&#xff1a;Logs an error message to the Apple System Log facility.&#xff0c;所以首先&#xff0c;NSLog就不是设计作为普通的debug log的&#xff0c;而是error log&#xff1b;其次&#xff0c;NSLog也并非是printf的简单…

Redis 6.5 服务端开启多线程源码

redis支持开启多线程&#xff0c;只有从socket到读取缓冲区和从输出缓冲区到socket这两段过程是多线程&#xff0c;而命令的执行还是单线程&#xff0c;并且是由主线程执行 借鉴&#xff1a;【Redis】事件驱动框架源码分析&#xff08;多线程&#xff09; 一、main启动时初始化…

nacos本地搭建+springCloud服务注册中心-nacos(简易实现)

一.nacos概述 nacos官网 二.Windows使用搭建nacos 较为完整使用参考 1.安装nacos 去nacos github下载nacos最新稳定版本&#xff0c;我用的是nacos-server-2.2.3.zip&#xff0c;下载后解压&#xff0c;得到 2.创建数据库 启动mysql&#xff0c;创建数据库nacos&#xff…

Day 72 固定激活函数的BP神经网络 (1. 网络结构理解)

代码&#xff1a; package dl;/*** Back-propagation neural networks. The code comes from*/public class SimpleAnn extends GeneralAnn{/*** The value of each node that changes during the forward process. The first* dimension stands for the layer, and the secon…

[迁移学习]领域泛化

一、概念 相较于领域适应&#xff0c;领域泛化(Domain generalization)最显著的区别在于训练过程中不能访问测试集。 领域泛化的损失函数一般可以描述为以下形式&#xff1a; 该式分为三项&#xff1a;第一项表示各训练集权重的线性组合&#xff0c;其中π为使该项最小的系数&a…

微信云托管(本地调试)⑥:nginx、vue刷新404问题

一、nginx默认路径 1.1、默认配置文件路径&#xff1a;/etc/nginx/nginx.conf 1.2、默认资源路径&#xff1a;/usr/share/nginx/html/index.html 二、修改nginx.conf配置 &#xff08;注意配置中的&#xff1a;include /etc/nginx/conf.d/*.conf; 里面包了一个server配置文件…

Typescript+React入门

初识Typescript 出现背景 Typescript&#xff08;以下简称TS&#xff09;实际上就是JavaScriptType&#xff0c;用数据类型的方式来约束了JS的变量定义 在JS的基础上增加了类型支持 在JS中大多数错误都是因为数据类型造成的&#xff0c;所以TS为了规避这个问题加入了类型限制…

Android性能优化—数据结构优化

优化数据结构是提高Android应用性能的重要一环。在Android开发中&#xff0c;ArrayList、LinkedList和HashMap等常用的数据结构的正确使用对APP性能的提升有着重大的影响。 一、ArrayList ArrayList内部使用的是数组&#xff0c;默认大小10&#xff0c;当数组长度不足时&…

探索OLED透明屏的参数及其在不同领域的应用

OLED透明屏作为一种创新的显示技术&#xff0c;具有高透明度、色彩鲜艳、观感独特等特点&#xff0c;正逐渐成为各个领域的热门选择。 为帮助您更好地了解和选择适合自己需求的OLED透明屏&#xff0c;尼伽便给您详细介绍一下OLED透明屏的参数&#xff0c;包括屏幕尺寸、分辨率…

SQL注入上传文件获取shell

SQL注入写文件的三个必要条件 Web目录有读写权限&#xff1a; 当目标网站的Web目录具有读写权限时&#xff0c;攻击者可以通过注入恶意SQL语句将恶意文件写入服务器上的Web目录。 知道文件的绝对路径&#xff1a; 攻击者需要知道目标系统上的文件的绝对路径&#xff0c;以便将…

Uni-Dock:GPU 分子对接使用教程

github文件下载&#xff1a; git clone https://github.com/dptech-corp/Uni-Dock.git cd Uni-Dock/example/screening_test wget https://github.com/dptech-corp/Uni-Dock/releases/download/1.0.0/unidock 将此文件加入到全局变量中 chmod x unidock sudo mv unidock /…