基于Robei EDA--实现串口通信

2024-02-08 21:04
文章标签 实现 通信 串口 eda robei

本文主要是介绍基于Robei EDA--实现串口通信,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、串口简介

串口作为常用的三大低速总线(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)beginrx_reg1 <= 1'b0;
endelse beginrx_reg1 <= rx;
endalways@(posedge clk or negedge reset)if(!reset)beginrx_reg2 <= 1'b0;
endelse beginrx_reg2 <= rx_reg1;
endalways@(posedge clk or negedge reset)if(!reset)beginrx_reg3 <= 1'b0;
endelse beginrx_reg3 <= rx_reg2;
end//start_negedge
always@(posedge clk or negedge reset)
if(!reset)beginstart_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)beginif(baud_cnt >= baud_cnt_MAX - 1'b1)baud_cnt <= 0;else baud_cnt <= baud_cnt + 1'b1;
end
elsebaud_cnt <= 0;//bit_flag
always@(posedge clk or negedge reset)
if(!reset)bit_flag <= 0;
else if(work_en == 1'b1)beginif(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;
elserx_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;
elsepo_flag <= 1'b0;

仿真文件

 //初始化系统时钟、全局复位和输入信号initial beginclk = 1'b1;reset <= 1'b0;rx <= 1'b1;#20;reset <= 1'b1;
#1000_000_00;
$finish;end//模拟发送8次数据,分别为0~7initial begin#200rx_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) begincase(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个时钟周期endendtask //任务以endtask结束

仿真文件里Verilog语法学习

task 任务名(
//此处写传递到任务里的参数
input xxx,
output xxx,
inout xxx
);endtask //任务以endtask结束

仿真波形

经过仿真,慢慢改正,成功完成。

三、串口发送模块

  • clk:时钟
  • reset:复位信号,低电平有效
  • pi_data:并行数据输入
  • pi_flag:并行数据输入有效信号
  • tx:发送端


波形设计

波形分析一:由于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?​​​​​​​

这篇关于基于Robei EDA--实现串口通信的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/692194

相关文章

MySQL中查找重复值的实现

《MySQL中查找重复值的实现》查找重复值是一项常见需求,比如在数据清理、数据分析、数据质量检查等场景下,我们常常需要找出表中某列或多列的重复值,具有一定的参考价值,感兴趣的可以了解一下... 目录技术背景实现步骤方法一:使用GROUP BY和HAVING子句方法二:仅返回重复值方法三:返回完整记录方法四:

IDEA中新建/切换Git分支的实现步骤

《IDEA中新建/切换Git分支的实现步骤》本文主要介绍了IDEA中新建/切换Git分支的实现步骤,通过菜单创建新分支并选择是否切换,创建后在Git详情或右键Checkout中切换分支,感兴趣的可以了... 前提:项目已被Git托管1、点击上方栏Git->NewBrancjsh...2、输入新的分支的

Python实现对阿里云OSS对象存储的操作详解

《Python实现对阿里云OSS对象存储的操作详解》这篇文章主要为大家详细介绍了Python实现对阿里云OSS对象存储的操作相关知识,包括连接,上传,下载,列举等功能,感兴趣的小伙伴可以了解下... 目录一、直接使用代码二、详细使用1. 环境准备2. 初始化配置3. bucket配置创建4. 文件上传到os

关于集合与数组转换实现方法

《关于集合与数组转换实现方法》:本文主要介绍关于集合与数组转换实现方法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、Arrays.asList()1.1、方法作用1.2、内部实现1.3、修改元素的影响1.4、注意事项2、list.toArray()2.1、方

使用Python实现可恢复式多线程下载器

《使用Python实现可恢复式多线程下载器》在数字时代,大文件下载已成为日常操作,本文将手把手教你用Python打造专业级下载器,实现断点续传,多线程加速,速度限制等功能,感兴趣的小伙伴可以了解下... 目录一、智能续传:从崩溃边缘抢救进度二、多线程加速:榨干网络带宽三、速度控制:做网络的好邻居四、终端交互

java实现docker镜像上传到harbor仓库的方式

《java实现docker镜像上传到harbor仓库的方式》:本文主要介绍java实现docker镜像上传到harbor仓库的方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录1. 前 言2. 编写工具类2.1 引入依赖包2.2 使用当前服务器的docker环境推送镜像2.2

C++20管道运算符的实现示例

《C++20管道运算符的实现示例》本文简要介绍C++20管道运算符的使用与实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录标准库的管道运算符使用自己实现类似的管道运算符我们不打算介绍太多,因为它实际属于c++20最为重要的

Java easyExcel实现导入多sheet的Excel

《JavaeasyExcel实现导入多sheet的Excel》这篇文章主要为大家详细介绍了如何使用JavaeasyExcel实现导入多sheet的Excel,文中的示例代码讲解详细,感兴趣的小伙伴可... 目录1.官网2.Excel样式3.代码1.官网easyExcel官网2.Excel样式3.代码

python实现对数据公钥加密与私钥解密

《python实现对数据公钥加密与私钥解密》这篇文章主要为大家详细介绍了如何使用python实现对数据公钥加密与私钥解密,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录公钥私钥的生成使用公钥加密使用私钥解密公钥私钥的生成这一部分,使用python生成公钥与私钥,然后保存在两个文

浏览器插件cursor实现自动注册、续杯的详细过程

《浏览器插件cursor实现自动注册、续杯的详细过程》Cursor简易注册助手脚本通过自动化邮箱填写和验证码获取流程,大大简化了Cursor的注册过程,它不仅提高了注册效率,还通过友好的用户界面和详细... 目录前言功能概述使用方法安装脚本使用流程邮箱输入页面验证码页面实战演示技术实现核心功能实现1. 随机