实验 08:UART 指令控制 LED
🛒 开发板购买: EK303 GW1N FPGA核心板
一、实验目的
- 掌握 UART 指令解析——从接收字节中识别命令并执行对应操作。
- 理解通信协议设计——定义简单的指令格式(命令码 + 参数)。
- 综合 UART 收发、按键消抖、LED 多模式驱动,实现 PC 端遥控 FPGA。
- 学习状态反馈——FPGA 执行命令后通过 UART 回复确认信息。
二、实验过程
2.1 硬件连接
| 信号 | 引脚 | 说明 |
|---|---|---|
| CLK | 98 | 50MHz |
| TXD | 12 | 发送到 PC |
| RXD | 11 | 接收 PC 指令 |
| LED1~3 | 8/9/10 | 受控 LED |
| KEY1~3 | 69/70/71 | 本地按键(同实验 07) |
2.2 指令协议设计
帧格式:1 字节命令 + 1 字节参数。
| 命令码 | 含义 | 参数 | 执行动作 |
|---|---|---|---|
0x01 | LED 开关 | 0x00=灭, 0x01=亮 | 控制指定 LED 亮或灭 |
0x02 | LED 闪烁频率 | 0~9 档位 | 设置 LED1 闪烁频率 |
0x03 | 流水方向 | 0x00=左, 0x01=右 | 切换 LED 流水方向 |
0x04 | 全亮/全灭 | 0xFF=全亮, 0x00=全灭 | 一键控制三个 LED |
0x05 | 查询状态 | 忽略 | 返回当前各 LED 状态 |
0xFF | 复位 | 0xAA | 所有 LED 灭,参数归默认 |
回复格式:0xAA + 命令码 + 结果码(0x00=成功, 0x01=失败)。
示例:PC 发送
02 05→ LED1 设为频率档位 5 → FPGA 回复AA 02 00。
2.3 Verilog 模块设计
模块名:uart_cmd
子模块:
| 模块 | 功能 |
|---|---|
uart_rx | UART 接收(复用实验 07) |
uart_tx | UART 发送(回复确认) |
cmd_parser | 指令解析状态机 |
led_driver | LED 多模式驱动(复用实验 03/05/07) |
key_debounce | 按键消抖(本地按键同步控制) |
指令解析状态机:
IDLE ──(rx_done)──→ CMD_RECV (存储第一字节)
↓
PARAM_RECV (等待第二字节)
↓
EXECUTE (根据命令执行动作)
↓
REPLY (发送确认帧)
↓
IDLE
关键信号:
| 信号 | 位宽 | 说明 |
|---|---|---|
cmd_byte | 8 | 接收到的命令码 |
param_byte | 8 | 接收到的参数 |
rx_byte_cnt | 1 | 接收字节计数(0=命令, 1=参数) |
cmd_valid | 1 | 有效命令标志 |
reply_data | 24 | 回复帧(3 字节:0xAA+命令+结果) |
2.4 引脚约束
| 信号 | 引脚 |
|---|---|
| TXD / RXD | 12 / 11 |
| LED1~3 | 8 / 9 / 10 |
| KEY1~3 | 69 / 70 / 71 |
⚠️ 引脚复用:RXD (Pin 11) / TXD (Pin 12) 默认用作 SSPI。使用前须关闭复用。详见
PIN_DUAL_PURPOSE.md。
三、实验现象
PC 端操作(用任意串口助手,115200-8N1,十六进制发送):
| PC 发送 | 现象 | FPGA 回复 |
|---|---|---|
01 01 | LED1 亮起 | AA 01 00 |
01 00 | LED1 熄灭 | AA 01 00 |
02 05 | LED1 闪烁频率变化 | AA 02 00 |
04 FF | 三个 LED 全部亮起 | AA 04 00 |
04 00 | 三个 LED 全部熄灭 | AA 04 00 |
03 01 | 流水灯方向反转 | AA 03 00 |
05 00 | — | AA 05 07(LED 状态 = 0b111) |
FF AA | LED 全灭、恢复默认 | AA FF 00 |
99 00 | 无效命令,LED 无变化 | AA 99 01 |
本地按键同步:按键操作与 UART 指令互不冲突,各自独立控制 LED 状态。
四、掌握知识点
| 知识点 | 说明 |
|---|---|
| 指令协议设计 | 命令码 + 参数 + 回复确认 的帧结构 |
| 字节流解析 | 用计数器区分命令字节和参数字节 |
| 状态反馈 | 执行后主动回复确认帧 |
| 错误处理 | 无效命令码返回错误码 |
| UART+GPIO 协同 | 串口遥控 + 按键本地控制 双通道 |
| 双向通信 | FPGA 既能接收也能发送 |
五、思考题
- 如何设计不定长指令——命令码后面跟变长的参数?
- 怎样加上帧头+校验和提高指令可靠性?
- 如果同时收到按键和 UART 指令,如何仲裁优先级?
- 能否把指令集扩展为 AT 命令风格(如
AT+LED=1,ON\r\n)?
附录:源码清单
top.v
// =============================================================================
// exp_08_uart_cmd - UART 指令控制 LED
// 芯片: GW1N-LV9LQ144C6/I5, 时钟: 50MHz (Pin 98)
//
// 功能:
// 通过 UART (115200bps, 8N1) 接收 PC 端指令控制 LED
// 指令格式: [命令码 1B] [参数 1B], FPGA 回复 [0xAA] [命令码] [结果码]
// 支持 6 种指令: LED开关/闪烁频率/流水方向/全亮全灭/查询状态/复位
// 本地按键 (KEY1~3) 也可独立控制 LED (与 UART 指令互不冲突)
//
// ⚠️ 引脚复用: RXD(Pin11)/TXD(Pin12) 默认 SSPI, build.tcl 必须
// set_option -use_sspi_as_gpio 1
// =============================================================================
module top (
input wire clk_50m, // 50MHz 系统时钟 (Pin 98)
input wire rxd, // UART 接收 (Pin 11, PULL_UP)
output reg txd, // UART 发送 (Pin 12)
input wire [2:0] key, // 按键 [KEY1,KEY2,KEY3] (Pin 69/70/71)
output reg [2:0] led // LED 输出 (Pin 8/9/10)
);
// =========================================================================
// 参数定义
// =========================================================================
// 波特率 115200: 50,000,000 / 115,200 ≈ 434 cycles/bit
localparam BAUD_DIV = 434;
localparam BAUD_CNT_W = 10;
localparam HALF_BAUD = BAUD_DIV / 2;
// 消抖: 20ms
localparam DEBOUNCE_MAX = 1_000_000 - 1;
localparam DB_CNT_W = 20;
// LED 闪烁分频 (用于闪烁模式)
localparam BLINK_SLOW = 26'd25_000_000; // 1Hz
// 指令超时: 1s = 50M cycles (防止半帧卡死)
localparam CMD_TIMEOUT = 28'd50_000_000;
// =========================================================================
// 命令码定义
// =========================================================================
localparam CMD_LED_ONOFF = 8'h01; // LED 开关
localparam CMD_LED_BLINK = 8'h02; // LED 闪烁频率
localparam CMD_FLOW_DIR = 8'h03; // 流水方向
localparam CMD_ALL = 8'h04; // 全亮/全灭
localparam CMD_QUERY = 8'h05; // 查询状态
localparam CMD_RESET = 8'hFF; // 复位
// 回复结果码
localparam REPLY_OK = 8'h00;
localparam REPLY_ERR = 8'h01;
// =========================================================================
// 内部信号 - 波特率
// =========================================================================
reg [BAUD_CNT_W-1:0] baud_cnt;
wire baud_tick;
// =========================================================================
// 内部信号 - UART 接收
// =========================================================================
reg [2:0] rx_sync;
wire rx_synced;
reg [3:0] rx_state;
reg [3:0] rx_bit_cnt;
reg [BAUD_CNT_W-1:0] rx_baud_cnt;
reg [7:0] rx_shift;
reg [7:0] rx_data;
reg rx_done;
// =========================================================================
// 内部信号 - 指令解析
// =========================================================================
reg [1:0] cmd_state; // 0=等待命令, 1=等待参数
reg [7:0] cmd_byte; // 收到的命令码
reg [7:0] param_byte; // 收到的参数
reg [27:0] cmd_timeout; // 指令超时计时器
reg cmd_exec; // 命令执行脉冲
// =========================================================================
// 内部信号 - LED 控制寄存器 (统一在一个 always 块中驱动)
// =========================================================================
reg [2:0] led_reg; // LED 亮灭寄存器 (1=亮)
reg [3:0] blink_speed; // 闪烁速度档位 0~9
reg flow_dir; // 流水方向: 0=左, 1=右
reg [1:0] led_mode; // 0=静态, 1=流水, 2=闪烁
// =========================================================================
// 内部信号 - 指令执行
// =========================================================================
reg cmd_done; // 指令执行完成, 触发回复
// =========================================================================
// 内部信号 - UART 发送
// =========================================================================
reg [3:0] tx_state;
reg [3:0] tx_bit_cnt;
reg [7:0] tx_data_reg;
reg tx_req; // 发送请求 (电平信号)
reg [23:0] reply_buf; // 回复帧缓冲: [0xAA, cmd, result]
reg [1:0] reply_idx; // 当前发送字节索引 0/1/2
// =========================================================================
// 内部信号 - 按键消抖
// =========================================================================
reg [2:0] key_sync0, key_sync1;
reg [2:0] key_stable;
reg [DB_CNT_W-1:0] key_db_cnt [0:2];
reg [2:0] key_prev;
wire [2:0] key_pressed;
// =========================================================================
// 1. 波特率发生器
// =========================================================================
always @(posedge clk_50m) begin
if (baud_cnt == BAUD_DIV - 1)
baud_cnt <= {BAUD_CNT_W{1'b0}};
else
baud_cnt <= baud_cnt + 1'b1;
end
assign baud_tick = (baud_cnt == BAUD_DIV - 1);
// =========================================================================
// 2. UART 接收器
// =========================================================================
always @(posedge clk_50m) begin
rx_sync <= {rx_sync[1:0], rxd};
end
assign rx_synced = rx_sync[2];
localparam RX_IDLE = 4'd0;
localparam RX_START = 4'd1;
localparam RX_DATA = 4'd2;
localparam RX_STOP = 4'd3;
localparam RX_DONE = 4'd4;
always @(posedge clk_50m) begin
case (rx_state)
RX_IDLE: begin
rx_done <= 1'b0;
if (rx_synced == 1'b0) begin
rx_state <= RX_START;
rx_baud_cnt <= {BAUD_CNT_W{1'b0}};
end
end
RX_START: begin
if (rx_baud_cnt == HALF_BAUD - 1) begin
if (rx_synced == 1'b0) begin
rx_state <= RX_DATA;
rx_bit_cnt <= 4'd0;
rx_baud_cnt <= {BAUD_CNT_W{1'b0}};
end else begin
rx_state <= RX_IDLE;
end
end else begin
rx_baud_cnt <= rx_baud_cnt + 1'b1;
end
end
RX_DATA: begin
if (rx_baud_cnt == BAUD_DIV - 1) begin
rx_shift[rx_bit_cnt] <= rx_synced;
rx_baud_cnt <= {BAUD_CNT_W{1'b0}};
if (rx_bit_cnt == 4'd7) begin
rx_state <= RX_STOP;
end else begin
rx_bit_cnt <= rx_bit_cnt + 1'b1;
end
end else begin
rx_baud_cnt <= rx_baud_cnt + 1'b1;
end
end
RX_STOP: begin
if (rx_baud_cnt == BAUD_DIV - 1) begin
rx_state <= RX_DONE;
rx_data <= rx_shift;
rx_done <= 1'b1;
end else begin
rx_baud_cnt <= rx_baud_cnt + 1'b1;
end
end
RX_DONE: begin
rx_done <= 1'b0;
rx_state <= RX_IDLE;
end
default: rx_state <= RX_IDLE;
endcase
end
// =========================================================================
// 3. 指令解析器 (命令码 + 参数)
// =========================================================================
localparam CMD_WAIT = 2'd0;
localparam CMD_PARAM = 2'd1;
always @(posedge clk_50m) begin
cmd_exec <= 1'b0;
if (rx_done) begin
cmd_timeout <= 28'd0;
case (cmd_state)
CMD_WAIT: begin
cmd_byte <= rx_data;
cmd_state <= CMD_PARAM;
end
CMD_PARAM: begin
param_byte <= rx_data;
cmd_exec <= 1'b1; // 触发执行
cmd_state <= CMD_WAIT;
end
endcase
end else begin
// 超时保护: 1s 未收到完整帧则回到等待命令
if (cmd_timeout == CMD_TIMEOUT) begin
cmd_state <= CMD_WAIT;
cmd_timeout <= 28'd0;
end else if (cmd_state == CMD_PARAM) begin
cmd_timeout <= cmd_timeout + 1'b1;
end
end
end
// =========================================================================
// 4. 指令执行 + 回复触发
// =========================================================================
always @(posedge clk_50m) begin
cmd_done <= 1'b0; // 默认清零
// ---- 指令执行 ----
if (cmd_exec) begin
case (cmd_byte)
CMD_LED_ONOFF: begin
led_reg <= param_byte[2:0];
led_mode <= 2'd0;
reply_buf <= {8'hAA, cmd_byte, REPLY_OK};
cmd_done <= 1'b1;
end
CMD_LED_BLINK: begin
blink_speed <= param_byte[3:0];
led_mode <= 2'd2;
reply_buf <= {8'hAA, cmd_byte, REPLY_OK};
cmd_done <= 1'b1;
end
CMD_FLOW_DIR: begin
flow_dir <= param_byte[0];
led_mode <= 2'd1;
reply_buf <= {8'hAA, cmd_byte, REPLY_OK};
cmd_done <= 1'b1;
end
CMD_ALL: begin
led_reg <= {param_byte[2], param_byte[1], param_byte[0]};
led_mode <= 2'd0;
reply_buf <= {8'hAA, cmd_byte, REPLY_OK};
cmd_done <= 1'b1;
end
CMD_QUERY: begin
// 返回当前 LED 状态
reply_buf <= {8'hAA, cmd_byte, {5'd0, led_reg}};
cmd_done <= 1'b1;
end
CMD_RESET: begin
if (param_byte == 8'hAA) begin
led_reg <= 3'b000;
blink_speed <= 4'd0;
flow_dir <= 1'b0;
led_mode <= 2'd0;
reply_buf <= {8'hAA, cmd_byte, REPLY_OK};
end else begin
reply_buf <= {8'hAA, cmd_byte, REPLY_ERR};
end
cmd_done <= 1'b1;
end
default: begin
reply_buf <= {8'hAA, cmd_byte, REPLY_ERR};
cmd_done <= 1'b1;
end
endcase
// ---- 按键控制 ----
end else if (key_pressed[0]) begin
led_reg[0] <= ~led_reg[0];
end else if (key_pressed[1]) begin
led_reg[1] <= ~led_reg[1];
end else if (key_pressed[2]) begin
led_reg <= ~led_reg;
end
end
// =========================================================================
// 5. UART 发送器 (回复确认帧, tx_req 电平触发)
// =========================================================================
localparam TX_IDLE = 4'd0;
localparam TX_START = 4'd1;
localparam TX_DATA = 4'd2;
localparam TX_STOP = 4'd3;
always @(posedge clk_50m) begin
// TX状态机检测cmd_done → 锁存tx_req
if (cmd_done) begin
tx_req <= 1'b1;
reply_idx <= 2'd0;
end
if (baud_tick) begin
case (tx_state)
TX_IDLE: begin
txd <= 1'b1;
if (tx_req) begin
tx_state <= TX_START;
tx_bit_cnt <= 4'd0;
tx_req <= 1'b0; // 清除请求
// 选择当前要发送的字节
case (reply_idx)
2'd0: tx_data_reg <= reply_buf[23:16]; // 0xAA
2'd1: tx_data_reg <= reply_buf[15:8]; // cmd
2'd2: tx_data_reg <= reply_buf[7:0]; // result
endcase
end
end
TX_START: begin
txd <= 1'b0;
tx_state <= TX_DATA;
tx_bit_cnt <= 4'd0;
end
TX_DATA: begin
txd <= tx_data_reg[tx_bit_cnt];
if (tx_bit_cnt == 4'd7)
tx_state <= TX_STOP;
else
tx_bit_cnt <= tx_bit_cnt + 1'b1;
end
TX_STOP: begin
txd <= 1'b1;
if (reply_idx == 2'd2) begin
tx_state <= TX_IDLE; // 最后一字节完成
end else begin
reply_idx <= reply_idx + 1'b1;
tx_state <= TX_START; // 继续发下一字节
end
end
default: tx_state <= TX_IDLE;
endcase
end
end
// =========================================================================
// 6. 按键消抖 (3 路)
// =========================================================================
genvar ki;
generate
for (ki = 0; ki < 3; ki = ki + 1) begin : key_gen
always @(posedge clk_50m) begin
key_sync0[ki] <= key[ki];
key_sync1[ki] <= key_sync0[ki];
end
always @(posedge clk_50m) begin
if (key_sync1[ki] != key_stable[ki]) begin
if (key_db_cnt[ki] == DEBOUNCE_MAX) begin
key_stable[ki] <= key_sync1[ki];
key_db_cnt[ki] <= {DB_CNT_W{1'b0}};
end else begin
key_db_cnt[ki] <= key_db_cnt[ki] + 1'b1;
end
end else begin
key_db_cnt[ki] <= {DB_CNT_W{1'b0}};
end
end
end
endgenerate
always @(posedge clk_50m) begin
key_prev <= key_stable;
end
assign key_pressed = key_prev & ~key_stable;
// =========================================================================
// 7. LED 动态驱动 (流水/闪烁)
// =========================================================================
reg [25:0] flow_timer;
always @(posedge clk_50m) begin
case (led_mode)
2'd0: begin
// 静态模式: 直接输出
led <= led_reg;
end
2'd1: begin
// 流水灯模式 (0.3s/步)
if (flow_timer == 26'd15_000_000) begin
flow_timer <= 26'd0;
if (flow_dir == 1'b0)
led <= {led[1:0], led[2]}; // 左移
else
led <= {led[0], led[2:1]}; // 右移
end else begin
flow_timer <= flow_timer + 1'b1;
end
end
2'd2: begin
// 闪烁模式
if (flow_timer == BLINK_SLOW) begin
flow_timer <= 26'd0;
led <= (led == 3'b000) ? led_reg : 3'b000;
end else begin
flow_timer <= flow_timer + 1'b1;
end
end
default: led <= led_reg;
endcase
end
endmodule