五月份嵌入式面试总结
1、sdk 编译 烧录 启动 调试串口2、屏幕驱动正常工作 demo正常启动。
目录
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的联系与区别
u-boot 如何使用设备树,设备树在 u-boot 中的作用是什么
4.9 在 u-boot 开发的过程中遇到过哪些常见问题,如何解决
4.11 u-boot 与 bootloader 安全性(u-boot在系统安全方面有哪些考虑,如何增强u-boot 的安全性)
5.4.5 uart 通信中的波特率误差是如何产生的,怎么减少误差
5.4.8 uart 通信中如何处理数据帧错误,常见的错误类型有哪些
1、札记
1.1、芯片的bring up 主要做哪些工作:
1、sdk 编译 烧录 启动 调试串口
2、屏幕驱动正常工作 demo正常启动
1.2 static 修饰局部变量 全局变量 函数的作用
1.2.1 static 修饰局部变量
- 改变局部变量的存储位置(栈区->静态存储区)和生命周期(函数调用期间->整个程序运行期间),但是不改变其作用域
1.2.2 static修饰全局变量
- 限制全局变量的作用域,使其仅在定义他的源文件内可见,不可以通过extern 引用
- 普通全局变量跨文件可见,static 可以避免命名冲突
1.2.3 static 修饰函数
- 限制函数的作用域,使其仅在定义他的源文件内可见,其他文件无法调用
- 普通函数跨文件可见,而 static 函数可以避免命名冲突,实现隐藏
1.3 const + volatile 关键字
const 表示变量是 只读 的,编译器会禁止代码对其直接修改,但是可以通过指针强制修改,或者是硬件修改
volatile 禁止编译器对其优化
const volatile 结合时,表示 变量本身不能被程序代码修改(const 特性 ),但是可能会被外部因素修改(volatile)
编译器会:禁止代码中显示修改该变量(触发编译错误)
禁止优化对该变量的访问(每次必须从内存读取)
1.4 strcpy strlen sizeof memcpy memset
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 安全性(u-boot在系统安全方面有哪些考虑,如何增强u-boot 的安全性)
- 安全启动:实现镜像签名验证,使用硬件安全模块实现安全启动链
- 防回滚保护:实现版本检查机制防止降级攻击
- 敏感数据保护:加密存储敏感环境变量实现安全存储区域
- 命令访问控制:实现命令权限管理禁用危险命令
- 调试接口保护:生产环境禁用 JTAG 调试接口实现调试授权机制
// 安全配置示例
#define CONFIG_FIT_SIGNATURE // 启用FIT签名
#define CONFIG_BOOTDELAY -1 // 禁用自动启动倒计时
#define CONFIG_CONSOLE_MUX // 控制台多路复用
#define CONFIG_CONSOLE_RECORD // 记录控制台活动
#define CONFIG_ENV_AES // 环境变量AES加密
#define CONFIG_ENV_ENCRYPTED // 启用环境变量加密
UART 协议
5.1 uart协议与其基本特点
- uart(通用异步收发器)是一种串行通信协议
- 异步通信,不需要时钟线
- 全双工通信,可以同时发送和接收数据
- 点对点通信,一般只连接两个设备
- 数据格式灵活可配置
5.2 uart 通信需要哪些数据线,每条线得作用是什么
- TX 发数据
- RX 收数据
- 流控线 : RTS(发送请求):表示本设备准备好接收数据 CTS(清除发送):表示对方设备准备好接收数据
5.3 uart 帧格式
uart 得标准帧格式主要包括
- 起始位:1位 固定为低电平,表示帧开始
- 数据位:5-9位(通常为 8 位),LSB 先发送
- 校验位 : 0-1 位 用于错误检测
- 停止位:1-2位 固定为高电平,表示帧结束
5.4 uart 配置和参数
5.4.1 波特率
常用波特率:
- 9600 bps : 低速通信,稳定性好
- 19200 bps : 中等速度
- 38400 bps :中高速
- 57600 bps : 高速
- 115200 bps :常用高速
- 230400 bps \ 460800bps 、921600 bps 高速特殊应用
选择考虑因素:
- 通信距离:距离越长,波特率应该越低
- 抗干扰要求:波特率越高,抗干扰性越好
- 数据吞吐量需求:高吞吐量需要高波特率
5.4.2 uart 有哪些常见的校验方式,各有什么特点
- 无校验:不进行校验,数据帧最短
- 奇校验:数据位加校验位中1的总数为奇数
- 偶校验:数据位加校验位中1的总数为偶数
- 标记校验:校验位固定为1
5.4.3 uart如何实现异步通信,如何保证数据同步
异步通信原理:不使用时钟线,发送方和接收方各自使用本地时钟通过起始位和停止位标记数据帧边界
同步机制:起始位触发接收方开始采样,接收方通常使用比波特率高的时钟(如 16倍 )进行过采样在每个位的中间点采样,减少误差影响,双方必须预先约定相同的波特率和帧格式。
5.4.4 什么是过采样,在uart 中作用是什么
- 过采样:接收方 使用 比 波特率高的频率对信号进行多次采样
- 常见的过采样率:16倍
- 作用 :提高抗噪声能力,精确定位每个位的中间点减少时钟误差的影响提高接收可靠性
5.4.5 uart 通信中的波特率误差是如何产生的,怎么减少误差
误差来源:
- 时钟频率偏差
- 分频系数取整误差
- 温度漂移
- 电源波动
减少误差的方法
- 使用高精度晶振
- 选择合适的时钟频率,使分频系数为整数
- PLL倍频获得更精确的时钟
- 自动波特率检测和校准
5.4.6 UART 接收方式(中断和轮询)
中断:
- 优点:CPU 利用率高,实时性好,不会丢失数据
- 缺点:中断处理开销,中断上下文限制
- 适用:数据量大或者不可预测的场景
轮询:
- 优点:实现简单,无中断开销
- 缺点:CPU利用率低,可能丢失数据
- 适用:数据量小且可预测的场景
DMA方式:
-
优点:CPU负担小,适合大量数据传输
- 缺点:配置复杂,资源占用多
- 使用:高速大数据量的传输场景
5.4.7 UART 环形缓冲区
- 环形缓冲区:首尾相连的固定大小的缓冲区,用于临时存储接收或发送的数据
- 实现要点:使用头尾指针管理数据头指针指向下一个写入位置,尾指针指向下一个读取位置,主要是需要实现循环需要考虑缓冲区满和空的判断条件
5.4.8 uart 通信中如何处理数据帧错误,常见的错误类型有哪些
常见错误类型:
- 帧错误:停止位不是高电平
- 校验错误:校验位计算结果不匹配
- 溢出错误:接收缓冲区满但仍有新数据进来
- 噪声错误:在采样点检测到噪声
错误处理方法
- 错误标志检测和清除
- 数据丢弃或标记
- 请求重发机制
- 错误统计和报告
5.4.9 uart的硬件流控,如何实现
硬件流控制 : 通过额外的信号线控制数据流,防止数据溢出
RTS/CTS 流控制:RTS(请求发送):接收方准备好接收数据时拉低 CTS (清除发送):发送方在 CTS 为低时才发送数据
实现方式:硬件自动控制:MCU 的 UART 外设自动处理软件控制:程序 控制 GPIO 引脚实现
5.4.10 提高 UART 的 抗干扰能力
- 降低波特率,增加信号稳定性
- 使用屏蔽线缆减少外部干扰
- 添加滤波电容消除高频噪声
- 使用差分信号传输
- 实现数据校验和重传机制
- 优化 PCB 布局,避免干扰源
5.4.11 uart 调试与故障排除
调试方法:
- 示波器观察波形信号,检查时序和电平
- 逻分抓捕完整信号
- 串口调试助手
- 环回测试验证硬件功能
- 调试日志
常见问题和解决方法:
- 无法通信:检查 TX / RX 是否交叉连接验证波特率,数据位,校验位,停止位。
- 数据错误:检查波特率误差是否过大
- 数据丢失:增加缓冲区大小优化中断处理速度考虑使用硬件流控实现 DMA 传输
- 通信不稳定 : 降低波特率增强抗干扰措施,检查电源质量
5.4.12 uart 性能和可靠性测试
- 吞吐量:单位时间内可靠传输的数据量
- 误码率:发送的已知数据并验证接收数据的正确性
- 长时间稳定性测试:持续通信测试,检查是否有间歇性故障
- 环境适应性测试:在不同温度,湿度,振动条件下测试
- 极限测试:测试最高可靠波特率和最大通信距离
SPI 协议
6.1 SPI 协议
6.1.1 SPI 基本原理
- SPI 是一种同步串行通信协议
- 全双工,可以同时收发数据
- 主从架构,一个主设备对应多个从设备
- 没有复杂的寻址机制,使用专用的片选选择从设备
- 没有应答机制,主设备无法确认从设备是否正确接收数据
6.1.2 SPI 需要哪些线,每条线的作用是什么
- SCLK(Serial Clock): 时钟信号,由主设备产生
- MOSI :
- MISO:
- CS
6.1.3 SPI 与 I2C UART 相比的优缺点
优点:
- 速度快,可以达到几十Mhz
- 全双工通信,效率高
- 协议简单,硬件实现容易
- 无须寻址开销,传输效率高
缺点:
- 需要更多的信号线(至少 4根 )
- 没有应答机制,无法确认数据是否接收正确
- 通信距离有限
- 多从设备时,需要多条片选线,引脚占用多
6.1.4 SPI 的工作模式。有什么区别
SPI 有四种工作模式:由CPOL 和 CPHA 决定
- CPOL (clock Polarity):时钟极性,决定空闲状态下 SCLK 的电平,CPOL=0:空闲状态电平为0
- CPHA(clock phase):时钟相位,决定数据采样的时刻,CPHA=1,表示再第一个时钟边沿采样
6.1.5:如何选择 SPI 的工作模式
- 根据从设备的要求选择,不同的从设备可能支持不同的模式
- 查阅从设备的数据手册,确定其支持的 SPI 模式
6.1.6 SPI 基本通信时序与实现
时序:
- 主设备将对应的从设备的 cs 线拉低
- 主设备开始产生时钟信号
- 主设备通过 MOSI 线发送数据,同时可以通过 MISO 线接收数据
- 数据传输完成后,主设备停止时钟,将 CS 线拉高
6.1.7 SPI 通信中数据是如何传输的
- 数据传输是基于移位寄存器的
- 主设备和从设备各有一个 8 位移位寄存器
- 每个时钟周期,两个寄存器同时完成移位一位
- 8 个时钟周期后,主从设备完成一个字节的交换
- 通常 MSB (最高位有效)先传输
6.1.8 SPI 性能和优化
1、影响因素
- 主设备的时钟频率设置
- 信号线的长度和布线设置
- 设备之间的电平兼容性
- 从设备的最大支持频率
- 软件实现效率(位带操作,DMA 等)
2、如何提高效率
- 硬件优化:使用更高的时钟频率(在设备支持的范围内)优化PCB布线,减少干扰,使用适当的上拉,下拉电阻确保信号质量
- 软件优化:使用DMA进行数据传输,减少 CPU 干预批量传输数据,减少CS 切换开销使用中断而非轮询方式处理数据优化缓冲区管理,减少数据拷贝
6.1.9 SPI 中常见的问题以及解决方法
- 时序问题:数据传输错误或者不稳定 (确认正确的工作模式,检查时钟频率是否过高)
- 多主机冲突:总线数据混乱 (SPI 不支持多主机,需要额外的仲裁机制)
- 长线传输问题: 高速时数据传输错误解决 (降低时钟频率,使用差分信号)
- 电平不兼容:通信不稳定或者无法通信 (使用电平转换器匹配不同设备的电平要求)
6.1.10 实际项目中如何选择 SPI 的时钟频率
- 查看所有从设备的最大支持频率,选择不超过最低的频率
- 考虑信号完整性,长距离传输需要降低频率
- 根据实际需要在可靠性和速度之间权衡
- 可以为不同设备设置不同的频率,在切换设备时从新配置
6.1.11 如何处理不同 SPI 设备的模式差异
- 使用软件控制,在访问不同设备前重新配置 SPI 控制器
- 创建设备抽象层,封装每个设备的特定配置
- 使用状态机管理设备切换和配置变更
- 在片选信号切换时,留出足够的延时,确保配置生效
6.1.12 编程和调试技巧
1、编写可靠的 SPI 驱动
- 实现设备抽象,封装硬件细节
- 使用状态机管理通信过程
- 实现超时机制,防止死锁
- 添加错误检测和恢复机制
- 考虑线程安全,特别是在多任务系统中
2、如何调试 SPI 通信问题
- 使用逻分观察实际波形信号
- 检查时钟频率,相位和极性设置
- 验证片选信号的正确操作
- 使用环回测试验证硬件功能
- 分布调试,先确保通信,再实现复杂功能
3、在嵌入式 RTOS 环境中,如何管理 SPI 资源
- 使用互斥量或者信号量保护 SPI 总线访问
- 实现设备管理器,处理多个任务能及时访问 SPI
- 优化任务优先级,确保关键任务能及时访问 SPI
- 考虑使用消息队列缓冲 SPI 请求,减少任务切换开销
- 实现超时机制,防止某个任务长时间占用 SPI 资源
I2C 协议
7.1 为什么I2C 需要上拉电阻
- 实现线与逻辑:I2C总线采用线与逻辑,多个设备共享一条总线,上拉电阻将总线拉到高电平(空闲状态),任何设备都可以通过将总线拉低来发送信号
- 开漏/开集输出:I2C设备只能主动拉低总线,不能主动拉高,上拉电阻提供将总线拉高的能力
- 防止总线冲突:当多个设备同时访问总线时,上拉电阻配合开楼输出可以防止短路的情况。
- 定义总线空闲,上拉电阻确保总线在无设备通信时,保持高电平
- 典型的上拉电阻值为 4.7k,但是具体值需要根据 总线电容,通信速率,设备数量,电源电压来选择
7.2 为什么 I2C 需要选择开漏输出
- 实现线与功能:开漏输出允许多个设备共线同一条总线但是不造成冲突
- 冲裁机制:在多主机环境中,开漏结构允许实现无损仲裁
- 时钟同步:开漏结构使从设备可以通过拉低 SCL 线来延长时钟周期(时钟拉伸),实现速度较慢的从设备与主设备的同步
7.3 I2C的地址位数是多少
- 7位地址格式(最常用),地址范围 0x00-0x7f (128 个地址 )实际可用地址为 112 个(传输时占用一个字节 : 7 位地址 + 1位读写标志 )
- 10 位地址格式 (扩展):地址范围 0x000-0x3fff (1024 个地址 )传输时占用两个字节,第一个字节 1111 0xx + R/W 第二个字节::剩余的 8 位地址
7.4 I2C 的通信速率有哪些
- 标准模式 (100 Kbps )
- 快速模式 (400 Kbps)
- 快速模式 Plus (1 Mbps)
- 高速模式 (3.4 Mbps)
- 超快速模式 (5 Mbps )
- 选择速度时需要考虑 : 总线电容,线路长度,抗干扰要求,设备兼容性
7.5 I2C 的基本时序是什么
- 起始条件 : SCL 高电平,SDA 从高电平切换到低电平表示通信开始
- 停止条件 : SCL 高电平 SDA 从低电平切换到高电平表示通信结束
- 数据传输:SCL 低电平时,SDA可以变化,SCL 高电平时,SDA必须保持稳定(数据有效,大端序)
- 应答机制(ACK/NACK):每传输 8位数据后,接收方需要给 ACK (低电平)表示接收成功
7.6 I2C如何实现多主机通信
- 总线仲裁:监测 SDA 线电平进行仲裁,尝试发送高电平,但监测到低电平的主机会失去仲裁权(低电平优先原则)
- 时钟同步:所有主机的 SCL 输出都是开漏,最慢的时钟会控制总线时钟频率,通过监测 SCL 实际电平实现同步
- 冲突检测:主机在发送每位数据的时候都会检查 SDA 实际电平,如果检测到的电平和期望的电平不同,就会失去仲裁权
7.7 I2C的时钟同步和时钟拉伸是什么
时钟同步:
- I2C 总线上的 SCL 是所有设备的 逻辑与 的结果
- 当任何设备将 SCL 拉低,总线 SCL 就为低
- 只有所有设备都释放 SCL (高阻态)。SCL 才会拉高
- 这也确保了最慢的设备也能跟上通信节奏
时钟拉伸:
- 从设备可以通过保持 SCL 为低电平来延长时钟周期
- 主要用于从设备需要更多时间来处理数据的情况
- 主设备必须等待 SCL 实际变为高电平后才能继续
- 这是 I2C协议中从设备控制通信速度的机制
7.8 I2C通信中常见的问题和解决方案
-
总线死锁:
SDA 或者 SCL 被某个设备一直拉低(解决方案,软件复位,主机产生 9 个时钟脉冲,尝试完成被中断的传输硬件复位:复位所有 I2C 设备电源循环,关闭再打开)
2.地址冲突
多个设备使用相同地址解决方案:使用带地址选择引脚的期间
3.时序问题
高速通信时数据错误解决方案:减小上拉电阻值(注意功耗增加)。减少总线电容(缩短线长,减少设备数量)降低通信速率
4.噪声干扰
通信不稳定,偶发错误解决方案 : 优化 PCB 布局,避免 I2C 线与高速信号线并行
7.9 计算 I2C 总线的上拉电阻的合适值
速度越快,电阻越低(通常是 4.7 KΩ)
7.10 I2C 与 SPI 的对比和选择依据
I2C 优势:
- 只需要两根信号线
- 支持多主机,多从机
- 内置寻址机制
- 支持时钟拉伸(适应不同速度设备)
I2C 劣势
- 速度较慢
- 协议开销较大
- 实现复杂度高
选择 I2C 场景
- 系统中有多个相同类型的设备需要连接
- PCB 空间或引脚资源受限
- 通信速度要求不高
- 需要标准化的协议
更多推荐




所有评论(0)