From Datasheet to Firmware: The Embedded Engineer's Prompt Playbook for Arduino, ESP32 and STM32
Every embedded developer knows the ritual: a new sensor arrives, you open a 600-page reference manual, and somewhere around page 412 you find that one bit in a control register that explains why your I2C bus is silent. The datasheet is not the enemy — it is the ground truth. But the time spent hopping between register maps, errata sheets, and HAL documentation is time not spent at the bench.
This is where a well-constructed prompt changes the workflow. Not by replacing the datasheet, but by turning it into a conversation. A good prompt asks the model to locate, summarize, and cross-check specific register behavior, then produce C code you can compile — while you keep the oscilloscope on the SPI lines. The prompts below are the ones I keep coming back to across Arduino, ESP32, and STM32 projects, from a blinking LED to an MQTT-connected battery sensor that must survive a winter on a coin cell.
One rule before we start: never paste a full confidential datasheet into a public model without checking your NDA. Instead, extract the relevant section, or describe the peripheral and ask the model to reason about the documented behavior you quote. Treat every generated line as a hypothesis to verify on hardware.
1. The Register Decoder: turning a bitfield into readable C
Task: Convert a peripheral register description into named bitfields and a set of readable macros.
Prompt:
You are an embedded C expert. Below is the description of the USART_CR1 register
from an STM32 reference manual (RM0008 section 27.6.4). Extract every bit, its
name, offset and reset value. Produce a C header with:
1) a union/struct bitfield definition,
2) volatile uint32_t access macros,
3) a short comment for each field explaining its effect.
Flag any field marked 'reserved' and warn about read-modify-write hazards.
Example result: The model returns a typedef union { struct { uint32_t UE:1; uint32_t UESM:1; ... }; uint32_t reg; } USART_CR1_t; plus macros like #define USART_CR1_UE (1U << 0). You paste it into your CMSIS-style header and compile. The value is not the code itself — it is the cross-check: when the model mislabels a reserved bit, you catch it because you are reading the same page.
2. Peripheral Bring-Up Checklist for UART, I2C, SPI
Task: Generate an ordered initialization sequence for a peripheral, including clock enable and pin muxing.
Prompt:
Generate a step-by-step bring-up checklist for I2C1 on an STM32F4 in master mode
at 100 kHz, using the standard peripheral library. For each step give the register
or HAL call, the expected value, and how to verify it with a logic analyzer.
Include the most common failure mode for each step.
Example result: A numbered list: enable RCC_APB1ENR.I2C1EN, configure GPIO as alternate function open-drain with pull-ups, set I2C_CR2.FREQ to the APB1 clock in MHz, program CCR for 100 kHz, set I2C_CR1.PE, then poll I2C_SR1.SB. The failure note for pin muxing — "forgot to set the alternate function number, bus stays high" — has saved me more than one afternoon.
3. PWM and ADC: from formula to working timer config
Task: Compute timer prescaler and auto-reload values, then generate the code.
Prompt:
I have an STM32 running at 84 MHz. I need a 20 kHz PWM on TIM3 channel 1 with
0-100% duty, and a 12-bit ADC sampling on PA0 triggered by the same timer.
Compute PSC and ARR, show the math, then give me the HAL init code and a
function to set duty cycle as a percentage. Explain the ADC trigger source choice.
Example result: The model shows 84 MHz / 20 kHz = 4200 ticks, picks PSC = 0, ARR = 4199, and produces HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1) with __HAL_TIM_SET_COMPARE. It correctly suggests ADC_EXTERNALTRIGCONV_T3_TRGO so the ADC samples synchronously with the PWM — a detail that is easy to miss and hard to debug later.
4. Datasheet Q&A with page citations
Task: Ask targeted questions and demand the source section for each answer.
Prompt:
Based on the ESP32 technical reference manual, answer:
1) What is the maximum I2C clock frequency in slave mode?
2) Which GPIO pins can be used for ADC2, and why does ADC2 conflict with Wi-Fi?
3) What is the deep-sleep current with RTC memory retained?
For each answer cite the chapter and section. If the manual does not state it,
say so explicitly instead of guessing.
Example result: A structured answer with citations like "Chapter 4, I2C Controller". The ADC2/Wi-Fi conflict is real and well documented — the model explains that ADC2 is shared with the Wi-Fi peripheral, so reads fail while the radio is active. That single insight redirects a design from ADC2 to ADC1 before the PCB is ordered.
5. Firmware Skeleton Generator for Arduino and ESP32
Task: Produce a non-blocking firmware skeleton with state machine and watchdog.
Prompt:
Write an Arduino/ESP32 sketch skeleton for a sensor node that:
- reads a BME280 over I2C every 60 seconds,
- publishes over MQTT to a broker,
- never uses delay() in the main loop,
- includes a millis()-based scheduler and an ESP task watchdog,
- handles Wi-Fi reconnect without blocking.
Use a simple state machine. Comment each transition.
Example result: A sketch with enum State { CONNECTING, READING, PUBLISHING, IDLE }, a millis() timer, esp_task_wdt_reset() calls, and a WiFi.setAutoReconnect(true) plus an explicit reconnect branch. The model's non-blocking pattern is the difference between a node that hangs on a dropped access point and one that recovers silently.
6. Debugging by Logs: reading a crash dump
Task: Interpret an ESP32 panic backtrace and propose a fix.
Prompt:
Here is an ESP32 Guru Meditation error after a reboot:
Guru Meditation Error: Core 1 panic'ed (LoadProhibited).
Backtrace: 0x400d1234:0x3ffb1f60 0x400d5678:0x3ffb1f80 ...
Explain what LoadProhibited means, how to map the addresses to functions with
addr2line, and list the three most likely causes in a FreeRTOS task context.
Give commands to decode the backtrace using xtensa-esp32-elf-addr2line.
Example result: The model explains that LoadProhibited is a load from an invalid address — usually a NULL or freed pointer. It supplies xtensa-esp32-elf-addr2line -pfiaC -e firmware.elf 0x400d1234, which maps the address to a function and line. Typical causes it lists: a task using a queue handle before creation, a stack overflow, or a pointer to a deleted object.
7. Oscilloscope Trace Interpretation
Task: Describe a waveform in text and get a diagnosis.
Prompt:
My I2C SDA line on an STM32 shows a rising edge that takes 1.5 microseconds to
reach 3.3 V with a 4.7k pull-up, and the ACK bit is sometimes read as NACK.
Bus speed is 400 kHz. Analyze the RC time constant, tell me whether the pull-up
is too weak, and recommend a resistor value with the math.
Example result: The model computes the bus capacitance implied by the rise time, notes that I2C Fast Mode specifies a maximum rise time of 300 ns, and recommends dropping to 2.2k or 1.8k pull-ups. You verify the actual rise time on the scope. The prompt does not replace measurement — it tells you what to measure and why.
8. MQTT and Home Assistant Integration
Task: Generate discovery payloads and a topic structure.
Prompt:
I have an ESP32 publishing temperature and humidity. Generate:
1) a clean MQTT topic tree,
2) a Home Assistant MQTT discovery payload for a temperature sensor,
3) the Arduino PubSubClient code to publish retained state and an availability topic
with last-will-and-testament.
Explain why the LWT matters for a battery node.
Example result: Topics like home/sensor1/state, home/sensor1/availability, a discovery JSON published to homeassistant/sensor/sensor1_temp/config, and client.setWill("home/sensor1/availability", "offline", true, true). The explanation of LWT — the broker publishes "offline" automatically if the node drops — is exactly what makes a battery sensor trustworthy in a dashboard.
9. Power Budget and Sleep Optimization
Task: Estimate battery life and reduce deep-sleep current.
Prompt:
Estimate the battery life of an ESP32 node powered by a 2000 mAh LiPo that wakes
every 10 minutes, spends 8 seconds awake at 80 mA average, and sleeps at 10 uA.
Show the math in mAh per day. Then list concrete ways to reduce awake time,
including light sleep vs deep sleep and disabling the Wi-Fi modem between sends.
Example result: The model computes awake energy per day and sleep energy, then totals them. Recommendations include batching readings, using WiFi.mode(WIFI_OFF) between transmissions, and choosing light sleep when you need to preserve RAM. Check the actual numbers against the datasheet's sleep current table — the model gives you the framework, the datasheet gives you the constant.
10. Code Review Prompt for Embedded C
Task: Audit firmware for portability, undefined behavior, and ISR safety.
Prompt:
Review this embedded C function as a senior firmware engineer. Look for:
- undefined behavior (signed overflow, unaligned access, volatile misuse),
- variables shared with an ISR without volatile or atomics,
- blocking calls inside an interrupt handler,
- missing bounds checks on buffer writes.
Return a table: line, issue, severity, fix.
Example result: A Markdown table flagging a shared uint32_t ticks counter missing volatile, a sprintf inside an ISR (severity high), and an off-by-one in a ring buffer. The table format makes it easy to paste into a pull request review on GitHub.
11. Errata and Silicon Revision Check
Task: Cross-reference a known issue with your silicon revision.
Prompt:
The STM32F103 errata mentions a limitation on the I2C peripheral. Summarize the
known I2C errata for this family, which silicon revisions are affected, and what
workarounds exist in the HAL. Tell me how to check the revision ID at runtime.
Example result: A summary of the documented I2C issues and the HAL workarounds, plus code to read the revision via the DBGMCU ID register or the HAL_GetREVID() helper. This is the prompt that turns an "impossible" bus lockup into a known erratum with a documented fix.
12. Unit Tests for Bare-Metal Logic with a Mock HAL
Task: Make firmware logic testable on a host machine.
Prompt:
Refactor this firmware module so the hardware access goes through a function-pointer
HAL struct. Then generate Unity test cases that run on a PC with a mocked HAL,
covering normal operation, sensor timeout, and a corrupted CRC.
Example result: A typedef struct { int (*i2c_read)(...); } hal_t; plus Unity tests using TEST_ASSERT_EQUAL. You now run make test on your laptop before flashing. This is the single biggest reliability upgrade for firmware that talks to sensors.
| Prompt | Best for | Key output |
|---|---|---|
| Register Decoder | New MCU, unfamiliar peripheral | C header with bitfields |
| Bring-Up Checklist | First power-on of a bus | Ordered steps + failure modes |
| PWM/ADC Math | Timers and analog chains | PSC/ARR + HAL code |
| Datasheet Q&A | Pin/clock/current questions | Cited answers |
| Firmware Skeleton | Starting a new node | Non-blocking state machine |
| Crash Debug | ESP32 panics | addr2line commands + causes |
| Scope Interpretation | Signal integrity issues | Resistor/timing math |
| MQTT/HA | IoT dashboards | Discovery payloads + LWT |
| Power Budget | Battery devices | mAh/day + sleep tips |
| Code Review | Pre-merge audit | Line-by-line issue table |
| Errata Check | Mysterious lockups | Workarounds + REVID code |
| Host Tests | Reliability | Unity tests + mock HAL |
The pattern across all twelve prompts is the same: you bring the ground truth from the datasheet, the model brings structure, code, and a checklist. Neither alone gets you to a working board faster. Together they cut the gap between "schematic done" and "firmware flashing" dramatically — and you spend more time with the logic analyzer, which is where the real answers live. Keep your datasheets open, verify every register value on hardware, and let the prompts handle the paperwork.
Comments