
项目 | 内容 |
课程时长 | 90分钟(2课时) |
适合对象 | 有一定Arduino基础的学生 |
教学目标 | 掌握ESP-NOW协议、结构体通信、硬件联动 |
硬件准备 | ESP32开发板×2、按键、蜂鸣器、WS2812灯环、杜邦线 |
第1阶段(15分钟):项目展示与原理讲解第2阶段(25分钟):发射端代码编写与测试第3阶段(25分钟):接收端代码编写与测试 第4阶段(15分钟):联调与功能演示第5阶段(10分钟):拓展思考与总结教师操作:提前准备好一套已经烧录好的设备,现场演示"找钥匙"场景
【教师旁白】"同学们,有没有遇到过这种情况——急着出门却找不到钥匙?今天我们要做一个'黑科技':按下按钮,钥匙就会发光+尖叫!"现场演示:

核心知识点:
边写边讲:"先搭骨架,再填血肉"
/** * 无线寻物器 - 发射端 (Transmitter) * 功能:检测按键按下,通过ESP-NOW发送寻物信号 */#include <WiFi.h> // ESP32 WiFi库#include <esp_now.h> // ESP-NOW协议库// ========== 引脚定义 ==========const int BUTTON_PIN = 0; // 开发板上的BOOT按键(GPIO0)const int LED_PIN = 2; // 板载LED,用于显示发送状态void setup() { Serial.begin(115200); Serial.println("\n=== 无线寻物器 - 发射端启动 ==="); pinMode(BUTTON_PIN, INPUT_PULLUP); // 上拉输入,按键按下为LOW pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); // TODO: 初始化ESP-NOW}void loop() { // TODO: 检测按键并发送 delay(10); // 短暂延时,防止看门狗复位}🔍 即时测试1:上传代码,打开串口监视器
=== 无线寻物器 - 发射端启动 ===边写边讲:"MAC地址就像快递地址,必须准确!"
void setup() { // ... 前面的代码 ... WiFi.mode(WIFI_STA); // 设置为Station模式 Serial.print("本机MAC地址: "); Serial.println(WiFi.macAddress()); // 打印MAC地址 // 复制这串地址!接收端需要它}🔍 即时测试2:
串口输出示例:本机MAC地址: 84:1F:E8:26:85:8C边写边讲:"结构体就像快递单,格式必须双方约定好!"
// ========== 关键:使用packed结构体 ==========// __attribute__((packed)) 告诉编译器:不要插入填充字节!// 这是通信协议的"合同",两端必须完全一致typedef struct __attribute__((packed)) { uint8_t command; // 1字节:命令类型(1=寻物) uint32_t timestamp; // 4字节:时间戳,防止重放攻击 uint16_t checksum; // 2字节:校验和,确保数据完整} Packet; // 总计:7字节// 定义命令常量const uint8_t CMD_FIND = 1;// 创建数据包实例Packet dataPacket;// 接收端MAC地址(临时用广播地址,稍后替换为实际地址)uint8_t receiverMacAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};💡 知识点讲解:
为什么要用 __attribute__((packed))?┌─────────────────────────────┐│ 普通结构体(有填充) ││ uint8_t [1字节] + 3字节填充 ││ uint32_t [4字节] ││ uint16_t [2字节] + 2字节填充 ││ 实际占用:12字节(浪费!) │├─────────────────────────────┤│ packed结构体(无填充) ││ uint8_t [1字节] ││ uint32_t [4字节] ││ uint16_t [2字节] ││ 实际占用:7字节(紧凑!) │└─────────────────────────────┘边写边讲:"就像手机配对蓝牙耳机,需要初始化+添加设备"
// 发送回调函数:报告发送成功还是失败void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { Serial.print("发送状态: "); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "成功 ✅" : "失败 ❌"); // LED闪烁反馈:成功亮1秒,失败不亮 digitalWrite(LED_PIN, status == ESP_NOW_SEND_SUCCESS ? HIGH : LOW); delay(100); digitalWrite(LED_PIN, LOW);}void setup() { // ... 前面的代码 ... // 初始化ESP-NOW if (esp_now_init() != ESP_OK) { Serial.println("ESP-NOW初始化失败!"); return; } // 注册发送回调 esp_now_register_send_cb(onDataSent); // 配置接收端信息(像保存联系人) esp_now_peer_info_t peerInfo = {}; memcpy(peerInfo.peer_addr, receiverMacAddress, 6); // MAC地址 peerInfo.channel = 0; // 使用当前WiFi信道 peerInfo.encrypt = false; // 不加密(简单项目) // 添加接收端 if (esp_now_add_peer(&peerInfo) != ESP_OK) { Serial.println("添加接收端失败!"); return; } Serial.printf("数据包大小: %d 字节\n", sizeof(Packet)); // 应显示7 Serial.println("初始化完成,等待按键...");}🔍 即时测试3:
预期输出:数据包大小: 7 字节初始化完成,等待按键...边写边讲:"软件消抖+三连发确保可靠送达"
// 校验和计算:简单的数据完整性验证uint16_t calculateChecksum(Packet *pkt) { return pkt->command + (pkt->timestamp & 0xFFFF) + (pkt->timestamp >> 16);}void loop() { // 检测按键按下(LOW表示按下,因为是上拉输入) if (digitalRead(BUTTON_PIN) == LOW) { delay(20); // 软件消抖:等待20ms // 再次确认按键确实按下 if (digitalRead(BUTTON_PIN) == LOW) { Serial.println("按键按下,准备发送寻物信号..."); // 填充数据包 dataPacket.command = CMD_FIND; dataPacket.timestamp = millis(); // 当前时间戳 dataPacket.checksum = calculateChecksum(&dataPacket); Serial.printf("发送数据 | 命令:%d | 时间戳:%lu | 校验和:%d\n", dataPacket.command, dataPacket.timestamp, dataPacket.checksum); // 发送3次确保到达(重要!无线可能丢包) for (int i = 0; i < 3; i++) { esp_err_t result = esp_now_send(receiverMacAddress, (uint8_t *)&dataPacket, sizeof(dataPacket)); Serial.printf("第%d次发送: %s\n", i + 1, result == ESP_OK ? "成功" : "失败"); delay(50); // 间隔50ms,避免拥塞 } Serial.println("等待按键释放..."); while (digitalRead(BUTTON_PIN) == LOW) { delay(10); // 等待释放,防止重复触发 } Serial.println("按键已释放,准备下一次检测\n"); } } delay(10);}🔍 即时测试4(使用广播地址测试):
预期输出:按键按下,准备发送寻物信号...发送数据 | 命令:1 | 时间戳:12345 | 校验和:12346第1次发送: 成功第2次发送: 成功第3次发送: 成功发送状态: 成功 ✅等待按键释放...按键已释放,准备下一次检测本文标签:#科创编程 #物联网 #教案 #编程思维 #电子爱好者 #少儿编程
![]() | ![]() |