驱动学习
驱动入门
驱动是底层硬件和上层软件的桥梁
分类:
裸机程序:直接和硬件,寄存器打交道
Linux系统:基于linux驱动框架编程,统一接口,指dev下面的设备节点
驱动分类:
- 字符设备:串行顺序依次访问
- 网络设备:面向数据包的接发
- 块设备:按任意顺序访问
linux源码:www.kernel.org
半导体产商拉去linux源码,针对cpu进行适配,后下发给客户
linux源码目录:~/kernel/kernel-5.10
| 目录 | 说明 |
|---|---|
| arch | 架构相关目录,里面存放许多CPU架构,如arm,x86 |
| block | 存放块设备相关代码,比如硬盘,SD卡 |
| crypto | 存放加密算法目录 |
| Documentation | 存放官方linux内核文档 |
| drivers | 驱动目录,存放linux系统支持的硬件设备驱动源码 |
| firmware | 存放固件目录 |
| fs | 存放支持的文件系统代码目录 |
| include | 存放公共的头文件目录 |
| init | 存放linux内核启动初始化代码 |
| ipc | 存放进程间通信代码 |
| kernel | 存放内核本身的代码文件 |
| lib | 存放库函数的文件夹 |
| mm | 存放内存管理的目录 |
| net | 存放网络相关代码,比如TCP/IP协议栈 |
| scripts | 存放脚本相关代码 |
| security | 存放安全相关代码 |
| sound | 存放音频相关代码 |
| tools | 存放linux用到的工具文件夹 |
| usr | 和linux内核启动有关代码 |
| virt | 内核虚拟机相关代码 |
第一个驱动:helloworld
写驱动由几部分组成:
1、头文件:内核相关,必须有#include<linux/module.h>和#include<linux/init>
2、驱动加载函数,加载驱动时被内核自动调用
3、驱动卸载函数,卸载时自动调用
4、许可证声明,内核遵守GPL协议,可使用GPL,GPLv2
可选:
5、模块参数
6、作者和版本信息
1 | // helloworld drivers |
编译linux驱动程序的两种方法:
1:将驱动放到内核的顶层目录的drivers中,随内核一起编译,烧写镜像
2:编译成内核模块,可以在系统运行时插入或者卸载,无需重启系统,后缀为.ko
simple makefile
1 | obj-m += helloworld.o # -m表示编译成模块 |
第一种方法:将驱动编译内核模块
1、创建makefile
2、写入上面makefile规则
3、编译内核源码并烧写
4、 可以在linux源码顶层目录的makefile设置这两个变量
export ARCH=arm64
export CROSS_COMPILE=交叉编译器的路径
5、make
模块加载命令
- insmod 模块名.ko:
- modprobe 模块名.ko:加载内核模块时同时加载依赖的模块
- rmmod 模块名.ko
- lsmod或者cat /proc/modules
- modinfo 模块名.ko:查看内核模块信息
make menuconfig
在内核源码顶层目录输入命令可以打开图形
sudo apt-get install libncurses5-dev
操作:按空格选择配置状态:M, *, 不编译
[]:两种状态,编译进内核
<>:三种
() :存放字符串或者15进制数
与menuconfig相关的文件
makefile,config,Kconfig
Kconfig:是图形化配置界面的源文件,make manuconfig实际读取arch/$(ARCH)/Kconfig
config和.config:config在arch/$(ARCH)/configs目录下,.config在内核源码顶层目录下
使用.config的配置来编译内核,make manuconfig修改后的文件即.config文件,.config > Kconfig
config是系统默认的配置文件,make xxx_deconfig会编译config成 .config
Kconfig语法
1 | # 主菜单标题,比如x86 4.19.232 |
bool/tristate/string代表三种括号,default则是kconfig的默认配置,如果没有.config则会用这里的默认配置,help是帮助信息
生产.config变成CONFIG_helloworld
依赖关系:
A依赖B;A反向依赖B,即有A则有B
1 | config A |
可选择项:
1 | choice |
注释:
comment “xxx”
在图形化界面选项下面显示
source:
1 | source "init/Kconfig" |
读取另一个Kconfig文件
第二种方法:将驱动编译进内核
1、
cd drivers & cd char进去驱动目录和字符设备目录
mkdir helloworld创建添加的驱动目录
touch Kconfig makefile helloworld.c创建三个文件
2、
Kconfig:(去上一级目录的Kconfig包含该文件 source hellowrold/Kconfig)
去顶层目录进行make menuconfig生产.config文件
1 | config helloworld |
3、
helloworld.c:
4、
makefile:在.config文件已经包含CONFIG_helloworld=y变量
在上一级makefile添加关联:obj-y += helloworld/
1 | obj-$(CONFIG_helloworld) += helloworld.o |
5、
build.sh里面function build_kernel()函数`包含
使用默认的config文件生产.config,覆盖了现有的.config,使用当前的.config覆盖默认的config
1 | make ARCH=$ARCH $KERNEL_DECONFIG $KERNEL_DEFONCIF_FRAMENT |
6、
./build.sh kernel
7、
烧写的dmesg
驱动模块传参
1、让驱动程序更加灵活,兼容性更强,根据传参走不同流程
2、设置安全校验,防止驱动被盗用
不足:
1、复杂化
2、占用资源空间
传递类型:
- 基本类型:moudle_param
- 数组:moudle_param_array
- 字符串:module_param_string

参数的读写权限定义在:include/linux/stat.h/include/uapi/linux/stat.h
1 | ... |
使用insmod hello.ko a=1, array=1,2,3 str=hello后可以看到打印信息
使用modinfo可以看到desc信息
符号表
解决多模块依赖问题
1 | EXPORT_SYMBOL(符号名) |
系统如何运行驱动
1 | #ifndef MODULE |
linux内核顶层的makefile定义了两个变量,决定编译进内核或模块的MODULE宏是否开启
1 | KBUILD_CFLAGS_KERNEL := |
1 | module_init -----> __define_initcall(fn, id, __sec) |
可以看到最终声明了一个__initcall_hello_world6的函数指针变量,放到.initcall6.init段中
内核驱动的module_init会按照编译的先后顺序放到.initcall6.init段中
除了module_init,其他init函数原型都是调用__define_initcall,最不过优先级不一样,启动顺序不一样,这些init函数放在init.h文件

在inlcude/asm-generic/vmlinux.lds.h文件中的定义宏INIT_CALLS
这个将**__initcall##level##__start关联到.initcall##level##.init和.initcall##level##s.init**段中,
可以看到代码中INIT_CALLS将段0到段7的内存区域按照顺序连接到一起,并且通过**__initcall##level##__start记录每个段的起始地址**
1 | #define INIT_CALLS_LEVEL(level) \ |
在init/main.c文件中的一个static全局变量数组存放这上面每个段的地址
1 | extern initcall_entry_t __initcall_start[]; |
以上数组最终在do_initcalls被使用,函数的调用流程图
在main.c中内核调用第一个函数start_kernel,包含了许多模块的初始化函数,
在rest_init中调用了kernel_thread使用一个内核线程去执行,
最后在do_initcalls函数去for循环initcall_levels数组,从0开始,所以在代码里看到数字越小优先级越高,同一个level带s优先级低

1 | static void __init do_initcall_level(int level, char *command_line) |
字符设备基础
设备号
linux规定每一个字符设备或块设备都必须有一个专属的设备号——一个设备号分为主设备号和次设备号
主设备:表示某一类驱动,如USB驱动,声卡驱动
此设备:表示该类别的第几个设备
开发字符设备,要注册设备号,向系统告诉用的什么设备,才能向系统注册设备
在include/linux/types.h中,设备号其实是32位无符号整型,其中高12位为主设备号,低20位为次设备号
1 | typedef u32 __kernel_dev_t; |
在include/linux/kdev.h中包含了设备号的操作方法宏
1 | #define MINORBITS 20 |
设备号分配:
- 静态分配:开发人员指定非被系统占用的设备号:cat /proc/devices
- 动态分配:系统自动分配
在include/linux/fs.h中定义了两个方法的分配函数
1 | // 设备号起始值 此设备号数量 设备名称 |
example
1 | #include <linux/moudle.h> |
字符设备
1、
在linux中使用cdev结构体描述一个字符设备,位于include/linux/cdev.h
1 | struct cdev{ |
2、
cdev_init初始化cdev结构体,建立cdev与ops的联系
1 | void cdev_init(strcut cdev*cdev, const struct file_operation*fops) |
3、
cdev_add向系统添加cdev结构体,向系统添加字符设备
1 | int cdev_add(struct cdev*p, dev_t dev, unsigned count) |
4、
cdev_del删除字符设备
file_operations
该结构体在include/linux/fs.h定义,使得应用层可以操作驱动
1 | struct file_operations { |
设备节点
通过ops操作设备节点文件就利用操作驱动,设备节点被创建在/dev目录下
1:手动创建
mknod 设备节点名称 设备类型(字符设备c 块设备b) 主设备号 次设备号
2:在注册设备时自动创建
udev机制:udev是一个用户程序,根据根据设备驱动的状态来自动创建或者删除设备节点
mdev机制:在嵌入式中使用mdev,是udev的简化版本,在使用busybox创建根文件系统时自动创建mdev
设备驱动 ——》 udev ——》 /sys/class/xxx
——》 /dev/xxx设备节点
当设备驱动加载到os,会在sys/class生成对应文件夹,udev自动响应,去这个文件夹找信息生成设备节点
create相关函数
struct device *device_create(struct class *cls, struct device *parent, dev_t dev_num, void *drvdata, const char *fmt, …);
1 | stuct class *class; |
用户空间和内核空间的数据交换
当我们在用户程序使用read和write时,实际借助了两个函数
1 | #include <linux/uaccess.h> |
设备结构体
随时驱动变复杂,将描述驱动的全局变量包装到一个结构体中
使用文件私有数据保存指向设备结构体的指针,linux面向对象的思想
rk3568开发板对应的交叉编译器:linux源码目录/prebuilts/gcc
设备管理
platform总线
soc包含cpu、时钟、复位电路、中断控制器、外设、GPIO、DMA、片上RAM等
soc上:数据总线、地址总线、控制总线
AHB高速总线
APB桥接APB总线:连接外设,速率低
物理总线:USB、I2C、SPI用于与传感器等通信
platfrom:虚拟总线,linux内核用于管理驱动信息,设备树依赖platform
让硬件信息和驱动程序分离,串口驱动、网口驱动,驱动注册到platfrom,可以用于热插拔

每个总线定义一个bus_type类型结构体,包含一个match函数
硬件信息和驱动,注册device后会通过match匹配
所有硬件包含device,所有驱动包含device_driver

1 | #include <linux/module.h> |
match和probe调用时机:
1、注册platform_driver:赋值platform_bus总线类型,这里有个match函数;将device_driver注册;
2、在driver_register中,首先查找name是否被注册;然后将drv添加到bus;
3、driver_attach这里会遍历bus维护的设备链表,并把__driver_attach传进去调用
4、__driver_attach会看驱动和硬件信息的name是否相同
调用driver_match_device->platform_match:首先匹配设备树,ACPI,id_table,最后匹配name
然后进入driver_probe_device->readly_probe->probe
通信协议
解决串口通信两两相联的痛点,有以下总线通讯:
USB
SPI
特点:
- 一主多从模式,只能有一个主机
- 串行同步数据:MOSI和SCK同时配合
- 包含4条线:SCK、MOSI、MISO、SS
片选信号线SS:高电平有效
SCK包含4中采集方式:当SCK高电平有效,只有在上升沿或下降沿数据信号才有效;
读写数据:读数据前也要先写入地址
IIC
特点:
芯片与芯片之间的通信
一主多从模式,任何设备都能成为主设备
串口通信两线为发送和接送,而IIC为SCL和SDA:
通信的工作机制:
SCL高电平为工作,低电平时休息:高低电平转换频率代表通信的速率:频率高,时延小
SDA高电平为1,低电平为逻辑0
数据传输的起始和结束表示:SCL都为高电平时,SDA高变低为起始位,低变高为结束位
数据传输的应答:主设备送完8位时,第9位SDA始终为0代表应答,否则代表无应答

寻址:
当总线连接多个设备时如何寻址,从设备包含多个寄存器时如何寻址
第一组数据的前7位代表器件地址(手册寻找),后一位代表读写位,0写1读
第二组数组代表子地址——子地址一旦设定,下次再读写时可以不设置
如何进行读写
第一组数据最后一位0代表接下来的多组数据都是写入
如果读数据,第一组最后一位0写入第二组的子地址,然后再发一次起始位S和第一组数据(器件地址+1)

预留器件地址:

CAN——ctl area net
通讯原理:
需要专门CAN收发器:用于将1/0的普通信号转换为差分信号:用两根线的电压差,如压差为0表示逻辑1
差分信号:通过双绞线缠绕,抗干扰能力强,只有一条时某点收到干扰电平发生跳变,两条线收到干扰压差不变;所以常规通讯通常10米,CAN信号可以传输1000米
ioctl接口
除了在应用层对设备进行读写数据之外,针对串口设备,驱动层还需要提供对串口波特率、奇偶校验位、终止位的设置,这些配置信息需要从应用层传递一些基本数据,仅仅是数据类型不同
1 | #include<sys/ioctl.h> |
内核原型函数为unlocked_ioctl
1 | long (*unlocked_ioctl)(struct file*, unsigned int, unsinged long); |

cmd解析:
有效利用32位,分配四块部分:

封装命令:
1 | /***************************************************** |
举例使用:
通过定义命令宏,使用上面内核封装好的宏,并给序列号
使用_IOC_DIR来解析方向,再使用access_ok(type, addr, size)来判断用户层传递的内存地址是否合法
1 | Doucumentation/ioctl/ioctl-number.txt |
1 | // pwm.h |
pinctrl和GPIO子系统
pinctrl
定义:
pintctl依赖于设备树,pinctrl是指引脚管理系统,它可以设置引脚的复用模式(例如GPIO、串口、网口)和电气属性。pinctrl子系统可以把某一个外设用到的管脚pin放在同一个pin controller节点下面,方便引脚的管理和设置。
这些定义在pin controller节点,Linux 源码 /Documentation/devicetree/bindings 下的 txt 文档查看soc厂商的pin controller 的节点里面的属性
语法:
1 | pinctrl-names = "default","wake up"; |
复用模式:
引脚复用模式(Pin Multiplexing)是指微控制器(MCU)或处理器的单个物理引脚,通过软件配置寄存器,切换为不同的内置硬件功能,以实现串口、SPI、I2C、ADC 等复杂外设功能,而非仅作为普通的 GPIO(通用输入输出)引脚使用。
比如一个引脚,默认是普通 GPIO(可控制高低电平),配置后可变成 UART 串口的接收端(RX),或 SPI 的时钟线(SCK)。
一个引脚可能的复用功能包括:
- 基础功能:GPIO 输入 / 输出
- 通信功能:UART(RX/TX)、SPI(SCK/MOSI/MISO)、I2C(SDA/SCL)
- 模拟功能:ADC(模拟信号输入)、DAC(模拟信号输出)
- 控制功能:PWM(脉冲宽度调制)、定时器捕获
如何配置引脚复用?
一、文档查询
查阅 RK3588 官方数据手册(Datasheet)和引脚复用手册(Pin Multiplexing Guide),明确目标引脚的 引脚编号(如 GPIO0_A0) 及支持的复用功能(如 UART、SPI、PWM 等)。
《RK3588 Pin Multiplexing Manual》:确认引脚的 电气特性(如电压域、驱动能力),确保与外设需求匹配(例如 I2C 引脚需兼容 3.3V 电平)。
二、硬件电路确定
- 外设匹配
- 若复用为通信接口(如 I2C、SPI),需确保外设电路符合复用功能的电气要求:
- I2C 引脚需外接上拉电阻(通常 4.7kΩ)。
- 高速接口(如 SPI)需注意阻抗匹配和走线长度。
- 若复用为模拟功能(如 ADC),需避免引脚连接数字电路干扰。
- 若复用为通信接口(如 I2C、SPI),需确保外设电路符合复用功能的电气要求:
- 电源与接地
- 确认引脚所属电压域(如 3.3V 或 1.8V)与外设供电一致,避免电平不匹配导致损坏。
三、设备树配置
定位设备树文件
- 核心设备树:
rk3588.dtsi(定义芯片级引脚复用模板)。 - 板级设备树:如
rk3588-evb.dts(针对具体开发板的配置,继承自rk3588.dtsi)。 - 推荐在板级设备树中修改(避免直接修改核心文件,便于维护)。
- 核心设备树:
配置 pinctrl 节点
RK3588 通过
pinctrl子系统管理引脚复用,需定义功能对应的引脚组:1
2
3
4
5
6
7
8
9
10
11/* 在板级设备树中添加 */
&pinctrl {
/* 定义I2C3的引脚组,命名自定义(如i2c3_pins) */
i2c3_pins: i2c3-0 {
/* 配置引脚复用功能:GPIO1_B2 -> I2C3_SDA,GPIO1_B3 -> I2C3_SCL */
rockchip,pins = <
1 RK_PB2 2 &pcfg_pull_up; /* 1:GPIO1组,PB2:引脚,2:I2C3_SDA功能,上拉 */
1 RK_PB3 2 &pcfg_pull_up; /* 1:GPIO1组,PB3:引脚,2:I2C3_SCL功能,上拉 */
>;
};
};- 参数说明:
1 RK_PB2:表示 GPIO1 组的 B2 引脚(对应物理引脚编号)。2:功能编号(需与文档中 I2C3_SDA 的功能码一致)。&pcfg_pull_up:引脚电气配置(上拉,其他可选下拉、浮空等)。
- 参数说明:
绑定外设节点
将定义的引脚组关联到对应的外设控制器节点(如 I2C3 控制器):
1
2
3
4
5
6
7
8
9
10
11&i2c3 {
status = "okay"; /* 使能I2C3控制器 */
pinctrl-names = "default"; /* 引脚组名称 */
pinctrl-0 = <&i2c3_pins>; /* 绑定上述定义的i2c3_pins */
/* 可添加外设设备节点(如传感器) */
sensor@48 {
compatible = "xxx,sensor";
reg = <0x48>;
};
};
四、注意事项
- 功能码匹配:不同复用功能对应唯一的功能码(如 I2C3_SDA 可能对应
2),需严格参考官方文档。 - 电气配置:根据外设需求设置引脚的上下拉、驱动强度(通过
pcfg_xxx配置)。 - 热插拔限制:部分引脚复用后不支持动态切换,需重启生效。
GPIO
当我们使用pinctrl子系统将我们的引脚设置为GPIO复用时,我们就要用GPIO子系统来操作GPIO,GPIO子系统是用于GPIO驱动的,用于设置GPIO引脚的配置(高低电平,输入输出等等)。
| 函数类别 | 函数原型 | 参数说明 | 返回值 / 功能 |
|---|---|---|---|
| gpio_request | int gpio_request(unsigned gpio, const char *label) |
- gpio:要申请的 GPIO 标号,可通过 of_get_named_gpio 从设备树获取- label:为 GPIO 设置的名字 |
返回 0 表示申请成功,其他值表示申请失败;功能是申请一个 GPIO 管脚 |
| gpio_free | void gpio_free(unsigned gpio) |
- gpio:要释放的 GPIO 标号 |
无返回值;功能是释放不再使用的 GPIO 管脚 |
| gpio_direction_input | int gpio_direction_input(unsigned gpio) |
- gpio:要设置为输入的 GPIO 标号 |
返回 0 表示设置成功,负值表示设置失败;功能是将某个 GPIO 设置为输入模式 |
| gpio_direction_output | int gpio_direction_output(unsigned gpio, int value) |
- gpio:要设置为输出的 GPIO 标号- value:GPIO 默认输出值(0 或 1) |
返回 0 表示设置成功,负值表示设置失败;功能是将某个 GPIO 设置为输出模式,并设置默认输出值 |
| gpio_get_value | int __gpio_get_value(unsigned gpio) |
- gpio:要获取值的 GPIO 标号 |
成功返回 GPIO 的值(0 或 1),失败返回负值;功能是获取某个 GPIO 的电平值 |
| gpio_set_value | void __gpio_set_value(unsigned gpio, int value) |
- gpio:要设置值的 GPIO 标号- value:要设置的电平值(0 或 1) |
无返回值;功能是设置某个 GPIO 的电平值 |
- 引脚导出与注销
gpio_export(int gpio):将指定 GPIO 编号导出到用户空间(如/sys/class/gpio/gpioXX)。gpio_unexport(int gpio):注销已导出的 GPIO。
- 方向配置
gpio_direction_input(int gpio):设置 GPIO 为输入模式。gpio_direction_output(int gpio, int value):设置 GPIO 为输出模式,并初始化电平(value为 0 或 1)。
- 电平读写
gpio_get_value(int gpio):读取输入 GPIO 的电平(返回 0 或 1)。gpio_set_value(int gpio, int value):设置输出 GPIO 的电平(0 为低,1 为高)。
- 中断配置(边沿检测)
gpio_set_debounce(int gpio, unsigned int debounce):设置防抖时间(毫秒)。gpio_request_irq(int gpio, irq_handler_t handler, unsigned long flags, const char *name, void *dev):申请 GPIO 中断,注册中断处理函数。
HPD中断流程
HPD(Hot Plug Detect)是DisplayPort/HDMI等显示接口的一个物理信号/引脚,用于检测显示器的插入和拔出。当显示器插入或拔出DP接口时,会产生电平变化,通过GPIO引脚可以检测到,从而产生中断。
- 驱动probe(初始化)
- 申请资源(包括HPD GPIO、IRQ),注册中断,但不使能。
- bind
- 资源ready,drm 桥/encoder ready
- 使能主中断
- 如果有HPD GPIO,调用
enable_irq(dp->hpd_irq);,HPD中断生效
- 后续HPD插拔事件发生
- 进入
dw_dp_hpd_irq_handler - 检查状态,调度
hpd_work处理
- 进入
w_dp_probe的主要流程,每一步都做了哪些准备:
- 内存分配和结构体初始化
1 | dp = devm_kzalloc(dev, sizeof(*dp), GFP_KERNEL); |
- 为整个DisplayPort驱动的数据结构分配内存。
- 获取设备ID
1 | id = of_alias_get_id(dev->of_node, "dp"); |
- 用于区分多个DP端口。
- 从设备树读取硬件参数和配置
1 | ret = dw_dp_parse_dt(dp); |
- 解析设备树,获得如“force-hpd”等属性,以及最大链路速率等硬件参数。
- 初始化锁、工作队列、完成量等内核同步机制
1 | mutex_init(&dp->irq_lock); |
- 用于保证多线程/中断下数据安全与流程调度。
- 寄存器地址映射,初始化regmap
1 | base = devm_platform_ioremap_resource(pdev, 0); |
- 将物理寄存器地址映射到内核空间,方便后续读写。
- 获取PHY、时钟、复位等硬件资源
1 | dp->phy = devm_of_phy_get(dev, dev->of_node, NULL); |
- 获取各种物理层、总线、音视频等相关时钟、控制器。
- 申请HPD GPIO和中断资源
1 | dp->hpd_gpio = devm_gpiod_get_optional(dev, "hpd", GPIOD_IN); |
- 申请HPD热插拔检测用的GPIO和中断,并注册中断处理函数(但不立即使能)。
常见的IRQ标志宏(内核通用)
这些宏定义在 Linux 内核头文件 <linux/irq.h> 中,最常用的有:
- IRQF_SHARED
允许多个驱动共享同一个中断线。 - IRQF_TRIGGER_RISING
上升沿触发 - IRQF_TRIGGER_FALLING
下降沿触发 - IRQF_TRIGGER_HIGH
高电平触发 - IRQF_TRIGGER_LOW
低电平触发 - IRQF_ONESHOT
用于线程化中断,禁止嵌套。 - IRQF_DISABLED(老版本)
触发时自动屏蔽本中断。
而irq_set_status_flags主要关注如下这些状态标志:
- IRQ_NOAUTOEN
注册后不自动使能,需要手动enable_irq()。 - IRQ_NOREQUEST
不允许通过request_irq()被申请。 - IRQ_NOPROBE
不允许被探测(自动分配中断号时会跳过)。 - IRQ_NO_BALANCING
禁止该中断在多核间迁移。 - IRQ_MOVE_PENDING
标记该中断正在迁移。 - IRQ_DISABLE_UNLAZY
禁止延迟disable。
- 申请主中断(IRQ)资源并注册中断处理函数
1 | dp->irq = platform_get_irq(pdev, 0); |
- 负责AUX、HDCP等事件的中断处理。
- 分配和注册extcon(外部连接)设备
1 | dp->extcon = devm_extcon_dev_allocate(dev, dw_dp_cable); |
- 用于通知系统显示器连接状态。
- 注册音频驱动
1 | dw_dp_register_audio_driver(dp); |
- 让DP支持音频输出。
- 注册AUX通道(DP的辅助通信通道)
1 | dp->aux.dev = dev; |
- 用于DP协议的辅助数据交换。
- 初始化DRM Bridge桥接结构体
1 | dp->bridge.of_node = dev->of_node; |
- 为后续连接到DRM显示管理框架做准备。
- 设置split mode(双通道模式)相关参数
- 如果设备树配置了split-mode,查找另一个DP端口并建立左右通道关联。
- 初始化HDCP加密流程相关结构体
1 | dw_dp_hdcp_init(dp); |
- 为后续内容保护认证做准备。
- 注册component组件,等待系统调用bind继续初始化
1 | return component_add(dev, &dw_dp_component_ops); |
代码:
1 | static int dw_dp_probe(struct platform_device *pdev) |
dp链路训练
- 链路训练就是DisplayPort发送端(如主控芯片)和接收端(如显示器)商量好如何传输数据,让信号既稳定又高速。
- 包括“协商速率”、“设置通道”、“信号质量检测”、“自动降级”等步骤。
主要流程——dw_dp_link_train:
- 能力检测——dw_dp_link_probe
- 发送端读取接收端(显示器)的能力,比如最大支持速率(bandwidth)、最大通道数(lanes)。
- 使用AUX通道读取DPCD寄存器(DisplayPort Configuration Data)。
- 读取sink_count(接收端数量),确保显示器已连接。
- 判断支持哪些训练模式(如TPS3/TPS4)。
- 配置参数——dw_dp_link_configure
- 根据双方能力,设置合适的速率和通道数,配置本地PHY和寄存器。
- 时钟恢复(Clock Recovery)——dw_dp_link_clock_recovery
- 发送端发出特殊的训练信号(Pattern 1),显示器检测信号是否稳定。
- 发送端和接收端通过AUX通道不断调整电气参数(如电压摆幅、预加重),直到时钟信号稳定。
- 检查时钟恢复是否成功(
drm_dp_clock_recovery_ok)。 - 如果失败,驱动会自动降低速率或减少通道数,然后重试。
- 信道均衡(Channel Equalization)——dw_dp_link_channel_equalization
- 时钟恢复成功后,进入信道均衡阶段,发出Pattern 2/3/4等训练信号,检测信号完整性和误码率。
- 同样通过AUX通道反馈参数,不断调整,直到信号质量达标。
- 检查均衡是否成功(
drm_dp_channel_eq_ok)。 - 如果失败,同样降级参数重试。
- 训练成功,关闭训练信号,进入正式数据传输模式。
dw_dp_link_train_full(全流程复合训练)dw_dp_link_train_fast(如果显示器支持快速训练)
1 | dw_dp_link_train (主流程入口) |
内核同步机制
一、工作队列(Workqueue)
- 基本概念
- 工作队列(workqueue)是Linux内核提供的一种异步任务机制。
- 用于将某些需要“慢慢做”的工作(比如耗时操作、不能在中断上下文里做的事)从中断或其他上下文“切换”到内核线程环境下执行。
- 本质上就是:把活扔到后台,让内核帮你排队慢慢做,不阻塞当前流程。
- 为什么需要工作队列?
- 有些操作不能在中断环境做(比如睡眠、访问用户空间、分配大块内存)。
- 如果直接在中断里做这些事,会导致系统卡死或异常。
- 工作队列让你只负责“标记”要做什么,剩下的交给内核安排。
- 使用流程(结合你的代码)
定义:
1 | struct work_struct hpd_work; |
初始化:
1 | INIT_WORK(&dp->hpd_work, dw_dp_hpd_work); |
- 这表示:定义一个叫
hpd_work的工作,并指定它的回调函数是dw_dp_hpd_work。
调度执行(通常在中断处理函数里)
1 | schedule_work(&dp->hpd_work); |
- 这表示:把
hpd_work这个工作放到系统的工作队列里,稍后由内核线程执行dw_dp_hpd_work函数。
回调函数内容(可以做比较复杂/耗时的操作):
1 | static void dw_dp_hpd_work(struct work_struct *work) |
4. 工作队列的特点
- 是异步的,非阻塞的
- 可以安全地做睡眠、耗时、复杂操作
- 适合从中断、定时器等不能阻塞的上下文切换到内核线程环境
二、完成量(Completion)
1. 基本概念
- 完成量(completion)是Linux内核提供的一种同步机制,用于“等待某个事件完成”。
- 适合场景:一个线程需要等另一个地方的任务完成(比如等硬件中断、DMA传输、AUX通道返回结果等)。
2. 使用流程(结合你的代码)
定义:
1 | struct completion complete; |
初始化:
1 | init_completion(&dp->complete); |
等待完成:
1 | wait_for_completion_timeout(&dp->complete, timeout); |
- 调用这个函数的地方会阻塞,直到完成量被“唤醒”或者超时。
唤醒完成(通常在中断处理函数里):
1 | complete(&dp->complete); |
- 调用
complete()后,所有等待这个完成量的线程都会被唤醒,继续执行。
3. 应用场景
- 例如发送AUX命令后,等待硬件中断返回结果(如你的
dw_dp_aux_transfer函数)。 - 线程A发起请求并
wait_for_completion,线程B(比如中断上下文)在任务完成后complete()它。
三、其它常见同步机制(简单了解)
- 信号量(semaphore):用于控制访问共享资源,允许多个线程访问,但有最大数量限制。
- 互斥锁(mutex):只允许一个线程访问临界区,防止数据竞争。
- 自旋锁(spinlock):适合在不能睡眠的环境下(如中断),很快执行加锁/解锁操作。
- 原子变量(atomic_t):用于简单的计数、状态标记等,保证操作原子性。
| 对比项 | 完成量(completion) | 信号量(semaphore) |
|---|---|---|
| 主要用途 | 等待事件完成,点对点同步 | 控制资源并发访问,资源池 |
| 支持计数 | 不支持(只用于事件的“完成”信号) | 支持(可设置并发数量) |
| 典型场景 | 等某个硬件事件/任务完成 | 限制并发访问、资源管理 |
| 多线程支持 | 通常一个等待者,一个唤醒者 | 多个等待者、多个持有者 |
| API差异 | init_completion, wait_for_completion, complete |
sema_init, down, up |
| 内核使用频率 | 用于等待异步事件(如AUX、DMA完成) | 用于限制设备并发访问(较早期常用) |
