RC Car Electronics


This, to some extent, is a continuation of this post, where I replaced the motors and some gears to fix one of my son’s RC cars.

I remove the electronics and replace them completely, including the remote for which I now use a freshly made wireless Wii Nunchuck.

The results are far from impressive in terms of manoeuvrability, but it was really fun to build and I think the handling can still be improved by tweaking the mix between the 2 joystick axes.

The Controller / Wii Nunchuck

Final, wireless Wii Nunchuck

Final, wireless Wii Nunchuck

I’ve always loved the look and feel of the Wii Nunchuck and wanted a wireless one.

I also have these old RX/TX wireless modules that have been gathering dust for more than 5 years.

It’s been a while since I had the idea of combining them, and finally found some time, now that my son wanted a new RC car and I thought it would be fun to build rather than buy…

So I took the nunchuck apart to find the I2C wires (I’ve already covered the protocol used in several other posts, including this one):

Internals of the nunchuck, you can see the GND, VCC, Clk and Data wires.

Internals of the nunchuck, you can see the GND, VCC, Clk and Data wires.

  • GND – black
  • VCC – red
  • Data – green
  • Clock – yellow

Then used an Arduino Pro Mini (8MHz, 3.3V) to do the interface between the I2C protocol and the 9600bps serial required by the Aurel wireless module:

Wii Nunchuck -> I2C -> Arduino Pro Mini -> 9600bps serial -> Aurel wireless module

Wii Nunchuck -> I2C -> Arduino Pro Mini -> 9600bps serial -> Aurel wireless module

Note that for the Aurel RTX-MID 3V module to go into TX mode, it needs both pins 5 & 6 to be pulled up, done here by using a 1KOhm resister connected to VCC.

And here’s the code that runs on the Arduino and does the translation, useful, especially that the old posts that deal with the nunchuck use either IOIO or LeJOS.

#include <Wire.h>

#define NUNCHUCK_ADDR 0x52  // 7bit addressing.  It would be 0xA4 in 8bit
#define NUNCHUCK_MEM_ADDR 0x40

static uint8_t nunchuck_buff[6];  // array to store nunchuck data

void setup(){
  Serial.begin(9600);
  nunchuck_init();
}


void loop(){
  if(nunchuck_get_data()) {
    // beginning of message marker
    Serial.write(0);
    Serial.write(255);
    // actual data
    Serial.write(nunchuck_buff, 6);
    // end of message error check
    uint8_t crc = 0;
    for(uint8_t i=0; i<6; i++) crc += nunchuck_buff[i];
    Serial.write(crc);
  }
}

// initialize the I2C system, join the I2C bus,
// and tell the nunchuck we're talking to it
void nunchuck_init()
{ 
  Wire.begin();                  // join i2c bus as master
  Wire.beginTransmission(NUNCHUCK_ADDR);  // transmit to device 0x52
  Wire.write(NUNCHUCK_MEM_ADDR);    // sends memory address
  Wire.write(0x00);
  Wire.endTransmission();
}


/**
 * To transform into something meaningful
 * joy_x_axis = nunchuck_buff[0];
 * joy_y_axis = nunchuck_buff[1];
 * accel_x_axis = nunchuck_buff[2];
 * accel_y_axis = nunchuck_buff[3];
 * accel_z_axis = nunchuck_buff[4];
 * 
 * // byte nunchuck_buf[5] contains bits for z and c buttons
 * z_button = !((nunchuck_buff[5] >> 0) & 1);
 * c_button = !((nunchuck_buff[5] >> 1) & 1);
 * 
 * // it also contains the least significant bits for the accelerometer data so we have to check each bit of byte outbuf[5]
 * if ((nunchuck_buff[5] >> 2) & 1) accel_x_axis += 2;
 * if ((nunchuck_buff[5] >> 3) & 1) accel_x_axis += 1;
 * 
 * if ((nunchuck_buff[5] >> 4) & 1) accel_y_axis += 2;
 * if ((nunchuck_buff[5] >> 5) & 1) accel_y_axis += 1;
 * 
 * if ((nunchuck_buff[5] >> 6) & 1) accel_z_axis += 2;
 * if ((nunchuck_buff[5] >> 7) & 1) accel_z_axis += 1;
 */
boolean nunchuck_get_data(){
  // 1. Send a request for data to the nunchuck
  Wire.beginTransmission(NUNCHUCK_ADDR);
  Wire.write(0x00);   // sends one byte
  Wire.endTransmission();
  
  delay(3);
  
  // 2. read data
  Wire.requestFrom(NUNCHUCK_ADDR, 6);
  //Serial.println(Wire.available());
  if(Wire.available() != 6) return false;
  for(int i=0; i<6; i++) nunchuck_buff[i] = nunchuk_decode_byte(Wire.read());

  return true;
}

