4、驱动模块初始化:模块入口函数、任务创建、调度器与优先级

好,咱们今天聊点实在的。驱动模块怎么跑起来?说白了,就是三个问题:入口在哪、任务怎么创建、调度器怎么管。我做了这么多年嵌入式,发现很多新手甚至老手,都在这里栽过跟头。

4.1 模块入口函数:一切从这里开始

每个PX4驱动模块,都有一个固定的入口。你想想看,系统启动后,总得有个地方叫醒你的驱动吧?这个入口就是module_main函数。

我个人习惯把入口函数写得尽量简洁。它只做三件事:解析参数、启动任务、返回结果。别把初始化逻辑全塞进去,否则后面调试起来你会想哭的。

extern "C" __EXPORT int led_control_main(int argc, char *argv[]);

int led_control_main(int argc, char *argv[])
{
    // 解析命令行参数
    if (argc < 2) {
        PX4_ERR("usage: led_control start|stop|status");
        return -1;
    }

    // 根据命令执行不同操作
    if (!strcmp(argv[1], "start")) {
        // 启动驱动任务
        return led_control_start();
    } else if (!strcmp(argv[1], "stop")) {
        return led_control_stop();
    } else if (!strcmp(argv[1], "status")) {
        return led_control_status();
    }

    return -EINVAL;
}

这里有个细节要注意:__EXPORT这个宏。它告诉编译器,这个函数要暴露给系统调用。我在项目中遇到过有人忘了加这个宏,结果驱动怎么都启动不了,查了半天才发现是符号没导出。

核心要点:入口函数必须用__EXPORT导出,且命名格式为模块名_main。这是PX4的约定,别搞特殊。

4.2 任务创建:让驱动真正跑起来

入口函数只是敲门砖。真正的驱动逻辑,需要在一个独立的任务里运行。PX4用的是NuttX实时操作系统,任务创建靠的是px4_task_spawn_cmd

嗯,这里要注意。任务创建不是随便new一个线程就完事了。你得指定栈大小、优先级、任务名。这些参数选不好,后面就是各种玄学问题。

static int led_control_start()
{
    // 检查是否已经在运行
    if (g_led_task != -1) {
        PX4_WARN("led_control already running");
        return -1;
    }

    // 创建任务
    g_led_task = px4_task_spawn_cmd("led_control",
                                     SCHED_DEFAULT,
                                     SCHED_PRIORITY_DEFAULT,
                                     2048,  // 栈大小,单位字节
                                     led_control_thread,
                                     nullptr);

    if (g_led_task < 0) {
        PX4_ERR("task spawn failed: %d", errno);
        return -errno;
    }

    return 0;
}

我曾经在项目里把栈大小设成了1024字节,结果驱动跑一会儿就莫名其妙崩溃。查了两天才发现是栈溢出了。你想想看,一个驱动里要处理消息队列、I2C通信、状态机,1024字节哪够啊?

经验之谈:栈大小建议从2048起步。如果驱动逻辑复杂,比如要处理大量数据,直接上4096。别省这点内存,省出来的都是坑。

4.3 调度器与优先级:谁先跑谁后跑

任务创建好了,谁先执行?这就涉及到调度器和优先级了。PX4/NuttX用的是优先级抢占式调度。说白了,优先级高的任务可以打断优先级低的任务。

优先级怎么设?我有个简单的原则:

  • 高优先级(100以上):留给时间关键型任务,比如传感器数据采集、控制输出。这些任务必须准时响应,晚一毫秒都可能炸机。
  • 中优先级(50-100):给常规驱动任务,比如LED控制、GPS数据处理。可以稍微等一等,但不能太久。
  • 低优先级(50以下):给后台任务,比如日志记录、参数更新。这些任务慢一点没关系。
优先级范围 典型任务 响应要求
100-255 传感器、控制输出 毫秒级
50-100 驱动模块、数据处理 十毫秒级
0-50 日志、参数、维护 百毫秒级

你可能会问:优先级设高了会不会有问题?当然会。我记得有一次,我把一个调试用的打印任务设成了高优先级,结果它一直抢占CPU,导致飞控控制周期不稳定,飞机在空中晃得跟喝醉了似的。

避坑指南:我曾经把两个驱动设成相同优先级,结果它们互相抢占,谁都没法完整执行一个周期。后来我加了个信号量做同步,才解决问题。记住:同优先级任务之间,要用同步机制协调。

4.4 任务循环:驱动的主心骨

任务创建后,真正干活的是任务线程函数。它通常是一个无限循环,不断检查是否有新数据、是否需要执行操作。

static int led_control_thread(int argc, char *argv[])
{
    // 初始化硬件
    if (led_hw_init() != 0) {
        PX4_ERR("hardware init failed");
        return -1;
    }

    // 订阅需要的主题
    int led_sub = orb_subscribe(ORB_ID(led_control));

    // 主循环
    while (!g_led_should_exit) {
        struct led_control_s led_data;
        bool updated = false;

        // 检查是否有新消息
        orb_check(led_sub, &updated);

        if (updated) {
            orb_copy(ORB_ID(led_control), led_sub, &led_data);
            // 执行LED控制
            led_set_color(led_data.color);
            led_set_brightness(led_data.brightness);
        }

        // 休眠等待,避免空转
        px4_usleep(10000); // 10ms
    }

    // 清理资源
    led_hw_deinit();
    orb_unsubscribe(led_sub);

    return 0;
}

这里有个关键点:px4_usleep。别小看这个休眠,它让出CPU给其他任务用。如果你不加休眠,这个任务会一直占着CPU,其他任务就别想跑了。

重要提醒:主循环里一定要有休眠或等待机制。否则就是忙等,CPU利用率直接飙到100%,飞控还能不能好好飞了?

4.5 整体流程:一张图看懂

说了这么多,咱们用一张图把整个流程串起来。从入口函数到任务创建,再到调度执行,每一步都环环相扣。

驱动模块初始化流程 module_main 入口 解析命令行参数 px4_task_spawn_cmd 调度器分配优先级 任务主循环执行 关键参数 • 栈大小:2048+ • 优先级:50-100 • 任务名:模块名 • 调度策略:SCHED_DEFAULT • 休眠间隔:10ms • 退出标志:g_xxx_exit • 订阅主题:orb_subscribe • 硬件初始化:xxx_hw_init • 资源清理:xxx_hw_deinit

这张图把整个流程串起来了。从入口函数开始,经过参数解析、任务创建、调度分配,最后进入主循环。每一步都有对应的代码实现,缺一不可。

好了,关于驱动模块初始化,核心就是这些。记住:入口要简洁、任务要合理、优先级要谨慎。下次你写驱动的时候,照着这个思路来,基本不会出大问题。