🌐 一、内核基础(驱动和 BSP 的地基)

Linux 内核启动流程?(从 U-Boot / ATF 到 init)

系统上电后,SoC 会从固化的启动介质(如 SPI NOR/NAND)加载第一阶段 Bootloader,通常是 SPL(Secondary Program Loader),接着加载第二阶段的 U-Boot。

在 U-Boot 中主要做这些事:

  • 初始化 DRAM,准备运行内核的内存环境
  • 初始化串口、网络、存储等基本外设(可选)
  • 读取设备树(.dtb)、kernel 镜像(zImage/Image)、initrd
  • 最后调用 bootmbooti 命令,引导内核

可选阶段:ATF(Arm Trusted Firmware)或 OpenSBI(RISC-V)

如果是 RISC-V 架构,这一步通常由 OpenSBI 负责:

  • 设置机器模式下的一些 CSR、trap handler
  • 创建 S-mode 的运行环境(包括 timer 中断、console、PMP)
  • 提供 SBI 接口(给 S-mode 的 Linux 用)

OpenSBI 初始化完毕后,跳转到 Linux 内核的 start_kernel()

简略版本

Linux 的启动流程可以分为三大阶段:Bootloader、内核、用户空间。上电后 U-Boot 加载 kernel 和 dtb,如果是 RISC-V,还会通过 OpenSBI 设置 S-mode 环境,最后跳转到内核的 start_kernel 函数。内核初始化内存、子系统、挂载 rootfs,然后启动第一个用户态程序 /sbin/init。这整个流程我在适配某个 RK3588 平台时调过很多次,比如串口出不来我就调 console 初始化、MMC 识别不了我就看设备树和 U-Boot 的 log。

设备树是什么?它的作用?你怎么写/改过?

设备树就是内核用来描述硬件结构的数据结构,是 Linux 移植过程中很关键的一部分。我写过/改过很多设备树,比如在适配 RK3588 的时候,我给 LED 加了 GPIO-led 节点,调过串口波特率、4G 控制引脚,解决过设备不能识别、设备树 compatible 不匹配驱动等问题。

项目 修改内容
4G 模块适配 加了 UART 节点、AT 控制用 GPIO、PWRKEY 控制
LED 灯适配 加了 GPIO-led 节点,测试开关状态
RS-232 开启对应 UART 节点,添加正确引脚复用

什么是 platform_device 和 platform_driver?怎么匹配?

​ 这是 Linux 设备模型中用于描述“非自动枚举外设”(比如 GPIO 控制器、LED、SPI、I2C 设备等)的核心机制。

  • platform_device:描述一个平台设备,包含设备的资源信息(内存地址、中断号、时钟等)
  • platform_driver:驱动这个设备的驱动程序,实现 probe/remove 等回调

每个设备或驱动都会挂到一条总线上,platform_device/driver是挂到了虚拟的platform_bus上,当有驱动模块插入或者设备创建的时候,就会去调用bus->match,如果匹配就会调用到probe。

中断处理流程(从硬件中断到 ISR)

  • 中断产生
  • 响应中断
    • 保存当前上下文
    • 调转到中断入口(中断向亮表)
    • 进入trap流程,riscv是stvec
  • GIC/PLIC识别中断来源
  • linux调用handle_irq
  • 最终调用驱动注册的irq

内核空间和用户空间的通信方式?(ioctl、procfs、sysfs、netlink)

内核和用户通信方式很多,最常用的是 ioctl,适合复杂控制命令;如果是属性类信息,比如设备模式、开关等,可以用 sysfs;调试或状态类信息用 procfs;而更复杂的场景,比如异步事件、结构体交换等,则会用 netlink。我在做外设驱动时可能会ioctl 去debug,值的一提的是platform框架就提供了sysfs来修改probe相关内容。

Linux 的内存管理机制简要说一下?

  • 每个进程都有独立的虚拟空间
  • 页表管理
    • 支持多级页表,例如4级,支持huge page
    • 硬件通过MMU转换,缺页触发page fault
  • 内存分配,kmalloc小块,vmalloc大块
  • 页面回收
    • 清理cold页面
    • dirty-page写回磁盘(swap)
  • slab/slub分配器
  • 内存映射

slub / slab有什么区别

