Factory 1: Mining drill, on an Arduino Uno
A drill digs ore and fills a hopper on a TFT. Press the button to unload it into the shipped counter, or watch the drill stop when the hopper is full. The whole build, an Arduino Uno and 5 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Intermediate About 30 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $37. On Mokxi they're free.
What you'll learn
- Displays
- Games
- Sound
- Choosing and reading a resistor
- How an LED lights and why it needs a resistor
- Difficulty: Intermediate
- Time: About 30 minutes
- Board: Arduino Uno R3
- Parts: 9
How it works
A drill digs ore and fills a hopper on a TFT. Press the button to unload it into the shipped counter, or watch the drill stop when the hopper is full.
Arduino Uno R3 runs the sketch below. The circuit around it: TFT display, 240x320, Piezo speaker, Resistor, 220 Ω, LED, green, Pushbutton. pinMode sets BUTTON_PIN as an input with its pull-up on. digitalWrite drives LED_PIN HIGH. digitalRead reads whether b.pin is HIGH or LOW. Serial.begin opens the serial port at 9600 baud. tone plays a note on PIEZO_PIN.
Parts list
9 parts, plus the jumper wires. They retail for about $37; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × TFT display, 240x320In Handheld Console Kit.
- 1 × Piezo speakerIn Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Resistor, 220 ΩIn Starter Kit, Claw Machine Kit, Halloween Prop Kit.
- 1 × LED, greenIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 1 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
- 3 × Power, 5 V
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| TFT display, 240x320 pin SCK | Arduino Uno R3 pin 13 |
| TFT display, 240x320 pin MOSI | Arduino Uno R3 pin 11 |
| TFT display, 240x320 pin CS | Arduino Uno R3 pin 10 |
| TFT display, 240x320 pin DC | Arduino Uno R3 pin 9 |
| Piezo speaker pin 1 | Arduino Uno R3 pin 6 |
| Resistor, 220 Ω pin 1 | Arduino Uno R3 pin 3 |
| Pushbutton pin 1a | Arduino Uno R3 pin 2 |
| Arduino Uno R3 pin GND | Ground |
| TFT display, 240x320 pin VCC | 5 V |
| TFT display, 240x320 pin GND | Ground |
| TFT display, 240x320 pin RST | 5 V |
| TFT display, 240x320 pin LED | 5 V |
| Piezo speaker pin 2 | Ground |
| LED, green pin A | Resistor, 220 Ω pin 2 |
| LED, green pin C | Ground |
| Pushbutton pin 2a | Ground |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Factory 1: Mining drill. The first machine of the Factory world series.
//
// A drill digs ore out of a patch and drops it into a hopper. The hopper holds
// eight lumps. When it is full the drill has nowhere to put the next lump and
// stops, which is the first thing every factory teaches you: a machine that
// cannot hand its work on has to wait. Press the button to unload the hopper
// into the shipped counter and the drill gets back to work.
//
// Wiring, board to parts:
// TFT CS -> pin 10 DC -> pin 9 SCK -> pin 13 MOSI -> pin 11
// RST and LED -> 5 V, VCC -> 5 V, GND -> GND
// Unload button: pin 2 to GND (the pin's own pull-up holds it high)
// Status LED: pin 3 through 220 ohm to GND
// Piezo: pin 6 to GND
//
// # What this teaches
//
// 1. A state machine. The drill is always in exactly one state, DRILLING or
// FULL, and each pass of loop() asks "what does this state do, and what
// makes it change?". Nothing else decides what the drill does.
// 2. Timing without delay(). Drilling takes DRILL_MS. Instead of freezing the
// board for that long, the sketch remembers when the lump started and asks
// millis() how long ago that was, so the button stays alive the whole time.
// 3. Debouncing. A switch chatters for a few milliseconds when it closes, so
// the sketch waits for the pin to settle before it believes a press.
// 4. Drawing only what changed. A TFT on an Uno is slow, so the world is
// painted once and after that only the tiles that change are touched.
//
// # The world
//
// The picture is a grid of 16 pixel tiles, 14 across and 10 down. A tile is
// named by its column and row, and tileX() and tileY() turn that into pixels.
#include "mokxi_ili9341.h"
const uint8_t TFT_CS = 10;
const uint8_t TFT_DC = 9;
const uint8_t TFT_SCK = 13;
const uint8_t TFT_MOSI = 11;
const uint8_t BUTTON_PIN = 2;
const uint8_t LED_PIN = 3;
const uint8_t PIEZO_PIN = 6;
SoftSpi spi(TFT_SCK, TFT_MOSI, TFT_CS);
Ili9341 tft(spi, TFT_DC);
const int16_t TILE = 16;
const int16_t WORLD_X = 8;
const int16_t WORLD_Y = 8;
const uint8_t COLS = 14;
const uint8_t ROWS = 10;
// The panel powers up black, so black is the ground: nothing needs painting
// until something is built on it, which saves repainting 36,000 pixels at start.
const uint16_t GROUND = ILI9341_BLACK;
const uint16_t ROCK = 0x4a49;
const uint16_t ORE = 0xfc60;
const uint16_t STEEL = 0x8c71;
const uint16_t DARK = 0x10a2;
const uint16_t GOOD = ILI9341_GREEN;
const uint16_t BAD = ILI9341_RED;
// Where things stand in the world.
const uint8_t DRILL_COL = 5;
const uint8_t DRILL_ROW = 4;
const uint8_t CHUTE_COL = 6;
const uint8_t HOPPER_COL = 8; // the hopper is two tiles wide ...
const uint8_t HOPPER_ROW = 3; // ... and four tall, eight slots in all
const uint8_t HOPPER_SLOTS = 8;
const unsigned long DRILL_MS = 1200; // one lump takes this long to dig
const unsigned long CHUTE_MS = 180; // how long the chute flashes
// A tactile switch does not close once: the contacts bounce for a few
// milliseconds, so a raw read of the pin can see several presses from one
// finger. This waits until the pin has held the same level for 20 ms, then
// reports the press exactly once.
struct Button {
uint8_t pin;
bool raw; // what the pin read last time round
bool stable; // the settled state
unsigned long since; // when `raw` last changed
};
static bool pressedOnce(Button &b) {
bool down = digitalRead(b.pin) == LOW;
unsigned long now = millis();
if (down != b.raw) {
b.raw = down;
b.since = now;
}
if (now - b.since >= 20 && b.stable != b.raw) {
b.stable = b.raw;
return b.stable;
}
return false;
}
enum DrillState { DRILLING, FULL };
DrillState state = DRILLING;
unsigned long lumpStarted = 0; // millis() when the current lump began
uint8_t progress = 0; // 0..16, how much of the lump is dug
uint8_t hopper = 0; // lumps waiting in the hopper
uint16_t shipped = 0; // lumps unloaded so far
unsigned long chuteUntil = 0; // the chute glows until this time
bool chuteLit = false;
bool bitFrame = false; // which of the two drill-bit pictures
unsigned long bitSwitched = 0;
Button unloadButton = {BUTTON_PIN, false, false, 0};
unsigned long ledToggled = 0;
bool ledOn = false;
static int16_t tileX(uint8_t col) { return (int16_t)(WORLD_X + col * TILE); }
static int16_t tileY(uint8_t row) { return (int16_t)(WORLD_Y + row * TILE); }
static void fillTile(uint8_t col, uint8_t row, uint16_t colour) {
tft.fillRect(tileX(col), tileY(row), TILE, TILE, colour);
}
// A square drawn inside a tile, `pad` pixels in from every edge.
static void insetTile(uint8_t col, uint8_t row, int16_t pad, uint16_t colour) {
tft.fillRect((int16_t)(tileX(col) + pad), (int16_t)(tileY(row) + pad), (int16_t)(TILE - 2 * pad),
(int16_t)(TILE - 2 * pad), colour);
}
static void drawOreTile(uint8_t col, uint8_t row) {
fillTile(col, row, ROCK);
// Two specks of ore. Their spot depends on the tile so the patch looks natural.
int16_t a = (int16_t)(2 + (col * 5 + row * 3) % 6);
int16_t b = (int16_t)(3 + (col * 3 + row * 7) % 6);
tft.fillRect((int16_t)(tileX(col) + a), (int16_t)(tileY(row) + b), 4, 4, ORE);
tft.fillRect((int16_t)(tileX(col) + 12 - a / 2), (int16_t)(tileY(row) + 12 - b / 2), 3, 3, ORE);
}
// The drill is a steel box with a bit that turns. Two pictures, swapped over
// and over, make it look like it is spinning.
static void drawDrill(bool working, bool frame) {
fillTile(DRILL_COL, DRILL_ROW, STEEL);
insetTile(DRILL_COL, DRILL_ROW, 2, DARK);
uint16_t bit = working ? ORE : BAD;
if (frame) {
tft.fillRect((int16_t)(tileX(DRILL_COL) + 3), (int16_t)(tileY(DRILL_ROW) + 7), 10, 2, bit);
} else {
tft.fillRect((int16_t)(tileX(DRILL_COL) + 7), (int16_t)(tileY(DRILL_ROW) + 3), 2, 10, bit);
}
}
// A thin bar under the drill that fills as the lump is dug.
static void drawProgress() {
int16_t x = tileX(DRILL_COL);
int16_t y = (int16_t)(tileY(DRILL_ROW + 1) + 4);
tft.fillRect(x, y, TILE, 5, DARK);
if (progress > 0) {
tft.fillRect(x, y, progress, 5, state == FULL ? BAD : GOOD);
}
}
static void drawChute(bool lit) {
fillTile(CHUTE_COL, DRILL_ROW, GROUND);
fillTile(CHUTE_COL + 1, DRILL_ROW, GROUND);
uint16_t c = lit ? ORE : STEEL;
tft.fillRect(tileX(CHUTE_COL), (int16_t)(tileY(DRILL_ROW) + 6), (int16_t)(2 * TILE), 4, c);
}
// Slot 0 is the bottom-left of the hopper, slot 1 the bottom-right, and so on up.
static void drawSlot(uint8_t slot) {
uint8_t col = (uint8_t)(HOPPER_COL + (slot & 1));
uint8_t row = (uint8_t)(HOPPER_ROW + 3 - (slot >> 1));
fillTile(col, row, STEEL);
insetTile(col, row, 2, slot < hopper ? ORE : DARK);
}
static void drawHopper() {
for (uint8_t i = 0; i < HOPPER_SLOTS; i++) {
drawSlot(i);
}
}
// Text is the slowest thing this board draws: every letter is a dozen small
// rectangles, and each rectangle is its own trip down the wire. So the words
// are painted once, and afterwards only the character or two that changed.
static void drawLabels() {
tft.text(8, 176, "HOPPER", ILI9341_WHITE, ILI9341_BLACK, 2);
tft.text(8 + 8 * 12, 176, "/8", ILI9341_WHITE, ILI9341_BLACK, 2);
tft.text(8, 198, "SHIPPED", ILI9341_WHITE, ILI9341_BLACK, 2);
}
static void drawCounts() {
tft.number(8 + 7 * 12, 176, hopper, ILI9341_YELLOW, ILI9341_BLACK, 2, 1);
tft.number(8 + 8 * 12, 198, shipped, ILI9341_YELLOW, ILI9341_BLACK, 2, 4);
}
static void drawStatus() {
tft.text(8, 220, state == FULL ? "FULL!" : "DIG ", state == FULL ? BAD : GOOD, ILI9341_BLACK, 2);
}
static void drawWorld() {
tft.rect(WORLD_X - 3, WORLD_Y - 3, COLS * TILE + 6, ROWS * TILE + 6, ROCK, 2);
// The ore patch: a 4 by 4 block of rock with ore in it.
for (uint8_t c = 1; c < 5; c++) {
for (uint8_t r = 3; r < 7; r++) {
drawOreTile(c, r);
}
}
drawChute(false);
drawHopper();
// A little crate for the shipped lumps, so the unload has somewhere to go.
fillTile(12, 5, STEEL);
insetTile(12, 5, 3, DARK);
drawDrill(true, false);
drawProgress();
drawLabels();
drawCounts();
drawStatus();
}
static void beep(unsigned int hz, unsigned int ms) { tone(PIEZO_PIN, hz, ms); }
static void report() {
Serial.print("hopper ");
Serial.print(hopper);
Serial.print(" shipped ");
Serial.println(shipped);
}
// The drill finished a lump: move it to the hopper.
static void lumpDone() {
// Tell the serial monitor first: it is instant, and the picture is not.
hopper++;
progress = 0;
lumpStarted = millis();
chuteUntil = lumpStarted + CHUTE_MS;
report();
drawSlot((uint8_t)(hopper - 1));
drawCounts();
beep(880, 40);
}
static void unload() {
if (hopper == 0) {
return;
}
shipped = (uint16_t)(shipped + hopper);
hopper = 0;
report();
beep(440, 80);
drawHopper();
if (state == FULL) {
state = DRILLING;
lumpStarted = millis();
progress = 0;
drawDrill(true, bitFrame);
drawProgress();
drawStatus();
}
drawCounts();
}
void setup() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
tft.begin();
drawWorld();
lumpStarted = millis();
report();
}
void loop() {
unsigned long now = millis();
// The button: one press, one unload, however long it is held.
if (pressedOnce(unloadButton)) {
unload();
}
// The state machine.
if (state == DRILLING) {
// How far through the lump are we, in sixteenths? millis() does the timing.
uint8_t now16 = (uint8_t)(((now - lumpStarted) * TILE) / DRILL_MS);
if (now16 > TILE) {
now16 = TILE;
}
if (now16 != progress) {
progress = now16;
drawProgress();
}
if (now - lumpStarted >= DRILL_MS) {
if (hopper < HOPPER_SLOTS) {
lumpDone();
} else {
state = FULL;
beep(220, 200);
drawDrill(false, bitFrame);
drawProgress();
drawStatus();
}
}
// Spin the bit.
if (now - bitSwitched >= 150) {
bitSwitched = now;
bitFrame = !bitFrame;
drawDrill(true, bitFrame);
}
}
// The chute glows for a moment after each lump.
bool lit = now < chuteUntil;
if (lit != chuteLit) {
chuteLit = lit;
drawChute(lit);
}
// The LED: steady while drilling, blinking when the hopper is full.
if (state == DRILLING) {
digitalWrite(LED_PIN, HIGH);
} else if (now - ledToggled >= 250) {
ledToggled = now;
ledOn = !ledOn;
digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
}
}
Code walkthrough
The key functions of the sketch, and what each one does.
setup sketch.ino
Runs once when the board starts. It gets the pins and libraries ready.
void setup() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
tft.begin();
drawWorld();
lumpStarted = millis();
report();
}loop sketch.ino
Runs over and over for as long as the board has power. This is where the project does its work.
void loop() {
unsigned long now = millis();
// The button: one press, one unload, however long it is held.
if (pressedOnce(unloadButton)) {
unload();
}
// The state machine.
if (state == DRILLING) {
// How far through the lump are we, in sixteenths? millis() does the timing.
uint8_t now16 = (uint8_t)(((now - lumpStarted) * TILE) / DRILL_MS);
if (now16 > TILE) {
now16 = TILE;
}
if (now16 != progress) {
progress = now16;
drawProgress();
}
if (now - lumpStarted >= DRILL_MS) {
if (hopper < HOPPER_SLOTS) {
lumpDone();
} else {
state = FULL;
beep(220, 200);
drawDrill(false, bitFrame);
drawProgress();
drawStatus();
// ... 26 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The screen stays white or black. TFT display
Check the SPI wiring: CS, DC, RST, MOSI, SCK. Power the module at 3.3 V logic, or use level shifting on 5 V boards. Call begin() and set the rotation. - Slow drawing or garbled colors. TFT display
Use hardware SPI pins and avoid redrawing the whole screen. Fill only changed areas. Confirm the library matches ILI9341 and the color order. - A passive piezo stays silent when you just set the pin HIGH. Piezo speaker
A passive piezo needs a changing signal. Use tone(pin, frequency) or a PWM wave. Around 2 to 4 kHz is loudest. A buzzer with a built in oscillator makes sound from plain DC instead. - Sound is very quiet or the pin seems overloaded. Piezo speaker
Drive the piezo from a pin directly, or through a 100 ohm resistor. For a bigger buzzer, switch it with a transistor. Check that you wired the right two legs and joined grounds. - The LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure. - A pull-up or pull-down resistor is missing, so the input floats and reads random values. Resistor
Tie the input to 5 V or ground through about 10k. Or use INPUT_PULLUP and wire the switch to ground. - The LED does not light, or it lights once and then dies. LED
LEDs have polarity. The longer leg (anode) goes toward the positive side. Always add a series resistor, about 220 ohms to 1 k ohm on a 5 V pin. - A blue or white LED stays dark on 3.3 V. LED
Blue and white LEDs drop about 3 V, so 3.3 V leaves almost nothing for the resistor. Use a 5 V supply, or a smaller resistor and a red or green LED.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.