树莓派Linux驱动开发实战:从GPIO到PWM与红外通信

发布时间:2026/7/12 2:00:02

树莓派Linux驱动开发实战:从GPIO到PWM与红外通信 在嵌入式开发领域树莓派因其丰富的接口和强大的社区支持成为学习Linux驱动开发的理想平台。很多开发者在使用树莓派进行项目开发时经常会遇到硬件设备无法识别、驱动加载失败等问题这些问题往往源于对Linux驱动机制理解不够深入。本文将系统讲解基于树莓派的Linux驱动开发全流程从基础概念到实战项目帮助开发者掌握驱动开发的核心技能。1. Linux驱动开发基础概念1.1 什么是设备驱动设备驱动是操作系统内核的一部分它充当硬件设备与操作系统之间的桥梁。当应用程序需要访问硬件时首先通过系统调用进入内核空间然后由对应的设备驱动完成对硬件的具体操作。在Linux系统中设备驱动通常以内核模块的形式存在这种设计使得驱动程序可以动态加载和卸载无需重新编译整个内核。对于树莓派这样的嵌入式平台理解模块化驱动的设计思想尤为重要。1.2 驱动分类与特点Linux设备驱动主要分为三类字符设备驱动、块设备驱动和网络设备驱动。字符设备以字节流的形式进行数据传输如GPIO、串口等块设备以数据块为单位进行操作如SD卡、硬盘等网络设备则负责网络数据包的收发。树莓派开发中最常接触的是字符设备驱动特别是GPIO、I2C、SPI等接口的驱动开发。这些驱动虽然功能不同但都遵循相似的内核编程接口和框架。1.3 树莓派驱动开发特殊性树莓派作为ARM架构的嵌入式设备其驱动开发与x86平台有一些区别。首先是交叉编译环境的搭建开发者通常在x86主机上编写代码然后交叉编译为ARM架构的可执行文件。其次是设备树的使用树莓派通过设备树来描述硬件配置驱动开发需要熟悉设备树的编写和调试。2. 开发环境搭建2.1 硬件准备进行树莓派驱动开发需要准备以下硬件设备树莓派开发板推荐3B或4B型号、Micro SD卡至少16GB、电源适配器、串口调试线或网线。对于外设驱动开发还需要相应的传感器或扩展板如LED、按键、温湿度传感器等。选择树莓派3B或4B型号是因为这些型号的文档和社区支持较为完善内核版本相对稳定适合学习使用。在实际开发中建议准备两块SD卡一块用于日常使用一块专用于驱动开发和调试。2.2 软件环境配置宿主机推荐使用Ubuntu 20.04 LTS或更新版本需要安装以下工具链# 安装交叉编译工具链 sudo apt-get update sudo apt-get install gcc-arm-linux-gnueabihf g-arm-linux-gnueabihf # 安装必要的开发工具 sudo apt-get install build-essential git bc bison flex libssl-dev # 安装树莓派内核头文件在树莓派上执行 sudo apt-get install raspberrypi-kernel-headers对于IDE选择VSCode是较好的选择需要安装C/C扩展和Remote-SSH扩展方便远程开发和调试。配置文件可以参考开源项目中的.vscode目录设置。2.3 内核源码获取与配置获取与树莓派当前运行内核版本一致的源码非常重要# 查看树莓派当前内核版本 uname -r # 根据版本号从官方仓库下载对应内核源码 wget https://github.com/raspberrypi/linux/archive/rpi-4.19.y.tar.gz tar -xzf rpi-4.19.y.tar.gz cd linux-rpi-4.19.y # 配置内核编译选项 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- bcm2709_defconfig内核版本必须与树莓派上运行的系统保持一致否则编译的驱动模块可能无法加载。建议将内核源码放在项目根目录便于管理和版本控制。3. 驱动开发基础实战3.1 第一个内核模块Hello World创建最简单的内核模块是学习驱动开发的第一步。新建hello.c文件#include linux/init.h #include linux/module.h #include linux/kernel.h static int __init hello_init(void) { printk(KERN_INFO Hello, Raspberry Pi Driver World!\n); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO Goodbye, Raspberry Pi Driver World!\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple Raspberry Pi driver example);对应的Makefile配置obj-m : hello.o KDIR : /home/username/linux-rpi-4.19.y PWD : $(shell pwd) default: $(MAKE) ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean编译并测试模块make # 将生成的hello.ko拷贝到树莓派 scp hello.ko piraspberrypi:~/ # 在树莓派上加载模块 sudo insmod hello.ko # 查看内核日志 dmesg | tail -5 # 卸载模块 sudo rmmod hello这个简单示例展示了驱动模块的基本结构初始化函数、退出函数、模块信息声明。printk用于内核日志输出与用户空间的printf类似但更加强大。3.2 字符设备驱动框架字符设备驱动是Linux驱动开发中最常见的类型它需要实现file_operations结构体中的各种操作函数#include linux/fs.h #include linux/cdev.h static int device_open(struct inode *inode, struct file *file) { printk(KERN_INFO Device opened\n); return 0; } static int device_release(struct inode *inode, struct file *file) { printk(KERN_INFO Device closed\n); return 0; } static ssize_t device_read(struct file *filp, char __user *buffer, size_t length, loff_t *offset) { // 读取设备数据的实现 return 0; } static ssize_t device_write(struct file *filp, const char __user *buffer, size_t length, loff_t *offset) { // 写入设备数据的实现 return length; } static struct file_operations fops { .owner THIS_MODULE, .open device_open, .release device_release, .read device_read, .write device_write, };完整的字符设备驱动还需要实现设备号的申请、cdev结构的初始化和添加等操作。这些函数构成了用户空间与内核空间数据交换的桥梁。3.3 设备树基础与使用设备树是描述硬件配置的数据结构在树莓派驱动开发中至关重要。创建一个简单的设备树源文件/dts-v1/; /plugin/; / { compatible brcm,bcm2835; fragment0 { target gpio; __overlay__ { my_device_pins: my_device_pins { brcm,pins 17; // GPIO17 brcm,function 0; // 输入模式 }; }; }; fragment1 { target-path /; __overlay__ { my_device { compatible company,my-device; status okay; pinctrl-names default; pinctrl-0 my_device_pins; gpios gpio 17 0; }; }; }; };编译设备树并启用# 编译设备树 dtc - -I dts -O dtb -o my_device.dtbo my_device.dts # 拷贝到树莓派 sudo cp my_device.dtbo /boot/overlays/ # 在/boot/config.txt中启用设备树覆盖 echo dtoverlaymy_device | sudo tee -a /boot/config.txt设备树使得硬件配置与驱动代码分离提高了驱动的可移植性和可维护性。在驱动中可以通过of_系列函数来获取设备树中定义的属性。4. GPIO驱动开发实战4.1 GPIO子系统概述树莓派的GPIO子系统提供了丰富的接口控制能力Linux内核通过GPIO子系统为驱动开发者提供了统一的编程接口。主要包括gpiolib框架、pinctrl子系统等。在驱动开发中我们不应该直接操作GPIO的物理地址而应该使用内核提供的GPIO操作函数这样可以保证代码的可移植性和稳定性。GPIO可以配置为输入、输出、中断等不同模式每种模式都有相应的API函数。4.2 LED驱动实现下面是一个完整的GPIO LED驱动示例#include linux/module.h #include linux/fs.h #include linux/gpio/consumer.h #include linux/interrupt.h #include linux/miscdevice.h #include linux/of.h #include linux/platform_device.h struct led_device { struct gpio_desc *gpio; struct miscdevice miscdev; int state; }; static ssize_t led_write(struct file *file, const char __user *buf, size_t len, loff_t *ppos) { struct led_device *led container_of(file-private_data, struct led_device, miscdev); char val; if (copy_from_user(val, buf, 1)) return -EFAULT; if (val 0) { gpiod_set_value(led-gpio, 0); led-state 0; } else if (val 1) { gpiod_set_value(led-gpio, 1); led-state 1; } return 1; } static ssize_t led_read(struct file *file, char __user *buf, size_t len, loff_t *ppos) { struct led_device *led container_of(file-private_data, struct led_device, miscdev); char state led-state 0; if (copy_to_user(buf, state, 1)) return -EFAULT; return 1; } static struct file_operations led_fops { .owner THIS_MODULE, .read led_read, .write led_write, }; static int led_probe(struct platform_device *pdev) { struct led_device *led; int ret; led devm_kzalloc(pdev-dev, sizeof(*led), GFP_KERNEL); if (!led) return -ENOMEM; led-gpio devm_gpiod_get(pdev-dev, led, GPIOD_OUT_LOW); if (IS_ERR(led-gpio)) return PTR_ERR(led-gpio); led-miscdev.minor MISC_DYNAMIC_MINOR; led-miscdev.name rpi_led; led-miscdev.fops led_fops; ret misc_register(led-miscdev); if (ret) { dev_err(pdev-dev, Failed to register misc device\n); return ret; } platform_set_drvdata(pdev, led); dev_info(pdev-dev, Raspberry Pi LED driver probed successfully\n); return 0; } static int led_remove(struct platform_device *pdev) { struct led_device *led platform_get_drvdata(pdev); misc_deregister(led-miscdev); dev_info(pdev-dev, Raspberry Pi LED driver removed\n); return 0; } static const struct of_device_id led_of_match[] { { .compatible company,rpi-led }, { }, }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver { .driver { .name rpi-led, .of_match_table led_of_match, }, .probe led_probe, .remove led_remove, }; module_platform_driver(led_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(Raspberry Pi LED Driver);对应的设备树配置/dts-v1/; /plugin/; / { compatible brcm,bcm2835; fragment0 { target-path /; __overlay__ { led_device { compatible company,rpi-led; status okay; led-gpios gpio 17 GPIO_ACTIVE_HIGH; }; }; }; };4.3 按键中断驱动按键驱动通常需要使用中断机制下面是基于中断的按键驱动示例#include linux/interrupt.h #include linux/gpio/consumer.h struct button_device { struct gpio_desc *gpio; int irq; struct miscdevice miscdev; }; static irqreturn_t button_interrupt(int irq, void *dev_id) { struct button_device *button dev_id; int state gpiod_get_value(button-gpio); printk(KERN_INFO Button %s\n, state ? released : pressed); return IRQ_HANDLED; } static int button_probe(struct platform_device *pdev) { struct button_device *button; int ret; button devm_kzalloc(pdev-dev, sizeof(*button), GFP_KERNEL); if (!button) return -ENOMEM; button-gpio devm_gpiod_get(pdev-dev, button, GPIOD_IN); if (IS_ERR(button-gpio)) return PTR_ERR(button-gpio); button-irq gpiod_to_irq(button-gpio); if (button-irq 0) return button-irq; ret request_irq(button-irq, button_interrupt, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, rpi_button, button); if (ret) { dev_err(pdev-dev, Failed to request IRQ\n); return ret; } dev_info(pdev-dev, Button driver probed, IRQ: %d\n, button-irq); return 0; }中断处理是驱动开发中的重要概念需要注意中断上下文的特点不能睡眠、不能调用可能引起睡眠的函数、处理要尽可能快。5. PWM驱动开发5.1 PWM子系统介绍PWM脉冲宽度调制在树莓派上常用于控制LED亮度、电机速度等。树莓派的PWM控制器支持硬件PWM可以提供精确的脉冲控制。Linux内核的PWM子系统提供了统一的接口驱动开发者可以通过pwm_系列函数来配置和使用PWM。树莓派上有两个PWM通道PWM0和PWM1可以通过设备树进行配置。5.2 PWM LED调光驱动下面是一个PWM LED调光驱动的实现#include linux/pwm.h struct pwm_led_device { struct pwm_device *pwm; struct miscdevice miscdev; }; static ssize_t pwm_led_write(struct file *file, const char __user *buf, size_t len, loff_t *ppos) { struct pwm_led_device *pwm_led container_of(file-private_data, struct pwm_led_device, miscdev); unsigned long duty_cycle; char data[10]; if (len sizeof(data) - 1) return -EINVAL; if (copy_from_user(data, buf, len)) return -EFAULT; data[len] \0; if (kstrtoul(data, 10, duty_cycle)) return -EINVAL; if (duty_cycle 100) duty_cycle 100; // 设置PWM占空比0-100% pwm_config(pwm_led-pwm, duty_cycle * 10000, 1000000); return len; } static int pwm_led_probe(struct platform_device *pdev) { struct pwm_led_device *pwm_led; int ret; pwm_led devm_kzalloc(pdev-dev, sizeof(*pwm_led), GFP_KERNEL); if (!pwm_led) return -ENOMEM; pwm_led-pwm devm_pwm_get(pdev-dev, NULL); if (IS_ERR(pwm_led-pwm)) return PTR_ERR(pwm_led-pwm); // 配置PWM参数周期1ms初始占空比0% pwm_config(pwm_led-pwm, 0, 1000000); pwm_enable(pwm_led-pwm); pwm_led-miscdev.minor MISC_DYNAMIC_MINOR; pwm_led-miscdev.name pwm_led; pwm_led-miscdev.fops pwm_led_fops; ret misc_register(pwm_led-miscdev); if (ret) return ret; return 0; }对应的设备树配置/dts-v1/; /plugin/; / { compatible brcm,bcm2835; fragment0 { target pwm; __overlay__ { pinctrl-names default; pinctrl-0 pwm0_gpio18; status okay; }; }; fragment1 { target-path /; __overlay__ { pwm_led { compatible company,pwm-led; pwms pwm 0 1000000; // PWM0, 周期1ms status okay; }; }; }; };5.3 音乐播放器驱动利用PWM可以制作简单的音乐播放器通过控制频率来播放不同音调// 定义音调频率Hz #define DO 262 #define RE 294 #define MI 330 #define FA 349 #define SOL 392 #define LA 440 #define SI 494 static void play_tone(struct pwm_device *pwm, int frequency, int duration_ms) { if (frequency 0) { pwm_disable(pwm); msleep(duration_ms); return; } int period_ns NSEC_PER_SEC / frequency; int duty_ns period_ns / 2; pwm_config(pwm, duty_ns, period_ns); pwm_enable(pwm); msleep(duration_ms); pwm_disable(pwm); msleep(50); // 音符间间隔 } // 播放小星星示例 static void play_twinkle_twinkle(struct pwm_device *pwm) { int melody[] {DO, DO, SOL, SOL, LA, LA, SOL, 0}; int durations[] {500, 500, 500, 500, 500, 500, 1000, 500}; for (int i 0; i ARRAY_SIZE(melody); i) { play_tone(pwm, melody[i], durations[i]); } }这种PWM应用展示了驱动开发的实际用途将硬件控制与具体应用场景结合。6. 红外接收驱动开发6.1 红外通信原理红外通信在遥控器等设备中广泛使用通常采用NEC协议。该协议使用38kHz的载波通过脉冲宽度编码数据。一个完整的NEC帧包括引导码、地址码、命令码等部分。在驱动开发中我们需要通过GPIO中断来精确测量脉冲宽度然后解码出原始数据。由于红外信号对时序要求严格驱动需要高效的中断处理函数。6.2 红外接收驱动实现#include linux/interrupt.h #include linux/timer.h struct ir_device { struct gpio_desc *gpio; int irq; struct timer_list timer; u32 raw_data; int bit_count; int state; spinlock_t lock; }; enum ir_state { STATE_IDLE, STATE_LEADER_PULSE, STATE_LEADER_SPACE, STATE_DATA_PULSE, STATE_DATA_SPACE }; // NEC协议时间阈值微秒 #define NEC_LEADER_PULSE_MIN 4000 #define NEC_LEADER_PULSE_MAX 5000 #define NEC_LEADER_SPACE_MIN 4000 #define NEC_LEADER_SPACE_MAX 5000 #define NEC_BIT_PULSE_MIN 500 #define NEC_BIT_PULSE_MAX 600 #define NEC_BIT_0_SPACE_MIN 500 #define NEC_BIT_0_SPACE_MAX 600 #define NEC_BIT_1_SPACE_MIN 1600 #define NEC_BIT_1_SPACE_MAX 1700 static irqreturn_t ir_interrupt(int irq, void *dev_id) { struct ir_device *ir dev_id; int value gpiod_get_value(ir-gpio); u64 timestamp ktime_get_ns(); static u64 last_time; u64 duration; unsigned long flags; spin_lock_irqsave(ir-lock, flags); if (last_time 0) { last_time timestamp; spin_unlock_irqrestore(ir-lock, flags); return IRQ_HANDLED; } duration (timestamp - last_time) / 1000; // 转换为微秒 last_time timestamp; switch (ir-state) { case STATE_IDLE: if (value 0 duration NEC_LEADER_PULSE_MIN duration NEC_LEADER_PULSE_MAX) { ir-state STATE_LEADER_SPACE; ir-raw_data 0; ir-bit_count 0; } break; case STATE_LEADER_SPACE: if (value 1 duration NEC_LEADER_SPACE_MIN duration NEC_LEADER_SPACE_MAX) { ir-state STATE_DATA_PULSE; } else { ir-state STATE_IDLE; } break; case STATE_DATA_PULSE: if (value 0 duration NEC_BIT_PULSE_MIN duration NEC_BIT_PULSE_MAX) { ir-state STATE_DATA_SPACE; } else { ir-state STATE_IDLE; } break; case STATE_DATA_SPACE: if (value 1) { if (duration NEC_BIT_1_SPACE_MIN duration NEC_BIT_1_SPACE_MAX) { ir-raw_data | (1 ir-bit_count); } else if (duration NEC_BIT_0_SPACE_MIN duration NEC_BIT_0_SPACE_MAX) { // 0位不需要设置因为初始为0 } else { ir-state STATE_IDLE; break; } ir-bit_count; if (ir-bit_count 32) { // 完整帧接收完成 printk(KERN_INFO IR Code: 0x%08x\n, ir-raw_data); ir-state STATE_IDLE; } else { ir-state STATE_DATA_PULSE; } } break; } spin_unlock_irqrestore(ir-lock, flags); return IRQ_HANDLED; }6.3 红外数据解码与应用接收到原始数据后需要按照NEC协议进行解码static void decode_nec_frame(u32 data) { u8 address (data 24) 0xFF; u8 address_inv (data 16) 0xFF; u8 command (data 8) 0xFF; u8 command_inv data 0xFF; // 检查反码是否正确 if ((address ^ address_inv) ! 0xFF || (command ^ command_inv) ! 0xFF) { printk(KERN_WARNING IR frame checksum error\n); return; } printk(KERN_INFO IR Remote: address0x%02x, command0x%02x\n, address, command); // 根据命令执行相应操作 switch (command) { case 0x45: // 电源键 // 执行电源相关操作 break; case 0x46: // 音量 // 增加音量 break; case 0x15: // 音量- // 减小音量 break; default: printk(KERN_INFO Unknown IR command: 0x%02x\n, command); } }红外驱动开发涉及精确的时序处理和协议解析是学习中断处理和状态机的优秀案例。7. 驱动调试与优化技巧7.1 常用调试工具驱动开发过程中调试是必不可少的环节。常用的调试工具包括printk最基本的内核日志输出工具支持不同的日志级别printk(KERN_DEBUG Debug message\n); // 调试信息 printk(KERN_INFO Info message\n); // 一般信息 printk(KERN_WARNING Warning message\n); // 警告信息 printk(KERN_ERR Error message\n); // 错误信息dmesg查看内核环状缓冲区中的消息dmesg | tail -20 # 查看最后20条内核消息 dmesg -w # 实时监控内核消息 dmesg | grep error\|fail # 过滤错误信息proc文件系统通过/proc接口获取系统信息// 在驱动中创建proc文件 struct proc_dir_entry *entry; entry proc_create(my_driver_info, 0444, NULL, proc_fops);sysfs接口提供设备信息的标准接口// 创建sysfs属性 static DEVICE_ATTR(version, 0444, show_version, NULL); static ssize_t show_version(struct device *dev, struct device_attribute *attr, char *buf) { return sprintf(buf, Driver Version: 1.0\n); }7.2 性能优化策略驱动性能优化需要考虑以下几个方面中断处理优化将耗时操作推迟到下半部执行// 使用tasklet处理中断下半部 static void irq_bottom_half(unsigned long data) { // 处理耗时操作 } DECLARE_TASKLET(my_tasklet, irq_bottom_half, 0); static irqreturn_t irq_handler(int irq, void *dev_id) { // 快速处理关键操作 tasklet_schedule(my_tasklet); // 调度下半部 return IRQ_HANDLED; }内存管理优化合理使用内核内存分配函数// 小内存分配使用kmalloc void *small_buf kmalloc(128, GFP_KERNEL); // 大内存分配使用vmalloc void *large_buf vmalloc(4096 * 10); // DMA内存分配 void *dma_buf dma_alloc_coherent(dev, size, dma_handle, GFP_KERNEL);电源管理在设备不使用时降低功耗static int device_suspend(struct device *dev) { // 挂起设备降低功耗 return 0; } static int device_resume(struct device *dev) { // 恢复设备运行 return 0; } static const struct dev_pm_ops device_pm_ops { .suspend device_suspend, .resume device_resume, };7.3 调试实践案例在实际调试中经常会遇到各种问题。以下是一个典型的调试流程示例问题现象驱动加载后系统崩溃排查步骤检查内核日志dmesg | tail -30确认堆栈跟踪信息检查内存访问是否越界验证硬件资源申请是否冲突常见问题解决// 1. 内存泄漏检测 #ifdef DEBUG #define MY_DEBUG(fmt, args...) printk(KERN_DEBUG fmt, ##args) #else #define MY_DEBUG(fmt, args...) #endif // 2. 使用BUG_ON进行断言调试 BUG_ON(device NULL); // 3. 使用WARN_ON输出警告 WARN_ON(irq 0); // 4. 动态调试支持 #ifdef CONFIG_DYNAMIC_DEBUG dynamic_dev_dbg(dev, Device state: %d\n, state); #endif8. 项目构建与自动化8.1 自动化构建系统大型驱动项目需要完善的构建系统管理。参考开源项目的rules.mk设计# rules.mk - 通用驱动构建规则 # 默认配置 CROSS_COMPILE ? arm-linux-gnueabihf- KDIR ? /lib/modules/$(shell uname -r)/build INSTALL_PATH ? /lib/modules/$(shell uname -r)/extra # 构建目标定义 .PHONY: all modules tests clean install # 自动检测模块文件 modules : $(obj-m) module_names : $(modules:.o.ko) # 自动检测测试程序 test_sources : $(wildcard *_test.c) test_programs : $(test_sources:.c) # 自动检测设备树文件 dts_sources : $(wildcard *.dts) dtbo_files : $(dts_sources:.dts.dtbo) all: modules tests dtbo modules: $(module_names) %.ko: %.o $(MAKE) -C $(KDIR) M$(CURDIR) modules tests: $(test_programs) %_test: %_test.c $(CROSS_COMPILE)gcc -o $ $ $(LDFLAGS) dtbo: $(dtbo_files) %.dtbo: %.dts dtc - -I dts -O dtb -o $ $ clean: $(MAKE) -C $(KDIR) M$(CURDIR) clean rm -f $(test_programs) $(dtbo_files) install: modules dtbo scp $(module_names) $(dtbo_files) $(INSTALL_PATH)/ if [ -n $(test_programs) ]; then \ scp $(test_programs) $(INSTALL_PATH)/; \ fi if [ -n $(INSTALL_FILES) ]; then \ scp $(INSTALL_FILES) $(INSTALL_PATH)/; \ fi8.2 模块化项目组织合理的项目结构有助于代码维护rpi-drivers/ ├── rules.mk # 通用构建规则 ├── 00-hello/ # Hello World示例 │ ├── hello.c │ ├── Makefile │ └── hello_test.c ├── 01-gpio_led/ # GPIO LED驱动 │ ├── led.c │ ├── led.dts │ ├── Makefile │ └── led_test.c ├── 02-gpio_key/ # 按键驱动 │ ├── key.c │ ├── key.dts │ ├── Makefile │ └── key_test.c ├── 03-device_io/ # 设备IO操作 │ └── ... ├── 04-pwm_led/ # PWM调光 │ └── ... ├── 05-pwm_musicbox/ # PWM音乐盒 │ └── ... ├── 06-infrared/ # 红外接收 │ └── ... └── document/ # 文档资料 └── ...每个子目录的Makefile示例# 01-gpio_led/Makefile obj-m : led.o # 自定义配置 CROSS_COMPILE /usr/local/bin/arm-linux-gnueabihf- KDIR /home/user/linux-rpi-4.19.y INSTALL_PATH piraspberrypi:~/modules LDFLAGS -lpthread -include ../rules.mk8.3 持续集成与测试驱动开发需要完善的测试体系单元测试为每个驱动编写测试程序// led_test.c - LED驱动测试程序 #include stdio.h #include fcntl.h #include unistd.h int main() { int fd open(/dev/rpi_led, O_RDWR); if (fd 0) { perror(Failed to open device); return -1; } // 测试LED开关 write(fd, 1, 1); // 打开LED sleep(1); write(fd, 0, 1); // 关闭LED close(fd); return 0; }自动化测试脚本#!/bin/bash # test_all.sh - 自动化测试脚本 echo 开始驱动测试... # 加载所有驱动模块 for module in *.ko; do echo 加载模块: $module sudo insmod $module if [ $? -ne 0 ]; then echo 错误: $module 加载失败 exit 1 fi done # 运行测试程序 for test in *_test; do echo 运行测试: $test ./$test if [ $? -ne 0 ]; then echo 错误: $test 测试失败 exit 1 fi done echo 所有测试通过!9. 生产环境注意事项9.1 驱动稳定性保障生产环境中的驱动需要具备更高的稳定性错误处理完善的错误检测和恢复机制static int device_probe(struct platform_device *pdev) { int ret; // 逐步申请资源每一步都要检查错误 ret request_irq(irq, handler, flags, name, dev); if (ret) { dev_err(pdev-dev, 无法申请IRQ: %d\n, ret); goto err_irq; } ret misc_register(miscdev); if (ret) { dev_err(pdev-dev, 无法注册设备: %d\n, ret); goto err_register; } return 0; err_register: free_irq(irq

相关新闻