It is entirely possible to create a project entirely in assembly language. However, in many cases we might still want to reuse some of the C code such as system initialization function and peripheral driver because recreating these codes in assembly can just be too much work.
To do this in Keil MDK, we can use the following steps:
1. Create a project, but without adding the Cortex® Microcontroller Software Interface Standard (CMSIS) software components to the project.
2. Manually copy a start-up code (for example, from one of the previous example project) into a file and name is as an assembly language file (e.g., “startup_stm32l053.s”) and add it to the project.
3. Optionally, manually modify this start-up code so that it does not call SystemInit().
4. In project setting, select MicroLib so that the assembly start-up file does not reference to “__use_two_region_memory”. Alternatively, just remove the heap setup information from the project.
5. Manually add a simple assembly file that contains __main, as follows.
PRESERVE8 ; Indicate the code here preserve
; 8 byte stack alignment
THUMB ; Indicate THUMB code is used
AREA |.text|, CODE, READONLY ; Start of CODE area
EXPORT __main ; Make function visible from outside
__main FUNCTION
B main
ENDFUNC
main FUNCTION
B . ; while(1)
ENDFUNC
END ; End of file
21.7.2. Hello World
One of the most common projects in programming classes is the hello world. It is reasonably easy to do that in C/C++. However, to do this in assembly language programming requires quite a lot of work because existing device drivers and header files are in C/C++, and they need to be ported to assembly code.
To make the setup similar to what we have already got, the SystemInit() function and clock/PLL configuration functions are also ported to assembly code files, and are called at the beginning of main(). In many cases, such work can be very time-consuming and error prone, and that is the key disadvantage of programming in assembly language.
To demonstrate this, I have create a simple program to print a text string via Universal Asynchronous Receiver/Transmitter (UART). Although the main program code is fairly short, the effort to create the system and clock initialization functions is significant (see project example code from book companion web site).
main.s – an assembly language program to print a “Hello” message via UART
PRESERVE8 ; Indicate the code here preserve
; 8 byte stack alignment
THUMB ; Indicate THUMB code is used
AREA |.text|, CODE, READONLY ; Start of CODE area
;--------------------------------------------------------------
EXPORT __main ; Make function visible from outside
__main FUNCTION
B main
ENDFUNC
;--------------------------------------------------------------
IMPORT SystemInit
IMPORT Config_32MHz_PLL_Clock
IMPORT UART_config
IMPORT UART_puts
main FUNCTION
BL SystemInit
BL Config_32MHz_PLL_Clock
BL UART_config
LDR r0,=HELLO_TEXT
BL UART_puts
B . ; while(1)
ENDFUNC
;--------------------------------------------------------------
LTORG ; Literal data
HELLO_TEXT DCB "Hello
", 0 ; Null terminated string
ALIGN 4
;--------------------------------------------------------------
END ; End of file
It is possible to pull in some of the C program codes for SystemInit() and peripheral control functions we have already prepared for C/C++ projects. However, since the C/C++ code will require CMSIS-CORE header files, so you will also need to add CMSIS-CORE header files, and might end up better off with creating the project in a C/C++ environment.
21.7.3. Additional Text Output Functions
In many case we need to display values, either it is UART or LCD, we still need some functions to convert the binary numbers into strings of characters so that the information is represent in a readable form. In the last example, we create a simple string printing function call UART_puts:
; Input R0 - starting address of text string. Null terminated
EXPORT UART_puts
UART_puts FUNCTION
PUSH {R4, LR}
MOV R4, R0
UART_puts_loop
LDRB R0, [R4]
CMP R0, #0
BEQ UART_puts_end
BL UART_putc
ADDS R4, R4, #1
B UART_puts_loop
UART_puts_end
POP {R4, PC}
ENDFUNC
To make the collection of functions more complete, functions for outputting values in hexadecimal and decimal formats are added.
A function call UART_put_Hex is developed to send hexadecimal numbers. This function calls the UART_putc function, which outputs single ASCII character each time it is called.
; Input R0 - value to be converted and output via UART
EXPORT UART_put_Hex
UART_put_Hex FUNCTION
; Output register value in hexadecimal format
; Input R0 = value to be displayed
PUSH {R0, R4-R7, LR} ; Save registers to stack
MOV R4, R0 ; Save register value to R3 because R0 is used
; for passing input parameter
MOVS R0,#'0' ; Starting the display with "0x"
BL UART_putc
MOVS R0,#'x'
BL UART_putc
MOVS R5, #8 ; Set loop counter
MOVS R6, #28 ; Rotate offset
MOVS R7, #0xF ; AND mask
UART_put_Hex_loop
RORS R4, R6 ; Rotate data value left by 4 bits(right 28)
MOV R0, R4 ; Copy to R0
ANDS R0, R7 ; Extract the lowest 4 bit
CMP R0, #0xA ; Convert to ASCII
BLT UART_put_Hex_Char0to9
ADDS R0, #7 ; If larger or equal 10, then convert to A-F
; (R0=R0+7+48)
UART_put_Hex_Char0to9
ADDS R0, #48 ; otherwise convert to 0-9
BL UART_putc ; Output 1 hex character
SUBS R5, #1 ; decrement loop counter
BNE UART_put_Hex_loop ; if all 8 hexadecimal characters been displayed
POP {R0, R4-R7, PC} ; then return, otherwise process next 4-bit
ENDFUNC
A function called UartPutDec for outputting decimal numbers is also created. Similar to the last function, it also uses the UART_putc function. An array of constant values (refer as masks in the program code) are used in the function to speed up the conversion of the value to a decimal string.
; Input R0 - value to be converted and output via UART
EXPORT UART_put_Dec
UART_put_Dec FUNCTION
; Output register value in decimal format
; Input R0 = value to be displayed
; For 32-bit value, the maximum number of digits is 10
PUSH {R4-R6, LR} ; Save register values
MOV R4, R0 ; Copy input value to R4 because R0 is
; used for character output
ADR R6, UART_put_Dec_Const ; Starting address of mask array
UART_put_Dec_CompareLoop1 ; compare until input value is same or
; larger than the current mask (…/100/10/1)
LDR R5, [R6] ; Get Mask value
CMP R4, R5 ; Compare input value to mask value
BHS UART_put_Dec_Stage2 ; Value is same or larger than current mask
ADDS R6, #4 ; Next smaller mask address
CMP R4, #10 ; Check for zero to 9
BLO UART_put_Dec_SmallNumber0to9
B UART_put_Dec_CompareLoop1
UART_put_Dec_Stage2
MOVS R0, #0 ; Initial value for current digit
UART_put_Dec_Loop2
CMP R4, R5 ; Compare to mask value
BLO UART_put_Dec_Loop2_exit
SUBS R4, R5 ; Subtract mask value
ADDS R0, #1 ; increment current digit
B UART_put_Dec_Loop2
UART_put_Dec_Loop2_exit
ADDS R0, #48 ; convert to ascii 0-9
BL UART_putc ; Output 1 character
ADDS R6, #4 ; Next smaller mask address
LDR R5,[R6] ; Get Mask value
CMP R5, #1 ; Last Mask
BEQ UART_put_Dec_SmallNumber0to9
B UART_put_Dec_Stage2
UART_put_Dec_SmallNumber0to9 ; Remaining value in R4 is from 0 to 9
ADDS R4, #48 ; convert to ascii 0-9
MOV R0, R4 ; Copy to R0 for display
BL UART_putc ; Output 1 character
POP {R4-R6, PC} ; Restore registers and return
ALIGN 4
UART_put_Dec_Const ; array of mask values for conversion
DCD 1000000000
DCD 100000000
DCD 10000000
DCD 1000000
DCD 100000
DCD 10000
DCD 1000
DCD 100
DCD 10
DCD 1
ALIGN
ENDFUNC
Using these functions, it is fairly easy to transfer information between your targeted systems to a different system via UART interface, e.g., a personal computer running a terminal program or customize the code to output information to a display to help software development, or as a user interface.