从0开始学linux韦东山教程Linux驱动入门实验班(4)
本人从0开始学习linux,使用的是韦东山的教程,在跟着课程学习的情况下的所遇到的问题的总结,理论虽枯燥但是是基础。本人将前几章的内容大致学完之后,考虑到后续驱动方面得更多的开始实操,后续的内容将以韦东山教程Linux驱动入门实验班的内容为主,学习其中的代码并手敲。做到锻炼动手能力的同时钻研其中的理论知识点。
摘要:这篇文档主要介绍的是以下几个方面,上文我们对节点以及驱动代码和应用代码的理解有所了解,这篇文档主要是学习驱动代码以及应用代码的八股文,并且对其有深入的了解,逐渐学会自己搭建架构。文章主体主要分为两部分,首先驱动八股文代码的部分讲解,led驱动以及硬件代码的逐行讲解其是如何配合八股文的。这篇文档本人认为是最重要的,正就是最基础的东西了,把细节真的理清楚了。
摘要关键词:驱动代码、应用代码、led驱动
本文详细介绍以下问题,如果你遇到了以下问题,看看我的方案能否解决。
1.驱动八股文代码
2.led灯驱动程序代码撰写详注
3.led_test.c应用代码详注
以下命令行想必已经在上文中见过并学习过。
cat /sys/kernel/debug/gpio
bear make
ctrl+h
arm-buildroot-linux-gnueabihf-gcc
cat /sys/kernel/debug/gpio:查看GPIO的引脚连接。
bear make:编译文件生成json文件,设置编译器为arm内核。
ctrl+h:vscode中的单文件中全局搜索。
arm-buildroot-linux-gnueabihf-gcc:将编译器设置为此编译器。
1.驱动八股文代码
gpio_drv.c 代码逐行解析
驱动程序部分,首先需要将模块进行初始化,以下的功能是可有可无的,得根据具体情况进行配置首先申请GPIO中断函数,这部分如果需要用中断唤醒GPIO的话如按键中断等,可以使用此函数。设置定时器函数是,定时器的初始化必须的部分。中断处理函数是触发中断后会自动进入中断触发函数。异步通知也就是按键按下后才传输数据,其它时候不传数据。接下来讲解其中的部分代码。
代码源文件件我就不放在这里了,只展示代码截图。
首先定义了一个结构体gpio_desc,结构体内部变量如图所示。char *name就是指针数组,可以承接任意长度的名字。你会发现这个结构体里面还有一个结构体,timer_list 给了key_timer,而timer_list长相如图所示,这也就是定时器结构体需要配置的一系列东西。
再然后就是 定义两个结构体gpio,其实那两个结构体可以拆开写,习惯应该是以下写法。定时器结构体可以不调用。
static struct gpio_desc gpios[2];gpios[0].gpio = 131;
gpios[0].irq = 0;
gpios[0].name = "gpio_100ask_1";
gpios[0].key = 0;gpios[1].gpio = 132;
gpios[1].irq = 0;
gpios[1].name = "gpio_100ask_2";
gpios[1].key = 0;
定义主设备号以及一系列参数,定义*button_fasync指针结构体。
fasync_struct 是 Linux 内核中用来实现异步信号通知机制的结构体。具体来说,异步信号通知是一种通知用户空间程序某些事件发生(例如设备状态改变)的机制,常用于字符设备驱动中。
定义了一个宏 NEXT_POS(x),用来计算循环缓冲区的下一个位置。计算当前位置 x 的下一个位置,同时保证如果当前位置超出了缓冲区的末尾,就回绕到缓冲区的起始位置。
假设 BUF_LEN = 4,那么:
NEXT_POS(0) 返回 1
NEXT_POS(1) 返回 2
NEXT_POS(2) 返回 3
NEXT_POS(3) 返回 0(因为 3 + 1 = 4,4 % 4 = 0)
按键内容清空函数,当读取内容等于写入内容时返回1,否则返回0。
is_key_buf_full函数返回当前写入的数据量,也就是已经输入了多少占了多少位置了。
输入按键,读取按键最终的输出是和w、r等相关的,函数并不复杂但是,具体怎么工作得看后面的代码。
static DECLARE_WAIT_QUEUE_HEAD(gpio_wait);
DECLARE_WAIT_QUEUE_HEAD 宏:这个宏用于声明并初始化一个等待队列头。它的作用是创建一个等待队列,并且确保它已经准备好可以使用。
这个函数可以看出按键定时器,输入的data格式和gpio_desc结构得一致,通过gpio_get_value函数读取gpio_desc中的gpio的状态。
最终返回的状态为int类型。
key = (gpio_desc->key) | (val<<8);
结合了按键的标识符和 GPIO 引脚的电平值,最终生成一个新的值 key
假设:
gpio_desc->key = 100(表示按键编号是 100)。
val = 1(表示 GPIO 引脚电平为高)。
那么,执行 key = (gpio_desc->key) | (val << 8) 后,得到的 key 是:
key = 100 | (1 << 8);
key = 100 | 256;
key = 356;
输入按键,等待中断,结束按键异步通讯。大概了解八股文后开始尝试应用,首先最简单的led灯的驱动。
2.led灯驱动程序代码撰写
bear make
ctrl+h
arm-buildroot-linux-gnueabihf-gcc
先讲解一下撰写代码的思路,首先应用程序部分设计write函数写入buf,led_write函数接收buf的内容。buf[0]表示控制哪一盏led,buf[1]表示亮灭。应用程序read函数调用驱动led_read读取函数读取buf状态。头文件配置就不做过多讲解了。
struct gpio_desc{int gpio;char *name;
} ;
static struct gpio_desc gpios[2] = {{131, "led0", },{132, "led1", },
};
如上图代码所示,结构体仅需要引脚号以及给它命名即可。具体引脚号为什么是131,132请参考下文。
/* 实现对应的open/read/write等函数,填入file_operations结构体 */
static ssize_t gpio_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{char tmp_buf[2];int err;int count = sizeof(gpios)/sizeof(gpios[0]);if (size != 2) return -EINVAL;err = copy_from_user(tmp_buf,buf,1);if(tmp_buf[0] >= count)return -EINVAL;tmp_buf[1] = gpio_get_value(gpios[tmp_buf[0]].gpio); err = copy_to_user(buf,tmp_buf,2);return 2;
}static ssize_t gpio_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{unsigned char ker_buf[2];int err;if (size != 2)return -EINVAL;err = copy_from_user(ker_buf, buf, size);if (ker_buf[0] >= sizeof(gpios)/sizeof(gpios[0]))return -EINVAL;gpio_set_value(gpios[ker_buf[0]].gpio, ker_buf[1]);return 2;
}
详述一下驱动的gpio_drv_read和gpio_drv_write代码。
gpio_drv_read
首先读取函数套用模板,定义一个tmp_buf[2]数组用于接收数据,首先使用count计算代码中写了几个GPIO结构体也就是设置了几个引脚。读取数据的时候主要是输输入格式为./led_test 0所以也就两个字符,如果不是两个字符(if (size != 2)),就不进入读取模式返回错误。
err = copy_from_user(tmp_buf,buf,1);复制成功,返回 0。tmp_buf目标地址,指向内核空间的缓冲区,用来接收从用户空间传递的数据。buf源地址,指向用户空间的缓冲区,存储了要传送的数据。 1是要复制的字节数为1。if(tmp_buf[0] >= count)超出了引脚设置的数量,就报错。一般这些报错是不会出现的。gpio_get_value() 是用来读取 GPIO 引脚的电平状态(高或低)的函数,tmp_buf[0]中会有引脚编号信息,然后读取此引脚的高低电平。再将copy_to_user(buf,tmp_buf,2);tmp_buf的数据传输回应用buf中。
gpio_drv_write
首先读取函数套用模板,定义一个ker_buf[2]数组用于写入状态, err = copy_from_user(ker_buf, buf, size);将应用输入的数据buf拷贝给ker_buf,如果ker_buf[0]大于设置引脚数报错。gpio_set_value(gpios[ker_buf[0]].gpio, ker_buf[1]);设置引脚ker_buf[0]也就是你输入的引脚编号的状态,状态为ker_buf[1],它肯定不会是on或者off,一定在某个地方进行了处理。
/* 定义自己的file_operations结构体 */
static struct file_operations gpio_key_drv = {.owner = THIS_MODULE,.read = gpio_drv_read,.write = gpio_drv_write,
};
定义入口函数,只要用户空间程序在 这个驱动程序注册并管理的设备文件(比如 /dev/gpio_key 或其他你创建的节点)上执行 read() 系统调用,内核就一定会调用你写的 gpio_drv_read 函数来处理该请求。 这就是 Linux 设备驱动模型的标准工作方式。
gpio_drv_init函数
/* 在入口函数 */
static int __init gpio_drv_init(void)
{int err;int i;int count = sizeof(gpios)/sizeof(gpios[0]);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);for (i = 0; i < count; i++){ // set pin as output err = gpio_request(gpios[i].gpio,gpios[i].name); if (err < 0) {printk("can not request gpio %d %s\n",gpios[i].gpio,gpios[i].name);return -ENODEV;}gpio_direction_output(gpios[i].gpio, 1);}/* 注册file_operations */major = register_chrdev(0, "100ask_led", &gpio_key_drv); /* /dev/gpio_desc */gpio_class = class_create(THIS_MODULE, "100ask_led_class");if (IS_ERR(gpio_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "100ask_led_class");return PTR_ERR(gpio_class);}device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "100ask_led"); /* /dev/100ask_gpio */return err;
}
驱动初始化,记录使用的gpio数量,gpio_request(gpios[i].gpio,gpios[i].name); 主要用于请求访问一个 GPIO(通用输入输出)引脚,通过上述构建的结构体填入。通过循环会依次注册131,led1;132,led2。if (err < 0)反馈引脚错误。gpio_direction_output(gpios[i].gpio, 1);是将gpio设置成高电平。
register_chrdev() 用于在内核中注册一个字符设备,第一个参数:主设备号。如果设置为 0,内核会自动分配一个未使用的主设备号。第二个参数:设备名称 “100ask_led”,这个名称会显示在 /proc/devices 文件中。第三个参数:一个 file_operations 结构体指针,gpio_key_drv 用于定义设备操作函数,如 read()、write() 等。device_create() 用于创建一个具体的设备,并为该设备在 /dev 目录下创建一个设备文件(例如 /dev/100ask_led)。
输入以下可以查看字符设备以及其设备号。
cat /proc/devices
小结:但是本人对gpio_class =class_create(THIS_MODULE, “100ask_led_class”);的理解也不够深入不知道其真正的作用。AI解释的也有点茫然。
这个时候我也在思考,驱动程序中为什么要创建device啊?为什么要class_create类呢?如果不这么干又会出现啥问题呢?
为什么要创建设备?
用户空间与内核空间的接口:
内核中的驱动程序和用户空间的应用程序之间的交互通常是通过设备文件来完成的。驱动程序使用 device_create() 来创建一个设备文件(如 /dev/100ask_led),用户空间的程序通过打开、读写这个文件与硬件进行交互。
设备文件作为内核和用户空间之间的通信桥梁,应用程序通过标准的文件操作接口(open()、read()、write()、ioctl() 等)来与硬件设备进行交互。
设备节点的管理:
创建设备节点(如 /dev/100ask_led)可以让用户空间程序通过设备路径直接访问驱动程序所管理的硬件。
驱动程序通过 device_create() 在 /dev 目录中创建一个设备文件,用户可以通过该设备文件执行相应的操作。
2. class_create 的作用:创建设备类
/sys/class 目录: 这行代码在 sysfs 文件系统中 (/sys/class/) 创建一个名为 “100ask_led_class” 的新设备类 (Class)。
分类管理: 设备类就像一个文件夹/分类器,用来逻辑上组织具有相同类型或功能的设备。例如,你可能会有 leds、input、backlight、tty 等不同的类。在这个例子中,你的 GPIO LED 驱动把自己归类到 “100ask_led_class” 类下。
3.不做 class_create 和 device_create 会出现什么问题?
/dev 节点不会自动创建! 这是最直接、最严重的后果。用户空间的程序(你的测试程序)无法打开设备文件(open("/dev/100ask_led", ...)),因为根本不存在 /dev/100ask_led。
/* 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数*/
static void __exit gpio_drv_exit(void)
{int i;int count = sizeof(gpios)/sizeof(gpios[0]);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);device_destroy(gpio_class, MKDEV(major, 0));class_destroy(gpio_class);unregister_chrdev(major, "100ask_led");for (i = 0; i < count; i++){gpio_free(gpios[i].gpio);}
}
1.device_destroy():销毁设备文件,删除 /dev/100ask_led。
2.class_destroy():销毁设备类 gpio_class,清理类相关的资源。
3.unregister_chrdev():注销字符设备,释放主设备号。
4.释放 GPIO 引脚资源:通过 gpio_free() 函数释放申请的 GPIO 引脚,确保硬件资源得以清理。
module_init(gpio_drv_init);
module_exit(gpio_drv_exit);
MODULE_LICENSE("GPL");
指定驱动程序模块初始化时执行的函数。指定驱动程序模块卸载时执行的清理函数。定义模块的许可证类型。MODULE_LICENSE() 宏用于声明驱动程序的许可证类型。在这里,“GPL” 表示该模块采用 GNU 通用公共许可证(GPL)。
3.led_test.c应用代码
static int fd;/** ./led_test <0|1|2..> on* ./led_test <0|1|2..> off* ./led_test <0|1|2..> */
int main(int argc, char **argv)
{int ret;char buf[2];int i;/* 1. 判断参数 */if (argc < 2) {printf("Usage: %s <0|1|2|...> [on | off]\n", argv[0]);return -1;}/* 2. 打开文件 */fd = open("/dev/100ask_led", O_RDWR);if (fd == -1){printf("can not open file /dev/100ask_led \n");return -1;}if (argc == 3){buf[0] = strtol(argv[1],NULL,0);if (strcmp(argv[2],"on") == 0)buf[1] = 0;else buf[1] = 1;ret = write(fd,buf,2);}else{buf[0] = strtol(argv[1],NULL,0);ret = read(fd,buf,2);if (ret == 2){printf("led %d status is %s\n",buf[0],buf[1] == 0 ? "on":"off");}}close(fd);return 0;
}
if (argc < 2)其实没什么用,也就是当你的输入为./led_test会执行一下。
fd = open(“/dev/100ask_led”, O_RDWR);打开方式是 O_RDWR,即以读写方式打开设备文件,系统调用打开设备文件 /dev/100ask_led。if (argc == 3)也就是./led_test 0 on。
buf[0] = strtol(argv[1],NULL,0);将命令行参数 argv[1] 转换为长整型数值并存储到 buf[0] 中,也就是引脚标号0给了buf[0],
strcmp() 是字符串比较函数,如果 argv[2] 与 “on” 相等,则返回 0 。上文埋下的伏笔在此处解决了。
ret = write(fd,buf,2);将 buf 中的前两个字节(即 buf[0] 和 buf[1])写入设备文件,也就会调用驱动查询的gpio_drv_write。不然就读取状态。最后释放fd。
其实到此你就该明白了,其实应用程序也就是调用100ask_led设备文件。buf也就是应用程序的数据,应用程序的数据通过write,read函数访问驱动程序的 gpio_drv_read,gpio_drv_write从而实现引脚的控制。驱动程序的tmp_buf和ker_buf其实也就是用来复制粘贴应用程序的buf数据的。
adb push led_test led_drv.ko root
cat /sys/kernel/debug/gpio
gpio的编号查看也就是输入以上命令行
可以看到gpio5的大小为128到159,131也就是gpio5上的gpio5_3。
以下为输入的命令行以及其最终的测试效果。
[root@100ask:~]# insmod led_drv.ko
[ 659.943017] led_drv: loading out-of-tree module taints kernel.
[root@100ask:~]# ./led_test 0 on
[root@100ask:~]# ./led_test 0 off
[root@100ask:~]# ./led_test 0
led 0 status is off