Full system development tutorial based on imx8m plus development board 4: Linux system development

Foreword:

The i.MX8M Plus development board has 4 Cortex-A53 cores, running at 1.8GHz; 1 Cortex-M7 core, running at 800MHz; in addition, it also integrates a 2.3 TOPS NPU, which greatly accelerates machine learning reasoning.

The development platform used in the full text is the FS-IMX8MPCA development board (Huaqing Yuanjian imx8mp development board) that cooperates with NXP official, supports Weston, ubuntu20.04, Android11 ​​and other operating systems; also supports Xenomai hard real-time kernel, EtherCAT bus, TSN Time-sensitive network, ROS1.0, ROS2.0 and other industrial and robot applications; can be used in industrial Internet, artificial intelligence, edge computing, multi-screen display and other application directions. Huaqing Foresight R&D Center has compiled a large number of development tutorials and recorded rich video teaching resources for free!

More information about the development board can be obtained by leaving a message in the comment area below~~~

Linux  system development

TF-A  compile

Configure the cross-compilation toolchain

Before compiling the source code, you need to import the cross-compilation toolchain. The specific installation process has been introduced in the previous chapters, so I won’t go into details here. If you haven’t installed it yet, you can refer to the chapter "Cross-compilation Toolchain Installation"

linux@ubuntu:$ source /opt/fsl-imx-xwayland/5.4-zeus/environment-setup-aarch64-poky-lin

ux

linux@ubuntu:$ $CC --version

 

Compile TF-A compile

Switch the current working directory to the TF-A source code directory, here is "~/workdir/imx8mp/imx-yocto-bsp/bsp

_source/imx-atf”

linux@ubuntu:$ cd ~/workdir/imx8mp/imx-yocto-bsp/bsp_source/imx-atf

 

You can clear the previous cache before compiling

linux@ubuntu:$ make clean PLAT=imx8mp

compile

linux@ubuntu:$ LDFLAGS="" make PLAT=imx8mp

 

After the compilation is successful, generate relevant images in the build/imx8mp/release/ directory

Compile and run Bootloader 

Configure the cross-compilation toolchain

Before compiling the source code, you need to import the cross-compilation toolchain. The specific installation process has been introduced in the previous chapter.

I won’t go into details here. If you haven’t installed it yet, please refer to the chapter "Cross Compilation Toolchain Installation"

linux@ubuntu:$ source /opt/fsl-imx-xwayland/5.4-zeus/environment-setup-aarch64-poky-lin

ux

linux@ubuntu:$ $CC --version

 

Bootloader compilation

Switch the current working directory to the bootloader source code directory, here is "~/workdir/imx8mp/bsp_source/u

boot-imx”

linux@ubuntu:$ cd ~/workdir/imx8mp/bsp_source/uboot-imx

 

⚫ configuration

linux@ubuntu:$ make imx8mp_ai_robot_defconfig

⚫ compile

linux@ubuntu: $ few

 

After the compilation is successful, the u-boot-spl.bin related image will be generated in the spl directory, and the u-boot.img image will be generated in the root directory

make imx-boot

The standard u-boot compiled in front of us can't start the device automatically, imx8mp needs to be built by imx-mkimage

imx-boot。

The imx-boot image includes U-boot, tf-a, uboot spl and ddr firmware. Therefore, we need to copy the previously compiled uboot-imx image, imx-atf and ddr firmware to the imx-boot/iMX8M directory to create an imx-boot image.

linux@ubuntu:$ cd ~/workdir/imx8mp/bsp_source/imx-boot

⚫ Copy u-boot image

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/

⚫ Copy DDR firmware

linux@ubuntu:$ cp ../firmware-imx-8.10/firmware/ddr/synopsys/ddr4_*_202006* iMX8M/

⚫ Copy tf-a image

linux@ubuntu:$ cp ../imx-atf/build/imx8mp/release/bl31.bin iMX8M/

⚫ Compile and generate imx-boot

linux@ubuntu:$ make SOC=iMX8MP flash_ai_robot

After the compilation is successful, a flash.bin file will be generated in the iMX8M directory, use this file to burn to

The development board can use the self-compiled u-boot and tf-a images to start the development board.

dos@windows:$ uuu -b emmc flash.bin

Introduction to common commands of u-boot 

Linux  system environment variables

The environment variables under linux can be set during the u-boot stage, after the countdown ends during the u-boot startup process

Press any key to stop at u-boot to set environment variables.

If you want to view the current environment variables, you can use the "print" command

If you want to restore the factory environment variables, you can use the "env default -f -a" command to reset

If you want to save the current environment variables, you can use "saveenv" to save them.

a)  Set the linux kernel loading address

⚫ Linux kernel loading address

u-boot=> setenv loadaddr 0x80080000

⚫ Device tree loading address

