login - registration

Back to the post list

Raspberry Pi 4 from scratch
2019 Sep 15

Hello everyone ! You are like me, you have received your Raspberry Pi 4 and you want to directly program the hardware without Linux ? You are in the right place.

The Raspberry Pi 4 has changed since previous versions, essential components no longer work with current programs, such as USB and Videocore :-( Therefore I thought it's an opportunity to start from scratch. You know the famous blank sheet ...

In this post, we will see how to create our very first program. And also how to use the Mailbox.

Start from scratch

Comments :

Aran (webmaster)
2019 Sep 15 18:22

SD card boot

First of all to start, the Raspberry Pi 4 needs an SD card with files that the firmware expects. Since we do not use Linux, we need only two in truth: start4.elf et kernel7.img.
In previous versions of the Raspberry Pi the system needed the file bootcode.bin on the SD card, but this one is now on the integrated EEPROM memory : https://www.raspberrypi.org/documentation/hardware/raspberrypi/booteeprom.md

When the Raspberry Pi 4 is turned on, the SoC loads the bootcode.bin file from the EEPROM, which then loads the start4.elf file (GPU firmware) in turn. The GPU runs start4.elf, and reads the text files config.txt if it is present on the SD card.
The config.txt file is used to configure the system : https://www.raspberrypi.org/documentation/configuration/config-txt/

To end the GPU loads the file kernel7.img, which is our operating system (replacing Linux).

There are variants of these files. I chose voluntarily the basic and 32-bit versions. All these files are available here : https://github.com/raspberrypi/firmware/tree/master/boot.

1st start

Here we go. Turn on your Raspberry Pi 4 without an SD card. What is going on ? Nothing ?
On the Raspberry Pi card, the ACT LED flashes 4 times, then settles. And repeat this process indefinitely. This means that the bootcode.bin did not find a start4.elf file. A first signal ;-)

Now let's format an SD card into FAT32, and put the file start4.elf on it. Restart Pi4 with the SD card. If all goes well, the ACT LED should now flash 7 times, indicating that the start4.elf file has been loaded.

A tool to format your SD card if needed: https://www.sdcard.org/downloads/formatter/

Raspberry Pi 4 ACT Led
Figure 1 : ACT led

Programming environment

As you can see in my previous posts, I program in assembly language. To type my programs I simply use a well-known text editor:


As with the C language, the program must be compiled to be executable by the processor. I use the software FASMARM. FASMARM also allows you to edit the program, but the text editor is too simple. Notepad++ brings better readability.


The compilation of a program is done in FASMARM with the key F9. But also directly from Notepad ++ with an F5 Run command. Just launch a script file with this line : fasmarm.exe kernel7.asm. The kernel7.asm file is the text file containing our program to compile.

Boot program

The Raspberry Pi loads the kernel7.img file into memory at the end of its boot process. This file is loaded at 0x8000 in 32-bit mode.

FASMARM is told to generate a file in the img format when compiling with this command :

format binary as 'img'

It is indicated that instructions will be loaded into memory at address 0x8000 with this instruction:

org 0x8000

The ARM processor will execute the first instruction at 0x8000 during startup. But also during a reset. We will therefore give this address a jump instruction to our main program.

For more flexibility, we load in the PC register the address of our startup program with these instructions :

	ldr pc,[vec_res]

vec_res:	dw start	; start program address

Vector table

In fact at this address 0x8000 the system expects to have other instructions, which constitutes the vector table. There are 8 vectors, which are used depending on the mode in which the Raspberry Pi switches. Here is the complete list :

	ldr pc,[vec_res]		; 0x00 : reset (mode SVC)
	ldr pc,[vec_und]		; 0x04 : undefined (mode UND)
	ldr pc,[vec_swi]		; 0x08 : software interrupt (mode SVC)
	ldr pc,[vec_pre]		; 0x0c : prefetch abort (mode ABT)
	ldr pc,[vec_dat]		; 0x10 : data abort (mode ABT)
	ldr pc,[vec_hyp]		; 0x14 : hyp mode
	ldr pc,[vec_irq]			; 0x18 : IRQ (mode IRQ)	
	ldr pc,[vec_fiq]			; 0x1c : FIRQ (mode FIQ)

