3、PM注册与回调机制:pm_register()函数详解、pm_activity()函数详解、回调函数链表的实现、通知机制(Notify)的设计
好,咱们今天来聊聊PM框架里最核心的注册与回调机制。说实话,这部分是NuttX低功耗设计的“神经中枢”。你想想看,系统怎么知道哪个驱动想睡觉?哪个驱动又醒了?全靠这套机制来传递消息。
3.1 pm_register()函数详解
这个函数,说白了就是“我要加入低功耗管理俱乐部”。每个想参与电源管理的驱动或模块,都得先调用它来注册自己。
函数原型长这样:
FAR struct pm_callback_s *pm_register(enum pm_state_e domain,
int domainid,
FAR struct pm_callback_s *callbacks);
参数看着多,其实好理解:
- domain:电源域编号。NuttX支持多个电源域,比如CPU域、外设域。我习惯把实时性要求高的放一个域,省电的放另一个域。
- domainid:域内的子ID。同一个域里可以有多个注册者,用这个区分。
- callbacks:回调函数结构体指针。这是关键,里面放着你的通知函数。
返回值是一个pm_callback_s指针。注册成功后会返回一个句柄,后面取消注册要用到它。
核心要点:pm_register()不会分配内存,它只是把回调结构体链接到内部链表里。所以你的回调结构体必须是静态的或者长期存在的,不能是局部变量!
我在项目中遇到过有人把回调结构体定义在函数内部,结果函数返回后结构体被销毁,系统再通知时直接崩溃。嗯,这个坑我替你们踩过了。
3.2 pm_activity()函数详解
这个函数是“我还在忙,别让我睡”。驱动在干活的时候,要不断调用它来告诉PM框架:我现在有活动,别降频,别休眠。
void pm_activity(int domain, int priority);
参数就两个:
- domain:电源域编号。
- priority:活动优先级。数值越大表示活动越重要,PM框架会据此决定是否允许休眠。
它的内部实现其实很简单:更新一个时间戳和优先级计数器。PM框架的定时器会定期检查这些计数器,如果超时没有新的pm_activity()调用,就认为系统空闲了,开始尝试降功耗。
我的建议:中断服务程序里不要调用pm_activity(),因为中断上下文可能不允许阻塞操作。我一般会在中断的下半部(比如工作队列)里调用。
你可能会问:调用频率多高合适?我个人习惯是:每次处理完一批数据后调用一次,优先级设成当前任务的紧急程度。比如音频播放设高优先级,后台同步设低优先级。
3.3 回调函数链表的实现
PM框架内部维护了一个回调链表。每个注册的驱动都对应一个链表节点。当电源状态要变化时,框架会遍历这个链表,挨个通知。
链表节点的结构体定义:
struct pm_callback_s {
struct pm_callback_s *next; /* 链表指针 */
uint8_t domain; /* 电源域 */
uint8_t domainid; /* 域内ID */
int (*prepare)(FAR struct pm_callback_s *cb, int domain,
enum pm_state_e pmstate);
int (*notify)(FAR struct pm_callback_s *cb, int domain,
enum pm_state_e pmstate);
int (*notify_prepare)(FAR struct pm_callback_s *cb, int domain,
enum pm_state_e pmstate);
};
三个回调函数指针:
- prepare:准备进入某状态。如果驱动说“不行,我现在不能睡”,返回非零值,状态切换就会被阻止。
- notify:状态已经切换完成的通知。驱动在这里做善后工作,比如关闭外设时钟。
- notify_prepare:这是NuttX后来加的,用于更精细的控制。我一般用prepare做检查,用notify做动作。
注意:回调函数是在PM框架的上下文中调用的,不能长时间阻塞。我曾经见过有人在notify里调用printf,结果串口还没初始化完,系统就卡死了。回调里只做轻量级操作,重活交给工作队列。
链表操作是标准的插入和遍历。注册时插入到链表头部,取消注册时从链表移除。遍历时如果某个回调返回错误,遍历会提前终止,状态切换失败。
3.4 通知机制(Notify)的设计
通知机制是整个PM框架的“消息总线”。它要解决一个核心问题:当电源状态变化时,如何高效、可靠地通知所有相关方。
设计思路是这样的:
- 状态机驱动:PM框架内部有一个状态机,管理着各个电源域的当前状态。
- 通知触发:当状态机决定切换状态时(比如从IDLE到SLEEP),开始遍历回调链表。
- 两阶段通知:先调用所有驱动的prepare回调,检查是否都能接受新状态。如果都OK,再调用notify回调执行实际切换。
- 超时保护:每个回调都有时间限制,超时未返回会被视为失败。
这种两阶段设计有什么好处?我举个例子:假设系统要进入深度睡眠,但UART还在接收数据。prepare阶段UART会返回失败,状态切换被取消,数据不会丢失。如果直接通知,UART可能来不及保存数据就被断电了。
设计精髓:通知机制不是简单的广播,而是带“投票”功能的协商过程。每个驱动都有否决权,这保证了系统的安全性。
实际代码中,通知的入口函数是pm_notify(),它被状态机调用。内部实现大致是:
static int pm_notify(int domain, enum pm_state_e newstate)
{
struct pm_callback_s *cb;
int ret;
/* 第一阶段:prepare */
cb = g_pmcbhead[domain];
while (cb) {
if (cb->prepare) {
ret = cb->prepare(cb, domain, newstate);
if (ret < 0) {
return ret; /* 有人反对,切换失败 */
}
}
cb = cb->next;
}
/* 第二阶段:notify */
cb = g_pmcbhead[domain];
while (cb) {
if (cb->notify) {
cb->notify(cb, domain, newstate);
}
cb = cb->next;
}
return OK;
}
代码看着简单,但实际要考虑并发问题。多个电源域可能同时切换,回调链表可能被并发修改。NuttX用自旋锁来保护链表操作,确保线程安全。
避坑指南:我曾经在回调里尝试调用pm_activity()来阻止休眠,结果造成了死锁——因为pm_activity()内部也用了同一把锁。记住:回调里不要调用任何PM框架的API,除非文档明确说可以。
最后说一点我的体会:通知机制的设计好坏,直接决定了低功耗系统的稳定性。好的通知机制能让各个驱动优雅地配合,坏的通知机制会导致系统频繁崩溃或者无法进入深度睡眠。NuttX这套两阶段+链表遍历的设计,虽然简单,但经过多年验证,非常可靠。
下一节我们会讲状态机的具体实现,看看这些回调是怎么被串联起来的。