probedog 发表于 2022-9-23 09:53

STM32MP1嵌入式Linux驱动开发指南V1.7——UART串口通信实验

UART串口通信实验
串口是“串行接口”的简称,即采用串行通信方式的接口。串行通信将数据字节分成一位一位的形式在一条数据线上逐个传送,其特点是通信线路简单,但传输速度较慢。因此串口广泛应用于嵌入式、工业控制等领域中对数据传输速度要求不高的场合。本章我们将使用ZYNQ开发板上的UART串口完成上位机与ZYNQ PL的通信。

probedog 发表于 2022-9-23 09:54

16.1 UART串口简介
串行通信分为两种方式:同步串行通信和异步串行通信。同步串行通信需要通信双方在同一时钟的控制下,同步传输数据;异步串行通信是指通信双方使用各自的时钟控制数据的发送和接收过程。
UART是一种采用异步串行通信方式的通用异步收发传输器(universal asynchronous receiver-transmitter),它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据。
UART串口通信需要两根信号线来实现,一根用于串口发送,另外一根负责串口接收。UART在发送或接收过程中的一帧数据由4部分组成,起始位、数据位、奇偶校验位和停止位,如图 7.5.13.1所示。其中,起始位标志着一帧数据的开始,停止位标志着一帧数据的结束,数据位是一帧数据中的有效数据。校验位分为奇校验和偶校验,用于检验数据在传输过程中是否出错。奇校验时,发送方应使数据位中1的个数与校验位中1的个数之和为奇数;接收方在接收数据时,对1的个数进行检查,若不为奇数,则说明数据在传输过程中出了差错。同样,偶校验则检查1的个数是否为偶数。



图 7.5.13.1 异步串行通信数据格式

probedog 发表于 2022-9-23 09:56

UART通信过程中的数据格式及传输速率是可设置的,为了正确的通信,收发双方应约定并遵循同样的设置。数据位可选择为5、6、7、8位,其中8位数据位是最常用的,在实际应用中一般都选择8位数据位;校验位可选择奇校验、偶校验或者无校验位;停止位可选择1位(默认),1.5或2位。串口通信的速率用波特率表示,它表示每秒传输二进制数据的位数,单位是bps(位/秒),常用的波特率有9600、19200、38400、57600以及115200等。
在设置好数据格式及传输速率之后,UART负责完成数据的串并转换,而信号的传输则由外部驱动电路实现。电信号的传输过程有着不同的电平标准和接口规范,针对异步串行通信的接口标准有RS232、RS422、RS485等,它们定义了接口不同的电气特性,如RS-232是单端输入输出,而RS-422/485为差分输入输出等。
RS232接口标准出现较早,可实现全双工工作方式,即数据发送和接收可以同时进行。在传输距离较短时(不超过15m),RS232是串行通信最常用的接口标准,本章主要介绍针对RS-232标准的UART串口通信。
RS-232标准的串口最常见的接口类型为DB9,样式如图 7.5.13.2所示,工业控制领域中用到的工控机一般都配备多个串口,很多老式台式机也都配有串口。但是笔记本电脑以及较新一点的台式机都没有串口,它们一般通过USB转串口线(图 7.5.13.3)来实现与外部设备的串口通信。



图 7.5.13.2 DB9接口



图 7.5.13.3 USB串口线
DB9接口定义以及各引脚功能说明如图 7.5.13.4所示,我们一般只用到其中的2(RXD)、3(TXD)、5(GND)引脚,其他引脚在普通串口模式下一般不使用,如果大家想了解,可以自行百度下。


图 7.5.13.4 DB9接口定义

probedog 发表于 2022-9-23 09:57

16.2 实验任务
本节实验任务是上位机通过串口调试助手发送数据给Zynq,Zynq PL端通过RS232串口接收数据并将接收到的数据发送给上位机,完成串口数据环回。

16.3 硬件设计
领航者ZYNQ开发板上RS232串口部分的原理图如下所示:



图 7.5.13.1 领航者RS232串口原理图
由于ZYNQ PL端串口输入输出引脚为TTL电平,用3.3V代表逻辑“1”,0V代表逻辑“0”;而计算机串口采用RS-232电平,它是负逻辑电平,即-15V-5V代表逻辑“1”,+5V+15V代表逻辑“0”。因此当计算机与ZYNQ通信时,需要加电平转换芯片SP3232,实现RS232电平与TTL电平的转换。
从图 7.5.13.1中可以看到,SP3232芯片端口的U2_RX和U2_TX并没有直接和ZYNQ的引脚相连接,而是连接到了P1排针上。RS232串口和RS485串口共用P1排针的UART2_TX和UART2_RX,而UART2_TX和UART2_RX是直接和ZYNQ的引脚相连接的。在使用时,使用跳线帽选择与ZYNQ相连接的串口类型,这样的设计方式实现了有限IO的复用。因此,在做RS232的通信实验时,需要使用杜邦线或者跳线帽将U2_RX和UART2_TX连接在一起,U2_TX和UART2_RX连接在一起。
本实验中,系统时钟、按键复位以及串口的接收、发送端口的管脚分配如下表所示:
表 16.3.1 串口通信实验管脚分配
信号名 方向 管脚 端口说明 电平标准
sys_clk input U18 系统时钟,50M LVCMOS33
sys_rst_n input N16 系统复位,低有效 LVCMOS33
uart_rxd input K14 串口接收 LVCMOS33
uart_txd output M15 串口发送 LVCMOS33

probedog 发表于 2022-9-23 09:58

对应的XDC约束语句如下所示:


[*]create_clock -period 20.000 -name clk
[*]set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33}
[*]set_property -dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33}
[*]set_property -dict {PACKAGE_PIN K14 IOSTANDARD LVCMOS33}
[*]set_property -dict {PACKAGE_PIN M15 IOSTANDARD LVCMOS33}

复制代码

16.4 程序设计
根据实验任务,我们不难想象本系统应该有一个串口接收模块,用来接收上位机发送的数据;还要有一个串口发送模块,用于将数据发回上位机;另外还应该有一个对数据进行环回控制的模块,它负责把从串口接收模块接收到的数据送给串口发送模块,以实现串口数据的环回。由此可以画出本次实验的系统框图,如下所示:



图 7.5.13.1 系统框图

probedog 发表于 2022-9-23 09:58

由系统总体框图可知,ZYNQ PL部分包括四个模块,顶层模块、接收模块、发送模块和数据环回模块。其中在顶层模块中完成对另外三个模块的例化,顶层模块原理图如下所示:



图 7.5.13.2 顶层模块原理图
在图 7.5.13.2中,uart_recv为串口接收模块,从串口接收端口uart_rxd来接收上位机发送的串行数据,并在一帧数据接收结束后给出通知信号uart_done。
uart_send为串口发送模块,以uart_en为发送使能信号。uart_en的上升沿将启动一次串口发送过程,将uart_din接口上的数据通过串口发送端口uart_txd发送出去。
uart_loop模块负责完成串口数据的环回功能。它在uart_recv模块接收完成后,将接收到的串口数据发送到uart_send模块,并通过send_en接口给出一个上升沿,以启动发送过程。
在编写代码之前,我们首先要确定串口通信的数据格式及波特率。在这里我们选择串口比较常用的一种模式,数据位为8位,停止位为1位,无校验位,波特率为115200bps。则传输一帧数据的时序图如下图所示:



图 7.5.13.3 串口通信时序图

probedog 发表于 2022-9-23 10:00

顶层模块的代码如下:


[*]1module uart_loopback_top(
[*]2      input         sys_clk,            //外部50M时钟
[*]3      input         sys_rst_n,          //外部复位信号,低有效
[*]4
[*]5      input         uart_rxd,         //UART接收端口
[*]6      output          uart_txd            //UART发送端口
[*]7      );
[*]8
[*]9//parameter define
[*]10 parameterCLK_FREQ = 50000000;         //定义系统时钟频率
[*]11 parameterUART_BPS = 115200;         //定义串口波特率
[*]12
[*]13 //wire define
[*]14 wire       uart_recv_done;            //UART接收完成
[*]15 wire uart_recv_data;            //UART接收数据
[*]16 wire       uart_send_en;                //UART发送使能
[*]17 wire uart_send_data;            //UART发送数据
[*]18 wire       uart_tx_busy;                //UART发送忙状态标志
[*]19
[*]20 //*****************************************************
[*]21 //**                  main code
[*]22 //*****************************************************
[*]23
[*]24 //串口接收模块
[*]25 uart_recv #(
[*]26   .CLK_FREQ       (CLK_FREQ),         //设置系统时钟频率
[*]27   .UART_BPS       (UART_BPS))         //设置串口接收波特率
[*]28 u_uart_recv(
[*]29   .sys_clk      (sys_clk),
[*]30   .sys_rst_n      (sys_rst_n),
[*]31
[*]32   .uart_rxd       (uart_rxd),
[*]33   .uart_done      (uart_recv_done),
[*]34   .uart_data      (uart_recv_data)
[*]35   );
[*]36
[*]37 //串口发送模块
[*]38 uart_send #(
[*]39   .CLK_FREQ       (CLK_FREQ),         //设置系统时钟频率
[*]40   .UART_BPS       (UART_BPS))         //设置串口发送波特率
[*]41 u_uart_send(
[*]42   .sys_clk      (sys_clk),
[*]43   .sys_rst_n      (sys_rst_n),
[*]44
[*]45   .uart_en      (uart_send_en),
[*]46   .uart_din       (uart_send_data),
[*]47   .uart_tx_busy   (uart_tx_busy),
[*]48   .uart_txd       (uart_txd)
[*]49   );
[*]50
[*]51 //串口环回模块
[*]52 uart_loop u_uart_loop(
[*]53   .sys_clk      (sys_clk),
[*]54   .sys_rst_n      (sys_rst_n),
[*]55
[*]56   .recv_done      (uart_recv_done),   //接收一帧数据完成标志信号
[*]57   .recv_data      (uart_recv_data),   //接收的数据
[*]58
[*]59   .tx_busy      (uart_tx_busy),   //发送忙状态标志
[*]60   .send_en      (uart_send_en),   //发送使能信号
[*]61   .send_data      (uart_send_data)    //待发送数据
[*]62   );
[*]63
[*]64 endmodule

复制代码

probedog 发表于 2022-9-23 10:01

在顶层模块中完成了对其余各个子模块的例化。需要注意的是,顶层模块中第10、11行定义了两个变量:系统时钟频率CLK_FREQ与串口波特率UART_BPS,使用时可以根据不同的系统时钟频率以及所需要的串口波特率设置这两个变量。我们可以尝试将串口波特率UART_BPS设置为其他值(如9600),在模块例化时会将这个变量传递到串口接收与发送模块中,从而实现不同速率的串口通信。
串口接收模块的代码如下所示:


