跳到主要内容

实验 08:UART 指令控制 LED

🛒 开发板购买: EK303 GW1N FPGA核心板


一、实验目的

  1. 掌握 UART 指令解析——从接收字节中识别命令并执行对应操作。
  2. 理解通信协议设计——定义简单的指令格式(命令码 + 参数)。
  3. 综合 UART 收发、按键消抖、LED 多模式驱动,实现 PC 端遥控 FPGA。
  4. 学习状态反馈——FPGA 执行命令后通过 UART 回复确认信息。

二、实验过程

2.1 硬件连接

信号引脚说明
CLK9850MHz
TXD12发送到 PC
RXD11接收 PC 指令
LED1~38/9/10受控 LED
KEY1~369/70/71本地按键(同实验 07)

2.2 指令协议设计

帧格式:1 字节命令 + 1 字节参数。

命令码含义参数执行动作
0x01LED 开关0x00=灭, 0x01=亮控制指定 LED 亮或灭
0x02LED 闪烁频率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_rxUART 接收(复用实验 07)
uart_txUART 发送(回复确认)
cmd_parser指令解析状态机
led_driverLED 多模式驱动(复用实验 03/05/07)
key_debounce按键消抖(本地按键同步控制)

指令解析状态机

IDLE ──(rx_done)──→ CMD_RECV (存储第一字节)

PARAM_RECV (等待第二字节)

EXECUTE (根据命令执行动作)

REPLY (发送确认帧)

IDLE

关键信号

信号位宽说明
cmd_byte8接收到的命令码
param_byte8接收到的参数
rx_byte_cnt1接收字节计数(0=命令, 1=参数)
cmd_valid1有效命令标志
reply_data24回复帧(3 字节:0xAA+命令+结果)

2.4 引脚约束

信号引脚
TXD / RXD12 / 11
LED1~38 / 9 / 10
KEY1~369 / 70 / 71

⚠️ 引脚复用:RXD (Pin 11) / TXD (Pin 12) 默认用作 SSPI。使用前须关闭复用。详见 PIN_DUAL_PURPOSE.md


三、实验现象

PC 端操作(用任意串口助手,115200-8N1,十六进制发送)

PC 发送现象FPGA 回复
01 01LED1 亮起AA 01 00
01 00LED1 熄灭AA 01 00
02 05LED1 闪烁频率变化AA 02 00
04 FF三个 LED 全部亮起AA 04 00
04 00三个 LED 全部熄灭AA 04 00
03 01流水灯方向反转AA 03 00
05 00AA 05 07(LED 状态 = 0b111)
FF AALED 全灭、恢复默认AA FF 00
99 00无效命令,LED 无变化AA 99 01

本地按键同步:按键操作与 UART 指令互不冲突,各自独立控制 LED 状态。


四、掌握知识点

知识点说明
指令协议设计命令码 + 参数 + 回复确认 的帧结构
字节流解析用计数器区分命令字节和参数字节
状态反馈执行后主动回复确认帧
错误处理无效命令码返回错误码
UART+GPIO 协同串口遥控 + 按键本地控制 双通道
双向通信FPGA 既能接收也能发送

五、思考题

  1. 如何设计不定长指令——命令码后面跟变长的参数?
  2. 怎样加上帧头+校验和提高指令可靠性?
  3. 如果同时收到按键和 UART 指令,如何仲裁优先级?
  4. 能否把指令集扩展为 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