最近系统梳理了一些在嵌入式与硬件开发中常用的通信协议,并探讨了它们在FPGA平台上的实现方法。本文将作为上篇,重点分析UART和PS/2协议的原理与硬件设计。

1. UART协议详解与FPGA实现
UART(通用异步收发传输器)是一种广泛使用的异步串行通信接口。它实际上是RS-232、RS-449等接口标准所依赖的底层通信核心,这些标准主要规定了电气特性、连接方式等物理层内容,而UART则定义了数据帧格式与传输控制逻辑,是理解网络通信协议的基础之一。
在深入代码前,先明确几个关键概念:
- 波特率:每秒传输的二进制位数(bps),如9600、115200。
- 起始位:标志一帧数据开始,为一个逻辑“0”电平。
- 数据位:实际传输的数据,长度可以是5、7或8位,传输顺序为最低位(LSB)在前。
- 奇偶校验位:用于简单的错误检测,发送方会根据数据位中“1”的个数自动填充此位,使“1”的总数保持奇数(奇校验)或偶数(偶校验)。
- 停止位:标志一帧数据结束,为逻辑“1”电平,长度可以是1、1.5或2位。
- 空闲位:线路无数据传输时的状态,保持为逻辑“1”。
时序图如下:

数据传输过程:
- 发送:线路空闲时为高电平。发送指令触发后,先拉低一个位时间作为起始位,随后依次发送数据位(从最低位开始)、校验位和停止位(高电平)。
- 接收:接收端持续检测线路。一旦发现下降沿(从高到低的跳变),便启动接收流程,按照约定的波特率采样并组装数据位,最后验证校验位。
由于UART没有专用的时钟线进行同步,为保证可靠性,通常采用过采样技术。例如,用16倍于波特率的时钟对每个数据位采样16次,取中间位置的采样值作为有效值,以此来对抗噪声和时序偏差,避免误码。
1.1 Verilog设计实例:UART回环测试
下面展示一个完整的UART回环实例,包含接收、发送和波特率生成模块。
1. 接收模块 (uart_rx):
module uart_rx(
input rxd,
input clk,
output receive_ack,
output reg [7:0] data_i
);
parameter IDLE = 0;
parameter RECEIVE = 1;
parameter RECEIVE_END = 2;
reg [3:0] CS, NS;
reg [4:0] count;
reg [7:0] data_o_tmp;
always @(posedge clk)
CS <= NS;
always @(*) begin
NS <= CS;
case(CS)
IDLE: if(!rxd) NS = RECEIVE;
RECEIVE: if(count == 7) NS = RECEIVE_END;
else NS = NS;
RECEIVE_END: NS = IDLE;
default: NS = IDLE;
endcase
end
always @(posedge clk)
if(CS == RECEIVE)
count <= count + 1;
else if(CS == IDLE | CS == RECEIVE_END)
count <= 0;
always @(posedge clk)
if(CS == RECEIVE) begin
data_i[6:0] <= data_i[7:1];
data_i[7] <= rxd;
end
assign receive_ack = (CS == RECEIVE_END) ? 1 : 0;
endmodule
2. 发送模块 (uart_tx):
module uart_tx(
input [7:0] data_o,
input clk,
input receive_ack,
output reg txd
);
parameter IDLE = 0;
parameter SEND_START = 1;
parameter SEND_DATA = 2;
parameter SEND_END = 3;
reg [3:0] CS, NS;
reg [4:0] count;
reg [7:0] data_o_tmp;
always @ (posedge clk)
CS <= NS;
always @ (*) begin
NS <= CS;
case(CS)
IDLE: begin if(receive_ack) NS = SEND_START; end
SEND_START: begin NS = SEND_DATA; end
SEND_DATA: begin if(count == 7) NS = SEND_END; end
SEND_END: begin if(receive_ack) NS = SEND_START; end
default: NS = IDLE;
endcase
end
always @(posedge clk)
if(CS == SEND_START)
count <= count + 1;
else if(CS == IDLE | CS == SEND_END)
count <= 0;
else
count <= count;
always @(posedge clk)
if(CS == SEND_START)
data_o_tmp <= data_o;
else if(CS == SEND_DATA)
data_o_tmp[6:0] <= data_o_tmp[7:1];
always @(posedge clk)
if(CS == SEND_START)
txd <= 0;
else if(CS == SEND_DATA)
txd <= data_o_tmp;
else if(CS == SEND_END)
txd <= 1;
endmodule
3. 波特率时钟生成模块 (clk_div):
module clk_div(
input clk,
output reg clk_out
);
parameter baud_rata = 9600;
parameter div_num = 'd125_000_000 / baud_rata; // 分频数 = 系统时钟频率 / 目标波特率
reg [15:0] num;
always @(posedge clk) begin
if(num == div_num) begin
num <= 0;
clk_out <= 1;
end
else begin
num <= num + 1;
clk_out <= 0;
end
end
endmodule
4. 顶层模块 (uart_top):
module uart_top(
input clk,
input rxd,
output txd
);
wire clk_9600;
wire receive_ack;
wire [7:0] data;
uart_tx u_tx (
.clk (clk_9600),
.txd (txd),
.data_o (data),
.receive_ack(receive_ack)
);
uart_rx u_rx (
.clk (clk_9600),
.rxd (rxd),
.data_i (data),
.receive_ack(receive_ack)
);
clk_div u_div (
.clk (clk),
.clk_out (clk_9600)
);
endmodule

