Home-made bike computer, or how Arduino got to the street


    Hello, Habr! I would like to share the story about creating a bike computer with my own hands. After buying a bike, I decided to somehow fix my progress, but mobile trackers were not the most convenient solution, and the BOGEER 823 already ordered there flew from China to me, which made a purchase on the spot meaningless. Therefore, I decided to try to make a bike computer with my own hands, at the same time laying the foundation for expansion in it: controlling the bike body kit (front and rear lights, a bell).

    A bit of preparation


    At the heart of any bike computer is a reed switch that detects wheel rotation, and everything else is elementary mathematics ... Of course, I searched the Internet for ready-made implementations and formulas that I need.
    As I already wrote, everything is tied to a reed switch : the magnet is fixed on the spoke, and the reed switch itself is on the “plug”. When the magnet on the plug closes the reed switch - this means that the wheel made a full revolution and the bike traveled a distance equal to:
    2 * Pi * R tires

    First version


    First of all, I made a list of what I needed from the bike computer:
    • Current speed
    • Current Trip Distance
    • Distance of all trips
    • Travel time
    • Current time
    • Backlight
    • Possibility of data exchange with a computer via a memory card


    Fortunately, all the necessary modules were at hand:

    • Arduino nano
    • Nokia 5110 LCD (blue backlight, via GND)
    • Soldered watch DS1302
    • SD card module


    I quickly put together a sandwich: on top of the screen, the second layer of the SD module and the arduinka itself, and the clock underneath it all. It turned out pretty compact. Of course, if I didn’t use ready-made modules, but “loose” and poisoned the boards, I could have won a lot of space.

    Result photo



    Having connected the reed switch, I started testing the first version of the sketch, which was based on the sketch obtained from the network for working with the display and the English language speedometer .
    And how much can you squeeze out of your chair?

    After making sure the design was working, I decided to conduct the first test. I planned to power from several AA batteries or a battery, but at that moment they weren’t at hand ... Believe me, riding a bicycle with a laptop under your arm is still a pleasure.
    Photos from the first test



    Guess why doesn't the bike ride back?


    Testing paid off. I found a mistake in the logic: the signal from the reed switch was always equal to the rotation of the wheel (if the minimum time for revolution has passed). It seems that everything is correct, but if you stop by fixing a magnet opposite the reed switch, the program considered that you were going very fast.

    Second version


    Having finished the program a bit, I put together a “power supply”: a set of five AA batteries. Before that, I tried to use the crown, but it was not enough for Arduinka with a body kit.

    Since my 5110th was on a red background, which means that the backlight in it was controlled by a logical zero, and not by one, I had to refuse software control via the analog port and add a simple button.
    In addition, the analog port without data produced random values, but this was decided by a simple resistor. But another surprise from the analog port made me break my head: the value of the reed switch circuit when powered by batteries was not the same as when using the USB port.

    Photos from the second test







    The second test showed that the system was fully operational. The only problem was the calculation error: the radius of the “26 inch wheel” turned out to be “13.5” inches. As a result, the odometer lied a little (thanks to Yandex.Maps for a convenient tool for calculating distances).

    Third version


    The device worked, but of course I wanted to complete the look. It just so happened that all I managed to find was only one building, and it turned out to be "narrow."

    Couple millimeters



    I had to change the location of the components: everything got into the case, but the total size became larger. In the process of soldering, I had to replace the Arduino Nano and the SD module: I own a soldering iron so-so and some of the conclusions simply wrecked it.

    Dimensions: 72x50x28mm
    I also had to work with a file ...







    After I checked the performance of all the modules, I decided to fix them with epoxy glue for greater resistance to shaking.

    In this form, the computer I will not leave. Sanding and painting ahead





    My haste played a cruel joke: I did not fix the wires under the screen well enough and one of them started to interfere (in fact, this affects the “contrast” parameter).

    It is treated with a simple tapping, as in old TVs




    A couple more corrections in the code, and the interface was brought to the final (at the moment) look.

    Device assembly

    1. Speed ​​(Km / H)
    2. Odometer (Distance in meters)
    3. Travel time (minutes)
    4. Current time (Moscow)



    The clock presented troubles: one of the contacts departed, and time began to skip (as if I had mixed up RST and CLK). But what’s most interesting - after the trip, the clock returned to normal.

    Perhaps the complaint can be presented to the corner of the mount: it is inconvenient to watch during the trip.

    Almost vertical fixture



    Yes, and the method of attaching electrical tape is not ideal, but I could not find a separate bracket for bicycle gadgets. Although, a spider is coming to me from China . I think that it is quite suitable for solving this problem.

    Final option








    Cost


    • Box BOX-G020 - 115r
    • Epoxylin - 95 rub
    • Batteries - 60r
    • Arduino Nano - 120 rub
    • Nokia LCD 5110 - 102p
    • Black electrical tape - 40r
    • SD module - 187р
    • DS1302 - 70r
    • Cases for batteries - 120r
    • Wires and resistors - 80r
    • Reed switch + magnet - 82r


    Total: ~ 1100r

    Related Links




    Summary


    I gained some more experience working with Arduino and building small working devices. I had fun a few hours, but most importantly - now I have a working bike computer. The plans are to write the normal saving / loading of “global” data to the memory card and add a button to display “global” statistics. To control external devices, I had six analog ports (three buttons & three thyristors) and somewhere I lost one digital one, for example, for a range finder (parking sensors) or a thermometer. I would very much like to solve the power problem, because the five AA batteries on the bicycle frame do not look very good.
    The cat that helped test the bike computer



    Sketch code
    #include 
    #include 
    //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;
    } 
    }
    

    Also popular now: