-
Notifications
You must be signed in to change notification settings - Fork 5
Memory Mapping
Here some information about how the various Teensies utilize the available on- and off chip memory, how you can specify which part of the memory to use and how to handle sketches with high memory demand.
Here a quick write up for those, only interested in how to generate variables, constants and functions in certain parts of the available memory:
Per default, code and constants are stored in FLASH and all variables (static, global, local and dynamic) are stored in RAM. Specifically, local variables are stored on the STACK, and dynamic variables, i.e. those created with malloc
or new
are stored in HEAP memory.
Examples:
unsigned i; // Global variable -> RAM
const float k = 5; // Constant -> FLASH
constexpr float x = 3.14; // Compile time constant usually not stored at all
char[] t1 = "Hello" // array of char variables -> RAM
const char[] t2 = "World"; // array of char constants -> FLASH
double* d; // Global variable -> RAM
void func(void) // Code -> FLASH
{
static float f; // Static variable -> RAM
int i; // local variable -> STACK part of the RAM
d = new double(2) // dynamic variable -> HEAP part of the RAM *)
//...
}
// *) the global pointer variable `d` is stored in RAM, the float
// which d points to is stored in the HEAP.
Since reading from FLASH is a bit slower than reading from RAM you might consider moving time critical functions to RAM. To do so you need to use the attribute FASTRUN before the function definition:
FASTRUN // <- move function to RAM
void func(void) // <- RAM
{
static float f; // <- RAM
double d; // <- STACK
// do something
}
The Teensy 4.x boards have two 512kB RAM banks RAM1 and RAM2.
Global, static and local variables behave like they do for the T3.x boards. All of those variables use a part of the first RAM bank by default. The part of RAM1 used for variables is called DTCM (Data Tightly Coupled Memory).
Unlike the T3.x boards the FLASH memory of the Teensy 4.x is external and significantly slower than RAM (though there is a 32KB code cache.) Therefore, the linker associates all code and constants to the ITCM (Instruction Tightly Coupled Memory) section of RAM1. Teensy startup code performs the actual move to RAM1.
char t1[] = "Hello"; // variable -> RAM1 (DTCM)
const char t2[] = "RAM"; // constant -> copied to DTCM
void func(void) // code .>copied to ITCM (RAM1)
{
char t4[] = "STACK"; // local -> RAM1 (STACK)
}
In case your application requires a lot of RAM you might consider leaving code which is not time critical or large constants in FLASH.
const char t1[] = "RAM"; // constant, but copied to RAM
PROGMEM const char t2 = "FLASH" // constant, leave in FLASH
FLASHMEM // do not copy this function to RAM
void func(void)
{
// do something not time critical
}
The T4.x boards have a second RAM block which, per default, is used for DMA buffers and the HEAP. You can also place normal variables into RAM2 by using the DMAMEM attribute.
DMAMEM int k = 5; // Place integer variable k into RAM2
byte* buf = new byte[1024]; // buf is a global 4 byte pointer variable and lives in DTCM (RAM1)
// it stores the address of a 1kB buffer in HEAP (RAM2).
Here a picture from the PJRC site (https://www.pjrc.com/store/teensy40_pins.html) showing the setup both RAM banks and the FLASH of the T4.x boards.
RAM2 is a little bit slower than RAM, and it is connected through a bus. To compensate for this disadvantage, there is a cache:
RAM2 has a 32KB cache. On startup, the cache is configured "Write Back". This means, if you store a value to RAM2, it may not be written to the RAM2 immedeately and may get held in the cache instead. DMA can only access RAM and will always use the values from RAM. For this purpose there are three functions:
-
arm_dcache_flush(void *addr, uint32_t size) - Flush data from cache to memory - Normally arm_dcache_flush() is used when metadata written to memory will be used by a DMA or a bus-master peripheral. Any data in the cache is written to memory. A copy remains in the cache, so this is typically used with special fields you will want to quickly access in the future. For data transmission, use arm_dcache_flush_delete().
-
arm_dcache_delete(void *addr, uint32_t size) - Delete data from the cache, without touching memory - Normally arm_dcache_delete() is used before receiving data via DMA or from bus-master peripherals which write to memory. You want to delete anything the cache may have stored, so your next read is certain to access the physical memory.
-
arm_dcache_flush_delete(void *addr, uint32_t size) - Flush data from cache to memory, and delete it from the cache. Normally arm_dcache_flush_delete() is used when transmitting data via DMA or bus-master peripherals which read from memory. You want any cached data written to memory, and then removed from the cache, because you no longer need to access the data after transmission.
Optionally, you can solder up to 16MB external RAM on the bottom side of a T4.1 board. If you want to place some variables in the second block (DMAMEM) or on the external RAM (EXTMEM) you can use the following code:
uint8_t buffer1[100 * 1024]; // Place a 100kB buffer in DTCM (RAM1)
DMAMEM uint8_t buffer2[300 * 1024]; // Place a 300kB buffer in the second RAM block
EXTMEM uint8_t buffer3[3* 1024 * 1024] // Place a 3MB buffer in the external RAM
If you want to have a closer look yourself, you can use the MemoryTool
which
consists of only two files MemoryTool.h
and MemoryTool.cpp
which you can copy to your sketch folder.
You can download it from here: https://github.com/luni64/TeensyHelpers.
Please note that this tool currently only works for T3.x Boards
int i; // global variable (uninitalized)
double x = 42; // global variable (initalized)
void setup(){
static int k; // static variable, stored in the same place as global variables
while (!Serial) {} // wait until the pc connected
printMemoryInfo(i); // print info about the variables
printMemoryInfo(x);
printMemoryInfo(k);
}
void loop(){
}
For a T3.6 this prints:
i
Start address: 0x1FFF'11EC
End address: 0x1FFF'11EF
Size: 4 Bytes
Location: RAM (not initialized)
x
Start address: 0x1FFF'0738
End address: 0x1FFF'073F
Size: 8 Bytes
Location: RAM (initialized)
k
Start address: 0x1FFF'11F0
End address: 0x1FFF'11F3
Size: 4 Bytes
Location: RAM (not initialized)
Lets see what happens if we change x
to a constant:
int i;
const double x = 42; // <== constant
void setup(){
static int k;
while (!Serial) {}
printMemoryInfo(i);
printMemoryInfo(x);
printMemoryInfo(k);
}
void loop(){
}
Since one can't change a constant at runtime the linker leaves the constant in the flash as you can see in the printout below:
i
Start address: 0x1FFF'11E4
End address: 0x1FFF'11E7
Size: 4 Bytes
Location: RAM (not initialized)
x
Start address: 0x0001'08C8
End address: 0x0001'08CF
Size: 8 Bytes
Location: FLASH
k
Start address: 0x1FFF'11E8
End address: 0x1FFF'11EB
Size: 4 Bytes
Location: RAM (not initialized)
What about c-strings:
char myString[] = "This is a global c-string"; // can be changed at runtime
const char myConstantString[] = "I'm constant"; // can not be changed
void setup()
{
while(!Serial){}
char myLocalString[] = "Local c-string";
printMemoryInfo(myString);
printMemoryInfo(myConstantString);
printMemoryInfo(myLocalString);
}
void loop(){
}
Here the printout which shows that if you want to place a string in the flash, all you need to do is declaring it const.
myString
Start address: 0x1FFF'0734
End address: 0x1FFF'074D
Size: 26 Bytes
Location: RAM (initialized)
myConstantString
Start address: 0x0001'0908
End address: 0x0001'0914
Size: 13 Bytes
Location: FLASH
myLocalString
Start address: 0x2002'FFD0
End address: 0x2002'FFDE
Size: 15 Bytes
Location: STACK
Teensy is a PJRC trademark. Notes here are for reference and will typically refer to the ARM variants unless noted.