OS-RT-Thread

我要学习嵌入式操作系统!

资源

正文

项目组成

原生 RT-Thread 的文件组成及其对应的功能可以理解为:

组件与服务层
  └─ components/          Shell、文件系统、网络、驱动框架等
        ↓
RT-Thread 内核
  ├─ src/                  线程、调度、IPC、定时器、内存管理
  └─ include/              内核 API 和公共数据类型
        ↓
CPU 移植层
  └─ libcpu/               上下文切换、中断、启动栈等
        ↓
BSP 板级支持包
  └─ bsp/<厂商>/<架构>/<开发板>/
        ├─ board/         启动文件、时钟、链接脚本、板级驱动
        ├─ applications/  应用代码
        ├─ packages/      第三方软件包
        ├─ rtconfig.h     编译配置
        └─ SConstruct     构建脚本
        ↓
MCU 硬件
  • src/:RT-Thread 内核源码,例如线程调度、信号量、互斥锁、消息队列、定时器、内存管理。

  • include/:内核对外提供的头文件和 API,例如 rtthread.h

  • libcpu/:不同 CPU 架构的移植代码。

  • components/:可选组件,包括 FinSH/MSH、DFS 文件系统、网络协议栈、POSIX、Libc、USB、驱动框架等。

  • bsp/:具体芯片和开发板的移植代码。

  • applications/:用户自己的业务程序,不属于 RT-Thread 内核。

  • packages/:第三方或社区软件包,例如 mbedTLS、NTP、AT 设备驱动等。

  • Kconfig/.config/rtconfig.h:功能裁剪和编译配置。

  • SConstruct/SConscript:SCons 构建脚本。

系统架构

architecturezh.png
  • 内核层:RT-Thread 内核,是 RT-Thread 的核心部分,包括了内核系统中对象的实现。

  • 组件与服务层:组件是基于 RT-Thread 内核之上的上层软件,例如虚拟文件系统、FinSH 命令行界面、网络框架、设备框架等。采用模块化设计,做到组件内部高内聚,组件之间低耦合。

  • RT-Thread 软件包:运行于 RT-Thread 物联网操作系统平台上,面向不同应用领域的通用软件组件,由描述信息、源代码或库文件组成。RT-Thread 已经支持的软件包数量已经达到 450+(默认不在 RT-Thread 系统中,RT-Thread/packages: packages index repository for rt-thread.)。

软件包安装

先安装 env-windows,使用命令:

shell
git clone --recursive --depth 1 https://gitee.com/mirrors_RT-Thread/env-windows.git

然后打开 env.exe,初次进入要安装一大堆虚拟环境:

webp

进入正确的 bsp 目录(必须包含 KconfigSConstructrtconfig.py),打开配置界面:

shell
menuconfig
webp

选择 RT-Thread online packages 尝试安装新的软件包:

webp

选中后,按 S 保存,然后按 Esc 退出。这一步会改变 .config

使用命令下载软件包:

shell
pkgs --update
webp

下载完成后,如果是标准的 RT-Thread BSP 可以使用如下命令生成 Keil 工程:

shell
scons --target=mdk5

但如果不是,则还是手动更新 include 路径好了……

操作系统

经典特征

大学教材里说“操作系统的特征”(以 Linux/Unix 这种通用操作系统为原型),通常首先指四个经典特征:

教材里的基本特征含义普通 RT-Thread
并发 Concurrency多个任务在一段时间内交替运行有(多线程抢占式优先级调度)
共享 Sharing多个任务共享 CPU、内存、设备有(信号量 Semaphore、互斥锁 Mutex、事件集 Event、邮箱 Mailbox、消息队列 Message Queue)
虚拟 Virtualization把一个物理资源抽象成多个逻辑资源(如一个 CPU 看起来像多个虚拟 CPU)部分具备(RT-Thread 没有 Linux/Unix 这种虚拟内存的概念)
异步 Asynchronism程序以不可预测的速度向前推进,需要同步机制协调有(信号量 Semaphore、事件集 Event、消息队列 Message Queue、回调 Callback、中断 Interrupt、计时器 Timer)

概念

大学教材概念RT-Thread 对应
进程 / 线程rt_thread
PCB / TCBrt_thread 控制块
调度器Scheduler
就绪队列Ready Queue
上下文切换Context Switch
信号量rt_sem
互斥量rt_mutex
消息队列rt_mq
事件rt_event
动态内存rt_malloc
内存池rt_mp
I/O 管理rt_device
文件系统DFS
ShellFinSH / MSH
时钟中断SysTick
系统时钟rt_tick
定时器rt_timer

特性

大学《操作系统》RT-Thread
CPU / 处理机管理Scheduler
进程/线程管理Thread
进程调度Priority Scheduler
进程同步Semaphore / Mutex / Event
进程通信Mailbox / Message Queue
中断管理Interrupt subsystem
时钟管理Tick / Timer
内存分配Heap / MemHeap / Memory Pool
虚拟内存MMU / Page Table(普通 MCU 版通常没有)
进程地址空间隔离Process isolation(普通 MCU 版通常没有)
文件管理DFS
设备管理RT-Thread Device Framework
网络lwIP / SAL 等
ShellFinSH / MSH
POSIX APIPOSIX compatibility layer
用户态 / 内核态User/Kernel Mode(普通 MCU 版通常没有)
多用户权限user/group/ACL 等(通常不是目标)

Shell 命令

主要命令

原生 RT-Thread 的命令行组件叫 FinSH。命令并不是固定不变的,取决于 Kconfig 是否启用了 DFS、设备驱动、网络组件等。以 MSH 模式为例:

命令作用
help列出当前系统中注册的所有命令及说明
version显示 RT-Thread 版本信息
clear清空终端屏幕
console set <设备名>切换 FinSH 控制台设备,例如切换到另一个串口
reboot重启系统
ps查看系统线程、优先级、状态、栈使用等信息
free查看堆内存总量、已使用量、峰值和剩余量
list查看内核对象信息

