Volume 1 - Complex programs

The programs in this sections are re-using concepts and techniques that are introduced in the ref:section about the basics <vol_0>.

Implement A Stack

What use is a stack within a CPU? And what if a CPU doesn’t have one?

Out of the box, the Digirule2 does not have an internal stack. It is however possible, to implement one and this implementation relies heavily on the indirect copy snippet that was presented earlier.

 1# Simple stack & operations
 2
 3# A stack is a data structure in which 
 4# all operations (Read & Write) take 
 5# place through its "head" pointer.
 6
 7# In ASM, it looks like an array (e.g stack[n])
 8# but unlike an array, "n" IS NOT specified freely 
 9# but through two simple rules:
10
11# 1 - Increase n AFTER Write
12# 2 - Decrease n BEFORE Read
13
14# Thus "n" always points to the next available 
15# value, at the TOP of the stack. 
16# Because of this, the Read and Write ops are more 
17# descriptively known as PUSH and POP.
18
19# Stacks are also known as First-In-First-Out 
20# or FIFO lists.
21
22# In this demo, we demonstrate this "FIFO" logic, by 
23# swapping the contents of two variables by:
24# PUSH R0
25# PUSH R1  
26# POP R0  
27# POP R1   
28
29.EQU status_reg=252 # The status register on the Digirule2
30.EQU zero_bit=0  
31
32# Initialise the stack head
33COPYLR stack stack_ptr
34
35# To push, point f_from to the variable to push to the 
36# head of the stack and call f_push
37# PUSH R0
38COPYLR R0 f_from
39CALL f_push
40
41# PUSH R1
42COPYLR R1 f_from
43CALL f_push
44
45# To pop, point f_to to the variable to pop the head of
46# stack to and call f_pop.
47# POP R0 
48# Which now receives the value of R1. >>FIFO<<
49COPYLR R0 f_to
50CALL f_pop
51
52# POP R1 
53# Which now receives the value of R0.
54COPYLR R1 f_to
55CALL f_pop
56HALT
57
58# Pushes the value of whatever f_from points to, to 
59# the top of the stack.
60f_push:
61COPYRR stack_ptr f_to
62CALL f_copy_ind
63INCR stack_ptr
64RETURN
65
66# Pops the value of the top of the stack to whatever f_to 
67# points to.
68f_pop:
69COPYRA stack_ptr
70SUBLA stack
71BCRSS zero_bit status_reg # NOTE THIS CHECK. It prevents >>UNDERFLOW<<
72DECR stack_ptr
73COPYRR stack_ptr f_from
74CALL f_copy_ind
75RETURN
76
77# Memory copy by indirect addressing via self-modification.
78# We construct a suitable absolute
79# addressing copy instruction (COPYRR) and
80# execute it as a sub-routine over f_from, f_to
81f_copy_ind:
82.DB 7
83f_from:
84.DB 0
85f_to:
86.DB 0
87RETURN
88
89R0:
90.DB 0xF0
91R1:
92.DB 0x0F
93stack:
94.DB 0xF0,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0x0F
95stack_ptr:
96.DB 0

Function calling conventions

The Digirule’s instruction set includes a CALL operation. It stores the value of the Program Counter (PC) to the internal stack and transfer execution to a different point in memory. Execution returns to the calling point once a RETURN or RETLA is encountered.

But, calling functions with an arbitrary number of arguments is slightly different and relies heavily on the CPUs stack.

This little demo shows a cdecl(ish) call convention to swap two numbers.

 1# Function calling and call conventions
 2
 3# Stacks allow us to define functions with 
 4# many different arguments.
 5
 6# This brings about the question of how to load 
 7# and clear the stack before and after a function 
 8# call.
 9
10# There are different ways to do this which are 
11# collectively known as "Calling Conventions".
12
13# Here, we are demonstrating a cdecl inspired 
14# scheme, adapted to the Digirule ASM.
15
16# In a higher level language, this demo would be
17# written as:
18# unsigned short f_add_two_numbers(unsigned short R0, unsigned short R1){
19#    return R0 + R1
20# }
21# ...
22# ...
23# R0 = f_add_two_numbers(2,2)
24
25.EQU status_reg=252 # The status register on the Digirule2
26.EQU zero_bit=0  
27
28# Initialise the stack head
29COPYLR stack stack_ptr
30
31COPYLR 2 R0
32COPYLR R0 f_from
33# Push 2 twice, so the function will do 2+2
34CALL f_push
35CALL f_push 
36CALL f_add_two_numbers  # Equivalent to: f_add_two_numbers(2,2) 
37COPYAR R0
38HALT
39
40
41# Self-explanatory
42f_add_two_numbers:
43COPYLR R0 f_to
44CALL f_pop
45COPYRA R0
46CALL f_pop
47ADDRA R0
48RETURN
49
50# Pushes the value of whatever f_from points to, to 
51# the top of the stack.
52f_push:
53COPYRR stack_ptr f_to
54CALL f_copy_ind
55INCR stack_ptr
56RETURN
57
58# Pops the value of the top of the stack to whatever f_to 
59# points to.
60# NOTE HERE: We re-use the value of the Accumulator which 
61# we may be already using in another part of the program.
62# This too is specified in the cdecl convention.
63f_pop:
64COPYAR T0 # Save the Accumulator
65COPYRA stack_ptr
66SUBLA stack
67BCRSS zero_bit status_reg # NOTE THIS CHECK. It prevents >>UNDERFLOW<<
68DECR stack_ptr
69COPYRA T0 # Restore the Accumulator
70COPYRR stack_ptr f_from
71CALL f_copy_ind
72RETURN
73
74# Memory copy by indirect addressing via self-modification.
75# We construct a suitable absolute
76# addressing copy instruction (COPYRR) and
77# execute it as a sub-routine over f_from, f_to
78f_copy_ind:
79.DB 7
80f_from:
81.DB 0
82f_to:
83.DB 0
84RETURN
85
86R0:
87.DB 0xF0
88T0:
89.DB 0xFF
90stack:
91.DB 0xF0,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0x0F
92stack_ptr:
93.DB 0

Recursion

Being able to call functions, leads naturally to the question “Can a function call itself?”.

This is called recursion and is a very important concept in computer science (and mathematics), since it allows us to express complex iterative processes that are composed of “setup” and repetitive steps.

Having defined a stack on the #Digirule it is relatively easy to express recursion.

This is demonstrated here via the recursive evaluation of a Fibonacci series evaluation.

What is the largest Fibonacci term that the Digirule can compute out of the box?

How many steps does it take to do that?

 1# Recursive Fibonacci
 2
 3.EQU status_reg=252 # The status register on the Digirule2
 4.EQU zero_bit=0  
 5.EQU carry_bit=1
 6
 7# Initialise the stack head
 8COPYLR stack stack_ptr
 9
10COPYLR 7 R0
11COPYLR R0 f_from
12CALL f_push
13CALL f_fibonacci
14COPYAR R0
15HALT
16
17
18# Self-explanatory
19f_fibonacci:
20COPYLR R0 f_to
21CALL f_pop
22COPYLA 2
23SUBRA R0
24BCRSS carry_bit status_reg
25JUMP f_fibonacci_ret_num
26DECR R0
27COPYLR R0 f_from
28CALL f_push
29CALL f_push
30CALL f_fibonacci
31COPYAR T1
32COPYLR R0 f_to
33CALL f_pop
34DECR R0
35COPYLR T1 f_from
36CALL f_push
37COPYLR R0 f_from
38CALL f_push
39CALL f_fibonacci
40COPYLR T1 f_to
41CALL f_pop
42ADDRA T1
43RETURN
44
45
46f_fibonacci_ret_num:
47COPYRA R0
48RETURN
49
50COPYRA R0
51CALL f_pop
52ADDRA R0
53RETURN
54
55# Pushes the value of whatever f_from points to, to 
56# the top of the stack.
57f_push:
58COPYRR stack_ptr f_to
59CALL f_copy_ind
60INCR stack_ptr
61RETURN
62
63# Pops the value of the top of the stack to whatever f_to 
64# points to.
65# NOTE HERE: We re-use the value of the Accumulator which 
66# we may be already using in another part of the program.
67f_pop:
68COPYAR T0 # Save the Accumulator
69COPYRA stack_ptr
70SUBLA stack
71BCRSS zero_bit status_reg # NOTE THIS CHECK. It prevents >>UNDERFLOW<<
72DECR stack_ptr
73COPYRA T0 # Restore the Accumulator
74COPYRR stack_ptr f_from
75CALL f_copy_ind
76RETURN
77
78# Memory copy by indirect addressing via self-modification.
79# We construct a suitable absolute
80# addressing copy instruction (COPYRR) and
81# execute it as a sub-routine over f_from, f_to
82f_copy_ind:
83.DB 7
84f_from:
85.DB 0
86f_to:
87.DB 0
88RETURN
89
90R0:
91.DB 0xF0
92T0:
93.DB 0xFF
94T1:
95.DB 0xFF
96stack_ptr:
97.DB 0
98stack:
99.DB 0xF0,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0x0F

Find the minimum value of an array of integers

One of the more detailed Digirule demos presented here, is that of a very simple random number generator, using a Linear Feedback Shift Register (LFSR). This random number generator, can be used to fill an array with random numbers.

But, what can we do with a set of random numbers?

We can find its minimum (or maximum)

What modifications would this code snippet require, so that it returns the maximum value of the array?

 1# Find the minimum value of an array of integers
 2
 3# Plain simple minimum number finder:
 4# Suppose that the first number is the minimum
 5# then compare it against all others. If you 
 6# find a number that is smaller than the initial 
 7# assumption, make THAT number your current "assumed" 
 8# min.
 9
10.EQU status_reg=252  # The status register on the Digirule
11.EQU carry_bit=1     # Carry bit field of the status reg
12.EQU zero_bit=0      # Zero bit field of the status reg
13
14# R0 Is the iteration bounded var
15# R1 Is the current min value of the array
16# R2 Is the index of the current min value
17
18COPYLR 0 R0                     # Locate the zero element of the array...
19COPYLA num_array
20ADDRA R0
21COPYAR f_from
22COPYLR R1 f_to                  # ...and make it the "current min value".
23CALL f_copy_ind
24COPYRR R0 R2
25loop_start:
26COPYLA num_array
27ADDRA R0                        # Fetch the next number from the array...
28COPYAR f_from
29COPYLR R3 f_to
30CALL f_copy_ind
31
32COPYRA R3
33SUBRA R1
34BCRSC carry_bit status_reg      # Is it smaller than the "current min value"?
35CALL mark_new_min               # If it is, make THAT number the "current min value".
36INCR R0                         # Otherwise, move to the next number to compare...
37COPYRA R0
38SUBLA 20
39BCRSS zero_bit status_reg
40JUMP loop_start                 # UNLESS, you have checked all N numbers (N=20 here) in
41HALT                            # which case, stop.
42
43mark_new_min:
44COPYRR R0 R2
45COPYRR R3 R1
46RETURN
47
48
49
50# Memory copy by indirect addressing via self-modification.
51# We construct a suitable absolute
52# addressing copy instruction (COPYRR) and
53# execute it as a sub-routine over f_from, f_to
54
55f_copy_ind:
56# memory prior to being called from the main program.
57.DB 7   # COPYRR opcode
58f_from:
59.DB 0
60f_to:
61.DB 0
62RETURN
63
64R0:
65.DB 0
66R1:
67.DB 0 
68R2:
69.DB 0
70R3:
71.DB 0
72
73num_array:
74.DB 1,3,8,12,150,14,38,22,110,20,191,88,175,61,59,42,139,222,215,0

Sorting an array of integers

Bubble Sort, Shaker Sort, Quicksort, Merge Sort, Heap sort, Insertion sort….Delay sort!

So many different ways to enforce order to a list of items.

This is a demonstration of the simplest, most primitive and slowest algorithm that is based on finding the minimum (or maximum) element in an array: “Selection Sort”.

The algorithm is composed of two steps:

  1. Find the minimum of an array of \(N-n\) numbers starting at position \(n\),

  2. Move that minimum value to position \(n\)

  3. Repeat from step 1 while \(n<N\)

As is, the agorithm sorts a list of integers in ascending order. What modifications would make the algorithm to sort in descending order?

  1# Sort a list of integers using "Select Sort".
  2
  3# Sorting algorithms are often used to 
  4# demonstrate the concept of computational complexity 
  5# (i.e. How many steps does it take for an algorithm 
  6# to complete a task).
  7
  8# Here is a demonstration of one of the simplest, 
  9# most primitive and slowest algorithms to sort a 
 10# list of integers. It is called "Select Sort" and it 
 11# is basically the repetitive application of 
 12# finding an extreme element (min or max) and then 
 13# bringing it to the beginning of the list: 
 14
 15# Find the min of ([5,12,6,1]) --> 1
 16# Move it to the beginning ([1,12,6,5])
 17# Find the min of (1,[12,6,5]) --> 5
 18# Move it to the beginning (1,[5,12,6])
 19# Find the min of (1,5,[12,6]) --> 6
 20# Move it to the beginning (1,5,6,12) DONE.
 21
 22.EQU status_reg=252
 23.EQU carry_bit=1
 24.EQU zero_bit=0
 25
 26COPYLR 0 R4
 27COPYLR 0 R5
 28index_loop:
 29CALL f_find_min     # Find the minimum...
 30
 31COPYLA num_array    # Move it to the beginning of the current block    
 32ADDRA R5
 33COPYAR f_from
 34COPYLR R6 f_to
 35CALL f_copy_ind
 36COPYAR f_to
 37COPYLA num_array
 38ADDRA R2
 39COPYAR f_from
 40CALL f_copy_ind
 41COPYAR f_to
 42COPYLR R6 f_from
 43CALL f_copy_ind
 44
 45INCR R5  
 46INCR R4
 47COPYRA R4
 48SUBLA 7                     # Have all elements been covered?
 49BCRSS zero_bit status_reg
 50JUMP index_loop             # If not, repeat this sequence of steps
 51HALT                        # Else, terminate.
 52
 53
 54# R0 Is the iteration bounded var
 55# R1 Is the current min value of the array
 56# R2 Is the index of the current value
 57# R3 Is the current value at the current 
 58#    array index
 59# R4 is the index of the number that is 
 60#    currently under test.
 61
 62f_find_min:
 63COPYRR R4 R0
 64COPYLA num_array
 65ADDRA R0
 66COPYAR f_from
 67COPYLR R1 f_to
 68CALL f_copy_ind
 69COPYRR R0 R2
 70loop_start:
 71COPYLA num_array
 72ADDRA R0
 73COPYAR f_from
 74COPYLR R3 f_to
 75CALL f_copy_ind
 76
 77# Now R3 has the next number at offset R0 from label num_array
 78# Compare it with the currently assumed minimum
 79
 80COPYRA R3
 81SUBRA R1
 82BCRSC carry_bit status_reg
 83CALL mark_new_min
 84INCR R0
 85COPYRA R0
 86SUBLA 8
 87BCRSS zero_bit status_reg
 88JUMP loop_start
 89RETURN
 90
 91mark_new_min:
 92COPYRR R0 R2
 93COPYRR R3 R1
 94RETURN
 95
 96# Memory copy by indirect addressing via self-modification.
 97# We construct a suitable absolute
 98# addressing copy instruction (COPYRR) and
 99# execute it as a sub-routine over f_from, f_to
100f_copy_ind:
101.DB 7   # COPYRR opcode
102f_from:
103.DB 0
104f_to:
105.DB 0
106RETURN
107
108R0:
109.DB 0
110R1:
111.DB 0 
112R2:
113.DB 0
114R3:
115.DB 0
116R4:
117.DB 0
118R5:
119.DB 0
120R6:
121.DB 0
122
123num_array:
124.DB 10,132,8,12,150,9,1,4,192,200,0,0,0,0,0,0,0,0,0,0,0,0,0,0

Longest Ripple Counter Ever

A ripple counter and a relatively accurate clock signal source are the basic building blocks of digital timers.

All that a ripple counter does is count clock pulses. Now, suppose we have some i and we start counting in binary by repeatedly calling i=i+1 (or i++ for short).

The values of i will be:

   MSb    LSb  Decimal
bit 76543210
   -------------------
    00000000     0
    00000001     1
    00000010     2
    00000011     3
    00000100     4
    00000101     5
    00000110     6
    00000111     7
    ........    ...
    11111111    255

