# Real-time Spectrum Analyzer

## Lab 7: Spectrum Analyzer in Action

### Part 3 - Oscilloscope & Signal Processing

### 1. โครงสร้างภาพรวมของ Lab

#### Why? - ทำไมต้องเรียนรู้เรื่องนี้

* **Hardware-in-the-Loop**: การเชื่อมต่อ UI กับ Hardware จริงเป็นทักษะสำคัญที่สุดของ Embedded Engineer ระบบ SCADA, HMI, และ Industrial Controller ล้วนต้อง map ข้อมูลจาก sensor ไปยัง parameter ของระบบ และแสดงผลแบบ real-time
* **ADC-to-Parameter Mapping**: ในระบบจริง เราต้องแปลงค่า ADC (0-100%) ไปเป็นพารามิเตอร์ที่มีความหมาย เช่น ความถี่ (100-5000 Hz), Duty Cycle (10-90%), Temperature Setpoint เป็นต้น ทักษะนี้ใช้ในทุกระบบ Control
* **Phase Accumulator**: เป็น pattern พื้นฐานของ Direct Digital Synthesis (DDS) ที่ใช้ในการสร้างสัญญาณดิจิทัล, PWM Controller, Motor Drive, และ Audio Synthesizer
* **Read-Only UI Sync**: การแสดง slider ที่ sync กับค่าจาก hardware input โดยไม่ให้ผู้ใช้แก้ไขผ่าน UI เป็น pattern ที่ใช้บ่อยในระบบ monitoring
* **EE Application**: Function Generator ที่ควบคุมด้วย POTEN + แสดงผลบน LED PWM เป็นพื้นฐานของ Signal Generator, Motor Speed Controller, Dimmer, Power Supply Regulation

#### What? - จะได้เรียนรู้อะไร

1. **Hardware ADC Integration**: อ่านค่า Potentiometer ผ่าน `aic_adc_read_percent()` แบบ real-time
2. **Parameter Mapping**: แปลง ADC 0-100% เป็น Frequency (100-5000 Hz) และ Duty Cycle (10-90%)
3. **Phase Accumulator**: สร้าง waveform output ไปยัง LED ด้วย phase accumulator pattern
4. **PWM LED Control**: ส่งค่า brightness (0-100%) ไปยัง LED3 Blue ผ่าน `aic_gpio_pwm_set_brightness()`
5. **Read-Only Slider**: ทำ slider ให้แสดงค่าจาก hardware โดยไม่รับ touch input
6. **3-Panel Navigation**: Custom panel (Scope/Gen/FFT) แทน TabView เพื่อพื้นที่ chart สูงสุด
7. **Real-Time Signal Processing**: FFT analysis ที่ใช้ค่าความถี่จาก POTEN จริง

#### How? - จะทำอย่างไร

1. Initialize hardware: `aic_scope_init()`, `aic_fft_init()`, `aic_gpio_init()`, `aic_gpio_pwm_init()`, `aic_sensors_init()`
2. สร้าง 3 panels (Scope/Gen/FFT) พร้อม navigation bar ด้านซ้าย
3. ใน timer callback (33ms) อ่านค่า POTEN และ map เป็น parameter ตาม active panel
4. Scope Tab: POTEN ควบคุม frequency, แสดง waveform + measurements
5. Gen Tab: POTEN ควบคุม duty cycle, output switch เปิด/ปิด LED3 PWM
6. FFT Tab: วิเคราะห์ spectrum ของ waveform ที่ POTEN กำหนดความถี่

<figure><img src="/files/eMO3009b0WNmQ5djTu6N" alt=""><figcaption></figcaption></figure>

***

### 2. หลักการทำงานและ Flowchart

#### 2.1 Hardware Connection Diagram

```
┌─────────────────────────────────────────────────────────────────┐
│         PSoC Edge E84 - HARDWARE CONNECTIONS                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   POTEN: 3.3V --[10K POT]-- ADC CH0 -- GND                      │
│   API: aic_adc_read_percent(AIC_ADC_CH0) -> 0-100%              │
│                                                                 │
│   LED3:  P16_5 --[R 330]-- LED3 (Blue) -- GND                   │
│   API: aic_gpio_pwm_set_brightness(AIC_LED_BLUE, 0-100)         │
│                                                                 │
│   ┌──────────┐     ┌─────────────┐     ┌──────────────┐         │
│   │  POTEN   │────>│  CM55 Core  │────>│  LED3 (Blue) │         │
│   │ ADC CH0  │     │  LVGL + PWM │     │  PWM P16_5   │         │
│   └──────────┘     └─────────────┘     └──────────────┘         │
│      Input          Processing           Output                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

***

### 3. ฟังก์ชันสำคัญ

#### 3.1 Hardware APIs

<table><thead><tr><th width="419.671142578125">Function</th><th>Description</th></tr></thead><tbody><tr><td><code>aic_gpio_init()</code></td><td>Initialize GPIO subsystem</td></tr><tr><td><code>aic_gpio_pwm_init(AIC_LED_BLUE)</code></td><td>Initialize PWM on LED3 Blue (P16_5)</td></tr><tr><td><code>aic_sensors_init()</code></td><td>Initialize sensor/ADC subsystem</td></tr><tr><td><code>aic_adc_read_percent(AIC_ADC_CH0)</code></td><td>Read potentiometer value (0-100%)</td></tr><tr><td><code>aic_gpio_pwm_set_brightness(AIC_LED_BLUE, pct)</code></td><td>Set LED3 brightness (0-100%)</td></tr><tr><td><code>aic_gpio_pwm_get_brightness(AIC_LED_BLUE)</code></td><td>Get current LED3 brightness</td></tr></tbody></table>

#### 3.2 Scope APIs

<table><thead><tr><th width="421.610107421875">Function</th><th>Description</th></tr></thead><tbody><tr><td><code>aic_scope_init()</code></td><td>Initialize scope subsystem</td></tr><tr><td><code>aic_fft_init(256)</code></td><td>Initialize FFT with 256-point size</td></tr><tr><td><code>aic_scope_generate_wave(buf, count, &#x26;config)</code></td><td>Generate waveform samples</td></tr><tr><td><code>aic_fft_calculate(input, output)</code></td><td>Calculate FFT spectrum</td></tr><tr><td><code>aic_fft_dominant_frequency(fft, bins, Fs)</code></td><td>Find peak frequency from FFT</td></tr><tr><td><code>aic_signal_peak_to_peak(buf, count)</code></td><td>Calculate Vpp (raw)</td></tr><tr><td><code>aic_signal_rms(buf, count)</code></td><td>Calculate RMS (raw)</td></tr></tbody></table>

#### 3.3 LVGL Read-Only Slider Pattern

<table><thead><tr><th width="202.7073974609375">Technique</th><th>Code</th></tr></thead><tbody><tr><td>Remove clickable flag</td><td><code>lv_obj_remove_flag(slider, LV_OBJ_FLAG_CLICKABLE)</code></td></tr><tr><td>Set value from code</td><td><code>lv_slider_set_value(slider, val, LV_ANIM_OFF)</code></td></tr><tr><td>Purpose</td><td>Slider reflects hardware input, user cannot drag</td></tr></tbody></table>

***

### 4. Code เต็ม

#### 4.1 Constants and Configuration

```c
#include "part3_hw_scope_example.h"
#include "part3_examples.h"
#include "../aic-eec/aic-eec.h"
#include "../aic-eec/scope.h"
#include "../aic-eec/gpio.h"
#include "../aic-eec/sensors.h"
#include <stdio.h>
#include <string.h>
#include <math.h>

/*******************************************************************************
 * Configuration
 ******************************************************************************/

#define HW_SCOPE_CHART_POINTS      200

/* Widget sizes - touch-friendly */
#define HW_DROPDOWN_WIDTH          110
#define HW_DROPDOWN_HEIGHT         35
#define HW_SLIDER_WIDTH            140
#define HW_SLIDER_HEIGHT           25
#define HW_SLIDER_EXT_CLICK        15
#define HW_SWITCH_WIDTH            55
#define HW_SWITCH_HEIGHT           28
#define HW_BUTTON_WIDTH            60
#define HW_BUTTON_HEIGHT           32
#define HW_LED_SIZE                20

#define HW_FFT_SIZE                256
#define HW_FFT_BINS                (HW_FFT_SIZE / 2)
#define HW_FFT_CHART_BINS          64

#define HW_SAMPLE_RATE             48000
#define HW_GEN_SAMPLE_RATE         10000
#define HW_DEFAULT_FREQUENCY       1000
#define HW_DEFAULT_AMPLITUDE       16000

#define HW_SCOPE_UPDATE_MS         33      /* 30 FPS */

/* Frequency range for POTEN control in Scope Tab */
#define HW_MIN_FREQUENCY           100
#define HW_MAX_FREQUENCY           5000

/* PWM frequency for LED output (fixed) */
#define HW_LED_PWM_FREQUENCY       1000
```

#### 4.2 Global Variables (สรุปสำคัญ)

```c
/* Panel navigation */
static lv_obj_t *hw_nav_btns[3], *hw_panels[3];
static uint8_t hw_active_panel = 0;

/* Scope panel: chart + controls + measurements + POTEN label */
static lv_obj_t *hw_scope_chart, *hw_scope_freq_slider;
static lv_obj_t *hw_scope_pot_label;   /* "POT: XX%" */

/* Gen panel: chart + duty slider + output switch + LED status */
static lv_obj_t *hw_gen_chart, *hw_gen_duty_slider, *hw_gen_switch;
static lv_obj_t *hw_gen_pot_label;     /* "POT: XX% -> Duty: XX%" */
static lv_obj_t *hw_gen_led_status;    /* "LED3: OFF/XX%" */

/* FFT panel: bar chart + gain slider + peak label */
static lv_obj_t *hw_fft_chart, *hw_fft_gain_slider;

/* State */
static aic_wave_type_t hw_wave_type = AIC_WAVE_SINE;
static uint32_t hw_frequency = HW_DEFAULT_FREQUENCY;
static uint8_t hw_duty = 50;
static bool hw_is_running = true;
static bool hw_gen_output_enabled = false;

/* Phase accumulator for LED PWM output */
static uint32_t hw_led_phase = 0;
static uint32_t hw_led_phase_inc = 0;

/* Buffers */
static int16_t hw_waveform_buffer[HW_SCOPE_CHART_POINTS];
static uint16_t hw_fft_output[HW_FFT_BINS];

/* (ดู source code เต็มใน part3_hw_scope_example.c สำหรับ variables ทั้งหมด) */
```

#### 4.3 LED PWM Output - Phase Accumulator

```c
/*******************************************************************************
 * LED PWM Output Control
 *
 * Generates waveform output to LED3 based on Gen Tab settings.
 * Uses phase accumulator for smooth waveform generation.
 *
 * Phase Accumulator Pattern:
 *   phase += increment     (increment = freq * 65536 / tick_rate)
 *   phase wraps at 65536   (16-bit resolution)
 *   Convert phase -> brightness based on waveform type
 ******************************************************************************/
static void hw_update_led_output(void)
{
    if (!hw_gen_output_enabled) {
        aic_gpio_pwm_set_brightness(AIC_LED_BLUE, 0);
        return;
    }

    /* Calculate phase increment based on generator frequency */
    /* Phase wraps at 65536 for smooth output */
    hw_led_phase_inc = (hw_gen_frequency * 65536) /
                       (1000 / HW_SCOPE_UPDATE_MS);
    hw_led_phase += hw_led_phase_inc;

    /* Convert phase (0-65535) to brightness (0-100%) */
    uint8_t brightness = 0;

    switch (hw_wave_type) {
        case AIC_WAVE_SQUARE: {
            /* Square wave with duty cycle threshold */
            uint32_t duty_threshold = (uint32_t)hw_duty * 65536 / 100;
            uint16_t phase_16 = hw_led_phase & 0xFFFF;
            brightness = (phase_16 < duty_threshold) ? 100 : 0;
            break;
        }

        case AIC_WAVE_SINE: {
            /* Sine wave: sinf() lookup for smooth LED dimming */
            float phase_rad = (float)(hw_led_phase & 0xFFFF)
                              / 65536.0f * 2.0f * 3.14159f;
            float sine_val = sinf(phase_rad);
            brightness = (uint8_t)((sine_val + 1.0f) * 50.0f);
            break;
        }

        case AIC_WAVE_TRIANGLE: {
            /* Triangle wave: linear ramp up then down */
            uint16_t phase_16 = hw_led_phase & 0xFFFF;
            if (phase_16 < 32768) {
                brightness = (uint8_t)(phase_16 * 100 / 32768);
            } else {
                brightness = (uint8_t)((65535 - phase_16) * 100 / 32768);
            }
            break;
        }

        case AIC_WAVE_SAWTOOTH: {
            /* Sawtooth wave: linear ramp up then reset */
            uint16_t phase_16 = hw_led_phase & 0xFFFF;
            brightness = (uint8_t)(phase_16 * 100 / 65535);
            break;
        }

        default:
            brightness = 50;  /* Default 50% */
            break;
    }

    aic_gpio_pwm_set_brightness(AIC_LED_BLUE, brightness);
}
```

#### 4.4 Timer Callback - Hardware Input Processing

```c
static void hw_timer_cb(lv_timer_t *timer)
{
    (void)timer;

    /* Touch Keep-Alive: ป้องกัน touch controller หลับ */
    static uint32_t touch_keepalive_cnt = 0;
    if (++touch_keepalive_cnt >= 30) {
        touch_keepalive_cnt = 0;
        lv_indev_t *indev = lv_indev_get_next(NULL);
        while (indev) {
            if (lv_indev_get_type(indev) == LV_INDEV_TYPE_POINTER) {
                lv_indev_reset(indev, NULL);
                break;
            }
            indev = lv_indev_get_next(indev);
        }
    }

    /* Read POTEN value (0-100%) */
    uint8_t pot_percent = aic_adc_read_percent(AIC_ADC_CH0);

    /* Update based on active panel */
    if (hw_active_panel == 0) {
        /*===============================================================
         * SCOPE PANEL - POTEN controls FREQUENCY
         *=============================================================*/
        if (hw_is_running && hw_scope_chart && hw_scope_series) {
            /* Map POTEN (0-100%) to frequency (100-5000 Hz) */
            uint32_t freq_range = HW_MAX_FREQUENCY - HW_MIN_FREQUENCY;
            hw_frequency = HW_MIN_FREQUENCY +
                           (pot_percent * freq_range / 100);

            /* Update frequency display */
            if (hw_scope_freq_label) {
                if (hw_frequency >= 1000) {
                    lv_label_set_text_fmt(hw_scope_freq_label,
                        "%.1fkHz", (double)hw_frequency / 1000.0);
                } else {
                    lv_label_set_text_fmt(hw_scope_freq_label,
                        "%uHz", (unsigned int)hw_frequency);
                }
            }

            /* Update slider to match POTEN (read-only) */
            if (hw_scope_freq_slider) {
                int32_t slider_val =
                    (int32_t)sqrtf((float)(hw_frequency - 100));
                if (slider_val > 100) slider_val = 100;
                lv_slider_set_value(hw_scope_freq_slider,
                                    slider_val, LV_ANIM_OFF);
            }

            /* Show POTEN percentage */
            if (hw_scope_pot_label) {
                lv_label_set_text_fmt(hw_scope_pot_label,
                    "POT: %u%%", (unsigned int)pot_percent);
            }

            /* Generate waveform with current settings */
            aic_wavegen_config_t config = {
                .type = hw_wave_type,
                .frequency_hz = hw_frequency,
                .sample_rate_hz = HW_SAMPLE_RATE,
                .amplitude = hw_amplitude,
                .dc_offset = 0,
                .duty_percent = hw_duty
            };

            aic_scope_generate_wave(hw_waveform_buffer,
                                    HW_SCOPE_CHART_POINTS, &config);

            for (int i = 0; i < HW_SCOPE_CHART_POINTS; i++) {
                int32_t scaled = 50 +
                    (hw_waveform_buffer[i] * 40) / 32767;
                lv_chart_set_value_by_id(hw_scope_chart,
                    hw_scope_series, i, scaled);
            }
            lv_chart_refresh(hw_scope_chart);

            /* Update measurements */
            int32_t p2p = aic_signal_peak_to_peak(
                hw_waveform_buffer, HW_SCOPE_CHART_POINTS);
            float vpp = (float)p2p / 32767.0f * 3.3f;
            int16_t rms_raw = aic_signal_rms(
                hw_waveform_buffer, HW_SCOPE_CHART_POINTS);
            float rms = (float)rms_raw / 32767.0f * 3.3f;

            if (hw_scope_vpp_label) {
                lv_label_set_text_fmt(hw_scope_vpp_label,
                    "Vpp: %.2fV", (double)vpp);
            }
            if (hw_scope_freq_meas_label) {
                lv_label_set_text_fmt(hw_scope_freq_meas_label,
                    "Freq: %uHz", (unsigned int)hw_frequency);
            }
            if (hw_scope_rms_label) {
                lv_label_set_text_fmt(hw_scope_rms_label,
                    "RMS: %.2fV", (double)rms);
            }
        }
    }
    else if (hw_active_panel == 1) {
        /*===============================================================
         * GENERATOR PANEL - POTEN controls DUTY CYCLE
         *=============================================================*/
        if (hw_gen_chart && hw_gen_series) {
            /* Map POTEN (0-100%) to Duty Cycle (10-90%) */
            hw_duty = 10 + (pot_percent * 80 / 100);
            if (hw_duty < 10) hw_duty = 10;
            if (hw_duty > 90) hw_duty = 90;

            /* Update duty slider to match POTEN */
            if (hw_gen_duty_slider) {
                lv_slider_set_value(hw_gen_duty_slider,
                                    hw_duty, LV_ANIM_OFF);
            }

            /* Update duty label */
            if (hw_gen_duty_label) {
                lv_label_set_text_fmt(hw_gen_duty_label,
                    "%u%%", (unsigned int)hw_duty);
            }

            /* Show POTEN->Duty mapping */
            if (hw_gen_pot_label) {
                lv_label_set_text_fmt(hw_gen_pot_label,
                    "POT: %u%% -> Duty: %u%%",
                    (unsigned int)pot_percent,
                    (unsigned int)hw_duty);
            }

            /* Generate waveform preview */
            aic_wavegen_config_t config = {
                .type = hw_wave_type,
                .frequency_hz = hw_gen_frequency,
                .sample_rate_hz = HW_GEN_SAMPLE_RATE,
                .amplitude = hw_amplitude,
                .dc_offset = 0,
                .duty_percent = hw_duty
            };

            aic_scope_generate_wave(hw_waveform_buffer,
                                    HW_SCOPE_CHART_POINTS, &config);

            for (int i = 0; i < HW_SCOPE_CHART_POINTS; i++) {
                int32_t scaled = 50 +
                    (hw_waveform_buffer[i] * 40) / 32767;
                lv_chart_set_value_by_id(hw_gen_chart,
                    hw_gen_series, i, scaled);
            }
            lv_chart_refresh(hw_gen_chart);

            /* Update LED output via phase accumulator */
            hw_update_led_output();

            /* Update LED status display */
            if (hw_gen_led_status) {
                if (hw_gen_output_enabled) {
                    uint8_t cur = aic_gpio_pwm_get_brightness(
                                      AIC_LED_BLUE);
                    lv_label_set_text_fmt(hw_gen_led_status,
                        "LED3: %u%%", (unsigned int)cur);
                    lv_obj_set_style_text_color(hw_gen_led_status,
                        lv_color_hex(0x00FF00), 0);
                } else {
                    lv_label_set_text(hw_gen_led_status, "LED3: OFF");
                    lv_obj_set_style_text_color(hw_gen_led_status,
                        lv_color_hex(0xFF6666), 0);
                }
            }
        }
    }
    else if (hw_active_panel == 2) {
        /*===============================================================
         * FFT PANEL - Same POTEN->freq mapping as Scope
         * Generate 256 samples -> aic_fft_calculate() -> Bar chart
         * Normalize with gain, show dominant frequency
         *=============================================================*/
        /* (โครงสร้างเดียวกับ Scope แต่ใช้ FFT แทน waveform display) */
        /* ดู source code เต็มใน part3_hw_scope_example.c */
    }

    hw_last_pot_percent = pot_percent;
}
```

#### 4.5 Event Callbacks

```c
/* Wave type dropdown callback (shared by Scope + Gen panels) */
static void hw_wave_type_cb(lv_event_t *e)
{
    if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
    lv_obj_t *dropdown = lv_event_get_target(e);
    hw_wave_type = (aic_wave_type_t)lv_dropdown_get_selected(dropdown);
}

/* Scope freq slider - read-only, controlled by POTEN */
static void hw_freq_slider_cb(lv_event_t *e)
{
    if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
    /* In HW mode, slider is read-only (controlled by POTEN) */
}

/* Run/Stop button */
static void hw_run_btn_cb(lv_event_t *e)
{
    if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;

    hw_is_running = !hw_is_running;

    if (hw_scope_run_led) {
        if (hw_is_running) {
            lv_led_on(hw_scope_run_led);
        } else {
            lv_led_off(hw_scope_run_led);
        }
    }

    lv_obj_t *btn = lv_event_get_target(e);
    lv_obj_t *label = lv_obj_get_child(btn, 0);
    if (label) {
        lv_label_set_text(label,
                          hw_is_running ? "Stop" : "Run");
    }
}

/* Gen panel: Frequency slider (user-controlled, 10-500 Hz) */
static void hw_gen_freq_slider_cb(lv_event_t *e)
{
    if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
    lv_obj_t *slider = lv_event_get_target(e);
    int32_t val = lv_slider_get_value(slider);

    hw_gen_frequency = (uint32_t)val;
    if (hw_gen_frequency < 10) hw_gen_frequency = 10;

    if (hw_gen_freq_label) {
        lv_label_set_text_fmt(hw_gen_freq_label,
            "%uHz", (unsigned int)hw_gen_frequency);
    }
}

/* Gen panel: Duty slider - read-only, controlled by POTEN */
static void hw_gen_duty_slider_cb(lv_event_t *e)
{
    if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
    /* In HW mode, this slider syncs with POTEN */
}

/* Gen panel: OUTPUT switch - enables/disables LED3 */
static void hw_gen_switch_cb(lv_event_t *e)
{
    if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
    lv_obj_t *sw = lv_event_get_target(e);
    hw_gen_output_enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);

    if (hw_gen_output_enabled) {
        hw_led_phase = 0;  /* Reset phase on enable */
    } else {
        aic_gpio_pwm_set_brightness(AIC_LED_BLUE, 0);
    }
}

/* FFT panel: Gain slider */
static void hw_fft_gain_slider_cb(lv_event_t *e)
{
    if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
    lv_obj_t *slider = lv_event_get_target(e);
    hw_fft_gain = (uint8_t)lv_slider_get_value(slider);
}

/* Navigation button callback (panel switching) */
static void hw_nav_btn_cb(lv_event_t *e)
{
    if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;

    int panel_idx = (int)(intptr_t)lv_event_get_user_data(e);

    for (int i = 0; i < 3; i++) {
        if (hw_panels[i]) {
            if (i == panel_idx) {
                lv_obj_remove_flag(hw_panels[i], LV_OBJ_FLAG_HIDDEN);
            } else {
                lv_obj_add_flag(hw_panels[i], LV_OBJ_FLAG_HIDDEN);
            }
        }
        if (hw_nav_btns[i]) {
            if (i == panel_idx) {
                lv_obj_set_style_bg_color(hw_nav_btns[i],
                    lv_color_hex(0x00ff88), 0);
                lv_obj_set_style_text_color(hw_nav_btns[i],
                    lv_color_hex(0x000000), 0);
            } else {
                lv_obj_set_style_bg_color(hw_nav_btns[i],
                    lv_color_hex(0x1a1a2e), 0);
                lv_obj_set_style_text_color(hw_nav_btns[i],
                    lv_color_hex(0xAAAAAA), 0);
            }
        }
    }
    hw_active_panel = panel_idx;
}
```

#### 4.6 Panel Creation Functions

```c
/*******************************************************************************
 * Create Scope Panel - Green theme, waveform display
 ******************************************************************************/
static void hw_create_scope_panel(lv_obj_t *panel)
{
    int32_t chart_w = hw_chart_width;
    int32_t chart_h = hw_chart_height;

    /* Chart with oscilloscope style */
    hw_scope_chart = lv_chart_create(panel);
    lv_obj_set_size(hw_scope_chart, chart_w, chart_h);
    lv_obj_align(hw_scope_chart, LV_ALIGN_TOP_LEFT, 0, 0);
    lv_chart_set_type(hw_scope_chart, LV_CHART_TYPE_LINE);
    lv_chart_set_point_count(hw_scope_chart, HW_SCOPE_CHART_POINTS);
    lv_chart_set_range(hw_scope_chart,
                       LV_CHART_AXIS_PRIMARY_Y, 0, 100);

    /* Green-on-dark oscilloscope theme */
    lv_obj_set_style_bg_color(hw_scope_chart,
                              lv_color_hex(0x001100), 0);
    lv_obj_set_style_line_color(hw_scope_chart,
                                lv_color_hex(0x003300), LV_PART_MAIN);
    lv_obj_set_style_size(hw_scope_chart, 0, 0, LV_PART_INDICATOR);
    lv_obj_set_style_line_width(hw_scope_chart, 2, LV_PART_ITEMS);
    lv_chart_set_div_line_count(hw_scope_chart, 4, 5);
    lv_obj_set_style_border_width(hw_scope_chart, 1, 0);
    lv_obj_set_style_border_color(hw_scope_chart,
                                  lv_color_hex(0x004400), 0);

    hw_scope_series = lv_chart_add_series(hw_scope_chart,
        lv_color_hex(0x00ff00), LV_CHART_AXIS_PRIMARY_Y);

    /* ROW 1: Dropdown | Freq Slider (read-only) | POT label | Run */
    hw_scope_dropdown = lv_dropdown_create(panel);
    lv_dropdown_set_options(hw_scope_dropdown,
        "Square\nSine\nTriangle\nSawtooth\nNoise\nPulse");
    lv_dropdown_set_selected(hw_scope_dropdown, 1);
    lv_obj_set_size(hw_scope_dropdown,
                    HW_DROPDOWN_WIDTH, HW_DROPDOWN_HEIGHT);
    lv_obj_align(hw_scope_dropdown, LV_ALIGN_BOTTOM_LEFT, 5, -50);
    lv_obj_add_event_cb(hw_scope_dropdown, hw_wave_type_cb,
                        LV_EVENT_VALUE_CHANGED, NULL);

    /* Frequency slider: READ-ONLY (syncs with POTEN) */
    hw_scope_freq_slider = lv_slider_create(panel);
    lv_obj_set_size(hw_scope_freq_slider,
                    HW_SLIDER_WIDTH - 20, HW_SLIDER_HEIGHT);
    lv_slider_set_range(hw_scope_freq_slider, 0, 100);
    lv_slider_set_value(hw_scope_freq_slider, 30, LV_ANIM_OFF);
    lv_obj_align(hw_scope_freq_slider,
                 LV_ALIGN_BOTTOM_MID, -40, -55);
    lv_obj_remove_flag(hw_scope_freq_slider,
                       LV_OBJ_FLAG_CLICKABLE);  /* Read-only! */

    hw_scope_freq_label = lv_label_create(panel);
    lv_label_set_text(hw_scope_freq_label, "1.0kHz");
    lv_obj_set_style_text_color(hw_scope_freq_label,
                                lv_color_hex(0x00ff00), 0);
    lv_obj_align_to(hw_scope_freq_label, hw_scope_freq_slider,
                    LV_ALIGN_OUT_RIGHT_MID, 5, 0);

    /* POTEN indicator (yellow) */
    hw_scope_pot_label = lv_label_create(panel);
    lv_label_set_text(hw_scope_pot_label, "POT: --%");
    lv_obj_set_style_text_color(hw_scope_pot_label,
                                lv_color_hex(0xFFFF00), 0);
    lv_obj_align(hw_scope_pot_label,
                 LV_ALIGN_BOTTOM_RIGHT, -80, -55);

    /* Run LED + Button */
    hw_scope_run_led = lv_led_create(panel);
    lv_obj_set_size(hw_scope_run_led, HW_LED_SIZE, HW_LED_SIZE);
    lv_led_set_color(hw_scope_run_led,
                     lv_palette_main(LV_PALETTE_GREEN));
    lv_obj_align(hw_scope_run_led,
                 LV_ALIGN_BOTTOM_RIGHT, -70, -55);
    lv_led_on(hw_scope_run_led);

    hw_scope_run_btn = lv_button_create(panel);
    lv_obj_set_size(hw_scope_run_btn,
                    HW_BUTTON_WIDTH, HW_BUTTON_HEIGHT);
    lv_obj_align(hw_scope_run_btn,
                 LV_ALIGN_BOTTOM_RIGHT, -5, -50);
    lv_obj_t *run_label = lv_label_create(hw_scope_run_btn);
    lv_label_set_text(run_label, "Stop");
    lv_obj_center(run_label);
    lv_obj_add_event_cb(hw_scope_run_btn, hw_run_btn_cb,
                        LV_EVENT_CLICKED, NULL);

    /* ROW 2: Measurements */
    hw_scope_vpp_label = lv_label_create(panel);
    lv_label_set_text(hw_scope_vpp_label, "Vpp: --");
    lv_obj_set_style_text_color(hw_scope_vpp_label,
                                lv_color_hex(0x00ff00), 0);
    lv_obj_align(hw_scope_vpp_label,
                 LV_ALIGN_BOTTOM_LEFT, 5, -10);

    hw_scope_freq_meas_label = lv_label_create(panel);
    lv_label_set_text(hw_scope_freq_meas_label, "Freq: --");
    lv_obj_set_style_text_color(hw_scope_freq_meas_label,
                                lv_color_hex(0xffff00), 0);
    lv_obj_align(hw_scope_freq_meas_label,
                 LV_ALIGN_BOTTOM_MID, 0, -10);

    hw_scope_rms_label = lv_label_create(panel);
    lv_label_set_text(hw_scope_rms_label, "RMS: --");
    lv_obj_set_style_text_color(hw_scope_rms_label,
                                lv_color_hex(0x00ffff), 0);
    lv_obj_align(hw_scope_rms_label,
                 LV_ALIGN_BOTTOM_RIGHT, -5, -10);
}

/*******************************************************************************
 * Create Generator Panel - Orange theme (key differences from Scope)
 ******************************************************************************/
static void hw_create_gen_panel(lv_obj_t *panel)
{
    /* Chart: Orange theme (0x110800 bg, 0xff8800 series) */
    /* Layout เหมือน Scope แต่เปลี่ยนสีเป็น Orange */

    /* KEY DIFFERENCE 1: Frequency slider is USER-CONTROLLED */
    hw_gen_freq_slider = lv_slider_create(panel);
    lv_slider_set_range(hw_gen_freq_slider, 10, 500);
    /* ไม่มี lv_obj_remove_flag(LV_OBJ_FLAG_CLICKABLE) */
    /* ผู้ใช้ drag ได้ (10-500 Hz สำหรับ visible LED effect) */

    /* KEY DIFFERENCE 2: Duty slider is READ-ONLY (POTEN) */
    hw_gen_duty_slider = lv_slider_create(panel);
    lv_slider_set_range(hw_gen_duty_slider, 10, 90);
    lv_obj_remove_flag(hw_gen_duty_slider,
                       LV_OBJ_FLAG_CLICKABLE);  /* Read-only! */

    /* KEY DIFFERENCE 3: OUTPUT switch controls LED3 */
    hw_gen_switch = lv_switch_create(panel);
    lv_obj_add_event_cb(hw_gen_switch, hw_gen_switch_cb,
                        LV_EVENT_VALUE_CHANGED, NULL);

    /* KEY DIFFERENCE 4: POTEN->Duty + LED status labels */
    hw_gen_pot_label = lv_label_create(panel);
    lv_label_set_text(hw_gen_pot_label, "POT: --% -> Duty: --%");

    hw_gen_led_status = lv_label_create(panel);
    lv_label_set_text(hw_gen_led_status, "LED3: OFF");

    /* (ดู source code เต็มสำหรับ layout coordinates) */
}

/*******************************************************************************
 * Create FFT Panel - Cyan theme, bar chart
 ******************************************************************************/
static void hw_create_fft_panel(lv_obj_t *panel)
{
    /* Bar chart: Cyan theme (0x001a1a bg, 0x00ffff series) */
    hw_fft_chart = lv_chart_create(panel);
    lv_chart_set_type(hw_fft_chart, LV_CHART_TYPE_BAR);
    lv_chart_set_point_count(hw_fft_chart, HW_FFT_CHART_BINS);
    /* CRITICAL: Bar spacing */
    lv_obj_set_style_pad_column(hw_fft_chart, 2, 0);

    /* Gain slider + Peak frequency label */
    hw_fft_gain_slider = lv_slider_create(panel);
    lv_slider_set_range(hw_fft_gain_slider, 10, 100);
    lv_obj_add_event_cb(hw_fft_gain_slider,
        hw_fft_gain_slider_cb, LV_EVENT_VALUE_CHANGED, NULL);

    hw_fft_dominant_label = lv_label_create(panel);
    lv_label_set_text(hw_fft_dominant_label, "Peak: -- Hz");

    /* (ดู source code เต็มสำหรับ layout coordinates) */
}
```

#### 4.7 Main Function

```c
void part3_ex8_hw_scope(void)
{
    /* === Initialize subsystems === */
    aic_scope_init();
    aic_fft_init(HW_FFT_SIZE);
    aic_gpio_init();                    /* GPIO subsystem */
    aic_gpio_pwm_init(AIC_LED_BLUE);   /* PWM on LED3 Blue */
    aic_sensors_init();                 /* ADC for POTEN */

    /* === Screen layout === */
    lv_obj_t *screen = lv_screen_active();
    int32_t screen_w = lv_obj_get_width(screen);
    int32_t screen_h = lv_obj_get_height(screen);

    int32_t nav_width = 70;
    int32_t footer_height = 22;
    hw_chart_width = screen_w - nav_width;
    hw_chart_height = screen_h - footer_height - 88;

    lv_obj_set_style_bg_color(screen, lv_color_hex(0x0a0a1e), 0);

    /* === Navigation bar (LEFT, 70px) === */
    /* 3 buttons: Scope, Gen, FFT at Y=50, 170, 290 */
    /* Active button: green (0x00ff88), Inactive: dark (0x1a1a2e) */
    /* ใช้ hw_nav_btn_cb กับ user_data = panel index */

    /* === Content panels (3 panels, show/hide) === */
    /* panel[0]=Scope (visible), panel[1]=Gen, panel[2]=FFT (hidden) */
    hw_create_scope_panel(hw_panels[0]);
    hw_create_gen_panel(hw_panels[1]);
    hw_create_fft_panel(hw_panels[2]);

    aic_create_footer(screen);

    /* === Timer: 33ms (30 FPS) === */
    hw_timer = lv_timer_create(hw_timer_cb, HW_SCOPE_UPDATE_MS, NULL);
}
```

***

### 5. องค์ความรู้และเทคนิค

#### 5.1 CRITICAL: Read-Only Slider Synced with Hardware

```c
/* Pattern: Slider ที่แสดงค่าจาก Hardware (ผู้ใช้ drag ไม่ได้)
 *
 * ขั้นตอน:
 * 1. สร้าง slider ปกติ
 * 2. ลบ flag CLICKABLE ออก
 * 3. ใน timer callback ตั้งค่าจาก hardware
 *
 * WRONG: (ผู้ใช้ drag ได้ แต่ค่าจะถูก overwrite ทุก 33ms)
 *   lv_obj_t *slider = lv_slider_create(panel);
 *   // ไม่ได้ลบ clickable flag
 *
 * CORRECT: (ผู้ใช้ drag ไม่ได้ แสดงค่าจาก POTEN)
 *   lv_obj_t *slider = lv_slider_create(panel);
 *   lv_obj_remove_flag(slider, LV_OBJ_FLAG_CLICKABLE);
 *   // ใน timer: lv_slider_set_value(slider, hw_val, LV_ANIM_OFF);
 *
 * ใช้กับ: Motor Speed Feedback, Temperature Readback,
 *         Pressure Gauge, Position Sensor Display
 */
