串口监视系统设计
实验任务
- 任务:基于 STEP-MAX10M08核心板 和 STEP BaseBoard V3.0底板 完成串口监视系统设计并观察调试结果。
- 要求:设计串口监视系统,实时监控串口(UART)接收数据,并将数据显示在底板的8位数码管上(仅限数字0~9)。
- 解析:通过FPGA编程驱动底板上的CP2102串口通信模块,接收来自PC(串口调试助手)或其他串口设备的数据,经过处理,最后通过驱动8位扫描式数码管模块,将接收到的数据显示在底板数码管上。
实验目的
本实验主要学习串口(UART)总线工作原理、协议及相关知识,练习如何使用FPGA驱动CP2102模块实现串口通信设计,同时复习上节中扫描式数码管模块的实例化应用。
- 熟悉串口(UART)总线工作原理及通信协议
- 完成基于FPGA的串口通信模块设计
- 完成串口监视系统设计实现
设计框图
根据前面的实验解析我们可以得知,该设计可以拆分成三个功能模块实现,
- UartBus: UART串口通信设计,实现串口通信数据传输。 * Decoder:将UART模块接收到的数据转换成用于数码管显示的BCD码。 * Segmentscan:通过驱动底板扫描式数码管将串口接收的数据显示出来。
顶层模块DisplayCtl通过实例化两个子模块并将对应的信号连接,最终实现串口监视系统的总体设计。UART通信是全双工的,接收和发送是两个独立的设计,本实验只需要接收数据,串口通信有两个关键因素:传输格式和传输速率,我们可以用两个模块分别实现: * Baud:控制UART通信数据传输速率。 * UartRx:根据数据传输速率节拍控制UART通信数据格式。
实验原理
UART接口介绍
在嵌入式领域里说的串口一般就是说的UART接口,通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称作UART,是一种通用串行数据总线,用于异步通信。该总线双向通信,可以实现全双工传输和接收。
在系统或计算机中说的串口一般就是说的RS232接口,也叫COM口,也叫DB9,老式的电脑和台式机上一般都有这个接口,接口有9个引脚,最重要的三个引脚:TXD、RXD、GND,基本通信逻辑与UART完全一致,为了增加串口通信的抗干扰能力,RS232串行通信接口定义了自己的电平标准,采用负逻辑电平,它定义+5~+12V为低电平,而-12~-5V为高电平,相当于在UART的基础上增加驱动器,将原来UART通信电平标准调整为RS232的电平标准,通信原理如下:
随着技术的发展,各种通信接口种类越来越多,方案越来越稳定,成本越来越低,体积越来越小巧,RS232串口通信接口方案逐渐被抛弃,取而代之的是各种更高速,更稳定,更小巧的接口,USB就是其中应用较广的,为了实现UART通信,一种USB转UART的方案被广泛应用,常用的USB转UART方案有CP2102、FT232、CH340等等
我们STEP BaseBoard V3.0底板集成的UART通信模块就是采用CP2102方案,FPGA通过UART总线驱动CP2102实现USB和UART之间的数据通信,最终实现FPGA与电脑之间的数据传输,UART通信的时序如下。
- 起始位:先发出一个逻辑 0 信号,表示传输字符的开始。
- 数据位:可以是5~8位逻辑 0 或 1 。如ASCII码( 7位) ,扩展BCD码( 8位) 。
- 校验位:数据位加上这一位后,使得 1 的位数应为偶数(偶校验)或奇数(奇校验)。
- 停止位:它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平。
- 空闲位:处于逻辑 1 状态,表示当前线路上没有资料传送。
UART模块连接
STEP BaseBoard V3.0底板上的基于CP2102方案的UART通信模块电路图如下:
上图为基于CP2102方案的UART通信模块电路图,可以看到CP2102方案非常简洁,无需外置USB通信时钟晶体(内部集成),CP2102芯片TXD和RXD分别与FPGA芯片RXD和TXD连接,同时两个信号都连接了LED灯,这样当UART通信时,随着数据传输对应LED灯也会快速闪烁,起到UART通信指示灯的作用。CP2102芯片DTR和RTS通过两个三极管搭建流控电路,连接WIFI模块ESP8266-12F,使用UART模块烧写ESP8266模块的固件时就无需手动进入固件烧写模式了,这个会在后续涉及WIFI通信的实验中详细介绍,这里可以不用理会。
UART驱动实现
SPI、I2C、UART总线对比表:
SPI总线 | I2C总线 | UART总线 |
SS | SCL | TXD |
SCK | SDA | RXD |
MOSI/MISO |
对于SPI总线,通信双方在总线使能的情况下,通过SCK的上升沿或下降沿触发完成总线数据的采样,这样通信双方就可以准确的接收到对方传送的数据了。对于I2C总线,通信接收方通过SCL的高电平触发完成总线数据的采样。综上,SPI总线中的SCK和I2C总线中的SCL在通信中起到时钟的作用,接收方都是根据时钟的对应状态采样数据,最终保证通信能够正常进行。
对于UART总线,TXD和RXD分别用于发送和接收数据,相当于两根独立工作的单线总线,没有了时钟线的配合,那么接收端应该怎样获取发送端传输的数据呢?其实也是有方法的,那就是通信双方需要约定好UART总线数据传输的通信速率和时序格式。
通信速率
UART的数据传输速度用波特率来描述,也就是UART每秒接收或发送的数据位。例如9600波特率表示每秒钟发送或接收9600比特的数据,即发送端需要将发送的每个数据位保持对应的时间,计算如下:
- 1s / 9600 = 1000000us / 9600 = 104.17us
小脚丫硬件上使用12MHz的时钟晶振,如果以12MHz时钟信号作为系统时钟,使用计数器延时完成UART通信数据采样,那么计数器延时计数终值计算如下:
- 12M / 9600 = 1250
因为波特率是协议里约定的,为保证协议的通用性和灵活性,波特率参数有固定的选项,不可以随意设置(如果UART通信双方都是自己编程的,可以根据自己的要求定义自己需要的波特率,这种情况除外),波特率参数选项很多,大家可以打开串口调试助手工具找到波特率配置列表查看,我们比较常用的波特率值有以下几种:
UART常用波特率:
1200 | 4800 | 9600 | 38400 | 115200 |
时序格式
关于时序格式在前面UART接口介绍部分也简单说了一下,通信过程中时序依次为:起始位、数据位、校验位、停止位、空闲位,其中数据位可以是5~8位,本设计我们使用8位数据,校验位可以省略,最后确定的时序格式如下:
前面所说的通信速率和时序格式其实就是UART通信中的两个重要的参数,需要传输的数据根据通信速率的节拍按照UART的时序格式输出,就可以实现UART通信了,可以按照下面三个步骤实现。
- 将需要发送的数据与起始位和停止位组成10bit位宽的数据
- 计数器计数延迟产生相应波特率需要的时序节拍
- 数据按照(起始位—bit0~bit7—停止位)的时序串行输出
例如,将8‘h73和8’h5a通过UART发送的时序,红色箭头为波特率对应的节拍点
对于UART发送数据来说,波特率节拍是自己产生的,数据是自己主动发出的,逻辑相对简单,而当UART接收数据的时候,因为不确定对方什么时候发送数据,所以需要对RX信号持续检测,当检测到有数据传送时,根据约定的波特率节拍采样,可以按照下面三个步骤实现。
- 检测UART的RXD信号的下降沿(自锁,完成接收后再解锁继续检测)
- 接收采样时,采样点应该在计数器的中值点进行
- 将采样后的数据按照UART时序的要求重新组成8bit的数据
例如,当UART的RX端接收到数据8‘h73和8’h5a的时候,红色箭头为检测到数据传输的点,绿色箭头为对应的采样节拍点(采样点在数据中间最是稳定)。
通过以上理论,我们了解了UART发送和接收数据的整个流程,两个过程中我们都需要波特率节拍,那么我们就可以设计一个节拍模块Baud,这样我们的发送和接收都可以实例化节拍模块用于产生对应波特率的节拍信号。
节拍模块Baud设计实现:
节拍模块Baud的端口程序实现如下:
module Baud # ( parameter BPS_PARA = 1250 //12MHz时钟时参数1250对应9600的波特率 ) ( input clk, //系统时钟 input rst_n, //系统复位,低有效 input bps_en, //接收或发送时钟使能 output reg bps_clk //接收或发送时钟输出 );
设计一个计数器用于分频产生对应波特率节拍信号,因为UART随时可能接收数据,所以节拍模块必须随时待命,保持计数器清零,当需要节拍信号时精准地输出。
计数器设计程序实现如下:
reg [12:0] cnt; //计数器计数满足波特率时钟要求 always @ (posedge clk or negedge rst_n) begin if(!rst_n) cnt <= 1'b0; else if((cnt >= BPS_PARA-1)||(!bps_en)) //当时钟信号不使能(bps_en为低电平)时,计数器清零并停止计数 cnt <= 1'b0; //当时钟信号使能时,计数器对系统时钟计数,周期为BPS_PARA个系统时钟周期 else cnt <= cnt + 1'b1; end
当bpsen(高有效)使能,计数器计数周期由参数BPSPARA来决定,前面数据接收时序部分了解到,从RX检测到下降沿开始计数器工作,到数据采样点需要半个节拍的时间,而数据发送时只要保证相邻两个节拍点之间的时间为一个计数器周期即可,所以我们可以在计数器计数到中值时产生一个脉冲信号充当节拍信号。
节拍信号产生程序实现如下:
//产生相应波特率的时钟节拍,接收模块将以此节拍进行UART数据接收 always @ (posedge clk or negedge rst_n) begin if(!rst_n) bps_clk <= 1'b0; else if(cnt == (BPS_PARA>>1)) //右移一位等于除以2,终值BPS_PARA为数据更替点,中值数据稳定,做采样点 bps_clk <= 1'b1; else bps_clk <= 1'b0; end
发送模块Uart_Tx设计实现:
前级电路通过txdatavalid和txdatain将需要发送的数据传输进来,当txdatavalid有脉冲信号时,txdatain信号为有效数据,拼接起始位和停止位后赋值给txdatar,同时控制节拍使能信号使能并自锁,然后等发送完10bit数据后解除使能。
数据发送控制程序实现如下:
output reg bps_en; //发送时钟使能 input bps_clk; //发送时钟输入 input tx_data_valid; //发送数据有效脉冲 input [7:0] tx_data_in; //要发送的数据 output reg uart_tx; //UART发送输出 reg [3:0] num; reg [9:0] tx_data_r; //融合了起始位和停止位的数据 //驱动发送数据操作 always @ (posedge clk or negedge rst_n) begin if(!rst_n) begin bps_en <= 1'b0; tx_data_r <= 10'd0; end else if(tx_data_valid && (!bps_en))begin bps_en <= 1'b1; //当检测到接收时钟使能信号的下降沿,表明接收完成,需要发送数据,使能发送时钟使能信号 tx_data_r <= {1'b1,tx_data_in,1'b0}; end else if(num==4'd10) begin bps_en <= 1'b0; //一次UART发送需要10个时钟信号,然后结束 end end
UART数据发送时序程序实现如下:
//当处于工作状态中时,按照发送时钟的节拍发送数据 always @ (posedge clk or negedge rst_n) begin if(!rst_n) begin num <= 1'b0; uart_tx <= 1'b1; end else if(bps_en) begin if(bps_clk) begin num <= num + 1'b1; uart_tx <= tx_data_r[num]; end else if(num>=4'd10) num <= 4'd0; end end
将节拍模块Baud和发送模块Uart_tx实例化并连接,完成发送功能的设计,如下
接收模块Uart_Rx设计实现:
首先对RX信号多级缓存消除亚稳态,同时检测下降沿,程序实现如下:
input uart_rx; //UART接收输入 reg uart_rx0,uart_rx1,uart_rx2; //多级延时锁存去除亚稳态 always @ (posedge clk) begin uart_rx0 <= uart_rx; uart_rx1 <= uart_rx0; uart_rx2 <= uart_rx1; end //检测UART接收输入信号的下降沿 wire neg_uart_rx = uart_rx2 & ~uart_rx1;
当检测RX有下降沿后,使能节拍使能信号,同时自锁直到完成接收操作后再复位节拍使能信号。程序实现如下:
reg [3:0] num; //接收时钟使能信号的控制 always @ (posedge clk or negedge rst_n) begin if(!rst_n) bps_en <= 1'b0; else if(neg_uart_rx && (!bps_en)) //当空闲状态(bps_en为低电平)时检测到UART接收信号下降沿,进入工作状态(bps_en为高电平),控制时钟模块产生接收时钟 bps_en <= 1'b1; else if(num==4'd9) //当完成一次UART接收操作后,退出工作状态,恢复空闲状态 bps_en <= 1'b0; end
根据节拍信号完成UART总线的数据采样,得到8位有效数据,程序实现如下:
reg [7:0] rx_data; //当处于工作状态中时,按照接收时钟的节拍获取数据 always @ (posedge clk or negedge rst_n) begin if(!rst_n) begin num <= 4'd0; rx_data <= 8'd0; end else if(bps_en) begin if(bps_clk) begin num <= num + 1'b1; if(num<=4'd8) rx_data[num-1] <= uart_rx1; //先接受低位再接收高位,8位有效数据 end else if(num == 4'd9) begin //完成一次UART接收操作后,将获取的数据输出 num <= 4'd0; end end else begin num <= 4'd0; end end
当UART接收操作完成后,将得到的8位有效数据输出给后级电路,程序实现如下:
//将接收的数据输出,同时控制输出有效信号产生脉冲 always @ (posedge clk or negedge rst_n) begin if(!rst_n) begin rx_data_out <= 8'd0; rx_data_valid <= 1'b0; end else if(num == 4'd9) begin rx_data_out <= rx_data; rx_data_valid <= 1'b1; end else begin rx_data_out <= rx_data_out; rx_data_valid <= 1'b0; end end
最后将节拍模块Baud和接收模块Uart_rx实例化并连接,完成发送功能的设计,如下
整个UART驱动设计是由两个独立的功能组合而成:发送功能部分和接收功能部分。UART功能总体设计框图如下:
当我们需要UART发送数据的时候只需要实例化发送功能部分设计,需要UART接收数据的时候只需要实例化接收功能部分设计,例如本设计中FPGA驱动UART模块接收电脑串口调试助手发出的数据,所以我们就只需要实例化接收功能部分设计即可。
系统总体实现
刚刚学习了UART通信模块,本设计只需要使用接收功能部分设计,每一次通信都会得到一个8位数据,怎样将8位数据对应得数据显示在数码管上呢?我来先来了解一下UART接受到的8位数据与要显示数字的关系
上图为电脑端友善串口调试助手的界面,当我们将硬件连接,在串口设置串口选定串口对应的端口,并按上图配置波特率、数据位、校验位、停止位、流控等,点击开始建立连接,接下来我们就可以在串口发送窗口输入要发送的数据,点击发送后数据传输出去。在发送设置有两个选项:ASCII和Hex ,
- 当选择ASCII的时候,通过UART发出的数据是数据窗口中字符的ASCII码值,每个字符的ASCII码值都是8位数据,所以窗口中字符数量与UART传输的次数是相等的,同时数字的值与ASCII码值相差48,例如数字0的ASCII码值为48。
- 当选择Hex的时候,通过UART发出的数据(必须是16进制数据)就是数据窗口中的数据本身,这样每次UART传输都会发送两个数字,如果只发送一个数字,则高位补零组成8位数据,例如发送数字1,实际UART传输的数据为8‘h01。
我们设计一个32位的移位寄存器对应8位数码管,按照BCD码格式每4位表示一个数字,每次接收到UART数据都存到移位寄存器中,同时控制数码管显示相应的数码管位,Decoder程序实现如下:
`ifdef HEX_FORMAT //如果用define定义过HEX_FORMAT //采用16进制格式,接收到的数据等于数值本身 wire [7:0] seg_data_r = rx_data_out; //移位寄存器,对应8位数码管数据BCD码 always @ (posedge rx_data_valid or negedge rst_n) begin if(!rst_n) seg_data <= 1'b0; else seg_data <= {seg_data[23:0],seg_data_r}; end //移位寄存器,对应8位数码管数据显示使能 always @ (posedge rx_data_valid or negedge rst_n) begin if(!rst_n) data_en <= 1'b0; else data_en <= {data_en[5:0],2'b11}; end `else //采用字符格式,接收到的数据为字符ASCII码值,与数字值相差48 wire [7:0] seg_data_r = rx_data_out - 8'd48; //移位寄存器,对应8位数码管数据BCD码 always @ (posedge rx_data_valid or negedge rst_n) begin if(!rst_n) seg_data <= 1'b0; else seg_data <= {seg_data[27:0],seg_data_r[3:0]}; end //移位寄存器,对应8位数码管数据显示使能 always @ (posedge rx_data_valid or negedge rst_n) begin if(!rst_n) data_en <= 1'b0; else data_en <= {data_en[6:0],1'b1}; end `endif
上面程序中ifdef……
else……endif语句为预编译指令,与C预演类似。如果我们使用串口助手Hex(16进制)格式发送数据,需要在程序中使用define定义参数HEX_FORMAT,如果使用ASCII格式发送数据,则不需要定义。
<code verilog>
define HEXFORMAT 串口助手使用Hex格式发送时定义HEX_FORMAT,否则不定义
</code>
综合后的设计框图如下:
#### 实验步骤
- 双击打开Quartus Prime工具软件;
- 新建工程:File → New Project Wizard(工程命名,工程目录选择,设备型号选择,EDA工具选择);
- 新建文件:File → New → Verilog HDL File,键入设计代码并保存;
- 设计综合:双击Tasks窗口页面下的Analysis & Synthesis对代码进行综合;
- 管脚约束:Assignments → Assignment Editor,根据项目需求分配管脚;
- 设计编译:双击Tasks窗口页面下的Compile Design对设计进行整体编译并生成配置文件;
- 程序烧录:点击Tools → Programmer打开配置工具,Program进行下载;
- 观察设计运行结果。
#### 实验现象
使用两根Micro-USB线同时连接核心板和底板的USB接口,将程序下载到FPGA中,数码管处于不显示的状态,打开电脑上的串口调试助手,按照前面图片配置相应参数,在数据发送窗口输入数字,点击发送观察底板数码管的变化,重新输入数字,点击发送再次观察底板数码管的变化。