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=yCONFIG_KGDB_SERIAL_CONSOLE=yCONFIG_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
2.5 本章知识体系
下面这张图总结了驱动开发环境搭建的核心流程:
从图中可以看出,交叉编译工具链是基础,设备树描述硬件,内核模块实现驱动逻辑,远程调试则是验证手段。四者环环相扣,缺一不可。
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 设置 |
好了,环境搭建这部分就讲到这里。工具链配好、设备树写对、模块能编译加载、远程调试能连上,你的驱动开发之路就正式开始了。接下来就是实战环节,我们会在后面的章节中一步步实现一个真正的多核异构驱动。