2. PS/2协议详解与FPGA实现
PS/2是一种双向同步串行通信协议,早期广泛用于连接键盘和鼠标。其接口为6针,但核心是时钟(Clock) 和数据(Data) 两根双向信号线(另加VCC和GND)。通信双方通过时钟同步,任何一方可通过将时钟线拉低来抑制对方发送数据。在PC架构中,主机(PC)拥有总线控制权。
PS/2的数据帧通常包含11-12位:
- 1个起始位(始终为逻辑0)
- 8个数据位(低位在前)
- 1个奇偶校验位(奇校验)
- 1个停止位(始终为逻辑1)
- 1个应答位(仅出现在主机发送数据给设备时)
PS/2设备到主机的时序图如下:

当时钟信号由设备产生并出现下降沿时,主机应读取数据线上的值。
2.1 Verilog设计实例:PS/2键盘解码
以下代码实现了FPGA作为主机,读取PS/2键盘扫描码的功能。设计中加入了数字滤波以消除机械按键带来的信号抖动,这是一种常见的数字逻辑设计技巧。
module ps2_keyboard(
input clk,
input clr,
input PS2C, // ps2 clock in
input PS2D, // ps2 data in
output [15:0] xkey
);
reg PS2CF;
reg PS2DF;
reg [7:0] ps2c_filter;
reg [7:0] ps2d_filter;
reg [10:0] shift1;
reg [10:0] shift2;
assign xkey = { shift2[8:1], shift1[8:1] }; // 组合两个字节的键码
// 数字滤波进程:消除时钟和数据线上的毛刺
always @(posedge clk or posedge clr) begin
if(clr) begin
ps2c_filter <= 8'b0;
ps2d_filter <= 8'b0;
PS2CF <= 1;
PS2DF <= 1;
end
else begin
ps2c_filter[7] <= PS2C;
ps2c_filter[6:0] <= ps2c_filter[7:1];
ps2d_filter[7] <= PS2D;
ps2d_filter[6:0] <= ps2d_filter[7:1];
if(ps2c_filter == 8'b1111_1111)
PS2CF <= 1; // 滤波后时钟为高
else if(ps2c_filter == 8'b0000_0000)
PS2CF <= 0; // 滤波后时钟为低
if(ps2d_filter == 8'b1111_1111)
PS2DF <= 1; // 滤波后数据为高
else if(ps2d_filter == 8'b0000_0000)
PS2DF <= 0; // 滤波后数据为低
end
end
// 数据移位采集进程:在滤波后时钟的下降沿采集数据位
always @(negedge PS2CF or posedge clr) begin
if(clr) begin
shift1 <= 11'b0;
shift2 <= 11'b0;
end
else begin
shift1 <= {PS2DF, shift1[10:1]};
shift2 <= {shift1[0], shift2[10:1]};
end
end
endmodule
参考文献:
[1] 汤勇明,张圣清,陆佳华. 搭建你的数字积木-数字电路与逻辑设计[M]. 北京:清华大学出版社,2017.