🏗️ 第2课:rv_control 芯片架构 — 大白话版

学完第1课的数字电路基础后,我们来完整理解一颗"芯片"是怎么组织的。

🎯 本课目标

理解 SoC(片上系统)的核心概念:CPU 怎么通过"总线"和"外设"通信,内存映射是什么,中断怎么工作。学完你能画出 rv_control 的架构图。

一、什么是 SoC?—— 用手机比喻

SoC(System on Chip,片上系统)就是把一台电脑的核心部件全部集成到一块芯片上。想象你的手机:它里面有一块芯片,同时包含了 CPU(处理器)、内存、WiFi 模块、蓝牙模块、GPU(显卡)——这些以前是分开的好几块芯片,现在全挤在一块上了。

rv_control 就是一个迷你版 SoC:它有 CPU、内存、4 种外设(I2C/SPI/UART/GPIO)、中断控制器——全部集成在一块 0.27mm² 的硅片上(比一粒盐还小!)。

rv_control 完整架构图

graph TB subgraph TOP["rv_control 顶层芯片"] CPU["🧠 rv32i_core
3级流水线 RV32I CPU
415行Verilog"] BUS["🔀 bus_fabric
单主机总线/路由器
207行Verilog"] RAM["💾 soc_ram
64KB 内部RAM
4字节通道"] I2C["📡 i2c_controller
I2C主机
100/400kHz"] SPI["📡 spi_controller
SPI主机
Mode 0-3"] UART0["📡 uart_controller x4
全双工UART
8字节FIFO"] GPIO["📡 gpio_controller
16位双向GPIO
可配中断"] end CPU -->|"指令端口(imem)"| BUS CPU -->|"数据端口(dmem,优先)"| BUS BUS -->|"地址0x0000_0000"| RAM BUS -->|"地址0x1000_0000"| I2C BUS -->|"地址0x1000_1000"| SPI BUS -->|"地址0x1000_2xxx"| UART0 BUS -->|"地址0x1000_6000"| GPIO I2C -->|"中断"| IRQ["⚡ interrupt_o
组合中断输出"] SPI -->|"中断"| IRQ UART0 -->|"中断"| IRQ GPIO -->|"中断"| IRQ IRQ -->|"外部中断请求"| CPU style CPU fill:#3987e5,color:#fff,stroke:#fff style BUS fill:#c98500,color:#000,stroke:#fff style RAM fill:#199e70,color:#fff,stroke:#fff

💡 注意看:CPU 通过两条线(指令和数据)连到总线上,但最终共享同一根总线。这就像一条高速公路——CPU 的"取指令"和"读写数据"两股车流汇入同一条路,数据车优先通过。

二、总线(bus_fabric)—— 芯片里的"路由器"

总线就是芯片内部的数据高速公路。CPU 是唯一能主动发起读写的一方(所以叫"单主机"总线),其他都是被动响应(叫"从设备")。总线的任务很简单:

1️⃣ CPU 说"我要读地址 0x10002008 的数据"
2️⃣ 总线看地址高位:0x10002 → 这是 UART0 的地盘!
3️⃣ 总线把读请求转发给 UART0
4️⃣ UART0 返回数据,总线送回 CPU

总线地址译码 —— 像邮局分拣信件

flowchart LR CPU["📮 CPU
发出地址"] --> DECODE{"🔍 地址译码器
看地址高位"} DECODE -->|"0x0000_xxxx"| RAM["💾 RAM
64KB"] DECODE -->|"0x1000_0xxx"| I2C["📡 I2C"] DECODE -->|"0x1000_1xxx"| SPI["📡 SPI"] DECODE -->|"0x1000_2xxx"| U0["📡 UART0"] DECODE -->|"0x1000_3xxx"| U1["📡 UART1"] DECODE -->|"0x1000_4xxx"| U2["📡 UART2"] DECODE -->|"0x1000_5xxx"| U3["📡 UART3"] DECODE -->|"0x1000_6xxx"| GPIO["📡 GPIO"] DECODE -->|"其他"| ERR["❌ 总线错误
返回0xDEAD_BEEF"] style DECODE fill:#c98500,color:#000 style ERR fill:#d03b3b,color:#fff

每个外设分配了 4KB 的地址空间(0x1000_0000 ~ 0x1000_0FFF 给 I2C)。虽然实际只用了几个寄存器(比如 8 个),但预留 4KB 可以防止地址冲突。

三、一个具体例子:CPU 怎么发一个 UART 字符?

假设固件代码写了一句:

*(volatile unsigned int*)0x10002008 = 'A';  // 把字符 'A' 写入 UART0 的 TXDATA 寄存器

写入过程(时序图)

sequenceDiagram participant CPU as 🧠 rv32i_core participant BUS as 🔀 bus_fabric participant UART as 📡 uart_controller CPU->>BUS: 我要写数据到地址 0x10002008
数据='A'(0x41), we=4'b0001 BUS->>BUS: 地址[31:12]=0x10002
→ 选中 UART0! BUS->>UART: addr=0x008, wdata=0x41, we=1 UART->>UART: 把 0x41 写入 TXDATA 寄存器
→ FIFO 收到字符 'A' UART-->>BUS: ready=1 (我收到了!) BUS-->>CPU: ready=1 (写入完成!) Note over UART: 随后 UART 自动把 'A'
通过串口发送出去
💡 关键理解:内存映射 I/O(MMIO)

CPU 不区分"内存地址"和"外设地址"——对 CPU 来说,读写 0x10002008 和读写 0x00000004 是一模一样的 load/store 指令。区别在于地址值本身:高位不同的地址会被总线路由到不同的地方。这就是"内存映射 I/O"——把外设寄存器假装成内存地址来访问。

四、内存映射全表

地址空间分布图

pie title rv_control 地址空间分配 "RAM (64KB)" : 65536 "I2C (4KB)" : 4096 "SPI (4KB)" : 4096 "UART0-3 (4x4KB)" : 16384 "GPIO (4KB)" : 4096
起始地址结束地址大小设备里面有什么
0x0000_00000x0000_FFFF64KBRAM程序代码 + 数据
0x1000_00000x1000_0FFF4KBI2C8个寄存器(CTRL/STATUS/TXDATA...)
0x1000_10000x1000_1FFF4KBSPI6个寄存器(CTRL/STATUS/TXDATA...)
0x1000_20000x1000_5FFF16KBUART0~3各7个寄存器(含8字节FIFO)
0x1000_60000x1000_6FFF4KBGPIO8个寄存器(含中断配置)

五、中断系统 —— 芯片的"紧急热线"

中断就是外设通知 CPU"我有事找你"的机制。CPU 不可能一直盯着 4 个 UART 看有没有数据——太浪费了。更好的办法是:UART 收到数据后自己举旗(拉高中断线),CPU 看到旗子就暂停手头工作去处理。

中断优先级链

flowchart LR BERR["🔴 总线错误
优先级0(最高)"] --> COMB["⚡ interrupt_o
组合中断输出"] GPIO_I["🟡 GPIO中断
优先级1"] --> COMB U0_I["🟢 UART0
优先级2"] --> COMB U1_I["🟢 UART1
优先级3"] --> COMB U2_I["🟢 UART2
优先级4"] --> COMB U3_I["🟢 UART3
优先级5"] --> COMB SPI_I["🔵 SPI
优先级6"] --> COMB I2C_I["🟣 I2C
优先级7(最低)"] --> COMB COMB --> CPU["🧠 CPU ext_irq_i"] style BERR fill:#d03b3b,color:#fff style COMB fill:#c98500,color:#000
⚠️ 一个实际存在的 Bug

总线设计了 8 位 IRQ 向量来管理优先级,但实际只连接了 bit 7(总线错误)。其他外设中断直接绕过了总线,在顶层用 OR 门合并。功能上能用,但优先级管理变弱了——这属于"能跑但不优雅"的工程状态。

六、关键参数速查

时钟频率200 MHz (5.0ns) 标准单元23,559 个 Die 面积520×520 µm (0.27mm²) 总功耗6.82 mW I/O 焊盘35 个 Setup Slack+1.52 ns Hold Slack+0.95 ps Route DRC0 错误