鸽了半年,几乎全忘了,幸亏前面还有两篇总结。出于快速体验嵌入式linux的目的,本篇与前两篇一样,重点在于使用、快速体验,uboot、linux、根文件系统不作深入理解,能用就行。
重新梳理一下脉络,本章学习的是linux驱动开发,主要内容涉及到怎么编写linux驱动 、怎么编译、怎么加载卸载。在此之前,还需要准备两件事,其一制作SD卡启动,因为由于时间有些久,实在是忘了。其二为以太网连接,先前由于以太网被占用,所以无论是移植还是驱动开发,使用的都是SD卡,没有用nfs挂载,但以太网终究要学习且更加方便。
那么如今目的很简单,先简单制作一张能用的SD卡启动,对前面内容的巩固与复习。然后再测试以太网通信,以便后面从虚拟机编译的模块.ko文件可以传输到开发板上的根文件系统里。接着开始正式编写模块驱动,使用到的是linux内核的头文件,再熟悉一些开发驱动用到的宏、函数、命名规则以及开发规则即可。最后是加载并测试驱动。
驱动(模块)开发是我们的主要目的,驱动开发有两种方式,一种是动态加载驱动模块 ,一种是静态编译驱动到内核。前者开发的是.ko文件,可以随时使用insmod加载到正在运行的linux上。后者如其名,在linux工程里创建驱动的.c文件,然后编译成镜像。由于linux编译时间过长,前者可以快速开发并测试驱动,节约大量编译时间,测试完毕后就可以通过后者添加到linux驱动里了
本应如此,遇到问题解决问题,但实际上遇到一些大坑,而不得不学习更多的内容,虽麻烦些,但印象却也深刻了许多。注意!本文的叙述顺序并非依据标准的知识点总结框架,而是按照笔者个人的学习历程展开。
一、制作SD卡启动
1,删除所有分区
①使用ls命令确认SD卡设备
先插上SD卡,并选择连接到虚拟机上
接着使用ls命令来列出所有sd设备,然后再拔掉SD卡设备,列出所有sd设备
bashls /dev/sd*
缺少哪一个,哪个就是SD卡,此处缺失/dev/sdb和/dev/sdb1,那么/dev/sdb就是SD卡设备
②使用fdisk命令删除SD卡所有分区
使用fdisk命令,进入fdisk工具界面
bashsudo fdisk /dev/sdb
使用命令p打印所有分区
输入d命令删除分区,由于只有一个分区,默认直接删除。
2,制作分区
如同前一篇博客所言,SD卡的起始位置需要先空10MB,供裸机程序uboot存放,还需再制作两个分区,一个存放linux镜像,另一个存放根文件系统。
①制作linux分区
在刚才的fdisk工具界面内,输入命令n来创建分区,接着按回车选择默认分区格式p,再输入20480(20480*512Byte=10MB)设置起始扇区,最后输入+500M确定创建分区的大小
②制作根文件系统分区
输入命令p打印分区,可以看到分区1所占扇区的位置为20480~1044479,所以第二个分区的起始扇区可以设为1044480,紧挨着第一个分区
除了起始扇区的位置外,其余按回车选择默认
从最后打印的分区可以看到,被薅走的1.5GB存储
③保存退出
输入命令w即可。不过这里出了一点点小意外
不过却并不影响分区创建
④为分区设定文件系统格式
先使用ls命令列出所有sd设备
bashls /dev/sd*
刚列出来时,并没有分区2(/dev/sdb2),重新插拔SD卡设备后再重新列出,就有了
使用下面命令,分别为两个分区设置格式FAT和EXT4
bashsudo mkfs.vfat /dev/sdb1
bashsudo mkfs.ext4 /dev/sdb2
第二个分区需要等待一段时间,制作好后,左侧就会出现两个USB一样的图标
3,烧录uboot
①编译uboot
②烧录uboot
使用烧录工具,下图使用的烧录工具是基于正点原子提供的工具的改版imxdownload烧写工具
出现下面错误是因为这个工具在编写时,使用的是C++来创建文件而非本地的linux命令,故而需要在前面添加sudo命令
bashsudo ./imx_download -b u-boot.bin -s /dev/sdb
4,拷贝linux镜像和设备树
①编译linux
在Makefile已经指定架构和编译器的情况下,运行脚本
bash#!/bin/bash # 通过chmod +x build.sh赋予权限 # 函数定义,用于执行不同的make命令 make_distclean() { echo "执行 make distclean" make distclean } make_imx_v7_defconfig() { echo "执行 make imx_v7_defconfig -j16" make imx_v7_defconfig -j16 } make_all() { echo "执行 make -j16" make -j16 } make_menuconfig() { echo "执行 make menuconfig" make menuconfig } # 当没有参数时,执行所有命令 if [ $# -eq 0 ]; then echo "没有参数,执行所有命令" make_distclean make_imx_v7_defconfig # make_menuconfig make_all else # 主逻辑,根据输入参数调用相应的函数 case "$1" in c) make_distclean ;; d) make_imx_v7_defconfig ;; a) make_all ;; m) make_menuconfig ;; *) echo "无效的参数: $1" echo "用法: $0 [{c|d|a|m}]" exit 1 ;; esac fi
根据提示找到镜像指定路径
②拷贝
为了把镜像和设备树拷贝到SD卡中,先创建一个目录/mnt,把SD卡挂载到上面,然后再把镜像和设备树复制到/mnt目录
bashsudo mount /dev/sdb1 /mnt
挂载后,左边的USB图标就会少掉一个
bashsudo cp arch/arm/boot/zImage /mnt
设备树就在arch/arm/boot/dts目录,进入后寻找到匹配的dtb文件,然后复制到/mnt目录中
bashsudo cp ./imx6ull-14x14-emmc-7-1024x600-c.dtb /mnt
使用sync后,然后再取消挂载
bashsudo umount /dev/sdb1
5,拷贝根文件系统
①传输根文件系统
使用FileZilla传输文件,调了好半天:NAT是给虚拟机上网用的,桥接是给以太网用的。虚拟机能ping主机不行,控制面板启用VMnet8。
开发资料A盘里有些根文件系统不能正常使用,不过笔者没有一一尝试,下面这个根文件系统是正常的
②拷贝
把传输的压缩包复制到已经挂载SD卡第二个分区的/mnt里,然后解压
bashsudo tar -xvjf rootfs.tar.bz2
解压后删除压缩包,然后使用sync同步,最后取消挂载
6、启动开发板
串口连接至电脑,插上SD卡后,拨码选择SD启动。然后进入uboot,设置启动命令
bashsetenv bootcmd 'load mmc 0:1 0x83000000 zimage; load mmc 0:1 0x83800000 imx6ull-14x14-emmc-7-1024x600-c.dtb; bootz 0x83000000 - 0x83800000'
保存启动命令后,重新复位
bashsaveenv
本来想自行动态计算地址,结果发现uboot的&运算有问题,格式正确也会报语法错误。后来用/0x1000和*0x1000来代替,但会一直卡在启动内核步骤。最后还是用回了以前的命令,这个先搁置 。
最终效果如下,除了壁纸中部细看略微有些条纹外一切正常(可能这是特点?):
二、网络连接
1,设置ip和子网掩码
①测试uboot
开发板上电后,按下任意键进入uboot里,通过下面命令设置开发板的ip、子网掩码和MAC(MAC地址不能重复)
bashsetenv ipaddr 192.168.1.254
bashsetenv netmask 255.255.255.0
bashsetenv ethaddr 00:11:22:33:44:55
然后设置主机的ip
bashsetenv serverip 192.168.1.255
最后保存
bashsaveenv
这里为了避免ip抢占,就把开发板和主机的ip设置得比较远,当然也可以不用192.168.1这个网段。
非常奇怪的是,无论去ping虚拟机还是ping开发板自身,都会出现下面数据错误,使用的是同一个u-boot,以前并未发生过。
不过还是找到了相关博客uboot下出现data abort错误导致重启解决办法
在uboot工程里的arch/arm/cpu/armv7/start.S 中,第130行左右,按照博客里的去修改。不得不说,大佬就是大佬,错误直接解决了
②设置linux
修改下面文件,设置eth0为静态IP,IP地址随意(需要注意,本篇后面其实使用的其实都是192.168.1.127,但图是192.168.1.254)
bashsudo vi /etc/network/interfaces
进入后把iface etho inet dhcp改为下面,都是vim的基本操作
bashauto eth0 iface eth0 inet static address 192.168.1.127 # 开发板的静态IP netmask 255.255.255.0 # 子网掩码 gateway 192.168.1.1 # 网关 dns-nameservers 8.8.8.8 # DNS服务器
(图中乱码可能是显示的问题)
修改完后,使用下面命令来重启网络
bashsudo /etc/init.d/networking restart
此时ip地址已被正确设置
ping虚拟机,可以看到一切正常(此时虚拟机的ip设为192.168.1.128,因为192.168.1.255是广播地址,还需要加上-b参数)
按Ctrl+C可以暂停操作。
可以看到虚拟机也能ping通开发板
2,建立连接
这里能使用的方法有很多
这里使用的是NFS,主机和开发板需要各自配置后,才能进行正常通信
①主机
先安装nfs服务
bashsudo apt-get install nfs-kernel-server
创建一个共享目录并赋予权限,比如在用户目录里创建,user自行替换
bashmkdir /home/user/nfs_share
bashchmod 777 /home/user/nfs_share
编辑NFS配置文件
/etc/exports
,添加共享目录和权限
bashsudo vim /etc/exports
bash/home/user/nfs_share 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)
重启NFS服务
bashsudo systemctl restart nfs-kernel-server
检查NFS共享是否生效
bashsudo exportfs -v
②主机给开发板联网
开发板需要下载nfs客户端,需要联网。联网可以使用IP转发,但这个有些麻烦(以后再说),可以直接使用Windows的网络共享功能,参考博客开发板和笔记本网线连接
到控制面板里,找到网络和Internet,再点击网络和共享中心进入下面步骤,点击更改适配器设置
右键WLAN(笔记本的一个网口已经通过以太网线与开发板连接,所以用的是WiFi),按如下设置
共享之后,使用ifconifg查看ip
设置完后,在开发板的linux里,ping百度网址
bashping www.baidu.com
联网是没有问题,但是这个ping出来的结果很慢,需要耐心等待
③开发板
既然可以联网,那么接下来需要开发板下载nfs客户端。不过这个根文件系统的apt没有资源列表,需要手动创建
bashtouch /etc/apt/sources.list
然后是添加网址,这里使用的是阿里镜像源,可自行替换需要的源
bashvi /etc/apt/sources.list
bashdeb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse
现在使用apt-get update就正常多了,不再会任何列表都没有了
到这一步都还挺顺利的,但无法使用apt下载。询问技术客服,他们说正点原子的根文件系统使用的是Yocto构建的,对apt的支持并不完善。前面为了快速体验开发, 只学习了怎么移植,并没有学习怎么制作根文件系统。
询问了DeepSeek,虽然apt功能强大,但对于嵌入式来说OPKG和RPM更适合(Yocto支持)。既如此,制作根文件系统先放一放,学Qt制作桌面时应该会用到。
使用下面命令检查根文件系统是否支持NFS,从结果来看,是支持的,应该是正点原子在已经提前移植过这些库了。那么就不需要安装nfs客户端了,包管理器下载先放一放。
bashwhich mount.nfs
需要注意的是,前面为了联网启用了网络共享功能,现在要尝试nfs挂载,那么就要关闭共享。同时使用ifconfig来查看ip是否正确,如果不正确,那么重启一下网络,再检查ip地址,确保开发板和虚拟机能互ping。开发板每次重启还需要输入下面命令来重启网络
bashsudo /etc/init.d/networking restart
前面在主机上使用的ip是192.168.1.128,路径是/home/user/nfs_share,下面就可以据此来建立nfs通信了,现在开发板上创建一个用于挂载的目录,比如/mnt/nfs
bashmkdir /mnt/nfs
本来应该使用下面这个命令建立连接的(用户名和ip自行替换),但需要指定版本
bashmount -t nfs 192.168.1.128:/home/fairy/nfs_share /mnt/nfs
在此之前,我们可以先试用下面命令来查看挂载点
bashdf -h
再使用添加了版本的挂载命令(如果版本3不行,试试4),该命令没有任何提示
bashmount -t nfs -o nfsvers=3 192.168.1.128:/home/fairy/nfs_share /mnt/nfs
再使用df -h,可以看到已经成功挂载了
3,测试nfs
刚才在联网的情况下顺便又测试了下包管理器,没想到opkg、rpm、dpkg一个能用的都没有,技术客服说需要手动管理。
使用ls查看虚拟机和开发板的挂载点,可以看到任何内容都没有
在虚拟机的挂载目录里,随便创建一个文件
可以看到开发板的挂载点里确实多了一个文件,挂载成功!
启用ip转发(废稿)编辑开发板的网络配置文件
/etc/network/interfaces
bashsudo vi /etc/network/interfaces
把网关改为虚拟机ip 192.168.1.128(这里把开发板的ip改为了192.168.1.127)
bashauto eth0 iface eth0 inet static address 192.168.1.127 netmask 255.255.255.0 gateway 192.168.1.128 dns-nameservers 8.8.8.8
重启网络驱动(开发板每次重启后还得手动重启网络)
bashsudo /etc/init.d/networking restart
编辑
/etc/resolv.conf
文件,修改DNS配置
bashsudo vi /etc/resolv.conf
修改为下面内容(开发板每次重启都会覆盖掉下面内容)
bashnameserver 8.8.8.8 nameserver 114.114.114.114
三、驱动编写_基础
回顾一下,前面折腾了那么久,无论是制作SD卡启动,还是使用nfs挂载,本质上都是为驱动编写提供便利条件,本篇最终目标"驱动编写"并没有变。
事实上学到现在这个程度,对linux的使用和搭建都有了一些基本的了解和熟悉,看视频不再是首选,文档是更推荐的选择(正点原子的文档质量很高)。可以从"跟随式"学习转为"主动学习",知道要实现什么样的应用(或解决什么样的问题),为此需要学习哪些内容,学习过程遇到问题怎么解决怎么取舍。发问,那么问题就已经解决了一半,遇到问题解决问题,那么学习路径就确立了。
1,动态加载_基础方式
文档是先做字符设备开发,再做LED驱动开发,循序渐进。不过直接做LED驱动也行,可以更快地看到实验结果,两者区别并不大,没有太大的难度壁垒。
这个过程可以分为两个步骤,其一,编写驱动、生成.ko文件、加载卸载驱动;其二为测试,编写一个应用程序,生成elf文件,通过运行程序来观察结果。注意多翻阅文档手册!
①从源码入手
找到开发盘里的led驱动,通过filezila传输到虚拟机中
在虚拟机中,用自己的IDE打开刚才传输的工程
需要修改一下Makefile里的路径KERNELDIR ,换成自己linux内核的目录。如果是CLion的话,根据错误提示,把构建目标all换成build,或者在Makefile里把build改为all
直接构建没有任何问题
②分析源码框架
下面是正点原子的led源码
cpp#include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/init.h> #include <linux/module.h> #include <linux/errno.h> #include <linux/gpio.h> #include <asm/mach/map.h> #include <asm/uaccess.h> #include <asm/io.h> /*************************************************************** Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 文件名 : led.c 作者 : 左忠凯 版本 : V1.0 描述 : LED驱动文件。 其他 : 无 论坛 : www.openedv.com 日志 : 初版V1.0 2019/1/30 左忠凯创建 ***************************************************************/ #define LED_MAJOR 200 /* 主设备号 */ #define LED_NAME "led" /* 设备名字 */ #define LEDOFF 0 /* 关灯 */ #define LEDON 1 /* 开灯 */ /* 寄存器物理地址 */ #define CCM_CCGR1_BASE (0X020C406C) #define SW_MUX_GPIO1_IO03_BASE (0X020E0068) #define SW_PAD_GPIO1_IO03_BASE (0X020E02F4) #define GPIO1_DR_BASE (0X0209C000) #define GPIO1_GDIR_BASE (0X0209C004) /* 映射后的寄存器虚拟地址指针 */ static void __iomem *IMX6U_CCM_CCGR1; static void __iomem *SW_MUX_GPIO1_IO03; static void __iomem *SW_PAD_GPIO1_IO03; static void __iomem *GPIO1_DR; static void __iomem *GPIO1_GDIR; /* * @description : LED打开/关闭 * @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED * @return : 无 */ void led_switch(u8 sta) { u32 val = 0; if (sta == LEDON) { val = readl(GPIO1_DR); val &= ~(1 << 3); writel(val, GPIO1_DR); } else if (sta == LEDOFF) { val = readl(GPIO1_DR); val |= (1 << 3); writel(val, GPIO1_DR); } } /* * @description : 打开设备 * @param - inode : 传递给驱动的inode * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量 * 一般在open的时候将private_data指向设备结构体。 * @return : 0 成功;其他 失败 */ static int led_open(struct inode *inode, struct file *filp) { return 0; } /* * @description : 从设备读取数据 * @param - filp : 要打开的设备文件(文件描述符) * @param - buf : 返回给用户空间的数据缓冲区 * @param - cnt : 要读取的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 读取的字节数,如果为负值,表示读取失败 */ static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { return 0; } /* * @description : 向设备写数据 * @param - filp : 设备文件,表示打开的文件描述符 * @param - buf : 要写给设备写入的数据 * @param - cnt : 要写入的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 写入的字节数,如果为负值,表示写入失败 */ static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { int retvalue; unsigned char databuf[1]; unsigned char ledstat; retvalue = copy_from_user(databuf, buf, cnt); if (retvalue < 0) { printk("kernel write failed!\r\n"); return -EFAULT; } ledstat = databuf[0]; /* 获取状态值 */ if (ledstat == LEDON) { led_switch(LEDON); /* 打开LED灯 */ } else if (ledstat == LEDOFF) { led_switch(LEDOFF); /* 关闭LED灯 */ } return 0; } /* * @description : 关闭/释放设备 * @param - filp : 要关闭的设备文件(文件描述符) * @return : 0 成功;其他 失败 */ static int led_release(struct inode *inode, struct file *filp) { return 0; } /* 设备操作函数 */ static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release, }; /* * @description : 驱动出口函数 * @param : 无 * @return : 无 */ static int __init led_init(void) { int retvalue = 0; u32 val = 0; /* 初始化LED */ /* 1、寄存器地址映射 */ IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4); SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4); SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4); GPIO1_DR = ioremap(GPIO1_DR_BASE, 4); GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4); /* 2、使能GPIO1时钟 */ val = readl(IMX6U_CCM_CCGR1); val &= ~(3 << 26); /* 清楚以前的设置 */ val |= (3 << 26); /* 设置新值 */ writel(val, IMX6U_CCM_CCGR1); /* 3、设置GPIO1_IO03的复用功能,将其复用为 * GPIO1_IO03,最后设置IO属性。 */ writel(5, SW_MUX_GPIO1_IO03); /*寄存器SW_PAD_GPIO1_IO03设置IO属性 *bit 16:0 HYS关闭 *bit [15:14]: 00 默认下拉 *bit [13]: 0 kepper功能 *bit [12]: 1 pull/keeper使能 *bit [11]: 0 关闭开路输出 *bit [7:6]: 10 速度100Mhz *bit [5:3]: 110 R0/6驱动能力 *bit [0]: 0 低转换率 */ writel(0x10B0, SW_PAD_GPIO1_IO03); /* 4、设置GPIO1_IO03为输出功能 */ val = readl(GPIO1_GDIR); val &= ~(1 << 3); /* 清除以前的设置 */ val |= (1 << 3); /* 设置为输出 */ writel(val, GPIO1_GDIR); /* 5、默认关闭LED */ val = readl(GPIO1_DR); val |= (1 << 3); writel(val, GPIO1_DR); /* 6、注册字符设备驱动 */ retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops); if (retvalue < 0) { printk("register chrdev failed!\r\n"); return -EIO; } return 0; } /* * @description : 驱动出口函数 * @param : 无 * @return : 无 */ static void __exit led_exit(void) { /* 取消映射 */ iounmap(IMX6U_CCM_CCGR1); iounmap(SW_MUX_GPIO1_IO03); iounmap(SW_PAD_GPIO1_IO03); iounmap(GPIO1_DR); iounmap(GPIO1_GDIR); /* 注销字符设备驱动 */ unregister_chrdev(LED_MAJOR, LED_NAME); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("zuozhongkai");
结合文档和源码,我们可以看到,一个驱动模块开发应该包含一下内容:
包含内核头文件
定义设备操作函数结构体
实现设备操作函数
定义模块初始化和退出函数
定义模块信息
此外还需要遵循一些特定的规范,比如:
模块信息通常放在文件的末尾,紧挨着模块的初始化和退出函数。
必须定义
MODULE_LICENSE
,其他模块信息(如作者、描述、版本号)是可选的,但建议尽量提供。......
一个简单的模块示例如下:
cpp#include <linux/module.h> #include <linux/init.h> static int my_open(void) { /*......*/ } /*......*/ static struct file_operations my_fops = { .owner = THIS_MODULE, // 指向当前模块 .open = my_open, // 打开设备 .read = my_read, // 读取设备 .write = my_write, // 写入设备 .release = my_release, // 关闭设备 }; static int __init my_init(void) { printk(KERN_INFO "Module loaded\n"); return 0; } static void __exit my_exit(void) { printk(KERN_INFO "Module unloaded\n"); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("A simple example module"); MODULE_VERSION("1.0");
③分析源码细节(人机对战)
- 为什么要用到
ioremap
和iounmap?
Linux 内核运行在虚拟地址空间(MMU),无法直接访问物理地址。
ioremap
的作用就是将硬件的物理地址映射到内核的虚拟地址空间,使得内核可以通过虚拟地址访问硬件寄存器。ioremap
返回的是虚拟地址指针。
ioremap
的作用是释放映射的虚拟地址空间,当模块卸载时,需要释放之前映射的虚拟地址空间,以避免内存泄漏。ioremap
和iounmap
确保内核能够安全地访问硬件资源,并在模块卸载时释放资源。所谓映射可以理解为MMU分配页表什么的,所以才需要释放虚拟地址,不然虚拟地址空间就会被消耗殆尽。
- 注册字符驱动设备这个步骤是干什么的?
字符设备注册是 将驱动程序与设备号关联,并在内核中注册设备。注册字符设备后,用户空间程序可以通过设备文件(如
/dev/led
)访问设备。
- 设备号是干什么的?为什么要分为主次两种?这样做是为了解决什么问题?
设备号用于在内核中唯一标识一个设备,内核通过设备号管理设备,确保多个设备可以共存,用户空间程序通过设备文件(如
/dev/led
)访问设备时,内核根据设备号找到对应的驱动程序。主设备号可以标识设备类型(如 LED、键盘、串口等),主设备号是全局唯一的,由内核或开发者分配,一个主设备号对应一个驱动程序。
次设备号用于标识具体设备,用于区分同一类型的多个设备。比如主设备号为
200
的 LED 设备可能有多个次设备号:次设备号0
表示第一个 LED、次设备号1
表示第二个 LED。通过主次设备号的设计,Linux 内核能够高效地管理大量设备,并为用户空间程序提供统一的访问接口。开发者只需实现一个驱动程序,即可支持多个设备实例,同一类型的多个设备可以共享一个驱动程序,减少了内核资源的浪费。
- 也就是在驱动模块开发时分配设备号,那么加载驱动模块时,就可以产生设备文件(如/dev/led)?那么分配设备号还有哪些方式?
在 Linux 驱动模块开发中,设备号的分配和设备文件的创建是两个关键步骤。加载驱动模块时,设备文件(如
/dev/led
)并不会自动生成,而是需要手动创建或通过其他机制自动创建。设备号的分配分为静态和动态两种,动态分配是通过使用
alloc_chrdev_region
函数由内核动态分配设备号。一般可以使用混合分配的方式,**优先动态分配,失败时回退到静态分配,**这样就可以兼具动态分配的灵活性和静态分配的稳定性。
- 为什么还要手动或自动创建设备文件?为什么不在加载驱动模块时创建呢?既然创建设备文件需要主次设备号,那我明明在驱动模块开发过程中分配了,为何还要在创建设备文件时再分配一次呢?
驱动模块加载时,内核会初始化驱动程序并分配设备号,但并不会自动创建设备文件。设备文件的创建通常由用户空间工具(如
mknod
)或内核机制(如udev
、devtmpfs
)完成。设备文件的创建可能涉及用户空间的配置(如权限、所有者等),这些配置不适合在内核中处理。设备文件的创建时机可能需要根据系统状态动态调整。除此之外还有一部分历史原因,早期的 Linux 内核没有自动创建设备文件的机制,开发者需要手动使用
mknod
创建设备文件。现代 Linux 内核引入了udev
和devtmpfs
,可以自动创建设备文件,但仍然保留了手动创建的选项。两者分离便于职责分离,有更大的灵活性。设备号的分配是内核的职责,用于管理设备和驱动程序。设备文件的创建是用户空间的职责,用于提供用户访问接口。两者分离后,设备文件的创建可以根据系统配置动态调整(如权限、所有者等),可以延迟到设备实际使用时(如热插拔设备)。
现代 Linux 内核提供了自动创建设备文件的机制,开发者无需手动使用
mknod
,使用udev
或devtmpfs
可以自动创建设备文件。udev
是 Linux 的用户空间设备管理器,负责管理/dev
目录下的设备文件,当内核检测到新设备时,udev
会根据规则自动创建设备文件。驱动程序需要在初始化时调用class_create
和device_create
函数,向udev
提供设备信息。
- open和write函数有什么区别?
- 在开发stm32的驱动时,可以调用各种库函数来辅助开发,在嵌入式Linux的驱动开发中,只能从寄存器级别开发、不能使用NXP官方提供的库函数吗?
在嵌入式 Linux 驱动开发中,与 STM32 的开发方式有所不同。STM32 的开发通常依赖于厂商提供的库函数(如 HAL 库或标准外设库),而在嵌入式 Linux 驱动开发中,通常不会直接使用厂商提供的库函数(如 NXP 提供的 SDK 库),而是通过以下方式操作硬件:
寄存器级别开发
使用内核提供的 API
使用设备树(Device Tree)
**使用现成的驱动框架,**Linux 内核提供了许多现成的驱动框架(如 I2C、SPI、USB 等),开发者可以基于这些框架实现驱动,而无需从零开始。
厂商提供的库函数通常是为裸机或 RTOS 环境设计的,而 Linux 内核运行在内核空间,对内存管理、中断处理等有严格的要求。如果直接使用厂商库函数可能导致内核崩溃或资源冲突。Linux 内核提供了丰富的 API 来操作硬件,这些 API 是专门为内核空间设计的,能够更好地与内核的其他部分协同工作。直接操作寄存器或使用内核 API 可以提高驱动的可移植性,使其更容易适配不同的内核版本和硬件平台。
......
......
④编译驱动模块和测试程序
稍微修改一下write代码,编译时发现了一个警告,万万没想到会出现C90标准
稍微查了一下,这是历史原因
但我有点不太相信,现代Linux都有使用Rust编写的部分了,不可能这般守旧才对
为了兼容性,沉重的历史包袱是难免的。在Makefile里添加这一句
bash# 添加 C11 标准支持 ccflags-y := -std=gnu11 -Wno-declaration-after-statement
为了测试驱动模块,还需要编写应用程序,此处即ledApp,为此,编译还需要添加一个目标ledApp
bashKERNELDIR := /home/fairy/Embedded/program/Alientek_Uboot_Linux/linux CURRENT_PATH := $(shell pwd) obj-m := led.o # 添加 C11 标准支持 ccflags-y := -std=gnu11 -Wno-declaration-after-statement all: kernel_modules ledApp kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules # 编译应用程序 ledApp: ledApp.c arm-linux-gnueabihf-gcc -o $@ $< clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
测试程序的代码很清晰,先打开文件,再写入数据,最后关闭。main函数里的两个参数,前者为参数个数,后者为参数指针数组。编译的程序为ledApp.elf(在linux里elf后缀一般不写),运行程序时传入参数是指在命令行中输入程序名称,并且在名称后输入一些内容(参数),比如下面,传入的第二个参数是1,第一个参数默认都是程序自身的名称
bash./ledApp 1
放在程序里下面这一步就是把传入的参数"1"存放到数组databuf里
⑤测试驱动模块
把驱动模块和测试应用程序都传输到虚拟机的nfs挂载点
在开发板的挂载点里可以看到文件已经成功传入
加载驱动模块
bashinsmod /mnt/nfs/led.ko
列出设备
bashlsmod
查看设备
bashcat /proc/devices
查看设备节点
bashls -l /dev/
可以看到是没有led设备节点的,因为没有创建设备节点,驱动模块使用的还是例程源码,并没有添加自动创建设备节点udev需要的相关函数。
创建字符设备节点
bashmknod /dev/led c 200 0
可以看到led设备节点已经创建成功了测试应用程序
测试之前需要关闭led自动闪烁功能
bashecho none > /sys/class/leds/sys-led/trigger
此时输入命令,才发现架构不对,虚拟机使用的是x86_x64,而开发板是arm32,应使用交叉编译工具链,也就是说前面的Makefile编译ledApp时,需要把gcc改为arm-linux-gnueabihf-gcc(已改)
此时使用下面0和1两个参数测试,实验结果与预期相符
bash./ledApp /dev/led 1
bash./ledApp /dev/led 0
使用rmmod卸载模块时,设备节点/dev/led并不会消失,还需要使用rm来手动删除
bashrmmod led.ko
bashrm /dev/led
⑥尝试新方法
前面和DeepSeek对话中,可以获知混合分配设备号更推荐,udev自动创建设备文件更现代。继续提问,还有更多更现代的做法,比如驱动和硬件分离,不过这要用到设备树,一些做法可以先放一放。
下面的代码只要用到了1、5和6,其他需要设备树配合
cpp#include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/init.h> #include <linux/module.h> #include <linux/errno.h> #include <linux/gpio.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/io.h> #include <linux/slab.h> #include <linux/uaccess.h> #define LED_MAJOR 200 /* 主设备号 */ #define LED_NAME "led" /* 设备名字 */ #define LEDOFF 0 /* 关灯 */ #define LEDON 1 /* 开灯 */ /* 寄存器物理地址 */ #define CCM_CCGR1_BASE (0X020C406C) #define SW_MUX_GPIO1_IO03_BASE (0X020E0068) #define SW_PAD_GPIO1_IO03_BASE (0X020E02F4) #define GPIO1_DR_BASE (0X0209C000) #define GPIO1_GDIR_BASE (0X0209C004) /* 映射后的寄存器虚拟地址指针 */ static void __iomem *IMX6U_CCM_CCGR1; static void __iomem *SW_MUX_GPIO1_IO03; static void __iomem *SW_PAD_GPIO1_IO03; static void __iomem *GPIO1_DR; static void __iomem *GPIO1_GDIR; /* 设备号 */ static dev_t devno; static struct cdev led_cdev; static struct class *led_class; static struct device *led_device; /* * @description : LED打开/关闭 * @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED * @return : 无 */ static void led_switch(u8 sta) { u32 val; if (sta == LEDON) { val = readl(GPIO1_DR); val &= ~(1 << 3); writel(val, GPIO1_DR); } else if (sta == LEDOFF) { val = readl(GPIO1_DR); val |= (1 << 3); writel(val, GPIO1_DR); } } /* * @description : 打开设备 * @param - inode : 传递给驱动的inode * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量 * 一般在open的时候将private_data指向设备结构体。 * @return : 0 成功;其他 失败 */ static int led_open(struct inode *inode, struct file *filp) { return 0; } /* * @description : 从设备读取数据 * @param - filp : 要打开的设备文件(文件描述符) * @param - buf : 返回给用户空间的数据缓冲区 * @param - cnt : 要读取的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 读取的字节数,如果为负值,表示读取失败 */ static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { return 0; } /* * @description : 向设备写数据 * @param - filp : 设备文件,表示打开的文件描述符 * @param - buf : 要写给设备写入的数据 * @param - cnt : 要写入的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 写入的字节数,如果为负值,表示写入失败 */ static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { unsigned char databuf[1]; int retvalue = copy_from_user(databuf, buf, cnt); if (retvalue < 0) { pr_err("kernel write failed!\r\n"); return -EFAULT; } led_switch(databuf[0]); return 0; } /* * @description : 关闭/释放设备 * @param - filp : 要关闭的设备文件(文件描述符) * @return : 0 成功;其他 失败 */ static int led_release(struct inode *inode, struct file *filp) { return 0; } /* 设备操作函数 */ static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release, }; /* * @description : 驱动入口函数 * @param : 无 * @return : 无 */ static int __init led_init(void) { u32 val; int retvalue; /* 1、寄存器地址映射 */ IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4); SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4); SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4); GPIO1_DR = ioremap(GPIO1_DR_BASE, 4); GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4); if (!IMX6U_CCM_CCGR1 || !SW_MUX_GPIO1_IO03 || !SW_PAD_GPIO1_IO03 || !GPIO1_DR || !GPIO1_GDIR) { pr_err("ioremap failed!\r\n"); retvalue = -ENOMEM; goto err_ioremap; } /* 2、使能GPIO1时钟 */ val = readl(IMX6U_CCM_CCGR1); val &= ~(3 << 26); /* 清除以前的设置 */ val |= (3 << 26); /* 设置新值 */ writel(val, IMX6U_CCM_CCGR1); /* 3、设置GPIO1_IO03的复用功能 */ writel(5, SW_MUX_GPIO1_IO03); /* 4、设置GPIO1_IO03的IO属性 */ writel(0x10B0, SW_PAD_GPIO1_IO03); /* 5、设置GPIO1_IO03为输出功能 */ val = readl(GPIO1_GDIR); val &= ~(1 << 3); /* 清除以前的设置 */ val |= (1 << 3); /* 设置为输出 */ writel(val, GPIO1_GDIR); /* 6、默认关闭LED */ val = readl(GPIO1_DR); val |= (1 << 3); writel(val, GPIO1_DR); /* 7、设备号混合分配 */ retvalue = alloc_chrdev_region(&devno, 0, 1, LED_NAME); if (retvalue < 0) { pr_err("dynamic alloc chrdev failed, try static alloc!\r\n"); devno = MKDEV(LED_MAJOR, 0); retvalue = register_chrdev_region(devno, 1, LED_NAME); if (retvalue < 0) { pr_err("static alloc chrdev failed!\r\n"); goto err_alloc_chrdev; } } /* 8、初始化 cdev */ cdev_init(&led_cdev, &led_fops); led_cdev.owner = THIS_MODULE; /* 9、添加 cdev 到内核 */ retvalue = cdev_add(&led_cdev, devno, 1); if (retvalue < 0) { pr_err("cdev_add failed!\r\n"); goto err_cdev_add; } /* 10、创建设备类 */ led_class = class_create(THIS_MODULE, LED_NAME); if (IS_ERR(led_class)) { pr_err("create class failed!\r\n"); retvalue = PTR_ERR(led_class); goto err_class_create; } /* 11、创建设备节点 */ led_device = device_create(led_class, NULL, devno, NULL, LED_NAME); if (IS_ERR(led_device)) { pr_err("create device failed!\r\n"); retvalue = PTR_ERR(led_device); goto err_device_create; } pr_info("LED driver initialized\n"); return 0; err_device_create: class_destroy(led_class); err_class_create: cdev_del(&led_cdev); err_cdev_add: unregister_chrdev_region(devno, 1); err_alloc_chrdev: iounmap(IMX6U_CCM_CCGR1); iounmap(SW_MUX_GPIO1_IO03); iounmap(SW_PAD_GPIO1_IO03); iounmap(GPIO1_DR); iounmap(GPIO1_GDIR); err_ioremap: return retvalue; } /* * @description : 驱动出口函数 * @param : 无 * @return : 无 */ static void __exit led_exit(void) { /* 销毁设备节点 */ device_destroy(led_class, devno); /* 销毁设备类 */ class_destroy(led_class); /* 删除 cdev */ cdev_del(&led_cdev); /* 释放设备号 */ unregister_chrdev_region(devno, 1); /* 取消映射 */ iounmap(IMX6U_CCM_CCGR1); iounmap(SW_MUX_GPIO1_IO03); iounmap(SW_PAD_GPIO1_IO03); iounmap(GPIO1_DR); iounmap(GPIO1_GDIR); pr_info("LED driver exited\n"); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("fairy"); MODULE_DESCRIPTION("LED Driver");
使用udev后,仅仅使用insmod加载驱动,就可以自动创建设备文件,使用ledApp测试时,实验结果如预期
卸载只需要使用rmmod,不必在手动使用rm删除设备节点
2,动态加载_新方式
继续翻阅文档,发现下一节,新字符设备驱动实验的观点与AI不谋而合
不过对于设备号的分配却不相同,文档是先静态后动态,而AI是先动态再静态。重新问了几次,它自己推翻了自己,问及原因时,它这样答道:
那么就遵循现代Linux驱动开发的推荐做法,使用动态分配。
同时文档里使用了"设置文件私有数据",这种做法在现代 Linux 驱动开发中也是非常常见且推荐的,因为它可以方便地在驱动的其他操作函数(如
read
、write
、release
等)中访问设备相关的数据。优化后的代码如下
cpp#include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/init.h> #include <linux/module.h> #include <linux/errno.h> #include <linux/gpio.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/io.h> #include <linux/slab.h> #include <linux/uaccess.h> #define LED_NAME "led" /* 设备名字 */ #define LEDOFF 0 /* 关灯 */ #define LEDON 1 /* 开灯 */ /* 寄存器物理地址 */ #define CCM_CCGR1_BASE (0X020C406C) #define SW_MUX_GPIO1_IO03_BASE (0X020E0068) #define SW_PAD_GPIO1_IO03_BASE (0X020E02F4) #define GPIO1_DR_BASE (0X0209C000) #define GPIO1_GDIR_BASE (0X0209C004) /* 设备结构体 */ struct led_dev { dev_t devno; /* 设备号 */ struct cdev cdev; /* 字符设备 */ struct class *class; /* 设备类 */ struct device *device; /* 设备实例 */ void __iomem *reg_base; /* 寄存器基地址 */ int led_state; /* LED 状态 */ }; static struct led_dev *led_devices; /* 设备实例 */ /* * @description : LED打开/关闭 * @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED * @param - reg_base: 寄存器基地址 * @return : 无 */ static void led_switch(u8 sta, void __iomem *reg_base) { u32 val; if (sta == LEDON) { val = readl(reg_base); val &= ~(1 << 3); writel(val, reg_base); } else if (sta == LEDOFF) { val = readl(reg_base); val |= (1 << 3); writel(val, reg_base); } } /* * @description : 打开设备 * @param - inode : 传递给驱动的inode * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量 * 一般在open的时候将private_data指向设备结构体。 * @return : 0 成功;其他 失败 */ static int led_open(struct inode *inode, struct file *filp) { struct led_dev *dev; /* 获取设备结构体 */ dev = container_of(inode->i_cdev, struct led_dev, cdev); filp->private_data = dev; /* 设置私有数据 */ pr_info("Device opened\n"); return 0; } /* * @description : 从设备读取数据 * @param - filp : 要打开的设备文件(文件描述符) * @param - buf : 返回给用户空间的数据缓冲区 * @param - cnt : 要读取的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 读取的字节数,如果为负值,表示读取失败 */ static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { return 0; } /* * @description : 向设备写数据 * @param - filp : 设备文件,表示打开的文件描述符 * @param - buf : 要写给设备写入的数据 * @param - cnt : 要写入的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 写入的字节数,如果为负值,表示写入失败 */ static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { struct led_dev *dev = filp->private_data; unsigned char databuf[1]; int retvalue = copy_from_user(databuf, buf, cnt); if (retvalue < 0) { pr_err("kernel write failed!\r\n"); return -EFAULT; } /* 使用设备私有数据 */ led_switch(databuf[0], dev->reg_base); return 0; } /* * @description : 关闭/释放设备 * @param - filp : 要关闭的设备文件(文件描述符) * @return : 0 成功;其他 失败 */ static int led_release(struct inode *inode, struct file *filp) { pr_info("Device released\n"); return 0; } /* 设备操作函数 */ static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release, }; /* * @description : 驱动入口函数 * @param : 无 * @return : 无 */ static int __init led_init(void) { u32 val; int retvalue; /* 动态分配设备结构体 */ led_devices = kzalloc(sizeof(struct led_dev), GFP_KERNEL); if (!led_devices) { pr_err("Failed to allocate device data\n"); return -ENOMEM; } /* 1、寄存器地址映射 */ led_devices->reg_base = ioremap(GPIO1_DR_BASE, 4); if (!led_devices->reg_base) { pr_err("ioremap failed!\r\n"); retvalue = -ENOMEM; goto err_ioremap; } /* 2、使能GPIO1时钟 */ val = readl(ioremap(CCM_CCGR1_BASE, 4)); val &= ~(3 << 26); /* 清除以前的设置 */ val |= (3 << 26); /* 设置新值 */ writel(val, ioremap(CCM_CCGR1_BASE, 4)); /* 3、设置GPIO1_IO03的复用功能 */ writel(5, ioremap(SW_MUX_GPIO1_IO03_BASE, 4)); /* 4、设置GPIO1_IO03的IO属性 */ writel(0x10B0, ioremap(SW_PAD_GPIO1_IO03_BASE, 4)); /* 5、设置GPIO1_IO03为输出功能 */ val = readl(ioremap(GPIO1_GDIR_BASE, 4)); val &= ~(1 << 3); /* 清除以前的设置 */ val |= (1 << 3); /* 设置为输出 */ writel(val, ioremap(GPIO1_GDIR_BASE, 4)); /* 6、默认关闭LED */ val = readl(led_devices->reg_base); val |= (1 << 3); writel(val, led_devices->reg_base); /* 7、动态分配设备号 */ retvalue = alloc_chrdev_region(&led_devices->devno, 0, 1, LED_NAME); if (retvalue < 0) { pr_err("Failed to allocate device number\n"); goto err_alloc_chrdev; } /* 8、初始化 cdev */ cdev_init(&led_devices->cdev, &led_fops); led_devices->cdev.owner = THIS_MODULE; /* 9、添加 cdev 到内核 */ retvalue = cdev_add(&led_devices->cdev, led_devices->devno, 1); if (retvalue < 0) { pr_err("cdev_add failed!\r\n"); goto err_cdev_add; } /* 10、创建设备类 */ led_devices->class = class_create(THIS_MODULE, LED_NAME); if (IS_ERR(led_devices->class)) { pr_err("create class failed!\r\n"); retvalue = PTR_ERR(led_devices->class); goto err_class_create; } /* 11、创建设备节点 */ led_devices->device = device_create(led_devices->class, NULL, led_devices->devno, NULL, LED_NAME); if (IS_ERR(led_devices->device)) { pr_err("create device failed!\r\n"); retvalue = PTR_ERR(led_devices->device); goto err_device_create; } pr_info("LED driver initialized\n"); return 0; err_device_create: class_destroy(led_devices->class); err_class_create: cdev_del(&led_devices->cdev); err_cdev_add: unregister_chrdev_region(led_devices->devno, 1); err_alloc_chrdev: iounmap(led_devices->reg_base); err_ioremap: kfree(led_devices); return retvalue; } /* * @description : 驱动出口函数 * @param : 无 * @return : 无 */ static void __exit led_exit(void) { /* 销毁设备节点 */ device_destroy(led_devices->class, led_devices->devno); /* 销毁设备类 */ class_destroy(led_devices->class); /* 删除 cdev */ cdev_del(&led_devices->cdev); /* 释放设备号 */ unregister_chrdev_region(led_devices->devno, 1); /* 取消映射 */ iounmap(led_devices->reg_base); /* 释放设备结构体 */ kfree(led_devices); pr_info("LED driver exited\n"); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("fairy"); MODULE_DESCRIPTION("LED Driver");
实验结果如预期,接下来可以使用设备树来尝试更新的方法
3,设备树的初步了解
关于设备树的介绍,先初步浏览了解一下,知道设备树与驱动开发相互配合,有函数可以访问设备树节点信息就行,再通过后面的例子进行深入学习,加深印象。
浏览了文档的后面内容,驱动这一章节的篇幅是真的大,如果一章一章地学,实在不符合我想要快速上手的目的。后面的章节有SPI、I2C什么的,这个还是等到需要用到的时候再专门查文档学习吧。
先尝试搭建Qt环境吧,如果成功的话,那么后续可以边开发驱动,边开发对应的界面来实现复杂功能的控制
四、Qt环境搭建尝试
尝试了许久,各种混账的兼容性问题频出,最终发现还是不如考古。正点原子资料盘里的虚拟机光盘里已经搭建好了所有环境,可以直接使用,可以根据目录跳转到本章的第3个的第④个。前面的内容少儿不宜,埋藏着笔者深深的怨气。
1、Qt安装_Windows(可跳过)
这里选用的是Qt5.15.2,这是Qt5的最后一个版本,同时也是LTS。从这个镜像网站里下载Qt Downloads
安装时,遵循一般博客里的做法即可,首先是要创建账号的。不过直接进入这个程序,下载还是会失败(贼他宝贝的麻烦),需要让Qt下载程序用镜像网站下载,参考博客windows安装QT时出现"无法下载存档......"解决办法 - lmore - 博客园
注意,腾讯的镜像网站里只有6.8以上的版本(一共就三个),不要用。清华的镜像可以下载5.12.2,但下载6.8.1也会报什么文档下载失败的错误。试了几个镜像,就清华的这个比较全,但这些镜像下载旧版要比官方好,但新版就不行了
在你Qt下载程序所在的目录,打开终端,输入下面命令,左边是你的Qt下载程序,输入前面./qt,然后按Tab键,一般就可以自动补全了。
bash.\qt-online-installer-windows-x64-4.8.1_2.exe --mirror https://mirrors.tuna.tsinghua.edu.cn/qt/
当选择版本时,一开始是没有Qt5之类的版本,把右边的Archive勾选上,然后再点击筛选
2,使用正点原子项目(可跳过)
①转移项目
把资料盘里的Qt应用程序复制到一个不含中文和特殊符号的路径
②编译项目
这里先测试一下这个Qt程序是什么样子的,选择MinGW64bit这个编译工具链
找到刚才那个项目里的pro文件
出现这个界面后,先勾选MinGW64bit(下图为32bit,都差不多),然后向下滑动,点击configure program
进入到下面项目
点击上方的构建栏,里面有运行
点击运行后,就可以编译出Qt应用程序了,一切如预期那样。不过要注意,此时编译的程序是x86_x64架构的,后缀名为exe,而非是开发板arm32架构(后缀名为elf文件)
3,交叉编译
①下载交叉编译工具链(可跳过)
在Downloads | 9.2-2019.12 -- Arm Developer下载9.2的交叉编译工具链,如果是Linux使用,那么就下载下面这个
如果是Windows,那么就下载这个
下载后,把它解压在一个合适的目录(不能含有中文)。
不过考虑到这个编译器暂时不会与其他编译器的名称起冲突,那么就先添加环境变量。按下Win+X,选择【系统】,再点击【高级系统设置】,再点击【环境变量】。要编辑的是下面这个Path
在里面把刚才的路径复制过去,下面是参考,自行修改
bashE:\Tools\Develop\ToolsKits\ARM\arm-gnu-toolchain-14.2.rel1-mingw-w64-i686-arm-none-linux-gnueabihf\bin
一路点击确定,最后重启电脑。重启后,打开终端,输入下面语句,观察是否有版本信息
basharm-none-linux-gnueabihf-gcc -v
②交叉编译Qt源码库_Windows(失败的)
下载5.12.2的Qt源码
Index of /archive/qt/5.15/5.15.2/single
下载后,找到一个不含中文的目录,解压
找到如下路径
由于我们的目标是编译arm32平台的linux程序,所以这里选择linux-arm-gnueabi-g++,用记事本打开,可以看到这里的编译器与我们下载的编译器基本是匹配的,而且前面也将环境变量添加上去了,就不需要再这里添加路径了。
所以只需要把arm-linux前缀改为arm-none-linux,gnueabi改为gnueabihf即可
在Qt源码目录,打开终端输入下面命令
bash./configure.bat -release -opensource -prefix E:\Tools\Develop\ToolsKits\Qt\qt-5.15.2-arm -xplatform linux-arm-gnueabi-g++ -nomake tests -nomake examples -no-opengl -skip qtvirtualkeyboard -skip qtwebengine
-prefix
:指定Qt库的安装路径,自行选择
-xplatform
:指定交叉编译平台
-nomake
:跳过不需要的模块以加快编译速度如果出现下面错误,在环境变量里添加MSVC的bin目录即可
这个nmake的路径比较复杂,首先找到安装的VS的目录,如下,2022是版本号,按此路径最终找到下面目录(自行替换)
bashC:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.41.34120\bin\Hostx64\x64
bashC:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.41.34120\include
不过运行这个bat脚本中可能会出现下面这个沙雕错误(荼毒无穷),添加环境变量也没有任何作用
根据VS安装目录,找到下面路径,在这里打开终端,执行下面命令
bash.\vcvarsall.bat x64
然后你就会发现,啥用没有。后续找了许久,发现忘了制定平台
bash./configure.bat -release -opensource -prefix E:\Tools\Develop\ToolsKits\Qt\qt-5.15.2-arm -xplatform linux-arm-gnueabi-g++ -nomake tests -nomake examples -no-opengl -skip qtvirtualkeyboard -skip qtwebengine -platform win32-g++
但我他宝贝的没高兴多久又寄了
找到下面路径
用记事本打开qglobal.h ,添加这个头文件
cpp#include <limits>
诸如此类,尝试了许多种方法,最终带着深深的怨气总算找到了疑似靠谱的方法(实用MSVC)。按Win打开菜单,找到VS的命令行,x64和x86随意,这里用的是
x64 Native Tools Command Prompt for VS 2022
在打开的cmd窗口中,使用cd命令跳转到Qt源码的目录,不过要注意的是在cd命令后加上/d参数,才能执行跨盘操作
bashcd /d E:/Tools/Develop/ToolsKits/Qt/qt-everywhere-src-5.15.2/
然后输入下面命令,不用加platform选项
bashconfigure.bat -release -opensource -prefix E:\Tools\Develop\ToolsKits\Qt\qt-5.15.2-arm -xplatform linux-arm-gnueabi-g++ -nomake tests -nomake examples -no-opengl -skip qtvirtualkeyboard -skip qtwebengine -platform win32-g++
输入y即可
如果出现下图,说明qmake.tconf里的编译工具集的名称没有写对,或者环境变量没有生效,自行检查
然后使用make构建,电脑有几核就输入几
bashmingw32-make -j16
然后就没有然后了,会报一些C++错误
③交叉编译Qt源码库_Linux(失败的)
下载5.12.2的Qt源码,选择下面那个tar.xz
Index of /archive/qt/5.15/5.15.2/single
然后下载工具链,Arm GNU Toolchain Downloads -- Arm Developer
通过Filezila传输到虚拟机里
工具链的解压用下面命令,Qt源码也是如此
bashtar -xvJf arm-gnu-toolchain-14.2.rel1-x86_64-arm-none-linux-gnueabihf.tar.xz
添加环境变量,需要修改下面文件
bashsudo vim /etc/environment
在原变量里加上冒号,后面再添加路径,路径改为自己的工具链路径
bashPATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/fairy/Embedded/Toolkits/toolchain/arm-gnu-toolchain-14.2.rel1-x86_64-arm-none-linux-gnueabihf/bin"
重启后,环境变量就会生效。
接着我们安装依赖,先更新apt
bashsudo apt update
然后安装依赖
bashsudo apt install build-essential libgl1-mesa-dev libxkbcommon-dev libxcb-xinerama0-dev libxcb-xinput-dev libfontconfig1-dev libfreetype6-dev libdbus-1-dev libicu-dev libssl-dev libjpeg-dev libpng-dev libpcre3-dev libz-dev
由于配置过长,需要配置一个脚本,路径/path/to/install/qt5.12.2自行替换
bash#!/bin/bash ./configure -prefix /path/to/install/qt5.12.2 \ -opensource \ -confirm-license \ -release \ -xplatform linux-arm-gnueabi-g++ \ -no-opengl \ -no-sse2 \ -no-xcb \ -qt-libjpeg \ -qt-libpng \ -qt-zlib \ -qt-pcre \ -qt-freetype \ -qt-harfbuzz \ -no-openssl \ -no-cups \ -no-dbus \ -no-glib \ -no-iconv \ -no-icu \ -no-eglfs \ -no-linuxfb \ -no-kms \ -no-gtk \ -no-xkbcommon \ -no-xcb-xlib \ -no-xinput2 \ -no-xcb-xinput \ -no-xcb-randr \ -no-xcb-shape \ -no-xcb-sync \ -no-xcb-xfixes \ -no-xcb-xkb \ -no-xkbcommon-x11 \ -no-xrender \ -no-xi \ -no-xext \ -no-fontconfig \ -no-freetype \ -no-harfbuzz \ -no-pcre \ -no-zlib \ -no-jpeg \ -no-png \ -no-gif \ -no-sqlite \ -no-libudev \ -no-evdev \ -no-mtdev \ -no-tslib \ -no-libinput \ -no-gstreamer \ -no-pulseaudio \ -no-alsa \ -no-vulkan \ -no-qml-debug \ -no-compile-examples \ -nomake examples \ -nomake tests \ -no-tslib
使用chmod赋予脚本权限,假设脚本为autoConfig.sh
bashchmod 777 autoConfig.sh
然后在源码目录里运行脚本,执行脚本之后就会报缺少limits什么的错误,到下面目录,找到qglobal.h 添加这个头文件即可
cpp#include <limits>
构建成功后
然后使用make
bashmake -j16
④交叉编译_正点原子
事实证明,有些护城河就不是河,简直就是天堑。知道C++的abi不稳定,但没想到会是这般不稳定,尝试了一天,在已有体系上5.12实在编译不了,我都快准备放弃Qt,使用LVGL了。最后,只能强忍着不适,继续尝试下去。
可能是MinGW版本不对,可能是GCC版本不对,可能是Qt配置的某些选项不对,可能是不同厂商的gcc对某些特定abi不兼容,可能是Ubuntu版本不对,......,可能性太多了,Windows平台是不寄予希望了,还是考古吧(已老实,求放过)。
按照文档指示,安装正点原子的ubuntu2016
在原有网卡基础上,再添加一个NAT模式,用于联网
设置好之后,打开虚拟机,进入设置,找到NetWork
进入NetWork后,可以看到两个Wired(有线连接),第一个往往是桥接模式(eth0),第二个是NAT模式(eth1)。为了与外界进行交互,我们修改第一个(桥接模式)的ip,把它设置为静态IP。
点击Options,找到IPv4 Settings,然后切换为手动模式(静态)
使用ifconfig查看,ip已被正确设置
记住下面这个ens37这个ip,这是NAT模式的,我们使用Filezila与虚拟机传输,用的ip就是它。此外,正点原子已经为这个Ubuntu安装好了FTP服务,并且已经配置好了。
如果出现乱码,在站点管理器里,把字符集设置为强制使用UTF-8
编译一二十分钟,然后报错,这一点我是万万没想到的。报了一个override错误
又重试了一遍,终于成功了。使用的脚本是正点原子里的。此步骤可以省略,因为正点原子虚拟光盘里已经有编译好的Kits
bash./configure -prefix /home/alientek/Qt/arm-qt \ -opensource \ -confirm-license \ -release \ -strip \ -shared \ -xplatform linux-arm-gnueabi-g++ \ -optimized-qmake \ -c++std c++11 \ --rpath=no \ -pch \ -skip qt3d \ -skip qtactiveqt \ -skip qtandroidextras \ -skip qtcanvas3d \ -skip qtconnectivity \ -skip qtdatavis3d \ -skip qtdoc \ -skip qtgamepad \ -skip qtlocation \ -skip qtmacextras \ -skip qtnetworkauth \ -skip qtpurchasing \ -skip qtremoteobjects \ -skip qtscript \ -skip qtscxml \ -skip qtsensors \ -skip qtspeech \ -skip qtsvg \ -skip qttools \ -skip qttranslations \ -skip qtwayland \ -skip qtwebengine \ -skip qtwebview \ -skip qtwinextras \ -skip qtx11extras \ -skip qtxmlpatterns \ -make libs \ -make examples \ -nomake tools -nomake tests \ -gui \ -widgets \ -dbus-runtime \ --glib=no \ --iconv=no \ --pcre=qt \ --zlib=qt \ -no-openssl \ --freetype=qt \ --harfbuzz=qt \ -no-opengl \ -linuxfb \ --xcb=no \ -tslib \ --libpng=qt \ --libjpeg=qt \ --sqlite=qt \ -plugin-sql-sqlite \ -I/home/alientek/tslib-1.21/arm-tslib/include \ -L/home/alientek/tslib-1.21/arm-tslib/lib \ -recheck-all
bashtime (make -j16)
简直就是神迹!
bashtime (make install)
后续又试了一下,同样编译器和构建命令的情况下,Ubuntu2024会出现下面错误
后续测试了一下Ubunt2024、Ubuntu2016与gcc9.2、gcc14.2的排列组合,只有Ubuntu2016和gcc9.2的组合可以正常编译。
换成Windows平台,使用同样的gcc编译器(9.2),同样的命令,只会编译出下面结果。也许是MinGW版本不对,MinGW32(gcc9.2)和MinGW64(gcc14.2)都不行
bash./configure.bat -prefix E:\Tools\Develop\ToolsKits\Qt\qt-5.15.2-arm-gcc -opensource -confirm-license -release -strip -shared -xplatform linux-arm-gnueabi-g++ -optimized-qmake -c++std c++11 --rpath=no -pch -skip qt3d -skip qtactiveqt -skip qtandroidextras -skip qtcanvas3d -skip qtconnectivity -skip qtdatavis3d -skip qtdoc -skip qtgamepad -skip qtlocation -skip qtmacextras -skip qtnetworkauth -skip qtpurchasing -skip qtremoteobjects -skip qtscript -skip qtscxml -skip qtsensors -skip qtspeech -skip qtsvg -skip qttools -skip qttranslations -skip qtwayland -skip qtwebengine -skip qtwebview -skip qtwinextras -skip qtx11extras -skip qtxmlpatterns -make libs -make examples -nomake tools -nomake tests -gui -widgets -dbus-runtime --glib=no --iconv=no --pcre=qt --zlib=qt -no-openssl --freetype=qt --harfbuzz=qt -no-opengl -linuxfb --xcb=no --libpng=qt --libjpeg=qt --sqlite=qt -plugin-sql-sqlite -recheck-all -platform win32-g++
MinGW64 8.1.0会报满屏的缺少定义的错误
⑤添加编译工具链
这些乱七八糟的的构建体系,给人一种"生命总会找到出路,甭管路子有多野"的恶感,linux(或者说Qt、C++)的护城河远比想象的牢固。
在虚拟机里安装Linux下的Qt,网址为Index of /official_releases/online_installers,与Windows下的安装基本一致
++这个还是遵循正点原子文档,使用这个命令在Ubuntu2016里下载,在Ubuntu2024里使用编译好的Qt模块,会提示缺少positioning模块++
好吧,正点原子这个光盘里什么都有,不用安装。
bashwget http://download.qt.io/archive/qt/5.12/5.12.9/qt-opensource-linux-x64-5.12.9.run
bashchmod u+x qt-unified-linux-x64-online.run
bashsudo ./qt-unified-linux-x64-online.run --mirror https://mirrors.tuna.tsinghua.edu.cn/qt/
安装的过程与正点原子相同,目录就默认在/opt/Qt, 安装过程可能出现下面提示 安装这个库即可,不然无法打开程序
bashsudo apt install libxcb-cursor0 libxcb-cursor-dev
安装完成后,可以输入下面命令来打开,或者直接到该目录下运行sh脚本
bash/opt/Qt/Tools/QtCreator/bin/qtcreator.sh &
点击左上角这个图表
输入一个Q即可看到安装好的Qt,不过这里最好不要使用这个图标直接运行程序,可以使用下面这个命令来运行,避免后面诸多环境变量不一致
bash/opt/Qt5.12.9/Tools/QtCreator/bin/qtcreator.sh &
把QtDesktop工程传输到虚拟机里,打开前删除.pro.user这个文件
使用Qt打开,选择第一个Kits(ATK-I.MX6U)和Desktop那个Kits(用于生成桌面应用程序观察效果),然后点击配置工程。这里没用之前编译的Qt库是有原因的,因为使用的时候又他宝贝的寄了。我只好非常可耻地使用正点原子编译好的Qt库,不折腾了
点击左侧的Projects,再次进入配置Kits的界面,左侧已经有两个Kits,点击哪个Kits的Build或者Run,该Kits的名称就会加粗,表示该工程使用这个Kits。
回到工程里,左下角也可以选择哪个Kits
选择桌面那个Kits,然后去点击运行,可以生成下图桌面程序
只能说还是厂商的靠谱, 虽说版本旧了,但至少能用,不会有那么多奇奇怪怪的问题
4,远程调试
①构建arm-linux程序
选择另一套kits,然后构建(不要运行),构建完成后,会在工程同级目录下有一个build目录,里面有arm架构的elf程序
②连接开发板
连接开发板的串口,输入下面命令,确保开发板的根文件系统版本大于v1.9。或者直接输入rysnc,如果出现一堆提示,那么就说明有rsync命令
bashcat /etc/version
回到虚拟机,在Tools栏里找到最后一个选项,点击设备。这里是默认设置好的,把里面的Host的IP改为实际开发板里的(记得在开发板里输入ifconfig确定ip),点击OK
然后点击Kits,可以看到这里已经配置好了rsync
在Projects下,点击刚才的rsync套件
再点击里面的run,由于这个工程名为Desktop,会与开发板里的Desktop程序起冲突
所以需要勾选上面的复选框,左面的路径可以到Build里去查找
添加一个SSH命令(图里多打了一个空格)
bash-p %{Device:SshPort} %{Device:UserName}@%{Device:HostAddress} 'mkdir -p %{CurrentRun:Executable:Path}'
再添加一个scp(传输程序用的)
bash-P %{Device:SshPort} %{CurrentRun:Executable:FileName} %{Device:UserName}@%{Device:HostAddress}:%{CurrentRun:Executable:FilePath}
下面这个路径也要改
再添加一个,设备就是刚才Remote Diretory下的程序,要勾选那个复选框
bash/opt/test/bin/QDesktop
回到Edit界面,点击运行,除了下方有一些红字外,一切正常
这个桌面程序是要比开发板自带的画质要低一些
在开发板的串口输入top命令,可以看到有两个QDesktop在运行(难怪刚才那么卡,图片还会一闪一闪的),应该是部署时没有正确沙掉进程
在这个界面下,输入k命令,后面跟着PID就可以沙雕对应进程了,这里保留时间短的(刚才烧录的程序)。输入q可以退出
不用担心开发板里的程序,开发板重启后会自动运行自带的桌面
可以看到虚拟机里的这个桌面,左下角是没有图片的
基本的Qt环境已经搭建好了,下一步开发Qt时会轻松不少,最主要的是能看到最终要实现的效果近在眼前。不过在虚拟机里开发着实不方便,后续准备尝试把开发界面的任务迁移至主机平台,使用CLion配合Qt Design什么的开发,部署调试再放到虚拟机里。
这个方案之所以可行,还是因为Qt强大的跨平台,不同平台相同接口。只不过不同的平台需要不同的库,而这个Qt库的编译是相当折磨,与java的"一次编写,到处运行"完全不一样。
之前还觉得LVGL使用纯C语编写,开发界面很麻烦,现在看来真的是很棒的设计!不会有那么多烦人的兼容性问题,而且界面的开发完全可以使用C++等来封装一些基本的lvgl接口,达到类"Qt"的那种开发效果。或者用别的语言来调用C编译的库,总之移植起来相当方便。
五、Linux驱动开发
有设备树的驱动开发,才算完整的Linux驱动开发嘛
1,设备树下的LED驱动
①初识设备树
每个节点(无论是根节点还是子节点)都是用一个花括号包起来。花括号中,上面是属性,下面是子节点(也可能没有)。这种写法很递归,也有点像C++的类,上面是"成员变量",下面是"成员函数定义",属性部分有些像标签语言。反正怎么好理解就怎么记,不讨论先有儿子还是先有爸爸的问题
以此类推(就不水了)
bash后面我们就以LED设备为例来讲解,下面我有这些问题: 1,compatible怎么用于匹配?名称是根据前面已经出现的,还是我自己随便起?还是说有固定的规则? 2,这里面默认触发模式是什么?有哪些模式?各个模式有什么用? 3,gpios属性被定义为<&gpio1 5 GPIO_ACTIVE_HIGH>,那么&gpio1是不是需要已经出现过的引脚? 4,我想要添加新设备,是不是可以自己再创建一个dts,然后使用include包含前面的dts文件,然后就可以在这个新文件里使用根节点追加的方式? 5,我暂时想不到什么问题了,你就以一名初学者的角度来帮我想想还有哪些问题值得问,然后解答它
②设备树编写
结合正点原子示例代码和文档,准备新建一个dts文件,比如mx6ull-alientek-emmc.dts,在里面引用前面的那个完备的dts
cpp#include "imx6ull-14x14-emmc-7-1024x600-c.dts"
依次往上找到被包含的dtsi文件ixm6ull-14×14-evk.dtsi,我们可以在一个dts文件里找到pinctrl里的gpio-leds(535行左右),这是属于iomuxc节点的
往上我们可以看到leds所在节点(107行左右),这些都是写好的。leds节点中没有state属性,那么默认就是okay(启用)
也就是说如果前面的dts没有定义这些节点,我们可以通过类似于下面这种方式来追加相关内容,这是一般的开发步骤。但evk板既然给了,那就不写了吧,因为我们知道它是怎么来的
根据文档,使用pinctrl后,还需要检查引脚是否冲突!文档中特别提到,阿尔法板是没有用到tsc这个接口的,我们需要把它注释掉
通过搜索功能,可以看到在650行附近有tsc的定义,我们注释掉即可
搜索gpio 3,可以看到外设节点tsc里也会用到GPIO1的3号引脚,这里的状态是disabled,所以不会冲突。也注释掉,比较阿尔法板并没有用到这个接口
可以看到上面三个dts是层层嵌套的,左边依赖且只依赖一个右边
为了方便测试,我们可以让新建的dts只包含imx6ull-14×14-evk,把左边两个定义的节点复制过来
cpp#include "imx6ull-14x14-evk.dts" &usdhc2 { pinctrl-names = "default", "state_100mhz", "state_200mhz"; pinctrl-0 = <&pinctrl_usdhc2_8bit>; pinctrl-1 = <&pinctrl_usdhc2_8bit_100mhz>; pinctrl-2 = <&pinctrl_usdhc2_8bit_200mhz>; bus-width = <8>; non-removable; status = "okay"; }; &i2c2 { goodix_ts@5d { reg = <0x5d>; }; }; &lcdif { display0: display { bits-per-pixel = <16>; bus-width = <24>; display-timings { native-mode = <&timing0>; timing0: timing0 { clock-frequency = <51000000>; hactive = <1024>; vactive = <600>; hfront-porch = <160>; hback-porch = <140>; hsync-len = <20>; vback-porch = <20>; vfront-porch = <12>; vsync-len = <3>; hsync-active = <0>; vsync-active = <0>; de-active = <1>; pixelclk-active = <0>; }; }; }; };
找到dts目录下的Makefile文件,看看是否有新建的dts文件(注意这里添加的是dtb),如果没有就找到对应位置添加
一切就绪后,使用下面命令来编译dtb
bashmake dtbs
③驱动编写
把原先的led工程复制一份,打开后开始编写驱动。这里先定义一个设备结构体
cpp/* 设备结构体 */ struct led_dev { dev_t devno; /* 设备号 */ struct cdev cdev; /* 字符设备 */ struct class *class; /* 设备类 */ struct device *device; /* 设备实例 */ int led_gpio; /* GPIO 引脚 */ int led_state; /* LED 状态 */ };
在init函数里分配这个设备结构体
cpp/* 动态分配设备结构体 */ led_devices = kzalloc(sizeof(struct led_dev), GFP_KERNEL); if (!led_devices) { pr_err("Failed to allocate device data\n"); return -ENOMEM; }
从这里可以看出led1是在/leds/led1这个路径上
那么就可以使用of_*函数来获取对应的设备树节点
cppstruct device_node *np; /* 从设备树中获取LED的GPIO */ np = of_find_node_by_path("/leds"); if (!np) { pr_err("Failed to find LED node in device tree\n"); retvalue = -ENODEV; goto err_find_node; }
接着获取LED的引脚
cpp/* 获取 LED GPIO 引脚 */ led_device->led_gpio = of_get_named_gpio(np, "led1", 0); if (led_device->led_gpio < 0) { pr_err("Failed to get LED GPIO\n"); ret = led_device->led_gpio; goto err_get_gpio; }
申请GPIO,这里的ret变量只是获取返回值状态
cpp/* 申请 GPIO */ ret = gpio_request(led_device->led_gpio, "led1"); if (ret) { pr_err("Failed to request LED GPIO\n"); goto err_gpio_request; }
调用GPIO函数,来设置GPIO状态,根据设备树可以知道它是低电平有效,那么高电平就是关闭LED
cpp/* 设置 GPIO 方向 */ gpio_direction_output(led_device->led_gpio, 1); // 默认关闭 LED
可以通过下面函数来设置GPIO的引脚值,其余与之前无异
cppgpio_set_value(dev->led_gpio, 0);
完整代码为
cpp#include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/slab.h> #include <linux/uaccess.h> #include <linux/of.h> #include <linux/of_gpio.h> #include <linux/gpio.h> #define DEVICE_NAME "led" // 设备名称 /* 设备结构体 */ struct led_dev { dev_t devno; // 设备号 struct cdev cdev; // 字符设备 struct class *class; // 设备类 struct device *device; // 设备实例 int led_gpio; // LED GPIO 引脚 }; static struct led_dev *led_device; // 设备实例 /* * @description : 打开设备 */ static int led_open(struct inode *inode, struct file *filp) { struct led_dev *dev = container_of(inode->i_cdev, struct led_dev, cdev); filp->private_data = dev; // 设置私有数据 pr_info("Device opened\n"); return 0; } /* * @description : 从设备读取数据 */ static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { return 0; } /* * @description : 向设备写数据 */ static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { struct led_dev *dev = filp->private_data; unsigned char databuf[1]; int ret; ret = copy_from_user(databuf, buf, cnt); if (ret < 0) { pr_err("Failed to copy data from user\n"); return -EFAULT; } /* 控制 LED */ if (databuf[0] == 1) { gpio_set_value(dev->led_gpio, 0); // 点亮 LED } else if (databuf[0] == 0) { gpio_set_value(dev->led_gpio, 1); // 关闭 LED } return 0; } /* * @description : 关闭设备 */ static int led_release(struct inode *inode, struct file *filp) { pr_info("Device released\n"); return 0; } /* 设备操作函数 */ static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release, }; /* * @description : 驱动入口函数 */ static int __init led_init(void) { int ret; struct device_node *np; /* 动态分配设备结构体 */ led_device = kzalloc(sizeof(struct led_dev), GFP_KERNEL); if (!led_device) { pr_err("Failed to allocate device data\n"); return -ENOMEM; } /* 查找设备树节点 */ np = of_find_node_by_path("/leds/led1"); if (!np) { pr_err("Failed to find LED node in device tree\n"); ret = -ENODEV; goto err_find_node; } /* 获取 LED GPIO 引脚 */ led_device->led_gpio = of_get_named_gpio(np, "gpios", 0); if (led_device->led_gpio < 0){ pr_err("Failed to get LED GPIO\n"); ret = led_device->led_gpio; goto err_get_gpio; } /* 申请 GPIO */ ret = gpio_request(led_device->led_gpio, "my-led"); if (ret){ pr_err("Failed to request LED GPIO\n"); goto err_gpio_request; } /* 设置 GPIO 方向 */ gpio_direction_output(led_device->led_gpio, 1); // 默认关闭 LED /* 动态分配设备号 */ ret = alloc_chrdev_region(&led_device->devno, 0, 1, DEVICE_NAME); if (ret < 0) { pr_err("Failed to allocate device number\n"); goto err_alloc_chrdev; } /* 初始化 cdev */ cdev_init(&led_device->cdev, &led_fops); led_device->cdev.owner = THIS_MODULE; /* 添加 cdev 到内核 */ ret = cdev_add(&led_device->cdev, led_device->devno, 1); if (ret < 0) { pr_err("Failed to add cdev\n"); goto err_cdev_add; } /* 创建设备类 */ led_device->class = class_create(THIS_MODULE, DEVICE_NAME); if (IS_ERR(led_device->class)) { pr_err("Failed to create class\n"); ret = PTR_ERR(led_device->class); goto err_class_create; } /* 创建设备节点 */ led_device->device = device_create(led_device->class, NULL, led_device->devno, NULL, DEVICE_NAME); if (IS_ERR(led_device->device)) { pr_err("Failed to create device\n"); ret = PTR_ERR(led_device->device); goto err_device_create; } pr_info("LED driver initialized\n"); return 0; err_device_create: class_destroy(led_device->class); err_class_create: cdev_del(&led_device->cdev); err_cdev_add: unregister_chrdev_region(led_device->devno, 1); err_alloc_chrdev: gpio_free(led_device->led_gpio); err_gpio_request: err_get_gpio: err_find_node: kfree(led_device); return ret; } /* * @description : 驱动出口函数 */ static void __exit led_exit(void) { /* 销毁设备节点 */ device_destroy(led_device->class, led_device->devno); /* 销毁设备类 */ class_destroy(led_device->class); /* 删除 cdev */ cdev_del(&led_device->cdev); /* 释放设备号 */ unregister_chrdev_region(led_device->devno, 1); /* 释放 GPIO */ gpio_free(led_device->led_gpio); /* 释放设备结构体 */ kfree(led_device); pr_info("LED driver exited\n"); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("LED Driver");
④驱动测试
由于我们没有对设备树的节点做任何修改,在不更换前面编译后的dtb的情况下,理应实现相同的效果。如果重新把设备树拷贝到SD卡的第一个分区里,记得在Uboot启动时把bootcmd也改了,因为两个设备树文件名称不同。
这里通过NFS挂载,把编译后的驱动模块传输到开发板上
bash/etc/init.d/networking restart
bashmount -t nfs -o nfsvers=3 192.168.1.128:/home/fairy/nfs_share /mnt/nfs
如果在设备树里就把default-trigger设置为none,那么就不需要通过下面这个设置了,否则灯还会闪
bashecho none > /sys/class/leds/sys-led/trigger
加载模块时可能会出现申请失败的问题,把申请GPIO的代码给注释就行了,原因在下面有。
可以通过下面命令来查看GPIO3(LED的GPIO引脚)是否被占用
bashcat /sys/kernel/debug/gpio
如前面提到,LED的引脚莫名其妙被占用,就是gpio-3旁边被一个"?"占用
后来试了一下,只要在设备树里定义了这个引脚,那么它就显示被"?"占用,实际上谁也没占用它。查了一下,发现内核会在启动时自动解析该节点,并调用
gpio_request
申请gpio1_io03
,也就是如果在设备树中已经定义了某个 GPIO 引脚,那么内核会优先占用该引脚,以便我们可以在用户空间中访问,比如前面通过下面命令改变LED的触发状态
bashecho none > /sys/class/leds/sys-led/trigger
比如亮灭
bashecho 1 > /sys/class/leds/sys-led/brightness # 点亮 LED echo 0 > /sys/class/leds/sys-led/brightness # 关闭 LED
这其实是"compatible = "gpio-leds";"所致,意思是交由内核管理,可以换成别的名称,比如compatible ="atkalpha-gpioled";
2,梳理
有第一个使用设备树开发linux驱动的经验后,学习linux驱动的脉络清晰了一些,希望其他的多少也能照葫芦画瓢。
下一步,可以尝试在正点原子创建的桌面上添加一个按钮,用于控制LED或蜂鸣器,试一下GUI与驱动的交互。等GUI与驱动开发都熟练时,或许可以更进一步学习linux内核、构建根文件系统等,尝试使用imx6ull官方最新适配的linux(或者Android),以及配套的uboot和根文件系统,再尝试Qt6.8.1?
嵌入式Linux入门难,各种环境搭建、兼容性问题就拦了一部分人。想精进也难,各种工具、命令、规则、协议栈、C语言的奇思妙法等,想想头都大。
希望大家都能有所收获!