韦东山嵌入式linux系列-LED 模板驱动程序的改造:设备树

1 总结 3 种写驱动程序的方法

1.1 资源和驱动在同一个文件里

详见:韦东山嵌入式linux系列-第一个实验-CSDN博客

应用程序调用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”配对了:

注意:一个平台设备只能配对一个平台驱动,一个平台驱动可以配对多个平台设备。

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-21 08:58:03       52 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-21 08:58:03       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-21 08:58:03       45 阅读
  4. Python语言-面向对象

    2024-07-21 08:58:03       55 阅读

热门阅读

  1. 数据结构(功能受限的表-栈&队列)

    2024-07-21 08:58:03       18 阅读
  2. Linux 下部署 syncthing 中继服务器

    2024-07-21 08:58:03       21 阅读
  3. 云计算遭遇的主要安全威胁

    2024-07-21 08:58:03       14 阅读
  4. 服务发现的艺术:Eureka中实现分布式服务目录

    2024-07-21 08:58:03       18 阅读
  5. 终端创建py虚拟环境

    2024-07-21 08:58:03       15 阅读
  6. log4j2启动异步日志与动态修改日志级别

    2024-07-21 08:58:03       17 阅读
  7. Leetcode【拥有最多糖果的孩子】

    2024-07-21 08:58:03       19 阅读
  8. python-docx,一个超酷的word处理Python库!

    2024-07-21 08:58:03       15 阅读
  9. Exploiting server-side parameter pollution in a query string

    2024-07-21 08:58:03       14 阅读