一、字符设备驱动要素
a. 设备号:用于在内核中,众多的设备驱动进行区分
b. 设备节点(设备文件):用户须知道设备驱动对应到哪个设备节点
c. 设备驱动进行操作:对文件进行操作,应用空间操作open、read、write等文件IO时,实际上驱动代码中对应执行的open、read、write等函数
二、开发流程
1.编译驱动模块代码(使用makefile)
KERNEL_PATH=/home/yky/Code/linux-3.14-fs4412 #内核中的Makefile需要配置交叉编译工具链
obj-m += 模块文件名.o #要编译为模块的文件
all:
make modules -C $(KERNEL_PATH) M=$(shell pwd) #借助已经编译好的内核,编译模块
# -C 指定内核路径
# M:当前模块的位置
2.编写驱动模块代码
1)符号导出
如果模块内容在其他模块中使用,可以使用导出声明关键字EXPORT_SYMBOL(内容名字); 注:如果模块只用于进行导出,可以不写入口声明以及定义
调用后内核模块中的函数或变量被导出到内核的全局符号表中,其他内核模块可以通过该符号表来访问导出的函数或变量。可用root用户使用:cat /proc/kallsyms:该命令将输出内核符号表中的所有符号(包括已导出和未导出的符号)以及它们的地址等信息,如果结果中包含 :
T:已导出的函数;
D:已导出的全局变量。
U:未定义符号(Undefined)。这些符号在内核中没有定义,可能是外部符号,需要在链接时解析。
B: BSS 段(Block Started by Symbol)。这些符号对应的是未初始化的全局变量,它们在程序执行之前都会被初始化为零。
A:绝对符号(Absolute)。这些符号在链接时被分配了一个固定的地址,不会被重定位。
R:表示只读数据段(Read-only Data)。这些符号对应的是只读的全局变量或常量,它们存储在只读的数据段中。
W:表示可写数据段(Writeable Data)。这些符号对应的是可写的全局变量,它们存储在可写的数据段中。
导出实例:
// 在内核模块中定义一个全局变量
int global_var = 42;
// 使用 EXPORT_SYMBOL 导出这个全局变量,以便其他内核模块可以使用
EXPORT_SYMBOL(global_var);
注意事项:
- 只能将全局变量、函数以及部分静态化的内容导出给其他内核模块使用,因为内核模块是在全局范围内存在的。
- 如果导出的符号是一个函数,那么其他模块必须具有适当的函数原型(prototype)以便正确使用。
- 必须小心使用 EXPORT_SYMBOL,因为这些符号会影响内核中的命名空间,不正确使用可能会导致命名冲突或者符号暴露给不应该使用它的模块。
2)参数传递
驱动变量可以通过insmod命令插入时指定(输入参数) 例如:insmod perm.ko a=10 b=5 p="okokok"
驱动中如何处理参数传递 module_param(name,type,perm);
MODULE_PARM_DESC(param3, "Description for param3"); 作用:给参数添加描述信息 须在参数解析后使用,添加后可在modinfo中查看
实例:
插入命令 insmod ShyShy_module.ko param1=42 param2="custom_value" param3=1
参数1:参数的名字,变量名参数2:参数的类型,int,char
参数3:/sys/modules 文件的权限,0666
例如:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
static int param1 = 0;
static char* param2 = "default_value";
static bool param3 = false;
module_param(param1, int, 0644);
MODULE_PARM_DESC(param1, "Description for param1"); // 给参数添加描述信息
module_param(param2, charp, 0644);
MODULE_PARM_DESC(param2, "Description for param2");
module_param(param3, bool, 0644);
MODULE_PARM_DESC(param3, "Description for param3");
//模块加载入口函数实现
static int __init ShyShy_module_init(void) {
printk(KERN_INFO "Module loaded with param1=%d, param2=%s, param3=%d\n", param1, param2, param3);
// Module initialization code here
return 0;
}
//模块卸载入口函数实现,主要用于资源释放、删除驱动
static void __exit ShyShy_module_exit(void) {
printk(KERN_INFO "Module unloaded\n");
// Module cleanup code here
}
module_init(ShyShy_module_init); //加载调用加载入口函数
module_exit(ShyShy_module_exit); //卸载调用卸载入口函数
MODULE_LICENSE("GPL"); //license 声明
MODULE_AUTHOR("CanCanNeedShyShy"); //模块名称
MODULE_DESCRIPTION("Your module description"); //模块描述
3)设备号注册、注销
a. 注册字符设备
定义: int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
参数:
major: 新的字符设备的主设备号;
name: 新的字符设备名称;
fops: 设备驱动程序定义的文件操作函数结构体,如读写函数、打开关闭函数等。
返回值:
0成功,否则失败
b.注销字符设备
定义: void unregister_chrdev(unsigned int major, const char *name);
参数:
major:表示要注销的字符设备的主设备号,
name:表示要注销的字符设备名称。 注:name 的字符串必须与之前在注册字符设备时所用的字符串完全一致。
实例:
#include <linux/fs.h> // for register_chrdev#include "ShyShydev.h"
int ShyShydev_major = 0; // 变量用于存储分配到的设备号
//存储字符设备的操作函数指针
struct file_operations ShyShydev_ops = {
.owner = THIS_MODULE,
.read = ShyShydev_read, // 指向读函数
.write = ShyShydev_write, // 指向写函数
};
int ShyShydev_init(void)
{
// 调用 register_chrdev 函数以分配设备号并注册设备
ShyShydev_major = register_chrdev(0, "ShyShydev", &ShyShydev_ops);
if (ShyShydev_major < 0) {
printk("ShyShydev registration failed\n");
return ShyShydev_major;
}
printk("ShyShydev module loaded with device major number %d\n", ShyShydev_major);
return 0;
}
void ShyShydev_exit(void)
{
(ShyShydev_major, "ShyShydev"); //注销设备号
printk("ShyShydev module unloaded\n");
}
MODULE_AUTHOR("ShyShy");
MODULE_DESCRIPTION("A sample driver");
MODULE_LICENSE("GPL");
module_init(ShyShydev_init);
module_exit(ShyShydev_exit);
4)设备节点注册与注销
a.手动创建
命令创建: mknod /dev/ShyShydevice c 主设备号 次设备号
使用udev规则通过配置文件创建:
通常文件在/etc/udev/rules.d/下,文件名一般为90-ShyShydevice.rules(设备号-名称)
文件内容为:KERNEL=="ShyShydevice", MODE="0666", GROUP="users"
保存后执行: udevadm control --reload; udevadm trigger
b.调用函数创建
class_create : Linux 内核中用于创建设备类(struct class)的函数,用于在/sys/class目录下创建设备类目录,并提供相应的 sysfs 接口,使得可以通过用户空间工具来管理和控制设备类的相关属性。
定义:
struct class *class_create(struct module *owner, const char *name);
参数:
owner:指向所属模块的指针。通常情况下,可以使用 THIS_MODULE,表示当前使用该函数的模块。
name:字符串,表示设备类的名称。该名称将成为 /sys/class 目录下的一个子目录,用于存放该设备类的相关信息。
返回值:
成功返回一个指向新创建设备类对象的指针(struct class *);失败返回ERR_PTR
device_create:函数用于在 sysfs 中创建一个设备节点,这个函数可以让内核动态地创建一个设备对象,并将其添加到 sysfs 中相应的位置。
定义:
struct device *device_create(struct class *class, struct device *parent,dev_t devt,
void *drvdata, const char *fmt, ...);
参数:
struct class *class:指向设备对象所属的设备类的指针。
struct device *parent:指向设备对象的父设备对象的指针,如果没有父设备对象,则为 NULL。
dev_t devt:表示设备文件的主设备号和次设备号。
void *drvdata:设备对象和对应设备驱动程序之间交互数据时使用的指针。如果没有需要传递给设备驱动程序的数据,可以设置为 NULL。
const char *fmt, ...:格式化字符串,需要根据 printf 函数的规则进行传参。这个字符串指定了设备对象的名称。
返回值:
成功返回指向创建的设备对象的指针,失败返回一个错误指针
c.注销设备节点
class_destory:销毁设备类。注:在调用 class_destroy 函数之前,必须确保没有任何设备对象存在,否则可能会导致未定义的行为
void class_destroy(struct class *class);
device_destory:函数将销毁指定设备号对应的设备对象,并从设备类的设备列表中移除此设备。同时,从 sysfs 中删除该设备对应的节点,释放相关资源。
定义:
void device_destroy(struct class *class, dev_t devt);
参数:
class:指向包含要销毁设备对象的设备类的指针。
devt:表示要销毁的设备的设备号。
实例:
#include <linux/init.h>#include <linux/module.h>
#include <linux/device.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
static struct class *ShyShy_class;
static struct device *ShyShy_device;
static int __init ShyShy_init(void)
{
int result;
// Create a class for the device
ShyShy_class = class_create(THIS_MODULE, "ShyShy_class");
if (IS_ERR(ShyShy_class)) {
pr_err("Failed to create class\n");
return PTR_ERR(ShyShy_class);
}
// Create a device and add it to sysfs
ShyShy_device = device_create(ShyShy_class, NULL, MKDEV(0, 0), NULL, "ShyShy_device");
if (IS_ERR(ShyShy_device)) {
pr_err("Failed to create device\n");
result = PTR_ERR(ShyShy_device);
goto class_destroy;
}
pr_info("ShyShy module loaded\n");
return 0;
class_destroy:
class_destroy(ShyShy_class);
return result;
}
static void __exit ShyShy_exit(void)
{
// Destroy the device and class
device_destroy(ShyShy_class, MKDEV(0, 0));
class_destroy(ShyShy_class);
pr_info("ShyShy module unloaded\n");
}
module_init(ShyShy_init);
module_exit(ShyShy_exit);
5)硬件初始化
a.控制外设
ioremap 函数是用于建立内核虚拟地址与物理地址之间映射关系的函数,即将给定的物理地址范围映射到内核虚拟地址空间中,并返回映射后的起始地址。
定义: void __iomem *ioremap(phys_addr_t offset, size_t size);
参数:
offset:物理地址的偏移量
size:映射大小
返回值:
返回映射后的内核虚拟地址,该地址是 void __iomem * 类型
iounmap 解除映射,释放相应的资源。
定义: void iounmap(void __iomem *addr);
参数:
addr:建立映射关系是返回的内核虚拟地址指针
b.操作寄存器地址
·通过指针取内容(*)
·IO内存访问函数:iowrite8、writew、rite16、writel、ioread8、ioread16、ioread32等,用于写入、读取地址内容
定义:void iowrite8(u8 value, void __iomem *addr);
void iowrite16(u16 value, void __iomem *addr);
void iowrite32(u32 value, void __iomem *addr);
u8 ioread8(void __iomem *addr);
u16 ioread16(void __iomem *addr);
u32 ioread32(void __iomem *addr);
实例:
#include <linux/module.h>#include <linux/init.h>
#include <linux/io.h>
#define DEVICE_BASE_ADDR 0x10000000 // 假设设备基地址为 0x10000000
static void __iomem *device_reg; // 定义设备寄存器的指针
static int __init io_mem_example_init(void)
{
// 1. 内存映射
device_reg = ioremap(DEVICE_BASE_ADDR, sizeof(u32));
if (!device_reg) {
printk(KERN_ERR "Failed to remap device memory\n");
return -ENOMEM;
}
// 2. 向设备寄存器写入值
u32 value_to_write = 0xDEADBEEF;
iowrite32(value_to_write, device_reg);
// 3. 从设备寄存器读取值
u32 value_read = ioread32(device_reg);
printk(KERN_INFO "Value read from device register: 0x%X\n", value_read);
return 0;
}
static void __exit io_mem_example_exit(void)
{
// 4. 解除内存映射
iounmap(device_reg);
}
module_init(io_mem_example_init);
module_exit(io_mem_example_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ShyShy");
MODULE_DESCRIPTION("Example of IO memory access in Linux kernel");
6)驱动文件操作
a.在驱动中实现文件io操作
字符设备是一种与字符流进行交互的设备,例如终端、串口等。file_operations 结构体包含了一系列函数指针,这些函数指针定义了对字符设备进行操作的方法。
实例:
#include <linux/fs.h>// 定义字符设备驱动程序的操作集合
static const struct file_operations ShyShy_fops = {
.open = ShyShy_open, // 指向打开设备的方法
.release = ShyShy_release, // 指向释放设备的方法
.read = ShyShy_read, // 指向读取数据的方法
.write = ShyShy_write, // 指向写入数据的方法
.llseek = ShyShy_llseek, // 指向移动文件指针的方法
// 其他成员的初始化
};
b.系统调用文件io操作
open() 、read() 、write()等
7)用户应用程序与内核驱动间的数据传输
a.将内核驱动空间拷贝数据给用户应用空间
copy_to_user:将数据从内核空间复制到用户空间
定义: unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
参数:
to:目标用户空间地址的指针,表示要将数据复制到的位置。
from:源内核空间地址的指针,表示要复制的数据的起始位置。
n:要复制的数据的字节数。
返回值:
成功返回0,失败返回复制失败的字节数
b.将用户应用空间拷贝数据给内核驱动空间
copy_from_user:将数据从用户空间复制到内核空间
定义: unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
参数:
to:目标内核空间地址的指针,表示要将数据复制到的位置。
from:源用户空间地址的指针,表示要复制的数据的起始位置。
n:要复制的数据的字节数。
返回值:
成功返回0,失败返回复制失败的字节数
c.应用程序编写
#include <stdio.h>#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define DEVICE_FILE "/dev/ShyShydevice"
int main() {
int fd;
char write_buffer[] = "Hello from user space";
char read_buffer[100];
// 打开设备文件
fd = open(DEVICE_FILE, O_RDWR);
if (fd < 0) {
perror("Failed to open the device file");
return -1;
}
// 向设备写入数据
if (write(fd, write_buffer, sizeof(write_buffer)) < 0) {
perror("Failed to write to the device");
close(fd);
return -1;
}
// 从设备读取数据
if (read(fd, read_buffer, sizeof(read_buffer)) < 0) {
perror("Failed to read from the device");
close(fd);
return -1;
}
// 输出读取到的数据
printf("Data read from device: %s\n", read_buffer);
// 关闭设备文件
close(fd);
return 0;
}
8)注意事项
a.加载入口实现资源申请,需要在卸载入口实现资源释放
b.在某个位置出错,要将之前申请的资源进行释放