1. Pin Allocation and Multiplexing
This guide demonstrates how to manage General Purpose Input/Output (GPIO) lines using the ESP32-S3 microcontroller within the Zephyr environment. We begin by analyzing the pinout mapping. According to the ESP32-S3-WROOM-1 datasheet, pins such as GPIO38 share functionality with other peripheral interfaces, specifically the Serial Peripheral Interface (SPI).
When configuring pin assignments, its critical to recognize which peripherals are occupying the physical lines. If a specific pin is assigned to a hardware module by default, you must disable that peripheral in the configuration layer to reclaim the pin for general-purpose use.
2. Inspecting Board Support Package (BSP)
The software configuration relies heavily on the Device Tree Source (DTS) files associated with the chosen development kit. For this implementation, we utilize the esp32s3_devkitm board definition.
To verify resource availability, navigate to the relevant DTSI files located within the BSP. Specifically, look for the controller hierarchy. On the ESP32-S3 architecture:
- The
gpio0peripheral registers handle signals from GPIO 0 to 31. - The
gpio1peripheral registers handle signals from GPIO 32 to 53.
In this scenario, since we are targeting GPIO38, we must interact with the gpio1 controller. Additionally, the configuration shows that SPI3 is enabled by default. If your application requires GPIO access on pins allocated to SPI3, the SPI bus status needs to be overridden to disabled.
3. Configuring the Device Tree
To successfully drive GPIO38, the device tree requires specific modifications to unmask the GPIO controller and disable conflicting bus protocols. The underlying esp32s3_common.dtsi typically defines memory layout, while esp32s3_devkitm-pinctrl.dtsi handles pin mappings.
Below is an example modification for the overlay file. Here, we define an LED connected to GPIO38 through the gpio-leds compatible string and explicitly deactivate the SPI3 interface to prevent contention.
/ {
aliases {
board_led = &user_blinker;
};
leds {
compatible = "gpio-leds";
user_blinker: blinker_0 {
gpios = <&gpio1 38 GPIO_ACTIVE_LOW>;
status = "okay";
};
};
};
/* Disable SPI to free up the pin */
&spi3 {
status = "disabled";
};
/* Ensure the GPIO controller is active */
&gpio1 {
status = "okay";
};
4. Application Logic: Output Control
Once the device tree is configured, the application can manipulate the pin sttate. The following C implementation blinks an indicator connected to GPIO38. It sets a 1-second ON/OFF cycle.
Note: The ESP32-S3 exhibits a hardware limitation on certain pins (specifically GPIO34 through GPIO39). These pins often default to a high-impedance input state and may not reliably reflect output levels when queried immediately. Reading back the state might return incorrect values during testing.
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/printk.h>
#define BLINKER_NODE DT_ALIAS(board_led)
static const struct gpio_dt_spec blinker = GPIO_DT_SPEC_GET(BLINKER_NODE, gpios);
int main(void)
{
int32_t err;
/* Verify hardware readiness */
if (!gpio_is_ready_dt(&blinker)) {
printk("Error: Blinker device unavailable\n");
return 0;
}
/* Set direction to output with push-pull driver */
err = gpio_pin_configure_dt(&blinker, GPIO_OUTPUT | GPIO_PUSH_PULL);
if (err != 0) {
printk("Configuration error: %d\n", err);
return 0;
}
while (1) {
/* Set high voltage (LED Off due to ACTIVE_LOW) */
err = gpio_pin_set_dt(&blinker, 1);
if (err == 0) {
printk("Status set HIGH (OFF)\n");
}
k_msleep(1000);
/* Set low voltage (LED On) */
err = gpio_pin_set_dt(&blinker, 0);
if (err == 0) {
printk("Status set LOW (ON)\n");
}
k_msleep(1000);
}
}
5. Application Logic: Input with Interrupts
Gathering input data efficiently often involves hardware interrupts rather than polling. The example below configures GPIO0 as an input switch. It uses a callback mechanism to handle state changes and implements a basic software debounce filter.
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/printk.h>
#define SWITCH_NODE DT_ALIAS(sw0)
static const struct gpio_dt_spec btn = GPIO_DT_SPEC_GET(SWITCH_NODE, gpios);
static bool system_active;
static int64_t last_trigger_time;
void switch_handler(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
(void)dev;
(void)pins;
(void)cb;
int64_t now = k_uptime_get_32();
/* Simple debounce check (300ms threshold) */
if ((now - last_trigger_time) > 300) {
system_active = !system_active;
last_trigger_time = now;
printk("Mode switched: %s\n", system_active ? "Active" : "Inactive");
}
}
int main(void)
{
struct gpio_callback cb_data;
int ret;
/* Initialize hardware check */
if (!gpio_is_ready_dt(&btn)) {
printk("Switch driver not ready!\n");
return -1;
}
/* Configure pin as input */
ret = gpio_pin_configure_dt(&btn, GPIO_INPUT);
if (ret != 0) {
printk("Setup failed: %d\n", ret);
return -1;
}
/* Enable Rising Edge Interrupt */
gpio_pin_interrupt_configure_dt(&btn, GPIO_INT_EDGE_RISING);
/* Register Callback */
gpio_init_callback(&cb_data, switch_handler, BIT(btn.pin));
gpio_add_callback(btn.port, &cb_data);
while (1) {
printk("System Status: %d\n", system_active);
k_msleep(500);
}
return 0;
}