slab: /slæb/

slub: /slʌb/

slab / slub 分配器是 Linux 内核中非常核心的一部分,用于高效地分配内存,尤其是 频繁申请/释放的小对象。它们属于内核的“内存分配器”子系统,目标是比 kmalloc() 更快、更省碎片、更好对齐。

slab / slub 分配器是 Linux 内核里专门用于“频繁分配释放的小内核对象”的内存池系统。

比如你在内核里频繁创建 / 删除一个结构体,比如 struct task_struct,那 slab/slub 会帮你更快、更安全地管理内存,不用每次都向物理页框要整块内存。

为啥不用 malloc()

  • 用户空间有 malloc(),但那是给用户程序的。
  • 内核不能用它,因为内核对性能和碎片容忍度要求极高。
  • 内核自己的通用分配器是 kmalloc(),但 kmalloc() 适合中等规模、一次性用途。
  • 对于大量重复、固定大小的小结构体slab/slub 更快更稳。

slab/slub/slob 的关系(别搞混)

分配器 特点 使用场景
slab 最早期,带缓存和局部性优化,复杂 老系统,嵌入式也可能用
slub 默认分配器(大多数内核默认),简单高效 推荐,支持 NUMA,锁粒度低
slob 超简陋、给资源极少的设备用(内存极少) 小内核、嵌入式 minimal 环境

你怎么看待设备驱动中的 probe 函数?调用流程是?

  • name
  • of_match_table
  • acpi

什么是共享中断?

共享中断(Shared IRQ)是指 多个设备共享同一个中断线(IRQ 号)。这种情况在中断线资源有限或板子设计简化时很常见,尤其是 x86 架构 PCI 设备某些 ARM SoC 外设 中。

在传统中断系统中,一个中断号通常只属于一个设备

而共享中断允许多个设备注册在同一个 IRQ 上,只要它们能自己判断中断来源即可。

这样可以节省中断资源,提高硬件利用率。

使用:

  • request的时候带SHARED标志位
  • dev_id必须唯一并且为非NULL
  • irq_handler必须能判断是不是自己需要处理的中断(比如读状态寄存器)

ioremap和IOMMU

ioremap是将io物理地址映射为虚拟地址的一种机制,让驱动中可以用虚拟地址来访问实际的物理IO寄存器。

IOMMU是用来让设备访问内存的一种硬件+驱动的机制,帮助外设将虚拟地址翻译成物理地址。比如DMA控制器,网卡等。

内存管理子系统

内存管理子系统在 Linux 中负责管理物理内存和虚拟内存,确保内存的高效分配与回收。Linux 使用虚拟内存技术为每个进程提供独立的内存空间,并通过页表映射虚拟地址到物理地址。内核使用伙伴系统管理物理内存,通过 slab 分配器提高内存分配效率。在内存不足时,系统会通过页面交换(swap)将不常用的内存页写入硬盘,释放物理内存给其他进程。内存管理还包括内存保护和隔离,防止进程越界访问内存。同时,内核提供了内存监控和调优机制,如 /proc/meminfo 查看内存使用情况,和 cgroups 用于限制进程的内存使用。

作用

内存管理子系统负责管理系统的所有内存资源,包括物理内存、虚拟内存、内存映射和内存保护等。其主要任务是高效地分配、使用和回收内存资源,并确保内存访问的安全性和稳定性。

虚拟内存与物理内存的区分

  • 虚拟内存:每个进程有独立的虚拟内存空间,通过分页技术,将虚拟地址映射到物理地址,虚拟地址提供了进程隔离和内存保护。

  • 物理内存:实际的硬件ram,Linux会跟踪物理ram的使用情况,通过页面管理高效利用内存

内存分配方式

  • 伙伴系统: 用于管理物理内存块,分配时按大小分配,释放时合并空闲块。
  • slab分配器: 高效的内存分配机制,用于频繁分配和回收内存块,减少内存碎片。
  • kmalloc,vmalloc:
  • 内核分配内存的两种方式,kmalloc 用于小块内存分配,vmalloc 用于较大的内存块。

页面交换与交换空间(Swap)

页面交换:当物理内存不足时,将不常用的内存页交换到硬盘的交换空间,释放物理内存给其他进程使用。

