底层物理结构:固定大小的 32 位数组

        在系统内核中,一个邮箱对象在物理内存里表现为一个固定长度的环形缓冲区。 最核心的物理限制是:这个缓冲区里的每一个数据格(一封邮件),大小被严格写死为 4 个字节。

0.邮箱在rt thread中的作用:

1:传递指针:如果有1000份的数据需要传输,直接把对应数据的内存地址传给对方,让对方从地址上读取,方便快捷

2:缓冲作用:邮箱本身自带一个缓冲队列。发送线程生产得快,可以先把“信件”排队塞进邮箱,然后继续干活;接收线程处理得慢也。关系,只要邮箱没满,数据就不会丢。

3:自动休眠与唤醒: 当邮箱里没有信件时,调用 rt_mb_recv 的接收线程会自动挂起,把宝贵的 CPU 资源让给其他线程。一旦发送线程投递了新邮件,操作系统会瞬间把接收线程唤醒。

4:中断与线程之间的通信:在嵌入式底层开发中,硬件中断(ISR,比如按键按下、串口收到数据)要求“快进快出”,绝对不能在中断里执行耗时的操作或死等。可以瞬间把状态码放入邮箱中。其他线程去读取该数据。

主要函数:

1. 创建邮箱:rt_mb_create

  • 作用: 在系统动态堆内存(Heap)中分配邮箱控制块,并根据你指定的大小,开辟一块连续的物理内存作为环形缓冲区。

  • rt_mailbox_t rt_mb_create(const char *name, rt_size_t size, rt_uint8_t flag);
    
  • 参数输入:

    • name:邮箱的名称(字符串,如 "mail_A")。

    • size邮箱的容量(能装多少封邮件)。如果你填 10,系统会在底层分配 10 * 4 = 40 个字节的缓冲区。

    • flag:阻塞唤醒规则。

      • RT_IPC_FLAG_FIFO:先进先出。多个线程等邮件时,谁先来排队,谁先拿到。

      • RT_IPC_FLAG_PRIO:优先级唤醒。优先级最高的线程优先拿到邮件。

  • 返回值:

    • 成功:返回邮箱控制块的句柄(rt_mailbox_t 类型的指针)。

    • 失败:返回 RT_NULL(通常是因为动态内存不足)。

2. 发送邮件:rt_mb_send

  • 作用: 将一个 32 位的数据(或地址)写入邮箱的环形缓冲区。如果此时有线程正在阻塞等待该邮箱,内核会立即触发调度将其唤醒。

  • rt_err_t rt_mb_send(rt_mailbox_t mb, rt_ubase_t value);
    
  • 参数输入:

    • mb:你要操作的邮箱句柄。

    • value:你要发送的数据。由于参数类型被强制限定为 rt_ubase_t(32 位整数),如果你要发送的是指针,必须进行强制类型转换,例如 (rt_ubase_t)data_ptr,否则编译器会报错。

  • 返回值:

    • RT_EOK:发送成功。

    • -RT_EFULL:邮箱已经满了。

  • 物理规则与使用场景: 该函数执行极快,不包含任何阻塞逻辑,因此它是完全中断安全的,被大量应用于硬件中断服务函数(ISR)中向线程传递底层数据。

3. 接收邮件:rt_mb_recv

  • 作用: 从邮箱的环形缓冲区中读取最早进入的一封邮件(4 个字节)。如果邮箱为空,当前线程将被挂起进入阻塞状态。

  • rt_err_t rt_mb_recv(rt_mailbox_t mb, rt_ubase_t *value, rt_int32_t timeout);
    
  • 参数输入:

    • mb:邮箱句柄。

    • value:需要提供一个存放数据的内存物理地址。内核在底层会执行写指令,把邮箱里的 4 字节数据直接写进这个地址里。(这就是为什么如果你的局部变量本身是指针 struct all_data *data,你还必须用 &data 对它再次取地址,把它所在的栈地址提供给内核)。

    • timeout:超时等待时间(单位:Tick)。

      • RT_WAITING_FOREVER:永久死等,直到拿到邮件。

      • 0:非阻塞读取。有邮件就读,没有立刻返回报错。

      • 正整数:最多挂起等待该数值的节拍数。

  • 返回值:

    • RT_EOK:成功接收到一封邮件,数据已经被写入你提供的 value 地址中。

    • -RT_ETIMEOUT:超时时间已到,但邮箱依然为空。

    • -RT_ERROR:发生其他内核级错误。

例子:假设有8个字节的数据要进行传输,邮箱的邮件最大是4字节,这时候我们可以传入数据的指针。其他接收线程可以读取地址去找到数据的值。

假设现在有温湿度数据:8个字节,通过把结构体的指针传入到邮箱中,其他线程通过rt_mb_recv读取。

#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>
#include <stdlib.h> // 提供 malloc 和 free 函数的声明支持

// 定义全局的线程控制块指针和邮箱控制块指针
rt_thread_t send_data_t;
rt_thread_t rec_data_t;
rt_mailbox_t box_c;

