Build a digital clock in Wireworld

Latching Clock

Score - 53,508 (of which only 36,828 is actively used due to the L-shaped design)

Clock Running

High Quality recording - https://1drv.ms/u/s!ArQEzxH5nQLKhvt_HHfcqQKo2FODLQ
Golly pattern - https://1drv.ms/u/s!ArQEzxH5nQLKhvwAmwCY-IPiBuBmBw

Guiding Principles -

  • Since this was my first time using a cellular automaton I avoided stringing together large premade components. One valid approach I did not take would have been a binary adder starting at zero and continuously adding one to the last output, followed by a binary to BCD converter, display demultiplexer, 7-segment decoder and 7-segment display.
  • It should be possible to cold start the clock. I imposed on myself the additional restriction that a single electron head placed at a specific conductor cell should correctly start the clock. I did not want to require careful manual synchronisation of many disparate flip-flops and individual timing elements before beginning the simulation.

Part I: The Minute Counter

Mathematics

Counting from 0 to 9 in binary (for the least significant minutes digit) goes as follows -

0 - 0000
1 - 0001
2 - 0010
3 - 0011
4 - 0100
5 - 0101
6 - 0110
7 - 0111
8 - 1000
9 - 1001

Reading that as columns, the least significant (2^0 units bit stream) goes 01010101, the 2^1 units stream goes 0011001100, the 2^2 units stream goes 0000111100 and the 2^3 units stream goes 0000000011.

The first one's easy - just flip-flip 01 forever. The third is a stream of four 1s, six 0s, phase shifted by six zeros. The fourth is a stream of eight 0s and two 1s.

The second is a bit harder as it's got a nasty asymmetry. However, I notice that (where . is concat operator):

0011001100 . 0011001100 = 0011001100 . NOT(1100110011) = 00110011001100110011 XOR 00000000001111111111 = 5(0011) XOR 00000000001111111111

(Incidentally, as alluded to later, the majority of my clock runs on a 60-beat ticker. The 00000000001111111111 double length wave is where the need for the 120-beat ticker comes in).

Design

The output streams from top to bottom go Units of Minutes (2^0, 2^1, 2^2, 2^3) then Tens of Minutes (2^0, 2^2, 2^1). Note that the bottom two wires are crossed.

Minute Counter Annotated

  1. 120-beat main clock.
  2. Where to place an electron for a cold start. Without any electron tail it splits in two directions but the diode immediately above catches one of these giving a nice cycling electron going around and round the 120-beat loop.
  3. 12-beat secondary clock.
  4. Coil of conductor + diode starts the secondary 12-beat clock. Words cannot describe how fiddly this little piece was to sync. You have to sync the 120 and 60 beat clocks, then sync in the 12-beat and frequency halver 24-beat pseudo clocks, followed by tying back the 24-beat clock to the 120-beat clock otherwise the XOR gate doesn't work.
  5. Phase shift.
  6. Flip-flop. A single electron on the input hits the set line first then after a very specific amount of time, hits the reset line giving precisely one pulse in, one pulse out.
  7. Adding humps here - on the reset line, increases the delay between set and reset on the flip-flop. Each extra hump gives an extra pulse. The flip-flop below has nine extra humps, so ten pulses between set and reset.
  8. XOR gate for my tricky 2^1 units of minutes line.
  9. AND-NOT gate and very specific part lengths means each electron pulse which comes past doubles back on itself and annihilates the electron behind. Frequency halver. Creates a 24-beat clock from the 12-beat secondary source.
  10. 60-beat secondary clock, which actually does most of the work. It's just easier to start a fast clock from a slower one, so the slowest clock (120-beats) is the master, even though it's barely used. The 60-beat clock is the heart of this thing.
  11. Feedback wire which carries electrons only when the 60-beat clock is ticking. It's used in conjunction with an AND-NOT gate to stop the clock being restarted repeatedly from the 120-beat master. Otherwise many horrible things happen & Ctrl-Z is saviour.
  12. The diode where the 60-beat clock is started from.
  13. This whole device is a flip flop, AND gate, and AND-NOT gate combined. It gives a latch. One pulse in starts it, one pulse in stops it.
  14. Loop of wire to calibrate the latch to 10 pulses on, 10 pulses off for a one in ten pulse input. Without it we get 12 pulses on, 8 pulses off. These ten on ten off latches form the basic components of the ten minute blocks in the same way the 6-micron (1 pulse) flip-flops formed the basic components of the minute units.
  15. The cold start initial pulse caused all sorts of problems including being two beats out of phase with the clocks it starts. This messes up the latches. This AND gate catches and disposes of out of sync pulses - in particular the starting pulse.
  16. This is a part of the design I somewhat regret in retrospect. It takes an electron, splits it into five and annihilates the five electrons behind, taking 111111 to 100000.
  17. This takes an electron and stitches it on the front. Two phases ahead to be precise. It takes 100000 and makes 101000. Combined with part 16 we get 111111 -> 100000 -> 101000. In retrospect I wish I'd done 111111 -> 101010 -> 101000; it would have achieved the same effect in less space.
  18. The above patterns are then pushed into the bottom latch to achieve 20 on, 40 off. This is split, half is phase shifted by 20 units, and then these form the two high order bit streams of the tens of minutes.

