From Concept to Code: My Iterative Journey to Multiplexing Module with ChatGPT

From Concept to Code: My Iterative Journey to Multiplexing Module with ChatGPT

I started with a pretty straightforward wish. I had an ESP32 with just one accessible analog pin, but I needed to read 16 analog channels. I explained my predicament to ChatGPT o3-mini-high model, saying, “I only have one channel, but I need to read 16 channels.” ChatGPT responded with several options—external ADCs, multiplexing, and more. I considered the choices and decided that using an analog multiplexer, specifically the CD74HC4067, was the best route for me. That was the spark that lit the fire for a much deeper discussion.

Step 1: The Initial Wish

I began by stating that I wanted to use the ESP32 to read 16 analog channels even though I had only one analog input available. ChatGPT quickly provided several options. He mentioned using analog multiplexers, external ADCs, and even reassigning pins if possible. In the end, I chose to use the CD74HC4067 multiplexer. At that point, I hadn’t yet detailed my desired software architecture or any timing requirements. Later, I asked for possible architectural designs and a set of requirements to support my goal.

Step 2: Architectural Design and Requirements

I then asked ChatGPT to come up with an architectural design for a software module that would interface with the CD74HC4067. He presented a layered design that separated hardware abstraction, multiplexer control, ADC reading, and task integration. I appreciated the structure—it was thorough and organized—but I noticed it was missing details on real-time performance and compile?time parameterization. So I requested that every adjustable parameter be defined as a preprocessor directive, and that the design include an RTOS Task/Thread approach with a queue to hold a timestamp for each set of 16 channel readings.

Step 3: Adding RTOS and Timestamps

With the idea now clearly taking shape, I confirmed that I was going to use the CD74HC4067. I specifically asked that a dedicated RTOS task be created to sequentially scan all 16 channels and that each full set of readings include a timestamp indicating when the reading was finished. ChatGPT updated the design accordingly, incorporating an ADC & sampling manager layer and specifying that the data package would contain both the channel data and the timestamp. At this point, I felt the design was solid but still needed more specific timing calculations.

Step 4: Timing Calculations and Sampling Modes

Next, I asked for a calculation of how many milliseconds it would take to read all 16 channels. ChatGPT provided several example scenarios based on different settling and ADC conversion delays. I liked the explanation but then pushed further: I wanted to add a parameter (set at compile time) that could take values like Fast Mode, Moderate Mode, or HighRes. This parameter would automatically adjust the multiplexer settling time and ADC conversion delay according to datasheet figures and previous measurements. The design evolved further with these added requirements.

Step 5: Complete Module Implementation

Then I requested a complete module—both .cpp and .h files—that incorporated all the requirements so far, including unit test functions. ChatGPT produced a detailed module, with all the adjustable parameters defined as preprocessor directives and real timing values chosen based on datasheet data (for instance, using a conversion time of around 11?μs for the ESP32). It even came with unit tests. While I was impressed with the thoroughness, I noticed that the code still used some blocking waits (for example, calls like ets_delay_us), which wasn’t acceptable for my real-time needs.

Step 6: The Non?Blocking Mandate

Finally, I insisted that no blocking waits be used anywhere in the code. I demanded that every timing delay must be implemented in a non?blocking manner using a state-machine approach, with the code yielding to the scheduler rather than busy-waiting. ChatGPT revised the design one last time, converting the sampling task into a non?blocking state machine that checks elapsed time and yields control using vTaskDelay(0) and taskYIELD(). With that, the design was complete, meeting all my requirements.


Reflections on AI as a System Architect, Developer, and Hardware Developer

My journey—from stating a simple problem to receiving a complete, non?blocking module—has been nothing short of fascinating. I learned that, through iterative conversation, I could convert a vague idea into a detailed set of requirements and a production-ready code solution. While some might argue that AI might miss subtle hardware nuances or integration issues, I found that the rapid iteration allowed me to focus on refining the design rather than reinventing every detail.

I now see that AI can act as both a system architect and a developer. It can quickly suggest modular designs, help define functional and non?functional requirements, and even generate code that adheres to real-time, non-blocking constraints. Of course, human oversight remains essential for verifying and adapting AI output to real-world hardware challenges. But overall, the workflow becomes significantly faster, leaving more time for testing, optimization, and creative problem solving.

You can see the requirements and the full code below.


