4、设备驱动注册与注销:模块化加载、设备节点创建、参数接口绑定

驱动写好了,怎么让它跑起来?

说白了,就是三个步骤:把驱动装进系统、给用户留个操作入口、把参数挂上去让飞控能调。我刚开始接触PX4驱动时,总觉得注册就是调个函数的事。后来踩了坑才明白——注册的顺序、节点的命名、参数的绑定时机,哪一步出问题,整个驱动都可能罢工。

4.1 模块化加载:让驱动成为“插件”

PX4的驱动加载方式有两种:编译进固件,或者作为模块动态加载。我个人更推荐后者,尤其是在调试阶段。

动态加载的好处很明显:改一行代码,不用重新烧录整个固件。你想想看,每次改个参数都要等编译+烧录,那效率得多低。

驱动模块的入口函数长这样:

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

这个 __EXPORT 是关键。它告诉编译器:这个函数要暴露出来,供系统调用。我在项目中遇到过有人忘了加这个宏,结果模块加载时系统根本找不到入口,报错信息还特别隐晦。

模块的启动逻辑,通常放在 main 函数里:

int my_driver_main(int argc, char *argv[])
{
    if (argc < 2) {
        PX4_ERR("usage: my_driver {start|stop|status}");
        return -1;
    }

    if (!strcmp(argv[1], "start")) {
        // 启动驱动
        return my_driver_start();
    } else if (!strcmp(argv[1], "stop")) {
        // 停止驱动
        return my_driver_stop();
    } else if (!strcmp(argv[1], "status")) {
        // 查询状态
        return my_driver_status();
    }

    return -1;
}
小技巧:我习惯在 start 分支里加一个 PX4_INFO 打印,输出驱动的版本号和编译时间。这样在排查问题时,一眼就能知道当前跑的是哪个版本。

4.2 设备节点创建:给用户一个“文件”接口

驱动注册完了,用户怎么跟它交互?在Linux里,一切皆文件。PX4继承了这一思想——驱动注册成功后,会在 /dev 下创建一个设备节点。

创建设备节点的核心函数是 register_driver

int register_driver(const char *name, const struct file_operations *fops, 
                    mode_t mode, void *priv);

参数说明:

参数 含义 我的建议
name 设备节点路径,如 /dev/my_driver 命名要规范,最好跟模块名一致
fops 文件操作函数集(open, close, read, write, ioctl等) 至少实现 open 和 ioctl,否则驱动没意义
mode 访问权限,通常用 0666 调试阶段用 0666,发布前记得收紧权限
priv 私有数据指针,通常传 this 用于在回调中访问驱动实例

我曾经犯过一个低级错误:在 register_driver 之前就调用了 ioctl。结果用户空间程序一跑就崩溃,查了半天才发现是节点还没创建。嗯,顺序很重要。

注销设备节点用 unregister_driver

int unregister_driver(const char *name);

这里有个坑:注销前要确保没有进程在占用这个节点。否则系统会报 EBUSY,节点删不掉。我建议在 stop 函数里先发一个信号通知用户程序退出,再执行注销。

4.3 参数接口绑定:让飞控能调你的驱动

驱动写好了,节点也创建了,但飞控怎么知道你的驱动有哪些参数可以调?这就需要参数接口绑定。

PX4的参数系统基于 uORBparam 模块。驱动要暴露参数,通常有两种方式:

  1. 直接使用 param_get / param_set:简单粗暴,适合参数少的驱动。
  2. 通过 Parameter 类封装:更规范,支持自动同步和回调。

我个人偏爱第二种。来看个例子:

class MyDriver : public ModuleBase<MyDriver>
{
public:
    MyDriver() :
        _param_sample_rate(this, "SMP_RATE", false),
        _param_gain(this, "GAIN", false)
    {
        // 构造函数里绑定参数
    }

    int init() {
        // 读取参数初始值
        _param_sample_rate.update();
        _param_gain.update();
        return 0;
    }

private:
    param_t _param_sample_rate;
    param_t _param_gain;
};

这里要注意 param_t 的第三个参数 false。它表示“不自动保存到文件”。如果你希望参数修改后能持久化,就改成 true

警告:参数绑定的时机很重要。我建议在驱动初始化完成、设备节点创建之后,再绑定参数。因为参数更新时可能会触发回调,如果驱动还没准备好,回调里访问了未初始化的变量,那就等着崩溃吧。

参数更新回调的写法:

void MyDriver::parameter_update_poll()
{
    bool updated = false;
    orb_check(_param_update_sub, &updated);

    if (updated) {
        orb_copy(ORB_ID(parameter_update), _param_update_sub, &param_update);
        _param_sample_rate.update();
        _param_gain.update();

        // 参数变了,重新配置硬件
        apply_new_settings();
    }
}

这段代码我建议放在驱动的主循环里,每隔几十毫秒调用一次。这样飞控通过 param set 修改参数后,驱动能及时响应。

4.4 整体流程:一张图说清楚

说了这么多,咱们用一张图把整个流程串起来:

驱动注册与注销核心流程 1. 模块入口 my_driver_main() 2. 初始化硬件 init() / probe() 3. 注册设备节点 register_driver() 4. 绑定参数接口 param_t / Parameter类 5. 进入主循环 task_main() / poll() 6. 注销与清理 unregister_driver() / deinit() 关键注意事项 • 入口函数必须 __EXPORT • 先初始化硬件再注册节点 • 节点命名要规范 • 参数绑定在初始化之后 • 注销前检查是否有进程占用 • 主循环中轮询参数更新 • 错误处理要完善 • 日志输出要清晰 常见错误: 1. 忘记 EXPORT 入口 2. 节点注册顺序颠倒 3. 参数回调访问空指针 4. 注销时资源未释放

4.5 避坑指南

做驱动开发这么多年,我总结了几条血泪教训:

  • 节点路径别写死:我建议用宏定义,方便后期移植。比如 #define DEVICE_PATH "/dev/my_driver"
  • 参数名要有前缀:PX4的参数命名空间是全局的。你的驱动参数最好加上模块名前缀,比如 MYD_SMP_RATE,避免跟其他驱动冲突。
  • 注销时要逆序:先停主循环,再解绑参数,最后注销节点。顺序反了,轻则内存泄漏,重则系统崩溃。
  • 多实例驱动要小心:如果你的驱动支持多个实例(比如多个传感器),每个实例的设备节点名要唯一。我习惯用 /dev/my_driver0/dev/my_driver1 这样的命名方式。

核心要点回顾:

  • 模块入口用 __EXPORT 暴露
  • 设备节点用 register_driver 创建,unregister_driver 注销
  • 参数绑定推荐用 Parameter 类,支持自动更新回调
  • 注册和注销的顺序至关重要,搞反了就是灾难

嗯,驱动注册与注销这块,说白了就是“先来后到”四个字。顺序对了,一切都顺。顺序错了,调试能调到你怀疑人生。希望今天的内容能帮你少走些弯路。


公众号:蓝海资料掘金营,微信deep3321