DIY mountain e-Bike version 2
This is the second version of my DIY mountain e-bike I made almost two years ago.
1 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.
1.1 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.
1.2 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.
2 Libraries
The Arduino libraries I used in this version are:
2.1 LiquidCrystal (LCD)
2.2 My fork of VescUART
To show all the information transmitted from the VESC: VescUART
2.3 TinyGPSPlus
A GPS module can give you a RTC on top of GPS data: TinyGPSPlus
2.4 Smoothed
I used this for smoothing out the data from sensors and also throttle values: Smoothed
2.5 PushButton
A library for easy implementation of push buttons: PushButton
3 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 |
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.
5 Result
Here is the result. I am very happy with how it turned out.

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.

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.



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)



6 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;
<int> rpmRead;
Smoothed // 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
(BUTTON_UP);
PushButton buttonUp(BUTTON_MID);
PushButton buttonMid(BUTTON_DOWN);
PushButton buttonDown
// Touchpad
#define TOUCH 33
<int> touchValue;
Smoothed
// LCD SDA 21 SCL 22
int lcdCurPage = 1;
int lcdPages = 2;
(0x27, 20, 4);
LiquidCrystal_I2C lcdchar 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;
<int> motorTemp;
Smoothed
// 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;
<float> gpsSpeed;
Smoothed
// VESC UART
;
VescUart UART#define RXD2 35
#define TXD2 19
<int> dRPM;
Smoothed <float> dC;
Smoothed <float> dP;
Smoothed <float> dV;
Smoothed float dTv = 0.0, dTm = 0.0, dAhC = 0.0, dAh = 0.0, rpmTokmph = 0.00133333333333;
bool noBreak = false;
void setup() {
// LCD
.init();
lcd.backlight();
lcd.begin(115200); // Serial terminal
Serial.begin(115200, SERIAL_8N1, RXD2, TXD2); // UART VESC
Serial2.begin(9600, SERIAL_8N1, RXD1); // GPS
Serial1.setSerialPort(&Serial2);
UART
(TOUCH, INPUT);
pinMode(ThermistorPin, INPUT);
pinMode
// switches
(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
pinMode
// Smoothed values
.begin(SMOOTHED_AVERAGE, 4);
touchValue.begin(SMOOTHED_AVERAGE, 20);
motorTemp.begin(SMOOTHED_AVERAGE, 8);
rpmRead.begin(SMOOTHED_AVERAGE, 10);
gpsSpeed.begin(SMOOTHED_AVERAGE, 15);
dRPM.begin(SMOOTHED_AVERAGE, 15);
dC.begin(SMOOTHED_AVERAGE, 15);
dP.begin(SMOOTHED_AVERAGE, 15);
dV.add(0); // avoid a division error
dRPM}
void loop() {
// Buttons
.update();
buttonUp.update();
buttonMid.update();
buttonDown
// 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
(lcdCurPage);
processPage}
void safetyStart(){
while (safetyInit == false) {
= map(analogRead(potPin), 0, 4095, 0, maxRPM);
throttle (10);
processPage// 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;}
(10);
delay}
}
void tempCalculator(){
= analogRead(ThermistorPin);
Vo .setCursor(14, 3);
lcd= R1 * (4095.0 / (float)Vo - 1.0);
R2 = log(R2);
logR2 = (1.0 / (c1 + c2*logR2 + c3*logR2*logR2*logR2));
T = T - 273.15;
Tc .add(Tc);
motorTemp}
void buttonsFunc(){
if (buttonUp.isClicked()) // Click event
{
if (lcdCurPage == lcdPages){
= 1;
lcdCurPage }else{
++;
lcdCurPage}
}
if (buttonMid.isClicked()) // Click event
{
.println("Button MIDDLE clicked!");
Serial}
if (buttonDown.isClicked()) // Click event
{
if (lcdCurPage == 1){
= lcdPages;
lcdCurPage }else{
--;
lcdCurPage}
}
if (buttonUp.isActive() || buttonDown.isActive() || buttonMid.isActive())
{
(LED, HIGH);
digitalWrite}
else
{
(LED, LOW);
digitalWrite}
}
void switchesFunc(){
= digitalRead(sw1);
sw1State
if (sw1State == 1){
= true;
noBreak }else if (sw1State == 0){
= false;
noBreak }
}
void vescRead(){
// Receive VESC values
if ( UART.getVescValues()) {
.add(UART.data.rpm);
dRPM.add(UART.data.inputVoltage);
dV.add(UART.data.avgMotorCurrent);
dC.add(UART.data.inputVoltage*UART.data.avgInputCurrent);
dP= UART.data.fetTemp;
dTv = UART.data.ampHours;
dAh = UART.data.ampHoursCharged;
dAhC }
}
void setRpm(){
.add(touchRead(TOUCH));
touchValue= analogRead(potPin);
throttle
if (noBreak == true){
.setBrakeCurrent(0);
UART.add(0);
rpmRead}
else if (touchValue.get() < 10){
.add(0);
rpmRead.setRPM(rpmRead.get());
UART}
else {
.add(map(throttle, 0, 4095, 0, maxRPM));
rpmRead.setRPM(rpmRead.get());
UART}
}
void gpsFunc(){
while (Serial1.available() > 0){
if (gps.encode(Serial1.read())){
(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());
sprintf= gps.location.lng();
longitude = gps.location.lat();
latitude = gps.satellites.value();
gpsSats = gps.location.isValid()?1:0;
gpsAvailable
// Speed averaging
.add(gps.speed.kmph());
gpsSpeed}
}
}
void printOnLcd(){
.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);
lcd}
void processPage(int pageNum){
switch (pageNum){
case 1:
(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");
sprintfbreak;
case 2:
(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());
sprintfbreak;
case 10:
(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));
sprintf}
();
printOnLcd}