Add KY-009 RGB LED test with SoftSPI touch and LVGL.

R/G/B color switch (GPIO22/27/4), PWM brightness, and flashable firmware/cyd package.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-23 11:17:31 +08:00
commit 53c57f1cfd
23 changed files with 1958 additions and 0 deletions

84
src/lamp.cpp Normal file
View File

@@ -0,0 +1,84 @@
#include "lamp.h"
#include "pins.h"
#include <Arduino.h>
static constexpr int LEDC_CH_R = 1;
static constexpr int LEDC_CH_G = 2;
static constexpr int LEDC_CH_B = 3;
static constexpr int LEDC_FREQ = 5000;
static constexpr int LEDC_RES = 8; // 0..255
static bool g_on = true;
static lamp_color_t g_color = LAMP_COLOR_R;
static uint8_t g_pct[3] = {60, 60, 60}; // R/G/B 各自记住亮度
static uint8_t duty_from_pct(uint8_t pct) {
if (pct > 100) {
pct = 100;
}
return (uint8_t)((pct * 255U) / 100U);
}
static void apply() {
uint8_t dr = 0, dg = 0, db = 0;
if (g_on) {
const uint8_t d = duty_from_pct(g_pct[g_color]);
if (g_color == LAMP_COLOR_R) {
dr = d;
} else if (g_color == LAMP_COLOR_G) {
dg = d;
} else {
db = d;
}
}
ledcWrite(LEDC_CH_R, dr);
ledcWrite(LEDC_CH_G, dg);
ledcWrite(LEDC_CH_B, db);
}
void lamp_init() {
ledcSetup(LEDC_CH_R, LEDC_FREQ, LEDC_RES);
ledcSetup(LEDC_CH_G, LEDC_FREQ, LEDC_RES);
ledcSetup(LEDC_CH_B, LEDC_FREQ, LEDC_RES);
ledcAttachPin(KY009_R_PIN, LEDC_CH_R);
ledcAttachPin(KY009_G_PIN, LEDC_CH_G);
ledcAttachPin(KY009_B_PIN, LEDC_CH_B);
g_on = true;
g_color = LAMP_COLOR_R;
g_pct[0] = g_pct[1] = g_pct[2] = 60;
apply();
Serial.printf("KY-009 R=%d G=%d B=%d mode=R %u%%\n", KY009_R_PIN, KY009_G_PIN,
KY009_B_PIN, (unsigned)g_pct[g_color]);
}
void lamp_set_on(bool on) {
g_on = on;
apply();
Serial.printf("Lamp %s\n", g_on ? "ON" : "OFF");
}
bool lamp_is_on() { return g_on; }
void lamp_set_color(lamp_color_t c) {
if (c > LAMP_COLOR_B) {
c = LAMP_COLOR_R;
}
g_color = c;
apply();
static const char *names[] = {"R", "G", "B"};
Serial.printf("Color %s %u%%\n", names[g_color], (unsigned)g_pct[g_color]);
}
lamp_color_t lamp_color() { return g_color; }
void lamp_set_brightness(uint8_t percent) {
if (percent > 100) {
percent = 100;
}
g_pct[g_color] = percent;
apply();
Serial.printf("Bright %u%%\n", (unsigned)g_pct[g_color]);
}
uint8_t lamp_brightness() { return g_pct[g_color]; }

46
src/main.cpp Normal file
View File

@@ -0,0 +1,46 @@
#include <Arduino.h>
#include <lvgl.h>
#include "lamp.h"
#include "pins.h"
#include "touch.h"
#include "ui.h"
static constexpr uint32_t DRAW_BUF_SIZE =
(SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8));
static uint32_t draw_buf[DRAW_BUF_SIZE / 4];
void setup() {
Serial.begin(460800);
delay(200);
Serial.println();
Serial.printf("LVGL %d.%d.%d | KY-009 R=%d G=%d B=%d\n", lv_version_major(),
lv_version_minor(), lv_version_patch(), KY009_R_PIN, KY009_G_PIN,
KY009_B_PIN);
Serial.println("BOOT_OK");
pinMode(TFT_BL_PIN, OUTPUT);
digitalWrite(TFT_BL_PIN, HIGH);
touch_init();
lamp_init();
lv_init();
lv_display_t *disp =
lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf));
lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270);
lv_indev_t *indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, touch_read_cb);
lv_indev_set_display(indev, disp);
ui_create();
}
void loop() {
lv_timer_handler();
lv_tick_inc(5);
delay(5);
}

125
src/touch.cpp Normal file
View File

@@ -0,0 +1,125 @@
#include "touch.h"
#include "pins.h"
#include <Arduino.h>
/**
* SoftSPI XPT2046
* BL↔TR 是矩形主对角线镜像(XY 对调),不是 180°(180° 会搞成 TL↔BR)。
* 先按 rot2 得到旧 UI,再做对角修正,反变换喂给 LVGL 270。
*/
int16_t g_dbg_phys_x = -1;
int16_t g_dbg_phys_y = -1;
int16_t g_dbg_logic_x = -1;
int16_t g_dbg_logic_y = -1;
bool g_dbg_touch_down = false;
static void soft_write(uint8_t data) {
for (int i = 7; i >= 0; --i) {
digitalWrite(CYD_TOUCH_MOSI, (data >> i) & 1);
digitalWrite(CYD_TOUCH_CLK, HIGH);
delayMicroseconds(2);
digitalWrite(CYD_TOUCH_CLK, LOW);
delayMicroseconds(2);
}
}
static uint16_t soft_read12() {
uint16_t v = 0;
digitalWrite(CYD_TOUCH_CLK, HIGH);
delayMicroseconds(2);
digitalWrite(CYD_TOUCH_CLK, LOW);
delayMicroseconds(2);
for (int i = 0; i < 12; ++i) {
digitalWrite(CYD_TOUCH_CLK, HIGH);
delayMicroseconds(2);
v = (uint16_t)((v << 1) | (digitalRead(CYD_TOUCH_MISO) ? 1u : 0u));
digitalWrite(CYD_TOUCH_CLK, LOW);
delayMicroseconds(2);
}
return v;
}
static uint16_t xpt_read(uint8_t cmd) {
digitalWrite(CYD_TOUCH_CS, LOW);
soft_write(cmd);
uint16_t v = soft_read12();
digitalWrite(CYD_TOUCH_CS, HIGH);
return v;
}
static int16_t clampi(int32_t v, int32_t lo, int32_t hi) {
if (v < lo) {
return (int16_t)lo;
}
if (v > hi) {
return (int16_t)hi;
}
return (int16_t)v;
}
void touch_init() {
pinMode(CYD_TOUCH_CS, OUTPUT);
pinMode(CYD_TOUCH_CLK, OUTPUT);
pinMode(CYD_TOUCH_MOSI, OUTPUT);
pinMode(CYD_TOUCH_MISO, INPUT);
pinMode(CYD_TOUCH_IRQ, INPUT);
digitalWrite(CYD_TOUCH_CS, HIGH);
digitalWrite(CYD_TOUCH_CLK, LOW);
Serial.println("Touch: SoftSPI + diagonal fix (BL/TR)");
}
void touch_read_cb(lv_indev_t * /*indev*/, lv_indev_data_t *data) {
uint16_t adc_x = xpt_read(0x90);
uint16_t adc_y = xpt_read(0xD0);
adc_x = (uint16_t)((adc_x + xpt_read(0x90)) / 2);
adc_y = (uint16_t)((adc_y + xpt_read(0xD0)) / 2);
const bool irq = (digitalRead(CYD_TOUCH_IRQ) == LOW);
const bool in_range =
(adc_x > 80 && adc_x < 4000 && adc_y > 80 && adc_y < 4000);
if (!in_range || (!irq && (adc_x < 200 || adc_y < 200))) {
data->state = LV_INDEV_STATE_RELEASED;
g_dbg_touch_down = false;
return;
}
const int16_t p_x = (int16_t)adc_x;
const int16_t p_y = (int16_t)(4095 - adc_y);
int16_t x = map(p_x, 200, 3700, 1, SCREEN_WIDTH);
int16_t y = map(p_y, 240, 3800, 1, SCREEN_HEIGHT);
x = clampi(x, 0, SCREEN_WIDTH - 1);
y = clampi(y, 0, SCREEN_HEIGHT - 1);
// 旧 UI(LVGL270):(y, 239-x) — 该空间里 BL↔TR,做主对角线镜像
// (ux,uy) -> (uy*(319/239), ux*(239/319))
const int32_t old_ux = y;
const int32_t old_uy = (SCREEN_WIDTH - 1) - x;
int16_t ui_x = clampi((old_uy * (UI_HOR_RES - 1)) / (UI_VER_RES - 1), 0,
UI_HOR_RES - 1);
int16_t ui_y = clampi((old_ux * (UI_VER_RES - 1)) / (UI_HOR_RES - 1), 0,
UI_VER_RES - 1);
// 反变换喂 LVGL,使最终落在 (ui_x, ui_y)
const int16_t feed_x = (int16_t)((SCREEN_WIDTH - 1) - ui_y);
const int16_t feed_y = ui_x;
g_dbg_phys_x = x;
g_dbg_phys_y = y;
g_dbg_logic_x = ui_x;
g_dbg_logic_y = ui_y;
g_dbg_touch_down = true;
static uint32_t last_log = 0;
if (millis() - last_log > 80) {
last_log = millis();
Serial.printf("Touch UI(%d,%d)\n", ui_x, ui_y);
}
data->point.x = feed_x;
data->point.y = feed_y;
data->state = LV_INDEV_STATE_PRESSED;
}

193
src/ui.cpp Normal file
View File

@@ -0,0 +1,193 @@
#include "ui.h"
#include "lamp.h"
#include "pins.h"
#include <Arduino.h>
#include <lvgl.h>
#include <stdio.h>
static constexpr uint32_t COL_BG = 0x0A1016;
static constexpr uint32_t COL_INK = 0xEAF0F6;
static constexpr uint32_t COL_MUTED = 0x7A8B9C;
static constexpr uint32_t COL_BTN = 0x243140;
static constexpr uint32_t COL_ON = 0x1B5E4A;
static constexpr uint32_t COL_R = 0xE85D4C;
static constexpr uint32_t COL_G = 0x3DDC97;
static constexpr uint32_t COL_B = 0x4FC3F7;
static lv_obj_t *lbl_state;
static lv_obj_t *lbl_bright;
static lv_obj_t *slider;
static lv_obj_t *btn_r;
static lv_obj_t *btn_g;
static lv_obj_t *btn_b;
static lv_obj_t *lbl_r;
static lv_obj_t *lbl_g;
static lv_obj_t *lbl_b;
static lv_obj_t *btn_light;
static lv_obj_t *lbl_light;
static void style_btn(lv_obj_t *btn, uint32_t bg) {
lv_obj_set_style_radius(btn, 10, 0);
lv_obj_set_style_bg_color(btn, lv_color_hex(bg), 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
lv_obj_set_style_shadow_width(btn, 0, 0);
lv_obj_set_style_border_width(btn, 0, 0);
lv_obj_set_style_pad_all(btn, 0, 0);
}
static uint32_t accent_for(lamp_color_t c) {
if (c == LAMP_COLOR_R) {
return COL_R;
}
if (c == LAMP_COLOR_G) {
return COL_G;
}
return COL_B;
}
void ui_refresh_lamp() {
const bool on = lamp_is_on();
const lamp_color_t c = lamp_color();
const uint8_t pct = lamp_brightness();
const uint32_t accent = accent_for(c);
if (lbl_state) {
static const char *names[] = {"RED", "GREEN", "BLUE"};
lv_label_set_text_fmt(lbl_state, "%s %s", names[c], on ? "ON" : "OFF");
lv_obj_set_style_text_color(lbl_state, lv_color_hex(on ? accent : 0xE85D4C),
0);
}
if (lbl_bright) {
lv_label_set_text_fmt(lbl_bright, "Brightness %u%%", (unsigned)pct);
}
if (slider) {
lv_slider_set_value(slider, pct, LV_ANIM_OFF);
lv_obj_set_style_bg_color(slider, lv_color_hex(accent), LV_PART_INDICATOR);
lv_obj_set_style_bg_opa(slider, on ? LV_OPA_COVER : LV_OPA_50, LV_PART_MAIN);
}
style_btn(btn_r, c == LAMP_COLOR_R ? COL_R : COL_BTN);
style_btn(btn_g, c == LAMP_COLOR_G ? COL_G : COL_BTN);
style_btn(btn_b, c == LAMP_COLOR_B ? COL_B : COL_BTN);
if (lbl_light) {
lv_label_set_text(lbl_light, on ? "Turn OFF" : "Turn ON");
}
if (btn_light) {
style_btn(btn_light, on ? COL_ON : COL_BTN);
}
}
static void on_light(lv_event_t *e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
return;
}
lamp_set_on(!lamp_is_on());
ui_refresh_lamp();
}
static void on_color(lv_event_t *e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
return;
}
const lamp_color_t c =
(lamp_color_t)(uintptr_t)lv_event_get_user_data(e);
lamp_set_color(c);
if (!lamp_is_on()) {
lamp_set_on(true);
}
ui_refresh_lamp();
}
static void on_slider(lv_event_t *e) {
const lv_event_code_t code = lv_event_get_code(e);
if (code != LV_EVENT_VALUE_CHANGED && code != LV_EVENT_RELEASED) {
return;
}
lv_obj_t *obj = (lv_obj_t *)lv_event_get_target(e);
const int32_t v = lv_slider_get_value(obj);
lamp_set_brightness((uint8_t)v);
if (!lamp_is_on() && v > 0) {
lamp_set_on(true);
}
ui_refresh_lamp();
}
static lv_obj_t *make_color_btn(lv_obj_t *scr, const char *txt, int x,
lamp_color_t c, lv_obj_t **out_lbl) {
lv_obj_t *btn = lv_button_create(scr);
lv_obj_set_size(btn, 88, 40);
lv_obj_align(btn, LV_ALIGN_TOP_LEFT, x, 72);
style_btn(btn, COL_BTN);
lv_obj_add_event_cb(btn, on_color, LV_EVENT_CLICKED, (void *)(uintptr_t)c);
lv_obj_t *lbl = lv_label_create(btn);
lv_label_set_text(lbl, txt);
lv_obj_set_style_text_color(lbl, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_18, 0);
lv_obj_center(lbl);
if (out_lbl) {
*out_lbl = lbl;
}
return btn;
}
void ui_create() {
lv_obj_t *scr = lv_screen_active();
lv_obj_set_style_bg_color(scr, lv_color_hex(COL_BG), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
lv_obj_t *lbl_title = lv_label_create(scr);
lv_label_set_text(lbl_title, "KY-009");
lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_20, 0);
lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, 0, 4);
lv_obj_t *lbl_pin = lv_label_create(scr);
lv_label_set_text_fmt(lbl_pin, "R%d G%d B%d", KY009_R_PIN, KY009_G_PIN,
KY009_B_PIN);
lv_obj_set_style_text_color(lbl_pin, lv_color_hex(COL_MUTED), 0);
lv_obj_set_style_text_font(lbl_pin, &lv_font_montserrat_14, 0);
lv_obj_align(lbl_pin, LV_ALIGN_TOP_MID, 0, 28);
lbl_state = lv_label_create(scr);
lv_label_set_text(lbl_state, "RED ON");
lv_obj_set_style_text_color(lbl_state, lv_color_hex(COL_R), 0);
lv_obj_set_style_text_font(lbl_state, &lv_font_montserrat_16, 0);
lv_obj_align(lbl_state, LV_ALIGN_TOP_MID, 0, 48);
btn_r = make_color_btn(scr, "R", 24, LAMP_COLOR_R, &lbl_r);
btn_g = make_color_btn(scr, "G", 116, LAMP_COLOR_G, &lbl_g);
btn_b = make_color_btn(scr, "B", 208, LAMP_COLOR_B, &lbl_b);
lbl_bright = lv_label_create(scr);
lv_label_set_text(lbl_bright, "Brightness 60%");
lv_obj_set_style_text_color(lbl_bright, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(lbl_bright, &lv_font_montserrat_16, 0);
lv_obj_align(lbl_bright, LV_ALIGN_TOP_MID, 0, 124);
slider = lv_slider_create(scr);
lv_obj_set_size(slider, UI_HOR_RES - 48, 28);
lv_slider_set_range(slider, 0, 100);
lv_slider_set_value(slider, 60, LV_ANIM_OFF);
lv_obj_align(slider, LV_ALIGN_TOP_MID, 0, 150);
lv_obj_set_style_bg_color(slider, lv_color_hex(COL_BTN), LV_PART_MAIN);
lv_obj_set_style_bg_color(slider, lv_color_hex(COL_R), LV_PART_INDICATOR);
lv_obj_set_style_bg_color(slider, lv_color_hex(COL_INK), LV_PART_KNOB);
lv_obj_set_ext_click_area(slider, 12);
lv_obj_add_event_cb(slider, on_slider, LV_EVENT_VALUE_CHANGED, nullptr);
lv_obj_add_event_cb(slider, on_slider, LV_EVENT_RELEASED, nullptr);
btn_light = lv_button_create(scr);
lv_obj_set_size(btn_light, UI_HOR_RES - 48, 44);
lv_obj_align(btn_light, LV_ALIGN_TOP_MID, 0, 190);
style_btn(btn_light, COL_ON);
lv_obj_add_event_cb(btn_light, on_light, LV_EVENT_CLICKED, nullptr);
lbl_light = lv_label_create(btn_light);
lv_label_set_text(lbl_light, "Turn OFF");
lv_obj_set_style_text_color(lbl_light, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(lbl_light, &lv_font_montserrat_18, 0);
lv_obj_center(lbl_light);
ui_refresh_lamp();
}