APB-UART-2 IP核设计:从总线协议到Verilog实现的串口通信基石
2026/8/7 4:26:02 网站建设 项目流程

1. 项目概述:APB-UART-2 是什么?

如果你在数字芯片设计或者FPGA开发的圈子里待过一阵子,肯定对APB和UART这两个词不陌生。APB-UART-2,听起来像是一个具体的IP核或者一个开源项目,没错,它通常指代一个基于AMBA APB总线协议的通用异步收发传输器(UART)控制器。简单来说,它就是一个“翻译官”,一头连着芯片内部低速、简单的APB总线,另一头连着外部复杂、异步的串行世界。当你的SoC(片上系统)需要一个串口来打印调试信息、与外部传感器通信,或者进行简单的数据交换时,这个IP就是最经典、最可靠的选择之一。

我接触过不少版本的UART IP,从最简单的16550兼容型到功能复杂的带FIFO和流控的版本。APB-UART-2这个名字本身就暗示了它的定位:一个符合第二代或某种优化后APB接口标准的UART控制器。它的核心价值在于,为SoC集成提供了一个标准化、可复用、经过验证的串口通信解决方案。你不用再从零开始写UART的Verilog代码,也不用担心时序收敛和协议兼容性问题,直接把这个IP像乐高积木一样插到你的系统总线上,配置几个寄存器,就能让芯片“开口说话”了。这对于加速产品开发、降低验证风险至关重要。无论是做芯片的前端设计验证,还是在FPGA上做原型开发,一个稳定可靠的APB-UART IP都是调试和通信的基石。

2. 核心需求与设计思路拆解

2.1 为什么是APB总线?

在SoC的世界里,总线是血管,数据是血液。AMBA总线家族是ARM公司推出的片上总线标准,几乎成了行业事实标准。其中,APB(Advanced Peripheral Bus)是专门为连接低速、低功耗外设设计的。它协议简单,接口信号少,本质上是一种非流水线、无等待状态的简单读写总线(尽管后来的APB协议也支持等待状态,即PREADY信号)。UART控制器正是一个典型的外设:它不需要像DMA或高速存储器那样极高的带宽,但对接口的简洁性和低功耗有要求。因此,将UART挂在APB总线上是绝配。设计APB-UART-2时,首要任务就是完美实现APB Slave接口,正确响应主设备(通常是CPU或DMA控制器)的读写操作,将总线上的数据准确地搬运到UART的内部寄存器中,或者将UART的状态和数据反馈给总线。

2.2 UART核心功能定位

UART协议本身很古老,但历久弥新。它的核心就是异步串行通信:没有时钟线,双方依靠预先约定好的波特率进行数据传输。APB-UART-2需要实现的核心功能模块包括:

  1. 发送器(Transmitter):将APB总线写入的并行数据,按照配置的帧格式(数据位、停止位、奇偶校验位)和波特率,转换成串行比特流从TX引脚输出。
  2. 接收器(Receiver):监视RX引脚,检测起始位,然后以约定的波特率采样后续数据位,完成串并转换,将数据存入接收缓冲区,并可通过状态寄存器或中断告知CPU。
  3. 波特率发生器(Baud Rate Generator):这是UART的“心跳”。它通常由一个可编程的分频器构成,根据系统输入时钟和配置的波特率除数(Divisor)产生发送和接收所需的时钟信号。波特率的精度和稳定性直接决定了通信的可靠性。
  4. 中断与控制逻辑:管理各种事件(如发送缓冲区空、接收数据就绪、帧错误、溢出错误等)并产生中断信号,让CPU能够及时处理。

一个成熟的APB-UART-2设计,往往还会集成FIFO(先入先出缓冲区)来提升性能。有了发送和接收FIFO,CPU可以一次性写入多个数据,UART控制器自行依次发送,减少了CPU频繁干预的开销;同样,接收端可以缓存多个字符,等积累到一定数量或超时再通知CPU读取,大大提升了系统效率。

2.3 寄存器文件设计思路

