🏗️ 第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 完整架构图
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
总线地址译码 —— 像邮局分拣信件
发出地址"] --> 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 寄存器
写入过程(时序图)
数据='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'
通过串口发送出去
CPU 不区分"内存地址"和"外设地址"——对 CPU 来说,读写 0x10002008 和读写 0x00000004 是一模一样的 load/store 指令。区别在于地址值本身:高位不同的地址会被总线路由到不同的地方。这就是"内存映射 I/O"——把外设寄存器假装成内存地址来访问。
四、内存映射全表
地址空间分布图
| 起始地址 | 结束地址 | 大小 | 设备 | 里面有什么 |
|---|---|---|---|---|
0x0000_0000 | 0x0000_FFFF | 64KB | RAM | 程序代码 + 数据 |
0x1000_0000 | 0x1000_0FFF | 4KB | I2C | 8个寄存器(CTRL/STATUS/TXDATA...) |
0x1000_1000 | 0x1000_1FFF | 4KB | SPI | 6个寄存器(CTRL/STATUS/TXDATA...) |
0x1000_2000 | 0x1000_5FFF | 16KB | UART0~3 | 各7个寄存器(含8字节FIFO) |
0x1000_6000 | 0x1000_6FFF | 4KB | GPIO | 8个寄存器(含中断配置) |
五、中断系统 —— 芯片的"紧急热线"
中断就是外设通知 CPU"我有事找你"的机制。CPU 不可能一直盯着 4 个 UART 看有没有数据——太浪费了。更好的办法是:UART 收到数据后自己举旗(拉高中断线),CPU 看到旗子就暂停手头工作去处理。
中断优先级链
优先级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
总线设计了 8 位 IRQ 向量来管理优先级,但实际只连接了 bit 7(总线错误)。其他外设中断直接绕过了总线,在顶层用 OR 门合并。功能上能用,但优先级管理变弱了——这属于"能跑但不优雅"的工程状态。