UART(Universal Asynchronous Receiver/Transmitter)作为最古老的串行通信协议之一,至今仍在嵌入式系统和FPGA设计中占据重要地位。与SPI、I2C等同步协议不同,UART采用异步通信机制,仅需两根信号线(TX和RX)即可实现全双工通信,这种简洁性使其成为设备间短距离通信的首选方案。
在FPGA中实现UART控制器具有独特优势:首先,我们可以完全自定义波特率、数据格式和缓冲区大小;其次,FPGA的并行处理能力可以轻松实现多通道UART;最重要的是,通过硬件描述语言实现的UART核可以无缝集成到更大的系统中。我曾在多个工业控制项目中采用FPGA实现自定义UART协议,相比现成的UART芯片,这种方案在时序控制和协议扩展性方面表现更优。
一个完整的UART帧包含以下几个关键部分:
空闲状态:TX线持续保持高电平(逻辑1),这个设计源于早期电传打字机的空闲状态标准。在实际应用中,我建议在代码中明确添加空闲状态检测逻辑,这能有效避免半双工通信中的冲突。
起始位(低电平)的1位宽度设计考虑了噪声容限。在FPGA实现时,我通常会采用过采样技术(通常16倍)来准确检测起始位边缘。具体做法是:当检测到连续3个低电平采样点时,才确认有效的起始位,这能有效抑制毛刺干扰。
数据位的传输顺序(LSB first)有其历史原因——早期串行设备采用简单的移位寄存器设计。在Verilog实现时,发送端的移位寄存器代码如下:
verilog复制always @(posedge clk) begin
if (tx_start) begin
tx_shift_reg <= {1'b1, data[7:0], 1'b0}; // 停止位+数据+起始位
end else begin
tx_shift_reg <= {tx_shift_reg[9:0], 1'b1}; // 右移发送
end
end
波特率误差必须控制在±2%以内(理想应<±1%)。以常见的100MHz系统时钟为例,产生115200bps的时钟分频计算如下:
code复制分频系数 = 系统时钟频率 / (波特率 × 过采样率)
= 100,000,000 / (115200 × 16)
≈ 54.25
实际实现时,我采用累加器方案来精确控制:
verilog复制reg [15:0] baud_acc;
always @(posedge clk) begin
baud_acc <= baud_acc + 16'd1152; // 1152 = (115200 << 4)/100MHz*65536
if (baud_acc[15]) begin
baud_ce <= 1'b1;
baud_acc <= baud_acc - 16'd32768 + 16'd1152;
end else begin
baud_ce <= 1'b0;
end
end
重要提示:对于高精度应用,建议使用PLL生成专用波特率时钟,或者选择系统时钟频率能被常见波特率整除的值(如125MHz系统时钟可精确生成115200bps)
发送状态机应包含以下状态:
verilog复制localparam [1:0]
TX_IDLE = 2'b00,
TX_START = 2'b01,
TX_DATA = 2'b10,
TX_STOP = 2'b11;
always @(posedge clk) begin
case (tx_state)
TX_IDLE: if (tx_trig) begin
tx_state <= TX_START;
tx_bit_cnt <= 3'd0;
end
TX_START: if (baud_ce) begin
tx_state <= TX_DATA;
end
TX_DATA: if (baud_ce) begin
if (tx_bit_cnt == 3'd7)
tx_state <= TX_STOP;
tx_bit_cnt <= tx_bit_cnt + 1'b1;
end
TX_STOP: if (baud_ce) begin
tx_state <= TX_IDLE;
end
endcase
end
接收端面临的主要挑战是时钟同步和噪声抑制,我的解决方案是:
verilog复制always @(posedge clk) begin
rx_sync1 <= UART_RX;
rx_sync2 <= rx_sync1;
rx_sync3 <= rx_sync2;
end
verilog复制always @(posedge clk) begin
if (rx_sync3 == rx_filtered)
rx_counter <= 4'd0;
else if (rx_counter == 4'd15)
rx_filtered <= ~rx_filtered;
else
rx_counter <= rx_counter + 1'b1;
end
verilog复制always @(posedge clk) begin
if (baud_ce) begin
case (sample_cnt)
5'd0: if (~rx_filtered) start_detect <= 1'b1;
5'd7: if (start_detect) rx_data[0] <= rx_filtered;
5'd23: rx_data[1] <= rx_filtered;
// ... 其他数据位采样点
default: ;
endcase
sample_cnt <= sample_cnt + 1'b1;
end
end
在FPGA中实现UART时,我强烈建议使用异步FIFO来解决跨时钟域问题。关键设计包括:
verilog复制// 写指针生成
always @(posedge wr_clk) begin
if (wr_en && !full) begin
wr_ptr_bin <= wr_ptr_bin + 1;
wr_ptr_gray <= bin2gray(wr_ptr_bin + 1);
end
end
// 读指针同步
always @(posedge wr_clk) begin
rd_ptr_gray_sync1 <= rd_ptr_gray;
rd_ptr_gray_sync2 <= rd_ptr_gray_sync1;
end
verilog复制assign full = (wr_ptr_gray == {~rd_ptr_gray_sync2[ADDR_WIDTH],
rd_ptr_gray_sync2[ADDR_WIDTH-1:0]});
assign empty = (rd_ptr_gray == wr_ptr_gray_sync2);
对于高速UART(≥115200bps),建议实现RTS/CTS流控:
verilog复制// RTS生成逻辑
assign UART_RTS = (fifo_count > FIFO_HIGH_WATERMARK);
// CTS处理逻辑
always @(posedge clk) begin
if (UART_CTS) tx_hold <= 1'b0;
else if (fifo_count < FIFO_LOW_WATERMARK) tx_hold <= 1'b1;
end
我常用的UART验证方法包括:
verilog复制assign UART_RX = UART_TX; // 最简单的自发自收
verilog复制always @(negedge UART_TX) begin
$display("Start bit detected at %t", $time);
#(BIT_PERIOD*1.5);
for (int i=0; i<8; i++) begin
rx_byte[i] = UART_TX;
#BIT_PERIOD;
end
$display("Received byte: %h", rx_byte);
end
verilog复制reg [15:0] error_count;
always @(posedge clk) begin
if (fifo_overflow) error_count <= error_count + 1;
end
在最近的一个工业控制器项目中,我们通过以下优化使UART稳定性显著提升: