A kind of AOV Solution

face1

1.介绍下linux设备驱动框架
2.linux驱动开发需要做哪些工作
3.介绍下uboot启动流程
4.介绍下uboot移植的基本步骤
5.uboot驱动开发需要做哪些工作
6.I2C/SPI通信原理
7.linux中断用什么类型的锁
8.linux移植的基本步骤

介绍下linux设备驱动框架

linux的外围设备驱动,都是通过bus + driver + device来管理的。外设都是通过总线来与CPU通讯,比如CPU — i2c host – i2c bus – i2c client 设备。
kernel实现各种总线的规范以及设备管理(设备检测,驱动绑定等),驱动程序只需要注册自己的驱动,实现对设备的读写控制接口即可。

这一类驱动通常是2个层次:总线子系统 + 驱动模块

关键接口

linux内核会提供三个关键接口:

  1. bus_register:
    kernel里面的各bus子系统(如:serio, usb, pci, …)会使用该函数来注册自己。

  2. driver_register
    驱动模块使用它来向总线系统注册自己,这样驱动模块只需要关注相应driver接口的实现。通常,bus子系统会对 driver_register来进行封装,如:
    serio 提供serio_register_driver()
    usb 提供usb_register_driver()
    pci提供 pci_register_driver()

  3. register_device
    各总线除了管理driver外,还管理device,通常会提供一支API来添加设备,如: input_register_device, serio_add_port.实现上都是通过一个链表对设备进行管理,通常是在初始化或者probe的时候, 添加设备。

    设备(device)指的是具体实现总线协议的物理设备,如对serio总线而言,i8042就是它的一个设备,而该总线连接的设备(鼠标,键盘)则是一个serio driver。

注册

bus.c 和 driver.c 分别对 bus,driver和device进行管理,提供注册bus, driver和查找 device 功能

bus_register(*bus)函数生成两个list,klist_devices和klist_drivers,分别用于保存设备和驱动

1
2
3
4
5
INIT_LIST_HEAD(&priv->interfaces);
klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);
klist_init(&priv->klist_drivers, NULL, NULL);

* priv是 struct subsys_private定义在 driver/base/base.h

driver_register(*drv)实际上就是调用 bus_add_driver(*drv) 把 drv 添加到 klist_drivers:

1
klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);

device_register(*dev)实际上就是调用bus_add_device(*dev) 把dev添加到klist_devices:

1
klist_add_tail(&dev->p->knode_bus, &bus->p->klist_devices);

以 hid_bus_type为例,执行 bus_register(&hid_bus_type) 后, hid_bus_type->p->klist_devices 和 hid_bus_type->p->klist_klist_drivers 这两个list 会被初始化,为后面的 driver和 device 注册做准备,driver数据结构如下:

1
2
3
4
5
6
7
8
static struct hid_driver tpkbd_driver = {
.name = "lenovo_tpkbd",
.id_table = tpkbd_devices,
.input_mapping = tpkbd_input_mapping,
.probe = tpkbd_probe,
.remove = tpkbd_remove,
};

注册driver时,它先经过 __hid_register_driver(&tpkbd_driver),设置一些基本参数。

1
2
3
hdrv->driver.bus = &hid_bus_type;
.....
driver_register(&hdrv->driver);

设置’driver.bus’字段后,driver和bus的对应关系就建立起来了。
然后,经过 driver_register 后, hid_bus_type->p->list_drivers保存了tpkbd_driver.

Q: driver模块是不知道 hid_driver 这个数据结构的,它如何能把它的指针放到list里面呢?

答案是”不能”, list_drivers 是不能保存 hid_driver 指针的。driver模块提供了一个接口: ‘struct device_driver’ , hid_driver 这个结构里面需要包含该结构。

1
2
3
4
5
struct hid_driver {
const struct hid_device_id *id_table;
/* private: */
struct device_driver driver;
}

注册的时候,取的是 driver 字段的地址,也就是 hid_driver.driver 的指针, driver_register(&hdrv->driver); 当从 driver模块 callback 到 hid-core模块的时候, 如

1
2
3
4
5
6
7
static int hid_bus_match(struct device *dev, struct device_driver *drv)
{
struct hid_driver *hdrv = container_of(drv, struct hid_driver, driver);
struct hid_device *hdev = container_of(dev, struct hid_device, dev);

return hid_match_device(hdev, hdrv) != NULL;
}

使用 container_of 就把 hid_driver.driver 的指针转换成了hid_driver 的指针--这个方法类似 OO编程里面使用基类指针指向派生类对象。Linux普通使用这个方法,来构建框架。