List 命令

list 支持的子命令包括:

Usage: list [options]
[options]:
    thread           - list threads
    timer            - list timers
    sem              - list semaphores
    event            - list events
    mutex            - list mutexs
    mailbox          - list mailboxs
    msgqueue         - list message queues
    memheap          - list memory heaps
    device           - list devices
    fd               - list file descriptors

网络组件 netdev

命令配置开关作用
ifconfigNETDEV_USING_IFCONFIG查看和配置网络接口
pingNETDEV_USING_PING发送 ICMP 请求,测试连通性和延迟
dnsnetdev 编译查看或设置 DNS 服务器
netstatNETDEV_USING_NETSTAT查看网络接口的 TCP/IP 统计信息

可选调试和内存命令

这些命令只有打开相应配置后才会出现:

命令依赖作用
backtraceRT_USING_LIBC查看当前线程或指定线程的调用栈
memcheckRT_USING_MEMTRACE检查堆内存块
memtraceRT_USING_MEMTRACE输出内存分配跟踪信息
memheapcheckRT_USING_MEMHEAPRT_USING_MEMTRACE检查 MemHeap
memheaptraceRT_USING_MEMHEAPRT_USING_MEMTRACE输出 MemHeap 内存跟踪信息

命令行交互功能

  • 上下方向键查看历史命令;
  • 保存最近 5 条命令;
  • Tab 命令补全;
  • 命令选项补全;
  • 文件路径补全;
  • 命令描述信息显示。

自定义命令

RT-Thread 里最常见的是用 MSH 命令导出宏

c
MSH_CMD_EXPORT()

把一个普通 C 函数注册成 shell 命令。

c
int command(int argc, char **argv)

这和 Linux 的 main(argc, argv) 很像。

例如做一个加法命令:

c
#include <rtthread.h>
#include <stdlib.h>
 
static int add(int argc, char **argv)
{
    int a;
    int b;
 
    if (argc != 3)
    {
        rt_kprintf("Usage: add <a> <b>\n");
        return -1;
    }
 
    a = atoi(argv[1]);
    b = atoi(argv[2]);
 
    rt_kprintf("%d + %d = %d\n",
               a,
               b,
               a + b);
 
    return 0;
}
 
MSH_CMD_EXPORT(add, add two numbers);

然后:

shell
msh /> add 10 20
10 + 20 = 30

结构体

线程 thread

概念

裸机通常是 while(1) 顺序执行;RTOS 可以让多个线程“看起来同时运行”,由调度器分配 CPU。

shell 里的 ps / list thread 命令可以查看当前系统中所有线程的运行状态:

shell
thread   pri  status   sp         stack size max used left tick error   tcb addr
--------------------------------------------------------------------------------
tshell    20  running  0x00000540 0x00001000   48%    0x0a     OK      0x203920e8
tcpip     10  suspend  0x00000490 0x00004000   08%    0x14     EINTRPT  0x2038cc98
字段含义说明
thread线程名称判断是哪一个线程
pri当前线程优先级数字越小,优先级通常越高,如果多个线程处于 ready 状态,通常优先级高的会先得到 CPU
status当前线程状态常见状态有(running、ready、suspend、close、init)
sp当前栈指针/栈相关信息一般主要供底层调试
stack size分配给线程的栈大小判断线程栈配置
max used历史最大栈使用率判断有没有栈溢出风险(如果接近 100%,则可能有栈溢出风险)
left tick当前剩余时间片用于观察时间片调度(同优先级线程之间可以使用时间片流转)
error线程错误码正常一般为 0
tcb addrTCB 的内存地址描述该线程的结构体在内存中的地址

状态机

Mermaid

代码

API典型状态变化含义
rt_thread_create()不存在 → INIT创建线程,但还没参与调度
rt_thread_startup()INIT → READY启动线程,放进就绪队列
rt_thread_mdelay()RUNNING → SUSPEND → READY当前线程主动延时,期间阻塞;时间到后重新就绪
rt_thread_delete()任意可删除状态 → CLOSE删除动态线程,退出调度
rt_thread_self()不改变状态只是获取“当前正在运行的线程”
线程创建
动态方式静态方式
rt_thread_create()rt_thread_init()
自动分配 TCB自己定义 struct rt_thread
自动分配栈自己定义数组作为栈
rt_thread_delete()rt_thread_detach()
动态线程
c
static void my_thread_entry(void *parameter)
{
    while (1)
    {
        /* 线程任务 */
 
        rt_thread_mdelay(1000);
    }
}
 
static int my_thread_init(void)
{
    rt_thread_t tid;
 
    /* 创建线程 */
    tid = rt_thread_create(
        "my_thread",         /* 线程名字 */
        my_thread_entry,     /* 线程入口函数 */
        RT_NULL,             /* 传给入口函数的参数 */
        1024,                /* 栈大小,单位字节 */
        20,                  /* 优先级,数字越小优先级越高 */
        10                   /* 时间片 tick */
    );
 
    if (tid == RT_NULL)
    {
        return -RT_ERROR;
    }
 
    rt_thread_startup(tid);
 
    return RT_EOK;
}
 
INIT_APP_EXPORT(my_thread_init);  // 把 my_thread_init() 注册到 RT-Thread 的应用初始化阶段。
静态线程

不通过堆动态分配 TCB 和栈:

c
#include <rtthread.h>
 
#define THREAD_STACK_SIZE 1024
#define THREAD_PRIORITY   20
#define THREAD_TIMESLICE  10
 
static struct rt_thread my_thread;
 
/* 自己准备线程栈 */
static rt_uint8_t thread_stack[THREAD_STACK_SIZE];
 
static void my_thread_entry(void *parameter)
{
    while (1)
    {
        rt_kprintf("static thread running\n");
 
        rt_thread_mdelay(1000);
    }
}
 
int my_thread_init(void)
{
    rt_thread_init(
        &my_thread,              /* TCB */
        "my_thread",             /* 线程名称 */
        my_thread_entry,         /* 入口函数 */
        RT_NULL,                 /* 参数 */
        thread_stack,            /* 栈 */
        sizeof(thread_stack),    /* 栈大小 */
        THREAD_PRIORITY,         /* 优先级 */
        THREAD_TIMESLICE         /* 时间片 */
    );
 
    rt_thread_startup(&my_thread);
 
    return 0;
}

软件定时器 timer

概念

RT-Thread 的 timer 本质上是经过指定的 tick,触发一个 timeout 回调函数。

rt_thread_mdelay(1000); 这类的只是让线程阻塞,不能算是创建了一个独立的定时器。

使用 list timer 可以查看当前系统中已经创建的定时器及其状态:

timer         periodic   timeout    activated     mode
------------ ---------- ---------- ----------- ---------
tshell       0x00000000 0x00000000 deactivated one shot
timer1       0x00000000 0x00000000 deactivated one shot
字段含义
timer定时器名称
periodic定时周期,也就是 init_tick
timeout下一次超时对应的 tick
activated当前是否已经启动
modeone shot(单次)或 periodic(持续)

状态机

Mermaid

代码

RT-Thread 的定时器 API 可以分成四组:创建/初始化、启动/停止、控制、删除/脱离

API作用对状态的影响
rt_timer_create()动态创建 timer创建后为 deactivated
rt_timer_init()静态初始化 timer初始化后为 deactivated
rt_timer_start()启动 timerdeactivated → activated
rt_timer_stop()停止 timeractivated → deactivated
rt_timer_control()修改周期、模式等可能修改 periodic / mode 等属性
rt_timer_delete()删除动态 timertimer 对象消失
rt_timer_detach()脱离静态 timertimer 对象从内核对象系统移除
c
#include <rtthread.h>
 
static rt_timer_t my_timer = RT_NULL;
 
/* 定时器超时回调函数 */
static void timer_timeout(void *parameter)
{
    rt_kprintf("timer timeout! tick = %d\n", rt_tick_get());
}
 
/* 创建并启动定时器 */
static int timer_start_demo(void)
{
    /* 已创建就不重复创建 */
    if (my_timer == RT_NULL)
    {
        my_timer = rt_timer_create(
            "demo",                    /* timer 名称 */
            timer_timeout,             /* 超时回调 */
            RT_NULL,                   /* 回调参数 */
            RT_TICK_PER_SECOND,        /* 1 秒 */
            RT_TIMER_FLAG_PERIODIC     /* 周期定时器 */
        );
 
        if (my_timer == RT_NULL)
        {
            rt_kprintf("create timer failed!\n");
            return -1;
        }
    }
 
    rt_timer_start(my_timer);
 
    rt_kprintf("timer started\n");
 
    return 0;
}
 
MSH_CMD_EXPORT(timer_start_demo, start timer demo);
msh /> timer_start_demo
timer started

timer timeout! tick = 12345
timer timeout! tick = 13345
timer timeout! tick = 14345
...

信号量 semaphore

概念

shell 中可以用 list sem 查看系统中的信号量:

semaphore                        v   suspend thread
-------------------------------- --- --------------
shrx                             000 0
shell_rx                         001 0
字段含义
semaphore信号量名称
v当前信号量值
suspend thread当前正在等待这个信号量的线程
Mermaid

代码

API作用典型状态变化 / 影响
rt_sem_create()动态创建信号量创建信号量并设置初始 value
rt_sem_init()静态初始化信号量初始化已有的 struct rt_semaphore
rt_sem_take()获取信号量value > 0value - 1;否则线程可能进入 SUSPEND
rt_sem_trytake()尝试获取信号量有资源则 value - 1,没有则立即失败,不阻塞
rt_sem_release()释放信号量有等待线程则唤醒一个;否则 value + 1
rt_sem_control()控制信号量常用于执行 IPC 控制操作,例如重置信号量
rt_sem_delete()删除动态信号量删除由 rt_sem_create() 创建的信号量
rt_sem_detach()脱离静态信号量对应 rt_sem_init(),从内核对象系统中移除

事件集 event

概念

RT-Thread 里的 event(事件集) 是一种线程间同步机制。它和信号量有点像,但更适合处理“多个条件/多个事件”的组合等待。

机制更像什么适合场景
Semaphore一个计数器“资源来了几个”
Event一组 bit 标志“哪些事情发生了”

list event 查看系统中的事件集:

event             set    suspend thread
------------  ---------- --------------
at_ce1        0x00000000 001:at_clnt1
at_se1        0x00000000 0
at_ce0        0x00000000 001:at_clnt0
at_se0        0x00000000 0

RT-Thread 的一个 event 用 32 位无符号整数保存事件,所以一个 event 对象最多可以表示 32 个事件,每一位代表一个事件。

bit31 ... bit3 bit2 bit1 bit0
             │    │    │
             │    │    └─ WiFi 连接成功
             │    └────── TCP 连接成功
             └─────────── 数据准备完成

一个适合使用 event 的例子:

Mermaid

代码

API作用
rt_event_create()动态创建事件集
rt_event_init()静态初始化事件集
rt_event_send()设置一个或多个事件位
rt_event_recv()等待指定事件
rt_event_control()控制 event 对象
rt_event_delete()删除动态 event
rt_event_detach()脱离静态 event

先定义两个事件:

c
#define EVENT_RX_READY    (1 << 0)
#define EVENT_TX_DONE     (1 << 1)

创建:

c
rt_event_t event;
 
event = rt_event_create(
    "my_event",
    RT_IPC_FLAG_FIFO
);

假设接收线程等待:

c
rt_uint32_t recved;
 
rt_event_recv(
    event,
    EVENT_RX_READY,
    RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
    RT_WAITING_FOREVER,
    &recved
);

如果现在还没有:

EVENT_RX_READY

那么这个线程会:

RUNNING
   ↓
rt_event_recv()
   ↓
SUSPEND

然后另一个线程:

c
rt_event_send(event, EVENT_RX_READY);

事件位变成:

0000 0001

RT-Thread 检查发现等待条件满足,于是:

接收线程
SUSPEND
   ↓
READY

等待线程被唤醒。

邮箱 mailbox

概念

RT-Thread 里的 mailbox(邮箱) 是一种线程间通信机制,用来在线程之间传递一个“消息值”。

shell 中可以用 list mailbox 查看系统中的邮件:

mailbox      entry size suspend thread
------------ ----  ---- --------------
mbox0        0000  0008 1:tcpip
etxmb        0000  0008 1:etx
erxmb        0000  0008 1:erx

RT-Thread 的 mailbox 中,每条消息本质上是一个 rt_ubase_t 大小的值,也就是和 CPU 字长对应的整数类型。

代码

API作用
rt_mb_create()动态创建 mailbox
rt_mb_init()静态初始化 mailbox
rt_mb_send()发送消息
rt_mb_send_wait()邮箱满时可以等待后再发送
rt_mb_recv()接收消息
rt_mb_control()控制 mailbox
rt_mb_delete()删除动态 mailbox
rt_mb_detach()脱离静态 mailbox

示例,先创建一个 mailbox:

c
#include <rtthread.h>
 
static rt_mailbox_t mb;
 
static int mailbox_init(void)
{
    mb = rt_mb_create(
        "mb",
        8,                  /* 最多存 8 条消息 */
        RT_IPC_FLAG_FIFO
    );
 
    if (mb == RT_NULL)
    {
        rt_kprintf("mailbox create failed\n");
        return -1;
    }
 
    return 0;
}

创建一个不断发送 101,102,103...的线程,如果邮箱满了,则会发送失败:

c
static void sender_entry(void *parameter)
{
    rt_ubase_t value = 100;
 
    while (1)
    {
        rt_mb_send(mb, value);
 
        rt_kprintf("send: %d\n", value);
 
        value++;
 
        rt_thread_mdelay(1000);
    }
}

创建一个接收线程,如果没有邮件,则会一直处于阻塞态:

c
static void receiver_entry(void *parameter)
{
    rt_ubase_t value;
 
    while (1)
    {
        rt_mb_recv(
            mb,
            &value,
            RT_WAITING_FOREVER
        );
 
        rt_kprintf("recv: %d\n", value);
    }
}
Mermaid

消息队列 msgqueue

概念

shell 中使用 list msgqueue 查看系统中的消息队列:

msgqueue entry suspend thread
-------- ----- --------------
mq1      0002  0
mq2      0000  1:worker
字段含义
msgqueue消息队列名称
entry当前队列中已有多少条消息
suspend thread当前有多少线程因为等待这个消息队列而挂起

消息队列可以将整个结构体的数据复制进去(Mailbox 发的是“一个值”;Message Queue 发的是“一块数据”):

MailboxMessage Queue
每条消息固定一个机器字每条消息可以是多个字节
常用于整数、指针常用于结构体、数据包
更轻量功能更强
通常不复制大块数据会复制消息内容

代码

API作用关键说明
rt_mq_create()动态创建消息队列指定消息大小、最大消息数、等待策略
rt_mq_init()静态初始化消息队列使用用户自己提供的内存缓冲区
rt_mq_send()发送普通消息队列满时通常立即返回失败
rt_mq_send_wait()等待发送消息队列满时可等待一段时间,直到有空位
rt_mq_urgent()发送紧急消息插到队列前面,比普通消息优先被接收
rt_mq_recv()接收消息队列为空时可选择等待或立即返回
rt_mq_control()控制消息队列可用于重置等控制操作
rt_mq_delete()删除动态消息队列对应 rt_mq_create()
rt_mq_detach()脱离静态消息队列对应 rt_mq_init()

一个完整但比较简洁的 RT-Thread 消息队列示例:一个线程每秒发送一条结构体消息,另一个线程负责接收。

c
#include <rtthread.h>
 
typedef struct
{
    int id;
    int value;
} sensor_msg_t;
 
static rt_mq_t sensor_mq = RT_NULL;
 
/* 发送线程 */
static void sender_entry(void *parameter)
{
    sensor_msg_t msg;
    int count = 0;
 
    while (1)
    {
        msg.id = 1;
        msg.value = count++;
 
        if (rt_mq_send(sensor_mq,
                       &msg,
                       sizeof(msg)) == RT_EOK)
        {
            rt_kprintf("send: id=%d value=%d\n",
                       msg.id,
                       msg.value);
        }
        else
        {
            rt_kprintf("message queue full!\n");
        }
 
        rt_thread_mdelay(1000);
    }
}
 
/* 接收线程 */
static void receiver_entry(void *parameter)
{
    sensor_msg_t msg;
 
    while (1)
    {
        /*
         * 队列为空时永久等待
         */
        if (rt_mq_recv(sensor_mq,
                       &msg,
                       sizeof(msg),
                       RT_WAITING_FOREVER) == RT_EOK)
        {
            rt_kprintf("recv: id=%d value=%d\n",
                       msg.id,
                       msg.value);
        }
    }
}
 
