【嵌入式】总结——Linux驱动开发(三)
鸽了半年,几乎全忘了,幸亏前面还有两篇总结。出于快速体验嵌入式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设备
ls /dev/sd*
缺少哪一个,哪个就是SD卡,此处缺失/dev/sdb和/dev/sdb1,那么/dev/sdb就是SD卡设备
②使用fdisk命令删除SD卡所有分区
使用fdisk命令,进入fdisk工具界面
sudo 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设备
ls /dev/sd*
刚列出来时,并没有分区2(/dev/sdb2),重新插拔SD卡设备后再重新列出,就有了
使用下面命令,分别为两个分区设置格式FAT和EXT4
sudo mkfs.vfat /dev/sdb1
sudo mkfs.ext4 /dev/sdb2
第二个分区需要等待一段时间,制作好后,左侧就会出现两个USB一样的图标
3,烧录uboot
①编译uboot
②烧录uboot
使用烧录工具,下图使用的烧录工具是基于正点原子提供的工具的改版imxdownload烧写工具
出现下面错误是因为这个工具在编写时,使用的是C++来创建文件而非本地的linux命令,故而需要在前面添加sudo命令
sudo ./imx_download -b u-boot.bin -s /dev/sdb
4,拷贝linux镜像和设备树
①编译linux
在Makefile已经指定架构和编译器的情况下,运行脚本
#!/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 ]; thenecho "没有参数,执行所有命令"make_distcleanmake_imx_v7_defconfig # make_menuconfigmake_allelse# 主逻辑,根据输入参数调用相应的函数case "$1" inc)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目录
sudo mount /dev/sdb1 /mnt
挂载后,左边的USB图标就会少掉一个
sudo cp arch/arm/boot/zImage /mnt
设备树就在arch/arm/boot/dts目录,进入后寻找到匹配的dtb文件,然后复制到/mnt目录中
sudo cp ./imx6ull-14x14-emmc-7-1024x600-c.dtb /mnt
使用sync后,然后再取消挂载
sudo umount /dev/sdb1
5,拷贝根文件系统
①传输根文件系统
使用FileZilla传输文件,调了好半天:NAT是给虚拟机上网用的,桥接是给以太网用的。虚拟机能ping主机不行,控制面板启用VMnet8。
开发资料A盘里有些根文件系统不能正常使用,不过笔者没有一一尝试,下面这个根文件系统是正常的
②拷贝
把传输的压缩包复制到已经挂载SD卡第二个分区的/mnt里,然后解压
sudo tar -xvjf rootfs.tar.bz2
解压后删除压缩包,然后使用sync同步,最后取消挂载
6、启动开发板
串口连接至电脑,插上SD卡后,拨码选择SD启动。然后进入uboot,设置启动命令
setenv bootcmd 'load mmc 0:1 0x83000000 zimage; load mmc 0:1 0x83800000 imx6ull-14x14-emmc-7-1024x600-c.dtb; bootz 0x83000000 - 0x83800000'
保存启动命令后,重新复位
saveenv
本来想自行动态计算地址,结果发现uboot的&运算有问题,格式正确也会报语法错误。后来用/0x1000和*0x1000来代替,但会一直卡在启动内核步骤。最后还是用回了以前的命令,这个先搁置 。
最终效果如下,除了壁纸中部细看略微有些条纹外一切正常(可能这是特点?):
二、网络连接
1,设置ip和子网掩码
①测试uboot
开发板上电后,按下任意键进入uboot里,通过下面命令设置开发板的ip、子网掩码和MAC(MAC地址不能重复)
setenv ipaddr 192.168.1.254
setenv netmask 255.255.255.0
setenv ethaddr 00:11:22:33:44:55
然后设置主机的ip
setenv serverip 192.168.1.255
最后保存
saveenv
这里为了避免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)
sudo vi /etc/network/interfaces
进入后把iface etho inet dhcp改为下面,都是vim的基本操作
auto eth0 iface eth0 inet staticaddress 192.168.1.127 # 开发板的静态IPnetmask 255.255.255.0 # 子网掩码gateway 192.168.1.1 # 网关dns-nameservers 8.8.8.8 # DNS服务器
(图中乱码可能是显示的问题)
修改完后,使用下面命令来重启网络
sudo /etc/init.d/networking restart
此时ip地址已被正确设置
ping虚拟机,可以看到一切正常(此时虚拟机的ip设为192.168.1.128,因为192.168.1.255是广播地址,还需要加上-b参数)
按Ctrl+C可以暂停操作。
可以看到虚拟机也能ping通开发板
2,建立连接
这里能使用的方法有很多
这里使用的是NFS,主机和开发板需要各自配置后,才能进行正常通信
①主机
先安装nfs服务
sudo apt-get install nfs-kernel-server
创建一个共享目录并赋予权限,比如在用户目录里创建,user自行替换
mkdir /home/user/nfs_share
chmod 777 /home/user/nfs_share
编辑NFS配置文件
/etc/exports
,添加共享目录和权限sudo vim /etc/exports
/home/user/nfs_share 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)
重启NFS服务
sudo systemctl restart nfs-kernel-server
检查NFS共享是否生效
sudo exportfs -v
②主机给开发板联网
开发板需要下载nfs客户端,需要联网。联网可以使用IP转发,但这个有些麻烦(以后再说),可以直接使用Windows的网络共享功能,参考博客开发板和笔记本网线连接
到控制面板里,找到网络和Internet,再点击网络和共享中心进入下面步骤,点击更改适配器设置
右键WLAN(笔记本的一个网口已经通过以太网线与开发板连接,所以用的是WiFi),按如下设置
共享之后,使用ifconifg查看ip
设置完后,在开发板的linux里,ping百度网址
ping www.baidu.com
联网是没有问题,但是这个ping出来的结果很慢,需要耐心等待
③开发板
既然可以联网,那么接下来需要开发板下载nfs客户端。不过这个根文件系统的apt没有资源列表,需要手动创建
touch /etc/apt/sources.list
然后是添加网址,这里使用的是阿里镜像源,可自行替换需要的源
vi /etc/apt/sources.list
deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiversedeb-src http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiversedeb-src http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiversedeb-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客户端了,包管理器下载先放一放。
which mount.nfs
需要注意的是,前面为了联网启用了网络共享功能,现在要尝试nfs挂载,那么就要关闭共享。同时使用ifconfig来查看ip是否正确,如果不正确,那么重启一下网络,再检查ip地址,确保开发板和虚拟机能互ping。开发板每次重启还需要输入下面命令来重启网络
sudo /etc/init.d/networking restart
前面在主机上使用的ip是192.168.1.128,路径是/home/user/nfs_share,下面就可以据此来建立nfs通信了,现在开发板上创建一个用于挂载的目录,比如/mnt/nfs
mkdir /mnt/nfs
本来应该使用下面这个命令建立连接的(用户名和ip自行替换),但需要指定版本
mount -t nfs 192.168.1.128:/home/fairy/nfs_share /mnt/nfs
在此之前,我们可以先试用下面命令来查看挂载点
df -h
再使用添加了版本的挂载命令(如果版本3不行,试试4),该命令没有任何提示
mount -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
sudo vi /etc/network/interfaces
把网关改为虚拟机ip 192.168.1.128(这里把开发板的ip改为了192.168.1.127)
auto eth0 iface eth0 inet staticaddress 192.168.1.127netmask 255.255.255.0gateway 192.168.1.128dns-nameservers 8.8.8.8
重启网络驱动(开发板每次重启后还得手动重启网络)
sudo /etc/init.d/networking restart
编辑
/etc/resolv.conf
文件,修改DNS配置sudo vi /etc/resolv.conf
修改为下面内容(开发板每次重启都会覆盖掉下面内容)
nameserver 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源码
#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
,其他模块信息(如作者、描述、版本号)是可选的,但建议尽量提供。……
一个简单的模块示例如下:
#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里添加这一句
# 添加 C11 标准支持 ccflags-y := -std=gnu11 -Wno-declaration-after-statement
为了测试驱动模块,还需要编写应用程序,此处即ledApp,为此,编译还需要添加一个目标ledApp
KERNELDIR := /home/fairy/Embedded/program/Alientek_Uboot_Linux/linux CURRENT_PATH := $(shell pwd)obj-m := led.o# 添加 C11 标准支持 ccflags-y := -std=gnu11 -Wno-declaration-after-statementall: kernel_modules ledAppkernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules# 编译应用程序 ledApp: ledApp.carm-linux-gnueabihf-gcc -o $@ $<clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
测试程序的代码很清晰,先打开文件,再写入数据,最后关闭。main函数里的两个参数,前者为参数个数,后者为参数指针数组。编译的程序为ledApp.elf(在linux里elf后缀一般不写),运行程序时传入参数是指在命令行中输入程序名称,并且在名称后输入一些内容(参数),比如下面,传入的第二个参数是1,第一个参数默认都是程序自身的名称
./ledApp 1
放在程序里下面这一步就是把传入的参数“1”存放到数组databuf里
⑤测试驱动模块
把驱动模块和测试应用程序都传输到虚拟机的nfs挂载点
在开发板的挂载点里可以看到文件已经成功传入
加载驱动模块
insmod /mnt/nfs/led.ko
列出设备
lsmod
查看设备
cat /proc/devices
查看设备节点
ls -l /dev/
可以看到是没有led设备节点的,因为没有创建设备节点,驱动模块使用的还是例程源码,并没有添加自动创建设备节点udev需要的相关函数。
创建字符设备节点
mknod /dev/led c 200 0
可以看到led设备节点已经创建成功了
测试应用程序
测试之前需要关闭led自动闪烁功能
echo none > /sys/class/leds/sys-led/trigger
此时输入命令,才发现架构不对,虚拟机使用的是x86_x64,而开发板是arm32,应使用交叉编译工具链,也就是说前面的Makefile编译ledApp时,需要把gcc改为arm-linux-gnueabihf-gcc(已改)
此时使用下面0和1两个参数测试,实验结果与预期相符
./ledApp /dev/led 1
./ledApp /dev/led 0
使用rmmod卸载模块时,设备节点/dev/led并不会消失,还需要使用rm来手动删除
rmmod led.ko
rm /dev/led
⑥尝试新方法
前面和DeepSeek对话中,可以获知混合分配设备号更推荐,udev自动创建设备文件更现代。继续提问,还有更多更现代的做法,比如驱动和硬件分离,不过这要用到设备树,一些做法可以先放一放。
下面的代码只要用到了1、5和6,其他需要设备树配合
#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
等)中访问设备相关的数据。
优化后的代码如下
#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键,一般就可以自动补全了。
.\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
在里面把刚才的路径复制过去,下面是参考,自行修改
E:\Tools\Develop\ToolsKits\ARM\arm-gnu-toolchain-14.2.rel1-mingw-w64-i686-arm-none-linux-gnueabihf\bin
一路点击确定,最后重启电脑。重启后,打开终端,输入下面语句,观察是否有版本信息
arm-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源码目录,打开终端输入下面命令
./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是版本号,按此路径最终找到下面目录(自行替换)
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.41.34120\bin\Hostx64\x64
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.41.34120\include
不过运行这个bat脚本中可能会出现下面这个沙雕错误(荼毒无穷),添加环境变量也没有任何作用
根据VS安装目录,找到下面路径,在这里打开终端,执行下面命令
.\vcvarsall.bat x64
然后你就会发现,啥用没有。后续找了许久,发现忘了制定平台
./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 ,添加这个头文件
#include <limits>
诸如此类,尝试了许多种方法,最终带着深深的怨气总算找到了疑似靠谱的方法(实用MSVC)。按Win打开菜单,找到VS的命令行,x64和x86随意,这里用的是
x64 Native Tools Command Prompt for VS 2022
在打开的cmd窗口中,使用cd命令跳转到Qt源码的目录,不过要注意的是在cd命令后加上/d参数,才能执行跨盘操作
cd /d E:/Tools/Develop/ToolsKits/Qt/qt-everywhere-src-5.15.2/
然后输入下面命令,不用加platform选项
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++
输入y即可
如果出现下图,说明qmake.tconf里的编译工具集的名称没有写对,或者环境变量没有生效,自行检查
然后使用make构建,电脑有几核就输入几
mingw32-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源码也是如此
tar -xvJf arm-gnu-toolchain-14.2.rel1-x86_64-arm-none-linux-gnueabihf.tar.xz
添加环境变量,需要修改下面文件
sudo vim /etc/environment
在原变量里加上冒号,后面再添加路径,路径改为自己的工具链路径
PATH="/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
sudo apt update
然后安装依赖
sudo 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自行替换
#!/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
chmod 777 autoConfig.sh
然后在源码目录里运行脚本,执行脚本之后就会报缺少limits什么的错误,到下面目录,找到qglobal.h 添加这个头文件即可
#include <limits>
构建成功后
然后使用make
make -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
./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
time (make -j16)
简直就是神迹!
time (make install)
后续又试了一下,同样编译器和构建命令的情况下,Ubuntu2024会出现下面错误
后续测试了一下Ubunt2024、Ubuntu2016与gcc9.2、gcc14.2的排列组合,只有Ubuntu2016和gcc9.2的组合可以正常编译。
换成Windows平台,使用同样的gcc编译器(9.2),同样的命令,只会编译出下面结果。也许是MinGW版本不对,MinGW32(gcc9.2)和MinGW64(gcc14.2)都不行
./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模块好吧,正点原子这个光盘里什么都有,不用安装。
wget http://download.qt.io/archive/qt/5.12/5.12.9/qt-opensource-linux-x64-5.12.9.run
chmod u+x qt-unified-linux-x64-online.run
sudo ./qt-unified-linux-x64-online.run --mirror https://mirrors.tuna.tsinghua.edu.cn/qt/
安装的过程与正点原子相同,目录就默认在/opt/Qt, 安装过程可能出现下面提示安装这个库即可,不然无法打开程序
sudo apt install libxcb-cursor0 libxcb-cursor-dev
安装完成后,可以输入下面命令来打开,或者直接到该目录下运行sh脚本/opt/Qt/Tools/QtCreator/bin/qtcreator.sh &
点击左上角这个图表
输入一个Q即可看到安装好的Qt,不过这里最好不要使用这个图标直接运行程序,可以使用下面这个命令来运行,避免后面诸多环境变量不一致
/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命令
cat /etc/version
回到虚拟机,在Tools栏里找到最后一个选项,点击设备。这里是默认设置好的,把里面的Host的IP改为实际开发板里的(记得在开发板里输入ifconfig确定ip),点击OK
然后点击Kits,可以看到这里已经配置好了rsync
在Projects下,点击刚才的rsync套件
再点击里面的run,由于这个工程名为Desktop,会与开发板里的Desktop程序起冲突
所以需要勾选上面的复选框,左面的路径可以到Build里去查找
添加一个SSH命令(图里多打了一个空格)
-p %{Device:SshPort} %{Device:UserName}@%{Device:HostAddress} 'mkdir -p %{CurrentRun:Executable:Path}'
再添加一个scp(传输程序用的)
-P %{Device:SshPort} %{CurrentRun:Executable:FileName} %{Device:UserName}@%{Device:HostAddress}:%{CurrentRun:Executable:FilePath}
下面这个路径也要改
再添加一个,设备就是刚才Remote Diretory下的程序,要勾选那个复选框
/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++的类,上面是“成员变量”,下面是“成员函数定义”,属性部分有些像标签语言。反正怎么好理解就怎么记,不讨论先有儿子还是先有爸爸的问题
以此类推(就不水了)
后面我们就以LED设备为例来讲解,下面我有这些问题: 1,compatible怎么用于匹配?名称是根据前面已经出现的,还是我自己随便起?还是说有固定的规则? 2,这里面默认触发模式是什么?有哪些模式?各个模式有什么用? 3,gpios属性被定义为<&gpio1 5 GPIO_ACTIVE_HIGH>,那么&gpio1是不是需要已经出现过的引脚? 4,我想要添加新设备,是不是可以自己再创建一个dts,然后使用include包含前面的dts文件,然后就可以在这个新文件里使用根节点追加的方式? 5,我暂时想不到什么问题了,你就以一名初学者的角度来帮我想想还有哪些问题值得问,然后解答它
②设备树编写
结合正点原子示例代码和文档,准备新建一个dts文件,比如mx6ull-alientek-emmc.dts,在里面引用前面的那个完备的dts
#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,把左边两个定义的节点复制过来
#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
make dtbs
③驱动编写
把原先的led工程复制一份,打开后开始编写驱动。这里先定义一个设备结构体
/* 设备结构体 */ struct led_dev {dev_t devno; /* 设备号 */struct cdev cdev; /* 字符设备 */struct class *class; /* 设备类 */struct device *device; /* 设备实例 */int led_gpio; /* GPIO 引脚 */int led_state; /* LED 状态 */ };
在init函数里分配这个设备结构体
/* 动态分配设备结构体 */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_*函数来获取对应的设备树节点
struct 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的引脚
/* 获取 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变量只是获取返回值状态
/* 申请 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
/* 设置 GPIO 方向 */gpio_direction_output(led_device->led_gpio, 1); // 默认关闭 LED
可以通过下面函数来设置GPIO的引脚值,其余与之前无异
gpio_set_value(dev->led_gpio, 0);
完整代码为
#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挂载,把编译后的驱动模块传输到开发板上
/etc/init.d/networking restart
mount -t nfs -o nfsvers=3 192.168.1.128:/home/fairy/nfs_share /mnt/nfs
如果在设备树里就把default-trigger设置为none,那么就不需要通过下面这个设置了,否则灯还会闪
echo none > /sys/class/leds/sys-led/trigger
加载模块时可能会出现申请失败的问题,把申请GPIO的代码给注释就行了,原因在下面有。
可以通过下面命令来查看GPIO3(LED的GPIO引脚)是否被占用
cat /sys/kernel/debug/gpio
如前面提到,LED的引脚莫名其妙被占用,就是gpio-3旁边被一个“?”占用
后来试了一下,只要在设备树里定义了这个引脚,那么它就显示被“?”占用,实际上谁也没占用它。查了一下,发现内核会在启动时自动解析该节点,并调用
gpio_request
申请gpio1_io03
,也就是如果在设备树中已经定义了某个 GPIO 引脚,那么内核会优先占用该引脚,以便我们可以在用户空间中访问,比如前面通过下面命令改变LED的触发状态echo none > /sys/class/leds/sys-led/trigger
比如亮灭
echo 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语言的奇思妙法等,想想头都大。
希望大家都能有所收获!
相关文章:

