一般以Linux(Ubuntu)为开发环境,嵌入式Linux开发的环境主要包括:交叉编译工具链,但由于一般用的都是Windows主机,所以要么双系统,要么配虚拟机。
配虚拟机的话就需要打通Ubuntu与Windows之间的通道,比如文件的传送,我用的是FillZilla客户端(ftp协议)。
在学习C语言时,我记得用的是codeblocks,当我们写好c程序后点击编译运行就能看到cmd命令窗口输出的结果。其实在我们点击编译运行这个按钮时,就相当于使用命令gcc来编译c程序使之成为可执行文件(在Windows下是exe格式,在Linux中是elf格式)。但是编译生成的可执行文件只能在此电脑(我的时x86)上运行,不能在arm架构上运行,所以我们可以借助交叉编译工具在x86架构上编译生成可以在arm架构上运行的程序。
那为什么不直接在arm架构上编译c文件呢?
第一:环境没有搭建好,比如开发板上连u-boot、Linux内核、跟文件系统都没有搭建好;
第二:就算环境搭建好了,并且也把交叉编译工具链放到了开发板上,但是由于开发办性能不是很好,编译需要相对较长的时间。
在Ubuntu中用gcc编译的c文件,用file命令查看生成的可执行文件:
参看:一段C语言代码编译、运行全过程解析
$ gcc -E hello.c > hello.i //会生成预处理后的C源文件hello.i
$ gcc -S hello.i //将hello.i编译成汇编文件hello.s
$ gcc -c hello.s //将汇编文件hello.s汇编成hello.o
$ gcc hello.o -o hello //将目标文件链接成可执行文件hello
$ ./hello // 运行可执行文件hello
我们在Ubuntu中编写一个c文件后,用gcc就能编译链接成可执行文件。但具体过程是怎样的呢?由上面可知,需要预处理,编译,汇编,链接。编译时会去找头文件,去哪找呢?gcc工具链中(一个文件夹);编译时也需要库文件,去哪找呢?还是在gcc工具链中。
在Ubuntu中用which gcc或whereis gcc
可以找到gcc的路径,gcc命令是在/usr/bin
下,而编译时所需的库文件在另外的地方,即/usr/lib
中,/usr/include
下是一些头文件。
ps. Linux中的usr是指Unix System Resource,即Unix系统资源的缩写。/usr 是系统核心所在,包含了所有的共享文件。它是 unix 系统中最重要的目录之一,涵盖了二进制文件,各种文档,各种头文件,还有各种库文件;还有诸多程序,例如 ftp,telnet 等等。
而对于交叉编译来说,Ubuntu是没有的,需要我们自己安装。交叉编译工具链我(正-原-的教程)安装到了/usr/local/arm
中去了。如下,
bin目录下是一些可执行文件,就是我们常用的交叉编译工具链,如arm-linux-gnueabihf-gcc, arm-linux-gnueabihf-objdump, arm-linux-gnueabihf-ld
等。其它目录就是一些库文件和头文件,也包含一些可执行文件。
如果没有设置环境变量的话,在某个shell中使用命令arm-linux-gnueabihf-gcc会报错,只有在交叉编译工具链的目录下才不会报错,解决方法就是将这一目录加入环境变量PATH中去。Linux下设置环境变量参请参考此
针对对所有用户都有效的方法:打开/etc/profile
文件,在末尾添加如下内容:
export PATH=$PATH:路径,如export PATH=$PATH:/usr/local/arm/./bin
不管什么开发,最终都是操作底层硬件寄存器。
这是最能体现直接操作寄存器的,比如初始化及配置串口时会直接操作相关寄存器。
void SerialInit()
{
TMOD=0x20;
TH1=0xf3; //波特率为4800
TL1=0xf3;
PCON=0x80; //SMOD为1,boundrate加倍
TR1=1;
SCON=0x50;
EA=1;
ES=1;
}
学习stm32时,最开始选择的库函数版本,当时还有寄存器版本,到现在又有了hal库。寄存器版本和c51差不多,直接操作寄存器;而库函数和hal库版本都是间接操作寄存器,即调用相关API对寄存器进行操作。下面是库函数版本:
u8 DS18B20_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
//使能PORTA口时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = DS18B20IO;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DS18B20PORT, &GPIO_InitStructure);
GPIO_SetBits(DS18B20PORT,DS18B20IO); //输出1
return DS18B20_Check();
}
其实在嵌入式Linux下开发的方式有很多,可以直接操作寄存器,也可以调用厂家的BSP包(相当于库函数),还可以采用框架(设备树、gpio和pinctrl子系统)等。整体过程如下:
即直接操作寄存器,如下:
/* main.h文件部分内容
* 部分相关寄存器地址
*/
#define CCM_CCGR0 *((volatile unsigned int *)0X020C4068)
#define GPIO1_DR *((volatile unsigned int *)0X0209C000)
/* main.c文件部分内容
* 将GPIO1_DR的bit3清零
*/
void led_on(void)
{
GPIO1_DR &= ~(1<<3);
}
即使用厂商提供的API,如下:
//bsp_clk.h头文件内容
#ifndef __BSP_CLK_H
#define __BSP_CLK_H
#include "imx6ul.h" //厂商写好的头文件
/* 函数声明 */
void clk_enable(void);
#endif
此开发方式涉及设备树、gpio子系统、pinctrl子系统、platform平台总线等。比如可以直接在驱动程序中定义相关寄存器物理地址,然后编写相应的驱动函数(如read、write等);也可以将寄存器物理地址表示在设备树中,在驱动中使用相关函数获取设备树中的寄存器物理地址。
设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做 DTS(Device Tree Source),这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如 CPU 数量、 内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等。
对于设备树,需掌握:
Linux 内核在启动的时候会解析 DTB 文件,然后在/proc/device-tree 目录下生成相应的设备树节点文件。
如果学习过 UCOS/FreeRTOS
应该知道,UCOS/FreeRTOS 移植 就是在官方的 SDK 包里面找一个和自己所使用的芯片一样的工程编译一下,然后下载到开发 板就可以了。那么 Linux 的移植是不是也是这样的,下载 Linux 源码,然后找个和我们所使用 的芯片一样的工程编译一下就可以了?很明显不是的!Linux 的移植要复杂的多,在移植 Linux 之前我们需要先移植一个 bootloader 代码,这个 bootloader 代码用于启动 Linux 内核(相当于Windows下的BIOS),bootloader 有很多,常用的就是 U-Boot。移植好 U-Boot 以后再移植 Linux 内核,移植完 Linux 内核以后 Linux 还不能正常启动,还需要再移植一个根文件系统(rootfs),根文件系统里面包含了一些最常用的命令和文件。所以 U-Boot、Linux kernel 和 rootfs 这三者一起构成了一个完整的 Linux 系 统,一个可以正常使用、功能完善的 Linux 系统
bootloader程序会先初始化DDR等外设,然后将Linux内核从flash(NAND, NOR FLASH,SD,MMC 等)拷贝到 DDR 中,最后启动 Linux 内核。
u-boot可以看作是一个功能丰富、复杂的裸机程序,在编译生成可执行文件前可以根据自己的需求配置相应的功能,比如USB、网络、SD卡等。配置好后通过以下命令编译。
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-mx6ull_14x14_ddr512_emmc_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j12
在uboot源码下的configs文件夹中,有许多配置文件,如下:
复制mx6ull_14x14_evk_emmc_defconfig,然后重命名为mx6ull_alientek_emmc_defconfig,修改其内容为:
CONFIG_SYS_EXTRA_OPTIONS="IMX_CONFIG=board/freescale/mx6ull_alientek_emmc/imximage.cfg,MX6ULL_EVK_EMMC_REWORK"
CONFIG_ARM=y
CONFIG_ARCH_MX6=y
CONFIG_TARGET_MX6ULL_ALIENTEK_EMMC=y
CONFIG_CMD_GPIO=y
在目录include/configs
下添加 ALPHA 开 发 板 对 应 的 头 文 件 ,复 制 include/configs/mx6ullevk.h
,并重命名为 mx6ull_alientek_emmc.h
。注意,头文件的头两行宏定义需要修改,根据头文件名修改,即#ifndef A, #define A改为#ifndef B, #define B
。mx6ull_alientek_emmc.h中有很多宏定义,这些宏定义基本用于配置 uboot,也有一些 I.MX6ULL 的配置项目。如果我们自己要想使能或者禁止 uboot 的某些功能,那就在mx6ull_alientek_emmc.h
里面做修改即可。
这个不用细究,真要用到时再研究。uboot 中有两个非常重要的环境变量 bootcmd 和 bootargs。
bootcmd 保存着 uboot 默认命令,uboot 倒计时结束以后就会执行 bootcmd 中的命令,这些命令一般都是用来启动 Linux 内核的,比如读取 EMMC 或者NAND Flash 中的 Linux 内核镜像文件和设备树文件到 DRAM 中,然后启动 Linux 内核。
bootargs 保存着 uboot 传递给 Linux 内核的参数,比如bootargs=“console= ttymxc0, 115200 root= /dev/mmcblk1p2 rootwait rw”
。
执行以下命令设置两个环境变量的值,然后直接输入boot即可启动Linux内核。
setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'
setenv bootcmd 'mmc dev 1; fatload mmc 1:1 80800000 zImage; fatload mmc 1:1 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000;' //从mmc启动
setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000' //从网络启动
saveenv
获取到源码后使用交叉编译工具链编译(因为我们是要移植到arm开发板上去)源码,命令如下:
#!/bin/sh
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16
编译完成以后就会在 arch/arm/boot 这个目录下生成一个叫做 zImage 的文件,zImage 就是我们要用的 Linux 镜像文件。另外也会在 arch/arm/boo/dts
下生成很多.dtb文件,这些.dtb 就是设备树文件。
进入到目录/sys/bus/cpu/devices/cpu0/cpufreq
中可以看到许多文件,这些文件是关于CPU频率等信息的。
Linux 中的根文件系统更像是一个文件夹或者叫做目录(在我看来就是一个文件夹,只 不过是特殊的文件夹),在这个目录里面会有很多的子目录。根目录下和子目录中会有很多的文 件,这些文件是 Linux 运行所必须的,比如库、常用的软件和命令、设备文件、配置文件等等。
根文件系统是 Linux 内核启动以后挂载(mount)的第一个文件系统,然后从根文件系统中读取初始化脚本,比如 rcS,inittab 等。根文件系统和 Linux 内核是分开的,单独的 Linux 内核是没法正常工作的,必须要搭配根文件系统。如果不提供根文件系统,Linux 内核 在启动的时候就会提示内核崩溃(Kernel panic)的提示。
根文件系统的这个“根”字就说明了这个文件系统的重要性,它是其他文件系统的根,没有这个“根”,其他的文件系统或者软件就别想工作。比如我们常用的 ls、mv、ifconfig 等命令其实就是一个个小软件,只是这些软件没有图形界面,而且需要输入命令来运行。这些小软件 就保存在根文件系统中,这些小软件是怎么来的呢?这个就是我们本章教程的目的,教大家来 构建自己的根文件系统,这个根文件系统是满足 Linux 运行的最小根文件系统,后续我们可以根据自己的实际工作需求不断的去填充这个最小根文件系统,最终使其成为一个相对完善的根文件系统。
得到BusyBox源码后,按需要进行配置(也可以通过可视化配置),然后编译(指定编译结果存放目录rootfs)完成后得到bin、sbin、usr三个目录,以及linuxrc 这个文件。前面说过 Linux 内核 init 进程最后会查找用户空间的 init 程序,找到以后就会运行这个用户空间的 init 程序,从而切换到用户态。如果 bootargs 设置 init=/linuxrc,那么 linuxrc 就是可以作为用户空间的 init 程序,所以用户态空间的 init 程序是 busybox 来生成的。
Linux 中的应用程序一般都是需要动态库的,当然你也可以编译成静态的,但是静态的可执行文件会很大。如果编译为动态的话就需要动态库,所以我们需要先根文件系统中添加动态库,在 rootfs 中创建一个名为“lib”的文件夹。
lib 文件夹创建好了,库文件从哪里来呢?lib 库文件从交叉编译器中获取,前面我们搭建交叉编译环境的时候将交叉编译器存放到了“/usr/local/arm/”目录中。交叉编译器里面有很多的 库文件,这些库文件具体是做什么的我们作为初学者肯定不知道,既然我不知道那就简单粗暴的把所有的库文件都放到我们的根文件系统中。
/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib
此目录下有很多的so(是通配符)和.a 文件,这些就是库文件,将此目录下所有的so*和.a文件都拷贝到 rootfs/lib 目录中。
/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/lib
此目录下也有很多的的so和.a 库文件,我们将其也拷贝到 rootfs/lib 目录中
在 rootfs 的 usr 目录下创建一个名为 lib 的目录,将如下目录中的 so 和.a 库文件都拷贝到 rootfs/usr/lib 目录中:
/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/usr/lib
rcS 是个 shell 脚本,Linux 内核启动以后需要启动一些服务,而 rcS 就是规定启动哪些文件的脚本文件。在 rootfs 中创建/etc/init.d/rcS 文件,然后在 rcS 中输入如下所示内容(创建好文件/etc/init.d/rcS 以后一定要给其可执行权限!):
#!/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/lib:/usr/lib
export PATH LD_LIBRARY_PATH runlevel
mount -a
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
在 rootfs 中创建/etc/fstab 文件,fstab 在 Linux 开机以后自动配置哪些需要自动挂载的分区,格式如下:
<file system> <mount point> <type> <options> <dump> <pass>
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
inittab 的详细内容可以参考 busybox 下的文件 examples/inittab。init 程序会读取/etc/inittab这个文件,inittab 由若干条指令组成。每条指令的结构都是一样的,由以“:”分隔的 4 个段组 成,格式如下:
格式:<id>:<runlevels>:<action>:<process>
#etc/inittab
::sysinit:/etc/init.d/rcS
console::askfirst:-/bin/sh
::restart:/sbin/init
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r 7 ::shutdown:/sbin/swapoff -a
至此,根文件系统基本构建完成。
测试方法就是使用 NFS 挂载,uboot 里面的 bootargs 环境变量会设置“root”的值,所以我们将 root 的值改为 NFS 挂载即可。
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.250:/home/zuozhongkai/linux/nfs/rootfs ip=192.168.1.251:192.168.1.250:192.168.1.1:255.255.255.0::eth0:off' //设置 bootargs
saveenv //保存环境变量
我们使用 Linux 的目的就是运行我们自己的软件,我们编译的应用软件一般都使用动态库,使用动态库的话应用软件体积就很小,但是得提供库文件,库文件我们已经添加到了根文件系统中。我们编写一个小小的测试软件来测试一下库文件是否工作正常,在根文件系统下创建一 个名为“drivers”的文件夹,以后我们学习 Linux 驱动的时候就把所有的实验文件放到这个文件夹里面。
再Ubuntu下编写hello,world文件,用交叉编译工具编译后拷贝到rootfs/drivers
文件夹中去,然后在开发板上运行hello文件。