4. LiteOS-M 内核基础:消息队列与软件定时器

消息队列和软件定时器,是 LiteOS-M 内核里两个非常基础但又极其重要的组件。说白了,消息队列解决的是任务间“怎么传数据”的问题,软件定时器解决的是“到了时间干什么”的问题。我在好几个项目里都吃过它们的亏,今天就把这些经验掰开揉碎了讲给你听。

本章核心知识点:
  • 消息队列的创建、发送、接收机制
  • 软件定时器的创建、启动、停止流程
  • 实际项目中的避坑指南
LiteOS-M 内核通信与定时机制 消息队列 创建 (LOS_QueueCreate) 发送 (LOS_QueueWrite) 接收 (LOS_QueueRead) 删除 (LOS_QueueDelete) 软件定时器 创建 (LOS_SwtmrCreate) 启动 (LOS_SwtmrStart) 停止 (LOS_SwtmrStop) 删除 (LOS_SwtmrDelete) 关键区别 消息队列:任务间通信,支持多任务并发读写 软件定时器:基于系统 Tick 的定时回调,单次或周期模式

4.1 消息队列:任务间的数据管道

消息队列本质上是一个内核管理的环形缓冲区。任务 A 往里面写数据,任务 B 从里面读数据。你想想看,如果没有消息队列,两个任务要共享数据就得用全局变量加信号量保护,代码写起来又臭又长。

我个人习惯把消息队列比作一个“邮筒”。发信的人把信塞进去,收信的人定时来取。邮筒有容量限制,满了就塞不进去了——这就是消息队列的阻塞机制。

4.1.1 创建消息队列

创建队列的 API 是 LOS_QueueCreate。原型长这样:

UINT32 LOS_QueueCreate(
    const CHAR *queueName,   // 队列名称,调试用
    UINT16 len,              // 队列长度(消息个数)
    UINT32 *queueID,         // 输出参数,队列句柄
    UINT32 flags,            // 标志位,一般填0
    UINT16 maxMsgSize        // 单条消息最大字节数
);

这里有个坑,我一开始就踩过。len 和 maxMsgSize 这两个参数,很多人搞混。len 是队列能存多少条消息,maxMsgSize 是每条消息最大能有多大。比如你设 len=10, maxMsgSize=64,那这个队列最多能存 10 条 64 字节的消息。

注意: 消息队列创建时会一次性分配 len * maxMsgSize 的内存。如果你设得太大,系统内存可能直接爆掉。我在一个低内存设备上就吃过这个亏,队列创建返回了错误码,排查了半天才发现是内存不够。

4.1.2 发送消息

发送消息用 LOS_QueueWrite。看代码:

UINT32 LOS_QueueWrite(
    UINT32 queueID,          // 队列句柄
    VOID *bufferAddr,        // 要发送的数据缓冲区
    UINT32 bufferSize,       // 数据长度
    UINT32 timeOut           // 超时时间,单位:Tick
);

timeOut 参数很关键。填 0 表示队列满了就直接返回失败,不等待。填 LOS_WAIT_FOREVER 表示死等,直到队列有空位。我建议在中断服务函数里用 0,在任务里根据实际情况选合适的超时值。

经验之谈: 中断里发消息一定要用非阻塞模式(timeOut=0)。为什么?因为中断上下文不能挂起等待。我曾经在一个定时器中断里用了阻塞发送,结果系统直接死锁,查了两天才发现是这个问题。

4.1.3 接收消息

接收消息用 LOS_QueueRead:

UINT32 LOS_QueueRead(
    UINT32 queueID,          // 队列句柄
    VOID *bufferAddr,        // 接收缓冲区
    UINT32 bufferSize,       // 缓冲区大小
    UINT32 timeOut           // 超时时间
);

接收的时候,bufferSize 必须大于等于消息的实际长度,否则数据会被截断。我记得有一次调试,发现收到的数据总是不对,打印出来一看,原来是接收缓冲区开小了,消息被截了一半。

4.1.4 消息队列使用示例

来个完整的例子,把创建、发送、接收串起来:

#define QUEUE_LEN       5
#define QUEUE_MSG_SIZE  32

UINT32 g_queueID;

// 发送任务
VOID SendTask(VOID)
{
    CHAR buf[] = "Hello LiteOS";
    UINT32 ret;
    
    ret = LOS_QueueWrite(g_queueID, buf, sizeof(buf), 0);
    if (ret != LOS_OK) {
        // 发送失败处理
        printf("Queue write failed, ret=%u\n", ret);
    }
}

// 接收任务
VOID RecvTask(VOID)
{
    CHAR buf[QUEUE_MSG_SIZE] = {0};
    UINT32 ret;
    
    ret = LOS_QueueRead(g_queueID, buf, QUEUE_MSG_SIZE, LOS_WAIT_FOREVER);
    if (ret == LOS_OK) {
        printf("Received: %s\n", buf);
    }
}