【嵌入式】总结——Linux驱动开发(三)
鸽了半年,几乎全忘了,幸亏前面还有两篇总结。出于快速体验嵌入式linux的目的,本篇与前两篇一样,重点在于使用、快速体验,uboot、linux、根文件系统不作深入理解,能用就行。 重新梳理一下脉络,本…...

计算机图形学:实验三 光照与阴影
一、程序功能设计 设置了一个3D渲染场景,支持通过键盘和鼠标控制交互,能够动态调整光源位置、物体材质参数等,具有光照、阴影和材质效果的场景渲染。 OpenGL物体渲染和设置 创建3D物体:代码中通过 openGLObject 结构体表示一个…...

「 机器人 」扑翼飞行器混合控制策略缺点浅谈
前言 将基于模型的控制与强化学习策略融合在扑翼飞行器中,虽然能够兼顾系统稳定性与极限机动能力,但也面临了更高的系统复杂性、对硬件算力与可靠性的额外要求,以及难以回避的能量效率等方面挑战。以下从四个方面进行归纳与分析。 1. 系统复杂性增加 1.1 两种控制方法的并存…...

蓝桥杯算法日常|c\c++常用竞赛函数总结备用
一、字符处理相关函数 大小写判断函数 islower和isupper:是C标准库中的字符分类函数,用于检查一个字符是否为小写字母或大写字母,需包含头文件cctype.h(也可用万能头文件包含)。返回布尔类型值。例如: #…...

