【盘古100Pro+开发板实验例程】FPGA学习 | 基于紫光 FPGA 的 UART 串口通信

发布于:2025-08-02 ⋅ 阅读:(18) ⋅ 点赞:(0)

本原创文章由深圳市小眼睛科技有限公司创作,版权归本公司所有,如需转载,需授权并注明出处(www.meyesemi.com)

1. 实验简介

实验目的:

      实现 FPGA 与 PC 之间的串口通信,并使用 PDS 软件内置的在线调试工具验证收发数据的准确性。 并根据接收到的不同数据去点亮开发板上的 LED 灯。 

      同时掌握在线 Debugger 工具的使用。

实验环境:

      Window11

      PDS2022.2-SP6.4

硬件环境:

      MES2L676-100HP

2. 实验原理

2.1. 串口原理

从图中我们可以看到标准串口接口是 9 根线,具体含义如下:

数据线:

TXD(pin 3):串口数据输出(Transmit Data)

RXD(pin 2):串口数据输入(Receive Data)

握手:

RTS(pin 7):发送数据请求(Request to Send)

CTS(pin 8):清除发送(Clear to Send)

DSR(pin 6):数据发送就绪(Data Send Ready)

DCD(pin 1):数据载波检测(Data Carrier Detect)

DTR(pin 4):数据终端就绪(Data Terminal Ready)

地线:

GND(pin 5):地线

其它:

RI(pin 9):铃声指示

通常我们用 RS232 串口仅用到了 9 根传输线中的三根:TXD,RXD,GND。但是对于数据传 输,双方必须对数据传输采用使用相同的波特率,约定同样的传输模式(传输架构,握手条件 等)。尽管这种方法对于大多数应用已经足够,但是对于接收方过载的情况这种使用受到限制。

RS232 的串口连接方式:

串口传输协议如下:

      起始位:先发出一个逻辑”0”信号,表示传输字符的开始。

      数据位:可以是 5~8 位逻辑”0”或”1”。如 ASCII 码(7 位),扩展 BCD 码(8 位)。 LSB 表示低位,MSB 表示高位,有效数据的传输顺序为低位在前高位在后。

      校验位:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验)。

      停止位:它是一个字符数据的结束标志。可以是 1 位、1.5 位、2 位的高电平。

      空闲位:处于逻辑“1”状态,表示当前线路上没有资料传送。

      波特率:uart 中的波特率就可以认为是比特率,即每秒传输的位数(bit)。一般选波特率都会有 9600,19200,115200 等选项。其实意思就是每秒传输这么多个比特位数(bit)。

       引入波特率的概念后可得到串口的传输节奏如下:

      细心的话可以发现数据的传输都是在数据稳定后的中心时刻,接收数据其实也是,都是在中心时刻采集数据,此时的数据是最稳定的。

2.2. 串口发送字符

从前面串口协议中可以了解到串口每次传输可以以有 5~8bit 数据,在计算机中字符通常用 ASCII 码(7bit)表示,所以字符的发送可以用 ASCII 码发送。

查询 ASCII 码表格可得到:“www.meyesemi.com”用到的字符对应 ASCII 码;

2.3. 串口接收数据点灯

      收到的数据最终转为 8bit 的并行数据,rx_data,而开发板上刚好也有 8 个 led 灯。1 表示亮,0 表示灭,那么 8 个 LED 灯就等同于 8bit 的变量。可以用二进制来表示,例如要全亮,那就是 8’b1111_1111,因此我们往 FPGA 发送串口数据时,可以通过 16 进制的方式来发送,例如我要点亮 LED1 和 LED5,那就发送 8’b0001_0001,转为 16 进制就是 8’h11。所以可以根据该规律去点亮开发板上的每一个灯。

3. 接口列表

uart_top.v 模块是整个工程的顶层模块

uart_data_gen.v 模块 是生成要发送的串口数据。

uart_tx.v 模块,主要完成串口发送功能。将要发送到 PC 的 8bit 并行数据转为一个波特周期发送一个 bit。

uart_rx.v 模块,主要完成串口接收功能,将 PC 发送到 FPGA 的串行的 8bit 数据转为并行的 8bit 数据给到用户使用。

4. 工程说明

实验的架构如图所示,分为 TX(发送模块)、BAUD(波特率计算模块)、RX(接收模块)。而 TOP 模块是顶 层,例化了这三个模块。我们从原理上分析波特率的计算是一个计数器,发射和接收可复用,但是我们在设计时为 保持 TX 或 RX 的完整性,故将波特周期计数器集成在各自模块内部。

