RASPI裸机4(MailBox)
本文介绍了树莓派3B中的Mailbox和UART硬件通信机制。Mailbox是SoC内部处理器核心间的硬件通信机制,采用"请求/响应"模型,通过特定寄存器实现ARM与GPU间的数据传输。树莓派3B包含两种Mailbox:VideoCore Mailbox用于ARM-GPU通信,ARM Local Mailbox用于多核同步。此外还详细分析了UART实现,包括PL011标准UAR
1 MailBox简介

这里的mailbox是SOC内部的硬件通信机制。
简单来说,它是两个不同处理器核心(例如ARM CPU和GPU/VPU或Cortex-M控制器)之间进行“跨界交流”的信箱。
Mailbox 通常由一组特定的硬件寄存器组成。最常见的交互模式是“请求/响应”模型:
1 写入数据:发送方将消息(通常是一个地址指针,指向内存中的详细数据结构)写入特定的 Mailbox 寄存器。
2 触发通知:写入操作会自动触发一个硬件中断(IRQ),告知接收方“你有新邮件”。
3 处理任务:接收方读取寄存器,根据地址去内存中获取具体任务,完成后将状态码写回。
4 返回结果:发送方收到回复通知,读取处理结果。
Mailbox 通常包含以下部分:
Channels(通道):一个 Mailbox 往往有多个通道。例如:
通道 0:用于电源管理。
通道 1:用于帧缓冲区(Framebuffer)设置。
通道 8:用于获取属性(如 CPU 温度、序列号)。
Mailbox Registers:
Status Register:判断信箱是否为空(Empty)或已满(Full)。
Read/Write Register:实际存放数据或内存指针的地方。
在 Linux 内核中,Mailbox有专门的框架(Framework)。驱动开发者不需要直接操作寄存器,而是使用mailbox_send_message() 等API。
Mailbox并不是ARM设计,ARM主要设计 CPU 内核(如 Cortex-A78)和总线规范(如 AMBA、AXI)。而Mailbox是由SoC芯片厂商(如 Broadcom、高通、瑞萨、华为海思)自行设计并集成到芯片中的硬件模块。所以每家的一些细节都可能有些差异。
除了Mailbox,硬件之间通讯还有以下常用手段:
| 机制 | 数据量 | 复杂度 | 主要用途 |
| Mailbox | 极小 (32/64 bit) | 低 | 控制命令、状态触发、地址传递 |
| 共享内存 | 极大 | 中 | 视频流、音频数据、大型缓冲区 |
| 硬件信号量 | 无 | 低 | 资源互斥、防冲突 |
| RPMSG | 中 | 高 | 跨系统(如 Linux 与 FreeRTOS)消息传递 |
回到树莓派3的mailbox。描述在"RP-008250-DS-1-bcm2836-peripherals.pdf"4.8。

但是这里用的并不是这个mailbox。在BCM2837里面,实际有两个mailbox。一个是GPU带的,一个是CPU的。因为BCM2837的机制是先启动GPU,所以用的是GPU的mailbox。
| 特性 | VideoCore Mailbox (代码) | ARM Local Mailbox (截图文档) |
| 物理基地址 | 0x3F00B880 |
0x40000080 起 |
| 沟通对象 | ARM <-> GPU (VideoCore) | Core X <-> Core Y (ARM Only) |
| 典型用途 | 设置显示屏、获取硬件信息 | 多核并行计算同步、核间中断 |
| 对应手册 | 《BCM2835 ARM Peripherals》 | 《BCM2836 ARM-local peripherals》 |
我详细看了BCM2835 ARM Peripherals,并没有找到具体描述里面Mailbox的。看了一下树莓派论坛:https://forums.raspberrypi.com/viewtopic.php?t=226514,里面甚至说地址是要去猜的。