每日十题八股-2025年1月24日
1.面试官:Kafka 百万消息积压如何处理? 2.面试官:最多一次、至少一次和正好一次有什么区别? 3.面试官:你项目是怎么存密码的? 4.面试官:如何设计一个分布式ID? 5.面试官:单点登录是怎么工作的…...

tomcat的accept-count、max-connections、max-threads三个参数的含义
tomcat的accept-count、max-connections、max-threads三个参数的含义 tomcat的accept-count、max-connections、max-threads三个参数的含义 max-connections:最大连接数 最大连接数是指,同一时刻,能够连接的最大请求数 需要注意的是&#x…...

【无标题】mysql python 连接
coding:utf8 import os import pymysql import yaml from common.log import logger class Mysql: # 处理.sql备份文件为SQL语句 def __read_sql_file(self,file_path): # 打开SQL文件到f sql_list = [] with open(file_path, ‘r’, encoding=‘utf8’) as f: # 逐行读取和…...

linux naive代理设置
naive linux客户端 Release v132.0.6834.79-2 klzgrad/naiveproxy GitHub Client setup Run ./naive with the following config.json to get a SOCKS5 proxy at local port 1080. {"listen": "socks://127.0.0.1:1080","proxy": "htt…...

[STM32 - 野火] - - - 固件库学习笔记 - - -十一.电源管理系统
一、电源管理系统简介 电源管理系统是STM32硬件设计和系统运行的基础,它不仅为芯片本身提供稳定的电源,还通过多种电源管理功能优化功耗、延长电池寿命,并确保系统的可靠性和稳定性。 二、电源监控器 作用:保证STM32芯片工作在…...

DBO优化最近邻分类预测matlab
蜣螂优化算法(Dung Beetle Optimizer,简称 DBO)作为一种新兴的群智能优化算法,于 2022 年末被提出,其灵感主要来源于蜣螂的滚球、跳舞、觅食、偷窃以及繁殖等行为。 本次使用的数据为 Excel 格式的分类数据集。该数据…...

【深入理解FFMPEG】命令行阅读笔记
这里写自定义目录标题 第三章 FFmpeg工具使用基础3.1 ffmpeg常用命令3.1.13.1.3 转码流程 3.2 ffprobe 常用命令3.2.1 ffprobe常用参数3.2.2 ffprobe 使用示例 3.3 ffplay常用命令3.3.1 ffplay常用参数3.3.2 ffplay高级参数3.3.4 ffplay快捷键 第4章 封装与解封装4.1 视频文件转…...

图形化数据报文转换映射工具
目录 概要整体架构流程技术名词解释技术细节小结 概要 在当今数字化时代,数据的处理和分析是企业、科研机构以及各类组织日常运营的核心环节。数据来源广泛,格式多样,常见的数据格式包括XML(可扩展标记语言)和JSON&a…...

智能体0门槛开发
分享一个智能体开发流程。 2025 年啊,好多专家还有行业报告都觉得这是智能体(AI Agent)应用的头一年。相关的应用在商业、工业、消费等好些领域都到了关键的时候,这意味着从实验室走向大规模实际应用的重要转变。而且呢࿰…...

ssh密钥登录GitHub时一直提示“Error: Permission denied (publickey)”
起因 环境:Windows10 背景:之前就是按照官方说明创建个rsa密钥,在git后台添加上,就行了,近期怎么添加怎么失败,总是“Error: Permission denied (publickey)”的提示! 尝试 各种尝试…...

mapbox加载geojson,鼠标移入改变颜色,设置样式以及vue中的使用
全国地图json数据下载地址 目录 html加载全部代码 方式一:使用html方式加载geojson 1. 初始化地图 2. 加载geojson数据 设置geojson图层样式,设置type加载数据类型 设置线条 鼠标移入改变颜色,设置图层属性,此处是fill-extru…...

考研机试题:打印日期
描述 给出年分m和一年中的第n天,算出第n天是几月几号。 输入描述: 输入包括两个整数y(1<y<3000),n(1<n<366)。 输出描述: 可能有多组测试数据,对于每组数据, 按 yyyy-mm-dd的格式将输入中对应的日期打印出来。 …...

OpenHarmonyOS 3.2 编译生成的hap和app文件的名称如何配置追加版本号?
找了一圈发现官方的文档都是最新的,3.2很多API都不支持,比如获取OhosAppContext,通过OhosAppContext来获取应用版本号,最终是通过读取app.json5的文件内容来读取版本号,最终修改entry下的hvigorfile.ts如下,…...

【openwrt】openwrt odhcpd配置介绍
odhcpd odhcpd是一个嵌入式DHCP/DHCPv6/RA服务器和NDP中继的进程,odhcpd是一个守护进程,用于服务和中继IP管理协议,以配置客户端和下游路由器。它试图遵循IPv6家用路由器的RFC 6204要求。odhcpd为DHCP、RA、无状态SLAAC和有状态DHCPv6、前缀委派提供服务器服务,并可用于在没…...

基于神经网络的视频编码NNVC(1):帧内预测
在H.266/VVC发布后,基于传统编码框架提升压缩率越来越难,随着深度学习的发展,研究人员开始尝试将神经网络引入编码器。为此,JVET工作组在2020年成立AHG11小组来专门进行基于神经网络的视频编码的研究。 为了方便研究,工…...

Android开发,待办事项提醒App的设计与实现
文章目录 1. 研究目的2. 主要内容3. 运行效果图4. 涉及到的技术点5. 开发环境6. 关于作者其它项目视频教程介绍 1. 研究目的 当今,随着时代的发展和计算机的普及,人们开始利用网络来记录并管理日常的事务,时下这方面的软件数不胜数。各种日程管理软件就是将每天的工作和事务安…...

豆瓣Top250电影的数据采集与可视化分析(scrapy+mysql+matplotlib)
文章目录 豆瓣Top250电影的数据采集与可视化分析(scrapy+mysql+matplotlib)写在前面数据采集(Visual Studio Code+Navicat)1.观察网页信息2.编写Scrapy代码(Visual Studio Code)2.1 创建Scrapy项目`doubanProject`2.2 创建爬虫脚本`douban.py`2.3 修改`douban.py`的代码2…...

MySQL索引——让查询飞起来
文章目录 索引是什么??硬件理解MySQL与存储 MySQL 与磁盘交互基本单位索引的理解B vs B聚簇索引 VS 非聚簇索引索引操作创建主键索引唯一索引的创建普通索引的创建全文索引的创建查询索引删除索引 在现代数据库应用中,查询性能是决定系统响应…...

Springboot集成Elasticsearch8.0(ES)版本,采用JAVA Client方式进行连接和实现CRUD操作
本文章介绍了 springboot t集成Elasticsearch8.0(ES)版本,如何通过 AVA Client方式进行连接和实现CRUD操作 在ES7.15版本之后,ES官方将高级客户端 RestHighLevelClient标记为弃用状态。同时推出了全新的 Java API客户端 Elasticsearch Java API Client,该客户端也将在 Ela…...

【Linux】APT 密钥管理迁移指南:有效解决 apt-key 弃用警告
引言 随着 Debian 11 和 Ubuntu 22.04 版本的推出,APT 的密钥管理方式发生了重大的变化。apt-key 命令被正式弃用,新的密钥管理机制要求使用 /etc/apt/keyrings/ 或 /etc/apt/trusted.gpg.d/ 来存储和管理密钥。这一变化对管理员和普通用户来说至关重要…...

洛谷P1143 进制转换
题目链接:P1143 进制转换 - 洛谷 | 计算机科学教育新生态 题目难度:普及— 解题思路:本题先将输入的数转为10进制,然后取模,最后倒着输出就好了,最后直接上代码 #include<bits/stdc.h> using namespa…...

99.12 金融难点通俗解释:毛利率
目录 0. 承前1. 简述2. 比喻:冰淇淋店赚钱2.1 第一步:准备材料2.2 第二步:卖冰淇淋2.3 第三步:计算毛利率 3. 生活中的例子3.1 好的毛利率3.2 一般的毛利率3.3 差的毛利率 4. 小朋友要注意4.1 毛利率高不一定好4.2 毛利率低不一定…...

HUMANITY’S LAST EXAM (HLE) 综述:人工智能领域的“最终考试”
论文地址:Humanity’s Last Exam 1. 背景与动机 随着大型语言模型(LLMs)能力的飞速发展,其在数学、编程、生物等领域的任务表现已超越人类。为了系统地衡量这些能力,LLMs 需要接受基准测试(Benchmarks&…...

C++从入门到实战(二)C++命名空间
C从入门到实战(二)C命名空间 前言一、C的第一个程序二、命名空间(一)为什么需要命名空间(二)定义命名空间(三)使用命名空间1.通过命名空间限定符:2.使用 using 声明&…...

C# OpenCV机器视觉:实现农作物病害检测
在酷热难耐的夏日,阳光似火舌般舔舐大地。阿强惬意地躺在老家院子摇椅上,哼着小曲,手边放着一碗冰镇西瓜,头顶大槐树宛如巨大遮阳伞,洒下斑驳阴凉。他本想趁假期回老家放松,远离城市喧嚣与代码 “纠缠”。 …...

开源软件协议介绍
一、可以闭源使用/不具传染性的协议 允许商业使用和分发 1、BSD:详细介绍 2、LGPL许可证:详细介绍 3、MPL2.0:详细介绍 二、具有传染性/使用后需要开源自身软件的协议 不建议商业使用 1、GPL许可证:详细介绍...