APB-UART-2与软件交互的窗口就是其寄存器文件。这部分设计直接决定了驱动开发的难易度和功能的灵活性。通常,一个典型的UART寄存器组会包括:

  • 数据寄存器(UARTx_DR):读写数据的主要通道。写操作将数据放入发送FIFO或缓冲区,读操作从接收FIFO或缓冲区取出数据。
  • 状态寄存器(UARTx_SR):反映UART的实时状态,如发送忙(BUSY)、发送缓冲区空(TXE)、接收缓冲区非空(RXNE)、以及各种错误标志(帧错误FE、溢出错误OE等)。软件通过轮询或结合中断来检查这些标志。
  • 控制寄存器(UARTx_CR):用于使能UART、使能发送/接收器、使能中断源(如TXEIE、RXNEIE)、配置奇偶校验等。
  • 波特率分频寄存器(UARTx_BRR):一个关键的可编程寄存器。波特率 = 输入时钟频率 / (16 * DIV)。其中DIV是一个浮点数,通常由BRR寄存器中的整数部分(DIV_Mantissa)和小数部分(DIV_Fraction)共同决定。软件通过计算并写入合适的DIV值来设定通信速率。

设计时,需要仔细规划寄存器的位域,确保功能清晰、无歧义,并且符合行业常见习惯(例如与16550寄存器有一定兼容性会大大方便软件移植),同时要考虑APB总线的位宽(通常是32位)进行合理映射。

3. 关键模块实现与Verilog代码解析

接下来,我们深入到RTL(寄存器传输级)设计层面,看看APB-UART-2的几个核心模块如何用Verilog实现。这里我会分享一些经过实战检验的代码片段和设计技巧。

3.1 APB Slave接口模块

这个模块是UART IP与外界通信的桥梁。它的核心是解析APB总线协议。

module apb_uart_slave #( parameter ADDR_WIDTH = 8, parameter DATA_WIDTH = 32 )( // APB接口信号 input wire PCLK, input wire PRESETn, input wire [ADDR_WIDTH-1:0] PADDR, input wire PSEL, input wire PENABLE, input wire PWRITE, input wire [DATA_WIDTH-1:0] PWDATA, output reg [DATA_WIDTH-1:0] PRDATA, output wire PREADY, output wire PSLVERR, // 内部寄存器接口 output reg reg_wr_en, output reg reg_rd_en, output reg [ADDR_WIDTH-1:0] reg_addr, output reg [DATA_WIDTH-1:0] reg_wdata, input wire [DATA_WIDTH-1:0] reg_rdata ); // APB状态机定义 localparam ST_IDLE = 2'b00; localparam ST_SETUP = 2'b01; localparam ST_ACCESS = 2'b10; reg [1:0] apb_state; // 地址与数据锁存 reg [ADDR_WIDTH-1:0] addr_latch; reg write_latch; // PREADY生成逻辑:这里我们设计为无等待,即始终返回就绪。 // 如果内部操作需要等待(如FIFO满),可以将PREADY拉低。 assign PREADY = (apb_state == ST_ACCESS) ? 1'b1 : 1'b0; assign PSLVERR = 1'b0; // 假设无错误响应 always @(posedge PCLK or negedge PRESETn) begin if (!PRESETn) begin apb_state <= ST_IDLE; reg_wr_en <= 1'b0; reg_rd_en <= 1'b0; addr_latch <= 'b0; write_latch <= 1'b0; PRDATA <= 'b0; end else begin // 默认清零使能信号 reg_wr_en <= 1'b0; reg_rd_en <= 1'b0; case (apb_state) ST_IDLE: begin if (PSEL && !PENABLE) begin // APB传输开始 apb_state <= ST_SETUP; addr_latch <= PADDR; write_latch <= PWRITE; end end ST_SETUP: begin if (PENABLE) begin // 进入ACCESS相位 apb_state <= ST_ACCESS; if (write_latch) begin reg_wr_en <= 1'b1; reg_addr <= addr_latch; reg_wdata <= PWDATA; end else begin reg_rd_en <= 1'b1; reg_addr <= addr_latch; end end end ST_ACCESS: begin // 在ACCESS相位,采样内部数据(读操作)或完成写操作 if (!PENABLE) begin // ACCESS相位结束 apb_state <= ST_IDLE; PRDATA <= reg_rdata; // 读数据锁存到PRDATA end end default: apb_state <= ST_IDLE; endcase end end endmodule

