Categories
Projects

DIY Hot Wheels speedometer

My nephew’s birthday was coming up in a couple days and I wanted to build or (3D) print him something. He loves Hot Wheels, so I thought I’d make him a speedometer with a digital speed readout. I envisioned two LEDs illuminating two photoresistors. When the car speeds by it would block one photoresistor and then the other. Simple! Well, suddenly it was a year or two later and his birthday was coming up again. I don’t remember why I didn’t finish the project the first time, but now I was determined. (I think for that previous birthday I printed him a rocket, so I wasn’t a complete failure of an uncle.)

Speedometer schematic.
Speedometer circuit schematic. Each photoresistor forms a voltage divider so that the voltage the analog pin sees varies with light level.

The figure above shows the circuit schematic for the speedometer. I used an Arduino Nano, a 9 V battery, two cadmium sulfide photoresistors, two 220 Ω resistors for the voltage divider, two 5 mm white LEDs, two current limiting 450 Ω resistors, an 8×32 LED display with MAX7219, and a 4 cm x 6 cm perfboard. A switch was added later. Most of the connections were made with 2- and 3-pin JST connectors as opposed to direct soldering, since this is still pretty much a prototype. You can see the assembled board in the figure below. Sorry, I didn’t take any underside photos.

Pre-enclosure photo of the assembled speedometer. I added a switch later, after an embarrassing “duh” moment.

I designed a very simple enclosure for the electronics. The important part is the separation of the light/photoresistor pairs. In my model this distance is 100 mm. I didn’t have time to come up with a secure latching mechanism for the lid (because I only had time to print it once). My choice of small posts, each with a bump, seemed OK in theory, but were much to small. Within the first week my nephew dropped the device onto carpet and all the posts broke. While I’m working on an updated model, the lid is being held in place with clear tape. Also, the part that holds the perfboard in the original model was made slightly longer than it should have been. The board still fits, but barely. You can download the STL here. Hopefully, I’ll update this post when I fix the design.

The assembled speedometer. I didn’t do a great job organizing the inside, but everything fits. The switch and Dupont header on the display touch, so the switch should really be moved.
The speedometer in action!

The Arduino code for the speedometer is below. It is adapted from an example in the MD_MAX72XX Arduino library (github), which you will need to compile the sketch. It is very basic. It simply finds a moving average for the ADC values associated with each photoresistor voltage divider, and triggers the next state when the instantaneous ADC value goes above two times the average. The speed in inches per second is found by dividing the length (3.9 inches) by the time between triggering the two photoresistors. This is sent to the display using the logic from the MD_MAX72XX example.

To summarize this project: it’s ugly, but it works!

// Code modified from the MD_MAX72XX example to display text from serial.
// Hot Wheels Speedometer
// kylelarsen.com

#include <MD_MAX72xx.h>
#include <SPI.h>

#define PRINT(s, v) { Serial.print(F(s)); Serial.print(v); }

// Define the number of devices we have in the chain and the hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 4

#define CLK_PIN   13  // or SCK
#define DATA_PIN  11  // or MOSI
#define CS_PIN    10  // or SS

// SPI hardware interface
MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);

// Text parameters
#define CHAR_SPACING  1 // pixels between characters

// Global message buffers shared by Serial and Scrolling functions
#define BUF_SIZE  75
char message[BUF_SIZE] = "Ready!";
bool newMessageAvailable = true;

// Distance between sensors
// LDR = Light dependent resistor

// Distance between LDRs
float ldr_dist = 3.937; // inches

int ldrA_pin = A3;
int ldrB_pin = A0;

// ADC values
unsigned int ldrA = 0;
unsigned int ldrB = 0;

// Last time LDR was triggered
unsigned long ldrA_time = 0;
unsigned long ldrB_time = 0;

// Number of points to average for baseline. Cannot go too high because of limited memory
const int     ldr_avg_num = 128;
// TO DO: Am I going to use this array for anything besides average?
unsigned long ldrA_avg_arr[ldr_avg_num];
unsigned long ldrB_avg_arr[ldr_avg_num];
unsigned long ldrA_avg    = 0;
unsigned long ldrB_avg    = 0;
unsigned int  ldr_avg_idx = 0;

// Simple state machine.
//  WAIT_A - waiting for the car to pass the first LDR
//  WAIT_B - waiting for the car to pass the second LDR
enum Status {WAIT_A, WAIT_B};
Status status = WAIT_A;

