-
Notifications
You must be signed in to change notification settings - Fork 1
8051 Code Banking
The Flash size on SoCs supported by Contiki is either 256KB (cc253x) or 128KB (Sensinode). As discussed in the Understanding 8051 Memory Spaces guide, the flash is mapped to the CODE memory space, which is 16 bits wide and can therefore only address up to 64KB.
When our firmware's code footprint is lower than 64KB, banking is irrelevant, since the entire program memory can be addressed by the CODE memory space without any tricks. When the footprint exceeds this 64KB threshold, in order to address the entire flash, the 8051 uses a technique called Code Banking.
This introductory page aims to describe the basics and only focuses on SoCs supported by Contiki. It is not intended as a replacement for the SDCC manual nor the SoC datasheets. Please see the reference list.
The flash is conceptually broken down into a number of segments called banks. The number of banks varies between devices but the technique is the same. Each bank is of a 32KB size, thus the cc2430 has 4 banks and the cc2530 has 8.
Since the size of the flash is over 64KB, we use 3 bytes to address all of it. So for instance on the cc2530 the physical addresses will be from 0x000000
to 0x03FFFF
The lower 32KB of the flash (physical addresses 0x000000
to 0x007FFF
), also called the common segment (or HOME or BANK0) is always addressed by the lower 32KB of the CODE memory space (address range 0x0000 - 0x7FFF
).
Note how the MSB of the physical address does not appear in this virtual address.
The higher 32KB of the CODE space (0x8000 - 0xFFFF
) are used to address the remaining N-1 code segments. Only one segment is referenced at each given point in time. A Special Function Register is used to tell the micro which bank is currently being referenced (this SFR is often called PSBANK
or FMAP
).
This table shows the mapping between physical addresses (on flash), the address in the CODE memory space and the value of the FMAP
SFR. See how the virtual address (the address as SDCC defines it), is made up of the value of FMAP
(MSB), followed by the address in CODE.
Segment | Physical Address | Virtual Address | FMAP | Address in CODE |
---|---|---|---|---|
HOME | 0x000000 - 0x007FFF | 0x000000 - 0x007FFF | 0x00 | 0x0000 - 0x7FFF |
BANK1 | 0x008000 - 0x00FFFF | 0x018000 - 0x01FFFF | 0x01 | 0x8000 - 0xFFFF |
BANK2 | 0x010000 - 0x017FFF | 0x028000 - 0x02FFFF | 0x02 | 0x8000 - 0xFFFF |
BANK3 | 0x018000 - 0x01FFFF | 0x038000 - 0x03FFFF | 0x03 | 0x8000 - 0xFFFF |
... | ||||
BANK7 | 0x038000 - 0x03FFFF | 0x078000 - 0x07FFFF | 0x07 | 0x8000 - 0xFFFF |
So far it sounds simple. However, there is a problem when code in bank X tries to call code in bank Y, if both X and Y are switched segments. If we make the call before we switch banks, we will end up calling code in the wrong flash area. If we switch to the correct bank before the call, we will no longer be able to make the call since the caller will have been switched out.
In order to solve this, SDCC uses a technique called a trampoline. Banked function calls go through a small piece of intermediate code which resides in the common segment. In simple terms, the story goes like this:
- The caller:
- Writes the address of the intended callee, including bank number, in registers R0, R1 and R2
- Invokes the trampoline (banked call)
- The trampoline:
- Saves the current bank on stack
- Loads the new bank
- Makes the call
- The callee:
- Runs (and possibly makes more banked calls)
- Returns to the trampoline (banked return)
- The trampoline:
- Loads the original bank (which is read from stack)
- Returns to the caller
It should be obvious from the above that banked calls place some burden on stack and impose some code size and processing overhead. However, banked calls are not always necessary. For example, we don't need a trampoline when the callee is in the common segment.
As discussed in the Understanding 8051 Memory Spaces guide, SDCC builds software using one of four possible memory models (Small, Medium, Large, Huge).
With Small, Medium or Large, the developer has to request banked calls explicitly in the code.
- A function prototype must have the
__banked
keyword so that the calling code will invoke the trampoline instead of making a normal call. - The actual routine must also have the keyword
__banked
so that the function itself knows to return to the trampoline.
void foo() __banked; void bar() __banked { /* This becomes a banked call because foo()'s prototype has the __banked keyword */ foo(); /* When we reach here, bar does a banked return, because it's own definition has the __banked keyword */ }
With the huge memory model, all function calls/returns invoke the trampoline. Example again:
void foo(); void bar() { /* This automatically becomes a banked call */ foo(); /* This automatically becomes a banked return */ }
Obviously this increases our code footprint a fair bit.
This is an undocumented feature in SDCC - Use at your own risk
In order to decrease code size and increase performance, the developer can use the __nonbanked
keyword to prevent SDCC from emitting banked calls/returns. Basically, __nonbanked
in --model-huge
does the inverse that __banked
does in the other models.
Be careful though, the following rules apply:
- It is safe to use
__nonbanked
for functions guaranteed to be in the common segment - For functions which reside in switched segments, it is safe to use
__nonbanked
if all calls to the function are performed by code residing in the same segment.
When writing banked software with SDCC, the developer must specify segments for each code module. This can be done with a #pragma
directive or from the command line. The main thing to stress here is that the linker is oblivious and does not check for bank overflows. If you allocate too many files in the same segment and its resulting code size is over 32 KB you will end up with a broken image.
For both platforms of interest, the contiki build system is banking-aware. Have a look in the various Makefiles in examples/cc2530dk
or examples/sensinode
. Some of the have the following line:
HAVE_BANKING=1
The HAVE_BANKING
instructs the build system whether to generate a bankable image or not.
- When
HAVE_BANKING
is undefined, the build system performs some guesswork: If we are building an image with uIPv6 support,HAVE_BANKING
is assigned the value of 1, otherwise it becomes 0. - When banking is requested, the system will automatically build contiki with the huge memory model
- When banking is not requested, the build uses the large model
The very last output of your build will look something like that:
Report =============== Code footprint: Area Addr Size Decimal ---------------------------------- -------- -------- -------- HOME,CSEG,CONST,XINIT,GS* 00000000 00007FF5 = 32757. bytes (REL,CON,CODE) BANK1 00018000 00007FFC = 32764. bytes (REL,CON,CODE) BANK2 00028000 00007FF9 = 32761. bytes (REL,CON,CODE) BANK3 00038000 0000472C = 18220. bytes (REL,CON,CODE) Other memory: Name Start End Size Max ---------------- -------- -------- -------- -------- PAGED EXT. RAM 0 256 EXTERNAL RAM 0x0000 0x14a9 5290 7936 ROM/EPROM/FLASH 0x0000 0x3c72b 116502 262144
This makes it possible to spot BANK overflows and other problems at a glance:
- The value listed under
Decimal
for all code segments must be lower than 32768. - For Sensinode devices, the value for
BANK3
must be lower than 30720.
Both ports use an automatic bank allocator. When building images with SDCC banking, the allocator will automatically meet the segment size requirements discussed in the previous section. The only thing you have to make sure is that if you write a new interrupt service routine, you must tell the allocator that it must reside in the HOME bank. There are two ways you can achieve this:
- Name your file with an
intr.c
suffix. For examplefoo_intr.c
orbar-intr.c
. When the allocator encounters a filename ending inintr.c
, it will automatically assign the file to the HOME bank. - If you want to give a different name to your file (e.g.
foo.c
) then add a line to thesegment.rules
file in your CPU dir. The line should be:HOME foo.c
Towards the end of the build, when the allocator runs, it will throw the following output (numbers will obviously vary between builds):
Bank Allocation =============== python ../../../cpu/cc253x/bank-alloc.py client obj_cc2530dk/segment.rules Total Size = 116502 bytes (101779 bankable, 2664 user-allocated, 12059 const+libs) Preallocations: HOME=14723 Bin-Packing results (target allocation): Segment - max - alloc HOME 32768 32757 BANK1 32768 32764 BANK2 32768 32761 BANK3 32768 18220
The line starting with Preallocations
lists the number of bytes that can not or may not be moved around. For example, standard libraries and files containing ISRs must reside in the HOME segment.
The lines following Segment - max - alloc
are of high importance. The last column will list how many bytes should be in each segment after the build is finished. The values listed here MUST be identical to the values reported under Decimal
at the end of the build output. Compare the target allocation listed above with the final segment sizes at the start of this section and see how the respective values are exact matches.
If you spot any differences, try a make clean; make
cycle. If the differences persist, you may be looking at a bug in the allocator. Please contact me with details.
- The SDCC manual. Look for the section entitled 'Bankswitching'.
- Section 2.2.2 'CPU Memory Space' of the 'CC253x/4x User Guide (Rev. C)' (Literature Number: SWRU191C April 2009–Revised January 2012)
- Section 11.2.2 'CPU Memory Space' of the 'CC2430 Data Sheet (rev. 2.1)' (Literature Number: SWRS036F)