[*]1   module uart_recv(
[*]2       input             sys_clk,                  //系统时钟
[*]3       input             sys_rst_n,                //系统复位,低电平有效
[*]4
[*]5       input             uart_rxd,               //UART接收端口
[*]6       outputreg       uart_done,                //接收一帧数据完成标志
[*]7       outputreg uart_data               //接收的数据
[*]8       );
[*]9
[*]10//parameter define
[*]11parameterCLK_FREQ = 50000000;                //系统时钟频率
[*]12parameterUART_BPS = 9600;                  //串口波特率
[*]13localparamBPS_CNT= CLK_FREQ/UART_BPS;       //为得到指定波特率,
[*]14                                                //需要对系统时钟计数BPS_CNT次
[*]15//reg define
[*]16reg      uart_rxd_d0;
[*]17reg      uart_rxd_d1;
[*]18reg clk_cnt;                           //系统时钟计数器
[*]19reg [ 3:0] rx_cnt;                              //接收数据计数器
[*]20reg      rx_flag;                           //接收过程标志信号
[*]21reg [ 7:0] rxdata;                              //接收数据寄存器
[*]22
[*]23//wire define
[*]24wire       start_flag;
[*]25
[*]26//*****************************************************
[*]27//**                  main code
[*]28//*****************************************************
[*]29//捕获接收端口下降沿(起始位),得到一个时钟周期的脉冲信号
[*]30assignstart_flag = uart_rxd_d1 & (~uart_rxd_d0);
[*]31
[*]32//对UART接收端口的数据延迟两个时钟周期
[*]33always @(posedge sys_clk or negedge sys_rst_n) begin
[*]34      if (!sys_rst_n) begin
[*]35          uart_rxd_d0 <= 1'b0;
[*]36          uart_rxd_d1 <= 1'b0;
[*]37      end
[*]38      else begin
[*]39          uart_rxd_d0<= uart_rxd;
[*]40          uart_rxd_d1<= uart_rxd_d0;
[*]41      end
[*]42end
[*]43
[*]44//当脉冲信号start_flag到达时,进入接收过程
[*]45always @(posedge sys_clk or negedge sys_rst_n) begin
[*]46      if (!sys_rst_n)
[*]47          rx_flag <= 1'b0;
[*]48      else begin
[*]49          if(start_flag)                        //检测到起始位
[*]50            rx_flag <= 1'b1;                  //进入接收过程,标志位rx_flag拉高
[*]51                                                //计数到停止位中间时,停止接收过程
[*]52          else if((rx_cnt == 4'd9) && (clk_cnt == BPS_CNT/2))
[*]53            rx_flag <= 1'b0;                  //接收过程结束,标志位rx_flag拉低
[*]54          else
[*]55            rx_flag <= rx_flag;
[*]56      end
[*]57end
[*]58
[*]59//进入接收过程后,启动系统时钟计数器
[*]60always @(posedge sys_clk or negedge sys_rst_n) begin
[*]61      if (!sys_rst_n)
[*]62          clk_cnt <= 16'd0;
[*]63      else if ( rx_flag ) begin                   //处于接收过程
[*]64          if (clk_cnt < BPS_CNT - 1)
[*]65            clk_cnt <= clk_cnt + 1'b1;
[*]66          else
[*]67            clk_cnt <= 16'd0;                   //对系统时钟计数达一个波特率周期后清零
[*]68      end
[*]69      else
[*]70          clk_cnt <= 16'd0;                     //接收过程结束,计数器清零
[*]71end
[*]72
[*]73//进入接收过程后,启动接收数据计数器
[*]74always @(posedge sys_clk or negedge sys_rst_n) begin
[*]75      if (!sys_rst_n)
[*]76          rx_cnt<= 4'd0;
[*]77      else if ( rx_flag ) begin                   //处于接收过程
[*]78          if (clk_cnt == BPS_CNT - 1)             //对系统时钟计数达一个波特率周期
[*]79            rx_cnt <= rx_cnt + 1'b1;            //此时接收数据计数器加1
[*]80          else
[*]81            rx_cnt <= rx_cnt;
[*]82      end
[*]83       else
[*]84          rx_cnt<= 4'd0;                        //接收过程结束,计数器清零
[*]85end
[*]86
[*]87//根据接收数据计数器来寄存uart接收端口数据
[*]88always @(posedge sys_clk or negedge sys_rst_n) begin
[*]89      if ( !sys_rst_n)
[*]90          rxdata <= 8'd0;
[*]91      else if(rx_flag)                            //系统处于接收过程
[*]92          if (clk_cnt == BPS_CNT/2) begin         //判断系统时钟计数器计数到数据位中间
[*]93            case ( rx_cnt )
[*]94               4'd1 : rxdata <= uart_rxd_d1;   //寄存数据位最低位
[*]95               4'd2 : rxdata <= uart_rxd_d1;
[*]96               4'd3 : rxdata <= uart_rxd_d1;
[*]97               4'd4 : rxdata <= uart_rxd_d1;
[*]98               4'd5 : rxdata <= uart_rxd_d1;
[*]99               4'd6 : rxdata <= uart_rxd_d1;
[*]100            4'd7 : rxdata <= uart_rxd_d1;
[*]101            4'd8 : rxdata <= uart_rxd_d1;   //寄存数据位最高位
[*]102            default:;
[*]103             endcase
[*]104         end
[*]105         else
[*]106             rxdata <= rxdata;
[*]107   else
[*]108         rxdata <= 8'd0;
[*]109 end
[*]110
[*]111 //数据接收完毕后给出标志信号并寄存输出接收到的数据
[*]112 always @(posedge sys_clk or negedge sys_rst_n) begin
[*]113   if (!sys_rst_n) begin
[*]114         uart_data <= 8'd0;
[*]115         uart_done <= 1'b0;
[*]116   end
[*]117   else if(rx_cnt == 4'd9) begin               //接收数据计数器计数到停止位时
[*]118         uart_data <= rxdata;                  //寄存输出接收到的数据
[*]119         uart_done <= 1'b1;                      //并将接收完成标志位拉高
[*]120   end
[*]121   else begin
[*]122         uart_data <= 8'd0;
[*]123         uart_done <= 1'b0;
[*]124   end
[*]125 end
[*]126
[*]127 endmodule

复制代码

probedog 发表于 2022-9-23 10:07

串口接收模块程序中29至42行是一个经典的边沿检测电路,通过检测串口接收端uart_rxd的下降沿来捕获起始位。一旦检测到起始位,输出一个时钟周期的脉冲start_flag,并进入串口接收过程。串口接收状态用rx_flag来标志,rx_flag为高标志着串口接收过程正在进行,此时启动系统时钟计数器clk_cnt与接收数据计数器rx_cnt。
由第13行的公式BPS_CNT = CLK_FREQ/UART_BPS可知,BPS_CNT为当前波特率下,串口传输一位所需要的系统时钟周期数。因此clk_cnt从零计数到BPS_CN-1时,串口刚好完成一位数据的传输。由于接收数据计数器rx_cnt在每次clk_cnt计数到BPS_CN-1时加1,因此由rx_cnt的值可以判断串口当前传输的是第几位数据。第87行至第109行就是根据clk_cnt的值将uart接收端口的数据寄存到接收数据寄存器对应的位,从而实现接收数据的串并转换。其中第92行选择clk_cnt计数至BPS_CNT/2时寄存接收端口数据,是因为计数到数据中间时的采样结果最稳定。
程序中需要额外注意的地方是串口接收过程结束条件的判定,由第52行可知,在计数到停止位中间时,标志位rx_flag就已经拉低。这样做是因为虽然此时一帧数据传输还没有完成(停止位只传送到一半),但是数据位已经寄存完毕。而在连续接收数据时,提前半个波特率周期结束接收过程可以为检测下一帧数据的起始位留出充足的时间。
我们使用上位机通过串口向开发板发送16进制数55,在串口接收过程中ILA抓取的波形图如下所示:



图 7.5.13.4 串口接收过程波形图
图 7.5.13.4中红色的触发线标识出了串口接收端uart_rxd的起始位,在整个接收过程中rx_flag保持为高电平,同时rx_cnt对串口数据进行计数。当rx_cnt计数到9时,串口数据接收完成,uart_done拉高,同时uart_data给出接收到的数据。从图中可以看到,接收模块能够正确接收串口数据并完成串并转换。

probedog 发表于 2022-9-23 10:09

串口发送模块代码如下所示:


[*]1   module uart_send(
[*]2       input         sys_clk,                  //系统时钟
[*]3       input         sys_rst_n,                //系统复位,低电平有效
[*]4
[*]5       input         uart_en,                  //发送使能信号
[*]6       inputuart_din,               //待发送数据
[*]7       output      uart_tx_busy,             //发送忙状态标志
[*]8       outputreg   uart_txd                  //UART发送端口
[*]9       );
[*]10
[*]11//parameter define
[*]12parameterCLK_FREQ = 50000000;            //系统时钟频率
[*]13parameterUART_BPS = 9600;                //串口波特率
[*]14localparamBPS_CNT= CLK_FREQ/UART_BPS;   //为得到指定波特率,对系统时钟计数BPS_CNT次
[*]15
[*]16//reg define
[*]17reg      uart_en_d0;
[*]18reg      uart_en_d1;
[*]19reg clk_cnt;                         //系统时钟计数器
[*]20reg [ 3:0] tx_cnt;                        //发送数据计数器
[*]21reg      tx_flag;                         //发送过程标志信号
[*]22reg [ 7:0] tx_data;                         //寄存发送数据
[*]23
[*]24//wire define
[*]25wire       en_flag;
[*]26
[*]27//*****************************************************
[*]28//**                  main code
[*]29//*****************************************************
[*]30//在串口发送过程中给出忙状态标志
[*]31assign uart_tx_busy = tx_flag;
[*]32
[*]33//捕获uart_en上升沿,得到一个时钟周期的脉冲信号
[*]34assign en_flag = (~uart_en_d1) & uart_en_d0;
[*]35
[*]36//对发送使能信号uart_en延迟两个时钟周期
[*]37always @(posedge sys_clk or negedge sys_rst_n) begin
[*]38      if (!sys_rst_n) begin
[*]39          uart_en_d0 <= 1'b0;
[*]40          uart_en_d1 <= 1'b0;
[*]41      end
[*]42      else begin
[*]43          uart_en_d0 <= uart_en;
[*]44          uart_en_d1 <= uart_en_d0;
[*]45      end
[*]46end
[*]47
[*]48//当脉冲信号en_flag到达时,寄存待发送的数据,并进入发送过程
[*]49always @(posedge sys_clk or negedge sys_rst_n) begin
[*]50      if (!sys_rst_n) begin
[*]51          tx_flag <= 1'b0;
[*]52          tx_data <= 8'd0;
[*]53      end
[*]54      else if (en_flag) begin               //检测到发送使能上升沿
[*]55            tx_flag <= 1'b1;                //进入发送过程,标志位tx_flag拉高
[*]56            tx_data <= uart_din;            //寄存待发送的数据
[*]57          end
[*]58                                              //计数到停止位结束时,停止发送过程
[*]59          else if ((tx_cnt == 4'd9) && (clk_cnt == BPS_CNT -(BPS_CNT/16))) begin
[*]60            tx_flag <= 1'b0;                //发送过程结束,标志位tx_flag拉低
[*]61            tx_data <= 8'd0;
[*]62          end
[*]63          else begin
[*]64            tx_flag <= tx_flag;
[*]65            tx_data <= tx_data;
[*]66          end
[*]67end
[*]68
[*]69//进入发送过程后,启动系统时钟计数器
[*]70always @(posedge sys_clk or negedge sys_rst_n) begin
[*]71      if (!sys_rst_n)
[*]72          clk_cnt <= 16'd0;
[*]73      else if (tx_flag) begin               //处于发送过程
[*]74          if (clk_cnt < BPS_CNT - 1)
[*]75            clk_cnt <= clk_cnt + 1'b1;
[*]76          else
[*]77            clk_cnt <= 16'd0;               //对系统时钟计数达一个波特率周期后清零
[*]78      end
[*]79      else
[*]80          clk_cnt <= 16'd0;                   //发送过程结束
[*]81end
[*]82
[*]83//进入发送过程后,启动发送数据计数器
[*]84always @(posedge sys_clk or negedge sys_rst_n) begin
[*]85      if (!sys_rst_n)
[*]86          tx_cnt <= 4'd0;
[*]87      else if (tx_flag) begin               //处于发送过程
[*]88          if (clk_cnt == BPS_CNT - 1)         //对系统时钟计数达一个波特率周期
[*]89            tx_cnt <= tx_cnt + 1'b1;      //此时发送数据计数器加1
[*]90          else
[*]91            tx_cnt <= tx_cnt;
[*]92      end
[*]93      else
[*]94          tx_cnt<= 4'd0;                  //发送过程结束
[*]95end
[*]96
[*]97//根据发送数据计数器来给uart发送端口赋值
[*]98always @(posedge sys_clk or negedge sys_rst_n) begin
[*]99      if (!sys_rst_n)
[*]100         uart_txd <= 1'b1;
[*]101   else if (tx_flag)
[*]102         case(tx_cnt)
[*]103             4'd0: uart_txd <= 1'b0;         //起始位
[*]104             4'd1: uart_txd <= tx_data;   //数据位最低位
[*]105             4'd2: uart_txd <= tx_data;
[*]106             4'd3: uart_txd <= tx_data;
[*]107             4'd4: uart_txd <= tx_data;
[*]108             4'd5: uart_txd <= tx_data;
[*]109             4'd6: uart_txd <= tx_data;
[*]110             4'd7: uart_txd <= tx_data;
[*]111             4'd8: uart_txd <= tx_data;   //数据位最高位
[*]112             4'd9: uart_txd <= 1'b1;         //停止位
[*]113             default: ;
[*]114         endcase
[*]115   else
[*]116         uart_txd <= 1'b1;                   //空闲时发送端口为高电平
[*]117 end
[*]118
[*]119 endmodule

复制代码

probedog 发表于 2022-9-23 10:10

串口发送模块与串口接收模块异曲同工,代码中也给出了详尽的注释,此处不再赘述。需要注意的是,在程序的59行,我们将tx_flag提前1/16个停止位拉低,是为了确保发送模块发送数据的时间略小于接收模块接收数据的时间,否则当连续传输大量数据时,发送数据的时间会不断累积,最终导致在做串口环回实验时丢失数据。
尽管串口发送数据只是接收数据的反过程,理论上在传输的时间上是一致的,考虑到我们模块里计算波特率会有较小的偏差,并且串口对端的通信设备(如电脑等)收发数据的波特率同样可能会出现较小的偏差,因此这里为了确保环回实验的成功,这里将发送模块的停止位略微提前结束。
需要说明的是,较小偏差的波特率在串口通信时是允许的,同样可以保证数据可靠稳定的传输。
另外我们在代码的第31行将用于标志串口发送过程的tx_flag信号赋值给uart_tx_busy,并通过模块端口输出。这样其他模块就可以通过检测uart_tx_busy信号是否为低电平,从而判断串口发送模块是否处于空闲状态。若uart_tx_busy为高电平,那么uart_send正处于发送过程,外部模块需要等待当前发送过程结束之后,才能通过uart_en信号的上升沿来启动新的发送过程。
图 7.5.13.5为串口发送过程中ILA抓取的波形图,我们使用开发板通过串口向上位机向发送16进制数55。图中绿色的触发线标识出了串口发送使能信号uart_en的上升沿。在检测到uart_en的上升沿后,en_flag会拉高一个时钟周期,此时将uart_din端口上的待发送数据寄存到tx_data中,并进入串口发送过程。
在整个发送过程中tx_flag保持为高电平,tx_cnt对串口数据进行计数,同时tx_data的各个数据位依次通过串口发送端uart_txd发送出去。当tx_cnt计数到9时,串口数据发送完成,开始发送停止位。在一个波特率周期的停止位发送完成后,串口发送过程结束,uart_tx_busy信号拉低,表明串口发送模块进入空闲状态。



图 7.5.13.5 串口发送过程波形图
从图 7.5.13.5中可以看出串口发送模块能够完成并串转换并正确发送串口数据。

probedog 发表于 2022-9-23 10:20

在介绍完串口的接收和发送模块后,最后我们来看一下串口环回模块的代码:


[*]1module uart_loop(
[*]2      input             sys_clk,                   //系统时钟
[*]3      input            sys_rst_n,               //系统复位,低电平有效
[*]4
[*]5      input            recv_done,               //接收一帧数据完成标志
[*]6      input       recv_data,               //接收的数据
[*]7
[*]8      input            tx_busy,                   //发送忙状态标志
[*]9      output reg       send_en,                   //发送使能信号
[*]10   output reg send_data                  //待发送数据
[*]11   );
[*]12
[*]13 //reg define
[*]14 reg recv_done_d0;
[*]15 reg recv_done_d1;
[*]16 reg tx_ready;
[*]17
[*]18 //wire define
[*]19 wire recv_done_flag;
[*]20
[*]21 //*****************************************************
[*]22 //**                  main code
[*]23 //*****************************************************
[*]24
[*]25 //捕获recv_done上升沿,得到一个时钟周期的脉冲信号
[*]26 assign recv_done_flag = (~recv_done_d1) & recv_done_d0;
[*]27
[*]28 //对发送使能信号recv_done延迟两个时钟周期
[*]29 always @(posedge sys_clk or negedge sys_rst_n) begin
[*]30   if (!sys_rst_n) begin
[*]31         recv_done_d0 <= 1'b0;
[*]32         recv_done_d1 <= 1'b0;
[*]33   end
[*]34   else begin
[*]35         recv_done_d0 <= recv_done;
[*]36         recv_done_d1 <= recv_done_d0;
[*]37   end
[*]38 end
[*]39
[*]40 //判断接收完成信号,并在串口发送模块空闲时给出发送使能信号
[*]41 always @(posedge sys_clk or negedge sys_rst_n) begin
[*]42   if (!sys_rst_n) begin
[*]43         tx_ready<= 1'b0;
[*]44         send_en   <= 1'b0;
[*]45         send_data <= 8'd0;
[*]46   end
[*]47   else begin
[*]48         if(recv_done_flag)begin               //检测串口接收到数据
[*]49             tx_ready<= 1'b1;                  //准备启动发送过程
[*]50             send_en   <= 1'b0;
[*]51             send_data <= recv_data;             //寄存串口接收的数据
[*]52         end
[*]53         else if(tx_ready && (~tx_busy)) begin   //检测串口发送模块空闲
[*]54             tx_ready <= 1'b0;                   //准备过程结束
[*]55             send_en<= 1'b1;                   //拉高发送使能信号
[*]56         end
[*]57   end
[*]58 end
[*]59
[*]60 endmodule

复制代码

uart_loop模块的代码比较简单,首先代码的25至38行实现了上升沿检测功能。当检测到recv_done信号的上升沿时,recv_done_flag输出一个时钟周期的高电平,标志着串口接收模块接收到了一帧数据。在代码的48至52行,在判断到recv_done_flag为高电平时,寄存接收到的数据recv_data到send_data中;同时将tx_ready信号拉高,表示已经准备好了待发送的数据。另外还要将send_en信号拉低,为接下来产生一个上升沿作准备。
uart_loop模块还有一个输入信号tx_busy,它是由串口发送模块所输出,该信号为高电平表示串口发送模块正处于发送过程中。在代码的第53行,当tx_ready信号为高电平时,如果检测到tx_busy为低电平,则说明串口发送模块处于空闲状态。此时将send_en信号拉高,由此产生一个上升沿,以启动串口发送模块的发送过程,将寄存到send_data中的数据发送出去。于此同时,将tx_ready信号拉低,等待下一个串口接收数据的到来。

probedog 发表于 2022-9-23 10:21

16.5 下载验证
编译工程并生成比特流.bit文件。接下来我们下载程序,验证上位机与开发板通过RS232串口进行串口数据环回功能。
首先我们需要准备一个图 7.5.13.3所示的USB转串口线,将USB接口一端插入电脑上的USB口,另一端DB9接口与开发板上的COM2接口相连接,并将P1排针上的两个跳帽按照如下图所示的方式连接:



图 7.5.13.1 串口连接和P1跳线帽连接
接下来分别连接JTAG接口和电源线,并打开电源开关。
注意上位机第一次使用USB转串口线与FPGA开发板连接时,需要安装USB串口驱动。在开发板随附的资料中找到“6_软件资料/1_软件/CH340驱动(USB串口驱动)”的文件夹,双击打开文件夹中的“SETUP.EXE”进行安装,驱动安装界面如图 7.5.13.2所示。界面中提示INF文件为CH341SER.INF,我们不需要理会(CH341,CH340驱动是共用的),直接点安装即可。



图 7.5.13.2 USB串口驱动驱动安装界面

probedog 发表于 2022-9-23 10:21

开发板电源打开后,将本次实验的bit文件下载到开发板中。
接下来打开串口助手。串口助手是上位机中用于辅助串口调试的小工具,可以选择安装使用开发板随附资料中“6_软件资料/1_软件/串口调试助手”文件夹中提供的XCOM串口助手。在上位机中打开串口助手XCOM V2.0,如下图所示:



图 7.5.13.3 串口助手操作界面
在串口助手中选择与开发板相连接的CH340虚拟串口,具体的端口号(这里是COM4)需要根据实际情况选择,可以在计算机设备器中查看,如下图所示:


图 7.5.13.4 电脑的设备管理器窗口
在串口助手中设置波特率为115200,数据位为8,停止位为1,无校验位,最后确认打开串口。
串口打开后,在发送文本框中输入数据“5A”并点击发送,可以看到串口助手中接收到数据“5A”,如图 7.5.13.3所示。串口助手接收到的数据与发送的数据一致,说明程序所实现的串口数据环回功能验证成功。
页: [1]
查看完整版本: STM32MP1嵌入式Linux驱动开发指南V1.7——UART串口通信实验