09 部署第一个驱动程序
本章节介绍如何在 Pico-G1 开发板上编写一个"hello word"驱动程序、并将驱动暴露成用户态接口,供应用程序调用,完成从驱动编写到编译再到调用的完整流程。本章节基于部署第一个应用程序 ,读者需先阅读部署第一个应用程序
驱动执行流程:
- 模块加载时执行 hellodrv_init
- 调用 misc_register() 注册设备
- 内核自动生成 /dev/hellodrv
- 用户态程序打开 /dev/hellodrv
- 用户态通过 ioctl 向驱动发命令
- 驱动通过 copy_to_user() 把数据返回给用户态
- 模块卸载时执行 hellodrv_exit
- 调用 misc_deregister() 注销设备
修改应用程序
请参考 部署第一个应用程序 将helloword.c改成下列格式,并按说明进行编译烧录。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define HELLODRV_MAGIC 'H'
#define HELLODRV_GET_MSG _IOR(HELLODRV_MAGIC, 0x01, char *)
int main(void)
{
int fd;
char buf[64] = {0};
printf("hello world from firmware!\n");
fd = open("/dev/hellodrv", O_RDWR);
if (fd < 0) {
perror("open /dev/hellodrv failed");
return -1;
}
if (ioctl(fd, HELLODRV_GET_MSG, buf) < 0) {
perror("ioctl HELLODRV_GET_MSG failed");
close(fd);
return -1;
}
printf("message from driver: %s\n", buf);
close(fd);
return 0;
}新建驱动程序
# 在SDK根目录中执行:
cd source/kernel/linux-5.10.y/drivers/misc
mkdir hellodrv
cd hellodrv
touch hellodrv.c
touch Makefile
touch Kconfig说明
linux-5.10.y是由menuconfig配置中的Linux System --> Kernel --> Kernel Version决定的,如果配置为4.9版本则需要更换为linux-4.9.y- misc 驱动类型是 Linux 里的“杂项设备驱动”,适合那些不属于特定子系统、但又需要提供 /dev 接口的小型驱动。它通常比完整字符设备驱动更简单,更适合做入门和验证。
编写驱动程序
编写hellodrv.c
- 在内核里注册一个设备
- 在板子上生成一个设备节点,比如 /dev/hellodrv
- 让用户态程序 helloworld 可以通过这个设备节点与驱动通信
- 先实现一个最小功能:通过 ioctl 从驱动读取一段固定字符串
ioctl
ioctl 是 I/O control 的缩写,意思是:输入输出控制接口。 它是 Linux 里用户态程序和驱动程序通信的一种方式。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/ioctl.h>
#include <linux/device.h>
/*
* 驱动名称
* 最终在 /dev 下生成的设备节点就是 /dev/hellodrv
*/
#define HELLODRV_NAME "hellodrv"
/*
* ioctl 魔数
* 用于区分不同驱动的 ioctl 命令,避免冲突
*/
#define HELLODRV_MAGIC 'H'
/*
* ioctl 命令:从驱动获取一段字符串
* _IOR 表示:用户态从内核态读取数据
* 第三个参数表示数据类型,这里是 char *
*/
#define HELLODRV_GET_MSG _IOR(HELLODRV_MAGIC, 0x01, char *)
/*
* 驱动返回给用户态的字符串
* 这里先写死一个固定消息,方便验证用户态和内核态的通信是否正常
*/
static const char *g_msg = "hello from driver";
/*
* 设备打开时的回调函数
* 当用户态执行 open("/dev/hellodrv", ...) 时会进入这里
*/
static int hellodrv_open(struct inode *inode, struct file *filp)
{
pr_info("hellodrv: device opened\n");
return 0;
}
/*
* 设备关闭时的回调函数
* 当用户态执行 close(fd) 时会进入这里
*/
static int hellodrv_release(struct inode *inode, struct file *filp)
{
pr_info("hellodrv: device closed\n");
return 0;
}
/*
* ioctl 回调函数
* 用户态程序可以通过 ioctl(fd, cmd, arg) 与驱动进行控制交互
*
* cmd:用户态传入的命令
* arg:用户态传入的参数,通常是一个用户空间地址
*/
static long hellodrv_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
char msg[64];
switch (cmd) {
case HELLODRV_GET_MSG:
/*
* 先清空临时缓冲区
*/
memset(msg, 0, sizeof(msg));
/*
* 将固定字符串复制到本地缓冲区
*/
snprintf(msg, sizeof(msg), "%s", g_msg);
/*
* 把内核中的数据拷贝到用户空间
* 不能直接把内核地址传给用户态,必须使用 copy_to_user
*/
if (copy_to_user((void __user *)arg, msg, strlen(msg) + 1))
return -EFAULT;
pr_info("hellodrv: ioctl GET_MSG\n");
return 0;
default:
/*
* 不认识的命令,返回非法参数错误
*/
return -EINVAL;
}
}
/*
* 文件操作表
* 内核通过它知道这个设备支持哪些操作
*/
static const struct file_operations hellodrv_fops = {
.owner = THIS_MODULE,
.open = hellodrv_open,
.release = hellodrv_release,
.unlocked_ioctl = hellodrv_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = hellodrv_ioctl,
#endif
};
/*
* misc 设备结构体
* 通过 misc_register 注册后,会自动生成 /dev/hellodrv
*/
static struct miscdevice hellodrv_miscdev = {
.minor = MISC_DYNAMIC_MINOR, /* 动态分配次设备号 */
.name = HELLODRV_NAME, /* 设备节点名 */
.fops = &hellodrv_fops, /* 绑定文件操作集 */
.mode = 0666, /* 设备节点权限:可读可写 */
};
/*
* 模块初始化函数
* 在驱动加载时执行
*/
static int __init hellodrv_init(void)
{
int ret;
ret = misc_register(&hellodrv_miscdev);
if (ret) {
pr_err("hellodrv: misc_register failed, ret=%d\n", ret);
return ret;
}
pr_info("hellodrv: module loaded, /dev/%s created\n", HELLODRV_NAME);
return 0;
}
/*
* 模块退出函数
* 在驱动卸载时执行
*/
static void __exit hellodrv_exit(void)
{
misc_deregister(&hellodrv_miscdev);
pr_info("hellodrv: module unloaded\n");
}
/*
* 指定模块入口和出口
*/
module_init(hellodrv_init);
module_exit(hellodrv_exit);
/*
* 模块信息
*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ljh");
MODULE_DESCRIPTION("Hello driver for helloworld app");编写Makefile
obj-$(CONFIG_HELLODRV) += hellodrv.o编写Kconfig
config HELLODRV
tristate "Hello driver"
help
A simple misc driver for user-space test.更改上级Makefile 和 Kconfig 文件
- 在source/kernel/linux-5.10.y/drivers/misc/Makefile 合适的位置加上这一行,意思是如果配置打开了 CONFIG_HELLODRV,就进入 hellodrv/ 这个目录继续编译。
obj-$(CONFIG_HELLODRV) += hellodrv/ - 在source/kernel/linux-5.10.y/drivers/misc/Kconfig 合适的位置加上这一行,意思是drivers/misc/hellodrv/Kconfig 里面还有一个新的配置项,要把它读进来
source "drivers/misc/hellodrv/Kconfig"
将驱动程序编译成内核模块
- 配置Hello Drv编译选项在末尾添加
vim source/kernel/linux-5.10.y/arch/arm/configs/xmorca_defconfigCONFIG_HELLODRV=m检测是否配置成功grep CONFIG_HELLODRV source/kernel/linux-5.10.y/arch/arm/configs/xmorca_defconfig - 编译内核确认配置是否生效
make linux查找驱动模块grep CONFIG_HELLODRV out/xm7206v12a/linux-5.10.y/.configfind out/xm7206v12a/linux-5.10.y -name "hellodrv.ko" - 将驱动模块装进rootfs中
cd out/xm7206v12a/rootfs/lib/ mkdir -p modules/5.10.0/extr # 回到SDK根目录后执行: cp -f out/xm7206v12a/linux-5.10.y/drivers/misc/hellodrv/hellodrv.ko out/xm7206v12a/rootfs/lib/modules/5.10.0/extr/ # 确定是否复制成功 ls -l out/xm7206v12a/rootfs/lib/modules/5.10.0/extr/hellodrv.ko # 重新打包镜像 make fs_image - 烧录镜像 请参考 烧录章节:镜像烧录 烧录 SPI 镜像。
挂载驱动模块
insmod lib/modules/5.10.0/extr/hellodrv.ko执行应用程序
/usr/bin/helloworld预期输出
/ # insmod lib/modules/5.10.0/extr/hellodrv.ko
hellodrv: module loaded, /dev/hellodrv created
/ # /usr/bin/helloworld
hello world from firmware!
hellodrv: device opened
hellodrv: ioctl GET_MSG
message from driver: hello from driver
hellodrv: device closed
/ #