Bit 0, the Least Significant bit (LSb), counts \(0,1\) and then resets and “signals” bit 1, its neighbour to the left to count \(1\). Once bit 1 has counted its maximum (\(0,1\)) it will reset and “signal” bit 2, its neighbour to the left to count \(1\)….And so on, all the way to the Most Significant bit (MSb)).

From this point of view a ripple counter is just a chain of tiny little elementary counters, each one counting to its maximum and then advancing the next counter.

This continues until i now assumes the value 255 (or 11111111 in binary). Now, something special happens: If we try to i++ once more, i wraps around and hits zero again.

This is a fancy way of saying i resets and we could “catch” that event and use it to increase another variable (j) to start counting. This, would now look like:

  • Increase variable i
    • Once i wraps around increase j
      • Once j wraps around increase k
        • Once k wraps around increase l
          • Once l wraps around increase m

And so on until we run out of memory.

In the following Digirule2 program, we use this concept to construct a ripple counter that is 224 BYTES long.

(Notice here the distinction between bit and Byte.)

A 1 byte long counter can count from \(0\) to \(2^{1 \times 8} - 1\), or \(255\).

A 224 byte long counter can count from \(0\) to \(2^{224 \times 8} - 1\) or …

279095111627852376407822673918065072905887935345660252615989519488029661278604994789701101367875859521849524793382568057369148405837577299984720398976429790087982805274893437406788716103454867635208144157749912668657006085226160261808841484862703257771979713923863820038729637520989894984676774385364934677289947762340313157123529922421738738162392233756507666339799675257002539356619747080176786496732679854783185583233878234270370065954615221443190595445898747930123678952192875629172092437548194134594886873249778512829119416327938768895

This number is big.

How Long Is Long?

Suppose that it takes the Digirule aproximately \(60e-6\) seconds 1 to increase the counter by 1. That is 60 microseconds.

  • This means that this number, describes \(1.67457066976711425845e535\) seconds.
    • Divided by 3600 seconds in an hour, it describes \(4.6515851937975396068e531\) hours.
      • Divided by 24 hours in a day, it describes \(1.93816049741564150283e530\) days.
        • Divided by 30 days in a month, it describes \(6.46053499138547167611e528\) months.
          • Divided by 12 months in a year, it describes \(5.38377915948789306342e527\) years.
            • Divided by 100 years in a century, it describes \(5.38377915948789306342e525\) centuries.
              • Divided by 100 centuries in a millenium, it describes \(5.38377915948789306342e523\) millenia.

That is \(\approx 5.4 \times 10^{523}\) millenia.

There is no single battery that will keep this counter going until it counts to its maximum.

There is no single lifetime (devoted to maintaining a running Digirule) within which to see its value wrap around.

And while this counter scans through each and every value between \(0\) and \(2^{1792}\) as it gallops through the millenia, it also goes through ALL the possible Digirule 2 programs that can be constructed in 224 bytes and, all the possible phrases in English 2 that can be described in 224 bytes.

  1# One huge ripple counter
  2
  3# The main idea behind this program was to write the 
  4# smallest program possible, using the most memory 
  5# possible.
  6
  7# The program is making use of some self-modifying code 
  8# to produce an INCR indirect: An INCR instruction that 
  9# increases the value of a specific memory location.
 10
 11# The counter is initialised at a very special occasion:
 12# It is about to reset itself from its maximum count back 
 13# down to 0. This creates a rippling effect as the reset 
 14# ripple travels from the Least Significant Byte (LSB) to 
 15# the Most Significant Byte (MSB).
 16 
 17# This counter is 224 BYTES long and the highest number it 
 18# can represent is:
 19
 20# 27909511162785237640782267391806507290588793534566025261
 21# 59895194880296612786049947897011013678758595218495247933
 22# 82568057369148405837577299984720398976429790087982805274
 23# 89343740678871610345486763520814415774991266865700608522
 24# 61602618088414848627032577719797139238638200387296375209
 25# 89894984676774385364934677289947762340313157123529922421
 26# 73873816239223375650766633979967525700253935661974708017
 27# 67864967326798547831855832338782342703700659546152214431
 28# 90595445898747930123678952192875629172092437548194134594
 29# 886873249778512829119416327938768895
 30
 31.EQU status_register=252        # Status register
 32.EQU zero_bit=0                 # Zero bit of the status reg
 33
 34count_again:
 35COPYLR counter incr_addr        # Sets the target of INCR to incr (the LSB)
 36CALL incr_ind                   # Increases the LSB
 37
 38count_higher:
 39BCRSS zero_bit status_register
 40JUMP count_again                # As soon as the LSB resets, increase 
 41INCR incr_addr                  # its most immediate MSB...
 42COPYRA incr_addr
 43SUBLA 253                       # Check if the counter has reached its maximum
 44BCRSC zero_bit status_register  # addressable byte and wrap around if it has.
 45JUMP count_again
 46CALL incr_ind                   # ...and keep rippling up the orders
 47JUMP count_higher               # of magnitude
 48
 49# Indirect INCR
 50# Set incr_addr then call incr_ind
 51incr_ind:
 52.DB 19
 53incr_addr:
 54.DB 0
 55RETURN
 56
 57# Start of the counter state
 58counter:
 59.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 60.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 61.DB 0xFF, 0xFF, 0xFF, 0xFF
 62.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 63.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 64.DB 0xFF, 0xFF, 0xFF, 0xFF
 65.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 66.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 67.DB 0xFF, 0xFF, 0xFF, 0xFF
 68.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 69.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 70.DB 0xFF, 0xFF, 0xFF, 0xFF
 71.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 72.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 73.DB 0xFF, 0xFF, 0xFF, 0xFF
 74.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 75.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 76.DB 0xFF, 0xFF, 0xFF, 0xFF
 77.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 78.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 79.DB 0xFF, 0xFF, 0xFF, 0xFF
 80.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 81.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 82.DB 0xFF, 0xFF, 0xFF, 0xFF
 83.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 84.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 85.DB 0xFF, 0xFF, 0xFF, 0xFF
 86.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 87.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 88.DB 0xFF, 0xFF, 0xFF, 0xFF
 89.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 90.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 91.DB 0xFF, 0xFF, 0xFF, 0xFF
 92.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 93.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 94.DB 0xFF, 0xFF, 0xFF, 0xFF
 95.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 96.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 97.DB 0xFF, 0xFF, 0xFF, 0xFF
 98.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
 99.DB 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 
100.DB 0xFF, 0xFF, 0xFF, 0xFF

Hacking the Status Register

Out of the 8 bits available in Digirule2’s status register, the CPU makes use of just 3:

  • 0 Zero bit (Set when the previous operation resulted in 0)

  • 1 Carry bit (Set when the previous operation results in a carry bit)

  • 2 Display bit (Determines what the ADDR LEDs display)

This leaves another 5 bits “hidden” in the status register that go unused.

Are these bits accessible via Digirule2 code? And if they are, can we use them to save and recall the status register bits themselves?

Answering these questions ends up being a fantastic brainteaser, primarily because the instructions we use to manipulate the status register, also modify the status register.

Are the “hidden” bits accessible?

The idea here is to set the higher bits (>3) and then try to read them back. If their state has been preserved, then they can be considered “accessible”.

Here is the code:

 1# Hacking the status register. Can it be done?
 2
 3# The Digirule2 has an 8bit status register but only uses 3 of those bits
 4# Is it possible to use the remaining 5 bits?
 5
 6# It turns out that is. The following program confirms that when
 7# executed on the actual hardware.  
 8
 9.EQU status_reg=252
10.EQU led_reg=255
11.EQU carry_bit=1
12.EQU zero_bit=0
13
14CBR 4 status_reg        # Clear bit 4 (this is the first of the "hidden" bits)
15BCRSS 4 status_reg      # Test bit 4
16JUMP disp_left          # If bit 4 is zero, display 0xF0    
17JUMP disp_right         # If bit 4 is one, display 0x0F
18HALT
19
20disp_right:
21COPYLR 0x0F R0
22JUMP display
23
24disp_left:
25COPYLR 0xF0 R0
26JUMP display
27
28display:
29COPYRR R0 led_reg
30JUMP display
31
32R0:
33.DB 0

Keying this in to the Digirule2 confirms the result. It is indeed possible to write and read to those “hidden” bits.

Using the “hidden” bits to save and restore the status register

This all comes down to saving bits 0,1 to higher bits (say for example 4 and 5).

