第1章:RTL设计实战——基于AMBA总线的UART控制器

各位同学,今天咱们来聊一个芯片设计里非常经典的话题——UART控制器。说实话,UART这东西看起来简单,但真正把它做扎实了,你会发现很多数字电路设计的通用套路都在里面。我个人习惯把UART当作入门AMBA总线设计的“敲门砖”,因为它麻雀虽小,五脏俱全。

1.1 整体架构:先画个图再说

动手写代码之前,我建议你先在脑子里(或者纸上)把架构理清楚。UART控制器说白了就三大块:总线接口、控制逻辑、数据通路。下面这张图是我自己总结的,你一看就明白。

UART控制器整体架构 AMBA总线接口 APB/AHB/AXI 地址译码 读写控制 寄存器配置 控制寄存器 状态寄存器 数据寄存器 波特率生成 分频计数器 时钟分频 采样时钟 发送状态机 IDLE → START → DATA → STOP 并转串输出 接收状态机 IDLE → START → DATA → STOP 串转并输入

你看,AMBA总线接口负责和CPU打交道,寄存器配置模块存着各种控制参数,波特率生成器产生采样时钟,发送和接收状态机各管各的。我当年第一次做这个设计时,就是照着这个框图一步步搭起来的。

1.2 寄存器配置:控制器的“大脑”

寄存器配置是整个UART的控制中心。CPU通过写寄存器来告诉UART“你要怎么干活”,通过读寄存器来知道“你干得怎么样了”。

地址偏移 寄存器名称 位宽 描述
0x00 UART_CTRL 8 控制寄存器:使能、奇偶校验、停止位等
0x04 UART_STATUS 8 状态寄存器:发送/接收就绪、溢出等
0x08 UART_DATA 8 数据寄存器:发送/接收数据
0x0C UART_BAUD 16 波特率分频值
小提示:我个人习惯把控制寄存器的每一位都定义成宏,比如`CTRL_TX_EN`、`CTRL_PARITY_EN`。这样写RTL时一眼就能看出每个bit是干嘛的,后期维护也方便。

1.3 波特率生成:时钟分频的艺术

波特率生成说白了就是分频。系统时钟比如50MHz,我们要产生9600bps的波特率,那就需要分频。计算公式很简单:

// 波特率分频值计算
baud_div = sys_clk_freq / (baud_rate * 16);
// 比如 50MHz / (9600 * 16) ≈ 325

为什么要乘以16?因为UART接收端要用16倍波特率的时钟来采样,这样能保证在数据位的中间采样,抗干扰能力强。嗯,这里要注意,分频值算出来要取整,取整带来的误差不能太大,一般要求误差小于2%。

避坑指南:我曾经在一个项目里直接用整数除法算分频值,结果误差到了3%,导致高波特率下通信不稳定。后来改成四舍五入取整,问题就解决了。所以,别小看这个取整操作。

1.4 发送状态机:数据怎么发出去

发送状态机负责把并行数据转成串行,加上起始位、校验位、停止位,然后一位一位地送出去。状态跳转其实很简单:

// 发送状态机伪代码
always @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    state <= IDLE;
    txd    <= 1'b1;  // 空闲时输出高电平
  end else begin
    case (state)
      IDLE: begin
        if (tx_start) state <= START;
      end
      START: begin
        txd <= 1'b0;  // 起始位
        if (bit_done) state <= DATA;
      end
      DATA: begin
        txd <= data[bit_cnt];  // 逐位发送
        if (bit_done & bit_cnt == 7) state <= STOP;
      end
      STOP: begin
        txd <= 1'b1;  // 停止位
        if (bit_done) state <= IDLE;
      end
    endcase
  end
end

你想想看,这个状态机其实就是一个简单的“流水线”,每个状态干一件事,干完了就跳到下一个。我在项目中遇到过一个问题:发送完最后一个数据位后,如果直接跳回IDLE,停止位还没发完就被打断了。所以一定要等停止位发送完成再跳转。

1.5 接收状态机:数据怎么收进来

接收比发送稍微复杂一点,因为要检测起始位,还要做采样。接收状态机的核心思想是:在16倍波特率时钟下,检测到RXD从高变低(起始位),然后等8个时钟周期(采样到起始位中间),再等16个时钟周期采样第一个数据位,以此类推。

// 接收采样逻辑
always @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    sample_cnt <= 0;
    rxd_data   <= 0;
  end else begin
    case (state)
      IDLE: begin
        if (rxd_negedge) begin  // 检测下降沿
          sample_cnt <= 0;
          state <= START;
        end
      end
      START: begin
        if (sample_cnt == 7) begin  // 采样到起始位中间
          sample_cnt <= 0;
          state <= DATA;
        end else begin
          sample_cnt <= sample_cnt + 1;
        end
      end
      DATA: begin
        if (sample_cnt == 15) begin  // 每个数据位采样一次
          sample_cnt <= 0;
          rxd_data[bit_cnt] <= rxd_sync;  // 采样值
          if (bit_cnt == 7) state <= STOP;
        end else begin
          sample_cnt <= sample_cnt + 1;
        end
      end
    endcase
  end
end
关键点:接收端一定要做同步处理!RXD是异步信号,进来后先打两拍同步,否则亚稳态会让你怀疑人生。我刚开始做的时候没注意这个,仿真没问题,上板子就乱收数据,后来加了同步器才搞定。

1.6 AMBA总线接口:和CPU握手

最后一步,把UART挂到AMBA总线上。这里以APB为例,因为APB协议简单,适合低速外设。APB接口主要做三件事:地址译码、读写控制、数据返回。

// APB接口示例
always @(posedge pclk or negedge rst_n) begin
  if (!rst_n) begin
    ctrl_reg <= 0;
    baud_reg <= 0;
  end else if (psel & penable & pwrite) begin
    case (paddr)
      ADDR_CTRL:  ctrl_reg <= pwdata[7:0];
      ADDR_BAUD:  baud_reg <= pwdata[15:0];
      ADDR_DATA:  tx_data  <= pwdata[7:0];  // 写数据触发发送
    endcase
  end
end

// 读操作
assign prdata = (psel & ~pwrite) ? 
                (paddr == ADDR_STATUS ? status_reg :
                 paddr == ADDR_DATA   ? rx_data   :
                 paddr == ADDR_CTRL   ? ctrl_reg  : 8'h0) : 8'h0;

你看,APB接口的代码其实很模板化,就是根据地址选通不同的寄存器。我建议你把地址映射做成参数化,这样以后改地址或者增加寄存器都很方便。

1.7 总结一下

好了,这一章的内容就这些。我们从整体架构开始,一步步拆解了UART控制器的各个模块:寄存器配置、波特率生成、发送状态机、接收状态机,最后挂上AMBA总线。说白了,一个UART控制器就是“CPU写寄存器→状态机干活→数据串行输出”这么个流程。

我个人觉得,学RTL设计最好的方法就是动手写代码。光看是看不会的,你得自己把状态机画出来,把时序图推一遍,然后一行一行地写Verilog。遇到问题不要怕,仿真器就是你的好朋友。

最后说一句:这一章的内容是后续所有章节的基础。后面的课程里,我们会基于这个UART控制器做验证、做综合、做时序分析。所以,请确保你完全理解了今天的内容。
可可精品资料库