交换空间:通常是硬盘的一块区域,用于存储交换出去的内存页,可以是交换分区(swap partition)或交换文件(swap file)。

内存映射

mmap:通过 mmap() 系统调用将文件或设备映射到进程的虚拟内存地址空间,支持文件共享和内存映射。

内存映射文件:文件可以映射到进程的虚拟内存中,允许直接在内存中操作文件内容,避免重复的磁盘 I/O 操作。

内存管理中的核心算法

页面置换算法:当内存不够用时,操作系统需要决定将哪些页面交换到硬盘,常用的算法有 LRU(最少使用)和 COW(写时复制)等。

内存压力管理:内存不足时,内核会采取措施,如杀死占用内存过多的进程,释放内存。

什么是伙伴系统

伙伴系统是 Linux 的内存管理算法,采用2的幂次方式分配内存。它通过将内存块成对分配,并在空闲时合并相邻的块,减少内存碎片,保证高效的内存使用。

举个例子,当有一个16K的空闲区域。

有一个进程申请了4K的区域,伙伴系统将这个16K切成两个8K,将其中一个8k切成2个4k。

此时将4k分配给这个进程。空闲区域变成了: 4k(used) 4k(free) 8k(free)

此时又有一个进程申请了2k的区域,伙伴系统将4k切成2个2k,然后给这个进程: 4k(used) 2k(used) 2k(free) 8k(free)

此时2k释放,变成了:4k(used) 4k(free) 8k(free)

将2个2k合并成更大的4k了。

此时4k释放,变成了16k(free)

COW

COW是Copy-On-Write,也就是写时复制。

COW 的基本思想是,在资源共享的初期,不做资源的复制,直到某个进程或线程修改了这些共享资源时,才会进行复制操作。也就是说,COW 在资源被写入时才会进行复制,避免了不必要的内存拷贝,节省了时间和空间。

  • fork时:当进程产生fork,内核会标记RW的pte,当fork的进程修改这些RW的页表的时候才会触发复制然后修改。

insmod的时候发生了什么

由于内核模块不像应用程序,默认占有自己独立的地址空间。

所以内核模块编译的时候需要生成地址无关代码,也就是需要可重定位的特性。

这里给出一个假设的模块重定位表:

Relocation section '.rela.text' at offset 0x240 contains 3 entries:
Offset Info Type Sym.Value Sym. Name
0000000000000020 00000000 R_X86_64_PC32 00000000 simple_init
0000000000000040 00000000 R_X86_64_PC32 00000000 simple_exit
0000000000000060 00000000 R_X86_64_PC32 00000000 printk
  • Offset:表示在模块中的某个位置需要进行重定位。

  • Type:重定位类型,这里是 R_X86_64_PC32,表示需要修改一个 32 位的相对地址。

  • Sym.Name:需要调整的符号的名称,例如 simple_initsimple_exitprintk

  • Sym.Value:符号的值(地址),在模块加载前是未定义的。

在模块加载时,内核根据重定位表中的信息执行以下操作:

  1. 内核加载模块到内存

insmodmodprobe 加载模块时,内核会将模块的代码段(如 .text)加载到内核地址空间中的某个位置。假设模块加载到地址 0x80000000

  1. 解析符号表

内核会解析模块中的符号表,找出所有需要重定位的符号(如 simple_initsimple_exitprintk)。这些符号在编译时并没有对应的内存地址,而是使用了相对地址或符号名称。

  1. 根据重定位表调整地址

接下来,内核会遍历重定位表,为每个需要调整的符号计算并替换地址。假设模块被加载到 0x80000000,我们可以按以下步骤调整符号:

  • **重定位 simple_init**:
    • 在重定位表中,simple_init 的偏移量是 0x20
    • 假设 simple_init 在编译时的地址是相对地址,因此它需要调整为内存中的实际地址 0x80002000(假设 simple_init 在模块的 .text 段中的实际位置)。
    • 内核会修改模块代码中的 0x20 位置,将该位置的地址调整为 0x80002000
  • **重定位 simple_exit**:
    • 同理,simple_exit 的偏移量是 0x40,需要调整为模块实际加载的地址 0x80002020
  • **重定位 printk**:
    • printk 是内核提供的函数,因此内核会根据内核符号表查找 printk 的实际地址,并将模块中的 printk 调用地址进行调整。

