Advanced Digirule 2 Programming

dgtools provides an assembler that can interpret labels as memory addresses and this makes it possible to start creating more involved programs.

However, one thing that the Digirule2 hardware itself does not specify is a stack which is an important part of more complex programs that include function calling.

This section first establishes a stack and associated operations (push, pop), later a set of registers and finally based on these pre-requisites it demonstrates function calling with parameters passed on the stack.

Defining a Stack

The Digirule 2 is capable of referencing approximately 256 bytes of main memory. Therefore, it should be possible to implement a stack as a statically allocated array of N values where the push and pop operations are carried out at the “head” of the stack.

The only problem here is that the Digirule 2 instruction set does not support indirect memory access. The command COPYRR copies memory content from one address to another address but (out of the box) it cannot copy memory from the value of a memory address (i.e. where a memory location is pointing to), to the value of another address.

Implementing indirect memory access

To achieve this, we are going to use the label capabilities of dgasm to “build” a COPYRR that can copy memory indirectly.

This is achieved by writing a “template” function:

1
2
3
4
5
6
7
f_copy_by_ref:
.DB 7
f_from:
.DB 0
f_to:
.DB 0
RETURN

This is part of dg_asm_examples/advanced/stack.dsf

The first byte (7) is the opcode of COPYRR, the second byte is labeled f_from and the third byte is labeld f_to.

The CPU is “tricked” into thinking that it executes a COPYRR but this is now a parametrisable COPYRR that copies between two addresses that can be the result of any computation performed by the code.

To copy between two references, calculate the references (if required), move the references to addresses f_from and f_to and then execute a CALL f_copy_byref. The CPU will fetch the 7, decide it is a COPYRR, read in the next two bytes, perform the copy, hit the RETURN and go back to where it was called from.

Getting the “head” of the stack

Defining the stack through dgasm is as trivial as defining a label and immediately populating it with values. For example:

1
2
stack:
.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

There is no particular reason for these default values other than making this memory range stand out in a memory dump.

The stack also needs a way of pointing to its head address.

Note

A stack is a data structure that can store and retrieve data through the operations of push and pop respectively. Both storage and retrieval are performed from a single end-point that always points at the top of the stack. Most commonly, this is called the “head” of the stack.

This can be achieved by using another “labeled” location:

1
2
stack_ptr:
.DB 0

Having defined the stack, we now need to define its two fundamental operations: push, pop. This is relatively easy, all we have to do is find out where the head is pointing to and send (or retrieve) a number to (or from) that location. This is achieved with:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
f_push:
CALL f_get_stack_head
COPYAR f_to
COPYLR r0 f_from
CALL f_copy_by_ref
INCR stack_ptr
RETURN

f_pop:
DECR stack_ptr
CALL f_get_stack_head
COPYAR f_from
COPYLR r0 f_to
CALL f_copy_by_ref
RETURN

f_get_stack_head:
COPYLA stack
ADDRA stack_ptr
RETURN

This is part of dg_asm_examples/advanced/stack.dsf

Note

This is a bit of an overkill for getting the head of a stack, because it assumes that the head has to be re-calculated prior to every push or pop. Such a mode of access would be necessary in the case of an array where elements can be stored to or read from randomly across any element of the array. Since the head of the stack can only be increased or decreased and is being assigned to its own memory space, a much faster way of working with it here would be to establish f_get_stack_head as f_init_stack and then use stack_ptr directly at subsequent calls.

But for these examples, we will take the scenic route, as it makes the program traces more interesting too.

All that f_push, f_pop do is to calculate where the head of the stack is and then pass that address as either the f_from or f_to “parameter” of a made-up COPYRR that now copies by reference.

But, how are these “low level” functions going to communicate with the rest of the code? The Digirule 2 does not specify a standardised register set.

By now, it should be clear that this is not a problem at all because we can use the labeled .DB capabilities of the assembler, to specify the equivalent of a “register” or even a complete set of registers.

For the purposes of this example, register r0 is used as the intermediate register for the f_push, f_pop functions.