上图所示的 Uart_data_gen 模块是用来生成要发送的数据。即“www.meyesemi.com”这一串字符。 生成后的数据给到 uart_tx 模块让其发送到 PC。Uart_rx 模块负责接收 PC 发来的数据,并做解析,然后点亮开发 板上的 LED 灯。

5. 代码模块说明

实现功能:接收到一个发送命令信号时,将 data[7:0] -> 依次发出{start,data[0:7],stop}

共 10bit 数据(无校验位,停止位 1bit);

这里主要讲解代码的重点设计部分,完整源码大家请参考工程源码,然后结合文档一起理解比较方便。 代码的设计主要是通过 bit 计数与 baud 计数控制状态跳转,在状态机中输出;

5.1. 串口发送模块
always @(posedge clk) begin
    if (tx_en) begin
        case (tx_state)
            IDLE: 
                uart_tx <= `UD 1'h1;  // 空闲状态输出高电平
                
            SEND_START: 
                uart_tx <= `UD 1'h0;  // start状态发送一个波特周期的低电平
                
            SEND_DATA: begin  // 发送状态每个波特周期发送一个bit
                case (tx_bit_cnt)
                    3'h0: uart_tx <= `UD tx_data[0];
                    3'h1: uart_tx <= `UD tx_data[1];
                    3'h2: uart_tx <= `UD tx_data[2];
                    3'h3: uart_tx <= `UD tx_data[3];
                    3'h4: uart_tx <= `UD tx_data[4];
                    3'h5: uart_tx <= `UD tx_data[5];
                    3'h6: uart_tx <= `UD tx_data[6];
                    3'h7: uart_tx <= `UD tx_data[7];
                    default: uart_tx <= `UD 1'h1;
                endcase
            end
            
            SEND_STOP: 
                uart_tx <= `UD 1'h1;  // 发送停止状态输出1个波特周期高电平
                
            default: 
                uart_tx <= `UD 1'h1;  // 其他状态默认与空闲状态一致,保持高电平输出
        endcase
    end
    else begin
        uart_tx <= `UD 1'h1;
    end
end

在代码的第 5 行中有个 tx_state 的变量,一共有四个状态 IDLE(空闲状态)、SEND_START(开始发送状态)、SEND_DATA(发送数据状态)、SEND_STOP(发送停止状态)。根据我们的串口发送协议。在 IDLE 状态下就直接保 持 uart_tx 总线为高电平即可。在 SEND_START 状态下发送起始为,一个波特周期的低电平,之后跳转到 SEND_DATA 状态,发送 8bit 数据,通过 tx_bit_cnt 来计数一共发了多少个 bit,tx_bit_cnt 从 0 计数到 7,一共发送 8bit 数据, 需要注意的时,发送时先把 tx_data 的低位发出去。发送完 8bit 数据后跳转到 SEND_STOP 状态,输出一个波特 周期的高电平,表示停止。至此就完成了一次串口数据的发送。

下面的这两个 always 块主要完成波特率的生成,在串口接收模块里也有相同的代码,故在接收模块里不再讲解。

always @(posedge clk) begin
    if (!tx_en) begin
        tx_bit_cnt <= `UD 3'h0;
    end
    else if ((tx_bit_cnt == 3'h7) && (clk_div_cnt == BPS_NUM)) begin
        tx_bit_cnt <= `UD 3'h0;
    end
    else if ((tx_state == SEND_DATA) && (clk_div_cnt == BPS_NUM)) begin
        tx_bit_cnt <= `UD tx_bit_cnt + 3'h1;
    end
    else begin
        tx_bit_cnt <= `UD tx_bit_cnt;
    end
end

always @(posedge clk) begin
    if (clk_div_cnt == BPS_NUM || (~tx_pluse_reg & tx_pluse)) begin
        clk_div_cnt <= `UD 16'h0;
    end
    else begin
        clk_div_cnt <= `UD clk_div_cnt + 16'h1;
    end