Functional Requirements:

  • Hardware Initialization:The module shall initialize all ESP32 digital pins for controlling the CD74HC4067 multiplexer and properly configure the ADC.
  • Multiplexer Channel Selection:The module shall enable selection of any channel (0–15) via digital outputs.
  • ADC Reading:The module shall perform ADC conversions from the multiplexer’s output.
  • RTOS Task for Sampling:A dedicated, non?blocking FreeRTOS task shall sequentially sample all 16 channels.
  • Data Packaging:After a full scan, the module shall package the 16 ADC readings along with a timestamp (in milliseconds) into a data package.
  • Queue Integration:Each data package shall be posted to a FreeRTOS queue for asynchronous processing.
  • Parameterization:Every adjustable parameter (sampling interval, ADC conversion delay, multiplexer settling delay, queue length, pin assignments, etc.) must be defined as a preprocessor directive.
  • Sampling Mode Parameter:The module shall support a compile?time selectable parameter (MODE_FAST, MODE_MODERATE, or MODE_HIGHRES) that adjusts the timing settings (such as settling time and ADC conversion delay) based on datasheet data and practical measurements.
  • Non?Blocking Operation:All timing delays must be implemented in a non?blocking manner (using a state-machine design and yielding to the scheduler) with no blocking waits.
  • Error Handling:The module shall detect and report errors (such as hardware initialization failures and queue overflows).

Non?Functional Requirements:

  • Real-Time Performance:The sampling task must operate with minimal jitter and be assigned sufficient priority.
  • Modularity & Maintainability:The design must separate hardware control, sampling logic, and RTOS interfacing to facilitate maintenance.
  • Efficiency:The module must conserve CPU cycles by avoiding blocking operations.
  • Scalability:The design should allow future enhancements (e.g., additional filtering or averaging) with minimal changes.
  • Testability:The module’s functionality must be verifiable through unit tests.


Final Code

Below is the complete final module code:

MultiplexerModule.h

cpp

#ifndef MULTIPLEXER_MODULE_H
#define MULTIPLEXER_MODULE_H

#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <stdint.h>

/*
  Module Parameters and Sampling Modes

  All adjustable parameters are defined as preprocessor directives.
  The values below are chosen based on published datasheet data and practical measurements.
*/

// Number of multiplexer channels.
#ifndef NUM_CHANNELS
  #define NUM_CHANNELS 16
#endif

// Sampling mode selection.
typedef enum {
  MODE_FAST,
  MODE_MODERATE,
  MODE_HIGHRES
} SamplingMode_t;

#ifndef SAMPLING_MODE
  #define SAMPLING_MODE MODE_FAST
#endif

// Timing parameters (in microseconds) based on sampling mode.
#if (SAMPLING_MODE == MODE_FAST)
  #define MUX_SETTLE_TIME_US     2    // Minimal settling delay.
  #define ADC_CONVERSION_TIME_US 11   // Measured ~11 μs conversion time.
#elif (SAMPLING_MODE == MODE_MODERATE)
  #define MUX_SETTLE_TIME_US     5
  #define ADC_CONVERSION_TIME_US 22
#elif (SAMPLING_MODE == MODE_HIGHRES)
  #define MUX_SETTLE_TIME_US     20
  #define ADC_CONVERSION_TIME_US 50
#else
  #error "Invalid SAMPLING_MODE selected"
#endif

// Full-scan sampling interval (in milliseconds).
#ifndef SAMPLING_INTERVAL_MS
  #define SAMPLING_INTERVAL_MS 10   // Adjust as required.
#endif

// Queue length for data packages.
#ifndef QUEUE_LENGTH
  #define QUEUE_LENGTH 10
#endif

// Hardware pin assignments (adjust as needed for your wiring).
#ifndef MUX_S0_PIN
  #define MUX_S0_PIN  25
#endif
#ifndef MUX_S1_PIN
  #define MUX_S1_PIN  26
#endif
#ifndef MUX_S2_PIN
  #define MUX_S2_PIN  27
#endif
#ifndef MUX_S3_PIN
  #define MUX_S3_PIN  14
#endif
#ifndef MUX_EN_PIN
  #define MUX_EN_PIN  12   // Optional enable pin (active LOW). If unused, tie low.
#endif
#ifndef MUX_ANALOG_PIN
  #define MUX_ANALOG_PIN  34  // Example: an ADC1 channel on the ESP32.
#endif

// Data package structure that holds a full set of readings with a timestamp.
typedef struct {
  uint16_t channelReadings[NUM_CHANNELS];  // ADC reading for each channel.
  uint32_t timestamp_ms;                   // Timestamp (in ms) when the scan finished.
} DataPackage_t;

// Public API class declaration.
class MultiplexerModule {
public:
  // Initialize the module: configure hardware, create queue, and start the sampling task.
  static void init();

  // Stop the sampling task and free allocated resources.
  static void stop();

  // Retrieve a data package from the module’s queue (non-blocking).
  static bool getData(DataPackage_t* pkg, TickType_t waitTime = 0);

  // (Optional) Set sampling mode at runtime.
  // NOTE: Dynamic mode changes are not supported because all timing parameters are compile-time.
  static void setSamplingMode(SamplingMode_t mode);

