C on a microcontroller
Objectives
Section titled “Objectives”By the end of this lesson, you will be able to
- Choose fixed-width types from stdint.h that fit the values you store, and explain the effect of overflow.
- Write correct set, clear and toggle bit operations using the read-modify-write pattern with bitwise operators.
- Explain when
volatileis required for variables shared with hardware or an interrupt.
Takes about 70 minutes (concepts 15 · practice 25 · lab 25 · check 5). You can do the first half entirely on your own computer; the lab uses the TESAIoT Dev Kit board (PSOC™ Edge E84).
Before you start
Section titled “Before you start”This course continues from someone who can already write Python or MicroPython. Try answering two questions in your head first.
- In MicroPython,
x = 250 + 10always gives 260. Do you think C on a microcontroller always gives 260 too? What does it depend on? - When you call
gpio.led(0).on()and the LED turns on, something inside the chip must change. What do you think that something is, and where does it live?
What you need: a C compiler on your computer (gcc or clang) for the first half, and for the lab: the TESAIoT Dev Kit board, ModusToolbox™ 3.6, and the source of the TESAIoT PSE84 Dev Kit SDK at commit ef72c1b.
See it work first
Section titled “See it work first”Open examples/01_types_and_bits.c and predict before you run it: what number will the count after +10 line print? Write your guess down, then compile and run it.
gcc -std=c11 -Wall -Wextra -o types_and_bits examples/01_types_and_bits.c./types_and_bitsIf you guessed 260, you weren’t the only one. The program prints count after +10 = 4, because uint8_t can only hold 0 to 255 — a value past that wraps around. The next lines hold two more surprises: the bytes 0x30 and 0xF8 combine into -2000, and a 32-bit product is not the same as a 64-bit one. This lesson explains all three of those lines.
Concepts
Section titled “Concepts”1. Fixed-width types and overflow
Section titled “1. Fixed-width types and overflow”C makes no promise about how many bits an int is — that depends on the compiler and architecture. But device registers, bytes on a bus, and protocol packets are defined as an exact number of bits, so firmware work uses the types from <stdint.h>, such as uint8_t, int16_t and uint32_t, which are the same width on every machine.
| Value to store | Suitable type | Why |
|---|---|---|
| A byte on an I2C or UART bus | uint8_t |
Matches the data size on the wire |
| A raw sensor value that can go negative, e.g. acceleration | int16_t |
16-bit two’s complement, as the chip sends it |
| A cycle counter, time in milliseconds, an address | uint32_t |
Wide enough, and matches the 32-bit CPU width |
| An intermediate product that might exceed 32 bits | uint64_t |
Widen before multiplying, not after |
Overflow of an unsigned type is clearly defined to wrap around modulo 2N — for example, uint8_t 250 + 10 gives 4. Overflow of a signed type, on the other hand, is undefined behaviour: the compiler assumes it never happens, so never write code that relies on it.
The SDK relies on this in several places. For example, 06_raw_register_access.c converts a value from the SHT40 sensor using a 64-bit intermediate variable, with a comment explaining why: “175000 * 65535 does not fit in 32 bits.” And 05_auto_push_task.c computes a cycle count with sensor_auto_get_push_count() - before, which stays correct even when the counter wraps around, because unsigned subtraction is modulo too.
2. Bitwise operators and read-modify-write
Section titled “2. Bitwise operators and read-modify-write”A single device register often packs several functions into different bit positions. If we overwrite the whole value, the bits belonging to other functions are lost along with it. The safe way is read-modify-write: read the current value, change only the bits that are ours, then write it back.
| What to do | Write it like this | Why the other bits don’t change |
|---|---|---|
| Set a bit | reg |= mask; |
x | 0 == x |
| Clear a bit | reg &= ~mask; |
x & 1 == x |
| Toggle a bit | reg ^= mask; |
x ^ 0 == x |
| Test a bit | if (reg & mask) |
Nothing is written |
| Write a multi-bit field | Clear the field first, then OR in the value shifted and masked | Old bits in the field don’t linger |
The comment in the SDK’s example sums it up most concisely: “The safe shape for any register write: read it, change only the bits you own, write it back, read it again to confirm.” (06_raw_register_access.c lines 170-175)
On real silicon we usually don’t touch the register directly — instead we call functions from Infineon’s PDL (Peripheral Driver Library) that do this for us, such as Cy_GPIO_Set(), Cy_GPIO_Clr() and Cy_GPIO_Inv(). The example 04_gpio_led_button.c explains that these calls are a single register write with no prior read, and are “atomic per pin on this part,” so there’s no need to guard against tasks that control different pins on the same port racing each other. That’s one reason to prefer PDL over writing |= to a GPIO register yourself. Another reason is LED and button polarity: the same example warns “POLARITY COMES FROM THE BSP, NOT FROM YOU” — use the name CYBSP_LED_STATE_ON instead of the number 1, because a later board revision might invert the polarity.
3. volatile is a promise to the compiler, not a lock
Section titled “3. volatile is a promise to the compiler, not a lock”A compiler with optimisation enabled is entitled to keep a variable’s value in a CPU register and drop reads or writes that look redundant or ineffective. If that variable is actually changed by “someone else” the compiler can’t see, the program breaks silently. volatile says that every read and every write must actually happen, in the order they appear in the code. Use it whenever a value can change from outside the program’s own flow of control, which includes
- A device register that hardware changes on its own (the PDL already declares its register structs this way).
- A variable an ISR writes and a task reads, such as
static volatile uint32_t radar_drdy_events, which the radar’s ISR increments (radar_task.c line 97). - A variable another core or DMA writes, and a flag one task sets that another task reads, such as
volatile bool tesaiot_radar_presence_detected(line 51). - A plain busy-wait counting loop, such as
for (volatile uint32_t d = 0; d < 300000; d++) {}in proj_cm55/main.c — withoutvolatile, the compiler could delete the whole loop since it has no visible effect.
What volatile does not give you is atomicity. count++ on a volatile variable is still a three-step read, modify, write, and an interrupt can still land in the middle of it. Guarding against that is the subject of lesson 1.3 and module 4.
Worked example
Section titled “Worked example”examples/01_types_and_bits.c runs in three parts.
- Part 1 prints the size of each type, then shows three kinds of overflow:
uint8_twrapping around, two bytes combining into a negativeint16_t, and a product that must be widened to 64 bits before multiplying. - Part 2 uses a simulated “register” of type
volatile uint32_t, setting, clearing and toggling bits one line at a time. - Part 3 writes a 3-bit MODE field with a single read and a single write, then reads the field back to check it.
Try changing things one at a time, predicting the result before you run it each time.
- Change
uint8_t counttouint16_tand run it. Why do you get 260 this time? - Remove the
(uint64_t)cast from therightline and run it. What do the two numbers on that line become? - Change
modeto 9 — what do you read back, and what does the mask protect?
Keep reading in the real SDK: the read-modify-write step in the 06_raw_register_access.c example reads the BMI270’s PWR_CTRL register, writes the same value back, then reads it again to confirm — all while holding the bus lock the whole time (locks are covered in lesson 5.2).
uint8_t pwr_before = 0u, pwr_after = 0u;bool w_ok = false, v_ok = false;
if (!sensor_i2c_lock(RAW_LOCK_TIMEOUT_MS)) { printf(" bus busy\r\n"); return SDK_EX_BUSY;}if (sensor_i2c_read_byte(ADDR_BMI270, BMI270_REG_PWR_CTRL, &pwr_before)) { /* The single-byte form... */ w_ok = sensor_i2c_write_byte(ADDR_BMI270, BMI270_REG_PWR_CTRL, pwr_before); /* ...and the general form, which is what you need for any * multi-byte register. Same byte again: still a no-op. */ if (w_ok) { w_ok = sensor_i2c_write_reg(ADDR_BMI270, BMI270_REG_PWR_CTRL, &pwr_before, 1u); } v_ok = sensor_i2c_read_byte(ADDR_BMI270, BMI270_REG_PWR_CTRL, &pwr_after);}sensor_i2c_unlock();Source: 06_raw_register_access.c lines 176-196 (Apache-2.0, tesaiot-pse84-devkit-sdk)
Why write the same value back? The file’s header comment answers this: if you wrote ACC_RANGE (0x41) from here instead, the chip would change its measurement range but the driver wouldn’t know — every acceleration value after that would be off by a factor of four, with no function ever returning an error. Read anything, but only write bits that nothing else relies on.
Practice
Section titled “Practice”Open practice/01_bitops.c. There are 2 gaps marked TODO: bits_set() and bits_clear(). Compile and run it before filling them in — you should see several failing tests. That’s proof the tests are actually checking something.
gcc -std=c11 -Wall -Wextra -o bitops practice/01_bitops.c && ./bitopsFill them in until the last line prints PASS: 0 failure(s).
Solution
Section titled “Solution”Try it yourself for at least 15 minutes first, then open solution/01_bitops.c. The answer is one line per function: *reg |= mask; and *reg &= ~mask;. The comments in the solution explain the most common bug: writing *reg = mask; or *reg &= mask;, which wipes out the bits belonging to other functions.
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: run the SDK’s GPIO and register examples on the board, and explain what you see using what you learned in this lesson.
If you haven’t built the SDK’s firmware template yet, do lesson 2.1 first. (An overview of building and flashing with ModusToolbox is in TESAIoT Firmware Stack, lesson 1.1.) The SDK’s examples are disabled by default; enable and pick which one to run with make variables.
cd bento-firmware-template-mtb-onlymake build -j ENABLE_PAGE_EXAMPLES=1 SDK_EXAMPLE_CM33=cm33/io/04_gpio_led_buttonmake program- Unplug the USB cable all the way and plug it back in after every flash (Appendix X #21 in the SDK docs: a reset through the debugger followed by a black screen, which looks like a failed flash even though it isn’t). Open a serial console at 115200 8N1 before plugging it in.
- Read the
[io/04]log — you should see the number of LEDs the BSP defines, the lineread-back: on=1 off=0 (expect 1 and 0), then press the SW2 button on the board a few times within five seconds. Watch theSW2 down/SW2 uplines and the debounced press count. - Build again with
SDK_EXAMPLE_CM33=cm33/sensors/06_raw_register_accessand watch for three lines: the chip id read back (should be0x24), the raw X, Y, Z acceleration values, and the linePWR_CTRL ... (verified, board unchanged). - In your lab notebook, answer: with the board lying flat, which raw axis is largest, and is it positive or negative? How is that value assembled from two bytes, and why does the example need to cast it to
uint16_tbefore shifting?
Evidence to keep in your portfolio: the serial console log from both examples, and a short answer to question 4.
Going further
Section titled “Going further”- Open
cy_gpio.hfrom mtb-pdl-cat1 @ release-v3.24.0 and findCy_GPIO_Pin_FastInit()and the constantsCY_GPIO_DM_STRONGandCY_GPIO_DM_PULLUP. Read how theoutValparameter affects pull-up mode, then compare it with the comment in04_gpio_led_button.cwarning that passing 0 makes the button “appears permanently pressed”. - Challenge: write a
field_read(reg, msk, pos)function to go withfield_write()in the exercise, and add your own test for it.
Next lesson: lesson 1.2, the memory map, stack and heap — asks where each variable in this lesson actually lives in memory.
Reflect
Section titled “Reflect”- Which variable in a program you once wrote in Python would silently overflow if moved to C?
- If a teammate marked every variable
volatile“just in case,” how would you explain to them why that doesn’t help, and might even slow things down?
References
Section titled “References”- SDK: cm33/sensors/06_raw_register_access.c (read-modify-write on an I2C device)
- SDK: cm33/io/04_gpio_led_button.c (the PDL calls behind gpio.led()/gpio.button())
- SDK: cm33/sensors/05_auto_push_task.c (an unsigned counter and subtracting its values)
- SDK: tesaiot-radar/radar_task.c (a volatile variable shared between an ISR and a task)
- Peripherals at a glance (SDK docs built from commit ef72c1b)
- Infineon mtb-pdl-cat1 (Peripheral Driver Library) @ release-v3.24.0
Review questions
Answer on your own first, then open the answer.
-
uint8_t level = 200; then level = (uint8_t)(level + 100); what is level? (Objective 1)
- 300
- 255 เพราะค่าถูกตรึงไว้ที่ค่าสูงสุด
- 44
- ไม่แน่นอน ขึ้นกับคอมไพเลอร์
Show answer
Answer: C. 44
uint8_t เก็บ 0 ถึง 255 และ overflow ของ unsigned นิยามให้วนกลับแบบ modulo 256 จึงได้ 300 - 256 = 44 ไม่ได้ตรึงค่าไว้ และไม่ใช่ undefined behaviour (ที่ไม่แน่นอนคือ overflow ของชนิด signed)
-
Which data-to-type pairings are appropriate? (choose all that apply) (Objective 1)
- ค่าดิบความเร่งจาก BMI270 ซึ่งเป็นเลข 16 บิตที่ติดลบได้ → int16_t
- ไบต์หนึ่งไบต์ที่อ่านจากบัส I2C → uint8_t
- ผลคูณ 175000 * 65535 ระหว่างทางคำนวณ → uint32_t ก็พอ
- ตัวนับมิลลิวินาทีตั้งแต่บูต → uint32_t
Show answer
Answer: A. ค่าดิบความเร่งจาก BMI270 ซึ่งเป็นเลข 16 บิตที่ติดลบได้ → int16_t · B. ไบต์หนึ่งไบต์ที่อ่านจากบัส I2C → uint8_t · D. ตัวนับมิลลิวินาทีตั้งแต่บูต → uint32_t
175000 * 65535 = 11,468,625,000 เกิน UINT32_MAX (4,294,967,295) ตัวอย่าง 06_raw_register_access.c ของ SDK จึงขยายเป็น 64 บิตก่อนคูณ ข้ออื่นเลือกชนิดตรงกับช่วงค่าและรูปแบบข้อมูลจริง
-
Which line clears bit 3 of reg without changing any other bit? (Objective 2)
- reg = ~(1u << 3);
- reg &= (1u << 3);
- reg &= ~(1u << 3);
- reg ^= (1u << 3);
Show answer
Answer: C. reg &= ~(1u << 3);
~(1u << 3) มี 0 เฉพาะบิตที่ 3 และ 1 ที่บิตอื่น AND จึงล้างเฉพาะบิตนั้น ข้อแรกเขียนทับทั้งตัว ข้อสองลบทุกบิตยกเว้นบิต 3 ข้อสี่สลับบิต ถ้าบิตเป็น 0 อยู่แล้วจะกลายเป็น 1
-
Order the lines into a read-modify-write of the MODE field with one read and one write. (Objective 2)
- v |= (mode << MODE_POS) & MODE_MSK;
- *reg = v;
- uint32_t v = *reg;
- v &= ~MODE_MSK;
Show answer
Correct order: C. uint32_t v = *reg; → D. v &= ~MODE_MSK; → A. v |= (mode << MODE_POS) & MODE_MSK; → B. *reg = v;
อ่านค่าเดิมมาไว้ในตัวแปร ล้างฟิลด์ ใส่ค่าใหม่ที่ตัดด้วย mask แล้วเขียนกลับครั้งเดียว ถ้าใส่ค่าก่อนล้าง บิตเก่าในฟิลด์จะค้างอยู่
-
Which variables should be declared volatile? (choose all that apply) (Objective 3)
- ธงที่ ISR ตั้งเป็น 1 แล้ว task วนอ่านรอ
- ตัวนับในลูปหน่วงเวลา for (d = 0; d < 300000; d++) {} ที่ไม่มีคำสั่งอื่นในลูป
- ตัวแปร local ที่ใช้รวมผลในฟังก์ชันคำนวณค่าเฉลี่ยธรรมดา
- ตารางค่าคงที่ const ที่ไม่มีใครเขียน
- ตัวนับ radar_drdy_events ที่ ISR เพิ่มค่าและ task อ่าน
Show answer
Answer: A. ธงที่ ISR ตั้งเป็น 1 แล้ว task วนอ่านรอ · B. ตัวนับในลูปหน่วงเวลา for (d = 0; d < 300000; d++) {} ที่ไม่มีคำสั่งอื่นในลูป · E. ตัวนับ radar_drdy_events ที่ ISR เพิ่มค่าและ task อ่าน
volatile ใช้เมื่อค่าเปลี่ยนนอกโฟลว์ที่คอมไพเลอร์เห็น (ISR ฮาร์ดแวร์ อีกคอร์) หรือเมื่อต้องการให้การอ่านเขียนเกิดจริงเช่นลูปหน่วงเวลา ตัวแปรคำนวณธรรมดาและค่าคงที่ไม่ต้องใช้ และ volatile ไม่ได้ทำให้ count++ เป็น atomic
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.
"C on a microcontroller" 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: "ภาษา C บนไมโครคอนโทรลเลอร์" จาก 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