ARM 嵌入式学习笔记 - 设备驱动程序基础 (III)
linux内核GPIO操作库函数
1.明确:
- “GPIO操作”:ARM处理器引脚具有复用功能,使用前
- 记得先配置为GPIO功能
- 一旦配置为GPIO功能,即可输入或者输出操作
- GPIO操作又分:输入操作和输出操作
- “输入操作”:此GPIO引脚的电平由外设来决定
- “输出操作”: 此GPIO引脚的电平由CPU来决定
- “库函数”:linux内核已经帮你实现,你只需调用即可
- linux内核提供的库函数的实现定义在内核源码中
2.linux内核提供的GPIO操作的库函数如下:
int gpio_request(unsigned gpio, const char *label)
- 函数功能:CPU的任何一个GPIO引脚硬件资源对于linux内核来说都是一种宝贵的资源,如果某个内核程序要向访问这个GPIO引脚硬件资源,首先必须向linux内核申请资源(类似malloc)
- 参数说明:
- gpio:GPIO引脚硬件在linux内核中的软件编号,也就是对于任何一个GPIO引脚,linux内核都给分配一个唯一的软件编号(类似GPIO引脚的身份证号)
- GPIO硬件 GPIO软件编号
- GPIOC12 PAD_GPIO_C+12
- GPIOB11 PAD_GPIO_B+11
- … …
- label:给申请的硬件GPIO引脚指定的名称,随便取
- 返回值:看内核大神的代码如何判断即可,照猫画虎
- 涉及头文件:只需将大神的代码使用的头文件全盘拷贝过来即可注意""包含的头文件不做参考,找别的代码
void gpio_free(unsigned gpio)
- 功能:内核程序如果不再使用访问GPIO硬件资源
- 记得要将硬件资源归还给linux内核,类似free
- 参数:
- gpio:要释放的GPIO硬件资源对应的软件编号
int gpio_direction_output(unsigned gpio, int value)
- 功能:配置GPIO引脚为输出功能,并且输出一个value值(1高电平/0低电平)
- 参数:
- gpio:GPIO硬件对应的软件编号
- value:输出的值
int gpio_direction_input(unsigned gpio)
- 功能:配置GPIO为输入功能
int gpio_set_value(unsigned gpio, int value)
- 功能:设置GPIO引脚的输出值为value(1:高/0:低)
- 前提是必须首先将GPIO配置为输出功能
int gpio_get_value(unsigned gpio)
- 功能:获取GPIO引脚的电平状态,返回值就是引脚的
- 电平状态(返回1:高电平;返回0:低电平)
- 此引脚到底是输入还是输出没关系!
案例:利用linux内核GPIO库函数实现加载驱动开灯
卸载驱动关灯
回顾:标准C的结构体使用
//声明描述LED硬件信息的数据结构 struct led_resource { int gpio; //LED灯对应GPIO引脚的软件编号 char *name;//LED灯的名称 int state; //LED的状态 }; //定义初始化四个LED灯的硬件信息对象 struct led_resource led_info[] = { {PAD_GPIO_C+12, "LED1", 0}, {PAD_GPIO_C+11, "LED2", 0}, {PAD_GPIO_C+10, "LED3", 0}, {PAD_GPIO_C+9, "LED4", 0} };
- 以上初始化的缺点是state字段无需初始化,但是按照传统的结构体初始化方式,state字段是必须要初始化的否则编译报错!
- 如何解决以上问题呢?也就是只初始化gpio和name字段而不用初始化state呢?
- 答:利用结构体的标记初始化方式即可
//定义初始化四个LED灯的硬件信息对象 struct led_resource led_info[] = { { .name = "LED1", .gpio = PAD_GPIO_C+12 }, { .name = "LED2", .gpio = PAD_GPIO_C+11 }, { .name = "LED3", .gpio = PAD_GPIO_C+10 }, { .name = "LED4", .gpio = PAD_GPIO_C+9 } };
结构体的标记初始化方式可以不用按照顺序,并且不用全部进行对成员初始化!
上位机实施步骤: mkdir /opt/drivers/day02/1.0 -p cd /opt/drivers/day02/1.0 vim led_drv.c vim Makefile make cp led_drv.ko /opt/rootfs/home/drivers 下位机测试: cd /home/drivers insmod led_drv.ko //开灯 rmmod led_drv //关灯
linux内核系统调用实现原理(了解)
面试题:谈谈对linux系统调用的理解
以write系统调用函数为例,掌握系统调用的实现过程:
例如:
int main(void) { write(1, "hello,world\n", 12); printf("hello,world\n"); return 0; }
- 1.当应用程序(进程)调用write系统调用函数, 首先会跑到
- C库的write函数的定义实现的地方
- 2.C库的write函数将会做两件事
- 1.首先保存write函数的系统调用号到R7寄存器
- “系统调用号”:linux内核给每一个系统调用函数分配唯一的软件编号(类似系统调用函数的身份证号)
- 定义内核源码的arch/arm/include/asm/unistd.h
- 例如:#define __NR_write (0+4)
- 2.然后调用swi指令触发一个软中断异常
- 新版本的ARM核(ARMV6开始)触发软中断的指令为svc
- 老版本的ARM核触发软中断的指令是swi,现在的新版编译器
- 同样支持swi指令!
- 3.一旦触发软中断异常,CPU核立马要处理软中断异常
- 硬件上自动将做:
- 备份CPSR到SPSR_SVC
- 设置CPSR
- MODE=SVC_MODE:切换SVC管理模式
- T=0:切换到ARM状态
- IF=11:禁止FIQ/IRQ中断
- 保存返回地址LR_SVC=PC-4
- 设置PC=0x08,至此CPU跑到0x08软中断处理的入口地址
- 至此开启了软件处理软中断异常的流程
- 软中断的处理入口地址里相关的代码有linux内核来实现也就是说当前进程由用户空间"陷入"内核空间
- 4.linux内核软中断处理的入口地址相关的代码将做
- 以下工作:
- 1.从R7寄存器取出之前保存的write函数的系统调用号4
- 2.以write系统调用号4为下标在内核的系统调用表(数组)中找到对应的一个内核函数sys_write,一旦找到对应的内核函数,继续调用此函数,调用完毕,最后原路返回到用户空间,至此write函数返回!
- “系统调用表”:本质就是一个数组,数组元素就是函数指针数组元素的下标就是系统调用号其定义在内核源码的arch/arm/kernel/calls.S
- 5.切记:要边说边画图!
linux内核设备驱动
8.1.何为设备驱动?
答:设备驱动的两大核心内容
- 1.必须有对硬件的操作访问
- 2.必须有硬件操作接口,用户能够通过这些接口来访问硬件
8.2.linux内核设备驱动的分类
字符设备驱动:对字符设备的访问按照字节流形式访问
例如:LED,按键,蜂鸣器,GPS(UART),GPRS(UART),BT(UART)
触摸屏(XY绝对坐标),LCD显示屏(像素点)
声卡,摄像头,各种硬件传感器(三轴,重力,光线,距离,温度等)
EEPROM存储器(I2C接口)
块设备驱动:对块设备的访问按照数据块进行,比如一次操作512字节
例如:硬盘,U盘,TF卡,SD卡,Nandflash,Norflash,EMMC
网络设备驱动:对网络设备驱动一般按照网络协议进行
例如:有线网卡和无线网卡
8.3.明确:linux系统的理念
“一切皆文件”:任何硬件外设都是以文件的形式存放用户访问文件本质上就是在访问对应的硬件外设字符设备对应的文件称之为字符设备文件块设备对应的文件称之为块设备文件网络设备无设备文件,通过socket套接字进行访问
8.4.字符设备文件特点属性
明确:字符设备文件本身代表的就是字符设备硬件本身
明确:字符设备文件存在于根文件系统必要目录的dev目录下
当然块设备文件也存于dev目录下
举例子:查看下位机的UART设备的字符设备文件
ls /dev/ttySAC* -lh 得到以下信息:
crw-rw---- 204, 64 /dev/ttySAC0
crw-rw---- 204, 65 /dev/ttySAC1
crw-rw---- 204, 66 /dev/ttySAC2
crw-rw---- 204, 67 /dev/ttySAC3
说明:
c:表示此设备文件对应的设备为字符设备
204:表示串口的主设备号
64/65/66/67:分别表示第一个,第二个,第三个,第四个串口的次设备号
ttySAC0:表示第一个串口的设备文件名
ttySAC1:表示第二个串口的设备文件名
ttySAC2:表示第三个串口的设备文件名
ttySAC3:表示第四个串口的设备文件名
注意:一个硬件外设个体有唯一的一个设备文件名
8.5.字符设备文件创建方法有两种:
1.手动创建,只需mknod命令
语法:
mknod /dev/字符设备文件名 c 主设备号 次设备号
例如:
mknod /dev/zhangsan c 250 0
2.自动创建
后续课程慢慢来
8.6.字符设备文件的访问
明确:访问字符设备文件本质就是在访问字符设备硬件 明确:字符设备文件的访问必须利用系统调用函数 例如: 打开UART0:注意:使用绝对路径! int fd = open("/dev/ttySAC0", O_RDWR) if (fd < 0) return -1; 从UART0读取数据: char buf[1024] = {0}; read(fd, buf, 1024); 向UART0写入数据: write(fd, "hello,world\n", 12); 关闭UART0: close(fd);
8.7.主设备号,次设备号,设备号
主设备号作用:应用程序根据字符设备文件的主设备号
在茫茫的内核驱动中找到对应的唯一的
设备驱动,一个设备驱动仅有唯一的主设备号
次设备号作用:设备驱动根据次设备号能够找到应用程序
要访问的硬件外设个体
总结:一个驱动仅有一个主设备号,一个硬件个体仅有一个次设备号
应用根据主设备号找驱动,驱动根据次设备号找硬件个体
所以:主,次设备号对于linux内核是一个宝贵的资源,某个
设备驱动必须要想linux内核申请主,次设备号
问:如何申请呢?
设备号:设备号包含主,次设备号
linux内核用dev_t(unsigned int)数据类型描述设备号信息
设备号的高12位用来描述主设备号
设备号的低20位用来描述次设备号
设备号和主,次设备号之间的转换操作宏:
设备号=MKDEV(已知的主设备号,已知的次设备号);
主设备号=MAJOR(已知的设备号);
次设备号=MINOR(已知的设备号);
作业:认真研读掌握MINOR宏的源码实现过程!
问:既然设备号对于linux内核是一种宝贵的资源,设备驱动
如何向内核申请资源呢?
答:利用以下函数即可申请
int alloc_chrdev_region(dev_t *dev,
unsigned baseminor,
unsigned count,
const char *name
);
函数功能:向内核申请设备号
参数:
dev:保存申请的设备号
包括主设备号和起始的次设备号
baseminor:希望申请的起始次设备号,一般给0
count:申请的次设备号的个数
如果baseminor=0,count=2,那么申请的次设备号分别是0和1
name:申请设备号指定的名称,随便取
将来通过执行cat /proc/devices查看
设备驱动一旦不再使用设备号,记得要将设备号资源归还给linux内核:
void unregister_chrdev_region(dev_t dev,
unsigned count);
功能:释放申请的设备号资源
dev:申请的设备号
count:申请的次设备号的个数
8.8.自行设计数据结构来描述字符设备驱动属性
//描述字符设备驱动 struct char_device { dev_t dev; //描述申请的设备号 int count; //描述申请的次设备号的个数 int (*open)(...); //给用户提供打开设备的接口 int (*close)(...); //给用户提供关闭设备的接口 int (*read)(...); //给用户提供读设备接口 int (*write)(...); //给用户提供写设备接口 };
浮想联翩,应用程序和驱动接口调用关系:
应用程序open->软中断->内核的sys_open->驱动的open接口
应用程序close->软中断->内核的sys_close->驱动的close接口
应用程序read->软中断->内核的sys_read->驱动的read接口
应用程序write->软中断->内核的sys_write->驱动的write接口
- 优化:
//描述字符设备驱动给用户提供的操作接口 struct file_operations { int (*open)(...); //给用户提供打开设备的接口 int (*close)(...); //给用户提供关闭设备的接口 int (*read)(...); //给用户提供读设备接口 int (*write)(...); //给用户提供写设备接口 }; //描述字符设备驱动 struct char_device { dev_t dev; //描述申请的设备号 int count; //描述申请的次设备号的个数 struct file_operations *ops;//给用户提供的操作接口 };
8.9.Linux内核描述字符设备驱动的数据结构
Linux内核描述给用户提供操作接口的数据结构
//描述字符设备驱动给用户提供的操作接口 struct file_operations { int (*open) (struct inode *, struct file *); //给用户提供打开设备的接口 int (*release) (struct inode *, struct file *); //给用户提供关闭设备的接口 ... }; 字符设备驱动和应用程序调用关系: 应用程序open->软中断->内核的sys_open->驱动的open接口 应用程序close->软中断->内核的sys_close->驱动的release接口 //描述字符设备驱动 struct cdev { dev_t dev; //描述申请的设备号 int count; //描述申请的次设备号的个数 struct file_operations *ops;//给用户提供的操作接口 ... };
配套函数:
cdev_init(strcut cdev *cdev,
struct file_operations *fops)
函数功能:初始化字符设备驱动对象,就是给字符设备
驱动对象添加一个硬件操作接口
cdev:要初始化的字符设备对象
fops:给用户提供的硬件操作接口
cdev_add(struct cdev *cdev, dev_t dev, int count)
函数功能:向内核注册添加一个字符设备对象,一旦添加完毕
内核就有一个真实的字符设备驱动
cdev:要注册的字符设备对象
dev:申请的设备号
count:申请的次设备号的个数
cdev_del(struct cdev *cdev)
函数功能:从内核中卸载字符设备对象,一旦卸载完毕
内核就不会有一个真实的字符设备驱动
8.10.编写字符设备驱动步骤
- 1.定义初始化硬件操作接口对象
- struct file_operations led_fops = {
- .open = led_open, //打开设备接口
- .release = led_close, //关闭设备接口
- };
- 2.定义初始化字符设备对象
- struct cdev led_cdev; //定义字符设备对象
- //led_cdev.ops = &led_fops
- cdev_init(&led_cdev, &led_fops);//给字符设备对象添加硬件操作接口
- 3.最终向内核注册字符设备对象
- cdev_add(&led_dev, 申请的设备号,次设备号的个数);
- 一旦注册成功,内核就有一个真实的字符设备驱动,并且
- 给用户提供硬件操作接口(open/release)
- 4.从内核卸载字符设备对象
- cdev_del(&led_cdev);
- 5.最后编写led_open/led_close接口
- 具体内容根据用户需求来定
案例:编写LED字符设备驱动,实现打开设备开灯,关闭设备关灯
- 注意:将四个LED灯作为一个硬件设备个体
- 实施步骤:
- 上位机执行:
mkdir /opt/drivers/day02/2.0 cd /opt/drivers/day02/2.0 vim led_drv.c //驱动 vim led_test.c //应用 vim Makefile make cp led_drv.ko /opt/rootfs/home/drivers arm-cortex_a9-linux-gnueabi-gcc -o led_test led_test.c