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的参数系统基于 uORB 和 param 模块。驱动要暴露参数,通常有两种方式:
- 直接使用
param_get/param_set:简单粗暴,适合参数少的驱动。 - 通过
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, ¶m_update);
_param_sample_rate.update();
_param_gain.update();
// 参数变了,重新配置硬件
apply_new_settings();
}
}
这段代码我建议放在驱动的主循环里,每隔几十毫秒调用一次。这样飞控通过 param set 修改参数后,驱动能及时响应。
4.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