中断,同步,异步

中断:中断(Interrupt) 是一种硬件机制,用来打断处理器(CPU)当前正在执行的程序流,转而去响应某些紧急或重要的事件。

同步:同步是一种软件机制,用来等待一件事情的完成才能执行下一件事,也就是说发起方需要等待结果。

异步:是一种软件机制,发起操作的一方 在操作完成之前不用等待,可以先干别的事,操作完成后另有机制通知。

SoftIRQ, tasklet, workqueue

特性 softirq tasklet workqueue
层级 最底层机制 基于 softirq 的封装 基于普通内核线程
执行上下文 中断上下文(不可阻塞) 中断上下文(不可阻塞) 进程上下文(可以睡眠)
并发性 高(每 CPU 并行) 自动串行化同一个 tasklet(同一 tasklet 不并发) 有线程调度,可完全并发
适用场景 极快的、实时性要求高的处理 简单延后处理,写起来方便 需要可能阻塞的较重任务,比如磁盘IO、内存分配
例子 网络协议栈收包 网卡收发包的小处理,定时器回调 文件系统刷盘,网络协议栈底层处理

当调用触发softirq函数时,softirq会设置一个pending标志位(bitmap),中断处理器(硬中断)返回时,如果检测到 softirq_pending 非 0,就会处理。也可以主动调用 do_softirq() 来处理 pending 的 softirq,比如内核线程或者一些中断处理代码中。

