Comments
Comments in EDL begin with # and end at the end of the current line
# This is a comment
Integers
At present EDL supports binary (% prefix),decimal and hexadecimal ($ prefix) representations for any numeric constants
%1010 $A 10
The above are all equivalent.
Variables
To declare a variable called foo, which is 8 bits in width (C equivalent unsigned char) in EDL :
DECLARE foo[8];
Variables can either be declared globally or within a block, however at present the compiler requires the declaration of the variable before its use. Scoping rules are the same as C.
Note variables are ALWAYS initialised to zero, and it is not possible to give them any other initial value (with the exception of constants).
Hidden/Internal Variables
DECLARE INTERNAL foo[8];
The above would declare the variable foo, but it will be private to the current file (not accessable from outside).
Constant Variables
Constant variables are supported, although at present the compiler will not treat them as such from an optimisation standpoint. See Aliases below.
Aliases
One of the more powerful (esoteric?) features of variable declarations is the ability to declare an ALIAS. Imagine a processor status register, these are often a sequence of named bit flags, packed into a larger register.
DECLARE Flags[3] ALIAS sign[1]:zero[1]:carry[1];
The above would declare a Flags variable of 3 bits in length. It would also declare sign as the MSB (Most significant bit), zero as the middle bit, and carry as the least significant bit. : is a bit concatenation operator, at present it is not supported on generic expressions, but that will change.
To declare a constant variable, you can do :
DECLARE MyMagicConstant[8] ALIAS %01010101;
The above would create MyMagicConstant (8 bits in length) with a decimal value of 85. It will be impossible to change the value of this variable.
A more complicated version of the above 2 ideas can be find in the 8080 emulation. The 8080 processor has a status register with 3 unused bits, according to the documentation, these bits are constant.
DECLARE FLAGS[8] ALIAS s[1]:z[1]:%0:ac[1]:%0:p[1]:%1:cy[1];
Which gives bits 5 and 3 a value of 0, and bit 1 a value of 1. Even if you were to assign a value to FLAGS within code, it would be impossible to modify the constant bits!
Assigning Values
Unlike many modern languages, = is not used to assign values to variables. Assignment like other features of the language was born from a need to keep the emulation description as close to the manufacturers description of the hardware. Assignment in EDL is therefor a directional affair and uses the symbols <- and -> and allows assignment in either direction.
foo <- $AA; $AA -> foo;
The above expressions are the same, and both assign foo with the value $AA.
Assigning Values - Automatic promotion/truncation
It is important to remember (since at present no warnings are generated by the compiler), that when assigning a value to a variable, the value will be truncated or expanded (as an unsigned integer) to the bit size of the variable. E.g.
DECLARE foo[2]; foo<-%1; # foo will be given the value %01 (%1 expanded to 2 bits) foo<-$A; # foo will be given the value %10 (%1010 clamped to 2 bits)
Input / Output
At present EDL does not support IO operations! To provide input into the system, external code (C program) must be provided. This is obviously a hinderance, and will change in the future. Output is possible via the DEBUG_TRACE expression, but its only really there for debugging - hence its name.
Debug Trace
DEBUG_TRACE "A string";
Will output a single line to the console, saying :
A string
DEBUG_TRACE foo;
Will output the contents of the variable foo plus its identifier to the console :
foo(10101010)By default variables are displayed in binary notation, this can be modified by using the BASE directive.
DEBUG_TRACE BASE 16,foo;
Would output :
foo(AA)
DEBUG_TRACE "10 in base 2 : ",10," and in base 16 : ",BASE 16,10;
Should obviously be :
10 in base 2 : 1010 and in base 16 : A
Functions
EDL supports the idea of FUNCTIONs which can save code duplication and help make more maintainable code. They are very similar to C functions although the syntax differs.
FUNCTION foobar { DEBUG_TRACE "Look at me!"; }
The above declares a function with no parameters and no return value. It simply prints "Look at me!" when called.
FUNCTION foo[8] returnsMagic8BitConstant { foo <- %10010011; }
This time, the function has a return value and when called will return an 8 bit value that is magical apparantly.
FUNCTION result[8] Add a[8],b[8] { result <- a + b; }
Finally, the above function will add two numbers together and return the result.
Automatic promotion/truncation of values is performed for parameters into the function, no warnings are currently generated.
Calling Functions
To call a function in EDL, you simply write CALL functionname(parameters). For example :
FUNCTION result[8] Add a[8],b[8] { result <- a + b; } FUNCTION foo { DECLARE answer[8]; answer <- CALL Add(5,4); DEBUG_TRACE answer; }
When foo is called, the following would be printed :
answer(00001001)
Unlike C, the compiler will currently allow you to CALL a function with no return type and assign it in an expression. In EDL the result of calling a function with no return value is always 0.
Pins
PINs are similar to normal DECLARE variables. However they are designed to be the external interface to the current file, effectively you should think of a file as being a black box with PINs connecting it to the outside world. PINs cannot be directly accessed outside of the current file, however depending on the type of PIN they will have accessors automatically created :
IN | Input only. Accessor SetPinPIN NAME. |
OUT | Output only. Accessor GetPinPIN NAME. |
BIDIRECTIONAL | Both Input and Output. Creates both IN and OUT accessors (see above) |
Handlers
HANDLERs are functions that are automatically called when even a PIN changes value, they can only be tied to PINs that recieve data from the outside world. HANDLERs must be given a rule that governs when they execute, see table below :
ALWAYS | Will be called whenever the pin recieves a value. |
CHANGED | Will be called whenever the pin recieves a value other than its current. |
TRANSITION(old,new) | Will be called when the stated transition occurs. Useful for pins that are edge triggered. |
PIN Hello[1]; HANDLER Hello ALWAYS { DEBUG_TRACE "Hello World!"; }
and the c driver :
extern void SetPinHello(uint8_t); int main(int argc,char** argv) { SetPinHello(1); }
would produce :
Hello World!
States
Because the hardware under emulation can usually be thought of as a state machine (certainly a cpu can), EDL provides a simple state machine declaration.
STATES a,b|c { STATE a { DEBUG_TRACE "State a"; } STATE b { DEBUG_TRACE "State b"; } STATE c { DEBUG_TRACE "State c"; } }
, means auto increment, where as | acts as an auto increment barrier. Each time the STATES block is reached, it will perform the defined action for its current state, if the state is auto incrementing, the state will be moved onto the next state. States are moved/changed on entry. The above code if entered 4 times, would produce :
State a State b State a State b
It is also possible to define sub states, the current pin accurate 8080 core uses these to represent the tstates for each of the machine cycles. It is important to note; when a sub state exists, it overides any auto incrementing behaviour of the parent. The following code demonstrates this fact, here the code was run 8 times :
STATES a,b { STATE a { STATES a1,a2 { STATE a1 { DEBUG_TRACE "State a1"; } STATE a2 { DEBUG_TRACE "State a2"; } } } STATE b { STATES b1,b2 { STATE b1 { DEBUG_TRACE "State b1"; } STATE b2 { DEBUG_TRACE "State b2"; } } } }
State a1 State a2 State a1 State a2 State a1 State a2 State a1 State a2
Unlike normal variables, state blocks are always global, but tied to a HANDLER. This is so that the state machine can be adjusted from an external function. At present you cannot declare state machines inside functions, but you can reference them (see testing state below).
Adjusting States
To force execution of a different state next time the state block is reached, you can use 1 of 2 methods.
HANDLER example { STATES first|second { } }
NEXT example.second;
The above demonstrates that it is not necassary to implement a state, the default implementation will do nothing - but the auto increment rules will still apply. The NEXT instruction shown would set the examples state machine to second the next time it runs.
HANDLER example { STATES first|second|third { STATE first { PUSH example.third; PUSH example.second; } STATE second { POP example; } STATE third { POP example; } } }
The above shows the other method of changing states, here if the states block was entered 4 times, it would go first,second,third and finally back to first. PUSH and POP can be used to remember a state and return to it at some future point. POP is currently a very expensive operation in EDL the compiler spits out a fair amount of code in order to allow the correct return location for sub states. This will improve in a future version.
Conditions
Conditions much like in c are done using the IF statement. At present there is no concept of an otherwise/else clause.
IF $A == 10 { DEBUG_TRACE "It certainly does!"; }
EDL supports ==,!=,<=,<,>=,> operators, which are ; is equal to, is not equal to, is less than or equal, is less than, is greater than or equal to, is greater than.
The above operators can be combined via any of the boolean operators &,| and ^, which are ; and, or, xor. IF always expects a single bit expression, and will execute the block of code below it if that expr is 1. The == etc, operators always return a single bit. This is why there is no C equivalent of && and ||.
Testing State
One useful feature is the ability to check which state a state machine is currently in. The @ operator is used for this, it will produce a 1 or 0 value depending on if the state machine is in the mentioned state or not. e.g.
IF example.first@ { DEBUG_TRACE "Current state for example is state first"; }
Arithmetic
EDL currently only supports add and subtract operations (I forgot to provide support for multiply,divide,remainder). They work as expected, but its worth remembering that for expressions the left and right side of the expression will always be expanded to the same bit size.
DECLARE foo[8]; foo <- %01 + %1011; # %01 is a smaller bit size than %1011 #therefor %01 is expanded to %0001 before the #addition. DEBUG_TRACE foo;
foo(00001100)
Casting
In EDL casting is a way to force the value of an expression to be a particular bit size. A future update will hopefully allow the cast to be applied on assignment to, although at present this is not supported.
DECLARE foo[8] ALIAS %00111100; DECLARE bar[8]; bar <- foo[2]; # will assign %00000000 to bar bar <- foo[2..6]; # will assign %00001111 to bar
As can be seen above, casting also allows you to take a bit range from an expression. Essentially foo[2] is shorthand for foo[0..1].
Affectors
EDL to date has been concerned with the emulation of processors. One feature common to almost all processors is the status word. As has already been shown, ALIAS allows the status word to be declared exactly as the processor works. But how about the updating of the various status' within that word? Well this is the role of affectors, they allow common cpu flags to be captured from any expression.
DECLARE register1[8]; DECLARE register2[8]; DECLARE register3[8]; DECLARE FLAGS[8] ALIAS s[1]:z[1]:%0:ac[1]:%0:p[1]:%1:cy[1]; AFFECT s AS BIT(7) { register1 + register2 } -> register3;
the above would add register1 and register2 together and store the result in register3. It would also store bit7 of the result into the s register of FLAGS. Affectors can be combined, so :
DECLARE register1[8]; DECLARE register2[8]; DECLARE register3[8]; DECLARE FLAGS[8] ALIAS s[1]:z[1]:%0:ac[1]:%0:p[1]:%1:cy[1]; AFFECT s AS BIT(7),z AS ZERO { register1 + register2 } -> register3;
would do as before, but also set the z bit of flags if the answer was zero (it would be cleared otherwise).
At present there are 6 different affectors available :
BIT(number) | Captures the bit specified from the expression result |
SIGN | Captures the MSB (most significant bit) from the expression result |
ZERO | If the expression result is zero, a 1 is stored, otherwise 0 is stored |
PARITYEVEN | If the parity of bits in the expression is even a 1 is stored, otherwise 0 is stored. Parity is the count of bits equal to 1 |
PARITYODD | If the parity of bits in the expression is odd a 1 is stored, otherwise 0 is stored |
CARRY(number) | If a carry would have occurred from the given bit number, then a 1 is stored. Otherwise 0 is stored (see below) |
CARRY at present is only supported for add and subtract operations. The bit number specified should be the point before where the carry would have occured to/from. This is a little awkward (and perhaps will change in the future).
DECLARE register1[4] ALIAS %0001; DECLARE register2[4] ALIAS %1111; DECLARE register3[4] ALIAS %0000; DECLARE register4[4]; DECLARE FLAGS[8] ALIAS s[1]:z[1]:%0:ac[1]:%0:p[1]:%1:cy[1]; AFFECT c AS CARRY(3) { register1 + register2 } -> register4; # register4 would be %0000 # c would be 1 - %0001 + %1111 is %0000 carry 1 ie Carry out of bit 3 is 1. AFFECT c AS CARRY(3) { register3 - register1 } -> register4; # register4 would be %1111 # c would be 1 - %0000 - %0001 is %1111 carry 1 ie Carry into bit 3 is 1.
In the near future OVERFLOW will be added, along with the possibility to invert the meaning of the affector (especially useful for those processors that invert the carry flags meaning on subtraction e.g.6502).
Shifts and Rotates
Shifts and rotates and variations of them are catered for by the ROL and ROR operators. ROL(value,bitsOut,bitsIn,amount) will perform a transformation on value (shifting it left by amount) and oring in the bitsIn. bitsOut will be set to bits shifted out of value.
DECLARE number[4] ALIAS %0110; DECLARE shiftedOut[4]; DECLARE result[4]; result <- ROL(number,shiftedOut,%11,2); DEBUG_TRACE result," ",shiftedOut;
result(1011) shiftedOut(0001)
ROR is the same as ROL excepts shifts right instead of left. At present a variable must always be passed to the bitsOut part of the expression, in the future this requirement will be relaxed to allow pure shifts to be implemented without having to have temporary storage.
A couple of examples of standard processor shifts and rotates :
ROL(number,shiftedOut,number[3..3],1) | Performs a rotate left of number by 1 bit |
ROR(number,shiftedOut,number[0..0],1) | Performs a rotate right of number by 1 bit |
ROL(number,shiftedOut,0,1) | Performs a logical shift left of number by 1 bit, (multiply by 2) |
ROR(number,shiftedOut,0,1) | Performs a logical shift right of number by 1 bit, (unsigned divide by 2) |
ROR(number,shiftedOut,1,1) | Performs an arithmetic shift right of number by 1 bit, (signed divide by 2) |
Instructions
Another fairly processor specific idea that is encapsulated in EDL, is that of instructions. This feature has been designed to allow disassemblers,assemblers,debug monitors and obviously emulation of cpu opcodes. An opcode on a processor is a sequence of bits that govern the operation that processor would perform. As an example the intel 8080 has an instruction which produces no results, commonly known as NOP (or No OPeration).
INSTRUCTION "NOP" %00000000 { }
"NOP" is the mnemonic representation of the instruction. %00000000 is the binary representation of the opcode.
Executing Instructions
Defining instructions is all well and good, but unless there is a way to execute them, they are only useful for generating disassembly.
EXECUTE IR;
When execution of the program reaches this point, it will call out to the instruction whose opcode matches the current contents of the variable IR.
Instructions continued
At present due to limitations in the compiler EXECUTE MUST occur before the definition of INSTRUCTIONs. This will again be repaired in a future version of the compiler.
If INSTRUCTIONs were limited to only a simple constant expression, declaring an emulation for a processor would be tedious at best. For this reason there are a few helpful features of INSTRUCTION definition. The next example shows one possible way to define the NOP instruction for the Intel 8080 which also takes care of the fact that the NOP instruction has multiple ocpodes (though only 1 is official).
INSTRUCTION "NOP" %00:UNOFFICIAL[3]:%000 { }
On the 8080, opcodes $00,$08,$10,$18,$20,$28,$30,$38 are all identical. The above INSTRUCTION definition basically defines all 8 of these as NOP. When the compiler sees an identifier with a bit size within an opcode definition it will automatically fill in the bits with all possible combinations.
Mappings
A second helpful feature is the idea of a MAPPING, they share a certain similarity to the C preprocessor macro #define. Although they are part of the language. The Intel 8080 has a number of opcodes which deal with the copying of one register into another (MOV A,B for instance). Mappings allow us to achieve all of the MOV register,register combinations in a single INSTRUCTION definition.
MAPPING SSS[3] { %000 "B" B; %001 "C" C; %010 "D" D; %011 "E" E; %100 "H" H; %101 "L" L; # %110 Not used %111 "A" A; } MAPPING DDD[3] { %000 "B" B; %001 "C" C; %010 "D" D; %011 "E" E; %100 "H" H; %101 "L" L; # %110 Not used %111 "A" A; } INSTRUCTION "MOV %M1,%M0" %01:DDD:SSS { DDD <- SSS; }
The above declares two mappings (SSS and DDD), they are identical SSS is considered the source operand, DDD the destination. Similar to what happens when an identifier + bit size (see Instructions continued) is given to an opcode definition, the compiler expands the SSS and DDD parts of the opcode. However it will now only expand them to the values specified in the first column of the mapping. So this gives 7 out of 8 possible bit values (since %110 is commented out). Within the instruction itself, you can reference the SSS or DDD's and when you do the compiler will insert the expression in the third column of the mapping in its place. Finally, the "MOV %M1,%M0" string (that is used in disassembler generation) automatically replaces the %Mn values with the appropriate item from the second column. So one possible opcode generated by the above is %01111011 the code below is equivalent :
INSTRUCTION "MOV A,E" %01111011 { A <- E; }
Instructions with immediate data
CISC processors often have multi operand instructions that would not fit within the opcode alone. E.g. "CALL $00FF" on the 8080 this encodes as %11001101 %11111111 %00000000 which is 3 bytes, the hi and lo order bytes of the address are flipped. In EDL these additional bytes can be specified (comma seperated) after the instruction opcode. However they are ignored, the reason being that the memory system is not part of the cpu and therefor the logic to retrieve those values is part of the instruction itself.
INSTRUCTION "CALL %$2%$1" %11001101,B2[8],B3[8] { }
For disassembler purposes the %$n components are used to specify the offset from the address being disassembled that the subsequent operands can be found.
Instances (or how to use other edl files)
As has been mentioned before, an edl file is supposed to be a black box, with the PINs providing the interface. This leads to the question of how to make use of these files from another. EDL uses the concept of an INSTANCE for this purpose.
#This file is saved as inverter.edl PIN IN INPUT[1]; PIN OUT OUTPUT[1]; HANDLER INPUT { OUTPUT <- ~INPUT; }
INSTANCE "inverter.edl" AS Chip1; FUNCTION test { DECLARE result[1]; Chip1 INPUT<-%0; result <- Chip1 OUTPUT; DEBUG_TRACE result; }
result(1)
The first file above, is a simple 1 bit inverter.. it will output the opposite of its input. The second file, shows how INSTANCEs work. The first parameter is a string containing the path to the edl file, the second parameter is how you wish it to be known.
To reference a PIN within an INSTANCEd module, you supply the name you chose before the PIN name (seperated by a space). In case it is not immediately obvious from the above, you can INSTANCE the same file multiple times, giving each a unique name.
Instances and scoping
Once an edl file is INSTANCEd any global declarations from within the INSTANCE become private to the edl file including it. This allows each file to maintain its black box behaviour, even when it uses others. For instance, if we had an edl file called alu.edl and we INSTANCEd it from within a file cpu.edl, and then in turn, INSTANCEd that within a file computer.edl, to the outside world (perhaps a C Language driver), the contents of alu.edl and cpu.edl would be inaccessable.
Interfacing to C
To call a HANDLER from C, you must set a value to the PINs accessor function - see Pins above. Currently any globally declared variables are also accessable (unless marked INTERNAL, however only the non aliased names are available, and care must be taken to match the bit size to the C Variable size. Its best to only access 8,16 or 32 bit values as they have direct representation in C. Similarly, any globally declared functions can be accessed from C, however similar care must be taken as with variables due to bit size matching.
It is also possible to call C functions from within an EDL file, however the C function must only use unsigned params and return values (void return value is allowed). The compiler will currently only allow 0,8,16 or 32 bit return values and 8,16 or 32 bit parameters.
C_FUNC_EXTERN [16] Multiply [8],[8]; FUNCTION test { DECLARE result[16]; result <- CALL Multiply(5,8); DEBUG_TRACE result; }
uint16_t Multiply(uint8_t a,uint8_t b) { return a*b; }
Would hopefully return :
result(0000000000101000)
Automatic promotion/truncation of values is performed for parameters into the function, no warnings are currently generated. One other warning, at present EDL is perfectly happy to use the return value of a C function that is declared void, EDL will use 0 instead.