Linux设备驱动开发笔记
一、硬件基础
1. 存储器类型
RAM(随机存储器)
-
特点:访问速度快、容量小、运行程序使用、掉电数据丢失、可线性访问
-
种类:
-
SRAM:静态RAM,速度快,成本高
-
DRAM:动态RAM,需要刷新
-
SDRAM:同步DRAM
-
DDRn:双倍数据速率SDRAM
-
ROM(只读存储器)
-
特点:访问速度慢、容量大、存储文件、掉电数据不丢失、大部分不可线性访问
-
种类:
-
PROM:可编程ROM
-
EPROM:可擦除可编程ROM
-
EEPROM:电可擦除可编程ROM
-
Flash/emmc
-
访问速率快
-
掉电数据不丢失
-
常用作存储设备
二、Linux启动流程
1. 总体流程对比
Windows: BIOS -> Windows -> 桌面
Linux: bootloader -> linux kernel -> rootfs
2. Linux启动三阶段
(1) Bootloader阶段
主要任务:
-
初始化CPU(设置工作模式)
-
初始化异常向量表
-
初始化堆栈
-
关中断
-
关看门狗(防止自动重启)
-
关Cache
-
关闭MMU(将虚拟地址转为物理地址)
-
初始化内存
-
初始化相关设备(串口、网卡)
-
集成相关协议
-
搬移内核到内存,向内核传参(控制台、根文件系统类型/位置)
-
启动内核
-
移交CPU控制权给内核
(2) Linux内核阶段
五大管理功能:
-
文件管理
-
进程管理
-
网络管理
-
内存管理
-
设备管理
启动过程:
-
内核启动到最后阶段加载(挂载)根文件系统
-
内核init进程退化为用户init进程
-
流程:
init(用户) -> services -> shell -> userapp
(3) 根文件系统
包含内容:
-
系统命令
-
启动脚本(系统服务、安装的应用程序)
-
配置文件
-
应用程序(工具、用户程序)
-
普通文件(文本、mp3、mp4)
3. imx6开发板启动方式
SD卡启动
-
系统上电,执行imx6内部ROM中的启动程序
-
根据boot mode选择对应外设
-
拷贝SD卡中bootloader前半部分到imx6内部RAM
-
bootloader初始化内存,并将自己后半部分搬移到内存执行
-
bootloader搬移SD卡中的内核(zImage)到内存0x80800000
-
PC指向0x80800000启动内核
-
内核挂载SD卡上的根文件系统
网络启动(内核和根文件系统在ubuntu上)
-
bootloader通过tftp下载ubuntu中的zImage到内存0x80800000
-
bootloader引导内核启动
-
内核通过nfs挂载ubuntu中的rootfs
内存地址布局:
0地址 <- 起始地址
0x80000000 <- RAM起始
0x80800000 <- 内核加载地址
三、Ubuntu环境准备
1. TFTP服务配置
用途 :传输内核镜像zImage和设备树文件dtb
步骤:
-
安装tftp服务
-
配置服务目录
-
拷贝zImage和xxx.dtb到tftp服务目录
2. NFS服务配置
用途 :通过网络挂载根文件系统
步骤:
-
安装nfs服务
-
配置服务目录
-
解压根文件系统到nfs目录
sudo tar -xvf rootfs.tar.bz2
3. 开发板挂载命令
# 在开发板上执行
mount -o nolock,nfsvers=3 192.168.1.3:/home/linux/nfs /mnt
四、U-Boot常用命令
基本命令
-
help/?:查看uboot支持的命令 -
reset:uboot阶段重启命令 -
ping:测试网络 -
printenv:打印环境变量 -
setenv name value:设置环境变量(字符串类型) -
setenv name:删除环境变量(值设为空) -
saveenv:保存环境变量(通常保存到MMC)
网络相关环境变量
# 设置开发板IP
setenv ipaddr 192.168.1.100
# 设置MAC地址
setenv ethaddr xx:xx:xx:xx:xx:xx
# 设置TFTP服务器IP
setenv serverip 192.168.1.3
TFTP下载命令
# 下载内核到内存
tftp 0x80800000 zImage
# 下载设备树到内存
tftp 0x83000000 imx6.dtb
启动内核命令
# 设置启动参数
setenv bootargs console=ttymxc0,115200 root=/dev/nfs \
nfsroot=192.168.1.3:/home/linux/nfs/imx6/rootfs,nfsvers=3 \
ip=192.168.1.100 init=/linuxrc
# 启动内核
bootz 0x80800000 - 0x83000000
启动参数解释:
-
console:指定Linux控制台 -
root:根文件系统类型为nfs -
nfsroot:nfs文件系统路径 -
ip:Linux内核启动阶段使用的IP地址 -
init:指定init进程(1号进程)
五、内核编译
1. 配置系统
-
Kconfig:定义make menuconfig中的配置选项 -
.config:存储配置结果 -
Makefile:根据配置编译对应模块
Makefile示例:
OBJ-$(CONFIG_MAIN) += main.o
OBJ-$(CONFIG_FUN1_MEMORY) += fun1.o
OBJ-$(CONFIG_FUN2_NET) += fun2.o
$(TARGET):$(OBJ)
gcc $^ -o $@
.config示例:
CONFIG_MAIN=y
CONFIG_FUN1_MEMORY=y
CONFIG_FUN2_NET=y
2. 内核编译步骤
# 1. 解压内核源码
sudo tar -xvf linuxxxxxx.tar.gz
# 2. 修改权限
sudo chmod 0777 linuxxxxxx -R
# 3. 导入默认配置
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_alientek_emmc_defconfig
# 4. 图形化配置
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# 5. 编译内核
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16
3. 内核镜像类型
-
Image:可以直接执行的内核镜像
-
zImage:解压程序 + Image的压缩包
-
uImage:64字节头信息 + zImage
4. 向内核添加新文件
以向drivers/char/添加demo.c为例:
步骤1 :创建并编辑drivers/char/demo.c
步骤2 :修改drivers/char/Makefile
obj-$(CONFIG_DEMO) += demo.o
步骤3 :修改drivers/char/Kconfig
config DEMO
tristate "Demo driver"
help
This is a demo driver.
步骤4:配置和编译
make menuconfig # 配置DEMO选项
make zImage # 编译内核
六、设备驱动分类
1. 字符设备驱动
-
数据按字节流访问(顺序性访问)
-
90%以上的设备都是字符设备
-
示例:LED、按键、串口
2. 块设备驱动
-
数据访问可以是随机的
-
一般是存储设备
-
示例:硬盘、SD卡
3. 网络设备驱动
-
集成复杂的协议栈
-
没有设备号(按名字维护)
-
示例:网卡
七、字符设备驱动开发
1. 驱动程序实现要素
-
硬件操作方法(open、read、write、close)
-
申请驱动模块对应的设备号
-
向系统注册驱动模块
2. 设备号
-
无符号32位整数
-
高12位:主设备号,代表设备类型(功能)
-
低20位:次设备号,区分同类不同设备
3. 开发工具ctags
# 在内核源码目录生成索引
ctags -R
# 使用快捷键
Ctrl + ] # 跳转到符号定义
Ctrl + o # 返回
4. 手动创建设备节点
# 格式:mknod 设备名 类型 主设备号 次设备号
mknod /dev/demo1 c 255 0
# 查看已加载驱动的主设备号
cat /proc/devices
八、字符设备驱动模板
1. 标准字符设备驱动
cpp
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
static struct file_operations fops;
static dev_t dev;
static struct cdev cdev;
static struct class *cls;
static int __init demo_init(void)
{
// 1. 分配设备号
dev = MKDEV(255, 0);
register_chrdev_region(dev, 1, "demo");
// 2. 初始化cdev结构
cdev_init(&cdev, &fops);
// 3. 添加cdev到系统
cdev_add(&cdev, dev, 1);
// 4. 创建设备类
cls = class_create(THIS_MODULE, "demo_class");
// 5. 创建设备节点
device_create(cls, NULL, dev, NULL, "demo1");
return 0;
}
static void __exit demo_exit(void)
{
// 清理顺序与初始化相反
device_destroy(cls, dev);
class_destroy(cls);
cdev_del(&cdev);
unregister_chrdev_region(dev, 1);
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
2. 杂项设备驱动(简化版)
cpp
#include <linux/miscdevice.h>
static struct file_operations fops;
static struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR, // 自动分配次设备号
.name = "demo", // 设备名
.fops = &fops // 文件操作
};
static int __init demo_init(void)
{
return misc_register(&misc_dev);
}
static void __exit demo_exit(void)
{
misc_deregister(&misc_dev);
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
九、内核模块编译
1. 配置为模块
-
修改Kconfig,将模块类型设为
tristate -
make menuconfig,将对应模块配置为M -
make modules编译模块
2. 模块操作命令
# 加载模块
insmod xxx.ko
# 卸载模块
rmmod xxx
# 查看已加载模块
lsmod
十、ioctl命令结构
cmd参数是32位整数,包含以下信息:
| 位段 | 名称 | 大小 | 说明 |
|---|---|---|---|
| 31-24 | type | 8 bits | 设备类型 |
| 23-16 | nr | 8 bits | 设备内的命令编号 |
| 15-14 | dir | 2 bits | 数据流向(读/写) |
| 13-0 | size | 14 bits | 参数的大小 |
定义宏:
#define _IO(type,nr) // 无参数命令
#define _IOR(type,nr,size) // 读命令
#define _IOW(type,nr,size) // 写命令
#define _IOWR(type,nr,size) // 读写命令
十一、设备驱动与设备资源
1. 总线类型
-
bus_type:总线类型基类 -
具体总线:i2c、spi、platform等
2. 设备资源匹配流程
设备树节点(compatible属性)
↓
驱动匹配表(compatible属性)
↓
匹配成功 → 执行probe函数
↓
在probe中注册字符设备驱动
↓
设备可用
3. Platform设备结构
struct platform_device {
const char *name; // 设备名,可用于匹配
int id;
struct device dev;
u32 num_resources;
struct resource *resource; // 设备资源
const struct platform_device_id *id_entry;
};
4. 资源结构体
struct resource {
resource_size_t start; // 资源起始地址
resource_size_t end; // 资源结束地址
const char *name; // 资源名称
unsigned long flags; // 资源标志
};
5. Platform驱动结构
struct platform_driver {
int (*probe)(struct platform_device *); // 设备探测
int (*remove)(struct platform_device *); // 设备移除
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver; // 包含name属性
const struct platform_device_id *id_table; // 匹配表
};
6. 平台驱动注册
// 注册宏
#define platform_driver_register(drv) \
__platform_driver_register(drv, THIS_MODULE)
// 注册函数
int __platform_driver_register(struct platform_driver *, struct module *);
// 注销函数
void platform_driver_unregister(struct platform_driver *);
十二、设备树(Device Tree)
1. 设备树文件类型
-
.dts:设备树源文件(可编辑)
-
.dtb:设备树编译后的二进制文件(内核解析)
2. 编译命令
make dtbs # 编译所有设备树
make pt.dtb # 编译pt.dts为pt.dtb
3. 设备树匹配机制
匹配条件:
-
设备树节点中有
compatible属性 -
驱动程序中有相同的
compatible属性 -
两者匹配成功则执行驱动的
probe函数
备用匹配 :如果没有compatible,可以用name匹配
十三、GPIO子系统
Linux内核提供的统一GPIO操作接口:
1. 常用函数
// 申请GPIO
int gpio_request(unsigned gpio, const char *label);
// 配置为输出
int gpio_direction_output(unsigned gpio, int value);
// 配置为输入
int gpio_direction_input(unsigned gpio);
// 设置输出值
void gpio_set_value(unsigned gpio, int value);
// 获取输入值
int gpio_get_value(unsigned gpio);
// 释放GPIO
void gpio_free(unsigned gpio);
2. 使用流程
// 1. 获取GPIO编号
int gpio_num = of_get_named_gpio(dev->of_node, "led-gpio", 0);
// 2. 申请GPIO
ret = gpio_request(gpio_num, "led");
// 3. 配置为输出
gpio_direction_output(gpio_num, 0);
// 4. 控制GPIO
gpio_set_value(gpio_num, 1); // 高电平
gpio_set_value(gpio_num, 0); // 低电平
// 5. 释放GPIO(在remove函数中)
gpio_free(gpio_num);