ESP32-WROOM-32U 모델을 2개 사용하고있습니다.
ESP32 NOW 로 리모콘과 로봇을 움직이는 코드를 만들어보자
ESP-NOW로 A(송신기/컨트롤러) → B(수신기/로봇) 에게 FORWARD / STOP / REVERSE 명령을 주고,
B는 모터드라이버(EN/DIR/PWM)를 안전하게 구동하는 양방향 코드가 오늘의 목표이다.
ESP32 와 ESP32 연결을 하기위해서는 서로의 mac 주소를 알아야합니다 밑에 코드를 업로드를 한후
MAC주소를 저장해주세요 (송신기, 수신기)
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/get-change-esp32-esp8266-mac-address-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#include <WiFi.h>
#include <esp_wifi.h>
void readMacAddress(){
uint8_t baseMac[6];
esp_err_t ret = esp_wifi_get_mac(WIFI_IF_STA, baseMac);
if (ret == ESP_OK) {
Serial.printf("%02x:%02x:%02x:%02x:%02x:%02x\n",
baseMac[0], baseMac[1], baseMac[2],
baseMac[3], baseMac[4], baseMac[5]);
} else {
Serial.println("Failed to read MAC address");
}
}
void setup(){
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.STA.begin();
Serial.print("[DEFAULT] ESP32 Board MAC Address: ");
readMacAddress();
}
void loop(){
}
포트번호 COM5, 흰색
송신기(컨트롤러)-MAC주소를 저장해주세요
송신기의 MAC주소 : 44:1d:64:f4:dd:c4

포트번호 COM6, 검정색
수신기(로봇)-MAC주소를 저장해주세요
수신기의 MAC주소 : ec:e3:34:21:a1:e8

이제 송신기 코드를 작성해보자
(송신기는 로봇의 컨트롤러여서 수신기의 mac주소를 알아야합니다)
// ===== A(송신기) — ESP-NOW Controller =====
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h> // ★ Wi-Fi 저수준 API(채널 설정 등) 사용 시 필요
// ★ 수신기(B)의 MAC 주소로 바꾸세요 (사용자 제공 주소 유지)
uint8_t PEER_MAC[] = { 0xEC, 0xE3, 0x34, 0x21, 0xA1, 0xE8 };
#define ESPNOW_CHANNEL 1 // ★ 송신/수신 모두 동일 채널이어야 통신됨
// 송수신에 사용할 페이로드(메시지) 구조체
struct ControlMsg {
char command; // 'F'/'S'/'R'
uint32_t seq; // 송신 시퀀스
uint32_t ms; // millis() 기준 타임스탬프
};
volatile uint32_t g_seq = 0;
// ===== 전송 완료 콜백 (보드 패키지 버전별 호환) =====
#if ESP_ARDUINO_VERSION_MAJOR >= 3
// ESP32 core v3.x (ESP-IDF 5): info + status
void onSendDone(const esp_now_send_info_t *info, esp_now_send_status_t status) {
Serial.print("[A] Send status: ");
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "SUCCESS" : "FAIL");
// ★ v3.x에서는 목적지 주소 필드명이 des_addr 입니다. (dest_addr 아님)
if (info) {
char macStr[18];
snprintf(macStr, sizeof(macStr),
"%02X:%02X:%02X:%02X:%02X:%02X",
info->des_addr[0], info->des_addr[1], info->des_addr[2],
info->des_addr[3], info->des_addr[4], info->des_addr[5]);
Serial.print("[A] dest="); Serial.println(macStr);
}
}
#else
// ESP32 core v2.x (ESP-IDF 4): mac + status
void onSendDone(const uint8_t *mac, esp_now_send_status_t status) {
Serial.print("[A] Send status: ");
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "SUCCESS" : "FAIL");
if (mac) {
char macStr[18];
snprintf(macStr, sizeof(macStr),
"%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
Serial.print("[A] dest="); Serial.println(macStr);
}
}
#endif
// ============================================
// 피어(수신기) 등록 유틸리티
bool addPeer(const uint8_t *mac) {
esp_now_peer_info_t peer{}; // 0으로 초기화
memcpy(peer.peer_addr, mac, 6);
peer.channel = ESPNOW_CHANNEL; // ★ 채널 지정(수신기와 동일)
peer.encrypt = false; // 암호화 사용 안 함(필요 시 true + 키 설정)
return (esp_now_add_peer(&peer) == ESP_OK);
}
void setup() {
Serial.begin(115200);
// ★ ESP-NOW는 STA 모드 권장
WiFi.mode(WIFI_STA);
// ★ 예전의 promiscuous on/off 트릭은 필요 없음. 채널만 직접 설정
esp_wifi_set_channel(ESPNOW_CHANNEL, WIFI_SECOND_CHAN_NONE);
// ESP-NOW 초기화 및 콜백 등록
if (esp_now_init() != ESP_OK) {
Serial.println("[A] ESP-NOW init failed");
while (1) delay(1000);
}
esp_now_register_send_cb(onSendDone); // 버전에 맞는 콜백이 자동 바인딩됨
// 피어(수신기) 추가
if (!addPeer(PEER_MAC)) {
Serial.println("[A] addPeer failed");
while (1) delay(1000);
}
Serial.println("[A] Ready. Type F/S/R + Enter");
}
void loop() {
// 시리얼에서 한 글자 읽어 명령 전송 ('F','S','R'만 사용)
if (Serial.available()) {
char c = toupper(Serial.read()); // 소문자 입력해도 대문자로 처리
if (c=='F' || c=='S' || c=='R') {
ControlMsg msg;
msg.command = c;
msg.seq = ++g_seq;
msg.ms = millis();
esp_err_t r = esp_now_send(PEER_MAC, (uint8_t*)&msg, sizeof(msg));
Serial.print("[A] Send cmd="); Serial.write(c);
Serial.print(", seq="); Serial.print(msg.seq);
Serial.print(", ret="); Serial.println(r==ESP_OK ? "OK" : "ERR");
}
}
// (선택) 버튼 입력 → 명령 전송 로직을 여기에 추가해도 됩니다.
// 예: GPIO0=정방향, GPIO2=정지, GPIO4=역방향 등
}
이제 수신기 코드를 작성해보자
(수신기는 로봇이여서 컨트로러의 송신기의 mac주소를 알아야합니다)
// ===== B(수신기) — ESP-NOW Robot =====
#include <Arduino.h>
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>
// ★ 송신기(A)의 MAC 주소 (제공: 44:1D:64:F4:DD:C4)
uint8_t PEER_MAC[] = { 0x44, 0x1D, 0x64, 0xF4, 0xDD, 0xC4 };
#define ESPNOW_CHANNEL 1 // ★ 송신/수신 동일 채널
// ---- 모터 핀 (ESP32-S 가정) ----
const int VSP_PIN = 21; // PWM(LEDC)
const int DIR_PIN = 18; // 방향
const int EN_PIN = 16; // Enable (Active-Low: LOW=Run)
// ---- PWM 설정 ----
int LEDC_CH = -1; // ★ v3: ledcAttach()가 채널을 반환
const int LEDC_FREQ = 1000; // 1 kHz
const int LEDC_BITS = 8; // 0~255 해상도
const int PWM_RUN = 200; // 구동 듀티(실험값)
const int PWM_START = 120; // 시동 최소 듀티(실험값)
// ---- 안전/타이밍 ----
const uint32_t DEADTIME_MS = 150;
const uint32_t FAILSAFE_MS = 500;
const uint32_t RAMP_STEP_MS = 10;
const int RAMP_STEP = 5;
// ---- 페이로드 ----
struct ControlMsg {
char command; // 'F','S','R'
uint32_t seq;
uint32_t ms;
};
volatile ControlMsg g_lastMsg{};
volatile bool g_hasNew = false;
volatile uint32_t g_lastRxMs = 0;
inline void pwmWrite(int duty) {
if (LEDC_CH >= 0) ledcWrite(LEDC_CH, constrain(duty, 0, 255));
}
void motorStopHard() {
pwmWrite(0);
digitalWrite(EN_PIN, HIGH); // Active-Low → HIGH=정지
}
void motorRunDir(bool forward) {
motorStopHard();
delay(DEADTIME_MS);
digitalWrite(DIR_PIN, forward ? HIGH : LOW);
delay(DEADTIME_MS);
digitalWrite(EN_PIN, LOW); // Run
int duty = PWM_START;
while (duty < PWM_RUN) {
pwmWrite(duty);
duty += RAMP_STEP;
delay(RAMP_STEP_MS);
}
pwmWrite(PWM_RUN);
}
void applyCommand(char c) {
switch (c) {
case 'F': motorRunDir(true); break;
case 'R': motorRunDir(false); break;
case 'S': motorStopHard(); break;
default: break;
}
}
#if ESP_ARDUINO_VERSION_MAJOR >= 3
void onRecv(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
if (len == sizeof(ControlMsg)) {
ControlMsg msg;
memcpy(&msg, data, sizeof(msg));
memcpy((void*)&g_lastMsg, &msg, sizeof(msg)); // volatile 복사는 memcpy로
g_hasNew = true;
g_lastRxMs = millis();
if (info) {
char macStr[18];
snprintf(macStr, sizeof(macStr),
"%02X:%02X:%02X:%02X:%02X:%02X",
info->src_addr[0], info->src_addr[1], info->src_addr[2],
info->src_addr[3], info->src_addr[4], info->src_addr[5]);
Serial.print("[B] from "); Serial.println(macStr);
}
} else {
Serial.print("[B] Unknown pkt len="); Serial.println(len);
}
}
void onSendDone(const esp_now_send_info_t *info, esp_now_send_status_t status) {
(void)info; (void)status;
}
#else
void onRecv(const uint8_t *mac, const uint8_t *data, int len) {
if (len == sizeof(ControlMsg)) {
ControlMsg msg;
memcpy(&msg, data, sizeof(msg));
memcpy((void*)&g_lastMsg, &msg, sizeof(msg));
g_hasNew = true;
g_lastRxMs = millis();
if (mac) {
char macStr[18];
snprintf(macStr, sizeof(macStr),
"%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
Serial.print("[B] from "); Serial.println(macStr);
}
} else {
Serial.print("[B] Unknown pkt len="); Serial.println(len);
}
}
void onSendDone(const uint8_t *mac, esp_now_send_status_t status) {
(void)mac; (void)status;
}
#endif
bool addPeer(const uint8_t *mac) {
esp_now_peer_info_t peer{};
memcpy(peer.peer_addr, mac, 6);
peer.channel = ESPNOW_CHANNEL;
peer.encrypt = false;
return (esp_now_add_peer(&peer) == ESP_OK);
}
void setup() {
Serial.begin(115200);
// 모터 핀
pinMode(DIR_PIN, OUTPUT);
pinMode(EN_PIN, OUTPUT);
digitalWrite(DIR_PIN, LOW);
digitalWrite(EN_PIN, HIGH); // 정지로 시작
// ★ PWM(LEDC): v3 간단 API — 채널 자동 할당
LEDC_CH = ledcAttach(VSP_PIN, LEDC_FREQ, LEDC_BITS); // 채널 번호 반환
if (LEDC_CH < 0) {
Serial.println("[B] ledcAttach failed");
while (1) delay(1000);
}
pwmWrite(0);
// ESP-NOW
WiFi.mode(WIFI_STA);
esp_wifi_set_channel(ESPNOW_CHANNEL, WIFI_SECOND_CHAN_NONE);
if (esp_now_init() != ESP_OK) {
Serial.println("[B] ESP-NOW init failed");
while (1) delay(1000);
}
esp_now_register_recv_cb(onRecv);
esp_now_register_send_cb(onSendDone);
// (옵션) 회신용 Peer 등록
if (!addPeer(PEER_MAC)) {
Serial.println("[B] addPeer failed (reply not needed then)");
}
g_lastRxMs = millis();
Serial.println("[B] Ready.");
}
void loop() {
if (g_hasNew) {
noInterrupts();
ControlMsg m;
memcpy(&m, (const void*)&g_lastMsg, sizeof(m)); // volatile → 일반복사
g_hasNew = false;
interrupts();
Serial.print("[B] cmd="); Serial.write(m.command);
Serial.print(", seq="); Serial.print(m.seq);
Serial.print(", age(ms)="); Serial.println((int)(millis() - m.ms));
applyCommand(m.command);
}
if (millis() - g_lastRxMs > FAILSAFE_MS) {
motorStopHard();
}
delay(5);
}