device 和 driver 绑定

当增加新device的时候,bus 会轮循它的驱动列表来找到一个匹配的驱动,它们是通过device id和 driver的id_table来进行 ”匹配”的,主要是在 driver_match_device()[drivers/base/base.h] 通过 bus->match() 这个callback来让驱动判断是否支持该设备,一旦匹配成功,device的driver字段会被设置成相应的driver指针 :

1
2
3
4
5
6
7
8
9
10
11
really_probe()
{
dev->driver = drv;
if (dev->bus->probe) {
ret = dev->bus->probe(dev);
...
} else if (drv->probe) {
ret = drv->probe(dev);
...
}
}

然后 callback 该 driver 的 probe 或者 connect 函数,进行一些初始化操作。

同理,当增加新的driver时,bus也会执行相同的动作,为驱动查找设备。因此,绑定发生在两个阶段:

1: 驱动找设备,发生在driver向bus系统注册自己时候,函数调用链是:

driver_register –> bus_add_driver –> driver_attach() [dd.c] -- 将轮循device链表,查找匹配的device。
2: 设备查找驱动,发生在设备增加到总线的的时候,函数调用链是:

device_add –> bus_probe_device –> device_initial_probe –> device_attach -- 将轮循driver链表,查找匹配的driver。

匹配成功后,系统继续调用 driver_probe_device() 来 callback ‘drv->probe(dev)’ 或者 ‘bus->probe(dev) –>drv->connect(),在probe或者connect函数里面,驱动开始实际的初始化操作。因此,probe() 或者 connect() 是真正的驱动’入口’。

对驱动开发者而言,最基本是两个步骤:

  1. 定义device id table.
  2. probe()或connect()开始具体的初始化工作。

image-20241018154127124

linux驱动开发需要做哪些工作

写驱动,同时要输出驱动设计文档,使用文档(包括dts怎么配置)。写完驱动一般会提供一个sample程序,示例如何使用接口。
有些并不是重头开始写的驱动,一般是看懂代码流程,做维护工作。或者在已有代码基础上加入新功能,或者是优化旧的驱动架构。

驱动一大部分是debug的活儿,写新feature的机会不多,适配device设备的活挺多,我即使是在原厂,和host打交道更多,但一般也会适配较多的从机设备,给客户一套推荐的从机设备集合。

介绍下uboot启动流程

介绍下uboot移植的基本步骤

U-Boot(Universal Bootloader)是一个开源的引导加载程序,常用于嵌入式系统中,用于引导操作系统的启动。U-Boot的移植是将U-Boot引导加载程序适配到特定的硬件平台上的过程。下面将详细讲解U-Boot的移植过程。

了解目标平台和硬件架构:
首先,需要详细了解目标硬件平台的体系结构、处理器类型、内存布局、外设和引导方式等硬件特性。这些信息对于移植U-Boot非常重要,因为U-Boot需要与硬件平台进行交互和初始化。

