1、系统框图

2、电路原理图
- TCS3200 S0 → ESP32 GPIO13- TCS3200 S1 → ESP32 GPIO16- TCS3200 S2 → ESP32 GPIO17- TCS3200 S3 → ESP32 GPIO15- TCS3200 OUT → ESP32 GPIO14- TCS3200 VCC → 3.3V/5V (根据传感器规格)- TCS3200 GND → GND
蜂鸣器
VCC → 3.3V
GND → GND
PWM→ ESP32 GPIO18
3、怎样开始运行
下载附件的代码eepw.zip,并按说明文档接好相关的线序,编译运行即可。
按串口日志的提示,完成颜色传感器的校准,就可以开始工作了。
4、实现步骤
前面的帖子已经说明了,怎样用AI做好颜色传感器的驱动。
接下来,我们继续做音频播放部分,这里我们直接找一个超级玛丽的python播放脚本,然后复制下来给AI抄作业。

在github上能找到这个,有播放的逻辑和曲谱。让AI实现PWM播放超级玛丽音乐,有3个比较蛋疼的地方。
1、如何正确使用pwm并实现pwm播放音效,由于GLM 4.6并没有那么智能,还是在提示词里直接说明去esp idf的SDK里直接搜example,并参考里面的代码正确实现PWM播放功能。截图找不到了,就先放着吧。
2、如何让GLM正确理解播放的逻辑,这里也反复折腾了2个回合,直到GLM真正意识到音轨和播放时长的正确映射关系。
3、反复解决GLM的一些小误会,例如错误把音符当做时长,算错index等等。

