Memory map, stack and heap
Objectives
Section titled “Objectives”By the end of this lesson, you will be able to
- Classify the variables of an example program as stack, heap or static memory.
- Read a task’s remaining stack from the counters the SDK exposes, and judge whether it is close to overflowing.
- Explain why dynamic allocation does not belong in an ISR or a real-time loop.
Takes about 70 minutes (concepts 15 · practice 25 · lab 25 · check 5).
Before you start
Section titled “Before you start”Two review questions from lesson 1.1.
- What happens when a
uint8_tis added past 255, and why does the SDK’s example widen to 64 bits before multiplying? - Which kinds of variables need to be
volatile, and doesvolatilehelp with acount++that an interrupt can land in the middle of?
See it work first
Section titled “See it work first”Open examples/02_where_it_lives.c. Predict before you run it: which address is larger — local_buf (a local variable in main) or g_zeroed (a global variable)? Then run it.
gcc -std=c11 -Wall -Wextra -o where_it_lives examples/02_where_it_lives.c./where_it_livesThe actual numbers on your machine won’t match your classmate’s, but you’ll see the addresses fall into clear groups: constants and global variables sit close together, the block malloc hands back sits in another region, local variables sit further away still, and the depth 1, 2, 3 lines show that the deeper the function calls go, the lower the stack frame’s address gets. Microcontrollers have the exact same groups — the difference is there’s no operating system randomising them: the linker script places every group, and we can read it.
Concepts
Section titled “Concepts”1. The memory map: who places what, where
Section titled “1. The memory map: who places what, where”| Section | Holds | Where it lives on the PSOC™ Edge E84 in the SDK’s template |
|---|---|---|
.text .rodata |
Code, const constants, string literals |
External QSPI flash — the CPU runs code directly from there (XIP) |
.data |
Static variables with a non-zero initial value, e.g. int g = 42; |
The initial value sits in flash and is copied into RAM at boot |
.bss |
Static variables with no initializer, or initialized to 0 | RAM zeroed at boot; takes no space in flash |
| heap | Blocks handed back by malloc() |
Whatever RAM is left after .bss |
| stack | Local variables, return addresses | The top of that RAM region, growing down towards lower addresses |
All of this can be read straight out of the SDK’s CM33 non-secure linker script
(pse84_ns_cm33.ld).
For example, .data is placed with > m33_data AT > m33_nvm_sel, meaning it runs from RAM but its master copy is kept in flash, and the copy table at line 190 is annotated “From load address in ext flash.” The heap is whatever grows to fill the gap, from the end of .bss to just before the stack, and the stack starts at __StackTop = ORIGIN(m33_data) + LENGTH(m33_data) with a default size of 0x1000 bytes (line 47).
The consequence, noted in a comment in that same linker script from real board experience, is that every byte .bss grows by is a byte the heap loses (“every byte of .bss costs a byte of heap one for one”, line 265). When a new screen was added, the heap shrank so far that mTLS started failing too, with WARN: Malloc failed arena=121812 used=121308 free=504 largest_free=0, and the comment concludes: “Eleven rounds of code review could not see that; the board said it in ten minutes.” A static variable is never free — it always takes space from someone else.
2. Per-task stacks and the high-water mark
Section titled “2. Per-task stacks and the high-water mark”On a system running FreeRTOS, each task has its own stack, sized in words (4 bytes on a 32-bit CPU) when the task is created. For example, the mtb-only template’s heartbeat task is created with xTaskCreate(bento_heartbeat_task, "HB", 256, NULL, 1, NULL) (proj_cm33_ns/main.c line 309). The 0x1000-byte stack in the linker script, on the other hand, belongs to main() before the scheduler starts, and to interrupt handlers afterwards.
To know how deep a task has driven its stack, FreeRTOS uses a colouring trick: when a task is created, it paints the whole stack with the byte 0xA5 (tskSTACK_FILL_BYTE in tasks.c), and uxTaskGetStackHighWaterMark() counts how many words of untouched 0xA5 remain. This value is the lowest it has ever been over the task’s lifetime — it can only go down, and the lower it is, the closer the task is to overflowing. The SDK exposes the same kind of counter in several places, such as ai_engine_stack_words() and ai_engine_stack_free_words() for the inference task, which the example 07_engine_health.c explains returns the “ALL-TIME MINIMUM. It only ever falls,” and warns that reading it has to scan the stack, so it should be read about once a second, not inside a display-drawing loop.
Don’t rely only on the automatic checker. configCHECK_FOR_STACK_OVERFLOW is set to 2 on both cores, and the CM33 hook prints FATAL: Stack overflow in task '...', but a comment in the linker script warns that it “only samples at a context switch” (line 307). A stack that overflows into neighbouring data between two checks can do damage before the hook ever fires. The approach that actually works is measuring the high-water mark while the system is under its heaviest load, and leaving headroom.
The rule of thumb this course uses in its exercises (this is our own rule, not a number from the SDK or FreeRTOS): fewer than 32 words free counts as critical; less than a quarter of the total free counts as low and calls for a bigger stack or moving large buffers off the stack.
3. The heap: why not allocate in an ISR or a real-time loop
Section titled “3. The heap: why not allocate in an ISR or a real-time loop”The SDK’s template sets configHEAP_ALLOCATION_SCHEME to heap_3 (CM33’s FreeRTOSConfig.h line 189), under which FreeRTOS’s pvPortMalloc() calls newlib’s malloc() directly, suspending the scheduler for the duration (vTaskSuspendAll() then xTaskResumeAll()). Dynamic allocation does not belong in an ISR or a real-time loop, for four reasons.
- Unsafe in an ISR context. FreeRTOS only allows an ISR to call functions ending in
FromISR, andxTaskResumeAll()is not one of them. - Unpredictable timing.
malloc()has to search for free space, and how long that takes depends on the heap’s condition at that moment — a loop that must stay on time can’t predict its own timing any more. - Fragmentation. After allocating and freeing blocks of varying sizes for long enough, the heap may have hundreds of free bytes in total with no single free run big enough. That’s why CM33’s
vApplicationMallocFailedHook()prints bothfreeandlargest_free— a plain “Malloc failed” doesn’t say whether the heap is actually empty or just fragmented (main.c lines 411-425). - No way to handle failure. In an ISR there’s nowhere to wait, nowhere to retry, and nothing should be printed at all.
The fix is to allocate everything at system start-up, or to use static buffers. The SDK does exactly this in several places: littlefs in the mtb-only variant is built with LFS2_NO_MALLOC, “because every buffer is supplied statically; a code path that would need the heap fails to compile instead of quietly allocating” (variants/mtb-only.mk lines 35-39), and the 10_littlefs_basics.c example declares a 512-byte buffer as static char s_buf[512];, with the reasoning “the runner task’s stack is not the place for it.”
Worked example
Section titled “Worked example”examples/02_where_it_lives.c runs in three parts.
- Part 1 prints the address of eight objects, labelled with which section they belong to, so you can check by eye that the addresses really do cluster by label.
- Part 2 prints how much space each kind of block takes, and whether it comes from the stack, the heap, or read-only data.
- Part 3 calls three nested functions; each one prints the address of its own local variable, showing the stack growing downward.
Try changing things and predicting the result before you run it.
- Move
uint8_t local_buf[64]out ofmain. What does its correct label change to? - Add
staticin front ofuint8_t local_buf[64](still insidemain). Which group does its address move to? - Change the condition
level < 3tolevel < 100000and run it. What does the program do, and what happens instead on a microcontroller with no operating system watching over it?
Practice
Section titled “Practice”Open practice/02_stack_watermark.c. This file simulates the way FreeRTOS measures the stack, using one array. There are 3 gaps to fill in.
stack_paint()— paint every byte with0xA5.high_water_bytes()— count the run of0xA5bytes continuing in from the end the stack has not reached.percent_used()— compute the percentage the same way the 07_engine_health example does, and must not divide by zero.
gcc -std=c11 -Wall -Wextra -o watermark practice/02_stack_watermark.c && ./watermarkNotice the test that calls simulate_use() more shallowly after having gone deep before — the expected value doesn’t change, because the high-water mark is the lowest value over the whole lifetime.
Solution
Section titled “Solution”Try it yourself for at least 15 minutes first, then open solution/02_stack_watermark.c. The part worth comparing is percent_used(): the solution checks both total == 0 and free > total before subtracting, because subtracting unsigned values into a negative result wraps around into a huge number (knowledge from lesson 1.1).
Check your understanding
Section titled “Check your understanding”Answer the 5 questions in quiz.yaml (shown at the bottom of this page on the website), covering all three objectives. Getting 4 or more right counts as finishing the lesson.
Task: read the stack counters of two real tasks on the board, and judge whether they are safe.
- Build the SDK’s template with examples enabled (the full steps are in lesson 2.1).
Then unplug the USB cable all the way and plug it back in.
Terminal window make build -j ENABLE_PAGE_EXAMPLES=1make program - On the screen, tap the SDK Examples card, choose
cm55/display/00_display_bringup, and press Run this example. Note down two values:stack %lu wordsfor the GFX task, andstack never used: %lu words (all-time low). - Choose
cm55/edge_ai/07_engine_healthand run it. Note the lineinference task stack: ... words granted, ... still free at its worst (...% used). This example reads these two values before even checking whether a model is running, so you get a stack reading even with no model active. (If you instead see a message that the task was never created, note that down too — that’s a valid measurement as well.) - Calculate the percentage used for the GFX task yourself, and judge both tasks using the exercise’s rule of thumb (critical, low, or safe).
- Open 10_littlefs_basics.c. Pick five variables, such as
s_buf,k_known_paths,s_allow_write,present,n, and classify each as stack, heap, or static, with a one-sentence reason each.
Evidence to keep in your portfolio: screenshots of both examples’ output, a table of the values you noted, the calculation, the judgement, and the table classifying the five variables.
Going further
Section titled “Going further”- Read the CM33 linker script’s
.cy_csr_bufferssection (lines 261-324). The team moved an 8 KB static buffer and one task’s stack out of.bssinto a separate RAM region, to give that space back to the heap, then added anASSERTto fail the build if those buffers ever crept back. Try explaining why “failing the build” is better than just writing a warning in the documentation. - The FreeRTOS documentation on memory management: compare heap_1 through heap_5, and notice that once you use heap_3,
configTOTAL_HEAP_SIZEin the config file no longer sets the actual heap size.
Next lesson: lesson 1.3, structs, pointers and a ring buffer
Reflect
Section titled “Reflect”- In a program you’ve written before, which large buffer was a local variable — and what would happen if you moved that program into a task with a 256-word stack?
- If a program had run fine for an hour, then started reporting
Malloc failedwith no leak in sight, what evidence would you collect to tell whether the heap is truly empty or just fragmented?
References
Section titled “References”- SDK: cm55/edge_ai/07_engine_health.c (ai_engine_stack_words and ai_engine_stack_free_words)
- SDK: cm33/storage/10_littlefs_basics.c (buffers and the storage API’s contract)
- SDK: cm55/display/00_display_bringup.c (the GFX task’s high-water mark)
- SDK: the CM33 non-secure linker script (pse84_ns_cm33.ld)
- Appendix X — Traps and anti-patterns (SDK docs built from commit ef72c1b)
- FreeRTOS documentation
- Infineon FreeRTOS @ release-v10.6.202 (the version the SDK’s template pulls in): task.h
Review questions
Answer on your own first, then open the answer.
-
In void task(void) { static uint8_t buf[512]; uint8_t tmp[16]; uint8_t *p = malloc(32); } which classification is right? (Objective 1)
- buf อยู่ใน stack เพราะประกาศในฟังก์ชัน, tmp อยู่ใน stack, ก้อน 32 ไบต์อยู่ใน heap
- buf เป็น static (.bss), tmp อยู่ใน stack, ตัวชี้ p อยู่ใน stack แต่ก้อน 32 ไบต์ที่มันชี้อยู่ใน heap
- buf เป็น static (.data), tmp อยู่ใน heap, p อยู่ใน heap
- ทั้งสามอยู่ใน heap เพราะเป็นอาร์เรย์
Show answer
Answer: B. buf เป็น static (.bss), tmp อยู่ใน stack, ตัวชี้ p อยู่ใน stack แต่ก้อน 32 ไบต์ที่มันชี้อยู่ใน heap
static ในฟังก์ชันมีอายุเท่าโปรแกรม และไม่มีค่าเริ่มต้นจึงอยู่ .bss ตัวแปร local ธรรมดาอยู่ใน stack frame ส่วน malloc ให้ก้อนใน heap โดยตัวชี้ที่เก็บที่อยู่ของก้อนนั้นเป็นตัวแปร local อยู่ใน stack
-
From the SDK's CM33 linker script, which statements are true? (choose all that apply) (Objective 1)
- ค่าเริ่มต้นของ .data เก็บใน flash และถูกคัดลอกมาไว้ RAM ตอนบูต
- heap เริ่มหลัง .bss ดังนั้นเพิ่มตัวแปร static ขนาดใหญ่แล้ว heap เล็กลง
- .bss กินพื้นที่ใน flash เท่ากับขนาดของมันเพื่อเก็บค่าศูนย์
- stack อยู่บนสุดของ RAM ก้อนนั้นและโตลงหาที่อยู่ต่ำ
Show answer
Answer: A. ค่าเริ่มต้นของ .data เก็บใน flash และถูกคัดลอกมาไว้ RAM ตอนบูต · B. heap เริ่มหลัง .bss ดังนั้นเพิ่มตัวแปร static ขนาดใหญ่แล้ว heap เล็กลง · D. stack อยู่บนสุดของ RAM ก้อนนั้นและโตลงหาที่อยู่ต่ำ
.data วางแบบ > m33_data AT > m33_nvm_sel และมีตาราง copy จาก flash ไป RAM ส่วน .heap ขยายจากท้าย .bss จนถึงก่อน stack คอมเมนต์จึงเตือนว่าทุกไบต์ของ .bss กินไบต์ของ heap ส่วน .bss เป็น NOLOAD ถูกล้างเป็นศูนย์ตอนบูต ไม่ต้องเก็บค่าใน flash
-
07_engine_health reports 2048 words granted and stack_free_words = 96. Which conclusion is best? (Objective 2)
- ใช้ไป 4.7% ปลอดภัยมาก
- ใช้ไปราว 95% ตอนที่ลึกที่สุด เหลือน้อยกว่าหนึ่งในสี่ ควรขยาย stack หรือหาบัฟเฟอร์ใหญ่ที่อยู่บน stack
- ค่านี้เป็นค่าตอนนี้เท่านั้น อีกวินาทีอาจกลับมาเหลือ 2000 words ไม่ต้องสนใจ
- task ล้นไปแล้ว เพราะเลขไม่เป็นศูนย์
Show answer
Answer: B. ใช้ไปราว 95% ตอนที่ลึกที่สุด เหลือน้อยกว่าหนึ่งในสี่ ควรขยาย stack หรือหาบัฟเฟอร์ใหญ่ที่อยู่บน stack
(2048 - 96) * 100 / 2048 ได้ราว 95% และ stack_free_words เป็นค่าต่ำสุดตลอดอายุ ลดได้อย่างเดียว ไม่กลับขึ้น ค่า 96 words ยังไม่ล้นแต่ต่ำกว่าหนึ่งในสี่ของทั้งหมดมาก ตามหลักของหลักสูตรถือว่าต่ำ
-
Why is configCHECK_FOR_STACK_OVERFLOW = 2 alone not enough? (Objective 2)
- เพราะมันตรวจเฉพาะตอนสลับ task stack ที่ล้นระหว่างนั้นทับข้อมูลข้าง ๆ ได้ก่อน hook จะทำงาน
- เพราะมันทำให้เฟิร์มแวร์ช้าจนใช้งานไม่ได้
- เพราะมันทำงานเฉพาะบน CM55
- เพราะมันลบค่า 0xA5 ออกจาก stack
Show answer
Answer: A. เพราะมันตรวจเฉพาะตอนสลับ task stack ที่ล้นระหว่างนั้นทับข้อมูลข้าง ๆ ได้ก่อน hook จะทำงาน
คอมเมนต์ใน linker script ของ SDK เขียนไว้ว่ามัน only samples at a context switch จึงควรวัด high-water mark ตอนระบบทำงานหนัก แล้วเผื่อที่ว่าง แทนการรอให้ hook จับได้
-
Which are reasons not to call malloc() in an ISR or a time-critical loop? (choose all that apply) (Objective 3)
- เวลาที่ malloc ใช้ขึ้นกับสภาพ heap ในตอนนั้น จึงเดาเวลาไม่ได้
- heap อาจเหลือรวมพอแต่ไม่มีช่องต่อเนื่องใหญ่พอ (fragmentation) แล้วล้มกลางทาง
- pvPortMalloc ของ heap_3 หยุดและปล่อย scheduler ซึ่งไม่ใช่ฟังก์ชัน FromISR
- malloc คืนหน่วยความจำจาก flash ซึ่งช้ากว่า RAM
Show answer
Answer: A. เวลาที่ malloc ใช้ขึ้นกับสภาพ heap ในตอนนั้น จึงเดาเวลาไม่ได้ · B. heap อาจเหลือรวมพอแต่ไม่มีช่องต่อเนื่องใหญ่พอ (fragmentation) แล้วล้มกลางทาง · C. pvPortMalloc ของ heap_3 หยุดและปล่อย scheduler ซึ่งไม่ใช่ฟังก์ชัน FromISR
heap อยู่ใน RAM ไม่ใช่ flash ข้อที่เหลือเป็นเหตุผลจริงทั้งหมด SDK จึงจองบัฟเฟอร์แบบ static เช่น s_buf[512] และ build littlefs ด้วย LFS2_NO_MALLOC ให้โค้ดที่ต้องใช้ heap คอมไพล์ไม่ผ่านไปเลย
Cite this lesson
If you teach from this lesson or reuse it in slides or documents, credit it with the text below. If you changed it, add (adapted) after the title.
"Memory map, stack and heap" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0
Thai attribution: "แผนที่หน่วยความจำ stack และ heap" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
This lesson adapts the source below; keep its credit too.
https://github.com/tesaiot/tesaiot-pse84-devkit-sdk/tree/ef72c1b658178eee8c38b1e47d28b006f80a59b5 · SDK examples and docs are linked at this commit, not copied into this course. Lessons quote short excerpts (at most 25 lines) with a link to the file at this commit and the credit (Apache-2.0, tesaiot-pse84-devkit-sdk).
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
Content is licensed CC BY-NC 4.0. Reuse it non-commercially and credit the Thai Embedded Systems Association (TESA) every time. · How to cite TESA