1. 开发板环境概述
HD-RK3506-EVM是万象奥科推出的一款基于瑞芯微RK3506处理器的嵌入式评估板。这块板子我上手用了两周,整体感觉硬件设计很扎实,接口丰富,特别适合做工业控制、智能终端这类嵌入式开发。板载的RK3506芯片采用四核Cortex-A35架构,主频1.3GHz,搭配Mali-G31 GPU,性能对于中等复杂度的嵌入式应用完全够用。
开发板预装了基于Linux 4.19内核的系统,这个内核版本算是比较新的稳定版。不过在实际开发中,我们经常需要为特定硬件或功能编译自定义内核模块,比如要驱动个特殊的传感器或者加个加密芯片什么的。这时候就得自己动手编译内核模块了。
注意:在开始编译前,请确保你的开发主机是x86_64架构的Linux系统(推荐Ubuntu 18.04/20.04),并且已经通过串口或SSH连接到开发板。交叉编译环境必须与开发板系统内核版本严格匹配。
2. 交叉编译环境搭建
2.1 工具链获取与配置
瑞芯微官方提供了完整的交叉编译工具链,我们需要先把这个装好。工具链的下载地址在万象奥科官网的HD-RK3506-EVM支持页面可以找到,文件名一般是gcc-linaro-6.3.1-2017.05-x86_64_arm-linux-gnueabihf.tar.xz这样的。
下载完成后,我是这样安装的:
bash复制sudo mkdir -p /opt/toolchains
sudo tar -xJf gcc-linaro-6.3.1-2017.05-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/toolchains
然后别忘了把工具链路径加到环境变量里。我习惯在~/.bashrc里加这几行:
bash复制export PATH=/opt/toolchains/gcc-linaro-6.3.1-2017.05-x86_64_arm-linux-gnueabihf/bin:$PATH
export CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH=arm
2.2 内核源码准备
编译模块必须要有和开发板运行的内核完全一致的源码。万象奥科通常会提供两个版本:
- 标准版:基于瑞芯微官方发布的基线代码
- 定制版:万象奥科自己做过硬件适配的版本
我建议直接用万象奥科提供的定制版源码,文件名类似hd-rk3506-linux-sdk-v1.0.0.tar.gz。解压后目录结构大概是这样的:
code复制kernel/
├── arch
├── drivers
├── include
└── ...
解压后第一件事是检查内核版本是否匹配:
bash复制cat kernel/include/config/kernel.release
这个输出应该和开发板上uname -r的结果完全一致。
3. 内核配置与编译准备
3.1 内核配置同步
开发板上的/proc/config.gz包含了当前运行内核的所有配置选项,我们需要把这个配置同步到编译环境:
bash复制adb pull /proc/config.gz
gunzip config.gz
cp config kernel/.config
然后在内核源码目录执行:
bash复制make oldconfig
make menuconfig
这一步会基于现有配置生成新的配置界面。我建议在这里把Loadable module support下的所有选项都打开,特别是:
code复制[*] Enable loadable module support
[*] Module unloading
[*] Forced module unloading
[*] Module versioning support
[*] Source checksum for all modules
3.2 内核头文件准备
编译外部模块需要内核头文件,执行:
bash复制make prepare
make scripts
这两个命令会准备好模块编译所需的各种脚本和头文件。
4. 编写和编译内核模块
4.1 最简单的Hello World模块
先来个经典的hello world模块练手。创建hello.c:
c复制#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, HD-RK3506-EVM!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
对应的Makefile:
makefile复制obj-m := hello.o
KDIR := /path/to/kernel
PWD := $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
编译命令:
bash复制make -C /path/to/kernel M=$PWD modules
成功后会生成hello.ko文件。
4.2 实际驱动模块示例
假设我们要为开发板上的一个GPIO设备写驱动,代码结构会更复杂些。典型的结构如下:
c复制#include <linux/module.h>
#include <linux/fs.h>
#include <linux/gpio.h>
#define DEVICE_NAME "my_gpio_dev"
static int major_num;
static int gpio_pin = 158; // 根据实际硬件连接修改
static int dev_open(struct inode *inodep, struct file *filep) {
printk(KERN_INFO "GPIO device opened\n");
return 0;
}
static struct file_operations fops = {
.open = dev_open,
// 其他操作函数...
};
static int __init gpio_init(void) {
if (!gpio_is_valid(gpio_pin)) {
printk(KERN_ALERT "Invalid GPIO pin\n");
return -ENODEV;
}
if (gpio_request(gpio_pin, "my_gpio")) {
printk(KERN_ALERT "GPIO request failed\n");
return -EBUSY;
}
major_num = register_chrdev(0, DEVICE_NAME, &fops);
printk(KERN_INFO "GPIO driver registered with major %d\n", major_num);
return 0;
}
static void __exit gpio_exit(void) {
gpio_free(gpio_pin);
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "GPIO driver unregistered\n");
}
module_init(gpio_init);
module_exit(gpio_exit);
MODULE_LICENSE("GPL");
对应的Makefile需要添加更多依赖:
makefile复制obj-m := gpio_driver.o
gpio_driver-objs := gpio_main.o gpio_ops.o
KDIR := /path/to/kernel
PWD := $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
5. 模块部署与调试
5.1 模块加载与卸载
把编译好的.ko文件传到开发板:
bash复制adb push hello.ko /data
然后在开发板上操作:
bash复制insmod /data/hello.ko
lsmod | grep hello
rmmod hello
dmesg | tail -n 10
5.2 常见问题排查
-
版本不匹配:
code复制insmod: ERROR: could not insert module hello.ko: Invalid module format解决方法:确保内核源码版本与开发板运行内核完全一致,重新编译。
-
符号未找到:
code复制Unknown symbol in module解决方法:使用
modinfo检查模块依赖,确保所有依赖模块已加载。 -
内存不足:
code复制Could not allocate memory解决方法:检查开发板剩余内存,优化模块内存使用。
6. 高级技巧与优化
6.1 模块签名验证
如果内核启用了模块签名验证,编译时需要额外步骤:
bash复制perl /path/to/kernel/scripts/sign-file sha512 /path/to/private_key.pem /path/to/public_key.der hello.ko
6.2 调试信息添加
在Makefile中添加调试信息:
makefile复制EXTRA_CFLAGS += -g -DDEBUG
然后可以用gdb调试:
bash复制arm-linux-gnueabihf-gdb vmlinux
(gdb) add-symbol-file hello.ko 0xbf000000
6.3 性能优化
-
减少模块大小:
makefile复制
EXTRA_CFLAGS += -Os -
关键函数标记为
__init和__exit:c复制static int __init my_init(void) { ... } static void __exit my_exit(void) { ... } -
使用
likely()/unlikely()优化分支预测:c复制if (unlikely(error_condition)) { handle_error(); }
7. 实际项目经验分享
在HD-RK3506-EVM上开发自定义驱动时,我遇到过几个坑值得分享:
-
GPIO编号问题:瑞芯微的GPIO编号系统比较特殊,不能直接用硬件引脚号。正确的做法是:
c复制#include <linux/gpio.h> #include <linux/of_gpio.h> int gpio = of_get_named_gpio(node, "gpio-name", 0); -
时钟管理:有些外设需要手动启用时钟,否则寄存器访问会失败:
c复制struct clk *clk = clk_get(dev, "pclk"); clk_prepare_enable(clk); // ... clk_disable_unprepare(clk); -
DMA缓存一致性:使用DMA时要注意缓存一致性问题:
c复制void *buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL); // ... dma_free_coherent(dev, size, buf, dma_handle); -
中断处理:注册中断处理程序时要注意flags选择:
c复制ret = request_irq(irq, handler, IRQF_TRIGGER_RISING | IRQF_SHARED, "my_irq", dev); -
设备树修改:很多硬件配置现在都放在设备树里,修改后要重新编译dtb:
bash复制
make dtbs
最后提醒一点:RK3506的文档有些地方写得比较模糊,遇到问题时最好的办法是参考内核源码中类似的驱动实现,比如drivers/gpio/gpio-rockchip.c就是个很好的参考。
