3

I'm working on a Raspberry Pi 3 (RPi3) bare-metal assignment for school and I am trying to get my assembly code to change the brightness of each diode in the RGB LED.

In simple, I am trying to get Pulse Width Modulation (PWM) to work on my RPi3 using assembly code and no libraries. After research (details below), I believe I have gathered all the steps necessary to setting up PWM to use on GPIOs 18 and 19. But, using my code, the LED is not even lighting up. Are there any steps that I am missing, or is there something I am not seeing in my code?


Steps Taken:

  • I simplified my code to being a top down procedure, except for one sub-procedure _wait, which is tested to be working. I wanted to eliminate as many potential issues such as misplaced stack pointers.
  • I am referencing the BCM2835 Arm Peripherals{1}.
  • I've tested the PWM on my Pi using Ultibo {2} and their PWM example with positive results. I even read/traced their source code to gather a list of procedures and hardware addresses they use. That is where I came up with my list that I use in my assembly code:

    1. Set RNG1 and RNG2
    2. Set MODE1 and MODE2
    3. Enable M/S Channels
    4. Set The Frequency:
      • Stop the PWM Clock {a}
      • Set the Frequency in the PWM CLK DIV Register {b}
      • Set Clock to Oscillator
      • Start the PWM Clock
    5. Set GPIO 18 and 19 as Input (This is redundant an unnecessary)
    6. Set GPIO 18 and 19 to Alternate Function 5 (Changes them to PWM Outputs instead of Inputs)
    7. Start an infinite loop writing to DAT1 and DAT2 with changing values between 0 and (range max value)

Notes:

{a} I used Ultibo Source Code to find the Address for PWM Clock because it is not in the BCM2835. Also, according to the Ultibo source code, the PWM clock uses the same design as the General Purpose Clock in page 107 and 108 of the BCM2835, so I will be using the same names.

{b} Also used Ultibo to see the formula to determine the numbers to put in for DIVI and DIVF: (Default Frequency/Desired Frequency) = DIVI remainder DIVF. The example uses a default of 19.2MHz and a desired frequency of 9.6MHz, which result in a DIVI of 2, and a DIVF of 0.


References:

{1} https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2835/BCM2835-ARM-Peripherals.pdf

{2} https://github.com/ultibohub/Examples/tree/master/18-PWMControl/RPi3


Code

.section .text
.global _start
_start:
@===============================================================
@ Pulse Width Modulator
@===============================================================
@ 1. Set Ranges
@ 2. Set Modes
@ 3. Enable M/S
@ 4. Enable Channels
@===============================================================
    LDR R5, =0x3F20C000  @ R5 = CTL  (PWM Control Register, See BCM2835 Pg 142-143)
    LDR R6, =0x3F20C010  @ R6 = RNG1 (See BCM2835 Pg 141)
    LDR R7, =0x3F20C020  @ R7 = RNG2 (All 32 bits dedicated to the value)

@ Set Ranges --------------------------------------------
    MOV R1, #0xFF  @ Desired Range from 0 to 255
    STR R1, [R6]   @ RNG1:=255
    MOV R0, #10
    BL  _wait      @ Wait 10 ms
    STR R1, [R7]   @ RNG2:=255
    MOV R0, #10
    BL  _wait      @ Wait 10 ms

@ Set Modes -------------------------------------------------
    LDR R1, [R5]   @ Get current Control Registers
    BIC R1, #1<<1  @ Clear Bit 1 (MODE1:=PWM_Mode)
    STR R1, [R5]   @ Store back to CTL
    MOV R0, #10
    BL  _wait      @ Wait 10ms

    LDR R1, [R5]   @ Get current Control Registers again
    BIC R1, #1<<9  @ Clear Bit 9 (MODE2:=PWM_Mode)
    STR R1, [R5]   @ Store bac
    MOV R0, #10
    BL  _wait      @ Wait 10ms

@ Enable M/S -----------------------------------------------
    LDR R1, [R5]   @ Get current Control Registers
    ORR R1, #1<<7  @ Set Bit 7 (MSEN1:=1)
    STR R1, [R5]   @ Store back
    MOV R0, #10
    BL  _wait      @ Wait 10ms

    LDR R1, [R5]   @ Get current Control Registers
    ORR R1, #1<<15 @ Set Bit 15 (MSEN2:=1)
    STR R1, [R5]   @ Store back
    MOV R0, #10
    BL  _wait      @ Wait 10ms

@ Enable Channels --------------------------------------------
    LDR R1, [R5]   @ Get current Control Registers
    ORR R1, #1<<0  @ Set Bit 0 (PWEN1:=1)
    STR R1, [R5]   @ Store back
    MOV R0, #10
    BL  _wait      @ Wait 10ms

    LDR R1, [R5]   @ Get current Control Registers
    ORR R1, #1<<8  @ Set Bit 8 (PWEN2:=1)
    STR R1, [R5]   @ Store back
    MOV R0, #10
    BL  _wait      @ Wait 10ms

@===========================================================
@ Clock Procedures
@===========================================================
@ 1. Stop Clock
@ 2. Set Frequency
@ 3. Set Clock to Oscillator Mode
@ 4. Start Clock
@===========================================================
    LDR R5, =0x3F1010A0    @ PWM CLK CTL (Clock Control Register)
    LDR R6, =0x3F1010A4    @ PWM CLK DIV (Clock Divisor Register)
    LDR R7, =0x5A000000    @ PWM CLK PASSWD

@ Stop Clock ----------------------------------------------
    LDR R3, [R5]    @ Read PWM Clock Control Register
    BIC R3, #1<<7   @ Clear BUSY bit
    BIC R3, #1<<4   @ Clear ENAB bit (to turn it off)
    ORR R3, R7      @ Set PASSWD
                    @ R3 is ready to write
    STR R3, [R5]    @ Store R3 to Clock Register (ENAB bit is cleared)
    MOV R0, #10
    BL  _wait       @ Wait 10ms

@ Set Frequency -------------------------------------------------
_clock_freq:
    MOV R3, #2<<12  @ 19.2MHz divided by 9.6MHz = 2 remainder 0
                    @ Shift 12 places to Integer part of Divisor
                    @ (BCM2835 Pg 108 for bit-packing of general clocks)
    ORR R3, R7      @ Set Password
                    @ R3 should now be 0x5A002000
_clock_freq_loop:
    MOV R0, #10
    BL  _wait            @ Wait 10ms each loop
    LDR R1, [R5]         @ Load Clock Control Register (to check BUSY bit)
    LSR R1, #7           @ Move Busy Bit to Bit Position 0
    AND R1, #1           @ Single out the Busy Bit
    CMP R1, #0           @ Check to see if it is cleared
    BNE _clock_freq_loop @ Try again if clock is busy
_clock_freq_end:
    STR R3, [R6]    @ Set Frequency to Clock Divisors Register
    MOV R0, #10
    BL  _wait

@ Set Oscillator Mode --------------------------------------
_clock_osc:
    LDR R3, [R5]    @ Read PWM Clock Control Register
    BIC R3, #1<<7   @ Clear BUSY bit
    BIC R3, #0xF    @ Clear SRC bits (0-3)
    ORR R3, #1      @ Set SRC to Oscillator Mode
    ORR R3, R7      @ Set PASSWD
                    @ R3 is ready to write
_clock_osc_loop:
    MOV R0, #10
    BL  _wait           @ Wait 10ms each loop
    LDR R1, [R5]        @ Load Clock Control Register
    LSR R1, #7          @ Move Busy Bit to Bit Position 0
    AND R1, #1          @ Single out the Busy Bit
    CMP R1, #0          @ Check to see if it is cleared
    BNE _clock_osc_loop @ Try again if clock is busy
_clock_osc_end:
    STR R3, [R5]    @ Store R3 to Clock Register (SRC = 1)
    MOV R0, #10
    BL  _wait       @ Wait 10ms

@ Start Clock -----------------------------------------
_clock_start:
    LDR R3, [R5]    @ Read PWM Clock Control Register
    BIC R3, #1<<7   @ Clear BUSY bit
    ORR R3, #1<<4   @ Set ENAB bit (to turn it back on)
    ORR R3, R7      @ Set PASSWD
                    @ R3 is ready to write 
_clock_start_loop:
    MOV R0, #10
    BL  _wait             @ Wait 10ms each loop
    LDR R1, [R5]          @ Load Clock Control Register 
    LSR R1, #7            @ Move Busy Bit to Bit Position 0
    AND R1, #1            @ Single out the Busy Bit
    CMP R1, #0            @ Check to see if it is cleared
    BNE _clock_start_loop @ Try again if clock is busy
