![]() | ![]() |
项目 | 内容 |
课程时长 | 90-120分钟 |
教学目标 | 理解GPIO控制 + 掌握ESP-NOW无线通信 |
硬件准备 | 2块ESP32、1个按键、1个RGB LED、杜邦线若干 |
难度等级 | ⭐⭐ 入门基础 |
第1部分:硬件连接讲解(15分钟)第2部分:接收端代码编写(35分钟)第3部分:发送端代码编写(35分钟)第4部分:联调测试与拓展(15-20分钟)【边画边讲】 在白板/屏幕上画出接线示意图:

💡 关键讲解点
讲师话术:"大家注意,这就是ESP-NOW协议的神奇之处——两块开发板之间不需要任何物理连线,它们通过2.4GHz WiFi频段直接通信,就像两台对讲机。"
RGB LED共阴极说明:
【边写边讲】
/* * ESP32无线指示灯 - 接收端 * 功能:接收ESP-NOW数据,控制RGB LED显示 * 硬件:ESP32 + RGB LED */#include <WiFi.h> // 【讲解】WiFi库,ESP-NOW依赖WiFi硬件#include <esp_now.h> // 【讲解】ESP-NOW协议核心库讲师话术:"esp_now.h 是乐鑫官方提供的库,ESP32出厂自带,不需要额外安装。ESP-NOW是乐鑫开发的无连接通信协议,比WiFi/蓝牙更简单快速。"
【边写边讲】
// ========== 引脚定义 ==========// 【讲解】使用宏定义,方便后期修改,代码可读性更好#define LED_R 18 // 红色引脚 → GPIO18#define LED_G 19 // 绿色引脚 → GPIO19 #define LED_B 21 // 蓝色引脚 → GPIO21// ========== 数据结构 ==========// 【讲解】struct = 结构体,把多个数据打包成一个"包裹"// 【类比】就像快递包裹,里面可以同时装"颜色"和"亮度"两件东西typedef struct { int colorCode; // 颜色指令:1=红, 2=绿, 3=蓝, 4=关, 5=呼吸 int brightness; // 亮度值:0-255(PWM占空比)} Message;Message incomingMsg; // 【讲解】创建一个变量,用来存收到的"包裹"讲师话术:"typedef 是给结构体起别名。Message 现在就是一种自定义数据类型,就像int、float一样,后面可以直接用。"
【边写边讲】
// ========== 接收回调 ==========// 【讲解】回调函数 = "中断处理函数",数据到达时自动触发,不需要手动调用// 【类比】就像门铃响了,你不用一直盯着门,听到铃声去开门就行void onDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { // 【讲解】memcpy = memory copy,把收到的字节流复制到我们的结构体变量中 memcpy(&incomingMsg, incomingData, sizeof(incomingMsg)); // ========== 调试输出(串口监视器查看)========== Serial.println("\n--- 收到新指令 ---"); // 【讲解】MAC地址是设备的"身份证号",6个字节,这里格式化打印 Serial.print("来自MAC: "); for (int i = 0; i < 6; i++) { Serial.printf("%02X", mac[i]); // %02X = 两位十六进制,不足补0 if (i < 5) Serial.print(":"); // 每两个字节用冒号分隔 } Serial.println(); // 【讲解】打印收到的具体数据,方便调试 Serial.print("颜色代码: "); Serial.println(incomingMsg.colorCode); Serial.print("亮度: "); Serial.println(incomingMsg.brightness); // 【讲解】调用执行函数,真正控制LED executeCommand(incomingMsg.colorCode, incomingMsg.brightness);}讲师话术:"注意这个函数的三个参数:mac是发送方的地址,incomingData是收到的原始字节数据,len是数据长度。这是ESP-NOW库规定的固定格式。"
【边写边讲】
// ========== 执行指令 ==========void executeCommand(int code, int brightness) { // 【讲解】安全第一步:先关灯,避免颜色混合 analogWrite(LED_R, 0); analogWrite(LED_G, 0); analogWrite(LED_B, 0); // 【讲解】constrain = 限制范围,确保亮度在0-255之间,防止溢出 int duty = constrain(brightness, 0, 255); // 【讲解】switch-case 多分支选择,比if-else更清晰 switch(code) { case 1: // 红色 analogWrite(LED_R, duty); Serial.println("执行:红色"); break; case 2: // 绿色 analogWrite(LED_G, duty); Serial.println("执行:绿色"); break; case 3: // 蓝色 analogWrite(LED_B, duty); Serial.println("执行:蓝色"); break; case 4: // 关闭 // 已经全关了,什么都不做 Serial.println("执行:关闭"); break; case 5: // 呼吸效果(拓展功能) breatheMode(); break; default: // 【讲解】默认情况,收到未知指令时亮白光 analogWrite(LED_R, duty); analogWrite(LED_G, duty); analogWrite(LED_B, duty); Serial.println("执行:白色"); break; }}// 【讲解】呼吸灯效果 = PWM占空比从0到255再到0,循环变化void breatheMode() { Serial.println("执行:呼吸灯"); for (int i = 0; i < 3; i++) { // 呼吸3次 // 逐渐变亮 for (int duty = 0; duty <= 255; duty += 5) { analogWrite(LED_R, duty); delay(10); // 延时10ms,控制变化速度 } // 逐渐变暗 for (int duty = 255; duty >= 0; duty -= 5) { analogWrite(LED_R, duty); delay(10); } }}讲师话术:"analogWrite 在ESP32上是PWM输出,不是真正的模拟信号。ESP32的PWM频率默认1kHz,占空比0-255对应0%-100%。"
【边写边讲】
// ========== 初始化 ==========void setup() { // 【讲解】串口初始化,波特率115200是ESP32常用值 Serial.begin(115200); Serial.println("\n=== ESP32无线指示灯接收端启动 ==="); // ========== GPIO初始化 ========== pinMode(LED_R, OUTPUT); pinMode(LED_G, OUTPUT); pinMode(LED_B, OUTPUT); // 【讲解】开机自检:红→绿→蓝依次亮起,确认硬件正常 analogWrite(LED_R, 255); delay(300); analogWrite(LED_R, 0); analogWrite(LED_G, 255); delay(300); analogWrite(LED_G, 0); analogWrite(LED_B, 255); delay(300); analogWrite(LED_B, 0); // ========== WiFi初始化(必须步骤)========== // 【讲解】ESP-NOW基于WiFi硬件,必须先启动WiFi WiFi.mode(WIFI_STA); // 设置为Station模式(客户端模式) // 【重点】打印MAC地址!发送端需要这个地址才能定向发送 Serial.print("本机MAC地址(请记录): "); Serial.println(WiFi.macAddress()); // ========== ESP-NOW初始化 ========== if (esp_now_init() != ESP_OK) { // ESP_OK = 0,表示成功 Serial.println("ESP-NOW初始化失败!"); return; // 失败则退出setup } // 【关键】注册接收回调函数,告诉系统"收到数据时调用onDataRecv" esp_now_register_recv_cb(onDataRecv); Serial.println("接收端准备就绪,等待指令...");}void loop() { // 【讲解】主循环空闲!所有工作都在中断回调中完成 // 这种设计叫"事件驱动",比轮询更高效 // 可以在这里添加其他功能,如读取传感器 delay(100); // 防止看门狗复位}讲师话术:"重点来了! 烧录这段代码后,立刻打开串口监视器,复制显示的MAC地址,比如84:1F:E8:26:85:8C,这是接收端的"门牌号",发送端必须知道它才能寄"包裹"。"
【边写边讲】
/* * ESP32无线指示灯 - 发送端(无线遥控器) * 功能:检测按键,发送指令控制接收端LED * 【注意】使用前需修改下面的MAC地址为实际接收端地址! */#include <WiFi.h>#include <esp_now.h>// ========== 引脚定义 ==========#define BUTTON_PIN 4 // 按键接GPIO4,使用内部上拉#define LED_R 18 // 本地红色LED(可选,用于反馈)#define LED_G 19 // 本地绿色LED#define LED_B 21 // 本地蓝色LED// ========== 接收端MAC地址【必须修改!】==========// 【讲解】把刚才记录的接收端MAC地址填在这里,格式:0xXX, 0xXX...uint8_t receiverAddress[] = {0x84, 0x1F, 0xE8, 0x26, 0x85, 0x8C};// 【提示】如果不知道MAC地址,可以先烧录接收端代码查看讲师话术:"uint8_t 是无符号8位整数,范围0-255,正好存一个字节。MAC地址6个字节,所以用数组存储。"
【边写边讲】
// ========== 数据结构 ==========// 【讲解】必须与接收端定义完全一致!这是双方的"共同语言"typedef struct { int colorCode; // 1=红, 2=绿, 3=蓝, 4=关 int brightness; // 亮度0-255} Message;Message msg; // 发送消息的数据容器// ========== 按键变量(用于消抖和手势识别)==========unsigned long lastDebounceTime = 0; // 上次状态变化时间const unsigned long debounceDelay = 50; // 消抖延时50msint lastButtonState = HIGH; // 上一次的按键状态(上拉默认高电平)int buttonState = HIGH; // 当前按键状态int clickCount = 0; // 单击计数(用于双击检测)unsigned long firstClickTime = 0; // 第一次单击时间unsigned long pressStartTime = 0; // 按下开始时间bool isLongPress = false; // 长按标志讲师话术:"这些变量用于按键消抖和手势识别。机械按键按下时会有抖动,如果不处理,单片机会认为你按了多次。"
【边写边讲】
void setup() { Serial.begin(115200); Serial.println("\n=== ESP32无线遥控器启动 ==="); // ========== GPIO初始化 ========== pinMode(BUTTON_PIN, INPUT_PULLUP); // 内部上拉,按键另一端接GND pinMode(LED_R, OUTPUT); pinMode(LED_G, OUTPUT); pinMode(LED_B, OUTPUT); setRGB(0, 0, 0); // 初始关闭本地LED // ========== WiFi初始化(关键步骤)========== WiFi.mode(WIFI_STA); WiFi.disconnect(); // 【重要】清除之前的WiFi连接状态 delay(100); // 等待WiFi硬件就绪 // 打印地址信息,方便调试 Serial.print("本机MAC: "); Serial.println(WiFi.macAddress()); Serial.print("目标MAC: "); for(int i=0; i<6; i++) { Serial.printf("%02X", receiverAddress[i]); if(i<5) Serial.print(":"); } Serial.println(); // ========== ESP-NOW初始化 ========== if (esp_now_init() != ESP_OK) { Serial.println("ESP-NOW初始化失败!"); return; } // ========== 添加对等节点(配对)========== // 【重点】这是发送端特有的步骤,相当于"把对方的地址存入通讯录" esp_now_peer_info_t peerInfo; // 定义结构体变量 // 【关键】先用memset清空结构体,避免垃圾数据干扰 memset(&peerInfo, 0, sizeof(peerInfo)); // 填充对方信息 memcpy(peerInfo.peer_addr, receiverAddress, 6); // 复制MAC地址 peerInfo.channel = 0; // 信道0 = 当前WiFi信道 peerInfo.encrypt = false; // 不加密(入门简单起见) peerInfo.ifidx = WIFI_IF_STA; // 【关键】使用STA接口 // 添加到ESP-NOW的"通讯录" if (esp_now_add_peer(&peerInfo) != ESP_OK) { Serial.println("添加接收端失败!"); return; } Serial.println("ESP-NOW初始化成功!"); // 开机自检 selfTest(); Serial.println("等待按键...");}讲师话术:"esp_now_peer_info_t 是ESP-NOW的对等节点信息结构体,必须正确填写。ifidx = WIFI_IF_STA 是常见错误点,如果不指定接口,可能导致发送失败!"
【边写边讲】
// ========== 主循环 ==========void loop() { handleButton(); // 持续检测按键}// ========== 按键处理(支持单击/双击/长按)==========void handleButton() { int reading = digitalRead(BUTTON_PIN); // ========== 步骤1:消抖处理 ========== // 【讲解】如果状态变了,记录时间 if (reading != lastButtonState) { lastDebounceTime = millis(); // millis()返回开机后的毫秒数 } // 【讲解】只有状态稳定超过50ms,才认为是有效变化 if ((millis() - lastDebounceTime) > debounceDelay) { // 状态确实改变了 if (reading != buttonState) { buttonState = reading; if (buttonState == LOW) { // 【按下】 pressStartTime = millis(); isLongPress = false; // 此时不立即处理,等待释放或超时 } else { // 【释放】 unsigned long duration = millis() - pressStartTime; if (duration > 1000) { // 按下超过1秒 = 长按 isLongPress = true; handleLongPress(); } else { // 短按 clickCount++; if (clickCount == 1) { firstClickTime = millis(); // 记录第一次单击时间 } } } } } // ========== 步骤2:双击检测 ========== // 【讲解】300ms内没有第二次单击,就判定为单击 if (clickCount > 0 && (millis() - firstClickTime) > 300) { if (clickCount == 1) { handleSingleClick(); } else if (clickCount >= 2) { handleDoubleClick(); } clickCount = 0; // 重置计数器 } lastButtonState = reading; // 保存当前状态供下次比较}// ========== 操作处理函数 ==========void handleSingleClick() { Serial.println("【单击】发送红色"); msg.colorCode = 1; msg.brightness = 255; sendMessage(); setRGB(255, 0, 0); // 本地LED也亮红,给用户反馈}void handleDoubleClick() { Serial.println("【双击】发送绿色"); msg.colorCode = 2; msg.brightness = 255; sendMessage(); setRGB(0, 255, 0);}void handleLongPress() { Serial.println("【长按】发送蓝色"); msg.colorCode = 3; msg.brightness = 255; sendMessage(); setRGB(0, 0, 255);}讲师话术:"这个按键逻辑实现了一个按键三种操作,是嵌入式开发的经典模式。记住:单击红、双击绿、长按蓝。"
【边写边讲】
// ========== 发送函数 ==========void sendMessage() { // 【讲解】esp_now_send是核心函数,参数:目标地址、数据指针、数据长度 esp_err_t result = esp_now_send( receiverAddress, // 目标MAC地址 (uint8_t *)&msg, // 强制转换为字节指针 sizeof(msg) // 数据大小(字节数) ); // 检查发送结果 if (result == ESP_OK) { Serial.println("✓ 发送成功"); } else { Serial.print("✗ 发送失败,错误码: "); Serial.println(result); // 常见错误:0x1234=未找到对方, 0x1235=未初始化 }}// ========== LED控制工具函数 ==========// 【讲解】共阴极LED:HIGH=亮,LOW=灭void setRGB(int r, int g, int b) { digitalWrite(LED_R, r > 0 ? HIGH : LOW); digitalWrite(LED_G, g > 0 ? HIGH : LOW); digitalWrite(LED_B, b > 0 ? HIGH : LOW);}// ========== 开机自检 ==========void selfTest() { Serial.println("自检中..."); setRGB(255, 0, 0); delay(200); // 红 setRGB(0, 255, 0); delay(200); // 绿 setRGB(0, 0, 255); delay(200); // 蓝 setRGB(0, 0, 0); // 灭 Serial.println("自检完成");}步骤 | 操作 | 预期结果 |
1 | 烧录接收端,打开串口监视器 | 显示MAC地址,LED红绿蓝自检 |
2 | 记录MAC地址,填入发送端代码 | 修改 |
3 | 烧录发送端,打开串口监视器 | 显示"初始化成功",LED自检 |
4 | 单击发送端按键 | 接收端亮红灯,双方串口显示成功 |
5 | 双击发送端按键(300ms内) | 接收端亮绿灯 |
6 | 长按发送端按键(>1秒) | 接收端亮蓝灯 |
7 | 观察发送端本地LED | 与接收端同步变色(反馈作用) |
❌ 问题1:发送失败,错误码显示 → 检查MAC地址是否填错 → 检查接收端是否已上电 → 检查两块板子距离是否太远(<10米内测试)❌ 问题2:接收端收到数据但LED不亮 → 检查LED是共阴极还是共阳极(代码默认共阴极) → 检查GPIO引脚号是否与实际接线一致❌ 问题3:按键不灵敏或误触发 → 调整debounceDelay(50ms可能需增大) → 检查按键焊接或接触是否良好int duration字段,实现"亮5秒后自动关闭"┌────────────────────────────────────────┐│ ESP-NOW 通信模型 ││ ││ 发送端 ───────→ 接收端 ││ (遥控器) (指示灯) ││ ││ ① 定义相同的数据结构("协议") ││ ② 发送端添加peer("通讯录") ││ ③ 接收端注册回调("门铃") ││ ④ 发送端esp_now_send("寄快递") ││ ⑤ 接收端自动触发onDataRecv("收件") ││ ││ 【核心】无连接 = 不需要WiFi路由器 ││ 【特点】低延迟(<10ms)、低功耗 │└────────────────────────────────────────┘推荐文档:
代码仓库:建议学生将代码上传GitHub,养成版本控制习惯。
本文标签:#科创编程 #物联网 #教案 #编程思维 #电子爱好者 #少儿编程
![]() | ![]() |