5. Data storage

This chapter discusses controlling data placement and its impact on application efficiency.

The 65816 has an address range of 16MB code and data space. Even though the address space 16MB, it can be seen as a sequence of 64K address ranges. While address calculations can cross 64K bank boundaries, this is not the most efficient way to utilize the 65816. Additionally, a single function must fit within a single 64K bank.

5.1. 64K banks

There are 256 such 64K banks on the 65816 numbered 00 to FF hexadecimal. The first bank 00 is special in that the system stack and the direct page must be located in it. Certain vectors, such as the reset and interrupt vectors also reside in bank 00.

Note

Do not confuse the term bank here with a banked memory system where it often is used to refer to an address range which acts as a window to a larger memory area. A bank in the 65816 refers to a 64K memory area.

Direct page

The direct page is 256 byte large and can be located anywhere in the first 64K of memory. The compiler reserves 32 bytes from the direct page for pseudo registers, forming the tiny area which uses the tiny attribute. The 65816 has several efficient addressing modes related to this area, providing shorter instructions compared to other areas.

The DP (direct page) register is 16 bits and points to the start of the direct page. This register is set up before the main() function is called and is expected to remain fixed while the application is running.

Near bank

The near bank is the bank pointed to by the data bank register. This is a single 64K bank that is in the middle of addressing efficiency.

The DB or data bank register is an 8 bit register that points to the active near bank. This register is set up before the main() function is called and is expected to remain fixed for the duration of the execution.

In the Small data model the near bank must be the same bank as the CPU stack, which is always in bank 00. This is because the default pointer is 16 bits wide in the Small data model and it must be possible to point to a data object either on the stack or the default static storage area.

5.2. Ways to store data

Data can be allocated as auto, static, or on the heap. Variable scope and the need for allocation during runtime determine placement.

As a general rule, use auto-allocated variables whenever possible. This offers the compiler maximum flexibility for resource allocation closest to the core, which typically results in the most efficient data access.

Auto variables

Auto variables include function parameters and local variables not defined with static. The compiler attempts to allocate these in processor registers or direct page pseudo registers; otherwise, the stack is used.

Auto variables are allocated only when used. Their registers can be reused for other auto variables or temporary data once no longer needed. This reuse also applies to stack locations.

Auto variables can have multiple live ranges; a variable with the same name might be used in distinct parts of a function. Internally, these are treated as different variables and may be allocated to various locations, potentially not existing between live ranges.

All auto variables associated with a function are deallocated upon function exit.

Note

If you take the address of an auto variable, you can pass its pointer to other functions, which is useful for temporary storage. However, avoid using such pointers outside their scope, as auto variables are deallocated on function exit. Be aware that taking an address allocates the variable on the stack, potentially increasing access cost compared to other auto variables.

Static variables

Global, module, or function static variables are allocated in global memory, occupying space for the application’s duration.

Their visibility varies. A global variable is universally visible but requires extern for use. A module static variable, declared with static at file scope, is visible within one compilation unit. A static local variable within a function’s scope is only visible there, retaining its value across function calls. Use static to differentiate it from an auto variable.

Dynamically allocated

A dynamically allocated variable is retrieved from a heap using the malloc function. This is useful when the required data size is unknown at program startup.

Note

Dynamically allocated variables are a potential problem in memory constrained systems if the program is left running for a long time due to heap fragmentation.

5.3. Address spaces

The compiler provides multiple address spaces, which are addressable memory areas with specific properties:

  • Address width for pointers

  • Width of the associated integral index type

  • Different instruction sequences for accessing various address spaces

  • An extension keyword or type attribute name

  • Section names tied to the address space for linking control

Address space attributes are always active in the compiler.

The Calypsi C compiler tool chain provides five address spaces, tiny (direct page), near (64K), far, far24 and huge. The latter three can address the full 16MB range where far and far24 limit object size to 64K minus one.

All unqualified pointers are either 16 or 24 bits wide, depending on the selected data model.

Tiny address space

The tiny address space is 256 bytes of memory located somewhere in the first 64K of memory. It has an address range 0x00-0xff. This corresponds to the direct page on the 65816.

A tiny pointer is 16 bits wide and occupies two bytes when stored in memory. While a single byte would suffice, the compiler uses two bytes for efficiency.

Due to its 256-byte size and sharing with pseudo registers, the tiny area has limited storage. However, careful use can lead to a more efficient application.

Near address space

The near address space is a single 64K bank. This address space is not available in the Small data model. The bank can be located to any 64K bank and is fixed to that bank.

A near memory pointer is 16 bits and occupies two bytes when stored in memory.

Far address space

The far address space covers the entire 16MB address range. The only limitation on far objects is that they must be at most 64K (minus one byte) large.

A far memory pointer is 24 bits and occupies four bytes when stored in memory.

Far24 address space

The far24 address space cover the entire 16MB address range. The only limitation on far24 objects are that they must be at most 64K (minus one byte) large.

A far24 memory pointer is 24 bits and occupies three bytes when stored in memory.

Note

The only difference between far24 and far is that far24 is stored in memory without 32 bits padding. This is useful when hardware or API definitions require unpadded pointer storage. The generated code to store a 24 bits pointer is somewhat larger compared to a 32 bits pointer on the 65816.

Huge address space

The huge address space covers the entire 16MB address range. The maximum size of an object is 16MB (minus one byte).

A huge memory pointer is 24 bits and occupies four bytes when stored in memory.

Summary

The following table summarizes the available address spaces.

Table 5.1 Address spaces

Memory type

Keyword

Address range

Pointer size

Index type

Direct page

__tiny

0x00-0xff

16 bits

int8_t

Near

__near

0x0000-0xffff

16 bits

int16_t

Far

__far

0x000000-0xffffff

32 bits

int16_t

Far24

__far24

0x000000-0xffffff

24 bits

int16_t

Huge

__huge

0x000000-0xffffff

32 bits

int32_t

Syntax

An address space attribute keyword such as __tiny is a type qualifier. Syntactically it works the same as other C language defined type qualifier, e.g. const and volatile.

The following declaration defines four variables in the Direct page address space:

__tiny int a, b;
int __tiny c, d;

The __tiny type qualifier applies to the closest type, int in this example. In C, the order between type qualifiers and types does not matter; they convey the same meaning.

Pointers

A pointer in C points to something in memory. Both the pointer itself and what it points to have types. As an example, the type char * is a pointer to a char.

Pointer types are easier to understand if you read them from right to left. The * is a pointer, so char * reads from right to left as “pointer to char”. This order of reading is especially useful when you mix in type qualifiers in pointer types, as it makes it a lot easier to read and understand what the type means.

int __attribute__((tiny)) * p1;
long * __attribute__((tiny)) p2;

Here, p1 is a pointer stored in default memory that points to an int in Direct page memory. p2 is a pointer stored in Direct page memory that points to a long in default memory.

Structures

You can place a structure in a specified address space. This means that all its members are in that address space. You cannot override individual structure members using an address space keyword. It is however possible to have members of the structure that point to a different address space.

struct tag {
  int __tiny * p;
  int value;
};

struct tag __tiny myTag;

This is however not allowed:

struct tag {
  int * __tiny p;     /* incorrect */
  int __tiny value;   /* incorrect */
};