The straightforward way to do this would be to store the status register, shift it left by 3,ORit with the saved value and then store that result back to the status register. To restore it we can simply shift right by 3 positions and have the status register set to the stored values.

The following code is indeed along those lines but with minor differences since:

  1. Logic operations such as OR go through the Accumulator but SHIFT** operations can be applied to numeric registers directly. Shifting and ORing to save the status register bits to higher bits would require an extra memory location. Which would then beg the question: Why not just save the status register to memory and move on? (But that is not what we are trying to do here).

  1. The SHIFT** instructions are through the carry bit. Therefore, to just do SHIFT on the Digirule2, it has to be preceded by an instruction that clears the carry bit.

  2. The COPYRR instruction modifies the zero bit.

For these reasons, saving and loading the values of the status register is carried out by plain simple bit operations. The code is almost identical which means that it can possibly be reduced and optimised even further (e.g. use the same function for saving and loading).

Here is the code:

 1# Hacking the status register. Save and Restore.
 2
 3# Since we confirmed that it is possible to use the "hidden" 
 4# bits of the status register, is it possible to use it so that
 5# we save and restore the status register WITHOUT using additional 
 6# memory?
 7
 8# It turns out that this is more difficult than it sounds at first, 
 9# because of the way the instruction set interacts with the carry 
10# bit.
11
12# The specific problem is that COPYRR modifies the zero flag. Therefore
13# any computation that ends with a COPYRR in order to set the status 
14# register to some new value ends up MODIFYING it as well.
15
16# The answer? Do EVERYTHING with bit operations.
17
18.EQU status_reg=252
19.EQU led_reg=255
20.EQU carry_bit=1
21.EQU zero_bit=0
22
23SBR carry_bit status_reg    # Set both bits of th status register
24SBR zero_bit status_reg
25CALL save_status_reg        # Save that value of the status register
26COPYRR status_reg R0        # Check the content of R0
27CBR carry_bit status_reg
28CBR zero_bit status_reg     # Modify both flags and check the content again
29COPYRR status_reg R0
30CALL load_status_reg
31COPYRR status_reg R0        # Recall the status register and examine it via R0.
32HALT
33
34save_status_reg:
35CBR 3 status_reg                # Clear both higher bits
36CBR 4 status_reg
37BCRSC zero_bit status_reg       # Test bit 0 if it is 1 set the 4th bit
38JUMP save_status_reg_zero_bit
39save_status_reg_test_carry_bit:
40BCRSC carry_bit status_reg      # Test bit 1 if it is 1 set the 5th bit
41JUMP save_status_reg_carry_bit
42save_status_reg_return:
43RETURN
44
45save_status_reg_zero_bit:
46SBR 3 status_reg
47JUMP save_status_reg_test_carry_bit
48save_status_reg_carry_bit:
49SBR 4 status_reg
50JUMP save_status_reg_return
51
52load_status_reg:                
53CBR zero_bit status_reg                # Clear both higher bits
54CBR carry_bit status_reg
55BCRSC 3 status_reg       # Test bit 3 if it is 1 set the zero bit
56JUMP load_status_reg_zero_bit
57load_status_reg_test_carry_bit:
58BCRSC 4 status_reg      # Test bit 4 if it is 1 set the carry bit
59JUMP load_status_reg_carry_bit
60load_status_reg_return:
61RETURN
62
63load_status_reg_zero_bit:
64SBR zero_bit status_reg
65JUMP load_status_reg_test_carry_bit
66load_status_reg_carry_bit:
67SBR carry_bit status_reg
68JUMP load_status_reg_return
69
70R0:
71.DB 0
1

Brent Hauser responded with all the details about the Digirule2’s timings on the Discord server. The 60 micro second estimate is a conservative estimate of the Digirule’s timing to execute the counter program. If the hardware counts a bit slower than that, then the millenia will keep piling up. Even if the Digirule was made to run 1 order of mangitude faster, that would only knock off one order of magnitude from that \(...e51\). It would still be, a big number.

2

“…phrases in English” is the quicker thing to describe here, since the ASCII table contains direct correspondences of numbers to English characters. If the memory was interpreted as Unicode, all languages would be possible but in that case, the maximum number of letters described by 224 bytes would varry.