# 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="https://1856353139-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MClo3nC-1US0rbK8Qau%2Fuploads%2FSn2n1H4ptD8Mz0IdcFzA%2Fweek5_ex7_custom_panel_scope.gif?alt=media&#x26;token=adc066d7-92c5-4af6-ab5d-fd96a5ce106e" 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