// Encode data to format that most wiimote drivers except
// only needed if you use one of the regular wiimote drivers
uint8_t nunchuk_decode_byte (uint8_t x)
{
  x = (x ^ 0x17) + 0x17;
  return x;
}

 

The Car itself

Old RC Car with electronics removed

Old RC Car with electronics removed

Beyond the Aurel wireless RX module, there’s a TI Stellaris Launchpad (LM4F120XL) that is the brain and then a TB6612FNG motor controller:

Electronics

Electronics

Electronics 2

Electronics 2

Lots of wires to solder but nothing complex in terms of connectivity:

Electronics Schema

Electronics Schema

The micro-controller listens to the serial port of the wireless module, and as soon as it receives any data it parses it and configures the PWM and the direction of the 2 motors. Nothing conceptually complex again, however there’s quite a bit of code as the wireless/serial protocol allows for error checking and I’m using interrupts for reception and timers for PWM:

// include the StellarisWare directory on the compiler path (Build / GNU Compiler / Directories)
#include "inc/hw_memmap.h"
#include "inc/hw_types.h"

#include "driverlib/gpio.h"
#include "driverlib/sysctl.h"
#include "driverlib/timer.h"
#include "driverlib/uart.h"
#include "utils/uartstdio.h"
#include <stdlib.h>

#define RED_LED   GPIO_PIN_1
#define BLUE_LED  GPIO_PIN_2
#define GREEN_LED GPIO_PIN_3


///////////// Console ~ UART0 ///////////////////////////////////////

// UART0 is mapped by the Stellaris Launchpad to the Virtual COM port available through Debug USB
void initConsole() {
	SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA);
	GPIOPinConfigure(GPIO_PA0_U0RX);
	GPIOPinConfigure(GPIO_PA1_U0TX);
	GPIOPinTypeUART(GPIO_PORTA_BASE, GPIO_PIN_0 | GPIO_PIN_1);

	SysCtlPeripheralEnable(SYSCTL_PERIPH_UART0);
	UARTStdioConfig(0, 115200, SysCtlClockGet());

	UARTprintf("UART Console initialised.\n");
}

/////////////// AUREL Wireless Commands - UART5 /////////////////////////////////////

volatile char _prevChar;
volatile tBoolean _readingMsg = false;
volatile char _msg[6];
volatile char _msgPos;
volatile tBoolean _validData = false;
volatile int _joystickX;
volatile int _joystickY;

tBoolean startNewMsg(char c) {
	tBoolean res = (_prevChar == 0) && (c == 255);
	_prevChar = c;
	return res;
}

void UART5IntHandler() {
	char currChar;
	while (UARTCharsAvail(UART5_BASE)) {
		currChar = UARTCharGet(UART5_BASE);

		if (startNewMsg(currChar)) {
			_readingMsg = true;
			_msgPos = 0;
		} else if (_readingMsg) {
			if (_msgPos >= 6) {
				// data finished, last byte is the CRC
				char crc = 0;
				for (char i = 0; i < 6; i++)
					crc += _msg[i];

				if (crc == currChar) {
					_joystickX = _msg[0];
					_joystickY = _msg[1];
					_validData = true;
				} else {
					_validData = false;
					UARTprintf("Wrong CRC: %d Expected: %d\n", currChar, crc);
				}

				_readingMsg = false;
			} else {
				// normal data, add it to the message
				_msg[_msgPos++] = currChar;
			}
		}
	}
}

void initCommandsUART() {
	SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOE);
	GPIOPinConfigure(GPIO_PE4_U5RX);
	GPIOPinConfigure(GPIO_PE5_U5TX);	// normally not needed as only receiving
	GPIOPinTypeUART(GPIO_PORTE_BASE, GPIO_PIN_4 | GPIO_PIN_5);

	SysCtlPeripheralEnable(SYSCTL_PERIPH_UART5);

	// speed of AUREL wireless is 9600bps 8-N-1
	UARTConfigSetExpClk(UART5_BASE, SysCtlClockGet(), 9600,
	UART_CONFIG_PAR_NONE | UART_CONFIG_STOP_ONE | UART_CONFIG_WLEN_8);

	UARTIntRegister(UART5_BASE, UART5IntHandler);
	UARTIntEnable(UART5_BASE, UART_INT_RT | UART_INT_RX);
	UARTEnable(UART5_BASE);

	UARTprintf("UART AUREL interface initialised.\n");
}

///////////////// 3 colour LEDs on the board ///////////////////////////////

