Creating a GNU Assembly-Only Project

Sometimes it makes sense to write everything in assembly, even these days. For example if using a tiny microcontroller. Or just if one just don’t need all the productivity of the C/C++ tools. And it is a good educational experience: getting hands-on on the lower levels.

Debugging an Assembly-Only Project

In this tutorial I’ll show how you can run an assembly only project for the NXP LPC845. We will create a C project, but then get rid of everything and create a starting point with an assembly file. For this I’m using the NXP MCUXpresso IDE with the LPC845-BRK board.

LPC845-BRK Board

Creating project

First, create a normal C project for the target board:

Creating C Project

Next, we can delete all the files an folders, but we keep the ‘source’ (empty) folder:

Removing not needed files

In that source folder we add a new assembly file with the .s file extension:

Adding assembly file

In this assembly file we will implement our code.

Vector Table

First, I add the vector table:

/* ----------- Vector table ------------------------------*/
.section .isr_vector, "a"
.align 4

.global vector_table
.global M0_NMI_Handler, M0_HardFault_Handler
.global __valid_user_code_checksum /* LPC ROM Library function */
.type vector_table, %object

   .word top_of_stack   @ Entry 0: Initial stack pointer
   .word ResetISR  @ Entry 0: Reset
   .word M0_NMI_Handler  @ Entry 1: NMI
   .word M0_HardFault_Handler  @ Entry 2: HardFault
   .word 0  @ Entry 3: ...
   .word 0  @ Entry 4: ...
   .word 0  @ Entry 5: ...
   .word __valid_user_code_checksum @ Entry 6: ...
   .word 0
   .word 0
   .word 0
   .word isr_default @ SVC
   .word 0
   .word 0
   .word isr_default @ PendSV
   .word isr_default @ SysTick
   @ Chip level
   .word isr_default @  SPI0_IRQHandler,             // 16: SPI0 interrupt
   .word isr_default @  SPI1_IRQHandler,             // 17: SPI1 interrupt
   .word isr_default @  DAC0_IRQHandler,             // 18: DAC0 interrupt
   .word isr_default @  USART0_IRQHandler,           // 19: USART0 interrupt
   .word isr_default @  USART1_IRQHandler,           // 20: USART1 interrupt
   .word isr_default @  USART2_IRQHandler,           // 21: USART2 interrupt
   .word isr_default @  Reserved22_IRQHandler,       // 22: Reserved interrupt
   .word isr_default @  I2C1_IRQHandler,             // 23: I2C1 interrupt
   .word isr_default @  I2C0_IRQHandler,             // 24: I2C0 interrupt
   .word isr_default @  SCT0_IRQHandler,             // 25: State configurable timer interrupt
   .word isr_default @  MRT0_IRQHandler,             // 26: Multi-rate timer interrupt
   .word isr_default @  CMP_CAPT_IRQHandler,         // 27: Analog comparator interrupt or Capacitive Touch interrupt
   .word isr_default @  WDT_IRQHandler,              // 28: Windowed watchdog timer interrupt
   .word isr_default @  BOD_IRQHandler,              // 29: BOD interrupts
   .word isr_default @  FLASH_IRQHandler,            // 30: flash interrupt
   .word isr_default @  WKT_IRQHandler,              // 31: Self-wake-up timer interrupt
   .word isr_default @  ADC0_SEQA_IRQHandler,        // 32: ADC0 sequence A completion.
   .word isr_default @  ADC0_SEQB_IRQHandler,        // 33: ADC0 sequence B completion.
   .word isr_default @  ADC0_THCMP_IRQHandler,       // 34: ADC0 threshold compare and error.
   .word isr_default @  ADC0_OVR_IRQHandler,         // 35: ADC0 overrun
   .word isr_default @  DMA0_IRQHandler,             // 36: DMA0 interrupt
   .word isr_default @  I2C2_IRQHandler,             // 37: I2C2 interrupt
   .word isr_default @  I2C3_IRQHandler,             // 38: I2C3 interrupt
   .word isr_default @  CTIMER0_IRQHandler,          // 39: Timer interrupt
   .word isr_default @  PIN_INT0_IRQHandler,         // 40: Pin interrupt 0 or pattern match engine slice 0 interrupt
   .word isr_default @  PIN_INT1_IRQHandler,         // 41: Pin interrupt 1 or pattern match engine slice 1 interrupt
   .word isr_default @  PIN_INT2_IRQHandler,         // 42: Pin interrupt 2 or pattern match engine slice 2 interrupt
   .word isr_default @  PIN_INT3_IRQHandler,         // 43: Pin interrupt 3 or pattern match engine slice 3 interrupt
   .word isr_default @  PIN_INT4_IRQHandler,         // 44: Pin interrupt 4 or pattern match engine slice 4 interrupt
   .word isr_default @  PIN_INT5_DAC1_IRQHandler,    // 45: Pin interrupt 5 or pattern match engine slice 5 interrupt or DAC1 interrupt
   .word isr_default @  PIN_INT6_USART3_IRQHandler,  // 46: Pin interrupt 6 or pattern match engine slice 6 interrupt or UART3 interrupt
   .word isr_default @  PIN_INT7_USART4_IRQHandler,  // 47: Pin interrupt 7 or pattern match engine slice 7 interrupt or UART4 interrupt
  @ Set the initial stack pointer to 0x10003be8

The above table is for the LPC845 (ARM-Cortex M0+), so if you are using a different device this will need tweaking. The table defines the initial PC (Reset) and MSP (Main Stack Pointer) which is set to the end of the RAM:

/* ----------- Stack (MSP) ------------------------------*/
 .section .stack
.align 4

    .word 0x10000000+0x3fe0     @ Set the initial stack pointer to the end of the RAM

Most vectors are pointing to a isr_default one, meaing they stay in there to indicate that the interrupt is not handled (yet):

  b isr_default

Below I have defined a few interrupt placeholders:

/* ----------- Interrupt Handlers ------------------------------*/
  b isr_default

  b M0_NMI_Handler

  b M0_HardFault_Handler

  @ Handle the interrupt here
  bx lr  @ Return from the interrupt


Finally, the Reset vector or startup handling. In this simple application I call a main routine:

/* ----------- Reset ------------------------------*/
.global ResetISR,
.section .after_vectors

  nop  @ just do something....
  b main


Finally, the main loop of the application:

/* ----------- Main ------------------------------*/
.global main
  b main

The last thing is a LPC845 specific CRP value:

/* ------------------------------------
 * Variable to store CRP value in. Will be placed automatically by the linker when "Enable Code Read Protect" selected. */
.global CRP_WORD
.section .crp
  .align 4
  .equ CRP_WORD, 0xffffffff

That’s it!

arm-none-eabi-size "LPC845_Assembly.axf"; # arm-none-eabi-objcopy -v -O binary "LPC845_Assembly.axf" "LPC845_Assembly.bin" ; # checksum -p LPC845 -d "LPC845_Assembly.bin";   text	   data	    bss	    dec	    hex	filename    784	      0	     80	    864	    360	LPC845_Assembly.axf


Debugging works the same as for any C/C++ application:

Debugging Assembly-Only Application


Of course, an assembly-only project is not for everyone and every project. But it is a good start for a tiny project, where you do not need any high level drivers or the productivity of C or C++. Or when you need to care about every byte of RAM and FLASH, then this is a good start.

You can find the project on GitHub.

Happy assembling 🙂



What do you think?

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.