static int mq_demo(void)
{
    rt_thread_t sender;
    rt_thread_t receiver;
 
    /* 创建消息队列
     *
     * 每条消息大小:sizeof(sensor_msg_t)
     * 最多保存:8 条消息
     */
    sensor_mq = rt_mq_create(
        "sensor",
        sizeof(sensor_msg_t),
        8,
        RT_IPC_FLAG_FIFO
    );
 
    if (sensor_mq == RT_NULL)
    {
        rt_kprintf("create message queue failed!\n");
        return -1;
    }
 
    /* 创建发送线程 */
    sender = rt_thread_create(
        "sender",
        sender_entry,
        RT_NULL,
        1024,
        20,
        10
    );
 
    /* 创建接收线程 */
    receiver = rt_thread_create(
        "receiver",
        receiver_entry,
        RT_NULL,
        1024,
        20,
        10
    );
 
    if (sender != RT_NULL)
        rt_thread_startup(sender);
 
    if (receiver != RT_NULL)
        rt_thread_startup(receiver);
 
    return 0;
}
 
MSH_CMD_EXPORT(mq_demo, message queue demo);
Mermaid

堆 memory heaps

概念

shell 的 list memheap 可以列举出系统中的内存堆对象使用情况:

memheap   pool size  max used size  available size
-------- ---------- -------------- --------------
sdram    33554432   48             33554384
heap     190584     7356           183228

RT-Thread 的 memheap 是一种动态内存管理机制,可以把一块连续 RAM 管理成堆,比如片内 SRAM、片外 SDRAM 都可以各自建立一个 memheap。

字段含义
memheap内存堆名称
pool size这块 heap 的总大小
max used size历史上曾经使用过的最大内存量
available size当前还可以使用的内存大小

代码

API作用
rt_memheap_init()初始化一块 memheap
rt_memheap_alloc()从指定 memheap 分配内存
rt_memheap_realloc()调整已分配内存大小
rt_memheap_free()释放内存
rt_memheap_info()获取 heap 使用信息
rt_memheap_detach()从系统中移除 memheap

一个比较典型的 RT-Thread memheap 示例:先拿一块静态数组模拟“独立内存区域”,把它初始化成一个 memheap,然后从里面申请和释放内存。

c
#include <rtthread.h>
 
/* 模拟一块独立 RAM:10 KB */
#define MY_HEAP_SIZE   (10 * 1024)
 
static rt_uint8_t my_heap_buffer[MY_HEAP_SIZE];
 
/* memheap 控制块 */
static struct rt_memheap my_heap;
 
static int memheap_demo(void)
{
    char *buf1;
    char *buf2;
 
    /* 1. 初始化内存堆 */
    if (rt_memheap_init(&my_heap,
                        "myheap",
                        my_heap_buffer,
                        sizeof(my_heap_buffer)) != RT_EOK)
    {
        rt_kprintf("memheap init failed!\n");
        return -1;
    }
 
    rt_kprintf("memheap initialized\n");
 
    /* 2. 从 myheap 中申请 1024 字节 */
    buf1 = rt_memheap_alloc(&my_heap, 1024);
 
    if (buf1 == RT_NULL)
    {
        rt_kprintf("buf1 alloc failed!\n");
        return -1;
    }
 
    rt_kprintf("buf1 alloc success: %p\n", buf1);
 
    /* 使用这块内存 */
    rt_snprintf(buf1, 1024, "Hello RT-Thread memheap!");
 
    rt_kprintf("buf1 content: %s\n", buf1);
 
    /* 3. 再申请 2048 字节 */
    buf2 = rt_memheap_alloc(&my_heap, 2048);
 
    if (buf2 == RT_NULL)
    {
        rt_kprintf("buf2 alloc failed!\n");
 
        rt_memheap_free(buf1);
        return -1;
    }
 
    rt_kprintf("buf2 alloc success: %p\n", buf2);
 
    /* 4. 释放第一块 */
    rt_memheap_free(buf1);
    buf1 = RT_NULL;
 
    rt_kprintf("buf1 freed\n");
 
    /* 5. 释放第二块 */
    rt_memheap_free(buf2);
    buf2 = RT_NULL;
 
    rt_kprintf("buf2 freed\n");
 
    return 0;
}
 
MSH_CMD_EXPORT(memheap_demo, memheap demo);
Mermaid

设备 device

概念

list device 是 RT-Thread 里用来查看当前已经注册到 I/O 设备框架中的所有设备的命令。它非常适合检查 UART、网卡、RTC、SD 卡、SPI/I2C 设备等有没有成功注册。

device   type                 ref count
-------- -------------------- ---------
e0       Network Interface    0
sd0      Block Device         1
rtc      RTC                  0
uart1    Character Device     0
uart0    Character Device     2
字段含义
device设备名称
type设备类型
ref count当前设备被打开的次数
Mermaid

状态机

Mermaid

代码

应用层
  ├─ 用户应用
  ├─ 软件包提供的应用/协议
  └─ RT-Thread 组件与服务
          ↓
RT-Thread 设备框架
          ↓
驱动实现
  ├─ 用户自写驱动
  ├─ 软件包驱动
  └─ BSP/官方驱动
          ↓
芯片 HAL / 寄存器
          ↓
硬件
类型主要职责典型位置
自己写的设备驱动适配具体芯片、板卡或外设bsp/.../boardlibraries/<厂商>_drivers
软件包里的驱动可复用、可分发的设备驱动或协议适配bsp/.../packages
RT-Thread 官方组件/服务提供通用系统能力,如文件系统、Shell、网络、日志components/dfscomponents/finshcomponents/netcomponents/utilities
RT-Thread 官方驱动框架统一设备模型和驱动接口components/drivers

RT-Thread 用 C 语言实现了类似面向对象的设备抽象

设备结构体 struct rt_device
        +
设备类型 enum rt_device_class_type
        +
