当前位置: 首页 > article >正文

【梦想终会实现】Linux驱动学习4

以LED作为标准件研究Linux内核驱动的实现。随着驱动的增多,为了防止代码被写的无法维护,Linux内核想到了更优的组织方式,即LED的架构。

1、

内核开发者+驱动工程师共同实现。

驱动框架就是内核开发者自己写了一部分公共代码,然后剩下的部分交给其他厂家的驱动开发工程师去写。

内核开发者-->芯片开发者-->厂商开发者。

不同设备的驱动框架不同(wifi、蓝牙、触摸屏),每个的驱动框架都要重新学习。

2、

模块分析:一般从下往上,先分析那些注册信息,装载卸载函数等。模块的入口和出口都是在下面的。

3、

subsys_initcall与module_init都是模块的注册操作。

区别为:subsys_initcall的宏为__define_initcall("4",fn,4);module_init的宏为__define_initcall("6",fn,6)。

内核在启动过程中需要顺序的做很多事,内核如何实现按照先后顺序去做很多初始化操作。内核的解决方案就是给内核启动时要调用的所有函数归类,然后每个类按照一定的次序去调用执行。这些分类名就叫.initcalln.init。n的值从1到8。内核开发者在编写内核代码时只要将函数设置合适的级别,这些函数就会被链接的时候放入特定的段,内核启动时再按照段顺序去依次执行各个段即可。

4、

操作/sys/class和/dev的区别:

操作/sys目录即操作对应的如下结构体:通过attribute形式操作驱动,是另一条路,就没有cdev那个操作了。

static struct device_attribute led_class_attrs[] = {
    __ATTR(brightness, 0644, led_brightness_show, led_brightness_store),
    __ATTR(max_brightness, 0444, led_max_brightness_show, NULL),
#ifdef CONFIG_LEDS_TRIGGERS
    __ATTR(trigger, 0644, led_trigger_show, led_trigger_store),
#endif 
    __ATTR_NULL,
};

操作/dev即操作file_operations结构体。

attribute和cdev是两种操作驱动的方式。

当你使用 __attribute__((sysfs)) 或者 __attribute__((visibility("default"))) 等属性来定义数据结构时,这通常与 sysfs 文件系统相关。sysfs 是一个虚拟文件系统,它将内核数据结构暴露为文件,可以通过 /sys 目录访问,是应用与内核间交互的接口,层级更清晰,类在/sys/device中显示,驱动程序在/sys/device中显示,内容是内核动态生成的。网络的详细信息可以在cd /sys/class/net/目录下找到,CPU信息也可以在cd /sys/devices/cpu/中找到。

已经有/dev目录了,还要/sys目录干嘛?

/sys 的内容是动态生成的,反映了当前的系统状态,而 /dev 中的设备文件通常在系统启动时或设备插入时创建。

/sys 目录支持即插即用(Plug and Play)设备的自动识别和管理。设备插入系统时,相应的 /sys 条目会被创建,允许用户和程序立即与之交互。

总的来说,/sys 目录的存在补充了 /dev 目录的功能,提供了更全面的系统设备信息,并允许用户空间程序与内核设备和驱动进行更深入的交互。它是了解和管理系统硬件资源的一个重要工具。而 /dev 更多的是关注于提供与设备进行I/O操作的实际接口

5、

LED驱动:

内核开发者提供: led_classdev_register -->芯片开发者:device_create(创建设备文件)

内核开发者-->芯片开发者-->厂商开发者。

本质上底层还是device_create以及cdev那些函数,只是芯片厂商做了一层封装而已。

// 1:Linux内核中类对象创建函数
/**
 * led_classdev_register - register a new object of led_classdev class.
 * @parent: The device to register.
 * @led_cdev: the led_classdev structure for this device.
 */
int led_classdev_register(struct device *parent, struct led_classdev *led_cdev)
{
	led_cdev->dev = device_create(leds_class, parent, 0, led_cdev,
				      "%s", led_cdev->name);
	if (IS_ERR(led_cdev->dev))
		return PTR_ERR(led_cdev->dev);

#ifdef CONFIG_LEDS_TRIGGERS
	init_rwsem(&led_cdev->trigger_lock);
#endif
	/* add to the list of leds */
	down_write(&leds_list_lock);
	list_add_tail(&led_cdev->node, &leds_list);
	up_write(&leds_list_lock);

	if (!led_cdev->max_brightness)
		led_cdev->max_brightness = LED_FULL;

	led_update_brightness(led_cdev);

#ifdef CONFIG_LEDS_TRIGGERS
	led_trigger_set_default(led_cdev);
#endif

	printk(KERN_DEBUG "Registered led device: %s\n",
			led_cdev->name);

	return 0;
}

// 2:linux内核中类创建函数,对应目录/sys/class/leds
static int __init leds_init(void)
{
	leds_class = class_create(THIS_MODULE, "leds");
	if (IS_ERR(leds_class))
		return PTR_ERR(leds_class);
	leds_class->suspend = led_suspend;
	leds_class->resume = led_resume;
	leds_class->dev_attrs = led_class_attrs;
	return 0;
}

6、meke menuconfig指令

是Linux内核编译中的一个指令,用于配置Linux内核编译选项。这个命令启动一个基于文本的用户界面,允许用户通过导航去注册卸载模块,它决定了哪些可以被编译进内核。其实就是一个个的宏开关。

7、

kernel里面的一些函数:

readl :读取32位寄存器的值。

writel:写入32位寄存器的值。

printk:内核打印函数

8、

LED驱动框架的层级调用关系:

s3c24xx_led_init -> s3c24xx_led_probe -> led_classdev_register(LED驱动框架提供) -> device_create(设备创建)


#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/leds.h>
#include <linux/gpio.h>
#include <linux/slab.h>

#include <mach/hardware.h>
#include <mach/regs-gpio.h>
#include <mach/leds-gpio.h>


struct led_classdev led_cdev1;
struct led_classdev led_cdev2;
struct led_classdev led_cdev3;

static void s5Pv210_led1_set(struct led_classdev *led_cdev,
			    enum led_brightness value)
{
	printk(KERN_DEBUG "led1");
}

static void s5Pv210_led2_set(struct led_classdev *led_cdev,
				enum led_brightness value)
{
	printk(KERN_DEBUG "led2");
}

static void s5Pv210_led3_set(struct led_classdev *led_cdev,
			    enum led_brightness value)
{
	printk(KERN_DEBUG "led3");
}

static int s5Pv210_led_probe(struct platform_device *dev)
{
	// 不必注册三个,只需用一个注册函数,在这里面使用led_classdev_register创建三个对象
	led_cdev1.brightness_set = s5Pv210_led1_set;
	led_cdev1.brightness     = 255;
	led_cdev1.name = "led1";
	led_classdev_register(NULL, &led_cdev1);  // 在 /sys/class/leds下面创建的,这是Led驱动框架提供的函数接口

	led_cdev2.brightness_set = s5Pv210_led2_set;
	led_cdev2.brightness     = 255;
	led_cdev2.name = "led2";
	led_classdev_register(NULL, &led_cdev2);

	led_cdev3.brightness_set = s5Pv210_led3_set;
	led_cdev3.brightness     = 255;
	led_cdev3.name = "led3";
	led_classdev_register(NULL, &led_cdev3);

	return 0;
}

static int s5Pv210_led1_remove(struct platform_device *dev)
{

	led_classdev_unregister(&led_cdev1);
	led_classdev_unregister(&led_cdev2);
	led_classdev_unregister(&led_cdev3);
}

module_init(s5Pv210_led_probe);
module_exit(s5Pv210_led1_remove);

MODULE_AUTHOR("zhulaoshi <zhulaoshi@zhu>");
MODULE_DESCRIPTION("S5Pv210 LED driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS("S5Pv210Led");

/sys目录下创建的新设备下目录包括:

对应程序中的执行位置为:

static struct device_attribute led_class_attrs[] = {
	__ATTR(brightness, 0644, led_brightness_show, led_brightness_store),
	__ATTR(max_brightness, 0444, led_max_brightness_show, NULL),
#ifdef CONFIG_LEDS_TRIGGERS
	__ATTR(trigger, 0644, led_trigger_show, led_trigger_store),
#endif
	__ATTR_NULL,
};

9、

Linux内核中为什么很少看到.h文件,为何将函数的注册、卸载等处理放在最后面?

很少看到.h文件?将本模块的头文件一般只有本模块使用,将用到的宏和声明放在一个源文件中,而非像用户空间那样,可以更有利于模块化和方便维护。且放置到.h中会增加编译时间,大型项目中一般不适用。

理解没有.h文件就理解为什么注册和卸载一般都放在后面了, 因为要先定义后使用,越是上一级的调用接口位置越靠后。

10、为什么需要JSON?

根因:跨平台使用,与主站、上位机之间通信时,不同平台设计语言不通,通过JSON字符串形式,进行转换,且键值对跨Bool、整形、数组,基本覆盖所有场景。、

11、

指针的运算是+-sizeof(变量)

当数组名作为实参传递时,一般要同步传递他的数据长度,因为作为入参传递后,函数名即变成指向首元素的指针。

container_of是根据结构体中某成员的地址获取整个结构体的地址,在kernel中经常使用,可以实现对原结构体所有成员的访问。

12、为什么内核中不适用驼峰法命名?

  1. 一致性:Linux 内核的命名风格倾向于使用全小写字母和下划线来命名变量、函数和类型。这种风格在内核代码库中是一致的,有助于开发者快速识别和理解代码。

  2. 可读性:全小写加下划线的命名方式使得代码中的变量和函数名更容易阅读,尤其是在长变量名和函数名中。驼峰命名法可能会导致单词之间的界限不明确,尤其是在单词以相同字母开始的情况下。

  3. 传统:Linux 内核的编码风格深受其早期历史和Unix传统的影响。Unix系统中普遍使用下划线命名法,因此这种风格也被继承到了Linux内核中。

  4. 工具兼容性:在早期Unix系统中,某些工具(如grep)对于驼峰命名处理不如下划线命名法那么高效。虽然现代工具已经能够很好地处理各种命名法,但这个历史原因仍然是使用下划线的因素之一。

13、linux内核是面向对象的吗?

Linux应该是最庞大的C语言代码库,没有之一,如此庞大的C代码库,若纯纯使用面向过程开发,那别人根本无法维护,故后续Linux代码新增时使用类面向对象的开发方式。即以类的形式抽离出某一驱动的共有部分作为一个结构体,成员变量为“属性”,成员函数为“方法”。

14、gpiolib学习:本质上是对共用的Gpio接口的抽象,同时加入互斥操作原理

要有架构的思想,类似于Linux中间件开发的思想!内核学习要有架构意识!越复杂的硬件越难从整体上把控,越需要有架构意识。时刻思考,为什么划分这么多文件?文件中这些函数为什么写到一个地方去?

学习方法:(这也是新接手代码的思想)

代码一条主线进去,别被其他吸引,不然没法看了;杂碎知识,彻底搞懂;学习架构思想,提升自己的大脑复杂度

名字研究:samsung_gpiolib_add_4bit_chips  

4bit:表示以4bit为base去操作的,chips表示操作n个gpio的chip,add表示底层应该有个接口作统一操作。在GPIO的底层代码中,根据4bit和2bit进行划分。这个2bit/4bit没法作为入参使用吗?非得设计成两个函数?必须设置为两个函数,若设计成入参,还得让上层去判断到底使用哪个,且对于寄存器。

如果两个函数在逻辑上代表不同的操作或者服务于不同的用途,即使它们相似,也应该保持为独立的函数,以便于理解和维护。例如Linux kernel中不同4bit/2bit寄存器的操作,针对不同的寄存器操作,需设计为不同的函数。如果设计为新增一个传参形式,别人会困惑,对于两个寄存器在一个地方的操作。

// 一层层调用,最后指向干活的函数名。当驱动被加载,通过/dev或/sys目录下设备文件操作下来,会直接索引到他所对应的函数,不然容易发生空引用
void __init samsung_gpiolib_add_4bit(struct s3c_gpio_chip *chip)
{
	chip->chip.direction_input = samsung_gpiolib_4bit_input;
	chip->chip.direction_output = samsung_gpiolib_4bit_output;
	chip->pm = __gpio_pm(&s3c_gpio_pm_4bit);
}

static int samsung_gpiolib_4bit_input(struct gpio_chip *chip,
				      unsigned int offset)
{
	struct s3c_gpio_chip *ourchip = to_s3c_gpio(chip);
	void __iomem *base = ourchip->base;
	unsigned long con;

    // 读改写三部曲
	con = __raw_readl(base + GPIOCON_OFF);         // 读
	con &= ~(0xf << con_4bit_shift(offset));       // 改
	__raw_writel(con, base + GPIOCON_OFF);         // 写

	gpio_dbg("%s: %p: CON now %08lx\n", __func__, base, con);

	return 0;
}

有大量注释的一般都是Kernel里面使用的。没有注释的一般是厂家提供的。

GPIO的注册即 把GPIO添加进数组里面。

static struct gpio_desc gpio_desc[ARCH_NR_GPIOS];

gpio_desc[id].chip = chip;

gpiolib着重看的文件:drivers/gpio/gpiolib.c  : gpio_add、gpio_request

gpiochip_is_requient:判断Gpio是否被使用,后续判断**是否被使用可以使用这个范例命名。

使用流程:gpiochip_is_requient(申请) ->gpio_direction_input/gpio_direction_output(配置输入输出)->gpio_set_value(配置具体值)

static ssize_t chip_ngpio_show(struct device *dev,
			       struct device_attribute *attr, char *buf)
{
	const struct gpio_chip	*chip = dev_get_drvdata(dev);

	return sprintf(buf, "%u\n", chip->ngpio);
}
static DEVICE_ATTR(ngpio, 0444, chip_ngpio_show, NULL);  // 配置属性不可写,NULL表示无写接口

内核注册过程:

gpiolib_sysfs_init ->gpiochip_export->device_create(创建设备对象/sys目录下)

15、

Kernel下各个目录的含义:

  1. arch/:这个目录包含了与体系结构相关的代码,例如x86、ARM、MIPS等。每个子目录对应于一种特定的处理器架构里面包含了该架构特有的汇编代码、头文件和实现文件。

  2. block/:包含了块设备驱动程序相关的代码。块设备是指那些可以随机访问的存储设备,如硬盘驱动器。

  3. certs/:这个目录用于存放内核构建过程中使用的证书文件,如SSL证书。

  4. crypto/:包含了加密和哈希算法的代码,用于在内核中实现安全特性。

  5. drivers/:这个目录包含了各种设备的驱动程序代码,包括硬盘、网络设备、USB设备等。

  6. fs/:包含了文件系统的代码,例如EXT4、NFS、VFAT等文件系统的实现。

  7. include/:这个目录包含了内核头文件,这些头文件定义了内核编程接口和许多重要的数据结构。

  8. init/:包含了内核初始化代码,它负责在系统引导时启动内核。

  9. ipc/:包含了进程间通信(IPC)的代码,如管道、信号、共享内存等。

  10. kernel/:这个目录包含了内核的核心代码,如进程管理、内存管理、中断管理、调度器等。

  11. lib/:包含了内核库代码,这些代码提供了通用的函数,被内核的其他部分使用。

  12. mm/:包含了内存管理相关的代码,负责管理物理内存和虚拟内存。

  13. net/:包含了网络相关的代码,如网络协议栈、网络设备驱动程序等。

  14. samples/:提供了内核编程的示例代码,用于教育和参考。

  15. scripts/:包含了内核构建过程中使用的脚本文件。

  16. sound/:包含了音频程序和音频子系统相关的代码。

  17. sys/:包含了与系统核心功能相关的代码,如系统调用处理、内核参数管理等。

  18. tools/:包含了内核开发中使用的工具,如配置工具、编译器等。

  19. usr/:包含了用户空间可用的内核代码,如一些特定的库和工具。

16、

将某个驱动编译进内核,一般给你提供的都是一个Makefile+实际代码。

将驱动由野生编译进内核思路:

将写好的驱动文件放置到内核中具有的位置。

在Makefile中添加相应的依赖项。

在Kconfig中添加相应的配置项。(关键)

meke menuconfig将对应的项配置成y(编辑进Zimage),m(编译成单独的ko文件),空(不编译)。

kernel中的.config文件作用:包含了内核编译时所有的配置选项,这些选项决定了内核的功能、特性、驱动支持和硬件兼容性。内核功能的裁剪就是配置的此文件。当编译内核时,顶层的Makefile会读取.config文件的内容,根据这些配置来决定哪些功能被编译进内核,哪些作为模块,哪些被禁用。.config文件中的每一行表示一个配置项,格式通常为CONFIGoption=value,其中option是配置项的名称,value = y (表示该功能被编译进内核)、m(表示该功能被编译成模块)、空(该模块未编译)。

Kconfig文件:Kconfig文件定义了在配置内核时用户可以看到的各种选项。每个选项都对应一个特定的功能、模块或驱动程序,用户可以选择启用或禁用。当用户运行make menuconfigmake xconfig等命令时,Kconfig文件被用来生成一个配置界面。这个界面允许用户交互式地配置内核选项。Kconfig是Linux内核配置过程的核心!提供了一种结构化的方式来定义。

obj-$(CONFIG_LEDS_88PM860X)             += leds-88pm860x.o 是什么意思?

这行代码是Linux内核源码树中的一个典型的Makefile语法,它用于根据内核配置决定是否编译某个特定的内核模块或文件。让我们分解这行代码的含义:

  • obj-$(CONFIG_LEDS_88PM860X):这是Makefile中的一个目标,它将根据配置选项CONFIG_LEDS_88PM860X的值来决定添加到编译列表中的对象文件(.o文件)。CONFIG_LEDS_88PM860X是在内核配置过程中设置的一个选项,通常对应于某种特定的LED驱动(在这个例子中是针对88PM860X芯片的LED驱动)。

  • $(CONFIG_LEDS_88PM860X)$()是Makefile中的宏(或变量)引用。CONFIG_LEDS_88PM860X是一个宏,它的值在内核配置时被设置。它通常是以下几种值之一:

    • y:表示该功能被编译进内核映像中。
    • m:表示该功能被编译为一个可加载的模块。
    • 如果没有被设置,那么它的值默认为空。
  • +=:这是Makefile中的追加操作符,用于将一个或多个文件添加到之前定义的变量中。在这个例子中,它用于将leds-88pm860x.o这个对象文件添加到由obj-$(CONFIG_LEDS_88PM860X)定义的列表中。

  • leds-88pm860x.o:这是要编译的对象文件。它通常是某个特定功能的实现,在这个例子中是88PM860X LED驱动的实现。

综上所述,这行Makefile代码的含义是:

如果CONFIG_LEDS_88PM860X配置选项被设置为y(编译进内核)或m(编译为模块),那么将leds-88pm860x.o这个对象文件添加到编译过程中,否则忽略它。这意味着,只有当用户在内核配置时启用了对应的LED驱动选项时,相应的驱动代码才会被编译和包含在内核中


http://www.kler.cn/a/535920.html

相关文章:

  • 第二次连接k8s平台注意事项
  • 芝法酱学习笔记(2.6)——flink-cdc监听mysql binlog并同步数据至elastic-search和更新redis缓存
  • 安卓开发,Reason: java.net.SocketTimeoutException: Connect timed out
  • k8s常见面试题2
  • axios如何利用promise无痛刷新token
  • LabVIEW自定义测量参数怎么设置?
  • Nginx与frp结合实现局域网和公网的双重https服务
  • 网站打开提示不安全
  • 深度剖析FFmpeg视频解码后的帧处理到Qt显示 从AVFrame到QImage的转换(二)
  • DeepSeek 和 ChatGPT-4o
  • K8s 常见面试题(K8s Common Interview Questions)
  • 如何正确配置您的WordPress邮件设置
  • 『python爬虫』获取免费IP代理 搭建自己的ip代理池(保姆级图文)
  • Redis数据变化监听:使用Spring Boot实现实时数据监控
  • 【2】高并发导出场景下,服务器性能瓶颈优化方案-异步导出
  • AI大模型评测对比2—ChatGPT对比DeepSeek
  • DeepSeek-VL2论文解读:用于高级多模态理解的专家混合视觉语言模型
  • 图论 - 临接矩阵与临接表介绍与分析对比
  • Linux进阶——远程连接服务器
  • salesforce SF CLI 数据运维经验分享
  • 2025模仿游戏 别人怎么做就什么做 做的过程中再加入自己的元素 模仿与创新
  • 深度学习中模型训练的过拟合与欠拟合问题
  • 计算机毕业设计Python+大模型疲劳驾驶检测系统 自动驾驶 面部多信息特征融合的疲劳驾驶检测系统 驾驶员疲劳驾驶风险检测 深度学习 机器学习 大数据
  • innoDB 如何解决幻读
  • 动手学图神经网络(10):利用 PyTorch Geometric 进行图分类
  • 设计模式-状态模式:让对象的行为随状态改变而清晰可控