vec_res:	dw start
vec_und:	dw 0
vec_swi:	dw 0
vec_pre:	dw 0
vec_dat:	dw 0
vec_hyp:	dw 0
vec_irq:	dw irqHandler
vec_fiq:	dw fiqHandler

For example, during an IRQ interrupt, the system automatically executes the instruction located at offset 0x18. Which causes a jump on the irqHandler routine.

Disabling interrupts

We will now write our main program, so at the address start. To avoid disturbances on the continuation of the operations, one disable the interrupts :


	I_Bit =	10000000b	; Irq flag bit in cpsr
	F_Bit =	01000000b	; Fiq flag bit in cpsr
	mrs r0,cpsr			; read status cpu
	orr r0,I_Bit OR F_Bit	; disable IRQ and FIRQ
	msr cpsr,r0			; write status cpu	

Vector table address

The address of the vector table is configured with these instructions :

	; Set the Address of the Vector Table in the VBAR register
	mov r0,0x8000
	mcr p15, 0, r0, c12, c0, 0	

Changing the CPU mode

We pass the CPU from Hypervisor mode to System mode :

; CPU modes :
	CPUMODE_USR	= 0x10		; cpu mode user
	CPUMODE_FIQ	= 0x11		; cpu mode FIQ
	CPUMODE_IRQ	= 0x12		; cpu mode IRQ
	CPUMODE_SVC	= 0x13		; cpu mode Supervisor
	CPUMODE_MON	= 0x16		; cpu mode Monitor
	CPUMODE_ABT	= 0x17		; cpu mode Abort
	CPUMODE_HYP	= 0x1a		; cpu mode Hypervisor
	CPUMODE_UND	= 0x1b		; cpu mode Undefined
	CPUMODE_SYS	= 0x1f		; cpu mode System
		mrs r0,cpsr					; Fetch the cpsr register which includes CPU mode bits
		and r1,r0,0x1F					; Mask off the CPU mode bits to register r1
		cmp r1,CPUMODE_HYP		; Check we are in HYP_MODE
			bne .if1.end				; Branch if not equal meaning was not in HYP_MODE
		bic r0,r0,0x1F					; Clear the CPU mode bits in register r0
		orr r0, r0,CPUMODE_SVC OR I_Bit OR F_Bit	; Logical OR SVC_MODE bits onto register with Irq/Fiq disabled
		msr spsr_cxsf,r0				; Hold value in spsr_cxsf
		add lr,pc,4					; Calculate address of .NotInHypMode label
		msr ELR_hyp,lr				; Set the address to ELR_hyp
		eret							; Elevated return which will exit at .NotInHypMode in SVC_MODE

Parking cores

The raspberry Pi 4 has 4 cores. We continue the operations only on the core 0. If the instructions are executed on another core, we make a loop :

	; Mode non HYP : Now park Core 1,2,3 into deadloop
	mrc p15, 0, r0, c0, c0, 5			; Read core id on ARM7 & ARM8
	ands r0,0x3					; Make core 2 bit bitmask in R0
	beq .if2.end               			; Core 0 jumps out
		b .SecondarySpinDeadloop
	; We now have core 0

Initialization of stacks

The CPU has 9 operating modes, briefly described here. In each mode a stack is necessary in particular for saving the registers when jumping to the subprograms.

For the moment, I only use System, IRQ, FIQ and Supervisor CPU modes. Here is the corresponding code to declare the addresses of the different stacks (initialization of the SP register in each mode) :

	STACKSVC	= 0x01000
	STACKIRQ	= 0x02000
	STACKFIQ	= 0x03000
	STACKSYS	= 0x04000
	cps CPUMODE_SYS			; stack in cpu mode System
	mov sp,STACKSYS	
	cps CPUMODE_IRQ			; stack in cpu mode IRQ
	mov sp,STACKIRQ
	cps CPUMODE_FIQ			; stack in cpu mode FIQ
	mov sp,STACKFIQ
	cps CPUMODE_SVC			; stack in cpu mode Supervisor
	mov sp,STACKSVC			; remain in supervisor mode

It's ready.

Here we made the essential code to have a mini operational operating system. We can compile the code with FASMARM, and put the file kernel7.img on the SD card.

If everything is OK, you should see the ACT led flash briefly and then turn off.

Hello World !

It is customary to do a "Hello world" to show the good functioning of a new system. At this stage it is impossible to do it on the screen, we will be content to light up the ACT led.