u-boot=> setenv fdtaddr 0x80f00000

⚫ Device tree file name

u-boot=> setenv fdt_file imx8mp-evk.dtb

b)  Display settings

⚫ HDMI display

u-boot=> setenv fdt_file imx8mp-ai-robot.dtb

⚫ LVDS and HDMI dual display

u-boot=> setenv fdt_file imx8mp-ai-robot-lvds070.dtb

⚫ MIPI and HDMI dual display

u-boot=> setenv fdt_file imx8mp-ai-robot-mipi070.dtb

⚫ LVDS, MIPI and HDMI three-screen display

u-boot=> imx8mp-ai-robot-mipi-lvds-dual.dts

c)  Set and file system location

⚫ Mount via 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'

⚫ Mount via eMMC

u-boot=> setenv rootfsinfo 'root=/dev/mmcblk0p2 rootwait rw' /* eMMC */

Introduction to Common Commands

The uboot command is similar to the linux line buffer command line. When we input commands to the terminal command line, these commands are not immediately recognized by the system, but are buffered into a buffer (that is, the system thinks that I have not finished typing), when After we press the Enter key (new line), the system thinks that the input is over, and then processes all the commands just entered in the buffer.

⚫ The first command: printenv/print

The printenv command does not take parameters, and its function is to print out all the environment variables in the system.

printenv environment variable name View the specified environment variable value.

⚫ Common environment variables

After bootdelay uboot starts, how many seconds after the countdown will automatically execute the statement of the environment variable bootcmd

After bootcmd counts down to 0, the statement inside is automatically executed

bootargs is used to provide the boot parameter statement to the kernel

ipaddr The IP address of the current development board

⚫ Set to add/change environment variables: setenv/set

Usage: set name value

For example: set bootdelay 3

⚫ Save environment variable changes: saveenv/save

The function of saveenv/save is to synchronously save the value of the environment variable in the memory to the partition of the environment variable in the flash

⚫ Network test command: ping

Usage: ping IP address

This command needs to set up the network environment of the development board first, including the gateway, subnet mask, and local IP address. Only then can the command be used.

⚫ tftp download command

It is used to download files from tftp server through tftp protocol.

eg: tftp 0x40480000 Image Download the Image file on the tftp server to the local memory

0x40480000 address.

⚫ Memory operation instructions: mw, md

md memory address is used to view the value on the memory address

md.b 0x40008000 100 Starting from memory address 0x40008000, check 0x100 bytes and output the value

md.w 0x40008000 100 Starting from memory address 0x40008000, check 0x100 16-bit values ​​and output the values

md.l 0x40008000 100 Starting from memory address 0x40008000, check 0x100 32-bit values ​​and output the values

mw is used to modify the value on the memory address

mw.b x40008000 0xab 100 0x100 byte space starting from memory address 0x40008000, set the value to 0xab

mw.w 0x40008000 0xabcd 100 0x200 byte space starting from memory address 0x40008000, every 16-bit value is set to 0xabcd

mw.l 0x40008000 0xabcdef88 100 0x400 byte space starting from memory address 0x40008000, each 32-bit value is set to 0xabcdef88

⚫ help command

The instructions introduced above are some commonly used instructions in the bootloader. If you need to use other instructions, you can pass

Use the help command to view the specific description and usage.

Linux  kernel source code compilation

Configure the cross-compilation toolchain

Before compiling the source code, you need to import the cross-compilation toolchain. The specific installation process has been introduced in the previous chapter.

I won’t go into details here. If you haven’t installed it yet, please refer to the chapter "Cross Compilation Toolchain Installation"

linux@ubuntu:$ source /opt/fsl-imx-xwayland/5.4-zeus/environment-setup-aarch64-poky-lin

ux

linux@ubuntu:$ $CC --version

 

linux compile

Switch the current working directory to the linux kernel source code directory, here is "~/workdir/imx8mp/imx-yocto-bsp

/bsp_source/ linux-imx” 

linux@ubuntu:$ cd ~/workdir/imx8mp/imx-yocto-bsp/bsp_source/linux-imx

 

Add nxp official standard imx_v8 configuration

linux@ubuntu:$ make imx_v8_defconfig

Add custom configuration

linux@ubuntu:$ ./scripts/kconfig/merge_config.sh -m .config arch/arm64/configs/aicar.config

After adding the configuration, we can configure and tailor the kernel through menuconfig

linux@ubuntu:$ make menuconfig

Compile the kernel file

linux@ubuntu: $ few

After the compilation is successful, the Image kernel image is generated in the arch/arm64/boot/ directory, and the device tree image is generated in the arch/arm64/boot/dts/freescale/ directory

Compile the device tree file separately

linux@ubuntu:$ make dtbs

After the compilation is successful, generate a device tree image in the arch/arm64/boot/dts/freescale/ directory

