前面已经移植了TF-A、Uboot和Linux kernel,就剩最后一个 rootfs(根文件系统)了,本章就来学习一下根文件系统的组成以及如何构建根文件系统。这是Linux系统移植的最后一步,根文件系统构建好以后就意味着拥有了一个完整的、可以运行的最小系统 。以后就在这个最小系统上编写、测试Linux驱动,移植一些第三方组件,逐步的完善这个最小系统。最终得到一个功能完善、驱动齐全、相对完善的操作系统。
根文件系统一般也叫做rootfs,那么什么叫根文件系统?看到“文件系统”这四个字,一般就是FATFS、FAT、EXT4、YAFFS和NTFS等这样的文件系统。在这里,根文件系统并不是以上这些文件系统代码,在学单片机的时候听到的FATFS是具体的文件系统代码,这个是属于Linux内核的一部分。Linux中的根文件系统更像是一个文件夹或者叫做目录,在这个目录里面会有很多的子目录。根目录和子目录中会有很多的文件,这些文件是Linux运行所必须的,比如库、常用的软件和命令、设备文件、配置文件等等。以后说到文件系统,如果不特别指明,统一表示根文件系统。对于根文件系统专业的解释,百度百科上是这么说的:
根文件系统首先是内核启动时所mount(挂载)的第一个文件系统,内核代码映像文件保存在根文件系统中 ,而系统引导启动程序会在根文件系统挂载之后从中把一些基本的初始化脚本和服务等加载到内存中去运行。
百度百科上说内核代码镜像文件保存在根文件系统中,但是嵌入式Linux并没有将内核代码镜像保存在根文件系统中,而是保存到了其他地方。比如NAND Flash的指定存储地址、EMMC专用分区中。根文件系统是Linux内核启动以后挂载(mount)的第一个文件系统,然后从根文件系统中读取初始化脚本,比如rcS、inittab等。根文件系统和Linux内核是分开的,单独的Linux内核是没法正常工作的,必须要搭配根文件系统。如果不提供根文件系统,Linux内核在启动的时候就会提示内核崩溃(Kernel panic)的提示,这个在之前的笔记中已经有看到过了。
根文件系统的这个“根”字就说明了这个文件系统的重要性,它是其他文件系统的根,没有这个“根”,其他的文件系统或者软件就不能工作。比如用的ls、mv、ifconfig等命令其实就是一个个小软件,只是这些软件没有图形界面,而且需要通过输入命令的形式来运行。这些小软件就保存在根文件系统中,这些小软件是怎么来的呢?这个就是本章教程的目的,构建自己的根文件系统, 这个根文件系统是满足Linux运行的最小根文件系统,后续可以根据自己的实际工作需求不断的去填充这个最小根文件系统,最终使其成为一个相对完善的根文件系统。
在构建根文件系统之前,先来看一下根文件系统里面大概都有些什么内容,以Ubuntu为例,根文件系统的目录名字为‘ ‘/’,可以直接cd进去,进入后输入“ls”查看根目录下的内容,结果如图所示:
接下来看一下常用的子目录。
看到“bin”应该能想到bin文件,bin文件就是可执行文件。所以此目录下存放着系统需要的可执行文件,一般都是一些命令,比如ls、mv等命令。此目录下的命令所有的客户都可以使用。
dev是device的缩写,所以此目录下的文件都是和设备有关的,此目录下的文件都是设备文件。在Linux下一切皆文件,即使是硬件设备,也是以文件的形式存在的,比如/dev/ttySTM0(STM32MP1根目录会有此文件)就表示STM32MP157的串口4,要想通过串口4发送或者接收数据就要操作文件/dev/ttySTM0,通过对文件/dev/ttySTM0的读写操作来实现串口4的数据收发。
此目录下存放着各种配置文件,可以进入Ubuntu的/etc目录看一下,里面的配置文件非常多!但是在嵌入式Linux下此目录会很简洁。
lib是library的简称,也就是库的意思,因此此目录下存放着Linux所必须的库文件。这些库文件是共享库,命令和用户编写的应用程序要使用这些库文件。
临时挂载目录,一般是空目录,可以在此目录下创建空的子目录,比如/mnt/sd、/mnt/usb这样就可以将SD卡或者U盘挂载到/mnt/sd或者/mnt/usb目录中。
此目录一般是空的,当Linux系统启动以后会将此目录作为proc文件系统的挂载点,proc是个虚拟文件系统,没有实际的存储设备。proc里面的文件都是临时存在的, 一般用来存储系统运行信息文件。
要注意,usr不是user的缩写,而是Unix Software Resource的缩写,也就是Unix操作系统软件资源目录。Linux一般被成为类Unix操作系统,苹果的MacOS也是类Unix操作系统。既然是软件资源目录,因此/usr目录下也存放着很多软件,一般系统安装完成以后此目录占用的空间最多。
此目录存放一些可以改变的数据。
此目录也用于存放一些可执行文件,但是此目录下的文件或者说命令只有管理员才能使用,主要用户系统管理。
系统启动以后此目录作为sysfs文件系统的挂载点,sysfs是一个类似于proc文件系统的特殊文件系统,sysfs也是基于ram的文件系统,也就是说它也没有实际的存储设备。此目录是系统设备管理的重要目录,此目录通过一定的组织结构向用户提供详细的内核数据结构信息。
可选的文件、软件存放区,由用户选择将哪些文件或软件放到此目录中。
关于Linux的根目录就介绍到这里,接下来的构建根文件系统就是研究如何创建上面这些子目录以及子目录中的文件。
上一小节说了,根文件系统里面就是一堆的可执行文件和其他文件组成的,那么其实需要一种工具,负责“收集”这些文件,然后将其打包,给开发者可以直接拿来用。答案是有的,它就叫做BusyBox!其名字分为“Busy”和“Box”,也就是忙碌的盒子。盒子是用来放东西的,忙碌的是因为它要提供根文件系统所需的文件,所以忙碌。 BusyBox是一个集成了大量的Linux命令和工具的软件,像ls、mv、ifconfig等命令BusyBox都会提供。 BusyBox就是一个大的工具箱,这个工具箱里面集成了Linux的许多工具和命令。一般下载 BusyBox的源码,然后配置 BusyBox,选择自己想要的功能,最后编译即可。
BusyBox可以在其官网下载到,官网地址为:BusuBox官网官网比较简陋,如下图所示:
在官网左侧的“Get BusyBox”栏有一行“Download Source”,点击“Download Source”即可打开BusyBox的下载页,如下图所示:
从上图可以看出,正点原子教程写出来的时候最新的BusyBox版本是1.32.0,下载busybox-1.32.0.tar.bz2这个压缩包即可,正点原子是提供的,并且将其放到了开发板光盘中,就是busybox-1.32.0.tar.bz2。BusyBox准备好以后就可以构建根文件系统了。
一般在Linux驱动开发的时候都是通过nfs挂载根文件系统的,当产品最终上市开卖的时候才会将根文件系统烧写到EMMC或者NAND中。所以要在 之前设置的nfs服务器目录中创建一个名为rootfs的子目录使用如下命令创建名为rootfs的子目录:
cd /home/zuozhongkai/linux/nfs mkdir rootfs |
创建好的rootfs子目录就用来存放根文件系统了。
将busybox-1.32.0.tar.bz2发送到Ubuntu中,存放位置随便选择。然后使用如下命令将其解压:
tar -vxjf busybox-1.32.0.tar.bz2 |
解压完成以后进入到busybox-1.32.0目录中,此目录中的文件和文件夹如下图所示:
同Uboot和Linux移植一样,打开busybox的顶层 Makefile,添加ARCH和CROSS_COMPILE的值,如下所示:
示例代码18.2.2.1 Makefile代码段
164 CROSS_COMPILE ?= /usr/local/arm/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf/bin/arm-none-linux-gnueabihf-
......
190 ARCH ?= arm
上述示例代码中CROSS_COMPILE使用了绝对路径,来防止编译出错。
如果默认直接编译busybox的话,在使用SecureCRT的时候中文字符是显示不正常的,中文字符会显示为“?”,比如中文目录,中文文件都显示为“?”。不知道从哪个版本开始busybox中的shell命令对中文输入即显示做了限制,即使内核支持中文但shell下也依然无法正确显示。
所以需要修改usybox源码,取消busybox对中文显示的限制,打开文件busybox-1.32.0/libbb/printable_string.c,找到函数printable_string2,缩减后的函数内容如下:
示例代码18.2.2.2 libbb/printable_string.c代码段
12 const char* FAST_FUNC printable_string2(uni_stat_t *stats, const char *str)
13 {
14 char *dst;
15 const char *s;
16
17 s = str;
18 while (1) {
19 unsigned char c = *s;
20 if (c == '\0') {
21 /* 99+% of inputs do not need conversion */
22 if (stats) {
23 stats->byte_count = (s - str);
24 stats->unicode_count = (s - str);
25 stats->unicode_width = (s - str);
26 }
27 return str;
28 }
29 if (c < ' ')
30 break;
31 if (c >= 0x7f)
32 break;
33 s++;
34 }
35
36 #if ENABLE_UNICODE_SUPPORT
37 dst = unicode_conv_to_printable(stats, str);
38 #else
39 {
40 char *d = dst = xstrdup(str);
41 while (1) {
42 unsigned char c = *d;
43 if (c == '\0')
44 break;
45 if (c < ' ' || c >= 0x7f)
46 *d = '?';
47 d++;
48 }
49 if (stats) {
50 stats->byte_count = (d - dst);
51 stats->unicode_count = (d - dst);
52 stats->unicode_width = (d - dst);
53 }
54 }
55 #endif
56 return auto_string(dst);
57 }
第31和32行,当字符大于0X7F以后就跳出去了。
第45和46行,如果支持UNICODE码的话,当字符大于0X7F就直接输出‘?’。
所以需要对这 4行代码进行修改,修改以后如下所示:
示例代码18.2.2.3 libbb/printable_string.c代码段
12 const char* FAST_FUNC printable_string2(uni_stat_t *stats, const char *str)
13 {
14 char *dst;
15 const char *s;
16
17 s = str;
18 while (1) {
19 unsigned char c = *s;
20 if (c == '\0') {
21 /* 99+% of inputs do not need conversion */
22 if (stats) {
23 stats->byte_count = (s - str);
24 stats->unicode_count = (s - str);
25 stats->unicode_width = (s - str);
26 }
27 return str;
28 }
29 if (c < ' ')
30 break;
31 /* 注释掉下面这个两行代码 */
32 /* if (c >= 0x7f)
33 break; */
34 s++;
35 }
36
37 #if ENABLE_UNICODE_SUPPORT
38 dst = unicode_conv_to_printable(stats, str);
39 #else
40 {
41 char *d = dst = xstrdup(str);
42 while (1) {
43 unsigned char c = *d;
44 if (c == '\0')
45 break;
46 /* 修改下面代码 */
47 /* if (c < ' ' || c >= 0x7f) */
48 if( c < ' ')
49 *d = '?';
50 d++;
51 }
52 if (stats) {
53 stats->byte_count = (d - dst);
54 stats->unicode_count = (d - dst);
55 stats->unicode_width = (d - dst);
56 }
57 }
58 #endif
59 return auto_string(dst);
60 }
示例代码18.2.2.3中修改的内容,主要就是禁止字符大于0X7F以后break和输出‘?’。
接着打开文件busybox-1.32.0/libbb/unicode.c,找到如下内容:
示例代码18.2.2.4 libbb/unicode.c代码段
1009 static char* FAST_FUNC unicode_conv_to_printable2(uni_stat_t *stats, const char *src, unsigned width, int flags)
1010 {
1011 char *dst;
1012 unsigned dst_len;
1013 unsigned uni_count;
1014 unsigned uni_width;
1015
1016 if (unicode_status != UNICODE_ON) {
1017 char *d;
1018 if (flags & UNI_FLAG_PAD) {
1019 d = dst = xmalloc(width + 1);
1020 while ((int)--width >= 0) {
1021 unsigned char c = *src;
1022 if (c == '\0') {
1023 do
1024 *d++ = ' ';
1025 while ((int)--width >= 0);
1026 break;
1027 }
1028 *d++ = (c >= ' ' && c < 0x7f) ? c : '?';
1029 src++;
1030 }
1031 *d = '\0';
1032 } else {
1033 d = dst = xstrndup(src, width);
1034 while (*d) {
1035 unsigned char c = *d;
1036 if (c < ' ' || c >= 0x7f)
1037 *d = '?';
1038 d++;
1039 }
1040 }
1041 if (stats) {
1042 stats->byte_count = (d - dst);
1043 stats->unicode_count = (d - dst);
1044 stats->unicode_width = (d - dst);
1045 }
1046 return dst;
1047 }
......
1139 return dst;
1140 }
第1028行,当字符大于0X7F以后, ,*d++就为‘?’。
第1036和1037行,当字符大于0X7F以后,*d也未’?'。
修改示例代码18.2.2.4,修改后如下所示:
示例代码18.2.2.5 libbb/unicode.c代码段
1009 static char* FAST_FUNC unicode_conv_to_printable2(uni_stat_t *stats, const char *src, unsigned width, int flags)
1010 {
1011 char *dst;
1012 unsigned dst_len;
1013 unsigned uni_count;
1014 unsigned uni_width;
1015
1016 if (unicode_status != UNICODE_ON) {
1017 char *d;
1018 if (flags & UNI_FLAG_PAD) {
1019 d = dst = xmalloc(width + 1);
1020 while ((int)--width >= 0) {
1021 unsigned char c = *src;
1022 if (c == '\0') {
1023 do
1024 *d++ = ' ';
1025 while ((int)--width >= 0);
1026 break;
1027 }
1028 /* 修改下面一行代码 */
1029 /* *d++ = (c >= ' ' && c < 0x7f) ? c : '?'; */
1030 *d++ = (c >= ' ') ? c : '?';
1031 src++;
1032 }
1033 *d = '\0';
1034 } else {
1035 d = dst = xstrndup(src, widh);
1036 while (*d) {
1037 unsigned char c = *d;
1038 /* 修改下面一行代码 */
1039 /* if (c < ' ' || c >= 0x7f) */
1040 if(c < ' ')
1041 *d = '?';
1042 d++;
1043 }
1044 }
1045 if (stats) {
1046 stats->byte_count = (d - dst);
1047 stats->unicode_count = (d - dst);
1048 stats->unicode_width = (d - dst);
1049 }
1050 return dst;
1051 }
......
1143 return dst;
1144 }
示例代码18.2.2.5中的修改内容,同样主要是禁止字符大于0X7F的时候设置为‘?’。busybox中文字符支持跟代码修改有关的就改好了,最后还需要配置busybox来使能unicode码,这个稍后配置busybox的时候再设置。
和之前编译Uboot、Linux kernel一样,要先对busybox进行默认的配置,有以下几种配置选项:
一般使用默认配置即可,因此使用如下命令先使用默认配置来配置一下busybox:
make defconfig |
busybox也支持图形化配置,通过图形化配置可以进一步选择自己想要的功能,输入如下命令打开图形化配置界面:
make menuconfig |
Location: -> Settings -> Build static binary (no shared libs) |
选项“Build static binary (no shared libs)”用来决定是静态编译是动态编译,静态编译的话就不需要库文件,但是编译出来的根文件系统会很大。动态编译要求根文件系统中有库文件,但是编译出来的 busybox会小很多。这不能采用静态编译!因为采用静态编译的话DNS会出问题!无法进行域名解析,配置如下图所示:
继续配置如下路径配置项:
Location: -> Settings -> vi-style line editing commands |
Location: -> Linux Module Utilities -> Simplified modutils |
默认会选中“Simplified modutils”,这里要取消勾选!!结果如下图所示:
继续配置如下路径配置项:
Location: -> Linux System Utilities -> mdev (17 kb) //确保下面的全部选中,默认都是选中的 |
结果如下图所示:
最后就是使能busybox的unicode编码以支持中文,配置路径如下:
Location: -> Settings -> Support Unicode //选中 -> Check $LC_ALL, $LC_CTYPE and $LANG environment variables //选中 |
结果如下图所示:
busybox的配置就到此结束了,也可以根据自己的实际需求选择配置其他的选项。
使用图形化修改过Busybox的配置以后最好保存一下配置文件,以防“make clean”以后删除掉以前的配置,在配置主界面上,选中“Save Configuration to an Alternate File”,如下图所示:
选中以后会需要输入配置文件名,Busybox的默认配置文件都是保存到configs目录下,配置文件的名字后缀必须是“_defconfig”,这里设置配置文件名字为“stm32mp1_atk_defconfig”,如下图所示:
点击上图中“Ok”即可完成保存,以后要使用自己的配置文件,直接输入以下命令:
make stm32mp1_atk_defconfig |
配置好busybox以后就可以编译了,可以指定编译结果的存放目录,肯定要将编译结果存放到前面创建的rootfs目录中,输入如下命令:
make make install CONFIG_PREFIX=/home/zuozhongkai/linux/nfs/rootfs |
COFIG_PREFIX指定编译结果的存放目录,比如正点原子的文档中存放到“/home/zuozhongkai/linux/nfs/rootfs”目录中,等待编译完成。编译完成以后如下图所示:
编译完成后会在busybox的工具和文件都会被安装到rootfs目录中。可以看到,rootfs目录下有bin、sbin和usr这三个目录,以及linuxrc这个文件。前面说过Linux内核init进程最后会查找用户空间的 init程序,找到以后就会运行这个用户空间的init程序,从而切换到用户态。如果bootargs设置init=/linuxrc,那么linuxrc就是可以作为用户空间的 init程序,所以用户态空间的init程序是busybox来生成的。
busybox的工作就完成了,但是此时的根文件系统还不能使用,还需要一些其他的文件,还需要继续完善rootfs。
Linux中的应用程序一般都是需要动态库的,当然也可以编译成静态的,但是静态的可执行文件会很大。如果编译为动态的话就需要动态库,所以需要向根文件系统中添加动态库。在rootfs中创建一个名为“lib”的文件夹,命令如下:
mkdir lib |
lib文件夹创建好了,库文件从哪里来呢?lib库文件从交叉编译器中获取,前面搭建交叉编译环境的时候将交叉编译器存放到了“/usr/local/arm/”目录中。交叉编译器里面有很多的库文件,可以直接把所有的库文件都放到根文件系统中。进入如下路径对应的目录:
/usr/local/arm/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf/arm-none-linux-gnueabihf/libc/lib |
此目录下有很多的*so*(*是通配符)文件,这些就是库文件,将此目录下所有的*so*文件都拷贝到rootfs/lib目录中,拷贝命令如下:
cp *so* /home/zuozhongkai/linux/nfs/rootfs/lib/ -d |
后面的“-d”表示拷贝符号链接,这里有个比较特殊的库文件ld-linux-armhf.so.3,此库文件也是个符号链接,相当于Windows下的快捷方式。会链接到库ld-2.30.so上,输入命令“ls ld-linux-armhf.so.3 -l”查看此文件详细信息,如图下图所示:
从上图可以看出,ld-linux-armhf.so.3后面有个“->”,表示其是个软连接文件,链接到文件ld-2.30.so,因为是一个“快捷方式”,因此大小只有10B。但是,ld-linux-armhf.so.3不能作为符号链接,否则的话在根文件系统中执行程序无法执行!所以需要ld-linux-armhf.so.3完成逆袭,由“快捷方式”变为“本尊”,方法很简单,那就是重新复制ld-linux-armhf.so.3,只是不复制软链接即可,先将rootfs/lib中的ld-linux-armhf.so.3文件删除掉,命令如下:
rm ld-linux-armhf.so.3 |
然后重新进入到/usr/local/arm/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf/arm-none-linux-gnueabihf/libc/lib目录中,重新拷贝ld-linux-armhf.so.3,命令如下:
cp ld-linux-armhf.so.3 /home/zuozhongkai/linux/nfs/rootfs/lib/ |
拷贝完成以后再到rootfs/lib目录下查看ld-linux-armhf.so.3文件详细信息,如下图所示:
从上图可以看出,此时ld-linux-armhf.so.3已经不是软连接了,而是实实在在的一个库文件,而且文件大小为1279392B。
继续进入如下目录中:
/usr/local/arm/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf/arm-none-linux-gnueabihf/lib |
此目录下也有很多的的*so*和.a库文件,将其也拷贝到rootfs/lib目录中,命令如下:
cp *so* *.a /home/zuozhongkai/linux/nfs/rootfs/lib/ -d |
rootfs/lib目录的库文件就这些了,完成以后的rootfs/lib目录如下图所示:
在rootfs的usr目录下创建一个名为lib的目录,将如下目录中的库文件拷贝到rootfs/usr/lib目录下:
/usr/local/arm/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf/arm-none-linux-gnueabihf/libc/usr/lib |
将此目录下的so和.a库文件都拷贝到rootfs/usr/lib目录中,命令如下:
cp *so* *.a /home/zuozhongkai/linux/nfs/rootfs/usr/lib/ -d |
完成后的rootfs/usr/lib目录如下图所示:
至此,根文件系统的库文件就全部添加好了,可以使用“du”命令来查看一下rootfs/lib和rootfs/usr/lib这两个目录的大小,命令如下:
cd rootfs //进入根文件系统目录 du ./lib ./usr/lib/ -sh //查看 lib和 usr/lib这两个目录的大小 |
结果如下图所示:
可以看出lib和usr/lib这两个文件的大小分别为158MB和89MB,加起来就是158+89=247MB,还是挺大的,但是正点原子STM23MP157开发板板载8GB的EMMC,无须担心存储不够。
在根文件系统中创建其他文件夹,如dev、proc、mnt、sys、tmp、etc和root等,创建完成以后如下图所示:
目前看起来已经完成了准备,可以测试一下。
测试根文件系统的时候不是直接烧写到EMMC里面,这样测试效率太低了,Ubuntu的rootfs目录已经保存了根文件系统,只需要在开发板上通过nfs挂载Ubuntu下的rootfs目录即可。也就是说,根文件系统一直在Ubuntu下,开发板通过网络在使用这个根文件系统,这样方便开发调试。
但是,Ubuntu18的 nfs默认只支持3和4版本的nfs,uboot默认使用的是版本2,所以直接需要修改Ubuntu18的nfs配置,否则nfs根文件系统会报如下图所示错误,导致无法挂载。
解决方法很简单,打开Ubuntu下的/etc/default/nfs-kernel-server文件,然后在最后面添加下面这一行:
RPCNFSDOPTS="--nfs-version 2,3,4 --debug --syslog" |
添加完成以后保存退出,输入如下命令重启NFS服务即可:
sudo /etc/init.d/nfs-kernel-server restart |
接下来要设置uboot下的bootargs环境变量,主要是设置里面的“root”值,将root的值改为NFS挂载即可。在Linux内核源码里面有相应的文档讲解如何设置,文档为Documentation/filesystems/nfs/ nfsroot.txt,格式如下:
root=/dev/nfs nfsroot=[ |
只有一个网口,名字为eth0。
根据上面的格式bootargs环境变量的root值如下:
root=/dev/nfs nfsroot=192.168.1.249:/home/zuozhongkai/linux/nfs/rootfs,proto=tcp rw ip=192.168.1.250:192.168.1.249:192.168.1.1:255.255.255.0::eth0:off |
“proto=tcp”表示使用TCP协议,“rw”表示nfs挂载的根文件系统为可读可写。启动开发板,进入uboot命令行模式,然后重新设置bootargs环境变量,命令如下:
setenv bootargs 'console=ttySTM0,115200 root=/dev/nfs nfsroot=192.168.1.249:/home/zuozhongkai/linux/nfs/rootfs,proto=tcp rw ip=192.168.1.250:192.168.1.249:192.168.1.1:255.255.255.0::eth0:off' //设置bootargs saveenv //保存环境变量 |
设置完成后使用“boot”命令启动Linux内核,结果如下图所示:
注意,由于是通过网络挂载根文件系统,因此Linux系统在启动的时候需要初始化网络,因此会卡一下,大概在初始化完EMMC以后会卡几秒 钟,这是正常现象。
可以输入“ls”测试一下,如下图所示:
可以看出 ls命令工作正常!那么是不是说明rootfs就制作成功了呢注意,在进入根文件系统的时候会有下面这一行错误提示:
can't run '/etc/init.d/rcS': No such file or directory |
提示很简单,说是无法运行“/etc/init.d/rcS”这个文件,因为这个文件不存在。如下图所示:
如果使用nfs挂载根文件系统的时候发现并没有进入根文件系统,那么可能的原因有一下几点。
1、网络硬件问题
可能是开发板网络硬件有问题,首先检查一下网线是否接好,网络硬件是否有损坏。最简单的检测方法就是烧写正点原子出厂系统,然后在出厂系统里面测试网络是否能正常工作,如果可以正常工作的话就说明不是硬件问题。
2、bootargs环境变量设置错误
bootargs环境变量设置错误是最常见的,典型的就是单词写错!少个字母或者多个字母,直接进入uboot里面,输入:
print bootargs |
输入上述命令以后就会打印出bootargs环境变量的值,如下图所示:
3、bootargs环境变量没有传递给内核
这个问题不常见,但却是最容易忽略的!bootargs环境变量的值会传递给内核作为命令行(command line)参数,Linux内核启动的时候会打印出命令行参数,如下图所示:
从上图可以看出,有“Kernel command line:”这一行,这一行就是命令行参数,可以看到命令行参数和bootargs环境变量一模一样,如果不一致的话就说明 bootargs没有传递给内核。
没有传递进来的原因无非两点:
rcS是个shell脚本,Linux内核启动以后需要启动一些服务,而rcS就是规定启动哪些文件的脚本文件。在rootfs中创建/etc/init.d/rcS文件,然后在rcS中输入如下所示内容:
示例代码18.4.1.1 /etc/init.d/rcS文件
1 #!/bin/sh
2
3 PATH=/sbin:/bin:/usr/sbin:/usr/bin:$PATH
4 LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/lib:/usr/lib
5 export PATH LD_LIBRARY_PATH
6
7 mount -a
8 mkdir /dev/pts
9 mount -t devpts devpts /dev/pts
10
11 echo /sbin/mdev > /proc/sys/kernel/hotplug
12 mdev -s
第1行,表示这是一个shell脚本。
第3行,PATH环境变量保存着可执行文件可能存在的目录,这样在执行一些命令或者可执行文件的时候就不会提示找不到文件这样的错误。
第4行,LD_LIBRARY_PATH环境变量保存着库文件所在的目录。
第5行,使用export来导出上面这些环境变量,相当于声明一些“全局变量”。
第7行,使用mount命令来挂载所有的文件系统,这些文件系统由文件/etc/fstab来指定,所以一会还要创建/etc/fstab文件。
第8和9行,创建目录/dev/pts,然后将devpts挂载到/dev/pts目录中。
第11和12行,使用mdev来管理热插拔设备,通过这两行,Linux内核就可以在/dev目录下自动创建设备节点。关于mdev的详细内容可以参考busybox中的docs/mdev.txt文档。
示例代码18.4.1.1中的rcS文件内容是最精简的,如果去看Ubuntu或者其他大型Linux操作系统中的rcS文件,就会发现其非常复杂。因为是初次学习,所以不用搞这么复杂的,而且这么复杂的rcS文件也是借助其他工具创建的,比如buildroot等。
创建好文件/etc/init.d/rcS以后一定要给其可执行权限!使用如下命令给予权限:
chmod 777 rcS |
设置好以后就重新启动Linux内核,启动以后如下图所示:
从上图可以看到,提示找不到/etc/fstab文件,还有一些其他的错误,先把/etc/fstab这个错误解决了。前面说了“mount -a”挂载所有根文件系统的时候需要读取/etc/fstab,因为/etc/fstab里面定义了该挂载哪些文件,接下来就是创建/etc/fstab文件。
在rootfs中创建/etc/fstab文件,fstab在Linux开机以后自动配置哪些需要自动挂载的分区,格式如下:
|
按照上述格式,在fstab文件中输入如下内容:
示例代码18.4.2.1 /etc/fstab文件
1 #<file system> <mount point> <type> <options> <dump> <pass>
2 proc /proc proc defaults 0 0
3 tmpfs /tmp tmpfs defaults 0 0
4 sysfs /sys sysfs defaults 0 0
fstab文件创建完成后重新启动Linux,结果如下图所示:
从上图可以看出,在运行/etc/init.d/rcS脚本第11行出现问题,提示不能创建“/proc/sys/kernel/hotplug”,这个是Linux内核配置问题,最后在讲解怎么处理。接下来还需要创建一个文件/etc/inittab。
inittab的详细内容可以参考busybox下的文件examples/inittab。init程序会读取/etc/inittab这个文件,inittab由若干条指令组成。每条指令的结构都是一样的,由以“:”分隔的4个段组成,格式如下:
|
参考busybox的examples/inittab文件,也创建一个/etc/inittab,在里面输入如下内容:
示例代码18.4.3.1 /etc/inittab文件
1 #etc/inittab
2 ::sysinit:/etc/init.d/rcS
3 console::askfirst:-/bin/sh
4 ::restart:/sbin/init
5 ::ctrlaltdel:/sbin/reboot
6 ::shutdown:/bin/umount -a -r
7 ::shutdown:/sbin/swapoff -a
第2行,系统启动以后运行/etc/init.d/rcS这个脚本文件。
第3行,将console作为控制台终端,也就是ttySTM0。
第4行,重启的话运行/sbin/init。
第5行,按下ctrl+alt+del组合键的话就运行/sbin/reboot,看来ctrl+alt+del组合键用于重
启系统。
第6行,关机的时候执行/bin/umount,也就是卸载各个文件系统。
第7行,关机的时候执行/sbin/swapoff,也就是关闭交换分区。
/etc/inittab文件创建好以后就可以重启开发板即可,至此!根文件系统要创建的文件就已经全部完成了。
根文件系统要创建的文件已经全部完成了,但还是会存在如下图所示问题:
前面已经说了,这是Linux内核配置问题,只要配置一下内核就行了,路径如下:
Location: -> Device Drivers -> Generic Driver Options ->Support for uevent helper //选中 |
配置如下图所示:
选中上图中的“Support for uevent helper”选项,选中以后就会在.config中存在:
CONFIG_UEVENT_HELPER=y |
配置好以后重新编译内核,然后使用新的内核启动,如下图所示:
从上图可以看出,没有任何错误提示,说明根文件系统工作已经正常了。接下来就要对根文件系统进行其他的测试,比如自己编写的软件运行是否正常、是否支持软件开机自启动、中文支持是否正常以及能不能链接等。
使用Linux系统的目的就是运行自己的软件,编译的应用软件一般都使用动态库,使用动态库的话应用软件体积就很小,但是得提供库文件,库文件已经添加到了根文件系统中。可以编写一个小小的测试软件来测试一下库文件是否工作正常,在根文件系统下创建一个名为“drivers”的文件夹,以后学习 Linux驱动的时候就把所有的实验文件放到这个文件夹里面。
在Ubuntu下使用vim编辑器新建一个hello.c文件,在hello.c里面输入如下内容:
示例代码18.5.1.1 hello.c文件
1 #include <stdio.h>
2
3 int main(void)
4 {
5 while(1) {
6 printf("hello world!\r\n");
7 sleep(2);
8 }
9 return 0;
10 }
hello.c内容很简单,就是循环输出“hello world ”,sleep相当于Linux的延时函数,单位为秒,所以sleep(2)就是延时2秒。编写好以后就是编译,因为是要在ARM芯片上运行的,所以要用交叉编译器去编译,也就是使用arm-linux-gnueabihf-gcc编译,命令如下:
arm-none-linux-gnueabihf-gcc hello.c -o hello |
使用arm-none-linux-gnueabihf-gcc将hello.c编译为hello可执行文件。使用“file”命令查看文件类型以及编码格式:
file hello //查看 hello的文件类型以及编码格式 |
hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked |
hello是个32位的LSB可执行文件,ARM架构的,并且是动态链接的。所以编译出来的hello文件没有问题。将其拷贝到开发板的rootfs/drivers目录下,在开发板中输入如下命令来执行这个可执行文件:
cd /drivers //进入 drivers目录 ./hello //执行 hello |
结果如下图所示:
可以看出, hello这个软件运行正常,说明根文件系统中的共享库是没问题的,要想终止hello的运行,按下“ctrl+c”组合键即可。可以通过让hello进入后台运行从而正常运行并使得终端可以使用。让一个软件进入后台的方法很简单,运行软件的时候加上“&”即可,比如“./hello &”就是让hello在后台运行。在后台运行的软件可以使用“kill -9 pid(进程 ID)”命令来关闭掉,首先使用“ps”命令查看要关闭的软件PID是多少,ps命令用于查看所有当前正在运行的进程,并且会给出进程的PID。
通过“ps”命令查到hello对应的PID为137,可以使用如下命令关闭在后台运行的hello软件:
kill -9 137 |
因为hello在不断的输出“hello world”所以输入看起来会被打断,其实是没有的,因为是输入,而 hello是输出。在数据流上是没有打断的,只是显示在SecureCRT上就好像被打断了,所以只管输入“kill -9 137”即可。 hello被kill以后会有提示,如下图所示:
再去用ps命令查看一下当前的进程,发现没有hello了。这个就是Linux下的软件后台运行以及如何关闭软件的方法,重点就是3个操作:软件后面加“&”、使用ps查看要关闭的软件PID、使用“kill -9 pid”来关闭指定的软件。
在ubuntu中向在rootfs目录新建一个名为“中文测试”的文件夹,然后在MobaXterm下查看中文名能不能显示正确。输入“ls”命令,结果如下图所示:
可以看出“中文测试”这个文件夹显示正常,接着“touch”命令在“中文测试”文件夹中新建一个名为“测试文档 .txt”的文件,并且使用vim编辑器在其中输入“这是一个中文测试文件”,借此来测试一下中文文件名和中文内容显示是否正 常。在MobaXterm中使用“cat”命令来查看“测试文档 .txt”中的内容,结果如下图所示:
从上图可以看出,“测试文档 .txt”的中文内容显示正确,而且中文路径也完全正常,说明根文件系统已经完美支持中文了!
在之前测试hello软件的时候都是等Linux启动进入根文件系统以后手动输入命令“./hello”来完成的。一般做好产品以后都是需要开机自动启动相应的软件,本节就以hello这个软件为例,讲解一下如何实现开机自启动。前面说过,进入根文件系统的时候会运行/etc/init.d/rcS这个shell脚本,因此可以在这个脚本里面添加自启动相关内容。添加完成以后的/etc/init.d/rcS文件内容如下:
示例代码18.5.3.1 /etc/init.d/rcS文件
1 #!/bin/sh
2 PATH=/sbin:/bin:/usr/sbin:/usr/bin
3 LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/lib:/usr/lib
4 runlevel=S
5 umask 022
5 export PATH LD_LIBRARY_PATH
6 export PATH LD_LIBRARY_PATH runlevel
7
8 mount -a
9 mkdir /dev/pts
10 mount -t devpts devpts /dev/pts
11
12 echo /sbin/mdev > /proc/sys/kernel/hotplug
13 mdev -s
14
15 #开机自启动
16 cd /drivers
17 ./hello &
18 cd /
第16行,进入drivers目录,因为要启动的软件存放在drivers目录下。
第17行,以后台方式执行hello这个软件。
第18行,退出drivers目录,进入到根目录下。
自启动代码添加完成以后就可以重启开发板,看看 hello这个软件会不会自动运行。结果如下图所示:
从上图可看出,hello开机自动运行,证明开机自启动成功。
这里说的外网是百度、淘宝等这些网站的测试。也
就是说看看开发板能不能上网,能不能和局域网外的这些网站进行通信。测试方法很简单,就是通过ping命令来ping一下百度的官网:www.baidu.com。输入如下命令:
ping www.baidu.com |
结果肯定是失败的,提示“bad address”,也就是地址不对。之所以出现这个错误提示是因为 www.baidu.com的地址解析失败了,并没有解析出其对应的IP地址。需要配置域名解析服务器的IP地址,一般域名解析地址可以设置为所处网络的网关地址,比如192.168.1.1。 也可以设置为114.114.1144.114,这个是运营商的域名解析服务器地址。
在rootfs中新建文件/etc/resolv.conf,然后在里面输入如下内容:
示例代码18.5.4.1 resolv.conf文件内容
1 nameserver 114.114.114.114
2 nameserver 192.168.1.1
设置很简单,nameserver表示这是个域名服务器,设置了两个域名服务器地址:114.114.114.114和192.168.1.1,也可以改为其他的域名服务器试试。如果使用“udhcpc”命令自动获取IP地址,“udhcpc”命令会修改nameserver值,一般是将其设置为对应的网关地址。修改好以后保存退出,重启开发板!重启以后重新ping一下百度官网,结果如下图所示:
可以看出ping百度官网成功了!域名也成功的解析了,至此!根文件系统就彻底的制作完成,这个根文件系统最好打包保存一下,防止以后做实验不小心破坏了根文件系统,又得从头制作根文件系统。uboot、Linux kernel、rootfs这三个共同构成了一个完整的Linux系统,现在的系统至少是一个可以正常运行的系统,后面就可以在这个系统上完成Linux驱动开发的学习。
一个最小的根文件系统已经制作好了,而且通过nfs测试正常,接下来就学习一下如何将其烧写到开发板 EMMC中。根文件系统烧写进去以后,一个完成的嵌入式Linux开发环境就准备好了,以后实际产品开发也是用的这套流程。
根文件系统打包和之前Linux内核移植中对uImage和stm32mp157d-atk.dtb打包方法一样,也是制
作ext4格式的根文件系统包,这里需要对/home/zuozhongkai/linux/nfs/rootfs目录进行打包,这里再讲解一遍。
首先新建一个ext4格式的磁盘,然后挂载这个ext4格式的磁盘,将/home/zuozhongkai/linux/nfs/rootfs这个目录下的文件拷贝到这个ext4磁盘即可。
首选创建一个名为rootfs文件夹,注意不要和 /home/zuozhongkai/linux/nfs/rootfs这个目录重
复了!比如教程中就在/home/zuozhongkai/linux/目录下创建一个名为“rootfs”目录,然后输入如下命令创建ext4磁盘:
示例代码18.6.1.1 ext4磁盘创建命令
1 cd rootfs
2 dd if=/dev/zero of=rootfs.ext4 bs=1M count=1024
3 mkfs.ext4 -L rootfs rootfs.ext4
第1行,进入rootfs目录。
第2行,使用dd命令创建一个名为rootfs.ext4的磁盘,由于根文件系统比较大,因此count设置为 1024,也就是根文件系统总空间为1GB,这个是可以调整的,要根据自己的实际根文件系统大小调整count的值,只要空间不超过开发板所使用的8GB EMMC即可。最好不要设置太大,否则烧写会很慢!
第3行,使用mkfs.ext4将rootfs.ext4磁盘格式化为ext4格式。
完成以后就会生成名为“rootfs.ext4”的磁盘。
首先创建一个目录用来挂载前面制作制作出来的rootfs.ext4,比如教程中创建目录/mnt/rootfs,命令如下:
sudo mkdir /mnt/rootfs |
接下来使用mount命令将rootfs.ext4挂载到/mnt/rootfs目录下,命令如下:
cd /home/zuozhongkai/linux/atk-mp1/linux/rootfs sudo mount rootfs.ext4 /mnt/rootfs/ |
挂载成功以后就将/home/zuozhongkai/linux/nfs/rootfs目录下的所有根文件系统文件拷贝到/mnt/bootfs目录下,命令如下:
cd /home/zuozhongkai/linux/nfs/rootfs/ sudo cp * /mnt/rootfs/ -drf |
拷贝完成后使用umount卸载/mnt/rootfs即可,命令如下:
sudo umount /mnt/rootfs |
至此,根文件系统就已经打包到rootfs.ext4中,稍后使用STM32CubeProgrammer软件将其烧写到EMMC里面。烧写之前最好在Windows下打开rootfs.ext4看一下,看看是否已经将根文件系统打包进去,如下图所示:
接下来就是将上一小节打包好的ext4格式的根文件系统 rootfs.ext4烧写到开发板的EMMC里面,使用STM32CubeProgrammer软件完成此操作。rootfs.ext4拷贝到以前创建的images目录下,如下图所示:
修改FlashLayout文件tf-a.tsv,在后面加入rootfs.ext4烧写脚本,如下图所示:
上图中第8行就是根文件系统rootfs.ext4的烧写脚本,设置好以后就可以使用STM32CubeProgrammer烧写系统了,烧写完成以后设置拨码开关从EMMC启动。启动以后进
入uboot的命令行,设置bootcmd和bootargs这两个环境变量,命令如下:
setenv bootcmd 'ext4load mmc 1:2 c2000000 uImage;ext4load mmc 1:2 c4000000 stm32mp157d-atk.dtb;bootm c2000000 - c4000000' setenv bootargs 'console=ttySTM0,115200 root=/dev/mmcblk1p3 rootwait rw' saveenv boot |
注意bootargs环境变量里面root的值为/dev/mmcblk1p3,说明根文件系统存放在mmcblk1的第3个分区。正点原子出厂系统的根文件系统是存放在mmcblk2的第3个分区,但这里不是,这是因为正点原子出厂系统使能了SDIO WIFI,而SDIO WIFI也是SDIO接口,如果使能了SDIO WIFI那么SDIO WIFI所使用的那个SDIO接口就变成了mmcblk1,而EMMC所使用的SDIO接口就会变为mmcblk2。
设置以后启动系统,此时就会从EMMC里面加载Linux系统镜像,并且正确读取到根系统,如下图所示:
从上图可以看出,正常进入根文件系统。至此, Linux系统移植就全部完成,包括tf-a、uboot、Linux kernel和rootfs。
这一章学习的是怎么用busybox来构建根文件系统。总体的构建过程不是很难:
首先修改Makefole来添加ARCH和CROSS_COMPILE编译器;然后修改busybox的中文字符支持,主要就是打开libbb/printable_string.c,找到printable_string2函数,把其中的>=0x7f相关删掉就可以了,libbb/unicode.c中的unicode_conv_to_printable2也是同理。
然后配置busybox,先make defconfig使用默认配置;然后使用图形化界面make menuconfig,修改Settings中的Build static binary(no shared libs)取消勾选;Settings中的vi-style line editing commands选中;Linux Module Utilities的Simplified modutils取消勾选;Linux System Utilities的mdev(17 kb)及以下都勾选;最后在Settings的Support Unicode的Check $LC_ALL, $LC_CTYPE and $LANG environment variables选中来支持中文。搞定之后make再make install CONFIG_PREFIX=/home/zuozhongkai/linux/nfs/rootfs编译一下就可以了。
因为是动态链接,需要手动添加lib。
在rootfs的/lib添加/usr/local/arm/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf/arm-none-linux-gnueabihf/libc/lib中的so,其中ld-linux-armhf.so.3需要删掉重新拷贝:
cp *so* /home/zuozhongkai/linux/nfs/rootfs/lib/ -d cp ld-linux-armhf.so.3 /home/zuozhongkai/linux/nfs/rootfs/lib/ |
之后拷贝/usr/local/arm/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf/arm-none-linux-gnueabihf/lib中的so和.a:
cp *so* *.a /home/zuozhongkai/linux/nfs/rootfs/lib/ -d |
在rootfs的usr/lib添加,在/usr/local/arm/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf/arm-none-linux-gnueabihf/libc/usr/lib拷贝so和.a库:
cp *so* *.a /home/zuozhongkai/linux/nfs/rootfs/usr/lib/ -d |
之后在rootfs手动创建dev、proc、mnt、sys、tmp、etc和root等就可以了。
首先修改nfs版本,在/etc/default/nfs-kernel-server文件中最后添加:
RPCNFSDOPTS="--nfs-version 2,3,4 --debug --syslog" |
之后重启nfs服务:
sudo /etc/init.d/nfs-kernel-server restart |
然后设置uboot下的bootargs环境变量:
setenv bootargs 'console=ttySTM0,115200 root=/dev/nfs nfsroot=192.168.1.249:/home/zuozhongkai/linux/nfs/rootfs,proto=tcp rw ip=192.168.1.250:192.168.1.249:192.168.1.1:255.255.255.0::eth0:off' //设置 bootargs saveenv //保存环境变量 |
示例代码18.4.1.1 /etc/init.d/rcS文件
1 #!/bin/sh
2
3 PATH=/sbin:/bin:/usr/sbin:/usr/bin:$PATH
4 LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/lib:/usr/lib
5 export PATH LD_LIBRARY_PATH
6
7 mount -a
8 mkdir /dev/pts
9 mount -t devpts devpts /dev/pts
10
11 echo /sbin/mdev > /proc/sys/kernel/hotplug
12 mdev -s
设置权限777后即可。
然后创建/etc/fstab文件:
示例代码18.4.2.1 /etc/fstab文件
1 #<file system> <mount point> <type> <options> <dump> <pass>
2 proc /proc proc defaults 0 0
3 tmpfs /tmp tmpfs defaults 0 0
4 sysfs /sys sysfs defaults 0 0
最后创建/etc/inittab文件:
示例代码18.4.3.1 /etc/inittab文件
1 #etc/inittab
2 ::sysinit:/etc/init.d/rcS
3 console::askfirst:-/bin/sh
4 ::restart:/sbin/init
5 ::ctrlaltdel:/sbin/reboot
6 ::shutdown:/bin/umount -a -r
7 ::shutdown:/sbin/swapoff -a
之后去配置一下Linux的内核(我自己测试的时候这个选项是直接选中的),路径如下:
Location: -> Device Drivers -> Generic Driver Options ->Support for uevent helper //选中 |
一些软件测试等具体操作就翻一下上文就好了,不赘述。
自行创建一个rootfs文件夹之后,使用如下命令创建ext4磁盘:
dd if=/dev/zero of=rootfs.ext4 bs=1M count=1024 mkfs.ext4 -L rootfs rootfs.ext4 |
然后与之前一样来挂载这个rootfs.ext4:
sudo mkdir /mnt/rootfs cd /home/zuozhongkai/linux/atk-mp1/linux/rootfs sudo mount rootfs.ext4 /mnt/rootfs/ cd /home/zuozhongkai/linux/nfs/rootfs/ sudo cp * /mnt/rootfs/ -drf sudo umount /mnt/rootfs |
之后再Windows中修改FlashLayout并设置uboot的环境变量bootcmd和bootargs:
P 0x22 rootfs FileSystem mmc1 0x04290000 rootfs.ext4 |
setenv bootcmd 'ext4load mmc 1:2 c2000000 uImage;ext4load mmc 1:2 c4000000 stm32mp157d-atk.dtb;bootm c2000000 - c4000000' setenv bootargs 'console=ttySTM0,115200 root=/dev/mmcblk1p3 rootwait rw' saveenv boot |
到这里其实已经构建了一套Linux的最小系统了,从之前的TF-A(I.mx6ull没有),然后是uboot、Linux kernel到现在的rootfs,齐活了。但是busybox构建不是那么方便,之后讲解更实用的buildroot来构建,然后应该系统移植就齐活了,之后的就是一些驱动应用移植,笔记也不会那么详细了。