4. LiteOS-M 内核基础:消息队列与软件定时器
消息队列和软件定时器,是 LiteOS-M 内核里两个非常基础但又极其重要的组件。说白了,消息队列解决的是任务间“怎么传数据”的问题,软件定时器解决的是“到了时间干什么”的问题。我在好几个项目里都吃过它们的亏,今天就把这些经验掰开揉碎了讲给你听。
- 消息队列的创建、发送、接收机制
- 软件定时器的创建、启动、停止流程
- 实际项目中的避坑指南
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 字节的消息。
4.1.2 发送消息
发送消息用 LOS_QueueWrite。看代码:
UINT32 LOS_QueueWrite(
UINT32 queueID, // 队列句柄
VOID *bufferAddr, // 要发送的数据缓冲区
UINT32 bufferSize, // 数据长度
UINT32 timeOut // 超时时间,单位:Tick
);
timeOut 参数很关键。填 0 表示队列满了就直接返回失败,不等待。填 LOS_WAIT_FOREVER 表示死等,直到队列有空位。我建议在中断服务函数里用 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 触发一次,在回调里发消息通知采集任务去读传感器。这样就把定时触发和数据处理解耦了,代码结构清晰很多。
4.4 常见问题与调试方法
最后,我总结几个实际调试中容易遇到的问题:
- 队列创建失败: 检查内存是否足够,len * maxMsgSize 别太大
- 消息发送超时: 接收任务是不是被挂起了?队列是不是满了?
- 定时器不触发: 检查 Tick 配置是否正确,定时器任务优先级是否被其他任务抢占
- 回调函数死循环: 回调里别做耗时操作,否则整个定时器系统都会卡住
调试的时候,我习惯在关键位置加打印日志,把队列的使用情况和定时器的状态打出来。LiteOS-M 也提供了 LOS_QueueInfoGet 和 LOS_SwtmrInfoGet 这些调试接口,可以查看队列和定时器的内部状态,非常实用。