korena's blog

9. Bare metal & low level init - part 4

In the previous post, we got to the point of initializing DRAM and testing our initialization code by copying a bit of code to an address within DRAM and successfully executing it. One major problem with that code is the fact that our assembly startup.s file got too large, so in this post, we're going to take a more modular approach to better manage the project setup, we will also setup the scene to follow the recommendation of the iROM document with regard to the structure of our initialization code.
You can find the full code at S5PV210-bootloader repository

New project structure

running ls -R command in the top directory gives:

korena@korena-solid:~/Development/Workspaces/car-dashboard (master)$ ls -R
.:
asm  bl2  doc  host_bin  host_scripts  host_tools  include  linker.lds  Makefile  README  src  target_bin

./asm:
startup.s  uart_mod.s bl2_asm.s  clock_mod.s  memory_mod.s  OS_test_mod.s
./bl2:
asm src BL2_linker.lds

./bl2/asm:

./doc:
k4t1gxx4qf_rev111(DDR2).pdf  S5PV210_iROM_ApplicationNote_Preliminary_20091126.pdf  Schematic

./doc/Schematic:
S70-1146.pdf  Tiny210-1148.pdf  TINYSDK2-1148.pdf

./host_bin:

./host_scripts:
debug.log  fuseBin.sh  kermrc  logport

./host_tools:
imageMaker.c  readFileBytes.c

./include:
S5PV210.inc

./src:

./target_bin:
README.md

Under the assembly source directory asm/ , we've modularized our startup.s into multiple parts named *_mod.s, these are basically the exact same functions we had previously jammed into startup.s, but are more readable and transferable within our code base, by transferable, I mean we could move them to any code compilation unit (code that is compiled, assembled and linked independently within the code base, so BL1 is a unit, BL2 is a separate unit, and the OS is another unit).

The reason we went this way is that, for us to follow the recommendation of Samsung's iROM documentation, BL1 is supposed to be verifying the integrity of BL2, and loading it to SRAM on a cold startup. The rest of what we've done, namely initializing memory, changing clock settings and copying the OS into DRAM is a task designated to BL2. Note that we're still dealing with a cold reset, meaning the processor is expected to be booting up from a hard reset, not a software reset or sleep mode, for those modes, the execution sequence or path would be different, and we'd have to check for certain conditions to figure that out, that is a topic for another day.

Minor modifications

In addition to restructuring our project's setup, some modifications were added to our code for UART string printing, basically, we need our printing function to figure out the length of a string without us having to pass it the length of that string, because itching the length of every string we save is a galactic gigantic nuisance.

asm/uart_mod.s :

/*For UART*/
.equ GPA0CON_OFFSET,            0x000
.equ GPA1CON_OFFSET,            0x020
.equ UART_BASE,                 0XE2900000
.equ UART0_OFFSET,              0x0000

.equ ULCON_OFFSET,                  0x00
.equ UCON_OFFSET,                   0x04
.equ UFCON_OFFSET,                  0x08
.equ UMCON_OFFSET,                  0x0C
.equ UTXH_OFFSET,                   0x20
.equ UBRDIV_OFFSET,                 0x28
.equ UDIVSLOT_OFFSET,       0x2C
.equ UART_UBRDIV_VAL,       34  
.equ UART_UDIVSLOT_VAL,         0xDDDD

.equ GPIO_BASE,  0xE0200000
.equ UART_CONSOLE_BASE, (UART_BASE + UART0_OFFSET)



.equ TERM, 0x00

.text
.code 32

.global uart_asm_init
.global uart_print
.global uart_print_hex
.global uart_print_string


uart_asm_init:
        stmfd sp!,{lr}
        /* set GPIO(GPA) to enable UART */
        @ GPIO setting for UART
        ldr     r0, =GPIO_BASE  @0xE0200000
        ldr     r1, =0x22222222
        str     r1, [r0,#GPA0CON_OFFSET]  @ storing 0010 in all reg fields, which configures PA0 for UART 0 and UART 1.

        ldr     r0, =UART_CONSOLE_BASE

        mov     r1, #0x0
        str     r1, [r0,#UFCON_OFFSET] @ resetting all bits in UFCON0 register, disabling FIFO, which means the Tx/Rx buffers 
                                       @ offer a single byte to hold transfer/receive data
        str     r1, [r0,#UMCON_OFFSET] @ resetting all bits in UMCON0 register, disabling auto flow control (AFC), and setting 'Request to 
                                            @ Send' bit to zero, which basically means that our UART0 module will tell the other side of the
                                            @ communication line that we can never receive anything, however, it is up to the sender to respect
                                            @ this, depending on it's configuration.

        mov     r1, #0x3
        str     r1, [r0,#ULCON_OFFSET]     @ setting bits [0:1] to 0b11, which means data packets are 8 bits long, standard stuff.

        ldr     r1, =0x3c5                  @ (0011-1100-0101)
        str     r1, [r0,#UCON_OFFSET]       @ Receive Mode: interrupt/polling mode, Transmit Mode: interrupt/polling mode,
                                            @ Send Break Signal: No, Loop-back Mode: disabled, Rx Error Status Interrupt: Enable,
                                            @ Rx Time Out: Enable,Rx Interrupt Type: level, Tx Interrupt Type: level.
                                            @  Clock Selection: PCLK,dont care for the rest.

        ldr     r1, =UART_UBRDIV_VAL   @ 34
        str     r1, [r0,#UBRDIV_OFFSET]

        ldr     r1, =UART_UDIVSLOT_VAL  @ 0xDDDD
        str     r1, [r0,#UDIVSLOT_OFFSET]
        
	ldr     r0,=init_uart_string
        mov     r1,#init_uart_len
        bl      uart_print_string

        ldmia sp!,{pc}


/*void uart_print_string(char* string, int size)*/

uart_print_string:
        stmfd sp!,{r0-r4,lr}
        ldr     r2, =UART_CONSOLE_BASE          @0xE29000000
1:
        ldrb    r3,[r0],#1
        mov     r4, #0x10000 @ delay
2:      subs    r4, r4, #1
        bne     2b
        strb    r3,[r2,#UTXH_OFFSET]    
        subs    r1,r1,#1
        bne     1b
        ldmia sp!,{r0-r4, pc}

/*void uart_print_hex(uint32_t hexToPrint)*/
uart_print_hex:
        stmfd sp!,{r0-r4,lr}
	mov r1,r0
	mov r2,#8
loopLag:
	mov	r1,r1,ror #28
	and	r0,r1,#0x0000000F  @ mask
	
	cmp r0,#10
	bge hexVal
	add r0,r0,#0x30
	bal printIt
hexVal:
	add r0,r0,#0x37
printIt:
	ldr	r3,=UART_CONSOLE_BASE 
	strb    r0,[r3,#0x20]   @ This is the UTXH register, the transmit buffer.	
        mov     r4, #0x10000 @ delay
2:      subs    r4, r4, #1
        bne     2b

	sub	r2,r2,#1
	cmp	r2,#0
	bne	loopLag	
	mov	r0,#0x0D
        strb    r0,[r3,#0x20]
        mov     r4, #0x10000 @ delay
3:      subs    r4, r4, #1
	bne     3b
        mov     r0,#0x0A
	strb	r0,[r3,#0x20]
#	mov	pc,lr
        ldmia sp!,{r0-r4, pc}

uart_print:
	stmfd sp!,{r0-r4, lr}
        ldr     r2, =UART_CONSOLE_BASE          @0xE29000000
1:
        ldrb    r3,[r0],#1
	cmp	r3,#TERM
	beq	done
        mov     r4, #0x10000 @ delay
2:      subs    r4, r4, #1
        bne     2b
        strb    r3,[r2,#UTXH_OFFSET]    
        b       1b
done:
        ldmia sp!,{r0-r4, pc}

.section .rodata
init_uart_string:
.ascii "UART 0 Initialization complete ...\n"
.set init_uart_len,.-init_uart_string

We've only added the function uart_print to the module, so from now on, we can just place a pointer to the first element of a character array in r0, and have the function print that array, note that the string needs to be terminated, because a 0x00 is what the function looks for to denote the end of the string to be printed. We could have just removed the string printing function we wrote earlier, but that means going back to all the previous strings and fixing them to work with the new function, but we're too cool to care about that.

Since we're still looking at the code above, lets take note of what was changed for the uart_mod.s module to be picked up and linked into startup.o without problems. The first thing you should notice is the .global directives that basically exposes the functions defined in this file to the linker, so when the compiler (assembler in this case) is compiling another module that uses these functions, it will trust that, the linker will find these objects defined somewhere in the compilation unit, this is kinda like your extern directive in C, a promise to the compiler that, if it does not flag these functions as undefined errors, the linker will find their definitions somewhere else and resolve them to their appropriate objects. All other modules expose their functionality the same way.

conclusion

We haven't done much in this post, we merely moved code around for a more convenient setup. This post will mark the end of low level initialization milestone, so we can move to the next milestone, where we will finally be writing C code instead of assembly, much more fun! This does not mean that we're done with assembly, after all, our initialization code is still very primitive, and needs a lot of work, specifically in the areas of waking up from a soft reset, or a sleep state.