DIY mountain e-bike version 2

DIY mountain e-Bike version 2
A second version for my e-bike

This is the second version of my DIY mountain e-bike I made almost two years ago.

Motivation

The previous version served me very well, having more than 2000 km on it without any major issue. This motivated me to invest some more time to improve the design a bit and go for a newer version. In the meanwhile, VESC and ESP32 got much cheaper and became an option for hobby projects. I used them both in this new version. The MicroPython on the ESP32 has been a motivation by itself, although I could not get it into talking to VESC UART and had to use ARDUNIO C++ for programming at the end.

ESP32

ESP32 offers more than previous Arduino Nano ATmega328P that I used before and is much easier to work with since almost all pins can be used for everything, including serial communications over UART. It also allows using Python through MicroPython library which would offer a lot since it support AsyncIO (uAsyncIO to be specific) for asynchronous programming. However, I could not get it to read the messages coming from the VESC motor controller using UART communication and had to go back to using Arduino C++ for programming.

VESC

VESC is an electronic speed controller (ESC) designed by Benjamin Vedder offering industry-level functionalities despite being affordable. By the time I started making the first version, VESC was still not very inexpensive, but several chinese companies stepped into making the hardware compatible with VESC specs and the price has dropped since then. For this version I used FLIPSKY FSESC V4 50A SK8-ESC which is based on the original vesc design and can be managed using the free VESC-tool costing only around €47.

Libraries

The Arduino libraries I used in this version are:

LiquidCrystal (LCD)

My fork of VescUART

To show all the information transmitted from the VESC: VescUART

TinyGPSPlus

A GPS module can give you a RTC on top of GPS data: TinyGPSPlus

Smoothed

I used this for smoothing out the data from sensors and also throttle values: Smoothed

PushButton

A library for easy implementation of push buttons: PushButton

Parts needed

The parts I used in this version are as follow. Note that some of them are the ones I had from my previous version including the motor, the battery and the battery charger.

Desc Price (€)
ESP-32 ESP-32S Development 2.4GHz Dual-Mode WiFi+Bluetooth Antenna Module 4.36
FLIPSKY FSESC V4 50A SK8-ESC with 5V/1.5A BEC 47
Ublox NEO6M GPS GY-GPS6MV2 GPS Modul NEO6MV2 7.90
Ethernet connectors 3.32
RC Turnigy Aerodrive SK3 5065-236kv #3D Monster 6-10S 49
Enamelled copper wire (KUPFERLACKDRAHT) 200g ⌀0.9 (I used this for rewinding the motor) 6.90
Cat5e patch cable 4

Performance

Performance is as it was before since the battery size and motor are the same. However, the noise and vibrations from the motor has decreased to around 20% of the previous version since the motor is now controlled using field-oriented control (FOC). The range has also increased by ~10% since the regenerative brake is now much more efficient. I rarely use the bike's brakes now, most of the time turning down the throttle would do the job with the regenerative brake.

Result

Here is the result. I am very happy with how it turned out.

IMG_20191118_081339_display.jpg

Figure 1: The overall design

One box in the front accommodates the ESP32 with GPS module and it is connected to the box in the back which holds the VESC motor controller. The connection is made with the use of a LAN cable.

IMG_20191114_091305_display.jpg

Figure 2: Control unit

This is how the control unit looks like. The rotating handle is connected to a potentiometer adjusting the throttle value using the ADC on ESP32. The three push buttons are only connected to page change on the display for now, but can (and will be) programmed later.

IMG_20191103_162348_display.jpg

Figure 3: A look inside the control unit. ESP32 can be seen on the left and GPS on the right. The cap has two pieces to be printed easier.

IMG_20191103_162354_1_display.jpg

Figure 4: Another look into the control box with the 20x4 I2C LCD on top

IMG_20191112_215510_1_display.jpg

Figure 5: Display brighness slider

This is another sliding potentiometer connected directly to the display. Two switches are also visible here: One is used to turn off the control box and one is used for disengaging the motor (sets brake power to zero)

IMG_20191112_215530_1_display.jpg

Figure 6: Water protection for LAN connector and USB port for ESP32

IMG_20191111_205655_1_display.jpg

Figure 7: Rear box which accommodates the battery and motor controller. This view is the battery compartment

IMG_20191111_205621_display.jpg

Figure 8: Rear box with VESC (FSESC 4.12)

Code

The complete source code can be found on the github page. I included a touch sensor which puts the engine into complete brake mode for when I do not have enough time to turn tht throttle down. This was initially meant to be connected to the bike's brake handles but was not producing a good result. The rest is more or less documented in the code. Please do not hesitate to contact me if there is any question or improvement suggestion.

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <VescUart.h>
#include <TinyGPS++.h>
#include <PushButton.h>
#include <Smoothed.h>

//Inits
#define sw1 27
int sw1State, potPin = 34, throttle = 0, maxRPM = 16000, rpm = 0;
Smoothed <int> rpmRead;
// int rpmInt = 150, rpmSens = 50, prevRPM = 0;

// Safety init
bool safetyInit = false, safetyMax = false, safetyMin = false;

// Buttons
#define BUTTON_UP 12
#define BUTTON_MID 13
#define BUTTON_DOWN 14
#define LED 2

PushButton buttonUp(BUTTON_UP);
PushButton buttonMid(BUTTON_MID);
PushButton buttonDown(BUTTON_DOWN);

// Touchpad
#define TOUCH 33
Smoothed <int> touchValue;


// LCD SDA 21 SCL 22
int lcdCurPage = 1;
int lcdPages = 2;
LiquidCrystal_I2C lcd(0x27, 20, 4);
char line1[21], line2[21], line3[21], line4[21];

// Thermistor variables (installed on motor)
int ThermistorPin = 4, Vo;
float R1 = 10000, logR2, R2, T, Tc, Tf, c1 = 1.009249522e-03, c2 = 2.378405444e-04, c3 = 2.019202697e-07;
Smoothed <int> motorTemp;

// The TinyGPS++ object
TinyGPSPlus gps;
// GPS UART
#define RXD1 5
#define TXD1 18
bool gpsOn = false;
char gpsDateTime[21];
float longitude = 0.0000, latitude = 0.0000;
int gpsSats = 0, gpsAvailable = 0;
Smoothed <float> gpsSpeed;

// VESC UART
VescUart UART;
#define RXD2 35
#define TXD2 19
Smoothed <int> dRPM;
Smoothed <float> dC;
Smoothed <float> dP;
Smoothed <float> dV;
float dTv = 0.0, dTm = 0.0, dAhC = 0.0, dAh = 0.0, rpmTokmph = 0.00133333333333;
bool noBreak = false;


void setup() {
  // LCD
  lcd.init();
  lcd.backlight();
  Serial.begin(115200); // Serial terminal
  Serial2.begin(115200, SERIAL_8N1, RXD2, TXD2);  // UART VESC
  Serial1.begin(9600, SERIAL_8N1, RXD1); // GPS
  UART.setSerialPort(&Serial2);

  pinMode(TOUCH, INPUT);
  pinMode(ThermistorPin, INPUT);

  // switches
  pinMode(sw1, INPUT_PULLDOWN);
  pinMode(BUTTON_UP, INPUT_PULLDOWN); // Set button as input
  pinMode(BUTTON_MID, INPUT_PULLDOWN); // Set button as input
  pinMode(BUTTON_DOWN, INPUT_PULLDOWN); // Set button as input
  pinMode(LED, OUTPUT); // Set onboard LED as output

  // Smoothed values
  touchValue.begin(SMOOTHED_AVERAGE, 4);
  motorTemp.begin(SMOOTHED_AVERAGE, 20);
  rpmRead.begin(SMOOTHED_AVERAGE, 8);
  gpsSpeed.begin(SMOOTHED_AVERAGE, 10);
  dRPM.begin(SMOOTHED_AVERAGE, 15);
  dC.begin(SMOOTHED_AVERAGE, 15);
  dP.begin(SMOOTHED_AVERAGE, 15);
  dV.begin(SMOOTHED_AVERAGE, 15);
  dRPM.add(0);  // avoid a division error
}

void loop() {
  // Buttons
  buttonUp.update();
  buttonMid.update();
  buttonDown.update();
  
  // Safety startup
  safetyStart();
  
  // Thermistor
  tempCalculator();
  
  // Buttons
  buttonsFunc();
  
  // switches
  switchesFunc();

  // VESC read values
  vescRead();

  // Receive GPS values
  gpsFunc();

  // Set RPM values
  setRpm();
  
  // Print things on LCD
  processPage(lcdCurPage);
}

void safetyStart(){
  while (safetyInit == false) {
    throttle = map(analogRead(potPin), 0, 4095, 0, maxRPM);
    processPage(10);  
    // wait for max min on throttle
    if (safetyMax == false) {
      if (throttle > maxRPM - 100){safetyMax = true;}
    }
    else {
      if (throttle < 100){safetyMin = true;}
    }
    if (safetyMax == true && safetyMin == true){safetyInit = true;}
    delay(10);
  }
}

void tempCalculator(){
  Vo = analogRead(ThermistorPin);
  lcd.setCursor(14, 3);
  R2 = R1 * (4095.0 / (float)Vo - 1.0);
  logR2 = log(R2);
  T = (1.0 / (c1 + c2*logR2 + c3*logR2*logR2*logR2));
  Tc = T - 273.15;
  motorTemp.add(Tc);
}

void buttonsFunc(){
  if (buttonUp.isClicked()) // Click event
  {
    if (lcdCurPage == lcdPages){
      lcdCurPage = 1;
    }else{
      lcdCurPage++;
    }
  }
  if (buttonMid.isClicked()) // Click event
  {
    Serial.println("Button MIDDLE clicked!");
  }
  if (buttonDown.isClicked()) // Click event
  {
    if (lcdCurPage == 1){
      lcdCurPage = lcdPages;
    }else{
      lcdCurPage--;
    }
  }
  if (buttonUp.isActive() || buttonDown.isActive() || buttonMid.isActive())
  {
    digitalWrite(LED, HIGH);
  }
  else
  {
    digitalWrite(LED, LOW);
  }
}

void switchesFunc(){
  sw1State = digitalRead(sw1);

  if (sw1State == 1){
    noBreak = true;
  }else if (sw1State == 0){
    noBreak = false;
  }
}

void vescRead(){
  // Receive VESC values
  if ( UART.getVescValues()) {
    dRPM.add(UART.data.rpm);
    dV.add(UART.data.inputVoltage);
    dC.add(UART.data.avgMotorCurrent);
    dP.add(UART.data.inputVoltage*UART.data.avgInputCurrent);
    dTv = UART.data.fetTemp;
    dAh = UART.data.ampHours;
    dAhC = UART.data.ampHoursCharged;
  }
}

void setRpm(){
  touchValue.add(touchRead(TOUCH));
  throttle = analogRead(potPin);

  if (noBreak == true){
    UART.setBrakeCurrent(0);
    rpmRead.add(0);
  }
  else if (touchValue.get() < 10){
    rpmRead.add(0);
    UART.setRPM(rpmRead.get());
  }
  else {
    rpmRead.add(map(throttle, 0, 4095, 0, maxRPM));
    UART.setRPM(rpmRead.get());
  }
}

void gpsFunc(){
  while (Serial1.available() > 0){
    if (gps.encode(Serial1.read())){
      sprintf(gpsDateTime, "%04d.%02d.%02d  %02d:%02d:%02d", gps.date.year(), gps.date.month(), gps.date.day(), gps.time.hour(), gps.time.minute(), gps.time.second());
      longitude = gps.location.lng();
      latitude = gps.location.lat(); 
      gpsSats = gps.satellites.value();
      gpsAvailable = gps.location.isValid()?1:0;

      // Speed averaging
      gpsSpeed.add(gps.speed.kmph());
    }
  }
}

void printOnLcd(){
  lcd.setCursor(0, 0);
  lcd.print(line1);
  lcd.setCursor(0, 1);
  lcd.print(line2);
  lcd.setCursor(0, 2);
  lcd.print(line3);
  lcd.setCursor(0, 3);
  lcd.print(line4);
}

void processPage(int pageNum){
  switch (pageNum){
    case 1:
      sprintf(line1, "%-5s%-5s%-4s%-3s%-3s", "Volt", "Curr",  "Pow", "Tv", "Tm"); 
      sprintf(line2, "%-5s%-5s%-4s%-3s%3d", String(dV.get(), 1), String(dC.get(), 1), String(dP.get(), 0), String(dTv, 0), motorTemp.get()); 
      sprintf(line3, "%-4s%-4s%-4s%-4s%-4s", "RPM", "set", "vSp", "gSp", "Brea"); 
      sprintf(line4, "%-4s%-4s%-4s%-4s%-3s", String(dRPM.get()/100., 0), String(rpmRead.get()/100., 0), String((float)dRPM.get()*rpmTokmph, 0), String(gpsSpeed.get(), 0), noBreak? "OFF":"ON"); 
      break;
    case 2:
      sprintf(line1, "%-20s", gpsDateTime); 
      sprintf(line2, "%-3s %-7s %-7s", gpsAvailable? "GPS":"N/A", String(latitude, 4), String(longitude, 4)); 
      sprintf(line3, "%-5s%-5s%-5s%-3s", "mAh", "-mAh", "Sat", "Touch"); 
      sprintf(line4, "%-5s%-5s%-5d%3d", String(dAh*1000, 0), String(dAhC*1000, 0), gpsSats, touchValue.get()); 
      break;
    case 10:
      sprintf(line1, "%-12s", "SAFETY LOCK"); 
      sprintf(line2, "%-4s%-6s%-4s%-6s", "MAX", (String)safetyMax, "MIN", (String)safetyMin); 
      sprintf(line4, "Throttle: %-7s", String(rpm/(float)maxRPM*100, 0)); 
  }
  printOnLcd();
}

Created: 2019-11-16 Sat 12:44