1.函数修饰符
(1) __init:本质上是个宏定义,这个宏定义的作用是将被它修饰的函数放入.init.text段中去(默认为.text)。内核启动后会统一加载.init.text 段中的这些模块安装函数。.init.text 段的函数加载完了之后就会把这个段给释放掉以节省内存。
(2) __exit
2.驱动模块中的头文件
应用编程中的头文件是编译器带来的 C 库函数(如 gcc 的头文件路径在/user/include 下),而驱动源代码中的头文件是内核源代码 include 目录下的头文件。
3.驱动编译的Makefile分析
KERNELDIR := /home/linux-imxrel_imx_4.1.15_2.1.0_ga_alientek(linux根目录) CURRENT_PATH := $(shell pwd) obj-m := chrdevbase.o build: kernel_modules kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules .PHONY:clean clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
4.裸机和Linux驱动中如何操控硬件
(1)寄存器地址不同: 裸机中直接使用物理地址;Linux驱动开发需要用该物理地址在内核中映射的虚拟地址, 其中物理地址是 CPU 设计时决定的,可从 datasheet 获取。
(2)编程方法不同:裸机中习惯直接用函数指针操作寄存器地址;而Linux驱动开发习惯用封装好的 io 读写函数来操作寄存器, 以实现最大程度的可移植性。
5.驱动设备文件的创建
- 方法一 手动创建
- (1) 设备文件指的是设备驱动抽象而来的文件,应用程序通过打开设备文件进而调用设备驱动。设备文件通过主次设备号与相应的file_operations 绑定
- (2) 使用 mknod 创建设备文件: mknod /dev/xxx c 主设备号 次设备号。
- 方法二 自动创建设备文件’
- 解决方案: udev(嵌入式中用的是 mdev)
- (1) udev 是应用层的一个应用程序,可以创建设备文件。
- (2)内核驱动和应用层 udev 之间有一套信息传输机制(netlink 协议)。
- (3) 应用层启用 udev,内核驱动中使用相应接口。
- (4)驱动注册和注销的信息都会被传输给 udev,由 udev 中应用层进行设备文件的创建和删除。
- 步骤
- (1) class_create
- (2) device_create
- (3)device_destory
- (4)class_destory
- 解决方案: udev(嵌入式中用的是 mdev)
6.设备号
(1)包括主设备号和次设备号。
(2) dev_t 类型: unsigned int 类型, 32 位,用于在驱动程序中定义设备编号。高 12 位为主设备号,低 20 位为次设备号
(3) MKDEV、 MAJOR、 MINOR 三个宏: MKDEV 用于由主设备号、次设备号获得设备号,MAJOR 用于由设备号获取主设备号,MINOR 用于由设备号获取次设备号。
7.使用 cdev_alloc
从内存角度体会 cdev_alloc 用与不用的差别:
不用的 cdev_alloc 时在程序开始时直接创建一个 cdev 结构体, 在加载程序时就占用了 cdev 结构体大小的内存,直到程序运行结束时释放;用 cdev_alloc 时程序开始时创建一个 cdev 结构体指针,在加载程序时只占用了指针大小(在 32 位系统中占 4 个字节)的内存,直到运行时调用 cdev_alloc才申请 cdev 结构体大小的内存,最后在调用 cdev_del 时释放。
8.内核的虚拟地址映射方法
内核中有 2 套虚拟地址映射方法: 动态和静态
(1)静态映射
①内核移植时以代码的形式硬编码,如果要更改必须源代码后重新编译内核。
②在内核启动时建立静态映射表,到关机时销毁,中间一直有效。
(2)动态映射
①驱动程序根据需要随时动态地建立映射、使用映射、销毁映射。
②映射是短期的、临时的。
具体操作
-
静态操作
定义 GPIO 寄存器的相关寄存器虚拟地址,并强制类型转换为 unsigned int 型指针,并声明为 volatile。#define GPJ0CON S5PV210_GPJ0CON #define GPJ0DAT S5PV210_GPJ0DAT #define rGPJ0CON *((volatile unsigned int *)GPJ0CON) #define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT) rGPJ0CON = 0x1111 1111; rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮 rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); //灭
-
动态操作(1)
#define GPJ0CON_PA 0xe0200240 //物理地址 #define GPJ0DAT_PA 0xe0200244 unsigned int *pGPJ0CON; //虚拟地址 unsigned int *pGPJ0DAT; pGPJ0CON = ioremap(GPJ0CON_PA, 4); pGPJ0DAT = ioremap(GPJ0DAT_PA, 4); *pGPJ0CON = 0x11111111; *pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮 *pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); //灭 iounmap(pGPJ0CON); iounmap(pGPJ0DAT);
两个寄存器一起映射:由于两个寄存器地址是挨着的,只需映射 GPJ0CON_PA(size 改为 8)得到*pGPJ0CON, *(pGPJ0CON + 1)就是*pGPJ0DAT。
-
动态操作(2)
仿效真实驱动中,用结构体封装的方式来进行单次多寄存器的地址映射,来替代多次
映射。结构体及指针: typedef struct GPJ0REG { volatile unsigned int gpj0con; volatile unsigned int gpj0dat; }gpj0_reg_t; gpj0_reg_t *pGPJ0REG; 宏定义: #define GPJ0_REGbase 0xe0200240 模块安装函数: // 2 步完成映射 pGPJ0REG = ioremap(GPJ0_REGbase, sizeof(gpj0_reg_t)); // 映射之后用指向结构体的指针来进行操作 pGPJ0REG -> gpj0con = 0x11111111; pGPJ0REG -> gpj0dat = ((0<<3) | (0<<4) | (0<<5)); // 亮 模块卸载函数: pGPJ0REG->gpj0dat = ((1<<3) | (1<<4) | (1<<5)); // 灭 // 解除映射 iounmap(pGPJ0REG);
8.内核提供的寄存器读写接口
writel 和 readl:写和读一个 32 位的寄存器;
使用动态映射虚拟地址和静态映射虚拟地址均可以使用。
9.内核中 LED 驱动框架的基本情况
- 1.相关文件
(1) drivers/leds 目录:驱动框架规定 LED 驱动应该放置的目录。
(2) led-class.c 和 led-core.c:这两个文件加起来属于 LED 驱动框架的第一部分,由内核开发者提供,描述所有厂家不同 LED 的相同部分逻辑。
(3) leds-xxx.c:是 LED 驱动框架的第二部分,由不同厂商的驱动工程师编写添加,他们结合自己的 LED 的设备情况,使用第一部分的接口来实现最终驱动功能;
以leds-s3c24xx.c 为例:通过调用 led_classdev_register(在 drivers/leds/led-class.c 中定义)。
来完成 LED 驱动的注册。 - 2.典型驱动开发行业现状
(1)内核开发者对驱动框架进行开发和维护、升级,对应 led-class.c 和 led-core.c。
(2) SoC 厂商的驱动工程师对设备驱动源码进行编写、调试,提供参考版本,对应leds-s3c24xx.c。
(3)做产品的厂商的驱动工程师以 SoC 厂商提供的驱动源码为基础,做移植和测试。
10.gpiolib 使用方法
(1)使用 gpio_request 申请使用一个 IO 口。
(2)使用 gpio_direction_input/gpio_direction_output 设置输入/输出模式。
(3)使用 gpio_set_value 设置输出值,使用 gpio_get_value 获取 IO 口的值。