void initLEDs() {
	// Enable and configure the GPIO port for the LED operation.
	SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOF);
	GPIOPinTypeGPIOOutput(GPIO_PORTF_BASE, RED_LED | BLUE_LED | GREEN_LED);
	GPIOPinWrite(GPIO_PORTF_BASE, RED_LED | BLUE_LED | GREEN_LED, 0x00);
}

///////////////// PWMs for the motors ///////////////////////////////////

void initPWM() {
	// we need PB0,1,5 and PD0,1,2 - for PWM but also simpul LOW/HIGH for direction of rotation
	SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOB | SYSCTL_PERIPH_GPIOD);
	GPIOPinTypeGPIOOutput(GPIO_PORTB_BASE,
			GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_5);
	GPIOPinTypeGPIOOutput(GPIO_PORTD_BASE,
			GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2);
	// all to LOW
	GPIOPinWrite(GPIO_PORTB_BASE, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_5, 0x00);
	GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2, 0x00);

	// Timer 2 - T2CCP0 / PB0 & T2CCP1 / PB1
	// Pin Mux-ing
	GPIOPinConfigure(GPIO_PB0_T2CCP0);
	GPIOPinConfigure(GPIO_PB1_T2CCP1);
	// give control of the pins to the Timer hardware
	GPIOPinTypeTimer(GPIO_PORTB_BASE, GPIO_PIN_0 | GPIO_PIN_1);

	SysCtlPeripheralEnable(SYSCTL_PERIPH_TIMER2);
	// split the 32bit timer into 2x16 bits
	TimerConfigure(TIMER2_BASE,
			TIMER_CFG_SPLIT_PAIR | TIMER_CFG_A_PWM | TIMER_CFG_B_PWM);
	// invert the PWM
	TimerControlLevel(TIMER2_BASE, TIMER_A, true);
	TimerControlLevel(TIMER2_BASE, TIMER_B, true);

	TimerLoadSet(TIMER2_BASE, TIMER_A, 100);
	TimerEnable(TIMER2_BASE, TIMER_A);
	TimerLoadSet(TIMER2_BASE, TIMER_B, 100);
	TimerEnable(TIMER2_BASE, TIMER_B);

	UARTprintf("PWM for motors initialised.\n");
}

int constrainPercentage(int percentage) {
	if (percentage < 0)
		return 0;
	else if (percentage >= 100)
		return 99;
	else
		return percentage;
}

// Speed is [-100, 100]
// PB0 is for speed, PB5/PD0 for direction
void setMotorA(int speed) {
	if (speed < 0) {
		GPIOPinWrite(GPIO_PORTB_BASE, GPIO_PIN_5, 0x00);
		GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_0, 0xFF);
	} else {
		GPIOPinWrite(GPIO_PORTB_BASE, GPIO_PIN_5, 0xFF);
		GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_0, 0x00);
	}

	// PWM Timer T2CCP0 / PB0
	TimerMatchSet(TIMER2_BASE, TIMER_A, constrainPercentage(abs(speed)));
}

// Speed is [-100, 100]
// PB1 is for speed, PD1/PD2 for direction
void setMotorB(int speed) {
	if (speed < 0) {
		GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_1, 0x00);
		GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_2, 0xFF);
	} else {
		GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_1, 0xFF);
		GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_2, 0x00);
	}

	// PWM Timer T2CCP1 / PB1
	TimerMatchSet(TIMER2_BASE, TIMER_B, constrainPercentage(abs(speed)));
}

///////////////////// MAIN LOOP ///////////////////////////////

int main(void) {
	// use 80MHz
	SysCtlClockSet(
			SYSCTL_SYSDIV_2_5 | SYSCTL_USE_PLL | SYSCTL_XTAL_16MHZ
					| SYSCTL_OSC_MAIN);

	initConsole();
	initCommandsUART();
	initPWM();

	while (true) {
		if (_validData) {
			int adjustedY = (int) ((_joystickY - 128) * 1.2);
			int adjustedX = (int) ((_joystickX - 128) * 0.75);
			setMotorA(adjustedY - adjustedX);
			setMotorB(adjustedY + adjustedX);

			UARTprintf("X: %d / Y: %d -- A: %d / B: %d\n", _joystickX,
					_joystickY, adjustedY - adjustedX, adjustedY + adjustedX);
		}

		SysCtlDelay(160000);
	}

	return 0;
}
Advertisements

3 Responses to RC Car Electronics

  1. Pingback: Arduino Blog » Turning a Wii Nunchuk into an RC car controller

  2. Pingback: Wireless Nunchuck R/C Remote! - worldnews

  3. Pingback: Wireless Nunchuck R/C Remote! | Hackaday

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: