简单的S3C44B0X Bootloader

本文介绍了作者为了学习,简化任务并制作了一个仅用于让跑马灯程序在裸机上运行的Bootloader。通过几个晚上的努力,作者成功实现了这个目标。Bootloader的存储布局见文中说明。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    上次基于u-boot写了一个跑马灯程序,见 《第一个跑马灯程序》 。但是这里有个问题,离了u-boot它就不能用了,因为缺少很多系统初始化工作,也没有人能把它加载到RAM中去运行。因此,为了学习(好强大的理由。。。),我们需要自己实现一个bootloader,来替代u-boot做这些事情。

    显然让我在这么短的时间里写一个和u-boot同级别的bootloader很不现实,所以让我们把问题尽可能的简化,目的是让跑马灯程序在裸机上也能跑起来就行了。

    OK,照着这个思路我奋斗了几个日夜(其实每天只有几小时的业余时间- -),经历无数的苦难(没办法,咱是无畏的菜鸟新手),终于达成了目标。

    下面来介绍一下该bootloader的思路,其存储布局见下图:
 

 

    首先将bootloader和内核程序(这里先用跑马灯程序替代)烧录进NOR Flash中物理地址0处,在系统加电后响应Reset异常,系统将从地址0处开始执行,这个位置就是bootloader的入口点。

    要注意的是在这个模型里,boot.s程序上限是256B,整个内核(head.s + main.c)的大小不能超过4K - 256B。否则就要对这些参数作调整才行。

    在bootloader执行其间除了初始化一些寄存器以外,还要把内核代码从ROM中搬到RAM去执行,这就要求我们需要在此之前先初始化好存储空间。

    bootloader执行流程如下:
       1. 异常向量初始化
       2. 初始化CPSR,包括关闭中断及设定svc模式等
       3. 关闭看门狗定时器
       4. 初始化系统时钟
       5. 初始化CPU Cache(这步可以不做,但是系统效率会明显下降许多)
       6. 初始化存储空间,主要是配置SDRAM
       7. 将内核程序复制到SDRAM中
       8. 跳转到SDRAM中执行内核程序

    内核的执行流程如下:
       1. 内核异常向量初始化
       2. 堆栈初始化
       3. 跳转到C程序中执行
       4. 执行内核任务(本例中使用万能的跑马灯程序替代- -b)

    上面的这一切看起来非常的简单,相信很多人都见过类似的描述。我最初也这么觉得,但随着逐步的深入,我也越来越郁闷。好让我们一步步的看。

    首先是boot.s,也即是该bootloader中真正属于bootloader的部分,也是本文的核心。

boot.s
.equ SYSCFG, 0x01c00000 .equ BWSCON, 0x01c80000 .equ WTCON, 0x01d30000 .equ PLLCON, 0x01d80000 .equ CLKCON, 0x01d80004 .equ KERNEL_START, 0x0c000100 @ 内核执行的起始位置(RAM) .equ KERNEL_VECTOR, 0x0c000000 @ 内核的异常向量入口(RAM) .equ KERNEL_DATA, 0x00000100 @ 内核数据在ROM中的起始位置 .equ KERNEL_CPLMT, 0x00001000 @ 复制界限(内核数据在ROM中的终止位置,4KB) zero: b reset ldr pc, = KERNEL_VECTOR @ 未定义协处理器指令 ldr pc, = KERNEL_VECTOR + 4 @ SWI(软中断) ldr pc, = KERNEL_VECTOR + 8 @ 预取指令中止 ldr pc, = KERNEL_VECTOR + 12 @ 数据中止 b . ldr pc, = KERNEL_VECTOR + 16 @ IRQ(普通中断请求) ldr pc, = KERNEL_VECTOR + 20 @ FIQ(快速中断请求) reset: @ 初始化CPSR寄存器 mov r0, #0xd3 @ 禁止IRQ和FIQ,ARM指令集状态,svc模式 msr cpsr_all, r0 @ 设置CPSR @ 禁止看门狗定时器 ldr r0, = WTCON mov r1, #0 str r1, [r0] @ 初始化系统时钟 ldr r0, = PLLCON ldr r1, = 0x48032 @ Fin=10MHz, Fpllo=40MHz, MDIV=0x48, PDIV=0x3, SDIV=0x2 str r1, [r0] ldr r0, = CLKCON ldr r1, = 0x7f88 @ 提供给所有外设 str r1, [r0] @ 初始化CPU cache ldr r0, = SYSCFG mov r1, #0xe @ 8KB cache,写缓冲使能 str r1, [r0] @ 初始化存储器 ldr sp, = smrdata ldmia sp, {r0-r11} ldr sp, = BWSCON stmia sp, {r0-r11} @ 将内核从0x00000100(ROM)复制到0x0c000000(RAM) ldr fp, = KERNEL_DATA @ 源地址 ldr ip, = KERNEL_VECTOR @ 目标地址 ldr sp, = KERNEL_CPLMT @ 复制大小=0xf00(字节) 1: ldmia fp!, {r0-r7} @ 每次复制8个字=32字节 stmia ip!, {r0-r7} cmp fp, sp bcc 1b @ 复制循环 ldr pc, = KERNEL_START @ 跳到内核开始处执行 @ 内存控制寄存器组的数据 smrdata : .word 0x01000000 @ BWSCON: DW6=16位 .word 0x00001900 @ BANKCON0: Tacs=0个时钟,Tcos=4个时钟,Tacc=2个时钟,Toch=0个时钟,Tcah=0个时钟 .word 0x00000700 @ BANKCON1: 默认 .word 0x00000700 @ BANKCON2: 默认 .word 0x00000700 @ BANKCON3: 默认 .word 0x00000700 @ BANKCON4: 默认 .word 0x00000700 @ BANKCON5: 默认 .word 0x00018000 @ BANKCON6: SDRAM,SCAN=8位,Trcd=2个时钟 .word 0x00000700 @ BANKCON7: 默认 .word 0x008c0590 @ REFRESH: 允许刷新,自动刷新,Trp=2个时钟,Trc=7个时钟,刷新频率100MHz(refresh counter=1424) .word 0x00000016 @ BANKSIZE: SLCKEN=1, BK76MAP=8M .word 0x00000020 @ MRSRB6: CL=2个时钟 
    代码流程还是比较清晰的,让我逐一解释。

    几个.equ定义就不多说了,参照代码和前面的图就应该会很清楚了。

    从zero开始是物理地址0,也是系统复位后的入口点。这里按照ARM的规定,放置异常向量表。首先复位异常就是我们bootloader的入口点,让它跳到下面的reset子程序去执行。其它的几个异常,除了保留异常,其它的都被定位到一个叫做KERNEL_VECTOR的地址附近去了。该地址就是内核异常表。在该系统里,内核可以不直接使用硬件的异常信号,而可以把它重定向到一个内核自己管理的异常向量表去。

    这样做有什么好处?首先可以让它们作为内核的一部分,这样可以随内核程序一起复制到RAM中,不需另写复制代码。其次为了方便内核管理,也方便代码书写(对于一些现有的代码也更易于移植),可以把它们重新组成一个向量表,其向量号可以自己定义。其中一个可能的方案如上,将undef映射到0号内核异常,将swi映射到1号内核异常,pabort、dabort分别映射到2号和3号,irq和fiq被映射到内核异常4号和5号。

    在后面的代码中还会看到这个内核异常向量表的定义,先让我们关注一下bootloader的主要工作,它们都在reset子程序中。

    首先是初始化CPSR寄存器,S3C44B0X手册上解释得很清楚了,这里就不多说了。

    关闭看门狗定时器,很简单,向WTCON寄存器写0就行了,详细看手册。

    系统时钟初始化是个比较头痛的问题。S3C44B0X的时钟源可以由外部晶体来提供,也可以直接使用外部时钟信号。通过配置OM[3:2]管脚来确定采用哪种方式,总的来说OM[3:2]=00使用外部晶体,OM[3:2]=01使用外部时钟。详细请参阅S3C44B0X手册。

    在我的板子上,OM3接地,OM2是通过跳线来设置的,原理图:
 


    这里我把5-6跳线接通,OM2短路接地,所以OM[3:2]=00,使用外部晶振作为我们的时钟源。

    接下来我们需要将外部晶振的频率转换为系统所需要的时钟频率。在S3C44B0X里这个操作是可编程的,需要程序员通过设置PLLCON寄存器来控制。说起这个PLLCON,自然就会引出这个著名的式子:
 


    Fin是输入频率,也就是外部晶振的频率,我们的目的是通过PLL(锁相环电路)将它转化成Fpllo作为我们的系统时钟频率。我们需要确定的是上面三个分频值m、p、s,也即是在PLLCON寄存器设置MDIV、PDIV、SDIV。

    除了那个公式,还有下面几条限制:
       1. 20MHz < Fpllo < 66MHz
       2. Fpllo * 2 ^ s < 170MHz
       3. s频分值应该尽可能的大
       4. 建议 1MHz < Fin / p < 2MHz

    比较晕是吧,网上有人提供了计算器来算这个。常用的一些典型数值如下:
 

No.

Fin(MHz)

Fout(MHz)

MDIV

PDIV

SDIV

1

10

40

0x48

0x3

0x2

2

10

50

0x2a

0x3

0x1

3

10

60

0x34

0x3

0x1

4

4

60

0x34

0x0

0x1

5

3

60

0x48

0x0

0x1

6

10

75

0x3a

0x3

0x1


    那么首先要确定Fin,也就是外部晶振频率,这就要看板子的原理图了:
 


    如图得知我的板子的晶振源是10MHz,在这里我选择第一个方案,也就是系统时钟为40MHz。于是有MDIV=0x48,PDIV=0x3,SDIV=0x2。

    初始化CPU Cache就相对比较简单了。当然你可以跳过这一步,其结果是系统效率会变得很低(跑马灯程序的话,无Cache比有Cahche慢了10倍以上)。我选择最简单的方案,把全部内部8K的SRAM都作为Cache使用,同时使能写缓冲。

    存储器初始化是最难的,下面的部分我也不能保证全部正确,请各位用批判的眼光来看待,如果发现有错误,强烈欢迎指正。

    如前面所说,由于我们要把内核程序从NOR Flash复制到SDRAM,所以需要在其之前先进行存储器的初始化。代码末尾那堆魔法数字一样的数据就是相关寄存器的设定值了。寄存器名字在注释中有给出。

    这里各位肯定要先翻S3C44B0X的手册了,上面的寄存器各位是什么意思我就不多说了,真正需要的数据我都在相应的注释中给出了,我们重点关注这些数据怎么求得。

    BWSCON:

    在本例中我们没有用到什么外部设备,于是只需要关心BANK0(NOR Flash)和BANK6(SDRAM)。由于BANK0的参数是确定的(寄存器相关位为只读),因此只需要关心SDRAM(BANK6)的设置。

    我的板子使用的SDRAM是现代的HY57V641620HGT-H,根据手册的第一页标题“4 Banks x 1M x 16Bit Synchronous DRAM”,就知道它是16位的了。另外,从该芯片的引脚也能看得出,数据输出引脚为DQ0~DQ15,16个。所以这里,DW6设为16位。

    BANKCON0:

    NOR Flash的配置,在设置其以前让我们仔细看看S3C44B0X手册里这张时序图,理解一些概念:
 

    看清楚Tacs,Tcos,Tacc,Toch,Tcah都是什么了吧?OK让我们看看Flash的芯片手册。我的板子使用AMD的AM29LV160D-90EI,手册中有时序图如下:
 


    很好,让我们对照两张时序图,可以很容易的得出:

        Tacs = tACC - tCE
        Tcos = tCE - tOE
        Tacc = tRC - tACC + tOE
        Toch = 0
        Tcah = 0

    于是再看Flash手册中的这个表:
 


    前面说了,我的ROM是AM29LV160D -90 EI,故在取那些数值时使用-90列:

        Tacs = tACC - tCE = 90 - 90 = 0ns
        Tcos = tCE - tOE = 90 - 35 = 55ns
        Tacc = tRC - tACC + tOE = 90 - 90 + 35 = 35ns
        Toch = 0 = 0ns
        Tcah = 0 = 0ns

    手册上提供的单位都是纳秒(ns),而寄存器需要的是时钟周期数。S3C44B0X手册中在一个很不起眼的地方说了一段很重要的话:

        All types of master clock in this memory controller correspond to the bus clock.
        For example, MCLK in DRAM and SRAM is same as the bus clock, and SCLK in SDRAM is also the same as the bus clock. In this chapter (Memory Controller), one clock means one bus clock.

    就是说存储器控制寄存器中涉及到时钟数的参数时,都是以总线时钟作为度量单位的。

    Flash的总线时钟等于系统时钟,也就是前面配置的40MHz,因此可以得知:
        T = 1 / 40MHz = 25ns
        Tacs = 0/25 = 0clk
        Tcos = 55/25 = 2.2clk
        Tacc = 35/25 = 1.4clk
        Toch = 0/25 = 0clk
        Tcah = 0/25 = 0clk

    在实际使用时的取值应该大于等于这些值,以免系统不稳定。因此最后我们取值:
        Tacs = 0clk
        Tcos = 4clk
        Tacc = 2clk
        Toch = 0clk
        Tcah = 0clk

    REFRESH:
 
    接下来该进行SDRAM的配置了。由于有些参数要先在MRSRB6中确定,所以让我们先跳过BANKCON6和REFRESH,看看MRSRB6如何配置。

    参照S3C44B0X手册,发现其实只有CL(CAS潜伏期)需要我们来选择。而HY57V641620HGT-H只支持2或3,而通常认为2的效率会明显优于3,因此我选用了2。

    然后HY57V641620HGT-H手册上下面这两张表(部分)非常重要,设置BANKCON6和REFRESH就靠它们了:
 



    BANKCON6:

    由于HY57V641620HGT -H ,我们只看-H这列的。显然Trcd=20ns。而当CL=2时,SDRAM的总线频率将是100MHz,即10ns,故Trcd=20/10=2clk。

    然后是SCAN。HY57V641620HGT-H手册对其地址引脚有如下解释:
 


    可知SCAN应填8位,即00。

    REFRESH:

    Trp和Trc在上面的表里都能查得出,Trp=2clk,Trc=7clk。而计算刷新频率的公式在S3C44B0X手册中有给出:
 


    注意其中刷新周期(refresh period)单位为微秒us,MCLK单位为MHz。

    这个刷新周期到哪里找呢?我的SDRAM手册第一页就有这么一句:4096 refresh cycles / 64ms。哈,这么一来显然刷新周期就是64ms/4096=15.625us。

    由于MCLK就是PLL产生的系统时钟40MHz,因此很容易算出refresh_count=1424。

    BANKSIZE:

    HY57V641620HGT-H的手册上说了:4 Banks x 1M x 16Bit Synchronous DRAM,可以算出它是8M的,因此BK67MAP设为8M/8M就可以了。

    呼,好累,至此存储器初始化就算是做完了。接下来就是照之前所说的,把内核程序从Flash搬进刚才初始化好的SDRAM里。代码很简单,注意好源地址和目标地址别弄错就行了。要说明的是由于这种方法一次复制4*8=32个字节,所以如果内核程序大小不能整除32的话会多复制几个字节:
        实际复制大小 = 向上取整(内核大小 / 32) * 32

    最后靠一个b指令跳进RAM中已放置好的内核代码入口点去执行就行了。

    接下来看head.s,内核程序的最前面部分,包含了内核异常向量表和入口点。

head.s
.equ KERNEL_STACK, 0x0c002000 @ 内核堆栈栈底 .equ KERNEL_LIMIT, 0x0c001000 @ 内核堆栈栈限 .text @ 这里是物理地址0x0c000000(RAM) vectors: b undef_handler @ 内核异常向量0 b swi_handler @ 内核异常向量1 b pabort_handler @ 内核异常向量2 b dabort_handler @ 内核异常向量3 b irq_handler @ 内核异常向量4 b fiq_handler @ 内核异常向量5 .space 0x100 - 6 * 4 @ 填充至256字节 @ 这里是物理地址0x0c000100 start: ldr sp, = KERNEL_STACK @ 初始化svc模式的堆栈 ldr sl, = KERNEL_LIMIT @ 设置svc模式的栈限 bl entry @ 跳转到C程序中执行 mov pc, #0 @ 软复位 undef_handler: swi_handler: pabort_handler: dabort_handler: irq_handler: fiq_handler: b . @ 目前什么都不做 
    这段代码没太多可说的,由于目前中断都是屏蔽的,也不需要什么异常处理,所以只要陷入异常就死循环。还有就是我们设定的内核入口点是0x0c000100,那就一定要保证start是从这个位置开始的,上面那个.space语句就是填充一些字节来确保这一点的。

    start子程序的任务很简单,就是调用C函数entry进入C执行。调用C程序之前要初始化好堆栈,这是常识。由于我们本例中只使用svc模式,所以只要初始化好svc模式的堆栈就行了。栈址的选择需要注意,保证一不能溢出,二不能被代码覆盖。

    GNU AS与ARMASM不同,使用外部符号不需要IMPORT声明,所以直接bl entry就可以了。

    本文所使用的跑马灯和 《第一个跑马灯程序》 一文中的不同,没有死循环,完成一次显示就会返回到head.s,而在head.s中通过mov pc, #0软复位,这样系统重启后又会执行第二次LED显示,如此反复,表现上和前文中的跑马灯程序并无二异。

led.c
#define PORT(addr) (*(volatile int *)(addr)) #define INTCON PORT(0x01e00000) #define INTMSK PORT(0x01e0000c) #define PCONC PORT(0x01d20010) #define PDATC PORT(0x01d20014) static void init(void) { INTCON = 0x7; /* 非向量模式,禁止IRQ,禁止FIQ */ INTMSK = 0x07ffffff; /* 屏遮所有中断 */ PCONC = 0xaaaaaa56; /* PC1=output,PC2=output,PC3=output */ } static void led(int num, int light) { if (light) PDATC |= 1 << num; else PDATC &= ~(1 << num); delay(1000); } static void delay(int times) { volatile int i; for (i = 0; i < (times << 10); ++i); } void entry(void) /* 因为该程序是通过符号链接的,而不是前例中的绝对地址,因此把该函数放在代码开始处也就不是必需的了 */ { init(); led(1, 1); led(2, 1); led(3, 1); led(1, 0); led(2, 0); led(3, 0); /* 直接返回到head.s */ }

    至此bootloader就写完了,剩下的任务自然是生成机器码。Makefile如下:

Makefile:
CC = arm-elf-gcc AS = arm-elf-as LD = arm-elf-ld OBJCOPY = arm-elf-objcopy ADDRESS = 0x0c000000 .PHONY : all clean all : image.bin clean : rm -f *.bin rm -f *.elf rm -f *.o image.bin : image.asm boot.bin kernel.bin nasm -f bin -o $@ $< kernel.bin : kernel.elf $(OBJCOPY) -O binary -R .comment -R .note -S $< $@ chmod a-x $@ kernel.elf : head.o main.o $(LD) -Ttext $(ADDRESS) -nostdinc -o $@ $^ chmod a-x $@ main.o : main.c $(CC) -Wall -O2 -c -o $@ $< head.o : head.s $(AS) -o $@ $< boot.bin : boot.elf $(OBJCOPY) -O binary -R .comment -R .note -S $< $@ chmod a-x $@ boot.elf : boot.o $(LD) -Ttext 0 -nostdinc -o $@ $< chmod a-x $@ boot.o : boot.s $(AS) -o $@ $

    其中有个NASM程序image.asm,它是用来把boot.bin和kernel.bin拼在一起的。按照前面的设计,kernel.bin必须从KERNEL_DATA也就是0x00000100处开始,而这里boot.bin大小为228字节,因此需要填充256-228=28个字节。

image.asm:
incbin "boot.bin" times 0x100 - ($ - $$) db 0 incbin "kernel.bin"

    make一下就能得到程序映像image.bin,但是怎么把它烧录进Flash?因为这次我们的目标机是完完全全的裸机,没有u-boot了,无法使用loadb,只好使用Flash烧录工具了。

    我用的是板子配套光盘上的Flash Programmer(很想找个开源的工具,可惜没找到),满心欢喜的点开它之后傻眼了,该烧录工具不支持binary格式。。。只支持s19、elf和hex等格式。

    一阵google,找到了这么一个开源的程序: bin2s19.cpp ,可以把binary转成s19格式(后缀是.cpp,其实是个纯c程序,最好把后缀名改成.c再用gcc编译)。

    假设上面那个程序编出来为bin2s19放在image.bin的生成目录下,执行:
        ./bin2s19 image.bin 0 0x02000000 image.s19
    就能生成s19文件image.s19。

    使用Flash Programmer可以成功地烧录该文件。然后Reset,熟悉的跑马灯又成功的再现了!泪水之余再次让我感到了学海茫茫,前路遥远,我要挺住啊!

    注:其实Flash Programmer自带了一个命令行程序BinToS19.exe,因为是win32的,配合我的编译环境使用起来不太方便。
    注2:上面bin2s19.cpp的链接已经失效,而我自己手上的文件也已经丢失,无奈之下自己看文档写了一个功能相同的程序, 源代码在此 。或者直接点开这里:
bin2s19.c
#include <stdio.h> #include <errno.h> #include <stdlib.h> #include <string.h> #define LINE_BYTES 0x10 #define min(a, b) ((a) < (b) ? (a) : (b)) static unsigned char data_buf[LINE_BYTES]; static char line_buf[64] = { 'S' }; static int str2hex(char *); static void show_help(char *); static int file_conv(FILE *, FILE *, unsigned long, size_t); static int line_conv(size_t *, unsigned long); int main(int argc, char **argv) { int err; FILE *bin, *s19; if (argc < 5) { show_help(argv[0]); return EINVAL; } bin = fopen(argv[1], "rb"); if (!bin) { err = errno; printf("cannot open file %s!/n", argv[1]); return err; } s19 = fopen(argv[4], "wb"); if (!s19) { err = errno; printf("cannot open file %s!/n", argv[4]); } else { err = file_conv(bin, s19, str2hex(argv[2]), str2hex(argv[3])); fclose(s19); } fclose(bin); return err; } static int str2hex(char *s) { int hex; sscanf(s, "%x", &hex); return hex; } static void show_help(char *app) { printf("Usage:/n/t%s [bin-file] [offset] [size] [s19-file]/n", app); } static int file_conv(FILE *bin, FILE *s19, unsigned long offset, size_t size) { int err; size_t rd_size; do { rd_size = fread(data_buf, 1, min(size, LINE_BYTES), bin); if (!rd_size) break; err = line_conv(&rd_size, offset); size -= rd_size; offset += rd_size; fputs(line_buf, s19); } while (!err); return err; } static int line_conv(size_t *size, unsigned long offset) { int i, err; size_t off_size; unsigned long checksum; char *p; unsigned char *off_buf; err = 0; if (offset + *size < *size) { printf("offset out of range!/n"); err = EFBIG; *size = ~offset; if (!*size) return err; } p = line_buf + 1; if (offset + *size > 0x1000000) off_size = 4; else if (offset + *size > 0x10000) off_size = 3; else off_size = 2; checksum = off_size + *size + 1; *p++ = '0' + off_size - 1; p += sprintf(p, "%02X", checksum); off_buf = (unsigned char *)(&offset); for (i = off_size - 1; i >= 0; i--) { p += sprintf(p, "%02X", off_buf[i]); checksum += off_buf[i]; } for (i = 0; i < *size; i++) { p += sprintf(p, "%02X", data_buf[i]); checksum += data_buf[i]; } checksum = ~checksum; p += sprintf(p, "%02X", checksum & 0xff); strcat(p, "/r/n"); return err; }
评论 39
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值