我最后都是不知道怎么找出来寄存器的,反正他们就是找出来了。。。
2 UART实现
uart因为之前在pico已经详细写过了,所以这里也不单独写一篇,就在这里作为一个章节。
这个部分的描述是在BCM2835-ARM-Peripherals.pdf里面。
https://www.raspberrypi.org/app/uploads/2012/02/BCM2835-ARM-Peripherals.pdf
树莓派3B实际上有两个UART。。。又是两个。。。
PL011 UART (UART0):这是 ARM 官方标准的 PrimeCell 外设。它是一个功能齐全、硬件级的串口,拥有独立的波特率时钟,支持奇偶校验、硬件流控(RTS/CTS)以及更大的 FIFO 缓冲区。
Mini UART (UART1):这是一个简化版的外设,包含在树莓派的辅助外设(Auxiliary Peripherals)模块中。它的设计目的是为了实现基本的串行通信,去掉了许多复杂特性(如奇偶校验支持和完整的流控)。
在这里,用的是uart1。描述如下:

这里又有问题了,这里的地址描述的是0x7E21,但是代码里面是0x3F21。查了一下。这个是总线地址 (Bus Address) 与 物理地址 (Physical Address) 的映射。也就是说0x7E是GPU视角下的地址,而 0x3F是ARM CPU视角下的地址。
树莓派的 BCM 芯片架构设计非常特殊。外设(UART、GPIO 等)实际上是连接在 VideoCore (GPU) 总线上的,0x7E就是GPU的总线。ARM 核心无法直接“看到” 0x7E 开头的总线地址,硬件内部有一个内存管理单元(MMU)或总线映射器,将外设区域映射到了 ARM 的 0x3F00 0000 之后。
(说实话,坑也太多了,让我搞估计早就挂了。。。)
代码如下:
uart.h
void uart_init();
void uart_send(unsigned int c);
char uart_getc();
void uart_puts(char *s);
void uart_hex(unsigned int d);
uart.c
#include "gpio.h"
/* Auxilary mini UART registers */
#define AUX_ENABLE ((volatile unsigned int*)(MMIO_BASE+0x00215004))
#define AUX_MU_IO ((volatile unsigned int*)(MMIO_BASE+0x00215040))
#define AUX_MU_IER ((volatile unsigned int*)(MMIO_BASE+0x00215044))
#define AUX_MU_IIR ((volatile unsigned int*)(MMIO_BASE+0x00215048))
#define AUX_MU_LCR ((volatile unsigned int*)(MMIO_BASE+0x0021504C))
#define AUX_MU_MCR ((volatile unsigned int*)(MMIO_BASE+0x00215050))
#define AUX_MU_LSR ((volatile unsigned int*)(MMIO_BASE+0x00215054))
#define AUX_MU_MSR ((volatile unsigned int*)(MMIO_BASE+0x00215058))
#define AUX_MU_SCRATCH ((volatile unsigned int*)(MMIO_BASE+0x0021505C))
#define AUX_MU_CNTL ((volatile unsigned int*)(MMIO_BASE+0x00215060))
#define AUX_MU_STAT ((volatile unsigned int*)(MMIO_BASE+0x00215064))
#define AUX_MU_BAUD ((volatile unsigned int*)(MMIO_BASE+0x00215068))
/**
* Set baud rate and characteristics (115200 8N1) and map to GPIO
*/
void uart_init()
{
register unsigned int r;
/* initialize UART */
*AUX_ENABLE |=1; // enable UART1, AUX mini uart
*AUX_MU_IER = 0;
*AUX_MU_CNTL = 0;
*AUX_MU_LCR = 3; // 8 bits
*AUX_MU_MCR = 0;
*AUX_MU_IER = 0;
*AUX_MU_IIR = 0xc6; // disable interrupts
*AUX_MU_BAUD = 270; // 115200 baud
/* map UART1 to GPIO pins */
r=*GPFSEL1;
r&=~((7<<12)|(7<<15)); // gpio14, gpio15
r|=(2<<12)|(2<<15); // alt5
*GPFSEL1 = r;
*GPPUD = 0; // enable pins 14 and 15
r=150; while(r--) { asm volatile("nop"); }
*GPPUDCLK0 = (1<<14)|(1<<15);
r=150; while(r--) { asm volatile("nop"); }
*GPPUDCLK0 = 0; // flush GPIO setup
*AUX_MU_CNTL = 3; // enable Tx, Rx
}
/**
* Send a character
*/
void uart_send(unsigned int c) {
/* wait until we can send */
do{asm volatile("nop");}while(!(*AUX_MU_LSR&0x20));
/* write the character to the buffer */
*AUX_MU_IO=c;
}
/**
* Receive a character
*/
char uart_getc() {
char r;
/* wait until something is in the buffer */
do{asm volatile("nop");}while(!(*AUX_MU_LSR&0x01));
/* read it and return */
r=(char)(*AUX_MU_IO);
/* convert carrige return to newline */
return r=='\r'?'\n':r;
}
/**
* Display a string
*/
void uart_puts(char *s) {
while(*s) {
/* convert newline to carrige return + newline */
if(*s=='\n')
uart_send('\r');
uart_send(*s++);
}
}
/**
* Display a binary value in hexadecimal
*/
void uart_hex(unsigned int d) {
unsigned int n;
int c;
for(c=28;c>=0;c-=4) {
// get highest tetrad
n=(d>>c)&0xF;
// 0-9 => '0'-'9', 10-15 => 'A'-'F'
n+=n>9?0x37:0x30;
uart_send(n);
}
}
要注意的就是要先配置 AUX_ENABLES 开启 Mini UART,并且把 GPIO 14/15 的功能切换为 Alt5。
/* map UART1 to GPIO pins */
r=*GPFSEL1;
r&=~((7<<12)|(7<<15)); // gpio14, gpio15
r|=(2<<12)|(2<<15); // alt5
*GPFSEL1 = r;
*GPPUD = 0; // enable pins 14 and 15
此外他的代码操作寄存器是直接(*AUX_MU_LSR&0x01),和之前看的SET,GET还是有点区别。
3 MailBox实现
mbox.h
/* a properly aligned buffer */
extern volatile unsigned int mbox[36];
#define MBOX_REQUEST 0
/* channels */
#define MBOX_CH_POWER 0
#define MBOX_CH_FB 1
#define MBOX_CH_VUART 2
#define MBOX_CH_VCHIQ 3
#define MBOX_CH_LEDS 4
#define MBOX_CH_BTNS 5
#define MBOX_CH_TOUCH 6
#define MBOX_CH_COUNT 7
#define MBOX_CH_PROP 8
/* tags */
#define MBOX_TAG_GETSERIAL 0x10004
#define MBOX_TAG_LAST 0
int mbox_call(unsigned char ch);
mbox.c
#include "gpio.h"
/* mailbox message buffer */
volatile unsigned int __attribute__((aligned(16))) mbox[36];
#define VIDEOCORE_MBOX (MMIO_BASE+0x0000B880)
#define MBOX_READ ((volatile unsigned int*)(VIDEOCORE_MBOX+0x0))
#define MBOX_POLL ((volatile unsigned int*)(VIDEOCORE_MBOX+0x10))
#define MBOX_SENDER ((volatile unsigned int*)(VIDEOCORE_MBOX+0x14))
#define MBOX_STATUS ((volatile unsigned int*)(VIDEOCORE_MBOX+0x18))
#define MBOX_CONFIG ((volatile unsigned int*)(VIDEOCORE_MBOX+0x1C))
#define MBOX_WRITE ((volatile unsigned int*)(VIDEOCORE_MBOX+0x20))
#define MBOX_RESPONSE 0x80000000
#define MBOX_FULL 0x80000000
#define MBOX_EMPTY 0x40000000
/**
* Make a mailbox call. Returns 0 on failure, non-zero on success
*/
int mbox_call(unsigned char ch)
{
unsigned int r = (((unsigned int)((unsigned long)&mbox)&~0xF) | (ch&0xF));
/* wait until we can write to the mailbox */
do{asm volatile("nop");}while(*MBOX_STATUS & MBOX_FULL);
/* write the address of our message to the mailbox with channel identifier */
*MBOX_WRITE = r;
/* now wait for the response */
while(1) {
/* is there a response? */
do{asm volatile("nop");}while(*MBOX_STATUS & MBOX_EMPTY);
/* is it a response to our message? */
if(r == *MBOX_READ)
/* is it a valid successful response? */
return mbox[1]==MBOX_RESPONSE;
}
return 0;
}
可以看到代码还是不难,就是一个函数mbox_call。先组合值,Mailbox 硬件协议规定,传递的内存地址必须是16 字节对齐的(即地址低 4 位必须为 0),将通道类型放进去。然后判断MBOX的状态,之后将组合好的值r写入MBOX_WRITE寄存器。此时GPU会收到一个中断并开始处理内存中的mbox数组。
之后再while1中读取响应,一旦MBOX_STATUS变化,并且MBOX_READ等于之前发送的组合值,然后第一个字节是回复标志。就表示接收成功了。
4 Main
main.c
#include "uart.h"
#include "mbox.h"
void main()
{
// set up serial console
uart_init();
// get the board's unique serial number with a mailbox call
mbox[0] = 8*4; // length of the message
mbox[1] = MBOX_REQUEST; // this is a request message
mbox[2] = MBOX_TAG_GETSERIAL; // get serial number command
mbox[3] = 8; // buffer size
mbox[4] = 8;
mbox[5] = 0; // clear output buffer
mbox[6] = 0;
mbox[7] = MBOX_TAG_LAST;
// send the message to the GPU and receive answer
if (mbox_call(MBOX_CH_PROP)) {
uart_puts("My serial number is: ");
uart_hex(mbox[1]);
uart_hex(mbox[5]);
uart_puts("\n");
} else {
uart_puts("Unable to query serial!\n");
}
// echo everything back
while(1) {
uart_send(uart_getc());
}
}
核心流程就是构建了一个查询序列号的mbox包,发送并接收,最后有一个echo循环。
还有一个就是start.S,和上一篇的几乎一样,这里就不多写了。
5 运行
在启动qemu的时候增加一个参数,-serial null -serial stdio,表示将第一个串口(UART0)挂载到空设备,将第二个串口(Mini UART)挂载到你的控制台(stdio)。
tom@PC-20241221RKUQ:~/rp3/raspi3-tutorial/04_mailboxes$ make
rm kernel8.elf *.o >/dev/null 2>/dev/null || true
aarch64-linux-gnu-gcc -Wall -O2 -ffreestanding -nostdinc -nostdlib -nostartfiles -c start.S -o start.o
aarch64-linux-gnu-gcc -Wall -O2 -ffreestanding -nostdinc -nostdlib -nostartfiles -c main.c -o main.o
aarch64-linux-gnu-gcc -Wall -O2 -ffreestanding -nostdinc -nostdlib -nostartfiles -c mbox.c -o mbox.o
aarch64-linux-gnu-gcc -Wall -O2 -ffreestanding -nostdinc -nostdlib -nostartfiles -c uart.c -o uart.o
aarch64-linux-gnu-gcc -nostdlib -nostartfiles -Wl,--hash-style=sysv start.o main.o mbox.o uart.o -T link.ld -o kernel8.elf
/usr/lib/gcc-cross/aarch64-linux-gnu/13/../../../../aarch64-linux-gnu/bin/ld: warning: kernel8.elf has a LOAD segment with RWX permissions
/usr/lib/gcc-cross/aarch64-linux-gnu/13/../../../../aarch64-linux-gnu/bin/ld: warning: .note.gnu.build-id section discarded, --build-id ignored
aarch64-linux-gnu-objcopy -O binary kernel8.elf kernel8.img
tom@PC-20241221RKUQ:~/rp3/raspi3-tutorial/04_mailboxes$ make run
qemu-system-aarch64 -M raspi3b -kernel kernel8.img -serial null -serial stdio
My serial number is: 0000000000000000
这里因为用的是QEMU模拟器,所以输出的序列号全是0。
不过试了下获取温度,mbox[2] = 0x00030006;
反而还有值。000061A8,算下来是25°。应该是QEMU模拟器的默认值。
最后,这里有一篇介绍mailbox的文章,我觉得写的也还可以:https://news.qq.com/rain/a/20250810A02TJJ00
更多推荐


所有评论(0)