20-Linux 芯片级移植及底层驱动

本章讲的是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

移植者的现代工作流:

  1. 写 arch/<arch>/boot/dts/vendor/soc.dtsi(片上外设)。
  2. 写 arch/<arch>/boot/dts/vendor/board.dts(板级差异)。
  3. 复用内核的通用子系统驱动 (8250 串口、pl061 GPIO、pl022 SPI...)。
  4. 只为"新硬件"写驱动(新的 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 驱动,由两部分组成:

  1. irq_chip:怎么屏蔽/使能/应答一个中断。
  2. 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 子系统管两件事:

  1. pin multiplexing(引脚复用):一根物理引脚可以当 UART、I2C、GPIO......
  2. 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 总结

  1. 芯片级移植在 7.2.5 的产物是设备树 + 通用子系统驱动,不再是板级文件。
  2. 时间 由 clocksource(读时间)+ clockevent(打节拍)组成,用 clockevents_config_and_register() 和 TIMER_OF_DECLARE()。
  3. 中断 由 irqchip(怎么操作)+ irq_domain(号怎么映射)组成, irq_domain 是核心抽象。
  4. SMP 在 ARM64 靠 PSCI 启动从核;CPU 热插拔用 cpuhp_setup_state()。
  5. earlycon 是无重编译的早期调试手段;ARM32 还有汇编级 DEBUG_LL。
  6. GPIO 用 gpiolib + 描述符接口;gpio-sim/gpio-virtuser 提供无硬件实验。
  7. pinctrl 管复用与电气配置,内核自动应用,驱动通常无需干预。
  8. CCF 统一时钟管理,提供端用 clk_hw 注册,消费端用 devm_clk_get*()。
  9. dmaengine 统一 DMA,提供端实现 dma_device,消费端用 dma_request_chan()。
  10. 无开发板时,QEMU virt + gpio-sim + dmatest + debugfs 足以覆盖本章全部知识点。
相关推荐
Snasph2 小时前
12- Linux 设备驱动的软件架构思想
linux 驱动学习
Snasph3 小时前
23-从 Linux 4.0 到 7.2.5 的迁移
linux 驱动学习
Snasph11 天前
01-设备驱动概述与开发环境构建
linux 驱动学习
Snasph11 天前
05-Linux 文件系统与设备文件
linux 驱动学习
Snasph11 天前
04-Linux 内核模块
linux 驱动学习