|
// |
|
// M5ATOM Balancing Robot |
|
// Kiraku Labo, 2022 |
|
// |
|
// 1. Power on and wait for a circle to shows up. |
|
// 2. Hold the robot upright. |
|
// 3. When a smile shows up, release the robot. |
|
// |
|
|
|
#include <M5Atom.h> |
|
#include <esp_now.h> |
|
#include <WiFi.h> |
|
#define CHANNEL 0 |
|
|
|
#define MAXDRV 255 |
|
#define SDA_PIN 26 |
|
#define SCL_PIN 32 |
|
#define MOTOR_R 0x65 |
|
#define MOTOR_L 0x63 |
|
|
|
#define BLU 0x0000ff |
|
#define GRN 0xff0000 |
|
#define RED 0x00ff00 |
|
#define BLK 0x000000 |
|
|
|
boolean serialMonitor=true; |
|
uint16_t counter=0, counterOverSpd=0, state=0; |
|
uint32_t time0=0, time1=0; |
|
float power, powerR, powerL, yawPower, maxPwr; |
|
float varAng, varOmg, varSpd, varDst, varIang, aveAbsOmg=0.0, yawAng=0.0; |
|
float Kang, Komg, KIang, Kyaw, Kdst, Kspd; |
|
float gyroYdata, gyroZdata, accXdata, accXoffset, gyroZoffset, gyroYoffset; |
|
const uint32_t interval = 10; // in msec |
|
const float dt=(float)interval/1000.0; // in sec |
|
float movePower=0.0, movePwrTarget, movePwrStep=1.0; |
|
float mechFactorR, mechFactorL; |
|
float spinStep, spinDest, spinTarget, spinFact=1.0; |
|
bool joyButton=false; |
|
|
|
int16_t drvOffset, punchSpd, punchSpd2, punchSpd2N, punchDur, punchCountL=0, punchCountR=0; |
|
int16_t motorDeadband=0, counterTrip=0; |
|
int8_t motorRdir=0, motorLdir=0; |
|
bool spinContinuous=false; |
|
|
|
byte joyMACaddr[] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};//{0x50, 0x02, 0x91, 0x86, 0x99, 0x68}; |
|
|
|
const byte smile[] = {0,0,0,0,0, |
|
0,1,0,1,0, |
|
0,0,0,0,0, |
|
1,0,0,0,1, |
|
0,1,1,1,0}; |
|
const byte frown[] = {0,0,0,0,0, |
|
0,1,0,1,0, |
|
0,0,0,0,0, |
|
0,1,1,1,0, |
|
1,0,0,0,1}; |
|
const byte circle[] = {0,1,1,1,0, |
|
1,0,0,0,1, |
|
1,0,0,0,1, |
|
1,0,0,0,1, |
|
0,1,1,1,0}; |
|
const byte square[] = {0,0,0,0,0, |
|
0,1,1,1,0, |
|
0,1,0,1,0, |
|
0,1,1,1,0, |
|
0,0,0,0,0}; |
|
const byte cross[] = {1,0,0,0,1, |
|
0,1,0,1,0, |
|
0,0,1,0,0, |
|
0,1,0,1,0, |
|
1,0,0,0,1}; |
|
const byte heart[] = {0,1,0,1,0, |
|
1,0,1,0,1, |
|
1,0,0,0,1, |
|
0,1,0,1,0, |
|
0,0,1,0,0}; |
|
const byte all[] = {1,1,1,1,1, |
|
1,1,1,1,1, |
|
1,1,1,1,1, |
|
1,1,1,1,1, |
|
1,1,1,1,1}; |
|
|
|
void setup() { |
|
M5.begin(true, true, true); //Serial, i2c(Wire1), disp |
|
Wire.begin(SDA_PIN, SCL_PIN, 50000); //clock=50kHz |
|
Wire.beginTransmission(MOTOR_R); //check motor driver |
|
if (Wire.endTransmission()!=0) { //motor driver connection problem |
|
ledDispImg(cross, RED); |
|
delay(5000); |
|
} |
|
M5.IMU.Init(); |
|
setupESPNOW(); |
|
delay(1500); |
|
resetMotor(); |
|
resetPara(); |
|
resetVar(); |
|
getBaselineOmg(); |
|
ledDispImg(circle, GRN); |
|
} |
|
|
|
void loop() { |
|
static uint32_t msec=0; |
|
getIMU(); |
|
switch (state) { |
|
case 0: |
|
if (joyButton) ledDispImg(circle, RED); |
|
else ledDispImg(circle, GRN); |
|
if (abs(accXdata)<0.25 && aveAbsOmg<1.0) { |
|
ledDispImg(square, GRN); |
|
getBaseline(); |
|
msec=millis(); |
|
resetVar(); |
|
state=1; |
|
} |
|
break; |
|
case 1: |
|
if (millis()-msec>200) state=2; |
|
break; |
|
case 2: |
|
if (abs(varAng)>30.0 || counterOverSpd>25) { |
|
if (serialMonitor) sendStatus(); |
|
resetMotor(); |
|
resetVar(); |
|
ledDispImg(frown, RED); |
|
state=0; |
|
} |
|
else { |
|
if (joyButton) ledDispImg(smile, RED); |
|
else ledDispImg(smile, GRN); |
|
calcTarget(); |
|
drive(); |
|
} |
|
break; |
|
} |
|
counter += 1; |
|
if (counter%25==0) { |
|
sendData("vs=" + String(varSpd) + "\nvd=" + String(varDst) + "\nva=" + String(varAng) + "\npw=" + String(power)); |
|
} |
|
if (counter >= 100) { |
|
counter = 0; |
|
if (serialMonitor) sendStatus(); |
|
} |
|
do time1 = millis(); |
|
while (time1 - time0 < interval); |
|
time0 = time1; |
|
} |
|
|
|
void getIMU() { |
|
readIMU(); |
|
varOmg = gyroYdata - gyroYoffset; |
|
aveAbsOmg = aveAbsOmg * 0.8 + abs(varOmg) * 0.2; |
|
yawAng += (gyroZdata - gyroZoffset) * dt; |
|
varAng = (varAng + varOmg * dt) * 0.995 + (accXdata - accXoffset) * 57.3 * 0.005; |
|
} |
|
|
|
void readIMU() { |
|
float gX, gY, gZ, aX, aY, aZ; |
|
M5.IMU.getGyroData(&gX,&gY,&gZ); |
|
M5.IMU.getAccelData(&aX, &aY, &aZ); |
|
gyroYdata=-gX; |
|
gyroZdata=-gY; |
|
accXdata=aZ; |
|
} |
|
|
|
void calcTarget() { |
|
spinTarget += spinStep; |
|
if (movePwrTarget < movePower-movePwrStep) movePwrTarget += movePwrStep; |
|
else if (movePwrTarget > movePower+movePwrStep) movePwrTarget -= movePwrStep; |
|
else movePwrTarget = movePower; |
|
} |
|
|
|
void drive() { |
|
varSpd += power * dt; |
|
varDst += Kdst * varSpd * dt; |
|
varIang += KIang * varAng * dt; |
|
power = varIang + varDst + (Kspd * varSpd) + (Kang * varAng) + (Komg * varOmg); |
|
if (abs(power) > 500.0) counterOverSpd += 1; |
|
else counterOverSpd=0; |
|
power = constrain(power, -maxPwr, maxPwr); |
|
yawPower = (yawAng - spinTarget) * Kyaw; |
|
powerR = power + yawPower + movePwrTarget; |
|
powerL = power - yawPower + movePwrTarget; |
|
int16_t ipowerL = (int16_t) constrain(powerL * mechFactorL, -maxPwr, maxPwr); |
|
if (ipowerL > 0) { |
|
if (motorLdir == 1) { |
|
punchCountL++; |
|
punchCountL = constrain(punchCountL, 0, 100); |
|
} |
|
else punchCountL=0; |
|
motorLdir = 1; |
|
if (punchCountL<punchDur) drvMotorL(max(ipowerL, punchSpd2)+drvOffset); |
|
else drvMotorL(ipowerL+motorDeadband+drvOffset); |
|
} |
|
else if (ipowerL < 0) { |
|
if (motorLdir == -1) { |
|
punchCountL++; |
|
punchCountL = constrain(punchCountL, 0, 100); |
|
} |
|
else punchCountL=0; |
|
motorLdir = -1; |
|
if (punchCountL<punchDur) drvMotorL(min(ipowerL, punchSpd2N) +drvOffset); |
|
else drvMotorL(ipowerL-motorDeadband+drvOffset); |
|
} |
|
else { |
|
drvMotorL(0); |
|
motorLdir = 0; |
|
} |
|
|
|
int16_t ipowerR = (int16_t) constrain(powerR * mechFactorR, -maxPwr, maxPwr); |
|
if (ipowerR > 0) { |
|
if (motorRdir == 1) { |
|
punchCountR++; |
|
punchCountR = constrain(punchCountL, 0, 100); |
|
} |
|
else punchCountR=0; |
|
motorRdir = 1; |
|
if (punchCountR<punchDur) drvMotorR(max(ipowerR, punchSpd2) +drvOffset); |
|
else drvMotorR(ipowerR+motorDeadband+drvOffset); |
|
} |
|
else if (ipowerR < 0) { |
|
if (motorRdir == -1) { |
|
punchCountR++; |
|
punchCountR = constrain(punchCountR, 0, 100); |
|
} |
|
else punchCountR=0; |
|
motorRdir = -1; |
|
if (punchCountR<punchDur) drvMotorR(min(ipowerR, punchSpd2N)+drvOffset); |
|
else drvMotorR(ipowerR-motorDeadband+drvOffset); |
|
} |
|
else { |
|
drvMotorR(0); |
|
motorRdir = 0; |
|
} |
|
} |
|
|
|
void drvMotorL(int16_t pwr) { |
|
drv8830(MOTOR_L, constrain(pwr, -255, 255)); |
|
} |
|
|
|
void drvMotorR(int16_t pwr) { |
|
drv8830(MOTOR_R, constrain(pwr, -255, 255)); |
|
} |
|
|
|
void drv8830(byte adr, int16_t pwr) { //pwr -255 to 255 |
|
byte dir, st; |
|
if (pwr < 0) dir = 2; |
|
else dir =1; |
|
byte ipower=(byte)(abs(pwr)/4); |
|
if (ipower == 0) dir=3; //brake |
|
ipower = constrain (ipower, 0, 63); |
|
st = drv8830read(adr); |
|
if (st & 1) drv8830write(adr, 0, 0); |
|
drv8830write(adr, ipower, dir); |
|
} |
|
|
|
void drv8830write(byte adr, byte pwm, byte ctrl) { |
|
Wire.beginTransmission(adr); |
|
Wire.write(0); |
|
Wire.write(pwm*4+ctrl); |
|
Wire.endTransmission(true); |
|
} |
|
|
|
int drv8830read(byte adr) { |
|
Wire.beginTransmission(adr); |
|
Wire.write(1); |
|
Wire.endTransmission(false); |
|
Wire.requestFrom((int)adr, (int)1, (int)1); |
|
return Wire.read(); |
|
} |
|
|
|
void resetMotor() { |
|
drv8830(MOTOR_R, 0); |
|
drv8830(MOTOR_L, 0); |
|
counterOverSpd=0; |
|
counterTrip=0; |
|
} |
|
|
|
void resetPara() { |
|
Kang=35.0; |
|
Komg=0.6; |
|
KIang=700.0; |
|
Kdst=11.0; |
|
Kspd=7.5; |
|
Kyaw=10.0; |
|
mechFactorL=0.7; |
|
mechFactorR=0.7; |
|
maxPwr=550.0; |
|
punchSpd=50; |
|
punchDur=1; |
|
motorDeadband=35; |
|
punchSpd2=max(punchSpd, motorDeadband); |
|
punchSpd2N=-punchSpd2; |
|
drvOffset=0; |
|
} |
|
|
|
void resetVar() { |
|
power=0.0; |
|
movePwrTarget=0.0; |
|
movePower=0.0; |
|
spinStep=0.0; |
|
spinDest=0.0; |
|
spinTarget=0.0; |
|
yawAng=0.0; |
|
varAng=0.0; |
|
varOmg=0.0; |
|
varDst=0.0; |
|
varSpd=0.0; |
|
varIang=0.0; |
|
} |
|
|
|
void getBaseline() { |
|
accXoffset=0.0; |
|
gyroZoffset=0.0; |
|
for (int i=0; i < 30; i++){ |
|
readIMU(); |
|
accXoffset += accXdata; |
|
gyroZoffset += gyroZdata; |
|
delay(10); |
|
} |
|
accXoffset /= 30.0; |
|
gyroZoffset /= 30.0; |
|
} |
|
|
|
void getBaselineOmg() { |
|
gyroYoffset=0.0; |
|
for (int i=0; i < 30; i++){ |
|
readIMU(); |
|
gyroYoffset += gyroYdata; |
|
delay(10); |
|
} |
|
gyroYoffset /= 30.0; |
|
} |
|
|
|
void sendStatus () { |
|
Serial.print("state="); Serial.print(state); |
|
Serial.print(" accX="); Serial.print(accXdata); |
|
Serial.print(" ang=");Serial.print(varAng); |
|
Serial.print(" "); |
|
Serial.print(millis()-time0); |
|
Serial.println("ms"); |
|
} |
|
|
|
void ledDispImg(const byte* p, uint64_t c) { |
|
for (byte i=0; i<25; i++) { |
|
M5.dis.drawpix(i, p[i]*c); |
|
} |
|
} |
|
|
|
void setupESPNOW() { |
|
Serial.print("MAC="); |
|
Serial.println(WiFi.macAddress()); |
|
WiFi.mode(WIFI_STA); |
|
esp_now_init(); |
|
esp_now_register_recv_cb(onRecv); |
|
esp_now_peer_info_t peerInfo={}; |
|
memcpy(peerInfo.peer_addr, joyMACaddr, 6); |
|
peerInfo.channel = CHANNEL; |
|
peerInfo.encrypt = false; |
|
esp_now_add_peer(&peerInfo); |
|
} |
|
|
|
void onRecv(const uint8_t * mac, const uint8_t *recvData, int len) { |
|
if ((int8_t)recvData[2]==1) joyButton=true; |
|
else joyButton=false; |
|
steer((int8_t)recvData[0], (int8_t)recvData[1]); |
|
} |
|
|
|
void sendData(String s) { |
|
esp_now_send(joyMACaddr, (const byte*)s.c_str(), s.length()); |
|
} |
|
|
|
void steer(int8_t x, int8_t y) { |
|
if (abs(y)>20) movePower=(float)y*0.5; |
|
else movePower=0.0; |
|
if (abs(x)>30) spinStep=(float)x*0.015; |
|
else spinStep=0.0; |
|
} |