目录
2.1.3 spin_lock 和 spin_lock_irqsave 的区别
2.1.9 tasklet 和工作队列能否休眠?运行在中断上下文还是进程上下文
3.copy_to_user 和 copy_from_user 的作用是什么
3、platform_device 和 platform_driver 关系
2、readl、writel 与 ioread32、iowrite32的联系与区别
1、札记
1.1、芯片的bring up 主要做哪些工作:
1、sdk 编译 烧录 启动 调试串口
2、屏幕驱动正常工作 demo正常启动
2、Linux驱动八股文
中断与同步互斥
2.1.1 内核同步互斥的几种方式
互斥锁、自旋锁、原子操作、禁止抢占、内存屏障
信号量、读写锁、顺序锁
2.1.2 互斥锁和自旋锁的区别
自旋锁:忙等、不可休眠、持有时间短、适合中断上下文
互斥锁:睡眠等,持有时间长
2.1.3 spin_lock 和 spin_lock_irqsave 的区别
区别在于中断开关,通常在中断上下文,需要 对寄存器进行操作,寄存器操作需要用 spin_lock_irqsave ,而 spin_lock 只是禁止内核抢占,适用于没有中断处理的场景,确保临界区资源不被中断程序访问
2.1.4 进程上下文和中断上下文有什么区别
进程上下文:用户态进程的执行环境,例如系统调用,内核线程,可休眠(允许调用可休眠函数,如果kmalloc msleep)
中断上下文: 硬中断、软中断触发的执行条件,不可休眠
2.1.5 进行上下文用什么锁
看进程能否休眠,可以休眠的话用互斥锁,比如系统调用,内核线程等场景都是可以休眠的
不可休眠:自旋锁,比如中断处理程序的上半部,持有自旋锁、原子操作的领域
2.1.6 中断上下文用什么锁
自旋锁
2.1.8 中断下半部的三种方式 以及有什么区别
软中断 tasklet 工作队列
tasklet 基于软中断,动态注册,而软中断是静态注册的
工作队列运行在进程上下文,可休眠 ;tasklet 和软中断是在中断上下文,不可休眠
2.1.9 tasklet 和工作队列能否休眠?运行在中断上下文还是进程上下文
tasklet : 中断上下文,禁止休眠
工作队列: 进程上下文,允许休眠
Linux驱动基础问题
2.2.1 驱动分类
- 字符设备驱动:按字节访问 如串口 按键
- 块设备驱动:按块访问 如硬盘 SD卡
- 网络设别驱动:网络接口设备
2.2.2 驱动模块基本结构
#include <linux/module.h> #include <linux/init.h> static int __init my_driver_init(void) { printk(KERN_INFO "Driver initialized\n"); return 0; } static void __exit my_driver_exit(void) { printk(KERN_INFO "Driver exited\n"); } module_init(my_driver_init); module_exit(my_driver_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("Sample Driver");
2.2.3 驱动的加载方式
- 静态加载: 编译进内核镜像
- 动态加载:编译为模块 (.ko)文件,使用 insmod/ modprobe 加载
- 对应模块的静态加载和动态加载可以通过menuconfig 界面进行选择
config EXAMPLE_DRIVER
tristate "Example Driver Support"
depends on NETDEVICES
help
This is an example driver for Linux.
tristate
是支持动态加载(<M>
)的关键字。通过
menuconfig
界面按Y/M/N
切换编译方式。依赖项(
depends on
)和默认值(default
)会影响最终行为。
2.2.4 字符驱动设备
// 分配设备号 dev_t dev; alloc_chrdev_region(&dev, 0, 1, "my_device"); // 初始化cdev结构 struct cdev *my_cdev = cdev_alloc(); cdev_init(my_cdev, &fops); my_cdev->owner = THIS_MODULE; // 添加字符设备 cdev_add(my_cdev, dev, 1); // 创建设备节点 struct class *my_class = class_create(THIS_MODULE, "my_class"); device_create(my_class, NULL, dev, NULL, "my_device");
2.2.5 文件操作结构体
static struct file_operations my_fops = { .owner = THIS_MODULE, .open = my_open, .release = my_release, .read = my_read, .write = my_write, .unlocked_ioctl = my_ioctl, };
2.2.6 常见面试问题
1. 字符设备驱动的主设备号和次设备号有什么作用
- 主设备号 标识设备驱动程序
- 此设备号 标识使用同一驱动的不同设备通过 MAJOR() 和 MINOR ()宏获取
2.如何实现设备的并发访问控制
- 使用自旋锁、互斥锁等同步机制
3.copy_to_user 和 copy_from_user 的作用是什么
- 安全在内核空间和用户空间之间复制数据
2.3 中断处理
2.3.1 中断注册流程
// 注册中断处理函数 int ret = request_irq(irq_num, my_interrupt_handler, IRQF_SHARED, "my_device", dev_id); // 中断处理函数 static irqreturn_t my_interrupt_handler(int irq, void *dev_id) { // 处理中断 // ... return IRQ_HANDLED; } // 释放中断 free_irq(irq_num, dev_id);
- 先 请求中断 -> 在写中断函数 -> 释放中断
2.3.2 中断注册流程
- 上半部 中断处理函数,快速响应
- 下半部 延迟处理 可调度
// 工作队列实现下半部 static struct work_struct my_work; static void my_work_handler(struct work_struct *work) { // 耗时操作 } static irqreturn_t my_interrupt_handler(int irq, void *dev_id) { // 快速处理 schedule_work(&my_work); return IRQ_HANDLED; } // 初始化 INIT_WORK(&my_work, my_work_handler);
2.3.4 常见面试问题
1、Linux 中断下半部有哪几种机制
- 软中断 : 静态分配,优先级高
- tasklet : 基于软中断,动态创建
- 工作队列:在进程上下文中执行,可睡眠
2、中断上下文有什么限制
- 不能睡眠
- 不能使用可能睡眠的函数 (互斥锁)
- 尽量减少处理时间
3、如何处理共享中断
共享中断是指多个设备共享一个硬件中断线,当中断触发,内核需要调用所有注册到这个irq 的设备处理函数处,处理函数中回去 检查中断源 和 返回处理结果 、
2.4 设备树与平台驱动
2.4.1 设备树基础
/* 设备树节点示例 */ my_device: my_device@50000000 { compatible = "vendor,my-device"; reg = <0x50000000 0x1000>; interrupts = <0 29 4>; clocks = <&clk 1>; status = "okay"; };
2.4.2 平台驱动模型
// 平台驱动结构体 static struct platform_driver my_platform_driver = { .probe = my_platform_probe, .remove = my_platform_remove, .driver = { .name = "my-device", .of_match_table = my_of_match, .pm = &my_pm_ops, }, }; // 设备树匹配表 static const struct of_device_id my_of_match[] = { { .compatible = "vendor,my-device" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_of_match); // 注册平台驱动 module_platform_driver(my_platform_driver);
2.4.3 常见面试问题
1.设备树的作用是什么
- 描述硬件设备,实现硬件与驱动分离支持运行适合
2、如何在驱动中获得设备树属性
- 通过设备树匹配节点(compatible)
- 提取常用属性(of函数)
#include <linux/of.h>
#include <linux/platform_device.h>
static int my_probe(struct platform_device *pdev)
{
struct device_node *node = pdev->dev.of_node;
struct resource *res;
void __iomem *regs;
int irq, ret;
u32 freq;
/* 1. 获取寄存器地址(通过 reg 属性) */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
regs = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(regs))
return PTR_ERR(regs);
/* 2. 获取中断号 */
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
/* 3. 读取自定义整数属性 */
ret = of_property_read_u32(node, "clock-frequency", &freq);
if (ret) {
dev_warn(&pdev->dev, "clock-frequency not specified, using default\n");
freq = 25000000; // 默认值
}
/* 4. 检查布尔属性 */
if (of_property_read_bool(node, "dma-capable")) {
setup_dma();
}
/* 注册中断处理函数 */
ret = devm_request_irq(&pdev->dev, irq, my_irq_handler, 0, "my-device", NULL);
if (ret)
return ret;
dev_info(&pdev->dev, "Device probed, freq=%d Hz\n", freq);
return 0;
}
static const struct of_device_id my_device_ids[] = {
{ .compatible = "vendor,my-device" },
{ }
};
MODULE_DEVICE_TABLE(of, my_device_ids);
static struct platform_driver my_driver = {
.driver = {
.name = "my-device",
.of_match_table = my_device_ids,
},
.probe = my_probe,
};
module_platform_driver(my_driver);
3、platform_device 和 platform_driver 关系
- platform_device 描述设备资源
- platform_driver 实现设别驱动通过总线和模型绑定
2.5 同步与互斥
2.5.1 常用的同步机制
- 自旋锁(spin_lock): 忙等待,适合短时间持锁,中断可用
- 互斥锁(mutex): 可能睡眠,适合长时间持锁
- 读写锁(rwlock): 读共享写独占
// 自旋锁示例 spinlock_t my_lock; spin_lock_init(&my_lock); spin_lock(&my_lock); // 临界区 spin_unlock(&my_lock); // 互斥锁示例 struct mutex my_mutex; mutex_init(&my_mutex); mutex_lock(&my_mutex); // 临界区 mutex_unlock(&my_mutex);
2.5.2 常见的面试问题
1. 自旋锁和互斥锁的区别
- 自旋锁:忙等待,不释放 cpu 适合短时间持有
- 互斥锁:可能睡眠,释放 cpu ,适合长时间持有
- 自旋锁可以用于中断上下文,互斥锁不可以
2.死锁概念
多并发进程因争夺资源而产生的相互等待的现象
本质:(1、资源有限;2、进程推进不合理 )
3.死锁的四个条件
- 互斥:涉及的资源非共享
- 占有且等待:进程每次申请他所需要的一部分资源,在等待新资源的同时继续占用已分配到的资源
- 不可剥夺:进程所获得的资源在未使用完毕之前不会被其他进程抢走
- 循环等待:某一个进程已获得的资源会被下一个进程请求
4.死锁的处理方式:防止死锁,避免死锁,检测死锁,解除死锁
预防死锁:
- 资源一次性分配(破坏请求和保持条件)
- 可剥夺资源(当进程的资源满足是,释放已占有资源,破坏不可剥夺条件)
- 资源有序分配(给每个资源赋予一个标号,按照编号顺序请求资源)
- 当某个进程一个请求得不到满足时,则剥夺他的所有条件
避免死锁:
- 系统在进行资源分配时,先计算资源分配的安全性,若分配会导致系统进入不安全状态,则取消此次资源分配(银行家算法)
检测死锁:
- 为每个进程和资源分配一个唯一的号码,然后建立资源分配表和进程等待表
解除死锁:在检测到死锁后,可以采用以下两个方面解除死锁:
- 剥夺资源:从其他进程剥夺足够多的资源分配给死锁进程,以接触死锁状态
- 撤销进程:撤销死锁进程,直到有足够的资源可用
5、如何避免死锁
一般来说,有三种避免死锁的技术
- 加锁顺序:(线程按照一定的顺序加锁)
- 加锁时间限制:(超过时限就放弃该锁的请求,并释放自己占用的资源)
- 死锁检测
6、在单核mcu上写多线程程序是否要加锁,
依旧要加锁的,线程锁适用于实现线程的同步和通信的,多线程之间依旧是要线程同步的,不使用线程锁的话,会导致共享数据的修改引起的冲突。
2.6 gpio与设备 io
// 获取GPIO int gpio = of_get_named_gpio(node, "reset-gpio", 0); if (gpio_is_valid(gpio)) { gpio_request(gpio, "reset"); // 设置方向 gpio_direction_output(gpio, 1); // 设置值 gpio_set_value(gpio, 0); msleep(10); gpio_set_value(gpio, 1); // 释放GPIO gpio_free(gpio); }
2.6.1 内存映射 io
// 映射IO内存 void __iomem *base; struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0); base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(base)) return PTR_ERR(base); // 读写寄存器 writel(value, base + OFFSET); value = readl(base + OFFSET);
2.6.3 常见面试问题
1、如何处理Linux下的 gpio 中断
在Linux下 处理 gpio 中断通常涉及 内核驱动 或 用户空间轮询/事件监听 两种方式
- 确认 gpio 编号
# 查看 GPIO 编号(假设物理引脚为 GPIO17) echo 17 > /sys/class/gpio/export ls /sys/class/gpio/gpio17 # 确认 GPIO 已导出
- 配置 gpio 为输入并启用中断
# 设置为输入模式 echo "in" > /sys/class/gpio/gpio17/direction # 设置中断触发方式(可选:rising, falling, both, none) echo "rising" > /sys/class/gpio/gpio17/edge
- 在用户空间监听中断
#include <stdio.h> #include <fcntl.h> #include <poll.h> int main() { int fd = open("/sys/class/gpio/gpio17/value", O_RDONLY); struct pollfd pfd = { fd, POLLPRI | POLLERR, 0 }; while (1) { int ret = poll(&pfd, 1, -1); // 阻塞等待中断 if (ret > 0) { lseek(fd, 0, SEEK_SET); // 重置文件指针 char buf[10]; read(fd, buf, sizeof(buf)); printf("Interrupt! Value: %s", buf); } } close(fd); return 0; }
2、readl、writel 与 ioread32、iowrite32的联系与区别
功能基本相同,都是用于32位 IO 访问 readl、writel
3、如何处理设备的字节序问题
使用 cpu_to_le32 \ le32_to_cpu 等转换函数明确区分大小端字节序使用位域
Linux 驱动框架系列
3.1 Linux 设备驱动模型
3.1.1 设备驱动模型基础
- 设备模型三要素:总线(bus)、设备(device)、驱动(driver)
- kobject:设备模型的基础对象,实现引用计数和 sysfs 导出
- 设备树:描述硬件信息的数据结构,减少硬编码
3.1.2 驱动匹配机制
// 平台驱动匹配示例
static const struct of_device_id my_of_match[] = {
{ .compatible = "vendor,my-device" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
static struct platform_driver my_platform_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my-device",
.of_match_table = my_of_match,
},
};
3.1.3 常见面试问题
1、Linux 设备驱动模型的核心组件有哪些
- kobject : 基础对象,提供引用技术和 sysfs 接口
- kset : kobject 的集合,管理相关对象
- device : 表示物理或者逻辑设备
- driver : 实现设备功能的代码
- bus : 连接设备和驱动的媒介
2、驱动和设备是如何匹配
- 基于总线的匹配机制设备树中的 compatible 属性与 驱动中的 of_match_table 匹配平台设备的 name 与 平台驱动的 name 匹配成功后调用 probe 函数
3、设备树在驱动开发中的作用是什么
- 描述硬件设备信息,减少硬编码实现硬件与驱动分离支持 运行时配置修改简化
3.2 GPIO 子系统
3.2.1 gpio子系统框架
- gpio 控制器 : 管理一组 gpio 引脚
- gpiochip :表示其中一个 gpio 控制器
- gpio_desc : 描述单个 gpio 引脚
- gpiolib : 提供同一的 gpio 操作接口
3.2.2 gpio 驱动实现
// GPIO控制器驱动示例
static const struct gpio_chip my_gpio_chip = {
.label = "my-gpio",
.owner = THIS_MODULE,
.base = -1, // 动态分配
.ngpio = 32, // 32个GPIO
.request = my_gpio_request,
.free = my_gpio_free,
.direction_input = my_gpio_direction_input,
.direction_output = my_gpio_direction_output,
.get = my_gpio_get,
.set = my_gpio_set,
};
static int my_gpio_probe(struct platform_device *pdev)
{
struct my_gpio_priv *priv;
int ret;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
priv->chip = my_gpio_chip;
priv->chip.parent = &pdev->dev;
ret = devm_gpiochip_add_data(&pdev->dev, &priv->chip, priv);
if (ret)
return ret;
platform_set_drvdata(pdev, priv);
return 0;
}
3.2.3常见面试问题
1、如何在驱动中使用 gpio
在 Linux 内核驱动中使用 GPIO 主要涉及 申请 GPIO、配置方向(输入/输出)、读写 GPIO 值、处理中断 等操作
2、gpio中断是如何实现的
首先需要在设备树中定义
my_device {
compatible = "my,gpio-device";
interrupt-parent = <&gpio>;
interrupts = <17 IRQ_TYPE_EDGE_RISING>; // GPIO17,上升沿触发
};
然后在驱动中注册中断处理函数
static irqreturn_t gpio_irq_handler(int irq, void *dev_id) {
printk(KERN_INFO "GPIO Interrupt triggered!\n");
return IRQ_HANDLED;
}
// 在 probe 函数中注册中断
int irq = gpiod_to_irq(gpio); // 新版 API
// int irq = gpio_to_irq(gpio_num); // 旧版 API
int ret = request_irq(irq, gpio_irq_handler, IRQF_TRIGGER_RISING, "my_gpio_irq", NULL);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
// 在 remove 函数中释放中断
free_irq(irq, NULL);
完整驱动
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
struct my_device_data {
struct gpio_desc *gpio;
int irq;
};
static irqreturn_t my_gpio_irq(int irq, void *dev_id) {
printk(KERN_INFO "Interrupt occurred!\n");
return IRQ_HANDLED;
}
static int my_probe(struct platform_device *pdev) {
struct my_device_data *data;
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
// 获取 GPIO
data->gpio = gpiod_get(&pdev->dev, "my-gpios", GPIOD_IN);
if (IS_ERR(data->gpio))
return PTR_ERR(data->gpio);
// 注册中断
data->irq = gpiod_to_irq(data->gpio);
if (request_irq(data->irq, my_gpio_irq, IRQF_TRIGGER_RISING, "my_gpio_irq", NULL)) {
gpiod_put(data->gpio);
return -EINVAL;
}
platform_set_drvdata(pdev, data);
return 0;
}
static int my_remove(struct platform_device *pdev) {
struct my_device_data *data = platform_get_drvdata(pdev);
free_irq(data->irq, NULL);
gpiod_put(data->gpio);
return 0;
}
static const struct of_device_id my_of_match[] = {
{ .compatible = "my,gpio-device" },
{},
};
MODULE_DEVICE_TABLE(of, my_of_match);
static struct platform_driver my_driver = {
.driver = {
.name = "my_gpio_driver",
.of_match_table = my_of_match,
},
.probe = my_probe,
.remove = my_remove,
};
module_platform_driver(my_driver);
MODULE_LICENSE("GPL");
3.3 Pinctrl 子系统
3.3.1 pinctrl 子系统架构
- pin controller :实现 SoC 上引脚的管理
- pinctrl driver : 实现 pin controller 的驱动
- pin configuration :配置引脚功能 上拉下拉
- pin muxing :配置引脚复用功能
3.3.2 pinctrl 驱动实现
// Pinctrl驱动示例
static const struct pinctrl_pin_desc my_pins[] = {
PINCTRL_PIN(0, "gpio0"),
PINCTRL_PIN(1, "gpio1"),
// ...
};
static const char * const my_groups[] = {
"uart0_grp", "i2c0_grp", "spi0_grp",
};
static const char * const my_functions[] = {
"uart", "i2c", "spi",
};
static const struct pinmux_ops my_pmx_ops = {
.get_functions_count = my_get_functions_count,
.get_function_name = my_get_function_name,
.get_function_groups = my_get_function_groups,
.set_mux = my_set_mux,
};
static const struct pinconf_ops my_pconf_ops = {
.pin_config_get = my_pin_config_get,
.pin_config_set = my_pin_config_set,
};
static struct pinctrl_desc my_pinctrl_desc = {
.name = "my-pinctrl",
.pins = my_pins,
.npins = ARRAY_SIZE(my_pins),
.pctlops = &my_pctlops,
.pmxops = &my_pmx_ops,
.confops = &my_pconf_ops,
.owner = THIS_MODULE,
};
3.3.3 常见面试问题
1、Pinctrl 子系统的作用是什么
管理和配置 SoC 上的引脚,实现引脚复用功能配置引脚电气特性(上拉 下拉 驱动强度等),与GPIO 子系统协同工作
2、Pinctrl 与 GPIO 子系统的关系是什么?
Pinctrl 负责引脚功能配置和复用 GPIO 子系统负责引脚的输入输出控制两者协同工作,Pinctrl 先配置引脚功能,然后 GPIO 控制引脚状态
3、设备树中如何描述 Pinctrl 配置
Pinctrl 配置通常分为两部分 Pin Controller 节点 (描述引脚控制硬件)和 Device 节点 (引用 Pin Controller ,配置具体功能)
// 1. Pin Controller 节点(由 SoC 厂商提供,一般位于 .dtsi 文件)
pinctrl: pinctrl {
compatible = "rockchip,rk3568-pinctrl";
reg = <0x0 0xfdc20000 0x0 0x10000>;
// 定义 GPIO 引脚组
gpio0: gpio0 {
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
// 定义 UART2 的引脚复用配置
uart2m0_xfer: uart2m0-xfer {
rockchip,pins =
// 引脚复用为 UART2,配置电气属性
<0 RK_PD1 1 &pcfg_pull_none>, // TXD
<0 RK_PD0 1 &pcfg_pull_none>; // RXD
};
};
在设备节点引用 Pinctrl
// 2. Device 节点(在板级 .dts 文件中)
&uart2 {
status = "okay";
pinctrl-names = "default"; // 状态名
pinctrl-0 = <&uart2m0_xfer>; // 引用具体的 pinctrl 配置
};
配置 GPIO 引脚
// 定义 GPIO 引脚配置
gpio_led: gpio-led {
rockchip,pins = <0 RK_PC5 0 &pcfg_pull_none>; // GPIO0_C5 作为普通 GPIO
};
// 设备节点引用
leds {
compatible = "gpio-leds";
pinctrl-names = "default";
pinctrl-0 = <&gpio_led>;
led1: led1 {
gpios = <&gpio0 RK_PC5 GPIO_ACTIVE_HIGH>;
};
};
配置 I2C 引脚
i2c1m0_pins: i2c1m0-pins {
rockchip,pins =
<0 RK_PB3 1 &pcfg_pull_none>, // SCL
<0 RK_PB4 1 &pcfg_pull_none>; // SDA
};
&i2c1 {
pinctrl-names = "default";
pinctrl-0 = <&i2c1m0_pins>;
status = "okay";
};
配置中断引脚
gpio_key: gpio-key {
rockchip,pins = <0 RK_PA0 0 &pcfg_pull_up>; // GPIO0_A0 上拉输入
};
&gpio_keys {
pinctrl-names = "default";
pinctrl-0 = <&gpio_key>;
button1: button1 {
gpios = <&gpio0 RK_PA0 GPIO_ACTIVE_LOW>;
interrupts-extended = <&gpio0 RK_PA0 IRQ_TYPE_EDGE_FALLING>;
};
};
调试
cat /sys/kernel/debug/pinctrl/pinctrl-devices # 列出所有 Pin Controller
cat /sys/kernel/debug/pinctrl/pinctrl-maps # 查看所有引脚映射
3.4 I2C子系统
3.4.1 I2C 子系统架构
- i2c_adapter : 表示 I2C 总线控制器
- i2c_algorithm : 表示 I2C 总线传输算法
- i2c_client : 表示I2C设备
- i2c_driver : 实现I2C设备驱动
3.4.2 I2C 驱动实现
// I2C设备驱动示例
static const struct i2c_device_id my_i2c_id[] = {
{ "my-i2c-device", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_i2c_id);
static const struct of_device_id my_of_match[] = {
{ .compatible = "vendor,my-i2c-device" },
{ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
static struct i2c_driver my_i2c_driver = {
.probe = my_i2c_probe,
.remove = my_i2c_remove,
.id_table = my_i2c_id,
.driver = {
.name = "my-i2c-device",
.of_match_table = my_of_match,
},
};
static int my_i2c_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
// 检查适配器能力
if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA))
return -ENODEV;
// 分配设备数据
struct my_data *data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
i2c_set_clientdata(client, data);
data->client = client;
// 初始化设备
return 0;
}
// 注册I2C驱动
module_i2c_driver(my_i2c_driver);
3.4.3 常见问题汇总
1、I2C 子系统的主要组件有哪些
- i2c_adapter : 表示 I2C 总线控制器
- i2c_algorithm : 表示 I2C 总线传输算法
- i2c_client : 表示I2C设备
- i2c_driver : 实现I2C设备驱动
2、I2C 设备驱动如何与设备匹配
通过 i2c_driver_id表匹配设备名称通过 of_match_table 匹配设备树种的 compatible 属性匹配成功后调用 probe 函数
3、如何在 I2C 驱动中进行数据传输
-
获取 I2C 设备句柄(
struct i2c_client *
)。 -
构造消息(
struct i2c_msg
),指定读写操作、从机地址、数据缓冲区等。 -
调用传输函数(
i2c_transfer
或i2c_master_send/recv
)。
struct i2c_msg {
__u16 addr; // 从机地址(7位或10位)
__u16 flags; // 标志位(如 I2C_M_RD 表示读操作)
__u16 len; // 数据长度
__u8 *buf; // 数据缓冲区
};
#include <linux/i2c.h>
#include <linux/module.h>
static int my_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id) {
u8 buf[2];
int ret;
// 示例:读取设备ID
ret = i2c_read_reg(client, 0x00, buf, 2);
if (ret < 0) {
dev_err(&client->dev, "Failed to read ID\n");
return ret;
}
dev_info(&client->dev, "Device ID: 0x%02X%02X\n", buf[0], buf[1]);
return 0;
}
static const struct i2c_device_id my_i2c_id[] = {
{ "my_i2c_device", 0 },
{}
};
MODULE_DEVICE_TABLE(i2c, my_i2c_id);
static struct i2c_driver my_i2c_driver = {
.driver = {
.name = "my_i2c_driver",
},
.probe = my_i2c_probe,
.id_table = my_i2c_id,
};
module_i2c_driver(my_i2c_driver);
MODULE_LICENSE("GPL");
调试技巧
1、查看 I2C 设备是否注册成功
ls /sys/bus/i2c/devices/ # 列出所有 I2C 设备
2、手动读写 I2C 设备
# 安装 i2c-tools
sudo apt install i2c-tools
# 扫描 I2C 总线
i2cdetect -y 1 # 查看总线1上的设备
# 读取寄存器
i2cget -y 1 0x50 0x00 # 从地址0x50读寄存器0x00
常见问题
-
传输失败(返回 -EIO):
-
检查从机地址是否正确。
-
确认设备树中 I2C 控制器已启用(
status = "okay"
)。
-
-
时钟速率不匹配:
-
在设备树中调整
clock-frequency
(如400000
表示 400kHz)。
-
-
多消息传输顺序错误:
-
确保
i2c_msg
数组的顺序符合设备协议要求。
-
3.5 SPI 子系统
3.5.1 SPI 子系统架构
- spi_master : 表示 SPI 控制器
- spi_device : 表示 SPI 设备
- spi_driver : 表示 SPI 设备驱动
- spi_message : 表示 SPI 传输消息
- spi_transfer : 表示单次 SPI 传输
3.5.2 SPI 驱动实现
// SPI设备驱动示例
static const struct of_device_id my_spi_of_match[] = {
{ .compatible = "vendor,my-spi-device" },
{ }
};
MODULE_DEVICE_TABLE(of, my_spi_of_match);
static const struct spi_device_id my_spi_id[] = {
{ "my-spi-device", 0 },
{ }
};
MODULE_DEVICE_TABLE(spi, my_spi_id);
static struct spi_driver my_spi_driver = {
.probe = my_spi_probe,
.remove = my_spi_remove,
.id_table = my_spi_id,
.driver = {
.name = "my-spi-device",
.of_match_table = my_spi_of_match,
},
};
static int my_spi_probe(struct spi_device *spi)
{
struct my_data *data;
// 检查SPI设备配置
if (spi->max_speed_hz > MAX_SPI_SPEED)
spi->max_speed_hz = MAX_SPI_SPEED;
if (spi->mode != SPI_MODE_0)
return -EINVAL;
// 分配设备数据
data = devm_kzalloc(&spi->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
spi_set_drvdata(spi, data);
data->spi = spi;
// 初始化设备
return 0;
}
// 注册SPI驱动
module_spi_driver(my_spi_driver);
3.6 设备树与驱动匹配
3.6.1 设备树基础
/* 设备树节点示例 */
soc {
i2c@12340000 {
compatible = "vendor,my-i2c-controller";
reg = <0x12340000 0x1000>;
interrupts = <0 29 4>;
#address-cells = <1>;
#size-cells = <0>;
sensor@48 {
compatible = "vendor,my-sensor";
reg = <0x48>;
interrupt-parent = <&gpio1>;
interrupts = <20 IRQ_TYPE_EDGE_FALLING>;
};
};
spi@12350000 {
compatible = "vendor,my-spi-controller";
reg = <0x12350000 0x1000>;
interrupts = <0 30 4>;
#address-cells = <1>;
#size-cells = <0>;
flash@0 {
compatible = "vendor,my-flash";
reg = <0>;
spi-max-frequency = <50000000>;
};
};
};
3.6.2 驱动匹配机制
- compatible : 与驱动的 of_match_table 匹配
- reg 属性 : 设备地址或 ID
- status 属性 : 控制设备是否启用
3.6.3 常见面试问题
1、设备树种的 compatible 属性有什么作用
用于匹配设备与驱动格式 "厂商,设备名",可以有多个 compatible 值 ,按照顺序匹配
2、驱动如何获得设备树中的属性
of 系列函数,可以获取设备树中的相关节点信息
3、如何处理设备树中的 GPIO 描述
Uboot 相关
4.1 u-boot 的基本概念和作用
u-boot 是一段裸机引导程序,主要的功能包括
- 硬件初始化: 初始化CPU 内存控制器 时钟等硬件
- 加载操作系统:将存储设备中的操作系统加载到内存并执行
- 提供命令行界面:允许用户通过串口等接口与系统交互
- 环境变量管理: 存储和管理系统启动参数
- 外设驱动支持:提供基本的外设驱动,如网络,存储设备
- 系统恢复机制:提供系统恢复和固件更新功能
4.2 u-boot 启动流程
u-boot 启动分一下下几个阶段:
- 执行最小化硬件初始化,设置时钟和内存控制器加载 U-Boot 镜像到 RAM
- 完成更全面的硬件初始化,设备内存映射初始化串口等通信设备初始化环境变量显示启动信息
- 命令处理阶段:检查自动启动倒计时,如果倒计时中断,进入命令行界面,执行预设的启动命令
- 操作系统加载阶段:从指定存储设备加载内核镜像准备内核启动参数跳转到内核入口下执行
// SPL启动流程简化代码示例
void board_init_f(ulong dummy)
{
/* 时钟初始化 */
clock_init();
/* 串口初始化 */
serial_init();
/* DRAM初始化 */
dram_init();
/* 加载U-Boot主镜像 */
spl_load_image();
/* 跳转到U-Boot主镜像 */
jump_to_image_no_args();
}
4.3 u-boot 环境变量
u-boot 中的环境变量是一组键值对,用于配置系统启动参数和行为。
- 环境变量存储:通常存储在 Flash/EEPROM 中的特定区域,使用 CRC 校验确保完整性
- 常用环境变量:bootargs : 传递给Linux内核的启动参数 bootcmd :自动启动时执行的命令时序,网络配置参数 bootdelay:自动启动倒计时时间
- 环境变量操作命令: printenv :显示环境变量 ; setenv : 设备环境变量; saveenv :保存环境变量
// 环境变量操作示例
// 设置bootargs
setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait'
// 设置复合命令
setenv bootcmd 'mmc dev 0; fatload mmc 0:1 ${loadaddr} uImage; bootm ${loadaddr}'
// 保存环境变量
saveenv
// 执行环境变量中的命令
run bootcmd
4.4 如何自定义 U-Boot 环境变量默认值
U-Boot 环境变量配置方式
1、配置文件方式: 在 include/config/board.h 中定义 CONFIG_EXTRA_ENV_SETTINGS 或在 board 中定义 board_env_default
2、defconfig 方式:在板级 defconfig 中添加 CONFIG_ENV_VARS_YBOOT_CONFIG=y 然后添加 CONFIG_ENV_VAR_[name]="value"
// 在头文件中定义默认环境变量
#define CONFIG_EXTRA_ENV_SETTINGS \
"bootargs=console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait\0" \
"bootcmd=mmc dev 0; fatload mmc 0:1 ${loadaddr} uImage; bootm ${loadaddr}\0" \
"ipaddr=192.168.1.100\0" \
"serverip=192.168.1.1\0"
4.5 u-boot 命令系统
u-boot 命令系统是如何实现的,如何添加自定义命令
u-boot 命令系统是基于命令表实现的,每个命令都是一个结构体,包括命令名,函数指针和帮助信息
struct cmd_tbl {
char *name; /* 命令名称 */
int maxargs; /* 最大参数数量 */
int repeatable; /* 是否可重复执行 */
int (*cmd)(struct cmd_tbl *, int, int, char * const []); /* 命令处理函数 */
char *usage; /* 使用说明 */
char *help; /* 帮助信息 */
};
4.6 u-boot设备树支持
u-boot 如何使用设备树,设备树在 u-boot 中的作用是什么
u-boot 支持设备树,用于描述硬件配置信息
- u-boot 中设备树的使用:硬件平台描述驱动程序配置向 LINUX 内核传递硬件信息
- 设备树在 u-boot 中的使用 :编译时生成的 dtb 文件 U-Boot 加载设备树到内存可以在运行时修改设备树启动内核时传递设备树地址
- 设备树相关命令 : fdt addr 设置设备树工作地址 ;fdt get :获取设备树节点属性;fdt set 设置设备树节点属性 fdt print:打印设备树内容
// 设备树操作示例
// 加载设备树到内存
fatload mmc 0:1 ${fdt_addr} ${fdtfile}
// 修改设备树中的属性
fdt addr ${fdt_addr}
fdt resize
fdt set /chosen bootargs "console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait"
// 启动内核并传递设备树
bootz ${kernel_addr} - ${fdt_addr}
4.7 u-boot 调试技巧
u-boot 调试方法主要包括 打印调试 GDB 调试 环境变量调试
- 打印调试:使用 printf /debug 函数输出信息设置不同级别的调试信息
- GDB 调试:配置 CONFIG_DEBUG_UART 使用JTAG 接口连接设置断点和单步执行
- 环境变量调试:设置 bootdelay 延长启动等待时间,使用setenv 修改启动参数进行测试
- 常见调试命令: bdinfo : 现实板级信息
- md/mm : 内存查看
- mw 内存写入
- mtest : 内存测试
// 调试配置示例
#define CONFIG_BOOTDELAY 10
#define CONFIG_DEBUG_UART
#define CONFIG_LOGLEVEL 8 // 最详细的日志级别
// 板级配置文件示例 (configs/myboard_defconfig)
CONFIG_ARM=y
CONFIG_ARCH_MYVENDOR=y
CONFIG_TARGET_MYBOARD=y
CONFIG_SYS_TEXT_BASE=0x80000000
CONFIG_NR_DRAM_BANKS=1
CONFIG_ENV_SIZE=0x2000
CONFIG_ENV_OFFSET=0x100000
CONFIG_SYS_PROMPT="MyBoard> "
// 板级初始化代码示例
int board_init(void)
{
/* 设置SDRAM基地址 */
gd->bd->bi_dram[0].start = CONFIG_SYS_SDRAM_BASE;
gd->bd->bi_dram[0].size = CONFIG_SYS_SDRAM_SIZE;
/* 初始化GPIO */
gpio_init();
return 0;
}
int dram_init(void)
{
/* 初始化SDRAM控制器 */
sdram_init();
return 0;
}
4.8 u-boot 与 linux 内核交互
u-boot 可以通过多种方式向 LINUX 内核传递参数
- bootargs 环境变量 : 包含内核命令行参数通过 bootargs 环境变量设置
- 设备树 : 通过 /chosen 节点传递参数可以在运行时修改设备树
- 常用的启动参数:console :控制台设备 root : 根文件系统位置 rootfstype:根文件系统类型
// 通过bootargs传递参数
setenv bootargs "console=ttyS0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait ip=dhcp"
// 通过设备树传递参数
fdt addr ${fdt_addr}
fdt resize
fdt set /chosen bootargs "console=ttyS0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait"
4.9 在 u-boot 开发的过程中遇到过哪些常见问题,如何解决
1.卡在特定阶段:
增加调试打印,定位卡住的具体位置,检查硬件初始化代码;
2.环境变量丢失:
检查环境变量存储区域配置,验证 flash 是否成功写入
3.网络功能不正常(无法通过网络加载文件)
检查网络硬件初始化,验证网络参数配置,使用ping 命令测试网络连接
4.内存初始化问题 (内存初始化失败导致系统不稳定)
检查内存控制器配置,调整时序参数,使用 mtest 命令测试内存
5. 设备树加载失败:(无法正确加载或解析设备树):
检查设备树格式,验证加载地址 ,使用 fdt print 命令检查设备树内容
4.10 u-boot 性能优化
- 减少不必要的初始化: 禁用不需要的设备驱动精简初始化流程
- 优化内存操作:使用 DMA 进行大块内存复制,优化内存控制器配置
- 减少启动延时:减少bootdelay 禁用不必要的等待
- 编译优化L使用适当的编译优化级别减少镜像大小
4.11 u-boot 与 bootloader 安全性