Variables in MAXQ assembly applications can be stored either in working registers (such as the accumulators A through A) or in data memory (SRAM). Storing variables in data memory provides a larger working area for the application variables, but does require additional access time.
The MaxQAsm assembler and MAX-IDE environment provide a mechanism to declare separate code and data segments, with a separate hex output file generated for each segment. At run time, MAX-IDE automatically loads the code segment file into program memory (typically flash) and the data segment file into data memory (typically RAM). However, as the data memory is volatile, the data segment contents will not remain intact once the microcontroller is powered off.
This application note uses the MAXQ2000 EV
(evaluation) kit to demonstrate first, how to save these preloaded data memory values in flash during the initial application run, and then how to refresh the data segment values from flash when the microcontroller is subsequently powered on again. This two-step process allows the same data segment mechanism to be used to declare and initialize variables, whether the application is in development (connected to a JTAG adapter and MAX-IDE) or running in the field.
Demonstration code for this application note is written for the MAXQ2000
microcontroller and the MAXQ2000 EV kit, but the code and principles shown are applicable to any MAXQ20-based microcontroller with rewriteable program flash memory.
The latest installation package and documentation for the MAX-IDE environment are available for free download.
Variables and Storage Locations
Embedded applications typically require a certain amount of working space to store state information, configuration settings, intermediate working values, loop counters, and the results of calculations. Values stored in this working space are typically known as variables and share the following characteristics.
- They are temporary. They do not need to be saved if the application is interrupted by a power failure or reset.
- They are accessed and updated frequently. They must be stored in locations that can be read from or written to quickly; there must be no limit on the number of times to which the locations can be written.
- They often have defined initial values. The user’s code must set them to a particular value at the beginning of the application.
In applications written in C or another high-level language and compiled into assembly code, the compiler typically handles the allocation of space for variables (as well as the process of initializing variables to predefined starting values) automatically. In this case, the user only needs to declare the variable, its type, and (optionally) its initial value. The compiler handles the rest.
unsigned int c = 0x1234;
However, when writing applications directly in MAXQ assembly language, the allocation of space for variables and setting the variables to initial values must be performed explicitly. This detailed action allows tighter control of the resources available on the MAXQ microcontroller, but adds some complexity to the system.
For small assembly-based applications or those which do not require a large amount of working space, internal registers can be used to store all application variables. This approach provides two important advantages:
- Compact, fast code. Register variables can be read from, written to, or copied to another register variable in as little as one instruction cycle, depending on the location of the register. On MAXQ20-based microcontrollers, no more than two instruction cycles will generally be required in the worst case.
- Direct operations on variables. Some internal register locations can be operated on directly. For example, any of the 16 working accumulators, A through A, can be selected (using the AP register) as the active accumulator, Acc. This means that if an operation needs to be performed on a variable stored in one of these registers, it can be performed directly on that register without having to copy the value out, perform the operation, and copy the value back in. Similarly, variables stored in the LC and LC registers can be used directly as loop counters by executing the djnz instruction.
A larger application, or an application requiring a larger number of working variables, can benefit from storing some or all of its variables in SRAM-based data memory. This method allows a much larger number of variables to be created, up to the limits imposed by the size of the data memory. Variables stored in this manner are accessed using one of the MAXQ20 core's standard data pointers, which can be used to read and write byte-sized or word-sized variables. (Note: all code examples in this application note assume that DP is configured to operate in word mode.)
move DP, #0010h ; Location of variable in data memory
move Acc, @DP ; Read variable
add #1 ; Increment variable value by 1
move @DP, Acc ; Store variable back in data memory
If a long series of calculations must be performed on a variable, the value of that variable can be copied into a working register, as shown in the example code above. All intermediate operations can be performed using that working register, and the value can be copied back out to the variable once calculations are complete.
Segment Declarations in MAX-IDE
Once the decision is made to store application variables in SRAM-based data memory, how do you determine where to store the variables?
Typically, all of the data memory is available for application use, except for the highest 32 bytes in memory which are used by the debugger. This means that declaring a variable is simply a matter of defining a location for it in data memory. This location is then used by code whenever the variable is read or written. The #define
macro can be used to associate a variable location with a symbolic name.
#define VarA #0020h
#define VarB #0021h
#define VarC #0022h
move DP, VarA ; Point to VarA variable
move Acc, @DP ; Read value of variable
move DP, VarB ; Point to VarB variable
move @DP, Acc ; Copy VarA to VarB
move DP, VarC ; Point to VarC variable
move @DP, #1234h ; Set VarC = 1234h
This approach works well enough, but there are several problems with it.
- The location of each variable must be determined in advance. This task can be time-consuming, especially if it is decided later to move all variables to a different area of data memory.
- Care must be taken not to accidentally use the same location for more than one variable. If this mistake is made, it can be difficult to track bugs.
- Any initial (starting) values for variables must be explicitly loaded by application code, as shown in the last line above. This action can consume a large amount of code space if there are many variables to initialize in this manner.
A more efficient approach takes advantage of MAX-IDE's mechanism to declare separate code and data segments. This method allows the application author to specify which portions of the assembly code file are destined for code space and which portions are destined for data space.
move DP, #VarA ; Point to VarA
move Acc, @DP ; Get current value of VarA
add #1 ; Increment it
move @DP, Acc ; Store value back in VarA
dw 0394h ; Initial value for VarA
In the above approach, addresses for variables declared in the data segment are determined automatically by the assembler as it parses the file with the same method used to assign addresses to labels in code space. Labels are used to assign symbolic names to these variable addresses, and the dw
statements can be used to initialize word-sized and byte-sized variables with starting values. In this case, assuming that no previous segment data
directive was found in the assembly file, the assembler will begin the data segment at address 0000h. This means that VarA
will be stored at word address 0000h. As in code space, the org
statement can force variables to be located at the beginning of a specified address.
Initializing the Data Segment
In the previous code listing, variable VarA
is defined (using the dw
statement) to have an initial value of 0394h. But this value is never loaded into VarA
in the code. How, then, is this value initialized? The answer is that the initialization of the data segment is performed automatically by MAX-IDE when the project is compiled and executed.
The MaxQAsm assembler responds to the segment data
directive by generating a secondary hex output file. Normally, a hex file is generated for a project containing code data. For example, if project "example.prj" is compiled, a hex file will be created named "example.hex" containing the code data generated by assembling the project files. If a data segment is defined, an additional hex file will be created named "example_d.hex", which contains the data assembled in this segment.
When the project is executed, MAX-IDE checks to see if a data segment file (ending in _d.hex) was generated during project compilation. If a data segment file exists, MAX-IDE uses the standard JTAG loader to load the data from this segment into the data SRAM of the device. This is done after the standard hex file has been loaded into program memory.
This method works well during the development cycle, when the device is connected to a JTAG adapter and MAX-IDE reloads the code and segment data before each application run. However, once the device is powered off and on and allowed to run independently (with no debugger connected), MAX-IDE no longer has the ability to load the data segment with the proper values before each run. The variables will no longer be set to their expected values, and the application may execute incorrectly as a result. This type of failure can be difficult to analyze, because as soon as the device is hooked back up to the debugger, MAX-IDE will begin loading the data segment again before each run and the problem will instantly vanish.
Saving and Restoring the Data Segment
A question remains: how can you make the application operate consistently, whether it is connected to a debugger (with MAX-IDE reloading code and data before each run) or free-running (with no particular contents in RAM guaranteed following power-up). The obvious solution is a two-step process: have the application save the variable values (once they have been initialized) in flash memory, and restore the values following each reset or power-up.
As a first step, the application must save values to flash memory. This action occurs the first time that the application is executed following each master erase and code load cycle.
- The application checks a "flag" location to verify that the variables have not been previously copied to flash. This flag can be a special-purpose, nonvariable location, or it can be shared with a variable as long as that variable has a nonzero initial value (to distinguish it from a blank RAM location).
- The application copies each variable value from data RAM to flash memory. On most MAXQ microcontrollers with rewriteable flash (such as the MAXQ2000) this is done using the UROM_flashWrite function.
- The application writes a flag in flash memory to indicate that the variables have been stored.
As the secondly step, on subsequent runs the application must restore the variable values from flash memory to their expected locations in data RAM.
- The application checks the flag location in flash to verify that the variable values have been stored.
- The application uses the UROM_copyBuffer routine to copy the variable values from flash memory to their proper locations in data RAM.
The code listing below demonstrates this saving-restoring method with the MAXQ2000 EV kit. In this code, variable values are stored in flash memory at addresses 7000h–71FFh.
;; Code memory (flash) : 0000h-7FFFh (word addr)
;; Data memory (RAM) : 0000h-03FFh (word addr)
ljump start ; Skip over password area
move DPC, #1Ch ; Set all pointers to word mode
move DP, #0F000h ; Check first variable value (flag)
lcall UROM_moveDP0 ; 'move GR, @DP' executed by Utility ROM
move Acc, GR
jump NE, copyToFlash
;; This is the "free-running" code, executed on subsequent power-ups, that copies
;; values from the flash back into their proper data segment locations.
move DP, #0F000h ; Source: Flash location 7000h
move BP, #0 ; Dest: Start of RAM
move Offs, #0
move LC, #100h ; Copy 256 words
;; This is the first-pass code. A bit of a trick here; because MAX-IDE enters
;; and exits the loader separately when loading the code and data segment files,
;; the application is allowed to execute briefly before the data segment file
;; has been loaded. The first four lines under copyFlash ensure that the
;; application waits for MAX-IDE to load the data segment file before continuing.
move DP, #0h ; Wait for flag variable to be loaded by MAX-IDE.
move Acc, @DP ; Note that this will reset the application; the
cmp #1234h ; data segment is not loaded while the application
jump NE, copyToFlash ; is still running.
move DP, #0 ; Start of RAM variable area
move A, #7000h ; Location in flash to write to
move LC, #100h ; Store 256 words in flash 7000h-70FFh
move DP, DP ; Refresh the data pointer to read values correctly,
; because calling UROM_flashWrite changes memory
; contexts and affects the cached @DP value
move A, A ; Location to write
move A, @DP++ ; Value to write (taken from RAM)
move Acc, A
move A, Acc
djnz LC, copyToFlash_loop
move PD0, #0FFh ; Set all port 0 pins to output
move PO0, #000h ; Drive all port 0 pins low (LEDs off)
move DPC, #1Ch ; Set pointers to word mode
move DP, #varA
move Acc, @DP
cmp #1234h ; Verify that the variable is set correctly
jump NE, fail
move PO0, #55h
The code and data segment facility provided by MAX-IDE provides a way to automatically declare variable locations in data memory and initialize those variables with starting values. Application code can then be used to cache those variable values in flash memory and restore them as needed. This approach allows an assembly-based application to take advantage of the data segment autoloading provided by MAX-IDE, while still operating consistently whether or not the microcontroller is connected to a JTAG debugger.