Firmware Whisperer: AI-Powered Prompts for Embedded Systems and IoT — Taming ESP32, STM32, and Arduino Without Burning Out

The embedded developer's world is a peculiar kind of hell. You're writing C code for a microcontroller with 320 KB of RAM, debugging a sensor that communicates over I2C at 3.3 volts, and praying that the watchdog timer doesn't reset your device at 3 AM. Meanwhile, your web developer friends are deploying to Kubernetes clusters with 64 GB of RAM and calling it 'resource-constrained.'

But here's the thing: AI language models have become surprisingly good at embedded systems. They can generate boilerplate for peripheral initialization, explain cryptic register-level documentation, and even suggest fixes for that weird timing bug in your interrupt handler. The catch? You need to ask the right questions. A vague prompt like 'write code for ESP32' gets you generic garbage. A precise prompt with register names, clock speeds, and peripheral configurations gets you production-ready code.

This article is a collection of battle-tested prompts for embedded development. I've organized them by complexity — from basic GPIO toggling to advanced DMA transfers — and included real examples of what the AI actually returns. Whether you're working with ESP32, STM32, or classic Arduino, these prompts will save you hours of datasheet reading and Stack Overflow scrolling.

Basic Level: Getting Started Without the Datasheet Marathon

1. GPIO Configuration for STM32 with HAL Library

Task: Generate initialization code for a GPIO pin with specific mode, pull-up/pull-down, and speed settings.

Prompt:

Write C code for STM32F4 using HAL library to configure PA5 as output push-pull, no pull-up/pull-down, low speed, and toggle it every 500ms in the main loop. Include clock enable for GPIOA. Assume HSE is 8 MHz and system clock is 168 MHz.

Example Result:

// Enable GPIOA clock
__HAL_RCC_GPIOA_CLK_ENABLE();

GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

while (1) {
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    HAL_Delay(500);
}

The AI correctly includes the clock enable macro and uses the HAL API. Note: HAL_Delay relies on SysTick, so ensure it's configured in HAL_Init().

2. Arduino Sketch for Reading a DHT22 Sensor

Task: Generate a complete Arduino sketch that reads temperature and humidity from a DHT22 sensor and prints to Serial.

Prompt:

Write an Arduino sketch for ESP32 that reads temperature and humidity from a DHT22 sensor connected to GPIO4. Use the DHT sensor library by Adafruit. Print values to Serial every 2 seconds. Include error handling for failed reads.

Example Result:

#include "DHT.h"
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

void setup() {
  Serial.begin(115200);
  dht.begin();
}

void loop() {
  delay(2000);
  float h = dht.readHumidity();
  float t = dht.readTemperature();
  if (isnan(h) || isnan(t)) {
    Serial.println("Failed to read from DHT sensor!");
    return;
  }
  Serial.print("Humidity: "); Serial.print(h);
  Serial.print(" %  Temperature: "); Serial.print(t);
  Serial.println(" *C");
}

The AI correctly uses the Adafruit library and includes NaN checks. For ESP32, you might need to specify the pin as 4 (not D4) depending on your board.

3. UART Communication on STM32 with Interrupts

Task: Set up UART with interrupt-driven reception.

Prompt:

Write C code for STM32F103 using HAL to initialize USART2 at 115200 baud, 8N1, with RX interrupt enabled. In the interrupt callback, echo received characters back. Use a global buffer of size 64.

Example Result:

UART_HandleTypeDef huart2;
uint8_t rx_buffer[64];

void USART2_IRQHandler(void) {
  HAL_UART_IRQHandler(&huart2);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
  if (huart->Instance == USART2) {
    HAL_UART_Transmit(&huart2, rx_buffer, 1, 10);
    HAL_UART_Receive_IT(&huart2, rx_buffer, 1);
  }
}

void MX_USART2_UART_Init(void) {
  huart2.Instance = USART2;
  huart2.Init.BaudRate = 115200;
  huart2.Init.WordLength = UART_WORDLENGTH_8B;
  huart2.Init.StopBits = UART_STOPBITS_1;
  huart2.Init.Parity = UART_PARITY_NONE;
  huart2.Init.Mode = UART_MODE_TX_RX;
  huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart2.Init.OverSampling = UART_OVERSAMPLING_16;
  HAL_UART_Init(&huart2);
  HAL_NVIC_SetPriority(USART2_IRQHandler, 0, 0);
  HAL_NVIC_EnableIRQ(USART2_IRQn);
  HAL_UART_Receive_IT(&huart2, rx_buffer, 1);
}

