第1章:RTL设计实战——基于AMBA总线的UART控制器
各位同学,今天咱们来聊一个芯片设计里非常经典的话题——UART控制器。说实话,UART这东西看起来简单,但真正把它做扎实了,你会发现很多数字电路设计的通用套路都在里面。我个人习惯把UART当作入门AMBA总线设计的“敲门砖”,因为它麻雀虽小,五脏俱全。
1.1 整体架构:先画个图再说
动手写代码之前,我建议你先在脑子里(或者纸上)把架构理清楚。UART控制器说白了就三大块:总线接口、控制逻辑、数据通路。下面这张图是我自己总结的,你一看就明白。
你看,AMBA总线接口负责和CPU打交道,寄存器配置模块存着各种控制参数,波特率生成器产生采样时钟,发送和接收状态机各管各的。我当年第一次做这个设计时,就是照着这个框图一步步搭起来的。
1.2 寄存器配置:控制器的“大脑”
寄存器配置是整个UART的控制中心。CPU通过写寄存器来告诉UART“你要怎么干活”,通过读寄存器来知道“你干得怎么样了”。
| 地址偏移 | 寄存器名称 | 位宽 | 描述 |
|---|---|---|---|
| 0x00 | UART_CTRL | 8 | 控制寄存器:使能、奇偶校验、停止位等 |
| 0x04 | UART_STATUS | 8 | 状态寄存器:发送/接收就绪、溢出等 |
| 0x08 | UART_DATA | 8 | 数据寄存器:发送/接收数据 |
| 0x0C | UART_BAUD | 16 | 波特率分频值 |
1.3 波特率生成:时钟分频的艺术
波特率生成说白了就是分频。系统时钟比如50MHz,我们要产生9600bps的波特率,那就需要分频。计算公式很简单:
// 波特率分频值计算
baud_div = sys_clk_freq / (baud_rate * 16);
// 比如 50MHz / (9600 * 16) ≈ 325
为什么要乘以16?因为UART接收端要用16倍波特率的时钟来采样,这样能保证在数据位的中间采样,抗干扰能力强。嗯,这里要注意,分频值算出来要取整,取整带来的误差不能太大,一般要求误差小于2%。
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
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。遇到问题不要怕,仿真器就是你的好朋友。