  // Unit test function (compiled only if UNIT_TEST is defined).
  #ifdef UNIT_TEST
  static void runUnitTests();
  #endif

private:
  // Enumeration of states in the non-blocking sampling state machine.
  typedef enum {
    STATE_SELECT_CHANNEL,
    STATE_WAIT_SETTLE,
    STATE_ADC_READ,
    STATE_WAIT_ADC,
    STATE_NEXT_CHANNEL,
    STATE_FULL_SCAN_WAIT
  } SamplingState_t;

  // The non-blocking state-machine-based sampling task.
  static void samplingTask(void* pvParameters);

  // Helper function to set the multiplexer to the specified channel.
  static void selectMuxChannel(uint8_t channel);

  // FreeRTOS queue handle for data packages.
  static QueueHandle_t muxQueue;
  
  // Task handle for the sampling task.
  static TaskHandle_t samplingTaskHandle;

  // Current sampling mode (for documentation; timing parameters remain compile-time).
  static SamplingMode_t currentMode;
};

#endif // MULTIPLEXER_MODULE_H
        

MultiplexerModule.cpp

cpp

#include "MultiplexerModule.h"

// Static member definitions.
QueueHandle_t MultiplexerModule::muxQueue = NULL;
TaskHandle_t MultiplexerModule::samplingTaskHandle = NULL;
SamplingMode_t MultiplexerModule::currentMode = SAMPLING_MODE;

// Helper function: set multiplexer control pins based on the channel (0–15).
void MultiplexerModule::selectMuxChannel(uint8_t channel) {
  digitalWrite(MUX_S0_PIN, (channel & 0x01) ? HIGH : LOW);
  digitalWrite(MUX_S1_PIN, (channel & 0x02) ? HIGH : LOW);
  digitalWrite(MUX_S2_PIN, (channel & 0x04) ? HIGH : LOW);
  digitalWrite(MUX_S3_PIN, (channel & 0x08) ? HIGH : LOW);
}

// Non-blocking sampling task implemented as a state machine.
void MultiplexerModule::samplingTask(void* pvParameters) {
  DataPackage_t pkg;
  uint8_t currentChannel = 0;
  SamplingState_t state = STATE_SELECT_CHANNEL;
  uint64_t stateStartTime = esp_timer_get_time();  // in microseconds
  uint64_t fullScanStartTime = esp_timer_get_time();
  uint16_t adcTemp = 0;

  // Run the state machine forever without blocking waits.
  for (;;) {
    uint64_t now = esp_timer_get_time();

    switch (state) {
      case STATE_SELECT_CHANNEL:
        selectMuxChannel(currentChannel);
        digitalWrite(MUX_EN_PIN, LOW);
        stateStartTime = now;
        state = STATE_WAIT_SETTLE;
        break;

      case STATE_WAIT_SETTLE:
        if (now - stateStartTime >= MUX_SETTLE_TIME_US) {
          state = STATE_ADC_READ;
        } else {
          vTaskDelay(0);
        }
        break;

      case STATE_ADC_READ:
        adcTemp = analogRead(MUX_ANALOG_PIN);
        stateStartTime = now;
        state = STATE_WAIT_ADC;
        break;

      case STATE_WAIT_ADC:
        if (now - stateStartTime >= ADC_CONVERSION_TIME_US) {
          pkg.channelReadings[currentChannel] = adcTemp;
          state = STATE_NEXT_CHANNEL;
        } else {
          vTaskDelay(0);
        }
        break;

      case STATE_NEXT_CHANNEL:
        currentChannel++;
        if (currentChannel < NUM_CHANNELS) {
          state = STATE_SELECT_CHANNEL;
        } else {
          pkg.timestamp_ms = (uint32_t)((esp_timer_get_time() - fullScanStartTime) / 1000);
          xQueueSend(muxQueue, &pkg, 0);
          currentChannel = 0;
          fullScanStartTime = esp_timer_get_time();
          state = STATE_FULL_SCAN_WAIT;
        }
        break;

      case STATE_FULL_SCAN_WAIT: {
          uint32_t elapsed_ms = (uint32_t)((esp_timer_get_time() - fullScanStartTime) / 1000);
          if (elapsed_ms >= SAMPLING_INTERVAL_MS) {
            state = STATE_SELECT_CHANNEL;
          } else {
            vTaskDelay(0);
          }
        }
        break;

      default:
        state = STATE_SELECT_CHANNEL;
        break;
    }
    taskYIELD();
  }
}

