The final (optional) program reads from the serial interface (memory locations 40960 - 40961) under the control of interrupts generated by the interface. Like the other programs it writes to the LEDs (memory location 2049).
All the programs make use of subroutines supported by the proper use of the stack for parameter passing and local variable storage. Since there is no operating system to call these programs they should not terminate.
The multiply program has a main routine and a single multiplication subroutine.
Before any processing can begin, the routine will load a fixed value into the stack pointer. For a simple program such as the multiply program, no further initialisation is needed.
The routine reads a value from the switches (memory location 2048), performs a bitwise AND operation with the value 255 and stores the result on the stack (this will be Operand1). It then takes the switch value again and shifts it right 8 times. The result of this calculation is stored on the stack (Operand2). Thus Operand1 is the low byte and Operand2 is the high byte of the switch value. The last value to be placed on the stack is 0 which will act as a placeholder for the result.
Note that this placeholder value will be overwritten by the multiplication subroutine without ever having been read. For this reason the value stored here is irrelevant. A value of zero is specified here only to aid with the debugging of this first program.
e.g.
The routine then calls the multiplication subroutine which performs the multiplication. On return the result is available on the stack. The main routine will take the result off the stack and write it to the LEDs (memory location 2049).
Having completed the operation, the routine should enter a tight loop which does nothing (this avoids the problem of instructions being read from uninitialised areas of memory).
This subroutine simply multiplies the two operands it finds on the stack and returns its result via the result placeholder on the stack.
Note that the main routine will produce two 8 bit values for multiplication but the subroutine must be able to cope with larger values since it will be used in other programs.
In any situation where more than one person will write assembly language for a processor, a subroutine call convention is required. The convention will tell you were to look for operands and results and also identifies registers which may be overwritten (i.e. destroyed) by the subroutine and those which will remain unaltered.
For the example programs, part of the calling convention is defined for you (i.e. the location of operands and results on the stack). It is left up to you to decide which registers in your design will be unaltered by the subroutine call and which may be destroyed.
Since any routine may act sometimes as calling routine and sometimes as subroutine, you are likely also to have to worry about the responsibilities for saving important data before or during a subroutine call.
In the simplest convention, no general purpose register is guaranteed to be unaltered by a subroutine call. In order to support the stack mechanism, an exception must be made for the stack pointer which will appear unchanged to the calling routine (this is because any stack pushes within the called routine will be matched by an equal number of pulls/pops before the called routine returns).
The factorial program demonstrates the power of the stack with its support of a recursive subroutine (one that can call itself). The power is apparent since each of the instances of the subroutine can co-exist, with each having a separate stack frame in which are stored its parameters and variables. In addition to the recursive subroutine, this program also uses the multiplication routine developed above.
The stack pointer is initialised. Note that this initialisation is not part of any loop. It should be executed only once (i.e. when the processor is reset).
This routine simply reads a number from the switches and calls the factorial subroutine to calculate its factorial. As before, parameter passing is done via the stack. The result returned by the factorial subroutine is written to the LEDs.
Having completed the operation, the routine should loop back to the start of processing section and re-read the switches. This avoids the problem of instructions being read from uninitialised areas of memory and gives a repeated pattern of operation when a fabricated microprocessor is tested.
The factorial program employs the recursive algorithm:
N! = (N-1)! * N (for N<>0) N! = 1 (for N==0)
Thus for N<>0 it must first call itself to calculate (N-1)! and then call the multiplication subroutine to multiply (N-1)! by N.
This subroutine should be a copy of the one provided for the multiply program.
This program should generate a sequence of psuedo-random numbers based on a seed read from the switches. The majority of the work is carried out in the random subroutine.
Your program should not terminate; it should produce a never ending stream of random numbers based on an initial seed value read from the switches. Each new random number should be written to the LEDs when it becomes available. To calculate a new random number based on an old random number (seed) you should use a subroutine - passing data to/from the routine via the stack.
The stack pointer is initialised.
The main routine reads a number from the switches and then calls the random subroutine to generate a new random number based on this seed. The returned result is written to the LEDs and then passed back to the random subroutine as the new seed value. The new result is then written to the LEDs and the random subroutine is called again. This sequence is repeated forever, thus a never ending stream of random numbers is generated based on a single reading of the switches.
This subroutine contains the code which generates a new random number based on a seed. You may use an algorithm of your choice to generate either 16 bit random numbers or 15 bit random numbers. If you feel you do not have time to research alternative algorithms you can use a simulation of a "linear feedback shift register" such as you have already met during your course. The following simple circuit would give a new 15 bit result every 15 clock cycles and should produce a maximal length sequence (32767 values before repetition):
Note that a simulation of this circuit should test the logical and shift operations supported by your processor.
You may find it useful to include other subroutines in this program, dependent on the algorithm that you choose.
The interrupt program has a main routine and an interrupt service routine. In addition it uses the factorial and multiply subroutines previously created. The interrupt service routine reads from the serial port and writes to a circular buffer. The main routine reads from the circular buffer, calculates a factorial result and writes the result to the LEDs.
This simple arrangement demonstrates the power of interrupts, allowing rapid servicing of an I/O device without significantly affecting the performance of a processor working on another task.
The circular buffer will occupy six words of main memory. Four words are taken up by the buffer itself with the remaining two used for the write pointer and the read pointer. The maximum capacity of the buffer is just three words as there will always be at least one space in the buffer (using all four locations would require a more complex arrangement of pointers to ensure that we can distinguish between a full buffer and an empty one). The buffer size is intentionally small so that buffer overflow conditions can be tested.
The interrupt service routine will act as data producer, it will read both pointers (to ensure that it does not overfill the buffer) but may only modify the write pointer.
The main routine will act as data consumer, it will read both pointers (to check if data is present) but may only modify the read pointer.
The initialisation process for an interrupt program is more complex. The order of operations is important to ensure that an interrupt is not triggered before the system is fully prepared:
Following initialisation, the routine repeatedly checks the circular buffer to see if data is present. If data is present, it reads a number from the buffer and calls the factorial subroutine to calculate the factorial of this number. The result is then written to the LEDs. Having completed this action, the routine goes back to checking the circular buffer.
The Interrupt Service Routine (ISR) reads a value from the serial interface (read from the serial interface data register at address 40960) and places it into the circular buffer (if the circular buffer is already full, the value will be read from the serial interface and then discarded). Control is then returned to the main routine. The action of the ISR must be transparent to the main routine i.e. the main routine will see no change in the state of any of its registers (including any flag registers that may exist).
Iain McNally
31-1-2012