I needed a microSD card on a Control Unit board running QP/C active objects over FreeRTOS.
The architecture rule on that project is absolute: no blocking calls inside an AO’s dispatch
handler. The SDMMC middleware does not care about my rules — SD_CardInit() blocks on an OSA
semaphore no matter what the Kconfig says.
That was the easy problem. The hard one showed up later: card detection worked for a while,
then broke unpredictably after a few insert/remove cycles, and once an init failed it never
recovered. On removal, the debugger showed the card-detect callback firing twice —
false, then immediately true.
The board was reporting a card that was not there, and it was doing it to itself.
In this article I show how the SdCard AO is wired — schematic to pin mux to worker task to
state machine — and then how the detection was actually fixed, because the first version was
architecturally correct and still unreliable.

The routing table that matters. Note CD_B and RESET_B are on dedicated USDHC1 alt-functions, not GPIO — that single fact shapes the whole design.
TL;DR
USDHC_AssertHardwareReset(). The
card-level SDMMC APIs block regardless of the “non-blocking” Kconfig option, so they live on a
dedicated worker task and never in AO dispatch. And on a card-detect line that is capacitively
coupled to the power-switch net, the interrupt tells you when to look — never what is true.Step 1 — Read the schematic before believing the pin mux
Hardware: microSD socket on USDHC1.
| Signal | Pad | Function | Note |
|---|---|---|---|
| CMD / CLK / DATA0-3 | J3 / K3 / K1, L1, J2, K2 | USDHC1 cmd/clk/data0-3 | Short on-board traces to the socket |
| CD (card detect) | L11 (GPIO_AD_B1_02) | USDHC1_CD_B — dedicated alt-function, not GPIO | Hardware CD; the USDHC1 controller reads this pin itself |
| RESET / power switch | M14 (GPIO_AD_B0_00) | USDHC1_RESET_B — dedicated alt-function, not GPIO | Drives the transistor pair that gates 3V3MEM → VSD_3V3 |
The discovery that shaped the whole design
CD and RESET are not generic GPIO here. BOARD_InitSdCard() in pin_mux.c muxes them as
IOMUXC_GPIO_AD_B1_02_USDHC1_CD_B and IOMUXC_GPIO_AD_B0_00_USDHC1_RESET_B. Two consequences:
- Card detection is fully hardware-driven (
kSD_DetectCardByHostCD) — no GPIO IRQ or software debounce needed in the BSP. (Or so I thought. See step 6.) - Power control must go through
USDHC_AssertHardwareReset(), notGPIO_PinWrite(). Once a pad is muxed to a peripheral, it belongs to that peripheral, not to GPIO.
Pull direction was checked against NXP’s own evkbmimxrt1060/sdmmc_examples reference
pin_mux.c before any driver code was written — the SD spec requires pull-ups, not
pull-downs, on CMD/DATA/CD for the open-drain identification phase. The field-by-field
reasoning is in the companion article on
pin config: pull/keeper, drive strength and slew rate.
Step 2 — Middleware setup, and one build-breaking export
In MCUXpresso Config Tools → Component Configuration → Middleware → sdmmc:
- SD card (
middleware.sdmmc.sd) - uSDHC host controller (
middleware.sdmmc.host.usdhc) — not SDHC or SDIF, those are for other NXP families - Transfer mode: non-blocking
- OSA selection: confirm it resolved to FreeRTOS (
component.osa_free_rtos), not bare-metal
Build-breaking gotcha
The Config Tool writes several CONFIG_MCUX_COMPONENT_middleware.sdmmc.*=y lines straight into
prj.conf for symbols that have no Kconfig prompt (.common, .osa, .usdhc.template).
Those are select-only targets, and strict Kconfig rejects assigning them directly:
error: MCUX_COMPONENT_middleware.sdmmc.common ... is assigned in a
configuration file, but is not directly user-configurable (has no prompt)
Fix: delete the generated lines. They are already pulled in transitively — .sd selects
.common, and .host.usdhc selects .osa and .usdhc.template.
What survives in prj.conf:
CONFIG_MCUX_COMPONENT_middleware.sdmmc.sd=y
CONFIG_MCUX_COMPONENT_middleware.sdmmc.host.usdhc=y
CONFIG_MCUX_COMPONENT_middleware.sdmmc.host.usdhc.non_blocking=y
Step 3 — Board SDMMC config
There was no sdmmc_config.h in this repo yet. The SDK’s middleware.sdmmc.usdhc.template
component ships only a generic stub at middleware/sdmmc/template/usdhc/sdmmc_config.h, which
has to be copied in and filled with board values — the same way pin_mux.c is generated once
and then hand-edited.
#define BOARD_SDMMC_SD_HOST_BASEADDR USDHC1
#define BOARD_SDMMC_SD_HOST_IRQ USDHC1_IRQn
#define BOARD_SDMMC_SD_CD_TYPE kSD_DetectCardByHostCD /* hardware CD_B */
#define BOARD_SDMMC_SD_POWER_RESET_ACTIVE_HIGH (1) /* RESET_B HIGH = power ON */
BOARD_SD_Config() wires the card descriptor’s usrParam.pwr callback to a function calling
USDHC_AssertHardwareReset(USDHC1, enable). That is what actually switches the transistor pair
and powers the card, since GPIO writes do nothing on a pin muxed to a peripheral alt-function.
The build system picks up a project-provided sdmmc_config.c and says so during CMake
configure:
-- Will use custom config file .../bsp/app/sdmmc_config.c from project
Step 4 — The blocking-API problem, and the worker-task pattern
Non-blocking does not mean what you want it to mean
SD_HostInit, SD_CardInit, SD_ReadBlocks,
SD_WriteBlocks — block internally on OSA semaphores while a transfer is in flight, even
with host.usdhc.non_blocking=y. That option only affects the low-level single-transfer API.
The higher-level card functions are still synchronous.That conflicts directly with “no blocking calls inside AO event handlers”. The resolution is to
never call these APIs from the AO’s dispatch function at all. bsp_sdcard.c owns a dedicated
FreeRTOS worker task that:
- Calls
BOARD_SD_Config()andSD_HostInit()once at startup. - Waits on a binary semaphore, given by the hardware CD_B interrupt callback. That callback is
ISR-safe by construction — it does
xSemaphoreGiveFromISRand nothing else. - On card insertion, power-cycles via
SD_SetCardPoweroff/on, then calls the blockingSD_CardInit(). - Posts a plain
QACTIVE_POST()— task context, not_FROM_ISR— back to theSdCardAO with the result.
The AO’s state machine never touches a blocking call. It only reacts to posted result signals.
Step 5 — The active object
SdCard is a singleton AO. The state machine is deliberately small:
stateTop
stateDisabled <- waits for SM_SDCARD_ENABLE_SIG
-> BSP_SdCard_Init(ao, initOkSig, initFailSig, removedSig)
stateEnabled
stateNoCard <- initial substate
HW_SD_CARD_INIT_OK_SIG -> publish SD_CARD_MOUNTED_SIG, -> stateMounted
HW_SD_CARD_INIT_FAIL_SIG -> publish SD_CARD_ERROR_SIG, stay in stateNoCard
stateMounted
HW_SD_CARD_REMOVED_SIG -> publish SD_CARD_UNMOUNTED_SIG, -> stateNoCard
SM_SDCARD_DISABLE_SIG -> BSP_SdCard_Deinit(), -> stateDisabled
Other AOs subscribe to SD_CARD_MOUNTED_SIG, SD_CARD_UNMOUNTED_SIG and SD_CARD_ERROR_SIG.
They never talk to the BSP or the worker task directly — no shared data between AOs.
A SystemManager_stateConfigureAndEnableSdCard state slots into the boot chain between Io
and Rte, publishing SM_SDCARD_ENABLE_SIG. No separate configure phase is needed because
init is already non-blocking from the AO’s point of view.
On priority: I first placed the AO at Io+1, then deliberately lowered it to Blinky+1. Its
dispatch is trivial — few events, no blocking — and the real work lives on a separate
low-priority worker task. There is no reason for it to out-rank I/O-critical AOs like Io,
I2CBusController or TouchController. Size AO priority to actual urgency, not to habit.
Step 6 — Where it went wrong
Everything above is architecturally correct, and it was not reliable in practice. It took an extended debugging session to find out why, and the answer is worth writing down because the lesson generalises.
Symptoms
- Detection worked for a while, then broke unpredictably after a few insert/remove cycles.
- After an
SD_CardInit()failure, detection corrupted and never recovered. - The HMI display went black while touch kept working — the SD path was starving the FlexIO-eDMA display.
- In the debugger, the CD callback fired twice on removal:
false, then immediatelytrue.
Root causes, stacked
SD_HostDoReset()on removal disabled all USDHC interrupt signals, and nothing re-armed them. Detection silently died.- Boot-with-card gap in the SDK. If a card is already inserted at power-on, the SDK arms no CD interrupt at all.
- RESET_B ↔ CD_B coupling, in hardware. The card power switch sits next to the card-detect line. Toggling it dumps a transient onto CD_B. With a card inserted, the card mechanically holds CD_B low and is immune. With the card removed, CD_B floats on just its 10 kΩ pull-up and the transient flips it — a fake insertion immediately after a real removal.
- Trusting the ISR-reported state. Acting on the value the CD interrupt reported, on a line this noisy, propagated every glitch straight into the state machine.
- Retry livelock. A persistent init failure, retried on every wake, power-cycled the card
each time. Each power-cycle self-glitched another CD interrupt. The resulting
SD_CardInit/DMA/interrupt churn is what starved the display.
The fix — four measures, and you need all four
Defense in depth: never trust a single signal on a coupled, bouncing line. The CD interrupt becomes a “something changed, go check” trigger. The live hardware pin is the source of truth.
- Arm the removal interrupt once at boot if a card is already present, closing the SDK’s boot-with-card gap. One write, during single-threaded init — never in the steady-state loop.
- Debounce and re-read the hardware on every edge. On a reported change, wait ~50 ms, then
read the actual pin with
SDMMCHOST_CardDetectStatus(), not the ISR flag. If it matches the last acted-on state, it was a false trigger: drain the semaphore and ignore it. - Power-cycle on insertion, then re-sync and drain. After
SD_CardInit(), re-read the live pin, reset the tracked state to it, and drain pending semaphore gives — absorbing the fake CD interrupts the power-cycle itself induces through RESET_B coupling. - Never power off on removal. The card is already gone; toggling RESET_B only glitches the now-floating CD line. The next insertion power-cycles cleanly anyway. This also matches the SDK examples, which only power-cycle on insertion.
The retry livelock disappears as a consequence. Acting only on a debounced, hardware-confirmed change of state means a persistent init failure is reported once and then waits for a genuine remove and re-insert. No churn, no display starvation.
Hardware recommendation for the next spin
Step 7 — Verifying it
- Host unit tests (CppUTest) cover the full lifecycle: disabled → enabled, mount, unmount,
error paths, disable-while-mounted, re-enable, plus two
Q_ASSERTtests for BSP init and deinit failure. The BSP is mocked, so the worker task and hardware are never touched in host tests. - Firmware build gates: both the
flexspi_nor_sdram_debug_qspyandflexspi_nor_sdram_debug_tracepresets must build clean. - On hardware, via QSpy: open QView, set the current object and AO object to
l_SdCard, filter for the matching AO IDs, and enable the global SM group (all) plusQS_QF_PUBLISH. AddBSP_logMsgWithUint()calls at each blocking-call boundary in the worker task — the trace then shows driver status codes rather than just “it failed”.
Cheat sheet — a new SDMMC-backed AO on this platform
| Step | Watch out for |
|---|---|
| Check schematic for CD/RESET routing | If muxed to a dedicated USDHC alt-function rather than GPIO, card detect is hardware-driven and power control must use USDHC_AssertHardwareReset(), not GPIO_PinWrite() |
Enable middleware.sdmmc.sd + a host controller | Pick the host matching your MCU’s actual peripheral (uSDHC on RT106x) — enabling SD card alone does not auto-select a host |
| Pick transfer mode | Non-blocking, to match the AO rule — but know it only covers the low-level transfer, not SD_CardInit and friends |
| Verify OSA resolved to FreeRTOS | It is a Kconfig choice — confirm component.osa_free_rtos, not _bm |
Write board sdmmc_config.h/.c | No template exists until you make one — copy the SDK’s generic template, fill in CD type, power-reset macros, IRQ priority |
| Isolate blocking SDMMC calls | They block on OSA semaphores regardless of the “non-blocking” Kconfig — run them on a dedicated worker task, never in AO dispatch |
| ISR-safe hand-off | The hardware CD callback runs in USDHC ISR context — give a semaphore there and nothing else |
prj.conf sanity | Do not let generated CONFIG_MCUX_COMPONENT_* lines assign non-prompted, select-only symbols |
| AO priority | Size it to actual urgency — an AO with no consumers and no blocking work belongs near the bottom |
| Card detect | Debounce and re-read the pin. Do not act on the ISR-reported level |
Summary
What works now: the SdCard AO mounts and unmounts reliably across repeated insert/remove
cycles, a failed init is reported once instead of livelocking, and the display no longer
starves. What is still open: the 100 nF cap on the card-detect net is a schematic change for
the next board spin, not something software can do.
Three things I would carry to any board:
- The interrupt tells you when to look, not what is true. On a coupled or bouncing line, debounce and re-read the hardware.
- A self-induced action can generate its own spurious interrupts. Power-cycling the card glitched the very line that reports the card. Re-sync and drain afterwards.
- Match the SDK structure, but not blindly. The SDK examples do not debounce because their reference boards do not have this RESET_B-to-CD_B coupling. Board-specific hardware realities need board-specific software on top of the SDK pattern.
And the one that cost the most time: fix all the root causes at once. Each of the four measures addresses a different failure. Removing any one of them brought a symptom back.
Related
- i.MX RT1062 pin config: pull/keeper, drive strength and slew rate — why CMD/DATA/CD need pull-ups
- FlexSPI + QSPI NOR on the i.MX RT1062 — the other storage path on this board
💬 Questions, corrections, or your own war story with card detect? Leave a comment below — I read and answer all of them.
💬 Discussion
Questions, corrections, or war stories from your own board? Leave a comment below — I read and answer all of them. (Sign-in with GitHub.)