前言
本次基于正点原子IMX6ULL开发板,使用platform框架 + miscdevice + 设备树DTS完成字符设备驱动开发。整套代码复用同一套驱动骨架,仅少量修改就可以实现蜂鸣器、LED、按键驱动,掌握一套模板快速开发外设。
环境:Ubuntu交叉编译环境,linux-imx-rel_imx_4.1.15_2.1.0_ga内核,nfs根文件系统,minicom串口。
一、基础知识点
- Platform平台驱动:把硬件资源(引脚、中断)放到设备树,驱动通过compatible字符串匹配设备树节点,实现设备和驱动分离。
- Misc设备(杂项设备):属于字符设备,主设备号固定为10,自动分配次设备号,不用手动管理cdev、class、device_create,简化字符设备代码。
- 设备树DTS:描述硬件引脚信息,编译生成dtb,内核启动解析dtb。⚠老版本dtc不支持//单行注释,只能/* */。
- mdev:根文件系统没有udev,insmod加载驱动后,需要
mdev -s扫描sysfs,自动在/dev创建设备节点。
二、实验1:蜂鸣器 Beep驱动
1. 设备树节点(添加在根节点/下)
mybeep { compatible = "pute,mybeep"; gpio-beep = <&gpio1 1 0>; status = "okay"; };compatible:匹配关键字,驱动of_match_table必须完全一致gpio-beep:自定义属性,驱动of_get_named_gpio读取GPIO编号
2. 驱动核心逻辑
- 框架:platform_driver,module_platform_driver注册
- GPIO方向:输出
gpio_direction_output(gpio_num, 默认电平),两个参数:引脚号 + 初始电平 - 文件操作:
.write回调。APP调用write,copy_from_user把用户层数据传到内核,gpio_set_value控制蜂鸣器高低电平。 - misc注册:
misc_register,自动在sys/class生成类。
3. APP测试程序
- open标志:
O_WRONLY只写 - write发送字符
1/0,内核判断,控制蜂鸣器响/关闭
4. 踩坑记录
device_create创建设备节点失败:改用misc简化代码,不用手动class_create- 加载驱动
ls /dev看不到节点:nfs文件系统无udev,执行mdev -s生成/dev设备文件 - APP打开报错
No such file or directory:- 检查交叉编译器,必须arm-linux-gnueabihf-gcc,不能x86编译器
- 确认
/dev/beep_misc存在
- write函数坑:
echo 1 > /dev/xxx传递的是ASCII字符'1',不是数字1,直接判断if(1 == val)不会触发,需要判断字符'1'
三、实验2:LED驱动
改动(在Beep驱动模板基础上修改)
LED和Beep同为输出外设,代码几乎完全一样:
- 设备树新增节点,compatible改成
"pute,myled",定义led的gpio属性 - probe内:
gpio_direction_output()设置输出 - fops:依旧使用
.write回调 - APP:O_WRONLY,write发送0/1控制亮灭
核心:LED、BEEP都是输出型外设,共用write模型。
四、实验3:KEY按键驱动【重点,和BEEP区分】
1. 设备树节点(复用板载KEY0原有pinctrl,避免pinctrl重复定义报错)
mykey0 { compatible = "pute,mykey"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_key0>; gpio-mykey0 = <&gpio1 18 1>; status = "okay"; };⚠坑:不要重复新建pinctrl节点,同一个PAD引脚不能同时在两组pinctrl,会报DTS语法错误。
IMX6ULL pad宏前缀:
MX6UL_PAD_,不是MX6ULL_PAD,多写L直接编译dtb报错。
2. 驱动改动(基于beep模板,4处核心修改)
- GPIO设置:输出 →输入
- Beep:
gpio_direction_output(gpio, 1);2个参数 - KEY:
gpio_direction_input(gpio);仅1个参数,不需要电平
- Beep:
- 文件操作回调替换
- Beep:
.write,用户→内核,写输出 - KEY:
.read,内核→用户,读取引脚状态
- Beep:
- 回调函数逻辑替换
- Beep:
copy_from_user+gpio_set_value() - KEY:
gpio_get_value()读取引脚电平 +copy_to_user把按键状态传给应用层
- Beep:
- 匹配字符串、全局变量名、misc设备名配套修改
3. APP程序改动
- open标志:
O_RDONLY只读 - while循环调用read,读取内核传回的按键状态字符,打印按下/松开
4. 驱动完整改动片段
// 替换write为read static ssize_t key_read(struct file *fp, char __user *puser, size_t n, loff_t *off) { char ch; int val; val = gpio_get_value(keygpiono); if(val == 0) ch = '1'; //按键按下 else ch = '0'; //松开 copy_to_user(puser, &ch, 1); return 1; } static struct file_operations fops = { .owner = THIS_MODULE, .read = key_read, }; //probe里面gpio设置 gpio_direction_input(keygpiono);五、通用编译&开发板操作流程(三个外设通用)
Ubuntu编译驱动ko
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs #编译设备树 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules #编译驱动模块Ubuntu交叉编译APP
arm-linux-gnueabihf-gcc xxx_app.c -o xxx_app开发板minicom操作
insmod xxx_drv.ko mdev -s # 自动生成/dev下设备节点 ls /dev/xxx_misc ./xxx_app rmmod xxx_drv六、DTS编译踩坑汇总
- 老dtc不识别
//注释,只能用/* */ - 所有符号
< > ;必须英文半角,复制粘贴引入中文符号直接报语法错误 - pinctrl节点必须写在&iomuxc{}内部
- 同一个引脚不能定义两组pinctrl,会冲突
- i.MX6ULL引脚宏:MX6UL_PAD_xxxx,不要写成MX6ULL_PAD
- 报错行号经常错位:实际错误在上一行少
;或者}
七、核心对比总结表
| 外设 | GPIO方向 | 驱动回调 | 数据流向 | APP打开方式 |
|---|---|---|---|---|
| Beep蜂鸣器 | 输出 | write | APP → 内核 → 硬件 | O_WRONLY |
| LED灯 | 输出 | write | APP → 内核 → 硬件 | O_WRONLY |
| KEY按键 | 输入 | read | 硬件 → 内核 → APP | O_RDONLY |
八、学习总结
- platform + misc 是嵌入式Linux简单字符设备的万能模板,输出外设用write,输入外设用read。
- 设备树只描述硬件资源,驱动通过compatible匹配,实现软硬分离。
- nfs根文件系统缺少udev,必须手动执行
mdev -s才能在/dev看到设备节点。 - 区分
copy_from_user和copy_to_user:- copy_from_user:用户空间 → 内核空间(写操作)
- copy_to_user:内核空间 → 用户空间(读操作)
- GPIO API区分:
gpio_direction_output(gpio,val):输出,两个参数gpio_direction_input(gpio):输入,仅一个参数