// Public initialization: configure pins, create the queue, and start the sampling task.
void MultiplexerModule::init() {
  pinMode(MUX_S0_PIN, OUTPUT);
  pinMode(MUX_S1_PIN, OUTPUT);
  pinMode(MUX_S2_PIN, OUTPUT);
  pinMode(MUX_S3_PIN, OUTPUT);
  pinMode(MUX_EN_PIN, OUTPUT);
  digitalWrite(MUX_EN_PIN, LOW);
  pinMode(MUX_ANALOG_PIN, INPUT);

  muxQueue = xQueueCreate(QUEUE_LENGTH, sizeof(DataPackage_t));
  if (muxQueue == NULL) {
    Serial.println("Error: Could not create multiplexer queue.");
    return;
  }

  BaseType_t result = xTaskCreatePinnedToCore(
    samplingTask,
    "MuxSamplingTask",
    4096,
    NULL,
    1,
    &samplingTaskHandle,
    1
  );
  if (result != pdPASS) {
    Serial.println("Error: Could not create sampling task.");
  }
}

// Public stop function.
void MultiplexerModule::stop() {
  if (samplingTaskHandle != NULL) {
    vTaskDelete(samplingTaskHandle);
    samplingTaskHandle = NULL;
  }
  if (muxQueue != NULL) {
    vQueueDelete(muxQueue);
    muxQueue = NULL;
  }
}

// Retrieve a data package from the queue (non-blocking).
bool MultiplexerModule::getData(DataPackage_t* pkg, TickType_t waitTime) {
  if (muxQueue == NULL) return false;
  return (xQueueReceive(muxQueue, pkg, waitTime) == pdPASS);
}

// Public function to set the sampling mode (runtime changes are not supported).
void MultiplexerModule::setSamplingMode(SamplingMode_t mode) {
  Serial.println("Runtime mode change not supported; set SAMPLING_MODE at compile time.");
}

#ifdef UNIT_TEST
// Unit tests.
void MultiplexerModule::runUnitTests() {
  Serial.println("Running unit tests for MultiplexerModule...");
  for (uint8_t ch = 0; ch < NUM_CHANNELS; ch++) {
    bool b0 = (ch & 0x01) != 0;
    bool b1 = (ch & 0x02) != 0;
    bool b2 = (ch & 0x04) != 0;
    bool b3 = (ch & 0x08) != 0;
    Serial.print("Channel ");
    Serial.print(ch);
    Serial.print(" expects S0=");
    Serial.print(b0 ? "HIGH" : "LOW");
    Serial.print(", S1=");
    Serial.print(b1 ? "HIGH" : "LOW");
    Serial.print(", S2=");
    Serial.print(b2 ? "HIGH" : "LOW");
    Serial.print(", S3=");
    Serial.println(b3 ? "HIGH" : "LOW");
  }
  uint32_t expected_us = NUM_CHANNELS * (MUX_SETTLE_TIME_US + ADC_CONVERSION_TIME_US);
  Serial.print("Expected full-scan time (μs): ");
  Serial.println(expected_us);
  if (sizeof(DataPackage_t) != (NUM_CHANNELS * sizeof(uint16_t) + sizeof(uint32_t))) {
    Serial.println("Error: DataPackage_t size mismatch.");
  } else {
    Serial.println("DataPackage_t size verified.");
  }
  Serial.println("Unit tests completed.");
}
#endif  // UNIT_TEST
        



This sounds like a fascinating deep dive into a common challenge! It's great to see how you're leveraging innovative solutions in embedded systems. How did the iterative process influence your approach to the project? Excited to read your insights!

赞
回复

Mert, while defining what you need to the GPT, what was the most problematic section? Did it stuck at some point?

Godwin Josh

Co-Founder of Altrosyn and DIrector at CDTECH | Inventor | Manufacturer

1 个月

It's fascinating how you transformed a seemingly simple limitation into a robust solution. The iterative dialogue approach you describe resonates with the Agile methodologies gaining traction in software development, where continuous feedback loops accelerate progress. Historically, engineers have often relied on trial and error, but your emphasis on AI-driven design suggests a paradigm shift towards more efficient problem-solving. Given that the ESP32 is increasingly popular in IoT applications, did you consider incorporating security measures into your design to protect against potential vulnerabilities? What are your thoughts on integrating blockchain technology for secure data transmission within this multi-channel system?

Lewis Bertolucci

Transforming Businesses | AI & Marketing Consultant | Strategic Growth Advisor

1 个月

Mert SOLKIRAN, this sounds like an incredible journey—can't wait to see how you've integrated innovation into your solutions. ??

James D. Feldman, CSP, CITE, CPIM

?? AI & Innovation in Hospitality | Customer Experience & Engagement Expert | CSP Speaker | Extraordinary Advisor | Author | Helping Hotels, Resorts & Restaurants Maximize Revenue & Guest, Customer, Employee Satisfaction

1 个月

That sounds like an incredible journey of innovation and problem-solving. ??

要查看或添加评论,请登录

Mert SOLKIRAN的更多文章

社区洞察

其他会员也浏览了