4. Renode脚本与平台描述语言
平台描述文件,也就是 .repl 文件,是 Renode 仿真世界的「硬件蓝图」。你写驱动之前,得先让 Renode 知道你模拟的芯片长什么样——有几个核、挂了多少外设、每个外设的寄存器在哪个地址、中断线怎么连的。
说白了,.repl 就是一份用特定语法写的「硬件说明书」。我刚开始接触 Renode 时,觉得这玩意儿不就是个配置文件吗?后来踩了几个坑才明白——repl 写错了,驱动跑得再漂亮也是白搭。
4.1 .repl 文件的结构骨架
一个典型的 .repl 文件长这样:
cpu: CPU.CortexM @ sysbus
size: 0x10000000
frequency: 100000000
uart0: UART.PL011 @ sysbus 0x40000000
-> cpu@0
gpio0: GPIO.PL061 @ sysbus 0x40010000
-> cpu@1
timer0: Timer.PL031 @ sysbus 0x40020000
-> cpu@2
irqctrl: IRQController.GIC @ sysbus 0x40030000
-> cpu@3
结构其实很清晰:
- 外设名称(如 uart0)——你自己起的,驱动里会用这个名字引用
- 外设类型(如 UART.PL011)——Renode 内置的仿真模型
- 挂载位置(@ sysbus)——所有外设都挂在系统总线上
- 基地址(如 0x40000000)——外设寄存器映射的起始地址
- 中断连接(-> cpu@0)——外设的中断输出连接到 CPU 的哪个 IRQ 号
4.2 IRQ中断控制器的repl描述
中断控制器是嵌入式系统的「交通警察」。所有外设的中断请求都先汇集到这里,再由它决定优先级、分发到哪个 CPU 核。
在 .repl 里描述中断控制器,核心是两点:中断号映射和中断线连接。
irqctrl: IRQController.GIC @ sysbus 0x40030000
numberOfInterrupts: 32
-> cpu@0
-> cpu@1
这里我声明了一个 GIC(Generic Interrupt Controller),支持 32 个中断源,并且同时连接到 CPU0 和 CPU1——这就是多核中断的基础。
每个外设的中断线怎么连?看这个例子:
uart0: UART.PL011 @ sysbus 0x40000000
-> irqctrl@5
gpio0: GPIO.PL061 @ sysbus 0x40010000
-> irqctrl@6
uart0 的中断输出连接到 irqctrl 的 5 号中断线,gpio0 连接到 6 号。驱动里配置中断时,用的就是这些中断号。
4.3 GPIO的寄存器映射与内存布局
GPIO 模块在 .repl 里看起来简单,但寄存器映射的细节很容易出错。以 PL061 GPIO 为例:
gpio0: GPIO.PL061 @ sysbus 0x40010000
-> cpu@1
基地址 0x40010000 意味着什么?意味着 GPIO 的所有寄存器都从 0x40010000 开始偏移。典型的 PL061 寄存器布局如下:
| 偏移地址 | 寄存器名称 | 功能描述 |
|---|---|---|
| 0x000 | GPIODATA | 数据寄存器(读写引脚电平) |
| 0x400 | GPIODIR | 方向寄存器(0=输入,1=输出) |
| 0x404 | GPIOIS | 中断感测寄存器 |
| 0x408 | GPIOIBE | 双边沿触发寄存器 |
| 0x40C | GPIOIEV | 中断事件寄存器 |
| 0x410 | GPIOIE | 中断使能寄存器 |
| 0x414 | GPIORIS | 原始中断状态寄存器 |
| 0x418 | GPIOMIS | 屏蔽中断状态寄存器 |
| 0x41C | GPIOIC | 中断清除寄存器 |
驱动里操作 GPIO 输出:
// 设置 GPIO0 为输出
*(volatile uint32_t*)(0x40010000 + 0x400) |= (1 << 0);
// 写高电平
*(volatile uint32_t*)(0x40010000 + 0x000) |= (1 << 0);
4.4 UART的repl描述与寄存器布局
UART 是调试时最常用的外设。PL011 UART 的 .repl 描述:
uart0: UART.PL011 @ sysbus 0x40000000
-> cpu@5
PL011 的寄存器布局(只列常用的):
| 偏移地址 | 寄存器 | 功能 |
|---|---|---|
| 0x000 | UARTDR | 数据寄存器(收发各一个字节) |
| 0x018 | UARTFR | 标志寄存器(TXFF=发送满,RXFE=接收空) |
| 0x024 | UARTIBRD | 整数波特率除数 |
| 0x028 | UARTFBRD | 小数波特率除数 |
| 0x030 | UARTIMSC | 中断屏蔽设置/清除 |
| 0x038 | UARTICR | 中断清除寄存器 |
驱动里发送一个字符:
void uart_putc(char c) {
// 等待发送 FIFO 不满
while (*(volatile uint32_t*)(0x40000000 + 0x018) & (1 << 5));
// 写入数据
*(volatile uint32_t*)(0x40000000 + 0x000) = c;
}
4.5 Timer的repl描述与配置
Timer 模块在 .repl 里同样简洁:
timer0: Timer.PL031 @ sysbus 0x40020000
-> cpu@2
PL031 Timer 的寄存器布局:
| 偏移地址 | 寄存器 | 功能 |
|---|---|---|
| 0x000 | TIMERLOAD | 加载值(倒计时初始值) |
| 0x004 | TIMERVALUE | 当前计数值(只读) |
| 0x008 | TIMERCTRL | 控制寄存器(使能、周期模式、中断使能) |
| 0x00C | TIMERISR | 中断状态寄存器 |
| 0x010 | TIMERMIS | 屏蔽中断状态寄存器 |
| 0x018 | TIMERBGLOAD | 后台加载值(周期模式自动重载) |
配置一个 1ms 周期的定时器:
// 假设时钟频率 100MHz,1ms = 100000 个时钟周期
*(volatile uint32_t*)(0x40020000 + 0x000) = 100000; // TIMERLOAD
*(volatile uint32_t*)(0x40020000 + 0x018) = 100000; // TIMERBGLOAD
// 使能:bit0=使能,bit1=周期模式,bit2=中断使能
*(volatile uint32_t*)(0x40020000 + 0x008) = 0x07;
4.6 内存布局与地址映射的SVG图解
下面这张图展示了我们刚才讲的所有外设在系统总线上的内存布局:
从这张图可以清楚看到:
- 所有外设都挂在系统总线上,每个外设有独立的基地址
- 外设的中断输出连接到 IRQ Controller,而不是直接连 CPU
- IRQ Controller 再根据配置把中断分发给不同的 CPU 核
4.7 多核场景下的repl注意事项
多核系统里,.repl 文件有几个容易出错的地方:
- 中断线不能冲突——两个外设不能连同一个中断号,否则中断处理会打架
- 外设实例化名称要唯一——比如 uart0、uart1,不能重名
- 地址范围不能重叠——每个外设的寄存器空间要独立,我见过有人把两个外设配到同一个基地址,结果 Renode 直接报错
- 中断亲和性——GIC 可以配置某个中断只发给特定 CPU,这在 .repl 里通过多个 -> cpu@N 实现
嗯,到这里你应该对 .repl 文件的写法有了比较完整的认识。从 IRQ 控制器到 GPIO、UART、Timer,每个外设的描述语法其实都遵循同样的模式——类型、地址、中断连接。掌握了这个规律,换任何芯片都能快速上手。