Multiple Source Files in Assembly

Using Multiple Files to Modularize Code

252

Introduction

As projects get larger and more complex, the importance of keeping a well-organized and well documented code base increases, and at some point it become imperative. Writing code that is well commented, easy to understand, and easy to modify makes it far easier to maintain and reuse your work. And in fact, most companies require certain basic organizational design practices.

In general, good software design practices include using smaller, well-documented blocks of code organized on sensible functional boundaries, using subroutines within a given source file to handle basic or repetitive functions (like polling a timer), and placing certain larger functions in separate source files. (But note! For certain time critical functions, the use of subroutines may not be warranted, because of the extra time taken to branch and swap contexts.)

When writing code to manage peripheral circuits or implement certain functions, consider defining “standard” interfaces that include all relevant parameters, even if they aren’t needed for the current/particular solution. For example, when designing software to generate a timing signal, consider an interface that makes the entire range of potential signal frequencies available via a parameter or two, even if the current solution needs only one particular frequency. By creating generic solutions while you are focused on a given task, you can create code that is easier to maintain and reuse, and avoid having to retrace your steps to add functions later.

Also consider grouping related subroutines together into a single source file. For example, all subroutines needed to access the GPIO devices on a given hardware platform (like the buttons, switches, and LEDs on the Blackboard) might be grouped into a single source file named “gpio.s”, and all assembly subroutines needed to access a hardware timer could be placed in a file called “timer.s”.

Before your assembly source files can be executed on the ARM, they must be processed by the Assembler. The Assembler creates a “relocatable object file” from your source code - that is, it replaces the mnemonics in your source file with their machine code counterparts, and then writes the list of machine codes to a file that can be placed anywhere in the ARM memory map. Part of the assembly process is replacing references to labels or symbols (like BNE label1”) with immediate operands that define exactly where in memory the label is located. If the label is in the same source file, the assembler has all the needed information. But if a source file references a label in a different source file, the assembler can’t resolve the memory address by itself. That’s where a tool called the “linker” comes in – its job is to combine related object files together into one executable image, and to make sure that all external label references are properly resolved.

Any label in any source file can be make visible outside the source file by identifying it with the .global directive. Labels identified with the .global directive are visible to the linker, and the linker can then associate those labels with actual addresses. Note that the “main” label at the start of your main program, and the “.global main” directive at the start of your source file, is an example of creating an external label that the linker can use (in this case, the label identifies the first executable instruction - more on that later).

Using Multiple Files

The code below shows some examples of subroutines that might be useful in controlling Blackboard’s seven segment display. The subroutines are contained in a single source file that might be named something like “sevensegdisp.s” so it can easily be identified in the project explorer.

Note that all labels identifying the start of subroutines are associated with .global directives at the top of the source file. Then, in the main source file that follows (this is the file that will use the .global labels/symbols), those same symbols/labels are identified with the .extern directive. The .extern directive tells the assembler that a label referenced in a source file is not defined in the source file, and so the linker must resolve its address.

This mechanism, of using the .global directive to make symbols/labels in a given source file visible outside of the source file, and of using the .extern directive to inform the tools that a given symbol/label is not defined in the current source file, can be used to share any symbols/labels between source files, whether they identify the start of a subroutine, or a constant, or anything else.

Subroutine sources


@Declare functions as global
@This makes the labels acccessible in other functions
.global enable_disp_hex
.global disable_disp
.global clear_disp

.equ SEG_CTL,	0x43c10000
.equ SEG_DATA,	0x43c10004

@enables the 7-seg display in hex mode
enable_disp_hex:
ldr r1,=SEG_CTL
mov r0,#1
str r0,[r1]
bx lr

@disables the 7-seg display
disable_disp:
ldr r1,=SEG_CTL
mov r0,#1
str r0,[r1]
bx lr

@clears the 7-seg display
clear_disp:
ldr r1,=SEG_DATA
mov r0,#0
str r0,[r1]
bx lr



Main Program

.extern enable_disp_hex
.extern	clear_disp

@program to enable, then clear display
@then sit in infinite loop
main:
bl enable_disp_hex
bl clear_disp
b .