In questa seconda parte del progetto, di Mario dedicata al Robot 4WD dual mode, leggerai una diversa tecnica, più attuale, per scrivere codice con l’aiuto dell’AI
ed approfondiremo il codice scritto dall’AI seguendo le linee guida impartite da Mario e dalla sua esperienza di robotica e programmazione.
Come funziona la modalità AVOID del Robot 4WD dual mode
Prima di guardare il codice, ti mostro lo schema a blocchi del comportamento.
È la cosa più utile per capire come ragiona il robot.
Schema a blocchi della modalità AVOID
Il robot legge la distanza davanti a sé.
Se non ci sono ostacoli entro 20 cm, va avanti.
Se ce n’è uno, si ferma e guarda a sinistra, al centro e a destra.
Il Robot 4WD dual mode poi decide:
- se esiste una via libera, gira verso il lato con più spazio e riparte
- se tutte e tre le direzioni sono bloccate, scatta il Panic Mode: indietreggia e gira
- se continua a trovare l’ostacolo davanti per più di cinque tentativi consecutivi, scatta l’Escape Mode: retromarcia, rotazione più lunga e ripartenza da capo con i contatori azzerati
Nella parte “va dritto” lo schema non mostra una cosa che sta nel codice: durante la marcia i due TCRT5000 correggono la traiettoria se un lato si avvicina troppo.
Lo vedremo tra poco analizzando il codice.
Il codice
Lo sketch é firmato “Mario Andrioletti & Copilot”.
// ============================================================================
// 4WD Robot by Mario Andrioletti & Copilot
// Funzioni: Servo scanning, SR04 filtrato, TCRT5000 con filtro dinamico,
// modalità MANUAL / AVOID, escape mode sicuro, LED AVOID.
// I 4 motori sono accoppiati in coppie: sinistra (2 motori) e destra (2 motori).
// ============================================================================
#include
#include
#include "SR04.h"
#include
// ---------------- PIN MOTORI (2 canali → 4 motori accoppiati) ----------------
const int left_ctrl = 2; // direzione motori sinistra
const int left_pwm = 5; // velocità motori sinistra
const int right_ctrl = 4; // direzione motori destra
const int right_pwm = 6; // velocità motori destra
// ---------------- SERVO TESTA ----------------
Servo myServo;
const int servopin = 10;
// ---------------- ULTRASUONI ----------------
#define TRIG_PIN 12
#define ECHO_PIN 13
SR04 sr04 = SR04(ECHO_PIN, TRIG_PIN);
// ---------------- TCRT5000 LATERALI ----------------
const int tcrtLeftPin = A0;
const int tcrtRightPin = A3;
int tcrtLeftBase = 0;
int tcrtRightBase = 0;
int tcrtLeftFilt = 0;
int tcrtRightFilt = 0;
const int tcrtAlpha = 4; // filtro dinamico
const int tcrtDelta = 80; // soglia rilevamento
const int tcrtHyst = 30; // isteresi
// ---------------- MATRICE LED ----------------
#define SCL_Pin A5
#define SDA_Pin A4
// ---------------- IR REMOTE ----------------
#define IR_PIN 3
// ---------------- VARIABILI GENERALI ----------------
int speeds = 150; // velocità base
float backFactor = 0.60; // riduzione velocità in retromarcia
int filteredDistance = 0; // distanza ultrasuoni filtrata
// ---------------- SERVO CALIBRATO ----------------
int servoCenter = 90;
int servoLeft = 150;
int servoRight = 30;
// ---------------- MODALITÀ ----------------
enum Mode { MANUAL, AVOID };
Mode mode = MANUAL;
// ---------------- AVOID ----------------
int avoidState = 0;
unsigned long avoidTimer = 0;
unsigned long forwardAfterTurnTimer = 0;
int distL = 0, distC = 0, distR = 0;
int turnTime = 400;
int avoidAttempts = 0; // tentativi per ESCAPE MODE
// ---------------- LED MODALITÀ AVOID ----------------
const int ledAvoid = 9;
// ============================================================================
// MATRICE LED – Icone
// ============================================================================
unsigned char start01[] = { 0x40, 0x8f, 0x99, 0xf9, 0x00, 0xff, 0x50, 0xff, 0x05, 0xff, 0x90, 0x9f, 0x09, 0xff, 0x60, 0x90 };
unsigned char avoid[] = { 0x3c, 0x42, 0x99, 0xbd, 0x99, 0x42, 0x3c, 0x00, 0x00, 0x3c, 0x42, 0x99, 0xbd, 0x99, 0x42, 0x3c };
unsigned char front_icon[] = { 0, 0, 0, 0, 0, 0x24, 0x12, 0x09, 0x12, 0x24, 0, 0, 0, 0, 0, 0 };
unsigned char back_icon[] = { 0, 0, 0, 0, 0, 0x24, 0x48, 0x90, 0x48, 0x24, 0, 0, 0, 0, 0, 0 };
unsigned char left_icon[] = { 0, 0, 0, 0, 0, 0, 0x44, 0x28, 0x10, 0x44, 0x28, 0x10, 0x44, 0x28, 0x10, 0 };
unsigned char right_icon[] = { 0, 0x10, 0x28, 0x44, 0x10, 0x28, 0x44, 0x10, 0x28, 0x44, 0, 0, 0, 0, 0, 0 };
unsigned char STOP01[] = { 0x2E, 0x2A, 0x3A, 0, 0x02, 0x3E, 0x02, 0, 0x3E, 0x22, 0x3E, 0, 0x3E, 0x0A, 0x0E, 0 };
// ============================================================================
// MATRICE LED – Funzioni base
// ============================================================================
void IIC_start() {
digitalWrite(SDA_Pin, HIGH);
digitalWrite(SCL_Pin, HIGH);
delayMicroseconds(3);
digitalWrite(SDA_Pin, LOW);
delayMicroseconds(3);
digitalWrite(SCL_Pin, LOW);
}
void IIC_end() {
digitalWrite(SCL_Pin, LOW);
digitalWrite(SDA_Pin, LOW);
delayMicroseconds(3);
digitalWrite(SCL_Pin, HIGH);
delayMicroseconds(3);
digitalWrite(SDA_Pin, HIGH);
delayMicroseconds(3);
}
void IIC_send(unsigned char send_data) {
for (byte mask = 0x01; mask != 0; mask <<= 1) {
digitalWrite(SDA_Pin, (send_data & mask) ? HIGH : LOW);
delayMicroseconds(3);
digitalWrite(SCL_Pin, HIGH);
delayMicroseconds(3);
digitalWrite(SCL_Pin, LOW);
}
}
void matrix_display(unsigned char matrix_value[]) {
IIC_start();
IIC_send(0xC0);
for (int i = 0; i < 16; i++) IIC_send(matrix_value[i]);
IIC_end();
IIC_start();
IIC_send(0x8A);
IIC_end();
}
void showMatrix(unsigned char pattern[]) {
matrix_display(pattern);
}
void scroll_start01() {
unsigned char frame[16];
for (int shift = 0; shift < 16; shift++) {
for (int i = 0; i < 16; i++) { int index = (i + shift) % 16; frame[i] = start01[index]; } matrix_display(frame); delay(300); } } // ============================================================================ // MOTORI – 4 motori accoppiati (sinistra/destra) // ============================================================================ void drive(bool leftDir, int leftSpeed, bool rightDir, int rightSpeed) { leftSpeed = constrain(leftSpeed, 0, 255); rightSpeed = constrain(rightSpeed, 0, 255); digitalWrite(left_ctrl, leftDir); digitalWrite(right_ctrl, rightDir); analogWrite(left_pwm, leftSpeed); analogWrite(right_pwm, rightSpeed); } void car_front() { drive(HIGH, speeds * 0.97, HIGH, speeds); } void car_back() { drive(LOW, speeds * backFactor, LOW, speeds * backFactor); } void car_left() { drive(LOW, speeds * backFactor, HIGH, speeds); } void car_right() { drive(HIGH, speeds, LOW, speeds * backFactor); } void car_Stop() { drive(LOW, 0, LOW, 0); } // ============================================================================ // ULTRASUONI – Lettura filtrata // ============================================================================ void updateUltrasuoni() { int d = sr04.Distance(); if (d > 0 && d < 300) { filteredDistance = (filteredDistance * 1 + d * 3) / 4; } } // ============================================================================ // TCRT5000 – Filtro dinamico + isteresi // ============================================================================ void updateTCRT() { int rawL = analogRead(tcrtLeftPin); int rawR = analogRead(tcrtRightPin); tcrtLeftFilt = (tcrtLeftFilt * (tcrtAlpha - 1) + rawL) / tcrtAlpha; tcrtRightFilt = (tcrtRightFilt * (tcrtAlpha - 1) + rawR) / tcrtAlpha; } bool leftSideClose() { int diff = tcrtLeftBase - tcrtLeftFilt; static bool state = false; if (!state && diff > tcrtDelta) state = true;
else if (state && diff < tcrtDelta - tcrtHyst) state = false; return state; } bool rightSideClose() { int diff = tcrtRightBase - tcrtRightFilt; static bool state = false; if (!state && diff > tcrtDelta) state = true;
else if (state && diff < tcrtDelta - tcrtHyst) state = false; return state; } // ============================================================================ // IR REMOTE – Comandi manuali + attivazione AVOID // ============================================================================ void updateIR() { if (IrReceiver.decode()) { uint8_t cmd = IrReceiver.decodedIRData.command; // STOP → modalità manuale if (cmd == 0x40) { mode = MANUAL; digitalWrite(ledAvoid, LOW); showMatrix(STOP01); car_Stop(); } // Comandi validi solo in MANUAL if (mode == MANUAL) { digitalWrite(ledAvoid, LOW); switch (cmd) { case 0x46: car_front(); showMatrix(front_icon); break; case 0x15: car_back(); showMatrix(back_icon); break; case 0x44: car_left(); showMatrix(left_icon); break; case 0x43: car_right(); showMatrix(right_icon); break; case 0xD: speeds = min(speeds + 10, 255); break; case 0x16: speeds = max(speeds - 10, 0); break; // Attiva modalità AVOID case 0x52: digitalWrite(ledAvoid, HIGH); showMatrix(avoid); mode = AVOID; avoidState = 0; avoidAttempts = 0; myServo.write(servoCenter); break; } } IrReceiver.resume(); } } // ============================================================================ // AVOID – Scansione servo + correzioni laterali + escape mode // ============================================================================ void updateAvoid() { if (mode != AVOID) return; switch (avoidState) { case 0: if (filteredDistance > 0 && filteredDistance < 20) avoidAttempts++; if (avoidAttempts > 5 && filteredDistance < 20) { car_back(); delay(300); myServo.write(servoRight); car_right(); delay(700); myServo.write(servoCenter); avoidAttempts = 0; avoidState = 0; break; } if (filteredDistance > 0 && filteredDistance < 20) { car_Stop(); myServo.write(servoLeft); avoidTimer = millis(); avoidState = 1; break; } if (leftSideClose() && !rightSideClose()) { drive(HIGH, speeds * 0.6, LOW, speeds * 0.3); myServo.write(servoRight); delay(40); myServo.write(servoCenter); break; } if (rightSideClose() && !leftSideClose()) { drive(LOW, speeds * 0.3, HIGH, speeds * 0.6); myServo.write(servoLeft); delay(40); myServo.write(servoCenter); break; } if (leftSideClose() && rightSideClose()) { drive(HIGH, speeds * 0.5, HIGH, speeds * 0.5); break; } avoidAttempts = 0; car_front(); break; case 1: if (millis() - avoidTimer > 180) {
distL = filteredDistance;
myServo.write(servoCenter);
avoidTimer = millis();
avoidState = 2;
}
break;
case 2:
if (millis() - avoidTimer > 180) {
distC = filteredDistance;
myServo.write(servoRight);
avoidTimer = millis();
avoidState = 3;
}
break;
case 3:
if (millis() - avoidTimer > 180) {
distR = filteredDistance;
myServo.write(servoCenter);
avoidState = 4;
}
break;
case 4:
if (distL < 20 && distC < 20 && distR < 20) { car_back(); delay(150); if (distL > distR) {
myServo.write(servoLeft);
car_left();
} else {
myServo.write(servoRight);
car_right();
}
delay(800);
myServo.write(servoCenter);
avoidState = 0;
break;
}
if (distC > 20) {
avoidAttempts = 0;
car_front();
avoidState = 0;
break;
}
if (distL > distR) {
myServo.write(servoLeft);
car_left();
} else {
myServo.write(servoRight);
car_right();
}
avoidTimer = millis();
avoidState = 5;
break;
case 5:
if (millis() - avoidTimer > (unsigned long)turnTime) {
car_front();
forwardAfterTurnTimer = millis();
avoidState = 6;
}
break;
case 6:
if (millis() - forwardAfterTurnTimer > 200) {
myServo.write(servoCenter);
avoidState = 0;
}
break;
}
}
// ============================================================================
// SETUP
// ============================================================================
void setup() {
pinMode(ledAvoid, OUTPUT);
digitalWrite(ledAvoid, LOW);
IrReceiver.begin(IR_PIN, ENABLE_LED_FEEDBACK);
pinMode(left_ctrl, OUTPUT);
pinMode(left_pwm, OUTPUT);
pinMode(right_ctrl, OUTPUT);
pinMode(right_pwm, OUTPUT);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
pinMode(SCL_Pin, OUTPUT);
pinMode(SDA_Pin, OUTPUT);
pinMode(tcrtLeftPin, INPUT);
pinMode(tcrtRightPin, INPUT);
long sumL = 0, sumR = 0;
for (int i = 0; i < 50; i++) {
sumL += analogRead(tcrtLeftPin);
sumR += analogRead(tcrtRightPin);
delay(5);
}
tcrtLeftBase = sumL / 50;
tcrtRightBase = sumR / 50;
tcrtLeftFilt = tcrtLeftBase;
tcrtRightFilt = tcrtRightBase;
myServo.attach(servopin);
myServo.write(servoCenter);
scroll_start01();
matrix_display(start01);
while (true) {
if (IrReceiver.decode()) {
IrReceiver.resume();
break;
}
}
}
// ============================================================================
// LOOP PRINCIPALE
// ============================================================================
void loop() {
updateIR();
updateUltrasuoni();
updateTCRT();
updateAvoid();
}
Provo a spiegartelo a blocchi, aggregando le parti ripetitive.
Librerie e pin
#include <Arduino.h> #include <IRremote.hpp> #include "SR04.h" #include <Servo.h>
Quattro librerie: la base di Arduino, IRremote per il telecomando, SR04 per l’ultrasuoni e Servo per la testa mobile.
const int left_ctrl = 2; const int left_pwm = 5; const int right_ctrl = 4; const int right_pwm = 6;
I quattro motori sono visti come due canali.
Per ognuno c’è un pin che decide la direzione e uno PWM che decide la velocità. Lo stesso schema vale per i pin del servo (D10), dell’ultrasuoni (TRIG D12, ECHO D13), dei TCRT (A0 e A3), della matrice (A5 e A4), dell’IR (D3) e del LED AVOID (D9).
Parametri e variabili
const int tcrtAlpha = 4; const int tcrtDelta = 80; const int tcrtHyst = 30;
Sono i tre numeri che regolano i TCRT. tcrtAlpha è il fattore del filtro, tcrtDelta è di quanto deve cambiare la lettura rispetto alla base per dire “ostacolo vicino”, tcrtHyst è l’isteresi che evita il rimbalzo tra vero e falso.
int speeds = 150; float backFactor = 0.60; int servoCenter = 90; int servoLeft = 150; int servoRight = 30;
La velocità base è 150 su 255. In retromarcia e nelle rotazioni il robot usa il 60% di quel valore, per non sbatterci contro. Il servo ha tre posizioni calibrate: centro a 90 gradi, sinistra a 150, destra a 30.
enum Mode { MANUAL, AVOID };
int avoidState = 0;
int avoidAttempts = 0;
int turnTime = 400;
La modalità è un enum con due valori.
Per AVOID c’è una macchina a stati (avoidState), un contatore di tentativi per l’Escape Mode e il tempo di rotazione, 400 ms.
Matrice LED
unsigned char front_icon[] = { 0, 0, 0, 0, 0, 0x24, 0x12, 0x09, ... };
Ogni icona (avanti, indietro, sinistra, destra, STOP, AVOID, animazione di avvio) è un array di 16 byte, uno per colonna della matrice 8×16. Ogni bit acceso è un LED acceso.
void IIC_start() { ... }
void IIC_end() { ... }
void IIC_send(unsigned char send_data) { ... }
Sono le tre funzioni che parlano con la matrice. Non uso la libreria Wire: il protocollo I2C è fatto a mano muovendo SDA e SCL con digitalWrite, con 3 microsecondi di pausa tra un fronte e l’altro. IIC_start e IIC_end generano le condizioni di start e stop, IIC_send manda un byte bit per bit, dal meno significativo.
void matrix_display(unsigned char matrix_value[]) { ... }
void showMatrix(unsigned char pattern[]) { matrix_display(pattern); }
matrix_display manda l’indirizzo 0xC0, poi i 16 byte del disegno, poi un secondo comando (0x8A) che accende il display con una certa luminosità. showMatrix è solo un nome più comodo da usare nel resto del codice.
void scroll_start01() { ... }
L’animazione di avvio: prende i 16 byte di start01 e li fa scorrere di una colonna per volta, con 300 ms tra un fotogramma e l’altro. L’operatore % 16 fa ricominciare il disegno dall’inizio.
Motori Robot 4WD dual mode
void drive(bool leftDir, int leftSpeed, bool rightDir, int rightSpeed) {
leftSpeed = constrain(leftSpeed, 0, 255);
rightSpeed = constrain(rightSpeed, 0, 255);
digitalWrite(left_ctrl, leftDir);
digitalWrite(right_ctrl, rightDir);
analogWrite(left_pwm, leftSpeed);
analogWrite(right_pwm, rightSpeed);
}
Tutta la gestione dei motori passa da qui. La funzione limita le velocità tra 0 e 255, imposta la direzione dei due lati e poi manda il PWM. Tutte le altre funzioni sono scorciatoie che la chiamano.
void car_front() { drive(HIGH, speeds * 0.97, HIGH, speeds); }
void car_back() { drive(LOW, speeds * backFactor, LOW, speeds * backFactor); }
void car_left() { drive(LOW, speeds * backFactor, HIGH, speeds); }
void car_right() { drive(HIGH, speeds, LOW, speeds * backFactor); }
void car_Stop() { drive(LOW, 0, LOW, 0); }
Cinque movimenti. In car_front il lato sinistro va al 97% della velocità: è una compensazione, perché i due lati non girano mai esattamente alla stessa velocità e senza correzione il robot tende a sbandare. In retromarcia entrambi i lati vanno al 60%. In car_left e car_right un lato avanza e l’altro retrocede, da cui la rotazione sul posto. car_Stop azzera il PWM.
Sensori
void updateUltrasuoni() {
int d = sr04.Distance();
if (d > 0 && d < 300) {
filteredDistance = (filteredDistance * 1 + d * 3) / 4;
}
}
La lettura viene scartata se vale zero (nessun eco) o supera i 300 cm. Quelle valide passano da una media pesata: un quarto del valore precedente e tre quarti della lettura nuova. Il valore si assesta in fretta ma una lettura sbagliata isolata non lo fa saltare. Il manuale indica come intervallo utile 5-200 cm.
void updateTCRT() {
int rawL = analogRead(tcrtLeftPin);
int rawR = analogRead(tcrtRightPin);
tcrtLeftFilt = (tcrtLeftFilt * (tcrtAlpha - 1) + rawL) / tcrtAlpha;
tcrtRightFilt = (tcrtRightFilt * (tcrtAlpha - 1) + rawR) / tcrtAlpha;
}
Stessa logica, applicata ai due TCRT: con tcrtAlpha a 4, il valore filtrato è tre quarti del vecchio più un quarto del nuovo. È un filtro passa-basso molto semplice, ma bastano poche righe per togliere il rumore.
bool leftSideClose() {
int diff = tcrtLeftBase - tcrtLeftFilt;
static bool state = false;
if (!state && diff > tcrtDelta) state = true;
else if (state && diff < tcrtDelta - tcrtHyst) state = false;
return state;
}
rightSideClose() è identica, con i valori del lato destro. La funzione confronta la lettura con la base misurata all’avvio. Avvicinandosi a un ostacolo la lettura scende, quindi diff cresce. Il sensore passa a “vicino” quando diff supera 80 e torna a “libero” solo quando scende sotto 50 (80 meno 30 di isteresi). Senza isteresi, a ridosso della soglia il segnale rimbalzerebbe tra i due stati a ogni ciclo. state è static, quindi la funzione ricorda il valore tra una chiamata e l’altra.
Telecomando Robot 4WD dual mode
void updateIR() {
if (IrReceiver.decode()) {
uint8_t cmd = IrReceiver.decodedIRData.command;
...
IrReceiver.resume();
}
}
Se è arrivato un codice IR lo legge, lo gestisce e poi chiama resume() per tornare in ascolto.
if (cmd == 0x40) {
mode = MANUAL;
digitalWrite(ledAvoid, LOW);
showMatrix(STOP01);
car_Stop();
}
Il tasto OK ferma il robot, spegne il LED AVOID, mostra STOP sulla matrice e riporta tutto in MANUAL. Funziona sempre, anche in AVOID: è il tuo freno di emergenza.
switch (cmd) {
case 0x46: car_front(); showMatrix(front_icon); break;
case 0x15: car_back(); showMatrix(back_icon); break;
case 0x44: car_left(); showMatrix(left_icon); break;
case 0x43: car_right(); showMatrix(right_icon); break;
...
}
Le quattro frecce hanno lo stesso schema: eseguono il movimento e mostrano l’icona corrispondente. I tasti 1 e 3 alzano o abbassano speeds di 10 alla volta, tra 0 e 255. Il tasto 0 (0x52) attiva AVOID: accende il LED, mostra l’icona AVOID, azzera stato e tentativi e riporta il servo al centro.
La macchina a stati AVOID
void updateAvoid() {
if (mode != AVOID) return;
switch (avoidState) { ... }
}
Se il robot 4WD dual mode è in MANUAL la funzione esce subito.
Altrimenti esegue il blocco corrispondente allo stato corrente.
Gli stati non usano delay() per le scansioni ma millis(), così il ciclo principale continua a girare e il telecomando resta reattivo.
Stato 0: guida e controllo
if (filteredDistance > 0 && filteredDistance < 20) avoidAttempts++;
Ogni volta che trova qualcosa a meno di 20 cm incrementa il contatore dei tentativi.
if (avoidAttempts > 5 && filteredDistance < 20) {
car_back(); delay(300);
myServo.write(servoRight);
car_right(); delay(700);
myServo.write(servoCenter);
avoidAttempts = 0;
...
}
È l’Escape Mode dello schema: oltre il quinto tentativo il robot indietreggia per 300 ms, gira a destra per 700 ms, rimette il servo al centro e azzera il contatore. Qui delay() è accettabile perché è una manovra breve e il robot è comunque in difficoltà.
if (filteredDistance > 0 && filteredDistance < 20) {
car_Stop();
myServo.write(servoLeft);
avoidTimer = millis();
avoidState = 1;
break;
}
Se l’ostacolo è a meno di 20 cm si ferma, gira la testa a sinistra e passa allo stato 1, memorizzando l’istante di partenza.
if (leftSideClose() && !rightSideClose()) {
drive(HIGH, speeds * 0.6, LOW, speeds * 0.3);
myServo.write(servoRight);
delay(40);
myServo.write(servoCenter);
break;
}
Qui entrano in gioco i TCRT. Se è vicino solo il lato sinistro, il robot si sposta a destra: il motore sinistro avanza al 60%, quello destro retrocede al 30%. Il servo fa un piccolo scatto verso destra e torna al centro in 40 ms. Il blocco per il lato destro è speculare, con velocità e direzioni invertite e il servo che scatta a sinistra.
if (leftSideClose() && rightSideClose()) {
drive(HIGH, speeds * 0.5, HIGH, speeds * 0.5);
break;
}
Se sono vicini entrambi i lati, tipicamente un corridoio stretto, va dritto a metà velocità.
avoidAttempts = 0; car_front();
Se nessuna condizione si è verificata, la strada è libera: azzera i tentativi e va avanti.
Stati 1, 2 e 3: la scansione
case 1:
if (millis() - avoidTimer > 180) {
distL = filteredDistance;
myServo.write(servoCenter);
avoidTimer = millis();
avoidState = 2;
}
break;
Il servo è già girato a sinistra. Si aspettano 180 ms perché arrivi in posizione e la lettura si stabilizzi, poi si salva la distanza come distL, si manda il servo al centro e si passa allo stato 2. Gli stati 2 e 3 sono la stessa cosa: il 2 salva distC e manda il servo a destra, il 3 salva distR e lo riporta al centro. Alla fine il robot conosce tre distanze: sinistra, centro, destra.
Stato 4: la decisione
if (distL < 20 && distC < 20 && distR < 20) {
car_back(); delay(150);
if (distL > distR) { myServo.write(servoLeft); car_left(); }
else { myServo.write(servoRight); car_right(); }
delay(800);
myServo.write(servoCenter);
avoidState = 0;
break;
}
È il Panic Mode. Se tutte e tre le direzioni sono sotto i 20 cm, indietreggia per 150 ms e gira per 800 ms dal lato con più spazio, anche se di poco.
if (distC > 20) {
avoidAttempts = 0;
car_front();
avoidState = 0;
break;
}
Se davanti si è liberato, riparte dritto: l’ostacolo era mobile, oppure era un falso positivo.
if (distL > distR) { myServo.write(servoLeft); car_left(); }
else { myServo.write(servoRight); car_right(); }
avoidTimer = millis();
avoidState = 5;
Nel caso normale, il centro è bloccato ma almeno un lato è libero. Il robot gira verso il lato più libero e passa allo stato 5.
Stati 5 e 6: uscita dalla manovra
case 5:
if (millis() - avoidTimer > (unsigned long)turnTime) {
car_front();
forwardAfterTurnTimer = millis();
avoidState = 6;
}
break;
Il robot gira per turnTime, 400 ms, poi riparte in avanti. Lo stato 6 aspetta 200 ms di marcia dritta, rimette il servo al centro e torna allo stato 0. Quei 200 ms servono a spostarsi dall’ostacolo prima di ricominciare a controllare.
Setup e loop
long sumL = 0, sumR = 0;
for (int i = 0; i < 50; i++) {
sumL += analogRead(tcrtLeftPin);
sumR += analogRead(tcrtRightPin);
delay(5);
}
tcrtLeftBase = sumL / 50;
tcrtRightBase = sumR / 50;
Nel setup(), tra le inizializzazioni dei pin, i TCRT vengono calibrati: 50 letture a 5 ms l’una, poi la media diventa la base per ciascun lato. È questo che permette al filtro di lavorare in modo relativo e non su una soglia assoluta. Una conseguenza pratica: accendi il robot con i lati liberi, altrimenti la base sarà falsata.
scroll_start01();
matrix_display(start01);
while (true) {
if (IrReceiver.decode()) {
IrReceiver.resume();
break;
}
}
Poi parte l’animazione e il robot aspetta che premi un qualsiasi tasto del telecomando. Solo dopo entra nel loop().
void loop() {
updateIR();
updateUltrasuoni();
updateTCRT();
updateAvoid();
}
Il ciclo principale è quattro chiamate: telecomando, ultrasuoni, TCRT, logica AVOID.
Tutto il resto vive nelle funzioni che hai appena visto.
Cosa ha fatto l’AI per il Robot 4WD dual mode
Il codice ha una scansione a tre direzioni, un Panic Mode, un Escape Mode con contatore dei tentativi, la correzione laterale con i TCRT e la matrice che mostra cosa sta facendo il robot.
Quello che l’AI non ha deciso è l’idea: montare due sensori a 45 gradi davanti:
per vedere i lati mentre il robot va dritto, è un’intuizione di Mario, nata guardando come si comportava il robot.
Trovi qui il link al progetto 3D scaricabile.
L’AI l’ha trasformata in funzioni (leftSideClose, rightSideClose) con filtro e isteresi, che è la parte da scrivere a mano con la possibilità di sbagliare.
Il rischio, con questo metodo, è non capire il codice che ti arriva.
Un buon antidoto è quello che abbiamo fatto qui: leggerlo riga per riga e spiegarlo.
Se riesci a farlo, lo sketch è tuo, e correggerlo la volta dopo diventa facile.
Come è stato scritto: l’AI nel progetto
Mario lo racconta così:
“Ho usato un po’ degli esempi, ma non mi soddisfacevano, così ho deciso per prova di usare l’IA per creare uno sketch da zero e vi sono riuscito.
Ho usato Copilot, all’inizio gli ho detto la configurazione del robot, i vari sensori e driver e piano piano, dopo prove e riprove, mi ha generato un codice veramente interessante.”
Il risultato è la modalità manuale con telecomando, che fa avanti, indietro, destra, sinistra e rotazione su se stesso, più la modalità autonoma, che – a detta di Mario – “se la cava in quasi tutte le situazioni al meglio”.
Cosa è cambiato nel modo di scrivere il codice
Prima, per un robot così, si partiva da un esempio del produttore e lo si adattava.
Funziona, ma ti porta dentro la logica di chi ha scritto l’esempio, e i suoi limiti: nel caso di un kit, ad esempio, un robot che guarda solo davanti e non sa uscire da un angolo.
Con un assistente AI il punto di partenza è un altro.
Non parti dal codice, parti dalla descrizione dell’hardware, descrivi come è composto il robot:
- ha quattro motori accoppiati in due canali;
- un driver;
- un HC-SR04 su servo;
- un ricevitore IR;
- una matrice LED;
e chiedi un comportamento. Il lavoro si sposta dallo scrivere le istruzioni al descrivere con precisione cosa vuoi.
Come si vede dal codice, quello che esce non è un copia-incolla di esempi.
È organizzato in funzioni piccole con un solo compito: drive per i motori, updateTCRT per i sensori, updateAvoid per la logica.
La macchina a stati con millis() al posto dei delay() è un’impostazione che chi inizia con Arduino spesso scopre tardi, a forza di robot che non rispondono al telecomando.
Il processo, però, non è “chiedo e ottengo”.
Mario parla di “prove e riprove”: ogni versione va caricata sul robot, messa su un pavimento e osservata.
Poi si torna dall’AI a riportare cosa non va: “in quel corridoio si pianta”, “gira dalla parte sbagliata”.
Costanti come turnTime, tcrtDelta o il 97% sul motore sinistro non vengono dalla teoria ma dal comportamento reale delle gomme sul pavimento di casa sua.





Il blog mauroalfieri.it ed i suoi contenuti sono distribuiti con Licenza