RP2350 UART Driver in Assembler

3 days ago 1

An RP2350 UART driver written entirely in Assembler.

/** * FILE: main.s * * DESCRIPTION: * RP2350 Bare-Metal UART. * * BRIEF: * Minimal bare-metal UART on the RP2350. This bypasses * SDK abstractions and demonstrates register-level control in assembler. * Clocks the external crystal oscillator (XOSC) at 14.5MHz. * * AUTHOR: Kevin Thomas * CREATION DATE: November 2, 2025 * UPDATE DATE: November 2, 2025 */ .syntax unified // use unified assembly syntax .cpu cortex-m33 // target Cortex-M33 core .thumb // use Thumb instruction set /** * Memory addresses and constants. */ .equ STACK_TOP, 0x20082000 .equ STACK_LIMIT, 0x2007a000 .equ XOSC_BASE, 0x40048000 .equ XOSC_CTRL, XOSC_BASE + 0x00 .equ XOSC_STATUS, XOSC_BASE + 0x04 .equ XOSC_STARTUP, XOSC_BASE + 0x0c .equ PPB_BASE, 0xe0000000 .equ CPACR, PPB_BASE + 0x0ed88 .equ CLOCKS_BASE, 0x40010000 .equ CLK_PERI_CTRL, CLOCKS_BASE + 0x48 .equ RESETS_BASE, 0x40020000 .equ RESETS_RESET, RESETS_BASE + 0x0 .equ RESETS_RESET_CLEAR, RESETS_BASE + 0x3000 .equ RESETS_RESET_DONE, RESETS_BASE + 0x8 .equ IO_BANK0_BASE, 0x40028000 .equ IO_BANK0_GPIO16_CTRL_OFFSET, 0x84 .equ PADS_BANK0_BASE, 0x40038000 .equ PADS_BANK0_GPIO16_OFFSET, 0x44 .equ UART0_BASE, 0x40070000 /** * Initialize the .vectors section. The .vectors section contains vector * table and Reset_Handler. */ .section .vectors, "ax" // vector table section .align 2 // align to 4-byte boundary /** * Vector table section. */ .global _vectors // export _vectors symbol _vectors: .word STACK_TOP // initial stack pointer .word Reset_Handler + 1 // reset handler (Thumb bit set) /** * @brief Reset handler for RP2350. * * @details Entry point after reset. Performs: * - Stack initialization * - Coprocessor enable * - GPIO16 pad/function configuration * - Branches to main() which contains the blink loop * * @param None * @retval None */ .global Reset_Handler // export Reset_Handler symbol .type Reset_Handler, %function .type Reset_Handler, %function Reset_Handler: bl Init_Stack // initialize MSP/PSP and limits bl Init_XOSC // initialize external crystal oscillator bl Enable_XOSC_Peri_Clock // enable XOSC peripheral clock bl Init_Subsystem // initialize subsystems bl UART_Release_Reset // ensure UART0 out of reset bl UART_Init // initialize UART0 (pins, baud, enable) bl Enable_Coprocessor // enable CP0 coprocessor b main // branch to main loop .size Reset_Handler, . - Reset_Handler /** * @brief Initialize stack pointers. * * @details Sets Main and Process Stack Pointers (MSP/PSP) and their limits. * * @param None * @retval None */ .type Init_Stack, %function Init_Stack: ldr r0, =STACK_TOP // load stack top msr PSP, r0 // set PSP ldr r0, =STACK_LIMIT // load stack limit msr MSPLIM, r0 // set MSP limit msr PSPLIM, r0 // set PSP limit ldr r0, =STACK_TOP // reload stack top msr MSP, r0 // set MSP bx lr // return /** * @brief Init XOSC and wait until it is ready. * * @details Configures and initializes the external crystal oscillator (XOSC). * Waits for the XOSC to become stable before returning. * * @param None * @retval None */ .type Init_XOSC, %function Init_XOSC: ldr r0, =XOSC_STARTUP // load XOSC_STARTUP address ldr r1, =0x00c4 // set delay 50,000 cycles str r1, [r0] // store value into XOSC_STARTUP ldr r0, =XOSC_CTRL // load XOSC_CTRL address ldr r1, =0x00FABAA0 // set 1_15MHz, freq range, actual 14.5MHz str r1, [r0] // store value into XOSC_CTRL .Init_XOSC_Wait: ldr r0, =XOSC_STATUS // load XOSC_STATUS address ldr r1, [r0] // read XOSC_STATUS value tst r1, #(1<<31) // test STABLE bit beq .Init_XOSC_Wait // wait until stable bit is set bx lr // return /** * @brief Enable XOSC peripheral clock. * * @details Sets the peripheral clock to use XOSC as its AUXSRC. * * @param None * @retval None */ .type Enable_XOSC_Peri_Clock, %function Enable_XOSC_Peri_Clock: ldr r0, =CLK_PERI_CTRL // load CLK_PERI_CTRL address ldr r1, [r0] // read CLK_PERI_CTRL value orr r1, r1, #(1<<11) // set ENABLE bit orr r1, r1, #(4<<5) // set AUXSRC: XOSC_CLKSRC bit str r1, [r0] // store value into CLK_PERI_CTRL bx lr // return /** * @brief Init subsystem. * * @details Initiates the various subsystems by clearing their reset bits. * * @param None * @retval None */ .type Init_Subsystem, %function Init_Subsystem: .GPIO_Subsystem_Reset: ldr r0, =RESETS_RESET // load RESETS->RESET address ldr r1, [r0] // read RESETS->RESET value bic r1, r1, #(1<<6) // clear IO_BANK0 bit str r1, [r0] // store value into RESETS->RESET address .GPIO_Subsystem_Reset_Wait: ldr r0, =RESETS_RESET_DONE // load RESETS->RESET_DONE address ldr r1, [r0] // read RESETS->RESET_DONE value tst r1, #(1<<6) // test IO_BANK0 reset done beq .GPIO_Subsystem_Reset_Wait // wait until done bx lr // return /** * @brief Enable coprocessor access. * * @details Grants full access to coprocessor 0 via CPACR. * * @param None * @retval None */ .type Enable_Coprocessor , %function Enable_Coprocessor: ldr r0, =CPACR // load CPACR address ldr r1, [r0] // read CPACR value orr r1, r1, #(1<<1) // set CP0: Ctrl access priv coproc 0 bit orr r1, r1, #(1<<0) // set CP0: Ctrl access priv coproc 0 bit str r1, [r0] // store value into CPACR dsb // data sync barrier isb // instruction sync barrier bx lr // return /** * @brief Release UART0 from reset and wait until it is ready. * * @details Clears the UART0 reset bit in the Reset controller (RESETS->RESET) * and polls the corresponding bit in RESETS->RESET_DONE until the * UART0 block is no longer in reset. This ensures UART registers are * accessible before configuring the peripheral. * * @param None * @retval None */ .type UART_Release_Reset, %function UART_Release_Reset: ldr r0, =RESETS_RESET // load RESETS->RESET address ldr r1, [r0] // read RESETS->RESET value bic r1, r1, #(1<<26) // clear UART0 reset bit str r1, [r0] // write value back to RESETS->RESET UART_Release_Reset_Wait: ldr r0, =RESETS_RESET_DONE // load RESETS->RESET_DONE address ldr r1, [r0] // read RESETS->RESET_DONE value tst r1, #(1<<26) // test UART0 reset-done bit beq UART_Release_Reset_Wait // loop until UART0 is out of reset bx lr // return /** * @brief Initialize UART0 (pins, baud divisors, line control and enable). * * @details Configures IO_BANK0 pins 0 (TX) and 1 (RX) to the UART function * and programs the corresponding pad controls in PADS_BANK0. It * programs the integer and fractional baud divisors (UARTIBRD and * UARTFBRD), configures UARTLCR_H for 8-bit transfers and FIFOs, * and enables the UART (UARTCR: UARTEN + TXE + RXE). * The routine assumes the UART0 base is available at the * `UART0_BASE` symbol. The selected divisors (IBRD=6, FBRD=33) are * chosen to match the expected peripheral clock; if your UART * peripheral clock differs, adjust these values accordingly. * * @param None * @retval None */ .type UART_Init, %function UART_Init: ldr r0, =IO_BANK0_BASE // load IO_BANK0 base ldr r1, =2 // FUNCSEL = 2 -> select UART function str r1, [r0, #4] // write FUNCSEL to GPIO0_CTRL (pin0 -> TX) str r1, [r0, #0x0c] // write FUNCSEL to GPIO1_CTRL (pin1 -> RX) ldr r0, =PADS_BANK0_BASE // load PADS_BANK0 base add r0, r0, #0x04 // compute PAD[0] address (PADS + 0x04) ldr r1, =0x04 // pad config value for TX (pull/func recommended) str r1, [r0] // write PAD0 config (TX pad) ldr r0, =PADS_BANK0_BASE // load PADS_BANK0 base again add r0, r0, #0x08 // compute PAD[1] address (PADS + 0x08) ldr r1, =0x40 // pad config value for RX (pulldown/IE as needed) str r1, [r0] // write PAD1 config (RX pad) ldr r0, =UART0_BASE // load UART0 base address ldr r1, =0 // prepare 0 to disable UARTCR str r1, [r0, #0x30] // UARTCR = 0 (disable UART while configuring) ldr r1, =6 // integer baud divisor (IBRD = 6) str r1, [r0, #0x24] // UARTIBRD = 6 (integer baud divisor) ldr r1, =33 // fractional baud divisor (FBRD = 33) str r1, [r0, #0x28] // UARTFBRD = 33 (fractional baud divisor) ldr r1, =112 // UARTLCR_H = 0x70 (FIFO enable + 8-bit) str r1, [r0, #0x2c] // UARTLCR_H = 0x70 (FIFO enable + 8-bit) ldr r1, =3 // RXE/TXE mask (will be shifted into bits 8..9) lsl r1, r1, #8 // shift RXE/TXE into bit positions 8..9 orr r1, r1, #1 // set UARTEN bit (bit 0) str r1, [r0, #0x30] // UARTCR = enable (UARTEN + TXE + RXE) bx lr // return /** * Initialize the .text section. * The .text section contains executable code. */ .section .text // code section .align 2 // align to 4-byte boundary /** * @brief Main application entry point. * * @details Implements the infinite blink loop. * * @param None * @retval None */ .global main // export main .type main, %function // mark as function main: .Push_Registers: push {r4-r12, lr} // push registers r4-r12, lr to the stack .Loop: bl UART0_In // call UART0_In bl UART0_Out // call UART0_Out b .Loop // loop forever .Pop_Registers: pop {r4-r12, lr} // pop registers r4-r12, lr from the stack bx lr // return to caller /** * @brief UART0 transmit (blocking). * * @details Waits for TX FIFO to be not full, then writes the lowest 8 bits of r0 to UART0. * Data to send must be in r0 on entry. * * @param r0: byte to transmit (lower 8 bits used) * @retval None */ .type UART0_Out, %function UART0_Out: .UART0_Out_Push_Registers: push {r4-r12, lr} // push registers r4-r12, lr to the stack .UART0_Out_loop: ldr r4, =UART0_BASE // base address for uart0 registers ldr r5, [r4, #0x18] // read UART0 flag register UARTFR into r5 ldr r6, =32 // mask for bit 5, TX FIFO full (TXFF) ands r5, r5, r6 // isolate TXFF bit and set flags bne .UART0_Out_loop // if TX FIFO is full, loop ldr r6, =0xff // mask for the 8 lowest bits ands r0, r0, r6 // mask off upper bits of r0, keep lower 8 bits str r0, [r4, #0] // write data to UARTDR .UART0_Out_Pop_Registers: pop {r4-r12, lr} // pop registers r4-r12, lr from the stack bx lr // return /** * @brief UART0 receive (blocking). * * @details Waits for RX FIFO to be not empty, then reads a byte from UART0 into r0. * * @param None * @retval r0: received byte (lower 8 bits valid) */ .type UART0_In, %function UART0_In: .UART0_In_Push_Registers: push {r4-r12, lr} // push registers r4-r12, lr to the stack .UART0_In_loop: ldr r4, =UART0_BASE // base address for uart0 registers (use r4 per convention) ldr r5, [r4, #0x18] // read UART0 flag register UARTFR into r5 ldr r6, =16 // mask for bit 4, RX FIFO empty RXFE ands r5, r5, r6 // isolate RXFE bit and set flags bne .UART0_In_loop // if RX FIFO is empty, loop ldr r0, [r4, #0] // load data from UARTDR into r0 (return value) .UART0_In_Pop_Registers: pop {r4-r12, lr} // pop registers r4-r12, lr from the stack bx lr // return /** * @brief Configure GPIO. * * @details Configures a GPIO pin's pad control and function select. * * @param r0 - PAD_OFFSET * @param r1 - CTRL_OFFSET * @param r2 - GPIO * @retval None */ .type GPIO_Config, %function GPIO_Config: .GPIO_Config_Push_Registers: push {r4-r12, lr} // push registers r4-r12, lr to the stack .GPIO_Config_Modify_Pad: ldr r4, =PADS_BANK0_BASE // load PADS_BANK0_BASE address add r4, r4, r0 // PADS_BANK0_BASE + PAD_OFFSET ldr r5, [r4] // read PAD_OFFSET value bic r5, r5, #(1<<7) // clear OD bit orr r5, r5, #(1<<6) // set IE bit bic r5, r5, #(1<<8) // clear ISO bit str r5, [r4] // store value into PAD_OFFSET .GPIO_Config_Modify_CTRL: ldr r4, =IO_BANK0_BASE // load IO_BANK0 base add r4, r4, r1 // IO_BANK0_BASE + CTRL_OFFSET ldr r5, [r4] // read CTRL_OFFSET value bic r5, r5, #0x1f // clear FUNCSEL orr r5, r5, #0x05 // set FUNCSEL 0x05->SIO_0 str r5, [r4] // store value into CTRL_OFFSET .GPIO_Config_Enable_OE: ldr r4, =1 // enable output mcrr p0, #4, r2, r4, c4 // gpioc_bit_oe_put(GPIO, 1) .GPIO_Config_Pop_Registers: pop {r4-r12, lr} // pop registers r4-r12, lr to the stack bx lr // return /** * @brief GPIO set. * * @details Drives GPIO output high via coprocessor. * * @param r0 - GPIO * @retval None */ .type GPIO_Set, %function GPIO_Set: .GPIO_Set_Push_Registers: push {r4-r12, lr} // push registers r4-r12, lr to the stack .GPIO_Set_Execute: ldr r4, =1 // enable output mcrr p0, #4, r0, r4, c0 // gpioc_bit_out_put(GPIO, 1) .GPIO_Set_Pop_Registers: pop {r4-r12, lr} // pop registers r4-r12, lr from the stack bx lr // return /** * @brief GPIO clear. * * @details Drives GPIO output high via coprocessor. * * @param r0 - GPIO * @retval None */ .type GPIO_Clear, %function GPIO_Clear: .GPIO_Clear_Push_Registers: push {r4-r12, lr} // push registers r4-r12, lr to the stack .GPIO_Clear_Execute: ldr r4, =0 // disable output mcrr p0, #4, r0, r4, c0 // gpioc_bit_out_put(GPIO, 1) .GPIO_Clear_Pop_Registers: pop {r4-r12, lr} // pop registers r4-r12, lr from the stack bx lr // return /** * @brief Delay_MS. * * @details Delays for r0 milliseconds. Conversion: loop_count = ms * 3600 * based on a 14.5MHz clock. * * @param r0 - milliseconds * @retval None */ .type Delay_MS, %function Delay_MS: .Delay_MS_Push_Registers: push {r4-r12, lr} // push registers r4-r12, lr to the stack .Delay_MS_Check: cmp r0, #0 // if MS is not valid, return ble .Delay_MS_Done // branch if less or equal to 0 .Delay_MS_Setup: ldr r4, =3600 // loops per MS based on 14.5MHz clock mul r5, r0, r4 // MS * 3600 .Delay_MS_Loop: subs r5, r5, #1 // decrement counter bne .Delay_MS_Loop // branch until zero .Delay_MS_Done: pop {r4-r12, lr} // pop registers r4-r12, lr from the stack bx lr // return /** * Test data and constants. * The .rodata section is used for constants and static data. */ .section .rodata // read-only data section /** * Initialized global data. * The .data section is used for initialized global or static variables. */ .section .data // data section /** * Uninitialized global data. * The .bss section is used for uninitialized global or static variables. */ .section .bss // BSS section
Read Entire Article