linker.ld
rp2350 from scratch
This documentation is generated with litsdoc
It's largely derived from the RP2350 datasheet
Linker script
The first file is the linker script which describes the layout of our program in memory. Everything you never wanted to know about linker script was very helpful and good to consult directly. The linker script will actually be used at the very end of the compilation process, once our code has been compiled to ARM binary code.
The MEMORY statement controls how memory addresses are allocated within our program. In higher level programming, when we talk about memory, we talk about it as though it's one contiguous area that lives in RAM.
On this system (and in other low-level contexts), addresses in memory can have special meanings which are handled at the layers beneath us. For example, we might load from a memory address and instead of reading from the system's (relatively low) RAM it might be read from the much larger flash memory. The system handles this transparently for us, but it's up to us to make sure we use the right addresses and use supported operations with those addresses (some memory for example can't contain executable code).
Magic memory addresses and registers are also how we interact with system processes, setting up functionality like the GPIO pins or PSRAM. At the point we're starting at, there is no code defined to handle stuff like this, though it'd normally be handled by the SDK or hardware abstraction layer in a higher level program.
MEMORY {
The flash memory is where our program lives; in the datasheet it's called XIP (executable in place) and exposed at the memory address 0x1000000.
Notice that we also define whether the memory can be (r)ead, e(x)ecuted or (w)ritten to
FLASH(rx): ORIGIN = 0x10000000, LENGTH = 2M
The RP2350 has 520kb of SRAM which we use for the program stack and a few other things. Both of the memory addresses here come from pg 30 of the datasheet
SRAM(rwx): ORIGIN = 0x20000000, LENGTH = 520K
}
The second portion of our linker script is SECTIONS; a section is a named block of data. The actual data of these sections will be supplied either by us or the C compiler later, but this tells the linker how to lay them out when linking.
SECTIONS {
The init section is the very beginning of our program. For us, it just contains a few magical constants that will be explained later.
.init :{
. is the "current location counter". What we're doing here is aligning it to a 4-byte boundary.
. = ALIGN(4);
This basically says "take the .init section from the object files and place it here"
*(.init);
And then whatever comes after this should also be 4-byte aligned
. = ALIGN(4);
Our program is put into flash memory with this directive.
} >FLASH
The text section is where the bulk of our program code will go. We're also putting another section, rodata ("read only data") along with it. .rodata contains constants.
.text :{
. = ALIGN(4);
This specific directive is saying put any section named .text and any section
beginning with .text (the .text* part) here
*(.text .text*);
*(.rodata .rodata*);
. = ALIGN(4);
} >FLASH
.data stores global constants
.data :{
. = ALIGN(4);
*(.data .data*);
. = ALIGN(4);
Notice that there is a different directive here: constants are loaded from flash memory, but stored in RAM (because they may be written to)
} >SRAM AT>FLASH
stack_start = ORIGIN(SRAM) + LENGTH(SRAM);
}
pico.c
Register access primitives. See page 26 of the datasheet
Basically, by doing some magical memory stuff to a specific address, we can modify registers. Register here doesn't refer to the base registers available to assembly like r0, r1 etc, but also to APB ("advanced peripheral bus") registers. APB registers are how we interact with other components on the device, like general purpose I/O pins and UART (universal asynchronous receiver/transmitter, serial monitor).
Since we'll be doing this all the time we define a set of macros for this
REG(x) dereferences a memory address
#define REG(x) (*((volatile unsigned int *)(x)))
#define REG_ALIAS_RW_BITS 0x0000
#define REG_ALIAS_XOR_BITS 0x1000
#define REG_ALIAS_SET_BITS 0x2000
#define REG_ALIAS_CLR_BITS 0x3000
Bus endpoints. See page 32 of the datasheet. This page is where the _BASE constants come from. This table also links out to the individual sections for each bus.
Resets ([chapter 7(https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf#page=495))) of the datasheet.
We use the reset base to make subsystem resets. Basically letting the system know that we're ready to use GPIO, UART and so on, by for each of those subsystems clearing the FRCE_ON ("force on") reset register and then setting the RESET_DONE register which... resets the reset, making the subsystem ready for our use.
#define RESETS_BASE 0x40020000
#define RESETS_DONE_OFFSET 0x8
#define RESET_IO_BANK0_BITS (1 << 6)
#define RESET_PADS_BANK0_BITS (1 << 9)
#define RESET_PIO0_BITS (1 << 11)
#define RESET_PLL_SYS_BITS (1 << 14)
#define RESET_PWM_BITS (1 << 16)
#define RESET_TIMER0_BITS (1 << 23)
#define RESET_UART0_BITS (1 << 26)
#define RESET_UART1_BITS (1 << 27)
The general purpose I/O bank (chapter 9) is a set of pins we can manipulate to interact with various components on the board as well as ones we might choose to connect (for example if we e.g. a soldered an LED circuit to one of the pins).
Despite the name, in some cases GPIO pins are constrained in what they can do based on the chip's design. See the table in the chapter for an exact list of functionality.
#define IO_BANK0_BASE 0x40028000
#define IO_BANK0_GPIO0_CTRL_OFFSET 0x4 + 0 * 0x8
#define IO_BANK0_GPIO1_CTRL_OFFSET 0x4 + 1 * 0x8
#define IO_BANK0_GPIO7_CTRL_OFFSET 0x4 + 7 * 0x8
#define IO_BANK0_GPIO23_CTRL_OFFSET 0x4 + 23 * 0x8
#define IO_BANK0_GPIO24_CTRL_OFFSET 0x4 + 24 * 0x8
#define IO_BANK0_GPIO25_CTRL_OFFSET 0x4 + 25 * 0x8
#define GPIO_FUNC_UART 2
#define GPIO_FUNC_PWM 4
#define GPIO_FUNC_SIO 5
#define GPIO_FUNC_PIO0 6
#define PADS_BANK0_BASE 0x40038000
#define PADS_BANK0_GPIO0_OFFSET 0x4
#define PADS_BANK0_GPIO1_OFFSET 0x4 + 1 * 0x4
#define PADS_BANK0_GPIO7_OFFSET 0x4 + 7 * 0x4
#define PADS_BANK0_GPIO23_OFFSET 0x4 + 23 * 0x4
#define PADS_BANK0_GPIO24_OFFSET 0x4 + 24 * 0x4
#define PADS_BANK0_GPIO25_OFFSET 0x4 + 25 * 0x4
#define PADS_BANK0_GPIO0_ISO_BITS 1 << 8
#define PADS_BANK0_GPIO0_OD_BITS 1 << 7
#define PADS_BANK0_GPIO0_IE_BITS 1 << 6
#define PADS_BANK0_GPIO0_PUE_BITS 1 << 3
#define PADS_BANK0_GPIO0_SCHMITT_BITS 1 << 1
#define SIO_BASE 0xd0000000
#define SIO_CPUID_OFFSET 0x0
#define SIO_GPIO_OE_CLR_OFFSET 0x40
#define SIO_GPIO_OUT_CLR_OFFSET 0x20
#define SIO_GPIO_OE_SET_OFFSET 0x38
#define SIO_GPIO_OUT_OFFSET 0x10
#define SIO_GPIO_OUT_XOR_OFFSET 0x28
#define SIO_FIFO_ST_OFFSET 0x50
#define SIO_FIFO_ST_VLD_BITS 1 << 0
#define SIO_FIFO_ST_RDY_BITS 1 << 1
#define SIO_FIFO_WR_OFFSET 0x54
#define SIO_FIFO_RD_OFFSET 0x58
main.c
#include "pico.c"
__attribute__((naked)) void start() {
Reset peripherals we're going to use; the first register we use is the FRCE_ON register. (Since it has an offset of 0x0, there's no additional definition for it).
REG(RESETS_BASE + REG_ALIAS_CLR_BITS) =
RESET_IO_BANK0_BITS | RESET_PADS_BANK0_BITS;
Then RESET_DONE
while ((~REG(RESETS_BASE + RESETS_DONE_OFFSET)) &
(RESET_IO_BANK0_BITS | RESET_PADS_BANK0_BITS))
;
Here we're saying that we're going to use PIN 7 for SIO (single purpose input/output) by selecting function 5
REG(IO_BANK0_BASE + IO_BANK0_GPIO7_CTRL_OFFSET) = GPIO_FUNC_SIO;
REG(PADS_BANK0_BASE + PADS_BANK0_GPIO7_OFFSET + REG_ALIAS_CLR_BITS) =
PADS_BANK0_GPIO0_ISO_BITS;
REG(SIO_BASE + SIO_GPIO_OE_SET_OFFSET) = 1 << 7;
Then we XOR GPIO pin 7 to toggle it on and off
for (;;) {
REG(SIO_BASE + SIO_GPIO_OUT_XOR_OFFSET) = 1 << 7;
for (unsigned int i = 0; i != 1000000; i++)
;
}
}
linker.ld
rp2350 from scratch
This documentation is generated with litsdoc
It's largely derived from the RP2350 datasheet
Linker script
The first file is the linker script which describes the layout of our program in memory. Everything you never wanted to know about linker script was very helpful and good to consult directly. The linker script will actually be used at the very end of the compilation process, once our code has been compiled to ARM binary code.
The MEMORY statement controls how memory addresses are allocated within our program. In higher level programming, when we talk about memory, we talk about it as though it's one contiguous area that lives in RAM.
On this system (and in other low-level contexts), addresses in memory can have special meanings which are handled at the layers beneath us. For example, we might load from a memory address and instead of reading from the system's (relatively low) RAM it might be read from the much larger flash memory. The system handles this transparently for us, but it's up to us to make sure we use the right addresses and use supported operations with those addresses (some memory for example can't contain executable code).
Magic memory addresses and registers are also how we interact with system processes, setting up functionality like the GPIO pins or PSRAM. At the point we're starting at, there is no code defined to handle stuff like this, though it'd normally be handled by the SDK or hardware abstraction layer in a higher level program.
MEMORY {
The flash memory is where our program lives; in the datasheet it's called XIP (executable in place) and exposed at the memory address 0x1000000.
Notice that we also define whether the memory can be (r)ead, e(x)ecuted or (w)ritten to
FLASH(rx): ORIGIN = 0x10000000, LENGTH = 2M
The RP2350 has 520kb of SRAM which we use for the program stack and a few other things. Both of the memory addresses here come from pg 30 of the datasheet
SRAM(rwx): ORIGIN = 0x20000000, LENGTH = 520K
}
The second portion of our linker script is SECTIONS; a section is a named block of data. The actual data of these sections will be supplied either by us or the C compiler later, but this tells the linker how to lay them out when linking.
SECTIONS {
The init section is the very beginning of our program. For us, it just contains a few magical constants that will be explained later.
.init :{
. is the "current location counter". What we're doing here is aligning it to a 4-byte boundary.
. = ALIGN(4);
This basically says "take the .init section from the object files and place it here"
*(.init);
And then whatever comes after this should also be 4-byte aligned
. = ALIGN(4);
Our program is put into flash memory with this directive.
} >FLASH
The text section is where the bulk of our program code will go. We're also putting another section, rodata ("read only data") along with it. .rodata contains constants.
.text :{
. = ALIGN(4);
This specific directive is saying put any section named .text and any section
beginning with .text (the .text* part) here
*(.text .text*);
*(.rodata .rodata*);
. = ALIGN(4);
} >FLASH
.data stores global constants
.data :{
. = ALIGN(4);
*(.data .data*);
. = ALIGN(4);
Notice that there is a different directive here: constants are loaded from flash memory, but stored in RAM (because they may be written to)
} >SRAM AT>FLASH
stack_start = ORIGIN(SRAM) + LENGTH(SRAM);
}
pico.c
Register access primitives. See page 26 of the datasheet
Basically, by doing some magical memory stuff to a specific address, we can modify registers. Register here doesn't refer to the base registers available to assembly like r0, r1 etc, but also to APB ("advanced peripheral bus") registers. APB registers are how we interact with other components on the device, like general purpose I/O pins and UART (universal asynchronous receiver/transmitter, serial monitor).
Since we'll be doing this all the time we define a set of macros for this
REG(x) dereferences a memory address
#define REG(x) (*((volatile unsigned int *)(x)))
#define REG_ALIAS_RW_BITS 0x0000
#define REG_ALIAS_XOR_BITS 0x1000
#define REG_ALIAS_SET_BITS 0x2000
#define REG_ALIAS_CLR_BITS 0x3000
Bus endpoints. See page 32 of the datasheet. This page is where the _BASE constants come from. This table also links out to the individual sections for each bus.
Resets ([chapter 7(https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf#page=495))) of the datasheet.
We use the reset base to make subsystem resets. Basically letting the system know that we're ready to use GPIO, UART and so on, by for each of those subsystems clearing the FRCE_ON ("force on") reset register and then setting the RESET_DONE register which... resets the reset, making the subsystem ready for our use.
#define RESETS_BASE 0x40020000
#define RESETS_DONE_OFFSET 0x8
#define RESET_IO_BANK0_BITS (1 << 6)
#define RESET_PADS_BANK0_BITS (1 << 9)
#define RESET_PIO0_BITS (1 << 11)
#define RESET_PLL_SYS_BITS (1 << 14)
#define RESET_PWM_BITS (1 << 16)
#define RESET_TIMER0_BITS (1 << 23)
#define RESET_UART0_BITS (1 << 26)
#define RESET_UART1_BITS (1 << 27)
The general purpose I/O bank (chapter 9) is a set of pins we can manipulate to interact with various components on the board as well as ones we might choose to connect (for example if we e.g. a soldered an LED circuit to one of the pins).
Despite the name, in some cases GPIO pins are constrained in what they can do based on the chip's design. See the table in the chapter for an exact list of functionality.
#define IO_BANK0_BASE 0x40028000
#define IO_BANK0_GPIO0_CTRL_OFFSET 0x4 + 0 * 0x8
#define IO_BANK0_GPIO1_CTRL_OFFSET 0x4 + 1 * 0x8
#define IO_BANK0_GPIO7_CTRL_OFFSET 0x4 + 7 * 0x8
#define IO_BANK0_GPIO23_CTRL_OFFSET 0x4 + 23 * 0x8
#define IO_BANK0_GPIO24_CTRL_OFFSET 0x4 + 24 * 0x8
#define IO_BANK0_GPIO25_CTRL_OFFSET 0x4 + 25 * 0x8
#define GPIO_FUNC_UART 2
#define GPIO_FUNC_PWM 4
#define GPIO_FUNC_SIO 5
#define GPIO_FUNC_PIO0 6
#define PADS_BANK0_BASE 0x40038000
#define PADS_BANK0_GPIO0_OFFSET 0x4
#define PADS_BANK0_GPIO1_OFFSET 0x4 + 1 * 0x4
#define PADS_BANK0_GPIO7_OFFSET 0x4 + 7 * 0x4
#define PADS_BANK0_GPIO23_OFFSET 0x4 + 23 * 0x4
#define PADS_BANK0_GPIO24_OFFSET 0x4 + 24 * 0x4
#define PADS_BANK0_GPIO25_OFFSET 0x4 + 25 * 0x4
#define PADS_BANK0_GPIO0_ISO_BITS 1 << 8
#define PADS_BANK0_GPIO0_OD_BITS 1 << 7
#define PADS_BANK0_GPIO0_IE_BITS 1 << 6
#define PADS_BANK0_GPIO0_PUE_BITS 1 << 3
#define PADS_BANK0_GPIO0_SCHMITT_BITS 1 << 1
#define SIO_BASE 0xd0000000
#define SIO_CPUID_OFFSET 0x0
#define SIO_GPIO_OE_CLR_OFFSET 0x40
#define SIO_GPIO_OUT_CLR_OFFSET 0x20
#define SIO_GPIO_OE_SET_OFFSET 0x38
#define SIO_GPIO_OUT_OFFSET 0x10
#define SIO_GPIO_OUT_XOR_OFFSET 0x28
#define SIO_FIFO_ST_OFFSET 0x50
#define SIO_FIFO_ST_VLD_BITS 1 << 0
#define SIO_FIFO_ST_RDY_BITS 1 << 1
#define SIO_FIFO_WR_OFFSET 0x54
#define SIO_FIFO_RD_OFFSET 0x58
main.c
#include "pico.c"
__attribute__((naked)) void start() {
Reset peripherals we're going to use; the first register we use is the FRCE_ON register. (Since it has an offset of 0x0, there's no additional definition for it).
REG(RESETS_BASE + REG_ALIAS_CLR_BITS) =
RESET_IO_BANK0_BITS | RESET_PADS_BANK0_BITS;
Then RESET_DONE
while ((~REG(RESETS_BASE + RESETS_DONE_OFFSET)) &
(RESET_IO_BANK0_BITS | RESET_PADS_BANK0_BITS))
;
Here we're saying that we're going to use PIN 7 for SIO (single purpose input/output) by selecting function 5
REG(IO_BANK0_BASE + IO_BANK0_GPIO7_CTRL_OFFSET) = GPIO_FUNC_SIO;
REG(PADS_BANK0_BASE + PADS_BANK0_GPIO7_OFFSET + REG_ALIAS_CLR_BITS) =
PADS_BANK0_GPIO0_ISO_BITS;
REG(SIO_BASE + SIO_GPIO_OE_SET_OFFSET) = 1 << 7;
Then we XOR GPIO pin 7 to toggle it on and off
for (;;) {
REG(SIO_BASE + SIO_GPIO_OUT_XOR_OFFSET) = 1 << 7;
for (unsigned int i = 0; i != 1000000; i++)
;
}
}