9 嵌入式Linux
1、介绍
嵌入式软件分类: Bootloader、Linux内核、根文件系统rootfs
1.1 bootloader
特点:
- 本质就是一个裸板程序
- 上电先运行,一旦启动linux内核,其生命周期结束
- bootloader仅仅是一个统称,uboot属于其中一种
三大功能:
1:上电首先做硬件初始化工作
例如:初始化CPU,内存,闪存,串口,网络等
2:从闪存(例如:EMMC)加载linux内核到内存并且从内存中启动linux内核
3:在启动linux内核之前,给linux内核传递一个启动参数,
告诉linux内核将来要挂接(找)的根文件系统rootfs在EMMC上的某个分区上或者上位机上,linux内核将来就会到这个地方去找(挂接)根文件系统rootfs
1.2 Linux内核
特点:
- 必须有bootloader启动
- linux内核一旦启动,bootloader生命结束
- linux内核本质就是一个大程序,也有入口函数(__stext/start_kernel),从上往下挨个执行,执行到最后会根据bootloader传递来的启动参数到某个地方去找根文件系统rootfs,一旦找到立刻执行根文件系统rootfs的init第一号进程(linux内核的归宿)
- linux内核永驻内存,只要系统不关机,一直运行,默默无闻的为应用程序提供服务open/mmap/brk/sbrk/socket/sendto/recvfrom
七大功能(又称七大子系统):
1:内存管理子系统:malloc/sbrk/brk/mmap/free/munmap等
2:进程管理子系统:fork/vfork/exit/切换/抢占等
3:系统调用子系统:open/read/write/close等
4:TCP/IP网络协议栈:数据链路层,网络层,传输层
5:文件系统:NTFS,FAT32,EXT4等
6:设备驱动:驱动各种硬件外设
7:平台相关代码(BSP):linux即可运行在X86处理器又可运行在ARM处理器
1.3 根文件系统rootfs
特点:
- 从他包含的内容来看,根文件系统rootfs包含的内容就是:
cd / 然后 ls 看到的所有内容组成根文件系统rootfs,例如:
bin目录:包含各种普通用户的命令
sbin目录:包含各种超级用户的命令
etc目录:包含各种配置文件
lib目录:包含各种库
…
- linux内核启动到最后会根据bootloader传递来的启动参数到某个地方去找根文件系统rootfs,一旦找到立刻执行根文件系统rootfs的sbin目录下的init第一号进程,一旦init第一号进程启动,进程运行到最后创建一个子进程来执行bin目录下的sh程序(shell终端程序),此时用户可以输入各种linux命令,至此系统控制权交给用户,父进程init等待着子进程的结束
1.4 嵌入式linux系统启动
启动流程:
1:->上电 ->CPU首先运行uboot ->uboot首先做硬件初始化,目的是为了linux内核运行,准备一个好的硬件环境
2:->uboot然后从EMMC的某个分区上加载linux内核到内存 ->uboot然后给linux内核传递一个启动参,告诉linux内核将来要找的根文件系统rootfs在某个地方
3:->uboot最后启动linux内核,内核一旦启动,uboot生命结束 ->linux内核一旦启动,首先做七大子系统的初始化 ->linux内核最终根据uboot传递的启动参数到某个地方找根文件系统rootfs-> 一旦找到根文件系统rootfs,linux内核立马运行根文件系统rootfs/sbin/init第一号进程
4:->init第一号进程创建一个子进程来执行根文件系统rootfs/bin/sh,至此用户可以输入各种命令来操作linux系统 ->父进程init等待着子进程的结束 ->至此linux系统启动完毕
2、在开发板上运行linux
ubootpak.bin:uboot的二进制可执行文件,已经烧写完毕
linux内核文件:uImage
rootfs根文件系统:rootfs_ext4.img
uboot中两个重要的环境变量:bootcmd:uboot能根据bootcmd将linux内核从闪存加载到内存
bootargs:内核根据bootargs指定的路径去找根文件系统
步骤
1:将相关文件复制到linux中。
2:连线(连接上位机和下位机)
网线:连接上位机和下位机,用于后期的NFS网络服务
串口线:连接上位机和下位机(UART0 DEBUG串口,位于中间),用于打印调试
OTG USB:连接上位机和下位机(网口旁边),用于下载烧写文件
电源线:通电,快速中断倒计时,进入下位机的uboot命令行模式
3:将uImage,rootfs_ext4.img烧写到下位机EMMC之前,先对EMMC进行分区规划
EMMC分区规划如下:
0-------512----------1M--------------65M---------------819M--------------剩余
保留 uboot uImage rootfs 大片
第一分区 第二分区 第三分区 暂时不分
mmcblk0boot0 mmcblk0p1 mmcblk0p2 //linux内核给每个分区指定的名称
4:正式根据分区规划对EMMC进行分区,重启下位机,进入uboot命令行模式执行:
fdisk 2 2 0x100000:0x4000000 0x4100000:0x2f200000
说明:
fdisk:uboot自带的分区命令
第一个参数2:明确:TF卡,SD卡,EMMC硬件特性一模一样,统称MMC
如果传递0:表示对SD0卡槽的TF进行分区
如果传递1:表示对SD1卡槽的TF进行分区
如果传递2:表示对EMMC进行分区
第二个参数2:表示要分两个分区,分别是uImage和rootfs所在的分区
注意:uboot所在的分区由linux内核自己来分
0x100000:0x4000000:指定第二分区uImage所在的分区起始地址和大小
0x4100000:0x2f200000:指定第三分区rootfs所在的分区起始地址和大小
5:下位机执行fastboot
--------------使用OTG USB烧录------------------------
6:利用OTG USB将uImage和rootfs_ext4.img烧写到对应的分区上即可
烧录uImage
烧录rootfs
烧写完毕,将OTG USB下载线拔掉!
--------------使用网络烧录------------------------
6:利用网络将uImage和rootfs_ext4.img烧写到对应的分区上即可
tftp 0x48000000 uImage //利用tftp服务从上位机的/tftpboot共享目录中下载uImage到下位机内存0x480000000
mmc write 0x48000000 0x800 0x3000 //将uImage从内存的0x48000000写入到EMMC的0x800起始地址(以sector=512字节为单位)烧写0x3000这么多块(6M)
tftp 0x48000000 rootfs_ext4.img //利用tftp服务从上位机的/tftpboot共享目录中下载rootfs到下位机内存0x480000000
mmc write 0x48000000 0x20800 0x32000 //将rootfs从内存的0x48000000写入到EMMC的0x20800起始地址(以sector=512字节为单位)烧写0x32000这么多块(100M)
烧录uImage
烧录rootfs_ext4.img
7:设置嵌入式linux系统启动的两个必要参数
uboot中有两个重要的环境变量:bootcmd和bootargs
- bootcmd环境变量作用:
当uboot启动,出现3,2,1倒计时,如果倒计时没有被中断,倒计时为0,uboot会自动执行bootcmd环境变量指定的命令,此命令用于将linux内核从EMMC的第二分区中拷贝到内存,或者直接在uboot命令行输入boot命令也能直接去执行bootcmd环境变量的命令 - bootargs环境变量作用:
其设置的环境变量值就是给linux内核传递的启动参数,告诉linux内核将来要找的根文件系统rootfs在某个地方,例如:EMMC的第三分区
设置步骤
重启下位机,进入uboot命令行执行:
setenv bootcmd mmc read 0x48000000 0x800 0x3000 \; bootm 0x48000000 //setenv:用于uboot来设置环境变量的值
saveenv // saveenv:用于uboot来保存环境变量的值到EMMC上
print //打印uboot的环境变量,查看bootcmd是否设置成功
/* 解释
结论:最终bootcmd环境变量的值为:mmc read 0x48000000 0x800 0x3000 \; bootm 0x48000000
- mmc read:uboot命令,用于将EMMC上的数据读取到内存中
- 0x48000000:读取到内存的0x48000000这个起始地址,也就是将来从EMMC上读取的数据放到内存的0x48000000这个地址上
- 0x800:从EMMC的0x800起始地址开始拷贝读取数据
注意:mmc read命令操作EMMC时,起始地址和大小必须以块(sector)为单位,不再以字节为单位,EMMC的1个块(sector)为512字节(16进制为0x200)
所以:0x800 = uImage所在的EMMC分区起始地址0x100000(1M)/0x200
- 0x3000:指定从EMMC读取的数据大小(同样是以sector块为单位)
0x3000 = uImage文件的大小/0x200
终极结论:将uImage从第二分区拷贝到内存的0x48000000这个起始地址
- “\;”:表示执行完前面的命令之后继续让uboot执行后面的命令
- bootm 0x48000000:从内存的0x48000000启动linux内核uImage,一旦启动,uboot生命结束 */
setenv bootargs root=/dev/mmcblk0p2 init=/linuxrc console=ttySAC0,115200 maxcpus=1 lcd=wy070ml tp=gslx680-linux
saveenv
/* 解释
设置环境变量bootargs的值为:root=/dev/mmcblk0p2 ...
- root=/dev/mmcblk0p2:告诉linux内核将来要挂接找的根文件系统rootfs在第三分区
- init=/linuxrc:linux内核一旦找到根文件系统rootfs,执行的第一个进程就是根目录下的linuxrc程序
注意:linuxrc会帮你启动第一号进程:/sbin/init
- console=ttySAC0,115200:指定将来linux内核打印输出的信息通过第一个串口来输出到上位机上,波特率115200
ttySAC0:第一个串口
ttySAC1:第二个串口
...
- maxcpus=1:只启动一个CPU核,CPU0核
- lcd=wy070ml:指定LCD显示屏的型号
- tp=gslx680-linux:指定触摸屏的型号
*/
8:测试下位机的linux系统
重启下位机,不要中断uboot的倒计时,静静等待者下位机linux系统启动
启动完毕,要求输入用户名和密码:
用户名:root
密码:123456
3、uboot
uboot属于bootloader的一种,还有armboot,vivi,redboot等,但是uboot严重依赖硬件信息,所以根据硬件差异修改官方uboot源码适配我们自己的板子
3.1 uboot的三大功能
主要有三大功能:硬件初始化、加载linux内核、挂载根文件系统
3.1.1 uboot的硬件初始化
硬件初始化功能:是为了运行linux内核提前准备一个好的硬件环境,但是开发板上的硬件不用全部初始化,因为uboot仅仅就是用来启动linux内核并且它的生命周期极短,如果做大量的无用的硬件初始化,跟linux内核运行毫无关系并且延长了uboot的启动时间(用户体验不好),关键如果做大量无用的初始化,内核一旦启动,boot生命结束所作的工作对于内核来说全部无效了,内核会重新初始化。
uboot中必须要初始化的八大硬件
1.初始化CPU:主要初始化Cache缓存,将Cache中的数据,指令进行清除和无效处理,为了防止 CPU上电之后上来就去Cache中拿去指令或者数据,造成各种异常
2.初始化系统时钟:I2C控制器的时钟,UART控制器的时钟,800MHz,200MHz,50MHz
3.初始化内存
4.初始化闪存
5.初始化网卡或者OTG USB
6.关闭中断:加快uboot启动的速度,缩短系统启动时间,提高用户体验,中断处理将来交给linux内核来处理
7.初始化串口
8.关闭看门狗:加快uboot启动的速度,防止系统复位,另一个方面,uboot在内核启动完后就结束了执行,此时不会喂狗,会产生复位信号
3.1.2 加载启动linux内核
- 从闪存上加载linux内核到内存并且从内存启动linux内核,
- 通过bootcmd环境变量实现
本地启动:setenv bootcmd mmc read 0x48000000 0x800 0x3000 \; bootm 0x480000000
网络启动: setenv bootcmd tftp 0x48000000 uImage \; bootm 0x48000000
去除错误的uboot环境变量:
例如:
setenv myseverip 192.168.1.8
saveenv
setenv myseverip
saveenv
3.1.3 挂载根文件系统rootfs
给linux内核传递启动参数告诉根文件系统rootfs位于闪存的某个分区上,通过bootargs环境变量来实现
例如:setenv bootargs root=/dev/mmcblk0p2 init=/linuxrc cosole=ttySAC0,115200 maxcpus=1 lcd=wy070ml tp=gslx680-linux
3.2 uboot源码
获取交叉编译器并且安装交叉编译器
建议:从芯片厂家获取
切记:交叉编译器的版本要和uboot源码版本要匹配
获取uboot源码
切记:从芯片厂家获取
注意:此代码仅仅支持芯片厂家的参考板,不一定能够在自己设计的开发板上运行起来
所以将来势必要根据硬件差异修改uboot源码
例如:X6818开发板的官方uboot源码位于:uboot.tar.bz2
uboot源码操作三步骤:
make distclean //获取最干净的uboot源码,只做一次
make abc_config //配置uboot源码,配置成能够运行在abc这个参考板上,只做一次
make //编译,只要修改了uboot源码,必须重新编译
ls
ubootpak.bin //三星独有 u-boot.bin
案例:交叉编译X6818开发板官方的uboot源码
上位机执行:将源码拷贝到linux中,进入uboot源码根目录
tar -xvf uboot.tar.bz2
//三步骤
make distclean
make x6818_config
make -j2/-j4/-j8 //采用2核或者4核或者8核同时编译
ls
ubootpak.bin //编译生成的uboot可执行文件
修改uboot源码中跟硬件信息相关的头文件
此头文件定义了大量的外设硬件信息,只需根据硬件差异修改对应的宏即可:
此头文件位于:uboot源码/include/configs/abc.h (abc就是参考板的名称)
案例:利用sourceinsight创建uboot源码工程,然后阅读x6818开发板对应的头文件:(sourceinsight具体安装参见文档:sourceinsight.txt)
位于:uboot/include/configs/x6818.h
相关的硬件信息和环境变量信息
#define CONFIG_SYS_TEXT_BASE 0x43C00000 /* uboot在内存中的起始地址 代码段*/
#define CONFIG_SYS_INIT_SP_ADDR CONFIG_SYS_TEXT_BASE /* 栈指针sp的地址 */
#define CONFIG_MEM_MALLOC_START 0x44000000 /* 堆区 */
#define CONFIG_MEM_MALLOC_LENGTH 32*1024*1024 /* 堆区大小 */
#define CONFIG_MEM_LOAD_ADDR 0x48000000 /* 默认的下载地址 */
#define CONFIG_SYS_SDRAM_BASE CFG_MEM_PHY_SYSTEM_BASE /* 内存的起始地址*/
#define CONFIG_SYS_SDRAM_SIZE CFG_MEM_PHY_SYSTEM_SIZE /* 内存的大小 */
#define CONFIG_BOOTCOMMAND “mmc read 0x48000000 0x800 0x3000 ; boot 0x48000000” //uboot的bootcmd环境变量默认的值,也可以通过setenv修改
#define CONFIG_BOOTARGS "lcd=wy070ml tp=gslx680" //uboot的bootargs环境变量默认的值,也可以通过setenv修改
#define CONFIG_SYS_PROMPT "tarena# " //uboot命令行提示符
#define CONFIG_S5P_SERIAL_INDEX CFG_UART_DEBUG_CH //指定uboot的调试串口
#define CONFIG_S5P_SERIAL_CLOCK CFG_UART_CLKGEN_CLOCK_HZ//指定uboot调试串口的时钟频率
#define CONFIG_S5P_SERIAL_PORT (void *)IO_ADDRESS(PHY_BASEADDR_UART0)//串口的基地址
#define CONFIG_BAUDRATE CFG_UART_DEBUG_BAUDRATE //串口的波特率
#if defined(CONFIG_ENV_IS_IN_MMC)
#define CONFIG_ENV_OFFSET 512*1024 /*512KB*/ //指定uboot的环境变量在EMMC上的起始地址
#define CONFIG_ENV_SIZE 32*1024 //32KB,环境变量占用的EMMC大小
结论:EMMC分区的信息如下:
0-------512----------512KB----------544KB--------1M-----------65M----------819M------剩余
保留 uboot 环境变量 目前没用 uImage rootfs
saveenv
如何知道uboot源码运行的入口函数呢:编译uboot命令时,执行:make V=1,通过编译信息可以获取uboot使用的链接脚本
结论:uboot代码段的第一个文件为:arch/arm/cpu/slsiap/s5p6818/start.o对应的源文件start.S,所以uboot运行的第一个文件为:arch/arm/cpu/slsiap/s5p6818/start.S,利用sourcesighth跟踪start.S文件即可顺藤摸瓜理清代码的执行流程即可
board_init_f函数:位于 uboot/common/board_f.c
board_init_f函数核心内容如下:
if (initcall_run_list(init_sequence_f))
hang(); //如果发现某个硬件初始化异常,直接让uboot进入死循环,不允许继续运行
// 研究发现init_sequence_f是一个函数指针数组,每个元素都是一个初始化函数
//此数组中放置了一堆的硬件初始化函数
static init_fnc_t init_sequence_f[] = {
...
board_early_init_f, //开发板外设的初始化函数
init_func_i2c, //初始化I2C总线
init_func_spi, //初始化SPI总线
NULL
};
// initcall_run_list 此函数就是将上面的初始化函数挨个调用一遍
int initcall_run_list(const init_fnc_t init_sequence[]) {
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)()) { //调用init_sequence_f数组中的一堆初始化函数
return -1; //初始化函数执行失败返回非0
}
}
return 0; //初始化函数执行成功返回0
}
// 研究其中一个初始化函数:board_early_init_f:
// 此函数位于:/opt/uboot/board/s5p6818/x6818/board.c
int board_early_init_f(void){
bd_gpio_init(); //初始化S5P6818的GPIO
bd_pmic_init(); //初始化电源管理芯片
nxp_rtc_init(); //初始化RTC
return 0; //成功
}
如果将来某个硬件初始化有问题,只需在数组中找到对应的硬件初始化函数,然后跟踪这个初始化函数找到问题所在解决问题即可
如果将来需要在uboot中添加一个新的硬件初始化,可以将初始化函数添加到init_sequence_f数组中,但是对初始化函数的返回值有严格要求:
初始化函数执行成功一定要返回0,失败返回非0
board_init_r函数:位于 uboot/common/board_r.c
board_int_r函数内容如下:
if (initcall_run_list(init_sequence_r))
hang(); // 研究函数指针数组init_sequence_r:
//此数组中放置了大量的硬件初始化函数
init_fnc_t init_sequence_r[] = {
...
initr_serial, //初始化串口
initr_flash, //初始化norflash
initr_nand, //初始化nandflash
initr_mmc, //初始化EMMC,TF卡,SD卡
initr_net, //初始化网卡,协议栈
run_main_loop, //最后uboot进入run_main_loop
}
归宿
run_main_loop->main_loop->cli_loop->cli_simple_loop:这些函数内部没有硬件初始化代码,所以这些函数无需跟踪,最终研究cli_simple_loop函数:
cli_simple_loop:
char lastcommand[CONFIG_SYS_CBSIZE]; //定义数组
for (;;) {
//获取从命令行中输入的uboot命令
cli_readline(CONFIG_SYS_PROMPT);
//将输入的uboot命令拷贝到数组lastcommand(将来存储的是命令的字符串)
strcpy(lastcommand, console_buffer);
//例如:lastcommadn="ping 192.168.1.8"
//运行输入的uboot命令
run_command_repeatable(lastcommand, flag);
//所以将来可以调用此函数在uboot中单独执行一个uboot命令
//例如:run_command_repeatable("ping 192.168.1.8", 0); //0表示不可重复执行
}
3.3 uboot的使用
将uboot命令行提示符修改为自己喜欢的提示符,然后在uboot中添加LED灯的开灯支持,上电等闪烁5次
1:修改x6818.h文件将提示符改成自己的提示符
2:在硬件初始化函数中,添加灯的操作
在board.c中添加led灯的初始化函数
#include "led.h"
/* call from u-boot */
int board_early_init_f(void)
{
int temp = 0;
bd_gpio_init();
bd_alive_init();
#if (defined(CONFIG_PMIC_NXE2000)||defined(CONFIG_PMIC_AXP228))&& !defined(CONFIG_PMIC_REG_DUMP)
bd_pmic_init();
#endif
#if defined(CONFIG_NXP_RTC_USE)
nxp_rtc_init();
#endif
led_all_init();
for(temp=0;temp<5;temp++){
led_all_on();
mdelay(1000);
led_all_off();
mdelay(1000);
}
return 0;
}
3:将led.c和led.h复制到board/s5p6818/x6818中
4:修改Makefile文件,添加对led.o
5:重新编译 输入make
6:将生成的ubootpak.bin烧录到开发板
3.4 显示LOGO
1:根据Image2Lcd制作图片,工具在Image2Lcd.rar中,LCD的显示原理是通过对像素点RGB三基色的刷新,因此需要将先设计好的logo图片转换为bmp格式,然后再使用Image2Lcd工具来制作logo的数组文件,该工具运行如图
将图片制作成数组放在一个.c文件中,在u-boot中添加代码实现将数组数据搬移到显存地址,实现logo显示。
2:将生成的文件放入u-boot的common目录下
3:在common下新建lcd_logo.c
#define LCD_VIDEO_ADDR (0x46000000) // 显存地址
#define LCD_BASE (LCD_VIDEO_ADDR) // 显存地址
#define ROW (600) //LCD的行数
#define COL (1024) //LCD的列数
#define W (501) //图片的长度
#define H (500) //图片的高数
#define X0 (COL/2 - W/2) //图片在lcd上显示的X位置
#define Y0 (ROW/2 - H/2) //图片在lcd上显示的Y位置
extern const unsigned char gImage_a[];
// 描点
void lcd_draw_pixel(int row, int col, int color)
{
unsigned long * pixel = (unsigned long *)LCD_BASE;
*(pixel + row * COL + col) = color;
}
// 清屏
void lcd_clear_screen(int color)
{
int i, j;
for (i = 0; i < ROW; i++)
for (j = 0; j < COL; j++)
lcd_draw_pixel(i, j, color);
}
// 画图
void lcd_draw_logo()
{
int i, j;
unsigned char *p = (unsigned char *)gImage_a;
int blue, green, red;
int color;
// 清屏
lcd_clear_screen(0x000000);
// 画图
for (i = Y0; i < (Y0 + H); i++)
for (j = X0; j < (X0 + W); j++)
{
blue = *p++;
green = *p++;
red = *p++;
color = red << 16 | green << 8 | blue << 0;
lcd_draw_pixel(i, j, color);
}
}
修改arch/arm/cpu/slsiap/common/cmd_draw_logo.c文件
extern void lcd_draw_logo(); //添加函数声明
static void fill_lcd(U32 FrameBase, int XResol, int YResol, U32 PixelByte)
{
#if 0
… //原先的内容注释掉
#endif
lcd_draw_logo();//将来可以把自己的画各种图形的函数放在这里调用
}
4:重新编译:make
5:此时ubootpak文件大小为1.1M,所以需要重新配置分区
修改emmc分区
----采用网络烧写----
EMMC分区
fdisk 2 2 0x400000:0x4000000 0x4400000:0x2f200000
更改uboot的环境变量地址
#define CONFIG_ENV_OFFSET 3*1024*1024 //指定uboot的环境变量在EMMC上的起始地址
#define CONFIG_ENV_SIZE 1024*1024 //环境变量占用的EMMC大小
烧录uboot
utf ubootpak.bin
update_mmc 2 2ndboot 0x48000000 0x200 0x200000
烧录uImage和rootfs_ext4
tftp uImage
mmc write 0x48000000 0x2000 0x3000
tftp rootfs_ext4.img
mmc write 0x48000000 0x22000 0x32000
更改环境变量
setenv bootcmd mmc read 0x48000000 0x2000 0x3000 \; bootm 0x48000000
setenv bootargs root=/dev/mmcblk0p2 init=/linuxrc console=ttySAC0,115200 maxcpus=1 lcd=wy070ml tp=gslx680-linux
saveenv
问题:
我在uboot源码中添加了LED和BEEP的功能支持,然后下载到EMMC上,第一个开机运行uboot都没问题但是每当设置环境变量,保存环境变量之后,一旦重启uboot无法启动
分析:原先没有添加额外功能的uboot和环境变量在EMMC上的存储空间没有形成叠加,重合,可以正常运行,但是添加完额外功能之后导致uboot体积变大跟环境变量在EMMC上的存储空间形成重合叠加,导致环境变量数据破坏了uboot代码,导致uboot无法启动
解决:只需将环境变量在EMMC上的起始地址往后挪到即可
问题:
uboot添加开机让蜂鸣器响一声,通知用户系统开机了,但是添加完毕之后,此功能无法生效,但是单独测试峰鸣器没有毛病
分析:uboot的代码存在GPI0的初始化代码,而蜂鸣器的代码不小心放到了原先自带的代码之前,导致uboot自带的代码重新覆盖了b咱们本身的初始化代码,导致问题出现
解决:所以uboot中添加功能时一定要注意顺序问题,只需将蜂鸣器的初始化放到自带的初始化代码之后
4、Linux内核
4.1 linux内核的七大功能
- 内存管理
- 进程管理
- 网络协议栈
- 系统调用
- 文件系统
- 设备驱动
- 平台相关(BSP)
4.2 linux内核源码
获取交叉编译器并且安装交叉编译器
建议:从芯片厂家获取
切记:交叉编译器的版本要和linux内核源码版本要匹配
获取linux内核源码
切记:从芯片厂家获取
注意:此代码仅仅支持芯片厂家的参考板,不一定能够在自己设计的开发板上运行起来
所以将来势必要根据硬件差异修改内核源码
例如:X6818开发板的官方内核源码位于:kernel.tar.bz2
先不要根据硬件差异修改官方的内核源码,先对内核源码做编译验证:
linux内核源码操作三步骤:
make distclean //获取最干净的代码,只做一次
make abc_defconfig //配置内核源码能够运行在abc这个参考板上,abc就是参考板的名称,只做一次
make uImage //交叉编译
ls 内核源码根目录/arch/arm/boot/uImage
案例:交叉编译X6818开发板的linux内核源码
上位机执行:上位机首先安装mkimage工具,此工具用来只做uImage
sudo cp uboot/tools/mkimage /sbin/ //没有交叉编译uboot的,先交叉编译uboot,编译完uboot之后会有mkimage
tar -xvf kernel.tar.bz2 // 解压
//进入内核源码根目录
make distclean
make x6818_defconfig
time make uImage -j2/-j4/-j8 //time命令获取编译内核源码所需的时间
ls arch/arm/boot/
uImage
配置步骤如下:
cd /kernel
make menuconfig //生成一个菜单
注意:需要执行以下命令安装ncurses库,安装命令:sudo apt-get install libncurses5-dev
1.System Type —>//系统类型配置选项
2.Boot options —> //启动选项配置
3.Device Drivers —> //内核支持的硬件设备驱动选项
//MTD:就是Norflash和Nandflash的驱动总称
<*> Memory Technology Device (MTD) support --->
//Norflash驱动
Mapping drivers for chip access --->
//Nandflash驱动
<*> NAND Device Support (NEW) --->
//S5P6818的Nandflash控制器驱动 也就是Nandflash的驱动
<*> Nexell nand support
//网卡驱动
[*] Network device support --->
//有线网卡驱动
[*] Ethernet driver support --->
//X6818网卡驱动
[*] Nexell devices
//输入设备驱动支持
Input device support --->
//键盘驱动
[*] Keyboards --->
//X6818开发板上的按键驱动
<*> SLsiAP push Keypad support
//游戏手柄摇杆驱动
[*] Joysticks/Gamepads --->
//触摸屏驱动
[*] Touchscreens --->
//X6818开发板的gslx680型号的触摸屏驱动
<*> GSLX680 touchscreen
//I2C驱动支持
<*> I2C support --->
//处理器内部的I2C控制器的驱动
简称I2C总线驱动
I2C Hardware Bus support --->
<*> Slsiap I2C //S5P6818的I2C控制器驱动
//1-Wire一线式总线驱动支持
<*> Dallas's 1-wire support --->
//外设驱动
1-wire Slaves--->
//DS18B20温度传感器驱动
<*> Thermal family implementation
//看门狗驱动支持
[*] Watchdog Timer Support --->
//摄像头驱动支持
<*> Multimedia support --->
//一定选:摄像头的框架驱动
<*> Video For Linux
//X6818开发板上的摄像头驱动
[*] Nexell V4L2 Devices --->
//FB驱动支持
Graphics supports-->
//LCD显示屏驱动
<*> Support for frame buffer devices --->
//S5P6818的LCD控制器的驱动
<*> SLsiAP framebuffer support
//声卡驱动
<*> Sound card support --->
//USB接口的硬件设备驱动支持
[*] USB support --->
//U盘驱动
<*> USB Mass Storage support
//USB转串口驱动支持
<*> USB Serial Converter support --->
//PL2303 USB转串口芯片的驱动
<*> USB Prolific 2303 Single Port Serial Driver
//EMMC,SD卡,TF卡驱动支持
<*> MMC/SD/SDIO card support --->
[*] Support SD/MMC CH 0 //SD0卡槽
[*] Use SD/MMC CH 0 DMA MODE
[*] Support SD/MMC CH 1 //SD1卡槽
[*] Use SD/MMC CH 1 DMA MODE
[*] Support SD/MMC CH 2 //EMMC驱动
[*] Use SD/MMC CH 2 DMA MODE
文件系统配置选项
明确:文件系统是用于访问磁盘上的文件或者目录不同的文件系统格式,对文件和目录的管理方式不一样,
例如:NTFS管理的硬盘分区对单个文件的大小是没有限制的,但是FAT32管理的硬盘分区对单个文件的大小是有限制的就是单个文件大小不能大于4GB
结论:只要用闪存,要想访问闪存的文件或者目录必须指定某种文件系统格式
File systems --->
//回忆:rootfs_ext4.img,此根文件系统镜像采用的NORFLASH
文件系统格式为EXT4
//ext4文件系统格式应用EMMC这种闪存
<*> The Extended 4 (ext4) filesystem
DOS/FAT/NT Filesystems --->
//VFAT就是FAT32文件系统格式
//应用于U盘,SD卡,TF卡
<*> VFAT (Windows-95) fs support
//NTFS应用于硬盘或者移动硬盘
<*> NTFS file system support
[*] Miscellaneous filesystems --->
//JFFS2文件系统格式应用于norflash和nandflash
//此文件系统格式管理的文件和目录是可以读可以写
< > Journalling Flash File System v2 (JFFS2) support //前提选中MTD驱动
//cramfs文件系统也可以应用nor或者nand
此文件系统管理的文件和目录只能读,不能写,并且是压缩的
一般利用这种特性保护数据
< > Compressed ROM file system support (cramfs)
[*] Network File Systems ---> (坑)
//采用NFS网络服务的根文件系统
如果uboot的bootargs支持NFS网络服务找
根文件系统,如果内核这个选项没有支持
内核同样找不到根文件系统
[*] Root file system on NFS
修改了linux内核驱动后,都需要重新编译源码才能生效
修改内核源码跟硬件信息相关的源文件,根据硬件的差异修改此文件即可
此文件位于:内核源码根目录/arch/arm/plat-s5p6818/x6818/device.c
此文件定义了大量的硬件外设的信息!
切记:此文件中定义大量的结构体变量来表示硬件信息,这些结构体变量将来给驱动程序使用
比如led
#if defined(CONFIG_LEDS_GPIO)
static struct gpio_led x4418_leds[] = {
{
.name = "led1", //D22
.default_trigger = "none",
.active_low = 1,
.gpio = PAD_GPIO_B + 26,
}, {
.name = "led2", //D23
.default_trigger = "none",
.active_low = 1,
.gpio = PAD_GPIO_C + 11,
}, {
.name = "led3", //D24
.default_trigger = "none",
.active_low = 1,
.gpio = PAD_GPIO_C + 7,
}, {
.name = "led4", //D25
.default_trigger = "none",// heartbeat
.active_low = 1,
.gpio = PAD_GPIO_C + 12,
},
};
5、NFS网络服务
各种应用程序属于根文件系统rootfs的一部分,现在如果要更新一个应用程序,需要将它放到根文件系统rootfs_ext4.img这个文件中,还得用fastboot下载烧写,将来错了,还需要修改,下载烧写开发效率很低,关键是降低了EMMC的使用寿命,干脆采用NFS网络服务,就是在上位机部署一个NFS网络服务的共享目录,将来各种应用程序放在上位机编辑和交叉编译,然后让下位机利用NFS网络服务到上位机找到共享目录,一旦找到共享目录就可以直接在下位机运行,以后应用程序出错了,只需在上位机修改和交叉编译,下位机直接运行即可,无需频繁的下载烧写,大大提高了开发效率也提高了EMMC的使用寿命
5.1 NFS的使用
使用步骤:
1:给上位机的linux系统配置一个静态IP地址
2:上位机安装NFS网络服务
安装NFS服务
sudo apt-get install nfs-kernel-server
上位机修改NFS配置文件(18.04系统之后的ubuntu系统都需要添加此配置 https://blog.csdn.net/u014452136/article/details/134098155 )
sudo vim /etc/default/nfs-kernel-server 在文件最后添加:
RPCNFSDOPTS="--nfs-version 2,3,4 --debug --syslog"
3:上位机指定NFS网络服务的共享目录
sudo vim /etc/exports 在文件最后添加:
/home/nfsroot *(rw,sync,no_root_squash)
保存退出即可
说明:
/home/nfsroot:将上位机的/home/nfsroot目录作为共享目录,将来下位机可以访问这个共享目录中的文件和目录
*:任何一个IP地址的客户端都可以来访问
rw:下位机可以对共享目录中的文件进行读或者写
sync:下位机如果修改了,同步更新上位机对应的文件
no_root_squash:普通用户也可以访问
4:重启上位机的NFS网络服务与新建NFS共享目录
sudo service nfs-kernel-server restart //只要修改了exports配置文件,必须重新启动NFS网络服务 此时静静等待下位机来访问
mkdir /home/nfsroot // 新建NFS共享目录
5:在上位机的NFS网络服务的共享目录中添加一个测试程序:
cd /home/nfsroot
sudo vim helloworld.c // 添加
#include <stdio.h>
int main(void)
{
while(1) {
printf("hello,world\n");
sleep(1);
}
return 0;
}
保存退出
交叉编译: arm-cortex_a9-linux-gnueabi-gcc -o helloworld helloworld.c
6:重启下位机,进入uboot的命令行执行,前提是已经烧写过rootfs_ext4.img根文件系统
7:进入下位机linux系统
ifconfig eth0 192.168.1.110 //给下位机的网卡配置IP地址,此IP地址必须和上位机的IP地址192.168.1.8在同一个网段
ping 192.168.1.9 //下位机ping上位机,保证网络是联通状态 出现类似:64 bytes from 192.168.1.8: seq=0 ttl=64 time=3.598 ms 信息表示网络联通了
按ctrl+c退出ping命令
mount -t nfs -o nolock 192.168.1.9:/home/nfsroot /mnt
说明:
mount:挂接命令
-t nfs:采用NFS网络服务来访问共享目录
-o nolock:对共享的文件不进行加锁访问
结果:利用NFS网络服务,将上位机/home/nfsroot共享目录挂接到下位机的/mnt目录上,以后在下位机访问mnt目录就是在访问上位机的/home/nfsroot目录
cd /mnt // 等价于进入上位机的/home/nfsroot共享目录中
ls
helloworld helloworld.c
./helloworld //在下位机运行上位机的helloworld可执行文件 下位机的linux系统利用NFS网络服务偷摸的将上位机的helloworld下载到下位机的内存并且启动
5.2 共享rootfs
1:重新设置bootargs,告诉内核根文件系统在上位机
setenv bootargs root=/dev/nfs nfsroot=192.168.1.9:/home/rootfs ip=192.168.1.110:192.168.1.9:192.168.1.1:255.255.255.0 init=/linuxrc console=ttySAC0,115200 maxcpus=1 lcd=wy070ml tp=gslx680-linux
saveenv
print //确认设置的bootargs正确无误!
说明:
root=/dev/nfs:告诉linux内核,将来利用NFS网络服务,到上位机去找根文件系统rootfs
nfsroot=192.168.1.9:/home/rootfs:告诉linux内核,找到IP地址为192.168.1.9这个上位机,并且这个上位机的/home/rootfs就是要找的根文件系统rootfs
ip=给下位机配置的新IP地址:指定上位机的IP:指定下位机的网关:指定下位机的掩码
2::准备一个在上位机存放的并且是已经做好的根文件系统rootfs
文件在rootfs_qt.tar.bz2
3:将文件保存到NFS共享目录下/home/rootfs
4:重启下位机,静静等待着下位机的linux系统启动
6、内核配置菜单
目的:在内核中添加一个自己驱动程序的配置选项,将来用户可以通过菜单来支持或者不支持自己的驱动,只需要修改Kconfig和Makefile文件
6.1 Kconfig文件
- 功能:生成一个菜单选项,将来通过make menuconfig供用户进行选择或者不选择操作
- 关键字:
config
用于生成一个配置选项,生成配置选项的名称:CONFIG_XXX
生成的配置选项CONFIG_XXX给Makefile使用
例如:config HELLOWORLD
结果:最终生成一个选项,其名称为:CONFIG_HELLOWORLD
tristate
表示生成的配置菜单对应的操作方式有三种:“<>”
1.按Y键选择为*,表示将来把这个菜单选项对应的驱动程序和uImage编译在一起
将来生成的配置选项CONFIG_XXX=y(其值为y)
类似:静态库
2.按N键选择为空,表示对应的驱动不进行编译,也就不支持了
3.按M键选择为M,表示将对应的驱动和uImage分开编译
类似:动态库
例如:tristate "hello,world"
将来make menuconfig中就会出现配置菜单选项的提示信息:helloworld
bool
表示生成的配置选项的操作方式有两种:“[]”
1.按Y键选择为*,表示将来把这个菜单选项对应的驱动程序和uImage编译在一起
将来生成的配置选项CONFIG_XXX=y(其值为y)
类似:静态库
2.按N键选择为空,表示对应的驱动不进行编译,也就不支持了
help
表示配置选项的帮助信息
例如:help
this is a test
将来通过按?键可以查看这个菜单的帮助信息
- 使用
config HELLOWORLD
(TAB键)tristate或者bool "hello,world"
(TAB键)help
this is a test
案例:向linux内核添加一个新的菜单选项
上位机执行:
cd kernel
vim drivers/char/Kconfig 找个合适的地方添加:
config HELLOWORLD
tristate "hello,world"
help
this is a test
config HELLOTARENA
bool "hello,tarena"
help
hello,tarena,this is a test
保存退出
所有的配置文件都在.config这个文件中
6.2 Makefile
vim drivers/char/Makefile
目的:按照指定的编译方式来编译驱动程序(在一起,分家,不编译)
语法: obj-$(CONFIG_XXX) += 驱动文件.o
说明:
1.如果按Y键选择为*,CONFIG_XXX=y,Makefile展开得到:obj-y += helloworld.o 将来把helloworld.c和uImage编译在一起
2.如果按M键选择为M,CONFIG_XXX=m,Makefile展开得到:obj-m += helloworld.o 将来把helloworld.c和uImage分开编译,编译生成helloworld.ko
3.如果按N键选择为空,CONFIG_XXX=空,Makfile展开得到:没有,不编译
6.3 案例
向linux内核添加LED驱动程序,驱动程序在led_drv.zip中,其中led_drv.c为驱动程序 led_test为应用程序
1:将led驱动复制到内核中
cp led_drv.c …/kernel/drivers/char/
2:修改内核的Kconfig,添加菜单选项
vim kernel/drivers/char/Kconfig 添加
config TARENA_LED
tristate "tarena led driver"
help
this is x6818 board led driver
3:修改Makefile来添加编译支持
vim drivers/char/Makefile 添加
obj-$(CONFIG_TARENA_LED) += led_drv.o
4:配置内核源码,make menuconfig 选择 TAREND_LED 这里输入y
5:检查.config,确保TARENA_LED是y
6:编译内核,并烧入开发板
7:交叉编译测试程序
将拷贝的ked_test.c进行交叉编译,并将生成的文件放入根文件系统中(/home/rootfs)
8:重启下位机,使用nfs网络来挂载根文件系统,然后再下位机执行上位机交叉编译生成的文件即可
注意当第四步输入m时,则表示将对应的驱动和uImage分开编译
5: 单独编译led驱动并将 make modules ,将生成的led_drv.ko文件拷贝到跟文件系统中(rootfs)
ls drivers/char/led_drv.ko
cp drivers/char/led_drv.ko /home/rootfs/home/drivers 将编译生成的LED驱动模块文件拷贝根文件系统中
6: 编译内核,并烧入开发板
7:交叉编译测试程序
8:重启下位机,使用nfs网络来挂载根文件系统,向linux内核安装LED驱动,然后再执行上位机交叉编译生成的文件即可
cd /home/drivers
insmod led_drv.ko //向linux内核安装LED驱动,位于内存中,insmod=insert module
再执行交叉编译的代码
rmmod led_drv //从linux内核中卸载LED驱动,rmmod=remove module
7、根文件系统rootfs
7.1 基本概念
- 根文件系统与普通文件系统有啥区别:
根文件系统不是一种文件系统格式,但是根文件系统里面的文件或者目录,你要想访问,必须基于某种文件系统格式 - 必要的八大目录:
bin目录:保存普通用户命令的目录
sbin目录:保存超级用户命令的目录
lib目录:保存各种库文件的目录
etc目录:保存系统配置文件的目录
usr目录:保存其它命令后者库文件的目录
dev目录:保存设备文件,跟驱动相关
proc目录:跟驱动相关
sys目录:跟驱动相关
注意:dev,proc,sys跟驱动相关,里面的内容由驱动自动创建!所以这三个目录只要创建好即可,里面的内容无需关注! - 可选目录:
home:作为普通用户主目录
mnt:作为U盘,TF卡,SD卡,闪存分区的挂节点
var:作为临时文件的存放目录
opt:存放自己的可执行文件等
其它目录根据用户需求自行创建
7.2 busybox
1:从www.busybox.net自己下载一个busybox源码,或者用文件里面的busybox-1.21.1.tar.bz2
2:修改busybox的Makefile指定处理器架构和交叉编译器
cd /home/busybox //进入busybox源码根目录
vim Makefile
ARCH ?= $(SUBARCH)
修改为: ARCH = arm 说明:指定将来运行在ARM架构
CROSS_COMPILE ?=
修改:CROSS_COMPILE = arm-cortex_a9-linux-gnueabi- 说明:指定交叉编译器
3:配置busybox源码,采用完整版驱动操作命令
cd /home/busybox
make menucofig
注意:出来的菜单都是各种命令的选项,选择*表示支持这个命令,不选择将来不支持这个命令,目前busybox默认支持的命令已经足够你用了!
Linux Module Utilities --->
//按N键去除简化版的insmod和rmmod命令
[ ] Simplified modutils
//一旦将上面选项去除,立马出现以下完整版的命令无脑全部选中:
[*] insmod
[*] rmmod
[*] lsmod
[*] Pretty output
[*] modprobe
[*] Blacklist support
[*] depmod
4:交叉编译
cd /home/busybox
make -j2
5:安装
make install
安装就是将编译busybox生成的二进制文件统一的拷贝到指定的某个目录下
busybox指定的默认安装目录为_install目录
ls /home/busybox/_install //查看编译生成的二进制文件内容
bin sbin usr //三大必要目录
6:进入生成目录查看
cd /home/busybox/_install
ls xxx
向根文件系统rootfs添加动态库到必要目录lib中
问:为何要添加动态库而没有静态库呢?
答:本质上位机编辑和交叉编译下位机运行开发模式 ,所以向根文件系统添加程序运行时的动态库即可
问:动态库在哪里呢?总不能自己写去吧
答:就在交叉编译器中,所以只需从交叉编译器中拷贝所需的动态库即可
问:如何知道一个应用程序所需哪些动态库呢?
答:总不能将无用的动态库也进行拷贝吧,这种全部拷贝,最终会浪费闪存空间
拷贝动态库的原则:需要哪些动态库就拷贝哪些动态库,获取所需动态库的命令:arm-cortex_a9-linux-gnueabi-readelf -d 应用程序
问:从交叉编译器中找到的动态库拷贝到哪里?
答:必须只能拷贝到根文件系统rootfs的lib必要目录
向根文件系统rootfs添加动态库的流程步骤如下:
1:实际开发时,如果上位机的linux系统中有多套交叉编译器,首先要明确编译busybox使用的交叉编译器的信息:
which is arm-cortex_a9-linux-gnueabi-gcc
得到:/home/down/toolchains/bin/arm-cortex_a9-linux-gnueabi-gcc
说明:编译busybox使用的交叉编译器路径:/home/down/toolchains
2:获取应用程序所需哪些动态库,将动态库进行拷贝添加,由于目前交叉编译busybox仅仅获取了一个可执行文件busybox,所以只需获取它所需的动态库即可
rm /home/rootfs -fr //先将存在的根文件系统删除
cp /home/busybox/_install /home/rootfs -frd
cd /home/rootfs //进入自己制作的根文件系统根目录
ls
bin sbin usr linuxrc
mkdir lib etc proc dev sys home var mnt // 创建必要目录和可选目录
3:获取busybox可执行程序所需的动态库
arm-cortex_a9-linux-gnueabi-readelf -d /hoom/rootfs/bin/busybox
得到:busybox只需两个动态库:libm.so.6和libc.so.6
//然后到交叉编译器中找到libc.so.6
cd /home/down/toolchains
find . -name libc.so.6
得到:./arm-cortex_a9-linux-gnueabi/sysroot/lib/libc.so.6
//查看动态库的属性
ls ./arm-cortex_a9-linux-gnueabi/sysroot/lib/libc.so.6 -lh
得到:./arm-cortex_a9-linux-gnueabi/sysroot/lib/libc.so.6 -> libc-2.18-2013.10.so
说明:libc.so.6仅仅是libc-2.18-2013.10.so的一个软连接(快捷方式)
// 所以拷贝时需要连同实体文件一起拷贝:
cp ./arm-cortex_a9-linux-gnueabi/sysroot/lib/libc.so.6 /home/rootfs/lib -d
cp ./arm-cortex_a9-linux-gnueabi/sysroot/lib/libc-2.18-2013.10.so /home/rootfs/lib -d
//然后到交叉编译器中找到libm.so.6
cd /home/down/toolchains/
find . -name libm.so.6
得到:./arm-cortex_a9-linux-gnueabi/sysroot/lib/libm.so.6
//查看动态库的属性(好习惯)
ls ./arm-cortex_a9-linux-gnueabi/sysroot/lib/libm.so.6 -lh
得到:./arm-cortex_a9-linux-gnueabi/sysroot/lib/libm.so.6 -> libm-2.18-2013.10.so
说明:libm.so.6仅仅是libm-2.18-2013.10.so的一个软连接(快捷方式)
所以拷贝时需要连同实体文件一起拷贝:
cp ./arm-cortex_a9-linux-gnueabi/sysroot/lib/libm.so.6 /home/rootfs/lib -d
cp ./arm-cortex_a9-linux-gnueabi/sysroot/lib/libm-2.18-2013.10.so /home/rootfs/lib -d
结论:将来拷贝动态库只需按照以上思路拷贝即可!
4:向根文件系统的lib目录添加加载器(动态链接库)
加载器功能:负责将动态库里的变量和函数加载到当前进程的地址空间上,并且加载器本质也是一个动态库并且加载器的名称都是以“ld-”开头,一般动态库都是以“lib”开头
加载器也在交叉编译器中,将拷贝加载器到根文件系统lib目录下:
cd /home/down/toolchains/
find . -name ld-*
得到:./arm-cortex_a9-linux-gnueabi/sysroot/lib/ld-2.18-2013.10.so
./arm-cortex_a9-linux-gnueabi/sysroot/lib/ld-linux.so.3
//查看文件属性
ls ./arm-cortex_a9-linux-gnueabi/sysroot/lib/ld-* -lh
得到:ld-linux.so.3是ld-2.18-2013.10.so的一个软连接
所以:一起拷贝
cp ./arm-cortex_a9-linux-gnueabi/sysroot/lib/ld-* /home/rootfs/lib -d
5:向根文件系统必要目录etc添加系统启动的必要配置文件和系统启动的脚本文件
向根文件系统rootfs的etc添加系统启动必要配置文件inittab添加过程如下:
vim /home/rootfs/etc/inittab 添加如下内容:
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
从嵌入式linux系统启动流程看待inittab文件使用:
1:上电-> CPU运行uboot-> uboot先做硬件初始化-> uboot然后从某个地方加载内核到内存-> uboot然后给内核传递启动参数-> uboot最后启动内核-> 内核启动,uboot生命结束
2:->内核首先做七大子系统初始化-> 内核运行到最后根据uboot传递的参数到某个地方找根文件系统-> 一旦找到运行根文件统/sbin/init第一号进程-> 第一号进程init首先打开根文件系统/etc/inittab文件-> init找到关键字sysinit,一旦找到,init进程创建一个子进程来执行sysinit指定的脚本程序rcS(位于根文件系统rootfs/etc/init.d目录下)父进程init等待着子进程结束
3:->rcS执行完毕,父进程init再找到关键字respawn-> 找到以后,init再次创建一个子进程来执行根文件系统/bin/sh这个程序,此程序就是shell程序,用户至此可以输入命令,父进程init等待着子进程sh结束!
6:向根文件系统的etc添加系统启动脚本文件rcS
mkdir /home/rootfs/etc/init.d/
vim /home/rootfs/etc/init.d/rcS 添加五句必要的命令:
mount -a
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
给脚本添加可执行权限 chmod 777 /home/rootfs/etc/init.d/rcS
mount -a:当执行此命令,系统自动解析执行根文件系统/etc/fstab文件
mount -t devpts devpts /dev/pts:用于telnet远程登录使用
echo /sbin/mdev > /proc/sys/kernel/hotplug:向hotplug写入字符串/sbin/mdev,跟驱动相关
mdev -s:跟驱动相关
7:向根文件系统的etc添加系统启动必要配置文件fstab
fstab文件跟驱动相关
vim /opt/rootfs/etc/fstab 添加三句必要配置命令:
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
保存退出
说明:
1. 这三句话跟驱动相关
2. proc,sysfs,tmpfs仅仅是三种文件系统格式,类似FAT32
3. /proc,/sys,/dev目录下创建的内容由内核驱动自己创建,创建于内存中,掉电丢失!
至此etc目录搞定,至此八大必要目录搞定!
8:精简根文件系统缩减体积
注意:目前根文件系统rootfs中占用磁盘空间最大的是各种动态库
所以对动态库进行精简体积:
arm-cortex_a9-linux-gnueabi-strip /opt/rootfs/lib/*
//查看rootfs的体积
du /opt/rootfs -lh //1.21.1版本制作出来的为2.7MB
结论:
产生研发阶段,不要对软件进行strip
产生发布阶段,软件必须strip,目的是节省占用的磁盘空间
9:烧录到开发板
7.3 添加程序实现开机自启动
自己编写和制作一个动态库,然后编写一个UC程序,调用自己的动态库在下位机运行起来,实现开机自启动
上位机实现步骤
1:编写test.h
mkdir /rootfs/home/apptest/ //创建源码存放目录
cd /rootfs/home/apptest
vim test.h 添加如下内容
#ifndef __TEST_H
#define __TEST_H
extern void my_test(void); //声明
#endif
2:编写test.c
#include <stdio.h>
#include <pthread.h>
void *thread_func(void *args){
while(1) {
printf("%s\n", __func__);
sleep(1);
}
}
void my_test(void){ //定义
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
sleep(100000);
printf("%s\n", __func__);
}
3:编写main.c
#include <stdio.h>
#include "test.h"
int main(void){
my_test(); //调用
return 0;
}
4:制作动态库和编译调用程序
arm-cortex_a9-linux-gnueabi-gcc -shared -fpic -lpthread -o libtest.so test.c
arm-cortex_a9-linux-gnueabi-gcc -o main main.c -L. -ltest
5: 添加自定义的动态库到根文件系统
下位机执行时提示libtest.so动态库找不到
需要链接自定义的动态库时,需要新建一个脚本来实现
vim /home/rootfs/etc/profile
export LD_LIBRARY_PATH=/home/apptest:$LD_LIBRARY_PATH
//添加可执行权限
chmod 777 /opt/rootfs/etc/profile
6:链接非自定义的动态库
参照上文的拷贝动态库笔记
7:自启动
如果没有添加自定义的动态库,则只在rcS文件末尾添加 /home/apptest/main &
如果有自定义的动态库。则只在profile文件末尾添加 /home/apptest/main &
则profile文件内容为
export LD_LIBRARY_PATH=/home/apptest:$LD_LIBRARY_PATH
/home/apptest/main &
8:总结
- rcS脚本设置的环境变量是局部的,rcS退出,环境变量消亡
- profile脚本设置的环境变量是全局的,profile脚本的退出,环境变量不会消亡
切记: - 自己制作的动态库和移植其它开源软件得到的,动态库一律不允许放到根文件系统的lib目录下, 需要单独放到某个自己创建的目录下,例如:/home/apptest
- 如果单独存放,还需要将库所在的路径添加到环境变量 LD_LIBRARY_PATH中
例如:export LD_LIBRARY_PATH=自己的动态库路径:$LD_LIBRARY_PATH - 根文件系统的lib目录只保存从交叉编译器中拷贝的标准系统相关的动态库
问:rcS中明明已经添加了LD_LIBRARY_PATH动态库的路径,为何软件在运行时又找不到了呢?
答:原因是rcS虽然在启动的时候被执行,并且里面先设置的LD_LIBRARY_PATH环境变量,但是随着rcS脚本的执行完毕,并且退出,此脚本设置的环境变量会消亡,所以当手动再次运行main时,会提示libtest.so找不到
问:如何解决呢?
答:只需将环境变量的设置或者应用程序的自启动全部,放在另一个脚本完成即可,此脚本位于根文件系统的etc目录下,名称叫profile,此脚本的执行晚于rcS,但是这个脚本设置的环境变量不会随着脚本的执行结束而消亡
7.4 制作img镜像
1:上位机实施步骤:
如果是ubuntu18.04系统需要修改mke2fs.conf配置文件
sudo vim /etc/mke2fs.conf
将里面的ext4属性修改为:
ext4 = {
features = has_journal,extent,huge_file,flex_bg,dir_nlink,extra_isize
inode_size = 256
}
2:修改内核EMMC驱动的bug,否则会出现mmc -220类似的错误,修改方法如下:
上位机执行:
cd /home/arm/kernel
vim drivers/mmc/core/mmc.c +295
然后将if语句整体注释掉,然后保存退出
make uImage -j4
烧写进开发板
3:将/home/rootfs目录制作成单个镜像文件rootfs_ext4.img
cd /home/
sudo dd if=/dev/zero of=rootfs_ext4.img bs=1k count=8192
说明:
-dd:用于创建一个单个镜像文件,类似:touch命令
-if(input file)=/dev/zero:将来创建的单个镜像文件里面的内容全部来自设备/dev/zero,/dev/zero设备能够源源不断产生0数据
-of(output file)=rootfs_ext4.img:指定将来创建单个镜像文件名rootfs_ext4.img,并且此文件里面填充全0(来自于/dev/zero)
-bs(block size)=1k:生成的rootfs_ext4.img以块为单位,一块1024字节
-count=8192:总共8192块
结论:生成的rootfs_ext4.img数据块为8MB,文件中的数据全是0
sudo mkfs.ext4 rootfs_ext4.img #格式化镜像文件rootfs_ext4.img,文件格式为ext4
sudo mkdir /mnt/initrd #创建一个目录,作为挂节目录
sudo mount -t ext4 -o loop rootfs_ext4.img /mnt/initrd
说明:挂接rootfs_ext4.img到目录/mnt/initrd,并且指定的文件系统类型ext4
挂接命令的结果就是将来只需要访问/mnt/initrd,本质就是在访问rootfs_ext4.img里面的内容
sudo cp /home/rootfs/* /mnt/initrd -frd #向目录initrd中拷贝rootfs的内容,本质上就是向rootfs_ext4.img拷贝rootfs的内容,
结果是:rootfs_ext4.img里面的内容就是/opt/rootfs里面的内容
sudo umount /mnt/initrd #卸载/mnt/initrd,将来initrd不再作为rootfs_ext4.img的入口
至此将rootfs_ext4.img烧写到开发板
执行以下命令查看内核的启动参数:
cat /proc/cmdline
root=/dev/mmcblk0p2 console=ttySAC0,115200 rootfstype=ext4 maxcpus=1 //说明内核确实到第三分区挂接了自己的根文件系统
7.5 cramfs压缩只读文件
嵌入式开发常用norflash或者nandflash, 那么文件系统就不会用ext4文件系统了,如何管理根文件系统rootfs包含的内容呢?
答:可以采用cramfs压缩只读文件系统格式
上位机实施步骤:
1.配置linux内核支持cramfs文件系统格式:
cd /home/arm/kernel
make menuconfig
File Systems->
[*] Miscellaneous filesystems --->
按Y键选中
<*> Compressed ROM file system support (cramfs)
make uImage -j4
烧写进开发板
2.将根文件系统rootfs制作成cramfs格式的镜像文件
cd /home
sudo mkfs.cramfs rootfs rootfs.cramfs //将/opt/rootfs目录做成rootfs.cramfs就是单个镜像文件
cp rootfs.cramfs /tftpboot
将rootfs.cramfs烧写入开发板
此时在下位机系统内,是无法编辑系统文件的,也不能对目录进行操作
结论:
- rootfs.cramfs本质就是一个文件,只是目前这个文件里包含的内容是/opt/rootfs根文件系统的内容,并且rootfs.cramfs里面的目录和访问的文件系统格式为cramfs
- cramfs文件系统格式应用于nor/nand/emmc,它管理的目录和文件是可读不可写的,并且是压缩的,它特别适合根文件系统rootfs不被用户非法破坏的场合,这种情况下咱们会将根文件系统制作成cramfs,就是为了保护。
7.6 ramdisk文件
问:不管是emmc还是norflash,nandflash,系统运行程序都会动态的从闪存拷贝到内存运行,无形之中速度肯定很慢,如何加快根文件系统中应用程序或者命令的执行速度呢?
答:采用ramdisk文件系统(本质就是ext2文件系统格式,ext4文件系统兼容ext2所以目前内核由于支持了ext4,所以也就支持ext2文件系统格式了)关键ramdisk文件系统整体运行在内存,速度快!缺点是占用内存并且掉电数据丢失!
上位机实施步骤:
1:制作ramdisk文件
cd /home/
dd if=/dev/zero of=initrd.img bs=1k count=8192
sudo mkfs.ext2 -F initrd.img
sudo mount -t ext2 -o loop initrd.img /mnt/initrd
sudo cp /home/rootfs/* /mnt/initrd/ -a
sudo umount /mnt/initrd
gzip --best -c initrd.img > ramdisk.img #压缩的,可读可写的ramdisk制作完毕
2.配置linux内核支持ramdisk(ram+disk=在内存中开辟一块区域当闪存)
cd /home/kernel
make menuconfig
Device Drivers->
[*] Block devices --->
//按Y选择为*
<*> RAM block device support
//按回车进入修改为8192,表示将来ramdisk要分配8MB内存
(8192) Default RAM disk size (kbytes) (NEW)
保存退出
make uImage -j4
3:下位机测试
3.1:先进行分区规划,这里由于采用显示LOGO的分区
EMMC存储空间的划分:
0x00----0x200----4MB----------10M-----------20M-------30M-----8GB
保留 uboot uImage rootfs 临时 大片
第1分区 第2分区 第3分区 第4分区 第5分区
mmcblk0boot0 mmcblk0p1 mmcblk0p2 mmcblk0p3 mmcblk0p4//内核给分区指定的官方名称
fdisk 2 3 0x400000:0x600000 0xa00000:0xa00000 0x1400000:0xa00000
分区规划:fdisk 2 3 0x400000:0x600000 0xa00000:0xa00000 0x1400000:0xa00000
3.2:烧写uImage和烧写ramdisk.img,使用网络烧写
mmc write 0x48000000 0x2000 0x3000 // 烧写uImage
mmc write 0x48000000 0x5000 0x4000 //将ramdisk.img烧写到第三分区
3.3:设置系统启动参数
setenv bootcmd mmc read 0x48000000 0x2000 0x3000 \;
mmc read 0x49000000 0x5000 0x4000 \;
bootm 0x48000000
setenv bootargs root=/dev/ram console=ttySAC0,115200
maxcpus=1 initrd=0x49000000,8M init=/linuxrc
saveenv
4:重新启动系统,测试自己制作的ramdisk.img
执行以下命令查看内核的启动参数:
cat /proc/cmdline
root=/dev/ram console=ttySAC0,115200
maxcpus=1 initrd=0x49000000,8M init=/linuxrc
注意:在系统内部直接创建文件,重启之后,文件会丢失,如果需要保存系统的重要数据,需要将这些内容保存到闪存中,通过mount来实现
1:先创建一个分区
mkfs.ext2 /dev/mmcblk0p3 //将第四分区格式化为ext2文件系统格式,将来访问/mnt目录本质就是访问EMMC的第四分区
cd /mnt //实质进入第四分区
mkdir helloworld //本质在EMMC第四分区创建一个新目录,数据不会丢失
2:挂载
mount -t ext2 /dev/mmcblk0p3 /mnt
注意:每次重启之后都需要重新挂载分区,所以需要将配置信息保存到自启文件中
7.7 telnet
1:拷贝nss,nsl相关动态库到lib目录,上位机执行:
cp /home/toolchains/arm-cortex_a9-linux-gnueabi/sysroot/lib/libnss_* /home/rootfs/lib/ -d
cp /home/toolchains/arm-cortex_a9-linux-gnueabi/sysroot/lib/libnsl* /home/rootfs/lib/ -d
2.下位机linux系统中执行:
vi /etc/passwd 添加如下内容:
root❌0:0:root:/root:/bin/sh
vi /etc/group 添加如下内容:
root❌0:
保存退出
adduser root
passwd root
此时输入新的密码例如:123456,然后确认新的密码:123456
telnetd 启动telnetd服务器程序
3.上位机远程登录,执行:
telnet 192.168.1.110
输入用户名:root
输入密码:123456
书籍
<<Linux内核设计与实现>> 第三版
<<Linux设备驱动程序>> 第三版