注意:这是一个简化的无等待状态APB Slave。在实际项目中,如果UART的发送FIFO已满,此时CPU写数据寄存器,Slave接口应该通过拉低PREADY信号插入等待周期,直到FIFO有空闲位置。PREADY的逻辑需要根据内部模块的实际状态来设计,这是实现可靠流控的基础。

3.2 波特率发生器模块

波特率发生器的精度至关重要。常见的实现方法是使用一个计数器,计数到设定值后产生一个时钟使能脉冲(baud_tick),这个脉冲的周期就是一位数据的时间宽度。

module baud_rate_gen #( parameter CLK_FREQ = 50_000_000, // 输入时钟频率,单位Hz parameter BAUD_RATE = 115200 )( input wire clk, input wire rst_n, input wire [15:0] divisor, // 来自BRR寄存器的分频系数 output wire tx_clk_en, // 发送时钟使能 output wire rx_clk_en // 接收时钟使能(通常为16倍波特率时钟) ); // 计算计数值:N = CLK_FREQ / (BAUD_RATE * 16) // 但这里我们使用可编程的divisor,divisor = N localparam RX_OVERSAMPLE = 16; // 接收端通常16倍过采样 reg [15:0] tx_counter; reg [15:0] rx_counter; reg tx_tick; reg rx_tick; // 发送波特率时钟生成 (1倍波特率) always @(posedge clk or negedge rst_n) begin if (!rst_n) begin tx_counter <= divisor - 1; // 从最大值开始减,方便产生脉冲 tx_tick <= 1'b0; end else begin if (tx_counter == 0) begin tx_counter <= divisor - 1; tx_tick <= 1'b1; end else begin tx_counter <= tx_counter - 1; tx_tick <= 1'b0; end end end assign tx_clk_en = tx_tick; // 接收波特率时钟生成 (16倍波特率,用于在数据位中间采样) always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rx_counter <= (divisor / RX_OVERSAMPLE) - 1; rx_tick <= 1'b0; end else begin if (rx_counter == 0) begin rx_counter <= (divisor / RX_OVERSAMPLE) - 1; rx_tick <= 1'b1; end else begin rx_counter <= rx_counter - 1; rx_tick <= 1'b0; end end end assign rx_clk_en = rx_tick; endmodule

实操心得:波特率分频系数的计算是驱动开发的第一步。公式DIV = CLK_FREQ / (16 * DESIRED_BAUD)。计算出的DIV通常不是整数。我们需要将整数部分写入BRR的高位(例如DIV_Mantissa),小数部分乘以16后取整写入BRR的低位(DIV_Fraction)。例如,系统时钟50MHz,目标波特率115200,DIV = 50_000_000 / (16 * 115200) ≈ 27.1267。整数部分27,小数部分0.1267*16≈2.03,取整为2。所以BRR应写入27 << 4 | 2。这样产生的实际波特率误差很小,在异步通信允许的范围内。

3.3 发送器(Transmitter)状态机

发送器负责将并行数据转换成串行比特流。它是一个典型的状态机。

