Friday 25 April 2014

Load/Save Virtual CPU Memory State & Debugging Code (C++)

Before we go further with our CPU project we need to do some house keeping on the code, we need to add some helpers, the first I'm going to add is a "debug" flag to the CPU code, this is not an official part of our specification, so we leave our diagram for the chip unchanged.

This flag however is going to make some logging come out of our code, so we need to add the following headers to the CPU class header.

#include <string>

And inside the CPU class we're going to add a new function:

void Log(const std::string& p_Message);

The body of this function is going to be very boring:

void CPU::Log(const std::string& p_Message)
{
std::cout << p_Message << std::endl;
}

Simple squirting the message string to the output stream.

And we're going to add calls to this message function from all the other functions we use, so we know what the CPU is going to do... But first we need to add the new flag to the CPU:

bool m_DebugMode;

Then update the CPU class constructor to default this to false....

CPU::CPU(Memory* p_TheMemory)
:
c_ReservedAddress(0),
c_BaseAddress(2),
c_JumpToAddress(1),
c_AddressCeiling(253),
m_ProgramCounter(c_BaseAddress),
m_Register0(0),
m_Register1(0),
m_OverflowError(false),
m_UnderflowError(false),
m_SignedMode(false),
m_TheMemory(p_TheMemory), // DOH!
m_Halt(false),
m_DebugMode(false)
{
}

Next we need to be able to create the CPU in debug node, so we're going to add a parameter to the constuctor, but we're going to make this parameter optional, if the programmer does not pass the flag it will default to false, i.e. no debugging.  Only when they actively pass the flag as true will one see the debug output.

CPU(Memory* p_TheMemory, const bool& p_DebugMode = false);

And the implementation:

CPU::CPU(Memory* p_TheMemory,
const bool& p_DebugMode)
:
c_ReservedAddress(0),
c_BaseAddress(2),
c_JumpToAddress(1),
c_AddressCeiling(253),
m_ProgramCounter(c_BaseAddress),
m_Register0(0),
m_Register1(0),
m_OverflowError(false),
m_UnderflowError(false),
m_SignedMode(false),
m_TheMemory(p_TheMemory),
m_Halt(false),
m_DebugMode(p_DebugMode) // AND THIS!
{
}

This code compiles, we've changed the CPU constructor, but our code can still call it with just the memory parameter because we've specified a default for the debug mode!

Our first piece of logging is if we get an unknown OP Code, in our Decode function we need to add a new case, the default case... And we want to stop the application running if there's an invalid op code:

default:
Log("Unknown Op code - halting");
Halt();
break;
I'm going to set mine to log and then Halt, you might just want to Log, its up to you.  I always want this to happen, so there's no checking for debug mode!

So what do we want the CPU to outptu when we're in Debug mode?... Well the program counter each step would be good to know, so how about we start in the while loop within the Run function, before the Fetch we can add the following code:

if ( m_DebugMode )
{
    std::cout << "[" << (int)m_ProgramCounter << "]" 
              << std::endl;
}

Next in each op code function I'm going to add a debug message with "Log", so here's a few:

void CPU::Load1()
{
if ( m_DebugMode )
{
Log("Load1");
}
Or

void CPU::JumpEqual()
{
if ( m_Register0 == m_Register1 )
{
if ( m_DebugMode )
{
Log ("Jump Equal - Jumping");
}
JumpTo();
}
else
{
if ( m_DebugMode )
{
Log ("Jump Equal - Not Jumping");
}
// Skip over the address of the jump!
++m_ProgramCounter;
}
}

Or

void CPU::ClearBoth()
{
if ( m_DebugMode )
{
Log("Clear Both");
}
ClearRegister0();
ClearRegister1();
}

Then we also have functions which we want a little more debugging out of... Such as Add, it would be useful to have the values of the registers before and then after...

void CPU::Add()
{
if ( m_DebugMode )
{
Log("Add");
std::cout << "Before [" << (int)m_Register0 << ", " << (int)m_Register1 << "]" << std::endl;
}

m_Register0 = m_Register0 + m_Register1;

if ( m_DebugMode )
{
std::cout << "After [" << (int)m_Register0 << ", " << (int)m_Register1 << "]" << std::endl;
}
}

So we stream out the two registers before and after, and the same in multiply, what about a more complex operation like store?... Well, we could output the register when we've read in the target address, then output the value from the register we're saving and then we know what we jsut wrote & where.

void CPU::Store()
{
if ( m_DebugMode )
{
Log("Store");
}

// Load the target address into register 1
m_Register1 = m_TheMemory->Read(m_ProgramCounter);

if ( m_DebugMode )
{
std::cout << "Target Address: " << (int)m_Register1 << std::endl;
std::cout << "Value to Write: " << (int)m_Register0 << std::endl;
}

++m_ProgramCounter; // Skip the memory location data
// Write the register 0 value to this address
m_TheMemory->Write(m_Register1, m_Register0);
// Remember the order of our parameters
// was ADDRESS then VALUE!

if ( m_DebugMode )
{
std::cout << "Written value: " << (int)m_TheMemory->Read(m_Register1) << std::endl;
}
}

Finally to use the Debug Mode we need to add something into the main program... Well, I'm going to have a bool set to false, and add an option to my menu which toggles the flag... So everytime I use the toggle option debugging switches "debugging = !debugging", and on the menu I'm going to output the debugging setting.  You can do this yourself :)

Once done I just feed this into my "new CPU" call as the second parameter, and voila I can turn CPU debugging on and off to run my program.

Next, typing in all those program bytes is getting on my nerves, so how about we load them from a text file?

In the main menu I'm adding "S" and "L" and they are going to Load and Save the current memory, given a filename.

I'm going to pipe these to a simple pair of functions which ask for a filename, and then call into the Memory Class to load and save itself.

So the two functions I'm adding to the main program are like this:

void LoadMemory(Memory* theMemory)
{
if ( theMemory != nullptr )
{
cout << endl << "Load Memory from: ";
string filename;
cin >> filename;
theMemory->Load(filename);
cout << endl;
}
}

void SaveMemory(Memory* theMemory)
{
if ( theMemory != nullptr )
{
cout << endl << "Save Memory to: ";
string filename;
cin >> filename;
theMemory->Save(filename);
cout << endl;
}
}

In the memory header I then add "#include <string>" and these two function prototypes:

/// Save to file
void Save(const std::string& p_Filename);

/// Load from file
void Load(const std::string& p_Filename);

The memory save function is then very simple, for each byte in our address space (including bytes 0 and 1) we're going to write out the integer as text on a line, so each line of our file will contain one byte.

void Memory::Save(const std::string& p_Filename)
{
using namespace std;
ofstream file (p_Filename, ios_base::out);
if ( file.good() )
{
for (byte i = 0; i < c_MaxAddress; ++i)
{
file << (int)m_MemorySpace[i] << endl;
}

file.close();
}
else
{
cout << "Bad path [" << p_Filename << "]" << endl;
}
}

If we could not open the file, then we output bad path.

Load is similar, but we don't know how many bytes the user might give us, so lets follow through the code:

void Memory::Load(const std::string& p_Filename)
{
Clear();
So we've cleared the memory and everything is zero, we now know we need to open a file, so at the top of the file I'm going to add "#include <fstream>" and we also know we're going to convert the text we load into a binary number in memory so we need "#include <cstdlib>" the standard C library implementation for C++.

Our code then continues, opening the file and checking its good:

using namespace std;
ifstream file(p_Filename, ios_base::in);
if ( file.good() )
{
Next we need two values the index (byte) we've read to  and a temporary location for the text to number conversion we're doing each step.

int i = 0;
int temp;
And while the file is good, that is whilst we've not run out of bytes...
while ( file.good() )
{
Read a line of text...
string buff;
file >> buff;
Convert it from text (ascii to integer)...

temp = atoi(buff.c_str());
And then we assign the byte we loaded into the memory space we've got, and move onto the next memory index.

m_MemorySpace[i] = (byte)temp;
++i;
}

file.close();
}
else
{
cout << "File not found [" << p_Filename << "]" << endl;
}
}

This function is quick and dirty, it has some problems, if you can see them or want to address them and check back with me what you spot to build experience/knowledge let me know in the comments below.

Now when we want to write a program, we can just open a text editor, add two lines with "0" and "0" on them at the top to skip our reserved memory area and then enter bytes of memory...

Remember once you've used load, you can use Report Memory to list back what you've loaded.

So go a head try to use your virtual CPU before you just jump to the page with the code.

3 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. As for reading the program into memory, you may want to check out this my version of it. As this is an 8 bit processor all the opcodes are stored as a single byte, so I figured it would be ideal to load the file in binary mode and just keep grabbing and storing the bytes as they're coming through the stream.

    void loadProgram(Memory* theMemory)
    {
    ClearMemory(theMemory); // Resets memory to 0

    cout << "Program name ( *.rom ): ";
    string filename;
    cin >> filename;

    ifstream ifs;
    ifs.open(filename, ios::in, ios::binary); // Open the file in binary mode

    ifs.seekg(0, ifs.end); // Start from the beginning and scroll to the end

    streamoff length; // streamoff is just a long long as files can get big

    length = ifs.tellg(); // Store the length of file (How many bytes)

    ifs.seekg(0, ifs.beg); // Go back to the beginning of the file for reading

    if(ifs.is_open())
    {
    for(int i = 1; i < length; i++) // Memory 0 is reserved for loading opcodes for execution
    {

    theMemory->Write(i , ifs.get()); // Store the bytes sequentially
    }

    cout << "OK!" << endl;
    }
    else
    {
    cout << "\nCould not load file: " << filename << endl;
    }
    }

    Let me know what you think.

    ReplyDelete
    Replies
    1. This does not allow you to write numbers to a text editor and save it. It pulls back you're written numbers as ASCII numerical values rather than the raw values you had saved originally. I had written my own assembler function and built this from that. Sorry!

      Delete