嵌入式底层驱动需要知道的基本知识

news2024/11/26 18:54:11

先说结论,能,肯定能,必须能!

但是,问题重点在于坚持,程序员这一行 ,下班回家一般都要10点了,再刷两个小时枯燥的学习视频,我想大多数人是坚持不下来的。

但是,我要说但是,"linux驱动开发其实并不难,难的是市面上没有靠谱的书籍和教学视频",就好像是你向一个半瓶水的模拟电路工程师请教电路设计原理,张口就是经验值,问就是别人都是这么设计的,能用就行,他能给你讲明白才有鬼了。

之所以说linux驱动开发不难是因为内核中已经实现了一套非常简洁,通用的驱动框架,自打2.6版本以后就没怎么变过,足以说明该驱动框架的优秀。而目前市面上的书籍和教学视频根本没有足够重视讲解驱动框架的内容,只是硬扣单个驱动的细节。作为单片机工程师,你跟linux驱动工程师之间差的就只是一个驱动框架而已。

说了这么多,是时候上干货了。还是坚持我一贯的理念,学习任何新鲜东西都应该是由远及近,先整体掌握全局,再深入探究细节。原则上,你只要认真看完下面的内容,就差不多算是入门了。废话不多说,上菜。

前置知识:

1. linux驱动模块整体是以面向对象思想来设计的,驱动中的每个节点都描述成一个对象。

2. 对象通常采用结构体的形式描述,结构体中的变量表示对象属性,函数指针表示对象行为。

3. 对象之间的继承关系采用内嵌父类结构体对象的形式体现。

4. 驱动中的同类对象一般采用链表的形式串联在一起,链表使用内嵌struct list_head的形式表示。

5. 内核中大量使用container_of宏,实现通过已知结构体对象内部元素的地址获取整个结构体起始地址的功能。

例:继承实现方法

/* 父类 */
struct ANIMAL {
int age;
int weight;
};

/* 子类 */
struct DOG {
struct ANIMAL animal; /* 通过内嵌父类对象,来实现继承关系 */
int variety;
};

/* 通过dog对象中animal对象的地址获取dog对象的起始地址 */
struct DOG *dog = container_of(ptr_animal, struct DOG, animal);

例:链表的使用方法

struct xxx_dev {
int id;
int num;
struct list_head node; /* 通过在对象中嵌入struct list_head节点,来实现链表功能 */
};

/* 遍历链表的时候,已知node地址,借助container_of宏可以获取到外层对象xxx_dev的起始地址。*/
struct xxx_dev *dev = container_of(prt_node, struct xxx_dev, node);

核心驱动框架:

linux内核中对不同的组成部分高度抽象,采用 "总线-设备-驱动"模型来组织某一层驱动代码,多层之间可以叠加。模型结构如下,总线作为桥梁和纽带,连接设备和对应的驱动。

图片

(核心驱动框架)

设备:挂载在各个总线上的的硬件设备或者虚拟设备,比如挂在I2C总线上的温湿度传感器, 挂载在平台总线上I2C控制器等,都被抽象描述成设备对象。使用结构体struct device表示设备基类,用来描述硬件设备的各种参数,具体各类设备可以通过内嵌基类对象,实现继承和扩展。

驱动:记录硬件设备状态的变量和控制硬件工作的函数的集合,负责对硬件设备进行初始化,并向上层代码提供操作接口,比如SOC中各类总线控制器驱动,以及外挂的总线设备驱动等,使用结构体struct device_driver表示驱动基类,具体各类驱动可以通过内嵌基类对象,实现继承和扩展。

总线:表示各种物理或者虚拟总线。总线作为桥梁和纽带,用来连接设备和驱动,并提供驱动注册,设备发现,设备注册/卸载等功能。常见的总线例如:平台总线,I2C总线,SPI总线,USB总线等。其中平台总线是驱动工程师最长接触的总线类型,有些书籍把平台总线叫做虚拟总线,说是那些没有实际物理总线的都归类为平台总线,我认为这个说法不对。平台总线应该是指SOC中那些内部互联用的总线,比如ARM SOC中的AXI, AHB, APB总线等,这些总线上连接的大量的控制器,都可以通过地址映射直接访问,所以这些控制器一般被称为平台设备,连接的总线被称为平台总线。使用struct bus_type表示总线基类,具体各类总线可以通过内嵌基类,实现继承和扩展。

驱动框架继承关系:

如前所述,Linux驱动框架中分别定义了"总线-设备-驱动"各对象的基类,其他各子类都是从基类继承而来,继承关系如下图:

图片

(继承关系)

用户接口:

所有的硬件都是为了实现某些具体功能而生的,驱动程序操作硬件设备就是为了给上层应用提供服务,但是linux内核为了安全,把运行空间分成了内核空间(kernel space)和用户空间(user space)两部分,其中内核代码运行在内核空间,应用程序运行在用户空间。应用程序通过系统调用接口,调用内核以及驱动提供的各种服务,示意图如下:

图片

(系统调用示意图)

但是硬件种类多种多样,对应的驱动数量也不胜枚举,而且还在不断的变化中,不可能为每种驱动都提供系统调用接口,好在多数设备的操作步骤都很类似,主要可以概括为:初始化,读,写,关闭等基本步骤。根据设备的功能属性和使用方式不同,内核中把设备大体分为:字符设备,块设备和网络设备三个大类。其中字符设备和块设备因为操作步骤跟文件操作很相似,所以复用了VFS(虚拟文件系统)提供的系统调用接口(open,release,read,write, ioctl等接口), 其在内核中分别使用 struct cdev和struct block_device表示, 在用户空间以特殊文件形式存在于/dev目录下,使用ls -ls /dev 可以查看各文件的属性,其中属性crw-rw-rw-以'c'打头的表示字符设备,属性brw-rw----以b打头的表示块设备。

cros@cros-pc:~$ ls -ls /dev/
total 0
0 crw------- 1 root root 5, 1 5月 28 00:04 console
0 crw-rw-rw- 1 root root 1, 7 5月 28 00:04 full
0 crw-rw---- 1 root kvm 10, 232 5月 28 00:04 kvm
0 brw-rw---- 1 root disk 8, 0 5月 28 00:04 sda
0 brw-rw---- 1 root disk 8, 1 5月 28 00:04 sda1
0 brw-rw---- 1 root disk 8, 2 5月 28 00:04 sda2

网络设备因为操作方式不同,无法复用VFS的系统接口,所以只能单独提供几个独享的系统调用接口,如下SYSCALL_DEFINEx宏的第一个参数就是系统调用的名字:

cros@cros-pc:~/home/cros/kernel$ grep -rn "SYSCALL_DEFINE*" net/socket.c
1213:SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
1254:SYSCALL_DEFINE4(socketpair, int, family, int, type, int, protocol,
1363:SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
1392:SYSCALL_DEFINE2(listen, int, fd, int, backlog)
1425:SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
1506:SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
1524:SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
1556:SYSCALL_DEFINE3(getsockname, int, fd, struct sockaddr __user *, usockaddr,
1587:SYSCALL_DEFINE3(getpeername, int, fd, struct sockaddr __user *, usockaddr,
1619:SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
1663:SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
1675:SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
1720:SYSCALL_DEFINE4(recv, int, fd, void __user *, ubuf, size_t, size,
1731:SYSCALL_DEFINE5(setsockopt, int, fd, int, level, int, optname,
1765:SYSCALL_DEFINE5(getsockopt, int, fd, int, level, int, optname,
1795:SYSCALL_DEFINE2(shutdown, int, fd, int, how)
1988:SYSCALL_DEFINE3(sendmsg, int, fd, struct user_msghdr __user *, msg, unsigned int, flags)
2057:SYSCALL_DEFINE4(sendmmsg, int, fd, struct mmsghdr __user *, mmsg,
2154:SYSCALL_DEFINE3(recvmsg, int, fd, struct user_msghdr __user *, msg,
2272:SYSCALL_DEFINE5(recvmmsg, int, fd, struct mmsghdr __user *, mmsg,
2317:SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)

另外,内核为了方便用户层操作设备,引入了sys文件系统,位于/sys目录下,其分别从总线,设备, 类等不同角度描述整个驱动框架,如下所示:

cros@cros-pc:~$ ls -ls /sys/
total 0
0 drwxr-xr-x 2 root root 0 6月 2 10:25 block
0 drwxr-xr-x 43 root root 0 6月 2 10:25 bus
0 drwxr-xr-x 68 root root 0 6月 2 10:25 class
0 drwxr-xr-x 4 root root 0 6月 2 10:25 dev
0 drwxr-xr-x 24 root root 0 5月 28 00:04 devices
0 drwxr-xr-x 6 root root 0 5月 28 00:04 firmware
0 drwxr-xr-x 10 root root 0 5月 28 00:04 fs
0 drwxr-xr-x 2 root root 0 6月 2 10:25 hypervisor
0 drwxr-xr-x 15 root root 0 5月 28 00:04 kernel
0 drwxr-xr-x 182 root root 0 6月 2 10:25 module
0 drwxr-xr-x 3 root root 0 6月 2 10:25 power

完整的用户接口如下图:

图片

(用户接口框架)

代码模板:

内核模块模板:

内核驱动模块基本通过如下模板,注册初始化函数和卸载函数,作为驱动代码的入口和出口。

/* 内核模块初始化函数 */
static int __init xxx_init(void)
{
}
/* 内核模块注销函数 */
static void __exit xxx_exit(void)
{
}
/* 注册初始化函数,使得自动或者手动安装驱动时,自动执行初始化函数 */
module_init(xxx_init);
/* 注册注销函数,使得自动或者手动安装驱动时,自动执行注销函数 */
module_exit(xxx_exit);

总线代码模板:

/* 总线类型结构体 */
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};

/* 内核模块初始化函数 */
int __init platform_bus_init(void)
{
int error;

/* 这里以平台总线为例,演示总线注册流程 */
error = bus_register(&platform_bus_type);
return error;
}

/* 注册初始化函数,使得自动或者手动安装驱动时,自动执行初始化函数 */
module_init(platform_bus_init);
/* 因为平台总线是内核中必不可少的基础总线,所以没有提供卸载函数 */

驱动代码模板:

/* 设备驱动结构体 */
static struct platform_driver at91_twi_driver = {
/* probe函数负责解析设备对象提供的参数,进行硬件初始化,并向上层提供操作接口 */
.probe = at91_twi_probe,
/* 设备卸载执行的操作 */
.remove = at91_twi_remove,
.id_table = at91_twi_devtypes
.driver = {
.name = "at91_i2c",
/* 用于跟设备匹配用的字段 */
.of_match_table = of_match_ptr(atmel_twi_dt_ids),
.pm = at91_twi_pm_ops,
},
};
/* 内核模块初始化函数 */
static int __init at91_twi_init(void)
{
/* 以平台设备驱动为例,演示驱动注册过程 */
return platform_driver_register(&at91_twi_driver);
}
/* 内核模块注销函数 */
static void __exit at91_twi_exit(void)
{
/* 以平台设备驱动为例,演示驱动卸载过程 */
platform_driver_unregister(&at91_twi_driver);
}
/* 注册初始化函数,使得自动或者手动安装驱动时,自动执行初始化函数 */
module_init(at91_twi_init);
/* 注册注销函数,使得自动或者手动安装驱动时,自动执行注销函数 */
module_exit(at91_twi_exit);

设备代码模板:

PS:新版本内核中因为引入了设备树,绝大多数设备都在设备树中描述了,内核初始化过程中会自动解析设备树,生成并注册设备,所以一下代码目前很少见了,此处只是为了解释原理和基本流程。

/* 平台设备结构体 */
static struct platform_device s3c24xx_uart_device0 = {
.id = 0,
};
static struct platform_device s3c24xx_uart_device1 = {
.id = 1,
};
static struct platform_device s3c24xx_uart_device2 = {
.id = 2,
};
static struct platform_device s3c24xx_uart_device3 = {
.id = 3,
};
struct platform_device *s3c24xx_uart_src[4] = {
&s3c24xx_uart_device0,
&s3c24xx_uart_device1,
&s3c24xx_uart_device2,
&s3c24xx_uart_device3,
};

/* 模块初始化函数 */
static int __init s3c_arch_init(void)
{
int ret;
/* 以平台设备为例,演示设备注册过程 */
ret = platform_add_devices(s3c24xx_uart_src, nr_uarts);
return ret;
}

/* 注册模块初始化函数,类似功能的宏还有很多,名字各不相同 */
arch_initcall(s3c_arch_init);

平台设备驱动框架:

平台设备驱动是开发人员接触最多,也是修改最多的一类驱动,因为其主要包括SOC内置的各种总线控制器,以及PWM,RTC,WDT等内置功能模块。基本都是跟芯片强相关的内容,所以每个SOC都需要单独开发对应驱动。

图片

(平台设备驱动举例)

总线设备驱动框架:

总线设备驱动相比于平台设备设备来说更复杂一些,一般包含两层驱动,底层是总线控制器驱动,上层是总线设备驱动。另外,因为总线控制器多种多样,为了统一上层的编程接口,驱动中会在中间增加core层,实现对总线控制器的抽象,并对上层提供统一的总线操作接口,类似于设计模式中的适配器模式。典型如I2C驱动框架中的struct i2c_adapter,以及SPI驱动框架中的struct spi_master。如下是I2C驱动框架,大家可以仔细品一下。

图片

(I2C设备驱动框架)

内核中还有很多支持热插拔的设备驱动,例如USB驱动,同一个USB接口,可能接了设备,也可能没有接设备,可能接了个U盘,也可能接了个鼠标。例如mmc驱动,mmc接口可能插了个MMC卡,也可能插了个SD卡,还可能插了个SDIO网卡。我们无法假设接口上到底接的是什么设备,但是我们可以通过电平信号判断是否接了设备。为了能够判断接口上接的是什么设备,以及设备具有怎样的参数,一般对应的协会都会指定一套完善的协议标准(例如USB协议,SD协议)。驱动代码中只要按照协议规定,跟设备进行通信,获取到对方提供的信息,然后根据协议进行解析,就可以获得所接硬件的详细信息。然后加载对应的驱动就可以正常使用硬件了。以下是mmc驱动框架,相比于I2C驱动框架,主要是多了协议解析部分,你再细品!

图片

(MMC驱动框架)

总结:

还是重点强调一点,学习新东西,一定是要由远及近,逐渐深入。先知道每个模块是干什么的,然后在学会怎么使用 ,最后才是深入去研究工作原理,以及如何修改。学习驱动开发更是这样,熟悉基本的驱动框架和各个模块的具体框架才是你第一步需要做的,剩下工作就是配置寄存器,初始化硬件设备了,这不就是单片机工程师现在正在做的事情吗?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/940852.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ABB D674A906U01流量计变送器模块

流量测量: 该模块用于准确测量液体或气体的流量,通常以标准单位(如立方米每小时或加仑每分钟)表示。 传感器技术: 它通常使用各种传感器技术(例如涡轮、电磁、超声波等)来检测流体的流动并进行…

冠达管理:股票停牌后会大涨吗?

股票停牌是指证券买卖所为了保护市场秩序、保护出资者利益等原因暂时中止某些股票的买卖。但是,股票停牌前的股价与停牌后的股价会有什么不同呢?股票停牌后是否会大涨呢?在本文中,咱们将从多个视点进行剖析,以帮助人们…

合宙Air724UG LuatOS-Air LVGL API控件--按钮 (Button)

按钮 (Button) 按钮控件,这个就不用多说了,界面的基础控件之一。 示例代码 – 按键回调函数 event_handler function(obj, event) if event lvgl.EVENT_CLICKED then print(“Clicked\n”) elseif event lvgl.EVENT_VALUE_CHANGED then print(“To…

java.lang.IllegalStateException: Unable to find

java.lang.IllegalStateException: Unable to find a SpringBootConfiguration, you need to use ContextConfiguration or SpringBootTest(classes…) with your test 错误场景:在使用mybatisplus做测试时,出现此错误 解决方案:SpringBoot…

【MCU】SD NAND芯片之国产新选择

文章目录 前言传统SD卡和可贴片SD卡传统SD卡可贴片SD卡 实际使用总结 前言 随着目前时代的快速发展,即使是使用MCU的项目上也经常有大数据存储的需求。可以看到经常有小伙伴这样提问: 大家好,请问有没有SD卡芯片,可以直接焊接到P…

python可视化matplotlib——绘制正弦和余弦

这是一个使用matplotlib库绘制正弦和余弦函数曲线的代码示例。代码中导入了需要的库,并设置了x轴和y轴的标签字体为华文楷体。然后,使用numpy生成一组x轴上的值t,并使用正弦函数生成对应的y轴值s,再使用余弦函数生成对应的y轴值z。…

使用Tampermonkey(篡改猴)向页面注入js脚本

一、Tampermonkey 简单介绍 Tampermonkey是一款浏览器插件,适用于Chrome、Microsoft Edge、Safari、Opera Next 和 Firefox。他允许我们自定义javascript给指定网页添加功能,或修改现有功能。也可以用来辅助调试,或去除网页广告等。 官网地…

Vulkan LoaderLayer

目录 一、Loader The Loader 二、Layer 调度链Dispatch Chains JSON 一、Loader Vulkan是一个层架构,由Vulkan ApplicationLoaderLayerICDs(Installable Client Drivers)组成。 Vulkan 是一个显式 API,可以直接控制 GPU 的实际工作方式。因此&#x…

get请求报错400 非法参数

get请求报错400 非法参数 背景&#xff1a;get请求数据&#xff0c;SpringBoot提供接口&#xff0c;返回400&#xff0c;报错非法参数此种情况排除接口本身错误之外&#xff0c;检查参数中有没有特殊字符 " < > [ \ ] ^ { | } 我这边就是因为其中一个参数中有中括…

数字货运保持深层角力,满帮业绩与投资价值双丰收

上半年&#xff0c;经济持续活跃&#xff0c;货运物流行业承担着帮助经济要素流通的职责&#xff0c;也成为最直接的受益者。 数字货运平台满帮8月23日发布的财报显示&#xff0c;2023年第二季度&#xff0c;其平台单量、用户量均取得显著增长&#xff0c;并带动平台业绩创下新…

【网络设备】交换机的概念、工作原理、功能以及以太网帧格式

个人主页&#xff1a;insist--个人主页​​​​​​ 本文专栏&#xff1a;网络基础——带你走进网络世界 本专栏会持续更新网络基础知识&#xff0c;希望大家多多支持&#xff0c;让我们一起探索这个神奇而广阔的网络世界。 目录 一、认识交换机 二、交换机的主要功能 1、数…

Android——基本控件(下)(十九)

1. 菜单&#xff1a;Menu 1.1 知识点 &#xff08;1&#xff09;掌握Android中菜单的使用&#xff1b; &#xff08;2&#xff09;掌握选项菜单&#xff08;OptionsMenu&#xff09;的使用&#xff1b; &#xff08;3&#xff09;掌握上下文菜单&#xff08;ContextMenu&am…

STM32F4_SD卡

目录 前言 1. SDIO协议简介 2. SDIO命令及响应 3. SD卡的操作模式及切换 4. STM32的SDIO接口 5. SDIO结构体 6. SDIO相关寄存器 7. 实验程序 7.1 main.c 7.2 SDIO_Card.c 7.3 SDIO_Card.h 前言 在之前的单片机学习过程中&#xff0c;我们已经了解到了单片机系统都需…

SQL server开启变更数据捕获(CDC)

一、CDC简介 变更数据捕获&#xff08;Change Data Capture &#xff0c;简称 CDC&#xff09;&#xff1a;记录 SQL Server 表的插入、更新和删除操作。开启cdc的源表在插入、更新和删除操作时会插入数据到日志表中。cdc通过捕获进程将变更数据捕获到变更表中&#xff0c;通过…

java子类继承父类方法、或者接口中方法的javadoc注释

说明 详情可以阅读&#xff1a; https://docs.oracle.com/en/java/javase/19/docs/specs/javadoc/doc-comment-spec.html#method-comment-inheritance 子类继承父类、或者子类实现接口&#xff0c;在子类中为了避免重复写注释&#xff0c;可以在子类方法注释的主要描述部分、或…

基于GitHooks实现项目自动实时部署

目录 基于GitHooks实现项目自动部署 基于SVNJenkins发布项目 基于GitHooks实现项目自动部署 以上创建的所有任务&#xff0c;构建工作是基于在开发人员提交完代码到远程仓库完成&#xff0c;通知运维后&#xff0c;需要手动执行构建任务&#xff0c;这样就有些不太方便。我们…

智能优化算法一元函数优化

目录 一、问题描述 二、解决方法 1.模拟退火 1.1 算法思路 1.2 求解代码 1.3 计算结果 2.粒子群算法 2.1 算法思路 2.2 求解代码 2.3 计算结果 3.遗传算法 3.1 算法思路 3.2 求解代码 3.3 计算结果 一、问题描述 本篇文章所做的是分别用模拟退火、粒子群算法…

MySQL 8.1安装

1. 下载地址 https://dev.mysql.com/downloads/mysql/8.0.html 我这里没有采用installer安装&#xff0c;因为installer安装依赖visual studio&#xff0c;所以&#xff0c;我下载的是zip文件。 最终下载的版本如下&#xff1a; 2. 添加环境变量 解压&#xff0c;添加环境…

图的存储:邻接表法

1.邻接表的定义 不同于邻接矩阵&#xff08;二维数组存储&#xff09;&#xff0c;邻接表采用的顺序链式存储实现的。 1.存储方式 顶点&#xff1a;使用结构体存储顶点&#xff0c;一个顶点包括顶点信息和指向第一条边或者弧的指针。用邻接表存储图&#xff1a;使用结构体存…

Spring Boot存在路径遍历漏洞CVE-2021-22118

文章目录 0.前言1.参考文档2.基础介绍1. 影响的版本2. **漏洞利用原理&#xff1a;** 3.解决方案3.1. 方案13.2. 方案23. 方案3 0.前言 背景&#xff1a;Spring Boot存在路径遍历漏洞。CVE-2021-22118: 官方 issue也有对此的记录&#xff0c;感兴趣可以看下 https://github.com…