Upgrade with Arduino Uno – A Fun way to recycle a toy car

So one day my daughter, which at the time was a couple of months short of 3 years old, caught sight on a learn-to-walk car which she wanted badly. It was neither expensive nor specially advanced and definitely intended for children below her age-group.

Activity car from KID (image source: Lekia.se)

The car has a dashboard with some buttons and a lever which didn’t do anything at all (disclaimer: at least on the version of the car that we bought). Some of the buttons couldn’t even be pressed. She didn’t notice this at the toy store but certainly did as soon as we got home. Verbal as she is she starts ranting – “This doesn’t work, that doesn’t work… and where are the batteries?”. After an attempted lecture from my side about getting what you pay for I realized that I could do something about it.

I have a background in engineering and an interest in programming. Fiddling with embedded systems has been a hobby for a long time and now I saw the golden opportunity to upgrade the car using one of the Arduino Uno’s that I had laying around only collecting dust. It was finally time for the Atmega328 to flex its muscles since most of the peripherals was going to come in handy and to actually do something of all the electronic junk I had laying around in some box for years, just in case a day such as this would appear. Huzzah!

Disclaimer: Potential hazard of overheating components leading to risk of explosion. Readers attempting to reproduce the steps below are doing so at their own risk. It is a good idea to keep electric stuff sheltered and isolated from small children.


Day 1 – The beginning

Plastic car acquired, offspring unhappy with its capabilities. Dusting of old knowledge of embedded systems and start planning upgrade. Prospect is good since the car has a “hood” attached with 8x M4 screws, this compartment will be used to house the Arduino Uno & most of the sensitive electronic components because of the low probability of vandalism.

Arduino Uno in car hood

Hidden compartment in the car ideal for mounting electronics


Day 2 – Scope of project

  • Power source: The Arduino will be powered by a 5000 mAh power bank connected to the USB port and the DC motor (only for sound effects) will be powered by an auxiliary 9V battery connected to the motor-driver IC. Since the DC motor will be “freewheeling” I don’t expect much current to be drawn.
  • Visuals: The car should of course have two front and two rear LED’s controlled by the Arduino. The LED’s are to blink if the (currently non-press-able) blinker-buttons are pressed. At this point I thought it to be a good idea for the buttons to be connected to the external interrupt pins PD2 & PD3 for better response of the blinky-subroutine in the Interrupt Service Routines (ISR). The (better) alternative would be to do a bit of parallel computing and throw in an ATtiny85 which could handle the blinkers but i didn’t have the time nor the energy to implement this. The LED’s output are connected to PC0 & PC1.
  • Sound effects: The already-built-in lever will be modified with a potentiometer (i.e. a variable resistor) connected to the Arduino’s 10-bit ADC through PC5 in order to drive a small DC motor with an unbalance which was used for motor sound. First reading the 10-bit ADC and then mapping the values to the 8-bit timer2 PWM on PB3. The DC motor is to be driven by a L293D motor driver IC and an auxiliary 9V battery. I thought about adding a piezo to timer0 but decided against it since I want the car to be discrete. Maybe for the next upgrade or something…
  • Interface: The already-built-in (press-able) 1x”ignition” (PB4) and 2x”blinker” (PD2, PD3) buttons on the dashboard are to be connected to electronic buttons. This will be a challenge since custom adapter plates must be manufactured to hold the electronics. Hence the necessity to buy a new 3D printer was finally justified (to my wife’s dismay). I got the Flashforge Adventurer 3. At this point the project wasn’t so cheap anymore LOL! Anyway, the “ignition” button is connected to PB4 and the “motor start” signal to the L293D is connected to PB5 on the Arduino.
  • Power consumption: In order to save some amps a sleep-function is implemented when the car is turned off. In order to wake it up again the external interrupt buttons are used (i.e. pressing one of the blinker-buttons). As I understood it using a powerbank is not optimal for capsuled projects since it will get drained regardless if the Arduino is sleeping or not, because of up converter-leakage. Also some LED’s on the Arduino never goes out, and who knows what else… Also the sonar and motor-driver IC are connected to VCC and ground and might consume power regardless of the Arduino being asleep, so unfortunately even at sleep the 5000mAh (probably lower because of converter inefficiency) powerbank wont last much longer than 1½ days.
  • Fail proof: Like any decent capsuled AVR project a watchdog is used to make sure you don’t need to dig up your microcomputer in order to reset in case of software hang-ups. The watchdog automatically resets the program in case it doesn’t reach a certain point in the software instructions.
  • Autobrake: This function was not condoned by my daughter but the official story is that “as a parent it is my obligation to think of safety first”. Also now I had an excuse for using the HY-SR05 sonar sensor and the SG90 servo which also were collecting dust in the shelf, and not least – to print out some brake discs from my newly-acquired 3D printer. The idea is to calculate the momentaneous speed (by calculating v=dx/dt, where x is the measured distance change and t is the measuring time interval at 10Hz) and act upon it in case a certain condition was met. The SG90 servo is used to apply the brakes using the 16-bit timer1 PWM at PB1 (16-bit PWM is necessary for achieving a 50Hz period for the servo to work, the 8-bit timers can’t achieve this slowness @16MHz CPU clock). The HY-SR05 has 5pins – VCC, GND, Echo, Trigger, Out which is to be connected to 5V, GND, PB0* (input capture pin), PD7 respectively and the “Out” pin in my case was not used (this pin is for connecting external measuring devices i think).

*Note that the 16-bit timer1 in the Arduino will be working double shifts used for both driving the servo PWM and for the sonar input capture by shamelessly re-configuring the timer1-setup back-and-forth at blazing speeds in the main loop.

Circuit schematic for the car made in “Fritzing”. All peripherals were connected through terminals on a hat which in turn was mounted on the Arduino


Day 3 – Building the prototype board

Before setting up the soldering station it is good-practice to set up the circuit on a prototype board for debugging and to make sure that everything runs as intended. This circuit will at a later stage be made permanent through soldering and applied on the Arduino (in other words, a “hat” will be made).

Breadboard prototype, not installed were the 9v battery, DC motor and the brake-servo


Day 4 – Programming

I prefer using the C-code type of programming (in Visual Studio Code) rather than arduinos “ADL” since

  1) This is the way I learned it

  2) I think it is more down-to-the-bone-marrow

  3) it enables you to make sense of the datasheet of the ATmegaXXX

  4) it saves a lot of space in memory

In some cases though I like the ADL so sometimes I throw that in, for instance when debugging with the serial port it is much easier to write “Serial.begin(115200); Serial.print(“ignition\n”);…etc) than messing around with USART functions. However if I mess around with the ESP8266 I stick to ADL only.

A great site for an in-depth insight of how to program “C-code-style” is “HeKilledMyWire“. Anyways, here is the code:

    //working version slv1.0_v2 - sleep

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>
#include <avr/sleep.h>

#define F_CPU 1600000UL
#define setbit(port,bit)  ((port) |=  (1<<bit)) 
#define clrbit(port,bit)  ((port) &= ~(1<<bit)) 
#define _BV(bit)   (1<<bit)

//Functions prototypes
//void timer0_init(void); 
void main_setup(void);
void goToSleep(void);
void disable_watchdog(void);
void restart_watchdog(void);
void restart_watchdog_wake(void);
void timer1_servo(void);         //servo PB1 OC1A
void timer1_capture(void);      //input capture sonar PB0
void timer2_init(void);         //for motor pwm
void interrupt_init(void);
void adc_init(void);            //Function to initialize/configure the ADC
uint8_t ignition; 
uint8_t readButton0(void);
uint8_t readButton1(void);
uint8_t readButton2(void);
void PWM_start(uint8_t DC);
void PWM_stop(void);

uint16_t adc_value;            //Variable used to store the value read from the ADC
uint8_t i=0;                //Variable for the for() loop 
uint16_t read_adc(uint8_t channel);    //Function to read an arbitrary analogic channel/pin
uint8_t DC;


uint16_t PW;
volatile uint8_t fromLoop=0;
volatile uint16_t pulse_width;
volatile uint8_t capt_flag;
volatile unsigned int overflow_cnt = 0;                     // count overflow
float x=0.0;
float x0=-1.0;
float v=0.0;