如果软中断太多,do_softirq() 处理超时(通常是1ms左右的门限),就不会继续在中断上下文里做了,而是唤醒一个普通进程叫 **ksoftirqd/***,比如 ksoftirqd/0(第0个CPU)。

ksoftirqd 是一个优先级较低的内核线程,它的 main loop 里就是不断检查 pending 的 softirq,然后慢慢处理。

tasklet 是基于 softirq 封装的更简单 API,它统一用 TASKLET_SOFTIRQ 这一项,把一堆小任务链在一起处理。


📟 二、驱动开发相关

设备树节点如何转换为设备

首先dts中的node会在linux内核中被解析为一个个的device node的结构体,然后生成struct device结构体,将device node挂到struct device下的of_node成员。

随后根据不同的总线转换成不同的设备。

以platform举例:

如果一个设备想变成platform设备,需要满足以下条件:

  • 一般情况下,只对设备树中根的第1级节点(/xx)注册成platform device,也就是对它们的子节点(/xx/*)并不处理。

  • 如果一个结点的compatile属性含有这些特殊的值(“simple-bus”,”simple-mfd”,”isa”,”arm,amba-bus”)之一,并且自己成功注册成了platform_device, 那么它的子结点(需含compatile属性)也可以转换为platform_device(当成总线看待)。

  • 根节点(/)是例外的,生成platfrom_device时,即使有compatible属性也不会处理

节点的什么属性会被转换

在老版本的内核中,platform_device部分是静态定义的,其实最主要的部分就是resources部分,这一部分描述了当前驱动需要的硬件资源,一般是IO,中断等资源。

在设备树中,这一类资源通常通过reg属性来描述,中断则通过interrupts来描述;

所以,设备树中的reg和interrupts资源将会被转换成platform_device内的struct resources资源。

能不能不通过dts去match

有的兄弟,有的。

设备可以通过platform_device_allocplatform_device_add这种手段手动注册设备到内核。

此时也可以完成match,因为dts最终就是转换为了device node节点,最终给了deivce结构体,然后再根据这个dts是什么bus下的来确定转换为什么设备。

驱动模块的状态

已加载 (Loaded)

已初始化 (Initialized)

已卸载 (Unloaded)

活动 (Active)

已禁用 (Disabled)

已移除 (Removed)

等待 (Pending)

正在加载 (Loading)

正在卸载 (Unloading)

加载失败 (Failed to Load)

字符设备驱动的核心结构?(cdev、file_operations)

  • 设备号分配
  • 初始化驱动
  • 注册字符设备
  • 实现ops
    • copy_to_user
    • copy_from_user

DMA 如何使用

首先在dts中进行配置,dma0为dma-controller:

uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <5>;
dmas = <&dma0 0 0 0>, <&dma0 1 0 0>; // tx 和 rx channel
dma-names = "tx", "rx";
};

驱动中获取dma:

struct dma_chan *rx_chan;
rx_chan = dma_request_chan(dev, "rx"); // dev 是 struct device*

配置传输参数:

  • 源地址(Source Address):数据传输的起始地址。
  • 目标地址(Destination Address):数据传输的目的地。
  • 数据传输方向:数据是从外设传输到内存,还是从内存传输到外设。
  • 传输模式:如是否支持循环缓冲区,数据传输是否是单次或连续的。
struct dma_slave_config config = {
.direction = DMA_DEV_TO_MEM,
.src_addr = UART_RX_REG,
.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE,
.src_maxburst = 1,
};
dmaengine_slave_config(rx_chan, &config);

提交并准备传输

dma_async_tx_descriptor *desc;
dma_cookie_t cookie;

desc = dmaengine_prep_slave_single(rx_chan, dma_buf_phys, len,
DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
desc->callback = your_callback_func;
desc->callback_param = your_data;

cookie = dmaengine_submit(desc);
dma_async_issue_pending(rx_chan);

什么时候

需要大量数据传输或者时用DMA,可以让CPU转去做其他事情。

DMA cache一致性问题

由于主存访问对于cpu来说很慢,所以有cache在中间做缓存。

  • 当DMA启动的时候,如果数据源是内存,但此时最新的数据在cache中,此时就会导致一致性问题,所以应当先将cache中的内容刷新到主存中,再启动传输。
  • 如果目的地址是内存,传输完毕后如果cache中有这段内存的数据,cpu会直接从cache中取,而不是内存,此时就会导致不是最新的数据,造成一致性问题。此时应该先将cache中的这段内存数据标记为无效。

DMA映射

为什么需要DMA映射?在传统裸机的时候,我们配置好src,dts,width,mode其实就可以开始传输了。

为什么在这里需要多个DMA映射呢?因为在linux的世界里比较复杂,dma访问的是物理地址,不能访问虚拟地址。

如果有IOMMU,dma_map_xx做的其实就是将虚拟地址转换成经过IOMMU的地址,让dma能够正常访问。

如果没有IOMMU,dma_map_xx做的其实就是v2p,然后可能刷一下cache这样的操作。

但是注意,这里不一定是绝对的pa,不同平台的物理地址规则(比如总线地址有无偏移)也可能不同,
所以永远用标准的 dma_map_xxx(),不要自己偷懒直接拿 virt_to_phys()。

一致性映射

含义:分配一块CPU和设备都能直接访问的内存,不需要额外同步。

特点:

CPU和设备看到的数据始终是一致的。

通常是通过 dma_alloc_coherent() 分配的。

缺点:

这种内存可能带缓存属性限制(比如不可缓存),速度比普通内存慢。

用量不能太大,适合小块数据,比如描述符、控制块。

流式映射

含义:用普通内存,在要DMA之前做同步,DMA完成后再同步。

特点:

通过 dma_map_single() 或 dma_map_page() 把内存映射给设备。

在 CPU写完后,要 dma_sync_for_device(),让设备能读到。

在 设备写完后,要 dma_sync_for_cpu(),让CPU能读到。

优点:

可以用普通缓存内存,访问快。

缺点:

需要手动同步(Cache维护),操作多了容易出错。

扩展DMA映射

含义:标准 dma_map/dma_unmap 之外,Linux 还扩展了一些特殊情况的接口,比如:

dma_map_resource():直接映射 MMIO 区域。

dma_mmap_coherent():把 coherent memory 映射到用户态。

特点:

主要是为了适配不同硬件场景,比如 MMIO、用户态共享等。

总结一句话:扩展是补充标准API没法覆盖的用法。

散列DMA映射

含义:支持把一大块数据分散在不同物理页上,设备可以连续读写。

特点:

不是一大块连续内存,而是多个小块拼起来。

通过 dma_map_sg() 和 dma_unmap_sg() 完成映射。

设备要求:

必须支持SG list(scatter-gather list),就是可以一次性收一组地址。

典型场景:

网络收发大包(sk_buff 里的数据零拷贝)。

大文件DMA(比如SCSI设备传输文件)。

regmap 框架你用过没?做寄存器读写有什么坑?

regmap 是 Linux 内核提供的一种通用框架,用来简化设备寄存器的读写操作。它将底层硬件接口(如 I2C、SPI、MMIO 等)与高层驱动逻辑进行抽象和统一处理。使用者不需要再关心i2c msg的拼接,时序控制等。

坑:

  • 寄存器缓存一致性问题: regmap 提供了缓存机制来提高性能,但在一些特殊情况下,缓存与实际硬件状态可能不同步,导致读到过时的数据或写入数据未能立即反映到硬件。为了解决这个问题,可能需要手动刷新缓存,或禁用缓存(REGMAP_CACHE)。
    • regmap_sync强制刷新缓存
    • 读取硬件状态前确保缓存已经更新
  • 增量访问:某些硬件设备不支持通过自动递增的方式来访问连续的寄存器。在这种情况下,使用 regmap 的某些操作可能会失败

syscon怎么使用

很多 SoC 芯片中,一个寄存器区域可能控制着多个不同模块,而这些模块又分别由不同的驱动来控制。为了避免多驱动重复映射同一段地址(导致资源冲突),Linux 引入了 syscon 来集中管理这些共享寄存器。

在驱动中如何使用?

有很多方法可以使用,前提是获得到syscon节点,例子:

struct regmap *regmap;
regmap = syscon_regmap_lookup_by_phandle(dev->of_node, "syscon");
if (IS_ERR(regmap))
return PTR_ERR(regmap);

regmap_read(regmap, offset, &val);

simple-mfd是什么

simple-mfd 是一个“容器设备”,用于声明一个寄存器区域下还有多个子设备(比如 clock、reset、pinctrl 等)。

SoC 里经常有一些硬件模块的寄存器都集中在一块区域,比如:

  • [0x00] 是 reset 控制器的寄存器
  • [0x10] 是 clock 控制器的寄存器
  • [0x20] 是 pinctrl 控制器的寄存器

你不可能一个设备就注册三次这些地址,于是 Linux 内核提供了一个机制叫 **mfd**,专门用来声明这种「一段地址包含多个设备」的场景。

告诉 Linux:

这不是单一功能的设备,而是一段地址区域里有多个子设备。

于是 Linux 会自动把子节点当成独立的 platform device 去注册,绑定各自的驱动。

设备驱动中你做过的调试手段有哪些?怎么查 kernel log、跟踪问题?

  • kgdb:
    • vmlinux,保存调是符号
    • 开启config
    • 通过串口调试
  • ftrace: 打印系统调用
  • strace: 用户空间跟踪
  • ioctrl: 修改驱动的一些参数达到debug效果
  • sysfs:跟ioctrl起的效果差不多

怎么写一个能挂到 sysfs 的驱动调试接口?

  • 创建kobject(挂载到/sys/kernel/debug下)
  • 添加sysfs属性到到kobject
  • 实现show和store函数

pinctrl驱动

以pinctrl-k230为例。

概念

pinctrl有function,group,pin和conf的概念。

  • function:功能,用来表示这些引脚包含了哪些组,一个function包含一个或多个组
  • group:组,用来表明一个或多个引脚的集合,比如rxd和txd是传输组,cts和rts是控制组
  • pin: 引脚,没什么好说的
  • conf:引脚除了复用为某个功能,还需要一些配置,比如上拉,下拉,此时conf就有了作用

实现函数

首先需要在probe函数中parse dts,然后将dts中的节点按自己的想法转成function, group, pin, conf。

对于conf,目前pinctrl框架已经实现了通用的属性节点解析和还原。可以通过api一键解析,再也不用使用厂商自定义的字段了,除非你要求的conf没在pinctrl框架实现。

随后要填充ops:

pinctrl-ops

  • pinctrl-ops
    • get_groups_count
    • get_groups_name
    • get_groups_pins
    • dt_node_to_map
    • dt_free_map

前三个看名字就知道什么意思了,将probe阶段解析的dts返回回去就可以了。

重点是dt_node_to_map,这个函数中将pinmux和和config都转成maps。

很显然这是对于group来说的,这里要注意的是,maps的个数应该是:

for (i = 0; i < func->ngroups; ++i) {
grp_id = func->group_idx[i];
/* npins of config map plus a mux map */
map_num += info->groups[grp_id].num_pins + 1;
}

