GPIO and interrupts
Objectives
Section titled “Objectives”By the end of this lesson, you will be able to
- Configure GPIO pins with PDL calls as outputs and as inputs, with the correct drive mode.
- Write a short, non-blocking ISR that hands work off to a task instead of doing heavy work inside itself.
- Debounce a button without using a blocking wait.
Takes about 70 minutes (concepts 15 · practice 25 · lab 25 · check 5).
Before you start
Section titled “Before you start”Two review questions from earlier modules.
- If an ISR and a task share a variable, how must it be declared, and is
volatilealone enough (lessons 1.1 and 1.3)? - When CM33 is halted at a breakpoint inside a button-reading loop, how can a button press get lost (lesson 3.1)?
See it work first
Section titled “See it work first”Open examples/07_debounce_trace.c. This file feeds in values “read from the pin” every 10 ms from a table, simulating one button press whose contact bounces both on press and release, plus one noisy reading. Predict before you run it: how many presses will a raw count see, and how many will the debounced count see?
gcc -std=c11 -Wall -Wextra -o debounce_trace examples/07_debounce_trace.c./debounce_traceThe raw count comes to 6, while the debounced count is 1, with PRESS and RELEASE times about 30 ms behind the first touch. Notice there isn’t a single delay anywhere in the code — the whole debouncer is a function called once per reading, which returns immediately every time.
Concepts
Section titled “Concepts”1. Drive mode is the whole job of GPIO
Section titled “1. Drive mode is the whole job of GPIO”A single PSOC™ Edge pin needs three things set: who owns the pin (HSIOM), how it drives (drive mode), and its initial value. The PDL bundles this into one call, Cy_GPIO_Pin_FastInit(port, pin, driveMode, outVal, hsiom). The SDK’s 04_gpio_led_button.c example has a section titled “DRIVE MODES ARE THE WHOLE JOB”:
static void leds_init(void){ for (unsigned i = 0U; i < LED_COUNT; i++) { Cy_GPIO_Pin_FastInit(s_leds[i].port, s_leds[i].pin, CY_GPIO_DM_STRONG, CYBSP_LED_STATE_OFF, /* dark from the first cycle */ HSIOM_SEL_GPIO); /* take the pin back from TCPWM */ }}
static void button_init(void){ /* outVal = CYBSP_BTN_OFF (1) is what energises the pull-up. */ Cy_GPIO_Pin_FastInit(CYBSP_USER_BTN1_PORT, CYBSP_USER_BTN1_NUM, CY_GPIO_DM_PULLUP, CYBSP_BTN_OFF, HSIOM_SEL_GPIO);}Source: 04_gpio_led_button.c lines 126-141 (Apache-2.0, tesaiot-pse84-devkit-sdk)
| To use a pin as | Drive mode from cy_gpio.h |
Worth knowing |
|---|---|---|
| An output driving an LED | CY_GPIO_DM_STRONG (push-pull) |
Give outVal the off value, so the pin doesn’t blink at start-up |
| A button input tied to ground | CY_GPIO_DM_PULLUP |
outVal must be 1 — this is the value that actually “turns on” the pull-up; sending 0 makes the button look permanently pressed |
| An input with an external resistor | CY_GPIO_DM_HIGHZ |
No internal pull |
| An active-high input, such as data-ready | CY_GPIO_DM_PULLDOWN |
The SDK’s radar driver uses this |
| An output that’s never read back | CY_GPIO_DM_STRONG_IN_OFF |
Turns off the input buffer |
On this board’s BSP, the user button CYBSP_USER_BTN1 is P7.0, set to CY_GPIO_DM_PULLUP with an initial value of 1, and LED1 and LED2 are on P10.7 and P10.5 as CY_GPIO_DM_STRONG (cycfg_pins.h lines 274-303 for the button and lines 466-500 for the LEDs). The button’s define name is BTN1, but the silkscreen on the board says SW2 — the SDK’s comment says to “Print the silkscreen name to a user” and to use the polarity values from the BSP (CYBSP_BTN_PRESSED = 0, CYBSP_LED_STATE_ON = 1) instead of writing the numbers yourself.
2. A good ISR: short, no waiting, no printing, and it hands work off
Section titled “2. A good ISR: short, no waiting, no printing, and it hands work off”An ISR interrupts everything of lower priority. While it runs, every lower-priority interrupt has to wait, and every task is halted. The SDK’s rule is therefore explicit: “Never printf from an IPC callback (ISR context)” (the catalogue’s README), and the TACP example explains that a function named _from_isr is one that “takes no mutex, allocates nothing, and prints nothing.” The shortest ISR in the SDK is the radar’s data-ready handler.
/** Radar data-ready interrupt handler */static void radar_data_ready_isr(void){ if (radar_drdy_events < 0xFFFFFFFFu) { radar_drdy_events++; } Cy_GPIO_ClearInterrupt(CYBSP_RADAR_INT_PORT, CYBSP_RADAR_INT_NUM); NVIC_ClearPendingIRQ(radar_irq_cfg.intrSrc);}Source: radar_task.c lines 109-117 (Apache-2.0, tesaiot-pse84-devkit-sdk). It does two things: records that an event happened (a volatile counter that saturates at its maximum rather than wrapping around) and clears the pin’s interrupt flag. Without clearing it, the ISR would be called again endlessly. All the real work lives in the radar’s task.
When a task needs waking up right away, use the FreeRTOS API functions ending in FromISR, then call portYIELD_FROM_ISR() so control can switch straight to the woken task as soon as the ISR finishes. Examples in the SDK: ipc_tesaiot_handler.c uses xSemaphoreGiveFromISR(), while sensor_auto_task.c copies a request into a queue with xQueueSendFromISR(), annotated “No printf here — this is ISR context.” Another FreeRTOS rule: an ISR that calls a FromISR API must have a priority no higher than configMAX_SYSCALL_INTERRUPT_PRIORITY, which CM33’s FreeRTOSConfig.h explains “sets the highest interrupt priority from which interrupt safe FreeRTOS API functions can be called.”
Setting up a GPIO pin’s interrupt has two layers: the pin layer (PDL) and the CPU’s NVIC layer. The SDK’s radar driver follows this order.
| Step | Calls (from bento_bgt60trxx_platform.c and radar_task.c) |
|---|---|
| Clear anything pending, configure the pin | Cy_GPIO_ClearInterrupt() then Cy_GPIO_Pin_FastInit(..., CY_GPIO_DM_PULLDOWN, ...) |
| Choose the edge, enable the pin’s mask | Cy_GPIO_SetInterruptEdge(..., CY_GPIO_INTR_RISING) and Cy_GPIO_SetInterruptMask(..., 1u) |
| Bind the ISR to an interrupt source | Cy_SysInt_Init(&cfg, isr), where cfg.intrSrc is the IRQ number and cfg.intrPriority is the priority |
| Enable it at the NVIC | NVIC_ClearPendingIRQ() then NVIC_EnableIRQ() |
3. Non-blocking debouncing: count “time”, not “how many reads in a row”
Section titled “3. Non-blocking debouncing: count “time”, not “how many reads in a row””A button’s contacts bounce for several milliseconds after both press and release. Debouncing means trusting a new value only once it’s held steady long enough. The SDK’s example reads every 10 ms and trusts a value once it’s seen three times in a row, for a total of 30 ms, warning: “Debounce in TIME, not by reading the pin twice in a row: two reads 200 ns apart are two samples of the same bounce.” (04_gpio_led_button.c lines 116-124)
“Non-blocking” means the debouncer never waits itself — it keeps its state in a struct and gets called once per reading, with the caller deciding the timing. That could be a task that calls vTaskDelay(pdMS_TO_TICKS(10)) between rounds (as the SDK’s example does), or an LVGL timer (as the Developer Hub’s Button Monitor example does, reading every 25 ms and trusting a value once two ticks agree). If a pin interrupt is used to help, the ISR should only “wake” the task that runs the debouncer — because a single bounce can trigger dozens of interrupts, and counting directly inside the ISR would give the same number as counting the raw signal.
Worked example
Section titled “Worked example”examples/07_debounce_trace.c runs in three parts.
- Part 1: the
raw[]table holds a value read every 10 ms, including bounces on both press and release, plus one noisy reading. - Part 2:
debounce_step()takes one value, updates the state in a struct, and returns an event — with no waiting inside. - Part 3: counts rising edges in the raw signal against the number of debounced PRESS events, on the same data.
Try changing things and predicting the result before you run it.
- Change
DEBOUNCE_POLLSto 1. How many presses does it count now, and how is this different from having no debouncing at all? - Change it to 10. How much does the PRESS event’s timing shift by, and how would a user feel about a button that slow to respond?
- Extend the noisy reading in the last row to three consecutive readings. Does the result change? How would you choose
DEBOUNCE_POLLSgiven that?
Practice
Section titled “Practice”Open practice/07_debounce.c. There are 3 gaps to fill in inside debounce_step(), and four test cases: a clean press, a bouncy press, a short noise spike, and a press held for a hundred thousand rounds.
gcc -std=c11 -Wall -Wextra -o debounce practice/07_debounce.c && ./debounceSolution
Section titled “Solution”Try it yourself for at least 15 minutes first, then open solution/07_debounce.c. The most common mistake is not stopping the agree count at DEBOUNCE_POLLS — if agree is a small type (such as uint8_t) and the button is held down a long time, the counter wraps around. The hundred-thousand-round held-press test exists to catch exactly this. Another thing to notice: the noise test passes even before you fill anything in (empty code always returns EV_NONE), so that test only means something once the real press-case tests pass too.
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: measure debounce quality on a real board, then design a button read that uses an interrupt and hands work off to a task.
- Build with
make build -j ENABLE_PAGE_EXAMPLES=1 SDK_EXAMPLE_CM33=cm33/io/04_gpio_led_button, then flash and unplug/replug the cable. (Full steps in lesson 2.1.) - During the five seconds the example watches the button, press SW2 three different ways, once each: slowly five times, as fast as you can, and with a light tap. Note the counted number against how many times you actually pressed it.
- (If you have a QWA309 base board with working buttons) flash the Button Monitor example from the Developer Hub (link at the bottom of the page), which reads buttons on P17.5 and P17.7 with a timer every 25 ms, and run the same experiment. Compare the results with step 2.
- On paper (or in code, if you’re ready), design reading SW2 with a falling-edge interrupt: list every PDL call following the table in concept 2, using the BSP’s
CYBSP_USER_BTN1_PORT,CYBSP_USER_BTN1_NUMandCYBSP_USER_BTN1_IRQ. Write an ISR that clears the flag and wakes a task withxSemaphoreGiveFromISR(), with the task callingdebounce_step(). Explain why the press isn’t counted directly inside the ISR. (If you try this on the board, note which interrupt priority you used and what happened — this course has not yet tested this code on the board.)
Evidence to keep in your portfolio: the table of results for all three press styles (and for the Button Monitor, if you tried it), the serial console log, and your draft ISR and task design with an explanation.
Going further
Section titled “Going further”- Open
cy_gpio.h@ release-v3.24.0 and findCY_GPIO_INTR_RISING,CY_GPIO_INTR_FALLINGandCY_GPIO_INTR_BOTH. Which edge should an interrupt-driven debouncer listen for? - The FreeRTOS documentation’s task notifications are a lighter alternative to a semaphore for waking a single task. The SDK has an example using
xTaskNotifyFromISR()in OPTIGA’s PAL (pal_i2c.c).
Next lesson: lesson 4.2, timers and clocks
Reflect
Section titled “Reflect”- Which everyday button have you run into that “registers twice from one press,” and can you now guess where its designer went wrong?
- If an ISR needed to do a millisecond’s worth of work, how would you split that work between the ISR and a task?
References
Section titled “References”- SDK: cm33/io/04_gpio_led_button.c (drive modes and non-blocking debouncing)
- SDK: CM33-side examples (what to know about ISR context)
- SDK: tesaiot-radar/radar_task.c (the data-ready ISR and interrupt setup)
- SDK: the BSP’s cycfg_pins.h (button and LED pins)
- Infineon mtb-pdl-cat1 (Peripheral Driver Library) @ release-v3.24.0
- Interrupt (Wikipedia)
Examples on the TESAIoT Developer Hub
Section titled “Examples on the TESAIoT Developer Hub”Try the real thing on the TESAIoT Dev Kit: open an example on the Developer Hub to read the code, download it, or flash a prebuilt firmware image.
- QWA309 — Push Button Monitor — reads two buttons on P17.5 and P17.7, active-low with pull-ups, using a timer every 25 ms, showing press/release state, press count, and hold time on LVGL. (The button names on the board’s silkscreen don’t agree across sources — go by the pin numbers.)
- QWA309 — Hardware Button Menu — navigates an LVGL menu with physical buttons, SW6=Move, SW5=Select (no touch involved) — a headless/kiosk UX pattern.
Review questions
Answer on your own first, then open the answer.
-
A button connects the pin to ground with no external resistor. Which call configures the pin correctly? (Objective 1)
- Cy_GPIO_Pin_FastInit(port, pin, CY_GPIO_DM_STRONG, 0, HSIOM_SEL_GPIO)
- Cy_GPIO_Pin_FastInit(port, pin, CY_GPIO_DM_PULLUP, 0, HSIOM_SEL_GPIO)
- Cy_GPIO_Pin_FastInit(port, pin, CY_GPIO_DM_PULLUP, 1, HSIOM_SEL_GPIO)
- Cy_GPIO_Pin_FastInit(port, pin, CY_GPIO_DM_HIGHZ, 1, HSIOM_SEL_GPIO)
Show answer
Answer: C. Cy_GPIO_Pin_FastInit(port, pin, CY_GPIO_DM_PULLUP, 1, HSIOM_SEL_GPIO)
ต้องมี pull-up ให้ขาอยู่ที่ 1 ตอนไม่กด และสำหรับ drive mode แบบ pull-up ค่า outVal คือสิ่งที่เปิด pull ตัวอย่างของ SDK เตือนว่าส่ง 0 แล้วปุ่มจะดูเหมือนถูกกดตลอด HIGHZ ไม่มี pull ขาจะลอย ส่วน STRONG เป็นขาออก
-
Why does the SDK GPIO example pass CYBSP_LED_STATE_OFF as the LED outVal, by name instead of 0? (Objective 1)
- เพื่อให้ขาไม่กะพริบตอนเริ่ม และค่าขั้วมาจาก BSP ซึ่งบอร์ดรุ่นอื่นอาจกลับขั้ว
- เพราะ PDL ไม่รับเลข 0
- เพื่อให้ LED สว่างที่สุดตอนเริ่ม
- ไม่มีเหตุผล เป็นแค่สไตล์
Show answer
Answer: A. เพื่อให้ขาไม่กะพริบตอนเริ่ม และค่าขั้วมาจาก BSP ซึ่งบอร์ดรุ่นอื่นอาจกลับขั้ว
ค่าเริ่มต้นกำหนดระดับขาตั้งแต่รอบแรกที่เป็นขาออก และคอมเมนต์ของ SDK บอกว่า polarity comes from the BSP, not from you เลข 1 ที่เขียนเองจะกลับด้านเงียบ ๆ บนบอร์ดที่ LED ต่อแบบ sink
-
Which belong in a GPIO ISR? (choose all that apply) (Objective 2)
- ล้าง interrupt flag ของขาด้วย Cy_GPIO_ClearInterrupt()
- ปลุก task ด้วย xSemaphoreGiveFromISR() แล้ว portYIELD_FROM_ISR()
- printf เวลาที่เกิดเหตุการณ์ลง console
- vTaskDelay(pdMS_TO_TICKS(30)) เพื่อรอให้ปุ่มหยุดเด้ง
- เพิ่มตัวนับ volatile แบบไม่วนกลับ
Show answer
Answer: A. ล้าง interrupt flag ของขาด้วย Cy_GPIO_ClearInterrupt() · B. ปลุก task ด้วย xSemaphoreGiveFromISR() แล้ว portYIELD_FROM_ISR() · E. เพิ่มตัวนับ volatile แบบไม่วนกลับ
ISR ควรสั้น บันทึกเหตุการณ์ ล้าง flag และส่งงานต่อ SDK ห้าม printf ในบริบท ISR และ ISR ห้ามรอหรือเรียก API ของ FreeRTOS ที่ไม่ได้ลงท้ายด้วย FromISR
-
Order the steps to enable a GPIO pin interrupt, following the SDK radar driver. (Objective 2)
- NVIC_ClearPendingIRQ() แล้ว NVIC_EnableIRQ()
- Cy_GPIO_SetInterruptEdge() และ Cy_GPIO_SetInterruptMask(..., 1u)
- Cy_GPIO_ClearInterrupt() แล้ว Cy_GPIO_Pin_FastInit() ของขา
- Cy_SysInt_Init(&cfg, isr) ผูก ISR กับหมายเลข IRQ และลำดับความสำคัญ
Show answer
Correct order: C. Cy_GPIO_ClearInterrupt() แล้ว Cy_GPIO_Pin_FastInit() ของขา → B. Cy_GPIO_SetInterruptEdge() และ Cy_GPIO_SetInterruptMask(..., 1u) → D. Cy_SysInt_Init(&cfg, isr) ผูก ISR กับหมายเลข IRQ และลำดับความสำคัญ → A. NVIC_ClearPendingIRQ() แล้ว NVIC_EnableIRQ()
ตั้งขาก่อน เลือกขอบและเปิด mask ของขา ผูก ISR กับแหล่ง interrupt แล้วจึงเปิดที่ NVIC ถ้าเปิด NVIC ก่อนผูก ISR interrupt ที่ค้างอยู่อาจกระโดดไปหา handler ที่ยังไม่ได้ตั้ง
-
Polling every 10 ms and believing 3 agreeing reads, with 5 ms of bounce, about how long after first contact is the press reported, and how is the wait done? (Objective 3)
- ทันที เพราะอ่านได้ 1 ครั้งแรกก็รายงาน
- ราว 30 ms โดยไม่มีการรอในฟังก์ชันกันเด้งเลย ผู้เรียกเรียกมันหนึ่งครั้งต่อการอ่าน
- ราว 30 ms โดยฟังก์ชันกันเด้งเรียก delay 30 ms ข้างใน
- ไม่รายงานเลย เพราะมีการเด้ง
Show answer
Answer: B. ราว 30 ms โดยไม่มีการรอในฟังก์ชันกันเด้งเลย ผู้เรียกเรียกมันหนึ่งครั้งต่อการอ่าน
สามการอ่านห่างกัน 10 ms คือราว 30 ms หลังค่านิ่ง ตัวกันเด้งเก็บสถานะใน struct แล้วคืนทันที การรอเป็นหน้าที่ของ task หรือ timer ที่เรียกมัน จึงไม่บล็อกงานอื่น
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.
"GPIO and interrupts" 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: "GPIO และ interrupt" จาก 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