module uart_tx #( parameter DATA_BITS = 8, parameter STOP_BITS = 1 )( input wire clk, input wire rst_n, input wire tx_start, // 启动发送信号 input wire [DATA_BITS-1:0] tx_data, // 待发送数据 input wire parity_en, // 奇偶校验使能 input wire parity_odd, // 1为奇校验,0为偶校验 input wire baud_tick, // 波特率时钟使能 output reg txd, // 串行输出 output reg tx_busy, // 发送忙标志 output reg tx_done // 发送完成标志(可用于中断) ); localparam ST_IDLE = 3'd0; localparam ST_START = 3'd1; localparam ST_DATA = 3'd2; localparam ST_PARITY = 3'd3; localparam ST_STOP = 3'd4; reg [2:0] state; reg [2:0] bit_index; // 计数发送的数据位 reg [DATA_BITS-1:0] data_shreg; // 数据移位寄存器 reg parity_bit; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= ST_IDLE; txd <= 1'b1; // 空闲状态为高电平 tx_busy <= 1'b0; tx_done <= 1'b0; bit_index <= 0; end else if (baud_tick) begin // 每个波特率周期状态才可能变迁 tx_done <= 1'b0; // 默认清零完成标志 case (state) ST_IDLE: begin txd <= 1'b1; tx_busy <= 1'b0; if (tx_start) begin state <= ST_START; tx_busy <= 1'b1; data_shreg <= tx_data; bit_index <= 0; // 计算奇偶校验位(如果需要) if (parity_en) begin parity_bit <= ^tx_data; // 先计算偶校验位 if (parity_odd) parity_bit <= ~parity_bit; // 如果是奇校验则取反 end end end ST_START: begin txd <= 1'b0; // 起始位 state <= ST_DATA; end ST_DATA: begin txd <= data_shreg[bit_index]; if (bit_index == DATA_BITS - 1) begin bit_index <= 0; if (parity_en) begin state <= ST_PARITY; end else begin state <= ST_STOP; end end else begin bit_index <= bit_index + 1; end end ST_PARITY: begin txd <= parity_bit; state <= ST_STOP; end ST_STOP: begin txd <= 1'b1; // 停止位 // 这里可以扩展支持1.5或2个停止位 tx_done <= 1'b1; // 发送完成,可产生中断 state <= ST_IDLE; end default: state <= ST_IDLE; endcase end end endmodule

注意事项:发送状态机的baud_tick是关键的节奏信号。必须确保在tx_start有效时,数据tx_data是稳定的,并且在整个发送过程中保持不变。tx_busy信号非常重要,它可以反馈给上游逻辑(如FIFO或APB接口),在忙时阻止新的发送请求,防止数据覆盖。tx_done脉冲信号通常用于触发发送完成中断。

4. 系统集成与寄存器映射实战

将各个模块集成起来,并定义清晰的寄存器映射,是IP交付的关键。下面是一个简化的顶层模块和寄存器定义示例。

4.1 顶层集成与接口

