今天先吐到这里。。
编写字符驱动的时候,我们大都是从现成的驱动中去查询函数的用法,所以不需要去记住那些函数的用法。
但是我们还是要记住一些基本的函数名,否则也就无法展开驱动的编写或者搜寻它们的用法:
- module_init(); module_exit();
- register_chrdev(); unregister_chrdev(); --上古版本
- 设备号申请:alloc_chrdev_region(); register_chrdev_region(); unregister_chrdev_region();
- 设备号处理:MAJOR(); MINOR(); MKDEV();
- 字符设备注册结构体:cdev; cdev_init(); cdev_add(); cdev_del();
- 内部操作:memcpy();
- 内外交互:copy_to_user(); copy_from_user();
- ioremap(); iounmap(); //采用设备树以后大多都使用of_iomap()
- 内存读函数:readb(); readw(); readl(); /* 8bit, 16bit, 32bit */
- 内存写函数:writeb(); writew(); writel(); /* 8bit, 16bit, 32bit */自动创建设备节点函数:
- class_create(); class_destroy();
- device_create(); device_destroy();
- 声明设备树中的节点: device_node
- GPIO使用:1:of_get_named_gpio() 2:gpio_request /gpio_free 3:gpio_direction_input/output 4: gpio_get_value() 5: gpio_set_value()
- 声明一个定时器结构体:timer_list
初始化定时器:init_timer(), 未设置周期时不会激活定时器
定时器结构体 .function = ?_function; 指定定时器回调函数
定时器结构体 .data = 回调函数传入的参数
定时器结构体 .expires=定时器超时时间,即周期
开启定时器:timer_add()
删除定时器:del_timer_sync() ://等待所有处理器用完该定时器后删除
立即删除定时器: del_timer() :
修改定时器:mod_timer() //修改定时器会立即激活定时器
时间转换: msecs_to_jiffies()Note: 与硬件定时器不同的是,使用内核定时器不需要我们去做寄存器初始化,另外内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
- 中断服务函数指针的声明: irqreturn_t (*handler)(int, void *);
- 中断申请: request_irq() // 可能会导致睡眠,所以不能用在禁止睡眠的代码段中
request_threaded_irq() -- 申请线程化的中断
从设备树获取中断号:irq_of_parse_and_map() // 如果使用GPIO的话可以使用函数 gpio_to_irq()
释放中断: free_irq()
- 中断申请: request_irq() // 可能会导致睡眠,所以不能用在禁止睡眠的代码段中
- 定义原子变量:atomic_t a ;
另外还有原子位操作, 但是并没有相应的变量结构,而是通过几个API函数直接对内存进行操作。
- 声明自旋锁: spinlock_t
初始化自旋锁: spin_lock_init()
对于临界区的访问:
在线程中可以用:
获取锁: spin_lock_irqsave()
释放锁: spin_unlock_irqrestore()
在中断服务函数中可以用:
获取锁: spin_lock()
释放锁: spin_unlock()
下半部中可以用:
获取锁: spin_lock_bh()
释放锁: spin_unlock_bh()
另外,
基于自旋锁,还衍生出了其他特定场合使用的锁,这些锁在驱动中用的不多,但是在 Linux 内核中常被用到。如:读写锁和顺序锁。 - struct semaphore sem; //定义信号量
sema_init(&sem, 1); //初始化信号
down(&sem);
//操作临界区
up(&sem);
- 互斥体等同于值为1的二值信号量,当互斥访问时优先使用,如下图
- 声明等待队列体:wait_queue_head_t
初始化等待队列头:init_waitqueue_head()
// 上面这两个可以用 DECLARE_WAIT_QUEUE_HEAD() 这一个函数来代替。
定义等待队列: DECLARE_WAITQUEUE()
添加等待队列到等待队列头:add_wait_queue() //紧接着
设置任务状态:_set_current_state()
进行任务切换,也就是让当前进程睡眠:schedule(),紧接着进行唤醒信号判断,(因为进程被唤醒后,会从睡眠点继续往下执行,我们需要判断是不是由信号唤醒的)
signal_pending(current),是信号唤醒返回真,则退出函数(不去读取值)否则返回假,即中断唤醒,就去读取值。
主动唤醒: wake_up_interruptible():只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程 wake_up() : 都能唤醒
等待事件然后自动唤醒:
设置任务状态:_set_current_state()
将进程移除等待队列: remove_wait_queue()
poll机制:
当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问的时候,驱动程序 file_operations 操作集中的 poll 函数就会执行。我们在编写驱动程序的时候需要提供对应的 poll 函 数。
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)
我们需要在驱动程序的 poll 函数中调用 poll_wait 函数,poll_wait 函数不会引起阻塞,只是 将应用程序添加到 poll_table 中,poll_wait 函数原型如下: void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
异步通知
在设备结构体中添加异步结构体指针: struct fasync_struct *async_queue
实现操作集中的fasync函数: int (*fasync) (int fd, struct file *filp, int on)
xxx_fasync 在fasync函数中只需要调用fasync_helper()来初始化fasync_struct 结构体 指针就行了
当应用程序通过“fcntl(fd, F_SETFL, flags | FASYNC)”改变 fasync 标记的时候,驱动程序 file_operations 操作集中的 fasync 函数就会执行。
关闭驱动文件的时候需要在 file_operations 操作集中的 release 函数中释放 fasync_struct,即调用xxx_fasync(-1, flip, 0)
向应用程序发送信号:kill_fasync()
无设备树platform驱动:
文件:xxxdevice.c
- 实现 platform_device结构体的成员变量(主要就是.name 和 resource 以及 resource size )
- 在模块的__init函数里调用platform_device_register()
- 在模块的__exit函数里调用 platform_device_unregister()
文件:xxxdevice.c
- 实现file_operations结构体,
- 实现platform_driver结构体成员变量
.probe中,用platform_ get_resource函数给声明的resource结构赋值,
(再加上无platform时__init的函数内容: resource内存映射,设备号、cdev、class、device)
.remove函数中,取消映射,注销,释放... (无platform时__exit的函数内容)
- 在模块的__init函数里调用platform_driver_register()
- 在模块的__exit函数里调用 platform_driver_unregister()
设备树下的platform驱动:
- 实现file_operations结构体
- 实现platform_driver结构体成员变量
.probe中,使用of函数获取节点以及节点的gpio,(再加上无platform时__init的函数内容:
resource内存映射,设备号、cdev、class、device)
.remove函数中,取消映射,注销,释放... (无platform时__exit的函数内容)
- 在模块的__init函数里调用platform_driver_register()
- 在模块的__exit函数里调用 platform_driver_unregister()
MISC 驱动:
//MISC 设备驱动的主设备号都为 10,不同的设备使用不同的从设备号。
- 实现miscdevice结构体(也就是设置里面的.minor 、.name和.fops,ops跟我们以往写的一样,这里赋给这个结构体,用以一步注册)
- 在probe函数中使用 misc_register(&上一步搞的结构体),一步register、init&add
- 在remove函数中使用 misc_deregister(&上一步搞的结构体),一步delete, unregister, destroy
- 使用 ls /sys/class/misc 查看我们注册的设备,ls /dev/xxx -l 可以看到设备的详细信息
input子系统:
input设备的本质是字符设备,,INPUT_MAJOR 定义 在 include/uapi/linux/major.h中,为13
- 向设备结构体中添加input_dev结构体指针
- 使用input_allocate_device()来给声明的input_dev结构体赋值
- 使用set_bit()函数设置input_dev结构体的evbit变量,事件类型 //产生哪类事件?
- 使用set_bit()函数设置input_dev结构体的keybit变量 ,事件码//产生该类事件的哪一个
- 在模块的__init函数里调用input_register_device(),步register、init&add,也不需要再实现fops
- 事件发生以后要使用input_event ( ) 函数来上报事件并使用input_sync()函数来同步事件
- 在__exit函数里面调用 input_unregister_device() 以及input_free_device() 这两个函数来代替之前的一系列unregister,del,destory 函数
- 当我们向 Linux 内核成功注册 input_dev 设备以后,会在/dev/input 目录下生成一 个名为“eventX(X=0….n)”的文件,这个/dev/input/eventX 就是对应的 input 设备文件
- Linux 内核会使用 input_event 结构体来表示输入事件,所以在编写测试程序时,需要创建一个input_event结构体来保存读入的信息,并从该结构体中取出我们需要的成员变量值。
- 另外也可以直接使用 hexdump 命令来查看 /dev/input/eventX的 文件内容,内容将以 input_event 类型的原始事件数据显示出来。
I2C 设备驱动:
(I2C适配器驱动是SOC厂商的job)
- 没有设备树的时候,要实现i2c_board_info结构体(也就是实现type--也就是名字,和addr--设备地址)
- 有设备树时,直接在i2c节点添加子节点type@address,属性reg和compatible也就是设置type和addr
- 在设备结构体里面,声明void *private_data ,它将指向I2C设备对应的i2c_client。
- 实现i2c_driver结构体
.probe中,使.private_data = client //client是probe函数传递进来的i2c_client结构体类型的指针。
client结构体里面含有我们的I2C设备地址等信息(在初始化i2c_msg结构体的时候,需要用到client里面保存的
设备地址信息)
(再加上无platform时__init的函数内容: 设备号、cdev、class、device)
.remove函数中,取消映射,注销,释放... (无platform时__exit的函数内容)
- 实现file_operations结构体
在读写i2c设备寄存器的函数中,使用i2c_msg 结构体,将其成员变量赋值后,作为参数传递给i2c_transfer函
数
- 在模块的__init函数里调用i2c_add_driver()
- 在__exit函数里面调用 i2c_del_driver()
//这里构造了i2c_msg结构体,然后使用i2c_transfer函数来实现传输,我们也可以使用Documentation中更加推荐的SMBUS一系列函数(属于i2c_transfer的子函数),直接使用里面封装好的接口函数,就不用那么麻烦的去自己构建i2c_msg结构体了。
//如果设备树修改正确的话,会在 /sys/bus/i2c/devices 目录下看到设备的子目录
SPI 设备驱动:
(SPI适配器驱动是SOC厂商的job)
- 没有设备树的时候,要实现spi_board_info结构体(type--也就是名字,addr)
- 有设备树时,直接在spi节点添加子节点type@address,属性reg和compatible也就是设置type和addr
- 在设备结构体里面,声明void *private_data ,它将指向spi设备对应的spi_device。
- 实现spi_driver结构体
.probe中,
1.无platform时__init的函数内容: 设备号、cdev、class、device)
2.使用of函数获取设备树中的片选gpio信息,并设置gpio为高电平,(在读写函数的头部,拉低引脚以选中)
3.初始化spi参数 (调用spi_setup函数)
4.使.private_data = spi //( probe 函数会向驱动提供当前 SPI 设备对应的 spi_device,
因此在 probe 函数中设置 private_data 为 probe 函数传递进来的 spi_device 参数。
5.初始化你的spi硬件设备,
.remove函数中,取消映射,注销,释放... (无platform时__exit的函数内容)
- 实现file_operations结构体
在读写SPI设备寄存器的函数中,
1.首先将片选引脚拉低,函数结尾要恢复至高电平
2.使用kzalloc函数申请一个spi_transfer结构体,然后初始化它的tx_buf或者rx_buf和数据长度len
3.声明一个spi_message结构体,并使用spi_message_init函数进行初始化
4.使用spi_message_add_tail函数将 spi_transfer 添加到 spi_message
5.使用spi_sync函数同步传输/spi_async异步传输
- 在模块的__init函数里调用 spi_register_driver
- 在__exit函数里面调用 spi_unregister_driver
多点触摸屏驱动(Type B):
以I2C设备驱动为框架,在其probe函数中申请并设置input子系统:
1. 哪类事件?EV_ABS(必须有),BTN_TOUCH(必须有)
2. 哪些值? ABS_MT_POSITION_X/Y
3. 初始化slots: input_mt_init_slots()
4. 注册申请的input_dev, input_register_device()
在中断服务函数中上报坐标信息:
1. input_mt_slot() //Type B 函数,用于产生 ABS_MT_SLOT 事件
2. input_mt_report_slot_state() //Type B 函数,用于产生 ABS_MT_TRACKING_ID 和
ABS_MT_TOOL_TYPE 事件,ABS_MT_TRACKING_ID 事 件 给 slot 关联一个 ABS_MT_TRACKING_ID
ABS_MT_TOOL_TYPE 事件指定触摸类型(是笔还是手指等)
3. input_report_abs() //A,B 通用,此函数上报触摸点坐标信息
4. input_mt_report_pointer_emulation() //获取当前触摸点数,然后与我们设置的最大触摸点数
量比较
4. 前面三步是根据Type A时序来循环上报所有坐标,最后使用input_sync()来发送SYN_REPORT事件
在remove函数中上释放资源,
当卸载驱动的时候 xxx_remove 函数就会执行,
我们在此函数释放一些资源并调用 input_unregister_device 来释放掉前面添加到内核中的 input_dev。