i.MX6ULL Linux 基础、驱动与 Qt 移植
A 移植概念相关
1. Uboot启动流程
flowchart TD
A[芯片上电] --> B[Boot ROM
固化在芯片内部的只读存储器]
B --> C[根据启动拨码开关选择源
如: eMMC, SD Card, NAND Flash等]
C --> D[Boot ROM 读取启动介质头信息
获取IVT, DCD等数据]
D --> E{校验并加载
第一阶段引导程序}
E -- 例如SPL --> F[初始化时钟, DDR等关键外设]
F -- 跳转 --> G[加载U-Boot第二阶段至DDR]
G --> H[U-Boot 第二阶段初始化
各种驱动和环境变量]
H --> I{启动内核或进入命令行}
I -- 无操作或命令 --> J[加载Linux内核]
I -- 用户打断 --> K[进入U-Boot命令行]
J --> L[启动操作系统]
2. Uboot移植
1. 获取基础代码
通过参考NXP官方的板子,在这个板子的Uboot代码基础上进行修改,我们参考的是mx6ull_14x14_evk_emmc_defconfig(硬件设计参考了哪块官方板卡,就使用哪块板卡的U-Boot作为基础)
2。编译官方的代码
编译官方代码,一是检查代码是否能成功编译,二是可以尝试代码是否可以在自己板子上运行,如果能运行,可以观察哪些驱动有问题,需要修改
3.创建新板配置
添加默认配置文件: 在 configs/目录下复制并修改官方板的配置文件(如 defconfig),更改名称和关键选项(指定新的板级头文件路径)
添加板级头文件 在 include/configs/目录下复制并修改官方板的头文件(如 .h)。这是最主要的配置中心,用于设置DDR容量、环境变量、驱动使能(如网络、LCD)、设备地址等。
添加板级代码目录: 在 board/vendor/(如 board/freescale/)下复制并修改官方板的板级目录。需要修改其中的 Makefile, Kconfig, 主源文件(如 .c文件)等,以适配新板的初始化代码(如GPIO、时钟)。
4.修改驱动
这个板子主要修改两部分具体驱动,重点都是修改环境变量和参数与引脚配置
- LCD屏幕
需要根据自己的屏幕来修改,这里主要修改了
(xres, yres):分辨率,修改为LCD面板的实际物理分辨率,例如从默认的480x272修改为1024x600
pixfmt:像素格式,也就是一个像素点是多少位
pixclock:像素时钟,每个像素时钟周期的长度,单位为皮秒。
panel:panel环境变量主要用于指定当前使用的 LCD 屏幕的类型或名称,在板级头文件中
name:结构体(t display_info_t)中个有个成员变量mode,也是一个结构体(fb_videomode),其中的成员(.name),也是指定LCD 屏幕的类型或名称,panel和name的值需要一样
然后可以通过启动uboot,设置环境变量panel
1 | setenv panel TFT7016 |
将环境变量保存到EMMC中,然后重新启动,检查LCD状态,若还有问题,继续检查代码是否修改完善
- 网络驱动
正点原子的 I.MX6U-ALPHA 开发板提供了这两个网络接口,其中 ENET1 和 ENET2 都使用 SR8201F 作为 PHY 芯片。NXP 官方的I.MX6ULL EVK 开发板使用 KSZ8081 这颗 PHY 芯片
因为修改了PHY芯片,所以网络驱动修改的核心是适配PHY芯片型号(驱动+地址)和正确控制复位引脚(时序+延时),正点原子教程通过删除原厂扩展芯片逻辑,添加自定义GPIO复位,并满足SR8201F的时序要求来完成移植
修改完成后通过配置相关变量
1 | setenv ipaddr 192.168.1.55 //开发板 IP 地址 |
然后通过ping命令检测是否修改成功
- 配置环境变量bootcmd & bootargs
环境变量bootcmd
bootcmd 保存着 uboot 默认命令,uboot 倒计时结束以后就会执行 bootcmd 中的命令。这些命令一般都是用来启动 Linux 内核的,比如读取 EMMC 或者 NAND Flash 中的 Linux 内核镜像文件和设备树文件到 DRAM 中,然后启动 Linux 内核。可以在 uboot 启动以后进入命令行设置 bootcmd 环境变量的值。如果 EMMC 或者 NAND 中没有保存 bootcmd 的值,那么 uboot 就会使用默认的值,板子第一次运行 uboot 的时候都会使用默认值来设置 bootcmd 环境变量,可以通过修改uboot文件的内容,设置bootcmd的默认值
下面主要说一下bootcmd具体内容:
1 | mmc dev 1 //切换到 EMMC |
总结就是从EMMC中读取zImage和dtb文件到内存中,然后启动linux
当然在后面学习驱动的时候,通常是通过tftp来下载zImage和dtb文件到内存中,通过NFS挂载根文件系统
1 | tftp 80800000 zImage; |
bootargs
bootargs 保存着 uboot 传递给 Linux 内核的参数,分别是
console:console 用来设置 linux 终端(或者叫控制台),也就是通过什么设备来和 Linux 进行交互,是串口还是 LCD 屏幕?如果是串口的话应该是串口几等等。一般设置串口作为 Linux 终端,这样我们就可以在电脑上通过 SecureCRT 来和 linux 交互了。这里设置 console 为 ttymxc0,因为 linux启动以后I.MX6ULL 的串口 1 在 linux 下的设备文件就是/dev/ttymxc0,在 Linux 下,一切皆文件。
root:t 用来设置根文件系统的位置,例如root=/dev/mmcblk1p2 用于指明根文件系统存放在mmcblk1 设备的分区 2 中,而在学习驱动的时候,我们通常使用nfs挂载
1 | //nfs挂载(其中的网络ip地址等要根据自己实际使用的更改) |
root 后面有“rootwait rw”,rootwait 表示等待 mmc 设备初始化完成以后再挂载,否则的话mmc 设备还没初始化完成就挂载根文件系统会出错的。rw 表示根文件系统是可以读写的,不加rw 的话可能无法在根文件系统中进行写操作,只能进行读操作
附一张正点原子手册详解
4.启动Uboot
有两种启动方式,从EMMC启动 从网络启动(nfs),前面已经说过了,启动需用用到的文件zImage(内核映像文件)和dtb文件(设备树文件),将在内核源文件编译后生成
3. 内核启动流程(NULL)
很明显,我肯定没弄懂启动流程,所以附两个流程图大致了解一下
graph TD
A[stext入口] --> B[关闭MMU/D-cache]
B --> C[校验处理器ID
__lookup_processor_type]
C --> D[校验启动参数
__vet_atags]
D --> E[创建页表
__create_page_tables]
E --> F[使能MMU
__enable_mmu]
F --> G[切换上下文
__mmap_switched]
G --> H[start_kernel初始化]
H --> I[rest_init创建进程]
I --> J[创建init进程 PID=1]
I --> K[创建kthreadd进程 PID=2]
I --> L[进入idle进程 PID=0]
J --> M[kernel_init_freeable]
M --> N[挂载根文件系统
prepare_namespace]
M --> O[执行用户空间init程序]
O --> P[用户空间初始化]
graph LR
内核态 -->|mount rootfs| 根文件系统
根文件系统 -->|exec /sbin/init| 用户态
4. 内核移植
1. 获取代码-编译-启动
通常使用Linux 图形配置界面,uboot也有,但是因为修改部分比较少,所以未使用
然后复制添加设备树文件和开发板默认配置文件,编译下载仅板子测试,注意要先挂载一份好的根文件系统,否则会启动失败
2. 修改驱动
- CPU 主频修改 当前 CPU 支持 198MHz、396MHz、528Mhz 和 792MHz 四种频率切换,其中调频策略为 ondemand,也就是定期检查负载,然后根据负载情况调节 CPU 频率。
- 修改 EMMC 驱动
使能 8 线 EMMC 驱动,Linux 内核驱动里面 EMMC 默认是 4 线模式的,4 线模式肯定没有 8 线模式的速度快,在设备树中修改
关闭 EMMC 1.8V 供电选项,因为板子的EMMC供电应该为3.3V
- 底板网络驱动修改
和之前的uboot原因一样,因为使用的网络PHY芯片不同,前三点都在设备树文件中修改
- 修改 SR8201F 的复位以及网络时钟引脚驱动
- 中修改 fec1 和 fec2 节点的 pinctrl-0 属性
- 修改 SR8201F 的 PHY 地址
- 修改 fec_main.c 文件 根据 SR8201F 收据手册上的要求,SR8201F 在复位结束以后需要等待至少 150ms 才能操作 SR8201F,因此这里添加了一个 200ms 的延时
3. 网络驱动测试
修改好设备树和 Linux 内核以后重新编译一下,得到新的 zImage 镜像文件和 dtb 设备树文件,使用网线连接后,ifconfig测试有几个网卡,然后打开网卡,用ping指令测试是否有网络
下面是正点原子总结的移植步骤:
总结一下移植步骤:
①、在 Linux 内核中查找可以参考的板子,一般都是半导体厂商自己做的开发板。
②、编译出参考板子对应的 zImage 和.dtb 文件。
③、使用参考板子的 zImage 文件和.dtb 文件在我们所使用的板子上启动 Linux 内核,看能
否启动。
④、如果能启动的话就万事大吉,如果不能启动那就悲剧了,需要调试 Linux 内核。不过
一般都会参考半导体官方的开发板设计自己的硬件,所以大部分情况下都会启动起来。启动
Linux 内核用到的外设不多,一般就 DRAM(Uboot 都初始化好的)和串口。作为终端使用的串口
一般都会参考半导体厂商的 Demo 板。
⑤、修改相应的驱动,像 NAND Flash、EMMC、SD 卡等驱动官方的 Linux 内核都是已经
提供好了,基本不会出问题。重点是网络驱动,因为 Linux 驱动开发一般都要通过网络调试代
码,所以一定要确保网络驱动工作正常。如果是处理器内部 MAC+外部 PHY 这种网络方案的
话,一般网络驱动都很好处理,因为在 Linux 内核中是有外部 PHY 通用驱动的。只要设置好复
位引脚、PHY 地址信息基本上都可以驱动起来。
⑥、Linux 内核启动以后需要根文件系统,如果没有根文件系统的话肯定会崩溃,所以确定 Linux
内核移植成功以后就要开始根文件系统的构建。
5. 根文件系统的构建
根文件系统首先是内核启动时所 mount(挂载)的第一个文件系统,内核代码像文件保存在根文件系统中,而系统引导启动程序会在根文件系统挂载之后从中把一些基本的初始化脚本和服务等加载到内存中去运行。
教程中是使用BusyBox 构建根文件系统,主要修改两部分 添加编译器 添加中文支持,选择动态编译后编译 busybox,会得到一个基础的根文件系统(BusyBox的工作也就到此为止)
然后是向根文件系统中添加向根文件系统添加 lib 库(动态库,如果是静态编译则不用,但是静态库很占内存,并且可能导致dns不能使用)和一些基础文件目录(如 dev、proc、mnt、sys、tmp 和 root 等),这里的lib库通过交叉编译器工具中获取,并且没有裁剪,是为了避免初学时因库文件缺失而带来的各种问题,裁剪是后续优化的工作,目前全部添加,内存就已经达到124MB。
目前如果直接nfs挂载根文件系统启动,会提示还缺少几个文件/etc/init.d/rcS /etc/fstab /etc/inittab,rcS 是个 shell 脚本,Linux 内核启动以后需要启动一些服务,而 rcS 就是规定启动哪些文件的脚本文件,fstab 在 Linux 开机以后自动配置哪些需要自动挂载的分区,inittab定义系统启动、关闭、重启等不同阶段应该执行什么动作以及在哪个终端上执行
最后就可以启动根文件系统,来测试是否构建成功了(可以通过运行c文件,测试添加开机自启动文件,测试网络,添加dns来测试)
当然还有其它的构建根文件系统的工具,比如buildroot,大家可以自行在网上查阅资料使用
6. 烧写系统
至此,三个部分,Uboot 内核 根文件系统,已经全部构建完成,就可以烧写整个系统到板子里开始学习linux驱动了,但是我们移植都是通过网络来测试的,在实际的产品开发中肯定不可能通过网络来运行,所以需要通过NXP 官方提供的 MfgTool 工具通过 USB OTG 口来烧写系统,具体方法也是修改官方的烧写脚本,在文件里面添加自己移植好的uboot,dtv,kernel,根文件系统,然后烧写到板子里。
B 驱动概念相关
1. Linux的内存管理
在 32位 linux系统重,通常会把内存映射为4GB,通过内存管理单元 MMU实现虚拟和物理地址的转换
- 程序所使用的内存地址叫做虚拟内存地址(Virtual Memory Address)
- 实际存在硬件里面的空间地址叫物理内存地址(Physical Memory Address)。
这里面会用到页表—内存管理的核心数据结构之一,32位系统下通常使用2级表或3级表
- 页全局目录(PGD)
- 页中间目录(PMD)(可选)
- 页表(PT)
驱动开发中如何通过物理地址得到虚拟地址
这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数ioremap 和 iounmap,当然如果使用设备树的可以使用 of_iomap 来直接获取设备节点对应的虚拟地址。
1 |
|
虚拟内存的优缺点
- 优点
扩大地址空间 每个进程独占一个4G空间,让每个线程都认为自己有很大的内存,虽然真实物理内存没那么多
内存保护 防止不同进程之间对物理内存的争夺,可以实现对特定内存实现读写保护,防止恶意篡改
避免内存碎片 虽然物理地址可能不连续,但是映射虚拟地址上可以连续
内存共享 可以实现进程间的通信,不同进程的虚拟地址可以指向同一个物理地址,实现内存共享(例如共享库),节省内存空间
- 缺点
虚拟内存需要额外构建数据结构(页 页表 页框 分别对应物理内存 查询使用 虚拟内存),占用空间
增加执行时间,虚拟地址到物理地址的转化过程
关于“申请虚拟内存不立即分配物理内存”
“只要不读写这个虚拟内存,操作系统就不会分配物理内存”——在原理上是正确的
现代操作系统(如Linux)的malloc等函数在申请内存时,分配的是虚拟内存。操作系统仅在程序实际访问(读取或写入)这块内存时,才会通过“缺页中断”机制为其分配真正的物理内存页,这个过程称为“按需分配”。
所以,单纯分配大量虚拟内存,理论上不会消耗对等的物理内存
2. 字符设备驱动
linux系统将设备分为3类:字符设备、块设备、网络设备
通常是应用层调用一些API函数,访问到内核里的驱动程序,然后再调用硬件外设,如下图
flowchart TD
subgraph A [用户空间]
direction TB
A1[应用程序
open, close, read, write等API]
A2[库函数
C库等]
end
subgraph B [内核空间]
direction TB
B1[系统调用接口
sys_open, sys_close, sys_read, sys_write]
B2[驱动程序
open, close, read, release等函数]
end
C[硬件设备]
A1 --调用--> A2
A2 --触发--> B1
B1 --分发--> B2
B2 --操作--> C
字符设备驱动基础编写流程
模块入口与出口 module_init, module_exit 指定驱动加载和卸载时执行的函数
设备号申请 alloc_chrdev_region(动态) 或 register_chrdev_region(静态)
初始化并注册 cdev (描述字符设备的结构体) cdev_init, cdev_add
自动创建设备节点 class_create, device_create
实现文件操作函数 定义并实现file_operations中的open, read, write, release等
资源释放与注销 cdev_del, unregister_chrdev_region, device_destroy, class_destroy 在模块退出时逆序释放所有资源
3. 设备树
设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做 DTS(Device Tree Source),这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如CPU 数量、 内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等
设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点,。Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,OF函数
下面演示一下获取设备节点和读取节点里的内容(不涉及错误处理)
1 |
|
设备树中提供硬件连接和配置的静态描述,是pinctrl和GPIO子系统等配置信息的载体
3. pinctrl 和 gpio 子系统实验
- pinctrl 子系统
要使用 pinctrl 子系统,我们需要在设备树里面设置 PIN 的配置信息,然后pinctrl 子系统要根据你提供的信息来配置 PIN 功能,一般会在设备树里面创建一个节点来描述 PIN 的配置信息
举个例子:
1 | pinctrl_test: testgrp { |
通常会将这个节点放在iomuxc节点下,pinctrl 驱动程序是通过读取“fsl,pins”属性值来获取 PIN 的配置信息
- GPIO 子系统
pinctrl 子系统重点是设置 PIN(有的 SOC 叫做 PAD)的复用和电气属性,如果 pinctrl 子系统将一个 PIN 复用为 GPIO 的话,那么接下来就要用到 gpio 子系统了。gpio 子系统顾名思义,就是用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO为输入输出,读取 GPIO 的值等。gpio 子系统的主要目的就是方便驱动开发者使用 gpio,驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API函数来操作 GPIO,Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大的方便了驱动开发者使用 GPIO。
举个例子:
1 | test { |
下面是一些常用API函数:
gpio_request() 用于申请一个 GPIO 管脚
gpio_free() 不使用某个 GPIO 了,通过该函数释放
gpio_get_value() 用于获取某个 GPIO 的值(0 或 1)
gpio_set_value() 用于设置某个 GPIO 的值
- 常见流程
- 添加 pinctrl 节点:设置 PIN 的复用和电气属性。
- 添加设备节点:配置 GPIO 的输入、输出或中断功能。
- 检查 PIN 冲突:确保 PIN 未被其他外设占用,避免资源冲突。
4. 处理并发与竞争的方法
Linux是一个多任务操作系统,肯定会存在多个任务共同操作同一段内存或者设备的情况,多个任务甚至中断都能访问的资源叫做共享资源,就和共享单车一样。在驱动开发中要注意对共享资源的保护,也就是要处理对共享资源的并发访问
在linux下,引发竞争问题的原因主要有这几种:
- 多线程并发,多个线程同时操作某个文件
- 抢占式并发,高优先级抢占访问了某个低优先级正在访问的文件
- 中断式并发,多是硬件中断抢占访问了当前线程正在访问的资源
- 多核(SMP)并发,多个核心同时访问某个资源
- 原子操作
一般原子操作用于变量或者位操作,原子操作就是指不能再进一步分割的操作,在linux内核中通过 atomic_t 结构体来实现
1 | //原子整形变量 |
通过定义原子变量,保证变量修改的过程中不被其它资源访问
位操作也是很常用的操作,Linux 内核也提供了一系列的原子位操作 API 函数,只不过原子位操作不像原子整形变量那样有个 atomic_t 的数据结构,原子位操作是直接对内存进行操作
下面是一些常用API函数:
1 | void set_bit(int nr, void *p); //将p地址的地nr位置1 |
- 自旋锁
原子操作只能对整型变量或者位进行保护,而实际使用不可能只有这些简单的临界区,所以引入自旋锁
对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁
linux内核中使用 spinlock_t结构体 来表示自旋锁
下面是相关的API函数
1 | spinlock_t lock; //首先定义自旋锁 |
下面介绍一个概念:死锁
举几个例子:
- 线程A获取自旋锁(此时自旋锁会自动禁止抢占),但是如果在线程A中却调用的一些休眠或者阻塞的函数,使得A主动放弃了CPU使用权,此时B线程开始运行了,如果B线程也想获取锁,可是锁在A线程,并且内核抢占也被关闭了,那B运行不了,A也无法运行,锁也无法释放,死锁便发生了
- 同理线程A获取自旋锁,此时中断发生了,抢走了CPU使用权,如果中断也想要自旋锁,中断就会一直自旋,那么A无法执行和释放锁,中断也无法继续执行,那么就会发生死锁
- A线程获取自旋锁,然后再临界区递归申请自旋锁1,那么线程A就会进入自旋,因为自己还没有释放锁,那么就会形成死锁
如何解决这两种死锁:
- 第一种就是杜绝在临界区执行休眠或者阻塞相关函数
- 第二种就是在获取自旋锁之前,禁止本地中断,同时linux内核提供了相关API函数
- 第三种就是杜绝递归申请自旋锁
1 | void spin_lock_irq(spinlock_t *lock) 禁止本地中断,并获取自旋锁。 |
- 读写锁 顺序锁
| 锁类型 | 读并发 | 写并发 | 读写并发 |
|---|---|---|---|
| 读写锁 | 可以 | 不可以 | 不可以 |
| 顺序锁 | 可以 | 不可以 | 可以 |
| 自旋锁 | 不可以 | 不可以 | 不可以 |
- 信号量 互斥体
这个在rt_thread中已经说明过,这里主要展示linux内核中相关API函数
1 | struct semaphore sem; /* 定义信号量 */ |
5. Linux内核定时器
定时器是我们最常用到的功能,一般用来完成定时功能,这里介绍Linux内核中定时器的使用方法,这里不展开系统节拍和时钟说明,主要说明在驱动中相关API函数如何使用
Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0
内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器,Linux 内核使用 timer_list 结构体表示内核定时器
1 | struct timer_list { |
下面是一个完整例子,包含初始化定时器结构体,注册定时器,定时器中断每隔1s打印一次消息,打印10次后不再打印
1 | struct timer_list timer; //定义一个定时器 |
上面个例子中,msecs_to_jiffies(1000)函数,可以将1000ms转化为jffies的单位,来实现符合s,ms的延时中断
在不使用中断的时候,linux内核也提供短延时函数如下:
1 | //分别为纳秒,微妙和毫秒延时 |
6. Linux内核中断框架
中断都是频繁使用的功能之一,下面主要介绍中断的相关重点,比如中断号,中断上半部和下半部,以及设备树中如何申请
- 中断号
每个中断都有一个中断号,通过中断号即可区分不同的中断,linux中用一个int的变量表示中断号,linux内核通过中断号找到对应的中断处理函数
申请中断号有两种:
一种是通过过 irq_of_parse_and_map 函数从设备树中的 interputs 属性中提取对应的设备号
如是使用GPIO的中断,可以使用 gpio_to_irq 函数来获取gpio对应的中断号
- 常用相关API函数
1 |
|
- 中断的上半部和下半部
我们之前使用request_irq()注册的中断函数就属于中断处理的上半部,只要中断触发,那么就会立即执行,而我们又知道中断必须快速执行,否则会影响整个系统,但现实中有些中断处理肯定很费时,所以这里引出下半部,举个例子:在stm32中中断函数里面通常设置一个标志位,然后在主函数循环里面读取这个标志位,再执行相关处理(这里就相当于是下半部)
总结一下:
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出
在linux中提供了很多下半部的机制:比如软中断,tasklet,工作队列,中断线程化
其中tasklet就是基于软中断实现的,下面的介绍在gt911的介绍里面也讲过了,这里偷懒粘贴一下:
| 特性维度 | 软中断 | Tasklet | 工作队列 | 中断线程化 |
|---|---|---|---|---|
| 本质/关系 | 传统下半部核心机制,基于中断上下文 | 基于软中断实现,更易用 | 传统下半部机制,运行于内核线程上下文 | 中断下半部的一种实现形式,但采用线程模型 |
| 执行上下文 | 中断上下文(软中断上下文) | 中断上下文(软中断上下文) | 进程上下文(普通内核线程) | 进程上下文(专用实时内核线程) |
| 是否可阻塞/睡眠 | 否 | 否 | 是 | 是 |
| 优先级 | 最高(在中断上下文执行) | 较高 | 较低(普通线程调度) | 可调度的实时优先级(通常高于普通工作队列) |
| 并发特性 | 同一种类可在多CPU上并行,需重入设计 | 同一tasklet在多个CPU上串行执行 | 可配置,但同一工作队列中的任务可能串行执行 | 每个中断有独立线程,可绑定到特定CPU |
| 编程复杂度 | 高(需考虑重入和并发安全) | 中(自动序列化简化编程) | 低(类似普通内核编程) | 低(类似普通线程编程) |
| 适用场景 | 高频、实时性要求极高的场景(如网络包处理) | 简单的设备中断处理,需要避免并发问题 | 耗时操作,需要睡眠或阻塞的场景(如文件I/O) | 复杂的驱动处理,需要实时性保证和可睡眠能力 |
- 设备树相关
在imx6u上,intc节点下就是中断控制器节点,还有gpio也可以当做中断节点使用
举个例子:
1 | intc: interrupt-controller@00a01000 { |
然后就是在设备节点引用这个中断控制器,举个例子:
1 | gt911:gt911@5d { |
7. 阻塞/非阻塞IO
这里的阻塞和非阻塞体现在是否进行休眠
阻塞: 用户态通过 read() 函数,然后进入内核态,如果设备不可用,那么就会进入休眠状态,阻塞到设备可用,然后返回数据给用户态
非阻塞:用户态通过 read() 函数,然后进入内核态,如果设备不可用,那么直接返回错误码,这个时候用户态可以继续重新读取数据,如果还是不可用,就继续返回设备码,如果可用,就返回数据给用户态
阻塞访问的优点在于如果文件不可操作的时候可以进入休眠状态,这样就可以让出CPU的资源,linux内核中通过等待队列来实现阻塞进程的唤醒工作,比如按键读取就可以使用阻塞IO访问
非阻塞访问的时候,linux内核通过轮询的方式来处理非阻塞访问
8. 异步通知
之前的阻塞/非阻塞IO 是用户通过 read() 函数主动读取,对于非阻塞还是得不断地轮询,这并不是很好的处理方式。
所以这里引入异步通知,当驱动程序有消息的时候,主动向应用程序发出通知,报告自己可以访问,然后应用程序再读写文件,其实我们之前用的中断也是一种异步机制
阻塞、非阻塞、异步通知,这三种是针对不同的场合提出来的不同的解决方法,异步通知的核心是信号,Linux内核中定义了很多信号,比如:
1 |
|
然后我们在应用程序中通过sighandler_t signa()函数来设置处理什么信号,设置相应的信号处理函数,比如SIGKILL信号,这个就是ctrl+c按下后内核发送的信号,下面用一个简单例子来演示:
1 | //信号处理函数 |
然后是驱动中的写法:
首先是 创建一个fasync_struct 结构体,然后再file_operations操作集中实现fasync函数,然后在fasync函数中通过调用fasync_helper来初始化之前创建的结构体
1 | struct xxx_dev{ |
当初始化完之后,我们可以调用kill_fasync()函数发送指定的信号给用户
1 | void kill_fasync(struct fasync_struct **fp, //要操作的结构体 |
在应用程序部分,对异步通知的处理要分成三步:
- 使用
signal函数来设置信号的处理函数 - 使用
fcntl(fd, F_SETOWN, getpid())将本应用程序的进程号告诉内核 - 开启异步通知,首先通过
flags = fcntl(fd, F_GETFL);来获取当前进程状态,然后通过fcntl(fd, F_SETFL, flags | FASYNC);来开启当前进程的异步通知功能,重点是通过fcntl函数设置进程状态为FASYNC
9. Linux IIC框架
首先要添加一个 i2c_client 结构体,是用来描述设备信息的,然后添加一个i2c_driver结构体,这是编写IIC驱动里面重点处理的内容,通常会有probe函数口和id匹配表等
1 | /* i2c驱动结构体 */ |
下面就是完善gt911_id_table,gt911_of_match_table,gt911_probe等
1 | const struct i2c_device_id gt911_id_table[] = { |
通过上面这些函数,就可以完善IIC的框架,然后可以通过2c_add_driver函数来注册驱动,但是如果再注册驱动的上下文没有其它的事,就可以使用module_i2c_driver(驱动模块的入口/出口辅助宏,用于常规驱动模块),这个更方便
1 | module_i2c_driver(gt911_i2c_driver); //当设备树/id匹配成功之后就可以进入probe函数了 |
10. Linux Input子系统
/待补充……/
C QT有关驱动移植和根文件系统的构建
1.触摸屏/显示相关
我这里使用的触摸屏ic引脚芯片是 gt911 ,如果读者使用的触摸屏是其它芯片的可以看相关视频学习驱动
,gt911具体驱动内容可以看这篇文章:基于imx6ull的gt911移植
2.支持QT的根文件系统的构建
具体教程可以参考文末的正点原子QT移植指南,这里简略介绍一下
这里的环境搭建与正点原子驱动教程中的一致,都是 arm-poky-linux-gnueabi-gcc 编译器
然后是下载编译触摸插件: tslib,这个插件在编写gt911触摸屏驱动之后验证的过程中也已经使用过
下载并编译ARM平台下的QT5.12.9源码 (按照正点原子的教程可能会出现的报错)如下
.Done. /home/mouse/Linux/tool/Qt-everywhere-src-5.12.9/qtbase/bin/qmake: 1: /home/mouse/Linux/tool/Qt-everywhere-src-5.12.9/qtbase/bin/qmake: Syntax error: word unexpected (expecting ")")解决办法是删掉解压的qt源码,然后重新解压再编译 (我的问题是这样解决的)下一步是将编译好的tslib插件和QT移植到最初学习驱动构建的busybox文件系统中,其中tslib再gt911已经移植过,QT移植过程类似,都是打包,解压然后配置环境变量
最后连接显示屏然后执行QT测试文件,如果屏幕上显示qt的图片,并且能触摸,那么就大功告成啦!
/usr/lib/arm-Qt/examples/widgets/animation/animatedtiles/animatedtiles当然在后面制作自己的QT应用的时候,需要使用安装QT的交叉编译器,然后才能在Ubuntu下编译出能在IMX6u上运行的程序,这个正点原子的教程也有,我这里列出安装之后环境变量的默认路径
/opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi,后期使用的时候只用先是能环境变量,然后在QT工程目录下执行qmake即可生成Makefile文件,然后make生成最终文件,拷贝到开发板上就可以运行啦
3.基于QT的简易项目
这里就不贴源码了,因为其实比较简单,具体QT相关内容可以等我整理相关内容文章,后面是打算尝试整合这阵子学习的内容,做一个智能家居类(当然可能会添加功能,改成其它类型的项目,也是为了学习)的物联网项目 基于imx6u的智能家居
参考文献
留言
有问题请指出,你可以选择以下方式:
- 在下方评论区留言
- 邮箱留言
- Title: i.MX6ULL Linux 基础、驱动与 Qt 移植
- Author: H_Haozi
- Created at : 2025-08-20 19:30:55
- Updated at : 2026-09-12 00:24:52
- Link: https://blog.haozi-haozi.cn/2025/08/20/embedded_imx6ull/
- License: This work is licensed under CC BY-NC-SA 4.0.