..

ARM assembly: blink

So I got myself a Raspberry Pi Pico and downloaded the C/C++ SDK.

The smallest program is blink.c:

/**
 * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include "pico/stdlib.h"

int main() {
    const uint LED_PIN = 25;
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);
    while (true) {
        gpio_put(LED_PIN, 1);
        sleep_ms(250);
        gpio_put(LED_PIN, 0);
        sleep_ms(250);
    }
}

Running make blink.s compiles the program and leaves a file called blink.c.s behind. Let’s have a look:

	.cpu cortex-m0plus
	.eabi_attribute 20, 1
	.eabi_attribute 21, 1
	.eabi_attribute 23, 3
	.eabi_attribute 24, 1
	.eabi_attribute 25, 1
	.eabi_attribute 26, 1
	.eabi_attribute 30, 2
	.eabi_attribute 34, 0
	.eabi_attribute 18, 4
	.file	"blink.c"
	.text
	.section	.text.startup.main,"ax",%progbits
	.align	1
	.p2align 2,,3
	.global	main
	.arch armv6-m
	.syntax unified
	.code	16
	.thumb_func
	.fpu softvfp
	.type	main, %function
main:
	@ args = 0, pretend = 0, frame = 0
	@ frame_needed = 0, uses_anonymous_args = 0
	push	{r4, r5, r6, lr}
	movs	r5, #128
	movs	r4, #208
	lsls	r5, r5, #18
	movs	r6, r5
	movs	r0, #25
	lsls	r4, r4, #24
	bl	gpio_init
	str	r5, [r4, #36]
.L2:
	movs	r0, #250
	str	r6, [r4, #20]
	bl	sleep_ms
	movs	r0, #250
	str	r5, [r4, #24]
	bl	sleep_ms
	b	.L2
	.size	main, .-main
	.ident	"GCC: (GNU Tools for Arm Embedded Processors 9-2019-q4-major) 9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]"

Well, that’s interesting. What does it all mean?

	.cpu cortex-m0plus

Target CPU. Valid values are the same as for the -mcpu command-line option. So this is for m0+.

	.eabi_attribute 20, 1
	.eabi_attribute 21, 1
	.eabi_attribute 23, 3
	.eabi_attribute 24, 1
	.eabi_attribute 25, 1
	.eabi_attribute 26, 1
	.eabi_attribute 30, 2
	.eabi_attribute 34, 0
	.eabi_attribute 18, 4

EABI is “embedded application binary interface”. The application binary interface is a set of conventions which allow different modules to work together (allowing C code to call assembly code and vice versa, for example). But what are those numbers? Those are attribute numbers, so this could be written as:

	.eabi_attribute Tag_ABI_FP_denormal, 1
	.eabi_attribute Tag_ABI_FP_exceptions, 1
	.eabi_attribute Tag_ABI_FP_number_model, AEABI_FP_number_model_ieee754_all
	.eabi_attribute Tag_ABI_align_needed, 1
	.eabi_attribute Tag_ABI_align_preserved, 1
	.eabi_attribute Tag_ABI_enum_size, 1
	.eabi_attribute Tag_ABI_optimization_goals, 2
	.eabi_attribute Tag_CPU_unaligned_access, 0
	.eabi_attribute Tag_ABI_PCS_wchar_t, 4

Do these things tell the assembler something? Probably. Do we need them in the .s file? Time will tell.

	.file	"blink.c"

Define the current file to be "blink.c".

	.text

Start assembling the following statements. It’s possible to also have a subsection number, but none is used here.

	.section	.text.startup.main,"ax",%progbits

Assemble the following code into a section called .text.startup.main. The letters "ax" letters mean:

  1. The section is allocatable
  2. The section is executable

@progbits means that the section contains data. Not sure why %progbits and not @progbits (it may be an ARM thing, not sure). This is as opposed to “section only occupies space”.

	.align	1

Align to 2-byte boundary (1 lsb is zero).

	.p2align 2,,3

Align to 4-byte boundary (2 lsb are zero), pad with “whatever” (possiby zero or no-op), and add at most 3 bytes.

Why both .align and .p2align? No idea. Maybe the clue is that .align tries to emulate different assemblers whereas .p2align is a GAS directive and is consistent across platforms. But then why generate both? Never mind.

	.global	main

Make the symbol main visible to ld.

	.arch armv6-m

The target architecture, same as for the -march command-line option.

	.syntax unified

Sets the syntax. Apparently divided is the old syntax, unified is the new syntax. The details are probably not very interesting at this point.

	.code	16

This is the same as .thumb. .code 32 is the same as .arm.

	.thumb_func

Says that the next symbol is the name of a Thumb encoded function. This is needed for the assembler to generate correct code for interworking between Arm and Thumb instructions, whatever that means.

	.fpu softvfp

Software floating point support.

	.type	main, %function

Marks the type of the symbol main as a function. There are a few other options, such as various types of data objects.

Right, so that was a lot of stuff. Most of this is basically boilerplate. Now let’s have a look at the actual code.

main:
	@ args = 0, pretend = 0, frame = 0
	@ frame_needed = 0, uses_anonymous_args = 0
	push	{r4, r5, r6, lr}
	movs	r5, #128
	movs	r4, #208
	lsls	r5, r5, #18
	movs	r6, r5
	movs	r0, #25
	lsls	r4, r4, #24
	bl	gpio_init
	str	r5, [r4, #36]
.L2:
	movs	r0, #250
	str	r6, [r4, #20]
	bl	sleep_ms
	movs	r0, #250
	str	r5, [r4, #24]
	bl	sleep_ms
	b	.L2
	.size	main, .-main
	.ident	"GCC: (GNU Tools for Arm Embedded Processors 9-2019-q4-major) 9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]"

The lines that start with @ are comments. GNU assembler is telling us something, hopefully at some point this will make sense.

Then we start. The fist instruction (push) is probably part of EABI - those are the registers we neet to restore before we return. But we never return, so that’s OK.

The instructions are given in a somewhat random order. But we can lump them together. For example:

	movs	r0, #25
	bl	gpio_init

This calls the function gpio_init with 25 as the argument (just like the C code).

	movs	r0, #250
	bl	sleep_ms

This calls sleep_ms with 250 (ms). Just like the C code again.

The b instruction simply jumps back to .L2, so that’s an infinite loop (as expected with while (true)).

Other than that, we have:

	str	r6, [r4, #20]

and

	str	r5, [r4, #24]

That’s what causes the LED to blink, so let’s see what that does. It stores the value of r5 or r6 at the address r4 + 20 or r4 + 24. So what are those values?

	movs	r4, #208
	lsls	r4, r4, #24

lsl is a logical shift left. It shifts r4 (208) 24 to the left, and puts the result back in l4. The result is 0xd0000000.

	movs	r5, #128
	lsls	r5, r5, #18
	movs	r6, r5

One thing is clear, r5 and r6 have the same value. It’s the 7th bit shifted left 18 times. In other words, 0x2000000, or “bit 25 on”. So what’s going on here? Time to look at the Pico datasheet.

0xd0000000 is the start address of the SIO (Single-cycle IO). The registered are mapped to word-aligned addresses.

	str	r5, [r4, #36]

36 (dec) is 0x24 (hex), and offset 0x24 is the GPIO_OE_SET register. OE is “output enable”, so writing 1 « 25 (r5) into it enables output to GPIO 25 (the LED on the Raspberry Pi Pico).

And now we’re back to this:

	str	r6, [r4, #20]

Offset 20 (dec) is 0x14 (hex) which is the GPIO_OUT_SET register, so writing 1 « 25 into it sets GPIO 25 and turns on the LED.

	str	r5, [r4, #24]

Offset 24 (dec) is 0x18 (hex) which is the GPIO_OUT_CLR register, which, as expected, clears the GPIO (turns the LED off).

So that’s that. We can have a look at the functions to see what exactly they do, but we can also use the SDK and call them instead.

The last bit doesn’t look very interesting:

	.size	main, .-main
	.ident	"GCC: (GNU Tools for Arm Embedded Processors 9-2019-q4-major) 9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]"

.size is generated to include auxiliary debugging information. .ident doesn’t actually emit anything.

Next time we’ll try to write the same program in a slightly different way and see if we can assemble, link and run it.