获取U-Boot源代码:
从U-Boot官方网站(https://www.denx.de/wiki/U-Boot/WebHome)或Git仓库获取最新的U-Boot源代码。选择下载稳定版本或开发版本。

配置U-Boot:
进入U-Boot源代码目录,在终端中运行make _defconfig命令,其中是目标硬件平台的名称。这将生成适用于目标平台的初始配置文件。

编辑U-Boot配置:
使用文本编辑器打开生成的配置文件,通常位于configs/_defconfig。根据目标硬件平台的需求,修改配置选项,如处理器类型、内存布局、外设等。

编译U-Boot:
在终端中运行make命令,编译U-Boot。编译过程可能需要一些时间,具体时间取决于硬件平台和源代码的大小。

配置引导方式:
根据目标平台的引导方式,修改U-Boot的引导设置。这可能涉及选择启动设备(如SD卡、NAND Flash、eMMC等)、设置启动命令和环境变量等。

添加设备驱动程序:
根据目标平台的需求,添加和配置设备驱动程序。这些驱动程序包括存储设备、网络接口、显示控制器等。根据硬件平台和操作系统需求,选择合适的驱动程序并进行配置。

以下是一些常见的U-Boot移植任务:

配置引导方式:根据目标平台的引导方式,设置U-Boot的引导选项。这可能包括选择启动设备(如SD卡、NAND Flash、eMMC等),设置引导分区,配置引导加载程序的位置和大小等。

配置环境变量:U-Boot使用环境变量来存储配置和参数。根据硬件平台的需求,配置环境变量,如网络设置、内存大小、启动命令等。

配置设备驱动程序:根据目标硬件平台的外设和硬件特性,添加和配置相应的设备驱动程序。这可能包括存储设备(如MMC、NAND Flash、SPI Flash等)、网络接口(如Ethernet、Wi-Fi等)、显示控制器等。

配置启动脚本:编写启动脚本,定义U-Boot的启动流程和加载操作系统的方式。这包括设置启动命令、指定内核镜像和设备树文件的位置等。

测试和调试:将移植好的U-Boot烧录到目标平台上,并进行测试和调试。确保U-Boot能够正确初始化硬件、加载和启动操作系统,并能够与外设进行交互。

uboot驱动开发需要做哪些工作

linux中断用什么类型的锁

中断中一般使用spinlock,并且要保证临界区时间短。不能使用mutex,mutex导致进程休眠切换到其他进程执行,如果其他进程也有对mutex的申请,但是获取不了,会导致中断处理阻塞,中断是不能被阻塞的。

spinlock不适用的函数:会引起进程调度的函数。一旦进程被调度,就会形成死锁,从而导致内核崩溃。例如:

1、copy_to_user

2、copy_from_user

3、kmalloc

4、msleep

一旦在获得自旋锁之后,再去调用这些会引起进程调度的函数,就会形成死锁

这些为什么会休眠?

这主要由操作系统的内存设计导致。

我们知道操作系统的内存设计一般由L1、L2、L3缓存以及内存组成。涉及内存的操作,在缓存上如果没有cache hint的话,操作系统会逐级去查找,知道引发缺页(page fault),从而调度进程,直到能够获取指定内容。

而上述的函数便是会引发进程调度的函数。

linux移植的基本步骤

Linux内核的移植是将Linux内核适配到特定的硬件平台上,使其能够在该硬件上运行。下面将详细讲解Linux内核的移植过程。

硬件平台了解:
首先,需要详细了解目标硬件平台的体系结构、处理器类型、内存布局、外设等硬件特性。这些信息对于移植Linux内核非常重要,因为内核需要与硬件平台进行交互和初始化。

获取内核源代码:
从Linux内核官方网站(https://www.kernel.org)或Git仓库获取最新的内核源代码。选择下载稳定版本或开发版本。

配置内核:
进入内核源代码目录,在终端中运行make 命令,其中是目标硬件平台的配置选项。这将生成适用于目标平台的初始配置文件。

编辑内核配置:
使用文本编辑器打开生成的配置文件(通常为.config),根据目标硬件平台的需求,修改内核配置选项,如处理器类型、内存布局、外设、驱动程序支持等。根据具体的需求启用或禁用特定的内核功能。

交叉编译工具链配置:
配置交叉编译工具链,以便将内核编译为适用于目标硬件平台的可执行文件。交叉编译工具链包括交叉编译器、链接器和其他编译工具,用于在主机系统上生成目标系统的可执行代码。

编译内核:
在终端中运行make命令,编译Linux内核。编译过程可能需要一些时间,具体时间取决于硬件平台和源代码的大小。

设备树(Device Tree)配置:
对于使用设备树的平台,你需要配置和编译相应的设备树文件(通常为.dts或.dtsi)。设备树描述了硬件平台的设备和资源信息,并在运行时提供给内核。根据硬件平台的设备树规范,编写或修改设备树文件,并在内核编译过程中进行配置和编译。

添加设备驱动程序:
根据目标硬件平台的需求,添加和配置设备驱动程序。这些驱动程序负责与硬件外设进行交互。根据硬件平台和操作系统需求,选择合适的设备驱动程序并进行配置。有些硬件平台可能需要自定义的设备驱动程序。

配置启动加载程序(Bootloader):
在移植Linux内核时,配置启动加载程序(bootloader),以便能够正确加载和启动内核。常见的启动加载程序包括U-Boot、GRUB、Syslinux等。

在配置启动加载程序时,指定内核映像文件的位置和名称,并设置启动参数,如命令行参数、设备树文件的位置等。这些配置可能需要在启动加载程序的配置文件中进行修改。

生成内核映像:
完成内核配置和设备驱动程序的添加后,使用交叉编译工具链编译内核源代码生成内核映像文件(通常为vmlinuz或zImage)。内核映像文件是可以直接在目标硬件平台上执行的二进制文件。

烧录内核映像:
将生成的内核映像文件烧录到目标硬件平台的启动设备上,例如SD卡、NAND Flash等。确保内核映像可以被启动加载程序正确识别和加载。