驱动入门

驱动是底层硬件和上层软件的桥梁

分类:

裸机程序:直接和硬件,寄存器打交道

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// helloworld drivers
#include <linux/module.h>
#include <linux/init>

// init函数必须int
static int helloworld_init(void)
{
// 内核里无法使用标准库
printk("helloworld\n");
return 0;
}

static void helloworld_exit(void)
{
printk("helloworld exit\n");
}

module_init(helloworld_init);
module_exit(helloworld_exit);

MOULDE_LICENSE("GPL");
MODULE_AUTHOR("zzh");
MODULE_VERSION("V1.0");

编译linux驱动程序的两种方法:

1:将驱动放到内核的顶层目录的drivers中,随内核一起编译,烧写镜像

2:编译成内核模块,可以在系统运行时插入或者卸载,无需重启系统,后缀为.ko

simple makefile

1
2
3
4
5
6
7
obj-m += helloworld.o	# -m表示编译成模块
KDIR = /home/linux-kernel/ #内核源码绝对路径
PWD ?= $(shell pwd) # makefile和源码路径
all:
make -C $(KDIR) M=$(PWD) modules #
clean:
rm -f *.ko *.0 *.mod.o *.mod.c *.symvers *.order

第一种方法:将驱动编译内核模块

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
2
3
4
5
6
7
8
9
10
11
12
13
# 主菜单标题,比如x86 4.19.232
mainmenu "Linux/$(ARCH)$(KERNELVERSION) Kernel Configuration"
#
menu
config helloworld
bool/tristate/string "hello world support"
default y
help
hello world
menu "yyy"
...
endmenu
endmenu

bool/tristate/string代表三种括号,default则是kconfig的默认配置,如果没有.config则会用这里的默认配置,help是帮助信息

生产.config变成CONFIG_helloworld

依赖关系:

A依赖B;A反向依赖B,即有A则有B

1
2
3
4
config A
depends on B
config A
select B

可选择项:

1
2
3
4
5
choice

choice
...
endchoice

注释:

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
2
3
4
5
config helloworld
bool "helloworld support"
default y
help
hellworld

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

image-20250927230636600

参数的读写权限定义在:include/linux/stat.h/include/uapi/linux/stat.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
#include <linux/moduleparam.h>
static int a=0;
static int array[5]={0};
static int array_size;
statc char str1[10]={0};

module_param(a, int, S_IRUGO);
MODULE_PARAM_DESC(a, "e.g a=1");

module_param(array, int, &array_size, S_IRUGO);
MODULE_PARAM_DESC(array, "e.g array=1,2,3");

module_param(str, str1, sizeof(str1), S_IRUGO);
MODULE_PARAM_DESC(str1, "e.g str=hello");

...在下面printk打印

使用insmod hello.ko a=1, array=1,2,3 str=hello后可以看到打印信息

使用modinfo可以看到desc信息

符号表

解决多模块依赖问题

1
2
EXPORT_SYMBOL(符号名)
EXPORT_SYMBOL_GPL(符号名) // 只适用包含GPL许可的模块

系统如何运行驱动

1
2
#ifndef MODULE
#define module_init(x) __initcall(x)

linux内核顶层的makefile定义了两个变量,决定编译进内核或模块的MODULE宏是否开启

