Skip to content

Make and build flags

By the end of this lesson, you will be able to

  1. Enable the SDK’s example catalogue with ENABLE_PAGE_EXAMPLES=1, and select a CM33-side example to run with SDK_EXAMPLE_CM33.
  2. Explain the difference between ?= and = assignments in Make, and their effect on the build.
  3. Explain why disabled examples add nothing to the firmware size at all.

Takes about 70 minutes (concepts 15 · practice 25 · lab 25 · check 5). The concepts and practice sections only need GNU Make on your computer — nothing to compile.

Two review questions from lesson 2.1.

  1. Why must make getlibs be run in all three projects, rather than once at the top-level folder?
  2. Which line on the serial console is the evidence that an mtb-only board booted successfully?

Open examples/flags.mk. This file doesn’t compile anything — it just prints how make understands each variable. Predict before you run it: how many files will the SOURCES line show in each case? Then run them one at a time.

Terminal window
make -f examples/flags.mk
make -f examples/flags.mk ENABLE_PAGE_EXAMPLES=1 SDK_EXAMPLE_CM33=cm33/io/04_gpio_led_button
ENABLE_PAGE_EXAMPLES=1 make -f examples/flags.mk

The first gives 2 files, the other two give 4, and make also tells you where each value came from (file, command line, environment). Notice two odd-looking lines: FROZEN ends up as just hello even though GREETING is hello world, and TRAILING looks like it’s 1 but shows equals 1: no. These two surprises are what this whole lesson is about.

1. The Makefile structure in the ModusToolbox template

Section titled “1. The Makefile structure in the ModusToolbox template”

The template has a two-layer Makefile. The top-level one (Makefile) only says which projects this application has — MTB_TYPE=APPLICATION and MTB_PROJECTS=proj_cm33_s proj_cm33_ns proj_cm55 — then includes ModusToolbox’s build system. Each project’s own Makefile, such as proj_cm33_ns/Makefile, is laid out like this:

Section Example from the file What it does
Shared values include ../common.mk The variant, the workspace location
Name and core APPNAME=proj_cm33_ns CORE=CM33 Tells ModusToolbox which core to build for
Components COMPONENTS+=FREERTOS RTOS_AWARE Enables the matching COMPONENT_-prefixed folders
Our own variables ENABLE_PAGE_EXAMPLES ?= 0 A default the user can override
Passed to C DEFINES+=ENABLE_PAGE_EXAMPLES=$(ENABLE_PAGE_EXAMPLES) Becomes a -D the code can use with #if
Excluding a folder CY_IGNORE += examples Stops ModusToolbox from looking for source in that folder
Libraries LDLIBS += .../libbento_hsm.a Hands the .a file to the linker
At the very end include $(CY_TOOLS_DIR)/make/start.mk (line 530) The build system reads every variable above right here

Order genuinely matters. The template’s README troubleshooting table records the symptom “Linker cannot find a function from lib/” as being caused by LDLIBS set after include start.mk: “ModusToolbox reads it while including that file; anything later never reaches the linker.”

2. Make variables: =, :=, ?=, +=, and who wins

Section titled “2. Make variables: =, :=, ?=, +=, and who wins”
Written as Meaning Expanded when
A = x Recursive: stores the text, expands every time it’s used At use — a variable defined later still takes effect
A := x Simple: expands immediately and stores the result At that line
A ?= x Assigns only when A is not already defined A value from the environment counts as already defined
A += x Appends to the existing value Following A’s existing type

A command-line value (make build X=1) beats every assignment inside a file. An environment value loses to = and := in a file, but beats ?=. That’s why the SDK declares ENABLE_PAGE_EXAMPLES ?= 0 and SDK_EXAMPLE_CM33 ?= (lines 321-322). If it were written ENABLE_PAGE_EXAMPLES = 0, the command make build ENABLE_PAGE_EXAMPLES=1 would still work — but a value set in the build machine’s environment would be silently swallowed.