end

      代码 1-11 行完成 tx_bit_cnt 的计数,代码 12-18 行,完成波特周期的计数,clk_div_cnt 是用来对我们输入 进来的 50MHZ 的时钟进行分频,以此来得到波特周期。其中 BPS_NUM 的值根据不同的波特率会有不同的结果, 默认是 115200,所以 BPS_NUM=50MHZ/115200≈434。计算公式为 BPS_NUM = CLK/BAUD_RATE。需要注意 的是,第 14 行的判断条件,clk_div_cnt 的清 0 除了每次计数到 BSP_NUM 后需要清 0 之外,当发送使能的上升 沿到达来到时,我们也需要清 0。tx_pluse 实际是发送使能信号,tx_pluse_reg 是 tx_pluse 打一拍后的信号, ~tx_pluse_reg & tx_pluse 表示是获取 tx_pluse 的上升沿。所以,该条件表示我们要开始发送数据了,为了能够 正常计数波特周期,故要把 clk_div_cnt 清 0,让其重新计数。

      代码第三行,当 tx_en 为低电平时,表示不处于发送状态,所以一直置 0。代码第 5 行,当 tx_bit_cnt==7 且 clk_div_cnt== BPS_NUM 时,把 tx_bit_cnt 清空,因为此时已经发送了 8 个 bit 的数据。代码第 7 行,在发送状 态下,每到来一个波特周期就让 tx_bit_cnt 不断+1。其余情况下,tx_bit_cnt 保持不变。需要注意的是,波特率在发送模块和接收模块里都是一样的,故接收模块将不讲解这一部分。

5.2. 串口接收模块

串口接收模块是发送模块的逆过程,设计思路区别不大,但是有如下几点需要注意:

1.接收开始信号,当 rx 下降沿到来后保持几个时钟周期的低电平,表明进入接收 start;

2.接收数据提取位置,在前面讲发送的时候都是在波特周期开始的位置变更数据,但是接收数据在提取时需 要在 rx 稳定时刻取数,也就是在波特周期的中间位置取数;

3.最终输出数据锁存,在最后 1bit 存入寄存器后需要对接收数据锁存,并在之后需要给 出数据使能信号, 表示输出数据有效;

串口接收代码的重点部分如下:

// 状态机状态跳转条件及跳转规律
always @(*) begin
    case (rx_state)
        IDLE: begin
            if (start)  // 监测到 start 信号到来,下一状态跳转到 start 状态
                rx_state_n = RECEIV_START;
            else
                rx_state_n = rx_state;
        end
        
        RECEIV_START: begin
            if (clk_div_cnt == BPS_NUM)  // 已完成接收 start 标志信号
                rx_state_n = RECEIV_DATA;
            else
                rx_state_n = rx_state;
        end
        
        RECEIV_DATA: begin
            if (rx_bit_cnt == 3'h7 && clk_div_cnt == BPS_NUM)  // 已完成 8bit 数据的传输
                rx_state_n = RECEIV_STOP;
            else
                rx_state_n = rx_state;
        end
        
        RECEIV_STOP: begin
            if (clk_div_cnt == BPS_NUM)  // 已完成接收 stop 标志信号
                rx_state_n = RECEIV_END;
            else
                rx_state_n = rx_state;
        end
        
        RECEIV_END: begin
            if (!uart_rx_1d)  // 数据线重新被拉低,表示新数据传输又发送 start 标志信号,需要跳转到 start 状态
                rx_state_n = RECEIV_START;
            else  // 没有其他状况出现时,回到空闲状态,等待 start 信号的到来
                rx_state_n = IDLE;
        end
        
        default: rx_state_n = IDLE;
    endcase
end

// 状态机输出
always @(posedge clk) begin
    case (rx_state)
        IDLE, RECEIV_START: begin  // 在空闲和 start 状态时将接收数据缓冲寄存器和数据使能置位
            rx_en <= `UD 1'b0;
            rx_data_reg <= `UD 8'h0;
        end
        
        RECEIV_DATA: begin
            if (clk_div_cnt == BPS_NUM[15:1])  // 在一个波特周期的中间位置取数据线上传输的数据
                rx_data_reg <= `UD {uart_rx, rx_data_reg[7:1]};  // 以循环右移的方式将 uart_rx 数据填入缓冲寄存器的最高位
        end
        
        RECEIV_STOP: begin
            rx_en <= `UD 1'b1;  // 输出使能信号,表示最新的数据输出有效
            rx_data <= `UD rx_data_reg;  // 将缓冲寄存器的值赋值给输出寄存器
        end
        
        RECEIV_END: begin
            rx_data_reg <= `UD 8'h0;
        end
        
        default: rx_en <= `UD 1'b0;
    endcase
end

      代码的 1-42 行主要完成 rx_state 的状态跳转。一共有 5 个状态。IDLE(空闲状态)、RECEIV_START(接收开始 状态)、RECEIV_DATA(接收数据状态)、RECEIV_stop(停止接收状态)、RECEIV_END(接收结束状态)。在 IDLE 状态 下,等待 start 信号到来,一旦 start 信号拉高,跳转到 RECEIV_START 状态。在 RECEIV_START 状态下经过一个 波特周期后就跳转到 RECEIV_DATA 状态,之后经过 8 个波特周期,即接收 8bit 数据后再跳转到 RECEIV_STOP 状态。在 RECEIV_STOP 下,等待一个波特周期后跳转到 RECEIV_END 状态,整个接收流程到此结束。

      接下来看代码的 45-70 行,主要执行 rx_state 在不同状态下的逻辑操作。在 IDLE 和 RECEIV_START 状态下都将接收使能和 rx_data_reg(接收数据缓存)置 0,等待接收数据到达。在 RECEIV_DATA 状态下,注意判断条件 是 clk_div_cnt==BPS_NUM[15:1],在前面说过,我们要在中心时刻采样数据才是最稳定的,所以 BPS_NUM[15:1] 其实就是 BPS_NUM>>1,即左移 1 位,就是除以 2。所以该状态就是每次到达波特周期的中心时刻就把数据给到 rx_data_reg 进行缓存,rx_data_reg<={uart_rx,rx_data_reg[7:1]}则是将进来的 uart 数据不断右移,最后 的结果就是最先进来的数据会放在低位。因为我们发送的时候也是先把最低位的数据先发出去,所以这里就是把 最先收到的数据放到低位,这样就和发送的数据一一对应。在 RECEIV_STOP 状态下,拉高 rd_en 信号,表示接收 的数据有效,并将 rx_data_reg 的值赋值给 rx_data(输出寄存器),所以这个时刻下,rx_data 和 rd_en 是同步的, 因此读者要正确使用接收的数据话可以用 rd_en 作为有效条件。在 RECEIV_END 状态下,将 rx_data_reg 清空。

      而波特率生成模块都集成在模块内部,并且和发送模块的是一模一样,故不再讲解。

5.3. 工程及现象

      通过使用在线 Debugger 工具来抓取收到的串口数据,并进行对比,比较收到的和我们发送的是否一致。

      打开 PDS 上方工具栏的 Insert 工具,如图所示:

     RAM Type 默认使用 Block RAM,采样深度选择 4096 即可,不用太大,添加采样时钟,这里的时钟最好是和我 们要采样的数据是同个时钟源,或者频率比数据时钟源高也行,但是不能低,否则可能会采样不到。

      添加信号的操作流程如下,添加完成然后保存。

首先点击 Modify Connections;

之后点击要抓取的信号,然后点击下方的 Make Connection 即可。

添加上图所示的全部信号并保存即可。

6. 在线 Debugger 工具的使用

6.1. Fabric 工具说明

      PDS 在线调试工具主要用于对 FPGA 内部实际运行中信号的捕获,便于调试者进行分析和定位问题,在调试 过程中可以很大程度上可以替代逻辑分析仪,避免繁琐的连线工作和节省成本。本 demo 通过 Fabric Inserter 和 Fabric Debugger 两个工具的使用实现在线调试。

      Fabric Inserter 工具将 DebugCore 自动插入用户的设计网表中生成新的设计网表,从而使用户不需要手 工在 HDL 代码中例化。

      Fabric Debugger 是一款界面化 FPGA 芯片调试工具,直接与 JtagHub 和 DebugCore 交互,可实时配置 目标 FPGA、设置触发条件并观测目标信号捕获结果。

      实现在线调试流程中,inserter 工具和 debugger 工具的作用如下图所示

6.2. Fabric 工具使用

点击 Insert 旁边的像瓢虫的图标”Debugger”。

在弹出的界面点击左上角的图标寻找 JTAG 设备,如下所示:

点击 OK,下载 sbit 文件以及导入 fic 文件。

将 rx_en 设置为上升沿触发。

      打开串口助手,波特率设置为 9600,然后打开端口,我的电脑是 COM15 端口,可以看到每隔 1s,串口助手 会收到 www,meyesemi.com 的字符串,具体端口号大家可以打开设备管理器进行查看,如下所示:

      接下来回到 Debugger 界面,运行。

      等待触发,此时串口助手发送 ff,注意勾选 16 进制发送。

      可以看到 rd_en 拉高,同时 rx_data 的值也为 0xff,和我们 PC 发送的数据一致。同时观察开发板上的 8 个 led 灯,可以看到 8 个 LED 灯全亮。ff 对应二进制 1111_1111,所以会亮 8 个灯。以此类推,1a 的二进制是 0001_1001,所以会亮 LED1、LED4、LED5 三个灯,大家可以自行尝试。


网站公告

今日签到

点亮在社区的每一天
去签到