1. module_platform_driver 和 platform_driver_probe
这两个函数都会调到__platform_driver_register,platform_driver_probe会多一些flag配置内容,用于non-hotpluggable device,有节省内存提升运行效率的效果(因为只在init阶段检测probe,过后就被删掉了)。
1.1 module_platform_driver
1. 参考driver示例
static struct platform_driver ofa_platform_driver = {
.probe = ofa_device_probe,
.remove = ofa_device_remove,
.driver = {
.name = "xxx,ofa",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(ofa_dt_ids),
},
};
module_platform_driver(ofa_platform_driver);
2. module_platform_driver宏定义
#define module_platform_driver(__platform_driver) \
module_driver(__platform_driver, platform_driver_register, \
platform_driver_unregister)
3. module_driver宏定义
#define module_driver(__driver, __register, __unregister, ...) \
static int __init __driver##_init(void) \
{ \
return __register(&(__driver) , ##__VA_ARGS__); \
} \
module_init(__driver##_init); \
static void __exit __driver##_exit(void) \
{ \
__unregister(&(__driver) , ##__VA_ARGS__); \
} \
module_exit(__driver##_exit);
4. 最终通过连接符预处理后,差不多等于这样
static int __init ofa_platform_driver_init(void) \
{ \
return platform_driver_register(&(ofa_platform_driver)); \
} \
module_init(ofa_platform_driver_init); \
static void __exit ofa_platform_driver_exit(void) \
{ \
platform_driver_unregister(&(ofa_platform_driver)); \
} \
module_exit(ofa_platform_driver_exit);
1.1.1 预处理运算符(拓展阅读)
在Linux Kernel中,经常看到“##”和“#”,也知道是连接符,就是直接把两个字符串连接起来的意思:
#、##和#@是预处理运算符,他们三者的作用分别是:
- #(单井号) —— 字符串化运算符。
- ##(双井号 )—— 连接运算符
- #@ —— 字符化运算符。(非ANSI C标准,gcc不支持)
#include <stdio.h>
#include <string.h>
#define STR(x) #x
#define PRINT_TWO_STR(a,b) printf("%s\n", STR(a##b))
int main()
{
printf ( "%s\n" , STR(just a test));
PRINT_TWO_STR(i want, money);
return 0;
}
预处理结果:
int main()
{
printf ( "%s\n" , "just a test" );
printf ( "%s\n" , "i wantmoney" );
return 0;
}
1.2 platform_driver_probe
1. 参考示例
platform_driver_probe( &isp_platform_driver, isp_platform_probe );
2. platform_driver_probe定义
#define platform_driver_probe(drv, probe) \
__platform_driver_probe(drv, probe, THIS_MODULE)
3. __platform_driver_probe位于driver/base/platform.c
1.2.1 使用说明
一般来说设备是不能被热插拔的,所以可以将probe()函数放在init段里面来节省driver运行时候的内存开销。所以使用platform_driver_probe时,一般probe函数是要加__init前缀的,不然没意义。
drvier和device匹配的方法有3种:
- 当一个设备注册的时候,他会在总线上寻找匹配的driver,platform device一般在系统启动很早的时候就注册了
- 当一个驱动注册[platform_driver_register()]的时候,他会遍历所有总线上的设备来寻找匹配,在启动的过程驱动的注册一般比较晚,或者在模块载入的时候
- 当一个驱动注册[platform_driver_probe()]的时候, 功能上和使用platform_driver_register()是一样的,唯一的区别是它不能被以后其他的device probe了,也就是说这个driver只能和一个device绑定
(以下可选读,摘自代码注释并作翻译,营养价值并不高)
Use this instead of platform_driver_register() when you know the
device is not hotpluggable and has already been registered, and you
want to remove its run-once probe() infrastructure from memory after
the driver has bound to the device.
当您知道设备不可热插拔并且已经注册,并且您想在驱动程序绑定到设备后从内存中删除其一次性运行的probe()基础结构时,请使用此选项而不是platform_driver_register()。One typical use for this would be with drivers for controllers
integrated into system-on-chip processors, where the controller
devices have been configured as part of board setup.
这方面的一个典型用途是将控制器的驱动器集成到片上系统处理器中,其中控制器设备已被配置为板设置的一部分。drv->driver.probe_type = PROBE_FORCE_SYNCHRONOUS; We have to run our
probes synchronously because we check if we find any devices to bind
to and exit with error if there are any.
我们必须同步运行探测,因为我们会检查是否找到要绑定的设备,如果有,则会错误退出。
2. workqueue 使用(浅谈)
Workqueue工作队列是利用内核线程来异步执行工作任务的通讯机制,Workqueue可以用作中断处理的Bottom-half机制,利用进程上下文来执行中断处理中耗时的任务,因此它允许睡眠,而Softirq和Tasklet在处理任务时不能睡眠。
2.1 中断下半部(拓展阅读)
这里做个拓展,有个经典的面试题,很多大厂都会问到,算是道送分(送命)题。
摘自:[https://www.elecfans.com/emb/202308082207043.html]
kernel中断处理的下半部实现都有哪些?
中断处理流程就被分为了两个部分:
第一个部分是中断处理程序(上半部),内核通过对它的异步执行完成对硬件中断的即时响应。
中断处理流程中的另外那一部分, 下半部 (bottom half)的任务主要是执行与中断相关的工作,这些工作没有被中断服务程序本身完成。
下半部并不需要指明一个确切时间,只要把这些任务推迟一点,让它们在系统不太繁忙并且中断恢复后执行就可以了。
上半部和下半部的主要区别 :
- 上半部指的是中断处理程序,下半部则指的是一些虽然与中断有相关性但是可以延后执行的任务。
- 上半部中断不能被相同类型的中断打断,而下半部依然可以被中断打断。
- 通常下半部在中断处理程序一返回就会马上运行。
- 上半部分简单快速 ,执行的时候禁止一些或者全部中断。
- 下半部分稍后执行 ,而且执行期间可以响应所有的中断。
Linux中,对中断下半部的实现主要有三种:
- 软中断
- tasklet
- work queue
(不再进一步延伸了,接下来回归工作队列主题)
2.2 工作队列结构体和函数介绍
工作队列的相关接口函数:
工作队列有一个非常适合的使用场景:kernel侧的业务建立和重连(含sleep等待)。
struct work_struct
用来描述work,初始化一个work并添加到工作队列后,将其传递到合适的内核线程来进行处理,它是用于调度的最小单位。
struct workqueue_struct
工作队列,work item都挂入到工作队列中。
struct worker
work item的处理者,每个worker对应一个内核线程。
struct worker_pool
worker池(内核线程池),是一个共享资源池,提供不同的worker来对work item进行处理。
2.3 使用示例
下面列举了两种参考使用方法:
- 使用自定义工作队列,可以短时间缓存多个不同工作,依次执行。
- 直接调用schedule_work的话,会放到系统system_wq队列中,与其他schedule_work调用者竞争。
- 本身都是通过queue_work实现。
static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}
示例一:使用自定义工作队列
1. 定义 workqueue
struct workqueue_struct *m_work_queue; /**< linux bottom half work queue. */
2. 创建workqueue
m_work_queue = create_workqueue( queue_name );
3. 创建work item。bh_work_handler为处理函数
struct work_struct *w_item = (struct work_struct *)kmalloc( sizeof( struct work_struct), GFP_ATOMIC );
INIT_WORK( (struct work_struct *)w_item, bh_work_handler );
4. 开始任务
queue_work(m_work_queue, (struct work_struct *)w_item);
5. 等待队列任务完成
flush_workqueue( m_work_queue );
6. 释放工作队列
destroy_workqueue( m_work_queue );
示例二:只创建工作
1. 创建工作,try_reconnect为工作处理函数
struct work_struct w_item;
INIT_WORK(&w_item, try_reconnect);
2. 调过工作队列,直接开始任务
schedule_work(&w_item);
3. 外设寄存器访问
最基本的应用场景,kernel支持将外设IO总线地址通过ioremap方式映射到内核虚拟地址空间使用。那么这个外设虚拟地址都支持哪些使用方式呢?
- write/read
- iowrite/ioread
- 直接地址操作
- memcpy
3.1 write/read和iowrite/ioread区别
先说结论,这两组函数均有很多厂商使用,然而没什么太大区别,以ioread32为例,其实是多了一层封装,使用时并不用太纠结,但不要混着用,选其中一种。
static inline u32 ioread32(const volatile void __iomem *addr)
{
return readl(addr);
}
还是进一步看下readl的实现(拓展阅读)
#define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(__v); __v; })
#define readl_relaxed(c) ({ u32 __r = le32_to_cpu((__force __le32)__raw_readl(c)); __r; })
#define __raw_readl __raw_readl
static __always_inline u32 __raw_readl(const volatile void __iomem *addr)
{
u32 val;
asm volatile(ALTERNATIVE("ldr %w0, [%1]",
"ldar %w0, [%1]",
ARM64_WORKAROUND_DEVICE_LOAD_ACQUIRE)
: "=r" (val) : "r" (addr));
return val;
}
再往下就是汇编了
#define __ALTERNATIVE_CFG(oldinstr, newinstr, feature, cfg_enabled) \
".if "__stringify(cfg_enabled)" == 1\n" \
"661:\n\t" \
oldinstr "\n" \
"662:\n" \
".pushsection .altinstructions,\"a\"\n" \
ALTINSTR_ENTRY(feature) \
".popsection\n" \
".subsection 1\n" \
"663:\n\t" \
newinstr "\n" \
"664:\n\t" \
".org . - (664b-663b) + (662b-661b)\n\t" \
".org . - (662b-661b) + (664b-663b)\n\t" \
".previous\n" \
".endif\n"
- write/read为ioremap标准配套函数,不同CPU体系架构上实现存在差异,架构内部(kernel/arch/xxx)调用地方比较多,配套关系可在ioremap函数注释中看到:
/**
* ioremap - map bus memory into CPU space
* @phys_addr: bus address of the memory
* @size: size of the resource to map
* * ioremap performs a platform specific sequence of operations to
* make bus memory CPU accessible via the readb/readw/readl/writeb/
* writew/writel functions and the other mmio helpers.
* ...
*/
- ioread/iowrite也可以支持大小端存储序列的外设(ioread32/ioread32be),而readl一般只用于小端存储序列。
/*
* io{read,write}{16,32,64}be() macros
*/
#define ioread16be(p) ({ __u16 __v = be16_to_cpu((__force __be16)__raw_readw(p)); __iormb(__v); __v; })
#define ioread32be(p) ({ __u32 __v = be32_to_cpu((__force __be32)__raw_readl(p)); __iormb(__v); __v; })
#define ioread64be(p) ({ __u64 __v = be64_to_cpu((__force __be64)__raw_readq(p)); __iormb(__v); __v; })
#define iowrite16be(v,p) ({ __iowmb(); __raw_writew((__force __u16)cpu_to_be16(v), p); })
#define iowrite32be(v,p) ({ __iowmb(); __raw_writel((__force __u32)cpu_to_be32(v), p); })
#define iowrite64be(v,p) ({ __iowmb(); __raw_writeq((__force __u64)cpu_to_be64(v), p); })
3.2 直接对ioremap地址读写
类似下面这种方式,可不可行呢?害,当然是可行的了 :)
#defone IP_HW_VERSION 0x100
void __iomem *reg_va = ioremap(reg_pa, reg_size);
/* 假设此外设总线带宽 32 bit */
uint32_t *reg_addr = (uint32_t *)reg_va;
// 注意:通过数组idx访问需要注意地址长度转换(u32 / u8 = 4)
printk("[array]IP hw version: 0x%x\n", reg_addr[IP_VERSION_REG/4]);
// 这种跟ioread类似,不需要地址长度转换。
reg_addr = reg_va + IP_VERSION_REG;
printk("[addr]IP hw version: 0x%x\n", *reg_addr);
// 标准使用方式
printk("[standard]IP hw version: 0x%x\n", readl(reg_va + IP_VERSION_REG));
虽然可行性是没问题的,不过iova和va区分下可读性会更好,对新人也友善一点。
3.4 memcpy
memcpy可以实现批量寄存器值备份,什么场景用呢?有些IP和外设可以通过程序调度来支持多用户上下文切换的,比如ISP处理器:在slot A支持分辨率A的case运算,在slot B时需要切换分辨率B的配置,那么客观上就存了在多组寄存器配置。一般driver实现上是先将reg值通过memcpy复制多份,需要注意的是memcpy的src地址申请方式和size大小。
使用方式1 - dma_alloc_coherent
dma_alloc_coherent,有些IP支持CDMA(configuration DMA),只需把dma_alloc_coherent申请的CDMA的地址配置到IP对应CMDA寄存器,IP上下文切换过程中会通过CDMA中的配置完成寄存器的加载,需要保证CDMA地址连续性。
#defone IP_HW_VERSION 0x100
uint32_t *reg_bak = dma_alloc_coherent(dev, size, &pa, flags);
// 需要多拷贝一个长度,因为地址是从0x00开始计算的,不然后面读IP_HW_VERSION时,值错误。ps:reg_va的定义在上节。
memcpy(reg_bak, reg_va, IP_HW_VERSION + 4);
printk("[dma_alloc]IP hw version: 0x%x\n", readl(reg_bak + IP_VERSION_REG));
使用方式2 - vmalloc
vmalloc,这种也是可以的。
uint32_t *reg_bak = (uint32_t *)vmalloc(IP_HW_VERSION + 4);
memcpy(reg_bak, reg_va, IP_HW_VERSION + 4);
printk("[vmalloc]IP hw version: 0x%x\n", readl(reg_bak + IP_VERSION_REG));
使用方式3 - 数组赋值(不可以的哦)
声明一个数组,把ioremap的寄存器copy过去。
uint32_t reg_bak[IP_HW_VERSION];
void __iomem *reg_va = ioremap(reg_pa, reg_size);
memcpy(reg_bak, reg_va, IP_HW_VERSION + 4);
如此会喜提calltrace,因为不可以把连续地址copy到非连续的地址上:
ERROR: Unexpected affinity info state
[ 1.019409] Insufficient stack space to handle exception!
[ 1.019411] ESR: 0x96000047 -- DABT (current EL)
[ 1.019412] FAR: 0xffff80001b2c7fe0
[ 1.019414] Task stack: [0xffff80001b2c8000..0xffff80001b2cc000]
[ 1.019416] IRQ stack: [0xffff800012420000..0xffff800012424000]
[ 1.019418] Overflow stack: [0xffff000ed51e02f0..0xffff000ed51e12f0]
...
[ 1.019475] Kernel panic - not syncing: kernel stack overflow
...
[ 1.019479] Call trace:
[ 1.019480] dump_backtrace+0x0/0x1b0
[ 1.019481] show_stack+0x18/0x24
[ 1.019482] dump_stack_lvl+0xcc/0xf4
[ 1.019483] dump_stack+0x18/0x58
[ 1.019484] panic+0x170/0x384
[ 1.019485] add_taint+0x0/0xac
[ 1.019487] handle_bad_stack+0x11c/0x144
[ 1.019488] __bad_stack+0xa0/0xa4
...
3.5 devm_ioremap和ioremap区别
devm_ioremap是对于ioremap的封装,除了操作ioremap本身,还会将映射的va添加到dev资源列表中,在driver detach时,自动释放掉关联资源,是一种更安全保险的使用方式。
void __iomem *devm_ioremap(struct device *dev, resource_size_t offset,
resource_size_t size)
{
return __devm_ioremap(dev, offset, size, DEVM_IOREMAP);
}
static void __iomem *__devm_ioremap(struct device *dev, resource_size_t offset,
resource_size_t size,
enum devm_ioremap_type type)
{
void __iomem **ptr, *addr = NULL;
ptr = devres_alloc(devm_ioremap_release, sizeof(*ptr), GFP_KERNEL);
if (!ptr)
return NULL;
switch (type) {
case DEVM_IOREMAP:
addr = ioremap(offset, size);
break;
case DEVM_IOREMAP_UC:
addr = ioremap_uc(offset, size);
break;
case DEVM_IOREMAP_WC:
addr = ioremap_wc(offset, size);
break;
case DEVM_IOREMAP_NP:
addr = ioremap_np(offset, size);
break;
}
// 添加到device devres_head资源列表中
if (addr) {
*ptr = addr;
devres_add(dev, ptr);
} else
devres_free(ptr);
return addr;
}