设计并实现UART串口通信控制器,实现8位复古电脑与PC之间的数据传输。UART是最古老也最持久的通信协议之一——从1970年代的终端到今天的嵌入式调试,它始终是芯片间通信的基础。
UART(Universal Asynchronous Receiver-Transmitter)即通用异步收发器。它的核心特点是异步——没有时钟线,收发双方靠约定波特率来同步。这与PS/2截然不同:
| 特性 | PS/2 | UART |
|---|---|---|
| 时钟 | 设备驱动时钟线 | 无时钟线,约定波特率 |
| 同步方式 | 硬件同步 | 异步(起始位同步) |
| 连线 | 4线(VCC/GND/CLK/DATA) | 3线(VCC/GND/TX/RX交叉) |
| 速率 | 10-16.7 Kbps | 300-115200 bps+ |
| 用途 | 键盘/鼠标 | 终端/调试/模块通信 |
UART没有时钟线,收发双方必须使用相同的波特率。波特率决定了每个比特的持续时间:
| 波特率 | 比特时间 | 分频系数(25MHz) | 每秒字符(8N1) |
|---|---|---|---|
| 9600 | 104.17 μs | 2604 | 960 |
| 19200 | 52.08 μs | 1302 | 1920 |
| 38400 | 26.04 μs | 651 | 3840 |
| 57600 | 17.36 μs | 434 | 5760 |
| 115200 | 8.68 μs | 217 | 11520 |
// uart.v - 可配置波特率UART收发器
module uart (
input wire clk,
input wire rst_n,
input wire [15:0] baud_div, // 波特率分频 = clk_freq/baud
// TX接口
input wire [7:0] tx_data,
input wire tx_start,
output reg tx_busy,
output reg tx_done,
// RX接口
output reg [7:0] rx_data,
output reg rx_ready,
output reg rx_error,
input wire rx_ack,
// 物理接口
output reg uart_tx,
input wire uart_rx
);
// ======== 发送器 TX ========
localparam TX_IDLE=3'd0, TX_START=3'd1,
TX_DATA=3'd2, TX_PARITY=3'd3,
TX_STOP=3'd4;
reg [2:0] tx_state;
reg [15:0] tx_baud_cnt;
reg [2:0] tx_bit_cnt;
reg [7:0] tx_shift;
reg tx_parity;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx_state <= TX_IDLE; tx_baud_cnt <= 16'd0;
tx_busy <= 1'b0; tx_done <= 1'b0;
uart_tx <= 1'b1;
end else begin
tx_done <= 1'b0;
case (tx_state)
TX_IDLE: begin
uart_tx <= 1'b1;
if (tx_start) begin
tx_shift <= tx_data;
tx_parity <= 1'b0;
tx_bit_cnt <= 3'd0;
tx_baud_cnt <= baud_div - 16'd1;
tx_busy <= 1'b1;
uart_tx <= 1'b0; // 起始位
tx_state <= TX_START;
end
end
TX_START: begin
if (tx_baud_cnt == 16'd0) begin
tx_baud_cnt <= baud_div - 16'd1;
uart_tx <= tx_shift[0];
tx_parity <= tx_parity ^ tx_shift[0];
tx_state <= TX_DATA;
end else
tx_baud_cnt <= tx_baud_cnt - 16'd1;
end
TX_DATA: begin
if (tx_baud_cnt == 16'd0) begin
tx_baud_cnt <= baud_div - 16'd1;
tx_bit_cnt <= tx_bit_cnt + 3'd1;
tx_shift <= tx_shift >> 1;
if (tx_bit_cnt == 3'd7) begin
uart_tx <= ~tx_parity;
tx_state <= TX_PARITY;
end else begin
uart_tx <= tx_shift[1];
tx_parity <= tx_parity ^ tx_shift[1];
end
end else
tx_baud_cnt <= tx_baud_cnt - 16'd1;
end
TX_PARITY: begin
if (tx_baud_cnt == 16'd0) begin
tx_baud_cnt <= baud_div - 16'd1;
uart_tx <= 1'b1; // 停止位
tx_state <= TX_STOP;
end else
tx_baud_cnt <= tx_baud_cnt - 16'd1;
end
TX_STOP: begin
if (tx_baud_cnt == 16'd0) begin
tx_busy <= 1'b0;
tx_done <= 1'b1;
tx_state <= TX_IDLE;
end else
tx_baud_cnt <= tx_baud_cnt - 16'd1;
end
endcase
end
end
// ======== 接收器 RX ========
reg [1:0] rx_sync;
always @(posedge clk) rx_sync <= {rx_sync[0], uart_rx};
wire rx_falling = (rx_sync == 2'b10);
localparam RX_IDLE=3'd0, RX_START=3'd1,
RX_DATA=3'd2, RX_PARITY=3'd3,
RX_STOP=3'd4;
reg [2:0] rx_state;
reg [15:0] rx_baud_cnt;
reg [2:0] rx_bit_cnt;
reg [7:0] rx_shift;
reg rx_parity;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_state <= RX_IDLE; rx_data <= 8'd0;
rx_ready <= 1'b0; rx_error <= 1'b0;
end else begin
if (rx_ack && rx_ready) rx_ready <= 1'b0;
case (rx_state)
RX_IDLE:
if (rx_falling) begin
rx_baud_cnt <= (baud_div >> 1) - 16'd1;
rx_state <= RX_START;
end
RX_START:
if (rx_baud_cnt == 16'd0) begin
if (rx_sync[1] == 1'b0) begin
rx_baud_cnt <= baud_div - 16'd1;
rx_bit_cnt <= 3'd0;
rx_parity <= 1'b0;
rx_state <= RX_DATA;
end else
rx_state <= RX_IDLE;
end else
rx_baud_cnt <= rx_baud_cnt - 16'd1;
RX_DATA:
if (rx_baud_cnt == 16'd0) begin
rx_baud_cnt <= baud_div - 16'd1;
rx_shift <= {rx_sync[1], rx_shift[7:1]};
rx_parity <= rx_parity ^ rx_sync[1];
rx_bit_cnt <= rx_bit_cnt + 3'd1;
if (rx_bit_cnt == 3'd7)
rx_state <= RX_PARITY;
end else
rx_baud_cnt <= rx_baud_cnt - 16'd1;
RX_PARITY:
if (rx_baud_cnt == 16'd0) begin
if (rx_parity ^ rx_sync[1])
rx_state <= RX_STOP;
else begin
rx_error <= 1'b1;
rx_state <= RX_IDLE;
end
end else
rx_baud_cnt <= rx_baud_cnt - 16'd1;
RX_STOP:
if (rx_baud_cnt == 16'd0) begin
if (rx_sync[1]) begin
rx_data <= {rx_sync[1], rx_shift[7:1]};
rx_ready <= 1'b1;
end else
rx_error <= 1'b1;
rx_state <= RX_IDLE;
end else
rx_baud_cnt <= rx_baud_cnt - 16'd1;
endcase
end
end
endmodule
verilator --lint-only 检查,无错误无警告。
// tb_uart.v - UART收发器仿真测试
module tb_uart;
reg clk, rst_n;
reg [15:0] baud_div;
reg [7:0] tx_data;
reg tx_start;
wire tx_busy, tx_done;
wire [7:0] rx_data;
wire rx_ready, rx_error;
reg rx_ack;
wire uart_tx, uart_rx;
// 自环:TX输出连接到RX输入
assign uart_rx = uart_tx;
uart uut (.*);
initial clk = 0;
always #20 clk = ~clk; // 25MHz
localparam BAUD_9600 = 16'd2604;
integer test_count;
integer pass_count;
initial begin
rst_n = 0; tx_start = 0; rx_ack = 0;
baud_div = BAUD_9600;
test_count = 0; pass_count = 0;
#100; rst_n = 1;
// 测试1:发送0x55 (01010101,交替位模式)
$display("--- Test 1: Send 0x55 ---");
tx_data = 8'h55; tx_start = 1;
#40; tx_start = 0;
wait (tx_done);
wait (rx_ready);
test_count = test_count + 1;
if (rx_data == 8'h55) begin
$display(" PASS: Loopback 0x55 received");
pass_count = pass_count + 1;
end else
$display(" FAIL: Expected 0x55, got 0x%02h", rx_data);
rx_ack = 1; #40; rx_ack = 0;
// 测试2:发送0xAA (10101010)
$display("--- Test 2: Send 0xAA ---");
tx_data = 8'hAA; tx_start = 1;
#40; tx_start = 0;
wait (tx_done);
wait (rx_ready);
test_count = test_count + 1;
if (rx_data == 8'hAA) begin
$display(" PASS: Loopback 0xAA received");
pass_count = pass_count + 1;
end else
$display(" FAIL: Expected 0xAA, got 0x%02h", rx_data);
rx_ack = 1; #40; rx_ack = 0;
// 测试3:发送0x00 (全0)
$display("--- Test 3: Send 0x00 ---");
tx_data = 8'h00; tx_start = 1;
#40; tx_start = 0;
wait (tx_done);
wait (rx_ready);
test_count = test_count + 1;
if (rx_data == 8'h00 && !rx_error) begin
$display(" PASS: Loopback 0x00 received");
pass_count = pass_count + 1;
end else
$display(" FAIL: 0x00 test, got 0x%02h err=%b", rx_data, rx_error);
$display("--- Results: %0d/%0d passed ---", pass_count, test_count);
$finish;
end
endmodule
| 地址 | 读 | 写 |
|---|---|---|
| $FF00 | UART接收数据 | UART发送数据 |
| $FF01 | 状态寄存器 | 波特率低字节 |
| $FF02 | — | 波特率高字节 |
状态寄存器位定义:
| 位 | 名称 | 含义 |
|---|---|---|
| 7 | TX_BUSY | 1=发送器忙 |
| 6 | TX_DONE | 1=发送完成(读后清零) |
| 5 | RX_READY | 1=接收数据就绪 |
| 4 | RX_ERROR | 1=帧错误或校验错误 |
| 3:0 | — | 保留 |
为UART的TX和RX各添加16字节FIFO缓冲。TX FIFO让CPU可以连续写入多个字节而不需要等待每次发送完成;RX FIFO防止CPU来不及读取时丢失数据。参照第19课的FIFO设计。
实现UART中断:RX就绪时产生中断(FIFO非空或达到半满阈值),TX完成时产生中断。编写中断服务程序的汇编代码框架,实现全双工中断驱动通信。
实现波特率自动检测:当收到第一个字节时,测量起始位和第一个数据位的宽度,自动计算波特率分频系数。提示:可以用系统时钟计数起始位(从下降沿到上升沿)的周期数来估算。
真实RS-232使用±12V电平(逻辑0=+3~+15V,逻辑1=-3~-15V),与TTL(0/5V)相反。研究MAX232芯片的工作原理,画出TTL↔RS-232的电平转换电路图。在Verilog仿真中,如何模拟这种反相逻辑?
你实现了完整的UART串口控制器!这包括:
你的8位电脑现在可以与外部世界"对话"了!串口是调试和程序加载的命脉。