30 января 2026

Скейтч для Arduino IDE. Управление двумя электромоторами через плату WEMOS D1 и драйвер L298N посредством WI-FI

 

Схема соединений «Пин-в-Пин»

Wemos D1 MiniНаправлениеДрайвер L298NКомментарий
D1———>IN1Мотор А (Левый)
D2———>IN2Мотор А (Левый)
D5———>IN3Мотор B (Правый)
D6———>IN4Мотор B (Правый)
D3———>Свет/РелеПлюс светодиода (через резистор!)
GND———>GNDОбщая «земля» (Обязательно)
5V<———+5VПитание Wemos (если нет USB)

Силовая часть (Клеммы драйвера L298N)

  1. OUT1 / OUT2: Два провода к левому мотору.

  2. OUT3 / OUT4: Два провода к правому мотору.

  3. 12V (VCC): Плюс аккумулятора (7.4V — 12V).

  4. GND: Минус аккумулятора.

  5. ENA / ENB: На этих пинах должны стоять джамперы (перемычки), соединяющие их с 5V (они там стоят по умолчанию с завода).

Полезные советы перед стартом:

  • Инверсия: Если при движении ползунка «Вверх» мотор крутится назад — просто поменяй местами два провода этого мотора в клеммах OUT.

  • Свет: Пин D3 выдает 3.3V. Если подключаешь обычный светодиод, не забудь про резистор (220–470 Ом). Если подключаешь мощную фару, используй транзистор или модуль реле.

  • Питание логики: Если ты питаешь Wemos через USB от пауэрбанка, а моторы от отдельного аккумулятора — все равно соедини их GND.

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

const char* ssid = "ИМЯ_ТВОЕЙ_СЕТИ";
const char* password = "ПАРОЛЬ_ОТ_WIFI";

ESP8266WebServer server(80);

// Пины для L298N
const int IN1 = D1; 
const int IN2 = D2;
const int IN3 = D5;
const int IN4 = D6;
const int lightPin = D3; 

int valM1 = 1024, valM2 = 1024, swState = 0;
const int deadzone = 35; // Размер мертвой зоны (1024 +/- 35)

const char PAGE[] PROGMEM = R"=====(
<!DOCTYPE HTML><html><head>
<meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0'>
<style>
  body { font-family: sans-serif; text-align: center; background: #121212; color: white; margin: 0; overflow: hidden; height: 100vh; }
  .header { height: 10vh; display: flex; align-items: center; justify-content: center; background: #1f1f1f; font-size: 1.2rem; font-weight: bold; border-bottom: 1px solid #333; }
  .container { display: flex; row-gap: 10px; justify-content: space-around; height: 85vh; align-items: flex-start; padding-top: 20px; }
  
  .slider-box { display: flex; flex-direction: column; align-items: center; width: 35%; }
  input[type=range][orient=vertical] { appearance: slider-vertical; width: 70px; height: 70vh; background: #333; cursor: pointer; border-radius: 10px; }
  
  .center-controls { width: 20%; display: flex; flex-direction: column; align-items: center; padding-top: 20px; }
  
  .switch { position: relative; display: inline-block; width: 60px; height: 34px; margin-bottom: 10px; }
  .switch input { opacity: 0; width: 0; height: 0; }
  .slider-round { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #444; transition: .4s; border-radius: 34px; }
  .slider-round:before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }
  input:checked + .slider-round { background-color: #f1c40f; box-shadow: 0 0 10px #f1c40f; }
  input:checked + .slider-round:before { transform: translateX(26px); }
  
  .label { font-size: 0.7rem; color: #888; text-transform: uppercase; margin-bottom: 5px; }
  .val { font-size: 1.1rem; margin-top: 10px; color: #2ecc71; font-family: monospace; min-height: 1.5em; }
</style>
</head>
<body>
  <div class="header">RC CONTROL PRO</div>
  <div class="container">
    <div class="slider-box">
      <div class="label">ЛЕВЫЙ</div>
      <input type='range' id='m1' min='0' max='2048' value='1024' orient='vertical' oninput='sendMotor(1, this.value)' ontouchend='resetMotor(1)' onmouseup='resetMotor(1)'>
      <div class='val' id='v1'>STOP</div>
    </div>

    <div class="center-controls">
      <div class="label">СВЕТ</div>
      <label class="switch">
        <input type="checkbox" id="sw" onchange="toggleSwitch(this.checked)">
        <span class="slider-round"></span>
      </label>
    </div>

    <div class="slider-box">
      <div class="label">ПРАВЫЙ</div>
      <input type='range' id='m2' min='0' max='2048' value='1024' orient='vertical' oninput='sendMotor(2, this.value)' ontouchend='resetMotor(2)' onmouseup='resetMotor(2)'>
      <div class='val' id='v2'>STOP</div>
    </div>
  </div>

<script>
function sendMotor(m, v) {
  let displayVal = "STOP";
  let dz = 35; 
  if (v > (1024 + dz)) displayVal = "↑ " + (v - 1024);
  else if (v < (1024 - dz)) displayVal = "↓ " + (1024 - v);
  
  document.getElementById('v' + m).innerHTML = displayVal;
  fetch('/set?m=' + m + '&v=' + v);
}

function resetMotor(m) {
  document.getElementById('m' + m).value = 1024;
  sendMotor(m, 1024);
}

function toggleSwitch(state) {
  fetch('/sw?state=' + (state ? 1 : 0));
}
</script>
</body></html>
)=====";

void driveMotor(int m, int value) {
  int speed = 0;
  // Проверка мертвой зоны
  if (value > (1024 + deadzone)) { 
    speed = map(value, 1024 + deadzone, 2048, 0, 1023); // Плавный старт после мертвой зоны
    if (m == 1) { analogWrite(IN1, speed); digitalWrite(IN2, LOW); }
    else { analogWrite(IN3, speed); digitalWrite(IN4, LOW); }
  } 
  else if (value < (1024 - deadzone)) {
    speed = map(value, 1024 - deadzone, 0, 0, 1023);
    if (m == 1) { digitalWrite(IN1, LOW); analogWrite(IN2, speed); }
    else { digitalWrite(IN3, LOW); analogWrite(IN4, speed); }
  } 
  else { // СТОП в мертвой зоне
    if (m == 1) { digitalWrite(IN1, LOW); digitalWrite(IN2, LOW); }
    else { digitalWrite(IN3, LOW); digitalWrite(IN4, LOW); }
  }
}

void handleUpdate() {
  int m = server.arg("m").toInt();
  int v = server.arg("v").toInt();
  if (m == 1) valM1 = v;
  if (m == 2) valM2 = v;
  driveMotor(m, v);
  server.send(200, "text/plain", "OK");
}

void handleSwitch() {
  swState = server.arg("state").toInt();
  digitalWrite(lightPin, swState);
  server.send(200, "text/plain", "OK");
}

void setup() {
  Serial.begin(115200);
  pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
  pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT);
  pinMode(lightPin, OUTPUT);
  
  analogWriteRange(1023); // Устанавливаем диапазон ШИМ

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  
  server.on("/", [](){ server.send(200, "text/html", PAGE); });
  server.on("/set", handleUpdate);
  server.on("/sw", handleSwitch);
  server.begin();
  Serial.println("\nIP Ready: " + WiFi.localIP().toString());
}

void loop() { server.handleClient(); }

Обновленный код для управления через точку доступа WI-FI создаваемой WEMOS D1.

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

const char* ap_ssid = "T-34-TANK";
const char* ap_pass = "12345678";

ESP8266WebServer server(80);

const int IN1 = D1; const int IN2 = D2; 
const int IN3 = D5; const int IN4 = D6; 

const char PAGE[] PROGMEM = R"=====(
<!DOCTYPE html><html><head><meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=0'>
<style>
  body{font-family:sans-serif;text-align:center;background:#0d0d0d;color:#fff;margin:0;height:100vh;overflow:hidden;touch-action:none;display:flex;flex-direction:column;align-items:center}
  #t{width:85vw;height:80px;margin-top:20px;-webkit-appearance:none;background:#333;border-radius:15px;border:1px solid #777}
  #t::-webkit-slider-thumb{width:80px;height:76px;background:#3498db;-webkit-appearance:none;border-radius:12px;border:2px solid #fff}
  .stop{width:160px;min-height:80px;margin:30px 0;background:#e74c3c;border:4px solid #fff;border-radius:20px;color:#fff;font-size:30px;font-weight:bold}
  #s{-webkit-appearance:slider-vertical;width:60px;height:55vh;background:#444;outline:none}
  #s::-webkit-slider-thumb{-webkit-appearance:none;width:60px;height:60px;background:#2ecc71;border:3px solid #fff;border-radius:50%}
</style></head><body>
    <input type='range' id='t' min='0' max='2048' value='1024' oninput='send()'>
    <button class='stop' onclick='stopAll()'>STOP</button>
    <input type='range' id='s' min='0' max='2048' value='1024' oninput='send()'>
<script>
  let last=0;
  function send(){
    let now=Date.now();
    if(now-last<50) return;
    last=now;
    fetch('/j?s='+document.getElementById('s').value+'&t='+document.getElementById('t').value);
  }
  function stopAll(){
    document.getElementById('s').value=1024;document.getElementById('t').value=1024;
    send();
  }
  document.addEventListener('touchmove',e=>{if(e.target.tagName!='INPUT')e.preventDefault();},{passive:false});
</script></body></html>
)=====";

void drive(int s_raw, int t_raw) {
  int dz = 90; 
  int speed = map(s_raw, 0, 2048, -1023, 1023);
  int steer = map(t_raw, 0, 2048, -250, 250); 
  
  if (abs(speed) < dz) speed = 0;
  if (abs(steer) < dz) steer = 0;

  // ИНВЕРСИЯ ПОВОРОТА ПРИ ДВИЖЕНИИ НАЗАД
  if (speed < -dz) {
    steer = -steer; 
  }

  int l, r;

  // РАЗВОРОТ НА МЕСТЕ
  if (speed == 0 && abs(steer) > 50) {
    l = (steer > 0) ? 400 : -400; 
    r = (steer > 0) ? -400 : 400;
  } else {
    l = speed + steer;
    r = speed - steer;
  }

  l = constrain(l, -1023, 1023);
  r = constrain(r, -1023, 1023);

  // Моторы
  auto runL = [](int v){
    if(v>50){analogWrite(IN3,v);digitalWrite(IN4,0);}
    else if(v<-50){digitalWrite(IN3,0);analogWrite(IN4,-v);}
    else{digitalWrite(IN3,0);digitalWrite(IN4,0);}
  };
  auto runR = [](int v){
    if(v>50){digitalWrite(IN1,0);analogWrite(IN2,v);}
    else if(v<-50){analogWrite(IN1,-v);digitalWrite(IN2,0);}
    else{digitalWrite(IN1,0);digitalWrite(IN2,0);}
  };
  runL(l); runR(r);
}

void setup() {
  pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
  pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT);
  analogWriteRange(1023); analogWriteFreq(450); 
  WiFi.softAP(ap_ssid, ap_pass);
  server.on("/", [](){ server.send_P(200, "text/html", PAGE); });
  server.on("/j", [](){
    drive(server.arg("s").toInt(), server.arg("t").toInt());
    server.send(200);
  });
  server.begin();
}
void loop() { server.handleClient(); }

Запуск проекта управление устройствами по WI-FI- Микроконтроллер Wemos D1

Лет пять-шесть назад я приобрел микроконтроллер Wemos D1, внешне очень похожий на Arduino Uno. Разница в том, что контроллером Wemos D1 можно управлять по Wi-Fi.

Wemos D1 способен подключаться к домашней Wi-Fi сети, и тогда управлять им можно с телефона, планшета, ноутбука или настольного компьютера. А еще он может создавать свою собственную точку доступа. Подключившись к его сети, можно управлять контроллером в «полевых» условиях, что очень удобно для мобильных гаджетов.




Все это было в планах еще при покупке, но сколько я ни бился, получалось программировать его только как обычный Arduino Uno, то есть без использования Wi-Fi. USB-разъем на плате Wemos D1 так называемый type-C. Информация в интернете была какой-то путаной, а выложенные скетчи не активировали беспроводные функции платы.

Все круто изменилось после моего знакомства с искусственным интеллектом, а именно с Google Gemini. Сначала я попросил его помочь с составлением макроса VBA для Excel. За 5–6 секунд он выдал код, который я, полный сомнений, вставил в модуль редактора. Каково же было мое удивление, когда код оказался рабочим! ИИ за считаные секунды накидал результат, на который я раньше тратил целый день кропотливого труда.

Я начал давать ему разные задачи по написанию кода, темы становились все сложнее и сложнее. Но ИИ с легкостью генерировал решения, а если в коде обнаруживалась ошибка, я просто «показывал» ему скриншот с текстом ошибки. ИИ тут же составлял новый вариант и писал что-то вроде: «Вот новый железобетонный код, удали старый и вставь этот».

А на днях я вспомнил о нереализованном проекте по управлению микроконтроллером через Wi-Fi. ИИ от Google, как постоянный житель всемирной паутины, прекрасно справился с задачей. В процессе отладки он давал очень толковые советы по улучшению внешнего вида сайта, создаваемого микроконтроллером по IP-адресу 192.168.4.1. Теперь, заходя на этот ресурс, я вижу два красивых вертикальных ползунка для управления двигателями маленького танка. Ноль — посередине. Двигаю пальцем вверх — и танк плавно едет вперед, вниз — назад. Один ползунок вверх, другой вниз — и танк разворачивается на месте! Между ползунков- кнопка включения/выключения фар. Использованы 5 выходных ШИМ/PWM пина микроконтроллера Wemos D1 из 9. На что пустить остальные свободные пины пока не придумал. 😊


Все прекрасно заработало. Запитал Wemos D1 и драйвер L298N для двух моторов танчика тремя литий-ионными аккумуляторами 18650. Они в сумме выдают примерно 12 вольт.
Вид с задней стороны.

Вид с переднкй стороны

Каждый каток снабжен аммортизатором

Рвёт с места, если тапок в пол- встает на дыбы

Как это работает:

  • Верхний горизонтальный ползунок — это «руль». Он управляет поворотами, меняя разность скоростей гусениц.

  • Длинный вертикальный ползунок — газ и реверс. От центра вверх — едем вперед, вниз — движение назад. Слайдер «ленивый»: он остается там, где вы его оставили, что очень удобно для длительного движения.

  • Кнопка STOP — большая красная кнопка в центре для экстренной мгновенной остановки всего.

Эволюция идеи: Поначалу интерфейс выглядел иначе: это были два вертикальных ползунка рядом, каждый из которых отвечал за свою гусеницу. Но испытания «в деле» быстро показали, что такая схема сложна для новичка.

Я пришел к выводу, что разделение на «Газ» и «Руль» гораздо эффективнее — теперь я могу уверенно управлять вездеходом одной рукой! ИИ помог не только написать код для ESP8266, но и внедрить хитрые алгоритмы: например, «буст» мощности при развороте на месте, чтобы моторы не пищали, и ограничение частоты команд для плавности без лагов.

В итоге, 03.02.2026 пришел еще к одному выводу. Управлять лучше через точку доступа WEMOS D1, исключив из цепочки роутер. Исчезли лаги.

Видео на Google disk: 



Обновление проекта "управление по WI-FI"

 Возникла идея (с подсказки ИИ) снабдить танчик видеокамерой, что бы получить эффект присуисивмя на месте водителя.  Возможно плата ESP32-CA...