函数指针操作接口
面向对象概念RT-Thread 中的实现作用
类(Class)struct rt_device定义所有设备共有的属性和操作
子类(Subclass)串口、SPI、I2C、Sensor 等专用设备结构体扩展某类设备的专用能力
对象(Object)已注册的具体设备,如 "uart0""spi10"表示一个实际硬件设备
类型标识enum rt_device_class_type标识字符设备、网络设备、RTC、SPI、PWM 等类型
成员变量typeflagopen_flaguser_data保存设备状态、权限和私有数据
虚函数表struct rt_device_ops,或 rt_device 中的函数指针保存具体驱动的操作函数
构造/注册rt_device_register()把设备加入 RT-Thread 设备列表
查找对象rt_device_find("uart0")按名称找到设备
打开设备rt_device_open()初始化或启用设备
读操作rt_device_read()调用具体驱动的读取函数
写操作rt_device_write()调用具体驱动的写入函数
控制操作rt_device_control()设置参数、启动 DMA、配置中断等
关闭设备rt_device_close()停止或释放设备
注销设备rt_device_unregister()从设备框架中注销
事件回调rx_indicatetx_complete通知接收数据或发送完成
多态应用只调用统一设备 API不同硬件使用相同的上层调用方式
私有成员扩展结构体或 user_data保存寄存器地址、DMA、缓存等驱动私有信息

示例:驱动一个 Flash 外设

Flash 初始化与挂载链
顺序文件关键函数作用
1applications/spi_flash.cspi1_flash_init()项目初始化入口
2libraries/XXX32_drivers/drv_spi.crt_hw_spi_device_attach()"spi10" 挂接到 "spi1",CS 使用 PD1
3components/drivers/spi/dev_spi_core.crt_spi_bus_attach_device_cspin()创建并注册 SPI 从设备
4components/drivers/spi/dev_spi_flash_sfud.crt_sfud_flash_probe()通过 SPI 识别 Flash,读取 JEDEC/SFDP 参数
5components/drivers/spi/sfud/src/sfud.csfud_device_init()初始化 SFUD Flash 对象
6dev_spi_flash_sfud.crt_device_register()将 Flash 注册成块设备 "sf_cmd"
7components/dfs/dfs_v1/src/dfs_fs.cdfs_mount()请求挂载 "sf_cmd"
8components/dfs/dfs_v1/filesystems/elmfat/dfs_elm.cdfs_elm_mount()f_mount()将块设备挂载为 FatFs 文件系统
SPI1 总线
  → spi10 SPI 从设备
    → sf_cmd Flash 块设备
      → / 文件系统路径
其他线程执行 CRUD 时的调用链
顺序文件关键函数作用
1其他线程自己的 .copen()read()write()close()应用调用公共文件接口
2components/dfs/dfs_v1/src/dfs_posix.copen()read()write()close()unlink()POSIX 接口实现
3components/dfs/dfs_v1/src/dfs_file.cdfs_file_open()dfs_file_read()dfs_file_write()管理文件描述符、路径和文件对象
4components/dfs/dfs_v1/filesystems/elmfat/dfs_elm.cdfs_elm_read()dfs_elm_write()DFS 到 FatFs 的适配
5同上f_read()f_write()f_close()f_unlink()FatFs 文件操作
6同上disk_read()disk_write()disk_ioctl()FatFs 到块设备的适配
7components/drivers/spi/dev_spi_flash_sfud.crt_sfud_read()rt_sfud_write()将逻辑扇区转换为 Flash 地址
8components/drivers/spi/sfud/src/sfud.csfud_read()sfud_erase_write()执行 Flash 读、擦除、写操作
9components/drivers/spi/dev_spi_core.crt_spi_transfer()rt_spi_send_then_recv()SPI 抽象层传输
10libraries/gd32_drivers/drv_spi.cspi_configure()spixfer()操作 GD32 SPI1 外设
11硬件SPI 寄存器和 Flash 芯片完成实际数据传输
应用线程
  ↓ read/write
DFS POSIX
  ↓
DFS 文件管理
  ↓
FatFs
  ↓
sf_cmd 块设备
  ↓
SFUD
  ↓
RT-Thread SPI 框架
  ↓
CPU SPI1 驱动
  ↓
Flash 芯片

文件描述符 fd

fdfile descriptor,文件描述符。RT-Thread 开启 DFS / POSIX 文件接口以后,会用一个小整数来代表“当前已经打开的文件、设备、socket、pipe 等对象”。

list fd 用来查看当前已打开的文件描述符。

fd type    ref magic  path
-- ------  --- -----  ------
 0 file      1 fdfd   /uart0
 1 socket    1 fdfd
 2 file      1 fdfd   /pipe0
 3 file      1 fdfd   /pipe0
字段含义
fd文件描述符编号
type描述符对应的对象类型,例如 file、socket
ref这个描述符对象的引用计数
magicDFS 内部用来检查结构有效性的标记,不是业务数据
path对应的文件、设备或管道路径

存储功能

举例:Elm-Chan FatFs 是实际文件系统,DFS 是统一接口层,SPI Flash 是底层存储介质。

Elm-Chan FatFs

Elm-Chan FatFs 是由 ChaN(Elm-Chan)编写的轻量级 FAT 文件系统模块,专门用于 MCU 和 RTOS。

Mermaid
open/read/write
      ↓
RT-Thread DFS(虚拟文件系统)
      ↓
dfs_elm.c(RT-Thread 适配层)
      ↓
FatFs 核心 ff.c
      ↓
diskio / SFUD
      ↓
SPI NOR Flash(sf_cmd)

它主要支持:

  • FAT12、FAT16、FAT32
  • 文件和目录操作
  • 长文件名
  • 创建、读写、删除、格式化文件系统
  • 通过 POSIX 接口使用,例如 open()read()write()stat()

该文件系统定义了如下数据结构:

c
FATFS fs;      // 文件系统对象
FIL file;      // 文件对象
DIR dir;       // 目录对象
FILINFO info;  // 文件信息

API