For that we will call on the Mailbox. It is a module integrated into the firmware of the Raspberry Pi, which makes it easier to communicate with the components. This routine receives an input message that describes the action to be performed. Let's start by creating our program for calling the mailbox.

The different stages are:

  • wait until we can write to the mailbox
  • write the address of our message to the mailbox with channel identifier
  • wait for the response
  • check the status of the response

Here is the corresponding code :

PERIPHERAL_BASE	= 0xfe000000		; peripheral base address Pi4
MAIL_BASE			= 0xb880			; Mailbox Base Address
MAIL0_READ			= 0x0			; Mailbox 0 Read Register
MAIL0_STATUS		= 0x18			; Mailbox 0 Status Register
MAIL1_RW			= 0x20			; Mailbox 1 Read/Write Register
MAIL_EMPTY			= 0x40000000		; There is nothing to read from the Mailbox
MAIL_FULL			= 0x80000000		; There is no space to write into the Mailbox
; mboxCall : make a mailbox call
; input r0		: mailbox channel
; input r1		: message address
; output r0	: =0 if error, =0x80000000 if success, =0x80000001 partial answer
align 4


	; wait until we can write to the mailbox
		ldr r3,[r2,MAIL0_STATUS]		; Read mailbox0 status from GPU
		tst r3,MAIL_FULL				; Make sure arm mailbox is not full
	bne .DO1

	; write the address of our message to the mailbox with channel identifier
	bic r1,0xf
	orr r3,r1,r0						; r3 = address + channel
	str r3,[r2,MAIL1_RW]

	; now wait for the response
		.DO3:						; is there a response?
			ldr r3,[r2,MAIL0_STATUS]	; Read mailbox0 status
			tst r3,MAIL_EMPTY		; Wait for data in mailbox
		bne .DO3
		ldr r3,[r2,MAIL0_READ]			; is it a response to our message?
		and r3,0xf
		cmp r3,r0
	bne .DO2							; different channel

	; read the status of the mailbox response
	ldr r0,[r1,4]

mov pc,lr

Message to the mailbox

The message for the mailbox must be in a specific format. Here is the structure of the message during a request, and the answer given.

mailbox messages
Figure 2 : mailbox messages

Let's take an example : we want to get the board revision of our Raspberry Pi.
This mailbox request is done with the tag identifier Get Board Revision whose value is 0x10002. A list of tags here.

The first word to give in our message is the size of the message in total. We will do the calculation automatically in the code. Then we indicate that the message is a query with a word with the value 0.

After indicating the tag identifier of our request, we specify the size of the buffer. This buffer contains the value associated with the request, and also the response that the mailbox will give. The buffer size must be sufficient to receive the response. For example, if the query value is 4 bytes, and the response is 8 bytes, then an 8 bytes buffer should be specified so that the response value is not truncated.
In our example, there is no value associated with the Get Board Revision tag, but the answer will be 4 bytes. So we put the size of the buffer at 4 (in green in the diagram above).

Then comes the query code with a word to zero, which means the process request. Our buffer is empty because there is no request value, but we must still provide a location for the response. So we put a word.

The message ends with the end tag, which is a zero word. It is possible to put several tag identifiers concatenated before the end tag, repeating the blue box above.

Here is what we get with the code :

struc MboxGetBoardRevision
	dw .end - $					; message size
	dw $00000000				; request code

	; Sequence Of Concatenated Tags
	dw Get_Board_Revision			; tag identifier 0x10002
	dw 0x00000004				; buffer size in bytes
	dw 0x00000000				; process request
	dw 0x00000000				; one word for the response

	dw 0x00000000				; end tag

We can now use our mboxCall program, and give it the message as a parameter.

	MAIL_TAGS	= 8			; ARM -> VC property channel
	align 16
	mboxGetBoardRevision		MboxGetBoardRevision
	mov r0,MAIL_TAGS				; mailbox channel 8
	mov r1,mboxGetBoardRevision		; message address
	bl mboxCall

And here is the answer obtained in the message :

message size0x0000001COur message is the size of 7*4 = 28 bytes
response code0x80000000Request Successful
tag identifier0x00010002
value buffer size0x00000004
response code0x80000004The value in response takes 4 bytes.
buffer response value0x00A03111The answer !
end tag0x00000000