```

#### 5.2 Phase Accumulator - DDS Pattern

```c
/* Phase Accumulator = หัวใจของ Direct Digital Synthesis (DDS)
 *
 * phase (32-bit) += increment  // ทุก timer tick
 * increment = freq * 65536 / tick_rate
 * ใช้ lower 16 bits (0-65535) เป็น waveform phase
 *
 * ข้อดี: Sub-Hz resolution, continuous phase, memory efficient
 * ข้อเสีย: Accuracy ขึ้นกับ timer jitter
 *
 * ใช้ในงานจริง: DDS chip (AD9850), Motor SVPWM, Audio Synth
 */
void set_frequency(uint32_t freq_hz, uint32_t tick_rate)
{
    phase_inc = (freq_hz * 65536) / tick_rate;
    /* phase ไม่ reset = continuous transition (ไม่กระตุก) */
}
```

#### 5.3 Hardware Scope vs Simulation Scope

```c
/* Ex8 = Ex7 + Hardware I/O
 * Freq/Duty: Slider(drag) -> POTEN(physical), Slider=read-only
 * LED Output: printf() -> PWM LED3 (Blue)
 * Input: None -> ADC CH0
 * Init: scope+fft -> +gpio+sensors
 * ที่เหลือเหมือนกัน: Timer 33ms, Software waveform/FFT
 */
```

#### 5.4 ADC-to-Parameter Mapping Patterns

```c
/* Linear: param = MIN + (adc * (MAX - MIN) / 100)
 * Log:    freq = MIN * exp(adc * log(MAX/MIN) / 100)  (audio)
 * Deadband: if (adc < 5%) output = 0;  (motor control)
 */
```

#### 5.5 Touch Keep-Alive Pattern

```c
/* Touch controller อาจ "หลับ" หลังจากไม่มี touch สักพัก
 * แก้โดย reset input device ทุก ~1 sec ใน timer callback:
 *   if (++cnt >= 30) { cnt=0; lv_indev_reset(indev, NULL); }
 * จำเป็นสำหรับ always-on display (SCADA, Dashboard)
 */
```

***

### 6. แบบฝึกหัด

#### Exercise 1: ADC Smoothing Filter

**โจทย์:** เพิ่ม Exponential Moving Average (EMA) filter ให้กับค่า ADC เพื่อลด noise:

* เก็บค่าก่อนหน้า `prev_pot` (static variable)
* คำนวณ: `filtered = (alpha * raw) + ((1 - alpha) * prev_pot)` โดย alpha = 0.3
* ใช้ integer math: `filtered = (3 * raw + 7 * prev) / 10`
* แสดง "RAW: XX% FLT: XX%" บน UI เทียบค่า raw vs filtered
* สังเกตว่า waveform ปรับ frequency ได้ smooth ขึ้นเมื่อหมุน POTEN เร็ว

#### Exercise 2: Dual POTEN Mode

**โจทย์:** เพิ่ม Mode Switch ให้ POTEN ควบคุมได้ 2 แบบใน Scope Tab:

* เพิ่ม Switch "FREQ/AMP" สำหรับเลือกว่า POTEN ควบคุม Frequency หรือ Amplitude
* Mode FREQ: เหมือนเดิม (100-5000 Hz)
* Mode AMP: POTEN ควบคุม amplitude (10-100%)
* แสดง mode ปัจจุบันบน UI: "Mode: FREQ" หรือ "Mode: AMP"
* เมื่อเปลี่ยน mode ค่าที่ไม่ได้ควบคุมจะ lock ไว้ที่ค่าล่าสุด
* ประยุกต์ใช้: Oscilloscope แบบ knob เดียวที่ปรับได้หลาย parameter


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.aic-eec.com/interfacing-with-infineon-psoc-tm-edge/hmi-development/spectrum-analyzer/real-time-spectrum-analyzer.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
