-
Notifications
You must be signed in to change notification settings - Fork 114
Development Tutorial Instruction Checklist
This document discusses how to implement your own instructions.
For this first step, you will be editing the virtual CPU definition in
cHardwareCPU.h and cHardwareCPU.cc, both of which are
found in the directory source/cpu/. Start by going to the
final section of the class definition in the header file and writing
the declaration for the new method that will be called whenever the
instruction is executed. For example, if you were going to add the
instruction minus-17
(which performs the oh-so-useful behavior
of subtracting 17 from the ?BX? register), you would add the line:
bool Inst_Minus17(cAvidaContext& ctx);
If possible, place it near other instructions of the same type. There are about a hundred methods cHardwareCPU. This instruction would likely fit best with the group of instruction described as "Single-Argument Math". That is, all those instructions that perform mathematical operation that only affect a single register.
All methods associated with instructions return a
bool value that determines if it was
successfully executed. Most instructions will always return true since they
have no way to fail. The convention that we use to designate a method
explicitly associated with an instruction is placing a prefix of
Inst_
in front of it.
Next, you have to write the function body in the code file (cHardwareCPU.cc). The method bodies will be listed at the end of this file in the same order that they were declared in the header. You would find the proper position, and write something to the effect of:
bool cHardwareCPU::Inst_Minus17(cAvidaContext& ctx) { const int reg_used = FindModifiedRegister(REG_BX); GetRegister(reg_used) -= 17; return true; }
The first line of this method uses a helper function called
FindModifiedRegister() to identify the register
that should be affected (it scans the next instruction to test if it is a
nop
), with a default value of REG_BX
passed in.
The second line then subtracts 17 from the value in that register. The
constant values and available helper functions will be described in more
detail below, as will a guide to accessing the components in the virtual
CPU. For the moment, you have finished implementing the method!
Note that this would be a good time to recompile if you want to test how well your implementation is going so far.
For this step, you will need to edit the code file. You would go into the method cHardwareCPU::initInstLib(void) and add in the line
cInstEntryCPU("minus-17", &cHardwareCPU::Inst_Minus17);
in the same order that it was defined in the class definition.
Since we want to use a pointer to the appropriate method, that is what we must pass into the dictionary. To obtain said pointer, we must list the class the function is part of (cHardwareCPU) follow it by a double colon (::) and then give the method name (Inst_Minus17) without the normal parentheses following it. The parentheses indicate that we should execute the method. Without them, it is just the data that represents the method, and by preceding this whole mess with an ampersand ('&') we get the pointer to the location in memory that the method resides.
IMPORTANT: If your instruction interacts with the population, resources, or IO, make sure to flag the instruction for speculative stall by adding a third argument, nInstFlags::STALL. For an example, look at the 'h-divide' instruction.
Compile again, and you should have your instruction ready for use.
This last part should be the easiest. If you want the new instruction you just created to be loaded on startup, you must add a line in the instruction set you are using (specified in the configuration file) to indicate its inclusion:
minus-17 1
And there you have it! Now the real trick is to test if its working
properly. I'd recommend using as a framework the creature
default-classic.org and modifying some of the long series of
nop-C
instructions inside of it to perform some math using the
new instruction (only the very first nop-C
cannot be changed). You
can then either go into zoom mode in the viewer and step through the creature,
or else use analyze mode trace its execution. If you are going to use zoom
mode, setup your modified creature as the START_CREATURE in configuration file.
If you want to use analyze mode, put the following lines into the
analyze.cfg
file in your work/ directory:
LOAD_ORGANISM inst_test.org TRACE
Where you have to replace inst_test.org with the name of the organism you want to trace. The new file will appear in the data/archive/ directory, with the same name as the one you loaded in, but a .trace appended to the end.
Various CPU components are often manipulated by instructions, and we need a standard way of doing this. We have settled on each component being associated with a method to access it, to provide a single location that can control that access. This has already been useful -- in a multi-threaded CPU (i.e., a CPU that has multiple positions in its genome being executed at the same time) each thread has its own registers and heads, so we need to always be sure we are manipulating the component of the active thread. If you simply use the following methods, they will always find the correct component for you.
-
void StackPush(int value); int StackPop(); void SwitchStack();
- There are two stacks in a normal CPU, and more in a multi-threaded version (one global stack, and one local to each thread). The first stack method will place a new value on the top of the currently active stack, the second will remove the top value, and the last will toggle the currently active stack.
-
cCPUHead& GetHead(int head_id); cCPUHead& IP();
-
Each thread in a CPU has four heads associated with it, designated by the
constants
HEAD_IP
,HEAD_READ
,HEAD_WRITE
, andHEAD_FLOW
. These heads each point to a position in memory, and all have their own purpose. A head can be accessed by passing the appropriate constant into the GetHead() method. The extra method IP() was added to more easily obtain just the instruction pointer. The IP is a very special head since it designates what instruction is going to be executed next, and often it will make code clearer if you obtain it by calling IP(). (It will show that you need to make sure of the special qualities of the instruction pointer.) -
int& Register(int reg_id);
-
There are three registers available, associated with the constants
REG_AX
,REG_BX
, andREG_CX
. If the Register() method is called, an integer reference will be returned associated with that register. Any change to this integer will make a corresponding change to the register in question. -
cCPUMemory& GetMemory();
- This method allows the programmer to access the full memory of the CPU. As you know, the class cCPUMemory is built on top of Genome, so you can manipulate it in all of the same ways.
These are only a sampling of the available methods of interacting with the components of the CPU, but they give you a good cross-section without overwhelming you with all of the possibilities. You should look through the source files if you want to see the other options that are available to you.
There are several very common tasks that are performed during the execution of many of the instructions. For each of these tasks we have created a helper function to ease the creation of new instructions.
-
void ReadLabel(); cCodeLabel& GetLabel(); cCPUHead FindLabel(int direction);
- ReadLabel() will read in the label (series of nop instructions) that immediately follows the instruction being executed. The label that was read can then be accessed (and even modified) using GetLabel(). Finally, the Findlabel() method takes single int argument that determines the direction (from the instruction pointer) in which the label should be search for. If this argument is a 1 it will search forward, and if its a -1, it will search backwards. A zero indicates that the search should start from the beginning of the genome, and proceed to the end. Once it finds the matching label, it will return a head at the position in which the label was found. These helper methods are particularly useful in any instruction that has to affect other portions of the source code. See the method Inst_HeadSearch for an example of how these are used.
-
int FindModifiedRegister(int default_register); int FindModifiedHead(int default_head);
- These two methods will look ahead and determine if a nop instruction is immediately following the instruction being executed. If so, it will return the register or head ID associated with that nop (for use in the rest of the method), and if no nop is found, it will automatically return the default value passed in. We used FindModifiedRegister in the example new instruction above.
-
int FindComplementRegister(int base_reg);
- Several instructions are defined as affecting a certain, modifiable register and its complement. In order to have a standard way of determining the complement of a register (which, by default, cycle in the same order as complement labels), we use this function whenever we need to determine one. See, for example, see the definition of Inst_IfNEqu().
To test your understanding of adding instruction into Avida, try writing two
new instructions. The first one is the mathematical instruction
cube
that will take the ?BX? register, and put
its value to the third power. If you look in the actual source files, you
will see that there is already a square
instruction that you can
model this on.
Next, you will implement the instruction if-twice
that will execute the next instruction if-and-only-if the value in the ?BX?
register is twice that of the value in its complement. In other words by
default, if would test of BX was twice CX, but if it is followed by a
nop-C
it will test if CX is twice AX.
For both of these instruction make sure to craft an organism to test that they are working properly!