![](https://habrastorage.org/getpro/habr/post_images/422/d3d/46a/422d3d46a7721f459ce5757470b35b04.png)
Здравствуй, Хабр! Мне хотелось бы поделиться историей о создании велокопомпьютера своими руками. После покупки велосипеда я решил как-то фиксировать свой прогресс, но мобильные трекеры оказались не самым удобным решением, а из Китая ко мне уже летел заранее заказанный там BOGEER 823, что делало покупку на месте бессмысленной. Поэтому я решил попытаться сделать велокомп своими руками, заодно заложив в него основу для расширения: управление обвесом велосипеда (передний и задний свет, звонок).
Немного подготовки
В основе любого велокомпьютера — геркон, который фиксирует оборот колеса, а всё остальное это элементарная математика… Конечно, я поискал в интернете готовые реализации и формулы, которые мне понадобятся.
Как я уже писал, всё завязано на геркон: магнит зафиксирован на спице, а сам геркон на «вилке». Когда магнит на вилке замыкает геркон — это значит, что колесо сделало полный оборот и велосипед проехал расстояние равное:
2*Pi*Rшины
Первая версия
Прежде всего я составил список того, что мне было нужно от велокомпьютера:
- Текущая скорость
- Дистанция текущей поездки
- Дистанция всех поездок
- Время в поездке
- Текущее время
- Подсветка
- Возможность обмена данным с компьютером через карту памяти
К счастью, все нужные модули были под рукой:
- Arduino nano
- Nokia 5110 LCD (синяя подсветка, через GND)
- Распаянные часы DS1302
- Модуль SD карт
Я быстро собрал бутерброд: сверху экран, вторым слоем SD модуль и сама ардуинка, а часы под всем этим. Получилось довольно компактно. Конечно, если бы я использовал не готовые модули, а «рассыпуху» и травил платы, можно было бы выиграть много места.
Подключив геркон, я начал тестировать первую версию скетча, который базировался на раздобытых из сети скетча для работы с дисплеем и англоязычном спидометре.
Убедившись в работоспособности конструкции, я решил провести первый тест. Питание я планировал от нескольких АА батареек или аккумулятора, но в тот момент под рукой их не оказалось… Поверьте, кататься на велосипеде с ноутбуком под мышкой то ещё удовольствие.
Тестирование принесло свои плоды. Я обнаружил ошибку в логике: сигнал от геркона всегда приравнивался к обороту колеса (если прошло минимальное время на оборот). Вроде, всё правильно, но если остановиться, зафиксировав магнит напротив геркона, то программа считала, что вы едете очень быстро.
Вторая версия
Немного доработав программу, я собрал «блок питания»: набор из пяти АА батареек. До этого я попытался использовать крону, но её для Ардуинки с обвесом оказалось мало.
Так как мой 5110-й был на красной подложке, а значит, подсветка в нём управлялась логическим нулём, а не единицей, от программного управления через аналоговый порт пришлось отказаться и добавить простую кнопку.
Кроме того, аналоговый порт без данных выдавал случайные значения, но это решилось простым резистором. А вот другой сюрприз от аналогового порта заставил меня поломать голову: значение замыкания геркона при питании от батареек были не такими, как при использовании USB порта.
Второй тест показал уже полную работоспособность системы. Единственной проблемой стала ошибка в расчётах: радиус «26 дюймового колеса» оказался «13.5» дюймов. В результате одометр немного врал (спасибо Яндекс.Картам за удобный инструмент для расчёта расстояний).
Третья версия
Устройство работало, но конечно хотелось придать законченность внешнему виду. Так уж получилось, что всё что мне удалось найти — только один корпус, и он оказался «узким».
Пришлось изменить расположение компонентов: в корпус всё влезло, но итоговый размер стал больше. В процессе перепайки пришлось заменить Arduino Nano и SD модуль: паяльником я владею так себе и часть выводов просто привёл в негодность.
Размеры: 72x50x28mm
После того, как я проверил работоспособность всех модулей, я решил зафиксировать их эпоксидным клеем для большей устойчивости к тряскам.
Моя торопливость сыграла злую шутку: я недостаточно хорошо зафиксировал провода под экраном и один из них стал пускать помехи (на самом деле это влияние на параметр «контрастность»).
Ещё пара исправлений в коде, и интерфейс был приведён к финальному (на текущий момент) виду.
Устройство в сборе
Неприятность преподнесли часы: один из контактов отошёл, и время стало скакать (как если бы я перепутал RST и CLK). Но что самое интересное — после поездки часы пришли в норму.
Пожалуй, претензию можно предъявить к углу крепления: во время поездки смотреть неудобно.
Да и способ крепления изолентой неидеален, но у меня не получилось найти отдельно кронштейн для велосипедных гаджетов. Хотя, из Китая ко мне едет паучок. Думаю, что он вполне подойдёт для решения данной проблемы.
Стоимость
- Корпус BOX-G020 — 115р
- Эпоксилин — 95р
- Батарейки — 60р
- Arduino Nano — 120р
- Nokia LCD 5110 — 102р
- Чёрная изолента — 40р
- SD module — 187р
- DS1302 — 70р
- Кейсы для батареек — 120р
- Провода и резисторы — 80р
- Геркон + магнит — 82р
Итого: ~1100р
Ссылки по теме
Итоги
Я получил ещё немного опыта работы с Arduino и сборки небольших работающих устройств. Весело провёл несколько часов, но что самое главное — теперь у меня есть работающий велокомпьютер. В планах написать нормальное сохранение/загрузку «глобальных» данных на карту памяти и добавить кнопку для отображения «глобальной» статистики. Для управления внешними устройствами у меня осталось шесть аналоговых портов (три кнопки & три тиристора) и где-то затерялся один цифровой, например, для дальномера (парктроник) или градусника. Очень хотелось бы решить проблему с питанием, ибо пять АА батареек на раме велосипеда не очень смотрятся.
Код скетча
#include <SD.h>
#include <DS1302.h>
//Function: This procedure applies to the Arduino driver NOKIA 5110 LCD.
//Time:September 4,2012
#define PIN_SCE 3
#define PIN_RESET 2
#define PIN_DC 4
#define PIN_SDIN 5
#define PIN_SCLK 6
#define reed A0//
#define LCD_C LOW
#define LCD_D HIGH
int count=0;
char dat[4];
char disp_tab[]={
'0','1','2','3','4','5','6','7','8','9'};
#define LCD_X 84
#define LCD_Y 48
const int chipSelect = 10;
static const byte ASCII[][5] =
{
{
0x00, 0x00, 0x00, 0x00, 0x00 } // 20
,{
0x00, 0x00, 0x5f, 0x00, 0x00 } // 21 !
,{
0x00, 0x07, 0x00, 0x07, 0x00 } // 22 "
,{
0x14, 0x7f, 0x14, 0x7f, 0x14 } // 23 #
,{
0x24, 0x2a, 0x7f, 0x2a, 0x12 } // 24 $
,{
0x23, 0x13, 0x08, 0x64, 0x62 } // 25 %
,{
0x36, 0x49, 0x55, 0x22, 0x50 } // 26 &
,{
0x00, 0x05, 0x03, 0x00, 0x00 } // 27 '
,{
0x00, 0x1c, 0x22, 0x41, 0x00 } // 28 (
,{
0x00, 0x41, 0x22, 0x1c, 0x00 } // 29 )
,{
0x14, 0x08, 0x3e, 0x08, 0x14 } // 2a *
,{
0x08, 0x08, 0x3e, 0x08, 0x08 } // 2b +
,{
0x00, 0x50, 0x30, 0x00, 0x00 } // 2c ,
,{
0x08, 0x08, 0x08, 0x08, 0x08 } // 2d -
,{
0x00, 0x60, 0x60, 0x00, 0x00 } // 2e .
,{
0x20, 0x10, 0x08, 0x04, 0x02 } // 2f /
,{
0x3e, 0x51, 0x49, 0x45, 0x3e } // 30 0
,{
0x00, 0x42, 0x7f, 0x40, 0x00 } // 31 1
,{
0x42, 0x61, 0x51, 0x49, 0x46 } // 32 2
,{
0x21, 0x41, 0x45, 0x4b, 0x31 } // 33 3
,{
0x18, 0x14, 0x12, 0x7f, 0x10 } // 34 4
,{
0x27, 0x45, 0x45, 0x45, 0x39 } // 35 5
,{
0x3c, 0x4a, 0x49, 0x49, 0x30 } // 36 6
,{
0x01, 0x71, 0x09, 0x05, 0x03 } // 37 7
,{
0x36, 0x49, 0x49, 0x49, 0x36 } // 38 8
,{
0x06, 0x49, 0x49, 0x29, 0x1e } // 39 9
,{
0x00, 0x36, 0x36, 0x00, 0x00 } // 3a :
,{
0x00, 0x56, 0x36, 0x00, 0x00 } // 3b ;
,{
0x08, 0x14, 0x22, 0x41, 0x00 } // 3c <
,{
0x14, 0x14, 0x14, 0x14, 0x14 } // 3d =
,{
0x00, 0x41, 0x22, 0x14, 0x08 } // 3e >
,{
0x02, 0x01, 0x51, 0x09, 0x06 } // 3f ?
,{
0x32, 0x49, 0x79, 0x41, 0x3e } // 40 @
,{
0x7e, 0x11, 0x11, 0x11, 0x7e } // 41 A
,{
0x7f, 0x49, 0x49, 0x49, 0x36 } // 42 B
,{
0x3e, 0x41, 0x41, 0x41, 0x22 } // 43 C
,{
0x7f, 0x41, 0x41, 0x22, 0x1c } // 44 D
,{
0x7f, 0x49, 0x49, 0x49, 0x41 } // 45 E
,{
0x7f, 0x09, 0x09, 0x09, 0x01 } // 46 F
,{
0x3e, 0x41, 0x49, 0x49, 0x7a } // 47 G
,{
0x7f, 0x08, 0x08, 0x08, 0x7f } // 48 H
,{
0x00, 0x41, 0x7f, 0x41, 0x00 } // 49 I
,{
0x20, 0x40, 0x41, 0x3f, 0x01 } // 4a J
,{
0x7f, 0x08, 0x14, 0x22, 0x41 } // 4b K
,{
0x7f, 0x40, 0x40, 0x40, 0x40 } // 4c L
,{
0x7f, 0x02, 0x0c, 0x02, 0x7f } // 4d M
,{
0x7f, 0x04, 0x08, 0x10, 0x7f } // 4e N
,{
0x3e, 0x41, 0x41, 0x41, 0x3e } // 4f O
,{
0x7f, 0x09, 0x09, 0x09, 0x06 } // 50 P
,{
0x3e, 0x41, 0x51, 0x21, 0x5e } // 51 Q
,{
0x7f, 0x09, 0x19, 0x29, 0x46 } // 52 R
,{
0x46, 0x49, 0x49, 0x49, 0x31 } // 53 S
,{
0x01, 0x01, 0x7f, 0x01, 0x01 } // 54 T
,{
0x3f, 0x40, 0x40, 0x40, 0x3f } // 55 U
,{
0x1f, 0x20, 0x40, 0x20, 0x1f } // 56 V
,{
0x3f, 0x40, 0x38, 0x40, 0x3f } // 57 W
,{
0x63, 0x14, 0x08, 0x14, 0x63 } // 58 X
,{
0x07, 0x08, 0x70, 0x08, 0x07 } // 59 Y
,{
0x61, 0x51, 0x49, 0x45, 0x43 } // 5a Z
,{
0x00, 0x7f, 0x41, 0x41, 0x00 } // 5b [
,{
0x02, 0x04, 0x08, 0x10, 0x20 } // 5c ¥
,{
0x00, 0x41, 0x41, 0x7f, 0x00 } // 5d ]
,{
0x04, 0x02, 0x01, 0x02, 0x04 } // 5e ^
,{
0x40, 0x40, 0x40, 0x40, 0x40 } // 5f _
,{
0x00, 0x01, 0x02, 0x04, 0x00 } // 60 `
,{
0x20, 0x54, 0x54, 0x54, 0x78 } // 61 a
,{
0x7f, 0x48, 0x44, 0x44, 0x38 } // 62 b
,{
0x38, 0x44, 0x44, 0x44, 0x20 } // 63 c
,{
0x38, 0x44, 0x44, 0x48, 0x7f } // 64 d
,{
0x38, 0x54, 0x54, 0x54, 0x18 } // 65 e
,{
0x08, 0x7e, 0x09, 0x01, 0x02 } // 66 f
,{
0x0c, 0x52, 0x52, 0x52, 0x3e } // 67 g
,{
0x7f, 0x08, 0x04, 0x04, 0x78 } // 68 h
,{
0x00, 0x44, 0x7d, 0x40, 0x00 } // 69 i
,{
0x20, 0x40, 0x44, 0x3d, 0x00 } // 6a j
,{
0x7f, 0x10, 0x28, 0x44, 0x00 } // 6b k
,{
0x00, 0x41, 0x7f, 0x40, 0x00 } // 6c l
,{
0x7c, 0x04, 0x18, 0x04, 0x78 } // 6d m
,{
0x7c, 0x08, 0x04, 0x04, 0x78 } // 6e n
,{
0x38, 0x44, 0x44, 0x44, 0x38 } // 6f o
,{
0x7c, 0x14, 0x14, 0x14, 0x08 } // 70 p
,{
0x08, 0x14, 0x14, 0x18, 0x7c } // 71 q
,{
0x7c, 0x08, 0x04, 0x04, 0x08 } // 72 r
,{
0x48, 0x54, 0x54, 0x54, 0x20 } // 73 s
,{
0x04, 0x3f, 0x44, 0x40, 0x20 } // 74 t
,{
0x3c, 0x40, 0x40, 0x20, 0x7c } // 75 u
,{
0x1c, 0x20, 0x40, 0x20, 0x1c } // 76 v
,{
0x3c, 0x40, 0x30, 0x40, 0x3c } // 77 w
,{
0x44, 0x28, 0x10, 0x28, 0x44 } // 78 x
,{
0x0c, 0x50, 0x50, 0x50, 0x3c } // 79 y
,{
0x44, 0x64, 0x54, 0x4c, 0x44 } // 7a z
,{
0x00, 0x08, 0x36, 0x41, 0x00 } // 7b {
,{
0x00, 0x00, 0x7f, 0x00, 0x00 } // 7c |
,{
0x00, 0x41, 0x36, 0x08, 0x00 } // 7d }
,{
0x10, 0x08, 0x08, 0x10, 0x08 } // 7e ←
,{
0x78, 0x46, 0x41, 0x46, 0x78 } // 7f →
};
void LcdCharacter(char character)
{
LcdWrite(LCD_D, 0x00);
for (int index = 0; index < 5; index++)
{
LcdWrite(LCD_D, ASCII[character - 0x20][index]);
}
LcdWrite(LCD_D, 0x00);
}
void LcdClear(void)
{
for (int index = 0; index < LCD_X * LCD_Y / 8; index++)
{
LcdWrite(LCD_D, 0x00);
}
}
void LcdInitialise(void)
{
pinMode(PIN_SCE, OUTPUT);
pinMode(PIN_RESET, OUTPUT);
pinMode(PIN_DC, OUTPUT);
pinMode(PIN_SDIN, OUTPUT);
pinMode(PIN_SCLK, OUTPUT);
digitalWrite(PIN_RESET, LOW);
digitalWrite(PIN_RESET, HIGH);
LcdWrite(LCD_C, 0x21 ); // LCD Extended Commands.
LcdWrite(LCD_C, 0xB1 ); // Set LCD Vop (Contrast).
LcdWrite(LCD_C, 0x04 ); // Set Temp coefficent. //0x04
LcdWrite(LCD_C, 0x14 ); // LCD bias mode 1:48. //0x13
LcdWrite(LCD_C, 0x0C ); // LCD in normal mode.
LcdWrite(LCD_C, 0x20 );
LcdWrite(LCD_C, 0x0C );
}
void LcdString(char *characters)
{
while (*characters)
{
LcdCharacter(*characters++);
}
}
void LcdWrite(byte dc, byte data)
{
digitalWrite(PIN_DC, dc);
digitalWrite(PIN_SCE, LOW);
shiftOut(PIN_SDIN, PIN_SCLK, MSBFIRST, data);
digitalWrite(PIN_SCE, HIGH);
}
void gotoXY(int x, int y)
{
LcdWrite( 0, 0x80 | x); // Column.
LcdWrite( 0, 0x40 | y); // Row.
}
void dispcountt(int count)
{
LcdCharacter(disp_tab[count/10000]);
LcdCharacter(disp_tab[count/1000%10]);
LcdCharacter(disp_tab[count/100%10]);
LcdCharacter(disp_tab[count%100/10]);
LcdCharacter(disp_tab[count%10]);
}
//storage variables
float radius = 13.5;//Радиус шины в дюймах. У 26 дюймовой шины, около 13.5 (магия)
boolean reedVal;
long timer = 0;//Время между оборотами в миллисекундах
float kmh = 0.00;//Скорость в км/ч
float circumference;
float distance = 0;//Дистанция поездки в метрах
long totalDistance = 0;//Дистанция всех поездок в метрах
long totalTime = 0;//Время всех поездок в минутах
float distanceBuffer = 0;//Несохранённая дистанция поездки в метрах
long timeBuffer = 0;//Несохранённое время поездки в секундах
float deltaD;//Проезжаем за оборот в метрах
boolean moving = false; //флаг - едем или нет
long time = 0;//Время езды в секундах
long lastTime = millis();
long duration = 0;
DS1302 rtc(9, 8, 7);
int maxReedCounter = 100;//Минимальное время в миллисекундах на оборот
int reedCounter;
void setup(void)
{
rtc.halt(false);
rtc.writeProtect(false);
pinMode(10, OUTPUT);//Магия для карты памяти
reedCounter = maxReedCounter;
deltaD = 2*3.415926535*radius*0.025;//Проезжается за один оборот колеса в метрах
circumference = 2*3.14*radius;//То же самое, но с меньшей точностью (для измерения скорости) и не в метрах
pinMode(A0, INPUT);
// TIMER SETUP- the timer interrupt allows preceise timed measurements of the reed switch
//for mor info about configuration of arduino timers see http://arduino.cc/playground/Code/Timer1
cli();//stop interrupts
//set timer1 interrupt at 1kHz
TCCR1A = 0;// set entire TCCR1A register to 0
TCCR1B = 0;// same for TCCR1B
TCNT1 = 0;
// set timer count for 1khz increments
OCR1A = 1999;// = (1/1000) / ((1/(16*10^6))*8) - 1
// turn on CTC mode
TCCR1B |= (1 << WGM12);
// Set CS11 bit for 8 prescaler
TCCR1B |= (1 << CS11);
// enable timer compare interrupt
TIMSK1 |= (1 << OCIE1A);
sei();//allow interrupts
//END TIMER SETUP
LcdInitialise();
LcdClear();
LcdString("Initializing SD card...");
pinMode(10, OUTPUT);
gotoXY(0, 1);
if (!SD.begin(chipSelect)) {
LcdString("Card failed, or not present");
}else{
LcdString("Card initialized.");
File logFile = SD.open("logfile.txt");
if (logFile)
{
gotoXY(0, 2);
LcdString("reading file");
while (logFile.available()) {
totalDistance = logFile.parseInt();
totalTime = logFile.parseInt();
}
} else {
gotoXY(0, 2);
LcdString("error read file");
}
}
LcdClear();
gotoXY(0, 0);
LcdString("Spd:");//скорость Км/Ч
gotoXY(0, 1);
LcdString("Dst:");//дистанция за поездку в метрах
gotoXY(0, 2);
LcdString("Drt:");//длительность поездки в минутах
gotoXY(0, 3);
LcdString("Clc:");//часы
}
ISR(TIMER1_COMPA_vect) {//Прерывание на частота 1 кГц для проверки геркона
if(analogRead(reed) >= 680 && analogRead(reed) <= 742){//Геркон замкнут при таких показаниях
reedVal = true;
}else{
reedVal = false;
}
if (reedVal){//Геркон замкнут
if (timer > 110){
moving = true;
}
if (reedCounter == 0){//Минимальное время между импульсами прошло
if(moving){
kmh = (56.8*float(circumference))/float(timer)*1.61;//километры в час
distance += deltaD;
distanceBuffer
}
reedCounter = maxReedCounter;//Сбрасываем reedCounter
timer = 0;//Сбрасываем таймер геркона
} else{
if (reedCounter > 0){//Не улетаем в минуса
reedCounter -= 1;//Уменьшаем reedCounter
}
}
}else{//Если геркон не замкнут
if (reedCounter > 0){//Не улетаем в минуса
reedCounter -= 1;//Уменьшаем reedCounter
}
}
if (timer > 2000){
kmh = 0;//Если долго нет сигналов от геркона, мы стоим.
moving = false;
}else{
timer += 1;//Увеличиваем таймер
}
}
void updateDisplay(){
for(int i = 0; i<=3; i++){
gotoXY(28, i);
LcdString(" ");
}
char Sensor1CharMsg[8];
gotoXY(28, 0);
if(moving){
String Sensor1String((int)(kmh), DEC);
Sensor1String.toCharArray(Sensor1CharMsg,(Sensor1String.length()+1));
LcdString(Sensor1CharMsg);
}else{
LcdString("0");
}
LcdString("KM/h");
gotoXY(28, 1);
String Sensor1String2((int)(distance), DEC);
Sensor1String2.toCharArray(Sensor1CharMsg,(Sensor1String2.length()+1));
LcdString(Sensor1CharMsg);
LcdString("m");
gotoXY(28, 2);
String Sensor1String3((int)(duration/60), DEC);
Sensor1String3.toCharArray(Sensor1CharMsg,(Sensor1String3.length()+1));
LcdString(Sensor1CharMsg);
LcdString("min");
gotoXY(28, 3);
String Sensor1String4(rtc.getTimeStr(FORMAT_SHORT));
Sensor1String4.toCharArray(Sensor1CharMsg,(Sensor1String4.length()+1));
LcdString(Sensor1CharMsg);
}
void loop(void)
{
if (reedVal){//Геркон замкнут
if (timer <= 110){
moving = false;//Мы остановились, замкнув геркон
}else{
moving = true;
}
}
updateDisplay();
saveData();
if (moving){
int d = (int)((millis()-lastTime)/1000)
timeBuffer += d;
duration += d;
}
lastTime = millis();
delay(1000);
}
void saveData(){
while(timeBuffer>=60){
timeBuffer-=60;
totalTime++;
}
while(distanceBuffer>=11){
distanceBuffer-=11;
totalDistance+=11;
}
}