//ISR's
ISR(WDT_vect){
  clrbit(TIMSK1,ICIE1);     // disable sonar

  unsigned char j;

  for(j=0;j<6;j++){
    PORTC ^=(1<<PC0);
    PORTC ^=(1<<PC1);
    _delay_ms(200);
  }
}

ISR(TIMER1_CAPT_vect){
  static uint16_t t0, t1;
  cli();
  if(bit_is_set(TCCR1B,ICES1)){  // if rising edge
    overflow_cnt = 0;
    t0 = ICR1;
    clrbit(TCCR1B,ICES1);     // set falling edge
    //Serial.print("debug1\n");
  }else{
    t1 = ICR1;
    pulse_width = t1 - t0 + (overflow_cnt << 16);
    setbit(TCCR1B,ICES1);     // set rising edge
    capt_flag = 1;
    //Serial.print("debug2\n");
  }
  sei();
}

ISR(TIMER1_OVF_vect){
  overflow_cnt++;                    // count overflow
}

ISR(INT0_vect){
  //button0 was pressed
  if(fromLoop==1){
    clrbit(TIMSK1,ICIE1);     // disable sonar
    disable_watchdog();
    unsigned char j;

    for(j=0;j<5;j++){
      PORTC ^=(1<<PC0);
      _delay_ms(200);
    }
    PORTC |=(1<<PC0); //resume led state
    restart_watchdog();
  }else{
    //from wake up
    restart_watchdog_wake();  //makes the arduino reset after sleep
  }
} 

ISR(INT1_vect){
  //button1 was pressed  
  if(fromLoop==1){
    clrbit(TIMSK1,ICIE1);     // disable sonar
    disable_watchdog();
    unsigned char j;

    for(j=0;j<5;j++){
      PORTC ^=(1<<PC1);
      _delay_ms(200);
    }
    PORTC |=(1<<PC1); //resume led state
    restart_watchdog();
  }else{
    //from wake up
    restart_watchdog_wake();
  }
} 

//main
int main(void){

  main_setup();
  
  //Loop
  for(;;){
    //power off state

    if(readButton2()==1){        //Verify the ignition button state
      _delay_ms(150);

      //lights
      PORTC |= ((1<<PC0)|(1<<PC1));
      //engine
      PORTB ^=(1<<PB5);     //XOR toggles the on-board led/motor on off
      timer2_init();        //set motor PWM on PB3 OC2A according to ADC vals
      //ADC
      adc_init();                  //Setup the ADC for throttle
      //misc
      Serial.print("ignition\n");
      ignition=1;

      fromLoop=1;

      //motor sounds
      unsigned char m;
      unsigned char r;
      for(m=0;m<2;m++){
        for(r=50;r<200;r++){
          PWM_start(r);
          _delay_ms(2);
        }
      }

      //on-start loop
      while(ignition==1){
        restart_watchdog();

        //brake servo
        if (v>1.0 && v<12.5) {
          cli();
          TIMSK1 &= ~(1<<ICIE1)|~(1<<TOIE1);  // disable input capture
          timer1_servo(); // switch timer1 task for servo job          
          OCR1A=260;
          _delay_ms(100); //time for servo adjustment
          OCR1A=375;
          _delay_ms(100);          
          sei();
          //switch timer1 task to sonar, reset timer and flags
          timer1_capture();          
        }
        
        //Send sonar pulse
        sonar_pulse();
        while(capt_flag==0);
        if(capt_flag){
          _delay_ms(100);                // frequency of measuring distance
          //Serial.print("hit\n");
          cli();                    // read atomically
          //speed calculations
          PW = pulse_width; // pulse time[ticks]
          if (PW<15000) {x = 0.0000005 * PW/2 * 34300;} else {x=-1;}  //TCNT1 res[s]*tick[-]*speedsound[cm/s]= distance[cm]
          if (x!=-1) {v=(x0-x)/(0.1-0.0)/100;} else {v=-1;}    //  v=dx/dt  momentan hastighet m/sek, ignore long range
          if (v>0.05) {v=v;} else {v=-1;}   //filter to ignore small speeds, first calc and reversing
          x0=x; //history distance
          timer1_capture(); //reset timer and flags
          sei();
          capt_flag = 0;
          //Serial.println(v);   //in m/s
        }
 
        //ADC
        cli();
        adc_value = read_adc(5);        //Read one ADC channel 5
        DC=map(adc_value,350,510,5,250);    //map ADC 10bit to 8bit duty cycle 1-90%
        //Serial.println(DC);
        PWM_start(DC);  //run motor   
        sei();

        //maybe switch off?
        if(readButton2()==1){        //Verify the button state
          _delay_ms(150);
          PORTB ^=(1<<PB5);                   //XOR toggles the on-board led/motor on off
          TIMSK1 &= ~(1<<ICIE1)|~(1<<TOIE1);  // disable input capture
          ADCSRA &= ~(1<<ADEN);                //shut off ADC
          PWM_stop();
          PORTC &= ~((1<<PC0)|(1<<PC1));
          ignition=0;
          Serial.print("stop\n");
          disable_watchdog();
          fromLoop=0;
          goToSleep();
        }          
      }        
    }
    
    //power off state
  }
  return 0;
}

//aux functions
void main_setup(void){
  Serial.begin(115200);
  unsigned char i=0;

  disable_watchdog();

  //LED blinkers
  DDRC |= ((1<<PC0)|(1<<PC1));
  PORTC &= ~(1<<PC0);
  PORTC &= ~(1<<PC1);  
  interrupt_init();   //interrupt from buttons for blinking

  //pull up motor enable button PB4
  DDRB &= ~(1<<PB4);          // Set PB4 as input for button
  PORTB = (1<<PB4);           // Enable PB4 pull-up resistor, pushed button = 0
  DDRB |= (1<<PB5);            ///PB5/digital 13 is an output - L293B h-bridge on/off
  PORTB &= ~(1<<PB5);



  //sonar shit
  timer1_capture();            // presc 8, ICP interrupt on rising edge = 1/Fcpu/presc = 0,0000005s = 0.5uS
  TIMSK1 &= ~(1<<ICIE1)|~(1<<TOIE1);                   // disable input capture
  DDRD |= (1<<PD7);            //Set PD7 pin as output for sonar pulse  (arduino pin 7)  
  DDRB &= ~(1<<PB0);           // PB0 input

  //servo init
  timer1_servo();               //Setup timer1
  OCR1A=375;      //brake open position
  _delay_ms(700);
  
  sei();                       //Enable Global Interrupt  
}

void goToSleep(void){
  ADCSRA = 0;                    //disable the ADC
  TCCR1A = 0;
  TCCR1B= 0;  
  TCCR2A = 0;
  TCCR2B = 0;

  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  cli();                         //stop interrupts to ensure the BOD timed sequence executes as required
  sleep_enable();
  //disable brown-out detection while sleeping (20-25µA)
  uint8_t mcucr1 = MCUCR | _BV(BODS) | _BV(BODSE);
  uint8_t mcucr2 = mcucr1 & ~_BV(BODSE);
  MCUCR = mcucr1;
  MCUCR = mcucr2;
  //sleep_bod_disable();           //for AVR-GCC 4.3.3 and later, this is equivalent to the previous 4 lines of code
  sei();                         //ensure interrupts enabled so we can wake up again
  sleep_cpu();                   //go to sleep
  sleep_disable();               //wake up here    
}

void disable_watchdog(void){
	cli(); //disable interrupt
	wdt_reset(); //watchdog reset
	MCUSR &= ~(1<<WDRF); 	/* Clear WDRF in MCUSR */
	WDTCSR |= ((1<<WDCE) | (1<<WDE));	// Write logical one to WDCE and WDE 
	// Keep old prescaler setting to prevent unintentional time-out
	WDTCSR = 0x00; 	// Turn off WDT 
	sei(); //enable interrupt
}

void restart_watchdog(void){
  cli();
  wdt_reset(); //watchdog reset
  /* Start timed equence */
  WDTCSR |= (1<<WDCE) | (1<<WDE);
  /* Set new prescaler(time-out) value = 64K cycles (~2 s) */
  WDTCSR = (1<<WDE) | (1<<WDIE) | (1<<WDP3);
  sei();
}

void restart_watchdog_wake(void){
  cli();
  wdt_reset(); //watchdog reset
  /* Start timed equence */
  WDTCSR |= (1<<WDCE) | (1<<WDE);
  /* Set new prescaler(time-out) value = 64K cycles (~1 s) */
  WDTCSR = (1<<WDE) | (1<<WDIE) | (1<<WDP2) | (1<<WDP1);
  sei();
}

