YOGYUI

[PROJ] Dimmable WS2812S RGB LED 모듈 제작 - (2) 본문

PROJECT

[PROJ] Dimmable WS2812S RGB LED 모듈 제작 - (2)

요겨 2023. 3. 22. 23:00
반응형

5. MCU 선정 및 HW 연결

2023년 1월부터 시작한 Matter 프로젝트는 EspressIf사의 ESP32 SoC를 메인 타겟으로 개발해왔기에, 앞으로 Matter 관련 포스팅도 (일단은) ESP32 위주로 작성해보려 한다

프로토타이핑에 사용할 보드는 공구함에 박혀있던 EspressIf의 공식 evaluation kit인 ESP32 DevKitC를 사용하기로 했다

(ESP32-WROOM-32E 4MB Flash SoC가 장착되어 있다)

 

결선은 간단하니 별도로 Schematic으로 그리지는 않고 사진으로만 첨부

  • GPIO18 - WS2812 모듈 데이터 라인 (DI)
  • GPIO19 - LED 드라이버 PWM 입력

6. 소스코드 작성

깃허브에 소스코드 완료

※ Matter 코드 호환성 유지를 위해 EspressIf의 SDK esp-idf는 version 4.4.3으로 체크아웃

https://github.com/YOGYUI/esp32-ws2812-dimmable

 

GitHub - YOGYUI/esp32-ws2812-dimmable

Contribute to YOGYUI/esp32-ws2812-dimmable development by creating an account on GitHub.

github.com

WS2812S 데이터라인 Timing Chart

WS2812S 데이터라인 High/Low 시그널은 1us 이하 수준으로 유지가 되어야하기 때문에, 다음과 같이 인라인 어셈블리로 구현했다 (더 스마트한 방법도 있는데, 나는 오실로스코프로 찍어가며 fine-tuning하는 노가다를 재밌어하는 구식 인간인지라.. ㅋㅋ)

static void IRAM_ATTR set_databit_low(uint8_t pin_no)
{
    // T0H: 220ns ~ 380ns
    GPIO_REG_WRITE(GPIO_OUT_W1TS_REG, 1UL << pin_no);
    __asm__ __volatile__(
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;");

    // T0L: 580ns ~ 1us
    GPIO_REG_WRITE(GPIO_OUT_W1TC_REG, 1UL << pin_no);
    __asm__ __volatile__(
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;");
}

static void IRAM_ATTR set_databit_high(uint8_t pin_no)
{
    // T1H: 580ns ~ 1us
    GPIO_REG_WRITE(GPIO_OUT_W1TS_REG, 1UL << pin_no);
    __asm__ __volatile__(
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;");

    // T1L: 220ns ~ 420ns
    GPIO_REG_WRITE(GPIO_OUT_W1TC_REG, 1UL << pin_no);
    __asm__ __volatile__(
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;"
    "nop; nop; nop; nop; nop; nop; nop; nop;");
}

 

약간의 튜닝을 거쳐 인라인 어셈블리를 위와 같이 맞추고 오실로스코프로 데이터 라인 측정 결과

(1) - Bit High On Time: 780ns

(2) - Bit High Off Time: 400ns

(3) - Bit Low On Time: 320ns

(4) - Bit Low Off Time: 800ns

로 데이터시트에 기재된 스펙으로 타이밍을 맞출 수 있었다

 

GPIO 핀 및 PWM 관련 파라미터는 모두 definition.h 헤더파일에 정의해뒀다

#define PIN_WS2812_PWM          19
#define PIN_WS2812_DATA         18
#define WS2812_PIXEL_COUNT      16
#define WS2812_REFRESH_TIME_MS  100
#define LED_PWM_FREQUENCY       50000
#define PWM_DUTY_MAX            400

LED 드라이버의 PWM 신호 주파수는 50KHz 정도로 설정하고, PWM duty를 1024단계 (2^10)로 나눈 뒤, 최대 duty를 400 정도로 잡아줬다 (LED brightness는 0 ~ 100 으로 퍼센트 단위로 명령하게 되는지라..)

esp-idf sdk를 통해 구현한 WS2812 모듈 초기화 코드는 다음과 같다

bool CWS2812Ctrl::initialize(uint8_t gpio_pin_no, uint16_t pixel_cnt)
{
    esp_err_t ret;

    m_gpio_pin_no = gpio_pin_no;
    m_pixel_values.resize(pixel_cnt);
    m_pixel_conv_values.resize(pixel_cnt);

    m_queue_command = xQueueCreate(10, sizeof(int));
    xTaskCreate(func_command, "TASK_WS2812_CTRL", 4096, this, TASK_PRIORITY_WS2812, &m_task_handle);

    gpio_config_t gpio_cfg;
    gpio_cfg.pin_bit_mask   = 1ULL << m_gpio_pin_no;
    gpio_cfg.mode           = GPIO_MODE_OUTPUT;
    gpio_cfg.pull_up_en     = GPIO_PULLUP_DISABLE;
    gpio_cfg.pull_down_en   = GPIO_PULLDOWN_DISABLE;
    gpio_cfg.intr_type      = GPIO_INTR_DISABLE;
    ret = gpio_config(&gpio_cfg);
    if (ret != ESP_OK) {
        GetLogger(eLogType::Error)->Log("Failed to configure GPIO (ret %d)", ret);
        return false;
    }

    ledc_timer_config_t ledc_timer_cfg;
    ledc_timer_cfg.speed_mode = LEDC_HIGH_SPEED_MODE;
    ledc_timer_cfg.duty_resolution = LEDC_TIMER_10_BIT;
    ledc_timer_cfg.timer_num = LEDC_TIMER_0;
    ledc_timer_cfg.freq_hz = LED_PWM_FREQUENCY;
    ledc_timer_cfg.clk_cfg = LEDC_AUTO_CLK;
    ledc_timer_config(&ledc_timer_cfg);

    ledc_channel_config_t ledc_ch_cfg;
    ledc_ch_cfg.gpio_num = PIN_WS2812_PWM;
    ledc_ch_cfg.speed_mode = LEDC_HIGH_SPEED_MODE;
    ledc_ch_cfg.channel = LEDC_CHANNEL_0;
    ledc_ch_cfg.intr_type = LEDC_INTR_DISABLE;
    ledc_ch_cfg.timer_sel = LEDC_TIMER_0;
    ledc_ch_cfg.duty = 0;
    ledc_ch_cfg.hpoint = 0;
    ledc_ch_cfg.flags.output_invert = 1;
    
    ledc_channel_config(&ledc_ch_cfg);

    return true;
}

WS2812의 밝기 제어 (PWM duty 제어) 및 색상 제어 (Data line bit 제어)는 모두 FreeRTOS Task 내에서 동작되도록 구현했다

void CWS2812Ctrl::func_command(void *param)
{
    CWS2812Ctrl *obj = static_cast<CWS2812Ctrl *>(param);
    int *cmd_type = nullptr;
    uint8_t brightness;
    uint32_t delay;

    GetLogger(eLogType::Info)->Log("Realtime Task for WS2812 Module Started");
    while (obj->m_task_keepalive) {
        if (xQueueReceive(obj->m_queue_command, (void *)&cmd_type, pdMS_TO_TICKS(WS2812_REFRESH_TIME_MS)) == pdTRUE) {
            if (*cmd_type == SETRGB) {
                for (size_t i = 0; i < obj->m_pixel_values.size(); i++) {
                    RGB rgb = obj->m_pixel_values[i];
                    obj->m_pixel_conv_values[i] = convert_rgb_to_u32(rgb);
                }
            } else if (*cmd_type == BLINK) {
                delay = obj->m_blink_duration_ms / 42;
                brightness = obj->get_brightness();

                for (uint32_t i = 0; i < obj->m_blink_count; i++) {
                    for (int v = 0; v <= 100; v+=5) {
                        obj->set_brightness(v, false, false);
                        vTaskDelay(pdMS_TO_TICKS(delay));
                    }
                    for (int v = 100; v >= 0; v-=5) {
                        obj->set_brightness(v, false, false);
                        vTaskDelay(pdMS_TO_TICKS(delay));
                    }
                }

                obj->set_brightness(brightness, false, false);
            }
        }

        for (auto & value : obj->m_pixel_conv_values) {
            for (uint8_t i = 0; i < 24; i++) {
                if (value & (1UL << (23 - i)))
                    set_databit_high(obj->m_gpio_pin_no);
                else
                    set_databit_low(obj->m_gpio_pin_no);
            }
        }
    }

    GetLogger(eLogType::Info)->Log("Realtime Task for WS2812 Module Terminated");
    vTaskDelete(nullptr);
}

xQueueCreate로 생성한 큐 객체 내부 내용을 태스크 함수 (무한)루프 내에서 반복적으로 100ms 타임아웃으로 체크한 뒤, WS2812 데이터라인을 refresh하게 구현!

만약 큐 내부에 명령 객체가 있다면, 명령 타입에 따라 RGB 버퍼값을 변경해주거나, Blink 기능을 위해 PWM Duty를 일정 주기로 반복 변경하는 기능을 수행한다

void CWebServer::init_spiffs()
{
    esp_vfs_spiffs_conf_t conf;
    conf.base_path = SPIFFS_BASE_PATH;      // File path prefix associated with the filesystem.
    conf.partition_label = PARTITION_LABEL; // Optional, label of SPIFFS partition to use. If set to NULL, first partition with subtype=spiffs will be used.
    conf.max_files = 5;                     // Maximum files that could be open at the same time.
    conf.format_if_mount_failed = false;    // If true, it will format the file system if it fails to mount.
    esp_err_t result = esp_vfs_spiffs_register(&conf);
    if (result != ESP_OK) {
        if (result == ESP_FAIL) {
            GetLogger(eLogType::Error)->Log("Failed to mount or format filesystem");
        } else if (result == ESP_ERR_NOT_FOUND) {
            GetLogger(eLogType::Error)->Log("Failed to find SPIFFS partition");
        } else {
            GetLogger(eLogType::Error)->Log("Failed to initialize SPIFFS (%s)", esp_err_to_name(result));
        }
        return;
    }

    size_t total = 0, used = 0;
    result = esp_spiffs_info(PARTITION_LABEL, &total, &used);
    if (result != ESP_OK) {
        GetLogger(eLogType::Error)->Log("Failed to get SPIFFS partition information (%s)", esp_err_to_name(result));
        return;
    } else {
        GetLogger(eLogType::Info)->Log("Partition size: total: %d, used: %d", total, used);
    }

    GetLogger(eLogType::Info)->Log("initialized SPIFFS");
}

bool CWebServer::start()
{
    stop();

    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.server_port = WEB_SERVER_PORT;
    config.uri_match_fn = httpd_uri_match_wildcard;

    GetLogger(eLogType::Info)->Log("Starting HTTP Server (port %d)", config.server_port);
    esp_err_t result = httpd_start(&m_handle, &config);
    if (result != ESP_OK) {
        GetLogger(eLogType::Error)->Log("Failed to start HTTP Server (ret: %d)", result);
        stop();
        return false;
    }

    register_uri_handler_get_dpot_state();
    register_uri_handler_post_dpot_config();
    register_uri_handler_get_ws2812_state();
    register_uri_handler_post_ws2812_config();
    register_uri_handler_post_ws2812_blink();
    register_uri_handler_get_common();
    
    GetLogger(eLogType::Info)->Log("Started");
    return true;
}

LED 밝기 제어는 PWM의 Duty Ratio를 변경하여 수행하게 된다

bool CWS2812Ctrl::set_brightness(uint8_t value)
{
    uint32_t duty = (uint32_t)((double)value / 100. * PWM_DUTY_MAX);
    return set_pwm_duty(duty, verbose);
}

bool CWS2812Ctrl::set_pwm_duty(uint32_t duty)
{
    esp_err_t ret;
    ret = ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0, duty);
    if (ret != ESP_OK) {
        GetLogger(eLogType::Error)->Log("Failed to set ledc duty (ret: %d)", ret);
        return false;
    }

    ret = ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0);
    if (ret != ESP_OK) {
        GetLogger(eLogType::Error)->Log("Failed to set update duty (ret: %d)", ret);
        return false;
    }

    return true;
}

10bit PWM이라 설정 가능 범위는 0 ~ 1023이며, 값 몇개에 대해 LED 드라이버의 출력 전압을 측정해봤다

(안타깝게도 집에 전류 Probe는 없어서 정작 원하는 전류는 측정하지 못했다 ㅠ)

LED 드라이버 출력단의 Ground랑 제어 회로 및 ESP32 Ground가 어쩔 수 없이 같이 묶이다보니 전반적으로 ground가 약간 불안정한 경향이 보였는데, 일반 ESP32와 WS2812 모두 별 문제없이 잘 동작하는 것을 보고 더이상 불필요한 디버깅은 안하기로 했다 ㅎㅎ

나중에 조명 제품을 정식으로 만들 일이 생긴다면 그 때 좀 더 회로 문제를 건드려보기로 하자

 

16개 LED 개별 색상 설정 테스트를 해보자

GetWS2812Ctrl()->set_pixel_rgb_value(0, 255, 0, 0);
GetWS2812Ctrl()->set_pixel_rgb_value(1, 255, 180, 0);
GetWS2812Ctrl()->set_pixel_rgb_value(2, 255, 255, 0);
GetWS2812Ctrl()->set_pixel_rgb_value(3, 0, 255, 0);
GetWS2812Ctrl()->set_pixel_rgb_value(4, 0, 255, 200);
GetWS2812Ctrl()->set_pixel_rgb_value(5, 0, 0, 255);
GetWS2812Ctrl()->set_pixel_rgb_value(6, 80, 0, 255);
GetWS2812Ctrl()->set_pixel_rgb_value(7, 255, 0, 220);
GetWS2812Ctrl()->set_pixel_rgb_value(8, 255, 0, 0);
GetWS2812Ctrl()->set_pixel_rgb_value(9, 255, 180, 0);
GetWS2812Ctrl()->set_pixel_rgb_value(10, 255, 255, 0);
GetWS2812Ctrl()->set_pixel_rgb_value(11, 0, 255, 0);
GetWS2812Ctrl()->set_pixel_rgb_value(12, 0, 255, 200);
GetWS2812Ctrl()->set_pixel_rgb_value(13, 0, 0, 255);
GetWS2812Ctrl()->set_pixel_rgb_value(14, 80, 0, 255);
GetWS2812Ctrl()->set_pixel_rgb_value(15, 255, 0, 220);

 

무지개 색을 표현하고 싶었는데, 어두운 환경에서 찍으니 빛번짐이 너무 심해 제대로 찍히지가 않네 ㅋㅋ

어차피 사진 예쁘게 찍는게 목적은 아니니 이정도로 만족...

7.  HTTP 웹호스팅 데모

Matter 플랫폼을 붙이기 전에 간단하게 외부 제어 테스트를 하기 위해, 웹호스팅 서비스도 구현했다

프론트엔드는 Vue 2.x로 구현했으며, webpack으로 빌드된 js, html, css 파일을 esp의 SPIFFS(Serial Peripheral Interface Flash File System)에 바이너리 파일로 컴파일하여 플래싱하도록 설정했다

사실 요즘 조금씩 시간내서 학습중인 Vuetify를 실습에 적용해보고자 억지로 집어넣은 감이 있다 ㅋㅋ

 

ESP32의 Soft-AP 모드를 활성화한 뒤, 웹 브라우저가 설치된 기기 (랩탑, 스마트폰 등)에서 HTTP를 통해 접속하면 다음과 같은 화면을 볼 수 있다

모바일 기기를 통해 제어를 해보자

이정도 수준이면 IoT 스마트 조명 개발을 시작할 준비가 완료됐다고 해도 무방할듯? ㅎㅎ


이제 On/Off 제어, 색상 변경, 밝기 변경이 가능한 모듈이 있으니, 요놈을 활용해 Matter endpoint 중 조명과 관련된 녀석들을 예제로 블로그에 포스팅해볼 예정 (on-off light, dimmable light, color-temperature light)

끝~!

 

[시리즈]

[PROJ] Dimmable WS2812S RGB LED 모듈 제작 - (1)

[PROJ] Dimmable WS2812S RGB LED 모듈 제작 - (2)

반응형