_clock_start_end:
    STR R3, [R5]     @ Store R3 to Clock Register (ENAB bit is set)
    MOV R0, #10
    BL  _wait        @ Wait 10ms

@======================================================
@ GPIO Procedures
@======================================================
@ 1. Set Pins 18 and 19 as Inputs
@ 2. Set to Alternate Function 5 (Overwrites Previous Step)
@======================================================
    LDR R3, =0x3F200004    @ R3 = GPIO_FSEL1

@ Set Pins as Input ---------------------------------
    LDR R1, [R3]           @ Load current FSEL1 options
    BIC R1, #0b111111<<24  @ Sets GPIOs 18 and 19 to Default (Input)
    STR R1, [R3]           @ Not sure why... but Ultibo does this in bcm2710.pas
    MOV R0, #10
    BL  _wait              @ Wait 10ms

@ Set Pins to Alternate Function 5 ---------------------
    LDR R1, [R3]           @ Load current FSEL1 options again
    BIC R1, #0b111111<<24  @ Clear whatever is in FSEL for 18 and 19 (should already be clear)
    ORR R1, #0b010010<<24  @ 010 010 (010 is alt function 5 for both GPIO 18 and 19)
                           @ Shifted over 8 sets of 3 bits
                           @ (8 * 3 = 24) into position for GPIO 18 and 19
    STR R1, [R3]           @ Store it into FSEL1
    MOV R0, #10
    BL  _wait              @ Wait 10ms

@==============================================
@ Set Data / Main Loop
@==============================================
    LDR R5, =0x3F20C014  @ R3 = PWM_DAT1
    LDR R6, =0x3F20C024  @ R4 = PWM_DAT2

@ Main Loop ----------------------------

    MOV   R3, #0
    MOV   R4, #255
_loop:
    CMP   R3, #255   @ Reset Counters When They
    MOVGT R3, #0     @ Go Beyond Range 0..255
    CMP   R4, #0
    MOVLT R4, #255

    STR   R3, [R5]   @ Set DAT1 to a number between 0..255
    MOV   R0, #10
    BL    _wait      @ Wait 10ms

    STR   R4, [R6]   @ Set DAT2 to a number between 0..255
    MOV   R0, #10
    BL    _wait      @ Wait 10ms

    ADD   R3, R3, #1 @ R3++
    SUB   R4, R4, #1 @ R4--
_reloop:
    BAL   _loop

@===============================
@ _wait
@===============================
@ Purpose:
@ Takes an initial time from
@ the TIMER, and continuously
@ checks if the amount of time
@ in R0 has elapsed.
@ 
@ Initial:
@ R0 = delay in ms
@
@ Registers Used:
@ R4-R7
@===============================
_wait:
_wait_prolog:
    PUSH    {R4-R7, LR}
    MOV     R6, #1000
    MUL     R4, R0, R6      @ R4 = desired delay (x1000 ms to us)
    LDR     R5, =0x3F003004 @ R5 = TIMER
    LDR     R6, [R5]        @ initial time (us)
_wait_loop:
    LDR     R7, [R5]        @ current time (us)
    SUB     R7, R7, R6      @ initial time - current time = time delayed
    CMP     R7, R4          @ if the desired delay has not been reached
    BLT     _wait_loop      @ then loop again
_wait_epilog:
    POP     {R4-R7, PC}

I'm expecing the LED to at least turn on and modulate on the clock, but it is not even turning on.

I've already tested the GPIOs for output and input functionality. Using the same hardware addresses I use in the code, I was able to get a blinking the LED, and also got another LED to Blink 3 times after a button press, so the GPIO function should be fine.

The only things I can't test as easily is if I am taking the correct steps to enabling PWM, or if I am properly setting the clock so that it doesn't fail.

