【嵌入式】总结——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、根文件系统不作深入理解,能用就行。 重新梳理一下脉络,本…...
python操作mysql
前言 在 Python3 中,我们可以使用mysqlclient或者pymysql三方库来接入 MySQL 数据库并实现数据持久化操作。二者的用法完全相同,只是导入的模块名不一样。我们推荐大家使用纯 Python 的三方库pymysql,因为它更容易安装成功。下面我们仍然以之…...
OpenCV:高通滤波之索贝尔、沙尔和拉普拉斯
目录 简述 什么是高通滤波? 高通滤波的概念 应用场景 索贝尔算子 算子公式 实现代码 特点 沙尔算子 算子公式 实现代码 特点 拉普拉斯算子 算子公式 实现代码 特点 高通滤波器的对比与应用场景 相关阅读 OpenCV:图像滤波、卷积与卷积核…...
游戏设备升级怎么选?RTX4070独显,ToDesk云电脑更具性价比
过新年、添喜气!正逢节期来临不知道各位是否都跟小编一样在考虑购置生活中的各样所需呐? 25年可谓是3A游戏大作之年,例如《GTA6》《文明7》《死亡搁浅2》《刺客信条:影》下半年落地的《塞尔达传说:新篇章》《生化危机9…...
【useLayoutEffect Hook】在浏览器完成布局和绘制之前执行副作用
目录 前言语法useLayoutEffect 对比 useEffect:示例 前言 useLayoutEffect 是 React 中的一个 Hook, 类似于 useEffect,但有一个关键的区别:它会在所有的 DOM 变更之后同步调用 effect。这意味着它可以读取 DOM 布局并同步重新渲…...
Llama 3:开源大模型的里程碑式突破
标题:Llama 3:开源大模型的里程碑式突破 文章信息摘要: Meta通过Llama 3展现了开源LLM的重大突破:采用超大规模训练数据和多阶段训练方法(SFT、rejection sampling、PPO和DPO),突破了传统的Chi…...
Spring 框架基础:IOC 与 AOP 原理剖析及面试要点
在上一篇中,我们深入探讨了 Java 反射机制,了解了它在运行时动态操作类和对象的强大能力。而今天,我们将进入 Spring 框架的世界。Spring 框架作为 Java 企业级开发中最流行的框架之一,极大地简化了企业级应用的开发过程。对于春招…...
《开源与合作:驱动鸿蒙Next系统中人工智能技术创新发展的双引擎》
在当今科技飞速发展的时代,鸿蒙Next系统作为一款具有创新性和前瞻性的操作系统,为人工智能技术的发展提供了广阔的舞台。而开源和合作则是推动鸿蒙Next系统中人工智能技术创新和发展的两大关键引擎。 开源:创新的源泉 代码共享与知识传播&am…...
Redis使用基础
1 redis介绍 Redis(Remote Dictionary Server ),即远程字典服务 ! 是完全开源的,遵守 BSD 协议,是一个高性能的 key-value 数据库。 使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并…...
React和Vue有什么区别,如何选择?
React和Vue有什么区别,如何选择? React 和 Vue 是当前最受欢迎的前端框架之一,两者在开发者中都有极高的声誉。它们都旨在帮助开发人员构建用户界面,但在实现方式和适用场景上有所不同。如果你正考虑在项目中选择 React 或 Vue&a…...
C++|开源日志库log4cpp和glog
文章目录 log4cpp 和 glog对比1. **功能对比**2. **易用性和配置**3. **性能**4. **线程安全**5. **日志输出**6. **功能扩展**7. **适用场景**8. **总结** 其它开源C日志库1. **spdlog**2. **easylogging**3. **Boost.Log**4. **loguru**5. **Poco Logging**6. **Qt Logging (…...
安卓程序作为web服务端的技术实现(三):AndServer作为服务
安卓程序作为web服务端的技术实现:AndServer 实现登录权限拦截-CSDN博客 安卓程序作为web服务端的技术实现(二):Room 实现数据存储-CSDN博客 经过两次捣鼓 AndServer已经能正常访问了 但是发现一个问题 就是当我app退出时 AndSe…...
数据结构(Java)——二叉树
1.概念 二叉树是一种树形数据结构,其中每个节点最多有两个子节点,通常被称为左子节点和右子节点。二叉树可以是空的(即没有节点),或者由一个根节点以及零个或多个左子树和右子树组成,其中左子树和右子树也分…...
深度学习系列76:流式tts的一个简单实现
1. 概述 使用queue,producer不断向queue中添加audio,然后consumer不断从queue中消费audio。 下面的样例使用melo来生成语音,需要先下载melo.tts。模型在https://myshell-public-repo-hosting.s3.amazonaws.com/openvoice/basespeakers/ZH/ch…...
数据结构(三) 排序/并查集/图
目录 1. 排序 2.并查集 3.图 1.排序: 1.1 概念: 排序就是将数据按照某种规则进行排列, 具有某种顺序. 分为内排序和外排序. 内排序就是: 将数据放在内存中的排序; 外排序是: 数据太多无法在内存中排序的. 1.2 插入排序: 插入排序包含: 直接插入排序和希尔排序. (1) 直接插入…...
WPA Supplicant 技术详解
目录 前言 1. 简介 2. 源码获取 3. 代码架构 3.1 模块结构 3.2. 主要文件和目录 3.3. 顶层模块 3.4 模块之间的关系 4. 工作流程简要描述 启动 加载配置 初始化 认证 数据传输 5. 编译与安装 5.1 编译 5.1.1 libnl库与openssl库准备 5.1.2 修改配置文件 5.…...
Avalonia UI MVVM DataTemplate里绑定Command
Avalonia 模板里面绑定ViewModel跟WPF写法有些不同。需要单独绑定Command. WPF里面可以直接按照下面的方法绑定DataContext. <Button Content"Button" Command"{Binding DataContext.ClickCommand, RelativeSource{RelativeSource AncestorType{x:Type User…...
macOS如何进入 Application Support 目录(cd: string not in pwd: Application)
错误信息 cd: string not in pwd: Application 表示在当前目录下找不到名为 Application Support 的目录。可能的原因如下: 拼写错误或路径错误:确保你输入的目录名称正确。目录名称是区分大小写的,因此请确保使用正确的大小写。正确的目录名…...
【探索 Kali Linux】渗透测试与网络安全的终极操作系统
探索 Kali Linux:渗透测试与网络安全的终极操作系统 在网络安全领域,Kali Linux 无疑是最受欢迎的操作系统之一。无论是专业的渗透测试人员、安全研究人员,还是对网络安全感兴趣的初学者,Kali Linux 都提供了强大的工具和灵活的环…...
《SwinIR:使用Swin-Transformer图像恢复》学习笔记
paper:2108.10257 GitHub:GitHub - JingyunLiang/SwinIR: SwinIR: 使用 Swin Transformer 进行图像修复 (官方仓库) 目录 摘要 1、Introduction 2、Related Work 2.1 图像修复 2.2 视觉Transformer…...
AR智慧点巡检系统探究和技术方案设计
一、项目背景 随着工业生产规模的不断扩大和设备复杂度的提升,传统的人工点巡检方式效率低下、易出错,难以满足现代化企业对设备运行可靠性和安全性的要求。AR(增强现实)技术的发展为点巡检工作带来了新的解决方案,通…...
电路研究9.2——合宙Air780EP使用AT指令
这里正式研究AT指令的学习了,之前只是接触的AT指令,这里则是深入分析AT指令了。 软件的开发方式: AT:MCU 做主控,MCU 发 AT 命令给模组的开发方式,模组仅提供标准的 AT 固件, 所有的业务控制逻辑…...
OpenCV相机标定与3D重建(62)根据两个投影矩阵和对应的图像点来计算3D空间中点的坐标函数triangulatePoints()的使用
加粗样式- 操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 这个函数通过使用立体相机对3维点的观测,重建这些点的三维坐标(以齐次坐标表示)。 cv::triangula…...
基于ollama,langchain,springboot从零搭建知识库四【设计通用rag系统】
需求: 1:可以自定义管理大模型,可自行选择ollama,openai,千问等大模型 2:自定义向量数据库,支持pgvector,elasticsearch,milvus(这三个目前比较常用ÿ…...
【Go面试】工作经验篇 (持续整合)
这里写目录标题 什么是逃逸分析服务端怎么接受客户端上传的文件说一下对gin框架的理解gin有哪些常用中间件gin怎么用swagger写接口文档nginx一般是用来做什么如果调用方法经常超时怎么办gin中怎么和mysql通信从mysql调数据到redis,如何同步延时双删redis ,mysql都不存在用户请求…...
“腾讯、钉钉、飞书” 会议开源平替,免费功能强大
在数字化时代,远程办公和线上协作越来越火。然而,市面上的视频会议工具要么贵得离谱,要么功能受限,甚至还有些在数据安全和隐私保护上让人不放心。 今天开源君给大家安利一个超棒的开源项目 - Jitsi Meet,这可是我在网…...
怎样使用树莓派自己搭建一套ADS-B信号接收系统
0 我们知道,ADS-B全称广播式自动相关监视系统,其实就是飞机发出的广播信号,用明码来对外发送自己的位置、高度、速度、航向等信息,是公开信息。连续接收到一架飞机发出的ADS-B信息后,可以通过其坐标点来描绘出飞机的航…...
终极的复杂,是简单
软件仿真拥有最佳的信号可见性和调试灵活性,能够高效捕获很多显而易见的常见错误,被大多数工程师熟练使用。 空间领域应用的一套数据处理系统(Data Handling System),采用抗辐FPGA作为主处理器,片上资源只包含10752个寄存器,软仿也是个挺花时间的事。 Few ms might take …...
粒子群算法 笔记 数学建模
引入: 如何找到全局最大值:如果只是贪心的话,容易被局部最大解锁定 方法有:盲目搜索,启发式搜索 盲目搜索:枚举法和蒙特卡洛模拟,但是样例太多花费巨量时间 所以启发式算法就来了,通过经验和规…...
Vue.js 嵌套路由和动态路由
Vue.js 嵌套路由和动态路由 在 Vue.js 开发中,Vue Router 是官方提供的路由管理器,用于构建单页应用(SPA)。它支持嵌套路由和动态路由,帮助开发者构建复杂的应用结构。 嵌套路由 嵌套路由允许在路由配置中定义子路由…...
Docker导入镜像
使用命令行进行处理: docker load < onething1_wxedge.tar如下图所示 查看状态 docker images...
C# OpenCV机器视觉:红外体温检测
在一个骄阳似火的夏日,全球却被一场突如其来的疫情阴霾笼罩。阿强所在的小镇,平日里熙熙攘攘的街道变得冷冷清清,人们戴着口罩,行色匆匆,眼神中满是对病毒的恐惧。阿强作为镇上小有名气的科技达人,看着这一…...
STM32项目分享:智能厨房安全检测系统
目录 一、前言 二、项目简介 1.功能详解 2.主要器件 三、原理图设计 四、PCB硬件设计 PCB图 五、程序设计 六、实验效果 七、资料内容 项目分享 一、前言 项目成品图片: 哔哩哔哩视频链接: STM32智能厨房安全检测系统 (资料分…...
docker 安装 redis 详解
在平常的开发工作中,我们经常会用到 redis,那么 docker 下应该如何安装 redis 呢?简单来说:第一步:拉取redis镜像;第二步:设置 redis.conf 配置文件;第三步:编写 docker-…...
《探秘鸿蒙Next:人工智能助力元宇宙高效渲染新征程》
在元宇宙的宏大愿景中,高效的渲染技术是构建沉浸式虚拟世界的关键。鸿蒙Next凭借与人工智能的深度融合,为元宇宙的渲染带来了全新的解决方案和无限可能。 智能场景分析与优化 人工智能能够对元宇宙场景进行智能分析。鸿蒙Next可以利用AI技术对场景中的…...
nginx分发请求超时切换服务
nginx的upstream模块实现超时自动切换服务 upstream testfail {server 192.168.1.218 max_fails1 fail_timeout10s;server 192.168.1.129 max_fails1 fail_timeout10s;} max_fails代表失败尝试次数,达到设置的次数则视为该服务不可用, fail_timeout代…...
vulfocus/fastjson-cnvd_2017_02833复现
漏洞概述 Fastjson 是阿里巴巴开发的一个高性能的 Java 库,用于将 Java 对象转换成 JSON 格式(序列化),以及将 JSON 字符串转换回 Java 对象(反序列化)。 fastjson在解析json的过程中,支持使用type字段来指…...
.Net Core微服务入门全纪录(五)——Ocelot-API网关(下)
系列文章目录 1、.Net Core微服务入门系列(一)——项目搭建 2、.Net Core微服务入门全纪录(二)——Consul-服务注册与发现(上) 3、.Net Core微服务入门全纪录(三)——Consul-服务注…...
OpenCV imread函数读取图像__实例详解
OpenCV imread函数读取图像__实例详解 本文目录: 零、时光宝盒 一、imread函数定义 二、imread函数支持的文件格式 三、imread函数flags参数详解 (3.1)、Flags-1时,样返回加载的图像(使用alpha通道,否…...
GPSd定时检测保活TCP GPS源
为了在 TCP GPS 源丢失连接时自动重新连接,可以编写一个监控脚本,定期检查 gpspipe 输出中的 TCP 源数据是否存在。如果检测到丢失,则使用 gpsdctl 或直接命令重新添加 TCP 源。 1、工具 检查并安装必要工具,本例需要使用 gpspi…...
得物App亮相第七届进博会,科技赋能打造消费新热点
在2024年11月5日至11月10日举办的第七届进博会舞台上,上海交易团虹口分团表现亮眼,其中得物作为来自虹口品质电商的践行者,备受众多参观者关注。 上海得物信息集团有限公司自2015年于上海虹口创立以来,始终坚守“满足年轻人对美好…...
单片机内存管理剖析
一、概述 在单片机系统中,内存资源通常是有限的,因此高效的内存管理至关重要。合理地分配和使用内存可以提高系统的性能和稳定性,避免内存泄漏和碎片化问题。单片机的内存主要包括程序存储器(如 Flash)和数据存储器&a…...
用Python绘制一只懒羊羊
目录 一、准备工作 二、Turtle库简介 三、绘制懒羊羊的步骤 1. 导入Turtle库并设置画布 2. 绘制头部 3. 绘制眼睛 4. 绘制嘴巴 5. 绘制身体 6. 绘制四肢 7. 完成绘制 五、运行代码与结果展示 六、总结 在这个趣味盎然的技术实践中,我们将使用Python和Turtle图形…...
Python 预训练:打通视觉与大语言模型应用壁垒——Python预训练视觉和大语言模型
大语言模型是一种由包含数百亿甚至更多参数的深度神经网络构建的语言模型,通常使用自监督学习方法通过大量无标签文本进行训练,是深度学习之后的又一大人工智能技术革命。 大语言模型的发展主要经历了基础模型阶段(2018 年到2021年)、能力探索阶段(2019年…...
神经网络梯度爆炸的原因及解决方案
在深度学习中,梯度爆炸(gradient exploding)是一种常见的训练问题,尤其是在深层神经网络中。梯度爆炸指的是在反向传播过程中,梯度值呈指数级增长,导致网络权重的大幅更新,从而使得网络变得不稳…...
WPS不登录无法使用基本功能的解决方案
前言 WPS不登录无法使用基本功能的原因通常是为了同步数据、提供更多高级功能或满足软件授权要求。然而,一些用户可能出于隐私或便捷性的考虑,不愿意登录账号。在这种情况下,WPS可能会限制未登录用户的使用权限,导致工具栏变灰…...
蓝桥杯lesson3---string的使用
🌈个人主页:羽晨同学 💫个人格言:“成为自己未来的主人~” string的概念 string字符串是一种更加高级的封装,string字符串中包含了大量的方法,这些方法使得字符串的操作变得更加简单,string的使用&…...
Java设计模式 三 工厂方法模式 (Factory Method Pattern)
工厂方法模式 (Factory Method Pattern) 是一种常见的创建型设计模式,旨在通过定义一个接口来创建对象,而将实例化对象的具体类延迟到子类中。工厂方法模式允许客户端通过工厂方法来创建对象,而不需要直接调用构造函数,这样可以减…...
日志收集Day005
1.filebeat的input类型之filestream实战案例: 在7.16版本中已经弃用log类型,之后需要使用filebeat,与log不同,filebeat的message无需设置就是顶级字段 1.1简单使用: filebeat.inputs: - type: filestreamenabled: truepaths:- /tmp/myfilestream01.lo…...
java开发,IDEA转战VSCODE配置(mac)
一、基本java开发环境配置 前提:已经安装了jdk、maven、vscode,且配置了环境变量 1、安装java相关的插件 2、安装spring相关的插件 3、vscode配置maven环境 打开 VsCode -> 首选项 -> 设置,也可以在setting.json文件中直接编辑&…...