基于Petalinux做Linux驱动开发。
部分图片和经验来源于网络,若有侵权麻烦联系我删除,主要是做笔记的时候忘记写来源了,做完笔记很久才写博客。
专栏目录:记录自己的嵌入式学习之路-CSDN博客
目录
1 一个完整的Linux系统(针对Zynq)
1.1 PS部分
1.2 PL部分(若没用到PL就不需要这个部分):
2 Petalinux的内核源码获取
3 系统编译过程
4 NFS挂载根文件系统(Rootfs)的条件
5 交叉编译
6 驱动模块的开发
6.1 驱动的运行方式
6.2 file_operations结构体
6.3 驱动模块的加载和卸载
6.4 一个驱动程序必须有的东西
6.5 字符设备的注册和注销
6.6 一个字符设备驱动必须有的东西
6.7 设备号
6.8 内核空间与用户空间
7 地址映射
7.1 相关函数
8 设备树
8.1 是什么
8.2 为什么
8.3 设备树相关概念
8.4 一个典型的设备树文件
8.5 设备树节点的基本格式
8.6 节点属性
8.7 特殊节点
8.8 如何定位一个节点
8.9 内核启动过程中设备树的解析过程
8.10 如何添加一个设备树节点
8.11 如何引用一个节点
8.12 驱动与设备树交互的函数
8.13 设备树使用注意事项
9 内核的内存申请
9.1 常见的作用域
9.2 驱动程序中常使用static的原因
9.3 动态内存申请
9.4 kmalloc函数
9.5 kzalloc函数
9.6 vmalloc函数
9.7 devm_kmalloc/devm_kzalloc函数
9.8 devm_kmalloc_array/kmalloc_array
10 驱动与用户空间的交互函数
10.1 read函数
10.2 write函数
10.3 unlocked_ioctl
10.4 对比
11 ioctl详解
11.1 ioctl协议的命令组成
11.2 ioctl的宏
11.3 用于输入输出的时候需要注意的点
12 Linux开发常用的头文件
12.1 驱动开发头文件
12.2 Linux应用开发头文件
13 Linux驱动开发常用的宏
1 一个完整的Linux系统(针对Zynq)
1.1 PS部分
五大要素:
(1)fsbl(First Stage Boot Loader) -> zynq_fsbl.elf
负责初始化PS部分的硬件并加载第二阶段的引导加载程序。
(2)uboot(Universal Boot Loader) -> u-boot.elf、boot.scr(boot引导)
Bootloader 是在操作系统运行之前执行的一段小程序。通过这段小程序,可以初始化硬件设备、建立内存空间的映射表,从而建立适当的系统软硬件环境,为最终调用操作系统内核做好准备。
boot.scr文件则是用于给uboot执行和启动相关的行为的脚本,其中包含了加载内核、设备树、根文件系统等操作,我个人感觉和uboot中的bootm、bootz等命令类似。而这个文件整体又和uboot中bootargs、bootcmd这两个环境变量的作用类似。
(3)设备树文件 -> system.dtb
一种描述硬件的数据结构。
(4)linux内核 -> image.ub、(zImage)、(uImage)
内核是Linux系统的核心,负责管理系统的硬件和软件资源。其中,image.ub可以直接通过uboot上的bootm命令进行Linux的引导启动,也是放在SD卡BOOT分区就能自动引导的内核镜像文件;zImage是一种经过gzip压缩的Linux内核镜像格式;uImage是U-Boot引导加载程序专用的内核镜像格式。它是在zImage或Image(不加压缩的内核镜像)的基础上加上一个U-Boot头部信息(U-BootHeader),使U-Boot能够识别并加载内核镜像。
注:image.ub其实包含了system.bit,zImage,system.dtb三者。
(5)根文件系统 -> rootfs.tar.gz
根文件系统提供了操作系统运行所需的文件和程序。
1.2 PL部分(若没用到PL就不需要这个部分):
(1)比特流(Bitstream)文件 -> system.bit
FPGA的配置文件,用于初始化PL部分的硬件。
2 Petalinux的内核源码获取
进行驱动开发时往往需要Linux的源码,但在Xilinx的Github中不一定有指定版本(petalinux编译时的版本)的Linux内核源码,例petalinux 2023.1的6.1.5版本的内核就没有打包在那里。
而petalinux编译的过程中,默认是会删除掉其解压出来的源码的。要拿到它编译的源码,可以修改<plnx_proj>/project-spec/meta-user/conf/petalinuxbsp.conf,加上RM_WORK_EXCLUDE += "linux-xlnx",编译后可以在<plnx_proj>/build/tmp/work-shared/zynq-generic-7z020/kernel-source中获取源码。
若需要uboot源码,则加上RM_WORK_EXCLUDE += “u-boot-xlnx”。
3 系统编译过程
普通的petalinux开发,最后会将fsbl、比特流文件、uboot、设备树这四个部分都打包到一个BOOT.BIN的文件中去。常用命令是:
petalinux-package --boot --fsbl --fpga --u-boot –-force
若进行Linux驱动开发,设备树文件、linux内核、根文件系统以及比特流文件都是有可能经常发生改动的,因此要尽量将这些分离出来。
(1)首先,是在编译时,仅将fsbl和uboot编译并打包进BOOT.BIN中,因为这两个文件基本不会变:
petalinux-build -c bootloader //编译fsbl
petalinux-build -c u-boot //编译uboot
petalinux-package --boot --fsbl --u-boot --dtb no –force//打包两者进BOOT.BIN,并放过其他成员。
(2)其次,是petalinux-build后修改boot.scr,因为以这样的方式编译出来的boot.scr脚本的内容是不对的,具体就是:
将部分对uImage的操作改为对zImage的操作(这里主要就是不使用image.ub改用zImage了);
并添加对system.bit的操作(添加关于比特流的内容是因为没有将比特流打包进BOOT.BIN,过程中生成的boot.sc也因此没有与其相关的操作,所以要手动添加);
具体可以看正点原子的教程或看正常用petalinux整合编译时的boot.scr文件,这里其实就是将其修改为正常boot.scr该有的样子。最后是将原版改名为boot.cmd.default,并删掉第一行(包含乱码的行)再使用以下命令重新生成为boot.scr:
mkimage -c none -A arm -T script -d boot.cmd.default boot.scr
要重新生成一个boot.scr的理由也很简单,就是因为boot.scr文件的头部带有一些生成的二进制数据,直接修改boot.scr是不行的。
(3)接着,给Linux源码增加设备树文件,从上面编译到的文件中取(路径为<petalinux项目根目录>/components/plnx_workspace/device-tree/device-tree),具体需要pcw.dtsi,pl.dtsi,system-top.dts,zynq-7000.dtsi和system-conf.dtsi这五个。将其放置到源码/arch/arm/boot/dts目录中并根据需要对system-user.dtsi和该目录下的MAKEFILE进行修改。
(4)然后,利用make xilinx_zynq_defconfig命令设置内核配置;并使用make -j8编译内核。该步会在arch/arm/boot中生成所需的内核镜像文件zImage,在arch/arm/boot/dts生成设备树二进制文件system-top.dtb。若仅修改了设备树文件,可以仅编译它,用命令make dts。
(5)其后,回到petalinux项目中,用petalinux-config -c rootfs命令和petalinux-build -c rootfs命令配置并编译根文件系统得到rootfs.tar.gz。
(6)最后,在SD卡的BOOT分区,放入文件。用BOOT.BIN,boot.scr,system.bit,zImage,system.dtb五个文件代替普通Petalinux开发的BOOT.BIN,boot.scr和image.ub三个文件的方案:
BOOT.BIN :来自步骤(1),是仅包含fsbl和uboot的启动引导文件;
boot.scr :来自步骤(2),是添加比特流操作行为且更改boot内核行为后的boot脚本;
system.bit :来自步骤(1),是petalinux项目生成的比特流文件;
zImage :来自步骤(4),是Linux内核;
system.dtb :来自步骤(4),由system-top.dtb文件改名获得,是设备树二进制文件。
(7)最最后,将根文件系统解压后放入SD卡的rootfs分区:
rootfs.tar.gz :来自步骤(5),根文件系统的压缩包。
(附加)使用NFS挂载根文件系统会更好,因为不用经常将SD卡拿出来修改rootfs分区,而boot分区则是进入Linux后随便改,反正进系统后该分区就没有实际的用处了。
4 NFS挂载根文件系统(Rootfs)的条件
(1)Ubuntu安装NFS
sudo apt install nfs-kernel-server
(2)Ubuntu一个创建NFS的挂载路径
假设为/home/xxx/workspace/nfs
(3)Ubuntu修改NFS配置文件
sudo vi /etc/exports
在文件末添加如下内容:
/home/xxx/workspace/nfs *(rw,sync,no_root_squash)
(4)Ubuntu重启rpcbind服务和NFS服务(其实重启系统最好)
sudo /etc/init.d/rpcbind restart
sudo systemctl start nfs-kernel-server.service
(5)网络硬件设置
Zynq开发板(用网线PS口)与Ubuntu(用网络桥接)连接到同一路由器上。
(6)Ubuntu网络软件设置
通过ifconfig命令查看Ubuntu的局域网IP地址、网关地址、掩码等。
(7)Zynq网络软件设置
开机并在倒计时前回车进入uboot,使用dhcp命令自动获取IP,依次使用以下命令将相关配置写入开发板,具体IP要根据实际情况更改:
setenv ipaddr 192.168.100.10 //开发板 ip 地址
setenv gatewayip 192.168.100.1 //开发板网关
setenv netmask 255.255.255.0 //开发板 ip 地址掩码
setenv serverip 192.168.100.2 //ubuntu ip地址
最后使用saveenv命令将其保存。
(8)Zynq boot命令参数设置
使用以下命令设置boot要执行的参数:
setenv bootargs 'console=ttyPS0,115200 root=/dev/nfs rw nfsroot=192.168.100.2:/home/XXX/RootFS_NFS,nfsvers=3 ip=192.168.100.10:192.168.100.2:192.168.100.1:255.255.255.0::eth0:off'
最后使用saveenv命令保存。
至此,设置完毕。后续调试时,只有boot分区需要提前放好在SD卡中,rootfs直接解压在Ubuntu的/home/xxx/workspace/nfs/rootfs目录后,打开Zynq开发板就能自动挂载根文件系统。
5 交叉编译
当主机平台(运行编译器的平台)和目标平台(产生的程序将在其上运行的平台)不兼容时,该过程就叫做交叉编译。
petalinux装好后的编译环境:
由none可以看出,这些编译器是没有编译linux的能力的。而Petalinux在构建linux系统过程中会编译生成linux交叉编译工具链,然后使用其构建 linux 系统,可以在Petalinux工程下使用“find . -name "arm-xilinx-linux-gnueabi-gcc"”命令找到相应的痕迹:
需要拿到交叉编译的SDK,需要对某一个petalinux项目执行petalinux-build --sdk,编译好的sdk安装脚本是/项目/image/linux中的sdk.sh,安装到某一地方后安装程序会提示后续需要配置sdk环境的命令,可以写个别名放到~/.bashrc中方便使用。
安装了SDK后的arm包:
6 驱动模块的开发
6.1 驱动的运行方式
(1)编译进内核;
(2)编译成模块,在内核启动后使用insmod命令加载驱动模块;
一般在调试开发时是编译成模块,开发完成后就可以在编译成模块和编译进内核中选择了。
6.2 file_operations结构体
位于Linux内核/include/linux/fs.h中,其中fs是file system的缩写。该结构体是Linux内核驱动操作函数集合。
6.3 驱动模块的加载和卸载
(1)加载
insmod:加载指定的.ko模块,但不能解决模块的依赖关系,比如 drv.ko依赖 first.ko这个模块,就必须先使用 insmod命令加载 first.ko这个模块,然后再加载 drv.ko 这个模块;
modprobe(推荐):modprobe 会分析模块的依赖关系,然后将所有依赖的模块都加载到内核中,因此 modprobe命令相比 insmod 要智能一些。其次modprobe还有错误检查、错误报告等功能;
注意:modprobe命令默认会去/lib/modules/<kernel-version>目录中查找模块,如果没有的话需要自行创建,如petalinux2020.2就需要创建/lib/modules/5.4.0-150-generic目录。
注意:使用modprobe命令前,需先运行depmod建立系统的模块依赖关系。
(2)卸载
rmmod(推荐):卸载某个模块;
modprobe:卸载某个模块及其依赖的模块,而因为该模块依赖的模块也有可能在被其他模块所使用,卸载会出问题,所以不推荐使用modprobe来卸载模块;
6.4 一个驱动程序必须有的东西
(1) 驱动入口函数:static int __init xxx_init(void)
(2) 驱动出口函数:static void __exit xxx_exit(void)
(3) 指定驱动入口函数的语句:module_init(xxx_init); // 指定后加载模块就会自动运行驱动入口函数
(4) 指定驱动出口函数的语句:module_exit(xxx_exit); // 指定后卸载模块就会自动运行驱动出口函数
(5) 开源许可证类型:MODULE_LICENSE(str) //添加模块LICENSE信息(必须)
(6) 作者信息:MODULE_AUTHOR(str