因为一个map只能对应pinmux或者conf,所以为每个引脚分配conf,然后多余的一个用来pinmux。

pinmux-ops

对于传进来的参数更新pinctrl的寄存器就可以了

pinconf-ops

跟pinmux相同

USB扫盲

USB中速率

Full-speed/high-speed/low-speed

其中full-speed能和任意一种模式结合,但high-speed和low-speed无法同时支持。

USB怎么检测连接和断开

USB HUB(root hub)的D-和D+都连接有15K下拉电阻。

  • Full-speed设备的D+引脚连接1.5K上拉电阻,当空闲D+由低到高则连接,由高到低则断开。
    • High-speed设备与Full-speed电气连接相同,断开相同。在连接的时候,当设备收到se0(复位)信号之后,会主动向hub发送K信号,表明支持High-speed
  • Low-speed设备的D-引脚连接1.5K上拉电阻,当空闲D+由低到高则连接,由高到低则断开。

当工作于high-speed的时候会断开上拉电阻,只保留hub的下拉电阻。

对于工作在high-speed模式的hub和dev,D+和D-都有45欧姆的下拉电阻,用来消除反射信号


🧩 三、BSP 适配和系统裁剪

你怎么给一个新板子适配 Linux?

你移植过哪几个平台的 BSP?都做了哪些模块的适配?

  • rk3568
    • can
    • 485
    • rtl8125
    • jl2101
    • ec20
    • intel 7260
  • a33
    • ap6212
    • r8152
  • k1
    • pmic
      • regulator
      • rtc
      • gpio
    • i2c
  • k230
    • npu
    • clock
    • eth
    • i2c
    • timer
    • watchdog
    • usb
    • thermal

