0.编译
0.1 各版块编译
0.1.1 SDK 板级配置文件
SDK 板级配置文件中提供了一些必要的配置信息。对于 RK3568 平台,其板级配置文件位于/device/rockchip/rk356x目录,如下所示:
该目录下有多个 BoardConfig-xxxx.mk 文件,这些.mk 文件便是板级配置文件。其中BoardConfig-rk3568-atk-evb1-ddr4-v10.mk 就是 ATK-DLRK3568 开发板所使用的板级配置文件;
我们在 SDK 根目录下执行“./build.sh lunch”时所列举出来的文件就是从/device/rockchip/rk356x/目录来的,如下所示:
这些.mk 文件其实是一个 sh 脚本文件,打开 BoardConfig-rk3568-atk-evb1-ddr4-v10.mk
0.1.2 U-Boot 开发
U-Boot 源码在/u-boot 目录,
RK 提供了一份文档详细向用户介绍了 Rockchip 平台 U-Boot 所涉及到的知识点、技术点,包括 Rockchip 平台 U-Boot 基础简介、U-Boot 架构、U-Boot 启动流程、U-Boot 系统模块、驱动模块、Kernel-DTB、AB 系统、AVB 安全启动、TPL、SPL、U-Boot 快捷键等等,内容非常多,对 U-Boot(Rockchip 平台 U-Boot)不太熟悉的用户建议一定要去看看;该文档的路径为:/docs/Common/UBOOT/Rockchip_Developer_Guide_UBoot_Nextdev_CN.pdf。
1. U-Boot 的设备树
- U-Boot 中,RK3568 的设备树文件是/arch/arm/dts/rk3568-evb.dts,该设备树文件包含了 rk3568.dtsi 和 rk3568-u-boot.dtsi
- 原生的 U-Boot 只支持 U-Boot 自己的 DTB,RK 平台在原生 U-Boot 基础上增加了 kernel DTB 机制的支持,即 U-Boot 会使用 kernel DTB 去初始化外设。这样设计的目的主要是为了兼容外设板级差异,譬如 power、clock、display 等。U-Boot 设备树负责初始化存储、调试串口等基础外设;而 kernel 设备树初始化存储、调试串口之外的外设,譬如 LCD 显示、千兆网等。执行 U-Boot 代码时先用 U-Boot 的设备树完成存储、调试串口的初始化操作,然后从存储上加载 kernel 的设备树并转而使用这份设备树继续初始化其余外设。所以用户一般不需要去修改 U-Boot 的设备树文件(除非更换调试串口)。
2.U-Boot 编译 - U-Boot 源码目录下提供了一个编译脚本 make.sh,可以直接使用该脚本编译 U-Boot 源码,譬如在 U-Boot 源码目录下执行如下命令编译 U-Boot:
./make.sh rk3568
- 编译完成后将会生成 uboot.img 和 rk356x_spl_loader_v1.13.112.bin 两个镜像文件。如下所示:
当然,除了这两个镜像之外,还生成了很多的.bin镜像,譬如 bl31_xxx.bin、tee.bin、u-boot.bin、u-boot-dtb.bin、u-boot-nodtb.bin 等等,这些镜像都是中间产物,最终烧录到开发板只有 uboot.img和 rk356x_spl_loader_v1.13.112.bin。
3. U-Boot 的配置
如果用户想要自己配置 U-Boot,可以在 U-Boot 源码目录下执行如下命令对其进行配置:
make rk3568_defconfig //选择配置文件
make menuconfig //打开图形化配置界面,通过 menuconfig 图形化界面、用户可自行对 U-Boot 进行配置
make savedefconfig //配置完成后运行以下命令保存配置,把配置信息保存到 defconfig 文件中
cp defconfig configs/rk3568_defconfig //用 defconfig 文件替换 rk3568_defconfig,保存配置后,我们可以直接在 U-Boot 源码目录下执行 make.sh 脚本编译 U-Boot 源码,也可以回到 SDK 根目录下执行 build.sh 脚本进行编译:
./make.sh rk3568 //在 U-Boot 源码目录下执行 make.sh 脚本编译
cd ../; ./build.sh uboot //或者在 SDK 根目录下执行 build.sh 脚本编译
0.1.3 kernel 开发
Linux 内核源码在/kernel 目录下:
自己运行./build.sh kernel发现会报错power domain。还是老实使用下面的make.sh
1. 内核编译
- 编译之前,我们先执行“make distclean”清理一下。在内核源码目录下有一个 make.sh 脚本文件,可以直接使用这个脚本文件来编译内核源码,直接运行即可,如下所示:
./make.sh
编译完成后,会生成 boot.img 以及 resource.img,该文件是一个 Android 格式启动镜像(Android bootimg),并非 FIT 格式;事实上,通过 make.sh 脚本编译后生成的 boot.img 确实是 Android 格式镜像;Android格式 boot.img 用于 RK3568 平台 Android 系统,而 FIT 格式 boot.img 才用于 RK3568 平台Linux 系统。
2. 直接通过 make.sh 脚本编译生成的 boot.img 并不是最终使用的 boot.img,可通过如下命令生成 FIT 格式的 boot.img,在kernel目录下,执行命令:
../device/rockchip/common/mk-fitimage.sh kernel/boot.img device/rockchip/rk356x/boot.its
2. kernel 的配置
如果用户想对内核源码进行配置,可执行如下命令打开 menuconfig 图形化配置界面(在内核源码目录下执行):
make ARCH=arm64 rockchip_linux_defconfig //加载配置文件
make ARCH=arm64 menuconfig //打开图形化配置界面
make ARCH=arm64 savedefconfig
cp defconfig arch/arm64/configs/rockchip_linux_defconfig //保存配置后,接下来我们可以回到 SDK 根目录下执行 build.sh 脚本编译内核源码:
cd ../ //回到 SDK 根目录下(当前处于内核源码目录下)
./build.sh kernel //执行 build.sh 脚本编译内核
// 除此之外,还可以在内核源码目录下执行 make.sh 脚本编译(在内核源码目录下执行):
make clean //先清理工程
./make.sh //执行脚本进行编译
../device/rockchip/common/mk-fitimage.sh kernel/boot.img device/rockchip/rk356x/boot.its//生成fit格式的boot.img
0.1.4 buildroot编译
- 编译之前,先进行配置;进入到 buildroot 目录下,执行如下命令进行配置(以 rk3568 平台为例):
source build/envsetup.sh rockchip_rk3568
命令中最后一个参数(rockchip_rk3568)用于指定目标平台的 defconfig 配置文件(不带_defconfig 后缀),所有目标平台的 defconfig 配置文件都存放在/configs 目录下,在该目录下可以找到 rk3568 的配置文件 rockchip_rk3568_defconfig,如下所示:
- 配置完成后,直接执行 make 或“make all”命令编译根文件系统:
sudo make
或者
sudo make all
执行如下命令编译指定的 package:
make <package_name>
编译结果
0.2 镜像烧录
Rockchip 平台提供了多种镜像烧写方式,譬如在 Windows 下通过瑞星微开发工具烧写、通过 SD 卡方式烧写、通过 FactoryTool 工具批量烧写(量产烧写工具、支持 USB 一拖多烧写)以及在 Ubuntu 下通过 Linux_Upgrade_Tool 工具烧写等等;总之,烧写镜像的方式有很多种,用户可以选择合适的烧写方式进行烧写,将镜像文件烧写至开发板。
可在rockdev文件夹中使用命令把所有软连接指向的文件复制到image文件夹中方便复制到windows环境烧录
cp -L ./* ../image && cp ./Mini*bin ../image
0.2.1 Windows 系统下烧写
Windows 下通过瑞芯微开发工具(RKDevTool)来烧写镜像。烧写之前,先将编译 SDK 得到的镜像文件(/rockdev/目录下的镜像)从 Ubuntu 系统拷贝到 Windows 下,譬如将这些镜像拷贝到 Windows 桌面 rk3568_images 目录,包括 boot.img、MiniLoaderAll.bin、
misc.img、oem.img、parameter.txt(分区表文件,不是镜像)、recovery.img、rootfs.img、uboot.img、userdata.img:
0.2.3 分区表 parameter.txt 介绍
RK 平台分区表文件 parameter.txt,该文件是一个 txt 文本文件。parameter.txt文件描述了开发板的分区表信息,每个分区的名字、分区的起始地址以及分区的大小等信息,我们来看下它的内容:
parameter.txt 文件中除了分区表信息之外,还包含其它标识,譬如 FIRMWARE_VER、MACHINE_MODEL、MACHINE_ID、MAGIC 等,详情请参考 RK 官方文档/docs/Common/TOOL/Rockchip_Introduction_Partition_CN.pdf。这里只给大家介绍 mtdparts 标识所定义的分区表信息。
mtdparts 定义的信息如下:
- rk29xxnand 是一个标识,为了兼容性,rockchip 平台都是用 rk29xxnand 做标识。诸如 0x00002000@0x00004000(uboot)、0x00002000@0x00006000(misc)等信息用于定义分区,@符号之前的数值是分区大小,@符号之后的数值是分区的起始位置,括号里面的字符是分区的名字;
- 所有数值的单位都是 sector(扇区),1 个 sector 为 512 字节。所以由此可知,uboot 分区的起始位置为 0x4000 sectors 位置,大小为 0x2000 sectors(4MB);misc 分区的起始位置为0x6000 sectors 位置,大小也是 0x2000 sectors。
- 为了性能,每个分区起始地址需要 32KB(64 sectors)对齐,大小也需要 32KB 的整数倍。
- 最后一个分区需要指定 grow 参数,表示将剩余存储空间全部分配给该分区:
userdata 分区虽然指定了起始位置,但并未指定分区大小,而是使用了“-”来代替,然后在分区名后面加入“:grow”,表示将剩余空间全部分配给 userdata 分区。每个分区的作用如下表所示:
分区名 | 说明 |
---|---|
uboot | 用于存放 uboot.img,uboot.img 镜像会烧录到该分区 |
misc | misc 分区是一个很重要的分区,其中包括 BCB 数据块,主要用于Android/Linux 系统、U-Boot 以及 recovery 之间的通信,misc.img 镜像会烧录到该分区 |
boot | 用于存放 boot.img,boot.img 镜像会烧录到该分区 |
recovery | 用于 recovery 模式,recovery.img 会烧录到该分区,系统引导 recovery.img进入到 recovery 模式。recovery 模式是一种用于对设备进行修复、升级更新的模式。 |
backup | 预留分区,暂时没有使用到 |
rootfs | 根文件系统分区,用于存放 rootfs.img,正常启动模式下的根文件系统镜像rootfs.img 会烧录到该分区 |
oem | 给厂家使用的一个分区,存放厂家的 APP 或数据,oem.img 镜像会烧录到该分区;系统启动之后,该分区会被挂载到根文件系统/oem 目录 |
userdata | 供最终用户使用的分区,存放用户的 APP 或数据;系统启动之后,该分区会被挂载到根文件系统/userdata 目录 |
0.2.4 配置
通过上小节分析可知,parameter.txt 文件中一共定义了 8 个分区,提供了每个分区的名字、起始地址以及分区大小,接下来我们需要手动配
这里需要注意几个点:
1.瑞芯微开发工具中地址数值的单位也是 sectors(扇区,一个扇区等于 512 字节);
2.第一项对应的是 MiniLoaderAll.bin 镜像,**它的烧录地址不用配置,直接使用 0x0 即可,**因为 MiniLoaderAll.bin 镜像有专门的烧录地址,无需用户配置,而且它的名字一般都是 Loader(或小写 loader),不要去改动它;
3.上图中第二项对应的是分区表 parameter.txt,**同样它的地址也不用配置,直接使用 0x0 即可,因为 parameter.txt 文件不会烧录到 Flash 中,但会读取该文件定义的分区、去初始化 Flash物理分区;**同样,它的名字为 parameter(或者大写 Parameter),不要去改动它,因为底层需要通过这个“parameter”名字来识别分区表文件。
4.除了 MiniLoaderAll.bin 和 parameter.txt 稍微特殊一点之外,其它镜像直接根据parameter.txt 分区表定义的起始地址进行配置即可,名字尽量使用 parameter.txt 文件中所定义的分区名。配置完成之后,我们还可以空白处右键点击【导出配置】将这些配置信息导出、保存到一个.cfg 文件中,方便下次直接导入;
将配置信息导出、保存到 config.cfg 文件中,方便下次导入该配置信息。
0.2.5 烧录
开发板先连接好电源适配器以及 OTG,烧写之前,让开发板进入 Maskrom 或 Loader 模式。譬如通过 Maskrom 模式烧写镜像,按住开发板上的 UPDATE 按键,然后给开发板上电或复位,此时设备便会进入 Maskrom 模式(瑞芯微开发工具会提示用户“发现一个 MASKROM
设备”),然后点击“执行”按钮烧录镜像:也可单独烧录某个指定镜像,譬如单独烧录 boot.img 到 boot 分区,只需勾选对应的这一项即可,因为有时我们仅仅只是为了更新某个分区、只需将镜像烧录到该分区替换旧的镜像而已,无需重烧整个系统。:
0.2.6 启动系统
烧录完成后会自动重启开发板,进入 Linux buildroot 系统,串口终端使用波特率1500000连接开发板
0.2.7 烧录 update.img
可以使用瑞芯微开发工具烧录 update.img 固件,首先将生成的 update.img 固件拷贝到 Windows 下,然后通过瑞芯微开发工具将其烧录到开发板,烧录参考系统开发手册 2.9.3 小节。
0.2.2 Linux环境烧录
可以使用 rkflash.sh 脚本烧写,/rkflash.sh是 RK 提供的烧录脚本,我们可以直接使用这个rkflash.sh脚本进行烧录;
当然,这个脚本也是调用了 upgrade_tool 工具执行烧录操作。首先让开发板处于 Maskrom(按住update上电或复位) 或 Loader 模式下,直接运行 rkflash.sh 脚本,即可将/rockdev目录下的镜像烧录到开发板(需要加入 sudo,否则操作会失败!):
sudo ./rkflash.sh
执行上述命令会将 rockdev 目录下的 boot.img、MiniLoaderAll.bin、misc.img、oem.img、recovery.img、rootfs.img、uboot.img、userdata.img 烧写到开发板对应分区。烧录完之后会自动复位开发板。
除了之外,还可单独烧录某个指定镜像,如下表所示:
命令 | 作用 |
---|---|
sudo ./rkflash.sh all | 烧录所有镜像 |
sudo ./rkflash.sh | 烧录所有镜像,与上等价 |
sudo ./rkflash.sh loader | 烧写 Loader(也就是 MiniLoaderAll.bin) |
sudo ./rkflash.sh parameter | 下载分区表 parameter.txt |
sudo ./rkflash.sh uboot | 烧写 uboot.img |
sudo ./rkflash.sh boot | 烧写 boot.img |
sudo ./rkflash.sh recovery | 烧写 recovery.img |
sudo ./rkflash.sh misc | 烧写 misc.img |
sudo ./rkflash.sh oem | 烧写 oem.img |
sudo ./rkflash.sh userdata | 烧写 userdata.img |
sudo ./rkflash.sh rootfs | 烧写根文件系统镜像 rootfs.img |
sudo ./rkflash.sh update | 烧写 update.img 镜像包 |
sudo ./rkflash.sh erase | 擦除 Flash 存储的所有数据 |
0.3 ssh登录
1.上电后串口线接uart接口,波特率1500000,网线连接J5网络即eth1,设置ip地址:
ifconfig eth1 192.168.137.3
2.开发板默认启用ssh,finalshell登录:192.168.137.1,用户密码默认root
1.uboot使用
打开 MobaXterm,设置好串口波特率1500000,最后复位开发板。在 MobaXterm 上出现“Hit key to stop autoboot(‘CTRL+C’):”倒计时的时候按下键盘上“CTRL+C”按键。为了加快系统启动速度,开发板默认是 0 秒倒计时,所以一般很难卡准时间按下按键。所以按下开发板复
位按键的时候,就一直按 CTRL+C,这样就能很轻松的进入到 uboot 的命令行。一般 uboot 在倒计时的时候随便按下键盘上哪个按键都可以进入 uboot 命令行,但是 RK3568 的 uboot 应该是被瑞芯微改过,只能按“CTRL+C”组合键!按其他按键是不能打断启动过程计入命令行的
1.1使用tftp加载镜像文件
- 两个文件放在~/linux/tftpboot文件夹中,配置好网络环境:sudo ifconfig ens33 192.168.137.4
- 开发板重启进入uboot命令界面,网络设置:下面指令尽量手敲,复制有可能提示找不到指令,找不到之后按CTRL+C
setenv ipaddr 192.168.137.3
setenv gatewayip 192.168.137.1
setenv netmask 255.255.255.0
setenv serverip 192.168.137.4
setenv ethaddr 00:11:22:33:44:55
saveenv
其它uboot命令使用可参考STM32MP1驱动学习
1.3 BOOT 操作命令
uboot 的本质工作是引导 Linux,所以 uboot 肯定有相关的 boot(引导)命令来启动 Linux。常用的跟 boot 有关的命令有:boot_fit 和 boot。
1、boot_fit 命令
如果学过 I.MX6U 或者 STM32MP1 的话,应该知道 uboot 使用 bootm 或者 bootz 这两个命令启动内核,需要提供 Linux 编译出来的 zImage 或 uImage 以及设备树文件,然后使用bootm 或 bootz 启动。但是 RK3568 最终的系统烧写文件只有一个 boot.img,Image 和设备树文件全部打包进 boot.img 这一个文件里面,所以就不能用 bootm 或 bootz,要用到 boot_fit 命令。
boot_fit 命令格式如下:
boot_fit [addr]
- addr 是可选的,也就是 boot.img 在 DRAM 中的位置。可以不需要,boot_fit 默认会从相应的 boot 分区里面读取 boot.img 然后解析启动 Linux 内核,比如当我们把系统烧写到 EMMC 里面以后,只需要一个 boot_fit 命令就可以完成 Linux 内核和设备树的提取、启动。
- 如果想要从网络启动系统,那么先把 boot.img 放到 Ubuntu 的 tftpboot 文件夹中,然后用tftp 命令将 boot.img 下载到开发板的 DRAM 中,然后使用 boot_fit 命令启动。首先是要先将boot.img 通过 tftp 下载到合适的 DRAM 地址处,这里需要用到 sysmem_search 命令找到一个合
适的存储起始地址,sysmem_search 命令格式如下:
sysmem_search size
size 就是要获取的内存大小,为十六进制的。一般来说就是 boot.img 大小,但是每次重新编译 Linux 内核以后 boot.img 大小都会变,每次都要获取内存起始地址太麻烦了。我们就直接获取一个 25MB 的空间就行了,所以 25MB=0X1900000,输入如下命令:
sysmem_serach 1900000
结果如图
可以看出,我当前得到的地址为 0XE9EF6400,大家以自己实际得到的地址为准!接下来就是通过 tftp 将 boot.img 下载到 0XE9EF6400 地址处,然后通过 boot_fit 命令启动,整个的命令如下:
tftp E9EF6400 boot.img
boot_fit E9EF6400
命令运行结果如图所示:
2、boot 和 bootd 命令
boot 和 bootd 其实是一个命令,它们最终执行的是同一个函数。为了方便起见,后面就统一使用 boot 命令,此命令也是用来启动 Linux 系统的,只是 boot 会读取环境变量 bootcmd 来启动 Linux 系统,bootcmd 是一个很重要的环境变量!其名字分为“boot”和“cmd”,也就是“引
导”和“命令”,说明这个环境变量保存着引导命令,其实就是多条启动命令的集合,具体的引导命令内容是可以修改的。
RK3568 开发板 bootcmd 默认值如图所示:
可以看出 bootcmd 默认值为“boot_fit;boot_android ${devtype} ${devnum};bootrkp;run distro_bootcmd”,其中 devtype 为 mmc,devnum 为 0,所以简化一下就是“boot_fit;boot_android mmc 0;”。也就是有两种启动内核的方式:boot_fit 和 boot_android,其中 boot_android 在 RK3568里面无效,因此实际有效的只有一个 boot_fit。后面的 bootrkp;run distro_bootcmd 未定义,所以无效。
因此在 RK3568 中,bootcmd 就是直接调用 boot_fit 命令来启动 Linux 系统的。uboot 倒计时结束以后会默认运行 bootcmd 环境变量里面的命令,所以 boot_fit 也就会默认执行。
2.SDK编译
- 正点原子出厂SDK包位置:/home/tao/linux/rk3568/rk3568_linux_sdk/
2.1 SDK自动编译
首先进入到 SDK 源码根目录下,在编译之前先执行如下命令指定 SDK 的板级配置文件:
./build.sh lunch 或者./build.sh BoardConfig-rk3568-atk-evb1-ddr4-v10.mk
build.sh 脚本其实是一个软链接文件,实际指向了 device/rockchip/common/build.sh 文件
可通过执行如下命令查看 build.sh 脚本的使用方法,比如:
./build.sh -h
如果buildroot初次编译时下载文件时间过长,可手动下载后将压缩包放在./buildroot/dl文件夹中
准备工作做完之后接下来便可以编译 SDK 了,进入到 SDK 源码根目录下,执行如下命令编译整个 SDK:
./build.sh all
编译完成后,会生成各种镜像,包括 boot.img、uboot.img、MiniLoaderAll.bin、rootfs.img、recovery.img 等等,但是这些镜像文件散布在各自的源码目录下、不方便用户查找,此时我们可以执行如下命令将它们打包到 SDK/rockdev 目录:
./build.sh firmware
或者直接执行 SDK 源码根目录下的./mkfirmware.sh 脚本(./build.sh firmware 命令其实就是执行了 mkfirmware.sh 脚本):
./mkfirmware.sh
执行完命令后进入到 rockdev 目录下,如下所示:
该目录下的文件基本都是软链接,链接到真正的镜像文件。除了使用“./build.sh all”命令外,我们还可以直接执行“./build.sh”脚本(不带任何参数)来编译整个 SDK;运行“./build.sh”命令会在“./build.sh all”命令的基础上增加如下操作:
1、执行./mkfirmware.sh 将所有镜像打包到 rockdev 目录
2、将 rockdev 目录下的所有镜像打包成一个 update.img 固件
3、复制 rockdev 目录下的镜像到 IMAGE/***_RELEASE_TEST/IMAGES 目录(表示编译日期)
4、保存各个模块的补丁到 IMAGE/_RELEASE_TEST/PATCHES 目录
注:./build.sh 和./build.sh allsave 命令一样
2.2 单独编译
2.2.1. 单独编译 U-Boot
通过 build.sh 脚本单独编译 U-Boot,在 SDK 源码根目录下执行如下命令:
./build.sh uboot
编译成功后,会生成如下两个镜像:
<SDK>/uboot/uboot.img
<SDK>/uboot/rk356x_spl_loader_v1.13.112.bin
rk356x_spl_loader_v1.13.112.bin 其实就是 MiniLoaderAll.bin,只是进行了重命名而已。
2.2.2. 单独编译 Kernel
通过 build.sh 脚本单独编译 Linux Kernel,在 SDK 源码根目录下执行如下命令:
./build.sh kernel
编译成功后会生成 boot.img 镜像,路径为:/kernel/boot.img。
执行“./build.sh kernel”命令会编译 Linux 内核源码,包括内核设备树、内核模块,如果我们需要单独编译内核模块,可以执行如下命令进行编译:
./build.sh modules
当然,编译内核模块之前需要先编译好内核源码
2.2.3. 单独编译 rootfs
rootfs 也就是根文件系统,RK3568 Linux SDK 支持多种根文件系统,包括 buildroot、yocto以及 Debian,通过 build.sh 脚本单独编译 buildroot 根文件系统,在 SDK 源码根目录下执行如下命令:
./build.sh buildroot
* 编译成功后会生 成 buildroot 根文件系统镜像 , 镜像输出在buildroot/output/rockchip_rk3568/images/目录下,编译生成了多个不同格式的 rootfs 镜像文件,对于 RK3568 平台来说,使用 ext4 格式镜像rootfs.ext2,并通常会将其重命名为 rootfs.img。
*除此之外,还可以通过“./build.sh rootfs”命令编译 buildroot 根文件系统,该命令用于编译根文件系统,但不局限于 buildroot,也可以编译 Yocto 以及 Debian;默认情况下编译的是buildroot , 可 以 通 过 环 境 变 量 RK_ROOTFS_SYSTEM 指 定 需 要 编 译 的 rootfs
```shell
export RK_ROOTFS_SYSTEM=buildroot
./build.sh rootfs
需要注意的是:编译根文件系统之前,需提前编译好 Linux 内核;因为编译根文件系统的过程中、也会编译部分未集成在内核源码中的驱动模块(单独提供驱动源码,譬如蓝牙驱动模块 hci_uart.ko)、而且也会将内核源码目录下编译生成的.ko 驱动模块拷贝至根文件系统(譬如
WiFi 驱动模块 8852bs.ko),所以必须先编译好内核。
2.2.4.单独编译 recovery
通过 build.sh 脚本单独编译 recovery,在 SDK 源码根目录下执行如下命令:
./build.sh recovery
recovery.img 用于进入 recovery 模式,该镜像会烧录到开发板 recovery 分区。recovery.img 是由多个镜像合并而成,其中包含 ramdisk(recovery 模式下挂载的根文件系统)、内核镜像、内核 DTB 以及资源镜像 resource.img。所以,在编译 recovery 之前,也必须提
前编译好 Linux 内核。编译成功后, 会生成 recovery.img , 该 镜 像 输 出 在buildroot/output/rockchip_rk356x_recovery/images/目录下
2.2.5 打包成 update.img 镜像
update.img 是多个镜像的集合体(由多个镜像打包合并而成),使用 RK 提供的工具可以将各个分立镜像(譬如 uboot.img、boot.img、MiniLoaderAll.bin、parameter.txt、misc.img、rootfs.img、oem.img、userdata.img、recovery.img 等)打包成一个 update.img 固件,方便用户烧录、升级。我们可以通过如下命令将 rockdev 目录下的各个分立镜像打包成一个 update.img 固件,使用 update.img 固件更加方便烧录、更新!
./build.sh updateimg
打包成功后,会在 rockdev 目录下生成 update.img 固件
2.2.6 SDK 清理
在 SDK 源码根目录下通过 build.sh 脚本可以执行清理操作,执行如下命令:
./build.sh cleanall
执行该命令将会清理 uboot、kernel、buildroot(rootfs、recovery)。
0.2.7 镜像介绍
镜像名称 | 作用 |
---|---|
uboot.img | uboot.img 是一种 FIT 格式镜像,它由多个镜像合并而成,其中包括trust 镜像(ARM Trusted Firmware + OP-TEE OS)、u-boot 镜像、u-boot dtb;编译 U-Boot 时会将这些镜像打包成一个 uboot.img。uboot.img 会烧录到开发板 uboot 分区 |
boot.img | boot.img 也是一种 FIT 格式镜像,它也是由多个镜像合并而成,其中包括内核镜像、内核 DTB、资源镜像 resource.img。 |
boot.img 会烧录到开发板 boot 分区 | |
MiniLoaderAll.bin | 该镜像是运行在 RK3568 平台 U-Boot 之前的一段 Loader 代码(也就是比 U-Boot 更早阶段的 Loader),MiniLoaderAll.bin 由 TPL 和 SPL |
95两部分组成,TPL 用于初始化 DDR,运行在 SRAM;而 SPL 运行在DDR,主要负责加载、引导 uboot.img。 | |
misc.img | 包含 BCB(Bootloader Control Block)信息,该镜像会烧写到开发板misc 分区。misc 分区是一个很重要的分区,其中存放了 BCB 数据块,主要用于 |
Android/Linux 系统、U-Boot 以及 recovery 之间的通信 | |
oem.img | 给厂家使用,用于存放厂家的 APP 或数据,该镜像会烧写至开发板oem 分区,系统启动之后会将其挂载到/oem 目录。 |
parameter.txt | 一个 txt 文本文件,是 RK3568 平台的分区表文件(记录分区名以及每个分区它的起始地址、结束地址);烧写镜像时,并不需要将parameter.txt 文件烧写到 Flash,而是会读取它的信息去定义分区。 |
recovery.img | recovery 模式镜像,recovery.img 用于进入 recovery 模式,recovery.img 会烧录到开发板 recovery 分区。recovery 模式是一种用于对设备进行修复、升级更新的模式。recovery.img 也是 FIT 格式镜像,也是由多个镜像合并而成,其中包括 ramdisk(进入recovery 模式时挂载该根文件系统)、内核镜像(进入 recovery 模式时启动该内核镜像)、内核 DTB 以及resource.img。 |
rootfs.img | 正常启动模式下对应的根文件系统镜像,包含有大量的库文件、可执行文件等。rootfs.img 会烧录到开发板 rootfs 分区 |
userdata.img | 给用户使用,可用于存放用户的 App 或数据;该镜像会烧写至开发板 userdata 分区,系统启动之后,会将其挂载到/userdata 目录 |
3.字符设备驱动编写
- 这里以led灯驱动程序作为字符设备驱动的demo编写
3.1 驱动程序 newchrled.c
- 新建名为“03_newchrled”文件夹,创建好以后新建 newchrled.c 文件,在 newchrled.c 里面输入如下内容:
#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 <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : newchrled.c
作者 : 正点原子
版本 : V1.0
描述 : LED驱动文件。
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2022/12/02 正点原子团队创建
***************************************************************/
#define NEWCHRLED_CNT 1 /* 设备号个数 */
#define NEWCHRLED_NAME "newchrled" /* 名字 */
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */
#define PMU_GRF_BASE (0xFDC20000)
#define PMU_GRF_GPIO0C_IOMUX_L (PMU_GRF_BASE + 0x0010)
#define PMU_GRF_GPIO0C_DS_0 (PMU_GRF_BASE + 0X0090)
#define GPIO0_BASE (0xFDD60000)
#define GPIO0_SWPORT_DR_H (GPIO0_BASE + 0X0004)
#define GPIO0_SWPORT_DDR_H (GPIO0_BASE + 0X000C)
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *PMU_GRF_GPIO0C_IOMUX_L_PI;
static void __iomem *PMU_GRF_GPIO0C_DS_0_PI;
static void __iomem *GPIO0_SWPORT_DR_H_PI;
static void __iomem *GPIO0_SWPORT_DDR_H_PI;
/* newchrled设备结构体 */
struct newchrled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct newchrled_dev newchrled; /* led设备 */
/*
* @description : LED打开/关闭
* @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
* @return : 无
*/
void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON) {
val = readl(GPIO0_SWPORT_DR_H_PI);
val &= ~(0X1 << 0); /* bit0 清零*/
val |= ((0X1 << 16) | (0X1 << 0)); /* bit16 置1,允许写bit0, bit0,高电平*/
writel(val, GPIO0_SWPORT_DR_H_PI);
}else if(sta == LEDOFF) {
val = readl(GPIO0_SWPORT_DR_H_PI);
val &= ~(0X1 << 0); /* bit0 清零*/
val |= ((0X1 << 16) | (0X0 << 0)); /* bit16 置1,允许写bit0,bit0,低电平 */
writel(val, GPIO0_SWPORT_DR_H_PI);
}
}
/*
* @description : 物理地址映射
* @return : 无
*/
void led_remap(void)
{
PMU_GRF_GPIO0C_IOMUX_L_PI = ioremap(PMU_GRF_GPIO0C_IOMUX_L, 4);
PMU_GRF_GPIO0C_DS_0_PI = ioremap(PMU_GRF_GPIO0C_DS_0, 4);
GPIO0_SWPORT_DR_H_PI = ioremap(GPIO0_SWPORT_DR_H, 4);
GPIO0_SWPORT_DDR_H_PI = ioremap(GPIO0_SWPORT_DDR_H, 4);
}
/*
* @description : 取消映射
* @return : 无
*/
void led_unmap(void)
{
/* 取消映射 */
iounmap(PMU_GRF_GPIO0C_IOMUX_L_PI);
iounmap(PMU_GRF_GPIO0C_DS_0_PI);
iounmap(GPIO0_SWPORT_DR_H_PI);
iounmap(GPIO0_SWPORT_DDR_H_PI);
}
/*
* @description : 打开设备
* @param – inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrled; /* 设置私有数据 */
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 newchrled_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 = 0;
int ret;
/* 初始化LED */
/* 1、寄存器地址映射 */
led_remap();
/* 2、设置GPIO0_C0为GPIO功能。*/
val = readl(PMU_GRF_GPIO0C_IOMUX_L_PI);
val &= ~(0X7 << 0); /* bit2:0,清零 */
val |= ((0X7 << 16) | (0X0 << 0)); /* bit18:16 置1,允许写bit2:0,bit2:0:0,用作GPIO0_C0 */
writel(val, PMU_GRF_GPIO0C_IOMUX_L_PI);
/* 3、设置GPIO0_C0驱动能力为level5 */
val = readl(PMU_GRF_GPIO0C_DS_0_PI);
val &= ~(0X3F << 0); /* bit5:0清零*/
val |= ((0X3F << 16) | (0X3F << 0)); /* bit21:16 置1,允许写bit5:0,bit5:0:0,用作GPIO0_C0 */
writel(val, PMU_GRF_GPIO0C_DS_0_PI);
/* 4、设置GPIO0_C0为输出 */
val = readl(GPIO0_SWPORT_DDR_H_PI);
val &= ~(0X1 << 0); /* bit0 清零*/
val |= ((0X1 << 16) | (0X1 << 0)); /* bit16 置1,允许写bit0,bit0,高电平 */
writel(val, GPIO0_SWPORT_DDR_H_PI);
/* 5、设置GPIO0_C0为低电平,关闭LED灯。*/
val = readl(GPIO0_SWPORT_DR_H_PI);
val &= ~(0X1 << 0); /* bit0 清零*/
val |= ((0X1 << 16) | (0X0 << 0)); /* bit16 置1,允许写bit0,bit0,低电平 */
writel(val, GPIO0_SWPORT_DR_H_PI);
/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (newchrled.major) { /* 定义了设备号 */
newchrled.devid = MKDEV(newchrled.major, 0);
ret = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
if(ret < 0) {
pr_err("cannot register %s char driver [ret=%d]\n",NEWCHRLED_NAME, NEWCHRLED_CNT);
goto fail_map;
}
} else { /* 没有定义设备号 */
ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME); /* 申请设备号 */
if(ret < 0) {
pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", NEWCHRLED_NAME, ret);
goto fail_map;
}
newchrled.major = MAJOR(newchrled.devid);/* 获取主设备号 */
newchrled.minor = MINOR(newchrled.devid);/* 获取次设备号 */
}
printk("newcheled major=%d,minor=%d\r\n",newchrled.major, newchrled.minor);
/* 2、初始化cdev */
newchrled.cdev.owner = THIS_MODULE;
cdev_init(&newchrled.cdev, &newchrled_fops);
/* 3、添加一个cdev */
ret = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
if(ret < 0)
goto del_unregister;
/* 4、创建类 */
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
if (IS_ERR(newchrled.class)) {
goto del_cdev;
}
/* 5、创建设备 */
newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
if (IS_ERR(newchrled.device)) {
goto destroy_class;
}
return 0;
destroy_class:
class_destroy(newchrled.class);
del_cdev:
cdev_del(&newchrled.cdev);
del_unregister:
unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);
fail_map:
led_unmap();
return -EIO;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit led_exit(void)
{
/* 取消映射 */
led_unmap();
/* 注销字符设备驱动 */
cdev_del(&newchrled.cdev);/* 删除cdev */
unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);
device_destroy(newchrled.class, newchrled.devid);
class_destroy(newchrled.class);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
3.2 测试程序ledApp.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : ledApp.c
作者 : 正点原子
版本 : V1.0
描述 : chrdevbase驱测试APP。
其他 : 无
使用方法 :./ledtest /dev/led 0 关闭LED
./ledtest /dev/led 1 打开LED
论坛 : www.openedv.com
日志 : 初版V1.0 2022/12/02 正点原子团队创建
***************************************************************/
#define LEDOFF 0
#define LEDON 1
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf[1];
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
/* 打开led驱动 */
fd = open(filename, O_RDWR);
if(fd < 0){
printf("file %s open failed!\r\n", argv[1]);
return -1;
}
databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */
/* 向/dev/led文件写入数据 */
retvalue = write(fd, databuf, sizeof(databuf));
if(retvalue < 0){
printf("LED Control Failed!\r\n");
close(fd);
return -1;
}
retvalue = close(fd); /* 关闭文件 */
if(retvalue < 0){
printf("file %s close failed!\r\n", argv[1]);
return -1;
}
return 0;
}
3.3编译驱动文件
编写 Makefile 文件,内容如下所示:
KERNELDIR := /home/tao/linux/rk3568/rk3568_linux_sdk/kernel#这里是sdk中kernel内核代码的路径
CURRENT_PATH := $(shell pwd)
obj-m := newchrled.o#-m指示生成.ko模块
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
输入如下命令编译出驱动模块文件:
make ARCH=arm64 // **ARCH=arm64 必须指定,否则编译会失败** ,这里和平时编译stm32mp157的程序不同
编译成功以后就会生成一个名为“newchrled.ko”的驱动模块文件。
3.4 编译测试 APP
输入如下命令编译测试 ledApp.c 这个测试程序:
/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc ledApp.c -o ledApp#需要使用绝对路径
编译成功以后就会生成 ledApp 这个应用程序。
3.5运行测试
由于测试demo使用了led0,所以先在开发板中输入如下命令关闭 LED0 的心跳灯功能:
echo none > /sys/class/leds/work/trigger
在 Ubuntu 中将上一小节编译出来的 newchrled.ko 和 ledApp 这两个文件通过 adb 命令发送到开发板的/lib/modules/4.19.232 目录下,命令如下:
adb push newchrled.ko ledApp /lib/modules/4.19.232
或者使用finalShell中拖动文件到开发板的**/lib/modules/4.19.232**目录中
发送成功以后进入到开发板目录 lib/modules/4.19.232 中,输入如下命令加载 newchrled.ko驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe newchrled //加载驱动
驱动加载成功以后会自动在/dev 目录下创建设备节点文件/dev/newchrdev,输入如下命令查看/dev/newchrdev 这个设备节点文件是否存在:
ls /dev/newchrled -l
结果如图 7.6.2.2 所示:
从图 中可以看出,/dev/newchrled 这个设备文件存在,而且主设备号为 236,次设备号为 0,说明设备节点文件创建成功。
驱动节点创建成功以后就可以使用 ledApp 软件来测试驱动是否工作正常,输入如下命令打开 LED 灯:
./ledApp /dev/newchrled 1 //打开 LED 灯
./ledApp /dev/newchrled 0 //关闭 LED 灯
输入上述命令以后观察 ATK-DLRK3568 开发板上的红色 LED 灯是否点亮,如果点亮的话说明驱动工作正常。如果报错permission denied则使用chmod添加权限,输入如下命令关闭 LED 灯:
如果要卸载驱动的话输入如下命令即可:
rmmod newchrled
4.设备树
4.1 设备树device-tree
- rk3568设备树路径**./rk3568_linux_sdk/kernel/arch/arm64/boot/dts/rockchip/rk3568-atk-evb1-ddr4-v10.dtsi**
- 设备树中的节点可在开发板中使用ls /proc/device-tree查看:
- 文件夹中rk3568.dtsi中主要是aliases节点,主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上 label,然后通过&label 来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点。
- rk3568-linux.dtsi中主要是chosen节点,chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重
点是 bootargs 参数。一般.dts 文件中 chosen 节点通常为空或者内容很少:
chosen: chosen {
bootargs = "earlycon=uart8250,mmio32,0xfe660000 console=ttyFIQ0 root=PARTUUID=614e0000-0000 rw rootwait";
};
4.2 节点添加参考文档
Linux 内核源码中有详细的 TXT 文档描述了如何添加节点,这些 TXT 文档叫做绑定文档,路径为:Linux 源码目录/Documentation/devicetree/bindings,如图所示:
比如我们现在要想在 RK3568 这颗 SOC 的 I2C 下添加一个节点,那么就可以查看Documentation/devicetree/bindings/i2c/i2c-rk3x.txt,此文档详细的描述了瑞芯微出品的 SOC 如何在设备树中添加 I2C 设备节点
4.3 设备树常用OF操作函数
设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。比如设备树使用 reg 属性描述了某个外设的寄存器地址为 0X02005482,长度为 0X400,我们在编写驱动的时候需要获取到 reg 属性的0X02005482 和 0X400 这两个值,然后初始化外设。Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 函数。这些 OF 函数原型都定义在 include/linux/of.h 文件中。
4.3.1 查找节点的 OF 函数
设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux 内核使用 device_node 结构体来描述一个节点,此结构体定义在文件 include/linux/of.h 中,定义如下:
51 struct device_node {
52 const char *name; /*节点名字 */
53 phandle phandle;
54 const char *full_name; /*节点全名 */
55 struct fwnode_handle fwnode;
57 struct property *properties; /*属性 */
58 struct property *deadprops; /*removed 属性 */
59 struct device_node *parent; /*父节点 */
60 struct device_node *child; /*子节点 */
61 struct device_node *sibling;
62 #if defined(CONFIG_OF_KOBJ)
63 struct kobject kobj;
64 #endif
65 unsigned long _flags;
66 void *data;
67 #if defined(CONFIG_SPARC)
68 unsigned int unique_id;
69 struct of_irq_controller *irq_trans;
70 #endif
71 };
与查找节点有关的 OF 函数有 5 个,我们依次来看一下。
1、of_find_node_by_name 函数
函数通过节点名字查找指定的节点,函数原型如下:
struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name:要查找的节点名字。
2、of_find_node_by_type 函数
函数通过 device_type 属性查找指定的节点,函数原型如下:
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值。
3、of_find_compatible_node 函数
函数根据 device_type 和 compatible 这两个属性查找指定的节点,函数原型如下:
struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compat)
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值,可以为 NULL,表示忽略掉 device_type 属性。
compat:要查找的节点所对应的 compatible 属性列表。
4、of_find_matching_node_and_match 函数
函数通过 of_device_id 匹配表来查找指定的节点,函数原型如下:
struct device_node *of_find_matching_node_and_match(struct device_node *from,const struct of_device_id *matches,const struct of_device_id **match)
matches:of_device_id 匹配表,也就是在此匹配表里面查找节点。
match:找到的匹配的 of_device_id。
5、of_find_node_by_path 函数
函数通过路径来查找指定的节点,函数原型如下:
inline struct device_node *of_find_node_by_path(const char *path)
函数参数和返回值含义如下:
path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径。
4.3.2 查找父/子节点的 OF 函数
1、of_get_parent 函数
函数用于获取指定节点的父节点(如果有父节点的话),函数原型如下:
struct device_node *of_get_parent(const struct device_node *node)
node:要查找的父节点的节点。
2、of_get_next_child 函数
函数用迭代的查找子节点,函数原型如下:
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)
函数参数和返回值含义如下:
node:父节点。
prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL,表示从第一个子节点开始。
4.3.3 提取属性值的 OF 函数
节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux 内核中使用结构体 property 表示属性,此结构体同样定义在文件 include/linux/of.h 中:
31 struct property {
32 char *name; /* 属性名字 */
33 int length; /* 属性长度 */
34 void *value; /* 属性值 */
35 struct property *next; /* 下一个属性 */
36 #if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
37 unsigned long _flags;
38 #endif
39 #if defined(CONFIG_OF_PROMTREE)
40 unsigned int unique_id;
41 #endif
42 #if defined(CONFIG_OF_KOBJ)
43 struct bin_attribute attr;
44 #endif
45 };
Linux 内核也提供了提取属性值的 OF 函数,我们依次来看一下。
1、of_find_property 函数
函数用于查找指定的属性,函数原型如下:
struct property *of_find_property(const struct device_node *np,const char *name,int *lenp)
np:设备节点。
name: 属性名字。
lenp:属性值的字节数
2、of_property_count_elems_of_size 函数
函数用于获取属性中元素的数量,比如 reg 属性值是一个数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:
int of_property_count_elems_of_size(const struct device_node *np,const char *propname, int elem_size)
np:设备节点。
proname: 需要统计元素数量的属性名字。
elem_size:元素长度。
3、of_property_read_u32_index 函数
函数用于从属性中获取指定标号的 u32 类型数据值(无符号 32位),比如某个属性有多个 u32 类型的值,那么就可以使用此函数来获取指定标号的数据值,此函数原型如下:
int of_property_read_u32_index(const struct device_node *np,const char *propname, u32 index, u32 *out_value)
np:设备节点。
proname: 要读取的属性名字。
index:要读取的值标号。
out_value:读取到的值
4、 of_property_read_u8_array 函数
int of_property_read_u8_array(const struct device_node *np,const char *propname, u8 *out_values, size_t sz)
int of_property_read_u16_array(const struct device_node *np, const char *propname, u16 *out_values, size_t sz)
int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values,size_t sz)
int of_property_read_u64_array(const struct device_node *np, const char *propname, u64 *out_values,size_t sz)
这 4 个函数分别是读取属性中 u8、u16、u32 和 u64 类型的数组数据,比如大多数的 reg 属性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值,分别为 u8、u16、u32 和 u64。
sz:要读取的数组元素数量。
5、of_property_read_u8 函数
int of_property_read_u8(const struct device_node *np, const char *propname,u8 *out_value)
int of_property_read_u16(const struct device_node *np, const char *propname,u16 *out_value)
int of_property_read_u32(const struct device_node *np, const char *propname,u32 *out_value)
int of_property_read_u64(const struct device_node *np, const char *propname,u64 *out_value)
有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用于读取 u8、u16、u32 和 u64 类型属性值
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值。
6、of_property_read_string 函数
函数用于读取属性中字符串值,函数原型如下:
int of_property_read_string(struct device_node *np, const char *propname,const char **out_string)
np:设备节点。
proname: 要读取的属性名字。
out_string:读取到的字符串值。
7、of_n_addr_cells 函数
函数用于获取#address-cells 属性值,函数原型如下:
int of_n_addr_cells(struct device_node *np)
8、of_n_size_cells 函数
函数用于获取#size-cells 属性值,函数原型如下:
int of_n_size_cells(struct device_node *np)
4.3.4 其他常用的 OF 函数
1、of_device_is_compatible 函数
函数用于查看节点的 compatible 属性是否有包含 name 指定的字符串,也就是检查设备节点的兼容性,函数原型如下:
int of_device_is_compatible(const struct device_node *device, const char *name)
2、of_get_address 函数
函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值,函数属性如下:
const __be32 *of_get_address(struct device_node *dev, int index, u64 *size,unsigned int *flags)
dev:设备节点。
index:要读取的地址标号。
size:地址长度。
flags:参数,比如 IORESOURCE_IO、IORESOURCE_MEM 等
返回值:读取到的地址数据首地址,为 NULL 的话表示读取失败。
3、of_translate_address 函数
函数负责将从设备树读取到的物理地址转换为虚拟地址,函数原型如下:
u64 of_translate_address(struct device_node *dev, const __be32 *addr)
in_addr:要转换的地址。
返回值:得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。
4、of_address_to_resource 函数
IIC、SPI、GPIO 等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux内核使用 resource 结构体来描述一段内存空间,“resource”翻译出来就是“资源”,因此用 resource结构体描述的都是设备资源信息
19 struct resource {
20 resource_size_t start;
21 resource_size_t end;
22 const char *name;
23 unsigned long flags;
24 unsigned long desc;
25 struct resource *parent, *sibling, *child;
26 };
对于 32 位的 SOC 来说,resource_size_t 是 u32 类型的。其中 start 表示开始地址,end 表示结束地址,name 是这个资源的名字,flags 是资源标志位,一般表示资源类型,大 家 一 般 最 常 见 的 资 源 标 志 就 是 IORESOURCE_MEM 、 IORESOURCE_REG 和
IORESOURCE_IRQ 等,我们回到 of_address_to_resource 函数,此函数看名字像是从设备树里面提取资源值,但是本质上就是提取 reg 属性值,然后将其转换为 resource 结构体类型,
int of_address_to_resource(struct device_node *dev, int index, struct resource *r)
index:地址资源标号。
r:得到的 resource 类型的资源值。
5、of_iomap 函数
of_iomap 函数用于直接内存映射,以前我们会通过 ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址,不需要使用 ioremap 函数了。当然了,你也可以使用 ioremap 函数来完成物理地址到虚拟地址的内存映射,只是在采用设备树以后,大部分的驱动都使用 of_iomap 函数了。of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段,of_iomap 函数原型如下:
void __iomem *of_iomap(struct device_node *np, int index)
index:reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
返回值:经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。
5.设备树下的 LED 驱动实验
5.1 修改设备树文件
在根节点“/”下创建一个名为“rk3568_led”的子节点,打开 rk3568-atk-evb1-ddr4-v10.dtsi文件,在根节点“/”最后面输入如下所示内容:
rk3568_led {
compatible = "atkrk3568-led";
status = "okay";
reg = <0x0 0xFDC20010 0x0 0x08 /* PMU_GRF_GPIO0C_IOMUX_L */
0x0 0xFDC20090 0x0 0x08 /* PMU_GRF_GPIO0C_DS_0 */
0x0 0xFDD60004 0x0 0x08 /* GPIO0_SWPORT_DR_H */
0x0 0xFDD6000C 0x0 0x08 >;/* GPIO0_SWPORT_DDR_H */
};
第 2 行,属性 compatible 设置 rk3568_led 节点兼容为“atkrk3568-led”。
第 3 行,属性 status 设置状态为“okay”。
第 4~7 行,reg 属性,非常重要!reg 属性设置了驱动里面所要使用的寄存器物理地址,比如第 4 行的“0x0 0xFDC20010 0x0 0x08”表示 RK3568 的 PMU_GRF_GPIO0C_IOMUX_L 寄存器,其中寄存器地址为 0xFDC20010,长度为 8 个字节。设备树修改完成以后在 SDK 顶层目录输入如下命令重新编译一下内核:
./make.sh && ../device/rockchip/common/mk-fitimage.sh kernel/boot.img device/rockchip/rk356x/boot.its
#./build.sh kernel这个命令会报错电源域有问题,用上面的命令,参照0.1.2kernel开发部分
编译完成以后在kernel文件夹中得到boot.img, 文件夹中 zboot.img 就是编译出来的内核+设备树打包在一起的文件。
烧写方法很简单,分两步:
1、开发板进入 LOADER 模式,前提是开发板已经烧写了 uboot,并且 uboot 正常运行。
2、打开 RKDevTool 工具,导入配置文件,然后将 boot.img 放到要烧写的目录,然后只烧写 boot.img
烧写完成以后启动开发板。Linux 启动成功以后进入到/proc/device-tree/目录中查看是否有“rk3568_led”这个节点,结果如图所示:
可以进入到图中的 rk3568_led 目录中,查看一下都有哪些属性文件,结果如图所示:
可以用 cat 命令查看一下 compatible、status 等属性值是否和我们设置的一致。
5.2 驱动程序
设备树准备好以后就可以编写驱动程序了。新建名为“04_dtsled”文件夹,创建好以后新建 dtsled.c 文件,在 dtsled.c 里面输入如下内容:
#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/of.h>
#include <linux/of_address.h>
//#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : dtsled.c
作者 : 正点原子
版本 : V1.0
描述 : LED驱动文件。
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2022/12/06 正点原子团队创建
***************************************************************/
#define DTSLED_CNT 1 /* 设备号个数 */
#define DTSLED_NAME "dtsled" /* 名字 */
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *PMU_GRF_GPIO0C_IOMUX_L_PI;
static void __iomem *PMU_GRF_GPIO0C_DS_0_PI;
static void __iomem *GPIO0_SWPORT_DR_H_PI;
static void __iomem *GPIO0_SWPORT_DDR_H_PI;
/* dtsled设备结构体 */
struct dtsled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
};
struct dtsled_dev dtsled; /* led设备 */
/*
* @description : LED打开/关闭
* @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
* @return : 无
*/
void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON) {
val = readl(GPIO0_SWPORT_DR_H_PI);
val &= ~(0X1 << 0); /* bit0 清零*/
val |= ((0X1 << 16) | (0X1 << 0)); /* bit16 置1,允许写bit0, bit0,高电平*/
writel(val, GPIO0_SWPORT_DR_H_PI);
}else if(sta == LEDOFF) {
val = readl(GPIO0_SWPORT_DR_H_PI);
val &= ~(0X1 << 0); /* bit0 清零*/
val |= ((0X1 << 16) | (0X0 << 0)); /* bit16 置1,允许写bit0,bit0,低电平 */
writel(val, GPIO0_SWPORT_DR_H_PI);
}
}
/*
* @description : 取消映射
* @return : 无
*/
void led_unmap(void)
{
/* 取消映射 */
iounmap(PMU_GRF_GPIO0C_IOMUX_L_PI);
iounmap(PMU_GRF_GPIO0C_DS_0_PI);
iounmap(GPIO0_SWPORT_DR_H_PI);
iounmap(GPIO0_SWPORT_DDR_H_PI);
}
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &dtsled; /* 设置私有数据 */
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @aram - 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 dtsled_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 = 0;
int ret;
u32 regdata[16];
const char *str;
struct property *proper;
/* 获取设备树中的属性数据 */
/* 1、获取设备节点:rk3568_led */
dtsled.nd = of_find_node_by_path("/rk3568_led");
if(dtsled.nd == NULL) {
printk("rk3568_led node not find!\r\n");
goto fail_find_node;
} else {
printk("rk3568_led node find!\r\n");
}
/* 2、获取compatible属性内容 */
proper = of_find_property(dtsled.nd, "compatible", NULL);
if(proper == NULL) {
printk("compatible property find failed\r\n");
} else {
printk("compatible = %s\r\n", (char*)proper->value);
}
/* 3、获取status属性内容 */
ret = of_property_read_string(dtsled.nd, "status", &str);
if(ret < 0){
printk("status read failed!\r\n");
} else {
printk("status = %s\r\n",str);
}
/* 4、获取reg属性内容 */
ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 16);
if(ret < 0) {
printk("reg property read failed!\r\n");
} else {
u8 i = 0;
printk("reg data:\r\n");
for(i = 0; i < 16; i++)
printk("%#X ", regdata[i]);
printk("\r\n");
}
/* 初始化LED */
/* 1、寄存器地址映射 */
PMU_GRF_GPIO0C_IOMUX_L_PI = of_iomap(dtsled.nd, 0);
PMU_GRF_GPIO0C_DS_0_PI = of_iomap(dtsled.nd, 1);
GPIO0_SWPORT_DR_H_PI = of_iomap(dtsled.nd, 2);
GPIO0_SWPORT_DDR_H_PI = of_iomap(dtsled.nd, 3);
/* 2、设置GPIO0_C0为GPIO功能。*/
val = readl(PMU_GRF_GPIO0C_IOMUX_L_PI);
val &= ~(0X7 << 0); /* bit2:0,清零 */
val |= ((0X7 << 16) | (0X0 << 0)); /* bit18:16 置1,允许写bit2:0, bit2:0:0,用作GPIO0_C0 */
writel(val, PMU_GRF_GPIO0C_IOMUX_L_PI);
/* 3、设置GPIO0_C0驱动能力为level5 */
val = readl(PMU_GRF_GPIO0C_DS_0_PI);
val &= ~(0X3F << 0); /* bit5:0清零*/
val |= ((0X3F << 16) | (0X3F << 0)); /* bit21:16 置1,允许写bit5:0,bit5:0:0,用作GPIO0_C0 */
writel(val, PMU_GRF_GPIO0C_DS_0_PI);
/* 4、设置GPIO0_C0为输出 */
val = readl(GPIO0_SWPORT_DDR_H_PI);
val &= ~(0X1 << 0); /* bit0 清零*/
val |= ((0X1 << 16) | (0X1 << 0)); /* bit16 置1,允许写bit0,bit0,高电平 */
writel(val, GPIO0_SWPORT_DDR_H_PI);
/* 5、设置GPIO0_C0为低电平,关闭LED灯。*/
val = readl(GPIO0_SWPORT_DR_H_PI);
val &= ~(0X1 << 0); /* bit0 清零*/
val |= ((0X1 << 16) | (0X0 << 0)); /* bit16 置1,允许写bit0,bit0,低电平 */
writel(val, GPIO0_SWPORT_DR_H_PI);
/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (dtsled.major) { /* 定义了设备号 */
dtsled.devid = MKDEV(dtsled.major, 0);
ret = register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME);
if(ret < 0) {
pr_err("cannot register %s char driver [ret=%d]\n",DTSLED_NAME, DTSLED_CNT);
goto fail_devid;
}
} else { /* 没有定义设备号 */
ret = alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME); /* 申请设备号 */
if(ret < 0) {
pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", DTSLED_NAME, ret);
goto fail_devid;
}
dtsled.major = MAJOR(dtsled.devid); /* 获取分配号的主设备号 */
dtsled.minor = MINOR(dtsled.devid); /* 获取分配号的次设备号 */
}
printk("dtsled major=%d,minor=%d\r\n",dtsled.major, dtsled.minor);
/* 2、初始化cdev */
dtsled.cdev.owner = THIS_MODULE;
cdev_init(&dtsled.cdev, &dtsled_fops);
/* 3、添加一个cdev */
ret = cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);
if(ret < 0)
goto del_unregister;
/* 4、创建类 */
dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);
if (IS_ERR(dtsled.class)) {
goto del_cdev;
}
/* 5、创建设备 */
dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME);
if (IS_ERR(dtsled.device)) {
goto destroy_class;
}
return 0;
destroy_class:
class_destroy(dtsled.class);
del_cdev:
cdev_del(&dtsled.cdev);
del_unregister:
unregister_chrdev_region(dtsled.devid, DTSLED_CNT);
fail_devid:
led_unmap();
fail_find_node:
return -EIO;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit led_exit(void)
{
/* 取消映射 */
led_unmap();
/* 注销字符设备驱动 */
cdev_del(&dtsled.cdev);/* 删除cdev */
unregister_chrdev_region(dtsled.devid, DTSLED_CNT); /* 注销设备号 */
device_destroy(dtsled.class, dtsled.devid);
class_destroy(dtsled.class);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
dtsled.c 文件中的内容和第七章的 newchrled.c 文件中的内容基本一样,只是 dtsled.c 中包含了处理设备树的代码,我们重点来看一下这部分代码。
第 45 行,在设备结构体 dtsled_dev 中添加了成员变量 nd,nd 是 device_node 结构体类型指针,表示设备节点。如果我们要读取设备树某个节点的属性值,首先要先得到这个节点,一般在设备结构体中添加 device_node 指针变量来存放这个节点。
第 176~182 行,通过 of_find_node_by_path 函数得到 rk3568_led 节点,后续其他的 OF 函数要使用 device_node。
第 185~190 行,通过 of_find_property 函数获取 rk3568_led 节点的 compatible 属性,返回值为 property 结构体类型指针变量,property 的成员变量 value 表示属性值。
第 193~198 行,通过 of_property_read_string 函数获取 rk3568_led 节点的 status 属性值。
第 201~210 行,通过 of_property_read_u32_array 函数获取 rk3568_led 节点的 reg 属性所有值,并且将获取到的值都存放到 regdata 数组中。第 208 行将获取到的 reg 属性值依次输出到终端上。
第 214~217 行,使用 of_iomap 函数一次性完成读取 reg 属性以及内存映射,of_iomap 函数是设备树推荐使用的 OF 函数。
5.3 编写测试 APP
本章直接使用上一章的 ledApp.c 文件复制到本章实验工程下即可。
1、编译驱动程序
编写 Makefile 文件,本章实验的 Makefile 文件和上一章实验基本一样,只是将 obj-m 变量的值改为 dtsled.o
输入如下命令编译出驱动模块文件:
make ARCH=arm64 //ARCH=arm64 必须指定,否则编译会失败
编译成功以后就会生成一个名为“dtsled.ko”的驱动模块文件。
2、编译测试 APP
输入如下命令编译测试 ledApp.c 这个测试程序:
/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc ledApp.c -o ledApp
编译成功以后就会生成 ledApp 这个应用程序。先在开发板中输入如下命令关闭 LED 的心跳灯功能:
echo none > /sys/class/leds/work/trigger
在 Ubuntu 中将上一小节编译出来的 dtsled.ko 和 ledApp 这两个文件通过 adb 命令或者scp发送到开发板的/lib/modules/4.19.232 目录下,命令如下:
adb push dtsled.ko ledApp /lib/modules/4.19.232
发送成功以后进入到开发板目录 lib/modules/4.19.232 中,输入如下命令加载 dtsled.ko 驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe dtsled //加载驱动
./ledApp /dev/dtsled 1 //打开 LED 灯
./ledApp /dev/dtsled 0 //关闭 LED 灯
rmmod dtsled.ko///卸载驱动
问题
1. buildroot 中make时找不到库libmpfr.so.4
sudo ln -s /usr/lib/x86_64-linux-gnu/libmpfr.so.6 /usr/lib/x86_64-linux-gnu/libmpfr.so.4