功能API
挂载f_mount()
打开文件f_open()
读取f_read()
写入f_write()
关闭f_close()
同步f_sync()
删除f_unlink()
创建目录f_mkdir()
遍历目录f_opendir() / f_readdir()
文件大小f_size()
移动指针f_lseek()
获取空间f_getfree()

代码示例

c
#include "ff.h"
 
void save_config(void)
{
    FIL file;
    UINT bw;
    
    f_open(&file, "config.txt", FA_WRITE | FA_CREATE_ALWAYS);
    char config[] =
        "id=001\n"
        "interval=1000\n";
    f_write(&file, config, strlen(config), &bw);
    f_close(&file);
}
 
void load_config(void)
{
    FIL file;
    char buf[128];
    UINT br;
 
    f_open(&file, "config.txt", FA_READ);
    f_read(&file, buf, sizeof(buf) - 1, &br);
    buf[br] = 0;
    printf("%s", buf);
    f_close(&file);
}

DFS

RT-Thread 里的 DFS 全称是 Device File System,本质上就是 RT-Thread 的虚拟文件系统层(VFS)。它的作用不是“自己规定一种磁盘格式”,而是把 FatFS、RomFS、DevFS、NFS 等不同文件系统统一到一套接口下面,让应用都能用类似 Linux 的 open / read / write / close 来操作。

Mermaid

shell

启用 DFS 和 POSIX 接口后,通常有:

命令作用
ls [path]列出目录或文件
cd <path>切换当前工作目录
pwd显示当前工作目录
cp <src> <dst>复制文件
mv <src> <dst>移动或重命名文件
cat <file>显示文件内容
rm [-frv] <file>删除文件或目录
mkdir <dir>创建目录
mkfs [-t type] <device>格式化存储设备
mount ...挂载文件系统
umount <path>卸载文件系统
df [path]查看磁盘空间使用情况
echo "text" [file]输出字符串,指定文件时写入文件
tail [-n number] <file>查看文件末尾内容

DFS v2 还可能增加:

ln       创建符号链接
link     创建硬链接
chmod    修改文件属性

在支持工作目录的配置下,MSH 还可以执行 .sh 脚本。

网络功能

架构

组件主要职责应用层会不会直接经常调用
SAL给应用提供统一 Socket 接口经常
NetDev管理网卡、IP、DNS、默认接口、UP/LINK 状态偶尔
lwIP / AT Socket真正实现 TCP/IP 或模块通信通常不直接碰
Ethernet / Wi-Fi / 4G 驱动和硬件打交道应用层基本不碰
Mermaid

NetDev

概念

RT-Thread 里的 NetDev 可以理解成:网络设备抽象层,用统一方式管理“网卡”。

一个 netdev 对象一般会保存这张网卡的网络信息,例如:

netdev
│
├── name
│    └── 网卡名称,如 e0 / wlan0
│
├── IP 地址
│    └── 192.168.1.100
│
├── 子网掩码
│    └── 255.255.255.0
│
├── Gateway
│    └── 192.168.1.1
│
├── DNS
│    └── 8.8.8.8
│
├── 网卡状态
│    ├── UP / DOWN
│    └── LINK UP / LINK DOWN
│
└── 操作接口
     ├── set_up
     ├── set_down
     ├── set_addr_info
     ├── set_dns_server
     └── ping 等

可以用 ifconfig 来查看各个网卡的信息:

network interface device: e0 (Default)
MTU: 1500
MAC: 00 11 22 33 44 55
FLAGS: UP LINK_UP INTERNET_UP DHCP_ENABLE
ip address: 192.168.1.100
gw address: 192.168.1.1
net mask  : 255.255.255.0
dns server #0: 192.168.1.1

network interface device: wlan0
MTU: 1500
MAC: ...
FLAGS: UP LINK_UP
ip address: 192.168.2.100
...

代码

API作用
netdev_get_by_name()按名字查找网卡
netdev_get_first_by_flags()按状态查找网卡
netdev_set_up()启用网卡(进入 UP)
netdev_set_down()关闭网卡(进入 DOWN)
netdev_set_addr_info()设置 IP / 掩码 / Gateway
netdev_set_dns_server()设置 DNS
netdev_set_default()设置默认网卡
netdev_is_up()查看接口是否启用(是否处于 UP 状态)
netdev_is_link_up()判断物理链路是否连通

NetDev 提供的是“管理和选择网络接口”的接口,不是强制系统任何时候只能启用一张网卡。

如果要实现:

Mermaid

则可以使用逻辑:

c
struct netdev *eth;
struct netdev *wifi;
struct netdev *lte;
 
eth  = netdev_get_by_name("e0");
wifi = netdev_get_by_name("wlan0");
lte  = netdev_get_by_name("4g0");
 
if (netdev_is_link_up(eth))
{
    netdev_set_default(eth);
}
else if (netdev_is_link_up(wifi))
{
    netdev_set_default(wifi);
}
else if (netdev_is_link_up(lte))
{
    netdev_set_default(lte);
}

netdev_set_default() 主要影响后续默认选择的网络路径,不会把已经建立好的 TCP/WebSocket 连接自动“搬”到另一张网卡。

比如:

TCP连接
   ↓
最初走 e0
   ↓
Ethernet 掉线
   ↓
netdev_set_default(4g0)

原来的 TCP socket 通常仍然会断掉。一般需要:

检测断线
   ↓
close(old_socket)
   ↓
切 default netdev
   ↓
重新 socket()
   ↓
重新 connect()

SAL

SAL 全称是 Socket Abstraction Layer(套接字抽象层)。作用是把不同网络协议栈统一成一套 BSD Socket API,让应用层不必关心底层到底是 lwIP、AT Socket 还是其他网络实现。

假设没有 SAL。

以太网使用 lwIP 时,你可能调用:

c
lwip_socket();
lwip_connect();
lwip_send();

4G 模块使用 AT Socket 时,底层又可能是:

c
at_socket();
at_connect();
at_send();

应用程序就和底层协议栈绑死了。

SAL 把它们统一成:

c
socket();
connect();
send();
recv();
close();

于是业务层只需要写:

c
int sock;
 
sock = socket(AF_INET, SOCK_STREAM, 0);
 
connect(sock,
        (struct sockaddr *)&server,
        sizeof(server));
 
send(sock, data, len, 0);
 
recv(sock, buf, sizeof(buf), 0);
 
closesocket(sock);

底层究竟调用:

c
lwip_connect()

还是:

c
at_connect()

由 SAL 来做适配。

API作用
socket()创建 Socket
connect()客户端连接服务器
bind()绑定本地 IP / 端口
listen()TCP Server 开始监听
accept()接受客户端连接
send()发送数据
recv()接收数据
sendto()UDP 发送
recvfrom()UDP 接收
shutdown()关闭连接方向
closesocket() / close()关闭 Socket
select() / poll()等待多个 Socket 事件
getaddrinfo()DNS / 地址解析

SAL 的上层 API 就是标准 BSD Socket 风格,因此很多 PC/Linux 网络代码可以比较容易移植到 RT-Thread。

对于 OCPP 协议,应用层网络大致会是:

Mermaid

数据解析功能

ArduinoJson

ArduinoJson 是一个嵌入式 C++ 里的 JSON 解析器 + JSON 生成器。这是个软件包,之前已经尝试用 RT-Thread 的包管理器安装上了。

项目中 Json 可能被用作通用数据格式和持久化配置格式。

Mermaid

API

场景重点 API
加载 JSONdeserializeJson()
读取 JSON[]as<T>()is<T>()、`
构造 JSON[]=add()createNestedObject()createNestedArray()
输出 JSONserializeJson()serializeJsonPretty()measureJson()

代码示例

Mermaid

定义一个 json:

json
{
    "device": {
        "name": "SensorNode01",
        "id": 1001,
        "version": "1.2.0"
    },
 
    "network": {
        "ip": "192.168.1.100",
        "port": 8080,
        "timeout": 5000
    },
 
    "system": {
        "mode": "auto",
        "debug": true,
        "logLevel": 2
    },
 
    "sensor": {
        "offset": 0.15,
        "scale": 1.02,
        "channels": [
            1,
            2,
            3
        ]
    }
}

程序运行时不直接操作 JSON,而是转换成结构体:

c++
#include <stdint.h>
#include <stdbool.h>
 
// 设备信息
typedef struct
{
    char name[32];
    int id;
    char version[16];
} DeviceInfo;
 
// 网络配置
typedef struct
{
    char ip[32];
    int port;
    int timeout;
} NetworkConfig;
 
// 系统配置
typedef struct
{
    char mode[16];
    bool debug;
    int logLevel;
} SystemConfig;
 
// 传感器配置
typedef struct
{
    float offset;
    float scale;
    int channels[8];
    int channelCount;
} SensorConfig;
 
// 总配置
 
typedef struct
{
    DeviceInfo device;
    NetworkConfig network;
    SystemConfig system;
    SensorConfig sensor;
} DeviceConfig;
 
DeviceConfig config;

读取 json:

c++
#include <ArduinoJson.h>
 
bool load_config(const char* json)
{
    /* 创建 JSON 内存池,所有解析出来的数据暂时存这里 */
    StaticJsonDocument<1024> doc;
    
    /* JSON 字符串 → JsonDocument */
    DeserializationError error = deserializeJson(doc, json);
    if(error)
    {
        return false;
    }
 
    /* 读取 device JSON: "device":{"name":"SensorNode01"} */
    strcpy(config.device.name, doc["device"]["name"] | "unknown");
    config.device.id = doc["device"]["id"] | 0;
    strcpy(config.device.version, doc["device"]["version"] | "0.0");
 
    /* 网络配置 */
    strcpy(config.network.ip, doc["network"]["ip"] | "0.0.0.0");
    config.network.port = doc["network"]["port"] | 80;
    config.network.timeout = doc["network"]["timeout"] | 3000;
    
    /* 系统参数 */
    strcpy(config.system.mode, doc["system"]["mode"] | "manual");
    config.system.debug = doc["system"]["debug"] | false;
    config.system.logLevel = doc["system"]["logLevel"] | 1;
    /* 数组解析 JSON: "channels": [1,2,3] */
    JsonArray channels = doc["sensor"]["channels"];
    config.sensor.channelCount = 0;
    for(JsonVariant v : channels)
    {
        config.sensor.channels[config.sensor.channelCount] = v.as<int>();
        config.sensor.channelCount++;
    }
    return true;
}

修改配置(结构体里的数据):

c++
config.system.debug = true;
config.sensor.scale = 1.05;

输出 json:

c++
void save_config(char* buffer, size_t size)
{
    StaticJsonDocument<1024> doc;
    
    // device
    JsonObject device = doc.createNestedObject("device");
    device["name"] = config.device.name;
    device["id"] = config.device.id;
    device["version"] = config.device.version;
    
    // network
    JsonObject network = doc.createNestedObject("network");
    network["ip"] = config.network.ip;
    network["port"] = config.network.port;
    network["timeout"] = config.network.timeout;
 
    // system
    JsonObject system = doc.createNestedObject("system");
    system["mode"] = config.system.mode;
    system["debug"] = config.system.debug;
    system["logLevel"] = config.system.logLevel;
 
    // 输出 JSON
    serializeJsonPretty(doc, buffer, size);
}

输出:

json
{
  "device": {
    "name": "SensorNode01",
    "id": 1001,
    "version": "1.2.0"
  },
  "network": {
    "ip": "192.168.1.100",
    "port": 8080,
    "timeout": 5000
  },
  "system": {
    "mode": "auto",
    "debug": true,
    "logLevel": 2
  }
}