..

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:

  1. Not use include files
  2. Use look-up tables
  3. 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.