2. R-Car平台概述

说实话,很多做单片机的朋友刚转过来做嵌入式 Linux 驱动,第一关就卡在环境搭建上。不是工具链装不上,就是模块加载报错,折腾一两天还没见到 Hello World。我当年也踩过不少坑,今天就把这些经验掰开揉碎讲给你听。

2.1 交叉编译工具链配置

为什么需要交叉编译?因为你的开发机是 x86 架构,而目标板是 ARM(或 RISC-V、MIPS)。你想想看,在 x86 上编译出来的二进制,ARM 处理器根本跑不了。所以我们需要一套能在 x86 上生成 ARM 指令的工具链。

2.1.1 工具链的选择

我个人习惯用 Linaro 提供的 GCC 工具链,稳定且社区活跃。当然,SoC 厂商(如 NXP、TI、Rockchip)也会提供定制版,里面可能包含一些硬件加速库,建议优先用官方的。

工具链来源 适用场景 注意事项
Linaro GCC 通用 ARM 开发 版本更新快,需匹配内核版本
SoC 厂商 SDK 特定芯片平台 包含 BSP 和优化库,推荐首选
Buildroot / Yocto 完整系统构建 学习曲线陡,但一劳永逸
发行版自带(如 apt) 快速验证 版本可能过旧,慎用

2.1.2 安装与配置

以 ARM64 为例,下载 Linaro 工具链后,解压到 /opt 目录:

wget https://releases.linaro.org/components/toolchain/binaries/latest/aarch64-linux-gnu/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz
sudo tar -xvf gcc-linaro-*.tar.xz -C /opt/

然后配置环境变量。我建议写到 ~/.bashrc 里,别每次开终端都 export 一遍:

export CROSS_COMPILE=aarch64-linux-gnu-
export ARCH=arm64
export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin
小技巧: 验证工具链是否生效,直接跑 aarch64-linux-gnu-gcc --version。如果输出版本信息,说明配置成功。我曾经遇到过 PATH 写错位置导致死活找不到编译器,折腾了半小时才发现是路径少了个斜杠。

2.2 设备树基础

设备树(Device Tree)说白了就是描述硬件信息的配置文件。以前内核里塞满了各种板级文件(board-*.c),每次换硬件都要重新编译内核。现在好了,设备树把硬件描述和内核代码解耦,换板子只需要改 dts 文件。

2.2.1 设备树文件结构

一个典型的设备树包含三个文件类型:

  • .dts:设备树源文件,人类可读的文本格式
  • .dtsi:设备树包含文件,类似 C 语言的 .h 头文件
  • .dtb:编译后的二进制文件,内核直接使用

编译命令很简单:

dtc -I dts -O dtb -o myboard.dtb myboard.dts

2.2.2 一个简单的设备树示例

假设我们有一个多核异构 SoC,包含两个 Cortex-A72 核和一个 Cortex-M4 核。设备树可以这样描述:

/dts-v1/;

/ {
    model = "My Heterogeneous SoC Board";
    compatible = "mycompany,myboard";

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

        cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a72";
            reg = <0x0>;
        };

        cpu@1 {
            device_type = "cpu";
            compatible = "arm,cortex-a72";
            reg = <0x1>;
        };

        cpu@2 {
            device_type = "cpu";
            compatible = "arm,cortex-m4";
            reg = <0x100>;
        };
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x0 0x80000000 0x0 0x80000000>;
    };

    chosen {
        bootargs = "console=ttyAMA0,115200 root=/dev/mmcblk0p2";
    };
};
注意: 设备树里的 reg 属性,地址和大小由 #address-cells 和 #size-cells 决定。很多新手在这里写错,导致内核无法正确识别内存范围。我曾经调试一个板子,内存始终只有 256MB,查了两天才发现是 cells 写成了 2 但实际只给了 1 个值。

2.3 内核模块编译与加载

驱动开发最常用的方式就是编译成内核模块(.ko 文件),这样可以动态加载卸载,不用每次都重新烧写整个内核。嗯,这里要注意,模块编译依赖内核源码树,不是随便找个 gcc 就能编译的。

2.3.1 编写一个简单的模块

先来个 Hello World:

// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Heterogeneous World!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Heterogeneous World!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module for heterogeneous SoC");

2.3.2 Makefile 的写法

这是关键,很多新手在这里翻车:

obj-m := hello.o

KDIR := /path/to/your/kernel/source
PWD := $(shell pwd)

all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

编译时执行:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-

2.3.3 模块加载与卸载

把生成的 hello.ko 拷贝到目标板上,然后:

# 加载模块
insmod hello.ko

# 查看模块信息
lsmod | grep hello

# 查看内核日志
dmesg | tail

# 卸载模块
rmmod hello
避坑指南: 我曾经在加载模块时遇到 Unknown symbol 错误,原因是内核版本和模块编译时的内核版本不一致。解决办法:确保 KDIR 指向目标板正在运行的内核源码树,或者使用 modprobe 自动处理依赖。

2.4 远程调试环境搭建

驱动开发免不了要调试。直接在目标板上用 printk 打日志效率太低,我建议搭建远程调试环境,用 GDB 配合 KGDB 或 JTAG 进行源码级调试。

2.4.1 KGDB 调试

KGDB 允许你通过串口或网络远程调试内核。配置内核时需要开启:

  • CONFIG_KGDB=y
  • CONFIG_KGDB_SERIAL_CONSOLE=y
  • CONFIG_KGDB_KDB=y

启动时在 bootargs 里添加:

kgdboc=ttyAMA0,115200 kgdbwait

然后在开发机上:

aarch64-linux-gnu-gdb vmlinux
(gdb) target remote /dev/ttyUSB0
(gdb) continue

2.4.2 网络调试(kgdboe)

串口速度太慢,我更喜欢用网络调试。配置内核开启 CONFIG_KGDBOE,然后:

echo "kgdboe@eth0" > /sys/module/kgdboc/parameters/kgdboc

开发机上:

aarch64-linux-gnu-gdb vmlinux
(gdb) target remote 192.168.1.100:1234
个人经验: 网络调试时,建议用交叉网线直连开发机和目标板,避免路由器干扰。我曾经在交换机上调试,结果 GDB 频繁断连,最后发现是交换机把调试包当成了广播风暴给丢了。

2.5 本章知识体系

下面这张图总结了驱动开发环境搭建的核心流程:

驱动开发环境搭建流程 驱动开发环境搭建核心流程 交叉编译工具链 aarch64-linux-gnu-gcc 设备树 .dts → .dtb 内核模块 .ko 编译与加载 远程调试 KGDB / JTAG 目标板运行 ARM / RISC-V SoC 关键点 • 工具链版本匹配内核 • 设备树 reg 属性正确 • 模块编译依赖内核源码

从图中可以看出,交叉编译工具链是基础,设备树描述硬件,内核模块实现驱动逻辑,远程调试则是验证手段。四者环环相扣,缺一不可。

2.6 常见问题与避坑

问题现象 可能原因 解决方案
insmod 报错 "Invalid module format" 内核版本不匹配 检查 uname -r 和编译时的内核版本
设备树编译报错 "syntax error" 缺少分号或括号不匹配 用 dtc -I dts -O dtb -o /dev/null 检查语法
GDB 连接不上目标板 串口参数错误或网络不通 先用 minicom 测试串口,再用 ping 测试网络
模块加载后无任何输出 printk 级别太低或日志被过滤 检查 /proc/sys/kernel/printk 设置
重要提醒: 多核异构场景下,不同核(如 A72 和 M4)可能使用不同的工具链。A72 用 aarch64-linux-gnu,M4 可能用 arm-none-eabi。千万别搞混了,否则编译出来的模块加载上去就是段错误。我见过一个同事把 M4 的固件当成内核模块加载到 A72 上,结果系统直接 panic。

好了,环境搭建这部分就讲到这里。工具链配好、设备树写对、模块能编译加载、远程调试能连上,你的驱动开发之路就正式开始了。接下来就是实战环节,我们会在后面的章节中一步步实现一个真正的多核异构驱动。