// 初始化
VOID QueueDemoInit(VOID)
{
    LOS_QueueCreate("DemoQueue", QUEUE_LEN, &g_queueID, 0, QUEUE_MSG_SIZE);
    // 创建发送和接收任务...
}

4.2 软件定时器:时间触发的利器

软件定时器,说白了就是让系统在指定时间后执行某个函数。它不像硬件定时器那样依赖具体的定时器外设,而是基于系统 Tick 中断实现的。

LiteOS-M 的软件定时器有两种模式:单次模式和周期模式。单次模式就是定时到了执行一次就结束,周期模式会一直循环执行。

4.2.1 创建软件定时器

创建 API 是 LOS_SwtmrCreate:

UINT32 LOS_SwtmrCreate(
    UINT32 interval,         // 定时周期,单位:Tick
    UINT8 mode,              // 模式:LOS_SWTMR_MODE_ONCE 或 LOS_SWTMR_MODE_PERIOD
    SWTMR_PROC_FUNC handler, // 回调函数
    UINT32 *timerID,         // 输出参数,定时器句柄
    UINT32 arg               // 回调函数参数
);

mode 参数决定了定时器的行为。单次模式适合做超时检测,周期模式适合做心跳任务。我建议你根据实际需求选,别图省事全用周期模式。

模式对比:
模式 行为 典型场景
LOS_SWTMR_MODE_ONCE 定时到期执行一次,然后停止 超时检测、延时操作
LOS_SWTMR_MODE_PERIOD 定时到期执行,然后重新开始计时 心跳包发送、周期性采样

4.2.2 启动与停止

启动和停止的 API 很简单:

// 启动定时器
UINT32 LOS_SwtmrStart(UINT32 timerID);

// 停止定时器
UINT32 LOS_SwtmrStop(UINT32 timerID);

启动后,定时器就开始倒计时。到了时间,回调函数会在软件定时器任务中被调用。注意,回调函数里不能做阻塞操作,比如等待信号量、读取消息队列等。为什么?因为软件定时器任务是一个系统任务,你把它阻塞了,其他定时器也跟着遭殃。

重要提醒: 软件定时器的回调函数是在任务上下文中执行的,但不是在创建定时器的那个任务里。它是在一个专门的系统定时器任务里统一调度的。所以回调函数里要注意线程安全问题,该加锁就加锁。

4.2.3 软件定时器使用示例

来个周期定时器的例子,每 1000 个 Tick 打印一次日志:

#define TIMER_INTERVAL  1000  // 1000 Tick

UINT32 g_timerID;

VOID TimerCallback(UINT32 arg)
{
    printf("Timer fired! arg=%u\n", arg);
}

VOID TimerDemoInit(VOID)
{
    UINT32 ret;
    
    ret = LOS_SwtmrCreate(TIMER_INTERVAL, 
                          LOS_SWTMR_MODE_PERIOD,
                          TimerCallback,
                          &g_timerID,
                          123);  // 传给回调的参数
    if (ret != LOS_OK) {
        printf("Timer create failed\n");
        return;
    }
    
    ret = LOS_SwtmrStart(g_timerID);
    if (ret == LOS_OK) {
        printf("Timer started\n");
    }
}

// 需要停止时调用
VOID StopTimer(VOID)
{
    LOS_SwtmrStop(g_timerID);
}

4.3 消息队列与定时器的配合使用

在实际项目中,消息队列和软件定时器经常搭配使用。比如,定时器到期后,在回调函数里往消息队列发一条消息,通知其他任务去处理。

我曾经在一个传感器数据采集项目里就是这么干的。定时器每 100ms 触发一次,在回调里发消息通知采集任务去读传感器。这样就把定时触发和数据处理解耦了,代码结构清晰很多。

实战技巧: 定时器回调里发消息时,一定要用非阻塞模式(timeOut=0)。因为回调函数执行时间越短越好,不能在里面等。如果队列满了,消息发不出去,那就丢一帧数据,总比把系统卡死强。

4.4 常见问题与调试方法

最后,我总结几个实际调试中容易遇到的问题:

  • 队列创建失败: 检查内存是否足够,len * maxMsgSize 别太大
  • 消息发送超时: 接收任务是不是被挂起了?队列是不是满了?
  • 定时器不触发: 检查 Tick 配置是否正确,定时器任务优先级是否被其他任务抢占
  • 回调函数死循环: 回调里别做耗时操作,否则整个定时器系统都会卡住

调试的时候,我习惯在关键位置加打印日志,把队列的使用情况和定时器的状态打出来。LiteOS-M 也提供了 LOS_QueueInfoGet 和 LOS_SwtmrInfoGet 这些调试接口,可以查看队列和定时器的内部状态,非常实用。