Programming Turing Machines
In the previous post, we built a LEGO Turing machine. However, we did not talk about how to run the machine. In this post, we focus on how to program Turing machines. Starting from a very naive implementation which only uses a single task, we will add more features to the machine incrementally:
- multi-task
- concurrent movements
- log via USB
- support its own programming language
- transfer a program via USB
Turing Machine
Turing Machine is an abstract computing device which was contrived by Alan Turing. It runs the following algorithm:
- Read : Read the current cell of the tape.
- Transition : Determine the next state, output, and shift direction based on the current state and the content of the current cell. If the new state is either Accept or Reject state, stop the machine.
- Write : If needed, change the value of the current tape cell.
- Move : Move the tape to the left or right based on the result of 2.
- Goto 1.
1. Simple, Single-task Turing Machine (Download)
We start with a simple implementation. This implementation has the following properites (limitations):
- Single-task
- Controlled by a Transition Relation
- Serialized Movement: Read - Write - Move, one by one.
- No Logging
1.1 Main Loop
TASK(TM)
{
while(true)
{
/* Read */
input = Read();
/* Make transition */
(state, output, dir) = transition(state, input);
if(state == HALT_STATE)
HALT;
/* Write (Optional) */
if(input != output) {
write(output);
}
/* Move */
move(dir);
}
}
The main loop is a direct translation of the algorithm presented
above. Repeatedly, it reads the tape cell, makes a transition which is
defined in transition function, optionally changes the current tape
cell, and moves the tape as directed.
1.2 Reader
_Bool read()
{
U16 color;
_Bool input;
/* 1. Move READ Header to TAPE */
nxt_motor_set_speed(READ_MOTOR, SPEED, 1);
while(nxt_motor_get_count(READ_MOTOR) <= READ_REV) {
/* do nothing */
}
nxt_motor_set_speed(READ_MOTOR, 0, 1);
/* 2. Read Sensor Value */
ecrobot_set_nxtcolorsensor(COLOR_SENSOR, NXT_LIGHTSENSOR_GREEN);
ecrobot_process_bg_nxtcolorsensor();
color = ecrobot_get_nxtcolorsensor_light(COLOR_SENSOR);
input = color < COLOR_THRESHOLD ? 1 : 0;
ecrobot_set_nxtcolorsensor(COLOR_SENSOR, NXT_LIGHTSENSOR_NONE);
ecrobot_process_bg_nxtcolorsensor();
/* 3. Move READ Header back */
nxt_motor_set_speed(READ_MOTOR, -SPEED, 1);
while(nxt_motor_get_count(READ_MOTOR) >= 0) {
/* do nothing */
}
nxt_motor_set_speed(READ_MOTOR, 0, 1);
return input;
}
read function does three operations:
- Move the READ header to the TAPE
- Read the sensor value
- Move the READ header back
Let’s focus on the actual reading operation (in step 2) because it is
relatively trivial to move the header back and forth (step 1 & 3). At
line 14, it turns on the color sensor and set it to
NXT_LIGHTSENSOR_GREEN mode. At line 16,
ecrobot_get_nxtcolorsensor_light function returns the raw value
which measures the reflection around the sensor. We use the
pre-defined constant COLOR_THRESHOLD to determine whether the given
raw value is interpreted as 0 or 1. (line 17). Then, it turns off the
light sensor (line 18).
Note that it is necessary to call ecrobot_process_bg_nxtcolorsensor
function at line 15 and line 19. When we change the sensor mode by
calling ecrobot_set_nxtcolorsensor at line 14, nothing really
happens on the sensor side. It only changes the sensor status on the
memory without affecting the sensor itself. It is the job of
ecrobot_process_bg_nxtcolorsensor function which makes requested
changes on the sensor side and copies the raw value of the sensor to
the memory. If we do not call ecrobot_process_bg_nxtcolorsensor at
line 14, 1) the sensor is not set to green light mode and 2) the
value we read at line 16 can be an outdated value— it is the value
of the sensor when ecrobot_process_bg_nxtcolorsensor function was
called last time.
1.3 Writer
void write(_Bool output)
{
int sign = output == 1 ? 1 : -1;
nxt_motor_set_speed(WRITE_MOTOR, sign * SPEED, 1);
while (sign * nxt_motor_get_count(WRITE_MOTOR) <= WRITE_REV) {
/* do nothing */
}
nxt_motor_set_speed(WRITE_MOTOR, 0, 1);
nxt_motor_set_count(WRITE_MOTOR,
nxt_motor_get_count(WRITE_MOTOR) % WRITE_REV);
}
Writing operation is simple. It flips the bit on the tape based on the
output value — clockwise to write 0 and counter-clockwise to write
1. The constant WRITE_REV is defined to be 180.
1.4 Tape Mover
void move(_Bool dir)
{
int sign = dir == RIGHT ? 1 : -1;
nxt_motor_set_speed(TAPE_MOTOR, sign * TAPE_MOV_SPEED, 1);
while (sign * nxt_motor_get_count(TAPE_MOTOR) <= TAPE_MOVE_REV) {
/* do nothing */
}
nxt_motor_set_speed(TAPE_MOTOR, 0, 1);
nxt_motor_set_count(TAPE_MOTOR,
nxt_motor_get_count(TAPE_MOTOR) % TAPE_MOVE_REV);
}
move function is similar to write. Based on the given argument
dir — which is either LEFT or RIGHT, it rotates the motor to
move the tape to the given direction. The constant TAPE_MOVE_REV is
set to 1800 (five cycles).
Limitations
This implementation works well, however, there are limitations of this implementation.
- Single-task with polling : We have one big monolithic task
doing every operation — read/write/move — in it. It uses
busy-wait polling to control motors as the following code (part of
movefunction).
while (sign * nxt_motor_get_count(TAPE_MOTOR) <= TAPE_MOVE_REV) {
/* do nothing */
}
Problem: It wastes CPU cycles and prevent us from doing other tasks. We are going to improve this by implementing multi-task version.
- Controlled by a Transition Relation : In this implementation, we need to provide a transition relation to program the machine. For example, the unary addition program has the following transition relation:
| State | Input | Output | Next State | Direction | |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | L | |
| 0 | 1 | 1 | 1 | L | |
| 1 | 0 | 1 | 2 | L | |
| 1 | 1 | 1 | 1 | L | |
| 2 | 0 | 0 | 3 | R | |
| 2 | 1 | 1 | 2 | L | |
| 3 | 0 | ERROR | |||
| 3 | 1 | 0 | 4 | R | |
| 4 | 0 | HALT | |||
| 4 | 1 | 1 | 4 | R |
This form is difficult to understand and maintain. We will introduce an assembly-like language to program the machine. With the new language, we may program the unary addition as follows:
/* Before it meets the first ones */
0: READ
CJUMP1 1
MOVE L
JUMP 0
/* scanning first operand */
1: MOVE L
READ
CJUMP1 1
WRITE 1
MOVE L
/* scanning second operand */
2: READ
CJUMP0 3
MOVE L
JUMP 2
/* last position */
3: MOVE R
READ
CJUMP0 ERR
WRITE 0
/* computation finished, keep moving to the right until it ends */
4: MOVE R
READ
CJUMP1 4
HALT
ERR:
ERROR
-
Serialized Movement: In the current implementation, Read/Write/Move operations occur one after another, even if it is physically possible to do them simulataneously in some cases. For example, the read header can start to move to the tape while the tape is being moved to the next position. The only constraint here is that the tape should not move when the colorsensor reads the value. By the same argument, we may move the writer lever and the read header simulataneously. However, we should be careful to avoid any physical collision. To implement concurrent movements, it is natural to program using multiple tasks. This extension will introduce more complexity to an implementation. We will see how to use our verification tool to make sure that an implementation has desired properties.
-
No Logging : Currently, we do not have any logging feature so that it is difficult to check the internal status of the machine or debug a program. We will start to add logging by using the LCD display in the NXT brick. Then, we will use USB communication to output log messages to the connected computer.