Another thing worth knowing: ifeq is decided the moment make reads that line. A comment in the same file warns that ENABLE_PAGE_BENTO_BUDDY “must be assigned BEFORE the CY_IGNORE ifneq below evaluates it — otherwise the variable is empty at parse time” (lines 60-64). And ifeq compares text literally — a value of 1 with a trailing space is not equal to 1. The SDK’s documentation records this trap as Appendix X #23: a whole feature disappears with no error anywhere. common.mk guards against this by $(strip)-ing the value of BENTO_VARIANT, then deliberately failing the build with $(error ...) if the value isn’t mtb-mpy or mtb-only (lines 80-89), because “a typo must not quietly select the wrong firmware.”

3. Why disabled examples add nothing to the firmware size

Section titled “3. Why disabled examples add nothing to the firmware size”

ModusToolbox doesn’t read a source list out of the Makefile — it walks the whole project folder and compiles every file it finds. A comment in CM33’s Makefile explains the consequence.

# Compiled ONLY with ENABLE_PAGE_EXAMPLES=1.
#
# MTB DISCOVERS SOURCES BY WALKING THE APP TREE. `SOURCES +=` adds; it does not
# subtract, and it cannot exclude a file the walk already found. Only CY_IGNORE
# removes one. Gating a subdirectory with `ifeq (...) SOURCES += wildcard`
# therefore does NOTHING — the files compile anyway, and the first symptom is a
# wall of "No such file or directory" for headers that subdirectory needs.
# Every exclusion below is a CY_IGNORE for that reason.
#
# sdk_examples_cm33_table.c is GENERATED by tools/gen_examples_table.py.
ifeq ($(ENABLE_PAGE_EXAMPLES),1)
INCLUDES += examples
# ... (lines 506-523: excludes a few groups even with examples enabled)
else
CY_IGNORE += examples
endif

Source: proj_cm33_ns/Makefile lines 494-527 (Apache-2.0, tesaiot-pse84-devkit-sdk); lines 506-523 trimmed for brevity.

When the flag is 0, the whole examples folder is CY_IGNOREd — the files in it never get compiled, so there are no object files, nothing at all for the linker to put in the image. There’s a second layer in the C code itself: main.c calls sdk_examples_cm33_start() only inside #if ENABLE_PAGE_EXAMPLES (lines 384-388). If the flag were off but a stray call remained, the build would fail at the link step — which is better than getting an image with half-finished pieces in it. The catalogue’s README sums it up: with the flag off, “not one byte of this tree reaches the firmware image.”

examples/flags.mk runs in four parts.

  • Part 1: ?= sets a default, then prints $(origin ...) to show whether the value came from the file, the command line, or the environment.
  • Part 2: GREETING = hello $(WHO) and FROZEN := hello $(WHO) are both declared before WHO is set. The first expands at use, so it becomes hello world; the second expands immediately, so it becomes hello .
  • Part 3: the SOURCES list changes with ENABLE_PAGE_EXAMPLES, and DEFINES passes both flags on to the C code.
  • Part 4: TRAILING’s value has a trailing space because of a comment at the end of the ifeq line, so it isn’t equal to 1 until it’s $(strip)ped.

Try changing things and predicting the result before you run it.

  1. Move the line WHO = world above FROZEN := .... What does FROZEN become now?
  2. Change ENABLE_PAGE_EXAMPLES ?= 0 to ENABLE_PAGE_EXAMPLES = 0, then run all three cases again. Which one changes its result?
  3. Run make -f examples/flags.mk 'ENABLE_PAGE_EXAMPLES=1 ' (note the space inside the quotes). This file accepts it because of $(strip) — what would the real SDK Makefile’s ifeq ($(ENABLE_PAGE_EXAMPLES),1) at line 504, which does not strip, do with the same value?

Open practice/feature.mk. The task is to make feature/feature.c compile only when ENABLE_FEATURE is 1, off by default, overridable from either the command line or the environment, and tolerant of a trailing space. There are 2 gaps to fill in. Check your work with

Terminal window
make -f practice/feature.mk check

Before fixing anything, you’ll see 2 FAIL lines, and make will end with an error, because check returns a non-zero status whenever even one line says FAIL. (A check that prints FAIL but returns 0 always looks green in CI — we’ll come back to this in lesson 6.2.) Fix each TODO one at a time and run check after each one; you’ll see which line flips from FAIL to PASS.