Part II: The Hour Counter

Explanation

The input to the hour counter is a single electron pulse, once an hour. The first step is to reduce this to a single electron pulse, once every twelve hours. This is achieved using several "latch & catch" primitives.

A "latch" is a 6-micron flip-flop connected to an AND-NOT and an AND gate to give a 6-micron on/off latch. A "catch" takes a continuous stream of electrons as input, allows the first through, then annihilates every other electron behind, until the stream ends at which point the catch resets.

Placing a latch, followed by a catch, in series, results in one electron in -> turns on the latch, one electron out the other end (rest caught by catch). Then second electron in -> turns off latch, catch silently resets. Net effect: first electron passes through, second electron is annihilated, and so on and so forth, irrespective of how long the delay is between those electrons.

Now chain two "latch & catch" in series, and you have only one in four electrons passing through.

Next, take a third "latch and catch", but this time embed an entire fourth latch and catch on the flip-flop SET line, between the AND-NOT gate and the flip-flop SET. I'll leave you to think about how this works, but this time only one in three electrons passes through, irrespective of how long the delay is between those electrons.

Finally, take the one in four electrons, and the one in three, combine them with an AND gate, and only one in twelve electrons pass through. This whole section is the messy squiggle of paths to the top left of the hour counter below.

Next, take the electron every twelve hours and split back into one every hour, but output each onto a different conductor wire. This is achieved using the long coiled conductor with thirteen exit points.

Take these electrons - one an hour down different conductors, and hit a flip-flop SET line. The RESET line on that same flip flop is then hit by the next hour's conductor, giving sixty pulses down each wire per hour.

Finally - take these pulses and pass them into seven and a half bytes of ROM (Read-Only Memory) to output the correct BCD bitstreams. See here for a more detailed explanation of WireWorld ROM: http://www.quinapalus.com/wires6.html

Design

Hour Counter Annotated

  1. One electron per hour input.
  2. First latch.
  3. First catch.
  4. "Latch & catch" embedded on an outer "latch & catch" SET line.
  5. AND gate.
  6. AM/PM latch (turned on/off once every twelve hours).
  7. Each loop of wire is 6x60=360 units long.
  8. Flip/Flop turned on its side to create a smaller profile.
  9. Seven and a half bytes of ROM.

Notes

  1. Due to its one electron per minute, 6-micron design, run the simulation at six generations per minute (one generation every 10 seconds) for a real-time clock.
  2. The AM/PM line is high (1) for AM, low (0) for PM. This might seem a slightly unusual way round to choose, but there is justification. During a cold start of the clock, the AM/PM line is naturally low (0) initially. As soon as the AM/PM line is pulled high (1), this indicates that the count has begun at 12:00AM. All output before this point should be disregarded, all output after this point is considered meaningful.

Useful Links

  • I learned the basics of WireWorld from http://www.quinapalus.com/wi-index.html. An excellent resource.

  • To create and simulate the cellular automaton I used Golly: http://golly.sourceforge.net/

  • I took the AND gate design from http://mathworld.wolfram.com/WireWorld.html

  • And I've only just found this webpage so didn't use it but it looks great: http://karlscherer.com/Wireworld.html


Binary counter clock (score = 134×33 = 4422 134×32 = 4288)

As promised 2⅔ years ago. Actual building time from scratch was less than 3 days.

I took inspiration from several sources, but in the end everything is custom made, and no cell is copied from any design by someone else. It makes royal use of the colors of WireWorldMarked (also by me, and supported by default by Golly). The colors are just eye-candy and don't change the rules of WireWorld.

This uses 10 micron technology (electrons spaced 10 gens apart). This makes small loops (memory cells for example) larger, however it allows not only for easier calculations but also compact axis-aligned ROMs and decoders, and it's the maximum throughput of a 'cross' gate. Everything makes use of 'negative' logic, where the presence of electrons is a logical '0' and missing electrons are '1'. In this sense, an OR gate, for example, does A=0 OR b=0 -> output 0. The sole reason for this is that it makes the binary counters, and in particular, the reset logic, much less complex.

