《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》 第 1 章 Linux 设备驱动概述及开发环境搭建
《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》
第 1 章 Linux 设备驱动概述及开发环境搭建
参考:宋宝华 著,机械工业出版社,2015年版
1.1 设备驱动的作用
1.1.1 驱动程序的本质
设备驱动程序(Device Driver)是操作系统内核与硬件设备之间的软件接口层。其本质是一组函数的集合,负责完成对硬件的初始化、数据读写、中断处理及资源管理等工作。
宋宝华在书中将设备驱动的作用概括为:
“驱动程序是硬件与软件之间的桥梁,它屏蔽了硬件的细节,使得上层软件可以通过统一的接口访问不同的硬件设备。”
从软件工程角度看,驱动程序实现了硬件无关性:无论底层是 UART、SPI、I2C 还是 USB 接口的设备,应用程序都通过相同的 open()、read()、write()、ioctl() 接口访问,驱动程序完成从标准接口到具体硬件操作的映射。
1.1.2 驱动程序的地位
任何计算机系统的运行都是系统硬件和软件共同作用的结果。没有硬件的软件是空中楼阁,没有软件的硬件只是一堆废铁。硬件是底层基础,是所有软件得以运行的平台;软件则是对硬件的操作和管理。
驱动程序在整个计算机软件体系中处于最底层,直接与硬件打交道,是硬件和上层软件之间的纽带。
┌──────────────────────────────────────┐
│ 应用软件(Application) │
├──────────────────────────────────────┤
│ 操作系统(OS Kernel) │
├──────────────────────────────────────┤
│ 设备驱动程序(Driver) ← 本书重点
├──────────────────────────────────────┤
│ 硬件(Hardware) │
└──────────────────────────────────────┘
1.1.3 驱动程序的功能
设备驱动程序主要完成以下功能:
-
对设备初始化和释放:在设备使用前完成硬件初始化(寄存器配置、时钟使能、复位等),在设备不再使用时释放相关资源。
-
对设备进行管理:包括实时监控设备的运行状态,处理设备的错误和异常。
-
提供对设备的操作接口:向上层提供统一的
open()、close()、read()、write()、ioctl()等操作接口,使应用程序无需关心底层硬件细节。 -
处理设备产生的中断:注册中断处理函数,响应硬件中断,完成数据传输或状态更新。
-
与内核其他子系统协作:与内存管理、进程调度、电源管理等内核子系统协同工作。
1.2 无操作系统时的设备驱动
1.2.1 裸机驱动的概念
在没有操作系统的嵌入式系统(裸机,Bare Metal)中,驱动程序以函数库的形式存在,由应用程序直接调用,没有操作系统的介入和管理。
这类系统通常运行在资源极为有限的单片机上,如 STM32、AVR、PIC 等,整个软件系统只有两层:
┌──────────────────────────────────────┐
│ 应用程序(main 函数) │
│ 直接调用驱动函数:LED_On()、 │
│ UART_Send()、SPI_Read() 等 │
└──────────────────┬───────────────────┘
│ 直接调用(无系统调用)
┌──────────────────▼───────────────────┐
│ 硬件驱动函数库 │
│ 直接操作寄存器地址 │
│ #define GPIOA_ODR (*(uint32_t*)0x40020014) │
└──────────────────┬───────────────────┘
│ 读写寄存器
┌──────────────────▼───────────────────┐
│ 硬件 │
│ GPIO / UART / SPI / I2C / ADC │
└──────────────────────────────────────┘
1.2.2 裸机驱动的典型案例
案例一:STM32 GPIO 驱动(LED 控制)
/*
* 裸机 GPIO 驱动示例(STM32F103)
* 直接操作寄存器,无任何操作系统介入
*/
/* 寄存器地址定义 */
#define RCC_BASE 0x40021000
#define GPIOC_BASE 0x40011000
#define RCC_APB2ENR (*(volatile uint32_t *)(RCC_BASE + 0x18))
#define GPIOC_CRH (*(volatile uint32_t *)(GPIOC_BASE + 0x04))
#define GPIOC_ODR (*(volatile uint32_t *)(GPIOC_BASE + 0x0C))
/* LED 连接在 PC13 */
void LED_Init(void)
{
/* 1. 使能 GPIOC 时钟(APB2ENR 第4位) */
RCC_APB2ENR |= (1 << 4);
/* 2. 配置 PC13 为推挽输出,最大速度 50MHz */
GPIOC_CRH &= ~(0xF << 20); /* 清除 PC13 配置位 */
GPIOC_CRH |= (0x3 << 20); /* MODE13=11(50MHz输出),CNF13=00(推挽) */
}
void LED_On(void)
{
GPIOC_ODR &= ~(1 << 13); /* PC13 输出低电平(LED 共阳极,低电平点亮) */
}
void LED_Off(void)
{
GPIOC_ODR |= (1 << 13); /* PC13 输出高电平 */
}
void LED_Toggle(void)
{
GPIOC_ODR ^= (1 << 13); /* 翻转 PC13 */
}
/* 简单延时函数 */
void Delay(uint32_t count)
{
while (count--);
}
/* 应用程序:直接调用驱动函数 */
int main(void)
{
LED_Init();
while (1) {
LED_On();
Delay(1000000);
LED_Off();
Delay(1000000);
}
}
案例二:STM32 UART 驱动(串口通信)
/*
* 裸机 UART 驱动示例(STM32F103)
*/
#define USART1_BASE 0x40013800
#define USART1_SR (*(volatile uint32_t *)(USART1_BASE + 0x00)) /* 状态寄存器 */
#define USART1_DR (*(volatile uint32_t *)(USART1_BASE + 0x04)) /* 数据寄存器 */
#define USART1_BRR (*(volatile uint32_t *)(USART1_BASE + 0x08)) /* 波特率寄存器 */
#define USART1_CR1 (*(volatile uint32_t *)(USART1_BASE + 0x0C)) /* 控制寄存器1 */
/* 状态寄存器标志位 */
#define USART_SR_TXE (1 << 7) /* 发送数据寄存器为空 */
#define USART_SR_RXNE (1 << 5) /* 接收数据寄存器非空 */
void USART1_Init(uint32_t pclk2, uint32_t baudrate)
{
/* 使能 USART1 和 GPIOA 时钟 */
RCC_APB2ENR |= (1 << 14) | (1 << 2);
/* 配置 PA9(TX)为复用推挽输出,PA10(RX)为浮空输入 */
/* ... GPIO 配置省略 ... */
/* 设置波特率:BRR = pclk2 / baudrate */
USART1_BRR = pclk2 / baudrate;
/* 使能发送(TE)、接收(RE)、USART(UE) */
USART1_CR1 = (1 << 3) | (1 << 2) | (1 << 13);
}
/* 发送一个字节 */
void USART1_SendByte(uint8_t data)
{
/* 等待发送缓冲区为空(轮询方式,会阻塞 CPU) */
while (!(USART1_SR & USART_SR_TXE));
USART1_DR = data;
}
/* 发送字符串 */
void USART1_SendString(const char *str)
{
while (*str) {
USART1_SendByte(*str++);
}
}
/* 接收一个字节(阻塞等待) */
uint8_t USART1_ReceiveByte(void)
{
while (!(USART1_SR & USART_SR_RXNE));
return (uint8_t)USART1_DR;
}
/* 应用程序 */
int main(void)
{
USART1_Init(72000000, 115200); /* 72MHz 主频,115200 波特率 */
USART1_SendString("Hello, Bare Metal!\r\n");
while (1) {
uint8_t ch = USART1_ReceiveByte(); /* 等待接收 */
USART1_SendByte(ch); /* 回显 */
}
}
1.2.3 裸机驱动的特点与局限
特点:
-
应用程序与驱动程序直接耦合,没有层次边界
-
驱动函数直接操作硬件寄存器地址,与硬件强绑定
-
程序结构简单,实时性好,适合资源极度受限的场景
局限性:
| 局限 | 说明 |
|---|---|
| 可移植性差 | 寄存器地址硬编码,换一款芯片需要重写全部驱动 |
| 无并发保护 | 没有任务调度和互斥机制,多任务场景下容易出现竞争 |
| 无内存管理 | 没有虚拟内存,内存分配完全由程序员手动管理 |
| 调试困难 | 只能通过 LED 闪烁、示波器等原始手段调试 |
| 功能单一 | 难以支持复杂的网络协议栈、文件系统等高级功能 |
结论:裸机驱动适用于功能简单、实时性要求高的小型嵌入式系统。当系统功能复杂到一定程度,就必须引入操作系统。
1.3 有操作系统时的设备驱动
1.3.1 操作系统对驱动的要求
当引入操作系统后,驱动程序不能再随意编写,必须按照操作系统规定的驱动框架和接口规范来实现。操作系统对驱动程序提出了以下要求:
- 驱动必须融入内核:驱动程序作为内核的一部分运行,必须遵守内核的编程规范
- 驱动必须提供统一接口:向上层提供标准的文件操作接口(
file_operations) - 驱动必须使用内核 API:不能直接调用 C 标准库,只能使用内核提供的函数
- 驱动必须处理并发:多个进程可能同时访问同一设备,驱动必须保证线程安全
1.3.2 操作系统为驱动提供的服务
操作系统为驱动程序提供了丰富的基础服务,使驱动开发者无需从零实现这些功能:
① 内存管理服务
/* 内核内存分配(不可睡眠场景,如中断上下文) */
void *ptr = kmalloc(1024, GFP_ATOMIC);
/* 内核内存分配(可睡眠场景,如进程上下文) */
void *ptr = kmalloc(1024, GFP_KERNEL);
/* 分配并清零内存 */
void *ptr = kzalloc(sizeof(struct my_dev), GFP_KERNEL);
/* 释放内存 */
kfree(ptr);
/* 设备管理内存(驱动卸载时自动释放,推荐使用) */
void *ptr = devm_kzalloc(&pdev->dev, sizeof(struct my_dev), GFP_KERNEL);
② 中断管理服务
/* 申请中断 */
int ret = request_irq(irq_num, /* 中断号 */
my_irq_handler, /* 中断处理函数 */
IRQF_SHARED, /* 中断标志 */
"my_device", /* 设备名称 */
dev); /* 传给处理函数的参数 */
/* 中断处理函数 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
/* 处理中断 */
return IRQ_HANDLED;
}
/* 释放中断 */
free_irq(irq_num, dev);
③ 同步与互斥服务
/* 自旋锁(适用于中断上下文,不可睡眠) */
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);
/* 临界区 */
spin_unlock(&lock);
/* 互斥锁(适用于进程上下文,可睡眠) */
struct mutex my_mutex;
mutex_init(&my_mutex);
mutex_lock(&my_mutex);
/* 临界区 */
mutex_unlock(&my_mutex);
/* 信号量 */
struct semaphore sem;
sema_init(&sem, 1);
down(&sem); /* P 操作 */
up(&sem); /* V 操作 */
④ 定时器服务
struct timer_list my_timer;
/* 初始化定时器(Linux 4.15+ 新接口) */
timer_setup(&my_timer, my_timer_callback, 0);
/* 设置超时时间并启动(jiffies 是内核时钟节拍计数) */
mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000)); /* 1秒后触发 */
/* 定时器回调函数 */
static void my_timer_callback(struct timer_list *t)
{
pr_info("定时器触发\n");
/* 如需周期性触发,在此重新设置定时器 */
mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000));
}
/* 删除定时器 */
del_timer_sync(&my_timer);
1.3.3 有操作系统时驱动的层次结构
以 Linux 系统为例,驱动程序处于内核空间的最底层:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
用户空间(User Space)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────────────────────────────────────────────┐
│ 应用程序 │
│ open("/dev/ttyS0") read() write() ioctl() │
└──────────────────────────┬──────────────────────────┘
│ glibc 封装系统调用
┌──────────────────────────▼──────────────────────────┐
│ C 运行库(glibc / musl) │
│ printf → write() | fopen → open() │
└──────────────────────────┬──────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━
内核空间(Kernel Space)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌──────────────────────────▼──────────────────────────┐
│ 系统调用接口(SCI) │
│ sys_open / sys_read / sys_write / sys_ioctl │
└──────────────────────────┬──────────────────────────┘
┌──────────────────────────▼──────────────────────────┐
│ 虚拟文件系统(VFS) │
│ 统一管理所有文件系统和设备文件 │
└──────────────────────────┬──────────────────────────┘
┌──────────────────────────▼──────────────────────────┐
│ 设备驱动程序 │
│ 实现 file_operations 中的 open/read/write/ioctl │
└──────────────────────────┬──────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━
硬件层
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌──────────────────────────▼──────────────────────────┐
│ GPIO │ UART │ SPI │ I2C │ USB │ PCIe │ 以太网卡 │
└─────────────────────────────────────────────────────┘
1.3.4 有无操作系统时驱动的对比
| 对比项 | 裸机驱动(无 OS) | Linux 驱动(有 OS) |
|---|---|---|
| 编程接口 | 自定义函数,随意命名 | 必须实现 file_operations 等内核规定接口 |
| 硬件访问 | 直接读写寄存器物理地址 | 通过 ioremap() 映射后访问 |
| 内存分配 | 静态数组或裸 malloc |
kmalloc()、kzalloc()、devm_kzalloc() |
| 中断处理 | 直接修改中断向量表 | request_irq() 注册中断处理函数 |
| 并发保护 | 无 | 自旋锁、互斥锁、信号量 |
| 调试手段 | LED 闪烁、示波器 | printk、dmesg、/proc、sysfs |
| 可移植性 | 差(寄存器地址硬编码) | 好(通过设备树描述硬件) |
| 开发复杂度 | 低 | 较高(需要学习内核框架) |
1.3.5 一次系统调用的完整路径
以应用程序读取串口数据为例,展示驱动在整个调用链中的位置:
/* 用户空间应用程序 */
int fd = open("/dev/ttyS0", O_RDONLY);
char buf[64];
int n = read(fd, buf, 64); /* ← 用户发起的调用 */
内核内部的完整调用链:
用户空间:read(fd, buf, 64)
│
│ 触发软中断(x86: int 0x80 / syscall 指令)
▼
内核入口:sys_read(fd, buf, 64) ← 系统调用层
│
▼
vfs_read(file, buf, 64, &pos) ← VFS 层
│ 根据 file->f_op 找到对应驱动的 read 函数
▼
file->f_op->read(file, buf, 64, &pos) ← 调用驱动注册的 read 函数
│
▼
uart_read(file, buf, 64, &pos) ← 串口驱动的具体实现
│ 操作 UART 硬件 FIFO 寄存器
▼
copy_to_user(buf, kernel_buf, n) ← 数据从内核空间拷贝到用户空间
│
▼
返回读取的字节数 n ← 返回用户空间
关键点:
copy_to_user()和copy_from_user()是内核与用户空间数据交换的唯一安全通道,不能直接用memcpy()代替,因为用户空间指针可能无效,直接访问会导致内核崩溃。
1.4 Linux 设备驱动
1.4.1 Linux 设备驱动的分类
Linux 内核将设备驱动分为三大类,这是 Linux 驱动框架的基础。
(1)字符设备驱动(Character Device Driver)
字符设备以字节流方式进行数据传输,数据按顺序访问,通常不支持随机定位(少数支持 llseek)。
典型字符设备:
-
串口(
/dev/ttyS0、/dev/ttyUSB0) -
键盘、鼠标(
/dev/input/event0) -
LED、蜂鸣器(
/dev/led) -
传感器(
/dev/temp) -
帧缓冲(
/dev/fb0)字符设备的标识:
ls -l /dev/ttyS0
# crw-rw---- 1 root dialout 4, 64 Jun 21 10:00 /dev/ttyS0
# ^ ^ ^
# c(字符设备) | 次设备号(Minor: 64)
# 主设备号(Major: 4)
字符设备驱动的核心结构体:
/*
* file_operations 结构体:字符设备驱动的核心
* 定义在 <linux/fs.h>
* 驱动程序通过实现其中的函数指针,向内核注册操作接口
*/
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open, /* 对应用户空间 open() */
.release = my_release, /* 对应用户空间 close() */
.read = my_read, /* 对应用户空间 read() */
.write = my_write, /* 对应用户空间 write() */
.unlocked_ioctl = my_ioctl, /* 对应用户空间 ioctl() */
.llseek = my_llseek, /* 对应用户空间 lseek() */
.mmap = my_mmap, /* 对应用户空间 mmap() */
.poll = my_poll, /* 对应用户空间 select()/poll() */
};
完整字符设备驱动案例(globalvar 虚拟内存设备):
/*
* globalvar.c
* 实现一个虚拟内存设备,可通过 /dev/globalvar 读写一块内核内存
* 这是宋宝华书中经典的入门字符设备驱动案例
*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#define GLOBALVAR_MAJOR 231
#define GLOBALVAR_NAME "globalvar"
#define MEM_SIZE 0x1000 /* 4KB */
/* 设备结构体:封装设备的所有私有数据 */
struct globalvar_dev {
struct cdev cdev; /* 内嵌的字符设备结构体 */
unsigned char mem[MEM_SIZE]; /* 设备对应的内存 */
struct mutex mutex; /* 互斥锁,保护并发访问 */
};
static struct globalvar_dev *globalvar_devp;
static int globalvar_open(struct inode *inode, struct file *filp)
{
/* 将设备结构体指针存入 filp->private_data */
filp->private_data = globalvar_devp;
return 0;
}
static int globalvar_release(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t globalvar_read(struct file *filp, char __user *buf,
size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalvar_dev *dev = filp->private_data;
if (p >= MEM_SIZE)
return 0;
if (count > MEM_SIZE - p)
count = MEM_SIZE - p;
mutex_lock(&dev->mutex);
/* 内核空间 → 用户空间:必须使用 copy_to_user */
if (copy_to_user(buf, dev->mem + p, count)) {
ret = -EFAULT;
} else {
*ppos += count;
ret = count;
pr_info("globalvar: 读取 %u 字节,偏移 %lu\n", count, p);
}
mutex_unlock(&dev->mutex);
return ret;
}
static ssize_t globalvar_write(struct file *filp, const char __user *buf,
size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalvar_dev *dev = filp->private_data;
if (p >= MEM_SIZE)
return 0;
if (count > MEM_SIZE - p)
count = MEM_SIZE - p;
mutex_lock(&dev->mutex);
/* 用户空间 → 内核空间:必须使用 copy_from_user */
if (copy_from_user(dev->mem + p, buf, count)) {
ret = -EFAULT;
} else {
*ppos += count;
ret = count;
pr_info("globalvar: 写入 %u 字节,偏移 %lu\n", count, p);
}
mutex_unlock(&dev->mutex);
return ret;
}
static loff_t globalvar_llseek(struct file *filp, loff_t offset, int orig)
{
loff_t ret;
switch (orig) {
case SEEK_SET:
if (offset < 0 || offset > MEM_SIZE) return -EINVAL;
filp->f_pos = offset;
ret = filp->f_pos;
break;
case SEEK_CUR:
if ((filp->f_pos + offset) < 0 ||
(filp->f_pos + offset) > MEM_SIZE) return -EINVAL;
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
return -EINVAL;
}
return ret;
}
static const struct file_operations globalvar_fops = {
.owner = THIS_MODULE,
.open = globalvar_open,
.release = globalvar_release,
.read = globalvar_read,
.write = globalvar_write,
.llseek = globalvar_llseek,
};
static int __init globalvar_init(void)
{
int ret;
dev_t devno = MKDEV(GLOBALVAR_MAJOR, 0);
/* 1. 静态申请设备号 */
ret = register_chrdev_region(devno, 1, GLOBALVAR_NAME);
if (ret < 0) {
pr_err("globalvar: 申请设备号 %d 失败\n", GLOBALVAR_MAJOR);
return ret;
}
/* 2. 动态分配设备结构体 */
globalvar_devp = kzalloc(sizeof(struct globalvar_dev), GFP_KERNEL);
if (!globalvar_devp) {
ret = -ENOMEM;
goto fail_malloc;
}
/* 3. 初始化互斥锁 */
mutex_init(&globalvar_devp->mutex);
/* 4. 初始化 cdev 并关联 file_operations */
cdev_init(&globalvar_devp->cdev, &globalvar_fops);
globalvar_devp->cdev.owner = THIS_MODULE;
/* 5. 向内核注册 cdev */
ret = cdev_add(&globalvar_devp->cdev, devno, 1);
if (ret) {
pr_err("globalvar: cdev_add 失败\n");
goto fail_cdev;
}
pr_info("globalvar: 驱动加载成功,主设备号 %d\n", GLOBALVAR_MAJOR);
return 0;
fail_cdev:
kfree(globalvar_devp);
fail_malloc:
unregister_chrdev_region(devno, 1);
return ret;
}
static void __exit globalvar_exit(void)
{
cdev_del(&globalvar_devp->cdev);
kfree(globalvar_devp);
unregister_chrdev_region(MKDEV(GLOBALVAR_MAJOR, 0), 1);
pr_info("globalvar: 驱动已卸载\n");
}
module_init(globalvar_init);
module_exit(globalvar_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("参考宋宝华《Linux设备驱动开发详解》");
MODULE_DESCRIPTION("globalvar 字符设备驱动");
用户空间测试程序:
/* test_globalvar.c */
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
int fd;
char write_buf[] = "Hello, globalvar!";
char read_buf[64] = {0};
/* 加载驱动后需先创建设备文件 */
/* sudo mknod /dev/globalvar c 231 0 */
fd = open("/dev/globalvar", O_RDWR);
if (fd < 0) { perror("open"); return -1; }
write(fd, write_buf, strlen(write_buf));
printf("写入:%s\n", write_buf);
lseek(fd, 0, SEEK_SET); /* 重新定位到文件头 */
read(fd, read_buf, sizeof(read_buf));
printf("读取:%s\n", read_buf);
close(fd);
return 0;
}
(2)块设备驱动(Block Device Driver)
块设备以固定大小的数据块(通常 512 字节或 4096 字节)为单位进行数据传输,支持随机访问。
典型块设备:
-
硬盘(
/dev/sda)、SSD(/dev/nvme0n1) -
eMMC(
/dev/mmcblk0)、SD 卡(/dev/mmcblk1) -
U 盘(
/dev/sdb)块设备的 I/O 路径:
应用程序 read()/write()
↓
VFS 层(虚拟文件系统)
↓
文件系统(ext4 / xfs / fat32)
↓
通用块层(Generic Block Layer)
↓ ← I/O 调度器(CFQ/Deadline/NOOP)在此合并、排序 I/O 请求
块设备驱动(SCSI / NVMe / MMC 驱动)
↓
物理存储介质(磁盘 / Flash)
块设备与字符设备的核心区别:
| 对比项 | 字符设备 | 块设备 |
|---|---|---|
| 数据单位 | 字节(Byte) | 块(Block,512B 或 4KB) |
| 随机访问 | 通常不支持 | 支持(可指定块号) |
| 内核缓冲 | 无(直接 I/O) | 有(页缓存 Page Cache) |
| 典型用途 | 传感器、串口 | 磁盘、Flash 存储 |
| 注册函数 | cdev_add() |
add_disk() |
| 核心结构体 | file_operations |
block_device_operations + gendisk |
(3)网络设备驱动(Network Device Driver)
网络设备是 Linux 三大设备类型中最特殊的一类,它不通过文件系统访问,而是通过套接字(Socket)接口与内核网络协议栈交互。
典型网络设备:
-
以太网卡(
eth0)、Wi-Fi 网卡(wlan0) -
蓝牙(
hci0)、虚拟网卡(lo、tun0)网络设备的特殊性:
-
没有对应的
/dev设备文件,通过ifconfig、ip命令管理 -
数据以**数据包(
sk_buff)**为单位传输 -
通过
register_netdev()注册到内核
/* 网络驱动核心操作集 */
static const struct net_device_ops my_netdev_ops = {
.ndo_open = my_net_open, /* ifconfig eth0 up */
.ndo_stop = my_net_stop, /* ifconfig eth0 down */
.ndo_start_xmit = my_net_xmit, /* 发送数据包 */
.ndo_get_stats = my_net_get_stats, /* 获取统计信息 */
};
/* 发送数据包 */
static netdev_tx_t my_net_xmit(struct sk_buff *skb, struct net_device *dev)
{
/* skb->data 指向数据包内容,skb->len 是数据包长度 */
hardware_send(skb->data, skb->len);
dev->stats.tx_packets++;
dev->stats.tx_bytes += skb->len;
dev_kfree_skb(skb); /* 释放 sk_buff */
return NETDEV_TX_OK;
}
1.4.2 Linux 设备驱动与内核的关系
驱动程序是 Linux 内核中代码量最大的部分。以 Linux 4.0 内核为例:
Linux 4.0 内核代码统计(约 1950 万行)
├── drivers/ 约 850 万行 ← 占比约 44%,最大
├── arch/ 约 270 万行 ← 体系结构相关代码
├── fs/ 约 130 万行 ← 文件系统
├── net/ 约 120 万行 ← 网络协议栈
├── include/ 约 110 万行 ← 头文件
├── sound/ 约 90 万行 ← 音频子系统
└── 其他 约 380 万行
驱动程序占内核代码总量的近一半,这说明硬件多样性是 Linux 内核复杂性的主要来源。
1.4.3 Linux 驱动程序的两种存在形式
Linux 驱动程序可以以两种形式存在:
形式一:编译进内核(Built-in)
# 在内核配置中选择 [*](编译进内核)
# make menuconfig 中:
# <*> Serial port driver ← 编译进内核,启动时自动加载
# 优点:启动时自动可用,无需手动加载
# 缺点:增大内核体积,修改驱动需要重新编译整个内核
形式二:内核模块(Loadable Kernel Module,LKM)
# 在内核配置中选择 [M](编译为模块)
# make menuconfig 中:
# <M> USB storage driver ← 编译为模块,按需加载
# 优点:按需加载,不增大内核体积,修改驱动无需重编内核
# 缺点:需要手动加载(insmod/modprobe)
# 加载模块
sudo insmod my_driver.ko
# 或
sudo modprobe my_driver
# 卸载模块
sudo rmmod my_driver
1.4.4 Linux 内核模块的基本框架
内核模块是 Linux 驱动开发的基础,每个模块必须包含以下要素:
#include <linux/init.h>
#include <linux/module.h>
/*
* 模块初始化函数
* __init:此函数只在初始化时执行一次,执行完毕后内核可释放其占用的内存
* 返回 0 表示成功,返回负值(如 -ENOMEM)表示失败
*/
static int __init mymodule_init(void)
{
printk(KERN_INFO "模块加载\n");
return 0;
}
/*
* 模块退出函数
* __exit:此函数只在模块卸载时执行
* 若模块被编译进内核(非模块方式),此函数不会被调用
*/
static void __exit mymodule_exit(void)
{
printk(KERN_INFO "模块卸载\n");
}
/* 注册初始化和退出函数(必须) */
module_init(mymodule_init);
module_exit(mymodule_exit);
/* 模块元信息 */
MODULE_LICENSE("GPL v2"); /* 许可证(必须声明) */
MODULE_AUTHOR("Author Name"); /* 作者 */
MODULE_DESCRIPTION("描述信息"); /* 描述 */
MODULE_VERSION("1.0"); /* 版本 */
1.4.5 设备号的概念
Linux 通过**主设备号(Major Number)和次设备号(Minor Number)**唯一标识一个设备:
- 主设备号:标识设备对应的驱动程序(同一驱动管理的设备共享主设备号)
- 次设备号:标识同一驱动管理的具体设备实例(如第1块硬盘、第2块硬盘)
/* 设备号相关宏(定义在 <linux/kdev_t.h>) */
dev_t devno;
/* 由主次设备号合成 dev_t */
devno = MKDEV(231, 0); /* 主设备号 231,次设备号 0 */
/* 从 dev_t 中提取主次设备号 */
int major = MAJOR(devno); /* 提取主设备号 */
int minor = MINOR(devno); /* 提取次设备号 */
/* 静态申请设备号(主设备号已知) */
register_chrdev_region(devno, 1, "my_device");
/* 动态申请设备号(由内核分配,推荐) */
alloc_chrdev_region(&devno, 0, 1, "my_device");
/* 释放设备号 */
unregister_chrdev_region(devno, 1);
# 查看系统已使用的设备号
cat /proc/devices
# Character devices:
# 1 mem
# 4 /dev/vc/0
# 4 tty
# 4 ttyS
# 5 /dev/tty
# ...
# Block devices:
# 8 sd
# 259 blkext
# ...
1.4.6 printk 调试
printk 是驱动开发中最基本的调试手段,支持 8 个日志级别:
/* 8 个日志级别(数字越小,优先级越高) */
printk(KERN_EMERG "0: 系统不可用,即将崩溃\n");
printk(KERN_ALERT "1: 必须立即采取行动\n");
printk(KERN_CRIT "2: 严重错误\n");
printk(KERN_ERR "3: 错误条件\n");
printk(KERN_WARNING "4: 警告条件\n");
printk(KERN_NOTICE "5: 正常但值得注意\n");
printk(KERN_INFO "6: 一般信息\n");
printk(KERN_DEBUG "7: 调试信息\n");
/* 简化宏(推荐使用,Linux 2.6.28+) */
pr_err("初始化失败,错误码:%d\n", ret);
pr_warn("参数超出范围:%d\n", val);
pr_info("驱动版本 %s 加载成功\n", VERSION);
pr_debug("寄存器值:0x%08x\n", reg); /* 仅定义 DEBUG 宏时输出 */
/* 带设备信息的打印(在 platform_driver 中推荐使用) */
dev_err(&pdev->dev, "probe 失败:%d\n", ret);
dev_warn(&pdev->dev, "配置参数异常\n");
dev_info(&pdev->dev, "设备 %s 注册成功\n", dev_name(&pdev->dev));
# 查看内核日志
dmesg
dmesg | grep "my_driver"
dmesg -w # 实时监控(类似 tail -f)
dmesg -c # 查看并清空缓冲区
# 控制控制台输出级别(只有级别小于此值的消息才显示在控制台)
cat /proc/sys/kernel/printk
# 7 4 1 7 (当前级别 / 默认级别 / 最低级别 / 启动默认级别)
echo 8 > /proc/sys/kernel/printk # 显示所有级别消息
1.5 Linux 设备驱动的开发环境搭建
1.5.1 开发模式:宿主机 + 目标机交叉编译
Linux 驱动开发通常采用交叉编译模式:
宿主机(Host) 目标机(Target)
───────────────────── ─────────────────────────
Ubuntu 20.04 x86_64 ARM Cortex-A9 开发板
(如 Exynos4412 / i.MX6ULL)
① 编写驱动代码(vim / VSCode)
② 交叉编译生成 .ko 文件 ──→ ③ 通过网络/串口传输到目标板
arm-linux-gnueabihf-gcc ④ insmod 加载驱动
⑥ 分析 dmesg 日志 ←── ⑤ 串口/网络输出调试信息
为什么需要交叉编译?
嵌入式目标板(ARM/MIPS/RISC-V)的 CPU 架构与宿主机(x86_64)不同,目标板资源有限(CPU 慢、内存小),无法在目标板上直接编译内核和驱动,因此需要在宿主机上使用交叉编译工具链生成目标板可执行的二进制文件。
1.5.2 安装宿主机开发工具
# Ubuntu 20.04 / 22.04
sudo apt-get update
sudo apt-get install -y \
build-essential \ # gcc、make、binutils 等基础编译工具
git \ # 版本控制
libncurses5-dev \ # make menuconfig 图形界面依赖
libssl-dev \ # 内核模块签名依赖
libelf-dev \ # 内核编译依赖(读取 ELF 格式)
flex bison \ # 词法/语法分析工具(内核 Kconfig 解析需要)
bc \ # 内核版本字符串计算
u-boot-tools \ # mkimage(制作 uImage 格式内核)
device-tree-compiler # dtc(编译 .dts 设备树源文件为 .dtb)
1.5.3 安装 ARM 交叉编译工具链
# 方法一:通过 apt 安装(推荐,简单快速)
# gnueabihf:硬浮点 ABI,适用于 Cortex-A 系列(有 FPU)
sudo apt-get install gcc-arm-linux-gnueabihf
# gnueabi:软浮点 ABI,适用于没有 FPU 的 ARM 处理器
sudo apt-get install gcc-arm-linux-gnueabi
# 验证安装
arm-linux-gnueabihf-gcc --version
# arm-linux-gnueabihf-gcc (Ubuntu 9.4.0-1ubuntu1~20.04) 9.4.0
# 查看工具链包含的工具
ls /usr/bin/arm-linux-gnueabihf-*
# arm-linux-gnueabihf-gcc ← C 编译器
# arm-linux-gnueabihf-g++ ← C++ 编译器
# arm-linux-gnueabihf-ld ← 链接器
# arm-linux-gnueabihf-objdump ← 反汇编工具
# arm-linux-gnueabihf-strip ← 去除调试信息
# 方法二:Linaro 官方工具链(版本更新,功能更完整)
wget https://releases.linaro.org/components/toolchain/binaries/\
latest-7/arm-linux-gnueabihf/\
gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
tar -xf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
export PATH=$PATH:$(pwd)/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin
设置交叉编译环境变量:
# 临时设置(仅当前终端有效)
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
# 永久设置(写入 ~/.bashrc)
echo 'export ARCH=arm' >> ~/.bashrc
echo 'export CROSS_COMPILE=arm-linux-gnueabihf-' >> ~/.bashrc
source ~/.bashrc
# 验证:编译一个简单的 ARM 程序
cat > hello.c << 'EOF'
#include <stdio.h>
int main() { printf("Hello ARM!\n"); return 0; }
EOF
arm-linux-gnueabihf-gcc -o hello_arm hello.c
file hello_arm
# hello_arm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV)...
1.5.4 获取 Linux 4.0 内核源码
# 方法一:从 kernel.org 下载源码包(推荐,速度快)
wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.0.tar.xz
tar -xf linux-4.0.tar.xz
cd linux-4.0
# 方法二:Git 克隆指定版本标签
git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux
git checkout v4.0 # 切换到 v4.0 标签
git log --oneline -5 # 查看最近5条提交记录
# 查看内核版本信息
head -5 Makefile
# VERSION = 4
# PATCHLEVEL = 0
# SUBLEVEL = 0
# EXTRAVERSION =
# NAME = Hurr durr I'ma sheep
# 内核源码目录结构
tree -L 1 linux-4.0/
# linux-4.0/
# ├── arch/ ← 体系结构相关代码(x86、ARM、MIPS 等)
# ├── block/ ← 块设备层
# ├── crypto/ ← 加密算法
# ├── drivers/ ← 设备驱动(最大目录)
# ├── fs/ ← 文件系统
# ├── include/ ← 内核头文件
# ├── init/ ← 内核初始化代码
# ├── ipc/ ← 进程间通信
# ├── kernel/ ← 内核核心(调度、信号等)
# ├── lib/ ← 内核通用库函数
# ├── mm/ ← 内存管理
# ├── net/ ← 网络协议栈
# ├── scripts/ ← 编译脚本
# ├── security/ ← 安全模块(SELinux 等)
# └── sound/ ← 音频子系统
1.5.5 配置并编译 Linux 4.0 内核
配置内核:
# 进入内核源码目录
cd linux-4.0
# 使用开发板默认配置(以 ARM Vexpress-A9 为例)
make ARCH=arm vexpress_defconfig
# 常用的 defconfig 文件(位于 arch/arm/configs/)
ls arch/arm/configs/ | grep -E "vexpress|imx|exynos|s3c"
# exynos_defconfig ← 三星 Exynos 系列
# imx_v6_v7_defconfig ← NXP i.MX6/i.MX7 系列
# s3c2410_defconfig ← 三星 S3C2410
# vexpress_defconfig ← ARM Vexpress 参考平台
# 图形化配置(可选,用于裁剪内核功能)
make ARCH=arm menuconfig
# 常用配置路径:
# → Device Drivers → Character devices (字符设备)
# → Device Drivers → Block devices (块设备)
# → Device Drivers → Network device support (网络设备)
# → Kernel hacking → printk and dmesg options (调试选项)
# → General setup → Kernel compression mode (内核压缩方式)
# 查看配置结果
grep CONFIG_SERIAL .config
grep CONFIG_USB .config
编译内核:
# 编译内核镜像(-j$(nproc) 使用所有 CPU 核心并行编译)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc) zImage
# 编译设备树文件(.dts → .dtb)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
# 编译所有内核模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc) modules
# 安装模块到指定目录(用于制作根文件系统)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
INSTALL_MOD_PATH=./rootfs modules_install
# 编译完成后的产物
ls -lh arch/arm/boot/zImage
# -rw-r--r-- 1 user user 3.8M Jun 21 10:00 arch/arm/boot/zImage
ls arch/arm/boot/dts/vexpress-v2p-ca9.dtb
# arch/arm/boot/dts/vexpress-v2p-ca9.dtb
1.5.6 编写并编译第一个内核模块
目录结构:
hello/
├── hello.c ← 驱动源码
└── Makefile ← 编译脚本
hello.c(含模块参数传递):
/*
* hello.c —— 第一个 Linux 内核模块
* 演示模块的加载、卸载及参数传递机制
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
/* 模块参数定义 */
static int num = 1;
static char *name = "world";
/*
* module_param(变量名, 类型, sysfs权限)
* 类型:bool, int, uint, long, ulong, charp, short, ushort
* 权限:0644(root读写,其他只读);0 表示不在 sysfs 中显示
*/
module_param(num, int, 0644);
module_param(name, charp, 0644);
MODULE_PARM_DESC(num, "打印次数,默认为1");
MODULE_PARM_DESC(name, "打印的名字,默认为world");
static int __init hello_init(void)
{
int i;
pr_info("hello: 模块开始加载\n");
for (i = 0; i < num; i++)
pr_info("hello: (%d/%d) Hello, %s!\n", i + 1, num, name);
pr_info("hello: 模块加载完成\n");
return 0;
}
static void __exit hello_exit(void)
{
pr_info("hello: Goodbye, %s!\n", name);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("参考宋宝华《Linux设备驱动开发详解》");
MODULE_DESCRIPTION("Hello World 内核模块");
MODULE_VERSION("1.0");
Makefile:
# ============================================================
# Makefile —— 内核模块编译脚本
# ============================================================
# 内核源码路径
# 本机调试:使用当前运行内核的头文件目录
KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build
# 交叉编译时,指向内核源码目录(取消注释并修改路径)
# KERNEL_DIR ?= /home/user/linux-4.0
# 当前模块源码所在目录
PWD := $(shell pwd)
# 指定要编译的模块(模块名.o)
obj-m := hello.o
# 若模块由多个源文件组成:
# obj-m := mymodule.o
# mymodule-objs := file1.o file2.o
# 默认目标:编译模块
all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
# 清理编译产物
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
.PHONY: all clean
编译、加载与测试:
# 编译
make
# 生成文件:hello.ko hello.o hello.mod.c hello.mod.o Module.symvers
# 查看模块信息
modinfo hello.ko
# filename: /home/user/hello/hello.ko
# version: 1.0
# description: Hello World 内核模块
# author: 参考宋宝华《Linux设备驱动开发详解》
# license: GPL v2
# parm: num:打印次数,默认为1 (int)
# parm: name:打印的名字,默认为world (charp)
# 加载模块(使用默认参数)
sudo insmod hello.ko
dmesg | tail -5
# [ 100.001] hello: 模块开始加载
# [ 100.002] hello: (1/1) Hello, world!
# [ 100.003] hello: 模块加载完成
# 卸载模块
sudo rmmod hello
dmesg | tail -2
# [ 200.001] hello: Goodbye, world!
# 加载模块(传入自定义参数)
sudo insmod hello.ko num=3 name="Linux4.0"
dmesg | tail -6
# [ 300.001] hello: 模块开始加载
# [ 300.002] hello: (1/3) Hello, Linux4.0!
# [ 300.003] hello: (2/3) Hello, Linux4.0!
# [ 300.004] hello: (3/3) Hello, Linux4.0!
# [ 300.005] hello: 模块加载完成
# 通过 sysfs 查看/修改模块参数(运行时动态修改)
cat /sys/module/hello/parameters/num
# 3
echo 5 | sudo tee /sys/module/hello/parameters/num
# 5
# 查看已加载模块列表
lsmod | grep hello
# hello 16384 0
# 列:模块名 | 内存大小 | 被引用次数
1.5.7 使用 QEMU 搭建 ARM 虚拟开发环境
对于没有实体开发板的情况,可以使用 QEMU 模拟 ARM 环境进行驱动开发和测试。
安装 QEMU:
sudo apt-get install qemu-system-arm
qemu-system-arm --version
# QEMU emulator version 6.2.0
制作最小根文件系统(BusyBox):
# 下载 BusyBox
wget https://busybox.net/downloads/busybox-1.36.0.tar.bz2
tar -xf busybox-1.36.0.tar.bz2
cd busybox-1.36.0
# 配置(启用静态链接,无需 glibc)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# → Settings → Build static binary (no shared libs) → 选中 [*]
# 编译并安装到 _install 目录
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- install
# 创建必要目录和初始化脚本
cd _install
mkdir -p proc sys dev etc/init.d tmp
cat > etc/init.d/rcS << 'EOF'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
echo "=== BusyBox 根文件系统启动成功 ==="
EOF
chmod +x etc/init.d/rcS
# 打包为 initramfs(cpio + gzip 格式)
find . | cpio -o --format=newc | gzip > ../initramfs.img
cd ..
启动 QEMU ARM 虚拟机:
qemu-system-arm \
-M vexpress-a9 \
-kernel linux-4.0/arch/arm/boot/zImage \
-dtb linux-4.0/arch/arm/boot/dts/vexpress-v2p-ca9.dtb \
-initrd initramfs.img \
-nographic \
-append "console=ttyAMA0 rdinit=/sbin/init" \
-m 512M \
-net nic -net user,hostfwd=tcp::2222-:22
# 参数说明:
# -M vexpress-a9 模拟 ARM Vexpress-A9 开发板(Cortex-A9 双核)
# -kernel 指定内核镜像
# -dtb 指定设备树文件
# -initrd 指定根文件系统
# -nographic 无图形界面,使用串口作为控制台
# -append 内核启动参数
# -m 512M 分配 512MB 内存
# -net ... hostfwd 将宿主机 2222 端口转发到虚拟机 22 端口(SSH)
# 退出 QEMU:按 Ctrl+A,然后按 X
向虚拟机传输驱动模块:
# 方法一:宿主机开启 HTTP 服务器
cd /path/to/driver/
python3 -m http.server 8080
# 在 QEMU 虚拟机内下载并加载
wget http://10.0.2.2:8080/hello.ko # 10.0.2.2 是 QEMU 的宿主机地址
insmod hello.ko
dmesg | tail -5
# 方法二:通过 SSH(需要虚拟机内有 dropbear/openssh)
scp -P 2222 hello.ko root@localhost:/root/
ssh -p 2222 root@localhost "insmod /root/hello.ko && dmesg | tail -5"
1.5.8 内核模块管理命令汇总
# ── 加载 ──────────────────────────────────────────────────
insmod hello.ko # 直接加载指定 .ko 文件,不处理依赖
insmod hello.ko num=3 # 加载时传入参数
modprobe hello # 智能加载,自动处理模块依赖关系
# ── 卸载 ──────────────────────────────────────────────────
rmmod hello # 卸载模块(模块名,不含 .ko)
modprobe -r hello # 卸载模块及其不再需要的依赖模块
# ── 查看 ──────────────────────────────────────────────────
lsmod # 列出所有已加载模块
lsmod | grep hello # 过滤特定模块
modinfo hello.ko # 查看模块详细信息(作者/版本/参数/依赖)
cat /proc/modules # 查看模块详细状态(包含内存地址)
# ── 依赖管理 ──────────────────────────────────────────────
depmod -a # 扫描所有模块,重新生成 modules.dep 依赖文件
# modules.dep 位于 /lib/modules/$(uname -r)/modules.dep
# ── 自动加载 ──────────────────────────────────────────────
# 开机自动加载:将模块名写入 /etc/modules
echo "hello" | sudo tee -a /etc/modules
# 加载时传参:在 /etc/modprobe.d/ 下创建配置文件
echo "options hello num=5 name=autoload" | \
sudo tee /etc/modprobe.d/hello.conf
本章小结
| 章节 | 核心知识点 | 关键 API / 命令 |
|---|---|---|
| 1.1 设备驱动的作用 | 驱动是硬件与软件的桥梁;屏蔽硬件细节;提供统一接口 | file_operations |
| 1.2 无 OS 时的驱动 | 直接操作寄存器;与应用紧耦合;可移植性差 | 寄存器地址宏、轮询等待 |
| 1.3 有 OS 时的驱动 | 遵循内核框架;使用内核 API;处理并发 | kmalloc、request_irq、mutex_lock |
| 1.4 Linux 设备驱动 | 三大分类(字符/块/网络);两种存在形式(内建/模块);设备号机制 | cdev_add、register_netdev、MKDEV |
| 1.5 开发环境搭建 | 交叉编译工具链;内核源码编译;Hello World 模块;QEMU 虚拟环境 | arm-linux-gnueabihf-gcc、insmod、dmesg |
学习路径建议
第一步:本机 x86 环境入门
→ 编写 Hello World 内核模块
→ 掌握 insmod / rmmod / dmesg / lsmod 基本操作
→ 理解 module_init / module_exit / MODULE_LICENSE
第二步:搭建 ARM 交叉编译环境
→ 安装 arm-linux-gnueabihf 工具链
→ 下载并编译 Linux 4.0 内核源码
→ 使用 QEMU 运行 ARM 虚拟机
第三步:编写字符设备驱动
→ 实现 globalvar 虚拟内存设备
→ 掌握 copy_to_user / copy_from_user
→ 学习设备号申请与 cdev 注册流程
第四步:在实体开发板上调试
→ 移植驱动到 ARM 开发板(如 i.MX6ULL)
→ 学习设备树(DTS)的编写与修改
→ 使用 sysfs / proc 接口调试驱动
参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)