Update kernel image via tftp server

The bridge network of VMware has been configured in the above chapters. In this section, the kernel and device tree will be downloaded through the TFTP server. We will introduce it in two parts. The first part is to check and accept by directly connecting to the computer through a network cable, and the second part is to connect through a router. Development board and computer.

Network cable directly connected to the computer

Here we use a direct connection network, that is, the development board is directly connected to the computer through a network cable.

 

First, start the Ubuntu virtual machine. Since we use direct connection, the virtual machine cannot automatically obtain an IP address.

We need to manually set the IP address.

Open the "/etc/network/interfaces" file of the virtual machine

linux@ubuntu:$ sudo vi /etc/network/interfaces

Add the following configuration

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

Here "ens33" represents the name of the network card, which can be viewed through the ifconfig command;

address is the static IP address to be set;

netmask is the subnet mask

gateway is the gateway address

 

Restart the network service after the setup is complete

linux@ubuntu:$ sudo reboot

After the setting is complete, use the ifconfig command to view the current IP address of ubuntu.

 

Before the development board is powered on, the debugging serial port and network cable need to be connected to the network port 1.

 

After that, power up the development board to make the program stay in the u-boot terminal.

 

Here we need to set several network-related environment variables to support network transmission.

Above we have set up the network configuration of ubuntu, here ubuntu acts as a TFTP server. we need to

Set the environment variables of the development board according to the configuration of ubuntu.

Set server IP address (ubuntu ip)

u-boot=> setenv serverip 192.168.100.240

Set the local IP address (development board ip)

u-boot=> setenv ipaddr 192.168.100.241

set gateway

u-boot=> setenv gatewayip 192.168.100.1

set subnet mask

u-boot=> setenv netmask 255.255.255.0

Set NIC 1 MAC address

u-boot=> setenv eth1addr 00:04:9f:07:0b:a5

Save environment variables

u-boot=> saveenv

After the setting is complete, you can use the ping command to test

u-boot=> ping 192.168.100.240

 

Download the linux kernel and device tree

Before downloading the kernel, you first need to confirm whether the TFTP server has been installed. If not, you need to install it according to the previous chapters. In addition, you need to copy the previously compiled Image linux kernel program and the device tree file imx8mp-ai- Put robot.dtb into the TFTP server, if it is built according to the previous tutorial, the path is [/tftpboot/]

 

Next, set the image name and device tree name to be downloaded in u-boot

u-boot=> setenv image Image

u-boot=> setenv fdt_file imx8mp-ai-robot.dtb

u-boot=> saveenv

If you want to store the downloaded image file to a specified storage device, such as an external sdcard or eMMC,

You need to set the current storage device.

Save to eMMC, execute the following command on the development board

u-boot=> mmc dev 2 0

Save to SDcard, execute the following command on the development board

u-boot=> mmc dev 1 0

Download the linux kernel file

u-boot=> tftpboot ${loadaddr} ${image}

Download device tree file

u-boot=> tftpboot ${fdt_addr} ${fdt_file}

starting program

u-boot=> run mmcargs

u-boot=> booti ${loadaddr} - ${fdt_addr}

Connect to computer via router

The previous section described how to transfer data to the TFTP server through a direct network connection, and this section describes how to connect through a router.

 

There is not much difference between connecting through a router and connecting directly, except that a DHCP server can be used to obtain an automatic IP address through a router.

Open the "/etc/network/interfaces" file of the virtual machine

linux@ubuntu:$ sudo vi /etc/network/interfaces

Add the following configuration

auto ens33

iface ens33 inet dhcp

Here, "ens33" is set to automatically obtain an IP address through DHCP

 

Restart the network service after the setup is complete

linux@ubuntu:$ sudo reboot

After the setting is complete, use the ifconfig command to view the current IP address of ubuntu.

 

First start the development board, so that the program stays in the u-boot terminal.

 

Here we need to set several network-related environment variables to support network transmission.

Above we have set up the network configuration of ubuntu, here ubuntu acts as a TFTP server. We need to set the environment variables of the development board according to the configuration of ubuntu.

Set server IP address (ubuntu ip) 

u-boot=> setenv serverip 192.168.101.47

Set NIC 1 MAC address

u-boot=> setenv eth1addr 00:04:9f:07:0b:a5

Save environment variables

u-boot=> saveenv

Download the linux kernel and device tree

Before downloading the kernel, you first need to confirm whether the TFTP server has been installed. If not, you need to install it according to the previous chapters. In addition, you need to copy the previously compiled Image linux kernel program and the device tree file imx8mp-ai- Put robot.dtb into the TFTP server, if it is built according to the previous tutorial, the path is [/tftpboot/]

 

Next, set the image name and device tree name to be downloaded in u-boot

u-boot=> setenv image Image

