目录
ARM裸机代码和驱动的区别
Linux系统组成
内核五大功能
设备驱动分类
内核类型
驱动模块
驱动模块示例
Makefile配置
命令
编码辅助工具
内核中的打印函数
printk 函数
修改打印级别
编辑
打印级别含义
驱动多文件编译
示例
模块传递参数
命令行传递参数
支持的数据类型
module_param 函数
MODULE_PARM_DESC 函数
示例
module_param_array 函数
字符设备驱动
Linux系统中一切皆文件
字符设备驱动步骤
字符设备驱动的注册
编辑
字符设备驱动的注销
以下是本人学习时的一些笔记,对初入门的驱动可能会有一些帮助,希望可以帮到大家~
ARM裸机代码和驱动的区别
共同点:
- 都能够操作硬件。
不同点:
- 裸机编程是直接用C语言给寄存器写值。
- 驱动编程遵循一定的框架和规范,通过往寄存器写值来控制硬件。
- 裸机代码独立编译和执行,而驱动依赖于内核进行编译和执行。
- 裸机程序一次只能执行一个任务,而驱动可以支持并发执行多个任务。
- 裸机程序只需一个
main
函数即可,而驱动需要遵循内核的框架和流程。
Linux系统组成
- 用户空间(0-3G):每个进程独占0-3G的虚拟地址空间。
- 内核空间(3-4G):所有进程共享3-4G的虚拟地址空间。
- 系统调用:应用程序通过系统调用(软中断SWI)与内核交互。
内核五大功能
- 进程管理:负责进程的创建、销毁和调度。
- 文件管理:通过文件系统(如ext2/ext3/ext4/YAFFS/JFFS等)来管理文件。
- 网络管理:通过网络协议栈(如OSI/TCP/IP)处理数据包的封装和拆解。
- 内存管理:负责用户空间和内核空间内存的分配和回收。
- 设备管理:管理设备驱动,如字符设备、块设备和网络设备。
设备驱动分类
- 字符设备驱动:如LED、鼠标、键盘、LCD、触摸屏等。
- 按照字节为单位访问,支持顺序访问。
- 创建设备文件,通过
open
,read
,write
,close
等操作访问。
- 块设备驱动:如摄像头、U盘、eMMC等。
- 按照块(通常是512字节)访问,支持顺序和随机访问。
- 创建设备文件,通过
open
,read
,write
,close
等操作访问。
- 网络设备驱动:如网卡。
- 按照网络数据包进行收发。
内核类型
- 宏内核:将主要功能集成在一个内核中。
- 优点:运行效率高。
- 缺点:任何一个部分出错都可能导致整个内核崩溃。
- 示例:Ubuntu, Android
- 微内核:只包含最基本的功能,其他功能通过服务的形式在用户空间实现。
- 优点:更高的稳定性和安全性。
- 缺点:相对较低的运行效率。
- 示例:HarmonyOS, QNX
驱动模块
- 三要素:入口、出口、许可证。
- 入口:资源的申请。
- 出口:资源的释放。
- 许可证:通常使用GPL许可。
(__init可以不指定,及可以不写,但是正常是写的)
驱动模块示例
#include <linux/init.h>
#include <linux/module.h>
//__init将hello_init放到.init.text段中
static int __init hello_init(void) {
// 初始化函数
return 0;
}
//__exit将hello_exit放到.exit.text段中
static void __exit hello_exit(void) {
// 清理函数
}
//告诉内核驱动的入口地址(函数名为函数首地址)
module_init(hello_init);
//告诉内核驱动的出口地址
module_exit(hello_exit);
//许可证
MODULE_LICENSE("GPL");
Makefile配置
KERNELDIR := /lib/modules/$(shell uname -r)/build/
PWD := $(shell pwd)
all:
make -C $(KERNELDIR) M=$(PWD) modules
clean:
make -C $(KERNELDIR) M=$(PWD) clean
obj-m += hello.o
命令
- 安装驱动模块:
sudo insmod hello.ko
- 卸载驱动模块:
sudo rmmod hello
- 查看已加载的模块:
lsmod
- 查看内核消息:
dmesg
- 清空内核消息:
sudo dmesg -C
或sudo dmesg -c
- 持续查看内核消息:
sudo dmesg -w
编码辅助工具
- 创建索引文件:
ctags -R
- 在vi中跳转至标签:
ctrl + ]
和ctrl + t
Ubuntu内核所对应的内核路径
内核中的打印函数
printk 函数
-
函数原型:
printk(打印级别 "内容")
-
示例:
printk(KERN_ERR "Fail%d", a); printk(KERN_ERR "%s:%s:%d\n", __FILE__, __func__, __LINE__); printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
-
查看内核打印级别:
vi -t KERN_ERR
-
内核打印级别定义:
#define KERN_EMERG "<0>" /* system is unusable */ #define KERN_ALERT "<1>" /* action must be taken immediately */ #define KERN_CRIT "<2>" /* critical conditions */ #define KERN_ERR "<3>" /* error conditions */ #define KERN_WARNING "<4>" /* warning conditions */ #define KERN_NOTICE "<5>" /* normal but significant condition */ #define KERN_INFO "<6>" /* informational */ #define KERN_DEBUG "<7>" /* debug-level messages */
-
打印级别范围:
- 从
<0>
到<7>
,<0>
为最高级别,<7>
为最低级别。
- 从
修改打印级别
-
查看当前打印级别:
cat /proc/sys/kernel/printk
-
打印级别的含义:
- 第一个数字:终端的级别。
- 第二个数字:消息的默认级别。
- 第三个数字:终端的最大级别。
- 第四个数字:终端的最小级别。
-
修改系统默认的级别:
su root echo 4 3 1 7 > /proc/sys/kernel/printk
-
添加修改级别命令:
echo 4 3 1 7 > /proc/sys/kernel/printk
打印级别含义
- 终端的级别:只有当消息的级别大于或等于终端级别时,消息才会在终端上显示。
- 消息的默认级别:如果没有特别指定,消息将采用此级别。
- 终端的最大级别:终端可以显示的最高级别。
安装驱动和卸载驱动时,消息会打印。
驱动多文件编译
示例
-
文件列表:
hello.c
add.c
-
Makefile:
obj-m := demo.o demo-y += hello.o add.o
-
说明:
-y
作用:将hello.o
和add.o
文件合并到demo.o
中。- 最终生成:
demo.ko
文件。
模块传递参数
命令行传递参数
- 命令示例:
sudo insmod demo.ko hello world
支持的数据类型
- 标准类型:
byte
,short
,ushort
,int
,uint
,long
,ulong
charp
: 字符串指针bool
: 布尔值,接受0/1
,y/n
,Y/N
invbool
: 布尔值,接受0/1
,y/n
,Y/N
,但意义相反(N
表示真)
module_param 函数
- 函数原型:
module_param(name, type, perm);
- 参数:
name
: 变量的名字。type
: 变量的类型。perm
: 权限,如0664
,0775
。
MODULE_PARM_DESC 函数
- 函数原型:
MODULE_PARM_DESC(_parm, desc);
- 参数:
_parm
: 变量。desc
: 描述字段。
示例
-
命令行参数:
sudo insmod hello.ko a=20 b=30 c=65 p="hello_world"
-
注意事项:
- 传递字符时使用 ASCII 码值。
- 传递字符串时不能包含空格。
module_param_array 函数
- 函数原型:
module_param_array(name, type, nump, perm);
- 参数:
name
: 数组名。type
: 数组的类型。nump
: 参数的个数,变量的地址。perm
: 权限。
练习:
1.byte类型如何使用 (传递参数用ascii)
2.如何给一个指针传递一个字符串
- 命令行参数:
sudo insmod hello.ko a=121 b=10 c=65 p="hello" ww=1,2,3,4,5
传参成功~
字符设备驱动
Linux系统中一切皆文件
-
应用层:
fd = open("led驱动的文件", O_RDWR); read(fd); write(fd); close(fd);
-
内核层:
- 驱动文件:
led_driver.c
- 驱动函数:
driver_open(); driver_read(); driver_write(); driver_close();
- 驱动文件:
-
结构体定义:
struct file_operations { int (*open)(struct inode *, struct file *); ssize_t (*read)(struct file *, char __user *, size_t, loff_t *); ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *); int (*release)(struct inode *, struct file *); ;
-
设备号:
- 32位无符号数字:
- 高12位:主设备号,用于区分设备类别。
- 低20位:次设备号,用于区分同一类别中的不同设备。
- 32位无符号数字:
-
硬件层:
- LED、UART、ADC、PWM 等设备。
字符设备驱动步骤
- 注册字符设备驱动 - 得到一个字符设备驱动的框架,并获得设备号。
- 确定操作的硬件设备 - 如 LED 灯(初始化灯)。
- 初始化灯 - 建立灯实际物理地址和虚拟地址之间的映射。
- 用户空间与内核空间数据交互 - 当用户使用时,驱动会被真正运行,涉及数据交互。
- 在应用层创建设备文件(设备节点)。
字符设备驱动的注册
-
函数原型:
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
-
参数:
major
:主设备号。- 如果填写的值大于0,它认为这个就是主设备号。
- 如果填写的值为0,操作系统会分配一个主设备号。
name
:设备名称。fops
:操作方法结构体。
-
返回值:
major > 0
:成功返回0,失败返回错误码(负数)。major = 0
:成功返回分配的主设备号,失败返回错误码(负数)。
-
查看设备信息:
cat /proc/devices
字符设备驱动的注销
-
函数原型:
void unregister_chrdev(unsigned int major, const char *name);
-
参数:
major
:主设备号。name
:设备名称。
-
返回值:无。