本系列教程以「i.MX6ULL」處理器的ARM開發闆為實驗基礎,學習記錄嵌入式Linux開發的各種知識與經驗,主要内容包括嵌入式Linux移植,嵌入式Linux驅動開發,嵌入式Linux應用開發等。
本系列教程将以野火的i.MX6ULL eMMC開發闆為硬體基礎,以「野火EBF6ULL Pro開發闆教程」和「正點原子i.MX6ULL阿爾法開發闆教程」為參考,進行學習實踐。
1 嵌入式Linux移植概述
Linux 的移植主要包括3部分:
-
移植「bootloader 代碼」, Linux 系統要啟動就必須需要一個 bootloader 程式,也就說晶片上電以後先運作一段bootloader程式。 這段bootloader程式會先初始化DDR等外設, 然後将Linux核心從flash(NAND,NOR FLASH,SD,MMC 等)拷貝到 DDR 中,最後啟動 Linux 核心。 bootloader有很多,常用的就是 U-Boot。
bootloader 和 Linux 核心的關系就跟 PC 上的 BIOS 和 Windows 的關系一樣,bootloader 就相當于 BIOS。
- 移植「Linux 核心」,Linux核心由一系列程式組成,包括負責響應中斷的中斷服務程式、負責管理多個程序進而分享處理器時間的排程程式、負責管理位址空間的記憶體管理程式、網絡、程序間通信的系統服務程式等。核心負責管理系統的硬體裝置。
-
移植「根檔案系統(rootfs)」,Linux 中的根檔案系統更像是一個檔案夾或者叫做目錄,在這個目錄裡面會有很多的子目錄。根目錄下和子目錄中會有很多的檔案,這些檔案是 Linux 運作所必須的,比如庫、常用的軟體和指令、裝置檔案、配置檔案等等。根檔案系統裡面包含了一些最常用的指令和檔案。
「U-Boot、Linux kernel和rootfs」 這三者一起構成了一個完整的Linux系統,一個可以正常使用、功能完善的Linux系統。
2 實驗開發闆簡介
本測試使用的開發闆為野火的i.MX6ULL eMMC開發闆
3 U-Boot簡介
uboot 的全稱是「Universal Boot Loader」,遵循 GPL 協定的開源軟體。
uboot 是一個裸機代碼,可以看作是一個裸機綜合例程。現在的 uboot 已經支援「液晶屏、網絡、USB」等進階功能。uboot 官網為 https://www.denx.de/wiki/U-Boot/
可以在uboot官網下載下傳uboot源碼,點選左側 Topics 中的“
Source Code
”,然後點選的“
FTP Server
” ,進入其 FTP 伺服器即可看到 uboot 源碼。
但我們移植uboot時一般不會直接用 uboot 官方的源碼的,官方的源碼是給半導體廠商準備的,半導體廠商會根據自家的晶片,維護自己晶片對應的uboot。
NXP(freescale)維護的的uboot位址: https://github.com/Freescale/u-boot-fslc
4 NXP uboot測試
uboot移植并不需要從零開始将 uboot 移植到我們現在所使用的開發闆上。因為半導體廠商通常都會自己做一個開發闆「原廠開發闆」,将uboot移植到他們自己的原廠開發闆上,再将這個uboot(原廠BSP 包)釋出出去。
市面上的開發闆,通常會參考原廠的開發闆做硬體,然後在原廠提供的 BSP 包上做修改,如正點原子和野火的 I.MX6ULL 開發闆參考的就是
「NXP官方的I.MX6ULL EVK開發闆」做的硬體:
4.1 編譯環境搭建
4.1.1 交叉編譯器下載下傳
嵌入式Linux開發,程式編譯通常在電腦端的Linux(如虛拟機中的Ubuntu)下進行編譯,Ubuntu 自帶gcc 編譯器,但該編譯器是針對 X86 架構的!而嵌入式Linux是ARM架構的, 是以需要一個在 X86 架構上可以編譯 ARM 架構代碼的 gcc編譯器,即「交叉編譯器」。
交叉編譯器有很多,本實驗使用 Linaro 出品的交叉編譯器,下載下傳位址:
https://releases.linaro.org/components/toolchain/binaries/4.9-2017.01/arm-linux-gnueabihf/
4.1.2 交叉編譯器安裝
在Ubuntu中建立目錄:/usr/local/arm,并将下載下傳的
gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf.tar.xz
複制到此檔案中,然後「解壓」,解壓指令如下:
sudo tar -vxf gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf.tar.xz
解壓完成以後會生成一個名為“gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf”的檔案夾,這個檔案夾裡面就是我們的交叉編譯工具鍊。
然後,需要将該目錄「添加到環境變量」中。打開/etc/profile 以後,在最後面輸入如下所示内容:
export PATH=$PATH:/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin
使用交叉編譯器之前還需要「安裝其它的庫」,指令如下:
sudo apt-get install lsb-core lib32stdc++6
安裝完之後,可以「檢視交叉編譯工具的版本号」,輸入如下指令:
arm-linux-gnueabihf-gcc -v
可以看到類似如下列印
以看出目前交叉編譯器的版本号為 4.9.4,說明交叉編譯工具鍊安裝成功。
4.2 編譯原廠uboot
編譯前還要在Ubuntu 中「安裝ncurses 庫」,安裝指令如下:
sudo apt-get install libncurses5-dev
在Ubuntu中建立存放uboot的目錄,如我的目錄是:
/home/xxpcb/myTest/imx6ull/uboot/nxp_uboot
然後,将「NXP(freescale)的uboot源碼」複制進來,這裡使用的是「正點原子」提供的NXP官方原版Uboot源碼包( uboot-imx-rel_imx_4.1.15_2.1.0_ga.tar.bz2)
然後進行解壓:
tar -vxjf uboot-imx-rel_imx_4.1.15_2.1.0_ga.tar.bz2
解壓後的源碼檔案如下:
首先看下「uboot的配置」,configs 目錄下有很多跟 I.MX6UL/6ULL 有關的配置,找到與mx6ull相同的,如下圖。
因為我這個開發闆是emmc版本的,所有就使用這個
mx6ull_14x14_evk_emmc_defconfig
。
編譯uboot使用下面3條指令:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distcleanmake ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_evk_emmc_defconfigmake V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8
這3條指令中 :
-
設定目标為 arm 架構ARCH=arm
-
指定所使用的交叉編譯器。CROSS_COMPILE
- 第1條指令相當于
,目的是清除工程,一般在第一次編譯的時候最好清理一下工程。make distclean
- 第2條指令相當于
,用于配置 uboot,配置檔案為 mx6ull_14x14_evk_emmc_defconfig。make mx6ull_14x14_evk_emmc_defconfig
- 第3條指令相當于
,也就是使用8核來編譯uboot。make -j8
為了友善的執行着3條指令,可以「将這些指令寫成shell腳本」,比如在uboot源碼目錄下建立一個build.sh檔案,寫入如下内容:
#!/bin/bash
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distcleanmake ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_evk_emmc_defconfigmake V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8
然後進行編譯:
./build.sh
編譯完成以後uboot 源碼多了一些檔案,其中
u-boot.bin
就是編譯出來的 uboot二進制檔案。 uboot是個裸機程式, 是以需要在其前面
加上頭部(IVT、 DCD等資料)才能在I.MX6U上執行,
u-boot.imx
檔案就是添加頭部以後的 u-boot.bin。
u-boot.imx 就是我們最終要燒寫到開發闆中的 uboot 鏡像檔案。
4.3 燒錄開發闆
這是的燒錄開發闆,實際是要「燒錄到SD卡中」,然後将SD卡插入開發闆,讓開發闆從SD卡啟動(需要在開發闆上設定撥碼開關來選擇啟動方式)。
4.3.1 燒錄到SD卡
「正點原子」專門編寫了一個小軟體用來将編譯出來的.bin 檔案燒寫到 SD 卡中,這個軟體叫做“
imxdownload
”
将imxdownload 複制到 Ubuntu 中的uboot源碼檔案夾,再使用如下指令,給予 imxdownload 可執行權限:
chmod 777 imxdownload
然後「電腦USB中插入SD卡(讀卡器)」,并在虛拟機中設定usb加載(VMware或VirtualBox虛拟機需要先安裝「增強功能」才能使用)
然後可以使用如下指令來檢視SD卡的挂載辨別符:
ls /dev/sd*
檢視輸出結果:
這裡的
/dev/sdb
就是我的SD卡。
❝
注:我第一次使用SD卡燒錄時,隻多出了/dev/sdb,但不知什麼情況,用了幾次後,再插入SD卡,就會同時多出來/dev/sdb和/dev/sdb1,但實際測 試,仍然把程式燒錄到/dev/sdb也能用)。
❞
imxdownload向SD卡燒寫led.bin檔案,指令格式如下:
./imxdownload u-boot.bin /dev/sdb
注意不能燒寫到或
/dev/sda
裝置裡面!那是系統磁盤。
sda1
燒寫過程會輸入如下資訊:
燒寫的最後一行會顯示燒寫大小、用時和速度,比如
u-boot.bin
燒寫到SD卡中的大小是 423KB,用時 1.7s,燒寫速度是 236KB/s。
注意這個燒寫速度,如果這個燒寫速度在幾百KB/s以下那麼就是正常燒寫。 如果這個燒寫速度大于幾十MB/s、甚至幾百MB/s那麼肯定是燒寫失敗了! 重新插拔/格式化SD卡或重新開機ubuntu再試。
燒寫完成以後會在目前工程目錄下生成一個
load.imx
的檔案,這個檔案就是軟體 imxdownload 根據 NXP 官方啟動方式介紹的内容, 在 bin 檔案前面添加了一些資料頭以後生成的。最終燒寫到SD卡裡面的就是這個imx檔案。
4.3.2 啟動開發闆
燒錄完之後,将「SD卡插入開發闆啟動」,使用「序列槽連接配接電腦」,檢視uboot啟動資訊。設定好序列槽參數(波特率115200)并打開,按鍵
「複位開發闆」。當序列槽列印上出現
Hit any key to stop autoboot
倒計時的時候「按下鍵盤上的Enter鍵」
,預設是 3 秒倒計時,在 3 秒倒計時結束以後如果沒有按下Enter鍵的話 uboot 就會使用預設參數來啟動 Linux 核心了。
如果在 3 秒倒計時結束之前按下Enter鍵,那麼就會進入 uboot 的指令行模式:
解讀一下這些資訊的含義:
- 第1行是 uboot 「版本号和編譯時間」:目前的 uboot 版本号是 2016.03,編譯時間是 2021/7 /11/15:22:25
- 第3、4 行是 「CPU 資訊」:目前使用的 CPU 是飛思卡爾(屬于NXP)的 I.MX6ULL (頻率為 792MHz),此時運作在 396MHz。這顆晶片是工業級的,結溫為-40°C~105°C
- 第 5 行是「複位原因」:I.MX6ULL 晶片上有個 POR_B 引腳,将這個引腳拉低即可複位 I.MX6ULL。
- 第 6 行是「闆子名字」,“MX6ULL 14x14 EVK”即NXP原廠開發闆的名字 。
- 第 7 行提示 「I2C 準備就緒」。
- 第 8 行提示目前闆子的「DRAM(記憶體)」 為 512MB
- 第 9 行提示目前有「兩個MMC/SD 卡控制器」:FSL_SDHC(0)和 FSL_SDHC(1)。I.MX6ULL支援兩個 MMC/SD,正點原子的 I.MX6ULL EMMC 核心闆上 FSL_SDHC(0)接的 SD(TF)卡,FSL_SDHC(1)接的 EMMC。
- 第10行是一條警告資訊,先忽略。
- 第 12、13 行是 「LCD 型号」,原廠預設的是TFT43AB (480x272)。
- 第 14~16 是「标準輸入、标準輸出和标準錯誤」所使用的終端,這裡都使用序列槽(serial)作為終端。
- 第 17 、18行是「切換到emmc的第0個分區上」,因為目前的 uboot 是 emmc 版本的,也就是從 emmc 啟動的。我們隻是為了友善将其燒寫到了 SD 卡上,但是它的“内心”還是 EMMC的。是以 uboot 啟動以後會将 emmc 作為預設存儲器 。
- 第 19行是「網口資訊」,提示我們目前使用的 FEC1 這個網口,I.MX6ULL 支援兩個網口。
- 第 20行提示「FEC1網卡位址沒有設定」(後面我們會講解如何在uboot 裡面設定網卡位址)。
- 第 22行提示「正常啟動」, 也就是說 uboot要從emmc裡面讀取環境變量和參數資訊啟動 Linux核心了。
- 第23行是「倒計時提示」,預設倒計時 3 秒,倒計時結束之前按下Enter鍵就會進入 Linux 指令行模式。如果在倒計時結束以後沒有按下Enter鍵,那麼 Linux 核心就會啟動,Linux 核心一旦啟動,uboot 就運作結束了。
-
第23行是在倒計時 3 秒内按了Enter鍵,符号=>
表示可以繼續與uboot進行「指令互動」。
看過了序列槽的uboot資訊,再來看一下闆子是實際運作情況:
由于原廠的uboot驅動的螢幕是TFT43AB (480x272),與我這裡螢幕不一樣,是以「螢幕沒有正常顯示」(現在的螢幕看起來有許多彩色的小點點),接下來,就是對uboot進行螢幕驅動的修改。
在本篇結束之前,再來研究一下uboot的序列槽指令。
4.4 uboot指令初探
上面說道,在uboot啟動的3 秒倒計時内,序列槽界面如果按下了Enter鍵,uboot就會輸出符号
=>
,則「可以繼續與uboot進行指令互動」。那可以輸入哪些指令呢?
4.4.1 help指令檢視所有指令
輸入
help
?
,然後按下回車即可檢視目前 uboot 所支援的指令:
=> help? - alias for 'help'base - print or set address offsetbdinfo - print Board Info structurebmode - sd1|sd2|qspi1|normal|usb|sata|ecspi1:0|ecspi1:1|ecspi1:2|ecspi1:3|esdhc1|esdhc2|esdhc3|esdhc4 [noreset]bmp - manipulate BMP image databoot - boot default, i.e., run 'bootcmd'bootd - boot default, i.e., run 'bootcmd'bootelf - Boot from an ELF image in memorybootm - boot application image from memorybootp - boot image via network using BOOTP/TFTP protocolbootvx - Boot vxWorks from an ELF imagebootz - boot Linux zImage image from memoryclocks - display clocksclrlogo - fill the boot logo area with blackcmp - memory compareconinfo - print console devices and informationcp - memory copycrc32 - checksum calculationdcache - enable or disable data cachedhcp - boot image via network using DHCP/TFTP protocoldm - Driver model low level accessecho - echo args to consoleeditenv - edit environment variableenv - environment handling commandserase - erase FLASH memoryexit - exit scriptext2load- load binary file from a Ext2 filesystemext2ls - list files in a directory (default /)ext4load- load binary file from a Ext4 filesystemext4ls - list files in a directory (default /)ext4size- determine a file's size
ext4write- create a file in the root directory
false - do nothing, unsuccessfully
fatinfo - print information about filesystem
fatload - load binary file from a dos filesystem
fatls - list files in a directory (default /)
fatsize - determine a file's sizefdt - flattened device tree utility commandsflinfo - print FLASH memory informationfstype - Look up a filesystem typefuse - Fuse sub-systemgo - start application at address 'addr'gpio - query and control gpio pinshelp - print command description/usagei2c - I2C sub-systemicache - enable or disable instruction cacheiminfo - print header information for application imageimxtract- extract a part of a multi-imageitest - return true/false on integer compareload - load binary file from a filesystemloadb - load binary file over serial line (kermit mode)loads - load S-Record file over serial lineloadx - load binary file over serial line (xmodem mode)loady - load binary file over serial line (ymodem mode)loop - infinite loop on address rangels - list files in a directory (default /)md - memory displaymdio - MDIO utility commandsmii - MII utility commandsmm - memory modify (auto-incrementing address)mmc - MMC sub systemmmcinfo - display MMC infomtest - simple RAM read/write testmw - memory write (fill)nfs - boot image via network using NFS protocolnm - memory modify (constant address)ping - send ICMP ECHO_REQUEST to network hostpmic - PMICprintenv- print environment variablesprotect - enable or disable FLASH write protectionreset - Perform RESET of the CPUrun - run commands in an environment variablesave - save file to a filesystemsaveenv - save environment variables to persistent storagesetenv - set environment variablessetexpr - set environment variable as the result of eval expressionsf - SPI flash sub-systemshowvar - print local hushshell variablessize - determine a file's size
sleep - delay execution for some time
source - run script from memory
test - minimal test like /bin/sh
tftpboot- boot image via network using TFTP protocol
true - do nothing, successfully
usb - USB sub-system
usbboot - boot from USB device
version - print monitor, compiler and linker version
=>
4.4.2 檢視指令的使用說明
指令的具體使用方法,可以輸入
help 指令名
? 指令名
檢視,以“bootz”這個指令為例:
=> help bootzbootz - boot Linux zImage image from memory
Usage:bootz [addr [initrd[:size]] [fdt]] - boot Linux zImage stored in memory The argument 'initrd' is optional and specifies the address of the initrd in memory. The optional argument ':size' allows specifying the size of RAW initrd. When booting a Linux kernel which requires a flat device-tree a third argument is required which is the address of the device-tree blob. To boot that kernel without an initrd image, use a '-' for the second argument. If you do not pass a third a bd_info struct will be passed instead
=>
4.4.3 資訊查詢指令
常用的和資訊查詢有關的指令有3個:
bdinfo
、
printenv
和
version
- bdinfo 闆子資訊
=> bdinfoarch_number = 0x00000000boot_params = 0x80000100DRAM bank = 0x00000000-> start = 0x80000000-> size = 0x20000000eth0name = FEC1ethaddr = (not set)current eth = FEC1ip_addr = <NULL>baudrate = 115200 bpsTLB addr = 0x9FFF0000relocaddr = 0x9FF47000reloc off = 0x18747000irq_sp = 0x9EF44EA0sp start = 0x9EF44E90FB base = 0x00000000=>
從列印資訊可以得出DRAM的「起始位址和大小、啟動參數儲存起始位址、波特率、sp(堆棧指針)起始位址」等資訊.
- printenv 列印環境變量
=> printenvbaudrate=115200board_name=EVKboard_rev=14X14boot_fdt=trybootcmd=run findfdt;mmc dev ${mmcdev};mmc dev ${mmcdev}; if mmc rescan; then if run loadbootscript; then run bootscript; else if run loadimage; thn run mmcboot; else run netboot; fi; fi; else run netboot; fibootcmd_mfg=run mfgtool_args;bootz ${loadaddr} ${initrd_addr} ${fdt_addr};bootdelay=3bootscript=echo Running bootscript from mmc ...; sourceconsole=ttymxc0ethact=FEC1ethprime=FECfdt_addr=0x83000000fdt_file=undefinedfdt_high=0xfffffffffindfdt=if test $fdt_file = undefined; then if test $board_name = EVK && test $board_rev = 9X9; then setenv fdt_file imx6ull-9x9-evk.dtb; fi; if tst $board_name = EVK && test $board_rev = 14X14; then setenv fdt_file imx6ull-14x14-evk.dtb; fi; if test $fdt_file = undefined; then echo WARNING:Could not determine dtb to use; fi; fi;image=zImageinitrd_addr=0x83800000initrd_high=0xffffffffip_dyn=yesloadaddr=0x80800000loadbootscript=fatload mmc ${mmcdev}:${mmcpart} ${loadaddr} ${script};loadfdt=fatload mmc ${mmcdev}:${mmcpart} ${fdt_addr} ${fdt_file}loadimage=fatload mmc ${mmcdev}:${mmcpart} ${loadaddr} ${image}mfgtool_args=setenv bootargs console=${console},${baudrate} rdinit=/linuxrc g_mass_storage.stall=0 g_mass_storage.removable=1 g_mass_storage.file=fat g_mass_storage.ro=1 g_mass_storage.idVendor=0x066F g_mass_storage.idProduct=0x37FF g_mass_storage.iSerialNumber="" clk_ignore_unusedmmcargs=setenv bootargs console=${console},${baudrate} root=${mmcroot}mmcautodetect=yesmmcboot=echo Booting from mmc ...; run mmcargs; if test ${boot_fdt} = yes || test ${boot_fdt} = try; then if run loadfdt; then bootz ${loadaddr} -${fdt_addr}; else if test ${boot_fdt} = try; then bootz; else echo WARN: Cannot load the DT; fi; fi; else bootz; fi;mmcdev=0mmcpart=1mmcroot=/dev/mmcblk0p2 rootwait rwnetargs=setenv bootargs console=${console},${baudrate} root=/dev/nfs ip=dhcp nfsroot=${serverip}:${nfsroot},v3,tcpnetboot=echo Booting from net ...; run netargs; if test ${ip_dyn} = yes; then setenv get_cmd dhcp; else setenv get_cmd tftp; fi; ${get_cmd} ${imag}; if test ${boot_fdt} = yes || test ${boot_fdt} = try; then if ${get_cmd} ${fdt_addr} ${fdt_file}; then bootz ${loadaddr} - ${fdt_addr}; else if est ${boot_fdt} = try; then bootz; else echo WARN: Cannot load the DT; fi; fi; else bootz; fi;panel=TFT43ABscript=boot.scr
Environment size: 2431/8188 bytes=>
這裡有很多的環境變量, 比如「baudrate、 board_name、 board_rec、 boot_fdt、 bootcmd」等。比如bootdelay這個環境變量就表示 uboot 啟動延時時間,預設 bootdelay=3,也就預設延時 3秒。前面說的 3 秒倒計時就是由 bootdelay 定義的。另外uboot中的環境變量都是字元串。
- version 版本資訊
=> version
U-Boot 2016.03 (Jul 11 2021 - 15:22:25 +0800)arm-linux-gnueabihf-gcc (Linaro GCC 4.9-2017.01) 4.9.4GNU ld (Linaro_Binutils-2017.01) 2.24.0.20141017 Linaro 2014_11-3-git=>