完全使用AI智能体来编程和实现demo或者项目代码,虽然会遇到各种各样的麻烦和弱智的操作,但是也不是没有好处。
好处之一就是,可以完全把自己放松下来,当成辅导下属或者带人完成一个demo或者项目。好处之二就是,会越来越重视,先做好技术栈,实现路径的规划,以及先制作项目文档,分解任务目标,再一步步推进实施的良好迭代习惯。
当然,最大的好处也就是可以把我们从繁杂的写代码,修改编译报错(API错误,语法等等)这些重复的工作里解脱出来,抽出时间去做其他的事儿。
最终实现的代码mario_audio.c
#include "mario_audio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/ledc.h"
#include "esp_log.h"
#include <string.h>
#include <inttypes.h>
static const char *TAG = "mario_audio";
// LEDC配置
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_DUTY_RES LEDC_TIMER_13_BIT
#define LEDC_FREQUENCY (5000) // 5KHz
#define LEDC_DUTY (4096/2) // 50%占空比
// 音频播放状态
static audio_state_t s_audio_state = AUDIO_STATE_STOPPED;
static TaskHandle_t s_audio_task_handle = NULL;
static int s_audio_gpio = -1;
static uint8_t s_volume = 30; // 默认音量30%
static const song_t* s_current_song = NULL;
// 各音轨独立的音符数组
static const uint8_t mario_track0_notes[] = {76,12,76,12,20,12,76,12,20,12,72,12,76,12,20,12,79,12,20,36,67,12,20,36};
static const uint8_t mario_track1_notes[] = {72,12,20,24,67,12,20,24,64,12,20,24,69,12,20,12,71,12,20,12,70,12,69,12,20,12,67,16,76,16,79,16,81,12,20,12,77,12,79,12,20,12,76,12,20,12,72,12,74,12,71,12,20,24};
static const uint8_t mario_track2_notes[] = {48,12,20,12,79,12,78,12,77,12,75,12,60,12,76,12,53,12,68,12,69,12,72,12,60,12,69,12,72,12,74,12,48,12,20,12,79,12,78,12,77,12,75,12,55,12,76,12,20,12,84,12,20,12,84,12,84,12};
static const uint8_t mario_track3_notes[] = {55,12,20,12,48,12,20,12,79,12,78,12,77,12,75,12,60,12,76,12,53,12,68,12,69,12,72,12,60,12,69,12,72,12,74,12,48,12,20,12,75,24,20,12,74,24,20,12,72,24,20,12,55,12,55,12,20,12,48,12};
static const uint8_t mario_track4_notes[] = {72,12,72,12,20,12,72,12,20,12,72,12,74,12,20,12,76,12,72,12,20,12,69,12,67,12,20,12,43,12,20,12,72,12,72,12,20,12,72,12,20,12,72,12,74,12,76,12,55,12,20,24,48,12,20,24,43,12,20,12,72,12,72,12,20,12,72,12,20,12,72,12,74,12,20,12,76,12,72,12,20,12,69,12,67,12,20,12,43,12,20,12,76,12,76,12,20,12,76,12,20,12,72,12,76,12,20,12,79,12,20,36,67,12,20,36};
static const uint8_t mario_track5_notes[] = {76,12,72,12,20,12,67,12,55,12,20,12,68,12,20,12,69,12,77,12,53,12,77,12,69,12,60,12,53,12,20,12,71,16,81,16,81,16,81,16,79,16,77,16,76,12,72,12,55,12,69,12,67,12,60,12,55,12,20,12,76,12,72,12,20,12,67,12,55,12,20,12,68,12,20,12,69,12,77,12,53,12,77,12,69,12,60,12,53,12,20,12,71,12,77,12,20,12,77,12,77,16,76,16,74,16,72,12,64,12,55,12,64,12,60,12,20,36};
static const uint8_t mario_track6_notes[] = {72,12,20,24,67,12,20,24,64,24,69,16,71,16,69,16,68,24,70,24,68,24,67,12,65,12,67,48};
// 各音轨独立的节奏数组
static const uint16_t mario_track0_tempos[] = {100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,100,100,20,100,100};
static const uint16_t mario_track1_tempos[] = {100,20,100,100,100,20,100,100,100,20,100,100,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,100,100,100,100,100,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,100};
static const uint16_t mario_track2_tempos[] = {100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20};
static const uint16_t mario_track3_tempos[] = {100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,100,100,20,100,100,100,20,100,100,100,20,100,20,100,20,100,20,100,20};
static const uint16_t mario_track4_tempos[] = {100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,100,100,100,20,100,100};
static const uint16_t mario_track5_tempos[] = {100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,100,100,100,100,100,100,100,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,20,100,100,100,100,100,100,20,100,20,100,20,100,20,100,20,100,100};
static const uint16_t mario_track6_tempos[] = {100,20,100,100,100,20,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,20,100,20,100,100};
// 音轨长度数组
static const int mario_track_lengths[] = {23, 51, 57, 57, 123, 137, 27};
// mario.py中的映射函数 - 音符到频率的转换
static uint16_t mario_note_to_freq(uint8_t note)
{
// 休止符逻辑: note < 55 或 > 127
if (note < 55 || note > 127) {
return NOTE_REST;
}
// MIDI到频率的映射 (基于mario.py逻辑)
switch(note) {
case 55: return 196; // G3
case 56: return 208; // G#3
case 57: return 220; // A3
case 58: return 233; // A#3
case 59: return 247; // B3
case 60: return 262; // C4
case 61: return 277; // C#4
case 62: return 294; // D4
case 63: return 311; // D#4
case 64: return 330; // E4
case 65: return 349; // F4
case 66: return 370; // F#4
case 67: return 392; // G4
case 68: return 415; // G#4
case 69: return 440; // A4
case 70: return 466; // A#4
case 71: return 494; // B4
case 72: return 523; // C5
case 73: return 554; // C#5
case 74: return 587; // D5
case 75: return 622; // D#5
case 76: return 659; // E5
case 77: return 698; // F5
case 78: return 740; // F#5
case 79: return 784; // G5
case 80: return 831; // G#5
case 81: return 880; // A5
case 82: return 932; // A#5
case 83: return 988; // B5
case 84: return 1047; // C6
default: return NOTE_REST;
}
}
// 超级玛丽主题曲定义 (完整版)
static const song_t mario_theme_song = {
.name = "Super Mario Theme (Complete)",
.melody = NULL,
.length = 7, // 7个音轨
.tempo = 0,
.note_duration = 0
};
/**
* @brief 配置LEDC播放指定频率
*/
static void mario_audio_set_frequency(uint32_t frequency)
{
if (frequency == NOTE_REST || frequency == 0) {
// 休止符:关闭PWM输出
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, 0);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
return;
}
// 计算LEDC频率和占空比
ledc_timer_config_t ledc_timer = {
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = frequency,
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.clk_cfg = LEDC_AUTO_CLK,
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
// 设置占空比(音量控制)
uint32_t duty = (1 << LEDC_DUTY_RES) * s_volume / 200; // 将音量转换为占空比
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
}
/**
* @brief 独立音轨播放逻辑
*/
static void mario_audio_task_independent_tracks(void* arg)
{
// 播放速度:1.2倍
float speed_factor = 1;
// 各音轨的音符数组和长度
const uint8_t* tracks[] = {mario_track0_notes, mario_track1_notes, mario_track2_notes, mario_track3_notes, mario_track4_notes, mario_track5_notes, mario_track6_notes};
const uint16_t* tempos[] = {mario_track0_tempos, mario_track1_tempos, mario_track2_tempos, mario_track3_tempos, mario_track4_tempos, mario_track5_tempos, mario_track6_tempos};
for (int i = 0; i < 5; i++) {
if (s_audio_state == AUDIO_STATE_STOPPED) {
break;
}
ESP_LOGI(TAG, "开始播放音轨 %d", i);
const uint8_t* track_notes = tracks[i];
const uint16_t* track_tempos = tempos[i];
int track_length = mario_track_lengths[i];
// 播放当前音轨的音符
for (int j = 0; j < track_length; j += 2) {
if (s_audio_state == AUDIO_STATE_STOPPED) {
break;
}
while (s_audio_state == AUDIO_STATE_PAUSED) {
vTaskDelay(pdMS_TO_TICKS(10));
}
// 解码:音符和时长交替
uint8_t this_note = track_notes[j];
uint16_t this_tempo = track_tempos[j];
uint16_t play_duration = (uint16_t)(this_tempo / speed_factor);
// 播放逻辑 - 简化版本
if (this_note < 55 || this_note > 127) {
// 休止符:静音
mario_audio_set_frequency(NOTE_REST);
} else {
// 正常音符:播放频率
uint16_t freq = mario_note_to_freq(this_note);
mario_audio_set_frequency(freq);
}
// 等待指定时间
vTaskDelay(pdMS_TO_TICKS(play_duration));
// 停止播放
mario_audio_set_frequency(NOTE_REST);
// 音符间间隔(速度调整)
vTaskDelay(pdMS_TO_TICKS((uint16_t)(20.0 / speed_factor)));
}
// 音轨间间隔(速度调整)
vTaskDelay(pdMS_TO_TICKS((uint16_t)(40.0 / speed_factor)));
}
// 播放完成,确保关闭输出
mario_audio_set_frequency(NOTE_REST);
s_audio_state = AUDIO_STATE_STOPPED;
s_current_song = NULL;
// 删除任务
s_audio_task_handle = NULL;
vTaskDelete(NULL);
}
/**
* @brief 初始化音频播放器
*/
esp_err_t mario_audio_init(int gpio_num)
{
if (s_audio_gpio != -1) {
ESP_LOGW(TAG, "音频播放器已初始化");
return ESP_ERR_INVALID_STATE;
}
ESP_LOGI(TAG, "初始化音频播放器,GPIO: %d", gpio_num);
// 配置LEDC定时器
ledc_timer_config_t ledc_timer = {
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY,
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.clk_cfg = LEDC_AUTO_CLK,
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
// 配置LEDC通道 - 参考ESP-IDF示例
ledc_channel_config_t ledc_channel = {
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = gpio_num,
.duty = 0, // Set duty to 0%
.hpoint = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
s_audio_gpio = gpio_num;
s_audio_state = AUDIO_STATE_STOPPED;
ESP_LOGI(TAG, "✅ 音频播放器初始化完成");
return ESP_OK;
}
/**
* @brief 反初始化音频播放器
*/
esp_err_t mario_audio_deinit(void)
{
if (s_audio_gpio == -1) {
return ESP_ERR_INVALID_STATE;
}
ESP_LOGI(TAG, "反初始化音频播放器");
// 停止播放
mario_audio_stop();
// 停止PWM输出
ledc_stop(LEDC_MODE, LEDC_CHANNEL, 0);
s_audio_gpio = -1;
ESP_LOGI(TAG, "✅ 音频播放器反初始化完成");
return ESP_OK;
}
/**
* @brief 播放指定歌曲 (使用独立音轨)
*/
esp_err_t mario_audio_play_song(const song_t* song)
{
if (s_audio_gpio == -1) {
ESP_LOGE(TAG, "音频播放器未初始化");
return ESP_ERR_INVALID_STATE;
}
if (s_audio_state == AUDIO_STATE_PLAYING) {
ESP_LOGW(TAG, "正在播放中,停止当前播放");
mario_audio_stop();
}
s_current_song = song;
s_audio_state = AUDIO_STATE_PLAYING;
// 创建独立音轨播放任务
BaseType_t ret = xTaskCreate(mario_audio_task_independent_tracks, "mario_tracks", 4096, NULL, 5, &s_audio_task_handle);
if (ret != pdPASS) {
ESP_LOGE(TAG, "创建音频播放任务失败");
s_audio_state = AUDIO_STATE_STOPPED;
s_current_song = NULL;
return ESP_FAIL;
}
ESP_LOGI(TAG, "开始播放: %s", song->name);
return ESP_OK;
}
/**
* @brief 播放超级玛丽主题曲(便捷函数)
*/
esp_err_t mario_audio_play_mario_theme(void)
{
return mario_audio_play_song(&mario_theme_song);
}
/**
* @brief 停止播放
*/
esp_err_t mario_audio_stop(void)
{
if (s_audio_state == AUDIO_STATE_STOPPED) {
return ESP_OK;
}
ESP_LOGI(TAG, "停止播放");
s_audio_state = AUDIO_STATE_STOPPED;
// 关闭PWM输出
mario_audio_set_frequency(NOTE_REST);
// 等待任务结束
if (s_audio_task_handle != NULL) {
vTaskDelay(pdMS_TO_TICKS(10));
if (s_audio_task_handle != NULL) {
vTaskDelete(s_audio_task_handle);
s_audio_task_handle = NULL;
}
}
s_current_song = NULL;
return ESP_OK;
}
/**
* @brief 暂停播放
*/
esp_err_t mario_audio_pause(void)
{
if (s_audio_state != AUDIO_STATE_PLAYING) {
return ESP_ERR_INVALID_STATE;
}
ESP_LOGI(TAG, "暂停播放");
s_audio_state = AUDIO_STATE_PAUSED;
// 关闭PWM输出
mario_audio_set_frequency(NOTE_REST);
return ESP_OK;
}
/**
* @brief 恢复播放
*/
esp_err_t mario_audio_resume(void)
{
if (s_audio_state != AUDIO_STATE_PAUSED) {
return ESP_ERR_INVALID_STATE;
}
ESP_LOGI(TAG, "恢复播放");
s_audio_state = AUDIO_STATE_PLAYING;
return ESP_OK;
}
/**
* @brief 设置音量
*/
esp_err_t mario_audio_set_volume(uint8_t volume)
{
if (volume > 100) {
volume = 100;
}
s_volume = volume;
ESP_LOGI(TAG, "音量设置为: %d%%", volume);
// 如果正在播放,更新当前音量
if (s_audio_state == AUDIO_STATE_PLAYING && s_current_song != NULL) {
// 音量会在下一个音符播放时生效
}
return ESP_OK;
}
/**
* @brief 获取当前播放状态
*/
audio_state_t mario_audio_get_state(void)
{
return s_audio_state;
}
/**
* @brief 检查是否正在播放
*/
bool mario_audio_is_playing(void)
{
return s_audio_state == AUDIO_STATE_PLAYING;
}然后叫AI把拾取颜色和播放马里奥音乐的功能给串联起来就好了。

其实在一开始,我要加入PWM播放超级玛丽的BGM时,AI智能体代理就问有没有想法把拾取颜色的功能和播放功能给联合起来,如下图。
最后,发一下AI整合好的main.c文件。
#include <stdio.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "tcs3200.h"
#include "mario_audio.h"
static const char *TAG = "COLOR_MARIO";
// 颜色传感器配置
#define TCS3200_S0_GPIO 13
#define TCS3200_S1_GPIO 17
#define TCS3200_S2_GPIO 16
#define TCS3200_S3_GPIO 15
#define TCS3200_OUT_GPIO 14 // 频率输出引脚
// 音频输出配置
#define PWM_AUDIO_GPIO_PIN 18
// 播放控制标志
static bool mario_playing = false;
/**
* @brief 检测是否为绿色(草绿色)
* @param rgb RGB颜色值
* @return true 如果检测到绿色,false 否则
*/
static bool is_green_color(uint32_t rgb)
{
uint8_t r = (rgb >> 16) & 0xFF;
uint8_t g = (rgb >> 8) & 0xFF;
uint8_t b = rgb & 0xFF;
// 绿色判断条件:
// 1. 绿色分量最大
// 2. 绿色分量超过一定阈值
// 3. 红色分量不能太高
// 4. 蓝色分量不能太高
if (g > 80 && // 绿色分量足够
g > r * 1.5 && // 绿色明显大于红色
g > b * 1.3 && // 绿色明显大于蓝色
r < 100 && // 红色分量不太高
b < 100) { // 蓝色分量不太高
return true;
}
return false;
}
/**
* @brief 颜色检测任务
*/
static void color_detection_task(void* arg)
{
ESP_LOGI(TAG, "启动颜色检测任务");
// 初始化颜色传感器
esp_err_t ret = tcs3200_init(TCS3200_S0_GPIO, TCS3200_S1_GPIO, TCS3200_S2_GPIO,
TCS3200_S3_GPIO, TCS3200_OUT_GPIO);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "颜色传感器初始化失败: %s", esp_err_to_name(ret));
vTaskDelete(NULL);
return;
}
// 初始化音频播放器
ret = mario_audio_init(PWM_AUDIO_GPIO_PIN);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "音频播放器初始化失败: %s", esp_err_to_name(ret));
vTaskDelete(NULL);
return;
}
// 设置默认音量
mario_audio_set_volume(30);
ESP_LOGI(TAG, "音量设置为: 30%%");
bool last_green_detected = false;
while (1) {
// 读取颜色
uint32_t rgb = tcs3200_read_rgb();
// 检测绿色
bool green_detected = is_green_color(rgb);
ESP_LOGI(TAG, "RGB: (%d,%d,%d) 绿色检测: %s",
(rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF,
green_detected ? "是" : "否");
// 检测到绿色且之前没有在播放 -> 开始播放
if (green_detected && !mario_playing && !mario_audio_is_playing()) {
ESP_LOGI(TAG, "
我要赚赏金