module apb_uart_top #( parameter CLK_FREQ = 50_000_000, parameter APB_ADDR_WIDTH = 12 )( // APB Slave接口 input wire PCLK, input wire PRESETn, input wire [APB_ADDR_WIDTH-1:0] PADDR, input wire PSEL, input wire PENABLE, input wire PWRITE, input wire [31:0] PWDATA, output wire [31:0] PRDATA, output wire PREADY, output wire PSLVERR, // UART物理接口 output wire TXD, input wire RXD, // 中断输出 output wire UART_IRQ ); // 内部信号声明 wire reg_wr_en, reg_rd_en; wire [7:0] reg_addr; // 我们只使用低8位地址,映射256字节空间 wire [31:0] reg_wdata, reg_rdata; // 寄存器文件输出信号 wire [31:0] dr_rdata, sr_rdata, cr_rdata, brr_rdata; wire dr_wr_en, brr_wr_en, cr_wr_en; // UART核心控制信号 wire tx_start; wire [7:0] tx_data; wire tx_busy, tx_done; wire [7:0] rx_data; wire rx_ready, rx_frame_err, rx_overrun_err; wire baud_tick_tx, baud_tick_rx; // 实例化APB Slave接口 apb_uart_slave #( .ADDR_WIDTH (8), .DATA_WIDTH (32) ) u_apb_slave ( .PCLK (PCLK), .PRESETn (PRESETn), .PADDR (PADDR[7:0]), // 仅使用低8位 .PSEL (PSEL), .PENABLE (PENABLE), .PWRITE (PWRITE), .PWDATA (PWDATA), .PRDATA (PRDATA), .PREADY (PREADY), .PSLVERR (PSLVERR), .reg_wr_en (reg_wr_en), .reg_rd_en (reg_rd_en), .reg_addr (reg_addr), .reg_wdata (reg_wdata), .reg_rdata (reg_rdata) ); // 实例化寄存器文件 uart_regfile u_regfile ( .clk (PCLK), .rst_n (PRESETn), .addr_i (reg_addr[3:0]), // 假设我们只定义16个寄存器 .wr_en_i (reg_wr_en), .wr_data_i (reg_wdata), .rd_en_i (reg_rd_en), .rd_data_o (reg_rdata), // 连接到各个功能模块的读写信号和数据 .dr_o (tx_data), .dr_wr_en_o (dr_wr_en), .dr_i (rx_data), .sr_i ({/* 拼接状态标志 */}), .cr_o ({/* 控制位,如奇偶校验使能等 */}), .brr_o (/* 波特率除数 */), .brr_wr_en_o (brr_wr_en) ); // 实例化波特率发生器 baud_rate_gen u_baud_gen ( .clk (PCLK), .rst_n (PRESETn), .divisor (brr_value), .tx_clk_en (baud_tick_tx), .rx_clk_en (baud_tick_rx) ); // 实例化发送器 uart_tx u_tx ( .clk (PCLK), .rst_n (PRESETn), .tx_start (dr_wr_en), // 写数据寄存器即启动发送 .tx_data (tx_data), .parity_en (cr_parity_en), .parity_odd (cr_parity_odd), .baud_tick (baud_tick_tx), .txd (TXD), .tx_busy (tx_busy), .tx_done (tx_done) ); // 实例化接收器 (uart_rx模块,代码类似发送器,略) uart_rx u_rx ( .clk (PCLK), .rst_n (PRESETn), .rxd (RXD), .baud_tick (baud_tick_rx), .parity_en (cr_parity_en), .parity_odd (cr_parity_odd), .data_o (rx_data), .data_ready_o (rx_ready), .frame_error_o (rx_frame_err), .overrun_error_o(rx_overrun_err) ); // 中断逻辑:例如,发送完成或接收数据就绪时产生中断 assign UART_IRQ = (tx_done && cr_tx_int_en) || (rx_ready && cr_rx_int_en); endmodule

4.2 寄存器映射表设计

一个清晰的寄存器映射表是软硬件协同的契约。下表定义了一个基础版本:

地址偏移 (Hex)寄存器名称读写类型位域描述 (31:0)
0x00UART_DR (数据寄存器)RWDR[7:0]: 数据位。写操作将数据加载到发送FIFO;读操作从接收FIFO读取数据。其他位保留。
0x04UART_SR (状态寄存器)ROTXE (位7): 发送缓冲区空。1=空,可写入新数据。
RXNE (位5): 接收缓冲区非空。1=有数据可读。
BUSY (位3): 发送忙。1=正在发送。
FE (位2): 帧错误。1=检测到无效停止位。
OE (位1): 溢出错误。1=接收数据被覆盖。
其他位保留或用于其他错误标志。
0x08UART_CR (控制寄存器)RWUE (位13): UART使能。1=使能整个UART。
TE (位3): 发送使能。1=使能发送器。
RE (位2): 接收使能。1=使能接收器。
TXEIE (位7): 发送缓冲区空中断使能。
RXNEIE (位5): 接收缓冲区非空中断使能。
PSEL (位9): 奇偶校验选择。0=偶校验,1=奇校验。
PEN (位8): 奇偶校验使能。
0x0CUART_BRR (波特率寄存器)RWDIV_Mantissa[15:4]: 波特率分频器整数部分。
DIV_Fraction[3:0]: 波特率分频器小数部分。
波特率 = f_PCLK / (16 * DIV), 其中DIV = DIV_Mantissa + (DIV_Fraction/16)。

设计要点:寄存器位域的设计要考虑到软件操作的便利性。例如,状态寄存器(SR)的某些位在读取后会自动清零(如错误标志位),这可以简化驱动程序的编写。数据寄存器(DR)的读写操作实际上是对发送和接收FIFO的操作,硬件内部需要做好仲裁和空满判断。

5. 验证、调试与常见问题排查

设计完成后的验证和在实际系统中的调试,才是真正考验一个IP是否可靠的关键环节。

5.1 仿真验证环境搭建