Try it yourself for at least 15 minutes first, then open solution/feature.mk. TODO 1 is changing := to ?=, and TODO 2 is starting SOURCES without feature/feature.c, then adding it with += inside ifeq ($(strip $(ENABLE_FEATURE)),1). Notice that the “command line turns it on” line passed even before you fixed anything, because a command-line value beats both := and ?=. A test that passes from the very start doesn’t prove your code is right — it only proves that particular case doesn’t separate a right answer from a wrong one. We’ll come back to this in lesson 6.1.

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: enable the example catalogue, choose one CM33-side example to run, and measure how much enabling examples changes the firmware size.

  1. Build first with examples disabled (the default), then find CM33_NS’s ELF file and measure the size of each section.
    Terminal window
    make build -j
    find proj_cm33_ns/build -name "*.elf"
    arm-none-eabi-size <the .elf file you found>
    Note down the text, data and bss columns.
  2. Build again with examples enabled, and none selected, then measure again.
    Terminal window
    make build -j ENABLE_PAGE_EXAMPLES=1
    Flash it with make program, unplug and replug the cable, then read the serial console. The runner will print a list starting === TESAIoT SDK examples on CM33_NS (...) ===, and tell you how to run one with SDK_EXAMPLE_CM33=<id>. Note the shape of the ids it prints (for example, cm33/io/04_gpio_led_button).
  3. Build again, this time picking one example, such as SDK_EXAMPLE_CM33=cm33/sensors/01_i2c_bus_scan, and watch for the line [sdk-example] running ... and the result line [sdk-example] ... -> 0 (ok) or another code.
  4. Try a name that doesn’t actually exist, such as the example under “Turning them on” in the catalogue’s README, which uses tesaiot_hsm/01_acquire_chip. What does the runner say? Compare it with the list the runner prints itself — which one is the truth for this commit?
  5. Compare the sizes from step 1 and step 2. Which section holds the difference, and using concept 3, explain why there’s no such difference when the flag is off.

Evidence to keep in your portfolio: both arm-none-eabi-size results, the example runner’s log (the list, the result of the one you chose, and the result for the nonexistent name), and a short explanation for question 5.

  • The template’s ./bento.sh menus asks make for each flag one at a time instead of reading the Makefile by eye. The template’s README explains why: some flags are set twice, once inside a board condition and once in the else, and reading the text by eye gets you the wrong answer. Try finding one such flag in proj_cm55/Makefile with grep -n "ENABLE_PAGE_" proj_cm55/Makefile.
  • The GNU Make documentation’s “The Two Flavors of Variables” and “Overriding Variables” sections cover concept 2 in full.

Next lesson: lesson 2.3, Git for firmware work

  • If a classmate said “I turned on ENABLE_PAGE_EXAMPLES but nothing changed,” what would be the first three questions you’d ask them?
  • Which kind of build flag should fail the build when its value is wrong, and which kind can quietly fall back to a default?

Review questions