// 定义用于传递数据的结构体模板
struct all_data
{
    int wendu; // 温度
    int shidu; // 湿度
};

// 数据写入函数,用于给传入的结构体指针指向的内存赋值
void all_data_write(struct all_data *data)
{
    data->shidu=10;
    data->wendu=20;
}

// 发送线程的入口函数(充当“生产者”角色)
void send_data(void *param)
{
    while(1)
    {
        // 1. 在堆(Heap)区动态申请一块独立内存,大小为一个结构体的尺寸。
        // 使用 malloc 保证了每次循环都有新的内存地址,防止发送端覆写尚未被接收的数据。
        struct all_data *data=(struct all_data *)malloc(sizeof(struct all_data));
        
        // 2. 调用写入函数,将具体数值填入这块刚刚申请到的内存中。
        all_data_write(data);
        
        // 3. 发送邮件:将存放数据的内存首地址作为邮件内容发送。
        // (rt_base_t) 强转是为了告诉编译器:把这个指针当成一个系统字长(32位系统下为4字节)的数值塞进邮箱。
        rt_mb_send_wait(box_c, (rt_base_t)data,RT_WAITING_FOREVER);
        
        // 4. 线程主动休眠 1000 毫秒(1秒),期间系统会将 CPU 交给其他有需要的线程执行。
        rt_thread_mdelay(1000);
    }
}

// 接收线程的入口函数(充当“消费者”角色)
void rec_data(void *param)
{
    while(1)
    {
        // 1. 准备一个空白的本地指针变量,用于承接发送端传来的地址(门牌号)。
        struct all_data *data;
        
        // 2. 阻塞接收邮件:线程在这里原地等待,直到邮箱里有数据。
        // (rt_ubase_t *)&data 将本地指针变量的地址交出去,让底层的接收函数直接把取到的地址数值写进 data 变量里。
        rt_mb_recv(box_c,(rt_ubase_t *)&data, RT_WAITING_FOREVER);
        
        // 3. 此时 data 已经成功指向了发送端申请的那块堆内存,通过指针箭头直接读取并打印数据。
        rt_kprintf("temp: %d, hum: %d\n", data->wendu, data->shidu);
        
        // 4. 【关键】使用完毕后,必须手动释放这块由 malloc 申请的堆内存,否则会导致系统内存泄漏!
        free(data);
    }
}

int main(void)
{
    // 1. 动态创建一个名为 "box_chen" 的邮箱组件。
    // 容量为 10(最多可缓存 10 个数据地址),等待该邮箱的线程按优先级大小来唤醒 (RT_IPC_FLAG_PRIO)。
    box_c=rt_mb_create("box_chen", 10,RT_IPC_FLAG_PRIO);
    
    // 2. 动态创建并启动“发送线程”。栈大小 4096 字节,优先级为 10。
    send_data_t=rt_thread_create("send_data", send_data,RT_NULL, 4096, 10, 10);
    rt_thread_startup(send_data_t);
    
    // 3. 动态创建并启动“接收线程”。栈大小 4096 字节,优先级为 10。
    rec_data_t=rt_thread_create("rec_data", rec_data,RT_NULL, 4096, 10, 10);
    rt_thread_startup(rec_data_t);
    
    return 0;
}

1:(struct all_data *)malloc(sizeof(struct all_data))如何理解

malloc 的工作是在堆区划出一块指定大小(比如 8 字节)的内存,然后把这块内存的首地址返回。但是申请这块内存是用来存整数、存字符,还是存你自定义的结构体 未知。因此,malloc 的返回值类型是 void *(无类型指针,也叫万能指针)。

(struct all_data *):malloc 返回的地址(4字节)是:温湿度结构体指针

注意:

a:如果使用全局变量来存储传感器数据:发送线程在第二次循环时更新了数据,但接收线程可能还没来得及处理第一次的数据。这会导致接收线程读到的是被覆盖后的数据。

b:没用while的情况下:如果使用局部变量,在函数结束时,内存被系统回收,这时候时候发过去的地址,读取时会出现乱码情况。

如果在while循环中,数据过快读取,也会造成重复覆盖数据。

c:使用 malloc 的优势: 每次循环都会在堆区申请一块全新且独立的内存。发送线程把数据写进新内存后,把指针扔进邮箱。这样发送线程就可以继续去申请下一块内存,绝对不会覆盖之前还未被接收的数据。

2: rt_mb_recv(box_c,(rt_ubase_t *)&data, RT_WAITING_FOREVER);如何理解

 rt_mb_send_wait(, (rt_base_t)data,):发送过来的是温湿度结构体的指针,接收的时候要用一个变量去接收这个指针

 struct all_data *data:定义结构体指针变量,在内存中分配地址存这个指针变量

 rt_mb_recv(,,)中间的参数要传入的是地址

&data把存放data指针的地址取出来,这时候data等于发送过来的温湿度结构体的指针

(rt_ubase_t *):类型强制转换

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