实验 07:UART 串口收发
🛒 开发板购买: EK303 GW1N FPGA核心板
一、实验目的
- 理解 UART 异步串行通信协议——起始位、数据位、停止位、波特率。
- 掌握 FPGA 中波特率发生器的设计——用计数器从 50MHz 产生精确的波特率时钟。
- 学会串并转换——发送端 8bit 并行 → 串行 TXD,接收端串行 RXD → 8bit 并行。
- 实现回环测试:FPGA 自发自收(TXD 接 RXD 或内部环回),验证收发逻辑正确性。
二、实验过程
2.1 硬件连接
| 信号 | 方向 | 引脚 | 说明 |
|---|---|---|---|
| CLK | in | 98 | 50MHz 系统时钟 |
| TXD | out | 12 | UART 发送端 |
| RXD | in | 11 | UART 接收端 |
| LED1~3 | out | 8/9/10 | 收发状态指示 |
回环测试:将 TXD (Pin 12) 与 RXD (Pin 11) 用杜邦线短接,FPGA 发送的数据立即被自己接收。
2.2 设计思路
UART 帧格式(8N1):
空闲(高) │起始(低)│ D0 │ D1 │ D2 │ D3 │ D4 │ D5 │ D6 │ D7 │ 停止(高) │空闲...
└──1bit──┘└─────────── 8 data bits ────────────┘└──1bit──┘
- 空闲态:TXD/RXD 保持高电平。
- 起始位:1 bit 低电平。
- 数据位:8 bit,LSB 先发。
- 停止位:1 bit 高电平。
- 一帧共 10 bit。
波特率选择:
| 波特率 | bit 周期 | 50MHz 计数值 |
|---|---|---|
| 9600 | 104.17 μs | 5208 |
| 115200 | 8.68 μs | 434 |
| 921600 | 1.09 μs | 54 |
推荐使用 115200 bps,计数值 = 50,000,000 / 115,200 ≈ 434。
模块划分:
┌─────────────┐ ┌─────────────┐
│ TX 发送模块 │ ──TXD──→│ RX 接收模块 │
│ (baud_tx) │ │ (baud_rx) │
└──────┬───────┘ └──────┬───────┘
│ │
[发送状态机] [接收状态机]
IDLE→START→D0..D7→STOP IDLE→START→D0..D7→STOP→DONE
发送模块 (baud_tx):
- 外部提供一个 8-bit 数据
tx_data和发送触发脉冲tx_start。 - 状态机:IDLE → 拉低 TXD(起始位)→ 依次移位输出 D0~D7 → 拉高 TXD(停止位)→ IDLE。
- 发送完成时输出
tx_done脉冲。
接收模块 (baud_rx):
- 检测 RXD 下降沿(起始位)。
- 等待半个 bit 周期后在 bit 中心采样(避免边缘抖动)。
- 依次采样 D0~D7 → 检测停止位 → 输出
rx_data[7:0]和rx_done脉冲。 - 若停止位不为高 → 帧错误
rx_err。
2.3 Verilog 模块设计
顶层模块名:uart_loopback
子模块:
| 模块 | 功能 |
|---|---|
baud_gen | 波特率发生器,输出 16 倍波特率采样时钟或 bit 周期脉冲 |
uart_tx | UART 发送器,并行→串行 |
uart_rx | UART 接收器,串行→并行 |
顶层端口:
| 端口 | 方向 | 位宽 | 说明 |
|---|---|---|---|
clk_50m | input | 1 | Pin 98 |
txd | output | 1 | Pin 12 |
rxd | input | 1 | Pin 11 |
led | output | 3 | Pin 8/9/10 |
逻辑描述:
- 波特率发生器产生 bit 周期使能脉冲
baud_tick。 - 发送端:自动周期性发送递增数据(0x00 → 0x01 → 0x02 → ...),间隔约 0.5 秒发一帧。
- 接收端:持续监听 RXD,收到完整一帧后比较
rx_data与预期值。 - 收发一致 → LED1 闪一下(数据正确);不一致 → LED2 常亮(错误)。
- LED3 在每次收到数据时翻转(指示接收活动)。
2.4 引脚约束
| 信号 | 约束 |
|---|---|
txd | IO_LOC ... 12; IO_TYPE=LVCMOS33 |
rxd | IO_LOC ... 11; IO_TYPE=LVCMOS33 PULL_MODE=UP |
RXD 空闲为高电平,建议启用内部上拉
PULL_MODE=UP,防止悬空时收到噪声数据。
⚠️ 引脚复用:RXD (Pin 11) 和 TXD (Pin 12) 默认用作 SSPI 功能。使用前须在 Gowin IDE
Dual-Purpose Pin中关闭 SSPI,或执行 TCL:set_option -use_sspi_as_gpio 1。详见PIN_DUAL_PURPOSE.md。
三、实验现象
| 连接方式 | 现象 |
|---|---|
| TXD-RXD 短接(回环) | FPGA 自发自收,LED1 周期性闪烁表示数据收发正确 |
| 不短接 | RXD 悬空拉高→收到全 1 数据→LED2 亮表示数据不匹配 |
| 用 USB 转串口模块连接 PC | PC 端串口助手收到 FPGA 发送的递增数据(00 01 02 ...),波特率 115200 |
| PC 发送数据给 FPGA | LED3 随数据变化,LED1 验证数据正确性 |
四、掌握知识点
| 知识点 | 说明 |
|---|---|
| UART 协议 | 起始位+8 数据+1 停止位 = 10 bit/帧 |
| 波特率 = 比特率 | 115200 bps = 每秒 11520 字节 |
| 波特率发生器 | 50M / 115200 ≈ 434 周期/bit |
| 过采样 | 16× 波特率采样,在 bit 中心取值 |
| 串并/并串转换 | 发送:并行→移位→串行; 接收:采样→移位→并行 |
| 下降沿检测 | RXD 从高变低 = 起始位标志 |
| 帧错误检测 | 停止位不为高→数据损坏 |
五、思考题
- 如果改成波特率 9600,计数值是多少?还有什么要改?
- 为什么接收端要在 bit 中心采样而不是边沿采样?
- 如何实现奇偶校验位(7 数据位 + 1 校验位)?
- 怎么知道 RXD 引脚有没有接反?如何自动检测 RXD/TXD 交换?
附录:源码清单
top.v
// =============================================================================
// exp_07_uart_basic - UART 串口收发
// 芯片: GW1N-LV9LQ144C6/I5, 50MHz
// 波特率 115200-8N1, TX 自动发递增数据, RX 校验, LED 指示
// =============================================================================
module top (
input wire clk_50m,
input wire rxd,
output reg txd,
output reg [2:0] led
);
localparam BAUD_DIV = 434; // 50M/115200
localparam HALF_BAUD = BAUD_DIV / 2; // 217
localparam TX_INTERVAL = 26'd25_000_000; // 0.5s
// ===== 波特率发生器 =====
reg [9:0] baud_cnt;
wire baud_tick;
always @(posedge clk_50m)
baud_cnt <= (baud_cnt == BAUD_DIV - 1) ? 10'd0 : baud_cnt + 1'b1;
assign baud_tick = (baud_cnt == BAUD_DIV - 1);
// ===== TX: 发送器 =====
reg [25:0] tx_timer;
reg tx_req; // 发送请求 (电平, 非脉冲!)
reg [7:0] tx_buf; // 待发送数据缓冲
reg [3:0] tx_state;
reg [3:0] tx_bit;
localparam TX_IDLE=0, TX_START=1, TX_DATA=2, TX_STOP=3;
// 定时触发发送
always @(posedge clk_50m) begin
if (tx_state == TX_IDLE && tx_req && baud_tick) begin
tx_req <= 1'b0; // 在状态机中清除
end else if (tx_timer == TX_INTERVAL - 1) begin
tx_timer <= 26'd0;
tx_req <= 1'b1;
tx_buf <= tx_buf + 1'b1;
end else begin
tx_timer <= tx_timer + 1'b1;
end
end
// TX 状态机
always @(posedge clk_50m) begin
if (baud_tick) begin
case (tx_state)
TX_IDLE: begin
txd <= 1'b1;
if (tx_req) begin
tx_state <= TX_START;
tx_bit <= 4'd0;
end
end
TX_START: begin
txd <= 1'b0;
tx_state <= TX_DATA;
tx_bit <= 4'd0;
end
TX_DATA: begin
txd <= tx_buf[tx_bit];
if (tx_bit == 4'd7)
tx_state <= TX_STOP;
else
tx_bit <= tx_bit + 1'b1;
end
TX_STOP: begin
txd <= 1'b1;
tx_state <= TX_IDLE;
end
endcase
end
end
// ===== RX: 接收器 =====
reg [2:0] rx_sync;
wire rx_s;
reg [3:0] rx_state;
reg [3:0] rx_bit;
reg [9:0] rx_cnt;
reg [7:0] rx_shift;
reg [7:0] rx_data;
reg rx_done;
localparam RX_IDLE=0, RX_START=1, RX_DATA=2, RX_STOP=3, RX_DONE=4;
always @(posedge clk_50m) rx_sync <= {rx_sync[1:0], rxd};
assign rx_s = rx_sync[2];
always @(posedge clk_50m) begin
case (rx_state)
RX_IDLE: begin
rx_done <= 1'b0;
if (rx_s == 1'b0) begin // 下降沿=起始位
rx_state <= RX_START;
rx_cnt <= 10'd0;
end
end
RX_START: begin
if (rx_cnt == HALF_BAUD - 1) begin
if (rx_s == 1'b0) begin // 确认起始位
rx_state <= RX_DATA;
rx_bit <= 4'd0;
rx_cnt <= 10'd0;
end else
rx_state <= RX_IDLE; // 假起始位
end else
rx_cnt <= rx_cnt + 1'b1;
end
RX_DATA: begin
if (rx_cnt == BAUD_DIV - 1) begin
rx_shift[rx_bit] <= rx_s;
rx_cnt <= 10'd0;
if (rx_bit == 4'd7)
rx_state <= RX_STOP;
else
rx_bit <= rx_bit + 1'b1;
end else
rx_cnt <= rx_cnt + 1'b1;
end
RX_STOP: begin
if (rx_cnt == BAUD_DIV - 1) begin
rx_state <= RX_DONE;
rx_data <= rx_shift;
rx_done <= 1'b1;
end else
rx_cnt <= rx_cnt + 1'b1;
end
RX_DONE: begin
rx_done <= 1'b0;
rx_state <= RX_IDLE;
end
default: rx_state <= RX_IDLE;
endcase
end
// ===== 校验 & LED =====
reg [7:0] expected;
reg rx_done_d1;
wire rx_pulse;
reg [25:0] led_timer;
reg [1:0] led_stat; // 01=OK 10=ERR
always @(posedge clk_50m) rx_done_d1 <= rx_done;
assign rx_pulse = rx_done && !rx_done_d1;
// 记录期望值 (TX发送时锁存)
always @(posedge clk_50m)
if (tx_state == TX_START && baud_tick)
expected <= tx_buf;
always @(posedge clk_50m) begin
if (rx_pulse) begin
led_stat <= (rx_data == expected) ? 2'b01 : 2'b10;
led_timer <= 26'd0;
led[0] <= ~led[0]; // LED3: 接收翻转
end else if (led_timer == 26'd15_000_000) begin
led_stat <= 2'b00;
end else begin
led_timer <= led_timer + 1'b1;
end
end
// LED1/2 输出
always @(posedge clk_50m)
case (led_stat)
2'b01: led[2:1] <= 2'b10; // LED1=亮(正确)
2'b10: led[2:1] <= 2'b01; // LED2=亮(错误)
default: led[2:1] <= 2'b00;
endcase
endmodule