对于数字设计,仿真(Simulation)是第一步。你需要搭建一个测试平台(Testbench),模拟APB总线的行为,并模拟外部的串行数据输入。

`timescale 1ns / 1ps module tb_apb_uart(); reg clk, rst_n; reg [31:0] PADDR; reg PSEL, PENABLE, PWRITE; reg [31:0] PWDATA; wire [31:0] PRDATA; wire PREADY, PSLVERR; wire TXD; reg RXD; // 实例化待测设计 apb_uart_top uut (.*); // 使用.*连接同名信号 // 时钟生成 always #10 clk = ~clk; // 50MHz时钟 // APB总线任务封装 task apb_write; input [31:0] addr; input [31:0] data; begin @(posedge clk); PSEL <= 1'b1; PWRITE <= 1'b1; PADDR <= addr; PWDATA <= data; @(posedge clk); PENABLE <= 1'b1; wait(PREADY); // 等待从设备就绪 @(posedge clk); PSEL <= 1'b0; PENABLE <= 1'b0; PWRITE <= 1'b0; $display("[APB_WRITE] Time=%t, Addr=0x%h, Data=0x%h", $time, addr, data); end endtask initial begin // 初始化 clk = 0; rst_n = 0; RXD = 1'b1; PSEL = 0; PENABLE = 0; PWRITE = 0; PADDR = 0; PWDATA = 0; #100 rst_n = 1; // 配置波特率 115200 (假设系统时钟50MHz) // DIV = 50M / (16*115200) ≈ 27.1267 => Mantissa=27, Fraction=2 apb_write(32'h0C, 32'h1B2); // 27<<4 | 2 = 0x1B2 // 使能UART,使能发送和接收 apb_write(32'h08, 32'h200C); // UE=1, TE=1, RE=1 // 发送一个字符 'A' (0x41) apb_write(32'h00, 32'h41); // 模拟从外部接收一个字符 'B' (0x42) #100000; // 等待一段时间 uart_send_byte(8'h42); // 自定义任务,模拟在RXD上发送一个字节 // 读取状态寄存器,检查是否有数据 // ... 此处省略APB读任务 // 读取数据寄存器 // ... #100000 $finish; end // 模拟UART发送一个字节的任务 task uart_send_byte; input [7:0] data; integer i; begin RXD = 1'b0; // 起始位 #(1000000000/115200); // 等待1个比特时间 for (i=0; i<8; i=i+1) begin RXD = data[i]; #(1000000000/115200); end RXD = 1'b1; // 停止位 #(1000000000/115200); $display("[UART_SIM] Sent byte: 0x%h", data); end endtask endmodule

5.2 上板调试与问题排查实录

仿真通过后,就要在FPGA或ASIC上实际运行了。这里分享几个我踩过的坑和解决方法。

问题一:通信完全无反应,TXD引脚无波形。

  • 排查步骤
    1. 时钟与复位:首先用示波器或逻辑分析仪检查输入时钟PCLK和复位信号PRESETn是否正常。这是所有逻辑的基础。
    2. APB总线活动:检查PSEL、PENABLE等APB信号在写操作时是否有跳变。如果没有,问题可能出在系统互联或CPU配置上。
    3. 寄存器配置:确认是否成功写入了使能位(UETE)。有时软件驱动配置顺序不对,或者写寄存器的地址映射有误。
    4. 波特率配置:计算并确认写入BRR寄存器的值是否正确。一个常见的错误是DIV_Fraction计算错误或忘记写入。
  • 解决:我遇到过因为系统时钟频率参数CLK_FREQ在顶层模块定义和软件计算时不一致,导致波特率偏差巨大,无法通信。统一时钟源的定义至关重要。

问题二:能发送但数据错误,或者接收不到数据。

  • 排查步骤
    1. 波形观察:用逻辑分析仪同时抓取TXD、RXD以及内部的baud_tick信号。看baud_tick的周期是否稳定且符合预期,看发送的波形时序是否正确(起始位低电平、数据位、停止位高电平)。
    2. 帧格式匹配:确认通信双方的帧格式(数据位、停止位、奇偶校验)是否完全一致。你的IP配置为8N1(8数据位,无校验,1停止位),而对方可能是7E1(7数据位,偶校验,1停止位),必然出错。
    3. 采样点:对于接收,确保你的接收器在数据位中间采样(这也是为什么接收时钟通常是16倍波特率,在第7、8、9个脉冲采样取多数值)。检查baud_tick_rx的频率是否是baud_tick_tx的16倍。
  • 解决:有一次发现接收数据位反转了,结果是uart_rx模块中从移位寄存器取位的顺序(LSB first还是MSB first)与发送端不匹配。UART协议通常是LSB(最低位)先发送

问题三:高波特率下通信不稳定,出现偶发性错误。

  • 排查步骤
    1. 时序约束:检查综合和实现后的时序报告,确保PCLK到内部逻辑,特别是到波特率生成计数器的路径满足时序要求。高波特率对时钟质量要求高。
    2. 亚稳态:RXD是异步输入信号,必须经过同步器(两级D触发器)处理后再进入接收状态机,否则极易产生亚稳态,导致采样错误。
    3. FIFO深度:如果使用了FIFO,检查在高数据流量下FIFO是否溢出。可以通过状态寄存器的溢出错误标志OE来判断。
  • 解决:在FPGA上,将外部输入的RXD信号用PCLK进行两级同步后,高波特率下的误码率显著下降。这是异步信号进入同步时钟域的标准做法

问题四:中断不产生或频繁产生。

  • 排查步骤
    1. 中断使能:检查控制寄存器CR中的TXEIERXNEIE等中断使能位是否置位。
    2. 中断标志:检查状态寄存器SR中对应的TXERXNE标志位是否在条件满足时被置起。中断信号通常是标志位与使能位的逻辑与。
    3. 标志清除:有些标志(如RXNE)在读取数据寄存器DR后会自动清除,有些错误标志可能需要软件写1清除。确认你的清除方式是否符合硬件设计。
    4. 中断信号毛刺:如果中断是电平敏感型,确保其持续时间足够长能被CPU捕获;如果是脉冲型,确保脉冲宽度足够。可以用逻辑分析仪抓取中断信号线观察。
  • 解决:曾遇到中断标志在产生后立即被意外清除的情况,原因是状态机设计有缺陷,在特定条件下错误地复位了标志位。仔细审查状态转移条件和标志位置位/清零逻辑后修复。

5.3 软件驱动开发要点

硬件IP需要软件驱动才能发挥作用。编写驱动时要注意:

  1. 初始化序列
    // 1. 可选:禁用UART (CR.UE = 0) // 2. 配置波特率寄存器 (BRR) uint32_t div = SystemCoreClock / (16 * desired_baudrate); UART->BRR = ((div / 16) << 4) | (div % 16); // 假设BRR格式如此 // 3. 配置数据格式 (CR中设置数据位、停止位、奇偶校验) UART->CR = (1<<13) | (1<<3) | (1<<2); // UE=1, TE=1, RE=1
  2. 发送数据(轮询方式)
    void uart_send_byte(uint8_t data) { while (!(UART->SR & (1<<7))) { // 等待TXE标志 // 超时处理 } UART->DR = data; }
  3. 接收数据(中断方式)
    // 中断服务函数 void UART_IRQHandler(void) { if (UART->SR & (1<<5)) { // RXNE标志 uint8_t received_data = UART->DR; // 处理数据... } // 清除其他中断标志... }
  4. 错误处理:在读取数据前后,检查FEOE等错误标志,并进行相应处理(如丢弃错误数据、重置接收状态等)。

设计一个稳定可靠的APB-UART-2 IP,是一个从协议理解、RTL实现、功能验证到系统集成、驱动开发的完整过程。每一个环节都需要仔细考量。上面分享的代码和思路是一个精简的骨架,在实际项目中,你可能还需要考虑更多细节,如FIFO的深度优化、硬件流控(RTS/CTS)的实现、DMA接口的支持、以及低功耗设计(时钟门控)等。希望这些从实战中总结的经验,能帮助你少走弯路,更快地构建出自己的串口通信基石。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询