When we last left off, we had a basic machine that could add two numbers. This time, we’ll add a new register to act as a set of flags to convey the machine status, instructions to allow us to set and reset these flags and a few other useful arithmetic instructions.
The New Architecture
We’re going to add a new register, which isn’t a large change
This will act not as register that holds a number, like the other registers in the CPU, but as a set of 32 binary flags we can use as indicators for things like errors and interrupts. Each bit in the register will be one flag, meaning we can mask the register with the flag value in order to get the value.
The register will be addressed with the number 0xC
.
We’ll start with a few obvious flags for now:
0x1
- This value will indicate a halt0x2
- Arithmetic overflows0x4
- Arithmetic underflows0x8
- Divide by zero
The new instructions
SUB
- Subtract I2 from I1 and store the result in DMUL
- Multiply I1 by I2 and store the result in DDIV
- Divide I1 by I2 and store the result in DSTAT
- Get the value of the I1th bit in the status register (SR
) and store it in DSET
- Set the value of the I1th bit to I2
The coding
The new register
This is pretty straightforward, same as the old one. First, replace __reserved9
with SR
in register.go
//...
__reserved8
SR
PC
//...
And then make sure it exists in the map when we call NewRegisterBank
:
func NewRegisterBank() *RegisterBank {
return &RegisterBank{
registerMap: map[uint8]*Register{
//...
SR: {0x00},
//...
}
}
}
And we’ll add some more constants, so we can easily refer to our status flags in code
const (
STATUS_HALT = 1 << iota
STATUS_OVERFLOW
STATUS_UNDERFLOW
STATUS_DIVIDE_BY_ZERO
STATUS_MEMORY_ERROR
)
The new instructions
Let’s start with arithmetic operations. We’ll also update our ADD
operation to set the overflow if it needs to
func (c *CPU) add(i1, i2 *Register) {
i1Val := uint64(i1.Value)
i2Val := uint64(i2.Value)
sum := i1Val + i2Val
if sum > 0xFFFFFFFF {
sr, err := c.registers.GetRegister(SR)
// We'll panic here because if the status register doesn't work then our machine may as well crash
if err != nil {
panic(err)
}
sr.Value = sr.Value | STATUS_OVERFLOW
}
i2.Value = uint32(sum & 0xFFFFFFFF)
}
Similarly, SUB
will do an underflow check since we technically don’t really support signed operations yet
func (c *CPU) sub(i1, i2 *Register) {
i1Val := int64(i1.Value)
i2Val := int64(i2.Value)
diff := i1Val - i2Val
if diff < 0 {
sr, err := c.registers.GetRegister(SR)
if err != nil {
panic(err)
}
sr.Value = sr.Value | STATUS_UNDERFLOW
diff = diff + 0xFFFFFFFF
}
i2.Value = uint32(diff & 0xFFFFFFFF)
}
MUL
will check overflow
func (c *CPU) mul(i1, i2 *Register) {
i1Val := int64(i1.value)
i2Val := int64(i2.value)
product := i1Val * i2Val
if product > 0xFFFFFFFF {
sr, err := c.registers.GetRegister(SR)
if err != nil {
panic(err)
}
sr.value = sr.value | STATUS_OVERFLOW
}
i2.value = uint32(product & 0xFFFFFFFF)
}
And finally, DIV
will check the divide by zero status
func (c *CPU) div(i1, i2 *Register) {
i1Val := i1.value
i2Val := i2.value
if i2Val == 0 {
sr, err := c.registers.GetRegister(SR)
if err != nil {
panic(err)
}
sr.value = sr.value | STATUS_DIVIDE_BY_ZERO
return
}
i2.value = i1Val / i2Val
}
Next, we’ll add in the instructions to get and set status flags
func (c *CPU) stat(i1, i2 *Register) {
var bit uint32 = 1 << (i1.value - 1)
sr, err := c.registers.GetRegister(SR)
if err != nil {
panic(err)
}
// Should be either 0 or 1
i2.value = (sr.value & bit) / bit
}
func (c *CPU) set(i1, i2 *Register) {
sr, err := c.registers.GetRegister(SR)
if err != nil {
panic(err)
}
var bit uint32 = 1 << (i1.value - 1)
if i2.value > 0 {
sr.value = sr.value | bit
} else {
sr.value = sr.value ^ bit
}
}
And finally putting the new instructions in the opcode switch statement
switch opcode {
//...
case SUB:
c.sub(i1, i2)
case MUL:
c.mul(i1, i2)
case DIV:
c.div(i1, i2)
case STAT:
c.stat(i1, i2)
case SET:
c.set(i1, i2)
}
Retrofitting the new halt flag
Previously, we used the CPU.Halted
boolean to track if our machine was halted. We’ll refactor a little to use that
status flag instead, starting with our HALT
operation
func (c *CPU) halt(_, _ *Register) {
sr, err := c.registers.GetRegister(SR)
if err != nil {
panic(err)
}
sr.value = sr.value | STATUS_HALT
}
Next, we’ll fix up our Tick
function to use that flag
func (c *CPU) Tick() error {
sr, err := c.registers.GetRegister(SR)
if err != nil {
return err
}
if sr.value&STATUS_HALT > 0 {
return fmt.Errorf("cannot tick on a Halted machine")
}
//...
}
Tidying up
As a kind of capstone, we’ll clean up the few spots that set Halted
to true on a memory error, instead
setting the memory error flag.
func (c *CPU) read(i1, i2 *Register) {
val, err := c.bus.Read(i1.Value)
if err != nil {
sr, err := c.registers.GetRegister(SR)
if err != nil {
panic(err)
}
sr.Value = sr.Value | STATUS_MEMORY_ERROR
return
}
i2.Value = val
}
func (c *CPU) write(i1, i2 *Register) {
err := c.bus.Write(i2.Value, i1.Value)
if err != nil {
sr, err := c.registers.GetRegister(SR)
if err != nil {
panic(err)
}
sr.Value = sr.Value | STATUS_MEMORY_ERROR
}
}
Conclusion
We have a machine that can tell us a little about itself now, on top of being able to perform some more simple arithmetic. Next up, we’ll implement some flow control to make loops and function calls possible. Code for this blog is available here