The complete example below pushes values 0,1,2,3,2,1,0,1,2 to the stack and terminates:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
start:
COPYLR 0 r0
CALL f_push
COPYLR 1 r0
CALL f_push
COPYLR 2 r0
CALL f_push
COPYLR 3 r0
CALL f_push
COPYLR 2 r0
CALL f_push
COPYLR 1 r0
CALL f_push
COPYLR 0 r0
CALL f_push
COPYLR 1 r0
CALL f_push
COPYLR 2 r0
CALL f_push
HALT

f_push:
CALL f_get_stack_head
COPYAR f_to
COPYLR r0 f_from
CALL f_copy_by_ref
INCR stack_ptr
RETURN

f_pop:
DECR stack_ptr
CALL f_get_stack_head
COPYAR f_from
COPYLR r0 f_to
CALL f_copy_by_ref
RETURN

f_get_stack_head:
COPYLA stack
ADDRA stack_ptr
RETURN

f_copy_by_ref:
.DB 7
f_from:
.DB 0
f_to:
.DB 0
RETURN


r0:
.DB 0

stack_ptr:
.DB 0
stack:
.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

This listing is available in dg_asm_examples/advanced/stack.dsf

Function calls using a stack

Now that the Digirule 2 has a stack, it can call any function with any number of argument by adopting a “calling convention” and defining a standardised set of registers.

In this section, the addition of two numbers is performed within the following two argument function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
q_add_ab:
CALL f_pop
COPYRR r0 t0
CALL f_pop
COPYRR r0 t1
COPYRA t0
ADDRA t1
COPYAR r0
CALL f_push
RETURN

This is part of dg_asm_examples/advanced/funcall.dsf

Here, q_add_ab first pops the numbers from the stack to “temporary registers”, performs the addition, pushes the result back on to the stack and returns. All that the caller has to do now is to pop the stack on the “other side of the call” to retrieve the result.

The complete listing is now:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
.EQU a=1
.EQU b=2

start:
COPYLR a r0
CALL f_push
COPYLR b r0
CALL f_push
CALL q_add_ab
CALL f_pop
COPYRR r0 255
HALT

q_add_ab:
CALL f_pop
COPYRR r0 t0
CALL f_pop
COPYRR r0 t1
COPYRA t0
ADDRA t1
COPYAR r0
CALL f_push
RETURN

f_push:
CALL f_get_stack_head
COPYAR f_to
COPYLR r0 f_from
CALL f_copy_by_ref
INCR stack_ptr
RETURN

f_pop:
DECR stack_ptr
CALL f_get_stack_head
COPYAR f_from
COPYLR r0 f_to
CALL f_copy_by_ref
RETURN

f_get_stack_head:
COPYLA stack
ADDRA stack_ptr
RETURN

f_copy_by_ref:
.DB 7
f_from:
.DB 0
f_to:
.DB 0
RETURN


r0:
.DB 0
r1:
.DB 0
r2:
.DB 0
r3:
.DB 0
r4:
.DB 0
r5:
.DB 0
r6:
.DB 0
r7:
.DB 0

t0:
.DB 0
t1:
.DB 0
t2:
.DB 0
t3:
.DB 0
t4:
.DB 0
t5:
.DB 0
t6:
.DB 0
t7:
.DB 0

stack_ptr:
.DB 0
stack:
.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

This is listing dg_asm_examples/advanced/stack.dsf.

It is basically a continuation of listing dg_asm_examples/intro/simpleadd_5.dsf and it could be called externally as per this example from the introductory section.

Conclusion

Now that Digirule 2 has a stack and a set of standardised registers, it is possible to start thinking about implementing a higher level language that compiles down to its assembly.

It should then be possible to write arbitrarily complex programs to carry out functionality possibly not envisaged for the Digirule 2.

But, aside from being 8bit and having a very limited amount of memory, there is nothing that can be expressed with a “stack machine” that Digirule cannot do.

It might be slow and somewhat difficult to define, but Digirule will eventally compute it.