tohum
← Blog’a dön

Konseptten Koda: ChatGPT ile Multiplexing Modülüne Uzanan İteratif Yolculuğum

Gayet basit bir dilekle yola çıktım. Elimde yalnızca tek bir erişilebilir analog pini olan bir ESP32 vardı, ama 16 analog kanal okumam gerekiyordu. Derdimi ChatGPT o3-mini-high modeline anlattım: “Sadece bir kanalım var, ama 16 kanal okumam lazım.” ChatGPT birkaç seçenekle döndü — harici ADC’ler, multiplexing ve daha fazlası. Seçenekleri değerlendirdim ve benim için en iyi yolun bir analog multiplexer, özellikle de CD74HC4067 kullanmak olduğuna karar verdim. Çok daha derin bir sohbetin fitilini ateşleyen kıvılcım işte buydu.

Adım 1: İlk Dilek

Elimde yalnızca tek bir analog giriş olmasına rağmen ESP32 ile 16 analog kanal okumak istediğimi söyleyerek başladım. ChatGPT hızla birkaç seçenek sundu. Analog multiplexer’lardan, harici ADC’lerden, hatta mümkünse pinleri yeniden atamaktan bahsetti. Sonunda CD74HC4067 multiplexer’ı kullanmayı seçtim. O noktada henüz istediğim yazılım mimarisini ya da herhangi bir zamanlama gereksinimini detaylandırmamıştım. Daha sonra, hedefimi destekleyecek olası mimari tasarımlar ve bir gereksinim seti istedim.

Adım 2: Mimari Tasarım ve Gereksinimler

Ardından ChatGPT’den CD74HC4067 ile arayüz kuracak bir yazılım modülü için mimari tasarım çıkarmasını istedim. Donanım soyutlamasını, multiplexer kontrolünü, ADC okumasını ve task entegrasyonunu birbirinden ayıran katmanlı bir tasarım sundu. Yapıyı beğendim — kapsamlı ve düzenliydi — ama gerçek zamanlı performans ve compile-time parametrelendirme detaylarının eksik olduğunu fark ettim. Bu yüzden ayarlanabilir her parametrenin bir preprocessor directive olarak tanımlanmasını ve tasarımın, her 16 kanallık okuma seti için bir timestamp tutan queue’lu bir RTOS Task/Thread yaklaşımı içermesini talep ettim.

Adım 3: RTOS ve Timestamp Eklemek

Fikir artık net biçimde şekillenirken, CD74HC4067 kullanacağımı teyit ettim. Özellikle, 16 kanalın tamamını sırayla tarayacak özel bir RTOS task’ı oluşturulmasını ve her tam okuma setinin, okumanın ne zaman bittiğini gösteren bir timestamp içermesini istedim. ChatGPT tasarımı buna göre güncelledi; bir ADC & örnekleme yöneticisi katmanı ekledi ve veri paketinin hem kanal verilerini hem de timestamp’i içereceğini belirtti. Bu noktada tasarımın sağlam olduğunu düşünüyordum, ama hâlâ daha somut zamanlama hesaplarına ihtiyaç vardı.

Adım 4: Zamanlama Hesapları ve Örnekleme Modları

Sonra 16 kanalın tamamını okumanın kaç milisaniye süreceğinin hesaplanmasını istedim. ChatGPT, farklı settling ve ADC dönüşüm gecikmelerine dayalı birkaç örnek senaryo sundu. Açıklamayı beğendim ama işi bir adım ileri götürdüm: Fast Mode, Moderate Mode veya HighRes gibi değerler alabilen, compile-time’da belirlenen bir parametre eklemek istedim. Bu parametre, multiplexer settling süresini ve ADC dönüşüm gecikmesini datasheet değerlerine ve önceki ölçümlere göre otomatik ayarlayacaktı. Tasarım bu yeni gereksinimlerle biraz daha evrildi.

Adım 5: Eksiksiz Modül Implementasyonu

Ardından, o ana kadarki tüm gereksinimleri karşılayan, unit test fonksiyonları da dahil eksiksiz bir modül — hem .cpp hem .h dosyaları — istedim. ChatGPT, ayarlanabilir tüm parametrelerin preprocessor directive olarak tanımlandığı ve gerçek zamanlama değerlerinin datasheet verilerine göre seçildiği (örneğin ESP32 için yaklaşık 11 µs’lik bir dönüşüm süresi kullanan) detaylı bir modül üretti. Üstelik unit testlerle birlikte geldi. Titizliğinden etkilenmiş olsam da, kodun hâlâ bazı bloklayan beklemeler (örneğin ets_delay_us gibi çağrılar) kullandığını fark ettim — bu da gerçek zamanlı ihtiyaçlarım için kabul edilebilir değildi.

Adım 6: Non-Blocking Şartı

Son olarak, kodun hiçbir yerinde bloklayan bekleme kullanılmamasında ısrar ettim. Her zamanlama gecikmesinin, busy-wait yapmak yerine state machine yaklaşımıyla non-blocking biçimde gerçekleştirilmesini ve kodun kontrolü scheduler’a bırakmasını şart koştum. ChatGPT tasarımı son bir kez revize etti; örnekleme task’ını, geçen süreyi kontrol eden ve vTaskDelay(0) ile taskYIELD() kullanarak kontrolü devreden non-blocking bir state machine’e dönüştürdü. Böylece tasarım tamamlandı ve tüm gereksinimlerimi karşıladı.


Sistem Mimarı, Geliştirici ve Donanım Geliştiricisi Olarak Yapay Zeka Üzerine Düşünceler

Basit bir problemi dile getirmekten eksiksiz, non-blocking bir modül teslim almaya uzanan yolculuğum, kelimenin tam anlamıyla büyüleyiciydi. İteratif sohbet yoluyla, belirsiz bir fikri detaylı bir gereksinim setine ve üretime hazır bir kod çözümüne dönüştürebileceğimi öğrendim. Yapay zekanın ince donanım nüanslarını ya da entegrasyon sorunlarını kaçırabileceği öne sürülebilir; ama ben, hızlı iterasyonun her detayı yeniden icat etmek yerine tasarımı rafine etmeye odaklanmamı sağladığını gördüm.

Artık yapay zekanın hem sistem mimarı hem de geliştirici olarak çalışabildiğini görüyorum. Modüler tasarımları hızla önerebiliyor, fonksiyonel ve fonksiyonel olmayan gereksinimleri tanımlamaya yardım edebiliyor, hatta gerçek zamanlı, non-blocking kısıtlara uyan kod bile üretebiliyor. Elbette yapay zeka çıktısını gerçek dünya donanım zorluklarına göre doğrulamak ve uyarlamak için insan gözetimi vazgeçilmez olmaya devam ediyor. Ama genel olarak iş akışı belirgin biçimde hızlanıyor; test, optimizasyon ve yaratıcı problem çözmeye daha çok zaman kalıyor.

Gereksinimleri ve kodun tamamını aşağıda görebilirsiniz.


Fonksiyonel Gereksinimler:

Fonksiyonel Olmayan Gereksinimler:


Nihai Kod

Nihai modül kodunun tamamı aşağıda:

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