Linux 驱动的分类
-
字符设备驱动 --- 鼠标/键盘,在对应
/dev目录下有设备文件 -
网络设备驱动 --- 网卡,使用
ifconfig进行配置 -
块设备驱动 --- eMMC、SD卡
最基本框架 main == 主函数
cs
#include <linux/init.h>
#include <linux/module.h>
// 驱动入口函数
static int hello_init(void)
{
return 0;
}
// 驱动出口函数
static void hello_exit(void)
{
return;
}
module_init(hello_init); // 告诉Linux,这个驱动入口函数是那个装载入口函数
module_exit(hello_exit); // 告诉Linux,这个驱动出口函数是那个装载出口函数
source insight === 只是代码阅读工具
调试硬件
之前学硬件开发 (功能开发 功能和你业务代码是杂糅在一起 比如你要开灯 串口通信 + led的控制 + 自定义协议 + 解析协议实现对应的 led的控制)单片机
裸机 ==== 寄存器操作
hal库操作 ==== cubmx hal库函数
freeRTOS实时操作系统
linux驱动开发
1、只给应用层提供对应操作硬件设备的接口,具体怎么实现什么功能怎么使用应用层管理 美颜相机(只给你提供相机接口(打开 读取 关闭))
Linux一切皆文件 /对应设备文件,/dev 去操作对应的硬件设备
2、Linux可以是开发 多任务去访问设备
Samba
Samba 是一个让 Windows、Linux 和 macOS 等不同操作系统的设备之间能够互相共享文件、打印机等资源的开源软件套件。
| 特点 | 虚拟机共享文件夹 | Samba | VSCode Remote-SSH |
|---|---|---|---|
| 依赖 | 依赖虚拟机软件(VMware Tools) | 依赖 Samba 服务 | 依赖 SSH 服务 |
| 传输方式 | 虚拟机专用通道 | 网络协议(SMB) | 网络协议(SSH) |
| 主要用途 | 临时传文件 | 文件共享(类 NAS) | 代码开发、调试 |
| 权限控制 | 较弱 | 精细 | 基于 SSH 用户权限 |
| 访问设备 | 仅限宿主机 | 局域网任何设备 | 仅限发起 SSH 连接的设备 |
| 实时性 | 需要手动刷新 | 需要手动刷新 | 实时同步,即时生效 |
| 开发体验 | 差(需手动拷贝) | 一般(需手动拷贝) | 极佳(本地化体验) |
驱动的代码是在内核层运行的;但是gcc是在应用程序,应用层运行的;所以gcc不能编译驱动代码
linux3.14的Makefile编译:linux源码的编译系统
所以需要在共享文件夹下运行Linux的Makefile
内核编译思想: Linux 内核源码的编译系统 可以编译我们编写的模块代码。
两种方式:
第一种(产品发布阶段)
把自己写的驱动源码拷贝到Linux内核源码树下,进行对应的配置编译,编译进内核。
通过编写Makefile和Kconfig文件,并执行make menuconfig配置界面来选择是否将驱动编译进内核。
第二种(产品研发阶段)
自己编译Makefile,然后利用Linux内核的编译系统,编译自己的模块代码。
Makefile告诉内核编译系统怎么编译你的.c源码,内核编译系统根据内核源码树里的规则和配置,最终生成一个可以在ARM开发板上加载的.ko驱动模块文件。
在linux编译系统(Makefile)里面KERNELRELEASE第一次调用的时候不赋值;第二次调用的时候才赋值;
我们写的makefile会被两次调用;
第一次我们在终端执行make的时候makefile被第一次调用;
第二次调用是linux编译系统调用我们写的makefile
所以通过KERNELRELEASE来判断是第一次还是第二次调用
cs
ifeq ($(KERNELRELEASE),)
ARM_BUILD_SYSTEM=/home/hqyj/fs4412/linux.3.14/linux-3.14
X86_BUILD_SYSTEM=/lib/modules/$(shell uname -r)/build
MODULE_PATH=$(shell pwd)
arm_module:
make -C $(ARM_BUILD_SYSTEM) M=$(MODULE_PATH) ARCH=arm CROSS_COMPILE=arm-linux- modules
x86_module:
make -C $(X86_BUILD_SYSTEM) M=$(MODULE_PATH) modules
clean:
make -C $(ARM_BUILD_SYSTEM) M=$(MODULE_PATH) clean
make -C $(X86_BUILD_SYSTEM) M=$(MODULE_PATH) clean
install:
cp ./Hello-driver.ko ~/fs4412/rootfs/
else
obj-m = Hello-driver.o //新的驱动只需要改这里
endif
虚拟机(也是linux系统)的内核源码的编译系统 在 /lib/modules/$(uname -r)/build 目录下,它是一个指向内核源码树的符号链接。
uname -r 来获取当前内核版本号
modules 是 Makefile 里的一个编译目标,执行它告诉编译系统只生成内核模块(.ko文件),而不是编译整个内核。
驱动模块操作指令
sudo insmod hello-driver.ko安装驱动文件
dmesg:查看驱动
sudo rmmod hello-driver.ko卸载驱动文件
lsmod:列出当前有哪些驱动
modinfo hello-driver.ko 查看驱动的信息
编译内核操作系统
make ARCH=arm exynos_defconfig //导入配置
make ARCH=arm CROSS_COMPILE=arm-linux- uImage
驱动传参
作用
1.设置驱动相关参数;比如设置缓冲区大小
2.设置完全校验;防止驱动被盗用
方法
1.传递普通的参数
bash
module_param(name, type, perm);
name: 传递的进去的名称
type: 类型
perm: 参数读写的权限 (S_IRUSR) (可以进去看一下 /sys/module/驱动名/parameters/)
bash
static int a = 0;
static char *my_char = "a"; //对于字符来说只能传字符指针
static int count = 0;
static int number[10];
module_param(a, int, 0644);
module_param(my_char, charp, 0644);
bash
insmod hello.ko a=1
2.传递数组
bash
module_param_array(name, type, nump, perm);
name: 传递的进去的名称
type: 类型
nump: 实际传入进去的参数个数
perm: 参数读写的权限 (S_IRUSR) (可以进去看一下 /sys/module/驱动名/parameters/)
bash
module_param_array(number, int, &count, 0644);
字符设备驱动框架
linux驱动本质写的 --接口--
字符设备和块设备会生成设备文件
应用层操作设备文件从而操作硬件设备;所以调用open等函数(函数接口linux框架规定死了)
通过linux提供的框架将open和硬件接口联系
函数接口怎么规定死
通过函数指针来规定返回值和参数;硬件设备调用函数指针来调用接口;回调函数

