前言:
i.MX8M Plus 开发板是一款拥有 4 个 Cortex-A53 核心,运行频率 1.8GHz;1 个 Cortex-M7 核心,运行频率 800MHz;此外还集成了一个 2.3 TOPS 的 NPU,大大加速机器学习推理。
全文所使用的开发平台均为与NXP官方合作的FS-IMX8MPCA开发板(华清远见imx8mp开发板),支持Weston、ubuntu20.04、Android11 等操作系统;同时支持 Xenomai 硬实时内核、EtherCAT 总线、TSN 时间敏感网络、ROS1.0、ROS2.0 等工业与机器人领域应用;可以用于工业互联网、人工智能、边缘计算、多屏异显等应用方向。华清远见研发中心编写了大量开发教程并录制了丰富视频教学资源免费提供给大家!
开发板更多资料可关注华清远见在线实验室领取~~~
Linux 系统开发
TF-A 编译
配置交叉编译工具链
在进行源码编译之前需要先导入交叉编译工具链,之前章节已经介绍过具体安装过程,这里不再赘述,如果还没有安装可参考《交叉编译工具链安装》章节
linux@ubuntu:$ source /opt/fsl-imx-xwayland/5.4-zeus/environment-setup-aarch64-poky-lin
ux
linux@ubuntu:$ $CC --version
编译 TF-A 编译
将当前工作目录切换到 TF-A 源码目录下,这里为“~/workdir/imx8mp/imx-yocto-bsp/bsp
_source/imx-atf”
linux@ubuntu:$ cd ~/workdir/imx8mp/imx-yocto-bsp/bsp_source/imx-atf
编译之前可以先清除之前的缓存
linux@ubuntu:$ make clean PLAT=imx8mp
编译
linux@ubuntu:$ LDFLAGS="" make PLAT=imx8mp
编译成功后在 build/imx8mp/release/目录下生成相关镜像
Bootloader 的编译与运行
配置交叉编译工具链
在进行源码编译之前需要先导入交叉编译工具链,之前章节已经介绍过具体安装过程,
这里不再赘述,如果还没有安装可参考《交叉编译工具链安装》章节
linux@ubuntu:$ source /opt/fsl-imx-xwayland/5.4-zeus/environment-setup-aarch64-poky-lin
ux
linux@ubuntu:$ $CC --version
Bootloader编译
将当前工作目录切换到 bootloader 源码目录下,这里为“~/workdir/imx8mp/bsp_source/u
boot-imx”
linux@ubuntu:$ cd ~/workdir/imx8mp/bsp_source/uboot-imx
⚫ 配置
linux@ubuntu:$ make imx8mp_ai_robot_defconfig
⚫ 编译
linux@ubuntu:$ make
编译成功后在 spl 目录下生成 u-boot-spl.bin 相关镜像,在根目录下生成 u-boot.img 镜像
制作 imx-boot
在我们前面编译的标准 u-boot 无法自动启动设备,imx8mp 需要通过 imx-mkimage 构建
imx-boot。
imx-boot 镜像包括 U-boot、tf-a、uboot spl 和 ddr 固件。因此我们需要将前面编译的 uboot-imx 镜像和 imx-atf 以及 ddr 固件复制到 imx-boot/iMX8M 目录下来制作 imx-boot 镜像。
linux@ubuntu:$ cd ~/workdir/imx8mp/bsp_source/imx-boot
⚫ 复制 u-boot 镜像
linux@ubuntu:$ cp ../u-boot-imx/u-boot-nodtb.bin iMX8M/
linux@ubuntu:$ cp ../u-boot-imx/spl/u-boot-spl.bin iMX8M/
linux@ubuntu:$ cp ../u-boot-imx/tools/mkimage iMX8M/mkimage_uboot
linux@ubuntu:$ cp ../u-boot-imx/arch/arm/dts/imx8mp-ai-robot.dtb iMX8M/
⚫ 复制 DDR 固件
linux@ubuntu:$ cp ../firmware-imx-8.10/firmware/ddr/synopsys/ddr4_*_202006* iMX8M/
⚫ 复制 tf-a 镜像
linux@ubuntu:$ cp ../imx-atf/build/imx8mp/release/bl31.bin iMX8M/
⚫ 编译生成 imx-boot
linux@ubuntu:$ make SOC=iMX8MP flash_ai_robot
编译成功之后会在 iMX8M 目录下生成 flash.bin 文件,使用此文件通过 uuu 工具烧录到
开发板便可使用自己编译的 u-boot 和 tf-a 镜像启动开发板。
dos@windows:$ uuu -b emmc flash.bin
u-boot 常用命令介绍
linux 系统环境变量
linux 下的环境变量可以通过在 u-boot 阶段进行设置,在 u-boot 启动过程中倒计时结束之
前按任意键停在 u-boot 进行环境变量设置。
如果要查看当前环境变量可以使用“print”命令
如果想要恢复至出厂时的环境变量可以使用“env default -f -a”命令进行重置
如果想要保存当前的环境变量可以使用“saveenv”进行保存。
a) 设置 linux 内核加载地址
⚫ linux 内核加载地址
u-boot=> setenv loadaddr 0x80080000
⚫ 设备树加载地址
u-boot=> setenv fdtaddr 0x80f00000
⚫ 设备树文件名称
u-boot=> setenv fdt_file imx8mp-evk.dtb
b) 显示设置
⚫ HDMI 显示
u-boot=> setenv fdt_file imx8mp-ai-robot.dtb
⚫ LVDS 和 HDMI 双屏显示
u-boot=> setenv fdt_file imx8mp-ai-robot-lvds070.dtb
⚫ MIPI 和 HDMI 双屏显示
u-boot=> setenv fdt_file imx8mp-ai-robot-mipi070.dtb
⚫ LVDS、MIPI 和 HDMI 三屏显示
u-boot=> imx8mp-ai-robot-mipi-lvds-dual.dts
c) 设置跟文件系统位置
⚫ 通过 NFS 挂载
u-boot=> setenv rootfsinfo 'root=/dev/nfs ip=dhcp nfsroot=${serverip}:${nfsroot},v3,tcp'
u-boot=> setenv rootfsinfo 'root=/dev/nfs ip=dhcp weim-nor nfsroot=${serverip}:${nfsroo
t},v3,tcp'
⚫ 通过 eMMC 挂载
u-boot=> setenv rootfsinfo 'root=/dev/mmcblk0p2 rootwait rw' /* eMMC */
常用命令介绍
uboot 命令类似于 linux 行缓冲命令行,当我们向终端命令行输入命令的时候,这些命令没有立即被系统识别,而是被缓冲到一个缓存区(也就是系统认为我换没有输入完),当我们按下回车键(换行)后,系统就认为输入完了,然后将缓冲区中所有刚才输入的命令拿去处理。
⚫ 第一个命令:printenv/print
printenv 命令不用带参数,作用是打印出系统中所有的环境变量。
printenv 环境变量名 查看指定的环境变量值。
⚫ 常用环境变量
bootdelay uboot 启动后,倒计时多少秒后自动执行环境变量 bootcmd 的语句
bootcmd 倒计时到 0 后,自动执行里面的语句
bootargs 是用于提供给内核的启动参数语句
ipaddr 当前开发板的 IP 地址
⚫ 设置添加/更改环境变量:setenv/set
用法:set name value
例如:set bootdelay 3
⚫ 保存环境变量的更改:saveenv/save
saveenv/save 作用是将内存中的环境变量的值同步保存到 flash 中环境变量的分区
⚫ 网络测试指令:ping
用法:ping IP 地址
此命令需要先设置好开发板的网络环境,包括网关,子网掩码,本机 IP 地址。之后才可以使用该命令。
⚫ tftp 下载命令
用于通过 tftp 协议从 tftp 服务器上下载文件。
eg: tftp 0x40480000 Image 将 tftp 服务器上的 Image 文件下载到本地内存的
0x40480000 地址处。
⚫ 内存操作指令:mw、md
md 内存地址 用于查看内存地址上的值
md.b 0x40008000 100 从内存地址 0x40008000 开始,查看 0x100 个字节并输出值
md.w 0x40008000 100 从内存地址 0x40008000 开始,查看 0x100 个 16 位值并输出值
md.l 0x40008000 100 从内存地址 0x40008000 开始,查看 0x100 个 32 位值并输出值
mw 用于修改内存地址上的值
mw.b x40008000 0xab 100 从内存地址 0x40008000 开始的 0x100 字节空间,设值为 0xab
mw.w 0x40008000 0xabcd 100 从内存地址 0x40008000 开始的 0x200 字节空间,每 16 位值设为 0xabcd
mw.l 0x40008000 0xabcdef88 100 从内存地址 0x40008000 开始的 0x400字节空间,每 32 位值设为 0xabcdef88
⚫ 帮助指令
上面介绍的指令为 bootloader 中常用的一些指令介绍,如果需要使用其它指令,可以通
过 help 命令来查看具体描述及用法。
Linux 内核源码编译
配置交叉编译工具链
在进行源码编译之前需要先导入交叉编译工具链,之前章节已经介绍过具体安装过程,
这里不再赘述,如果还没有安装可参考《交叉编译工具链安装》章节
linux@ubuntu:$ source /opt/fsl-imx-xwayland/5.4-zeus/environment-setup-aarch64-poky-lin
ux
linux@ubuntu:$ $CC --version
linux 编译
将当前工作目录切换到 linux 内核源码目录下,这里为“~/workdir/imx8mp/imx-yocto-bsp
/bsp_source/ linux-imx”
linux@ubuntu:$ cd ~/workdir/imx8mp/imx-yocto-bsp/bsp_source/linux-imx
添加 nxp 官方标准 imx_v8 配置
linux@ubuntu:$ make imx_v8_defconfig
添加自定义配置
linux@ubuntu:$ ./scripts/kconfig/merge_config.sh -m .config arch/arm64/configs/aicar.config
添加完配置后我们可以通过 menuconfig 进行内核配置和裁剪
linux@ubuntu:$ make menuconfig
编译内核文件
linux@ubuntu:$ make
编译成功后在 arch/arm64/boot/目录下生成 Image 内核镜像,在arch/arm64/boot/dts/freescale/目录下生成设备树镜像
单独编译设备树文件
linux@ubuntu:$ make dtbs
编译成功后在 arch/arm64/boot/dts/freescale/目录下生成设备树镜像
通过 tftp 服务器更新内核镜像
上面章节已经将 VMware 的桥接网络配置好了,本小节将要通过 TFTP 服务器来下载内核及设备树,我们分为两部分介绍,第一部分通过网线直连电脑的方式进行验收,第二部分通过路由器连接开发板和电脑。
网线直连电脑
这里我们采用的是直连网络,即开发板通过网线直连电脑。
首先启动 Ubuntu 虚拟机,由于我们采用直连方式,所以虚拟机无法自动获取到 IP 地址,
需要我们手动设置 IP 地址。
打开虚拟机的“/etc/network/interfaces”文件
linux@ubuntu:$ sudo vi /etc/network/interfaces
添加如下配置
auto ens33
iface ens33 inet static
address 192.168.100.240
netmask 255.255.255.0
gateway 192.168.100.1
dns-nameserver 192.168.100.1
这里“ens33”代表网卡名,可以通过 ifconfig 命令查看;
address 为要设置的静态 IP 地址;
netmask 为子网掩码
gateway 为网关地址
设置完成后重启网络服务
linux@ubuntu:$ sudo reboot
设置完成后使用 ifconfig 命令查看当前 ubuntu 的 IP 地址。
在开发板上电之前需将调试串口、网线接入网口 1。
之后给开发板上电,使程序停留在 u-boot 终端。
这里我们需要设置几个与网络相关的环境变量,以支持网络传输。
在上面我们已经设置好了 ubuntu 的网络配置,这里 ubuntu 充当 TFTP 服务器。我们需要
按照 ubuntu 的配置来设置开发板的环境变量。
设置服务器 IP 地址(ubuntu ip)
u-boot=> setenv serverip 192.168.100.240
设置本机 IP 地址(开发板 ip)
u-boot=> setenv ipaddr 192.168.100.241
设置网关
u-boot=> setenv gatewayip 192.168.100.1
设置子网掩码
u-boot=> setenv netmask 255.255.255.0
设置网卡 1 MAC 地址
u-boot=> setenv eth1addr 00:04:9f:07:0b:a5
保存环境变量
u-boot=> saveenv
设置完成后可以使用 ping 命令来进行测试
u-boot=> ping 192.168.100.240
下载 linux 内核与设备树
在进行内核下载之前,首先需要确认 TFTP 服务器是否已经安装完毕,如果还没有安装则需要根据前面章节的内容进行安装,此外还需要将之前编译好的 Image linux 内核程序与设备树文件 imx8mp-ai-robot.dtb 放入 TFTP 服务器,如果是按照前面教程搭建路径为【/tftpboot/】
下面在 u-boot 中设置要下的镜像名称和设备树名称
u-boot=> setenv image Image
u-boot=> setenv fdt_file imx8mp-ai-robot.dtb
u-boot=> saveenv
如果希望将下载下来的镜像文件存储到指定的存储设备,例如外部 sdcard 或者 eMMC,
则需要设置当前存储设备。
保存到 eMMC,在开发板执行如下指令
u-boot=> mmc dev 2 0
保存到 SDcard,在开发板执行如下指令
u-boot=> mmc dev 1 0
下载 linux 内核文件
u-boot=> tftpboot ${loadaddr} ${image}
下载设备树文件
u-boot=> tftpboot ${fdt_addr} ${fdt_file}
启动程序
u-boot=> run mmcargs
u-boot=> booti ${loadaddr} - ${fdt_addr}
通过路由器连接电脑
上小节讲述了如何通过网路直连进行 TFTP 服务器的数据传输,本小节则讲述如何通过路由器连接。
通过路由器连接与直连其实差异并不大,只是通过路由器可以使用 DHCP 服务器,进行自动 IP 地址获取。
打开虚拟机的“/etc/network/interfaces”文件
linux@ubuntu:$ sudo vi /etc/network/interfaces
添加如下配置
auto ens33
iface ens33 inet dhcp
这里将“ens33”设置成了通过 DHCP 自动获取 IP 地址
设置完成后重启网络服务
linux@ubuntu:$ sudo reboot
设置完成后使用 ifconfig 命令查看当前 ubuntu 的 IP 地址。
首先启动开发板,使程序停留在 u-boot 终端。
这里我们需要设置几个与网络相关的环境变量,以支持网络传输。
在上面我们已经设置好了 ubuntu 的网络配置,这里 ubuntu 充当 TFTP 服务器。我们需要按照 ubuntu 的配置来设置开发板的环境变量。
设置服务器 IP 地址(ubuntu ip)
u-boot=> setenv serverip 192.168.101.47
设置网卡 1 MAC 地址
u-boot=> setenv eth1addr 00:04:9f:07:0b:a5
保存环境变量
u-boot=> saveenv
下载 linux 内核与设备树
在进行内核下载之前,首先需要确认 TFTP 服务器是否已经安装完毕,如果还没有安装则需要根据前面章节的内容进行安装,此外还需要将之前编译好的 Image linux 内核程序与设备树文件 imx8mp-ai-robot.dtb 放入 TFTP 服务器,如果是按照前面教程搭建路径为【/tftpboot/】
下面在 u-boot 中设置要下的镜像名称和设备树名称
u-boot=> setenv image Image
u-boot=> setenv fdt_file imx8mp-ai-robot.dtb
u-boot=> saveenv
这里与直连网路不同的是,不再需要指定本机 ip、网关、子网掩码等环境变量,这些信
息只需要通过 dhcp 获取即可
下载 linux 内核文件
u-boot=> dhcp ${loadaddr} ${image}
下载设备树文件
u-boot=> dhcp ${fdt_addr} ${fdt_file}
启动程序
u-boot=> run mmcargs
u-boot=> booti ${loadaddr} - ${fdt_addr}
基于 busybox 的最小文件系统制作
busybox 源码编译及安装
可以从 http://busybox.net/downloads/网站下载 busybox-1.29.3 源码用于制作 Linux 文件系
统,为了方便,已将源码放进了光盘。
安装交叉编译工具链。
linux@ubuntu:$ sudo apt-get install gcc-aarch64-linux-gnu
linux@ubuntu:$ sudo apt-get install g++-aarch64-linux-gnu
linux@ubuntu:$ sudo apt-get install libncurses5-dev libncursesw5-dev
验证开发工具是否安装正确,显示版本信息如下图所示。
linux@ubuntu:$ aarch64-linux-gnu-gcc -v
建立源码目录
将【华清远见-I.MX8M Plus 开发资料-2021-06-02\程序源码\busybox】下的 busybox-1.29.
3.tar.bz2 拷贝至该目录。
linux@ubuntu:$ tar xvf busybox-1.29.3.tar.bz2 //解压源码
linux@ubuntu:$ cd busybox-1.29.3
配置 busybox 源码:
将顶层目录下的 Makefile 文件中的 CROSS_COMPILE 字段修改为“aarch64-linux-gnu-”
可以使用如下命令配置源码
linux@ubuntu:$ make ARCH=arm64 menuconfig
选择退出,选择保存
编译源码:
linux@ubuntu:$ make
安装:
busybox 默认安装路径为源码目录下的_install
linux@ubuntu:$ make install
进入安装目录可看到如下目录:
linux@ubuntu:$ cd _install
linux@ubuntu:$ ls
bin linuxrc sbin usr
添加主要系统启动文件
创建其他需要的目录:
linux@ubuntu:$ cd _install
linux@ubuntu:$ mkdir dev etc mnt proc var tmp sys root
添加库:
将工具链中的库拷贝到_install 目录下:
linux@ubuntu:$ cp –a /usr/aarch64-linux-gnu/lib/ .
删除静态库:
linux@ubuntu:$ rm lib/*.a
添加系统启动文件:
在 etc 下添加文件 inittab,文件内容如下:
注意:修改文件均为_install 目录下
etc/inittab
这里我们挂载的文件系统有三个 proc、sysfs 和 tmpfs。
回到创建的文件系统处,在 etc 下创建 init.d 目录,并在 init.d 下创建 rcS 文件,rcS 文件内容为:etc/init.d/rcS
为 rcS 添加可执行权限:
linux@ubuntu:$ chmod a+x init.d/rcS
在 etc 下添加 profile 文件,文件内容为:etc/profile
此时一个最小文件系统就制作完成了,可以将_install 目录下的文件复制到/source/rootfs 目
录下用于 NFS 挂载。
linux@ubuntu:$ cp * /source/rootfs -a 当前路径为_install
通过 NFS 挂载文件系统
NFS 方式是开发板通过 NFS 挂载放在主机(PC )上的根文件系统。此时在主机在文件系统中进行的操作同步反映在开发板上;反之,在开发板上进行的操作同步反映在主机中的根文件系统上。实际工作中,我们经常使用 NFS 方式挂载系统,这种方式对于系统的调试非常便。
在进行本章实验前需首先确保 NFS 服务已经按照前面章节安装成功。本实验与 TFTP 实验相同,也分为直连和通过路由器两个部分。其中 ip 地址设置部分参考《通过 TFTP 服务器下载内核》章节设置即可,这里不在赘述。
在通过 NFS 启动网路文件系统之前,首先要确保 ubuntu 虚拟机目录下【/source/】存在
rootfs 文件系统,如果不存在该文件,则需要在【华清远见-I.MX8M Plus 开发资料-2021-06-
02\程序源码/文件系统源码】目录下提取 rootfs.tar.xz 压缩包,解压到/source/目录下,解压完成之后会在/source/目录下生成 rootfs 目录。
在开发板上电之前需将调试串口、网线接入网口 1。
配置好环境之后启动开发板,在使程序停留在 u-boot 终端。
使用 NFS 启动这里同样网路连接方式分为直连方式和路由器方式。
⚫ 直连方式
设置 serverip
这里假设服务器 ip 为 192.168.103.100 和开发板 ip 地址 192.168.103.1
u-boot=> setenv serverip 192.168.103.100
u-boot=> setenv ipaddr 192.168.103.101
设置 nfsroot
nfsroot 为我们在服务器上 nfs 服务器所在的工作目录,在前面章节我们已经将其设置为
“/source/rootfs”
u-boot=> setenv nfsroot /source/rootfs
设置 bootargs
u-boot=> setenv bootargs cnotallow=${console} root=/dev/nfs ip=${ipaddr}:::::eth0:off nfsr
oot=${serverip}:${nfsroot},v3,tcp
启动内核
u-boot=> run loadimage
u-boot=> run loadfdt
u-boot=> booti ${loadaddr} - ${fdt_addr}
⚫ 路由器方式
设置 serverip
这里假设服务器 ip 为 192.168.103.100
u-boot=> setenv serverip 192.168.103.100
设置 nfsroot
nfsroot 为我们在服务器上 nfs 服务器所在的工作目录,在前面章节我们已经将其设置为
“/source/rootfs”
u-boot=> setenv nfsroot /source/rootfs
设置 bootargs
u-boot=> setenv bootargs cnotallow=${console} root=/dev/nfs ip=dhcp nfsroot=${serverip}:
${nfsroot},v3,tcp
启动内核
u-boot=> run loadimage
u-boot=> run loadfdt
u-boot=> booti ${loadaddr} - ${fdt_addr}
这里需要注意的是直连方式和路由器方式,启动内核部分都是加载外部存储器中的程序进行启动的,如果需要通过 TFTP 启动参考《通过 TFTP 服务器下载内核》即可。
LED 驱动开发之设备树编写
设备树结构分析
imx8mp uboot 设备树位于“arch/arm/dts/”目录下,linux 内核设备树位于
“arch/arm64/boot/dts/freescale/”目录下。
imx8mp 设备树结构如下图所示:
由上图可知“imx8mp.dtsi”为设备树的底层设备描述文件,主要描述了 SoC 级的控制器相关
信息,例如在开发中经常用到的“I2C 总线控制器”、“SPI 总线控制器”等都是在该设备树中描述,此外该文件中还添加了一些头文件,这些头文件是用于设备树中对一些宏定义的支持。在 imx8mp.dtsi 的下级分别有“imx8mp-evk.dts”、“imx8mp-ai-robot.dts”、“imx8mpevk-rpmsg.dts”、“imx8mp-evk-dsp.dts”四个文件,其中 imx8mp-evk.dts 是默认使用的设备树文件,可提供板载设备全功能驱动;imx8mp-ai-robot.dts 是作为“工业及机器人领域”的扩展设备树,可以支持 ROS、xenomai 及 EtherCAT 等功能扩展;imx8mp-evk-rpmsg.dts 主要用于 Cortex-A53 与 Cortex-M7 协同工作时使用的设备树,该设备树中将一部分外设资源分配给了 Cortex-A53 核心使用,一部分分配给 Cortex-M7 核心使用;imx8mp-evk-dsp.dts 主要是用于 DSP 相关功能支持。
IOMUXC 配置简介
我们在控制一个外部设备时,第一步通常都是在配置 SoC 上的引脚功能。对于 imx8mp
我们可以通过内部的 IOMUX 控制器来配置管脚的电压水平、驱动强度和迟滞等属性。具体引脚配置属性可以参考 NXP 官方提供的《i.MX 8M Plus Applications Processor Datasheet for Industrial Products》手册中的“External Signals and Pin Multiplexing”章节。IOMUX控制器的配置参考“IOMUX Controller (IOMUXC)”
IOMUX 控制器包含四组寄存器:
⚫ 通用寄存器(IOMUXC_GPRx):由控制锁相环频率、电压和其他通用配置的寄存器组成。
⚫ Daisy Chain 控制寄存器(IOMUXC_<Instance_port>_SELECT_INPUT):使 IC 可以在一
个 pad 上共享多个功能块。这种共享是通过复用 pad 的输入和输出信号来实现的。
⚫ MUX 控制寄存器(改变 pad 模式):
选择使用 pad 的 8 种不同功能(ALT 模式)中的哪一种功能。
使用下列寄存器中单独或分组设置 pad 功能:
IOMUXC_SW_MUX_CTL_PAD_<PAD NAME>
IOMUXC_SW_MUX_CTL_GRP_<GROUP NAME>
⚫ Pad 控制寄存器(改变 Pad 特性)涉及以下寄存器:
IOMUXC_SW_PAD_CTL_PAD_<PAD_NAME>
IOMUXC_SW_PAD_CTL_GRP_<GROUP NAME>
可设置的特性
SRE: 速率设置,可配置为 FAST 或者 SLOW。
DSE: 管脚驱动强度,可配置为 low, medium, high 或者 max
ODE: 可设置为开漏输出或者 CMOS 输出
HYS: 设置输入触发方式可配置为 CMOS 或者施密特
PUS: 设置管脚的内部上拉或者下拉
PUE: 设置在低功耗模式下默认的上拉或者下拉
PKE: 开启或关闭低功耗模式上拉或者下拉
下面的示例演示如何在设备树中使用 IOMUX XML Code
这里我们来分析第 7 行的配置,pinctrl 原理都是相同的这里以 7 行为例:
MX8MP_IOMUXC_UART1_RXD__UART1_DCE_RX 本质是一个宏定义,定义文件为 ar
ch/arm64/boot/dts/freescale/imx8mp-pinfunc.h,这里列出该定义的原型
#define MX8MP_IOMUXC_UART1_RXD__UART1_DCE_RX 0x220 0x480 0x5E8 0x0
0x4这里可以看到该宏的值为 0x220 0x480 0x5E8 0x0 0x4,那么这组数字是什么含义呢?
关于这组数字在 imx8mp-pinfunc.h 文件的第 11 行有解释“<mux_reg conf_reg input_reg mux_mode input_val>”这里的“mux_reg conf_reg input_reg”为三组 IOMUXC 控制器所对应的
寄存器的偏移量。
mux_reg=0x220 所对应的寄存器是 IOMUXC_SW_MUX_CTL_PAD_UART1_RXD
可以看到该寄存器的偏移量为 220h。同理我们可以得到另外两个寄存器 conf_reg=0x480
和 input_reg=0x5E8 分别对应 IOMUXC_SW_PAD_CTL_PAD_UART1_RXD 和 IOMUXC_UA
RT1_UART_RXD_MUX_SELECT_INPUT 寄存器。这里例如 input_reg 所对应的值为 0 则表
示没有使用。
接下来看 mux_mode input_val 这两个值,这两个值分别表示 mux_reg 和 input_reg 这两
个寄存器要设置的值。例如这里 mux_mode=0x0 则对应 IOMUXC_SW_MUX_CTL_PAD_UA
RT1_RXD 寄存器设置为 0;input_reg=0x4 对应IOMUXC_UART1_UART_RXD_MUX_SELE
CT_INPUT 设置为 0x4。这里我们就将 MX8MP_IOMUXC_UART1_RXD__UART1_DCE_RX
这个宏所对应的含义解释清楚了。
对于上面对 MX8MP_IOMUXC_UART1_RXD__UART1_DCE_RX 宏的解释其实还有一个问题,这个宏设置寄存器偏移量时我们设置了三个寄存器,但是这个宏里只指定了两个寄存器的值,那么有一个寄存器的值没有被设定,这个寄存器就是 conf_reg 所对应的 IOMUXC_SW_PAD_CTL_PAD_UART1_RXD 寄存器,该寄存器的值在设备树被指定,我们再来看设备树的第 7 行,MX8MP_IOMUXC_UART1_RXD__UART1_DCE_RX 0x140 这里的 0x140即为 IOMUXC_SW_PAD_CTL_PAD_UART1_RXD 寄存器要设置的值。
这部分内容可参考 linux 内核中对应的说明文档:
Documentation/devicetree/bindings/pinctrl/fsl,imx-pinctrl.txt
Documentation/devicetree/bindings/pinctrl/fsl,imx8mp-pinctrl.txt
LED 驱动开发之驱动程序设计
platform 总线
要满足 Linux 设备模型,就必须要有总线、设备和驱动。但是有的设备并没有对应的物理总线,比如 LED、RTC 和蜂鸣器等。为此,内核专门开发了一种虚拟总线——platform 总线,用来连接这些没有物理总线的设备或者一些不支持热插拔的设备,接下来我们要用到的
LED 设备就是挂接在这条总线上的。
平台驱动是用 struct platform_driver 结构来表示的,他的定义如下。
C++ Code
驱动开发者关心得主要成员如下。
probe:总线发现有匹配的平台设备时调用。
remove:所驱动的平台设备被移除时或平台驱动注销时调用。
shutdown、suspend 和 resume:电源管理函数,在要求设备掉电、挂起和恢复时被调用。
内嵌的 struct device_driver 的 pm 成员也有对应的电源管理函数。
id_table:平台驱动可以驱动的平台设备 ID 列表,可用于和平台设备匹配。
向平台总线注册和注销的平台驱动的主要函数如下。
platform_driver_register(drv)
void platform_driver_unregister(struct platform_driver *);
因为在驱动中,经常在模块初始化函数中注册一个平台驱动,在清除函数中注销一个平台驱动,可以参考如下代码片段。
C++ Code
代码第 7 行至代码第 14 行定义了一个平台驱动,名字为 farsight-led;第 1 行至第 5 行是用于设备树匹配,匹配设备树中 compatible 的值为“farsight-led”的配置项当着两项匹配成功时执行 led_drv_probe 函数。代码第 19 行使用 platform_driver_register 向 platform 总线注册一个 platform driver 设备。
上面提到该驱动匹配设备树中 compatible 的值为“farsight-led”的配置项,这里列出完整设备树配置。
C++ Code
字符设备驱动编写
在正式学习字符设备驱动的编写之前,我们首先来看看相关的基础知识。在类 UNIX 系
统中,有一个众所周知的说法,即“一切皆文件”,当然网络设备是一个例外。这就意味着
设备最终也会体现为一个文件,应用程序要对设备进行访问,最终就会转化为对文件的访问,
这样做的好处是统一了对上层的接口。设备文件通常位于/dev 目录下,使用下面的命令可以看到很多设备文件及其相关的信息。
root@imx8mp:# ls -l /dev
total 0
……
brw-rw---- 1 root disk 8, 0 Jul 4 10:07 sda
brw-rw---- 1 root disk 8, 1 Jul 4 10:07 sda1
brw-rw---- 1 root disk 8, 2 Jul 4 10:07 sda2
brw-rw---- 1 root disk 8, 5 Jul 4 10:07 sda5
……
crw--w---- 1 root tty 4, 0 Jul 4 10:07 tty0
crw-rw---- 1 root tty 4, 1 Jul 4 10:07 tty1
……
面列出的信息中,前面的字母“b”表示的是块设备,“c”表示的是字符设备。比如上面的 sda、sda1、sda2、sda5 就是块设备,实际上这些设备是笔者的 Ubuntu 主机上的一个硬盘和这个硬盘上的三个分区,其中 sda 表示的是整个硬盘,而 sda1、sda2、sda5 分别是三个分区。tty0、tty1 就是终端设备,shell 程序使用这些设备来同用户进行交互。从上面的打印信息来看,设备文件和普通文件有很多相似之处,都有相应的权限,所属的用户和组,修改时间和名字。但是设备文件会比普通文件多出两个数字,这两个数字分别叫主设备号和次设备号。这两个号是设备在内核中的身份或标志,是内核用于区分不同设备的唯一信息。通常内核用主设备号区别一类设备,次设备号用于区分同一类设备的不同的个体或不同的分区。而路径名则是用户层用于区别设备的信息。
现在的 Linux 系统中,设备文件通常是自动创建的。即便如此,我们还是可以通过 mknod
命令来手动创建一个设备文件,如下所示。
root@imx8mp:# mknod /dev/vser0 c 256 0
root@imx8mp:# ls -li /dev/vser0
126695 crw-r--r-- 1 root root 256, 0 Jul 13 10:03 /dev/vser0
要实现一个字符设备驱动,其中最重要的事就是要构造一个 cdev 结构对象,并让 cdev 同
设备号和设备的操作方法集合相关联,然后将该 cdev 结构对象添加到内核的 cdev_map 散列表中。下面我们逐步来实现这一过程,首先就是在驱动中注册设备号,代码如下(完整的代码请参见“”)。
在模块的初始化函数中,首先在代码第 38 行使用 MKDEV 宏将主设备号和次设备号合并成一个设备号。
第 5 行定义了一个 struct cdev 类型的变量 cdev,在第 12 行到第 16 行定义了一个 struct file_operations 类型的全局变量 dev_ops。我们知道,这两个数据结构是实现字符设备驱动的关键。其中 cdev 代表了一个具体的字符设备,而 dev_ops 是操作该设备的一些方法。代码第27 行调用了 cdev_init 函数初始化了 cdev 里面的部分成员,另外一个最重要的操作就是将 cdev里面的 ops 指针指向了 dev_ops,这样在通过设备号找到 cdev 对象后,就能找到相关的操作方法集合,并调用其中的方法。cdev_init 函数的原型如下,第一个参数是要初始化的 cdev 地址,第二个参数是设备操作方法集合的结构地址。
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
代码第 29 行将一个 owner 成员赋值为 THIS_MUDULE,owner 是一个指向 struct module
类型变量的指针,THIS_MUDULE 是包含驱动的模块中的 struct module 类型对象的地址,类似于 c++中的 this 指针。这样就能通过 cdev 或 dev_ops 找到对应的模块,在对前面两个对象进行访问时都要调用类似于 try_module_get 的函数增加模块的引用计数,其目的是这两个对象在使用的过程中,模块是不能被卸载的,因为模块被卸载的前提条件是引用计数为 0。cdev 对象初始化好以后,就应该添加到内核中的 cdev_map 散列表当中,调用的函数是cdev_add,其函数原型如下。
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
在初始化函数中添加了 cdev 对象,那么在清除函数中自然就应该删除该 cdev 对象,代码第 85 行演示了这一操作,实现的函数是 cdev_del,其函数原型如下。
void cdev_del(struct cdev *p);
操作 GPIO 管脚
GPIO 应该是每个嵌入式设备都避免不了的。现在内核里面多了 gpiod 的来控制 gpio 口,
相对于原来的形式,使用 gpiod 的好处是我们申请后不进行 free 也没有什么问题。
使用以下函数获取 GPIO 设备,多个设备时需要附带 index 参数。函数返回一个 GPIO 描
述符,或一个错误编码,可以使用 IS_ERR()进行检查:
struct gpio_desc *gpiod_get_index(struct device *dev,
const char *con_id, unsigned int idx,
enum gpiod_flags flags)
如果要设置 GPIO 的输入或者输出模式,可以使用如下两个函数实现:
int gpiod_direction_input(struct gpio_desc *desc)
int gpiod_direction_output(struct gpio_desc *desc, int value)
如果要读取 GPIO 口的电平状态则使用下面的函数:
int gpiod_get_value(const struct gpio_desc *desc);
如果要设置 GPIO 口的电平状态则使用下面的函数:
void gpiod_set_value(struct gpio_desc *desc, int value);
下面我们来分析 GPIO 控制逻辑。代码如下:
C++ Code
这部分代码比较简单,这里我们只关注与 GPIO 相关程序,程序第 6 行是在获取设备树中对 gpio 口的配置,第 21 行是将对应 IO 口设置为高电平,第 25 行的将对应 IO 口设置为低电平。
上层应用程序开发
该驱动我们通过 ioctl 函数来控制 LED 灯的状态。ioctl 函数为了处理设备非数据的操作(这些可以同过 read、write 接口来实现),内核将对设备的控制操作委派给了 ioctl 接口,ioctl 也是一个系统调用,其函数原型如下。
int ioctl(int d, int request, ...);
0x12345678 表示点灯,而 0x12345679 表示灭灯等。但是这个操作码,或者更符合内核的
说法是命令,应该具有一定的编码规则,这个我们在后面会介绍。…是 C 语言中实参个数可变的函数原型声明形式,但在这里表示的是第三个参数可有可无。比如对于刚才的 LED 例子,第三个参数可以用于指定将哪个 LED 点亮或熄灭,如 0 表示 LED0,1 表示 LED1 等。因为第三个形参是 unsigned long 类型的,所以除了可以传递数字值而外,还可以传递一个指针,这样就可以和内核空间交互任意多个字节的数据。
查看前面的 file_operations 结构的定义,和 ioctl 系统调用对应的驱动接口函数是
unlocked_ioctl,另外还有一个 compat_ioctl,compat_ioctl 是为了处理 32 位程序和 64 位内核兼容的一个函数接口,和体系结构相关。unlocked_ioctl 的函数原型如下。
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
第一个参数还是是打开的文件的 file 结构指针,第二个参数和系统调用的第二个参数
request 对应,第三个参数对应系统调用函数的第三个参数之前说到用于 ioctl 的命令需要遵从一种编码规则,那么这个编码规则是怎样的呢?在当前的内核源码版本中,命令按照如下方式组成。
上述内容摘自内核文档“Documentation/ioctl/ioctl-decoding.txt”。也就是说一个命令由 4个部分组成,每个部分有规定的意义和位宽限制。之所以这样定义命令,而不是简单的用 0、1、2、……来定义命令,是为了避免命令定义的重复,从而导致应用程序误操作,把一个命令发送给本不应该执行他的驱动程序,而驱动程序又错误地执行了这个命令。采用这种机制,使得驱动有机会来检查这个命令是否属于驱动,从一定程度上避免了这种问题的发生。理想的要求是比特 15 位到比特 8 位所定义的幻数在一种体系结构下是全局唯一的,但很显然,这很难做到。尽管如此,我们还是应该遵从内核所规定的这种命令定义形式。内核提供了一组宏来定义命令、提取命令中的字段信息,代码如下。
定义命令所使用的最底层的宏是_IOC,他将 4 个部分通过移位合并在一起。假设要定义一个设置串口帧格式的命令,那么按照前面的规则,这个命令要带参数,并且是将数据写入到驱动,那么最高两个比特是 01,如果要写入的参数是一个 struct option 的结构,而结构占12 个字节,那么比特 29 到比特 16 的 10 进制值应该是 12,如果定义幻数为字母 s,命令码为2,最终就应使用_IOC(1,‘s’,0,12)来定义该命令。不过内核还提供了更方便的宏,刚才那个命令可以通过_IOW(‘s’,2,struct option)来定义。另外还有 4 个宏_IOC_DIR、_IOC_TYPE、
_IOC_NR 和_IOC_SIZE 来分别提取命令中的 4 个部分。
本程序中对于命令码的定义如下:
该程序最终实现了 LED 灯的闪烁,每 1s 变化一次状态
程序运行
⚫ 添加设备树
首先我们需要在内核源码中,添加我们对应 LED 的设备树文件。需要注意的是由于系统
默认已经设置好的 LED 的配置,所以我们需要先将原来的 leds 配置项屏蔽。
打开“arch/arm64/boot/dts/freescale/imx8mp-ai-robot-base.dts”文件,将 leds 配置项屏蔽。
添加 my_led 设备节点,在设备树“/”下添加如下配置
⚫ 部署设备树
重新编译设备树,此部分可参考《linux》编译小节,编程成功后会在“arch/arm64/boot/d
ts/freescale/”目录下生成“imx8mp-ai-robot-base.dtb”文件,将该文件使用 tftp 方式下载到开
发板并启动系统。
⚫ 编译驱动程序
将“华清远见-I.MX8M Plus 开发资料-2021-06-02\程序源码”下的“gpio_demo”导入到虚拟机中
在进行源码编译之前需要先导入交叉编译工具链,之前章节已经介绍过具体安装过程,这里不再赘述,如果还没有安装可参考《交叉编译工具链安装》章节
linux@ubuntu:$ source /opt/fsl-imx-xwayland/5.4-zeus/environment-setup-aarch64-poky-lin
ux
linux@ubuntu:$ $CC --version
进入到 gpio_demo 源码中,修改 Makefile 文件,将 KERNELDIR 变量修改为内核源码所在路径。修改完成后使用 make 命令编译即可
linux@ubuntu:$ make
之后使用如下命令编译应用程序代码
linux@ubuntu: $CC ledapp.c -o ledapp
编译完成后会在当前目录生成“imx8mp_led_driver.ko”文件和“ledapp”文件,将着两个文件复制到“/source/rootfs/”目录下,使用 NFS 挂载启动即可,在目标板中运行了。
⚫ 运行应用程序
首先需要先通过 tftp 启动的方式来更新设备树配置,之后通过 NFS 网络文件系统挂载方式挂载 rootfs。
启动成功之后进入到“imx8mp_led_driver.ko”文件和“ledapp”文件所在的目录下,按照如下命令安装驱动程序并运行应用程序。
linux@ubuntu: insmod imx8mp_led_driver.ko
linux@ubuntu: ./ledapp
标签:imx8m,ubuntu,boot,开发板,plus,内核,linux,imx8mp,设备 From: https://blog.51cto.com/u_15343919/6189843