void printText(uint8_t modStart, uint8_t modEnd, char *pMsg);

void setup()
{
  mx.begin();
  mx.control(MD_MAX72XX::INTENSITY, 1);

  Serial.begin(57600);  
}

void loop()
{
  if (newMessageAvailable)
  {
    // Send message to display
    printText(0, MAX_DEVICES-1, message);
    newMessageAvailable = false;
  }

  // Read the values for the LDRs
  ldrA = analogRead(ldrA_pin);
  ldrB = analogRead(ldrB_pin);

  // Add to rolling average array
  ldrA_avg_arr[ldr_avg_idx] = ldrA;
  ldrB_avg_arr[ldr_avg_idx] = ldrB;

  ldr_avg_idx++;

  if (ldr_avg_idx >= ldr_avg_num)
  {
    ldr_avg_idx = 0;
  }

  // Do the rolling average
  int i;
  ldrA_avg = 0;
  ldrB_avg = 0;
  for (i = 0; i < ldr_avg_num; i++)
  {
    // If we reach a value that is exactly 0 then the ldrN_avg_arr hasn't filled up yet.
    if (ldrA_avg_arr[i] == 0 || ldrB_avg_arr[i] == 0)
    {
      break;
    }
    
    ldrA_avg += ldrA_avg_arr[i];
    ldrB_avg += ldrB_avg_arr[i];
  }

  ldrA_avg /= i; // ldr_avg_num;
  ldrB_avg /= i; // ldr_avg_num;

  // The trigger threshold is two times the average
  if (status == WAIT_A && ldrA > 2*ldrA_avg) {
    ldrA_time = millis();
    status = WAIT_B;
  }

  // Again, the trigger threshold is two times the average,
  // but the car must have already passed the first LDR.
  if (status == WAIT_B && ldrB > 2*ldrB_avg) {
    ldrB_time = millis();

    float dt = ((float)(ldrB_time - ldrA_time) / 1000 ); // seconds

    // Speed = Distance / time    
    sprintf(message, "%d in/s", (int)(ldr_dist / dt));
    newMessageAvailable = true;
    
    status = WAIT_A;
  }

  // Ten second timeout
  if (status == WAIT_B && (millis() - ldrA_time) > 10000)
  {
    status = WAIT_A;
    ldrA_time = 0;
  }

  // View with arduino or other plotter
  Serial.print(ldrA);
  Serial.print(",");
  Serial.print(ldrB);
  Serial.print(",");
  Serial.print(ldrA_avg);
  Serial.print(",");
  Serial.print(ldrB_avg);
  Serial.print("\n");
  
  
  delay(5);
  
}


void printText(uint8_t modStart, uint8_t modEnd, char *pMsg)
// Print the text string to the LED matrix modules specified.
// Message area is padded with blank columns after printing.
{
  uint8_t   state = 0;
  uint8_t   curLen;
  uint16_t  showLen;
  uint8_t   cBuf[8];
  int16_t   col = ((modEnd + 1) * COL_SIZE) - 1;

  mx.control(modStart, modEnd, MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);

  do     // finite state machine to print the characters in the space available
  {
    switch(state)
    {
      case 0: // Load the next character from the font table
        // if we reached end of message, reset the message pointer
        if (*pMsg == '\0')
        {
          showLen = col - (modEnd * COL_SIZE);  // padding characters
          state = 2;
          break;
        }

        // retrieve the next character form the font file
        showLen = mx.getChar(*pMsg++, sizeof(cBuf)/sizeof(cBuf[0]), cBuf);
        curLen = 0;
        state++;
        // !! deliberately fall through to next state to start displaying

      case 1: // display the next part of the character
        mx.setColumn(col--, cBuf[curLen++]);

        // done with font character, now display the space between chars
        if (curLen == showLen)
        {
          showLen = CHAR_SPACING;
          state = 2;
        }
        break;

      case 2: // initialize state for displaying empty columns
        curLen = 0;
        state++;
        // fall through

      case 3:  // display inter-character spacing or end of message padding (blank columns)
        mx.setColumn(col--, 0);
        curLen++;
        if (curLen == showLen)
          state = 0;
        break;

      default:
        col = -1;   // this definitely ends the do loop
    }
  } while (col >= (modStart * COL_SIZE));

  mx.control(modStart, modEnd, MD_MAX72XX::UPDATE, MD_MAX72XX::ON);
}