环境搭建:
在linux共享文件夹下创建驱动存放文件:
使用 tar -xvf 压缩包文件名 将文件解压,生成的文件linux-3.14中创建si文件夹
sourceinsight:
操作流程:
工程存储路径:
文件存储地址保存:
将Driver,Fs,include 都点击ALL Tree添加到文件中
基础操作:
搜索文件:
自动矫正代码:
字体更改:
驱动基本概念:
1.裸机驱动 mpu6050 i2c
直接操作控制器,让硬件工作,实现硬件功能和如何使用这些功能是在一起
控制器:在SOC芯片内部的一套电路,用来控制硬件设备
GPIO,timer pwm,UART,adc,i2c,spi,usb,camera,lcd
结论:只要是外设都需要控制器来控制
结论:SOC = CPU核 + 外设控制器
2.Linux 驱动
因为操作系统中,同类硬件设备一般只有一个,但是想操作硬件设备的进程会很多。这就带来一个问题,应用层如何访问硬件设备以及多个人同时访问的时候,如何解决并发问题。
Linux 驱动本质是向应用层提供访问硬件设备的函数接口,也就是说驱动只是提供硬件的功能函数接口,而如何使用这些功能由应用层代码去做。
注意:Linux下的驱动提供函数接口,必须遵从设备驱动的框架,便于Linux操作系统对设备进行管理。
Linux 驱动 = Linux 驱动框架 + 裸机操作
内核中添加驱动程序:
1.基础操作
在/mnt/hgfs/share/driver-learn/led-driver 下创建led-driver.c文件
sourceslight 工程文件下也会生成该文件便于检查与修改
2. 在Linux内核中添加代码
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL v2");
static int led_driver_init(void) //入口函数
{
printk("Hello linux module init!\n");
return 0;
}
static void led_driver_exit(void)//出口函数
{
printk("Hello linux module exit!\n");
}
module_init(led_driver_init);//告诉linux模块入口函数,加载模块代码到操作系统的时候会调用
module_exit(led_driver_exit);//告诉linux模块出口函数,从操作系统中卸载模块代码的时候调用
在首次编译后会报错
报错原因:内核空间的模块程序无法像用户空间程序一样编译,它需要内核空间的编译系统来编译
修改:
注意:
3.模块Makefile编写
uname -r 查看内核版本
ubuntu系统自带的Linux内核编译系统(pc机,x86)
/lib/modules/3.13.0-32-generic/build/Makefile问:如何在自己编写的Makefile中使用Linux内核的编译系统
答:make -C linux内核编译系统的路径 M=需要编译的模块代码路径 modules
make 调用当前目录下的Makefile-----------切换到------------>linux内核编译系统的路径Makefile
4.模块操作
modinfo xxx.ko 查看模块详细信息sudo insmod xxx.ko加载模块------>模块入口函数lsmod查看已加载模块sudo rmmod (lsmod查看挂在的模块的名称)删除模块-------->模块出口函数查看内核空间printk打印的信息:dmesg,清除dmesg使用arm加固,在板子上面卸载模块的时候提示没有目录,需要手动创建目录mkdir /lib/modules/3.14.0 -p
板上电以后会直接生成led_driver.ko文件然后使用操作模块进行验证操作重复查找看是否移植到板上
5.驱动实现:
应用层调用函数接口,驱动实现接口功能
编写驱动核心工作:
1.提供驱动设备的操作函数接口
2.如何找到提供的操作函数接口
1.完成接口功能实现,提供接口
完成接口功能实现,并添加到file_operations结构体,在头文件linux/fs.h中
提供驱动设备的操作函数接口
2.为函数接口绑定设备并申请设备号
cdev用来描述字符设备
模块入口中所使用函数原型:
register_chrdev_region():
原 型: int register_chrdev_region(dev_t from, unsigned count, const char *name)
功 能: 注册字符设备
@param1: 起始设备号
@param2: 设备数量
@param3: 设备名字
@return: 成功返回0,失败返回负数
cdev_init():
原型
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
功能
用于初始化cdev结构体,并填充其成员ops
参数
cdev:字符设备
fops :驱动操作函数集合
返回值
无
void cdev_init(struct cdev *, const struct file_operations *); //初始化,建立cdev和file_operation 之间的连接
struct cdev *cdev_alloc(void); //动态申请一个cdev内存
void cdev_put(struct cdev *p); //释放
int cdev_add(struct cdev *, dev_t, unsigned); //注册设备,通常发生在驱动模块的加载函数中
void cdev_del(struct cdev *); //注销设备,通常发生在驱动模块的卸载函数中 作者:有AI野心的电工和码农 https://www.bilibili.com/read/cv23584919/ 出处:bilibili
3.添加模块
在编译后出现busy报错:设备号被占用
解决方法:查看注册主设备号cat /proc/devices
4.关联设备号(字符设备)
5.添加设备文件并查看
进入根目录下的/deveg: sudo mknod /dev/led-device c 240 0
查看设备文件主次设备号是否与驱动注册设备号一致
注意:查看设备文件权限后无法编译需要使用sudo来进行编译
6.编写打开设备文件
执行应用程序(Linux):
Linux应用程序与驱动程序互相关系:
6.驱动设备号获取
(1) 驱动的标识:设备号
12bit(主设备号)+ 20bit(次设备号) = 32bit
主设备号(MAJOR):标识一类设备;
次设备号(MINOR):为了区分同类型设备的不同设备;
Linux内核的驱动程序是通过设备文件中包含的设备号文件来访问需要的驱动程序的
(2) 在内核中如何描述描述文件?
a. struct inode 描述文件属性信息(文件类型,权限,大小修改时间,设备号[设备文件])
b. struct file 描述一个打开的文件(打开的方式,文件偏移量,...)
[注意:只要打开一次文件,就会分配一次]
(3)创建字符驱动过程:
a.对字符驱动进行描述并且给自己设计的结构体对象分配空间
b.提供硬件设备的操作函数接口
//led自定义操作命令EG:
static long led_device_ioctl(struct file *file,
unsigned int cmd, unsigned long arg)
{
struct led_device *pled = file->private_data;
switch(cmd){
case LED_DEVICE_ON:
printk("led device on\n");
led_on(pled);
break;
case LED_DEVICE_OFF:
printk("led device off\n");
led_off(pled);
break;
}
return 0;
}
c.申请空闲设备号 并使用(将struct cdev结构体添加到系统中去)
struct led_device *pled;
static int led_num = 0;
//向系统注册多个设备号
pled->devno = MKDEV(LED_DEVICE_MAJOR,LED_DEVICE_MINOR + led_num);
err = register_chrdev_region(pled->devno,1, "led-device");
if(err){
printk("Fail to register_chrdev_region\n");
goto err_register_chrdev_region;
}
led_num ++;
return pled;
//判断错误就会释放掉设备的属性结构体
err_register_chrdev_region:
kfree(pled);
mknod 设备文件名 设备文件类型 主设备号 次设备号
mknod /dev/led c 250 0
(4) 应用层访问底层字符设备驱动的过程?
答:open---->设备文件
struct inode:设备号
--------->struct cdev
它的一个成员记录操作硬件设备的函数接口
(struct file_operations)
字符数设备或者块设备,通过设备文件(属性信息中包含的设备号)来找到底层驱动程序
寻找成功之后:
struct inode 结构体记录struct cdev这个结构体首地址
struct file 结构体记录struct file_operations这个结构体首地址
(5)驱动程序必须导出设备号信息
//到处设备号信息
//1.创建类:/sys/class/目录
//THIS_MODULE即是__this_module这个变量的地址,__this_module会指向这个模块起始的地址空间,恰好是struct module变量定义的位置。__this_module 是内核模块的编译工具链为当前内核模块产生的struct module 类型对象,所以THIS_MODULE实际上是当前内核模块对象的指针。
//class_create 创建设备文件
led_cls = class_create(THIS_MODULE, "led-devices");
if (IS_ERR(led_cls)) {
printk("class_create() failed for led-devices\n");
return PTR_ERR(led_cls);
}
//2.创建设备:生成uevent文件,包含设备号信息
pled->dev = device_create(led_cls,NULL,pled->devno,
NULL,"fs4412-led-device%d",led_num);
if(IS_ERR(pled->dev)){
printk("Fail to device_create\n");
goto err_device_create;
}
err_device_create:
cdev_del(&pled->led_cdev);
(6)创建过程中的涉及结构体:
struct inode 结构体记录struct cdev这个结构体首地址
//记录led_device的操作函数接口
cdev_init(&pled->led_cdev,&led_device_ops);
#include <linux/cdev.h>
struct cdev {
struct kobject kobj; //内核用于管理字符设备驱动, kobject就是内核里最底层的类. 内核里会自动管理此成员.
struct module *owner; //通常设为THIS_MODULE, 用于防止驱动在使用中时卸载驱动模块
const struct file_operations *ops; //怎样操作(vfs), 也就是实现当用户进程进行open/read/write等操作时,驱动里对应的操作.
struct list_head list; //内核链表节点,内核里自动管理此成员.
dev_t dev; //设备号
unsigned int count; //设备数
};
struct file 结构体记录struct file_operations这个结构体首地址
//记录led设备的操作函数接口
static const struct file_operations led_device_ops = {
.open = led_device_open,
.release = led_device_close,
.unlocked_ioctl = led_device_ioctl,
//通过命令形式来控制硬件设备,相当linux系统给我们提供扩展系统功能的一个接口,可以由用户自定义命令来让硬件执行不同的代码。
};