The AI correctly sets up the interrupt and re-arms the reception in the callback. Remember to enable the USART2 clock in HAL_UART_MspInit.

Advanced Level: Peripherals and Protocols

4. I2C Scanner for ESP32

Task: Scan the I2C bus and list all connected device addresses.

Prompt:

Write an Arduino sketch for ESP32 that scans the I2C bus on pins SDA=21, SCL=22 and prints all detected device addresses in hexadecimal. Use Wire library.

Example Result:

#include <Wire.h>
void setup() {
  Wire.begin(21, 22);
  Serial.begin(115200);
  Serial.println("I2C Scanner");
}

void loop() {
  byte error, address;
  int nDevices = 0;
  for (address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    if (error == 0) {
      Serial.print("I2C device found at 0x");
      if (address < 16) Serial.print("0");
      Serial.println(address, HEX);
      nDevices++;
    }
  }
  if (nDevices == 0) Serial.println("No I2C devices found");
  delay(5000);
}

This is a classic utility. The AI correctly handles the 7-bit address range and formats output.

5. SPI Communication with a Shift Register (74HC595)

Task: Control 8 LEDs via SPI and a 74HC595 shift register.

Prompt:

Write C code for STM32F4 using HAL SPI1 to send a byte to a 74HC595 shift register. Configure SPI1 as master, 8-bit, MSB first, prescaler 16. Use PA5 as SCK, PA7 as MOSI, PA4 as GPIO output for latch (RCLK). Provide a function to set individual LEDs.

Example Result:

SPI_HandleTypeDef hspi1;

void SPI1_Init(void) {
  hspi1.Instance = SPI1;
  hspi1.Init.Mode = SPI_MODE_MASTER;
  hspi1.Init.Direction = SPI_DIRECTION_1LINE;
  hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
  hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
  hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
  hspi1.Init.NSS = SPI_NSS_SOFT;
  hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
  hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
  HAL_SPI_Init(&hspi1);
}

void setLEDs(uint8_t value) {
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
  HAL_SPI_Transmit(&hspi1, &value, 1, 100);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
}

The AI correctly uses software NSS and toggles the latch pin. Note: for 74HC595, you need to toggle RCLK after transmission.

6. Timer PWM Generation on STM32

Task: Generate a PWM signal on a timer channel.

Prompt:

Write C code for STM32F4 using HAL to generate PWM on TIM3 Channel 1 (PA6) with frequency 1 kHz and 50% duty cycle. Assume APB1 timer clock is 84 MHz. Include initialization and start function.

Example Result:

TIM_HandleTypeDef htim3;

void TIM3_PWM_Init(void) {
  TIM_OC_InitTypeDef sConfigOC = {0};
  htim3.Instance = TIM3;
  htim3.Init.Prescaler = 84 - 1; // 84 MHz / 84 = 1 MHz
  htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim3.Init.Period = 1000 - 1; // 1 MHz / 1000 = 1 kHz
  htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  HAL_TIM_PWM_Init(&htim3);
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 500; // 50% duty
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}

The AI correctly calculates prescaler and period for 1 kHz. You must also configure PA6 as alternate function in GPIO init.

Expert Level: RTOS, DMA, and Low-Power Modes

7. FreeRTOS Task Creation on ESP32

Task: Create two tasks with different priorities and stack sizes.

Prompt:

Write Arduino code for ESP32 using FreeRTOS to create two tasks: Task1 toggles LED on GPIO2 every 500ms with priority 1, Task2 reads analog value from GPIO34 every 100ms with priority 2 and prints to Serial. Use xTaskCreatePinnedToCore to pin Task1 to core 0 and Task2 to core 1. Include stack size of 2048 words for each.

Example Result:

#include <Arduino.h>

void Task1(void *pvParameters) {
  pinMode(2, OUTPUT);
  while (1) {
    digitalWrite(2, !digitalRead(2));
    vTaskDelay(500 / portTICK_PERIOD_MS);
  }
}

void Task2(void *pvParameters) {
  while (1) {
    int val = analogRead(34);
    Serial.println(val);
    vTaskDelay(100 / portTICK_PERIOD_MS);
  }
}

void setup() {
  Serial.begin(115200);
  xTaskCreatePinnedToCore(Task1, "Task1", 2048, NULL, 1, NULL, 0);
  xTaskCreatePinnedToCore(Task2, "Task2", 2048, NULL, 2, NULL, 1);
}

void loop() {}

The AI correctly uses xTaskCreatePinnedToCore and vTaskDelay. Note: portTICK_PERIOD_MS is typically 1 ms on ESP32.

8. DMA Transfer for ADC on STM32

Task: Configure ADC with DMA to continuously sample and store values in a buffer.

Prompt:

Write C code for STM32F4 using HAL to configure ADC1 with DMA2 Stream0 to sample channel 0 (PA0) continuously into a buffer of 100 elements. Use circular mode, 12-bit resolution, and software trigger. Include DMA interrupt to process half-buffer.

Example Result:

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
uint16_t adc_buffer[100];

void ADC_DMA_Init(void) {
  hadc1.Instance = ADC1;
  hadc1.Init.Resolution = ADC_RESOLUTION_12B;
  hadc1.Init.ContinuousConvMode = ENABLE;
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc1.Init.NbrOfConversion = 1;
  HAL_ADC_Init(&hadc1);

  HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 100);
}

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) {
  // Process first half of buffer
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
  // Process second half of buffer
}

The AI correctly uses HAL_ADC_Start_DMA and provides callbacks for half and full transfer. Ensure DMA is configured in circular mode in HAL_ADC_MspInit.

9. Low-Power Mode with Wake-Up on Interrupt

Task: Put STM32 into STOP mode and wake up on external interrupt.

Prompt:

Write C code for STM32L4 using HAL to enter STOP mode with RTC wake-up every 10 seconds. Configure EXTI line 13 (PA13) as wake-up source on rising edge. After wake-up, toggle LED on PB0 and re-enter STOP mode.

Example Result:

void enter_stop_mode(void) {
  HAL_SuspendTick();
  HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
  SystemClock_Config(); // Reconfigure clock after wake-up
  HAL_ResumeTick();
}

void EXTI15_10_IRQHandler(void) {
  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
  if (GPIO_Pin == GPIO_PIN_13) {
    HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
  }
}

The AI correctly suspends SysTick and reconfigures the clock. Note: after STOP mode, the system clock defaults to MSI, so you must call SystemClock_Config().

10. Debugging Hard Fault on Cortex-M

Task: Decode a hard fault exception and find the offending instruction.

Prompt:

Explain how to debug a HardFault on STM32F4. Provide code to extract the stacked registers (R0-R3, R12, LR, PC, xPSR) from the stack frame in the HardFault_Handler and print them via UART. Assume the stack pointer is MSP.

Example Result:

void HardFault_Handler(void) {
  __asm volatile (
    "tst lr, #4\n"
    "ite eq\n"
    "mrseq r0, msp\n"
    "mrsne r0, psp\n"
    "b hard_fault_handler_c\n"
  );
}

void hard_fault_handler_c(uint32_t *hardfault_args) {
  volatile uint32_t stacked_r0 = hardfault_args[0];
  volatile uint32_t stacked_r1 = hardfault_args[1];
  volatile uint32_t stacked_r2 = hardfault_args[2];
  volatile uint32_t stacked_r3 = hardfault_args[3];
  volatile uint32_t stacked_r12 = hardfault_args[4];
  volatile uint32_t stacked_lr = hardfault_args[5];
  volatile uint32_t stacked_pc = hardfault_args[6];
  volatile uint32_t stacked_psr = hardfault_args[7];
  printf("PC = 0x%08X\n", stacked_pc);
  while (1);
}

The AI correctly uses inline assembly to determine which stack was active. The PC value points to the instruction that caused the fault.

Conclusion: From Datasheet Hell to Productive Prototyping

Embedded development doesn't have to be a solitary struggle with thousand-page reference manuals. AI models, when prompted with precision, can generate correct initialization code, explain peripheral behavior, and even help debug hard faults. The key is to be specific: mention the exact microcontroller, clock speed, pin numbers, and library version. Treat the AI as a junior engineer who knows the syntax but needs your architectural guidance.

Start with the basic prompts to generate boilerplate, then move to advanced ones for protocol integration, and finally use expert prompts for RTOS and low-power optimization. Always verify the generated code against the official reference manual — AI can hallucinate register names or miss errata. With practice, you'll cut your development time significantly and focus on the creative parts of embedded design.

For more practical guides on AI-assisted development, explore our blog at asibiont.com/blog.

← All posts

Comments