对应硬件来说只写操作设备的接口
怎么找到接口;对于上层来说只有设备文件;所以要设备文件和接口关联;
所以通过linux设备框架来关联
如果要调用别人的接口;只能通过函数指针或者函数名(linux官方不知)来调用;函数指针可以规定死返回值和参数并且可以用回调函数
函数接口怎么规定的
file_operations
是放在结构体FILE_OPEAATION;里面存放了很多函数;有很多返回值和参数还有回调函数;接口调用这个结构体
我们需要把对应的函数写到这个结构体中
高类聚低耦合:把相关的紧密放一起 (高内聚)**,不相关的别扯上关系(**低耦合)。
cs
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
// ============================================
// 第一部分:定义接口规范(框架层)
// ============================================
/*
* struct sensor_ops - 传感器操作接口
* @init: 初始化传感器,返回值: 0成功,负数失败
* @read_data: 读取传感器数据,通过指针传出数据,返回值: 0成功,负数失败
*
* 这是一个"契约",所有硬件厂商必须实现这两个函数
*/
struct sensor_ops {
int (*init)(void);
int (*read_data)(int *data);
};
// ============================================
// 第二部分:通用框架(不依赖具体硬件)
// ============================================
/*
* framework_get_data - 通用的传感器数据获取函数
* @ops: 传感器操作结构体指针
*
* 返回值: 0成功,负数失败
*
* 这个函数不知道具体是哪个厂商的传感器
* 通过函数指针回调具体实现
*/
int framework_get_data(struct sensor_ops *ops) {
int data;
int ret;
if (!ops) {
printk(KERN_ERR "framework: ops is NULL\n");
return -EINVAL;
}
// 通过函数指针调用初始化
ret = ops->init();
if (ret < 0) {
printk(KERN_ERR "framework: sensor init failed\n");
return ret;
}
// 通过函数指针读取数据
ret = ops->read_data(&data);
if (ret < 0) {
printk(KERN_ERR "framework: sensor read failed\n");
return ret;
}
printk(KERN_INFO "framework: sensor data = %d\n", data);
return 0;
}
// ============================================
// 第三部分:A厂商实现(具体硬件)
// ============================================
/*
* sensorA_init - A厂商传感器初始化
* 返回值: 0成功,负数失败
*/
int sensorA_init(void) {
printk(KERN_INFO "Sensor A: initializing...\n");
// 假设A厂商传感器需要配置GPIO
// gpio_request(18, "sensorA");
// gpio_direction_output(18, 1);
printk(KERN_INFO "Sensor A: init done\n");
return 0;
}
/*
* sensorA_read - A厂商传感器读取数据
* @data: 传出数据指针
* 返回值: 0成功,负数失败
*/
int sensorA_read(int *data) {
if (!data) {
printk(KERN_ERR "Sensor A: data pointer is NULL\n");
return -EINVAL;
}
// 模拟A厂商传感器读取数据
// 实际可能是读取GPIO或ADC值
*data = 25; // 假设读到温度25度
printk(KERN_INFO "Sensor A: read data = %d\n", *data);
return 0;
}
// ============================================
// 第四部分:B厂商实现(具体硬件)
// ============================================
/*
* sensorB_init - B厂商传感器初始化
* 返回值: 0成功,负数失败
*/
int sensorB_init(void) {
printk(KERN_INFO "Sensor B: initializing...\n");
// 假设B厂商传感器需要配置I2C
// i2c_set_clientdata(...);
// i2c_master_send(...);
printk(KERN_INFO "Sensor B: init done\n");
return 0;
}
/*
* sensorB_read - B厂商传感器读取数据
* @data: 传出数据指针
* 返回值: 0成功,负数失败
*/
int sensorB_read(int *data) {
if (!data) {
printk(KERN_ERR "Sensor B: data pointer is NULL\n");
return -EINVAL;
}
// 模拟B厂商传感器读取数据
// 实际可能是通过I2C读取
*data = 30; // 假设读到温度30度
printk(KERN_INFO "Sensor B: read data = %d\n", *data);
return 0;
}
// ============================================
// 第五部分:使用框架(模块入口)
// ============================================
/*
* 静态定义两个厂商的操作结构体
* 在编译时就确定了具体调用哪个函数
*/
static struct sensor_ops a_ops = {
.init = sensorA_init, // 指向A厂商的初始化函数
.read_data = sensorA_read, // 指向A厂商的读取函数
};
static struct sensor_ops b_ops = {
.init = sensorB_init, // 指向B厂商的初始化函数
.read_data = sensorB_read, // 指向B厂商的读取函数
};
/*
* sensor_test - 测试传感器功能
* @use_a: 1表示使用A厂商,0表示使用B厂商
*
* 演示运行时动态选择硬件
*/
void sensor_test(int use_a) {
struct sensor_ops *ops;
if (use_a) {
printk(KERN_INFO "Testing Sensor A...\n");
ops = &a_ops;
} else {
printk(KERN_INFO "Testing Sensor B...\n");
ops = &b_ops;
}
// 使用同一个框架函数,但调用不同的硬件实现
framework_get_data(ops);
}
// ============================================
// 第六部分:模块初始化和退出
// ============================================
/*
* module_init - 模块加载时执行
*/
static int __init sensor_module_init(void) {
printk(KERN_INFO "Sensor module init\n");
// 测试A厂商传感器
sensor_test(1);
// 测试B厂商传感器
sensor_test(0);
return 0;
}
/*
* module_exit - 模块卸载时执行
*/
static void __exit sensor_module_exit(void) {
printk(KERN_INFO "Sensor module exit\n");
}
// 指定模块的初始化和退出函数
module_init(sensor_module_init);
module_exit(sensor_module_exit);
// 模块信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Sensor framework demo with function pointers");
MODULE_VERSION("1.0");
linux应用程序和驱动程序相互关系
底层写驱动的对应的本质:为上层提供接口,函数接口,硬件的函数接口。
向上层应用工程师提供函数接口的方法,函数怎么写?怎么规定写法
在c语言中函数指针来限制函数的写法
在c++有语法可以对子类函数接口来进行限制,多态的场合写的纯虚函数 ,在抽象类里面,就是这个函数没有实现,留给子类实现,那它为什么不实现,要让子类实现?上课应该讲过那个王者荣耀那个类,那个类里面应该有技能那个函数,写英雄那个类的那个哥们,他知道技能怎么实现嘛?他不知道,他知道需要技能,但是他不知道怎么实现 ,那就存在这种问题。实现对应技能是由对应的子类去实现,加了对应的纯虚函数。
信号回调函数
cs
#include <signal.h>
typedef void (sighandler_t)(int); // void ()(int) ->sighandler_t
sighandler_t signal(int signum, sighandler_t handler);
void ()(int) signal (int signum,void (handler)(int)) //错误
void (* signal (int signum,void (*handler)(int)))(int)
总结:
我们写驱动就是给别人提供一种函数接口,给上层的工程师调用,但是上层工程师不知道我们今天给提供了什么接口函数,不知道函数名,调用不了,但是他必须要调用我们函数,这个时候能想到的就是函数指针。
还有整个内核在设计的时候,它肯定会让应用层的程序调用到底层驱动,又因为操作系统是驱动之前写好的,操作系统在写的时候应该已经考虑到了让应用层去调用底层驱动,应该有一套自己的设计,但是它不知道我们驱动写好之后,对应的驱动硬件操作函数叫啥。它不知道,因为我们现在写的,所以它这里也采用了函数指针,我们把对应的函数填充到函数指针中。