(Note: LEGO® is a trademark of the LEGO Group, which neither sponsors, not authorizes, nor supports this website.)

Introduction

LEGO® Mindstorms is a wonderful platform to playfully get used to robotics and programming. Kids can complete their existing LEGO® collection with motors and sensors, and the graphical programming environment is easy to understand, but it also contains complex concepts like commands, iterations, branching, variables, and events.

LEGO® Mindstorms is, on the other hand, not very cheap (especially for the inquisitive offspring). Though my son already had a rich collection of LEGO® Technic models with motors, lights, and switches, it lacks of sensors and even programming.

But he was very much interested in that stuff.

So I thought, let's try to fill the gap and build an open-source platform based on LEGO® Technic, that is cheap enough (goal <100€) and also easy to understand. It seemed that Arduino was the perfect choice, for the following reasons:

  • An Arduino Nano costs less than 20€, sometimes even below 10€.
  • You can power it with a regular LEGO® Technic battery.
  • It starts up in less than a second (in contrast to the Raspberry Pi for instance), a circumstance that is important when building models with a switchable battery.
  • It has enough inputs/outputs for about 10 devices.

The programming is done in C, which is not so intuitive, but you can compensate that by developing a custom graphical IDE, or at least a better API. We decided to do the API first (see below).

Power supply

Conrad breadboards are best to play around and experiment. They are prototype circuit boards to plug parts easily, using the standard 1/10 inch grid. Conrad also provides jumper wires in various lengths, that also fit into the shield jacks of the Arduino:

On the LEGO® side, we have to improvise more, as the plugs and devices are not intended to solder anything or tinker with them. A good shot are the extension cables. When splitting one of them in the middle, you get a male and a female jack, both with open wiring to work with:

LEGO® Technic uses 4-wire cables with an interesting assignment:

2 wires correspond to...

  • the positive and negative pole of the battery, when it is turned on,
  • no voltage, when it is turned off,
  • and reverse polarity (negative resp. positive), when it is turned on in reverse.

The other two wires behave the same, but they are not inverted. When the battery is turned on in any way, one is positive, the other is negative.

You usually attach motors to the first (inverting) pair of wires, so you can control the direction by switching the battery or any switch in between. LED lamps, for instance, use the other pair of wires to always get a proper polarity.

The second pair is also very handy for our project, as we can connect the Arduino to it.

Control motors and lamps

First of all, we want to control LEGO® motors and lamps with our Arduino. Here we have three challenges:

  • The currents are too high for the Arduino (it provides at most 40mA per pin).
  • The voltage does not match (the Arduino outputs 5V, LEGO® has 9V).
  • Motors require managable inversion of polarity (see above).

We can solve the first two problems with MOSFETs, transistors that can switch-though foreign voltages almost lossless and that only need a minimum of gate current. And we can drive the MOSFETs with PWM, a technique that enables variable power by fast on-and-off switching, without producing the loss heat of a linear regulator.

To inverse the polarity, we utilize bistable relays, so that only the direction switching consumes power. When applying a voltage to a bistable relay, it switches to the one position, but when removing that voltage, it stays in that position. A negative power switches the relay back to the other position.

To control a MOSFET, we need one Arduino pin, for the relay, we need two:

When switching one relay at a time, we can get along with 4 pins for 3 relays:

We want to provide 3 invertable, and 2 non-invertable outputs, to use the Arduino to the max. All 5 outputs should support PWM (variable power).

To do so, we connect the MOSFETs and relays to the following Arduino pins:

  • Pin 2: Relays common
  • Pin 3: MOSFET A
  • Pin 5: MOSFET B
  • Pin 6: MOSFET C
  • Pin 7: Relay A
  • Pin 8: Relay C
  • Pin 9: Relay B
  • Pin 10: MOSFET D
  • Pin 11: MOSFET E

The following table shows how to drive the relays to switch them into forwards resp. backwards position, or leave them in their current state:

Pin 7 (A) Pin 9 (B) Pin 8 (C) Pin 2 (common) Effect
0 0 0 0 none (stable)
1 0 0 0 Relay 1 forwards
0 1 1 1 Relay 1 backwards
0 1 0 0 Relay 2 forwards
1 0 1 1 Relay 2 backwards
0 0 1 0 Relay 3 forwards
1 1 0 1 Relay 3 backwards

Sensors

Now let's tackle the sensors. We are using the analog inputs of the Arduino, which provides the following advantages:

  • The inputs understand a wide voltage spectrum (0V to 5V).
  • We don't need to occupy pins we could use as outputs.

Admittedly, the analog inputs are slower than the digital ones, but we accept that.

We can connect various sensor types to the inputs:

  • Buttons and switches
  • Knobs (pots)
  • IR light barrier
  • Ultrasonic distance sensors
  • many more...

The inputs have a high resistance (as usual with micro controllers), so you should not use them in an open configuration, as this will lead to measurement errors because of the environment radiation. In case of a button or switch, you use a pull-up or pull-down resistor:

All Arduinos have at least 6 analog inputs, so we take this number as a basis for our design.

Remote control, start, stop

To prevent the program from starting as soon as the circuit is powered, and to cancel it when the code runs wild, we also add two fixed buttons, a reset/stop button (red), and a play button (green), which we connect to the Arduino reset pin, and with one of the digital inputs, respectively.

Finally it is handy to control the circuit with a remote control. We attach an IR sensor with digital output to another Arduino digital pin. We don't have any support in the Leguino API yet, but we already made experiences in other projects (sorry, that's a German page).

Final circuit

Altogether, this is the overall wiring and pin assignment:

As of today, we only did one hand-wired prototype of the circuit. In the future, we should design and build a PCB board version of Leguino. Maybe you want to contribute and provide something?

Case

After we finished the circuitry and wiring, we needed a case that fits into the LEGO® world.

The analog inputs require an new type of jack, as LEGO® Technic only contains actors (motors, lights), as well as hard-wired switches. We chose plugs from rail transport modelling, as they are cheap, combinable, and well manageable even for small hands.

With a little drilling, the 2x1 hole bricks serve well as cases for those plugs, but even buttons and LEDs fit inside the holes. The outputs are put on top of the case.

These are some photos building and finishing the case and circuit prototype:

Cost

Building the prototype, we had the following cost: (complete parts list on GitHub)

  • Arduino Nano: 12€
  • Relays: 10€
  • MOSFETs: 5€
  • Misc. electronic parts: 25€
  • LEGO® case: 5€
  • Power Functions plug: 20€
  • Sum: About 80€

So we actually reached the goal to stay below 100€.

Software

Now that we have the hardware of the Leguino project, we aim for the software.

As already mentioned, a direct programming using Arduino C code is possible, but not too intuitive. Sometimes we need to address the pins for an output, and we have to memorize the inversion state (did we already set the relay correctly?). We also have to take care of other parallelly active outputs, and the design prevents the simultaneous activation of two outputs with different direction.

Besides it is nice to think in terms of connected devices, e.g.:

  • Light on resp. off
  • Motor forwards, stop, half thrust, backwards, etc.
  • Treads even require two outputs (as they have two motors):
    • Full thrust forwards resp. backwards
    • Half thrust to the left
    • Turn around on the spot
    • Stop

You even wish for time control like “2 seconds drive” or “light flashes every second”, without burdening the main loop with such control flow logic.

The same holds for the inputs. A strong API is desirable that supports queries like:

  • Has the button been pressed?
  • What is the position of the knob?
  • What is the distance to the obstacle (ultrasonic sensor)?

So we want to provide the following API regarding outputs (motors, lights, etc.):

class SingleActor : public Actor
{
	SingleActor(int8 output);
	uint16 getCurrentValue();
	Sequence * getSequence();
	void off();
	void on();
	void on(uint16 msecs);
	void setSequence(Sequence * sequence);
	void setValue(int8 value);
	void setValue(int8 value, uint16 msecs);
};

class Motor : public SingleActor
{
	Motor(int8 output);
	void reverse();
	void reverse(uint16 msecs);
};

class Tread : public Actor
{
	Tread(int8 leftOutput, int8 rightOutput);
	uint16 getCurrentLeftValue();
	uint16 getCurrentRightValue();
	void move(int8 direction, int8 thrust);
	void move(int8 direction, int8 thrust, uint16 msecs);
	void moveLeft(int8 thrust);
	void moveLeft(int8 thrust, uint16 msecs);
	void moveRight(int8 thrust);
	void moveRight(int8 thrust, uint16 msecs);
	void moveStraight(int8 thrust);
	void moveStraight(int8 thrust, uint16 msecs);
	void stop();
	void turnAroundLeft(int8 thrust);
	void turnAroundLeft(int8 thrust, uint16 msecs);
	void turnAroundRight(int8 thrust);
	void turnAroundRight(int8 thrust, uint16 msecs);
};

And this should be the API for sensors:

class SingleSensor : public Sensor
{
	SingleSensor(int8 input);
	uint16 getRawValue();
	virtual void update(uint16 timeStep);
};

class Switch : public SingleSensor
{
	Switch(int8 input);
	bool isOff();
	bool isOn();
	bool waitOff(uint16 timeout = 0);
	bool waitOn(uint16 timeout = 0);
};

class LightSensor : public SingleSensor
{
	LightSensor(int8 input);
	int16 getBrightness();
	bool waitBrighter(int16 value, uint16 timeout = 0);
	bool waitDarker(int16 value, uint16 timeout = 0);
};

class LightBarrier : public SingleSensor
{
	LightBarrier(int8 input);
	bool isClear();
	bool isHit();
	void setThresholds(int16 hitThreshold, int16 clearThreshold);
	bool waitClear(uint16 timeout = 0);
	bool waitHit(uint16 timeout = 0);
};

class DistanceSensor : public SingleSensor
{
	DistanceSensor(int8 input);
	int16 getDistanceCm();
	int16 getDistanceMm();
	bool waitFartherCm(int16 cm, uint16 timeout = 0);
	bool waitFartherMm(int16 mm, uint16 timeout = 0);
	bool waitNearerCm(int16 cm, uint16 timeout = 0);
	bool waitNearerMm(int16 mm, uint16 timeout = 0);
};

This is how a simple example program, that utilizes the APIs, could look like:

#include <Leguino.h>

DistanceSensor * entfernung;
Motor * motor;

void setup() {
	// Start Leguino (either with WAIT_FOR_PLAY or NO_WAIT).
	// This line must always be the first in the setup.
	leguino.setup(WAIT_FOR_PLAY);
	
	// Create sensors and assign them to connectors
	leguino.add(entfernung = new DistanceSensor(IN_1));

	// Create actors and assign them to connectors
	leguino.add(motor = new Motor(OUT_A));
}

void loop() {
	// Update Leguino. This line must always be the first in the loop.
	leguino.update();

	if (entfernung->getDistanceCm() > 50) {
		// When the distance in front of the sensor is more than 50cm, move forward.
		motor->on();
	} else if (entfernung->getDistanceCm() < 30) {
		// When the distance in front of the sensor is less than 30cm, move back.
		motor->reverse();
	} else {
		// Otherwise stop
		motor->stopp();
	}
}

More examples and a complete documentation of all actors and sensors methods are available on the GitHub-Projekt:

Project “Robot dog”

My son and I have built the following robot with Leguino. It has distance sensors and follows the object in front of him at a distance of 30cm, like a dog following its master.

Project “Sweets vending machine”

As a school charity project my son invented a machine that dispenses sweets for money. He presented it at two stands (at school and at a research institute), and the proceeds were used to safe bees.

Download and contribution

Want to build the project too?
Or do you want to contribute improvements, extensions, or photos?

All source code, circuits, and instructions are published open-source in the GitHub project:

Have fun downloading and forking!