1 总结 3 种写驱动程序的方法
1.1 资源和驱动在同一个文件里
应用程序调用open等函数最简单的方法是驱动层也提供对应的drv_open,应用程序调用read,驱动层也提供对应的drv_read等等。需要写出驱动层的函数,为了便于管理,将这些函数放到file_operations结构体中,即第一步定义对应的file_operations结构体,并实现对应的open等程序(第一步);实现完成之后,将file_operations结构体通过register_chrdev注册到内核(第二步);然后通过入口函数调用注册函数(chrdev),安装驱动程序的时候,内核会调用入口函数,完成file_operations结构体的注册(第三步);有入口函数就有出口函数(第四步)。对于字符设备驱动程序而言,file_operations结构体是核心,每一个驱动程序都对应file_operations结构体,内核中有众多file_operations结构体,怎么才能找到对应的结构体呢?
应用程序要访问驱动程序,需要打开一个设备结点,里面有主设备号,根据设备结点的主设备号在内核中找到对应的file_operations结构体。注册结构体时足以提供主设备号,可以让内核分配;最后就是完善信息,创建类,创建设备。
1.2 资源用 platform_device 指定、驱动在 platform_driver 实现
详见:
韦东山嵌入式linux系列-驱动进化之路:总线设备驱动模型-CSDN博客
韦东山嵌入式linux系列-LED 模板驱动程序的改造:总线设备驱动模型-CSDN博客
左边是和具体硬件打交道,右边对应是抽象。通过总线bus管理,两两匹配,右边的代码基本不用修改,左边的代码针对不同的硬件提供不同的device。会有大量的,导致内核庞大臃肿。
首先用通用的platform_device结构体定义资源(引脚)代替之前的board_A_led.c;
再用platform_driver(里面有probe函数)结构体代替之前的chip_demo_gpio.c,当左右两边匹配的时候,probe函数就会被调用。
匹配之后具体做什么呢?
(1)记录资源;(2)对于每一个引脚要调用device_create(辅助作用)
这三个文件会被编译成三个ko文件。
入口函数注册platform_driver(在leddrv.c中)
1.3 资源用设备树指定驱动在 platform_driver 实现
核心永远是 file_operations 结构体。
上述三种方法,只是指定“硬件资源”的方式不一样。
从图可以知道, platform_device/platform_driver 只是编程的技巧,不涉及驱动的核心。
从源代码文件 dts 文件开始,设备树的处理过程为
① dts 在PC机上被编译为 dtb 文件;
② u-boot 把dtb文件传给内核;
③ 内核解析dtb文件,把每一个节点都转换为 device_node 结构体;
④ 对于某些 device_node 结构体,会被转换为 platform_device 结构体。
2 怎么使用设备树写驱动程序
2.1 设备树节点要与 platform_driver 能匹配
驱动要求设备树节点提供什么,我们就得按这要求去编写设备树。但是,匹配过程所要求的东西是固定的:
① 设备树要有 compatible 属性,它的值是一个字符串
② platform_driver 中要有 of_match_table,其中一项的.compatible 成员设置为一个字符串
③ 上述 2 个字符串要一致
左边是设备结点,右边是platform_drvier,两个compatible匹配
2.2 设备树节点指定资源, platform_driver 获得资源
如果在设备树节点里使用reg属性 , 那么内核生成对应的platform_device 时会用 reg 属性来设置 IORESOURCE_MEM 类型的资源
如果在设备树节点里使用 interrupts属性,那么内核生成对应的platform_device 时会用 reg 属性来设置 IORESOURCE_IRQ 类型的资源。对于 interrupts 属性,内核会检查它的有效性,所以不建议在设备树里使用该属性来表示其他资源。
在我们的工作中,驱动要求设备树节点提供什么,我们就得按这要求去编写设备树。驱动程序中根据 pin 属性来确定引脚,那么我们就在设备树节点中添加pin属性
设备树节点中:
#define GROUP_PIN(g,p) ((g<<16) | (p))
100ask_led0 {
compatible = "100ask,led";
pin = <GROUP_PIN(5, 3)>;
};
驱动程序中,可以从 platform_device 中得到 device_node,再用of_property_read_u32 得到属性的值
struct device_node* np = pdev->dev.of_node;
int led_pin;
int err = of_property_read_u32(np, “pin”, &led_pin);
3 开始编程
左边设计设备树文件,右边构造platform_drvier
3.1 修改设备树添加 led 设备节点
在本实验中,需要添加的设备节点代码是一样的,你需要找到你的单板所用的设备树文件,在它的根节点下添加如下内容:
#define GROUP_PIN(g,p) ((g<<16) | (p))
/ {
winter_led@0 {
compatible = "winter_leddrv";
pin = <GROUP_PIN(3, 1)>;
};
winter_led@1 {
compatible = "winter_leddrv";
pin = <GROUP_PIN(5, 8)>;
};
};
对于百问网使用 STM32MP157 板子:
◼设备树文件是:内核源码目录 中arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dts
◼ 修改、编译后得 到 arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dtb 文件。
◼ 然后使用 nfs ssh 等方式把新编译出来的 dtb 去覆盖老文件。
编译
make dtbs
3.2 修改 platform_driver 的源码
chip_demo_gpio.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include "led_resource.h"
#include "led_operations.h"
#include "led_drv.h"
static int g_ledpins[100]; // 记录引脚
static int g_ledcount = 0; // 计数器
// init函数
static int board_demo_led_init(int which)
{
printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
printk("init gpio: group: %d, pin: %d\n", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
switch(GROUP(g_ledpins[which]))
{
case 0:
{
printk("init pin of group 0...\n");
break;
}
case 1:
{
printk("init pin of group 1...\n");
break;
}
case 2:
{
printk("init pin of group 2...\n");
break;
}
case 3:
{
printk("init pin of group 3...\n");
break;
}
}
return 0;
}
// ctl函数
static int board_demo_led_ctl(int which, char status)
{
printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
printk("set led: %s: group: %d, pin: %d\n", status ? "on" : "off",
GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
switch(GROUP(g_ledpins[which]))
{
case 0:
{
printk("init pin of group 0...\n");
break;
}
case 1:
{
printk("init pin of group 1...\n");
break;
}
case 2:
{
printk("init pin of group 2...\n");
break;
}
case 3:
{
printk("init pin of group 3...\n");
break;
}
}
return 0;
}
static struct led_operations board_demo_led_operations = {
.init = board_demo_led_init,
.ctl = board_demo_led_ctl,
};
// get函数
struct led_operations *get_board_led_operations(void)
{
return &board_demo_led_operations;
}
// probe函数
// (1)记录引脚;(2)对于每一个引脚要调用device_create(辅助作用)
static int chip_demo_gpio_probe(struct platform_device *device)
{
// 需要从platform_device中找到对应的设备结点,取出里面的pin属性
struct device_node* np;
int led_pin, err;
np = device->dev.of_node;
if (!np)
{
return -1;
}
// 从np结点中读出pin属性,存到led_pin中
err = of_property_read_u32(np, "pin", &led_pin);
// 记录引脚
g_ledpins[g_ledcount] = led_pin;
// 同时创建device
led_device_create(g_ledcount);
g_ledcount++;
return 0;
}
// remove函数
static int chip_demo_gpio_remove(struct platform_device *device)
{
struct device_node* np;
int i = 0;
int led_pin;
int err;
// 获取node结点
np = device->dev.of_node;
if (!np)
{
return -1;
}
// 取出pin属性
err = of_property_read_u32(np, "pin", &led_pin);
for (i = 0; i < g_ledcount; i++)
{
if (g_ledpins[i] == led_pin)
{
// 注销
led_device_destroy(i);
g_ledpins[i] = -1;
break;
}
}
for (i = 0; i < g_ledcount; i++)
{
if (g_ledpins[i] != -1)
{
break;
}
}
if (i == g_ledcount)
{
g_ledcount = 0;
}
return 0;
}
static const struct of_device_id winter_leds[] = {
{ .compatible = "winter_leddrv" },
};
// platform_driver结构体
// name一致
static struct platform_driver chip_demo_gpio_drv = {
.driver = {
.name = "winter_led",
.of_match_table = winter_leds,
},
.probe = chip_demo_gpio_probe,
.remove = chip_demo_gpio_remove,
};
// 注册这个结构体,入口出口
static int chip_demo_gpio_drv_init(void)
{
int err;
err = platform_driver_register(&chip_demo_gpio_drv);
// 向上层提供了led的操作函数
register_led_operations(&board_demo_led_operations);
return 0;
}
// 出口函数,注销用
static void chip_demo_gpio_drv_exit(void)
{
platform_driver_unregister(&chip_demo_gpio_drv);
}
// 修饰为入口函数和出口函数
module_init(chip_demo_gpio_drv_init);
module_exit(chip_demo_gpio_drv_exit);
MODULE_LICENSE("GPL");
主要看里面的 platform_driver:
// probe函数
// (1)记录引脚;(2)对于每一个引脚要调用device_create(辅助作用)
static int chip_demo_gpio_probe(struct platform_device *device)
{
// 需要从platform_device中找到对应的设备结点,取出里面的pin属性
struct device_node* np;
int led_pin, err;
np = device->dev.of_node;
if (!np)
{
return -1;
}
// 从np结点中读出pin属性,存到led_pin中
err = of_property_read_u32(np, "pin", &led_pin);
// 记录引脚
g_ledpins[g_ledcount] = led_pin;
// 同时创建device
led_device_create(g_ledcount);
g_ledcount++;
return 0;
}
// remove函数
static int chip_demo_gpio_remove(struct platform_device *device)
{
struct device_node* np;
int i = 0;
int led_pin;
int err;
// 获取node结点
np = device->dev.of_node;
if (!np)
{
return -1;
}
// 取出pin属性
err = of_property_read_u32(np, "pin", &led_pin);
for (i = 0; i < g_ledcount; i++)
{
if (g_ledpins[i] == led_pin)
{
// 注销
led_device_destroy(i);
g_ledpins[i] = -1;
break;
}
}
for (i = 0; i < g_ledcount; i++)
{
if (g_ledpins[i] != -1)
{
break;
}
}
if (i == g_ledcount)
{
g_ledcount = 0;
}
return 0;
}
static const struct of_device_id winter_leds[] = {
{ .compatible = "winter_leddrv" },
};
// platform_driver结构体
// name一致
static struct platform_driver chip_demo_gpio_drv = {
.driver = {
.name = "winter_led",
.of_match_table = winter_leds,
},
.probe = chip_demo_gpio_probe,
.remove = chip_demo_gpio_remove,
};
Makefile
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /home/book/100ask_stm32mp157_pro-sdk/Linux-5.4
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o led_drv_test led_drv_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f led_drv_test
# 参考内核源码drivers/char/ipmi/Makefile
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o
# leddrv.c board_demo.c 编译成 100ask.ko
obj-m += led_drv.o chip_demo_gpio.o
编译
现在不需要②了,上一次文件leddrv.c,里面注册file_operations结构体,结构体中有open/write等函数,open函数会去调用底层chip_demo_gpio.c中提供的int函数来初始化引脚;write函数会调用底层chip_demo_gpio.c中提供的ctl函数来控制引脚,上下分层。上面的leddrv和硬件关系不大,下面的chip_demo_gpio.c用来操作具体的硬件。
4 测试
在开发板挂载 Ubuntu 的NFS目录
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs/ /mnt
将ko文件和测试代码拷贝到挂载目录,安装驱动
板子上这四个是需要的
mount /dev/mmcblk2p2 /boot
需要将ubuntu系统nfs目录下的stm32mp157c-100ask-512d-lcd-v1.dtb文件拷贝到板子的/boot目录中(记得备份原来的)
再重启开发板
查看
这里第一个winter是我20240720早上的demo:韦东山嵌入式linux系列-LED 模板驱动程序的改造:设备树-CSDN博客
加载驱动:
insmod led_drv.ko
insmod chip_demo_gpio.ko
./led_drv_test /dev/winter_led0 on
./led_drv_test /dev/winter_led0 off
./led_drv_test /dev/winter_led1 on
./led_drv_test /dev/winter_led1 off
这个实验并没有点灯效果,仅仅是打印
5 调试技巧
/sys 目录下有很多内核、驱动的信息
5.1 设备树的信息
以下目录对应设备树的根节点,可以从此进去找到自己定义的节点。
cd /sys/firmware/devicetree/base/
节点是目录,属性是文件。
属性值是字符串时,用 cat 命令可以打印出来;属性值是数值时,用hexdump 命令可以打印出来。
5.2 platform_device 的信息
以下目录含有注册进内核的所有 platform_device:
/sys/devices/platform
一个设备对应一个目录,进入某个目录后,如果它有“ driver”子目录,就表示这个 platform_device 跟某个 platform_driver 配对了。比 如 下 面 的 结 果 中 , 平 台 设 备 “ winter_led@0 ” 已 经 跟 平 台 驱 动“ winter_led”配对了。
系统总线、平台总线下面的drivers,是winter_led driver,因为平台drvier的名字是winter_led
chip_demo_gpio.c
这就是文件系统的设备结点,可以操作它们实现点灯
5.3 platform_driver 的信息
以下目录含有注册进内核的所有 platform_driver:
/sys/bus/platform/drivers
一个 driver 对应一个目录,进入某个目录后,如果它有配对的设备,可以直接看到。
比如下面的结果中,平台驱动“winter_led”跟 2 个平台设备“平台设备“ winter_led@0”、“ winter_led@1”配对了:
注意:一个平台设备只能配对一个平台驱动,一个平台驱动可以配对多个平台设备。