给SoC新手的保姆级指南:用APB总线连接你的第一个UART外设(附Verilog代码)
第一次接触SoC设计时,面对各种总线协议和外设接口,很多初学者都会感到无从下手。APB作为AMBA总线家族中最简单的一员,是连接低速外设的理想选择。本文将带你从零开始,用Verilog实现一个通过APB总线控制的UART16550外设模块,并在FPGA开发板上实际运行。
1. 准备工作:理解APB总线基础
APB(Advanced Peripheral Bus)是ARM公司设计的片上总线协议,专门用于连接低速外设。与高性能的AHB或AXI总线相比,APB的协议简单得多,非常适合初学者入门。
APB总线的主要特点包括:
- 无流水线操作
- 每次传输需要2个时钟周期
- 所有信号在时钟上升沿采样
- 低功耗设计
典型的APB总线信号包括:
verilog复制input PCLK, // 总线时钟
input PRESETn, // 低电平有效复位
input PSEL, // 从设备选择
input PENABLE, // 使能信号
input PWRITE, // 读写控制
input [31:0] PADDR, // 地址总线
input [31:0] PWDATA, // 写数据总线
output [31:0] PRDATA, // 读数据总线
output PREADY, // 传输完成指示
output PSLVERR // 错误指示
提示:APB3.0协议增加了PREADY和PSLVERR信号,支持等待状态和错误报告,比早期版本更实用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. UART16550外设设计
UART(Universal Asynchronous Receiver/Transmitter)是最常用的串行通信接口之一。16550是经典的UART控制器,具有以下特性:
- 可编程波特率
- 发送/接收FIFO
- 中断支持
- 多种数据格式选择
2.1 寄存器映射设计
UART16550通常通过8个寄存器进行控制,我们将其映射到APB地址空间:
| 地址偏移 | 寄存器名称 | 访问方式 | 功能描述 |
|---|---|---|---|
| 0x00 | RBR/THR | R/W | 接收缓冲/发送保持寄存器 |
| 0x04 | IER | R/W | 中断使能寄存器 |
| 0x08 | IIR/FCR | R/W | 中断识别/FIFO控制寄存器 |
| 0x0C | LCR | R/W | 线路控制寄存器 |
| 0x10 | MCR | R/W | Modem控制寄存器 |
| 0x14 | LSR | R | 线路状态寄存器 |
| 0x18 | MSR | R | Modem状态寄存器 |
| 0x1C | SCR | R/W | 暂存寄存器 |
2.2 Verilog实现核心代码
以下是APB-UART接口的核心代码片段:
verilog复制module apb_uart (
input PCLK,
input PRESETn,
input PSEL,
input PENABLE,
input PWRITE,
input [31:0] PADDR,
input [31:0] PWDATA,
output reg [31:0] PRDATA,
output PREADY,
output PSLVERR,
// UART物理接口
output TXD,
input RXD
);
// 寄存器定义
reg [7:0] THR; // 发送保持寄存器
reg [7:0] RBR; // 接收缓冲寄存器
reg [7:0] IER; // 中断使能寄存器
reg [7:0] LCR; // 线路控制寄存器
reg [7:0] LSR; // 线路状态寄存器
// 内部信号
wire [7:0] reg_addr = PADDR[7:0]; // 只使用低8位地址
wire write_en = PSEL & PENABLE & PWRITE;
wire read_en = PSEL & PENABLE & ~PWRITE;
// 写操作处理
always @(posedge PCLK or negedge PRESETn) begin
if (!PRESETn) begin
THR <= 8'h0;
IER <= 8'h0;
LCR <= 8'h0;
end else if (write_en) begin
case (reg_addr)
4'h0: THR <= PWDATA[7:0]; // 写入发送寄存器
4'h4: IER <= PWDATA[7:0];
4'hC: LCR <= PWDATA[7:0];
endcase
end
end
// 读操作处理
always @(*) begin
if (read_en) begin
case (reg_addr)
4'h0: PRDATA = {24'h0, RBR};
4'h4: PRDATA = {24'h0, IER};
4'h8: PRDATA = {24'h0, IIR};
4'hC: PRDATA = {24'h0, LCR};
4'h14: PRDATA = {24'h0, LSR};
default: PRDATA = 32'h0;
endcase
end
end
assign PREADY = 1'b1; // 我们的简单实现不需要等待状态
assign PSLVERR = 1'b0; // 无错误报告
// UART发送逻辑
uart_tx tx_inst (
.clk(PCLK),
.resetn(PRESETn),
.data_in(THR),
.wr(write_en && (reg_addr == 4'h0)),
.txd(TXD),
.busy(LSR[5])
);
// UART接收逻辑
uart_rx rx_inst (
.clk(PCLK),
.resetn(PRESETn),
.rxd(RXD),
.data_out(RBR),
.rd(read_en && (reg_addr == 4'h0)),
.data_ready(LSR[0])
);
endmodule
3. 仿真验证与调试
3.1 编写Testbench
验证APB-UART模块的正确性需要一个完整的测试环境。以下是使用SystemVerilog编写的简单测试平台:
verilog复制module apb_uart_tb;
reg PCLK;
reg PRESETn;
reg PSEL;
reg PENABLE;
reg PWRITE;
reg [31:0] PADDR;
reg [31:0] PWDATA;
wire [31:0] PRDATA;
wire PREADY;
wire PSLVERR;
wire TXD;
reg RXD;
apb_uart dut (.*);
// 时钟生成
initial begin
PCLK = 0;
forever #10 PCLK = ~PCLK;
end
// 复位生成
initial begin
PRESETn = 0;
#100 PRESETn = 1;
end
// 测试任务:APB写操作
task apb_write(input [31:0] addr, input [31:0] data);
@(posedge PCLK);
PSEL = 1;
PWRITE = 1;
PADDR = addr;
PWDATA = data;
@(posedge PCLK);
PENABLE = 1;
@(posedge PCLK);
PSEL = 0;
PENABLE = 0;
endtask
// 测试任务:APB读操作
task apb_read(input [31:0] addr, output [31:0] data);
@(posedge PCLK);
PSEL = 1;
PWRITE = 0;
PADDR = addr;
@(posedge PCLK);
PENABLE = 1;
@(posedge PCLK);
data = PRDATA;
PSEL = 0;
PENABLE = 0;
endtask
// 主测试流程
initial begin
// 初始化
PSEL = 0;
PENABLE = 0;
PWRITE = 0;
PADDR = 0;
PWDATA = 0;
RXD = 1; // UART空闲状态
// 等待复位完成
@(posedge PRESETn);
// 配置UART: 8位数据,无校验,1位停止位
apb_write(32'h0C, 32'h03); // 写入LCR
// 发送一个字符
apb_write(32'h00, 32'h41); // 发送'A'
// 检查LSR状态
reg [31:0] lsr;
do begin
apb_read(32'h14, lsr);
end while (!lsr[6]); // 等待发送完成
// 模拟接收一个字符
#1000;
// 发送起始位
RXD = 0;
#8680; // 9600波特率下1位的时间
// 发送数据0x55 (01010101)
RXD = 1; #8680;
RXD = 0; #8680;
RXD = 1; #8680;
RXD = 0; #8680;
RXD = 1; #8680;
RXD = 0; #8680;
RXD = 1; #8680;
RXD = 0; #8680;
// 发送停止位
RXD = 1; #8680;
// 检查接收数据
reg [31:0] rbr;
apb_read(32'h00, rbr);
if (rbr[7:0] == 8'h55)
$display("接收测试通过!");
else
$display("接收测试失败!");
#100 $finish;
end
endmodule
3.2 常见问题排查
在实际实现中,新手常会遇到以下问题:
-
地址对齐错误:
- APB总线通常要求32位对齐访问
- 确保PADDR[1:0]在访问8/16位寄存器时正确处理
-
时序不满足:
- 当外设响应较慢时,需要置PREADY=0插入等待状态
- 在复杂设计中,可能需要添加流水线寄存器
-
寄存器访问冲突:
- 某些寄存器在读写时有特殊行为(如THR/RBR共享地址)
- 需要根据LCR的DLAB位正确解码寄存器访问
-
时钟域交叉:
- UART波特率时钟通常与APB总线时钟不同
- 需要添加适当的同步逻辑
4. FPGA实现与验证
4.1 引脚约束与综合
在Xilinx Vivado中,需要为设计添加适当的约束文件(.xdc):
tcl复制# 时钟约束
create_clock -name pclk -period 10 [get_ports PCLK]
# 复位约束
set_property PULLUP true [get_ports PRESETn]
# UART引脚约束
set_property PACKAGE_PIN F20 [get_ports TXD]
set_property IOSTANDARD LVCMOS33 [get_ports TXD]
set_property PACKAGE_PIN F19 [get_ports RXD]
set_property IOSTANDARD LVCMOS33 [get_ports RXD]
4.2 实际测试步骤
- 将设计下载到FPGA开发板
- 使用USB转串口工具连接UART接口
- 在PC端打开串口终端(如Putty或Tera Term)
- 通过APB总线发送测试字符
- 验证接收功能
注意:实际波特率可能会有偏差,可能需要调整UART时钟分频系数。
5. 进阶扩展
完成基础功能后,可以考虑以下增强功能:
-
FIFO缓冲:
- 实现16/64字节的发送/接收FIFO
- 减少CPU中断频率
-
中断支持:
- 添加中断输出信号
- 实现中断优先级和屏蔽功能
-
DMA接口:
- 添加APB到DMA的接口
- 支持大数据块传输
-
自动流控:
- 实现RTS/CTS硬件流控
- 防止数据丢失
-
多UART实例:
- 参数化设计支持多个UART通道
- 共享部分逻辑资源
verilog复制// FIFO实现的示例代码片段
module uart_fifo #(
parameter WIDTH = 8,
parameter DEPTH = 16
)(
input clk,
input resetn,
input wr,
input rd,
input [WIDTH-1:0] din,
output [WIDTH-1:0] dout,
output full,
output empty
);
reg [WIDTH-1:0] mem [0:DEPTH-1];
reg [4:0] wptr, rptr;
always @(posedge clk or negedge resetn) begin
if (!resetn) begin
wptr <= 0;
rptr <= 0;
end else begin
if (wr && !full) begin
mem[wptr[3:0]] <= din;
wptr <= wptr + 1;
end
if (rd && !empty) begin
rptr <= rptr + 1;
end
end
end
assign dout = mem[rptr[3:0]];
assign full = (wptr[4] != rptr[4]) && (wptr[3:0] == rptr[3:0]);
assign empty = (wptr == rptr);
endmodule
在实际项目中,我经常发现FIFO深度设置不当会导致数据丢失。对于115200波特率及以上的高速通信,建议至少使用64字节的FIFO深度。同时,添加水位标记中断可以在FIFO半满时提前通知CPU处理数据,避免缓冲区溢出。