The request went well. My raspberry Pi has for Revision Board the value 0x00A03111, which corresponds to the Pi 4 1GB of RAM.

lighted bulb

Let there be light !

We will now be able to turn on our ACT LED with a new mailbox message. We use for this a request on the GPIO virtual buffer. Here is the code :

struc MboxSetGPIOVirtualBuffer
	dw .end - $					; message size
	dw $00000000				; request code

	dw Set_GPIO_Virtual_Buffer		; tag identifier 0x48020
	dw 0x00000004				; buffer size in bytes
	dw 0x00000000				; process request
	dw 0xC0000000 + 0x400000		; request value

	dw 0x00000000				; end tag

	align 16
	mboxSetGPIOVirtualBuffer		MboxSetGPIOVirtualBuffer
	mov r0,MAIL_TAGS				; mailbox channel 8
	mov r1,mboxSetGPIOVirtualBuffer		; message address
	bl mboxCall

The ACT LED should light up and stay on.

The above code may not work. Here is another method to turn on the led :

struc MboxGetGPIOVirtualBuffer
	dw .end - $					; message size
	dw $00000000				; request code

	dw Get_GPIO_Virtual_Buffer		; tag identifier 0x40010
	dw 0x00000004				; buffer size in bytes
	dw 0x00000000				; process request
	dw 0						; response value buffer

	dw 0x00000000				; end tag

	align 16
	mboxGetGPIOVirtualBuffer			MboxGetGPIOVirtualBuffer
	mov r0,MAIL_TAGS				; mailbox channel 8
	mov r1,mboxGetGPIOVirtualBuffer		; message address
	bl mboxCall
	mov r1,mboxGetGPIOVirtualBuffer
	ldr r0,[r1,20]						; read the GPIO Virtual Buffer address
	and r0,0x3fffffff					; conversion to physical address
	ldr r1,[r0]
	orr r1,0x00010000					; update of the led status to ON
	str r1,[r0]

It's over for this post. Feel free to tell me if you enjoyed, or if you have questions. A message is always a pleasure.

mail smiley
2019 Sep 18 21:05

Very informative!
Tho, I dont understand the last part.. you say it's possible that the "LED, stay on" script could sometimes not work?

Why is that? Please elaborate..
Btw the images are a nice touch!
2019 Sep 18 21:09

Oh and one last thing, the RSS feed does technically work, tho all of the posts are in the wrong order?

Anyways, I wish you good luck with your Pi 4!
Aran (webmaster)
2019 Sep 18 22:11

Unfortunately I did not find much information about the GPIO virtual buffer. Only sources that allowed me to write this code : https://github.com/vanvught/rpidmx512/blob/master/lib-bcm2835/src/bcm2837_gpio_virt.c and https://github.com/Terminus-IMRC/rpi3-gpiovirtbuf/blob/master/rpi3-gpiovirtbuf.c
I think that the request value points to the address of the gpio virtual buffer, to which we add the value that we want to modify. Here the value must point to the gpio of the led. I feel that this address can change. This is why the second solution is safe, because we will just get the address of the GPIO virtual buffer, before writing the value.

Yes thank you for the appreciation, I also find that the pictures make the post more pleasant to read.

I reversed the rss feed, it's actually more logical that the most recent is first.

Aran (webmaster)
2019 Oct 3 12:20


Here is more information on the light up of the ACT LED. We can do without the Mailbox.The LED is wired on the GPIO42 for the Pi4 (not present on the pins). You have to configure this GPIO42 in output, and set it in the GPSET register. Here is the code :

PERIPHERAL_BASE		= 0xfe000000	; Pi 4
GPIO_BASE				= 0x00200000
GPIO_GPFSEL4			= 0x10		; GPIO Function Select 4
GPIO_FSEL2_OUT		= 0x40		; FSEL42 output
GPIO_GPSET1			= 0x20		; GPIO Pin Output Set 1

ldr r0,[r4,GPIO_GPFSEL4]
bic r0,0x1c0					; clear bits 8-6
orr r0,GPIO_FSEL2_OUT		; GPIO 42 in output
str r0,[r4,GPIO_GPFSEL4]	

mov r0,1 SHL 10				; bit 42 = 1 (set GPIO 42)
str r0,[r4,GPIO_GPSET1]		; light up the led

Add a comment

Page : 1