04 · GPIO & Pin Muxing¶
Time to make the chip do something visible. On the S32K, driving a pin involves two separate hardware modules — a split that confuses newcomers for about ten minutes and then becomes second nature: the PORT module decides what a pin is (its function, pull resistors, interrupt behavior), and the GPIO module decides what a GPIO-configured pin does (drive high, drive low, read). This module covers both, drives the S32K144EVB's RGB LED, reads its user buttons, and shows the same job in SDK-style and register-level code side by side.
The PORT/GPIO split¶
Each pin on the S32K144 belongs to a port (A–E) and appears in two peripherals:
| Module | Register block | Role |
|---|---|---|
| PORT (PORTA…PORTE) | PORTx->PCR[n] |
Pin Control Register per pin: MUX (which peripheral owns the pin), pull-up/down enable, passive filter, interrupt config |
| GPIO (PTA…PTE) | PTx->PDDR/PDOR/PSOR/PCOR/PTOR/PDIR |
Direction, output data, set/clear/toggle, input read |
A pin does nothing as GPIO until its PCR MUX field is set to 1 (ALT1 = GPIO). Other MUX values hand the pin to peripherals — the same physical pin might be ADC input (ALT0/analog), GPIO (ALT1), LPUART TX (e.g. ALT2), or FlexCAN (another ALT), depending on the pin. The mapping lives in the chip's IO Signal Description table — you look it up, you don't guess.
And remembering module 3: enable the PORT clock in the PCC first, or
the first PCR write hard-faults.
Driving the EVB's RGB LED (register-level)¶
On the S32K144EVB the RGB LED sits on PTD15 (red), PTD16 (green), PTD0 (blue), wired active-low — the pin sinks current, so writing 0 turns the LED on.
#include "S32K144.h"
#define LED_RED_PIN 15u /* PTD15, active-low */
#define LED_GREEN_PIN 16u /* PTD16, active-low */
static void leds_init(void)
{
/* 1. Clock the PORT D configuration module (PCC!) */
PCC->PCCn[PCC_PORTD_INDEX] |= PCC_PCCn_CGC_MASK;
/* 2. PORT: make the pins GPIO (MUX = 1) */
PORTD->PCR[LED_RED_PIN] = PORT_PCR_MUX(1);
PORTD->PCR[LED_GREEN_PIN] = PORT_PCR_MUX(1);
/* 3. GPIO: outputs, and start OFF (active-low → drive high) */
PTD->PSOR = (1u << LED_RED_PIN) | (1u << LED_GREEN_PIN);
PTD->PDDR |= (1u << LED_RED_PIN) | (1u << LED_GREEN_PIN);
}
static void led_red(bool on)
{
if (on) PTD->PCOR = (1u << LED_RED_PIN); /* clear = sink = ON */
else PTD->PSOR = (1u << LED_RED_PIN); /* set = high = OFF */
}
The set/clear/toggle registers deserve a pause, because they're a genuinely good hardware idea:
| Register | Effect of writing bit n = 1 |
|---|---|
PDOR |
Directly sets the whole output register (read-modify-write needed for one pin) |
PSOR |
Sets output bit n high — other bits untouched, single write |
PCOR |
Clears output bit n — atomic, no read-modify-write |
PTOR |
Toggles output bit n |
Why it matters: PDOR |= ... is a read-modify-write — if an interrupt
changes another pin between your read and your write, one of the changes is
lost. PSOR/PCOR/PTOR are single atomic stores. In ECU code where
interrupts are always in play, prefer PSOR/PCOR/PTOR always — it's a
correctness habit, not a style preference.
A blink, with a crude delay for now (module 8 replaces this properly):
int main(void)
{
leds_init();
for (;;) {
PTD->PTOR = (1u << LED_RED_PIN); /* toggle */
for (volatile uint32_t i = 0; i < 500000u; i++) { } /* ~busy-wait */
}
}
Reading the user buttons¶
The EVB's buttons SW2 (PTC12) and SW3 (PTC13) are wired through the board's own resistors so that the pin reads 1 while pressed, 0 when idle (check your board's schematic — polarity and pulls are a board decision, not a chip decision; many other boards wire buttons active-low with pull-ups instead).
static void buttons_init(void)
{
PCC->PCCn[PCC_PORTC_INDEX] |= PCC_PCCn_CGC_MASK;
PORTC->PCR[12] = PORT_PCR_MUX(1); /* GPIO; board provides pull */
/* PDDR bit stays 0 = input (reset default) */
}
static bool sw2_pressed(void)
{
return (PTC->PDIR & (1u << 12)) != 0u; /* EVB: high when pressed */
}
If a board gives you no external resistors, enable the internal pull-up in
the PCR instead: PORT_PCR_MUX(1) | PORT_PCR_PE_MASK | PORT_PCR_PS_MASK
(PE = pull enable, PS = 1 selects pull-up) — then pressed reads 0 with the
button wired to ground.
Buttons bounce (contacts chatter for a few milliseconds), so real firmware samples them periodically — e.g. from the 10 ms tick you'll build in module 8 — and accepts a new state only after 2–3 identical samples. In a car this is bread and butter: every door switch, stalk, and steering-wheel button lands on some ECU's GPIO input with exactly this treatment, and the debounced result is often broadcast on CAN for other ECUs to consume.
SDK-style vs register-level, side by side¶
The S32K1 SDK expresses pin muxing as a table consumed once at startup, usually generated by the S32DS pin tool:
/* SDK-style: one array describes every pin, one call applies it */
const pin_settings_config_t g_pin_mux_InitConfigArr[] = {
{ .base = PORTD, .pinPortIdx = 15u,
.mux = PORT_MUX_AS_GPIO, .direction = GPIO_OUTPUT_DIRECTION,
.gpioBase = PTD, .initValue = 1u /* off (active-low) */ },
{ .base = PORTC, .pinPortIdx = 12u,
.mux = PORT_MUX_AS_GPIO, .direction = GPIO_INPUT_DIRECTION,
.gpioBase = PTC },
};
PINS_DRV_Init(2u, g_pin_mux_InitConfigArr); /* apply the table */
PINS_DRV_ClearPins(PTD, (1u << 15)); /* red ON */
PINS_DRV_SetPins(PTD, (1u << 15)); /* red OFF */
uint32_t v = PINS_DRV_ReadPins(PTC); /* read whole port C */
| Register-level | SDK-style | |
|---|---|---|
| Transparency | Total — you see every bit | Structure fields, mapped to bits for you |
| Verbosity for many pins | Grows fast | One table entry per pin, scales nicely |
| Portability S32K1→S32K3 | Register names change | API concept carries over (RTD has an equivalent Port/Dio layer) |
| Production practice | Used in tiny/bootloader code | Config tables are the norm (and AUTOSAR MCAL works exactly this way) |
Both compile to the same few register writes. Learn the registers once so the SDK is never a mystery; use tables so 60-pin configs stay reviewable.
Cheat sheet¶
| Item | Notes |
|---|---|
| PORT vs GPIO | PORTx->PCR[n] = pin function/pulls; PTx = pin data/direction |
| Make pin GPIO | PORTx->PCR[n] = PORT_PCR_MUX(1) (after PCC clock enable!) |
| Output | PTx->PDDR |= bit, then PSOR/PCOR/PTOR to drive |
| PSOR / PCOR / PTOR | Atomic set / clear / toggle — prefer over PDOR read-modify-write |
| Input | PDDR bit = 0 (default); read PTx->PDIR |
| Internal pull-up | PCR bits PE=1, PS=1 |
| EVB RGB LED | PTD15 red, PTD16 green, PTD0 blue — active-low |
| EVB buttons | SW2 = PTC12, SW3 = PTC13 — read high when pressed (board wiring) |
| Debounce | Sample every ~10 ms, accept after 2–3 stable samples |
How It Actually Works¶
The PORT/GPIO split you saw earlier is not a software convenience — it mirrors two physically separate hardware blocks. PORT contains per-pin analog/digital pad control (the actual transistors setting drive strength, pull-up/down resistor value, slew rate, and which of up to 8 alternate peripheral signals is electrically routed to that pin via an internal multiplexer tree). GPIO is a completely different block that just reads/writes the digital logic-level register once PORT has committed a pin to "GPIO function" (MUX=1).
Setting a pin's PCR (Pin Control Register) MUX field doesn't "tell software to treat this as GPIO" — it physically closes one signal path in an analog multiplexer and opens all the others, so a pin muxed to UART_TX has its physical trace electrically disconnected from the GPIO output driver entirely; writing to GPIOx->PDOR for that pin has zero effect on the physical voltage until you re-mux it.
Drive strength and slew-rate bits in the PCR aren't cosmetic — they set the effective on-resistance and gate-drive ramp of the pad's output FETs. A fast slew rate on a long PCB trace driving an inductive automotive load creates ringing (undershoot/overshoot from trace inductance and the FET's fast dV/dt) that can violate the receiving device's absolute maximum ratings or radiate EMI that fails automotive EMC testing — this is why the "slow" slew setting exists and is often mandatory on automotive designs even though it costs switching speed.
(Described from the S32K reference manual's PORT/GPIO chapters; not measured on physical silicon in this course.)
Exercise¶
Write (and build, if you have the toolchain from module 2) a program where
the RGB LED cycles red → green → blue on each press of SW2, and SW3 turns
the LED off entirely. Requirements: use only PSOR/PCOR/PTOR for output
(no PDOR read-modify-write), detect the press edge (a held button must
not keep cycling), and structure it as leds_init()/buttons_init() plus a
tiny state machine in the loop. No hardware? Write it anyway and desk-check
it: for the input sequence idle→press→hold→release→press, write down every
register write your code performs — this trace is exactly what you'd see
stepping through in the S32DS debugger.