回顾:
1.linux系统包括用户空间和内核空间
用户空间特点
内核空间特点
2.linux内核编程框架
module_init
insmod
module_exit
rmmod
3.linux内核程序的编译
Makefile
4.linux内核程序的命令行传参
全局变量
数据类型:没有float/double
传参声明:module_param(name,type,perm)
5.linux内核符号导出
目的:多文件之间的访问
EXPORT_SYMBOL/EXPORT_SYMBOL_GPL
6.linux内核打印函数printk
能够指定八个打印级别(数字越小,输出级别越高)
通过默认打印数据级别决定打印信息是否打印输出
默认打印级别的设置方法:两种
echo xxx > /proc/sys/kernel/printk
debug/quiet/loglevel
*************************************************
7.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 //关灯
8.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.切记:要边说边画图!
**************************************************
8.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
cp led_test /opt/rootfs/home/drivers
下位机测试:
cd /home/drivers
insmod led_drv.ko
cat /proc/devices //查看申请的主设备号
Character devices:
主设备号 设备名称
1 mem
5 /dev/tty
...
250 tarena
mknod /dev/myled c 申请的主设备号 0 //创建设备文件myled
./led_test //运行
9.编写一个字符设备驱动的完整步骤
1.先写头文件
2.搭建框架
入口函数
出口函数
各种修饰
GPL
入口和出口函数里面先不要写代码
3.该声明的声明,该定义的定义,该初始化的初始化
先搞定硬件然后软件
4.填充入口和出口函数
先写注释
后写代码
5.最后根据用户需求编写各个硬件操作接口函数
可以放到前面写
可以放到后面写,但要前面做声明
1.linux系统包括用户空间和内核空间
用户空间特点
内核空间特点
2.linux内核编程框架
module_init
insmod
module_exit
rmmod
3.linux内核程序的编译
Makefile
4.linux内核程序的命令行传参
全局变量
数据类型:没有float/double
传参声明:module_param(name,type,perm)
5.linux内核符号导出
目的:多文件之间的访问
EXPORT_SYMBOL/EXPORT_SYMBOL_GPL
6.linux内核打印函数printk
能够指定八个打印级别(数字越小,输出级别越高)
通过默认打印数据级别决定打印信息是否打印输出
默认打印级别的设置方法:两种
echo xxx > /proc/sys/kernel/printk
debug/quiet/loglevel
*************************************************
7.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 //关灯
8.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.切记:要边说边画图!
**************************************************
8.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
cp led_test /opt/rootfs/home/drivers
下位机测试:
cd /home/drivers
insmod led_drv.ko
cat /proc/devices //查看申请的主设备号
Character devices:
主设备号 设备名称
1 mem
5 /dev/tty
...
250 tarena
mknod /dev/myled c 申请的主设备号 0 //创建设备文件myled
./led_test //运行
9.编写一个字符设备驱动的完整步骤
1.先写头文件
2.搭建框架
入口函数
出口函数
各种修饰
GPL
入口和出口函数里面先不要写代码
3.该声明的声明,该定义的定义,该初始化的初始化
先搞定硬件然后软件
4.填充入口和出口函数
先写注释
后写代码
5.最后根据用户需求编写各个硬件操作接口函数
可以放到前面写
可以放到后面写,但要前面做声明