Shannon's Ultimate Machine, take 2

The original Ultimate Machine is famously attributed to Claude Elwood Shannon, one of the fathers of modern computing. In "Voice Across the Sea", Arthur C. Clarke describes Shannon's creation the following way:

"Nothing could be simpler. It is merely a small wooden casket, the size and shape of a cigar box, with a single switch on one face. When you throw the switch, there is an angry, purposeful buzzing. The lid slowly rises, and from beneath it emerges a hand. The hand reaches down, turns the switch off and retreats into the box. With the finality of a closing coffin, the lid snaps shut, the buzzing ceases and peace reigns once more. The psychological effect, if you do not know what to expect, is devastating. There is something unspeakably sinister about a machine that does nothing - absolutely nothing - except switch itself off."

A video of the original design can be found here.

Several dozen hobbyists built their own Ultimate Machines - the concept even made it into a recent edition of Make; my own previous take is documented here. That said, I always wanted to see something true to Clarke's account: elegant, simple, and chillingly sinister - and all the prior attempts seemed to fall short, mine included. So, here is a yet another try, with this specific goal in mind.

1. Parts inventory

The centerpiece of this iteration is a cheap but nice cigar box (20 x 15 x 8 cm), purchased on the net for about $17.

The remaining electronic components are:

Most of these components were selected for their aesthetics or immediate availability alone, and can be substituted freely.

All the mechanical components - including the hand that toggles the switch, the cam that lifts the lid, or the bezel of the LED display - were machined on my Roland MDX-540 CNC mill in RenShape 460. Flexible molds were then cast in ShinEtsu KE-1310ST, and the final components made out of IPI IE-3075 and OC-7086. If you want to learn more, a very detailed description of this manufacturing process is given here.

2. Circuitry and software

The circuit is based on a very straightforward use of ATmega48P. The microcontroller has a single input - a two-position toggle switch protruding from the box - and several outputs: The entire circuit looks as follows:

The whole device is powered with a 5V, 2A DC adapter, which gives ample head room for driving both motors at once. A cheaper 1A supply should be just as sufficient for non-simultaneous operation with these motors, however.

The basic algorithm for the MCU is as follows:

  1. Load cycle count from EEPROM,
  2. Send cycle count to the LED display,
  3. Retract motor 2 (hand actuator) for a time needed to hit the mechanical limiter,
  4. Retract motor 1 (lid cam) for a time needed to hit the mechanical limiter,
  5. Turn off internal LEDs,
  6. Wait for the switch to be toggled,
  7. Turn on internal LEDs,
  8. Extend motor 1 (lid cam) for a time needed to hit the mechanical limiter,
  9. Extend motor 2 (hand actuator) for a time needed to toggle the switch,
  10. If switch not toggled as expected, display '????', wait for user help,
  11. Increase cycle count, store in EEPROM,
  12. Go to 2.
