本章讲的是SoC 移植者 的工作:把一颗新芯片(或一块新板)bring-up 到能跑 Linux。 这些"底层驱动"是所有上层驱动的地基------没有它们,字符/块/网络驱动都无从谈起。
7.2.5 的现实 :ARM32 的 mach-* 目录已基本消失,芯片级移植的产出物变成 设备树(.dtsi)+ 通用子系统驱动(clk/pinctrl/gpio/irqchip/dma/timer)。 本章逐个子系统讲"驱动怎么写"和"怎么在 QEMU 里验证"。
20.1 ARM Linux 底层驱动的组成和现状
组成(从底到上):
┌─────────────────────────────────────────────┐
│ 时钟驱动 (CCF) ------ 提供所有外设时钟 │
│ 复位驱动 (reset) ------ 提供复位控制 │
│ pinctrl 驱动 ------ 引脚复用/电气配置 │
│ GPIO 驱动 ------ 通用 IO │
│ 中断控制器驱动 ------ irqchip + irq_domain │
│ 时钟节拍驱动 ------ clockevent + clocksource│
│ dmaengine 驱动 ------ DMA 通道 │
│ regulator / OPP ------ 电源(见第 19 章) │
│ SMP/CPU 热插拔 ------ 多核启动与上下线 │
│ earlycon/DEBUG_LL ------ 最早期调试输出 │
└─────────────────────────────────────────────┘
现状对比:
| 方面 | 4.0 时代 | 7.2.5 |
|---|---|---|
| 移植产物 | mach-xxx/ + board-xxx.c |
.dtsi/.dts + 子系统驱动 |
| 时钟 | 平台私有 API + 部分 CCF | CCF(Common Clock Framework)统一 |
| 引脚 | mach-*/iomux-*.c |
pinctrl 子系统 |
| GPIO | 整数 GPIO 号 | GPIO 描述符 + gpiolib |
| 中断 | 直接 irq_set_chip_and_handler |
irqchip + irq_domain |
| 定时器 | 各平台私有 | clockevent/clocksource 统一 |
| DMA | 平台私有 DMA API | dmaengine 统一 |
| 设备创建 | board-*.c 静态定义 |
device tree 自动 populate |
移植者的现代工作流:
- 写
arch/<arch>/boot/dts/vendor/soc.dtsi(片上外设)。 - 写
arch/<arch>/boot/dts/vendor/board.dts(板级差异)。 - 复用内核的通用子系统驱动 (
8250串口、pl061GPIO、pl022SPI...)。 - 只为"新硬件"写驱动(新的 irqchip、新的 clk 控制器等)。
20.2 内核节拍驱动
内核需要两个"时间基准":
| 组件 | 作用 | 关键结构 |
|---|---|---|
| clocksource | 提供"现在几点"(读时间) | struct clocksource |
| clock_event_device | 提供"定时中断"(打节拍) | struct clock_event_device |
典型实现:
cpp
#include <linux/clockchips.h>
#include <linux/clocksource.h>
#include <linux/interrupt.h>
/* ---- clocksource:一个可读的计数器 ---- */
static u64 my_timer_read(struct clocksource *cs)
{
return readl(priv->base + TIMER_COUNT);
}
static struct clocksource my_clocksource = {
.name = "my-timer",
.rating = 300,
.read = my_timer_read,
.mask = CLOCKSOURCE_MASK(32),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
/* ---- clock_event:定时中断 ---- */
static int my_timer_set_next_event(unsigned long cycles,
struct clock_event_device *evt)
{
writel(cycles, priv->base + TIMER_LOAD);
writel(1, priv->base + TIMER_CTRL); /* 启动 */
return 0;
}
static int my_timer_set_state_shutdown(struct clock_event_device *evt)
{
writel(0, priv->base + TIMER_CTRL);
return 0;
}
static irqreturn_t my_timer_interrupt(int irq, void *dev_id)
{
struct clock_event_device *evt = dev_id;
writel(1, priv->base + TIMER_INTCLR); /* 清中断 */
evt->event_handler(evt); /* 通知内核 */
return IRQ_HANDLED;
}
static struct clock_event_device my_clockevent = {
.name = "my-timer",
.features = CLOCK_EVT_FEAT_ONESHOT,
.rating = 300,
.set_next_event = my_timer_set_next_event,
.set_state_shutdown = my_timer_set_state_shutdown,
.set_state_oneshot = my_timer_set_state_shutdown,
.cpumask = cpu_possible_mask,
};
static int __init my_timer_init(struct device_node *np)
{
int irq = irq_of_parse_and_map(np, 0);
clocksource_register_hz(&my_clocksource, rate);
clockevents_config_and_register(&my_clockevent, rate, 1, 0xffffffff);
request_irq(irq, my_timer_interrupt, IRQF_TIMER,
"my-timer", &my_clockevent);
return 0;
}
TIMER_OF_DECLARE(my_timer, "myvendor,my-timer", my_timer_init);
7.2.5 要点:
timer_setup(timer, callback, flags)注册普通定时器(旧init_timer()已删除)。clockevents_config_and_register()是标准注册入口。TIMER_OF_DECLARE()让定时器在设备树早期就初始化(早于常规驱动)。- 高精度定时器(hrtimer)由 clockevent 支撑。
CLOCK_EVT_FEAT_ONESHOT是 tickless 的前提。
在 QEMU 上观察:
bash
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
cat /sys/devices/system/clocksource/clocksource0/available_clocksource
cat /proc/timer_list | head -40 # 需 CONFIG_TIMER_STATS/DEBUG
dmesg | grep -i "clocksource\|timer"
20.3 中断控制器驱动
中断控制器驱动 = irqchip 驱动,由两部分组成:
- irq_chip:怎么屏蔽/使能/应答一个中断。
- irq_domain:把硬件中断号(hwirq)翻译成 Linux 虚拟中断号(virq)。
cpp
#include <linux/irq.h>
#include <linux/irqdomain.h>
#include <linux/of_irq.h>
static void my_irq_mask(struct irq_data *d)
{
struct my_irqchip *chip = irq_data_get_irq_chip_data(d);
u32 hwirq = d->hwirq;
writel(readl(chip->base + EN) & ~BIT(hwirq), chip->base + EN);
}
static void my_irq_unmask(struct irq_data *d)
{
struct my_irqchip *chip = irq_data_get_irq_chip_data(d);
writel(readl(chip->base + EN) | BIT(d->hwirq), chip->base + EN);
}
static struct irq_chip my_irq_chip = {
.name = "my-irqchip",
.irq_mask = my_irq_mask,
.irq_unmask = my_irq_unmask,
.irq_ack = my_irq_mask, /* 简化 */
};
static int my_irq_domain_map(struct irq_domain *d, unsigned int virq,
irq_hw_number_t hwirq)
{
irq_set_chip_and_handler(virq, &my_irq_chip, handle_level_irq);
irq_set_chip_data(virq, d->host_data);
irq_set_noprobe(virq); /* 不做探测(避免误触发) */
return 0;
}
static const struct irq_domain_ops my_irq_domain_ops = {
.map = my_irq_domain_map,
.xlate = irq_domain_xlate_twocell, /* 匹配 #interrupt-cells = <2> */
};
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_irqchip *chip = dev_id;
u32 pending = readl(chip->base + PENDING);
int i;
for (i = 0; i < chip->nirqs; i++) {
if (pending & BIT(i)) {
int virq = irq_find_mapping(chip->domain, i);
generic_handle_irq(virq); /* 调用上层 handler */
}
}
return IRQ_HANDLED;
}
static int my_irqchip_probe(struct platform_device *pdev)
{
struct my_irqchip *chip;
struct irq_domain *domain;
chip = devm_kzalloc(&pdev->dev, sizeof(*chip), GFP_KERNEL);
chip->base = devm_platform_ioremap_resource(pdev, 0);
chip->nirqs = 32;
domain = devm_irq_domain_create_linear(&pdev->dev,
chip->nirqs,
&my_irq_domain_ops,
chip);
if (!domain)
return -ENOMEM;
chip->domain = domain;
irq_set_chained_handler_and_data(platform_get_irq(pdev, 0),
my_irq_handler, chip);
return 0;
}
关键 API:
| API | 作用 |
|---|---|
irq_domain_add_linear() / devm_irq_domain_create_linear() |
创建线性 irq_domain |
irq_domain_add_hierarchy() |
创建级联(子)domain |
irq_create_mapping() / irq_find_mapping() |
映射/查找 virq |
irq_set_chip_and_handler() |
绑定 irq_chip 和 flow handler |
handle_level_irq / handle_edge_irq / handle_fasteoi_irq |
标准 flow handler |
generic_handle_irq(virq) |
分发到 virq |
irq_set_chained_handler_and_data() |
级联中断 |
irq_domain_xlate_twocell / _onecell |
标准 xlate |
7.2.5 要点:
irq_domain是 4.0 之后中断子系统的核心抽象,所有现代 irqchip 都用它。- 用
devm_irq_domain_create_linear()自动清理。 handle_fasteoi_irq是现代控制器(GICv3、PCI MSI)的首选 flow handler。- 级联控制器(GPIO 扩展芯片)用
irq_domain_add_hierarchy()。
在 QEMU 上观察:
bash
cat /proc/interrupts
ls /sys/kernel/irq/
cat /sys/kernel/debug/irq/irqs/1 # 需 CONFIG_GENERIC_IRQ_DEBUGFS
cat /sys/kernel/debug/irq/domains/
20.4 SMP 多核启动以及 CPU 热插拔驱动
SMP 启动流程(ARM64 为例):
Boot CPU 上电
│
解析设备树 (cpus 节点)
│
初始化 PSCI / spin-table (secondary boot method)
│
通过 PSCI CPU_ON 唤醒其它 CPU
│
从 CPU 进入 secondary_start_kernel
│
每个 CPU 初始化自己的 per-cpu、中断、timer
│
全部 online
关键点:
| 概念 | 说明 |
|---|---|
| PSCI | ARM64 标准的 CPU 上电协议(drivers/firmware/psci/) |
| spin-table | 老式 ARM 多核启动方式(已过时) |
secondary_start_kernel |
从 CPU 入口(arch 代码) |
| per-cpu 变量 | DEFINE_PER_CPU() / this_cpu_ptr() |
smp_call_function_many() |
让所有 CPU 执行同一函数 |
CPU 热插拔(见第 19.8 节):
cpp
static int my_cpu_online(unsigned int cpu) { return 0; }
static int my_cpu_offline(unsigned int cpu) { return 0; }
my_hp_state = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "my:online",
my_cpu_online, my_cpu_offline);
cpuhp_remove_state(my_hp_state);
7.2.5 要点:
CPUHP_AP_ONLINE_DYN是动态状态;需要固定顺序时用CPUHP_*常量。cpuhp_setup_state_multi()用于多实例。- CPU0 不能下线。
- 用
cpumask_*()管理 CPU 集合,避免直接操作位图。
在 QEMU 上实验:
bash
# 宿主机:-smp 4 启动
# guest 内
nproc
cat /sys/devices/system/cpu/online
echo 0 > /sys/devices/system/cpu/cpu3/online
cat /sys/devices/system/cpu/offline
echo 1 > /sys/devices/system/cpu/cpu3/online
cat /proc/interrupts # 每个 CPU 的中断列
20.5 DEBUG_LL 和 EARLY_PRINTK 的设置
问题 :内核启动最早期 (串口驱动还没加载)怎么打印调试信息? 答案:earlycon / DEBUG_LL。
| 机制 | 架构 | 说明 |
|---|---|---|
DEBUG_LL |
ARM32 专属 | 汇编级调试输出(arch/arm/Kconfig.debug) |
earlycon |
通用 | 早期控制台,比 DEBUG_LL 晚、比串口驱动早 |
EARLY_PRINTK |
x86 | 早期打印(arch/x86/Kconfig.debug) |
ARM32 的 DEBUG_LL (arch/arm/Kconfig.debug):
bash
CONFIG_DEBUG_LL=y
CONFIG_DEBUG_LL_UART_8250=y # 或其它 UART
CONFIG_DEBUG_UART_PHYS=0x09000000
CONFIG_DEBUG_UART_VIRT=0xf8090000
CONFIG_DEBUG_UART_8250_SHIFT=2
通用 earlycon(推荐,ARM64/x86 都支持):
# 命令行指定(最灵活)
earlycon=uart8250,mmio,0x9000000,115200
earlycon=pl011,mmio32,0x9000000
earlycon # 用设备树/SPCR 里的 stdout-path
cpp
chosen {
stdout-path = "serial0:115200n8";
};
QEMU 实验:
qemu-system-aarch64 -machine virt -nographic \
-kernel Image \
-append "console=ttyAMA0 earlycon=pl011,mmio32,0x9000000 keep_bootcon"
观察点 :earlycon 生效时,内核启动最开头 就有输出; keep_bootcon 让早期控制台不被注销(方便看完整日志)。
7.2.5 要点:
- ARM32 的
DEBUG_LL仍是唯一"汇编级"调试手段。 - ARM64/x86 推荐用
earlycon,无需重编译内核,命令行即可。 earlycon依赖CONFIG_SERIAL_EARLYCON=y(默认开)。EARLY_PRINTK在 x86 上对应earlyprintk命令行参数。
20.6 GPIO 驱动
GPIO 驱动 = 芯片的通用 IO 控制器驱动,基于 gpiolib。
cpp
#include <linux/gpio/driver.h>
static int my_gpio_get(struct gpio_chip *gc, unsigned int offset)
{
struct my_gpio *g = gpiochip_get_data(gc);
return !!(readl(g->base + DATA) & BIT(offset));
}
static void my_gpio_set(struct gpio_chip *gc, unsigned int offset, int val)
{
struct my_gpio *g = gpiochip_get_data(gc);
u32 reg = readl(g->base + DATA);
if (val)
reg |= BIT(offset);
else
reg &= ~BIT(offset);
writel(reg, g->base + DATA);
}
static int my_gpio_direction_output(struct gpio_chip *gc,
unsigned int offset, int val)
{
struct my_gpio *g = gpiochip_get_data(gc);
writel(readl(g->base + DIR) | BIT(offset), g->base + DIR);
my_gpio_set(gc, offset, val);
return 0;
}
static int my_gpio_direction_input(struct gpio_chip *gc, unsigned int offset)
{
struct my_gpio *g = gpiochip_get_data(gc);
writel(readl(g->base + DIR) & ~BIT(offset), g->base + DIR);
return 0;
}
static const struct gpio_chip my_gpio_template = {
.label = "my-gpio",
.owner = THIS_MODULE,
.get_direction = my_gpio_get_direction,
.direction_input = my_gpio_direction_input,
.direction_output = my_gpio_direction_output,
.get = my_gpio_get,
.set = my_gpio_set,
.set_config = my_gpio_set_config,
.base = -1, /* 动态分配 */
.ngpio = 32,
.can_sleep = false,
};
static int my_gpio_probe(struct platform_device *pdev)
{
struct my_gpio *g;
struct gpio_chip *gc;
g = devm_kzalloc(&pdev->dev, sizeof(*g), GFP_KERNEL);
g->base = devm_platform_ioremap_resource(pdev, 0);
gc = devm_kzalloc(&pdev->dev, sizeof(*gc), GFP_KERNEL);
*gc = my_gpio_template;
gc->parent = &pdev->dev;
gc->base = -1;
return devm_gpiochip_add_data(&pdev->dev, gc, g);
}
7.2.5 要点:
- 整数 GPIO 接口(
gpio_request()/gpio_get_value())已废弃/禁用 , 新代码一律用描述符接口 :devm_gpiod_get()/gpiod_get_value()/gpiod_set_value()。 gpiochip用devm_gpiochip_add_data()注册。- 中断支持:
struct gpio_irq_chip(gc->irq),支持 irq_domain 级联。 gpio-sim(drivers/gpio/gpio-sim.c,CONFIG_GPIO_SIM)是无硬件实验的关键: 通过 configfs 凭空创建 GPIO 芯片。
gpio-sim 实验:
bash
mount -t configfs none /sys/kernel/config
modprobe gpio-sim
mkdir -p /sys/kernel/config/gpio-sim/mybank
cd /sys/kernel/config/gpio-sim/mybank
echo 8 > num_lines
mkdir line0
echo "my-led" > line0/name
echo 1 > line0/consumer # 预占用
cd ..
echo 1 > mybank/live # 激活!现在有了一个 8 线的 GPIO 芯片
gpiodetect
gpioinfo
gpioset mybank 0=1
gpioget mybank 0
gpio-virtuser (drivers/gpio/gpio-virtuser.c)是 7.2.5 新增的 "用户空间 GPIO 驱动",可以从用户态完全模拟一个 GPIO 控制器 (见 Documentation/admin-guide/gpio/gpio-virtuser.rst)。
gpio-virtio (CONFIG_GPIO_VIRTIO)配合 QEMU 的 virtio-gpio 设备, 提供更真实的虚拟 GPIO。
20.7 pinctrl 驱动
pinctrl 子系统管两件事:
- pin multiplexing(引脚复用):一根物理引脚可以当 UART、I2C、GPIO......
- pin configuration(电气配置):上下拉、驱动强度、slew rate。
cpp
#include <linux/pinctrl/pinctrl.h>
#include <linux/pinctrl/pinmux.h>
#include <linux/pinctrl/pinconf.h>
#include <linux/pinctrl/pinconf-generic.h>
static const struct pinctrl_pin_desc my_pins[] = {
PINCTRL_PIN(0, "GPIO0"),
PINCTRL_PIN(1, "GPIO1"),
PINCTRL_PIN(2, "GPIO2"),
/* ... */
};
static int my_set_mux(struct pinctrl_dev *pctldev, unsigned int func_sel,
unsigned int group_sel)
{
/* 把 group_sel 的引脚切到 func_sel 的功能 */
return 0;
}
static const struct pinmux_ops my_pinmux_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 int my_pinconf_set(struct pinctrl_dev *pctldev, unsigned int pin,
unsigned long *configs, unsigned int num_configs)
{
unsigned int i;
for (i = 0; i < num_configs; i++) {
enum pin_config_param param = pinconf_to_config_param(configs[i]);
u32 arg = pinconf_to_config_argument(configs[i]);
switch (param) {
case PIN_CONFIG_BIAS_PULL_UP: /* 上拉 */ break;
case PIN_CONFIG_BIAS_PULL_DOWN: /* 下拉 */ break;
case PIN_CONFIG_DRIVE_STRENGTH: /* 驱动强度 */ break;
case PIN_CONFIG_INPUT_ENABLE: break;
case PIN_CONFIG_OUTPUT_ENABLE: break;
default: return -ENOTSUPP;
}
}
return 0;
}
static const struct pinconf_ops my_pinconf_ops = {
.pin_config_set = my_pinconf_set,
};
static struct pinctrl_desc my_pinctrl_desc = {
.name = "my-pinctrl",
.pins = my_pins,
.npins = ARRAY_SIZE(my_pins),
.pmxops = &my_pinmux_ops,
.confops = &my_pinconf_ops,
.owner = THIS_MODULE,
};
static int my_pinctrl_probe(struct platform_device *pdev)
{
return devm_pinctrl_register_and_init(&pdev->dev, &my_pinctrl_desc,
NULL, &pctldev) ?:
pinctrl_enable(pctldev);
}
设备树:
cpp
&iomuxc {
pinctrl_uart0: uart0grp {
fsl,pins = <
MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1
>;
};
};
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart0>;
};
7.2.5 要点:
- pinctrl 由内核自动应用 (
pinctrl_bind_pins()),驱动通常什么都不用做。 - 标准 pinconf 参数:
PIN_CONFIG_BIAS_PULL_UP/DOWN、PIN_CONFIG_DRIVE_STRENGTH、PIN_CONFIG_SLEW_RATE、PIN_CONFIG_INPUT_DEBOUNCE。 - 用
devm_pinctrl_register_and_init()+pinctrl_enable()。 - 常见的"通用"pinctrl 驱动:
pinctrl-single、pinctrl-stub(测试用)。
QEMU 实验 :QEMU virt 机器上的 PL061 GPIO 驱动就是一个完整的 pinctrl/gpio 组合, 可对照阅读 drivers/gpio/gpio-pl061.c。
20.8 时钟驱动
CCF(Common Clock Framework) 统一管理所有时钟。
cpp
#include <linux/clk-provider.h>
#include <linux/platform_device.h>
/* ---- 一个固定频率时钟(最简单) ---- */
static int my_fixed_clk_probe(struct platform_device *pdev)
{
struct clk_hw *hw;
u32 rate;
if (device_property_read_u32(&pdev->dev, "clock-frequency", &rate))
return -EINVAL;
hw = devm_clk_hw_register_fixed_rate(&pdev->dev, "my-fixed-clk",
NULL, 0, rate);
if (IS_ERR(hw))
return PTR_ERR(hw);
return devm_of_clk_add_hw_provider(&pdev->dev, of_clk_hw_simple_get, hw);
}
/* ---- 一个分频器时钟(PLL -> divider) ---- */
static const struct clk_div_table my_div_table[] = {
{ .val = 0, .div = 1 },
{ .val = 1, .div = 2 },
{ .val = 2, .div = 4 },
{ .val = 3, .div = 8 },
{ }
};
static int my_div_clk_probe(struct platform_device *pdev)
{
struct clk_hw *hw;
struct clk_parent_data parent = { .index = 0 };
hw = devm_clk_hw_register_divider_table(&pdev->dev, "my-div-clk",
"my-fixed-clk", 0,
base + DIV_REG, 0, 3,
my_div_table, &my_lock);
if (IS_ERR(hw))
return PTR_ERR(hw);
return devm_of_clk_add_hw_provider(&pdev->dev, of_clk_hw_simple_get, hw);
}
CCF 的时钟类型:
| 类型 | 注册函数 | 用途 |
|---|---|---|
| 固定频率 | devm_clk_hw_register_fixed_rate() |
晶振 |
| 固定因子 | devm_clk_hw_register_fixed_factor() |
简单倍/分频 |
| 分频器 | devm_clk_hw_register_divider_table() |
可编程分频 |
| 多路选择 | devm_clk_hw_register_mux() |
时钟源选择 |
| 门控 | devm_clk_hw_register_gate() |
开关时钟 |
| 复合 | devm_clk_hw_register_composite() |
上述组合 |
消费端:
cpp
struct clk *clk = devm_clk_get(dev, "apb_pclk");
clk_prepare_enable(clk);
clk_set_rate(clk, 100000000);
rate = clk_get_rate(clk);
clk_disable_unprepare(clk);
/* 7.2.5 推荐的一行式接口 */
struct clk *clk2 = devm_clk_get_enabled(dev, NULL);
/* 需要已 prepare 但未 enable 时用: */
struct clk *clk3 = devm_clk_get_prepared(dev, NULL);
7.2.5 要点:
- CCF 是 4.0 之后时钟子系统的唯一答案,平台私有时钟 API 已消失。
- 驱动用
devm_clk_get()/devm_clk_get_enabled()/devm_clk_get_prepared()。 - 提供端用
clk_hw系列注册函数 +devm_of_clk_add_hw_provider()。 - 设备树用
clocks/clock-names描述连接。 clk_summary(debugfs) 能看到整棵时钟树和每个时钟的使用者。
bash
cat /sys/kernel/debug/clk/clk_summary
cat /sys/kernel/debug/clk/clk_orphan_summary
20.9 dmaengine 驱动
dmaengine 统一了 DMA 控制器驱动和 DMA 使用者。
提供端(DMA 控制器驱动):
cpp
#include <linux/dmaengine.h>
static int my_dma_probe(struct platform_device *pdev)
{
struct dma_device *ddev;
struct my_dma_chan *ch;
int ret;
ddev = devm_kzalloc(&pdev->dev, sizeof(*ddev), GFP_KERNEL);
ddev->dev = &pdev->dev;
ddev->copy_align = DMAENGINE_ALIGN_4_BYTES;
INIT_LIST_HEAD(&ddev->channels);
dma_cap_set(DMA_MEMCPY, ddev->cap_mask);
dma_cap_set(DMA_SLAVE, ddev->cap_mask);
dma_cap_set(DMA_CYCLIC, ddev->cap_mask);
ddev->device_alloc_chan_resources = my_alloc_chan_resources;
ddev->device_free_chan_resources = my_free_chan_resources;
ddev->device_prep_dma_memcpy = my_prep_memcpy;
ddev->device_prep_slave_sg = my_prep_slave_sg;
ddev->device_prep_dma_cyclic = my_prep_cyclic;
ddev->device_issue_pending = my_issue_pending;
ddev->device_terminate_all = my_terminate_all;
ddev->device_tx_status = my_tx_status;
ddev->device_config = my_device_config;
ddev->src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
ddev->dst_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
ddev->directions = BIT(DMA_MEM_TO_MEM) | BIT(DMA_MEM_TO_DEV) |
BIT(DMA_DEV_TO_MEM);
/* 为每个通道注册 */
ret = dma_async_device_register(ddev);
if (ret)
return ret;
/* 让设备树能通过 phandle 找到这个控制器 */
return of_dma_controller_register(pdev->dev.of_node,
my_of_xlate, ddev);
}
消费端(使用 DMA 的设备驱动):
cpp
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
struct dma_chan *chan;
struct dma_slave_config cfg = {
.direction = DMA_MEM_TO_DEV,
.dst_addr = my_fifo_phys,
.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
.dst_maxburst = 8,
};
struct dma_async_tx_descriptor *tx;
dma_cookie_t cookie;
/* 1. 申请通道(设备树 dmas/dma-names) */
chan = dma_request_chan(dev, "tx");
if (IS_ERR(chan))
return PTR_ERR(chan);
/* 2. 配置 */
dmaengine_slave_config(chan, &cfg);
/* 3. 准备传输(scatter-gather) */
tx = dmaengine_prep_slave_sg(chan, sgl, sg_len,
DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);
tx->callback = my_dma_callback;
tx->callback_param = dev;
/* 4. 提交并启动 */
cookie = dmaengine_submit(tx);
dma_async_issue_pending(chan);
/* 5. 结束/清理 */
dmaengine_terminate_sync(chan);
dma_release_channel(chan);
设备树:
cpp
dma0: dma-controller@c000000 {
compatible = "myvendor,my-dma";
reg = <0x0c000000 0x1000>;
#dma-cells = <2>;
dma-channels = <8>;
};
uart0: serial@9000000 {
dmas = <&dma0 0 1>, <&dma0 1 1>;
dma-names = "tx", "rx";
};
7.2.5 要点:
| 4.0 | 7.2.5 | 说明 |
|---|---|---|
dma_request_slave_channel() |
dma_request_chan() |
返回 ERR_PTR |
dma_request_channel() |
dma_request_chan() 或 dma_request_chan_by_mask() |
|
| 手动 free | devm_dma_request_chan() |
推荐 |
dmaengine_terminate_all() |
dmaengine_terminate_sync() |
同步终止 |
| 平台私有 DMA | dmaengine 统一 |
QEMU 实验 :QEMU virt 上没有独立的 DMA 控制器,但可以:
- 阅读
drivers/dma/dw/(DesignWare DMA,很多 SoC 用)。 - 用
Documentation/driver-api/dmaengine/里的文档学习。 - 用
dmatest(CONFIG_DMA_ENGINE+dmatest模块)测试任意 DMA 控制器:
bash
modprobe dmatest
echo 0 > /sys/module/dmatest/parameters/run # 先确保停止
echo 1 > /sys/module/dmatest/parameters/run
dmesg | grep dmatest
20.10 常见错误
| 错误 | 现象 | 修正 |
|---|---|---|
init_timer() |
已删除 | 用 timer_setup() |
忘记 CLOCK_EVT_FEAT_ONESHOT |
无法 tickless | 加 feature 标志 |
不用 irq_domain 直接 irq_set_chip |
无法解析 DT 中断 | 用 irq_domain |
handle_edge_irq 用在电平控制器 |
中断丢失/风暴 | 用 handle_level_irq |
| 整数 GPIO API | 已禁用 | 用 gpiod_*() |
| 驱动里手动 apply pinctrl | 重复配置 | 内核自动应用 |
clk_get() 不配对 clk_put() |
引用泄漏 | 用 devm_clk_get*() |
clk_prepare() 不配对 clk_unprepare() |
警告 | 用 devm_clk_get_enabled() |
dma_request_slave_channel() |
已弃用 | 用 dma_request_chan() |
dmaengine_terminate_all() |
已弃用 | 用 dmaengine_terminate_sync() |
| CPU0 热插拔 | 拒绝/崩溃 | CPU0 不可下线 |
| earlycon 地址写错 | 无早期输出 | 核对 UART 物理地址 |
20.11 本章实验清单
| 实验 | 目的 | 关键观察 |
|---|---|---|
| clocksource 观察 | 学习时间基准 | current_clocksource |
/proc/interrupts |
学习中断统计 | 各 CPU 中断计数 |
| irq domain 调试 | 学习 irqchip | /sys/kernel/debug/irq/domains/ |
SMP -smp 4 |
学习多核启动 | nproc/online |
| CPU 热插拔 | 学习上下线 | cpu3/online |
| earlycon | 学习早期调试 | 启动最开头有输出 |
| gpio-sim | 学习 gpiolib | gpiodetect/gpioset |
| gpio-virtuser | 学习用户态 GPIO | 文档实验 |
| clk_summary | 学习 CCF | 时钟树与使用者 |
| pinctrl 阅读 | 学习引脚复用 | gpio-pl061.c |
| dmatest | 学习 dmaengine | `dmesg |
20.12 总结
- 芯片级移植在 7.2.5 的产物是设备树 + 通用子系统驱动,不再是板级文件。
- 时间 由 clocksource(读时间)+ clockevent(打节拍)组成,用
clockevents_config_and_register()和TIMER_OF_DECLARE()。 - 中断 由 irqchip(怎么操作)+ irq_domain(号怎么映射)组成,
irq_domain是核心抽象。 - SMP 在 ARM64 靠 PSCI 启动从核;CPU 热插拔用
cpuhp_setup_state()。 - earlycon 是无重编译的早期调试手段;ARM32 还有汇编级
DEBUG_LL。 - GPIO 用 gpiolib + 描述符接口;
gpio-sim/gpio-virtuser提供无硬件实验。 - pinctrl 管复用与电气配置,内核自动应用,驱动通常无需干预。
- CCF 统一时钟管理,提供端用
clk_hw注册,消费端用devm_clk_get*()。 - dmaengine 统一 DMA,提供端实现
dma_device,消费端用dma_request_chan()。 - 无开发板时,QEMU
virt+gpio-sim+dmatest+ debugfs 足以覆盖本章全部知识点。