I'm not a skateboarder, but I have decent balance and can lay down code. So instead of learning to ride someone else's board, I built my own.
I wanted to be able to adjust my ride on the fly without having to pair up my phone, so I added a simple settings interface and utilized the existing switches.
My gryo filter uses a simple principle for smooth and reliable angle measurement. As the accelerometer jumps around quite a bit while in motion, I factored it's parameter depending how far off it was from the current gyro reading. The further the accelerometer was from the gyro, the less I mixed into the current angle routine. I then averaged the total output over the last ten readings with a bias closer to zero.
The drive routine is simple. Multiply power by current angle, then compound additional power when desired angle is reached. This helps to keep the board within the desired operational angle.
The overdrive works by adding additional power over time while the desired angle is maintained. This allows the board to continue to reach higher speeds while maintaining a comfortable ride angle.
My board is utilizing a 48 volt, 800 watt hub motor and a VESC for the drive train. Powered by a 12s2p Samsung 18650 25R battery pack, I get a little over 10kms of range, a comfortable cruising speed of 20km/h, and a top speed of just over 35km/h.
The controller is an Arduino Nano V3 paired with an MPU6050.
Total firmware size is 15082 bytes (49%) of program storage & 1154 bytes (56%) of dynamic memory. Eeprom storage is 28 bytes.
The board is 30 inches long and 9.5 inches wide. The aluminum railes are 1 inch wide, 2 inches tall, 1/8 inch wall thickness and 28 inches long with a 45 degree cut on each end.
Operating the Board
Safety First
Wrap yourself in a thick mattress before stepping on this board...
But seriously, wear a helmet and a well fitting pair of flat souled shoes.
Gloves, elbow and knee pads are also a smart idea.
Powering Up The Board
On power up, you must first wait for the counter to reach 0 and display the battery voltage. This step is the software calibrating the gyro offset.
The board must be in a still position for this to take place. If the board senses any movement, the counter will reset to 10 and restart the calibration. This process takes place everytime you power on the board.
Arming the Board
To arm the board, you must first place the board at a greater than 10 degree angle with the back on the ground.
You then must step on the back button or foot sensor, followed by the front button or foot sensor. The display will now show 1111 and then quickly to 0. To engage the motor, you must tilt the board to the level position.
The board will now be armed and in operation.
You must be standing on atleast one of the two buttons or foot sensors to maintain operation of the board. Due to the wait of the board, the breaks will immediately engage when both buttons are released to prevent the board from being a powered projectile.
Settings Menu
To enter the settings menu, hold down the front button or foot sensor for atleast 5 seconds then release. The rear button or foot sensor must not be pressed for this procedure.
To cycle through the menu, press and release the front button or foot sensor.
To alter a setting, press and release the back button or foot sensor.
To reset a value to its lowest setting, hold the back button or foot sensor for atleast 3 seconds and release.
To exit the menu and save your settings, hold down the front button or foot sensor again for atleast 5 seconds. Your settings are now saved and you will be shown the voltage displays.
Setting #1 (Way Mode)
This setting determines whether the board move only forward or in both directions.
1001 = 1 way. The board will only move in the forward direction. 1002 = 2 way. The board will move in both directions.
I like to place the board in 1 way for long journeys, and in 2 way for playing around.
Setting #2 (Center Point) (Degrees * 10)
This allows the user to set a custom center point. Angle the board to the desired angle and press and release the rear foot button/pad.
I like to set about 2 degrees to the rear foot so that I am level while cruising and the nose is a little higher when climbing bumps.
Settings #3 (Center Zone) (Degrees)
This sets desired cruising angle.
My setting is 7 (3007).
Setting #4 (Angle Power) (Multiplier * 10)
This is the power to apply for angle.
My setting is 3.5 (4035).
Setting #5 (Angle Retain Power) (Multiplier * 10)
This is the power multiplied by each degree over the Center Zone. This is added to the Angle Power.
This should be set high enough to make it difficult to angle beyond the Center Zone, but not to high as to force the board to jerk back and forth.
My setting is 1.5 (5015).
Setting #6 (Overdrive Angle) (Degrees)
This is the angle to start applying over drive too.
This is the value to start multiplying by each degree after Overdrive Angle.
This value should be set low enough that the board doesn't feel like it is kicking up when activated.
My Setting is 0.5 (7005).
Setting #8 (Overdrive Max) (Percent of Total Forward Throttle)
This is the maximum overdrive to add. This should not be set to high as to max out the forward throttle. If you max out the thottle, you will lose angle retention.
My setting is 30 (8030).
Setting #9 (Start Power) (Percent of Total Throttle * 10)
This is the initial power sent to the motor when past 0 in either direction.
Usually not needed for most motors, but lets say your motor required 2% power to start to turn under load, you would set a value of 2.0 (9020).
My setting is 0 (9000).
Setting #1 (Way Mode)
This setting determines whether the board move only forward or in both directions. I like to place the board in 1 way for long journeys, and in 2 way for playing around.
1001 = 1 way. The board will only move in the forward direction. 1002 = 2 way. The board will move in both directions.
Setting #2 (Max Power Mode) (Percent)
This setting determines how much of the 100% throttle to utilize in either direction.
Do not exceed 100 (2100)
Setting #3 (Center Zone) (Degrees)
This setting determines, in degrees, how far to tilt the board prior to utilizing the overdrive variable. This is your equal power to tilt zone.
I find a setting of 7 (3007) works best for me. The minimum setting is 5 as their has to be value less then the set value to allow the software to subtract incurred overdrive.
Setting #4 (Center Power) (Multiplied by 10)
This is the equal multiplier for standard power to angle ratio.
Ex: a setting of 30 (4030) will provide 3.0% duty to angle to drive the board. At 10 degrees of angle, the board will be attempting to drive the motor at 30% power.
Setting #5 (Overdrive Power) (Multiplied by 10)
This is the overdrive power variable. This will tell the board to add this value devided by 10 times how many degrees beyond the center zone to the overdrive variable.
Returning the board to an angle less than the center zone will subtract this value devided by 10 times how many degrees inside of the center zone from the overdrive variable.
The higher this setting, The more the board will feal like it is fighting you. This is by design as it naturally pushes the board to the center zone value. Holding the angle past the center zone will steadily increase the boards speed.
Be warned: The incurred overdrive value must be scrubbed before the board can be stopped in the level position. This is simply done by leaning back, but the more overdrive incurred the harder it will be to perform a sudden stop.
Personally I find 5 (5005) to be a good value for this setting.
Setting #6 (Start Power) (Percent) (Multiplied by 10)
This is the initial power sent to the motor when past 0 in either direction.
Usually not needed for most motors, but lets say your motor required 2% power to start to turn under load, you would set a value of 20 (6020).
Setting #7 (Kick Power) (Percent)
This is kick power in percent. This will cause the board to jump this value to the board once it reaches the Max Power Setting minus this value. This is designed to keep the board from traveling beyond the Max Power Setting.
Setting #8 (Factory Reset)
This is the factory reset settings. Hold the back button for atleast 3 secconds under this option to reset all values to their minimum values. The board will then save and exit the settings menu.
This setting is soon to be replaced by the set level option.
Specs & Hardware
Performance
Actual Range - 10 km
Top Speed - 36.7 km/h (Before I got to scared to go any faster.)
Full Charge from Empty - 3amp Charger - Between 2 and 2.5 hours.
Board Dimensions
Outer Dimensions
30 Inches Long
9.5 Inches Wide
0.5 Inches Thick
Wheel Cut Out
11 Inches long
6.5 Inches Wide
Located at exact center. Due to the location of the tire on the axel,
the tire will be slightly to the left side. As the tire valve
is on the right side, this leaves enough clearence. If you ride
Goofy Foot, you may want to mount the wheel in the opposite direction.
My setup utilizes 2 waterproof momentary switches mounted flush to act as the front and rear safety/settings switches. These switches are wired in pull-up configuration. I also utilize a Piezo Buzzer and a TM1637 display module.
Button One (Back Foot Button)
Utilizing a 4k7 resistor I bridge digital pin 6 to 5 volt positive, then wire the switch from pin 6 to ground when pressed.
Button Two (Front Foot Button)
Utilizing a 4k7 resistor I bridge digital pin 7 to 5 volt positive, then wire the switch from pin 7 to ground when pressed.
Piezo Buzzer
The buzzer I wire positive to digital pin 10 and negative to ground.
TM1637 Display
I wire VIN to 5 volt positive, GND to ground, CLK to digital pin 4, and DIO to digital pin 3.
MPU6050 Module
VCC to 5 volt positive, GND to ground, SCL to A5 (SCL), SDA to A4 (SDA).
More Pending...
Arduino Nano Code
Note: disconnect UART cable from VESC before programming Arduino, even if VESC is powered off.
Use these versions of the required libraries. Some have been modified to work better with the Arduino Nano.
Version 1.11 now released.
This new version is now smoother and has a couple of new settings available.
Note: Upgrading to this version will erase your current settings.
BoardApp1-11.ino
404: File Not Found
MPU6050x.ino
//range -32768 to 32767
#include "Wire.h" // This library allows you to communicate with I2C devices.
#define MPU_ADDR 0x68
#define GYROM 131
#define ACCM 182
double accFilter (double filterArray[], int filterSize, double &lastFValue, double value, int multiplier) {
double fMin = value;
double fMax = value;
for (int i = 0; i < filterSize; i++) {
filterArray[i] = filterArray[i + 1];
if (filterArray[i] < fMin) {fMin = filterArray[i];}
if (filterArray[i] > fMax) {fMax = filterArray[i];}
}
filterArray[filterSize] = value;
if (filterArray[filterSize] < fMin) {fMin = filterArray[filterSize];}
if (filterArray[filterSize] > fMax) {fMax = filterArray[filterSize];}
double fValue = (fMin + fMax) / 2;
if (fValue <= lastFValue + multiplier && fValue >= lastFValue - multiplier) {
if (fValue < 0 && lastFValue > fValue) {fValue = lastFValue;}
else if (fValue > 0 && lastFValue < fValue) {fValue = lastFValue;}
else {lastFValue = fValue;}
} else {lastFValue = fValue;}
return fValue;
}
void startMPUx () {
Wire.begin();
Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties
Wire.beginTransmission(MPU_ADDR); // Begins a transmission to the I2C slave (GY-521 board)
Wire.write(0x6B); // PWR_MGMT_1 register
Wire.write(0x00); // set to zero (wakes up the MPU-6050)
Wire.endTransmission(true);
}
void readMPUx () {
static long timer;
static bool gyroReady = false;
static double gyroXH, gyroXL, gyroXD, gyroXS;
static double gRoll; // Gyro
static double fRollA[9];
static double fRollL = 0;
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x3B); // starting with register 0x3B (ACCEL_XOUT_H) [MPU-6000 and MPU-6050 Register Map and Descriptions Revision 4.2, p.40]
Wire.endTransmission(false); // the parameter indicates that the Arduino will send a restart. As a result, the connection is kept active.
Wire.requestFrom(MPU_ADDR, 14, true); // request registers
const long cycleTime = timer; timer = millis(); const int cycle = timer - cycleTime; // Time Position
// "Wire.read()<<8 | Wire.read();" means two registers are read and stored in the same variable
const double accXT = Wire.read()<<8 | Wire.read(); // reading registers: 0x3B (ACCEL_XOUT_H) and 0x3C (ACCEL_XOUT_L)
const double accYT = Wire.read()<<8 | Wire.read(); // reading registers: 0x3D (ACCEL_YOUT_H) and 0x3E (ACCEL_YOUT_L)
const double accZT = Wire.read()<<8 | Wire.read(); // reading registers: 0x3F (ACCEL_ZOUT_H) and 0x40 (ACCEL_ZOUT_L)
const double tempT = Wire.read()<<8 | Wire.read(); // reading registers: 0x41 (TEMP_OUT_H) and 0x42 (TEMP_OUT_L)
const double gyroXT = Wire.read()<<8 | Wire.read(); // reading registers: 0x43 (GYRO_XOUT_H) and 0x44 (GYRO_XOUT_L)
const double gyroYT = Wire.read()<<8 | Wire.read(); // reading registers: 0x45 (GYRO_YOUT_H) and 0x46 (GYRO_YOUT_L)
const double gyroZT = Wire.read()<<8 | Wire.read(); // reading registers: 0x47 (GYRO_ZOUT_H) and 0x48 (GYRO_ZOUT_L)
const double aRoll = -atan2(accYT, sqrt((accXT * accXT) + (accZT * accZT))) * 223 / M_PI;
const double temp = tempT / 340 + 36.53;
if (gyroReady) {
const double gyroX = gyroXT - gyroXD;
gRoll += gyroX * cycle / GYROM / 1000;
double alphaM = 1;
if (gRoll > aRoll) {alphaM = gRoll - aRoll;}
else if (aRoll > gRoll) {alphaM = aRoll - gRoll;}
double alpha = 1 - (0.1 / alphaM);
if (alpha > 0.99) {alpha = 0.99;}
gRoll = gRoll * alpha + aRoll * (1 - alpha);
double fRoll = accFilter(fRollA, 9, fRollL, gRoll, 1);
newMpuData(fRoll, temp, cycle);
//displayNumber(fRoll * 10);
} else {
static double lastY;
static int sameCount = 0; //Gyro Calibration
if (sameCount == 1000) {
gyroXD = (gyroXH + gyroXL) / 2;
gyroXS = gyroXH - gyroXL;
gRoll = aRoll;
gyroReady = true;
} else if (lastY < aRoll - 2 || lastY > aRoll + 2) {
lastY = (int) aRoll;
gyroXH = gyroXT;
gyroXL = gyroXT;
gyroXS = 3;
sameCount = 0;
} else {
if (gyroXT > gyroXH) gyroXH = gyroXT;
if (gyroXT < gyroXL) gyroXL = gyroXT;
sameCount++;
displayNumber(10 - (sameCount / 100));
}
}
}
TM1637Display.cpp
// Author: avishorp@gmail.com
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
extern "C" {
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
}
#include "TM1637Display.h"
#include <Arduino.h>
#define TM1637_I2C_COMM1 0x40
#define TM1637_I2C_COMM2 0xC0
#define TM1637_I2C_COMM3 0x80
//
// A
// ---
// F | | B
// -G-
// E | | C
// ---
// D
const uint8_t digitToSegment[] = {
// XGFEDCBA
0b00111111, // 0
0b00000110, // 1
0b01011011, // 2
0b01001111, // 3
0b01100110, // 4
0b01101101, // 5
0b01111101, // 6
0b00000111, // 7
0b01111111, // 8
0b01101111, // 9
0b01110111, // A
0b01111100, // b
0b00111001, // C
0b01011110, // d
0b01111001, // E
0b01110001 // F
};
static const uint8_t minusSegments = 0b01000000;
TM1637Display::TM1637Display(uint8_t pinClk, uint8_t pinDIO, unsigned int bitDelay)
{
// Copy the pin numbers
m_pinClk = pinClk;
m_pinDIO = pinDIO;
m_bitDelay = bitDelay;
// Set the pin direction and default value.
// Both pins are set as inputs, allowing the pull-up resistors to pull them up
pinMode(m_pinClk, INPUT);
pinMode(m_pinDIO,INPUT);
digitalWrite(m_pinClk, LOW);
digitalWrite(m_pinDIO, LOW);
}
void TM1637Display::setBrightness(uint8_t brightness, bool on)
{
m_brightness = (brightness & 0x7) | (on? 0x08 : 0x00);
}
void TM1637Display::setSegments(const uint8_t segments[], uint8_t length, uint8_t pos)
{
// Write COMM1
start();
writeByte(TM1637_I2C_COMM1);
stop();
// Write COMM2 + first digit address
start();
writeByte(TM1637_I2C_COMM2 + (pos & 0x03));
// Write the data bytes
for (uint8_t k=0; k < length; k++)
writeByte(segments[k]);
stop();
// Write COMM3 + brightness
start();
writeByte(TM1637_I2C_COMM3 + (m_brightness & 0x0f));
stop();
}
void TM1637Display::clear()
{
uint8_t data[] = { 0, 0, 0, 0 };
setSegments(data);
}
void TM1637Display::showNumberDec(int num, bool leading_zero, uint8_t length, uint8_t pos)
{
showNumberDecEx(num, 0, leading_zero, length, pos);
}
void TM1637Display::showNumberDecEx(int num, uint8_t dots, bool leading_zero,
uint8_t length, uint8_t pos)
{
showNumberBaseEx(num < 0? -10 : 10, num < 0? -num : num, dots, leading_zero, length, pos);
}
void TM1637Display::showNumberHexEx(uint16_t num, uint8_t dots, bool leading_zero,
uint8_t length, uint8_t pos)
{
showNumberBaseEx(16, num, dots, leading_zero, length, pos);
}
void TM1637Display::showNumberBaseEx(int8_t base, uint16_t num, uint8_t dots, bool leading_zero,
uint8_t length, uint8_t pos)
{
bool negative = false;
if (base < 0) {
base = -base;
negative = true;
}
uint8_t digits[4];
if (num == 0 && !leading_zero) {
// Singular case - take care separately
for(uint8_t i = 0; i < (length-1); i++)
digits[i] = 0;
digits[length-1] = encodeDigit(0);
}
else {
//uint8_t i = length-1;
//if (negative) {
// // Negative number, show the minus sign
// digits[i] = minusSegments;
// i--;
//}
for(int i = length-1; i >= 0; --i)
{
uint8_t digit = num % base;
if (digit == 0 && num == 0 && leading_zero == false)
// Leading zero is blank
digits[i] = 0;
else
digits[i] = encodeDigit(digit);
if (digit == 0 && num == 0 && negative) {
digits[i] = minusSegments;
negative = false;
}
num /= base;
}
}
if(dots != 0)
{
showDots(dots, digits);
}
setSegments(digits, length, pos);
}
void TM1637Display::bitDelay()
{
delayMicroseconds(m_bitDelay);
}
void TM1637Display::start()
{
pinMode(m_pinDIO, OUTPUT);
bitDelay();
}
void TM1637Display::stop()
{
pinMode(m_pinDIO, OUTPUT);
bitDelay();
pinMode(m_pinClk, INPUT);
bitDelay();
pinMode(m_pinDIO, INPUT);
bitDelay();
}
bool TM1637Display::writeByte(uint8_t b)
{
uint8_t data = b;
// 8 Data Bits
for(uint8_t i = 0; i < 8; i++) {
// CLK low
pinMode(m_pinClk, OUTPUT);
bitDelay();
// Set data bit
if (data & 0x01)
pinMode(m_pinDIO, INPUT);
else
pinMode(m_pinDIO, OUTPUT);
bitDelay();
// CLK high
pinMode(m_pinClk, INPUT);
bitDelay();
data = data >> 1;
}
// Wait for acknowledge
// CLK to zero
pinMode(m_pinClk, OUTPUT);
pinMode(m_pinDIO, INPUT);
bitDelay();
// CLK to high
pinMode(m_pinClk, INPUT);
bitDelay();
uint8_t ack = digitalRead(m_pinDIO);
if (ack == 0)
pinMode(m_pinDIO, OUTPUT);
bitDelay();
pinMode(m_pinClk, OUTPUT);
bitDelay();
return ack;
}
void TM1637Display::showDots(uint8_t dots, uint8_t* digits)
{
for(int i = 0; i < 4; ++i)
{
digits[i] |= (dots & 0x80);
dots <<= 1;
}
}
uint8_t TM1637Display::encodeDigit(uint8_t digit)
{
return digitToSegment[digit & 0x0f];
}
TM1637Display.h
// Author: avishorp@gmail.com
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#ifndef __TM1637DISPLAY__
#define __TM1637DISPLAY__
#include <inttypes.h>
#define SEG_A 0b00000001
#define SEG_B 0b00000010
#define SEG_C 0b00000100
#define SEG_D 0b00001000
#define SEG_E 0b00010000
#define SEG_F 0b00100000
#define SEG_G 0b01000000
#define SEG_DP 0b10000000
#define DEFAULT_BIT_DELAY 100
class TM1637Display {
public:
//! Initialize a TM1637Display object, setting the clock and
//! data pins.
//!
//! @param pinClk - The number of the digital pin connected to the clock pin of the module
//! @param pinDIO - The number of the digital pin connected to the DIO pin of the module
//! @param bitDelay - The delay, in microseconds, between bit transition on the serial
//! bus connected to the display
TM1637Display(uint8_t pinClk, uint8_t pinDIO, unsigned int bitDelay = DEFAULT_BIT_DELAY);
//! Sets the brightness of the display.
//!
//! The setting takes effect when a command is given to change the data being
//! displayed.
//!
//! @param brightness A number from 0 (lowes brightness) to 7 (highest brightness)
//! @param on Turn display on or off
void setBrightness(uint8_t brightness, bool on = true);
//! Display arbitrary data on the module
//!
//! This function receives raw segment values as input and displays them. The segment data
//! is given as a byte array, each byte corresponding to a single digit. Within each byte,
//! bit 0 is segment A, bit 1 is segment B etc.
//! The function may either set the entire display or any desirable part on its own. The first
//! digit is given by the @ref pos argument with 0 being the leftmost digit. The @ref length
//! argument is the number of digits to be set. Other digits are not affected.
//!
//! @param segments An array of size @ref length containing the raw segment values
//! @param length The number of digits to be modified
//! @param pos The position from which to start the modification (0 - leftmost, 3 - rightmost)
void setSegments(const uint8_t segments[], uint8_t length = 4, uint8_t pos = 0);
//! Clear the display
void clear();
//! Display a decimal number
//!
//! Dispaly the given argument as a decimal number.
//!
//! @param num The number to be shown
//! @param leading_zero When true, leading zeros are displayed. Otherwise unnecessary digits are
//! blank. NOTE: leading zero is not supported with negative numbers.
//! @param length The number of digits to set. The user must ensure that the number to be shown
//! fits to the number of digits requested (for example, if two digits are to be displayed,
//! the number must be between 0 to 99)
//! @param pos The position of the most significant digit (0 - leftmost, 3 - rightmost)
void showNumberDec(int num, bool leading_zero = false, uint8_t length = 4, uint8_t pos = 0);
//! Display a decimal number, with dot control
//!
//! Dispaly the given argument as a decimal number. The dots between the digits (or colon)
//! can be individually controlled.
//!
//! @param num The number to be shown
//! @param dots Dot/Colon enable. The argument is a bitmask, with each bit corresponding to a dot
//! between the digits (or colon mark, as implemented by each module). i.e.
//! For displays with dots between each digit:
//! * 0.000 (0b10000000)
//! * 00.00 (0b01000000)
//! * 000.0 (0b00100000)
//! * 0.0.0.0 (0b11100000)
//! For displays with just a colon:
//! * 00:00 (0b01000000)
//! For displays with dots and colons colon:
//! * 0.0:0.0 (0b11100000)
//! @param leading_zero When true, leading zeros are displayed. Otherwise unnecessary digits are
//! blank. NOTE: leading zero is not supported with negative numbers.
//! @param length The number of digits to set. The user must ensure that the number to be shown
//! fits to the number of digits requested (for example, if two digits are to be displayed,
//! the number must be between 0 to 99)
//! @param pos The position of the most significant digit (0 - leftmost, 3 - rightmost)
void showNumberDecEx(int num, uint8_t dots = 0, bool leading_zero = false, uint8_t length = 4, uint8_t pos = 0);
//! Display a hexadecimal number, with dot control
//!
//! Dispaly the given argument as a hexadecimal number. The dots between the digits (or colon)
//! can be individually controlled.
//!
//! @param num The number to be shown
//! @param dots Dot/Colon enable. The argument is a bitmask, with each bit corresponding to a dot
//! between the digits (or colon mark, as implemented by each module). i.e.
//! For displays with dots between each digit:
//! * 0.000 (0b10000000)
//! * 00.00 (0b01000000)
//! * 000.0 (0b00100000)
//! * 0.0.0.0 (0b11100000)
//! For displays with just a colon:
//! * 00:00 (0b01000000)
//! For displays with dots and colons colon:
//! * 0.0:0.0 (0b11100000)
//! @param leading_zero When true, leading zeros are displayed. Otherwise unnecessary digits are
//! blank
//! @param length The number of digits to set. The user must ensure that the number to be shown
//! fits to the number of digits requested (for example, if two digits are to be displayed,
//! the number must be between 0 to 99)
//! @param pos The position of the most significant digit (0 - leftmost, 3 - rightmost)
void showNumberHexEx(uint16_t num, uint8_t dots = 0, bool leading_zero = false, uint8_t length = 4, uint8_t pos = 0);
//! Translate a single digit into 7 segment code
//!
//! The method accepts a number between 0 - 15 and converts it to the
//! code required to display the number on a 7 segment display.
//! Numbers between 10-15 are converted to hexadecimal digits (A-F)
//!
//! @param digit A number between 0 to 15
//! @return A code representing the 7 segment image of the digit (LSB - segment A;
//! bit 6 - segment G; bit 7 - always zero)
uint8_t encodeDigit(uint8_t digit);
protected:
void bitDelay();
void start();
void stop();
bool writeByte(uint8_t b);
void showDots(uint8_t dots, uint8_t* digits);
void showNumberBaseEx(int8_t base, uint16_t num, uint8_t dots = 0, bool leading_zero = false, uint8_t length = 4, uint8_t pos = 0);
private:
uint8_t m_pinClk;
uint8_t m_pinDIO;
uint8_t m_brightness;
unsigned int m_bitDelay;
};
#endif // __TM1637DISPLAY__
VescUart.cpp
#include "VescUart.h"
#include <HardwareSerial.h>
void VescUart::setSerialPort(HardwareSerial* port) {serialPort = port;}
int VescUart::receiveUartMessage(uint8_t * payloadReceived) {
// Messages <= 255 starts with "2", 2nd byte is length
// Messages > 255 starts with "3" 2nd and 3rd byte is length combined with 1st >>8 and then &0xFF
uint16_t counter = 0;
uint16_t endMessage = 256;
bool messageRead = false;
uint8_t messageReceived[256];
uint16_t lenPayload = 0;
//uint32_t timeout = millis() + 100; // Defining the timestamp for timeout (100ms before timeout)
//while ( millis() < timeout && messageRead == false) {
while ( messageRead == false) {
while (serialPort->available()) {
messageReceived[counter++] = serialPort->read();
if (counter == 2) {
switch (messageReceived[0])
{
case 2:
endMessage = messageReceived[1] + 5; //Payload size + 2 for sice + 3 for SRC and End.
lenPayload = messageReceived[1];
break;
case 3: break; // ToDo: Add Message Handling > 255 (starting with 3)
default: break;
}
}
if (counter >= sizeof(messageReceived)) break;
if (counter == endMessage && messageReceived[endMessage - 1] == 3) {
messageReceived[endMessage] = 0;
messageRead = true;
break; // Exit if end of message is reached, even if there is still more data in the buffer.
}
}
}
bool unpacked = false;
if (messageRead) {unpacked = unpackPayload(messageReceived, endMessage, payloadReceived);}
if (unpacked) {return lenPayload;} // Message was read
else {return 0;} // No Message Read
}
bool VescUart::unpackPayload(uint8_t * message, int lenMes, uint8_t * payload) {
uint16_t crcMessage = 0;
uint16_t crcPayload = 0;
// Rebuild crc:
crcMessage = message[lenMes - 3] << 8;
crcMessage &= 0xFF00;
crcMessage += message[lenMes - 2];
// Extract payload:
memcpy(payload, &message[2], message[1]);
crcPayload = crc16(payload, message[1]);
if (crcPayload == crcMessage) {return true;} else {return false;}
}
int VescUart::packSendPayload(uint8_t * payload, int lenPay) {
uint16_t crcPayload = crc16(payload, lenPay);
int count = 0;
uint8_t messageSend[256];
if (lenPay <= 256) {
messageSend[count++] = 2;
messageSend[count++] = lenPay;
} else {
messageSend[count++] = 3;
messageSend[count++] = (uint8_t)(lenPay >> 8);
messageSend[count++] = (uint8_t)(lenPay & 0xFF);
}
memcpy(&messageSend[count], payload, lenPay);
count += lenPay;
messageSend[count++] = (uint8_t)(crcPayload >> 8);
messageSend[count++] = (uint8_t)(crcPayload & 0xFF);
messageSend[count++] = 3;
messageSend[count] = '\0';
// Sending package
serialPort->write(messageSend, count);
// Returns number of send bytes
return count;
}
bool VescUart::processReadPacket(uint8_t * message) {
COMM_PACKET_ID packetId;
int32_t ind = 0;
packetId = (COMM_PACKET_ID)message[0];
message++; // Removes the packetId from the actual message (payload)
switch (packetId){
case COMM_GET_VALUES: // Structure defined here: https://github.com/vedderb/bldc/blob/43c3bbaf91f5052a35b75c2ff17b5fe99fad94d1/commands.c#L164
ind = 4; // Skip the first 4 bytes
data.avgMotorCurrent = buffer_get_float32(message, 100.0, &ind);
data.avgInputCurrent = buffer_get_float32(message, 100.0, &ind);
ind += 8; // Skip the next 8 bytes
data.dutyCycleNow = buffer_get_float16(message, 1000.0, &ind);
data.rpm = buffer_get_int32(message, &ind);
data.inpVoltage = buffer_get_float16(message, 10.0, &ind);
return true;
break;
default:
return false;
break;
}
}
bool VescUart::getVescValues(void) {
uint8_t command[1] = { COMM_GET_VALUES };
uint8_t payload[256];
packSendPayload(command, 1);
// delay(1); //needed, otherwise data is not read
int lenPayload = receiveUartMessage(payload);
if (lenPayload > 55) {
bool read = processReadPacket(payload); //returns true if sucessful
return read;
}
else
{
return false;
}
}
void VescUart::setDuty(float duty) {
int32_t index = 0;
uint8_t payload[5];
payload[index++] = COMM_SET_DUTY;
buffer_append_int32(payload, (int32_t)(duty * 100000), &index);
packSendPayload(payload, 5);
}
void VescUart::setCurrent(float current) {
int32_t index = 0;
uint8_t payload[5];
payload[index++] = COMM_SET_CURRENT;
buffer_append_int32(payload, (int32_t)(current * 1000), &index);
packSendPayload(payload, 5);
}
void VescUart::setBrakeCurrent(float brakeCurrent) {
int32_t index = 0;
uint8_t payload[5];
payload[index++] = COMM_SET_CURRENT_BRAKE;
buffer_append_int32(payload, (int32_t)(brakeCurrent * 1000), &index);
packSendPayload(payload, 5);
}
VescUart.h
#ifndef _VESCUART_h
#define _VESCUART_h
#include <Arduino.h>
#include "datatypes.h"
#include "buffer.h"
#include "crc.h"
class VescUart
{
/** Struct to store the telemetry data returned by the VESC */
struct dataPackage {
float avgMotorCurrent;
float avgInputCurrent;
float dutyCycleNow;
long rpm;
float inpVoltage;
};
public:
/**
* @brief Class constructor
*/
/** Variabel to hold measurements returned from VESC */
dataPackage data;
/**
* @brief Set the serial port for uart communication
* @param port - Reference to Serial port (pointer)
*/
void setSerialPort(HardwareSerial* port);
/**
* @brief Set the serial port for debugging
* @param port - Reference to Serial port (pointer)
*/
/**
* @brief Sends a command to VESC and stores the returned data
*
* @return True if successfull otherwise false
*/
bool getVescValues(void);
/**
* @brief Sends values for joystick and buttons to the nunchuck app
*/
void setNunchuckValues(void);
/**
* @brief Set the current to drive the motor
* @param current - The current to apply
*/
void setCurrent(float current);
/**
* @brief Set the current to brake the motor
* @param brakeCurrent - The current to apply
*/
void setBrakeCurrent(float brakeCurrent);
/**
* @brief Set the rpm of the motor
* @param rpm - The desired RPM (actually eRPM = RPM * poles)
*/
void setRPM(float rpm);
/**
* @brief Set the duty of the motor
* @param duty - The desired duty (0.0-1.0)
*/
void setDuty(float duty);
/**
* @brief Help Function to print struct dataPackage over Serial for Debug
*/
void printVescValues(void);
private:
/** Variabel to hold the reference to the Serial object to use for UART */
HardwareSerial* serialPort = NULL;
/** Variabel to hold the reference to the Serial object to use for debugging.
* Uses the class Stream instead of HarwareSerial */
Stream* debugPort = NULL;
/**
* @brief Packs the payload and sends it over Serial
*
* @param payload - The payload as a unit8_t Array with length of int lenPayload
* @param lenPay - Length of payload
* @return The number of bytes send
*/
int packSendPayload(uint8_t * payload, int lenPay);
/**
* @brief Receives the message over Serial
*
* @param payloadReceived - The received payload as a unit8_t Array
* @return The number of bytes receeived within the payload
*/
int receiveUartMessage(uint8_t * payloadReceived);
/**
* @brief Verifies the message (CRC-16) and extracts the payload
*
* @param message - The received UART message
* @param lenMes - The lenght of the message
* @param payload - The final payload ready to extract data from
* @return True if the process was a success
*/
bool unpackPayload(uint8_t * message, int lenMes, uint8_t * payload);
/**
* @brief Extracts the data from the received payload
*
* @param message - The payload to extract data from
* @return True if the process was a success
*/
bool processReadPacket(uint8_t * message);
/**
* @brief Help Function to print uint8_t array over Serial for Debug
*
* @param data - Data array to print
* @param len - Lenght of the array to print
*/
void serialPrint(uint8_t * data, int len);
};
#endif
buffer.cpp
/*
Copyright 2012-2014 Benjamin Vedder benjamin@vedder.se
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* buffer.c
*
* Created on: 13 maj 2013
* Author: benjamin
*/
#include "buffer.h"
void buffer_append_int16(uint8_t* buffer, int16_t number, int32_t *index) {
buffer[(*index)++] = number >> 8;
buffer[(*index)++] = number;
}
void buffer_append_uint16(uint8_t* buffer, uint16_t number, int32_t *index) {
buffer[(*index)++] = number >> 8;
buffer[(*index)++] = number;
}
void buffer_append_int32(uint8_t* buffer, int32_t number, int32_t *index) {
buffer[(*index)++] = number >> 24;
buffer[(*index)++] = number >> 16;
buffer[(*index)++] = number >> 8;
buffer[(*index)++] = number;
}
void buffer_append_uint32(uint8_t* buffer, uint32_t number, int32_t *index) {
buffer[(*index)++] = number >> 24;
buffer[(*index)++] = number >> 16;
buffer[(*index)++] = number >> 8;
buffer[(*index)++] = number;
}
void buffer_append_float16(uint8_t* buffer, float number, float scale, int32_t *index) {
buffer_append_int16(buffer, (int16_t)(number * scale), index);
}
void buffer_append_float32(uint8_t* buffer, float number, float scale, int32_t *index) {
buffer_append_int32(buffer, (int32_t)(number * scale), index);
}
int16_t buffer_get_int16(const uint8_t *buffer, int32_t *index) {
int16_t res = ((uint16_t) buffer[*index]) << 8 |
((uint16_t) buffer[*index + 1]);
*index += 2;
return res;
}
uint16_t buffer_get_uint16(const uint8_t *buffer, int32_t *index) {
uint16_t res = ((uint16_t) buffer[*index]) << 8 |
((uint16_t) buffer[*index + 1]);
*index += 2;
return res;
}
int32_t buffer_get_int32(const uint8_t *buffer, int32_t *index) {
int32_t res = ((uint32_t) buffer[*index]) << 24 |
((uint32_t) buffer[*index + 1]) << 16 |
((uint32_t) buffer[*index + 2]) << 8 |
((uint32_t) buffer[*index + 3]);
*index += 4;
return res;
}
uint32_t buffer_get_uint32(const uint8_t *buffer, int32_t *index) {
uint32_t res = ((uint32_t) buffer[*index]) << 24 |
((uint32_t) buffer[*index + 1]) << 16 |
((uint32_t) buffer[*index + 2]) << 8 |
((uint32_t) buffer[*index + 3]);
*index += 4;
return res;
}
float buffer_get_float16(const uint8_t *buffer, float scale, int32_t *index) {
return (float)buffer_get_int16(buffer, index) / scale;
}
float buffer_get_float32(const uint8_t *buffer, float scale, int32_t *index) {
return (float)buffer_get_int32(buffer, index) / scale;
}
bool buffer_get_bool(const uint8_t *buffer, int32_t *index) {
if (buffer[*index] == 1)
{
index++;
return true;
}
else
{
index++;
return false;
}
}
void buffer_append_bool(uint8_t *buffer,bool value, int32_t *index) {
if (value == true)
{
buffer[*index] = 1;
(*index)++;
}
else
{
buffer[*index] = 0;
(*index)++;
}
}
buffer.h
/*
Copyright 2012-2014 Benjamin Vedder benjamin@vedder.se
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* buffer.h
*
* Created on: 13 maj 2013
* Author: benjamin
*/
#ifndef BUFFER_H_
#define BUFFER_H_
#include <stdint.h>
void buffer_append_int16(uint8_t* buffer, int16_t number, int32_t *index);
void buffer_append_uint16(uint8_t* buffer, uint16_t number, int32_t *index);
void buffer_append_int32(uint8_t* buffer, int32_t number, int32_t *index);
void buffer_append_uint32(uint8_t* buffer, uint32_t number, int32_t *index);
void buffer_append_float16(uint8_t* buffer, float number, float scale, int32_t *index);
void buffer_append_float32(uint8_t* buffer, float number, float scale, int32_t *index);
int16_t buffer_get_int16(const uint8_t *buffer, int32_t *index);
uint16_t buffer_get_uint16(const uint8_t *buffer, int32_t *index);
int32_t buffer_get_int32(const uint8_t *buffer, int32_t *index);
uint32_t buffer_get_uint32(const uint8_t *buffer, int32_t *index);
float buffer_get_float16(const uint8_t *buffer, float scale, int32_t *index);
float buffer_get_float32(const uint8_t *buffer, float scale, int32_t *index);
bool buffer_get_bool(const uint8_t *buffer, int32_t *index);
void buffer_append_bool(uint8_t *buffer,bool value, int32_t *index);
#endif /* BUFFER_H_ */
crc.cpp
/*
Copyright 2012-2014 Benjamin Vedder benjamin@vedder.se
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* crc.c
*
* Created on: 26 feb 2012
* Author: benjamin
*/
#include "crc.h"
// CRC Table
const unsigned short crc16_tab[] = { 0x0000, 0x1021, 0x2042, 0x3063, 0x4084,
0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad,
0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7,
0x62d6, 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a,
0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672,
0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719,
0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7,
0x0840, 0x1861, 0x2802, 0x3823, 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948,
0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50,
0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b,
0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97,
0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe,
0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca,
0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3,
0x5004, 0x4025, 0x7046, 0x6067, 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d,
0xd31c, 0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214,
0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c,
0xc50d, 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3,
0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d,
0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806,
0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e,
0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1,
0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b,
0x9de8, 0x8dc9, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0,
0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 };
unsigned short crc16(unsigned char *buf, unsigned int len) {
unsigned int i;
unsigned short cksum = 0;
for (i = 0; i < len; i++) {
cksum = crc16_tab[(((cksum >> 8) ^ *buf++) & 0xFF)] ^ (cksum << 8);
}
return cksum;
}
crc.h
/*
Copyright 2012-2014 Benjamin Vedder benjamin@vedder.se
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* crc.h
*
* Created on: 26 feb 2012
* Author: benjamin
*/
#ifndef CRC_H_
#define CRC_H_
/*
* Functions
*/
unsigned short crc16(unsigned char *buf, unsigned int len);
#endif /* CRC_H_ */
Base Code For Micro Projects (No Display, One Arm Switch/Pad)
This is Firmware Version 1.11 cut down for smaller projects.
The board will beep when the Gyro is calibrated and beep again when Armed. During operation, the board will beep when 85% of max speed is reached in either direction.
Requires a piezo buzzer. (The one found in most PCs or are provided with new computer cases.)
Adjust your settings at the top of the code. Add your drive routine near the bottom inside the motorRoutine routine.