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:
- The section is allocatable
- 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.