u-boot=> setenv fdt_file imx8mp-ai-robot.dtb

u-boot=> saveenv

The difference here is that it is no longer necessary to specify the local ip, gateway, subnet mask and other environment variables.

The information only needs to be obtained through dhcp

Download the linux kernel file

u-boot=> dhcp ${loadaddr} ${image}

Download device tree file

u-boot=> dhcp ${fdt_addr} ${fdt_file}

starting program

u-boot=> run mmcargs

u-boot=> booti ${loadaddr} - ${fdt_addr}

Minimal file system production based on busybox

Busybox  source code compilation and installation

You can download the source code of busybox-1.29.3 from http://busybox.net/downloads/ to make a Linux file system

System, for convenience, the source code has been put into the CD.

Install the cross-compilation toolchain.

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

Verify that the development tools are installed correctly, and the version information is displayed as shown in the figure below.

linux@ubuntu:$ aarch64-linux-gnu-gcc -v

 

Create a source directory

Put the busybox-1.29.

3.tar.bz2 copied to the directory.

linux@ubuntu:$ tar xvf busybox-1.29.3.tar.bz2 //Decompress the source code

linux@ubuntu:$ cd busybox-1.29.3

Configure busybox source code:

Change the CROSS_COMPILE field in the Makefile in the top directory to "aarch64-linux-gnu-"

 

You can use the following command to configure the source code

linux@ubuntu:$ make ARCH=arm64 menuconfig

 

Select Exit, Select Save

Compile the source code:

linux@ubuntu: $ few

Install:

The default installation path of busybox is _install in the source code directory

linux@ubuntu:$ make install

Enter the installation directory to see the following directory:

linux@ubuntu:$ cd _install

linux@ubuntu:$ ls

bin linuxrc sbin usr

Add the main system startup file

Create other required directories: 

linux@ubuntu:$ cd _install

linux@ubuntu:$ mkdir dev etc mnt proc var tmp sys root

Add library:

Copy the library in the toolchain to the _install directory:

linux@ubuntu:$ cp –a /usr/aarch64-linux-gnu/lib/ .

Remove the static library:

linux@ubuntu:$ rm lib/*.a

Add system startup file:

Add the file inittab under etc, the content of the file is as follows:

Note: The modified files are all in the _install directory

 etc/inittab

 

Here we mount the file system with three proc, sysfs and tmpfs.

Go back to the created file system, create the init.d directory under etc, and create the rcS file under init.d, the content of the rcS file is: etc/init.d/rcS

 

Add executable permissions for rcS:

linux@ubuntu:$ chmod a+x init.d/rcS

Add a profile file under etc, the file content is: etc/profile

 

At this point, a minimal file system is created, and the files in the _install directory can be copied to the /source/rootfs directory

Recorded for NFS mounts.

linux@ubuntu:$ cp * /source/rootfs -a The current path is _install 

Mount the file system via NFS

The NFS method is that the development board mounts the root file system on the host computer (PC) through NFS. At this time, the operations performed on the file system of the host are reflected on the development board synchronously; otherwise, the operations performed on the development board are simultaneously reflected on the root file system of the host. In actual work, we often use NFS to mount the system, which is very convenient for system debugging.

Before proceeding with the experiments in this chapter, you must first ensure that the NFS service has been successfully installed according to the previous chapters. This experiment is the same as the TFTP experiment, and it is also divided into two parts: direct connection and router. The ip address setting part can be set by referring to the chapter "Downloading the Kernel via TFTP Server", so I won't go into details here.

Before starting the network file system through NFS, first ensure that [/source/] exists in the ubuntu virtual machine directory

rootfs file system, if this file does not exist, it needs to be in [Huaqing Vision-I.MX8M Plus Development Information-2021-06-

02\Program Source Code/File System Source Code] Extract the rootfs.tar.xz compressed package from the directory and decompress it to the /source/ directory. After the decompression is completed, the rootfs directory will be generated under the /source/ directory.

 

Before the development board is powered on, the debugging serial port and network cable need to be connected to the network port 1.

 

After configuring the environment, start the development board and make the program stay in the u-boot terminal.

 

Use NFS to start. Here, the same network connection method is divided into direct connection method and router method.

⚫ Direct connection

set serverip

Here it is assumed that the server ip is 192.168.103.100 and the development board ip address is 192.168.103.1

u-boot=> setenv serverip 192.168.103.100

u-boot=> setenv ipaddr 192.168.103.101

set nfsroot

nfsroot is the working directory where our nfs server is located on the server, we have set it to

“/source/rootfs”

u-boot=> setenv nfsroot /source/rootfs

set bootargs

u-boot=> setenv bootargs console=${console} root=/dev/nfs ip=${ipaddr}:::::eth0:off nfsr

oot=${serverip}:${nfsroot},v3,tcp

start the kernel

u-boot=> run loadimage

u-boot=> run loadfdt

u-boot=> booti ${loadaddr} - ${fdt_addr}

⚫ Router method

set serverip

Here it is assumed that the server ip is 192.168.103.100

u-boot=> setenv serverip 192.168.103.100

set nfsroot

nfsroot is the working directory where our nfs server is located on the server, we have set it to

“/source/rootfs”

u-boot=> setenv nfsroot /source/rootfs

set bootargs

u-boot=> setenv bootargs console=${console} root=/dev/nfs ip=dhcp nfsroot=${serverip}:

${nfsroot},v3,tcp

start the kernel

u-boot=> run loadimage

u-boot=> run loadfdt

u-boot=> booti ${loadaddr} - ${fdt_addr}

It should be noted here that in the direct connection mode and the router mode, the kernel part is started by loading the program in the external memory. If you need to start it through TFTP, refer to "Downloading the Kernel Through a TFTP Server".

Device tree writing for LED  driver development

Device tree structure analysis

The imx8mp uboot device tree is located in the "arch/arm/dts/" directory, and the linux kernel device tree is located in

Under the "arch/arm64/boot/dts/freescale/" directory.

The imx8mp device tree structure is shown in the figure below:

 

It can be seen from the above figure that "imx8mp.dtsi" is the bottom device description file of the device tree, which mainly describes the SoC-level controller-related

Information, such as "I2C bus controller" and "SPI bus controller" that are often used in development are described in the device tree. In addition, some header files are added to this file. These header files are used for Support for some macro definitions in the device tree. There are four files under imx8mp.dtsi: "imx8mp-evk.dts", "imx8mp-ai-robot.dts", "imx8mpevk-rpmsg.dts", and "imx8mp-evk-dsp.dts". Among them, imx8mp- evk.dts is the device tree file used by default, which can provide full-function drivers for onboard devices; imx8mp-ai-robot.dts is an extended device tree for "industrial and robot fields", which can support functional extensions such as ROS, xenomai, and EtherCAT ;imx8mp-evk-rpmsg.dts is mainly used for the device tree used when Cortex-A53 and Cortex-M7 work together. In this device tree, part of the peripheral resources are allocated to the Cortex-A53 core, and some are allocated to the Cortex-M7 Core use; imx8mp-evk-dsp.dts is mainly used for DSP related function support.

Introduction to IOMUXC  configuration

When we control an external device, the first step is usually to configure the pin function on the SoC. for imx8mp

We can configure properties such as voltage level, drive strength and hysteresis of the pin through the internal IOMUX controller. For specific pin configuration properties, please refer to the "External Signals and Pin Multiplexing" chapter in the "i.MX 8M Plus Applications Processor Datasheet for Industrial Products" manual officially provided by NXP. The configuration of the IOMUX controller refers to "IOMUX Controller (IOMUXC)"

The IOMUX controller consists of four sets of registers:

⚫ General-purpose registers (IOMUXC_GPRx): Consists of registers that control PLL frequency, voltage, and other general-purpose configurations.

⚫ Daisy Chain control register (IOMUXC_<Instance_port>_SELECT_INPUT): enables the IC to

Share multiple function blocks on a pad. This sharing is achieved by multiplexing the pad's input and output signals.

⚫ MUX control register (change pad mode):

Choose which of 8 different functions (ALT modes) of the pad to use.

Use the following registers to set pad functions individually or in groups:

IOMUXC_SW_MUX_CTL_PAD_<PAD NAME>

IOMUXC_SW_MUX_CTL_GRP_<GROUP NAME>

⚫ Pad control registers (change Pad characteristics) involve the following registers:

IOMUXC_SW_PAD_CTL_PAD_<PAD_NAME>

IOMUXC_SW_PAD_CTL_GRP_<GROUP NAME>

configurable properties

SRE: Speed ​​setting, which can be configured as FAST or SLOW.

DSE: Pin drive strength, configurable as low, medium, high or max

ODE: can be set to open drain output or CMOS output

HYS: set the input trigger mode can be configured as CMOS or Schmidt

PUS: Set the internal pull-up or pull-down of the pin

PUE: Set the default pull-up or pull-down in low power mode

PKE: Enable or disable low power mode pull-up or pull-down

The following example demonstrates how to use IOMUX XML Code in the device tree

 

Here we analyze the configuration of line 7, the principle of pinctrl is the same, here we take line 7 as an example:

MX8MP_IOMUXC_UART1_RXD__UART1_DCE_RX is essentially a macro definition, the definition file is ar

ch/arm64/boot/dts/freescale/imx8mp-pinfunc.h, the prototype of the definition is listed here

#define MX8MP_IOMUXC_UART1_RXD__UART1_DCE_RX 0x220 0x480 0x5E8 0x0

0x4 Here we can see that the value of this macro is 0x220 0x480 0x5E8 0x0 0x4, so what is the meaning of this group of numbers?

About this group of numbers is explained in the 11th line of the imx8mp-pinfunc.h file "<mux_reg conf_reg input_reg mux_mode input_val>" where "mux_reg conf_reg input_reg" corresponds to the three groups of IOMUXC controllers

Register offset.

The register corresponding to mux_reg=0x220 is IOMUXC_SW_MUX_CTL_PAD_UART1_RXD

 

It can be seen that the offset of this register is 220h. In the same way we can get the other two registers conf_reg=0x480

and input_reg=0x5E8 correspond to IOMUXC_SW_PAD_CTL_PAD_UART1_RXD and IOMUXC_UA respectively

RT1_UART_RXD_MUX_SELECT_INPUT register. Here, for example, the value corresponding to input_reg is 0, then the table

Indicates not used.

 

 

Next, look at the two values ​​of mux_mode input_val, which represent the two values ​​of mux_reg and input_reg respectively.

The value to be set for each register. For example, here mux_mode=0x0 corresponds to IOMUXC_SW_MUX_CTL_PAD_UA

RT1_RXD register is set to 0; input_reg=0x4 corresponds to IOMUXC_UART1_UART_RXD_MUX_SELE

CT_INPUT is set to 0x4. Here we will MX8MP_IOMUXC_UART1_RXD__UART1_DCE_RX

The meaning of this macro is explained clearly.

There is actually another problem with the above explanation of the MX8MP_IOMUXC_UART1_RXD__UART1_DCE_RX macro. We set three registers when setting the register offset in this macro, but only two register values ​​are specified in this macro, so the value of one register is not Set, this register is the IOMUXC_SW_PAD_CTL_PAD_UART1_RXD register corresponding to conf_reg, the value of this register is specified in the device tree, let’s look at the seventh line of the device tree, MX8MP_IOMUXC_UART1_RXD__UART1_DCE_RX 0x140 Here 0x140 is IOMUXC_SW_PAD_CTL_PAD_U The value to be set for the ART1_RXD register.

For this part, please refer to the corresponding documentation in the linux kernel:

Documentation/devicetree/bindings/pinctrl/fsl,imx-pinctrl.txt

Documentation/devicetree/bindings/pinctrl/fsl,imx8mp-pinctrl.txt

Driver design for LED  driver development

platform  bus

To satisfy the Linux device model, there must be buses, devices, and drivers. However, some devices do not have a corresponding physical bus, such as LED, RTC and buzzer. To this end, the kernel has specially developed a virtual bus - the platform bus, which is used to connect these devices without a physical bus or some devices that do not support hot plugging. Next we will use

The LED device is attached to this bus.

The platform driver is represented by the struct platform_driver structure, and its definition is as follows.

C++ Code

 

The main members that driver developers care about are as follows.

probe: Called when the bus finds a matching platform device.

remove: Called when the driven platform device is removed or when the platform driver is logged out.

shutdown, suspend, and resume: Power management functions that are called when the device is required to power down, suspend, and resume.

The pm member of the embedded struct device_driver also has corresponding power management functions.

id_table: A list of platform device IDs that the platform driver can drive, which can be used to match with platform devices.

The main functions of the platform driver for registering and unregistering with the platform bus are as follows.

platform_driver_register(drv)

void platform_driver_unregister(struct platform_driver *);

Because in the driver, a platform driver is often registered in the module initialization function, and a platform driver is canceled in the clear function, you can refer to the following code snippet.

C++ Code

 

Lines 7 to 14 of the code define a platform driver named farsight-led; lines 1 to 5 are used for device tree matching, matching the configuration in which the value of compatible in the device tree is "farsight-led" Item Executes the led_drv_probe function when two items match successfully. Line 19 of the code uses platform_driver_register to register a platform driver device with the platform bus.

As mentioned above, the driver matches the configuration item whose compatible value is "farsight-led" in the device tree, and the complete device tree configuration is listed here.

C++ Code

 

Character device driver writing

Before formally learning the writing of character device drivers, let's take a look at the relevant basics. In UNIX-like systems

In the Internet of Things, there is a well-known saying that "everything is a file", of course, network equipment is an exception. which means

The device will eventually be embodied as a file, and the application program will eventually convert it into a file access if it wants to access the device.

The advantage of this is to unify the interface to the upper layer. Device files are usually located in the /dev directory, and you can see many device files and their related information by using the following commands.

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

……

​In the information listed above, the preceding letter "b" indicates a block device, and "c" indicates a character device. For example, sda, sda1, sda2, and sda5 above are block devices. In fact, these devices are a hard disk on the author’s Ubuntu host and three partitions on this hard disk, where sda ​​represents the entire hard disk, and sda1, sda2, and sda5 There are three partitions respectively. tty0 and tty1 are terminal devices, which are used by shell programs to interact with users. Judging from the above printed information, the device file has many similarities with ordinary files, and both have corresponding permissions, users and groups they belong to, modification time and name. However, the device file will have two more numbers than the ordinary file, and these two numbers are called the major device number and the minor device number. These two numbers are the identity or logo of the device in the kernel, and are the only information used by the kernel to distinguish different devices. Usually the kernel uses the major device number to distinguish a type of device, and the minor device number is used to distinguish different individuals or different partitions of the same type of device. The path name is the information used by the user layer to distinguish devices.

In modern Linux systems, device files are usually created automatically. Even so, we can still pass mknod

command to manually create a device file as shown below.

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

To implement a character device driver, the most important thing is to construct a cdev structure object, and let cdev

The device number is associated with the set of operation methods of the device, and then the cdev structure object is added to the cdev_map hash table of the kernel. Let's implement this process step by step. First, register the device number in the driver. The code is as follows (see "" for the complete code).

 

 

In the initialization function of the module, first use the MKDEV macro on the 38th line of the code to combine the major device number and the minor device number into one device number.

Line 5 defines a variable cdev of type struct cdev, and lines 12 to 16 define a global variable dev_ops of type struct file_operations. We know that these two data structures are the key to realizing the character device driver. Among them, cdev represents a specific character device, and dev_ops is some methods of operating the device. Line 27 of the code calls the cdev_init function to initialize some members in the cdev. Another most important operation is to point the ops pointer in the cdev to dev_ops, so that after finding the cdev object through the device number, you can find the relevant operation method collection and call methods on it. The prototype of the cdev_init function is as follows, the first parameter is the address of the cdev to be initialized, and the second parameter is the structure address of the set of device operation methods.

void cdev_init(struct cdev *cdev, const struct file_operations *fops);

Line 29 of the code assigns an owner member to THIS_MUDULE, and owner is a pointer to the struct module

The pointer of the type variable, THIS_MUDULE is the address of the struct module type object in the module containing the driver, similar to the this pointer in C++. In this way, the corresponding module can be found through cdev or dev_ops. When accessing the previous two objects, a function similar to try_module_get must be called to increase the reference count of the module. The purpose is that the module is used during the use of these two objects. It cannot be unloaded, because the prerequisite for the module to be unloaded is that the reference count is 0. After the cdev object is initialized, it should be added to the cdev_map hash table in the kernel. The function called is cdev_add, and its prototype is as follows.

int cdev_add(struct cdev *p, dev_t dev, unsigned count);

If the cdev object is added in the initialization function, then the cdev object should naturally be deleted in the clearing function. Line 85 of the code demonstrates this operation. The implemented function is cdev_del, and its function prototype is as follows.

void cdev_del(struct cdev *p);

Operate GPIO pins

GPIO should be unavoidable for every embedded device. Now there are more gpiods in the kernel to control the gpio port,

Compared with the original form, the advantage of using gpiod is that there is no problem if we do not free after application.

Use the following functions to get GPIO devices, and the index parameter is required for multiple devices. The function returns a GPIO description

A descriptor, or an error code, can be checked using IS_ERR():

struct gpio_desc *gpiod_get_index(struct device *dev,

 const char *con_id, unsigned int idx,

 enum gpiod_flags flags)

If you want to set the input or output mode of GPIO, you can use the following two functions:

int gpiod_direction_input(struct gpio_desc *desc)

int gpiod_direction_output(struct gpio_desc *desc, int value)

If you want to read the level status of the GPIO port, use the following function:

int gpiod_get_value(const struct gpio_desc *desc);

If you want to set the level state of the GPIO port, use the following function:

void gpiod_set_value(struct gpio_desc *desc, int value);

Let's analyze the GPIO control logic below. code show as below:

C++ Code

 

 

This part of the code is relatively simple, here we only focus on the GPIO-related programs, the sixth line of the program is to configure the gpio port in the device tree, the 21st line is to set the corresponding IO port to high level, and the 25th line will The corresponding IO port is set to low level.

upper layer application development

In this driver, we control the state of the LED light through the ioctl function. The ioctl function handles the non-data operations of the device (these can be realized through the read and write interfaces), the kernel delegates the control operation of the device to the ioctl interface, and ioctl is also a system call, and its function prototype is as follows.

int ioctl(int d, int request, ...);

0x12345678 means turning on the light, and 0x12345679 means turning off the light, etc. But this opcode, or more kernel-compliant

The argument is a command, and it should have certain coding rules, which we will introduce later. ...is the function prototype declaration form in C language with variable number of actual parameters, but here it means that the third parameter is optional. For example, for the LED example just now, the third parameter can be used to specify which LED to turn on or off, such as 0 means LED0, 1 means LED1 and so on. Because the third formal parameter is of unsigned long type, in addition to passing a numeric value, a pointer can also be passed, so that any number of bytes of data can be exchanged with the kernel space.

Looking at the definition of the previous file_operations structure, the driver interface function corresponding to the ioctl system call is

unlocked_ioctl, there is another compat_ioctl, compat_ioctl is a function interface for processing 32-bit programs and 64-bit kernel compatible, and related to the architecture. The function prototype of unlocked_ioctl is as follows.

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

The first parameter is still the file structure pointer of the opened file, the second parameter and the second parameter of the system call

The third parameter corresponds to the request, and the third parameter corresponds to the third parameter of the system call function. It is said that the command used for ioctl needs to follow an encoding rule, so what is the encoding rule? In the current kernel source code version, the command follows Composed as follows.

 

​The above content is taken from the kernel document "Documentation/ioctl/ioctl-decoding.txt". That is to say, a command consists of 4 parts, and each part has a specified meaning and bit width limitation. The reason why the command is defined in this way, instead of simply using 0, 1, 2, ... to define the command, is to avoid the repetition of command definition, which will cause the application to misoperate and send a command to the driver that should not execute it. program, and the driver executes this command by mistake. With this mechanism, the driver has the opportunity to check whether the command belongs to the driver, which avoids the occurrence of this problem to a certain extent. The ideal requirement is that the magic number defined by bit 15 to bit 8 is globally unique under an architecture, but obviously, this is difficult to achieve. Nevertheless, we should follow the command definition form specified by the kernel. The kernel provides a set of macros to define commands and extract field information in commands. The code is as follows.

 

​The lowest-level macro used to define commands is _IOC, which merges 4 parts together by shifting. Suppose you want to define a command to set the serial port frame format, then according to the previous rules, this command must have parameters and write data to the drive, then the highest two bits are 01, if the parameter to be written is a struct option structure, and the structure occupies 12 bytes, then the decimal value of bit 29 to bit 16 should be 12, if the magic number is defined as the letter s, the command code is 2, and finally you should use _IOC(1,'s ',0,12) to define the command. However, the kernel also provides a more convenient macro. The command just now can be defined by _IOW('s',2,struct option). There are also 4 macros _IOC_DIR, _IOC_TYPE,

_IOC_NR and _IOC_SIZE to extract the 4 parts of the command respectively.

The definition of the command code in this program is as follows:

 

The program finally realizes the blinking of the LED light, changing the state every 1s

Program running

⚫ Add device tree

First of all, we need to add the device tree file corresponding to the LED in the kernel source code. It should be noted that due to the system

The LED configuration has been set by default, so we need to shield the original leds configuration item first.

Open the "arch/arm64/boot/dts/freescale/imx8mp-ai-robot-base.dts" file and block the leds configuration item.

Add the my_led device node, and add the following configuration under the device tree "/"

 

 

​ ⚫ Deploy device tree

Recompile the device tree. For this part, please refer to the "linux" compilation section. After the programming is successful, it will be in "arch/arm64/boot/d

ts/freescale/” directory to generate the “imx8mp-ai-robot-base.dtb” file, download the file to the open

Issue the board and start the system.

⚫ Compile the driver

Import "gpio_demo" under "Huaqing Vision-I.MX8M Plus Development Data-2021-06-02\Program Source Code" into the virtual machine

Before compiling the source code, you need to import the cross-compilation toolchain. The specific installation process has been introduced in the previous chapters, so I won’t go into details here. If you haven’t installed it yet, you can refer to the chapter "Cross-compilation Toolchain Installation"

linux@ubuntu:$ source /opt/fsl-imx-xwayland/5.4-zeus/environment-setup-aarch64-poky-lin

ux

linux@ubuntu:$ $CC --version

 

Enter the gpio_demo source code, modify the Makefile file, and modify the KERNELDIR variable to the path where the kernel source code is located. After the modification is complete, use the make command to compile

linux@ubuntu: $ few

 

Then compile the application code using the following command

linux@ubuntu: $CC ledapp.c -o ledapp

 

After the compilation is completed, the "imx8mp_led_driver.ko" file and the "ledapp" file will be generated in the current directory, copy the two files to the "/source/rootfs/" directory, use NFS to mount and start, and run on the target board up.

⚫ Run the application

First, you need to update the device tree configuration through tftp startup, and then mount the rootfs through the NFS network file system mount method.

After the startup is successful, go to the directory where the "imx8mp_led_driver.ko" and "ledapp" files are located, install the driver and run the application according to the following commands.

linux@ubuntu: insmod imx8mp_led_driver.ko

linux@ubuntu: ./ledapp

Guess you like

Origin blog.csdn.net/u014170843/article/details/130149939