05_bootloader开发

发布时间 2023-12-14 15:15:57作者: StarAire

05_bootloader开发

需要准备:usb转串口线、SD卡、MINI USB

程序没有运行的时候是放在Nand flash(相当于硬盘)中的,这个地址为程序地址
运行起来的时候是放在DRAM(相当于内存)里的,这个地址为程序链接地址

1. ARM 启动顺序

1.1. 第一个点亮LED的程序 (GPIO)

参考No OS(裸机程序)\src下的程序

我们打开第一个例程1.leds_s

有以下几个文件

Makefile  mkv210_image.c  start.S  write2sd
  • start.S 汇编源代码
  • mkv210_image.c 校验程序
  • Makefile 编译
  • write2sd shell脚本,写入sd卡,调用的dd指令

gcc配置

arm-linux-gcc-4.5.1-v6-vfp-20120301.tgz 拷贝到工作目录页

然后解压缩tar -xzf arm-linux-gcc-4.5.1-v6-vfp-20120301.tgz

之后拷贝到/user/local/目录下

sudo vim /etc/profile

添加路径

source /etc/profile 然后重启


然后make进行编译,生成210.bin文件,然后就是烧写

这里提供三个烧写方法:

1、将SD卡透传到虚拟机中,使用dd烧录

将SD卡拔下来,插到电脑,然后透传到虚拟机里,虚拟机 ls /dev/sd* 确认新增设备

执行./write2sd(这里根据实际情况修改)

sudo dd iflag=dsync oflag=dsync if=210.bin of=/dev/sdb seek=1

of=/dev/sdb 修改为自己的设备路径

2、生成一个虚拟的空镜像,将bin文件烧录到空镜像中,然后再用windows的disk烧录工具手动烧录到SD卡里
(这个是源于我的读卡器暂时没法透传到虚拟机里,所以才用的这个方法)

先生成一个空的镜像:count表示大小容量,这里为4M

sudo dd if=/dev/zero of=test.img bs=1M count=4

然后将bin文件烧到这个镜像里

sudo dd iflag=dsync oflag=dsync if=210.bin of=test.img seek=1

和上面一样,只是目标改为镜像文件

然后拷贝到win主机,手动通过Win32DiskImager烧录

3、使用友善之臂工具MiniTools烧录(调试方便)不需要插拔SD卡

跟之前烧录android镜像一样,把images文件夹里的Superboot210.bin烧录到SD卡中

然后将images文件夹拖入 SD的目录下

修改为USB调试模式,添加USB-Mode = yes

#This line cannot be removed. by FriendlyARM(www.arm9.net)

CheckOneButton=No
Action = Install
OS = Android

LCD-Mode = No
LCD-Type = S70 

LowFormat = No
VerifyNandWrite = No
CheckCRC32=No

StatusType = Beeper | LED
# 添加
USB-Mode = yes

################### Android 4.0.3 ####################
Android-BootLoader = Superboot210.bin
Android-Kernel = Android/zImage
Android-CommandLine = root=/dev/mtdblock4 rootfstype=yaffs2 console=ttySAC0,115200 init=/linuxrc androidboot.console=ttySAC0 skipcali=yes ctp=3
Android-RootFs-InstallImage = Android/rootfs_android-mlc2.img

################### Android 2.3.1 ####################
#Android-BootLoader = Superboot210.bin
#Android-Kernel = Android2.3.1/zImage
#Android-CommandLine = root=/dev/mtdblock4 rootfstype=yaffs2 console=ttySAC0,115200 init=/linuxrc androidboot.console=s3c2410_serial0 skipcali=yes ctp=3
#Android-RootFs-InstallImage = Android2.3.1/rootfs_android-mlc2.img

################### Linux ####################
Linux-BootLoader = Superboot210.bin
Linux-Kernel = Linux/zImage
Linux-CommandLine = root=/dev/mtdblock4 rootfstype=yaffs2 console=ttySAC0,115200 init=/linuxrc
Linux-RootFs-InstallImage = Linux/rootfs_qtopia_qt4-mlc2.img

################### Windows CE6.0 ####################
WindowsCE6-Bootloader = Superboot210.bin
WindowsCE6-BootLogo = WindowsCE6\bootlogo.bmp
WindowsCE6-InstallImage = WindowsCE6\NK.bin
WindowsCE6-RunImage = WindowsCE6\NK.bin


卡插入板子,SD卡启动

打开minitools,屏幕显示 板子的信息,板子上显示USB Mode:connected表示已连接,则连接成功

软件切换到User bin

选择 Download and run,我们程序下载到RAM中,所以偏移地址填0x20000000
(为什么是0x20000000,我们可以查看手册S5PV210_UM_REV1.1.pdf第一章section01 overview的第二节memory map 内存映射图,可以看到,DRAM0的起始地址为0x2000 0000)

选择Install to NandFlash,我们程序下载到ROM里,然后运行的时候回搬到RAM中运行

这里当然采用RAM,因为flash里还有系统,不能刷掉

下面选择需要烧录的bin文件,然后点击download and run

下载之后发现之前没法运行,这里是因为,之前编译的文件中

led.bin: start.o
        arm-linux-ld -Ttext 0x0 -o led.elf $^
        arm-linux-objcopy -O binary led.elf led.bin
        arm-linux-objdump -D led.elf > led_elf.dis
        gcc mkv210_image.c -o mkmini210
        ./mkmini210 led.bin 210.bin

%.o : %.S
        arm-linux-gcc -o $@ $< -c

%.o : %.c
        arm-linux-gcc -o $@ $< -c

clean:
        rm *.o *.elf *.bin *.dis mkmini210 -f

arm-linux-ld -Ttext 0x0 -o led.elf $^ 链接地址写在0x00 需要改成0x20000000 (暂时不太清楚原来0x0啥意思)

led.bin: start.o
        arm-linux-ld -Ttext 0x20000000 -o led.elf $^
        arm-linux-objcopy -O binary led.elf led.bin
        arm-linux-objdump -D led.elf > led_elf.dis
        gcc mkv210_image.c -o mkmini210
        ./mkmini210 led.bin 210.bin

%.o : %.S
        arm-linux-gcc -o $@ $< -c

%.o : %.c
        arm-linux-gcc -o $@ $< -c

clean:
        rm *.o *.elf *.bin *.dis mkmini210 -f

一些疑问

1、这里的0x20000000和0x0分别是什么意思
2、iROM,iRAM(SRAM),DRAM,NAND的关系
2、启动运行程序是个什么样的过程

1.2. S5PV210启动原理

需要手册
1、ARM手册 ARM Cortex-A(armV7)编程手册V4.0.pdf
2、芯片手册 S5PV210_UM_REV1.1.pdf

内部iRAM 96kb
内部iROM 64kb

启动大致过程
1、首先,执行内部iROM里面固化的一段代码(上电自动执行) ,会把外部ROM(例如 NAND flash)里的bootloader(包含BL1和BL2,下面解释)搬运到iRAM里,在iRAM里执行
2、bootloader执行完了之后,将外部ROM(例如 NAND flash)里的OS程序搬到DRAM里去执行

  • iROM code:是一段精简的程序,相对独立,存储在内部存储中
  • First boot loader(BL1):精简,存在外存里,和安全启动相关
  • Second boot loader(BL2):包含复杂的代码,平台相关,存在外村中
  • iROM的作用

    • 初始化系统时钟,设置看门狗,初始化栈和堆
    • 加载BL1
  • BL1的作用

    • 初始化RAM,关闭Cache,设置栈
    • 加载BL2
  • BL2的作业

    • 初始化其他外设
    • 加载OS内核

按照三星《S5PV210 UM REV1.1》手册上说明的启动流程为:S5PV210上电将从IROM(interal ROM)处执行固化的启动代码,它对时钟等初始化、对启动设备进行判断,并从启动设备中复制BL1(最大16KB)到IRAM0xd002_0000处,其中0xd002_0010之前的16个字节储存的BL1的校验信息和BL1尺寸)中,并对BL1进行校验,校验OK转入BL1进行执行;
首先解释一下我认为的BL0BL1BL2:
(1)BL0:是指S5PV210IROM中固化的启动代码;
(2)BL1:是指在IRAM自动从外扩存储器(nand/sd/usb)中拷贝的uboot.bin二进制文件的头最大16K代码:
(3)BL2:是指在代码重定向后在内存中执行的的UB00T的完整代码;
(4)三者之间关系是:(Interal ROM固话代码)BL0BL1(bootloader的前16kB)加载到IRAM;BL1然后在IRAM中运行将BL2(其实整个bootloader)加载SDRAM(DDR)

1.2.1. reset状态

Basic Intialzation in IROM PLL Setting in IROM First Boot/Second Boot Loader Loading DRAM Setting in Second Boot Loader OS Loading Restore Previous State
Hardware Reset
WatchDog Reset
Wake up from SLEEP
SW Reset
Wake up from DEEP_STOP
Wake up from DEEP_IDLE

1.2.2. 启动的详细过程

图可查看6.2.2 BOOTING SEQUENCE EXAMPLE

  • iROM中执行

    • Disable看门狗
    • 初始化Cache控制器
    • 初始化栈和堆区域
    • 检查安全key
    • 设置clock
    • 检查OM pin码,加载BL1到iRAM
    • 如果是安全模式,进行完整校验
    • 如果校验通过,跳到BL1的iRAM地址(0xD0020010)
  • SRAM启动过程如下

    • 从启动设备加载BL1到IRAM
    • 如果安全启动,执行完整性校验
    • 如果校验成功,跳到IRAM中的BL2
    • 如果校验失败,停止BL1
    • 加载OS kernel到DRAM
    • 跳转到DRAM(0x20000000 or 0x40000000)
  • DRAM启动顺序

    • 如果是从SLEEP,DEEP_STOP,DEEP_IDLE唤醒,则恢复上一个状态
    • 跳转到OS kernel

1.2.3. 阐述一下iROM、iRAM、DRAM、NAND的关系

让我们来解释一下上面0x0和0x20000000 的问题

如果采用直接烧录到SD卡的形式,程序上电后执行IROM的程序,然后搬运ROM起始的位置(存放着bin文件)到iRAM里运行,而IRAM位于0初始的位置

如果采用烧录bootloader的形式,程序上电后执行IROM的程序,然后搬运bootloader到IRAM执行,IRAM里运行着和电脑USB通信的程序,我们下载bin程序到0x20000000(DRAM)的位置,然后bootloader跳转到DRAM那里执行程序(猜测)

还是不太清楚Makefile里链接地址的作用

2. 接口编程的准备工作

跳到 06_ARM硬件接口开发


欢迎回来,这里换了rockey老师讲解

2.1. 了解开发板资源

1、找CPU,用什么样的架构的CPU,为了找到系统上电后,第一条执行的代码,我们该放到哪里

ARM:异常向量表(reset) 0x0

2、0x接的是什么芯片,flash(nor-flash),ROM

S5PV210:SOC
SOC = CPU + Controler

3、这些地址都被芯片公司重新定义,去芯片公司的datasheet中去寻找memory map这样的章节
片内资源:SFR
片外资源:
找异常向量表中reset向量地址对应的是什么
S5PV210 :0x0 --- irom --- 固化的code --- jump to new addr

4、boot程序
设置始终clock fre

5、接口开发(boot + interface)

片外资源的地址确定

DRAM空间
程序访问的地址,落在哪个分块区间上,CPU就自动的对该分块的片选信号置为有效
SROM空间

2.2. bootloader的作用和步骤

boot + loader

  • boot的目的:跳到C语言中

    • (要让SP指向可读可写的设备区间(DRAM),因为C语言的函数需要压栈,如果是递减栈,SP要放到高地址)
      • 每个模式的SP都是独立的
      • 用哪些模式,就要初始化哪些模式下的sp
    • 关闭看门狗,关闭中断,MMU,CACHE
    • 配置系统工作时钟
    • 配置SDRAM的控制器(行地址数,列地址数,多少块,周期性的充电)
    • 代码搬移
      • 执行速度问题,把程序从存储器(nor flash)搬移到快速的内存(RAM)
      • 只把存储器的一部分代码执行处理,把存储在其它位置上的代码搬移到内存
      • (nor flash有地址总线和数据总线,和ram类似,但是nandflash只有数据总线,没有地址总线,只能通过nand flash控制器去访问)
    • bl main
  • loader的目的:执行应用逻辑,点灯、uart、load linux kernel

2.3. 创建接口开发的工程

工程搭建Makefile

1、通用的Makefile,支持 SD卡启动在uboot下直接运行在ram

  • 二者区别在
    • 程序运行时的地址不同
      • DRAM:0x20000000
      • SD卡:0x0
    • SD:需要添加一个头信息,前16KB,需要有校验(三星手册规定)
    • RAM:因为已经经过boot引导了,所以不需要校验程序

2、TARGET: DEP
COMMAND

# define var
TARGET := led.bin 
BUILD  := led

# 选择SD还是RAM
ENV := SD
SDTOOLS := ./mk210

COBJS += start.o
COBJS += main.o

CROSS_COMPILE := arm-linux-

CC        := $(CROSS_COMPILE)gcc
LD        := $(CROSS_COMPILE)ld
OBJCOPY   := $(CROSS_COMPILE)objcopy

CFLAGS += -Wall
CFLAGS += -I./inc


ifeq ($(ENV),RAM)
LDFLAGS += -Ttext=0x20000000
else
LDFLAGS += -Ttext=0x0
endif


# way
all: $(TARGET)

ifeq ($(ENV),RAM)
$(TARGET): $(BUILD)
    $(OBJCOPY) -O binary $^ $@

else
$(TARGET): $(BUILD)
    $(OBJCOPY) -O binary $^ $@.tmp
    $(SDTOOLS) $@.tmp $@
endif


$(BUILD):$(COBJS)
    $(LD) $(LDFLAGS) -O $@ $^
    
%.o:%.c
    $(CC) $(CFLAGS) -c -o $@ $^
%.o:%.S
    $(CC) $(CFLAGS) -c -o $@ $^

clean:
    rm -rf $(TARGET) $(BUILD) *.o *.tmp

工程搭建链接脚本

1、链接脚本是什么
告诉链接器如何工作的一个文本文件

1.o 2.o 3.o -> build

如何安排这几个.o文件的排序

2、要素

  • 哪一个.o文件放到代码段的起始位置
  • 所有的.o文件放到哪个基地址之上
    • ld -Ttext=xxx
  • 代码段、数据段等等是不连续的

3、基本语法

  • SECTIONS
  • .text 代码段
  • .rodata 只读数据段(常量段) char *p = "hello"; 这里的"hello"就是存在rodata段
  • .data 数据段(初始化)
  • .bss 未初始化的数据段
  • . = ALIGN(4); 4字节对齐

新建一个文件 map.lds


SECTIONS
{
    . = 0x0;
    
    . = ALIGN(4);
    .text :
    {
        start.o
        *(.text)
    }
    
    . = ALIGN(4);
    .rodata :
    {
        *(.rodata)
    }
    
    . = ALIGN(4);
    .data :
    {
        *(.data)
    }    

    . = ALIGN(4);
    .bss :
    {
        *(.bss)
    }    
}

修改一个makefile


# define var
TARGET := led.bin 
BUILD  := led

# 选择SD还是RAM
ENV := SD
SDTOOLS := ./mk210

COBJS += start.o
COBJS += main.o

CROSS_COMPILE := arm-linux-

CC        := $(CROSS_COMPILE)gcc
LD        := $(CROSS_COMPILE)ld
OBJCOPY   := $(CROSS_COMPILE)objcopy

CFLAGS += -Wall
CFLAGS += -I./inc

# 新增链接脚本
LDFLAGS += -Tmap.lds
ifeq ($(ENV),RAM)
LDFLAGS += -Ttext=0x20000000
else
LDFLAGS += -Ttext=0x0
endif


# way
all: $(TARGET)

ifeq ($(ENV),RAM)
$(TARGET): $(BUILD)
    $(OBJCOPY) -O binary $^ $@

else
$(TARGET): $(BUILD)
    $(OBJCOPY) -O binary $^ $@.tmp
    $(SDTOOLS) $@.tmp $@
endif


$(BUILD):$(COBJS)
    $(LD) $(LDFLAGS) -O $@ $^
    
%.o:%.c
    $(CC) $(CFLAGS) -c -o $@ $^
%.o:%.S
    $(CC) $(CFLAGS) -c -o $@ $^

clean:
    rm -rf $(TARGET) $(BUILD) *.o *.tmp

这里链接地址计算规则是,链接脚本里的地址为基地址,makefile中-Ttext 指定的地址加上基地址为实际链接地址

工程搭建C代码点灯

前面我们都是直接操作的寄存器地址,这样的写法是不利于大型工程的构建的,所以我们要在寄存器的基础上封装一层数据结构,然后我们通过数据结构可以更方便的操作我们的寄存器

首先打开芯片手册 S5PV210_UM_REV1.1.pdf,找到2.2.1 REGISTER MAP

我们可以看到每个引脚组的寄存器都差不多,都是六个寄存器,所以我们可以把这些寄存器封装成一个结构体

struct s5pv210_gpio_bank {
    unsigned int con;
    unsigned int dat;
    unsigned int pud;
    unsigned int drv;
    unsigned int con_pdn;
    unsigned int pub_pdn;
    unsigned int reserve[2]; // 保留
};

然后我们可以把引脚组封装在一个结构体里

struct s5pv210_gpio {
    struct s5v210_gpio_bank gpio_a0;
    struct s5v210_gpio_bank gpio_a1;
    struct s5v210_gpio_bank gpio_b;
    struct s5v210_gpio_bank gpio_c0;
    struct s5v210_gpio_bank gpio_c1;
    struct s5v210_gpio_bank gpio_d0;
    struct s5v210_gpio_bank gpio_d1;
    struct s5v210_gpio_bank gpio_e0;
    struct s5v210_gpio_bank gpio_e1;
    struct s5v210_gpio_bank gpio_f0;
    struct s5v210_gpio_bank gpio_f1;
    struct s5v210_gpio_bank gpio_f2;
    struct s5v210_gpio_bank gpio_f3;
    struct s5v210_gpio_bank gpio_g0;
    struct s5v210_gpio_bank gpio_g1;
    struct s5v210_gpio_bank gpio_g2;
    struct s5v210_gpio_bank gpio_g3;
    struct s5v210_gpio_bank gpio_i;
    struct s5v210_gpio_bank gpio_j0;
    struct s5v210_gpio_bank gpio_j1;
    struct s5v210_gpio_bank gpio_j2;
    struct s5v210_gpio_bank gpio_j3;
    struct s5v210_gpio_bank gpio_j4;

};

然后我们定义一下GPIO的首地址

#define S5PV210_GPIO_BASE (0xE0200000)

#define gpio_base ((struct s5pv210_gpio *)S5PV210_GPIO_BASE)


我们将上面的代码放到s5pv210.h

#ifndef S5PV210_H
#define S5PV210_H

// gpio寄存器组的定义
struct s5pv210_gpio_bank {
    unsigned int con;
    unsigned int dat;
    unsigned int pud;
    unsigned int drv;
    unsigned int con_pdn;
    unsigned int pub_pdn;
    unsigned int reserve[2]; // 保留
};

// gpio组的定义 
struct s5pv210_gpio {
    struct s5pv210_gpio_bank gpio_a0;
    struct s5pv210_gpio_bank gpio_a1;
    struct s5pv210_gpio_bank gpio_b;
    struct s5pv210_gpio_bank gpio_c0;
    struct s5pv210_gpio_bank gpio_c1;
    struct s5pv210_gpio_bank gpio_d0;
    struct s5pv210_gpio_bank gpio_d1;
    struct s5pv210_gpio_bank gpio_e0;
    struct s5pv210_gpio_bank gpio_e1;
    struct s5pv210_gpio_bank gpio_f0;
    struct s5pv210_gpio_bank gpio_f1;
    struct s5pv210_gpio_bank gpio_f2;
    struct s5pv210_gpio_bank gpio_f3;
    struct s5pv210_gpio_bank gpio_g0;
    struct s5pv210_gpio_bank gpio_g1;
    struct s5pv210_gpio_bank gpio_g2;
    struct s5pv210_gpio_bank gpio_g3;
    struct s5pv210_gpio_bank gpio_i;
    struct s5pv210_gpio_bank gpio_j0;
    struct s5pv210_gpio_bank gpio_j1;
    struct s5pv210_gpio_bank gpio_j2;
    struct s5pv210_gpio_bank gpio_j3;
    struct s5pv210_gpio_bank gpio_j4;

};


// gpio组基地址
#define S5PV210_GPIO_BASE (0xE0200000)

#define gpio_base ((struct s5pv210_gpio *)S5PV210_GPIO_BASE)


#endif

然后我们新建led.cled.h

写我们的LED初始化和开关灯程序

led.h


#ifndef LED_H
#define LED_H

void led_init();

void led_blink(int status);



#endif


led.c


#include "s5pv210.h"

// led引脚为A0_0,1,2,3

void led_init()
{
    gpio_base->gpio_j2.con |= 0x00001111; // 配置输出
    gpio_base->gpio_j2.dat |= 0x0f; // 配置输出
}

void led_blink(int status)
{
    if(status)
    {
        gpio_base->gpio_j2.dat &= ~(0x0f);
    }
    else
    {
        gpio_base->gpio_j2.dat |= 0x0f;
    }
}


下面编写main.c




#include "led.h"

void delay(unsigned long count)
{
    volatile unsigned long i = count;
    while(i--);
}

int main()
{
    led_init();
    while(1)
    {
        led_blink(1);
        delay(0x1000000);
        led_blink(0);
        delay(0x1000000);   
    }    
    return 0;
}


再加一个启动文件start.S

.text
.global _start

_start:
    BL main

loop:
    B loop

修改一下上面的Makefile

# define var
TARGET := led.bin 
BUILD  := led

# 选择SD还是RAM
ENV := RAM
SDTOOLS := ./mk210

COBJS += start.o
COBJS += main.o
COBJS += led.o

CROSS_COMPILE := arm-linux-

CC        := $(CROSS_COMPILE)gcc
LD        := $(CROSS_COMPILE)ld
OBJCOPY   := $(CROSS_COMPILE)objcopy

CFLAGS += -Wall
CFLAGS += -I./

# 新增链接脚本
LDFLAGS += -Tmap.lds
ifeq ($(ENV),RAM)
LDFLAGS += -Ttext=0x20000000
else
LDFLAGS += -Ttext=0x0
endif


# way
all: $(TARGET)

ifeq ($(ENV),RAM)
$(TARGET): $(BUILD)
	$(OBJCOPY) -O binary $^ $@
	arm-linux-objdump -D $^ > led_elf.dis
else
$(TARGET): $(BUILD)
	$(OBJCOPY) -O binary $^ $@.tmp
	$(SDTOOLS) $@.tmp $@
endif


$(BUILD):$(COBJS)
	$(LD) $(LDFLAGS) -o $@ $^
    
%.o:%.c
	$(CC) $(CFLAGS) -c -o $@ $^
%.o:%.S
	$(CC) $(CFLAGS) -c -o $@ $^

clean:
	rm -rf $(TARGET) $(BUILD) *.o *.tmp

加上上面的map.lds

SECTIONS
{
    . = 0x0;

    . = ALIGN(4);
    .text :
    {
        start.o
        *(.text)
    }

    . = ALIGN(4);
    .rodata :
    {
        *(.rodata)
    }

    . = ALIGN(4);
    .data :
    {
        *(.data)
    }

    . = ALIGN(4);
    .bss :
    {
        *(.bss)
    }
}


最终为七个文件

led.c  led.h  main.c  Makefile  map.lds  s5pv210.h  start.S

输入make编译,最后把生成的bin文件通过minitools烧到板子里
现象:LED闪烁

s5pv210.h再加入一些代码

#define __REG(x)         (*(volatile unsigned int *)(x))

#define readb(a)         (*(volatile unsigned char *)(a))
#define readw(a)         (*(volatile unsigned short *)(a))
#define readl(a)         (*(volatile unsigned int *)(a))

#define writeb(v,a)     (*(volatile unsigned char *)(a) = (v))
#define writew(v,a)     (*(volatile unsigned short *)(a) = (v))
#define writel(v,a)     (*(volatile unsigned int *)(a) = (v))

至此,我们的软件框架就搭建好了,接下来开始剩下的接口学习


再回到06_ARM硬件接口开发