Some additional steps to vary motor speed, implement response delays, and bail out early if switch is toggled again, are also present in the code; the actual implementation looks the following way: #define F_CPU 1000000UL #include <avr/io.h> #include <avr/sleep.h> #include <avr/interrupt.h> #include <avr/eeprom.h> #include <util/delay.h> /************************** * User-friendly typedefs * **************************/ typedef int8_t s8; typedef uint8_t u8; typedef int16_t s16; typedef uint16_t u16; typedef int32_t s32; typedef uint32_t u32; /************** * MCU pinout * **************/ #define B_SWITCH 0 /* Toggle switch input (0 when pointing up) */ #define B_LED0 1 /* Internal illumination LED #1 */ #define B_LED1 2 /* Internal illumination LED #2 */ #define C_A0 0 /* LED display address line 0 */ #define C_A1 1 /* LED display address line 1 */ #define C_DATAx4 2 /* LED display data bits 0-3 (4-7 set to 011) */ #define D_LID_DOWN 0 /* Lid motor: polarity #1 */ #define D_LID_UP 1 /* Lid motor: polarity #2 */ #define D_ARM_OUT 2 /* Arm motor: polarity #1 */ #define D_ARM_IN 3 /* Arm motor: polarity #2 */ #define D_SP_SM 4 /* Motor speed: small push-pull resistor */ #define D_SP_LG 5 /* Motor speed: large push-pull resistor */ /**************** * Timings (ms) * ****************/ #define LID_CLOSE 2200 /* Time to fully close lid (1.2V) */ #define LID_OPEN 1400 /* Time to open lid (2.8V) */ #define ARM_RETRACT 1800 /* Time to retract arm (1.2V) */ #define ARM_EXTEND_S 1600 /* Time to extend arm (1.2V) */ #define ARM_EXTEND_F 200 /* Time to extend arm (3.3V) */ #define START_DELAY 1000 /* Switch toggle response delay */ #define ACTION_DELAY 200 /* Delay between movement steps */ #define DISPLAY_DELAY 500 /* Count increment delay */ #define BRAKE_TIME 10 /* Motor braking time */ /************************** * DLO3416 output routine * **************************/ void display_number(u16 num) { u8 d1, d2, d3, d4; d1 = (num % 10) << C_DATAx4; d2 = ((num / 10) % 10) << C_DATAx4; d3 = ((num / 100) % 10) << C_DATAx4; d4 = ((num / 1000) % 10) << C_DATAx4; /* This ordering is intentional, to avoid glitches when flipping two address bits at once (WR- is always pulled down in the circuit). */ PORTC = d3 | 0; _delay_us(2); PORTC = d1 | 0; _delay_us(2); PORTC = d1 | _BV(C_A0); _delay_us(2); PORTC = d2 | _BV(C_A0); _delay_us(2); PORTC = d2 | _BV(C_A0) | _BV(C_A1); _delay_us(2); PORTC = d4 | _BV(C_A0) | _BV(C_A1); _delay_us(2); PORTC = d4 | _BV(C_A1); _delay_us(2); PORTC = d3 | _BV(C_A1); _delay_us(2); } void display_qmarks(void) { PORTC = ('?' << C_DATAx4) | 0; _delay_us(2); PORTC = ('?' << C_DATAx4) | _BV(C_A0); _delay_us(2); PORTC = ('?' << C_DATAx4) | _BV(C_A0) | _BV(C_A1); _delay_us(2); PORTC = ('?' << C_DATAx4) | _BV(C_A1); _delay_us(2); } /****************** * EEPROM counter * ******************/ u16 cycle_count; void load_count() { cycle_count = eeprom_read_word((const u16*)0x1); } void store_count() { eeprom_write_word((u16*)0x1, cycle_count); } /************************* * Motor output routines * *************************/ void retract_arm() { PORTD = _BV(D_ARM_IN) | 0; /* 1.2V */ _delay_ms(ARM_RETRACT); PORTD = 0; _delay_ms(BRAKE_TIME); } void extend_arm() { PORTD = _BV(D_ARM_OUT) | 0; /* 1.2V */ _delay_ms(ARM_EXTEND_S); PORTD = _BV(D_ARM_OUT) | _BV(D_SP_SM); /* 3.3V */ _delay_ms(ARM_EXTEND_F); PORTD = 0; _delay_ms(BRAKE_TIME); } void close_lid() { /* Anti-seize: */ PORTD = _BV(D_LID_UP) | _BV(D_SP_LG); /* 2.8V */ _delay_ms(50); PORTD = _BV(D_LID_DOWN) | 0; /* 1.2V */ _delay_ms(LID_CLOSE); PORTD = 0; _delay_ms(BRAKE_TIME); } void open_lid() { PORTD = _BV(D_LID_UP) | _BV(D_SP_LG); /* 2.8V */ _delay_ms(LID_OPEN); PORTD = 0; _delay_ms(BRAKE_TIME); } void led_on() { PORTB = _BV(B_SWITCH) | _BV(B_LED0) | _BV(B_LED1); } void led_off() { PORTB = _BV(B_SWITCH); } /*************** * Entry point * ***************/ int main() { CLKPR = _BV(CLKPCE); /* Enable clock prescaler change */ CLKPR = 0b00000011; /* Clock prescaler: 8 (1 MHz) */ PCMSK0 = _BV(PCINT0); /* Interrupt on pin 14 (PB0) */ PCICR = _BV(PCIE0); /* Enable interrupt 0 */ /* Pin configuration: */ DDRB = 0b11111110; DDRC = 0b11111111; DDRD = 0b11111111; PORTB = _BV(B_SWITCH); load_count(); display_number(cycle_count); retract_arm(); close_lid(); led_off(); sei(); set_sleep_mode(SLEEP_MODE_IDLE); sleep_enable(); while (1) sleep_cpu(); } /**************************** * Switch interrupt handler * ****************************/ ISR(PCINT0_vect, ISR_BLOCK) { /* Allow switch bounce... */ _delay_ms(50); /* We only care about falling edge events. */ if (PINB & _BV(B_SWITCH)) return; led_on(); _delay_ms(START_DELAY); open_lid(); _delay_ms(ACTION_DELAY); /* Switch toggled back? Retreat. */ if (PINB & _BV(B_SWITCH)) { close_lid(); led_off(); return; } extend_arm(); /* Switch still enabled? Display '????' and wait for user assistance. */ if (!(PINB & _BV(B_SWITCH))) { display_qmarks(); while (!(PINB & _BV(B_SWITCH))); } led_off(); _delay_ms(ACTION_DELAY); retract_arm(); _delay_ms(ACTION_DELAY); close_lid(); /* Update EEPROM, LED display */ _delay_ms(DISPLAY_DELAY); cycle_count++; display_number(cycle_count); store_count(); }

3. Mechanical design

The gear motors used in the project nominally run at 5V and deliver 60 RPM, with stall torque of 3,300 g/cm². Taking driver voltage drop into account, the maximums attainable in this circuit are about 42 RPM, with 2,300 g/cm² at stall; the minimums (with both speed control outputs set to low) are about 12 RPM and 600 g/cm². Therefore, there is no need for any additional transmission; a simple cam attached directly to the motor lifts the lid; then, to toggle the switch, an arm simply rotates in place, toggling the switch about 5 cm away from the center of rotation. The force needed for the switch is about 250 gf, well within the 450 gf limit afforded by the motor at full speed:

4. Final result

The finished box looks like this:

You can see a crappy video of the box in action here:

The box currently resides in our Google office, and the counter reads over 200 cycles today.

5. Questions? Comments?

You can reach me at lcamtuf@coredump.cx.

Your lucky number: 16078591