Design Intent, Plans and Requirements

When we talk about design intent we might not consider at what “level” or what “kind” of intent. Agentic coding CLIs allow us to express our intent as free text. It could be a mix of stakeholder goals, detailed component requirements, interface requirements etc. Consider our naive weather station prompt from last week:

Prompt: Create an application that reads temperature and humidity from the attached BME280 via I2C every 10 s. The application shall correct the readings and print them to the serial console as degrees C and percent relative humidity respectively.

This is a requirement masquerading as a request, but its expected result is a low level implementation in code, so it leaves out important details on how to read the temperature, correct the readings and print them. All of those details will be inferred. With the agentic AI CLIs we could ask for a plan that we can correct iteratively before we allow the agent to write any code. This allows us loop on some of the assumptions that resulted from our initial prompt before any code is written. But the plans are in terms of the delta between the current state and the interpretation of the next goal state, and they are thrown away once acted upon, so they are not really useful to retain as requirements.

In embedded projects we traditionally manage requirements in layers such as stakeholder, system and software requirements. Then we further split them into functional and non-functional, design and interface requirements. How we do this and the exact language we use depends on what industry and what kind of certification we are targeting, but the purpose of the entire exercise is to enable organisations to develop products that adhere to strict safety and quality standards. If an organisation is an entity consisting of people with different experiences and motivations that exhibit non-deterministic behaviour and outputs, perhaps a similar approach can work for AI agents?

A Layered Requirements Model

The following graph shows a hierarchy of system and software engineering requirement types and their relationships. In this example, the terminology derives mainly from IEEE 29148/1016 and DO-178C and the language varies across industries and safety/integrity criticality levels but the concepts are quite similar in between. This can be a lot to process if not familiar with Systems Engineering concepts or the V-model, but gives us the terminology we need to reason about how we can improve our approach. Skim this part if it feels dense, and use it as reference for later if needed.

flowchart TD
    L1["Stakeholder Requirements (StRS)"]:::standard
    L2["System Requirements (SyRS)"]:::standard

    subgraph HW_Branch ["Hardware Engineering (simplified)"]
        direction TB
        HW_Arch["HW Architecture (Compute, Power, IO)"]:::hardware
        HW_Schematic["HW Design (Schematics, BOM)"]:::hardware
        HW_Phys["Physical Silicon & Traces (The board)"]:::hardware
        HW_Arch --> HW_Schematic --> HW_Phys
    end

    subgraph SW_Branch ["Software Engineering"]
        direction TB
        L3["Software Requirements (SRS / HLR)
Consumes HSI as input"]:::software subgraph L4 ["SAD: Architectural Partitioning"] direction TB App_Layer["Application Layer Partition
(Domain Logic Structure)"]:::l4box HAL_Layer["HAL Partition
(Hardware Abstraction Structure)"]:::l4box end L5["Unit/Component Requirements (LLR)
(Component Logic, Interfaces & Contracts)"]:::software L3 --> L4 App_Layer -->|Component & resource allocation| L5 HAL_Layer -->|Driver & abstraction allocation| L5 end HSI{"[HSI Document]
Physical / Digital Contract
(Registers, Interrupts, Memory Map, Timing)"}:::hsi L1 -->|Decomposes into| L2 L2 -->|Hardware allocation| HW_Arch L2 -->|Software allocation| L3 HW_Phys -.->|Defines physical interface| HSI HSI -.->|Consumed by SW requirements process| L3

See Appendix for definitions.

Interface Mechanisms

In addition to requirements we need to reason about interfaces as they allow us to separate concerns between layers and domains. This graph shows examples of interface mechanisms in our embedded context that can be versioned independently of other requirements.

flowchart TD
    subgraph APP ["Application Layer"]
        direction LR
        A["Control"]:::software
        B["Processing"]:::software
        C["Diagnostics"]:::software
    end

    subgraph IFC ["Interface Mechanisms"]
        direction LR
        API["API Contracts
Headers"]:::icd SHR["Shared Data
Queues/Mem"]:::icd BLD["Build Config
Devicetree"]:::icd end subgraph PROV ["Provider Partition"] direction LR X["Comm Peripheral (HAL driver)"]:::software Y["Sensor Interface (HAL driver)"]:::software Z["Storage Backend (HAL driver)"]:::software F["Message Queue (shared component)"]:::software L["Logger / Telemetry (shared component)"]:::software end %% Edge lengths and consolidated bidirectional arrows APP -->|Calls| API API -->|Fullfills| PROV APP <-->|Cmds & Data| SHR PROV <-->|Cmds & Data| SHR BLD -.->|Constrains| APP BLD -.->|Constrains| PROV

The Software Requirements for the Weather Station Example

Our example project is too trivial for a complete set of process documents, so we will go directly from System Requirements to the Software Requirements Specification layer with an inline Software Architectural Description section for design requirements and Interface Control Document for interfaces. We will also assert the combined board-and-overlay DeviceTree are granular enough to serve as our HSI. The Devicetree contains only a subset of what must be known to succesfully implement HAL and drivers, but it is enough to map our application layer code to the drivers already provided with Zephyr. We will state in our requirements that they must be resolved to code without introducing new HAL or driver components. Finally we are assuming that the LLRs can be inferred completely and that we don’t need to spell them out. This is a leap we can afford in our example since we don’t have stringent certification requirements, and that will prove itself experimentally.

First we establish a set of System Requirements (SysRS). Each requirement needs a unique ID which is a bit awkward in markdown but it will become clear later why this is needed. As you can see only the first requirement SyRS-WX-001 is reflected in our naive prompt, and we have taken out the references to hardware as it will be implied by the HSI in another layer. We also add requirements for the monitoring we introduced in the previous post, and clarified that the error state indication must also propagate to console.

IDSystem Requirement
SyRS-WX-001The system shall acquire ambient temperature and humidity measurements every 10 seconds and output the readings to the serial console.
SyRS-WX-002The system shall make the most recent, physically valid temperature and humidity measurements continuously available to other internal system functions.
SyRS-WX-003The system shall detect a sensor communication loss if valid data is not received for 30 seconds, and notify the user via the serial console.
SyRS-WX-004The system shall provide a continuous visual warning via a dedicated hardware indicator (LED) while the sensor communication loss condition is active.

As per the layered model in the diagram, we combine these requirements with the HSI and get the following combined Software Requirements Specification (SRS): weatherstation_srs.md

Generating code from SRS

Prompt: Read the repository that contains an empty application for the nRF54L15 on the nRF54L15-DK board with a pre-configured DeviceTree. Read the specification from specs/weatherstation_srs.md. Generate a step-by-step plan for the implementation. For every function, struct, or significant logic block you write, you must include a comment tagging the specific requirement ID it fulfills (e.g., // Implements: SRS-WX-022).

Gemini said:

I have developed a plan based on the SRS and the DeviceTree overlay. I’ll now present it for your approval.

Full Gemini response:Implementation Plan for Weather Station Application

Phase 1: Environment & Configuration

  1. Configuration (prj.conf): Enable required Zephyr subsystems: CONFIG_SENSOR, CONFIG_BME280, CONFIG_I2C, CONFIG_GPIO, and CONFIG_CBPRINTF_FP_SUPPORT for floating-point printk.
  2. Shared Memory Implementation:
    • Create src/Weather_SharedMem.h and src/Weather_SharedMem.c.
    • Define Weather_SharedData_t as per §5.1.
    • Implement Weather_GetSharedData() to return a pointer to a singleton instance.
    • Initialize with sentinel values: temperature_degC = -999.0f, humidity_pctRH = 0xFF, update_counter = 0, error_active = false.
    • Implement a mutex mechanism to ensure atomicity of updates (Requirement: §5.1).

Phase 2: Core Components 3. Weather Sensor Component (src/weather_sensor.c/h): * Define Weather_Sensor_Task as a thread with a 10s period (SRS-WX-001). * Initialize BME280 using device_is_ready(). * Implement the measurement loop: * Fetch samples from BME280 (SRS-WX-002). * Validate range: T (-40 to +85°C), RH (0 to 100%) (SRS-WX-023). * Handle fetch/get errors by retaining previous values and omitting updates (SRS-WX-022). * On success: Update shared memory, increment counter atomically, and store timestamp (SRS-WX-003, SRS-WX-004, SRS-WX-005). * Console output: Print formatted values or error message depending on error_active state (SRS-WX-006, SRS-WX-007).

  1. Staleness Monitor Component (src/weather_monitor.c/h):
    • Define Staleness_Monitor_Task as a thread with a 1s period (SRS-WX-020).
    • Compare the current time with the last successful update timestamp.
    • If the gap exceeds 30s: Set error_active = true and assert the fault LED (SRS-WX-020, SRS-WX-008).
    • If gap <= 30s: Set error_active = false and deassert the fault LED (SRS-WX-021, SRS-WX-009).

Phase 3: Integration & Validation 5. Main Entry (src/main.c): * Start both task threads. * Ensure all functions and logic blocks are tagged with Requirement IDs (e.g., // Implements: SRS-WX-001).

Does this plan meet your expectations? Please let me know if you’d like any adjustments before I proceed with the implementation.

Prompt: Yes (The Gemini plan was accepted without modification and it provided a summary of the results.)

Gemini said:

Implementation Summary: …

Full Gemini response:
  1. Configuration: Updated prj.conf with CONFIG_SENSOR, CONFIG_BME280, CONFIG_I2C, CONFIG_GPIO, and floating-point support for printk.
  2. Shared Memory Component:
    • src/Weather_SharedMem.h/c: Implements the singleton data structure with mutex protection.
    • Uses SYS_INIT to ensure initialization with sentinel values before application threads start.
  3. Weather Sensor Component:
    • src/weather_sensor.h/c: Implements a 10s periodic task that fetches data from the BME280, validates it against physical ranges, updates shared memory, and prints to the console as per §5.2.
  4. Staleness Monitor Component:
    • src/weather_monitor.h/c: Implements a 1s periodic task that monitors the last update timestamp. If data is older than 30s, it sets the error flag and asserts the fault LED (led0).
  5. Integration:
    • src/main.c: Defines and starts the task threads.
    • CMakeLists.txt: Updated to include all source files.


The code builds without errors. Below we can see the difference between our previous approach and the new approach with our consolidated SRS requirements model.

Console output:

T: 22.0 C, RH: 29 %
T: 22.2 C, RH: 28 %
T: 22.2 C, RH: 28 %
...


main.c:

#include <zephyr/kernel.h>
#include "Weather_SharedMem.h"
#include "weather_sensor.h"
#include "weather_monitor.h"

// Implements: §4.2 Task Mapping
#define STACK_SIZE 1024
#define PRIORITY 7

K_THREAD_DEFINE(weather_sensor_tid, STACK_SIZE,
                Weather_Sensor_Task_Entry, NULL, NULL, NULL,
                PRIORITY, 0, 0);

K_THREAD_DEFINE(staleness_monitor_tid, STACK_SIZE,
                Weather_Monitor_Task_Entry, NULL, NULL, NULL,
                PRIORITY, 0, 0);

int main(void)
{
    // Tasks started by K_THREAD_DEFINE
    return 0;
}

weather_sensor.c:

void Weather_Sensor_Task_Entry(void *p1, void *p2, void *p3) {
    const struct device *const dev = DEVICE_DT_GET_ANY(bosch_bme280);

    // Implements: §3.3 Operational Modes (INIT)
    if (dev == NULL || !device_is_ready(dev)) {
        printk("Error: Device \"%s\" is not ready; check the devicetree.\n", dev->name);
        return;
    }
    while (1) {
        struct sensor_value temp, humid;
        int rc;

        // Implements: SRS-WX-001 (Measurement Period 10s)
        int64_t next_run = k_uptime_get() + K_SECONDS(MEASUREMENT_PERIOD_S).ticks;

        // Implements: SRS-WX-002 (Retrieve data via sensor_sample_fetch)
        rc = sensor_sample_fetch(dev);

        if (rc == 0) {
            // Implements: SRS-WX-002 (sensor_channel_get)
            sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, &temp);
            sensor_channel_get(dev, SENSOR_CHAN_HUMIDITY, &humid);

            float32_t t_val = sensor_value_to_double(&temp);
            float32_t h_val = sensor_value_to_double(&humid);

            // Implements: SRS-WX-023 (Physical Range Check)
            if (t_val >= TEMP_MIN_DEGC && t_val <= TEMP_MAX_DEGC &&
                h_val >= (float32_t)HUMID_MIN_PCT && h_val <= (float32_t)HUMID_MAX_PCT) {
                
                // Implements: SRS-WX-003, SRS-WX-004, SRS-WX-005 (Update shared memory)
                Weather_UpdateSharedData(t_val, (uint8_t)h_val);
            } else {
                // Implements: SRS-WX-023 (Discard invalid readings)
            }
        } else {
            // Implements: SRS-WX-022 (Discard reading on sensor fetch error)
        }

        const volatile Weather_SharedData_t* shared = Weather_GetSharedData();

        // Implements: SRS-WX-006, SRS-WX-007, §5.2 (UART output format)
        if (shared->error_active) {
            // Implements: SRS-WX-007 (Error format)
            printk("Error: Sensor communication failure\n");
        } else if (shared->update_counter > 0) {
            // Implements: SRS-WX-006 (Normal format)
            printk("T: %.1f C, RH: %u %%\n", 
                   (double)shared->temperature_degC, 
                   (unsigned int)shared->humidity_pctRH);
        }

        // Wait for next cycle
        k_sleep(K_TIMEOUT_ABS_MS(next_run));
    }
}

Full output: step1

See the difference? Unlike our previous attempts, the link between intent and code becomes clear. We previously discovered that the LLM we are using has enough training on Zephyr and our board to successfully reason about DeviceTree, Kconfig and other concepts we depend on for the design. Now we have introduced more granular, clear and explicitly separated functional requirements alongside design and interface sections for precision, and introduced a persistence format in the form of a consolidated Software Requirements Specification. We also added traceability to code in the generation step which makes the mapping between requirements and code easy to see and understand. It is much less likely that we will be breaking this implementaiton unintentionally now as the requirements change.

Summary and Next Steps

Next time we will explore the benefits of this approach and how we can use LLMs to manage the increase in granularity of instructions that we added to the workflow.

Appendix: System Requirements Concepts Reference

Abbr.NameDescriptionOriginAlso Known As
StRSStakeholder Requirements SpecificationFormally stated, traceable requirements from stakeholders with measurable acceptance criteria.IEEE 29148
SyRSSystem Requirements SpecificationBlack-box system behavior, technology-agnostic. What the complete system does, not how.IEEE 29148
HSIHardware/Software InterfaceBoundary contract between HW and SW. Defines registers, interrupts, memory maps, timing, etc.DO-178CHW/SW ICD
SRSSoftware Requirements SpecificationWhat the software must do, derived from system requirements and constrained by the HSI. The document that contains HLRs.IEEE 29148HLR document (DO-178C)
LLRLow-Level RequirementA software requirement detailed enough that source code can be directly implemented from it without further information.DO-178C; ASPICE SWE.3Detailed SW Requirement, Module/Component/Unit Requirement
SADSoftware Architectural DesignPartitioning of software requirements into components, relationships, interfaces, resource allocation, and design rationale.ASPICE SWE.2; DO-178C; IEEE 1016Software Architecture Document, SDD (ambigous acronym)
ICDInterface Control DocumentThe complete, controlled description of an interface between two systems or components. Contains both requirements (IRS) and design (IDD).Industry conventionInterface Spec, Protocol Spec. Contains both IRS and IDD content
IRSInterface Requirements SpecificationThe requirements side of an interface — what it shall do. Testable behavioral statements independent of implementation.IEEE 29148Interface Requirement Document
IDDInterface Design DescriptionThe design side of an interface — how it is implemented. Concrete representations: byte formats, register addresses, protocol encoding.IEEE 1016Interface Design Document, Wire Format Spec
HALHardware Abstraction LayerThe software layer that isolates application logic from hardware specifics. Provides a stable API that survives hardware changes.Industry convention