Answer on your own first, then open the answer.

  1. You want the CM33 LED-and-button example to run at boot. Which command is right for the commit this course uses? (Objective 1)

    1. make build ENABLE_PAGE_EXAMPLES=1 SDK_EXAMPLE_CM33=cm33/io/04_gpio_led_button
    2. make build SDK_EXAMPLE_CM33=cm33/io/04_gpio_led_button (ไม่ต้องเปิดธงอื่น)
    3. make build ENABLE_PAGE_EXAMPLES=1 SDK_EXAMPLE_CM33=04_gpio_led_button.c
    4. make build ENABLE_PAGE_EXAMPLES=0 SDK_EXAMPLE_CM33=cm33/io/04_gpio_led_button
    Show answer

    Answer: A. make build ENABLE_PAGE_EXAMPLES=1 SDK_EXAMPLE_CM33=cm33/io/04_gpio_led_button

    ต้องเปิดธงหลัก ENABLE_PAGE_EXAMPLES=1 ก่อน ไม่อย่างนั้นโฟลเดอร์ examples ถูก CY_IGNORE และไม่มีตัวรัน ส่วน id ต้องตรงกับรายการใน sdk_examples_cm33_table.c ซึ่งมีรูปแบบ cm33/<กลุ่ม>/<ชื่อ> ตัวรันเทียบด้วย strcmp ชื่อไฟล์จึงใช้ไม่ได้

  2. You build with ENABLE_PAGE_EXAMPLES=1 and leave SDK_EXAMPLE_CM33 empty. What appears on the console? (Objective 1)

    1. ตัวรันรันตัวอย่างทุกตัวเรียงกัน
    2. ไม่มีอะไรเลย เพราะไม่ได้เลือกตัวอย่าง
    3. ตัวรันพิมพ์รายการตัวอย่างทั้งหมดพร้อมวิธีรัน แล้วไม่รันอะไร
    4. build ล้มเพราะ SDK_EXAMPLE_CM33 ว่าง
    Show answer

    Answer: C. ตัวรันพิมพ์รายการตัวอย่างทั้งหมดพร้อมวิธีรัน แล้วไม่รันอะไร

    Makefile เขียนไว้ว่าค่าว่างคือ list them and run nothing เพราะคำถามแรกของนักพัฒนาคือมีอะไรให้เรียกบ้าง และการไม่รันอะไรโดยปริยายกันไม่ให้การเปิดธงเดียวไปเปิดวิทยุหรือแตะ secure element โดยไม่ตั้งใจ

  3. The Makefile has FLAG = 0 and the user runs FLAG=1 make build (environment). What is FLAG, and what if the line were FLAG ?= 0? (Objective 2)

    1. เป็น 1 ทั้งสองแบบ
    2. เป็น 0 กับ = และเป็น 1 กับ ?=
    3. เป็น 1 กับ = และเป็น 0 กับ ?=
    4. เป็น 0 ทั้งสองแบบ
    Show answer

    Answer: B. เป็น 0 กับ = และเป็น 1 กับ ?=

    ค่าจาก environment แพ้การกำหนดค่าในไฟล์ด้วย = หรือ := แต่ ?= ให้ค่าเฉพาะเมื่อตัวแปรยังไม่ถูกนิยาม และค่าจาก environment นับว่านิยามแล้ว ถ้าส่งเป็น make build FLAG=1 (command line) จะเป็น 1 ทั้งสองแบบ

  4. A Makefile has A = x$(B), then C := x$(B), then B = y. What are A and C when used? (Objective 2)

    1. A = xy และ C = x
    2. A = x และ C = xy
    3. ทั้งคู่เป็น xy
    4. ทั้งคู่เป็น x
    Show answer

    Answer: A. A = xy และ C = x

    = เก็บข้อความไว้และขยายตอนใช้ ซึ่งตอนนั้น B เป็น y แล้ว ส่วน := ขยายทันทีตอนอ่านบรรทัด ขณะนั้น B ยังว่าง

  5. Why do the SDK examples add nothing to the image when ENABLE_PAGE_EXAMPLES=0? (choose all that apply) (Objective 3)

    1. โฟลเดอร์ examples ถูก CY_IGNORE ทั้งโฟลเดอร์ จึงไม่มีไฟล์ใดถูกคอมไพล์
    2. main.c เรียกตัวรันตัวอย่างเฉพาะใน #if ENABLE_PAGE_EXAMPLES จึงไม่มีการอ้างถึงโค้ดตัวอย่าง
    3. ตัวอย่างถูกคอมไพล์แล้วบีบอัดใน flash จึงไม่กินที่
    4. ใช้ ifeq ครอบ SOURCES += ของโฟลเดอร์ examples ก็ตัดไฟล์ออกได้เหมือนกัน
    Show answer

    Answer: A. โฟลเดอร์ examples ถูก CY_IGNORE ทั้งโฟลเดอร์ จึงไม่มีไฟล์ใดถูกคอมไพล์ · B. main.c เรียกตัวรันตัวอย่างเฉพาะใน #if ENABLE_PAGE_EXAMPLES จึงไม่มีการอ้างถึงโค้ดตัวอย่าง

    ModusToolbox หาซอร์สจากการเดินโฟลเดอร์ SOURCES += เพิ่มได้แต่ลบไม่ได้ Makefile ของ SDK จึงบอกว่าการครอบ SOURCES += ด้วย ifeq does NOTHING มีแต่ CY_IGNORE ที่ตัดออก และไม่มีการบีบอัดโค้ดอะไรแบบนั้น

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.

"Make and build flags" 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: "Make และตัวแปรของการ build" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0

Lesson link: https://tesaiot.github.io/tesa-qualification-program/en/courses/embedded-c-foundations/m02-build-and-version/l02-make-and-build-flags/

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).

Full guide: how to cite TESA

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