Often, you will need to define large structures and arrays in your program, and even though your device might appear to have adequate data memory to hold them, there are restrictions imposed by the 8-bit Microchip PIC® architectures that affect the placement and access of large objects in your programs. To ensure you get the best performance out of your programs, it is worth understanding these restrictions and the memory allocation schemes used by the MPLAB® XC8 Compiler to place large objects in memory.
Issues Associated with Large Objects
There are two peculiarities of the classic 8-bit PIC memory layouts that affect the maximum size and placement of your global objects (those objects that have permanent storage duration): data banks, and the location of Special Function Registers (SFRs) and common memory, both of which fragment the memory space. There are, however, features in the PIC18 and Enhanced Mid-range device architectures that permit the MPLAB XC8 compiler to work around these peculiarities and allow you to define objects that can be located in more than one bank.
As the data memory on 8-bit PIC devices is banked, and file register instructions (which access data memory) can only specify an offset into the currently selected bank, the compiler might need to output bank-selection instructions prior to accessing any object. If multi-byte objects were allowed to straddle a bank boundary, the compiler would have to produce more than one bank selection instruction to access all the object's bytes, and if it was not known where the boundary was located, which could easily be the case, then a bank selection instruction would be necessary before accessing each byte. To keep the number of generated bank selection instructions to a minimum, the compiler usually requires that the sections used to hold objects are located wholly within a bank.
In the following example, stylised code that increments a 4-byte integer is shown for three cases: where the integer is wholly contained in bank 2, where the integer straddles banks 2 and 3 (and the boundary occurs at a known location), and where the integer straddles banks 2 and 3 (but the boundary location is not known). The code to perform this operation increases in size as the compiler has to output more bank selection instructions.
No bank boundary | Known bank boundary | Unknown bank boundary |
---|---|---|
select bank 2 increment object move 0 to wreg add w with carry to object+1 add w with carry to object+2 add w with carry to object+3 |
select bank 2 increment object move 0 to wreg add w with carry to object+1 select bank 3 add w with carry to object+2 add w with carry to object+3 |
select bank of object increment object move 0 to wreg select bank of object+1 add w with carry to object+1 select bank of object+2 add w with carry to object+2 select bank of object+3 add w with carry to object+3 |
Some devices face another complication in that there are gaps in their General Purpose RAM (GPR). This is not an issue for PIC18 devices, but for other parts, the location of the SFRs and the common (unbanked) memory is such that it breaks up the GPR into smaller discrete areas of memory that are not contiguous.
In the following figure, the green GPR memory shown on the left occurs in discrete blocks, separated by the SFRs (shown in red) and common memory (yellow). The address of the first GPR location in a bank is not one higher than the address of the last GPR location in the previous bank, due to this arrangement.
The C language requires that each byte of an object must follow immediately after the other in memory, which is to say that there cannot be gaps in the object’s memory. When accessing array elements, too, the compiler accesses memory at an offset from the array’s base address, where that offset is calculated from the index specified. That offset calculation does not allow for there being gaps in the object, so there cannot be gaps between array elements. (The compiler may insert gaps between structure members to word align each member if required.) To avoid these gaps, the compiler again ensures that the sections that hold the objects are placed wholly within the GPR area in one bank.
If you need to define a large object (such as an array or structure) whose size exceeds the amount of GPR available in one data bank, this creates a problem, as the compiler's ordinary sections can never hold such a large object. Projects targeting Baseline or (non-enhanced) Mid-range devices are unable to define objects so large. For the other devices, a compiler solution can be used but the nature of this workaround differs, depending on the device.
The PIC18 Solution
PIC18 devices do not have GPR segregated by SFRs or common memory, but they still use banked data memory. To allow large objects with PIC18 devices, the compiler does two things. It first allocates large objects to special sections that are allowed to cross-bank boundaries. Next, whenever these objects need to be accessed, the compiler outputs code that makes no assumptions about which bank the object might live in.
There are several instructions and compiler features that allow unbanked access to these objects. The first is unbanked instructions, such as movff or movffl, which can access data memory using a full address (not just an offset into the currently selected bank). Alternatively, the compiler can use the File Select Register (FSR) to indirectly access these objects. The FSR register also holds a full address and so acts independently of the device's memory banks.
The Enhanced Mid-range Solution
As well as banking issues, address fragmentation of the GPR must be considered when building for Enhanced Mid-range devices. As with PIC18 devices, the compiler places large objects into special sections that are not limited to the size of the GPR in each bank; however, with these devices, the compiler places these sections in a special address space, called linear data memory.
Linear data memory is implemented by all Enhanced Mid-range devices and avoids the issue of memory gaps by having a linear address space available alongside the normal data memory addresses. This space is not additional memory, rather it is a unique range of contiguous addresses that map back to the device’s banked memory. Importantly, the memory used by the SFRs and common memory is not part of the linear memory, so that each GPR address in banked memory can be accessed by a contiguous range of linear addresses.
In the diagram shown earlier, as seen on the right, the discrete blocks of GPR can be accessed as one contiguous block of memory in the linear address space for those devices that support this feature.
Linear addresses are used by the FSR registers. When accessing memory via an FSR, the device checks the address the FSR holds. If it is in the linear memory space, it is first converted to the corresponding address of banked data memory, and then normal indirect access takes place. The compiler always uses linear addresses with any large object, thus large objects are always accessed indirectly via an FSR pointer register.
Consequences of Large Objects
Regardless of the device and particularly in complex expressions, the code to access large objects is typically larger and slower than the code used to access smaller objects that are assumed to fit entirely within a single bank. For this reason, the compiler will only allocate objects to the large memory sections if their size exceeds a threshold, based on the size of the GPR present in the device's data banks. This threshold, however, can create an interesting phenomenon.
Typically, as a small object is made larger in size, it becomes more and more difficult to place in memory, as there are fewer positions available for it to use without it crossing a bank boundary. However, once the object’s size reaches the large object threshold, it will be placed in sections that ignore bank boundaries, and the compiler could have less difficulty finding it a suitable free location. It is not uncommon to see memory errors for programs that define relatively large objects, but that the errors disappear if the total size of the objects to be positioned increases.
In the diagram below, in the left box, five objects are to be placed in memory on an Enhanced Mid-range device. It is clear from their sizes that they will not fit wholly within any data bank, and a can't find space memory error will result. In the right box, the total size of the four objects to be placed is larger than that of the five shown on the left. Even so, as some objects are now above the large object threshold, they have been allocated to linear memory and the other objects are placed around them in banked memory with no errors. The far right of the diagram shows how the objects appear in the regular banked memory space. The large objects appear in multiple data banks but they will always be accessed using a linear address in an FSR register.
Summary
When building for PIC18 or Enhanced Mid-range devices, you may define large global objects whose size exceeds the amount of GPR memory available in a single bank. The compiler will automatically swap to a different means of storing and accessing such objects, but the code to access them might be larger and slower in complex expressions.