什么是 device tree overlay?你什么时候会用它?

在运行时或加载阶段对基础设备树(Base Device Tree)进行扩展或修改的机制。它可以在不更改原始设备树文件的前提下,动态地添加、修改、启用设备节点。

通常是dts文件编译成dtbo。

加载:

  • uboot使用fdt apply xxx.dtbo加载
  • 内核开启overlay
    • mount -t configfs none /sys/kernel/config
    • mkdir /sys/kernel/config/device-tree/overlays/my_overlay
    • cat my_overlay.dtbo > /sys/kernel/config/device-tree/overlays/my_overlay/dtbo

Linux 启动卡在 early boot,你怎么排查?

  • 检查uboot参数
  • 检查镜像格式,内存地址设置
  • 检查是否内存覆盖
  • 加上earlycon参数
  • 检查串口配置

怎么在系统裁剪中定位无用模块?怎么控制编译 size?

  • GCOV / LCOV:编译内核/应用带上 CONFIG_GCOV,运行后看哪些模块根本没被调用过。
    • 可以生成覆盖报告,检查哪些c根本没被调用过
  • LTO:LTO,全称 Link Time Optimization(链接时优化),是一种 跨文件级别的编译优化技术。简单说,它可以让编译器在链接阶段看到整个程序(或整个内核),从而进行更深入的优化,比如函数内联、删除死代码、消除冗余符号等。
  • 去除不需要的config
    • nfs
    • 多余的文件系统
    • 多余的网络协议,如rndis等
    • 未使用的CPU/架构相关选项(defcofig会开很多)
    • GUI框架等
    • scripts/config --disable CONFIG_SOMETHING
  • modprobe -r去除不需要的模块看看是否系统稳定

🧪 四、调试和实战经验

你调过哪些 tricky 的 bug?怎么解决的?

你怎么用串口/网口调试板子?

你有没有接触过 kgdb、crash、ftrace 或 perf 这些工具?

一次调不出网口/USB/WiFi 的问题,你会怎么一步步排查?

你适配过哪些外设?网卡、串口、I2C 设备、屏幕、按键、LED?分别遇到过什么问题?


🚀 五、进阶与加分项

  1. 熟悉 device tree 编写和调试工具,比如 dtc/dumpdtb?
  2. 了解 OpenWrt / Buildroot / Yocto 哪些?能快速出系统?
  3. 有没有做过 patch 提交或者分析 mainline kernel 的提交?