Miguel Aragon
  • 148
  • 1
  • 9
  • 1
    Nicely written question. No idea how to answer it. +1 – fuz May 04 '19 at 23:11
  • Did you try this in something like C first, something easier to program (can do baremetal in C), to prove the algorithm? did you try simply blinking the leds first with push pull then try the pwm later? did you work one peripheral at a time or just go for it and try to write a bunch of code for everything at once? theres a lot here, and not an idea of how you got here. – old_timer May 05 '19 at 03:17
  • @old_timer No I haven't used C yet. I'm a bit unfamiliar with using C with address pointers, but I think it will be good to study that. For my assembly, I started off by getting the LED to turn on as an output, just to test that the base address I was using was correct. Then I just downloaded open source code online that used PWM for the RPi3, so I can see what steps they took and addresses they used. It is a bit difficult testing if the PWM works at all because of how many steps just to enable it. – Miguel Aragon May 05 '19 at 15:16
  • if its not easier then fine for some folks it is...there is a baremetal forum at raspberrypi.org have you tried that? – old_timer May 05 '19 at 18:43
  • +1 from me too for the quality of the question. Can't help much as all these bits and addresses tell me nothing. Carefully read through the code and couldn't spot any 'silly' mistakes, got puzzled with BAL for about 5 seconds :) Could you also show the _wait code, for completeness? (and also to make sure it doesn't corrupt anything). – tum_ May 05 '19 at 22:31
  • @tum_ Thanks, I added it to the code, and added the .global header, so it is how it looks in my file. Most of my code is just top down, because I didn't want to debug my stack, with the exception of _wait. I use my _wait procedure in other working source codes. It preserves all the registers, except R0, which I account for. – Miguel Aragon May 05 '19 at 23:06
  • @old_timer Yea I have been searching everywhere, including those forums, and I can gather bits and pieces of information that got me here. I think I will also ask my question there as well. Thanks. – Miguel Aragon May 05 '19 at 23:31
  • @MiguelAragon As someone who never worked with GPIOs directly I'm a bit puzzled with a silly question: the Ultibo example uses GPIO as **output** to control the LEDs and this kind of makes sense to me. While you're trying to use them as **input** and this, in my blurred mind picture, translates to "you're trying to read from the LEDs". Anyway, my view is probably completely wrong. (Have you considered trying to get help from Broadcom?) – tum_ May 06 '19 at 08:36
  • @tum_ Thank you for your suggestion, I just emailed them now. I'll update with any response. As for using the GPIO as **input**, I traced Ultibo's source code to `function BCM2710GPIOFunctionSelect` lines 6390-6402 in bcm2710.pas source file. In my code the line uses hard value of `0x3F000000` can also look like `BIC R1, #0b111111<<24 @ Restore GPIO to Default` which just gets overwritten with 010010 a few lines down. I put that comment because I was not sure why they set it as an input just before setting them to Alt Function 5, and not just setting them directly to Alt Function 5. – Miguel Aragon May 06 '19 at 12:27
  • @old_timer So I studied C a bit more, and I ported my project to C source, testing all the hardware addresses. It did make it so much faster to debug and change around code! So I got it to work now, but all I did was changes the order of my little procedures. **Clock Procedures**, then **PWM Procedures**, then **GPIO Procedures**, and finally the **Main Loop**. Unfortunately, I'm not sure _why_ this works, so I am hesitant to submit this as an "answer." Do you have any recommendations? – Miguel Aragon Jun 30 '19 at 18:28
  • no need for asm, just use c, dude – Phil Jul 09 '20 at 20:15

1 Answers1

1

I haven't tried the code but at first glance it seems that you never leave the wait subroutine.

In @ Set Ranges branch to _wait,

and in wait you do

PUSH {R4-R7, LR},

which is right because LR store the address of the point where the subroutine was called. But in the end you do

POP {R4-R7, PC}.

If you push the LR you should retrieve LR rather than PC and you also need to tell the subroutine to go back to the address that called it, saved to the LR register, and then be able to go back with BX LR.

I don't exclude that there are other problems but a simple change to the code with the above references could be this:

_wait:
_wait_prolog:
    PUSH    {R4-R7, LR}
    MOV     R6, #1000
    MUL     R4, R0, R6      @ R4 = desired delay (x1000 ms to us)
    LDR     R5, =0x3F003004 @ R5 = TIMER
    LDR     R6, [R5]        @ initial time (us)
_wait_loop:
    LDR     R7, [R5]        @ current time (us)
    SUB     R7, R7, R6      @ initial time - current time = time delayed
    CMP     R7, R4          @ if the desired delay has not been reached
    BLT     _wait_loop      @ then loop again
_wait_epilog:
    POP     {R4-R7, LR}
    BX      LR
Lluke
  • 21
  • 5
  • 1
    `POP` into PC is a valid way of returning from a function, except it doesn't switch between ARM/Thumb before ARMv5. Raspberry Pi 3 is ARMv8, and the question does not contain Thumb code. – Timothy Baldwin Sep 04 '20 at 10:47