1
2
KBUILD_CFLAGS_KERNEL :=
KBUILD_CFLAGS_MODULE :=
1
2
3
module_init   ----->    __define_initcall(fn, id, __sec)
static initcall_t __initcall_##fn##-d __used \
__attribute__((__section__(#__sec ".innit"))) = fn;

可以看到最终声明了一个__initcall_hello_world6的函数指针变量,放到.initcall6.init段中

内核驱动的module_init会按照编译的先后顺序放到.initcall6.init段中

除了module_init,其他init函数原型都是调用__define_initcall,最不过优先级不一样,启动顺序不一样,这些init函数放在init.h文件

image-20250928011753106

在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define INIT_CALLS_LEVEL(level)						\
__initcall##level##_start = .; \
KEEP(*(.initcall##level##.init)) \
KEEP(*(.initcall##level##s.init)) \

#define INIT_CALLS \
__initcall_start = .; \
KEEP(*(.initcallearly.init)) \
INIT_CALLS_LEVEL(0) \
INIT_CALLS_LEVEL(1) \
INIT_CALLS_LEVEL(2) \
INIT_CALLS_LEVEL(3) \
INIT_CALLS_LEVEL(4) \
INIT_CALLS_LEVEL(5) \
INIT_CALLS_LEVEL(rootfs) \
INIT_CALLS_LEVEL(6) \
INIT_CALLS_LEVEL(7) \
__initcall_end = .;

在init/main.c文件中的一个static全局变量数组存放这上面每个段的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
extern initcall_entry_t __initcall_start[];
...
static initcall_entry_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};

以上数组最终在do_initcalls被使用,函数的调用流程图

在main.c中内核调用第一个函数start_kernel,包含了许多模块的初始化函数,

在rest_init中调用了kernel_thread使用一个内核线程去执行,

最后在do_initcalls函数去for循环initcall_levels数组,从0开始,所以在代码里看到数字越小优先级越高,同一个level带s优先级低

image-20250928014454402

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static void __init do_initcall_level(int level, char *command_line)
{
initcall_entry_t *fn;

parse_args(initcall_level_names[level],
command_line, __start___param,
__stop___param - __start___param,
level, level,
NULL, ignore_unknown_bootoption);

do_trace_initcall_level(initcall_level_names[level]);
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(initcall_from_entry(fn));
}

static void __init do_initcalls(void)
{
int level;
size_t len = saved_command_line_len + 1;
char *command_line;

command_line = kzalloc(len, GFP_KERNEL);
if (!command_line)
panic("%s: Failed to allocate %zu bytes\n", __func__, len);

for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) {
/* Parser modifies command_line, restore it each time */
strcpy(command_line, saved_command_line);
do_initcall_level(level, command_line);
}

kfree(command_line);
}

字符设备基础

设备号

linux规定每一个字符设备或块设备都必须有一个专属的设备号——一个设备号分为主设备号和次设备号

主设备:表示某一类驱动,如USB驱动,声卡驱动

此设备:表示该类别的第几个设备

开发字符设备,要注册设备号,向系统告诉用的什么设备,才能向系统注册设备

在include/linux/types.h中,设备号其实是32位无符号整型,其中高12位为主设备号,低20位为次设备号

1
2
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;

在include/linux/kdev.h中包含了设备号的操作方法宏

1
2
3
4
5
6
#define MINORBITS	20
#define MINORMASK ((1U << MINORBITS) - 1)

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

设备号分配:

  • 静态分配:开发人员指定非被系统占用的设备号:cat /proc/devices
  • 动态分配:系统自动分配

在include/linux/fs.h中定义了两个方法的分配函数

1
2
3
4
5
6
7
8
//  设备号起始值  此设备号数量  设备名称
// 成功返回0,失败小于0
int register_chrdev_region(dev_t, unsigned, const char*)
// 保存申请到的设备号 次设备号的起始地址,一般0 设备号数量 设备号名称
// 成功返回0,失败小于0
int alloc_chrdev_region(dev_t*, unsigned, unsigned, const char*)

unregister_chrdev_region(dev_t, unsigned)

example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <linux/moudle.h>
#include <linux/init.h>
#include <linux/kdev_t.h>
#include <linux/types.h>
#include <linux/fs.h>

static int major = 0;
static int minor = 0;

module_param(major, int, S_IRUGO);
module_param(minor, int, S_IRUGO);

dev_t dev_num;

static int _init(void)
{
int ret;
if(major) {
dev_num=MKDEV(major,minor);
ret=register_chrdev_region(deV_num,1,"chardev_name");
} else {
ret=alloc_chrdev_region(&dev_num,0,1,"alloc_name");
major=MAJOR(dev_num);
minor=MINOR(dev_num);
}
return 0;
}

static void _exit(void)
{
unregister_chrdev_region(dev_num,1);
}

module_init();
module_exit();

MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxx");

字符设备

1、

在linux中使用cdev结构体描述一个字符设备,位于include/linux/cdev.h

1
2
3
4
5
6
7
8
struct cdev{
struct kobject kobj;
struct module*owner; //所属模块
const struct file_operations*ops; // 文件操作结构体,系统调用和驱动程序的桥梁
struct list_head list;
dev_t dev; //设备号
unsigned int count;
};

2、

cdev_init初始化cdev结构体,建立cdev与ops的联系

1
2
3
4
5
6
7
void cdev_init(strcut cdev*cdev, const struct file_operation*fops)
{
memset(cdev,0,sizeof(*cdev));
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops=fops;
}

3、

cdev_add向系统添加cdev结构体,向系统添加字符设备

1
2
3
4
5
6
7
8
9
10
11
12
13
int cdev_add(struct cdev*p, dev_t dev, unsigned count)
{
int error;
p->dev=dev;
p->count=count;

error=kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if(error)
return error;
kobject_get(p->kobj.parent);
return 0;
}

4、

cdev_del删除字符设备

file_operations

该结构体在include/linux/fs.h定义,使得应用层可以操作驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
struct file_operations {
struct module *owner;
fop_flags_t fop_flags;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
unsigned int flags);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
void (*splice_eof)(struct file *file);
int (*setlease)(struct file *, int, struct file_lease **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);
int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *,
unsigned int poll_flags);
int (*mmap_prepare)(struct vm_area_desc *);
} __randomize_layout;

设备节点

通过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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
stuct class *class;
struct devices *device;

static int modulecdev_init(void)
{
...
class = class_create(THIS_MODULE,"test");
devices = device_create(class, NULL, dev_num, NULL, "test");
}

static void modulecdev_exit(void)
{
...
devices_destoy(class, dev_num);
class_destroy(class);
}

用户空间和内核空间的数据交换

当我们在用户程序使用read和write时,实际借助了两个函数

1
2
3
#include <linux/uaccess.h>
unsigned long copy_from_user(void*to, const void __user*from, usigned long n);
unsigned long copy_to_user(void __user*to,cons void*from, usinged long n);

设备结构体

随时驱动变复杂,将描述驱动的全局变量包装到一个结构体中

使用文件私有数据保存指向设备结构体的指针,linux面向对象的思想

rk3568开发板对应的交叉编译器:linux源码目录/prebuilts/gcc

设备管理

platform总线

soc包含cpu、时钟、复位电路、中断控制器、外设、GPIO、DMA、片上RAM等

soc上:数据总线、地址总线、控制总线

AHB高速总线

APB桥接APB总线:连接外设,速率低

物理总线:USB、I2C、SPI用于与传感器等通信

platfrom:虚拟总线,linux内核用于管理驱动信息,设备树依赖platform

让硬件信息和驱动程序分离,串口驱动、网口驱动,驱动注册到platfrom,可以用于热插拔

image-20251026235713322

每个总线定义一个bus_type类型结构体,包含一个match函数

硬件信息和驱动,注册device后会通过match匹配

所有硬件包含device,所有驱动包含device_driver

image-20251027001514241

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <linux/module.h>
#include <linux/init.h>
#include <linux/platfrom_device.h>

void hello_release(struct device* dev){}

struct resource res[] = {
[0]={
.start = 0x139d0000,
.end = 0x139d0000 + 0x3,
.flags = IORESOURCE_MEM,
},
[1]={
.start = 199,
.end = 199,
.flags = IORESOURCE_IRQ,
},
};

struct platform_device hello_device = {
.name="xxx"
.id = 1,
.num_resources = ARRAY_SIZE(res),
.resource = res;
.dev.realease = hello_release,
};
static int hello_init(void)
{
return platform_device_register(&hello_release);
}

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

https://blog.csdn.net/daocaokafei/article/details/113063481?ops_request_misc=%257B%2522request%255Fid%2522%253A%25228bd628df47a68babe5d221c110e9290f%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=8bd628df47a68babe5d221c110e9290f&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-9-113063481-null-null.nonecase&utm_term=match&spm=1018.2226.3001.4450

通信协议

解决串口通信两两相联的痛点,有以下总线通讯:

USB

SPI

特点:

  • 一主多从模式,只能有一个主机
  • 串行同步数据:MOSI和SCK同时配合
  • 包含4条线:SCK、MOSI、MISO、SS

片选信号线SS:高电平有效

SCK包含4中采集方式:当SCK高电平有效,只有在上升沿或下降沿数据信号才有效;

读写数据:读数据前也要先写入地址

image-20250824152701090

IIC

特点:

  • 芯片与芯片之间的通信

  • 一主多从模式,任何设备都能成为主设备

  • 串口通信两线为发送和接送,而IIC为SCL和SDA:

通信的工作机制:

SCL高电平为工作,低电平时休息:高低电平转换频率代表通信的速率:频率高,时延小

SDA高电平为1,低电平为逻辑0

数据传输的起始和结束表示:SCL都为高电平时,SDA高变低为起始位,低变高为结束位

数据传输的应答:主设备送完8位时,第9位SDA始终为0代表应答,否则代表无应答

image-20250824011332942

寻址:

当总线连接多个设备时如何寻址,从设备包含多个寄存器时如何寻址

第一组数据的前7位代表器件地址(手册寻找),后一位代表读写位,0写1读

第二组数组代表子地址——子地址一旦设定,下次再读写时可以不设置

如何进行读写

第一组数据最后一位0代表接下来的多组数据都是写入

如果读数据,第一组最后一位0写入第二组的子地址,然后再发一次起始位S和第一组数据(器件地址+1)

image-20250824013445116

预留器件地址:

image-20250824013648619

CAN——ctl area net

通讯原理:

需要专门CAN收发器:用于将1/0的普通信号转换为差分信号:用两根线的电压差,如压差为0表示逻辑1

差分信号:通过双绞线缠绕,抗干扰能力强,只有一条时某点收到干扰电平发生跳变,两条线收到干扰压差不变;所以常规通讯通常10米,CAN信号可以传输1000米

ioctl接口

除了在应用层对设备进行读写数据之外,针对串口设备,驱动层还需要提供对串口波特率、奇偶校验位、终止位的设置,这些配置信息需要从应用层传递一些基本数据,仅仅是数据类型不同

1
2
3
4
5
#include<sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
@fd:指定的设备
@cmd:驱动层的命令和应用层的命令要统一封装
失败返回-1,设置error

内核原型函数为unlocked_ioctl

1
2
long (*unlocked_ioctl)(struct file*, unsigned int, unsinged long);
第三个参数为用户空间传递的数据

image-20251012233531564

cmd解析:

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

image-20251012233601291

封装命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*****************************************************
*type :设备类型
*nr :命令序号
*size :用户传递的数据类型(int ,char ,struct name ..)
*****************************************************/
/* used to create numbers */
_IO(type,nr) :没有数据传递的命令
_IOR(type,nr,datatype) :从驱动中读取数据
_IOW(type,nr,datatype) :向驱动中写入数据
_IOWR(type,nr,datatype) :双向传送
-----------------------------------------------------
_IOC_NONE :值为0,无数据传输
_IOC_READ :值为1,从设备驱动读取数据
_IOC_WRITE :值为2,向设备驱动写入数据
_IOC_READ|_IOC_WRITE :双向数据传送
-----------------------------------------------------
/* used to decode ioctl numbers.. */
_IOC_DIR(cmd) :从命令中提取方向
_IOC_TYPE(cmd) :从命令中提取设备类型
_IOC_NR(cmd) :从命令中提取序号
_IOC_SIZE(cmd) :从命令中提取数据大小

举例使用:

通过定义命令宏,使用上面内核封装好的宏,并给序列号

使用_IOC_DIR来解析方向,再使用access_ok(type, addr, size)来判断用户层传递的内存地址是否合法

1
2
3
Doucumentation/ioctl/ioctl-number.txt
'k' 00-0F linux/spi/spidev.h conflict!
'k' 00-05 video/kyro.h conflict!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// pwm.h
#ifndef _PWM_H
#define _PWM_H
#define DEV_FIFO_TYPE 'k'
#define DEV_FIFO_CLEAN _IO(DEV_FIFO_TYPE,0)
#define DEV_FIFO_GETVALUE _IOR(DEV_FIFO_TYPE,1,int)
#define DEV_FIFO_SETVALUE _IOW(DEV_FIFO_TYPE,2,int)
#endif

//hello_ioctl.h
...
static int knum=99;
long hello_ioctl(struct*filep, unsinged int cmd, unsigned long arg)
{
long err,ret;
void __user*argp=(void __user*)arg;
int __user*p=argp;

if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE) {
pr_err("cmd %u,bad magic 0x%x/ 0x%x.\n",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
return -ENOTTY;
}
if(_IOC_DIR(cmd)&_IOC_READ)
ret=!access_ok(VERIFY_WRITE,(void __user*)arg,_IOC_SIZE(cmd));
else if(_IOC_DIR(cmd)&_IOC_WRITE)
ret=!access_ok(VERIFY_READ,(void _user*)arg,_IOC_SIZE(cmd));
if(ret) {
pr_err("bad access %ld.\n",ret);
return -EFAULT;
}
switch(cmd)
{
case DEV_FIFO_CLEAN:
printk("DEV_FIFO_CLEAN\n");
break;
case DEV_FIFO_GETVALUE:
err=put_user(knum,p);
printk();
break;
case DEV_FIFO_SETVALUE:
err=get_user(knum,p);
printk();
break;
default:
return -EINVAL;
}
return err;
}
static strcut file_operations hello_ops={
...
.unlocked_ioctl=hello_ioctl,
};

// test.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#inlcude <fcntl.h>
#include <sys/ioctl.h>
#include "pwm.h"

main()
{
int fd, len, num;
fd=open("/dev/hellodev", O_RDWR);
if(fd<0) {
perror("open faile\n");
return;
}
ioctl(fd, DEV_FIFO_CLEAN);
ioctl(fd, DEV_FIFO_GETVALUE, &num);
num=77;
ioctl(fd, DEV_FIFO_SETVALUE, &num);
close(fd);
}

pinctrl和GPIO子系统

pinctrl

定义:

pintctl依赖于设备树,pinctrl是指引脚管理系统,它可以设置引脚的复用模式(例如GPIO、串口、网口)和电气属性。pinctrl子系统可以把某一个外设用到的管脚pin放在同一个pin controller节点下面,方便引脚的管理和设置。

这些定义在pin controller节点,Linux 源码 /Documentation/devicetree/bindings 下的 txt 文档查看soc厂商的pin controller 的节点里面的属性

语法:

1
2
3
4
5
6
pinctrl-names = "default","wake up";
//定义设备的状态,可以有多个状态,default 为状态 0,wake up 为状态 1

pinctrl-0 = <&pinctrl_hog_1>;
//第 0 个状态(pinctrl-0)所对应的引脚配置,也就是 default 状态对应的引脚在 pin controller 里面定义好的节点,即 pinctrl_hog_1 里面的管脚配置
pinctrl-1 = <&pinctrl_hog_2>;//第 1 个状态(pinctrl-1)所对应的引脚配置,也就是 wake up 状态对应的引脚在 pin controller 里面定义好的节点 ,即pinctrl_hog_2 里面的管脚配置

复用模式:

引脚复用模式(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),需避免引脚连接数字电路干扰。
  • 电源与接地
    • 确认引脚所属电压域(如 3.3V 或 1.8V)与外设供电一致,避免电平不匹配导致损坏。

三、设备树配置

  1. 定位设备树文件

    • 核心设备树:rk3588.dtsi(定义芯片级引脚复用模板)。
    • 板级设备树:如rk3588-evb.dts(针对具体开发板的配置,继承自rk3588.dtsi)。
    • 推荐在板级设备树中修改(避免直接修改核心文件,便于维护)。
  2. 配置 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:引脚电气配置(上拉,其他可选下拉、浮空等)。
  3. 绑定外设节点

    • 将定义的引脚组关联到对应的外设控制器节点(如 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>;
      };
      };

四、注意事项

  1. 功能码匹配:不同复用功能对应唯一的功能码(如 I2C3_SDA 可能对应2),需严格参考官方文档。
  2. 电气配置:根据外设需求设置引脚的上下拉、驱动强度(通过pcfg_xxx配置)。
  3. 热插拔限制:部分引脚复用后不支持动态切换,需重启生效。

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 的电平值
  1. 引脚导出与注销
    • gpio_export(int gpio):将指定 GPIO 编号导出到用户空间(如/sys/class/gpio/gpioXX)。
    • gpio_unexport(int gpio):注销已导出的 GPIO。
  2. 方向配置
    • gpio_direction_input(int gpio):设置 GPIO 为输入模式。
    • gpio_direction_output(int gpio, int value):设置 GPIO 为输出模式,并初始化电平(value为 0 或 1)。
  3. 电平读写
    • gpio_get_value(int gpio):读取输入 GPIO 的电平(返回 0 或 1)。
    • gpio_set_value(int gpio, int value):设置输出 GPIO 的电平(0 为低,1 为高)。
  4. 中断配置(边沿检测)
    • 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引脚可以检测到,从而产生中断。

  1. 驱动probe(初始化)
    • 申请资源(包括HPD GPIO、IRQ),注册中断,但不使能
  2. bind
    • 资源ready,drm 桥/encoder ready
    • 使能主中断
    • 如果有HPD GPIO,调用enable_irq(dp->hpd_irq);,HPD中断生效
  3. 后续HPD插拔事件发生
    • 进入 dw_dp_hpd_irq_handler
    • 检查状态,调度hpd_work处理

w_dp_probe主要流程,每一步都做了哪些准备:

  1. 内存分配和结构体初始化
1
dp = devm_kzalloc(dev, sizeof(*dp), GFP_KERNEL);
  • 为整个DisplayPort驱动的数据结构分配内存。
  1. 获取设备ID
1
id = of_alias_get_id(dev->of_node, "dp");
  • 用于区分多个DP端口。
  1. 从设备树读取硬件参数和配置
1
ret = dw_dp_parse_dt(dp);
  • 解析设备树,获得如“force-hpd”等属性,以及最大链路速率等硬件参数。
  1. 初始化锁、工作队列、完成量等内核同步机制
1
2
3
mutex_init(&dp->irq_lock);
INIT_WORK(&dp->hpd_work, dw_dp_hpd_work);
init_completion(&dp->complete);
  • 用于保证多线程/中断下数据安全与流程调度。
  1. 寄存器地址映射,初始化regmap
1
2
base = devm_platform_ioremap_resource(pdev, 0);
dp->regmap = devm_regmap_init_mmio(dev, base, &dw_dp_regmap_config);
  • 将物理寄存器地址映射到内核空间,方便后续读写。
  1. 获取PHY、时钟、复位等硬件资源
1
2
3
4
5
6
7
8
dp->phy = devm_of_phy_get(dev, dev->of_node, NULL);
dp->apb_clk = devm_clk_get(dev, "apb");
dp->aux_clk = devm_clk_get(dev, "aux");
dp->i2s_clk = devm_clk_get(dev, "i2s");
dp->spdif_clk = devm_clk_get(dev, "spdif");
dp->hclk = devm_clk_get_optional(dev, "hclk");
dp->hdcp_clk = devm_clk_get(dev, "hdcp");
dp->rstc = devm_reset_control_get(dev, NULL);
  • 获取各种物理层、总线、音视频等相关时钟、控制器。
  1. 申请HPD GPIO和中断资源
1
2
3
4
5
6
7
8
9
10
dp->hpd_gpio = devm_gpiod_get_optional(dev, "hpd", GPIOD_IN);
if (dp->hpd_gpio) {
dp->hpd_irq = gpiod_to_irq(dp->hpd_gpio);
irq_set_status_flags(dp->hpd_irq, IRQ_NOAUTOEN);
ret = devm_request_threaded_irq(dev, dp->hpd_irq, NULL,
dw_dp_hpd_irq_handler,
IRQF_TRIGGER_RISING |
IRQF_TRIGGER_FALLING |
IRQF_ONESHOT, "dw-dp-hpd", dp);
}
  • 申请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。
  1. 申请主中断(IRQ)资源并注册中断处理函数
1
2
3
dp->irq = platform_get_irq(pdev, 0);
irq_set_status_flags(dp->irq, IRQ_NOAUTOEN);
devm_request_threaded_irq(..., dw_dp_irq_handler, ...);
  • 负责AUX、HDCP等事件的中断处理。
  1. 分配和注册extcon(外部连接)设备
1
2
dp->extcon = devm_extcon_dev_allocate(dev, dw_dp_cable);
devm_extcon_dev_register(dev, dp->extcon);
  • 用于通知系统显示器连接状态。
  1. 注册音频驱动
1
2
dw_dp_register_audio_driver(dp);
devm_add_action_or_reset(dev, dw_dp_unregister_audio_driver, dp);
  • 让DP支持音频输出。
  1. 注册AUX通道(DP的辅助通信通道)
1
2
3
4
5
dp->aux.dev = dev;
dp->aux.name = dev_name(dev);
dp->aux.transfer = dw_dp_aux_transfer;
drm_dp_aux_register(&dp->aux);
devm_add_action_or_reset(dev, dw_dp_aux_unregister, dp);
  • 用于DP协议的辅助数据交换。
  1. 初始化DRM Bridge桥接结构体
1
2
3
4
dp->bridge.of_node = dev->of_node;
dp->bridge.funcs = &dw_dp_bridge_funcs;
dp->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID | DRM_BRIDGE_OP_HPD;
dp->bridge.type = DRM_MODE_CONNECTOR_DisplayPort;
  • 为后续连接到DRM显示管理框架做准备。
  1. 设置split mode(双通道模式)相关参数
  • 如果设备树配置了split-mode,查找另一个DP端口并建立左右通道关联。
  1. 初始化HDCP加密流程相关结构体
1
dw_dp_hdcp_init(dp);
  • 为后续内容保护认证做准备。
  1. 注册component组件,等待系统调用bind继续初始化
1
return component_add(dev, &dw_dp_component_ops);

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
static int dw_dp_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct dw_dp *dp;
void __iomem *base;
int id, ret;

ret = dw_dp_parse_dt(dp);
if (ret)
return dev_err_probe(dev, ret, "failed to parse DT\n");

mutex_init(&dp->irq_lock);
INIT_WORK(&dp->hpd_work, dw_dp_hpd_work);
init_completion(&dp->complete);

base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base))
return PTR_ERR(base);

dp->regmap = devm_regmap_init_mmio(dev, base, &dw_dp_regmap_config);
if (IS_ERR(dp->regmap))
return dev_err_probe(dev, PTR_ERR(dp->regmap),
"failed to create regmap\n");

dp->phy = devm_of_phy_get(dev, dev->of_node, NULL);
if (IS_ERR(dp->phy))
return dev_err_probe(dev, PTR_ERR(dp->phy),
"failed to get phy\n");

dp->apb_clk = devm_clk_get(dev, "apb");
if (IS_ERR(dp->apb_clk))
return dev_err_probe(dev, PTR_ERR(dp->apb_clk),
"failed to get apb clock\n");

dp->aux_clk = devm_clk_get(dev, "aux");
if (IS_ERR(dp->aux_clk))
return dev_err_probe(dev, PTR_ERR(dp->aux_clk),
"failed to get aux clock\n");

dp->i2s_clk = devm_clk_get(dev, "i2s");
if (IS_ERR(dp->i2s_clk))
return dev_err_probe(dev, PTR_ERR(dp->i2s_clk),
"failed to get i2s clock\n");

dp->spdif_clk = devm_clk_get(dev, "spdif");
if (IS_ERR(dp->spdif_clk))
return dev_err_probe(dev, PTR_ERR(dp->spdif_clk),
"failed to get spdif clock\n");

dp->hclk = devm_clk_get_optional(dev, "hclk");
if (IS_ERR(dp->hclk))
return dev_err_probe(dev, PTR_ERR(dp->hclk),
"failed to get hclk\n");

dp->hdcp_clk = devm_clk_get(dev, "hdcp");
if (IS_ERR(dp->hdcp_clk))
return dev_err_probe(dev, PTR_ERR(dp->hdcp_clk),
"failed to get hdcp clock\n");

dp->rstc = devm_reset_control_get(dev, NULL);
if (IS_ERR(dp->rstc))
return dev_err_probe(dev, PTR_ERR(dp->rstc),
"failed to get reset control\n");

dp->hpd_gpio = devm_gpiod_get_optional(dev, "hpd", GPIOD_IN);
if (IS_ERR(dp->hpd_gpio))
return dev_err_probe(dev, PTR_ERR(dp->hpd_gpio),
"failed to get hpd GPIO\n");
if (dp->hpd_gpio) {
dp->hpd_irq = gpiod_to_irq(dp->hpd_gpio);
if (dp->hpd_irq < 0)
return dev_err_probe(dev, dp->hpd_irq,
"failed to get hpd irq\n");

irq_set_status_flags(dp->hpd_irq, IRQ_NOAUTOEN);
ret = devm_request_threaded_irq(dev, dp->hpd_irq, NULL,
dw_dp_hpd_irq_handler,
IRQF_TRIGGER_RISING |
IRQF_TRIGGER_FALLING |
IRQF_ONESHOT, "dw-dp-hpd", dp);
if (ret) {
dev_err(dev, "failed to request HPD interrupt\n");
return ret;
}
}

dp->irq = platform_get_irq(pdev, 0);
if (dp->irq < 0)
return dp->irq;

irq_set_status_flags(dp->irq, IRQ_NOAUTOEN);
ret = devm_request_threaded_irq(dev, dp->irq, NULL, dw_dp_irq_handler,
IRQF_ONESHOT, dev_name(dev), dp);
if (ret) {
dev_err(dev, "failed to request irq: %d\n", ret);
return ret;
}

dp->extcon = devm_extcon_dev_allocate(dev, dw_dp_cable);
if (IS_ERR(dp->extcon))
return dev_err_probe(dev, PTR_ERR(dp->extcon),
"failed to allocate extcon device\n");

ret = devm_extcon_dev_register(dev, dp->extcon);
if (ret)
return dev_err_probe(dev, ret,
"failed to register extcon device\n");

ret = dw_dp_register_audio_driver(dp);
if (ret)
return ret;

ret = devm_add_action_or_reset(dev, dw_dp_unregister_audio_driver, dp);
if (ret)
return ret;

dp->aux.dev = dev;
dp->aux.name = dev_name(dev);
dp->aux.transfer = dw_dp_aux_transfer;
ret = drm_dp_aux_register(&dp->aux);
if (ret)
return ret;

ret = devm_add_action_or_reset(dev, dw_dp_aux_unregister, dp);
if (ret)
return ret;

dp->bridge.of_node = dev->of_node;
dp->bridge.funcs = &dw_dp_bridge_funcs;
dp->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID |
DRM_BRIDGE_OP_HPD;
dp->bridge.type = DRM_MODE_CONNECTOR_DisplayPort;

platform_set_drvdata(pdev, dp);

if (device_property_read_bool(dev, "split-mode")) {
struct dw_dp *secondary = dw_dp_find_by_id(dev->driver, !dp->id);

if (!secondary)
return -EPROBE_DEFER;

dp->right = secondary;
dp->split_mode = true;
secondary->left = dp;
secondary->split_mode = true;
}

dw_dp_hdcp_init(dp);

return component_add(dev, &dw_dp_component_ops);
}

dp链路训练

  • 链路训练就是DisplayPort发送端(如主控芯片)和接收端(如显示器)商量好如何传输数据,让信号既稳定又高速。
  • 包括“协商速率”、“设置通道”、“信号质量检测”、“自动降级”等步骤。

主要流程——dw_dp_link_train:

  1. 能力检测——dw_dp_link_probe
    • 发送端读取接收端(显示器)的能力,比如最大支持速率(bandwidth)、最大通道数(lanes)。
    • 使用AUX通道读取DPCD寄存器(DisplayPort Configuration Data)。
    • 读取sink_count(接收端数量),确保显示器已连接。
    • 判断支持哪些训练模式(如TPS3/TPS4)。
  2. 配置参数——dw_dp_link_configure
    • 根据双方能力,设置合适的速率和通道数,配置本地PHY和寄存器。
  3. 时钟恢复(Clock Recovery)——dw_dp_link_clock_recovery
    • 发送端发出特殊的训练信号(Pattern 1),显示器检测信号是否稳定。
    • 发送端和接收端通过AUX通道不断调整电气参数(如电压摆幅、预加重),直到时钟信号稳定。
    • 检查时钟恢复是否成功(drm_dp_clock_recovery_ok)。
    • 如果失败,驱动会自动降低速率或减少通道数,然后重试。
  4. 信道均衡(Channel Equalization)——dw_dp_link_channel_equalization
    • 时钟恢复成功后,进入信道均衡阶段,发出Pattern 2/3/4等训练信号,检测信号完整性和误码率。
    • 同样通过AUX通道反馈参数,不断调整,直到信号质量达标。
    • 检查均衡是否成功(drm_dp_channel_eq_ok)。
    • 如果失败,同样降级参数重试。
  5. 训练成功,关闭训练信号,进入正式数据传输模式。
    • dw_dp_link_train_full(全流程复合训练)
    • dw_dp_link_train_fast(如果显示器支持快速训练)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dw_dp_link_train (主流程入口)

├── dw_dp_link_probe (能力检测)

├── dw_dp_link_configure (参数配置)

├── dw_dp_link_clock_recovery (时钟恢复)
│ ├── dw_dp_link_train_set_pattern (设置训练信号)
│ ├── dw_dp_link_train_update_vs_emph (调整电气参数)
│ └── drm_dp_dpcd_read_link_status (读取显示器反馈)

├── dw_dp_link_channel_equalization (信道均衡)
│ ├── dw_dp_link_train_set_pattern (设置训练信号)
│ ├── dw_dp_link_train_update_vs_emph (调整电气参数)
│ └── drm_dp_dpcd_read_link_status (读取显示器反馈)

└── dw_dp_link_train_set_pattern(DP_TRAINING_PATTERN_DISABLE) (关闭训练信号)

内核同步机制

一、工作队列(Workqueue)

  1. 基本概念
  • 工作队列(workqueue)是Linux内核提供的一种异步任务机制。
  • 用于将某些需要“慢慢做”的工作(比如耗时操作、不能在中断上下文里做的事)从中断或其他上下文“切换”到内核线程环境下执行。
  • 本质上就是:把活扔到后台,让内核帮你排队慢慢做,不阻塞当前流程
  1. 为什么需要工作队列?
  • 有些操作不能在中断环境做(比如睡眠、访问用户空间、分配大块内存)。
  • 如果直接在中断里做这些事,会导致系统卡死或异常。
  • 工作队列让你只负责“标记”要做什么,剩下的交给内核安排。
  1. 使用流程(结合你的代码)

定义:

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
2
3
4
static void dw_dp_hpd_work(struct work_struct *work)
{
// 检查HPD状态、重训练链路、重新配置视频流等
}

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完成) 用于限制设备并发访问(较早期常用)