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)到IRAM
(0xd002_0000
处,其中0xd002_0010
之前的16个字节储存的BL1
的校验信息和BL1
尺寸)中,并对BL1
进行校验,校验OK
转入BL1
进行执行;
首先解释一下我认为的BL0
、BL1
、BL2
:
(1)BL0
:是指S5PV210
的IROM
中固化的启动代码;
(2)BL1
:是指在IRAM
自动从外扩存储器(nand/sd/usb
)中拷贝的uboot.bin
二进制文件的头最大16K
代码:
(3)BL2
:是指在代码重定向后在内存中执行的的UB00T
的完整代码;
(4)三者之间关系是:(Interal ROM
固话代码)BL0
将BL1
(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
- (要让SP指向可读可写的设备区间(DRAM),因为C语言的函数需要压栈,如果是递减栈,SP要放到高地址)
-
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 $^ [email protected]
$(SDTOOLS) [email protected] $@
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 $^ [email protected]
$(SDTOOLS) [email protected] $@
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.c
、led.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 $^ [email protected]
$(SDTOOLS) [email protected] $@
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硬件接口开发