linux内核驱动开发

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)

总结:

我们写驱动就是给别人提供一种函数接口,给上层的工程师调用,但是上层工程师不知道我们今天给提供了什么接口函数,不知道函数名,调用不了,但是他必须要调用我们函数,这个时候能想到的就是函数指针。

还有整个内核在设计的时候,它肯定会让应用层的程序调用到底层驱动,又因为操作系统是驱动之前写好的,操作系统在写的时候应该已经考虑到了让应用层去调用底层驱动,应该有一套自己的设计,但是它不知道我们驱动写好之后,对应的驱动硬件操作函数叫啥。它不知道,因为我们现在写的,所以它这里也采用了函数指针,我们把对应的函数填充到函数指针中。

相关推荐
心心喵2 小时前
[linux] nohup和pm2的区别 进程保活
linux·运维·服务器
NGINX开源社区3 小时前
F5 NGINX Ingress Controller 5.3.0 新增功能
运维
醉熏的石头3 小时前
Ubuntu 中的编程语言(中)
linux·ubuntu·scala
SelectDB5 小时前
Apache Doris 在 AgentLogsBench 中领先,支撑 Agent 可观测性生产负载
运维·数据库·agent
荣-5 小时前
从两天到十几分钟:一套 YT Crash 自动化分析工具完整工程复盘
大数据·运维·自动化
酷炫的水壶6 小时前
使用memc-nginx和srcache-nginx模块构建高效透明的缓存机制
运维·nginx·缓存
dddwjzx6 小时前
嵌入式Linux C应用编程入门——信号
linux·嵌入式
范什么特西7 小时前
网络代理问题
java·linux·服务器
姚不倒7 小时前
F5 SSL Profile 证书卸载深入篇
运维·网络协议·负载均衡·ssl·f5