/*piezo pwm - for another time maybe
void timer0_init(void){
  DDRD = ((1<<PD5)|(1<<PD6));                      //Set pwm pins from timer 0 as outputs
  TCCR0A = ((1<<COM0A1)|(1<<WGM01)|(1<<WGM00));    //Enable pwm mode in pin PD6 and set the WGM bits to Fast pwm mode
  TCCR0A |= (1<<COM0B1);                           //Enable pwm mode in pin PD5
  TCCR0B = ((1<<CS01)|(1<<CS00));                  //Set prescaler to 32
  OCR0A = tune
  OCR0B = tune  
}*/

//servo pwm
void timer1_servo(void){
  TCCR1A|=(1<<COM1A1)|(1<<WGM11);        //NON Inverted PWM
  TCCR1B|=(1<<WGM13)|(1<<WGM12)|(1<<CS11)|(1<<CS10); //PRESCALER=64 MODE 14(FAST PWM)
  ICR1=4999;  //fPWM=50Hz 
  DDRB|=(1<<PB1);   //PWM Pins as Output
  //OCR1A = according to postions of servo arm
}

//input capture timer1
void timer1_capture(){
  TCCR1A &= ~((1<<WGM11)|(1<<WGM10));    //fclear wgms
  TCCR1B &= ~((1<<WGM13)|(1<<WGM12));
  TCCR1B = 0;                        // clear TCCR1B
  TCNT1 = 0;                         // clear TCNT1
  ICR1 = 0;
  TIFR1 = (1<<ICF1)|(1<<TOV1);       // clear input capture flag,timer1 overflow flag

  TCCR1B = (1<<ICES1)|(1<<CS11);     // set rising edge detect, prescaler /8 (2MHz resolution on distance time measurements)
  TIMSK1 = (1<<ICIE1)|(1<<TOIE1);    // (re)enable input capture interrupt and timer overlow interrupt
}

void sonar_pulse(void){
  DDRD |= (1<<PD7);                      //Set PD7 pin as output for sonar pulse  (arduino pin 7)  
  PORTD |= (1<<PD7);
	_delay_us(10);
  PORTD &= ~(1<<PD7);
}

//motor pwm
void timer2_init(void){
  DDRB = (1<<PB3);                      //Set pwm pin from timer2 as output  
	TCCR2A |= ((1<<COM2A1)|(1<<WGM21)|(1<<WGM20));    //Enable pwm mode in pin PB3 (pin11) and set the WGM bits to Fast pwm mode
	TCCR2B |= (1<<CS22);                  //Set prescaler to 256
	//OCR0A = varible according to ADC;
}

void PWM_start(uint8_t DC){
  DC=255-DC;
	OCR2A = DC;        //Set new duty cycle
}

void PWM_stop(void){
	TCCR2A &= ~(1<<COM2A1);    //Disable pwm
}

//Interrupt genom knappar
void interrupt_init(void){
  DDRD &= ~(1<<PD2);          // Set PD2 as input (Using for interrupt INT0)
  DDRD &= ~(1<<PD3);          // Set PD3 as input (Using for interrupt INT1)
  PORTD = ((1<<PD2)|(1<<PD3));           // Enable PD2&3 pull-up resistor, pushed button = 0
  EICRA |= ((1<<ISC11)|(1<<ISC01));   //trigga på falling edge
  EIMSK |= ((1<<INT1)|(1<<INT0));         // local enable int0  &1
}

uint8_t readButton0(void){
 if((PIND & (1<<PD2)) == 0){        //If the button was pressed
 _delay_ms(5); }        //Debounce the read value
 if((PIND & (1<<PD2)) == 0){        //Verify that the value is the same that what was read
 return 1; }            //If it is still 0 its because we had a button press
 else{                    //If the value is different the press is invalid
 return 0; }
}

uint8_t readButton1(void){
 if((PIND & (1<<PD3)) == 0){        //If the button was pressed
 _delay_ms(5); }        //Debounce the read value
 if((PIND & (1<<PD3)) == 0){        //Verify that the value is the same that what was read
 return 1; }            //If it is still 0 its because we had a button press
 else{                    //If the value is different the press is invalid
 return 0; }
}

uint8_t readButton2(void){
 if((PINB & (1<<PB4)) == 0){        //If the button was pressed
 _delay_ms(25); }        //Debounce the read value
 if((PINB & (1<<PB4)) == 0){        //Verify that the value is the same that what was read
 return 1; }            //If it is still 0 its because we had a button press
 else{                    //If the value is different the press is invalid
 return 0; }
}

void adc_init(void){
 ADCSRA |= ((1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0));    //16Mhz/128 = 125Khz the ADC reference clock
 ADMUX |= (1<<REFS0);                //Voltage reference from Avcc (5v)
 ADCSRA |= (1<<ADEN);                //Turn on ADC
 ADCSRA |= (1<<ADSC);                //Do an initial conversion because this one is the slowest and to ensure that everything is up and running
 while(ADCSRA & (1<<ADSC));            //Wait until the conversion is done
}
 
uint16_t read_adc(uint8_t channel){
 ADMUX &= 0xF0;                    //Clear the older channel that was read
 ADMUX |= channel;                //MUX channels PC0-5
 ADCSRA |= (1<<ADSC);                //Starts a new conversion
 while(ADCSRA & (1<<ADSC));            //Wait until the conversion is done
 return ADCW;                    //Returns the ADC value of the chosen channel
}

Day 5 – Soldering

Transfer prototype layout to a more permanent form on perfboard. The perfboard is to be mounted on the Arduino Uno as a “hat”.  This exercise took me a large amount of hours.

Circuit board v1. It’s not pretty.


Day 6 – Disaster strikes

Ouf… Circuit board v1 is short-circuited in ways I couldn’t even begin to understand at 2 a.m. I got shorts on places I hadn’t even soldered a wire to. I thought I was dreaming! It took me a while but eventually I realized what I thought was isolated islands of conductor plates actually weren’t isolated at all, they had to be connected INSIDE the perfboard. Circuit board v1 is a throw-off. Countless hours of work wasted. I’m glad I didn’t mount it on the Arduino before testing or else something nasty might have happened.

Multi-meter goes “Beeeeep” 😦 Circuit board v1 is a throw-off.


Day 7 – Back to the saddle

The day after it was time to go back to soldering at square one. This time I made sure that the perfboard behaved the way I wanted BEFORE spending long hours soldering the circuit. Also I had bought some longer more flexible connector-pins for the Arduino “misalignment” so I didn’t have to split the perfboard. This time it went a lot quicker since I got the routine down. And sure enough, after running a contact-check with the multimeter the system seemed to work when mounted on the Arduino. Phew!

Testing of auxiliary functions

Day 8 – Brake mechanics

This part must have been the hardest. Early versions of the brake design had an eraser pried to the arm and during crash-tests (without passenger) it only resulted in the servo-arm disconnecting from the servo. The frictional force simply was to great for the connection to transfer to the body of the car. I had a feeling this would happen but somewhere I thought that I would not reach this far in the design. Yet here we are! A design where the servo does not transfer any other loads but the normal load from applying pressure is probably a good idea, and yet again the 3D printer would come in handy (shocker).

brake_v1

You can almost see how inadequate the brake-design is. Once the servo applies enough pressure on the disc it will create a large moment around the arm-connection and consequently twist the arm from the cog straight off.


Day 9  – Revision of brake design

A solution for the braking issue is to transmit the friction forces to the car body through some kind of structure, and relieve the servo arm of all forces except applying braking pressure. After having a hard look on the surroundings of the compartment where the servo was already mounted, the new design proposal quickly took shape and a suitable solution was found. After spending some time measuring distances and modelling with the FreeCAD software I had the 3D model which was exported to .stl files sent to my Flashforge Adventurer 3 and after a few minutes I was holding the parts in my hand. I think it’s absolutely astonishing that this is possible nowadays – from idea to design to prototype manufacturing in just a few moments!

The new assembly required adjusting the “braking” position of the servo in the software (by optimizing OCR1A).

arduino arduino arduino arduino arduino arduino arduino arduino arduino arduino uino arduino arduino arduino arduino arduino arduino arduino arduino arduino