The clock's real-time speed is 480 gens per minute (large green loop) which translates to 0.125s per gen or 8 gens/s. The loop expands the bounding box by 1 in the vertical direction, but with any smaller number the carries propagate too slow and it never displays the correct time. Unfortunately it will never be real-time in Golly because Golly only applies a delay between gens, and with a non-negligible duration of calculating a gen, it will run slow if you set the delay to 0.125. In fact, the speed appears to be non-constant and fluctuates by several percents.

[edit] This is v2 in which I've synchronized the display segments. This way the maximum delay between the final change of the leftmost display and the next change of the rightmost display (between 24:00->0:00) is 170 gens. The V1 display update took 500 gens, longer than 480 gens (a 'minute'), oops. I was about to boast how good I am for squeezing a loop of 440 in a box 1 less tall, then choosing 400 gens because it divides a minute nicely, but I wanted a significantly higher number, and was done waiting until someone pointed out "hey why don't you just use a smaller loop and some dividers?". So I used three dividers and a loop of 150 gens for a rate of 1200 gens/min (20Hz). The squeezed loop is still visible in the explanation image below.

Clock part A v2.png Clock all v2.png

High-speed animation at 7200 gens/s (1.2MiB)
Real-time animation of only the display (600KiB)
(animations are of v1 but still give an impression of the clock)

Extra explanations and some dividers before final space-optimizations:

(the gray arrows point to cells in the middle of the machinery which are either present or absent, depending on if that bit should contribute to the reset logic.)

Clock explanations.png

Golly pattern with everything I made until now: Clock (Mark Jeronimus).mc


Delay line memory - 51 x 2880 = 146880

Image

Zoomed out:

Image

Output comes out the top of each loop.

I put all the states directly on the wire with this lua, letting golly step the electrons forward between bits so we don't have to follow the wire with a cursor.

I used this naive method to set a bar and crash course wireworld, golly and lua.

local g = golly()

local minutes_in_day = 1440 -- 60x24
local interval = 4 -- how often to send electrons

local function bcd4(num)
    num=math.floor(num)
    local t={}
    for b=4,1,-1 do
        t[b]=math.floor(math.fmod(num,2))
        num=(num-t[b])/2
    end
    return table.concat(t)
end

local function makewire(x,y1,y2)
    for y1=1,y2 do g.setcell(x,y1,3) end
end

local function makeloop(x,y,size)
    local len = size/2 - 1
    makewire(x,y+1,len); makewire(x+2,y+1,len) -- main wires
    g.setcell(x+1,y,3); g.setcell(x+1,y+len,3) -- endcape
end

local function paint(x,y,pattern)
    for v in string.gmatch(pattern,".") do
        if v=="1" then g.setcell(x, y, 1); g.setcell(x, y-1, 2) end
        x = x + 4
    end
    g.show(pattern);g.update() -- slows things down but more interesting to watch
    for i=1,interval do g.step() end
end

for x=0,63,4 do makeloop(x,0,minutes_in_day * interval) end

for hour = 0,23 do
      for minute = 0,59 do
         paint( 0, 2, bcd4(hour/10) .. bcd4(hour%10) .. bcd4(minute/10) .. bcd4(minute%10) )
      end
end

For testing I added these top wires and watched their tips.

Imgur

Here's the script to collect the 4 sets of 4 wire BCD to eyeball.

-- watches 16 wires spaced 4 apart starting at (0,-4)
local ticks = 1440 -- set to match the length of your 24 hour loop
local g = golly()
local output = ""
local nums = {  ["0000"] = "0", ["0001"] = "1", ["0010"] = "2", ["0011"] = "3", ["0100"] = "4",
                ["0101"] = "5", ["0110"] = "6", ["0111"] = "7", ["1000"] = "8", ["1001"] = "9",
                ["1010"] = "A", ["1011"] = "B", ["1100"] = "C", ["1101"] = "D", ["1110"] = "E",
                ["1111"] = "F" } -- full set in case we have errors (i did)

for i=0,ticks,1 do
   local text = ""
   for i=0,48,16 do -- set your X here, change the 0 and 48
       local word = ""
       for j=0,15,4 do
            local bit = g.getcell(i+j,-4) -- set your Y here, change -4
            if bit == 0 or bit == 3 then word = word .. "0" else word = word .. "1" end
       end
       text = text .. nums[word]
   end
   g.show(text); output = output..' '..text
   g.update(); g.step();g.step();g.step();g.step()
end
g.note(output)

Final answer requires pruning the always-zero lines and routing the rest to their correct BCD inputs.