```markdown
### 代码概述
该代码实现了一个基于 ESP32 的多功能交互设备,集成了触摸按键(TTP229)、WS2812B 彩灯、蜂鸣器、OLED 显示屏等功能。支持以下模式:
- **琴键演奏**:通过触摸输入触发音调和灯光反馈。
- **灯光交互模式**:按键点亮对应 LED 并渐暗。
- **渐变循环灯光**:自动流动的彩虹亮度效果。
- **祝福曲播放**:选择音乐并通过蜂鸣器播放,同时控制灯光同步。
- **游戏模式**:模拟下落方块,用户点击消除。
- **动画模式**:原为灯光动画,现需改为 OLED 动画。
当前存在的问题:
1. 音乐列表中后两首为空占位,导致 `TP4` 错误触发播放。
2. `TP7` 触发的 “Animation” 当前是灯光动画,要求改为 OLED 动画,并保留原有功能结构。
---
### 修改思路
#### 1. **修复空音乐占位问题**
- 原因:`music_tracks[1]` 和 `music_tracks[2]` 为 `NULL`,长度为 1,但代码未做判断直接启动任务,导致播放非法数据或默认第一首。
- 解决方案:为第二、第三首歌曲添加简单的模拟旋律(如短音阶),避免空指针且可正常播放。
#### 2. **替换 `run_animation()` 为 OLED 动画**
- 原功能:调用 `run_animation()` 播放灯光波浪动画。
- 新需求:使用 OLED 屏幕播放简单动画(例如移动笑脸或滚动条)。
- 实现方式:编写新的 `oled_animation()` 函数,在 OLED 上绘制动态内容,不涉及 WS2812B 灯光。
#### 3. **提醒依赖库**
- 若使用复杂 OLED 图形操作,推荐引入 `ssd1306.h` 或图形库提升开发效率。
- 本修改仍使用现有底层绘图函数,无需额外头文件。
#### 4. **保留功能**
- 所有菜单逻辑、触摸映射、LED 控制、蜂鸣器音调、状态机均保持不变。
- 只修改音乐数据和动画行为。
---
### 修改后的完整代码(main.c)
```c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#include "driver/ledc.h"
#include "driver/spi_master.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_random.h"
#include "esp_rom_sys.h"
#include "led_strip.h"
#define TAG "TTP229_EXT_FIXED"
#define WS2812B_PIN 2
#define NUM_LEDS 12
#define TTP229_SCL 27
#define TTP229_SDO 26
#define BUZZER_PIN 25
#define OLED_SPI_HOST HSPI_HOST
#define OLED_MOSI_PIN 17
#define OLED_SCLK_PIN 4
#define OLED_DC_PIN 18
#define OLED_CS_PIN 19
#define OLED_RST_PIN 5
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_BUF_SIZE ((OLED_WIDTH * OLED_HEIGHT) / 8)
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
/* ---------- 全局与声明 ---------- */
led_strip_handle_t led_strip;
static uint8_t led_brightness[NUM_LEDS];
static uint8_t led_active[NUM_LEDS];
static SemaphoreHandle_t led_mutex = NULL;
typedef enum {
MODE_NORMAL = 0,
MODE_LED_KEY_INTERACT,
MODE_LED_GRADIENT_CYCLE,
MODE_BLESSING_PLAY,
MODE_GAME
} app_mode_t;
static volatile app_mode_t g_mode = MODE_NORMAL;
/* gradient */
static volatile bool gradient_run = false;
static int gradient_pos = 0;
/* blessing task control */
static TaskHandle_t blessing_task_handle = NULL;
static volatile bool blessing_running = false;
/* game state */
static bool game_running = false;
static uint8_t game_regions[7];
static TickType_t game_next_spawn;
/* OLED */
static uint8_t oled_buf[OLED_BUF_SIZE];
static spi_device_handle_t spi_oled;
/* 菜单状态 */
typedef enum {
MENU_NONE,
MENU_MUSIC_SELECT,
MENU_FUNC_SELECT
} menu_state_t;
static volatile menu_state_t current_menu = MENU_NONE;
static int selected_item = 0;
static int brightness_level = 4; // 亮度等级:0~4(4=最大)
static int pitch_offset_semitones = 0; // 音高偏移(半音)
/* 映射:TP -> LED 索引 */
static const int8_t tp_to_led_idx[16] = {
/* TP0 */ 8, /* TP1 */ 9, /* TP2 */ 10, /* TP3 */ 11,
/* TP4 */ -1, /* TP5 */ -1, /* TP6 */ -1, /* TP7 */ -1,
/* TP8 */ 3, /* TP9 */ 2, /* TP10 */ 1, /* TP11 */ 0,
/* TP12 */ 4, /* TP13 */ 5, /* TP14 */ 6, /* TP15 */ 7
};
/* 游戏键映射 */
static const int8_t tp_to_game_region[16] = {
/* TP0 */ -1, /* TP1 */ 1, /* TP2 */ -1, /* TP3 */ 0,
/* TP4 */ -1, /* TP5 */ -1, /* TP6 */ -1, /* TP7 */ -1,
/* TP8 */ -1, /* TP9 */ 5, /* TP10 */ -1, /* TP11 */ 6,
/* TP12 */ 4, /* TP13 */ -1, /* TP14 */ 3, /* TP15 */ 2
};
/* 彩虹颜色 */
const uint8_t rainbow_colors[NUM_LEDS][3] = {
{255,0,48}, {255,89,0}, {255,183,0}, {255,233,0},
{136,255,0}, {0,255,136}, {0,204,255}, {0,64,255},
{89,0,255}, {183,0,255}, {255,0,191}, {255,0,98}
};
/* 16 键基础频率 */
static const int note_freqs[16] = {
262,294,330,349,370,392,415,440,466,494,523,554,587,622,659,698
};
/* TTP 按键重映射 */
static const uint8_t key_map[16] = {3,2,1,0,15,14,13,12,8,9,10,11,4,5,6,7};
/* 小音符位图 (12x12) */
static const uint8_t bm_music[] = {
0x10,0x10,0x10,0x18,0x18,0x1C,0x1C,0x0E,0x06,0x06,0x00,0x00,
0x1F,0x1F,0x0F,0x0F,0x07,0x07,0x03,0x03,0x01,0x01,0x00,0x00
};
/* 祝福曲调 */
typedef struct { int freq; int dur; } tone_t;
static const tone_t blessing[] = {
{494, 600}, {440, 600},
{523, 150}, {440, 150}, {392, 150}, {492, 150}, {523, 150}, {494, 150}, {440, 300},
{440, 150}, {392, 150}, {440, 150}, {587, 150}, {494, 150}, {494, 150}, {523, 150}, {587, 150},
{587, 150}, {659, 150}, {392, 150}, {659, 150}, {587, 150}, {523, 150}, {494, 150}, {392, 300},
{494, 150}, {523, 150}, {587, 150}, {523, 600},
{0, 600}, {0, 600},
{0, 600}, {0, 600},
{0, 600}, {0, 600}, {494, 150}, {523, 150},
{587, 150}, {494, 150}, {587, 150}, {494, 150}, {587, 150}, {659, 150}, {587, 150}, {587, 150}, {587, 150},
{523, 150}, {494, 150}, {440, 150}, {440, 150}, {392, 150}, {494, 150}, {494, 150}, {0, 150}, {392, 150}, {440, 150},
{494, 150}, {523, 150}, {494, 150}, {494, 150}, {392, 150}, {440, 150}, {494, 150}, {494, 150}, {523, 150}, {494, 150}, {494, 150},
{440, 150}, {440, 150}, {494, 150}, {494, 150}, {440, 150}, {392, 150}, {494, 150}, {494, 150}, {0, 150}, {494, 150}, {523, 150},
{587, 150}, {494, 150}, {587, 150}, {494, 150}, {587, 150}, {392, 150}, {440, 150}, {392, 150}, {440, 150}, {392, 150},
{440, 150}, {440, 150}, {494, 150}, {494, 150}, {440, 150}, {392, 150}, {494, 150}, {494, 150}, {0, 150}, {494, 150}, {523, 150},
{587, 150}, {494, 150}, {587, 150}, {494, 150}, {587, 150}, {659, 150}, {587, 150}, {659, 150}, {587, 150}, {659, 150}, {587, 150},
{523, 150}, {523, 150}, {587, 150}, {587, 150}, {523, 150}, {494, 150}, {587, 150}, {587, 150}, {0, 150}, {494, 150}, {523, 150},
{440, 150}, {440, 150}, {392, 150}, {392, 150}, {440, 150}, {392, 150}, {392, 150}, {392, 150}, {659, 150}, {740, 150},
{440, 150}, {494, 300}, {392, 150}, {440, 150}, {440, 150}, {440, 150}, {494, 150},
{440, 150}, {440, 150}, {440, 150}, {523, 150}, {440, 150}, {659, 150}, {659, 150}, {740, 150},
{440, 150}, {494, 300}, {392, 150}, {440, 150}, {440, 150}, {494, 150},
{440, 150}, {440, 150}, {440, 150}, {523, 150}, {440, 150}, {740, 150}, {659, 150},
{587, 300}, {392, 150}, {659, 150}, {740, 150}, {392, 150}, {392, 150}, {494, 150},
{392, 150}, {392, 150}, {392, 150}, {392, 150}, {392, 150}, {659, 150}, {392, 150}, {659, 150}, {659, 150},
{523, 300}, {494, 300}, {440, 300}, {392, 300}, {440, 300}, {494, 300}, {523, 300},
{494, 300}, {0, 300}, {626, 150}, {626, 150}, {392, 300},
{392, 300}, {392, 300}, {392, 300}, {523, 300}, {392, 300}, {659, 300},
{440, 300}, {494, 300}, {440, 300},
{494, 300}, {392, 150}, {392, 150}, {440, 150}, {392, 150}, {0, 300}, {494, 300},
{494, 300}, {392, 150}, {494, 150}, {494, 150}, {659, 150}, {523, 150}, {494, 300},
{659, 300}, {392, 150}, {659, 150}, {587, 150}, {587, 150}, {494, 300},
{494, 150}, {392, 150}, {440, 150}, {494, 150}, {523, 150}, {494, 150}, {440, 150}, {392, 150},
{494, 300}, {392, 150}, {392, 150}, {440, 150}, {392, 150}, {0, 300}, {494, 300},
{494, 300}, {392, 150}, {494, 150}, {494, 150}, {659, 150}, {523, 150}, {494, 300},
{659, 300}, {392, 150}, {659, 150}, {587, 150}, {587, 150}, {494, 300},
{523, 300}, {392, 150}, {659, 150}, {587, 150}, {523, 150}, {494, 150}, {440, 150},
{440, 150}, {392, 150}, {440, 150}, {392, 150}, {0, 300}, {0, 300},
{0, 600}, {0, 600}, {0, 600}, {0, 450}, {494, 150},
{494, 150}, {392, 150}, {392, 150}, {392, 150}, {494, 150}, {494, 150}, {392, 150}, {392, 150}, {392, 150}, {494, 150},
{494, 150}, {392, 150}, {392, 150}, {392, 150}, {494, 150}, {523, 150}, {494, 150}, {494, 150},
{494, 150}, {392, 150}, {392, 150}, {392, 150}, {494, 150}, {494, 150}, {392, 150}, {392, 150}, {392, 150}, {494, 150},
{494, 150}, {392, 150}, {392, 150}, {392, 150}, {440, 150}, {392, 150}, {392, 150}, {494, 150}
};
static const int blessing_len = sizeof(blessing) / sizeof(blessing[0]);
// === 新增:第二、第三首歌曲旋律 ===
static const tone_t song2[] = {
{523, 200}, {587, 200}, {659, 200}, {784, 400},
{659, 200}, {587, 200}, {523, 400},
{0, 200}
};
static const int song2_len = sizeof(song2) / sizeof(song2[0]);
static const tone_t song3[] = {
{392, 150}, {440, 150}, {494, 150}, {523, 150},
{587, 150}, {659, 150}, {740, 150}, {784, 300},
{0, 300}
};
static const int song3_len = sizeof(song3) / sizeof(song3[0]);
/* 音乐列表 - 已填充真实数据 */
static const char* music_names[3] = {"Blessing", "Song 2", "Song 3"};
static const tone_t* music_tracks[3] = {blessing, song2, song3}; // 修改:不再为 NULL
static const int music_lengths[3] = {blessing_len, song2_len, song3_len};
/* 字体数据:6x8 ASCII */
const uint8_t font6x8[95 * 6] = {
0x00,0x00,0x00,0x00,0x00,0x00, // ' '
0x00,0x03,0x00,0x03,0x00,0x00, // "
0x14,0x3E,0x14,0x3E,0x14,0x00, // #
0x24,0x2A,0x7F,0x2A,0x12,0x00, // $
0x43,0x33,0x08,0x66,0x61,0x00, // %
0x36,0x49,0x55,0x22,0x50,0x00, // &
0x00,0x05,0x03,0x00,0x00,0x00, // '
0x00,0x1C,0x22,0x41,0x00,0x00, // (
0x00,0x41,0x22,0x1C,0x00,0x00, // )
0x14,0x08,0x3E,0x08,0x14,0x00, // *
0x08,0x08,0x3E,0x08,0x08,0x00, // +
0x00,0x50,0x30,0x00,0x00,0x00, // ,
0x08,0x08,0x08,0x08,0x08,0x00, // -
0x00,0x60,0x60,0x00,0x00,0x00, // .
0x20,0x10,0x08,0x04,0x02,0x00, // /
0x3E,0x51,0x49,0x45,0x3E,0x00, // 0
0x00,0x42,0x7F,0x40,0x00,0x00, // 1
0x42,0x61,0x51,0x49,0x46,0x00, // 2
0x21,0x41,0x45,0x4B,0x31,0x00, // 3
0x18,0x14,0x12,0x7F,0x10,0x00, // 4
0x27,0x45,0x45,0x45,0x39,0x00, // 5
0x3C,0x4A,0x49,0x49,0x30,0x00, // 6
0x01,0x71,0x09,0x05,0x03,0x00, // 7
0x36,0x49,0x49,0x49,0x36,0x00, // 8
0x06,0x49,0x49,0x29,0x1E,0x00, // 9
0x00,0x36,0x36,0x00,0x00,0x00, // :
0x00,0x56,0x36,0x00,0x00,0x00, // ;
0x08,0x14,0x22,0x41,0x00,0x00, // <
0x14,0x14,0x14,0x14,0x14,0x00, // =
0x00,0x41,0x22,0x14,0x08,0x00, // >
0x02,0x01,0x51,0x09,0x06,0x00, // ?
0x3E,0x41,0x5D,0x55,0x1E,0x00, // @
0x7C,0x12,0x11,0x12,0x7C,0x00, // A
0x7F,0x49,0x49,0x49,0x36,0x00, // B
0x3E,0x41,0x41,0x41,0x22,0x00, // C
0x7F,0x41,0x41,0x22,0x1C,0x00, // D
0x7F,0x49,0x49,0x49,0x41,0x00, // E
0x7F,0x09,0x09,0x09,0x01,0x00, // F
0x3E,0x41,0x49,0x49,0x7A,0x00, // G
0x7F,0x08,0x08,0x08,0x7F,0x00, // H
0x00,0x41,0x7F,0x41,0x00,0x00, // I
0x20,0x40,0x41,0x3F,0x01,0x00, // J
0x7F,0x08,0x14,0x22,0x41,0x00, // K
0x7F,0x40,0x40,0x40,0x40,0x00, // L
0x7F,0x02,0x0C,0x02,0x7F,0x00, // M
0x7F,0x04,0x08,0x10,0x7F,0x00, // N
0x3E,0x41,0x41,0x41,0x3E,0x00, // O
0x7F,0x09,0x09,0x09,0x06,0x00, // P
0x3E,0x41,0x51,0x21,0x5E,0x00, // Q
0x7F,0x09,0x19,0x29,0x46,0x00, // R
0x46,0x49,0x49,0x49,0x31,0x00, // S
0x01,0x01,0x7F,0x01,0x01,0x00, // T
0x3F,0x40,0x40,0x40,0x3F,0x00, // U
0x1F,0x20,0x40,0x20,0x1F,0x00, // V
0x3F,0x40,0x38,0x40,0x3F,0x00, // W
0x63,0x14,0x08,0x14,0x63,0x00, // X
0x07,0x08,0x70,0x08,0x07,0x00, // Y
0x61,0x51,0x49,0x45,0x43,0x00, // Z
0x00,0x7F,0x41,0x41,0x00,0x00, // [
0x02,0x04,0x08,0x10,0x20,0x00, // '\\'
0x00,0x41,0x41,0x7F,0x00,0x00, // ]
0x04,0x02,0x01,0x02,0x04,0x00, // ^
0x40,0x40,0x40,0x40,0x40,0x00, // _
0x00,0x01,0x02,0x04,0x00,0x00, // `
0x20,0x54,0x54,0x54,0x78,0x00, // a
0x7F,0x44,0x44,0x44,0x38,0x00, // b
0x38,0x44,0x44,0x44,0x20,0x00, // c
0x38,0x44,0x44,0x44,0x7F,0x00, // d
0x38,0x54,0x54,0x54,0x18,0x00, // e
0x08,0x7E,0x09,0x01,0x02,0x00, // f
0x18,0xA4,0xA4,0xA4,0x7C,0x00, // g
0x7F,0x08,0x04,0x04,0x78,0x00, // h
0x00,0x44,0x7D,0x40,0x00,0x00, // i
0x40,0x80,0x84,0x7D,0x00,0x00, // j
0x7F,0x10,0x28,0x44,0x00,0x00, // k
0x00,0x41,0x7F,0x40,0x00,0x00, // l
0x7C,0x04,0x18,0x04,0x78,0x00, // m
0x7C,0x08,0x04,0x04,0x78,0x00, // n
0x38,0x44,0x44,0x44,0x38,0x00, // o
0xFC,0x24,0x24,0x24,0x18,0x00, // p
0x18,0x24,0x24,0x18,0xFC,0x00, // q
0x7C,0x08,0x04,0x04,0x08,0x00, // r
0x48,0x54,0x54,0x54,0x20,0x00, // s
0x04,0x04,0x3F,0x44,0x40,0x00, // t
0x3C,0x40,0x40,0x20,0x7C,0x00, // u
0x1C,0x20,0x40,0x20,0x1C,0x00, // v
0x3C,0x40,0x30,0x40,0x3C,0x00, // w
0x44,0x28,0x10,0x28,0x44,0x00, // x
0x1C,0xA0,0xA0,0x7C,0x00,0x00, // y
0x44,0x64,0x54,0x4C,0x44,0x00, // z
0x00,0x08,0x36,0x41,0x00,0x00, // {
0x00,0x00,0x77,0x00,0x00,0x00, // |
0x00,0x41,0x36,0x08,0x00,0x00, // }
0x02,0x01,0x02,0x04,0x02,0x00 // ~
};
/* ---------- 前向声明 ---------- */
static void ws2812b_init(void);
static void ttp229_init(void);
static uint16_t ttp229_read_keys(void);
static void buzzer_init(void);
static void buzzer_play_tone(int freq);
static void oled_refresh(void);
static void oled_clear(void);
static void oled_draw_bitmap(int x, int y, int w, int h, const uint8_t *bm);
static void oled_draw_face_happy(void);
static void oled_draw_music_symbol(void);
static void led_update_from_state(void);
static void led_fade_task(void *arg);
static void blessing_task(void *arg);
static void start_blessing_task(void);
static void stop_blessing_task(void);
static void game_spawn_block(void);
static void game_draw(void);
void oled_animation(void); // 替换 run_animation
static void oled_draw_menu(const char* title, const char* items[], int item_count);
/* --- 新增:OLED 动画函数 --- */
void oled_animation(void) {
int x = 0;
for (int frame = 0; frame < 100 && xTaskGetTickCount() > 0; frame++) {
memset(oled_buf, 0, OLED_BUF_SIZE);
// 绘制移动的笑脸眼睛
int lx = x, rx = x + 20;
if (lx >= OLED_WIDTH - 10) break;
for (int yy = 20; yy <= 22; yy++) {
for (int xx = lx + 30; xx <= lx + 40; xx++) {
if (yy == 21 || yy == 22) {
int page = yy / 8;
int idx = page * OLED_WIDTH + xx;
oled_buf[idx] |= (1 << (yy & 7));
}
}
}
for (int yy = 20; yy <= 22; yy++) {
for (int xx = rx + 85; xx <= rx + 95; xx++) {
if (yy == 21 || yy == 22) {
int page = yy / 8;
int idx = page * OLED_WIDTH + xx;
oled_buf[idx] |= (1 << (yy & 7));
}
}
}
// 微笑弧线
for (int xx = 46; xx < 82; xx++) {
int yy = 42 + (int)(4.0f * sinf(((xx - 46) / 36.0f) * M_PI));
if (yy < 0 || yy >= OLED_HEIGHT) continue;
int page = yy / 8;
int idx = page * OLED_WIDTH + xx;
oled_buf[idx] |= (1 << (yy & 7));
if (yy + 1 < OLED_HEIGHT) {
int page2 = (yy + 1) / 8;
oled_buf[page2 * OLED_WIDTH + xx] |= (1 << ((yy + 1) & 7));
}
}
oled_refresh();
x += 2;
vTaskDelay(pdMS_TO_TICKS(80));
uint16_t k = ttp229_read_keys();
if (k & ((1<<4)|(1<<5)|(1<<6)|(1<<7))) break;
}
oled_clear();
oled_draw_face_happy();
}
/* ========== 原有函数保持不变 ========== */
static void ws2812b_init(void){
led_strip_config_t strip_config = {
.strip_gpio_num = WS2812B_PIN,
.max_leds = NUM_LEDS,
};
led_strip_rmt_config_t rmt_config = {
.resolution_hz = 10 * 1000 * 1000,
};
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config,&rmt_config,&led_strip));
if (!led_mutex) led_mutex = xSemaphoreCreateMutex();
for (int i=0;i<NUM_LEDS;i++){
led_brightness[i]=255;
led_active[i]=0;
led_strip_set_pixel(led_strip, i, rainbow_colors[i][0], rainbow_colors[i][1], rainbow_colors[i][2]);
}
led_strip_refresh(led_strip);
}
static void ttp229_init(void) {
gpio_config_t io_conf = {0};
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = (1ULL << TTP229_SCL);
gpio_config(&io_conf);
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pin_bit_mask = (1ULL << TTP229_SDO);
gpio_config(&io_conf);
gpio_set_level(TTP229_SCL, 1);
vTaskDelay(pdMS_TO_TICKS(10));
}
static uint16_t ttp229_read_keys(void) {
uint16_t data = 0;
gpio_set_level(TTP229_SCL, 1);
esp_rom_delay_us(2);
for (int i = 0; i < 16; i++) {
gpio_set_level(TTP229_SCL, 0);
esp_rom_delay_us(2);
int bit = gpio_get_level(TTP229_SDO);
if (bit == 0) data |= (1 << i);
gpio_set_level(TTP229_SCL, 1);
esp_rom_delay_us(2);
}
return data;
}
static void buzzer_init(void) {
ledc_timer_config_t timer_conf = {
.duty_resolution = LEDC_TIMER_10_BIT,
.freq_hz = 1000,
.speed_mode = LEDC_LOW_SPEED_MODE,
.timer_num = LEDC_TIMER_0
};
ledc_timer_config(&timer_conf);
ledc_channel_config_t channel_conf = {
.channel = LEDC_CHANNEL_0,
.duty = 0,
.gpio_num = BUZZER_PIN,
.speed_mode = LEDC_LOW_SPEED_MODE,
.timer_sel = LEDC_TIMER_0
};
ledc_channel_config(&channel_conf);
}
static void buzzer_play_tone(int freq) {
if (freq <= 0) {
ledc_stop(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0);
return;
}
ledc_set_freq(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0, freq);
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 512);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
}
static void oled_send_cmd(const uint8_t *data, size_t len) {
spi_transaction_t t = {
.length = len * 8,
.tx_buffer = data,
.flags = 0
};
gpio_set_level(OLED_DC_PIN, 0);
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_oled, &t));
}
static void oled_send_data(const uint8_t *data, size_t len) {
spi_transaction_t t = {
.length = len * 8,
.tx_buffer = data,
.flags = 0
};
gpio_set_level(OLED_DC_PIN, 1);
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_oled, &t));
}
static void oled_refresh(void) {
const uint8_t setcol[] = {0x21, 0, OLED_WIDTH-1};
const uint8_t setpage[] = {0x22, 0, (OLED_HEIGHT/8)-1};
oled_send_cmd(setcol, sizeof(setcol));
oled_send_cmd(setpage, sizeof(setpage));
oled_send_data(oled_buf, OLED_BUF_SIZE);
}
static void oled_clear(void) {
memset(oled_buf, 0, OLED_BUF_SIZE);
oled_refresh();
}
static void oled_draw_bitmap(int x, int y, int w, int h, const uint8_t *bm) {
int bytes_per_col = (h + 7)/8;
for (int col=0; col<w; col++) {
for (int b=0;b<bytes_per_col;b++) {
int src_idx = col*bytes_per_col + b;
uint8_t v = bm[src_idx];
for (int bit=0;bit<8;bit++) {
int yy = y + b*8 + bit;
int xx = x + col;
if (yy>=y+h) break;
if (xx<0 || xx>=OLED_WIDTH || yy<0 || yy>=OLED_HEIGHT) continue;
int page = yy/8;
int dst_idx = page*OLED_WIDTH + xx;
if (v & (1<<bit)) oled_buf[dst_idx] |= (1<<(yy&7));
}
}
}
}
static void oled_draw_face_happy(void) {
memset(oled_buf, 0, OLED_BUF_SIZE);
for (int yy = 20; yy <= 22; yy++) {
for (int xx = 30; xx <= 40; xx++) {
if (yy == 21 || yy == 22) {
int page = yy / 8;
int idx = page * OLED_WIDTH + xx;
oled_buf[idx] |= (1 << (yy & 7));
}
}
}
for (int yy = 20; yy <= 22; yy++) {
for (int xx = 85; xx <= 95; xx++) {
if (yy == 21 || yy == 22) {
int page = yy / 8;
int idx = page * OLED_WIDTH + xx;
oled_buf[idx] |= (1 << (yy & 7));
}
}
}
for (int xx = 46; xx < 82; xx++) {
int yy = 42 + (int)(4.0f * sinf(((xx - 46) / 36.0f) * M_PI));
if (yy < 0 || yy >= OLED_HEIGHT) continue;
int page = yy / 8;
int idx = page * OLED_WIDTH + xx;
oled_buf[idx] |= (1 << (yy & 7));
if (yy + 1 < OLED_HEIGHT) {
int page2 = (yy + 1) / 8;
oled_buf[page2 * OLED_WIDTH + xx] |= (1 << ((yy + 1) & 7));
}
}
oled_refresh();
}
static void oled_draw_music_symbol(void) {
memset(oled_buf,0,OLED_BUF_SIZE);
oled_draw_bitmap(56, 20, 12, 12, bm_music);
oled_refresh();
}
static void oled_draw_menu(const char* title, const char* items[], int item_count) {
memset(oled_buf, 0, OLED_BUF_SIZE);
int len = strlen(title) * 6;
int x = (OLED_WIDTH - len) / 2;
int y = 0;
for (int i = 0; title[i]; i++) {
uint8_t ch = title[i] - ' ';
const uint8_t* font = &font6x8[ch * 6];
for (int col = 0; col < 6; col++) {
if (col >= 6) break;
uint8_t b = font[col];
for (int bit = 0; bit < 8; bit++) {
if (b & (1 << bit)) {
int py = y + bit, px = x + i * 6 + col;
if (px < OLED_WIDTH && py < OLED_HEIGHT) {
oled_buf[(py / 8) * OLED_WIDTH + px] |= (1 << (py & 7));
}
}
}
}
}
y = 16;
for (int i = 0; i < item_count; i++) {
if (i == selected_item) {
oled_buf[(y / 8) * OLED_WIDTH + 2] |= (1 << (y & 7));
}
len = strlen(items[i]);
for (int j = 0; j < len; j++) {
int px = 10 + j * 6, py = y;
uint8_t ch = items[i][j] - ' ';
const uint8_t* font = &font6x8[ch * 6];
for (int col = 0; col < 6; col++) {
uint8_t b = font[col];
for (int bit = 0; bit < 8; bit++) {
if (b & (1 << bit)) {
int ppy = py + bit;
if (px + col < OLED_WIDTH && ppy < OLED_HEIGHT) {
oled_buf[(ppy / 8) * OLED_WIDTH + px + col] |= (1 << (ppy & 7));
}
}
}
}
}
y += 10;
}
oled_refresh();
}
static void set_led_rgb(int idx, uint8_t r, uint8_t g, uint8_t b, uint8_t brightness) {
uint8_t r2 = (r * brightness)/255;
uint8_t g2 = (g * brightness)/255;
uint8_t b2 = (b * brightness)/255;
led_strip_set_pixel(led_strip, idx, r2, g2, b2);
}
static void led_update_from_state(void) {
if (!led_mutex) return;
xSemaphoreTake(led_mutex, portMAX_DELAY);
for (int i=0;i<NUM_LEDS;i++){
uint8_t b = led_brightness[i];
if (b==0) led_strip_set_pixel(led_strip,i,0,0,0);
else set_led_rgb(i, rainbow_colors[i][0], rainbow_colors[i][1], rainbow_colors[i][2], b);
}
led_strip_refresh(led_strip);
xSemaphoreGive(led_mutex);
}
static void led_fade_task(void *arg) {
while (1) {
bool changed = false;
xSemaphoreTake(led_mutex, portMAX_DELAY);
if (g_mode == MODE_LED_KEY_INTERACT) {
for (int i=0;i<NUM_LEDS;i++){
if (!led_active[i] && led_brightness[i] > 0) {
int nb = (int)led_brightness[i] - 8;
if (nb < 0) nb = 0;
led_brightness[i] = (uint8_t)nb;
changed = true;
}
}
} else if (g_mode == MODE_LED_GRADIENT_CYCLE && gradient_run) {
gradient_pos++;
for (int i=0;i<NUM_LEDS;i++){
int pos = (i + gradient_pos) % NUM_LEDS;
float v = (1.0f + sinf((pos*2.0f*M_PI)/NUM_LEDS)) * 0.5f;
int b = 80 + (int)(175.0f * v);
if (b < 0) b = 0;
if (b>255) b = 255;
led_brightness[i] = (uint8_t)b;
}
changed = true;
} else {
for (int i=0;i<NUM_LEDS;i++){
if (led_brightness[i] != 255) { led_brightness[i] = 255; changed = true; }
}
}
xSemaphoreGive(led_mutex);
if (changed) led_update_from_state();
vTaskDelay(pdMS_TO_TICKS(40));
}
}
static void blessing_task(void *arg) {
ESP_LOGI(TAG, "Blessing task started");
const tone_t* track = music_tracks[selected_item];
int len = music_lengths[selected_item];
while (blessing_running) {
for (int i=0;i<len && blessing_running;i++){
int f = track[i].freq;
int d = track[i].dur;
if (f > 0) buzzer_play_tone(f);
else buzzer_play_tone(0);
int led_idx = i % NUM_LEDS;
xSemaphoreTake(led_mutex, portMAX_DELAY);
led_brightness[led_idx] = 255;
xSemaphoreGive(led_mutex);
led_update_from_state();
int chunks = d / 20;
if (chunks < 1) chunks = 1;
for (int c=0;c<chunks && blessing_running;c++){
vTaskDelay(pdMS_TO_TICKS(d / chunks));
}
buzzer_play_tone(0);
vTaskDelay(pdMS_TO_TICKS(40));
}
}
buzzer_play_tone(0);
ESP_LOGI(TAG, "Blessing task exit");
blessing_task_handle = NULL;
vTaskDelete(NULL);
}
static void start_blessing_task(void) {
if (blessing_running) return;
blessing_running = true;
BaseType_t r = xTaskCreate(blessing_task, "blessing", 4096, NULL, 5, &blessing_task_handle);
if (r != pdPASS) {
ESP_LOGW(TAG,"Create blessing task failed");
blessing_running = false;
blessing_task_handle = NULL;
}
}
static void stop_blessing_task(void) {
if (!blessing_running) return;
blessing_running = false;
int t = 0;
while (blessing_task_handle != NULL && t < 50) { vTaskDelay(pdMS_TO_TICKS(50)); t++; }
buzzer_play_tone(0);
}
static void game_spawn_block(void) {
int tries = 0;
while (tries < 10) {
int r = esp_random() % 7;
if (!game_regions[r]) { game_regions[r] = 1; break; }
tries++;
}
}
static void game_draw(void) {
memset(oled_buf,0,OLED_BUF_SIZE);
int region_w = OLED_WIDTH / 7;
for (int i=0;i<7;i++){
int x = i*region_w + 4;
int y = 12;
int w = region_w - 8;
int h = 40;
if (game_regions[i]) {
for (int yy=y; yy<y+h; yy++) for (int xx=x; xx<x+w; xx++){
int page = yy/8; int idx = page*OLED_WIDTH + xx;
oled_buf[idx] |= (1 << (yy&7));
}
} else {
for (int xx=x;xx<x+w;xx++){
int yy=y; int page=yy/8; oled_buf[page*OLED_WIDTH+xx] |= (1<<(yy&7));
yy=y+h-1; page=yy/8; oled_buf[page*OLED_WIDTH+xx] |= (1<<(yy&7));
}
for (int yy=y;yy<y+h;yy++){
int xx=x; int page=yy/8; oled_buf[page*OLED_WIDTH+xx] |= (1<<(yy&7));
xx=x+w-1; page=yy/8; oled_buf[page*OLED_WIDTH+xx] |= (1<<(yy&7));
}
}
}
oled_refresh();
}
void app_main(void) {
ESP_LOGI(TAG,"App start");
ws2812b_init();
ttp229_init();
buzzer_init();
gpio_config_t io_conf = {0};
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = (1ULL<<OLED_DC_PIN) | (1ULL<<OLED_CS_PIN) | (1ULL<<OLED_RST_PIN);
gpio_config(&io_conf);
spi_bus_config_t buscfg = {
.miso_io_num = -1,
.mosi_io_num = OLED_MOSI_PIN,
.sclk_io_num = OLED_SCLK_PIN,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = OLED_BUF_SIZE + 8
};
ESP_ERROR_CHECK(spi_bus_initialize(OLED_SPI_HOST, &buscfg, 1));
spi_device_interface_config_t devcfg = {
.clock_speed_hz = 8*1000*1000,
.mode = 0,
.spics_io_num = OLED_CS_PIN,
.queue_size = 1,
};
ESP_ERROR_CHECK(spi_bus_add_device(OLED_SPI_HOST, &devcfg, &spi_oled));
gpio_set_level(OLED_RST_PIN, 0);
vTaskDelay(pdMS_TO_TICKS(10));
gpio_set_level(OLED_RST_PIN, 1);
vTaskDelay(pdMS_TO_TICKS(10));
const uint8_t init_cmds[] = {
0xAE,0xD5,0x80,0xA8,0x3F,0xD3,0x00,0x40,0x8D,0x14,0x20,0x00,0xA1,0xC8,0xDA,0x12,0x81,0xCF,0xD9,0xF1,0xDB,0x40,0xA4,0xA6,0xAF
};
oled_send_cmd(init_cmds, sizeof(init_cmds));
oled_clear();
xTaskCreate(led_fade_task, "led_fade", 4096, NULL, 5, NULL);
uint16_t prev_keys = 0;
ESP_LOGI(TAG,"Main loop start");
while (1) {
uint16_t keys = ttp229_read_keys();
uint16_t newly = (keys & ~prev_keys);
uint16_t releases = (~keys) & prev_keys;
bool tp4_pressed = keys & (1 << 4);
bool tp5_pressed = keys & (1 << 5);
// === 功能键单独处理 ===
if (newly & (1 << 4)) {
if (current_menu == MENU_NONE && !(tp5_pressed)) {
if (g_mode == MODE_LED_KEY_INTERACT) {
g_mode = MODE_NORMAL;
oled_clear();
oled_draw_face_happy();
} else {
g_mode = MODE_LED_KEY_INTERACT;
oled_clear();
oled_draw_face_happy();
}
vTaskDelay(pdMS_TO_TICKS(200));
}
}
if (newly & (1 << 5)) {
if (current_menu == MENU_NONE && !(tp4_pressed)) {
if (g_mode == MODE_LED_GRADIENT_CYCLE) {
g_mode = MODE_NORMAL;
gradient_run = false;
oled_clear();
oled_draw_face_happy();
} else {
g_mode = MODE_LED_GRADIENT_CYCLE;
gradient_run = true;
oled_clear();
oled_draw_face_happy();
}
vTaskDelay(pdMS_TO_TICKS(200));
}
}
if (newly & (1 << 6)) {
if (current_menu == MENU_NONE) {
current_menu = MENU_MUSIC_SELECT;
selected_item = 0;
g_mode = MODE_NORMAL;
stop_blessing_task();
oled_draw_menu("Music", music_names, 3);
} else if (current_menu == MENU_MUSIC_SELECT) {
current_menu = MENU_NONE;
oled_clear();
oled_draw_face_happy();
}
vTaskDelay(pdMS_TO_TICKS(200));
}
if (newly & (1 << 7)) {
if (current_menu == MENU_NONE) {
current_menu = MENU_FUNC_SELECT;
selected_item = 0;
static const char* func_items[] = {"Game", "Animation"};
oled_draw_menu("Func", func_items, 2);
} else if (current_menu == MENU_FUNC_SELECT) {
current_menu = MENU_NONE;
oled_clear();
oled_draw_face_happy();
}
vTaskDelay(pdMS_TO_TICKS(200));
}
// === 菜单内导航 ===
if (current_menu == MENU_MUSIC_SELECT) {
if (newly & (1 << 5)) {
selected_item = (selected_item - 1 + 3) % 3;
oled_draw_menu("Music", music_names, 3);
} else if (newly & (1 << 7)) {
selected_item = (selected_item + 1) % 3;
oled_draw_menu("Music", music_names, 3);
} else if (newly & (1 << 4)) {
if (music_tracks[selected_item]) {
stop_blessing_task();
start_blessing_task();
g_mode = MODE_BLESSING_PLAY;
oled_clear();
oled_draw_music_symbol();
}
current_menu = MENU_NONE;
}
vTaskDelay(pdMS_TO_TICKS(150));
prev_keys = keys;
continue;
}
if (current_menu == MENU_FUNC_SELECT) {
if (newly & (1 << 4)) {
selected_item = (selected_item - 1 + 2) % 2;
static const char* items[] = {"Game", "Animation"};
oled_draw_menu("Func", items, 2);
} else if (newly & (1 << 6)) {
selected_item = (selected_item + 1) % 2;
static const char* items[] = {"Game", "Animation"};
oled_draw_menu("Func", items, 2);
} else if (newly & (1 << 5)) {
if (selected_item == 0) {
g_mode = MODE_GAME;
game_running = true;
memset(game_regions, 0, sizeof(game_regions));
game_next_spawn = xTaskGetTickCount() + pdMS_TO_TICKS(800);
oled_clear();
game_draw();
} else {
g_mode = MODE_NORMAL;
oled_animation(); // 改为 OLED 动画
}
current_menu = MENU_NONE;
}
vTaskDelay(pdMS_TO_TICKS(150));
prev_keys = keys;
continue;
}
// === 游戏模式:可随时按 TP7 退出 ===
if (g_mode == MODE_GAME && game_running) {
if (xTaskGetTickCount() > game_next_spawn) {
game_spawn_block();
game_next_spawn = xTaskGetTickCount() + pdMS_TO_TICKS(1000 + (esp_random()%2000));
game_draw();
}
if (newly & (1 << 7)) {
g_mode = MODE_NORMAL;
game_running = false;
oled_clear();
oled_draw_face_happy();
}
}
// === 处理琴键输入 ===
if (keys) {
bool played_any = false;
for (int i=0;i<16;i++){
uint8_t tp = key_map[i];
if (keys & (1 << tp)) {
if (tp>=4 && tp<=7) continue;
if (g_mode == MODE_GAME) {
int region = tp_to_game_region[tp];
if (region >= 0 && region < 7) {
if (game_regions[region]) {
game_regions[region] = 0;
buzzer_play_tone(880);
vTaskDelay(pdMS_TO_TICKS(80));
buzzer_play_tone(0);
game_draw();
} else {
buzzer_play_tone(220);
vTaskDelay(pdMS_TO_TICKS(60));
buzzer_play_tone(0);
}
}
} else {
float ratio = powf(2.0f, pitch_offset_semitones / 12.0f);
int adjusted_freq = (int)(note_freqs[i] * ratio);
if (g_mode != MODE_BLESSING_PLAY) {
buzzer_play_tone(adjusted_freq);
played_any = true;
}
int led_idx = tp_to_led_idx[tp];
if (g_mode == MODE_LED_KEY_INTERACT && led_idx >= 0 && led_idx < NUM_LEDS) {
xSemaphoreTake(led_mutex, portMAX_DELAY);
led_active[led_idx] = 1;
led_brightness[led_idx] = 255;
xSemaphoreGive(led_mutex);
led_update_from_state();
}
if (g_mode == MODE_LED_GRADIENT_CYCLE && led_idx >= 0 && led_idx < NUM_LEDS) {
xSemaphoreTake(led_mutex, portMAX_DELAY);
led_brightness[led_idx] = 255;
xSemaphoreGive(led_mutex);
led_update_from_state();
}
if (g_mode == MODE_NORMAL) {
memset(oled_buf,0,OLED_BUF_SIZE);
int bars = NUM_LEDS;
int w = OLED_WIDTH / bars;
for (int bi=0;bi<bars;bi++){
int x = bi*w + 1;
int base_y = OLED_HEIGHT - 2;
int bar_h = (bi == i) ? (OLED_HEIGHT/2) : (OLED_HEIGHT/6);
for (int yy=base_y-bar_h; yy<=base_y; yy++){
for (int xx = x; xx < x + w - 2; xx++){
if (xx<0 || xx>=OLED_WIDTH || yy<0 || yy>=OLED_HEIGHT) continue;
int page = yy/8; int idx = page*OLED_WIDTH + xx;
oled_buf[idx] |= (1 << (yy&7));
}
}
}
oled_refresh();
}
}
}
}
vTaskDelay(pdMS_TO_TICKS(40));
} else {
if (g_mode != MODE_BLESSING_PLAY) buzzer_play_tone(0);
}
if (releases) {
for (int i=0;i<16;i++){
uint8_t tp = key_map[i];
if (releases & (1<<tp)) {
int led_idx = tp_to_led_idx[tp];
if (g_mode == MODE_LED_KEY_INTERACT && led_idx >=0 && led_idx < NUM_LEDS) {
xSemaphoreTake(led_mutex, portMAX_DELAY);
led_active[led_idx] = 0;
xSemaphoreGive(led_mutex);
}
}
}
}
if (newly & (1 << 5)) {
if (current_menu == MENU_NONE && g_mode == MODE_NORMAL) {
oled_clear();
oled_draw_face_happy();
}
}
prev_keys = keys;
vTaskDelay(pdMS_TO_TICKS(20));
}
}
```
---
### 知识点
1. **SPI 驱动 OLED 显示原理**
通过 SPI 发送命令与数据,控制 SSD1306 屏幕刷新帧缓冲区。
2. **蜂鸣器 PWM 音调生成**
使用 LEDC 模块调节频率与占空比输出不同音符。
3. **有限状态机设计模式**
利用枚举与全局变量管理多模式切换,确保逻辑清晰无冲突。
```