Controlling the RGB LED on pico display
The pico display pack is a small LCD with four buttons and an RGB LED. The LED can be controlled using PWM for which the Raspberry Pi Pico has hardward support.
For this attempt I’m trying to combine a few things:
- Not use include files
- Use look-up tables
- Call functions
Rome wasn’t built in a day…
Some constants from the datasheet:
.syntax unified
.cpu cortex-m0plus
.equ IO_BANK0_GPIO6_CTRL_OFFSET, 0x34
.equ IO_BANK0_GPIO7_CTRL_OFFSET, 0x3c
.equ IO_BANK0_GPIO8_CTRL_OFFSET, 0x44
.equ IO_BANK0_GPIO6_CTRL_FUNCSEL_VALUE_PWM_A_3, 0x4
.equ IO_BANK0_GPIO7_CTRL_FUNCSEL_VALUE_PWM_B_3, 0x4
.equ IO_BANK0_GPIO8_CTRL_FUNCSEL_VALUE_PWM_A_4, 0x4
.equ CH3_CSR, 0x3c
.equ CH3_DIV, 0x40
.equ CH3_CTR, 0x44
.equ CH3_CC, 0x48
.equ CH3_TOP, 0x4c
.equ CH4_CSR, 0x50
.equ CH4_DIV, 0x54
.equ CH4_CTR, 0x58
.equ CH4_CC, 0x5c
.equ CH4_TOP, 0x60
Some standard preamble. I’m using the easiest option here, so the C
runtime calls main
instead of finding a way to get rid of everything
and just have “pure” assembly.
Starting off with setting the function. The GPIO numbers are taken from the Pimoroni product page, but I did cheat a bit because it wasn’t working at first - I checked the SDK and realised that GPIO number is not the same as pin number…
.section text.startup,"ax"
.align 2
.global main
.code 16
.thumb_func
.type main, %function
main:
@ The function we want here is PWM, which is F4
ldr r4, io_bank0_base
movs r1, #IO_BANK0_GPIO6_CTRL_FUNCSEL_VALUE_PWM_A_3
str r1, [r4, #IO_BANK0_GPIO6_CTRL_OFFSET]
movs r1, #IO_BANK0_GPIO7_CTRL_FUNCSEL_VALUE_PWM_B_3
str r1, [r4, #IO_BANK0_GPIO7_CTRL_OFFSET]
movs r1, #IO_BANK0_GPIO8_CTRL_FUNCSEL_VALUE_PWM_A_4
str r1, [r4, #IO_BANK0_GPIO8_CTRL_OFFSET]
For the LED, I’m passing R, G, B components in three different registers. It’s not very efficient, but there will plenty of time to work on efficiency. For now I’m just trying to get things to work.
I’ve written a few functions to avoid repeating myself, so set_rgb
sets the colours. PWM uses a threshold and the colours are the
thresholds.
enable_led
simply enables the PWM machinery. More details below.
@ Start with 0 for all channels (3 A/B, 4 A)
movs r0, #0
movs r1, #0
movs r2, #0
bl set_rgb @ set the colours before we
bl enable_led @ enable the LED
Trying to use index-based look-up. So r4
is the index into
the colour array. But r4
is just a byte offset, not an index.
movs r4, #0
The program is just one big loop. In each iteration we first get the colour from the table, and set the LED. I’m doing something a bit more involved, starting with 0 and adding a “colour value” until it gets to the maximum value. For example, if I want to get to 0xff0000 (red), I start with 0 and add 0x020000 127 times (yes, it gets to 0xfe but that’s good enough).
main_loop:
adr r5, colours
ldr r5, [r5, r4] @ r5 is the value to add/subtract
movs r6, #0 @ r6 is the accumulator
The reason I’m loading r5
is that I’m out of registers for
some reason. Still need to understand what’s going on with “high” and
“low” registers, but for now it’s good enough.
Two loops, one to go up (to the full colour) and one to go down (back to zero). In each loop I simply count to 127. I could do all kinds of tricks to avoid the explicit counter, but I just don’t want to.
The code is pretty straight-forward.
movs r7, #0x7f
up_loop:
movs r0, r6
bl expand
bl set_rgb
bl delay
add r6, r6, r5 @ increase
subs r7, r7, #1
bne up_loop
movs r7, #0x7f
down_loop:
movs r0, r6
bl expand
bl set_rgb
bl delay
subs r6, r6, r5 @ decrease
subs r7, r7, #1
bne down_loop
Last but not least - update the byte offset (add 4, not 1 because that casues the program to crash).
adds r4, r4, #4 @ increment colour index
cmp r4, #48 @ was that the last one?
bne main_loop @ no, continue
movs r4, #0 @ from the start
b main_loop
Enabling the PWM channels. We also need to invert the outputs, I don’t know why but that’s what the SDK does.
.code 16
.thumb_func
enable_led:
ldr r0, pwm_base
movs r1, #13 @ enable the PWM channel, invert outputs A and B
str r1, [r0, #CH3_CSR]
movs r1, #5 @ enable the PWM channel, invert output A
str r1, [r0, #CH4_CSR]
bx lr
Set the LED values. I’m also squaring the values (so 0 to 255 becomes 0 to 65052 but that’s fine). The SDK calls it “gamma” so we’ll go with that.
@ r0 = red
@ r1 = green
@ r2 = blue
.code 16
.thumb_func
set_rgb:
ldr r3, pwm_base
@ red and green
muls r0, r0, r0 @ square r0 for gamma
muls r1, r1, r1 @ square r1 for gamma
lsls r1, #16
adds r0, r0, r1
str r0, [r3, #CH3_CC]
@ blue
muls r2, r2, r2 @ square r2 for gamma
str r2, [r3, #CH4_CC]
bx lr
This extracts the R, G and B componet from a single word that
holds all three. To be honest, set_rgb
could have done that,
but that’s OK.
@ r0 is the input, expected to contain three bytes (rgb)
@ output:
@ r0 contains the red component
@ r1 contains the green component
@ r2 contains the blue component
.code 16
.thumb_func
expand:
movs r3, #255 @ mask
movs r2, r0 @ r2 = r0 & 0xff
ands r2, r3
movs r1, r0 @ r1 = (r0 >> 8) & 0xff
lsrs r1, #8
ands r1, r3
lsrs r0, #16 @ r0 = r0 >> 16
bx lr
And for the delay, I’m just counting to 131072 (or rather from 131072 downwards). Later on I’ll try to use the actual hardware timers.
.code 16
.thumb_func
delay:
movs r0, #1
lsls r0, #17
delay_loop:
subs r0, r0, #1
bne delay_loop
bx lr
Some more constants. I don’t think I can just use equ
because I can’t
load a 32 bit value into a register. Instead I’m using ldr
with an
address.
.align 2
io_bank0_base:
.word 0x40014000
pwm_base:
.word 0x40050000
And the colours (12 colours):
colours:
.word 0x00020000
.word 0x00020100
.word 0x00020200
.word 0x00010200
.word 0x00000200
.word 0x00000201
.word 0x00000202
.word 0x00000102
.word 0x00000002
.word 0x00010002
.word 0x00020002
.word 0x00020001
I’m happy with the result, it’s not very sophisticated, but it works and I’ve learnt a few things along the way.