0 - Introduction
In previous posts we already made a working Chip8 Emulator both on Ubuntu and on a Esp32 with the help of the Arduino libs and U8g2. In this article though, we will ditch every help we previously had and implement everything from scratch, as it has become tradition in our ‘Baremetal Arduino’ series.
Before starting, you should know that you can use any code editor, but with the help of VSCode, following this article becomes a little easier.
Sadly, because of the I2C display we will be using, we only get about 60 instructions per second and 30 frames per second, this makes games very slow. If you have an SPI display and know how to set it up, you will get a playable emulator, but still way worse than the Esp32 and Ubuntu versions.
Keep in mind that this is more of a proof of concept than an actual console/device you will be able to play on.
As for hardware, here is what you will need:
- 1x Arduino Mega
- 1x 16 Button Keypad
- 1x Passive Buzzer
- 1x Logical Shifter
- 1x I2C SD1306 Display
- 5x 10k Ω resistors (or a Starter kit)
- 1x Red Led (Optional)
- 1x Breadboard (Optional)
1 - Wiring and Project Setup
The first thing we will connect is going to be the keypad, to do it, we will need atleast 9 wires and 4 resistors, the resistance can vary a little but between 1K and 10K is perfect. The first four pins are going to be the output pins, and the other four, with the resistors, are going to be the input-pulldown pins:
To connect our display, we will be using a logic shifter to convert our 5V I2C signal to a 3.3V signal, connect the pins like the images below. Things to keep in mind, the OE pin connects to 3.3V through a resistor (1K to 10K), the A side is the lower voltage and the B side is the higher voltage:
Connecting the buzzer is very easy, just plug negative to Ground, and positive to pin 10. If you have a passive buzzer (the one with 3 pins), connect negative to ground, positive to 3.3V and I/O to pin 10:
At the end, with every component connected, you should have a circuit like this:
Before we start to code the emulator we need to do some basic setup. One of the files you will need is the Makefile, it allows us to compile and upload our code to the Arduino:
# ./Makefile
SPORT=/dev/ttyACM0
BAUD=115200
F_CPU=16000000UL
MMCU=atmega2560
PROGRAMMER_ID=wiring
# ====================================================================================================
# directories
DIR_SRC=src
DIR_OUT=out
OUT_NAME=program
# ====================================================================================================
# files
ELF=$(DIR_OUT)/$(OUT_NAME).elf
HEX=$(DIR_OUT)/$(OUT_NAME).hex
EXE=$(DIR_OUT)/$(OUT_NAME)
# find all c files in src dir (ignore test.c file)
SOURCES := $(shell find $(DIR_SRC) -name '*.cpp')
OBJECTS := $(patsubst $(DIR_SRC)/%.cpp, $(DIR_OUT)/%.o, $(SOURCES))
# ====================================================================================================
# compilers
CC=avr-gcc
OCP=avr-objcopy
# upload to arduino cmd
UPLOADCMD = sudo avrdude -V -c $(PROGRAMMER_ID) -p $(MMCU) -P $(SPORT) -b $(BAUD) -D -U flash:w:$(HEX):i
# ====================================================================================================
# compiler flags
CC_FLAGS= -Os -Wall -DF_CPU=$(F_CPU)
# linker flags
LD_FLAGS=
# ====================================================================================================
# compile files
$(DIR_OUT)/%.o: $(DIR_SRC)/%.cpp
mkdir -p $(dir $@)
$(CC) $(LD_FLAGS) $(CC_FLAGS) -mmcu=$(MMCU) -c -o $@ $<
$(ELF): $(OBJECTS)
$(CC) $(LD_FLAGS) $(CC_FLAGS) -mmcu=$(MMCU) -o $@ $(OBJECTS)
$(HEX): $(ELF)
$(OCP) $(ELF) -O ihex $(HEX)
# ====================================================================================================
# 'functions'
.PHONY: all, upload, flash, clean
dir:
mkdir -p $(DIR_OUT)
# compile
all: $(HEX) # compile hex file
# uploads the hex file to the arduino
flash:
$(UPLOADCMD)
# builds and uploads the hex file to the arduino
upload: all
$(UPLOADCMD)
# rm build files
clean:
rm $(HEX) $(ELF)
If you are using VSCode, like i am, you will need to create a folder named ‘.vscode’ and in it, create a file named ‘c_cpp_properties.json’:
{
"configurations":
[
{
"name": "AVR ArdMega",
"includePath": [
"${workspaceFolder}/**",
"/usr/lib/avr/include"
],
"defines": [
"__AVR_ATmega2560__"
],
"compilerPath": "/usr/bin/avr-gcc",
"cStandard": "c11",
"cppStandard": "gnu++11",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}
2 - Pins
Now we can move onto the coding. The first thing we will code is a driver to control the Mega’s pins. Here you can see a photo of the layout that your folder should have by the end of this section:
The first file we will create is an header file that will contain an interface, that means that we will create another class that inherits this one, and implements the actual code.
Besides this interface class, we will also create two enums, one for the pin mode and another for the state:
// ./src/tavr/pins.h
#ifndef _tavr_pins_h_
#define _tavr_pins_h_
#include <inttypes.h>
enum EPinMode : uint8_t
{
INPUT = 0,
OUTPUT = 1,
INPUT_PULLUP = 2,
INPUT_PULLDOWN = 3
};
enum EPinLevel : uint8_t
{
LOW = 0,
HIGH = 1
};
class TPin
{
public:
/* Set pin mode */
virtual void mode(EPinMode m) = 0;
/* Set pin mode */
virtual void mode(uint8_t m) { mode((EPinMode)m); }
/* Set pin state */
virtual void set(EPinLevel level) = 0;
/* Set pin state */
virtual void set(uint8_t level) { set((EPinLevel)level); }
/* Read pin state */
virtual uint8_t read() = 0;
};
#endif // _pins_h_
With the interface done, we will now create a new class inside the mega folder, and it will inherit the interface we just created. In this file, we will create 4 private variables, one to store the data direction register, one for the port register, one for the pin register and one for the bit. Below this new class we will also create variables for each digital pin of the Mega, but we will declare them as external, so that we can create them in the cpp file.
// ./src/tavr/mega/pins.h
#ifndef _tavr_mega_pins_h_
#define _tavr_mega_pins_h_
#include "../pins.h"
class TPin_Mega : public TPin
{
private:
volatile uint8_t* ddr; /* DDRx */
volatile uint8_t* port; /* PORTx */
volatile uint8_t* pin; /* PINx */
uint8_t bit; /* Pxn */
public:
TPin_Mega(volatile uint8_t* ddr, volatile uint8_t* port, volatile uint8_t* pin, uint8_t bit);
/* Set pin mode */
void mode(EPinMode mode) override;
/* Set pin state */
void set(EPinLevel v) override;
/* Read pin state */
uint8_t read() override;
};
#define LED_BUILTIN D13
#define PIN_OC2A D10
extern TPin_Mega D0;
extern TPin_Mega D1;
extern TPin_Mega D2;
extern TPin_Mega D3;
extern TPin_Mega D4;
extern TPin_Mega D5;
extern TPin_Mega D6;
extern TPin_Mega D7;
extern TPin_Mega D8;
extern TPin_Mega D9;
extern TPin_Mega D10;
extern TPin_Mega D11;
extern TPin_Mega D12;
extern TPin_Mega D13;
extern TPin_Mega D14;
extern TPin_Mega D15;
extern TPin_Mega D16;
extern TPin_Mega D17;
extern TPin_Mega D18;
extern TPin_Mega D19;
extern TPin_Mega D20;
extern TPin_Mega D21;
extern TPin_Mega D22;
extern TPin_Mega D23;
extern TPin_Mega D24;
extern TPin_Mega D25;
extern TPin_Mega D26;
extern TPin_Mega D27;
extern TPin_Mega D28;
extern TPin_Mega D29;
extern TPin_Mega D30;
extern TPin_Mega D31;
extern TPin_Mega D32;
extern TPin_Mega D33;
extern TPin_Mega D34;
extern TPin_Mega D35;
extern TPin_Mega D36;
extern TPin_Mega D37;
extern TPin_Mega D38;
extern TPin_Mega D39;
extern TPin_Mega D40;
extern TPin_Mega D41;
extern TPin_Mega D42;
extern TPin_Mega D43;
extern TPin_Mega D44;
extern TPin_Mega D45;
extern TPin_Mega D46;
extern TPin_Mega D47;
extern TPin_Mega D48;
extern TPin_Mega D49;
extern TPin_Mega D50;
extern TPin_Mega D51;
extern TPin_Mega D52;
extern TPin_Mega D53;
extern TPin_Mega D54;
extern TPin_Mega D55;
extern TPin_Mega D56;
extern TPin_Mega D57;
extern TPin_Mega D58;
extern TPin_Mega D59;
extern TPin_Mega D60;
extern TPin_Mega D61;
extern TPin_Mega D62;
extern TPin_Mega D63;
extern TPin_Mega D64;
extern TPin_Mega D65;
extern TPin_Mega D66;
extern TPin_Mega D67;
extern TPin_Mega D68;
extern TPin_Mega D69;
#endif // _pins_h_
On the cpp file, we create a constructor that takes in all the registers and bit and stores them, then we create the mode, set and read functions and finally, we create the pins we declared in the header file:
// ./src/tavr/mega/pins.cpp
#include "pins.h"
#include <avr/io.h>
TPin_Mega::TPin_Mega(volatile uint8_t* ddr, volatile uint8_t* port, volatile uint8_t* pin, uint8_t bit) :
ddr(ddr), port(port), pin(pin), bit(bit) {}
void TPin_Mega::mode(EPinMode mode)
{
switch (mode)
{
case EPinMode::OUTPUT:
*(this->ddr) |= _BV(this->bit); // ddr output
return;
case EPinMode::INPUT_PULLUP:
*(this->ddr) &= ~_BV(this->bit); // ddr input
*(this->port) |= _BV(this->bit); // pullup
return;
case EPinMode::INPUT:
case EPinMode::INPUT_PULLDOWN:
default:
*(this->ddr) &= ~_BV(this->bit); // ddr input
*(this->port) &= ~_BV(this->bit); // pulldown
return;
}
}
void TPin_Mega::set(EPinLevel level)
{
switch (level)
{
case EPinLevel::HIGH:
*(this->port) |= _BV(this->bit);
return;
case EPinLevel::LOW:
default:
*(this->port) &= ~_BV(this->bit);
return;
}
}
uint8_t TPin_Mega::read()
{
if (*(this->ddr) & _BV(this->bit)) // is output?
return (*(this->port) & _BV(this->bit)) ? 1 : 0; // read from port
else
return (*(this->pin) & _BV(this->bit)) ? 1 : 0; // read from pin
}
TPin_Mega D0 = TPin_Mega(&DDRE, &PORTE, &PINE, PE0);
TPin_Mega D1 = TPin_Mega(&DDRE, &PORTE, &PINE, PE1);
TPin_Mega D2 = TPin_Mega(&DDRE, &PORTE, &PINE, PE4);
TPin_Mega D3 = TPin_Mega(&DDRE, &PORTE, &PINE, PE5);
TPin_Mega D4 = TPin_Mega(&DDRG, &PORTG, &PING, PG5);
TPin_Mega D5 = TPin_Mega(&DDRE, &PORTE, &PINE, PE3);
TPin_Mega D6 = TPin_Mega(&DDRH, &PORTH, &PINH, PH3);
TPin_Mega D7 = TPin_Mega(&DDRH, &PORTH, &PINH, PH4);
TPin_Mega D8 = TPin_Mega(&DDRH, &PORTH, &PINH, PH5);
TPin_Mega D9 = TPin_Mega(&DDRH, &PORTH, &PINH, PH6);
TPin_Mega D10 = TPin_Mega(&DDRB, &PORTB, &PINB, PB4);
TPin_Mega D11 = TPin_Mega(&DDRB, &PORTB, &PINB, PB5);
TPin_Mega D12 = TPin_Mega(&DDRB, &PORTB, &PINB, PB6);
TPin_Mega D13 = TPin_Mega(&DDRB, &PORTB, &PINB, PB7);
TPin_Mega D14 = TPin_Mega(&DDRJ, &PORTJ, &PINJ, PJ1);
TPin_Mega D15 = TPin_Mega(&DDRJ, &PORTJ, &PINJ, PJ0);
TPin_Mega D16 = TPin_Mega(&DDRH, &PORTH, &PINH, PH1);
TPin_Mega D17 = TPin_Mega(&DDRH, &PORTH, &PINH, PH0);
TPin_Mega D18 = TPin_Mega(&DDRD, &PORTD, &PIND, PD3);
TPin_Mega D19 = TPin_Mega(&DDRD, &PORTD, &PIND, PD2);
TPin_Mega D20 = TPin_Mega(&DDRD, &PORTD, &PIND, PD1);
TPin_Mega D21 = TPin_Mega(&DDRD, &PORTD, &PIND, PD0);
TPin_Mega D22 = TPin_Mega(&DDRA, &PORTA, &PINA, PA0);
TPin_Mega D23 = TPin_Mega(&DDRA, &PORTA, &PINA, PA1);
TPin_Mega D24 = TPin_Mega(&DDRA, &PORTA, &PINA, PA2);
TPin_Mega D25 = TPin_Mega(&DDRA, &PORTA, &PINA, PA3);
TPin_Mega D26 = TPin_Mega(&DDRA, &PORTA, &PINA, PA4);
TPin_Mega D27 = TPin_Mega(&DDRA, &PORTA, &PINA, PA5);
TPin_Mega D28 = TPin_Mega(&DDRA, &PORTA, &PINA, PA6);
TPin_Mega D29 = TPin_Mega(&DDRA, &PORTA, &PINA, PA7);
TPin_Mega D30 = TPin_Mega(&DDRC, &PORTC, &PINC, PC7);
TPin_Mega D31 = TPin_Mega(&DDRC, &PORTC, &PINC, PC6);
TPin_Mega D32 = TPin_Mega(&DDRC, &PORTC, &PINC, PC5);
TPin_Mega D33 = TPin_Mega(&DDRC, &PORTC, &PINC, PC4);
TPin_Mega D34 = TPin_Mega(&DDRC, &PORTC, &PINC, PC3);
TPin_Mega D35 = TPin_Mega(&DDRC, &PORTC, &PINC, PC2);
TPin_Mega D36 = TPin_Mega(&DDRC, &PORTC, &PINC, PC1);
TPin_Mega D37 = TPin_Mega(&DDRC, &PORTC, &PINC, PC0);
TPin_Mega D38 = TPin_Mega(&DDRD, &PORTD, &PIND, PD7);
TPin_Mega D39 = TPin_Mega(&DDRG, &PORTG, &PING, PG2);
TPin_Mega D40 = TPin_Mega(&DDRG, &PORTG, &PING, PG1);
TPin_Mega D41 = TPin_Mega(&DDRG, &PORTG, &PING, PG0);
TPin_Mega D42 = TPin_Mega(&DDRL, &PORTL, &PINL, PL7);
TPin_Mega D43 = TPin_Mega(&DDRL, &PORTL, &PINL, PL6);
TPin_Mega D44 = TPin_Mega(&DDRL, &PORTL, &PINL, PL5);
TPin_Mega D45 = TPin_Mega(&DDRL, &PORTL, &PINL, PL4);
TPin_Mega D46 = TPin_Mega(&DDRL, &PORTL, &PINL, PL3);
TPin_Mega D47 = TPin_Mega(&DDRL, &PORTL, &PINL, PL2);
TPin_Mega D48 = TPin_Mega(&DDRL, &PORTL, &PINL, PL1);
TPin_Mega D49 = TPin_Mega(&DDRL, &PORTL, &PINL, PL0);
TPin_Mega D50 = TPin_Mega(&DDRB, &PORTB, &PINB, PB3);
TPin_Mega D51 = TPin_Mega(&DDRB, &PORTB, &PINB, PB2);
TPin_Mega D52 = TPin_Mega(&DDRB, &PORTB, &PINB, PB1);
TPin_Mega D53 = TPin_Mega(&DDRB, &PORTB, &PINB, PB0);
TPin_Mega D54 = TPin_Mega(&DDRF, &PORTF, &PINF, PF0);
TPin_Mega D55 = TPin_Mega(&DDRF, &PORTF, &PINF, PF1);
TPin_Mega D56 = TPin_Mega(&DDRF, &PORTF, &PINF, PF2);
TPin_Mega D57 = TPin_Mega(&DDRF, &PORTF, &PINF, PF3);
TPin_Mega D58 = TPin_Mega(&DDRF, &PORTF, &PINF, PF4);
TPin_Mega D59 = TPin_Mega(&DDRF, &PORTF, &PINF, PF5);
TPin_Mega D60 = TPin_Mega(&DDRF, &PORTF, &PINF, PF6);
TPin_Mega D61 = TPin_Mega(&DDRF, &PORTF, &PINF, PF7);
TPin_Mega D62 = TPin_Mega(&DDRK, &PORTK, &PINK, PK0);
TPin_Mega D63 = TPin_Mega(&DDRK, &PORTK, &PINK, PK1);
TPin_Mega D64 = TPin_Mega(&DDRK, &PORTK, &PINK, PK2);
TPin_Mega D65 = TPin_Mega(&DDRK, &PORTK, &PINK, PK3);
TPin_Mega D66 = TPin_Mega(&DDRK, &PORTK, &PINK, PK4);
TPin_Mega D67 = TPin_Mega(&DDRK, &PORTK, &PINK, PK5);
TPin_Mega D68 = TPin_Mega(&DDRK, &PORTK, &PINK, PK6);
TPin_Mega D69 = TPin_Mega(&DDRK, &PORTK, &PINK, PK7);
To test our new class, let’s create a main file where we set our builtin LED’s pin (D13) as output and then making it blink in an infinite loop:
// ./src/main.cpp
#include "tavr/mega/pins.h"
#include <util/delay.h>
int main()
{
LED_BUILTIN.mode(OUTPUT);
while (1)
{
LED_BUILTIN.set(HIGH);
_delay_ms(200);
LED_BUILTIN.set(LOW);
_delay_ms(800);
}
}
3 - Utils
Our next step will be to create the uart driver, but before we do so, let’s create two new utility classes/files.
The first utility file we will create, will help us append strings, characters and also convert ints to strings. Let’s declare the append and from int functions, in all of them, we pass a buffer and a maximum length in, the buffer is where we will output, maxlen helps us keep within the bounds of the array and the other arguments are the value or string we want to append:
// ./src/tavr/utils/strings.h
#ifndef _tavr_utils_strings_h_
#define _tavr_utils_strings_h_
#include <inttypes.h>
/* @brief Writes a string onto a buffer
* @param buf The buffer where to write the string to
* @param maxlen max length of the buffer
* @param msg The message to write
*/
uint16_t str_append(char* buf, uint16_t maxlen, const char* msg);
/* @brief Writes a char onto a buffer
* @param buf The buffer where to write the char to
* @param maxlen max length of the buffer
* @param c the character to write
*/
uint16_t str_append(char* buf, uint16_t maxlen, char c);
/* @brief Writes an uint onto a buffer in string form
* @param buf the buffer where to write the number to
* @param maxlen max length of the buffer
* @param v the number to print
* @param base the base to use for printing (ex. 10 = 0-9, 16 = 0-F)
*/
uint16_t str_fromint(char* buf, uint16_t maxlen, uint32_t v, uint32_t base = 10);
#endif
In the cpp file, create the first append function, this one will copy everything in the ‘msg’ argument until a null character is found, or until the max length is reached. The second append function, we will check the size and then append a character with a null character after it. Finally, the last function will take in an integer, convert it into a reversed string and finally we will copy the reversed string to the buffer, while also checking bounds.
// ./src/tavr/utils/strings.cpp
#include <inttypes.h>
uint16_t str_append(char* buf, uint16_t maxlen, const char* msg)
{
uint16_t i = 0;
while (msg[i] != '\0' && i < maxlen - 1) // until the msg ends or the buffer is full
{
buf[i] = msg[i]; // copy to buffer
i++;
}
buf[i] = '\0'; // add null char at the end
return i; // return length added
}
uint16_t str_append(char* buf, uint16_t maxlen, char c)
{
if (maxlen > 1) // if there is space
{
buf[0] = c; // add char
buf[1] = '\0'; // add null char
return 1; // return length
}
return 0; // return length 0 (did not write)
}
uint16_t str_fromint(char* buf, uint16_t maxlen, uint32_t v, uint32_t base)
{
if (maxlen <= 1) // return if buffer has no space
return 0;
if (v == 0) // if number is 0, add 0 and the null char
{
buf[0] = '0'; // add 0
buf[1] = '\0'; // add null char
return 1;
}
char temp[11]; // 11 digits is the max an uint32 base 10 can have
uint8_t i = 0;
while (v > 0) // while the number is greater than 0
{
uint8_t digit = v % base; // get the digit
temp[i++] = (digit < 10) ? (digit + '0') : (digit - 10 + 'A'); // add digit
v /= base; // divide by base
}
uint8_t j = 0;
while (i > 0 && j < maxlen - 1) // add string to buffer (reversed)
buf[j++] = temp[--i];
buf[j] = '\0'; // null char
return j; // return size
}
The Ringbuffer class is a data type that takes in a buffer and makes it act as a ‘never ending’ buffer, as long as you read faster or at the same speed as you write into it. We will need a pointer to a buffer, the length of it, and then two indices, a head and a tail. As for functions, we only need four, push, pop, length and clear:
// ./src/tavr/utils/ringbuf.h
#ifndef _tavr_utils_ringbuf_h_
#define _tavr_utils_ringbuf_h_
#include <inttypes.h>
/* Buffer with 'no end' as long as you
* read it faster than you write to it */
class TRingbuf
{
uint8_t* data; /* internal buffer */
uint16_t maxlen; /* internal buf len */
uint16_t head; /* write pointer */
uint16_t tail; /* read pointer */
public:
/* @brief Creates a ring buffer with the given array and size
* @param internal_buf internal buffer
* @param maxlen internal buffer size */
TRingbuf(uint8_t* internal_buf, uint16_t maxlen);
/* @brief Saves a value to the buffer
* @param val value to store in the buffer */
uint8_t push(uint8_t val);
/* @brief Reads a value from the buffer
* @param out pointer to store the value read */
uint8_t pop(uint8_t* out);
/* @brief Calculates the ammount of data in the buffer */
uint16_t length();
/* @brief Empties the buffer. Sets head and tail to 0 */
void clear();
};
#endif
With the help of this Wikipedia page, we then implement the functions we declared in the header file:
// ./src/tavr/utils/ringbuf.cpp
#include "ringbuf.h"
TRingbuf::TRingbuf(uint8_t* internal_buf, uint16_t maxlen)
{
this->data = internal_buf;
this->maxlen = maxlen;
this->head = 0;
this->tail = 0;
}
uint8_t TRingbuf::push(uint8_t val)
{
uint16_t n = this->head + 1; // next pos
if (n >= this->maxlen) // reached end of buffer
n = 0;
if (n == this->tail) // buffer full
return 0;
this->data[this->head] = val; // save data
this->head = n; // update head
return 1;
}
uint8_t TRingbuf::pop(uint8_t* out)
{
if (this->head == this->tail) // empty
return 0;
uint16_t n = this->tail + 1; // next pos
if (n >= this->maxlen) // reached end of buffer
n = 0;
*out = this->data[this->tail]; // read data
this->tail = n; // update tail pos
return 1;
}
uint16_t TRingbuf::length()
{
if (this->head == this->tail) // empty buf
return 0;
else if (this->tail < this->head) // tail behind head
return (this->maxlen - this->head + this->tail);
else return this->tail - this->head; // tail after head
}
void TRingbuf::clear()
{
this->head = 0;
this->tail = 0;
}
4 - Uart
With the utility files ready, we can now move into the Uart. On the Arduino mega there are four, the class we will create will be able to control all four, but we will only use Uart0, as that is the one connected to your computer via USB.
Like we did with the pins, let’s create an interface, which will contain some send functions, one to read and one to send a new line character:
// ./src/tavr/uart.h
#ifndef _tavr_uart_h_
#define _tavr_uart_h_
#include <inttypes.h>
class TUart
{
public:
/* @brief Read a value from the uart
* @param out pointer to store the value read */
virtual uint8_t read(uint8_t* out) = 0;
/* @brief Send a string via uart
* @param str string to send */
virtual void send(const char* str) = 0;
/* @brief Send a string via uart
* @param c char to send */
virtual void send(uint8_t c) = 0;
/* @brief Send bytes via uart
* @param data data to send
* @param len data size */
virtual void send(const uint8_t* buf, uint16_t len) = 0;
/* @brief Send a number as string via uart
* @param v number to send */
virtual void sendNumber(uint32_t n, uint16_t base) = 0;
/* @brief Sends a new line char */
virtual void newline() = 0;
};
#endif
The class that inherits our interface will override all it’s functions, and then add a few static ones. We do that because we will need ISR (interrupts) and those can’t be bound to a class function. Besides the static functions we also create some variables, one to keep track of the uart number, one to know if the uart is currently sending, two arrays and two ring buffers:
// ./src/tavr/mega/uart.h
#ifndef _tavr_mega_uart_h_
#define _tavr_mega_uart_h_
#include <inttypes.h>
#include "../uart.h"
#include "../utils/ringbuf.h"
#ifndef UART_MEGA_BUFSIZE
/* Buffer size of UARTs for the Arduino Mega (two buffers of this size are created) */
#define UART_MEGA_BUFSIZE 256
#endif
/* Ammount of UARTs in the Arduino Mega */
#define UART_MEGA_COUNT 4
class TUart_Mega : public TUart
{
private:
uint8_t m_uartN; // number of the uart
uint8_t m_txflag; // true if the uart is currently sending
uint8_t m_txbuf_[UART_MEGA_BUFSIZE]; // send buffer (internal buffer for the ring buffer)
uint8_t m_rxbuf_[UART_MEGA_BUFSIZE]; // receive buffer (internal buffer for the ring buffer)
TRingbuf m_txbuf; // send ringbuf
TRingbuf m_rxbuf; // receive ringbuf
static TUart_Mega* m_uart0; // Static pointer to the class that manages uart0
static TUart_Mega* m_uart1; // Static pointer to the class that manages uart1
static TUart_Mega* m_uart2; // Static pointer to the class that manages uart2
static TUart_Mega* m_uart3; // Static pointer to the class that manages uart3
public:
/* @brief Intializes the UART
* @param baud baud rate to use
* @param doubleSpeed non-zero = use double speed mode
*/
TUart_Mega(uint8_t uartN, uint32_t baud, uint8_t doubleSpeed);
/* @brief Read a value from the uart
* @param out pointer to store the value read */
uint8_t read(uint8_t* out) override;
/* @brief Send a string via uart
* @param str string to send */
void send(const char* str) override;
/* @brief Send a string via uart
* @param c char to send */
void send(uint8_t c) override;
/* @brief Send bytes via uart
* @param data data to send
* @param len data size */
void send(const uint8_t* data, uint16_t len) override;
/* @brief Send a string via uart
* @param v number to send */
void sendNumber(uint32_t v, uint16_t base) override;
/* @brief Sends a new line char */
void newline() override;
static TUart_Mega* getUart0(); // Return pointer to uart0 class
static TUart_Mega* getUart1(); // Return pointer to uart1 class
static TUart_Mega* getUart2(); // Return pointer to uart2 class
static TUart_Mega* getUart3(); // Return pointer to uart3 class
void isr_send(); // function called when the uart finishes sending
void isr_receive(uint8_t v); // function called when the uart finishes receiving
};
#endif
In the cpp file, we will create some extra functions, the first one converts a baudrate to it’s ubrrn value, the ‘uart_init’ value initializes the given uart by it’s number, then we implement the interface functions and finally, we create the ISRs that will get the Uart class, using the static methods we created, and then call the classes ‘isr_receive’ or ‘isr_send’ functions:
// ./src/tavr/mega/uart.cpp
#include "uart.h"
#include <avr/io.h>
#include <avr/interrupt.h>
#include "../utils/ringbuf.h"
#include "../utils/strings.h"
uint16_t uart_ubrrn_from_baud(uint32_t baud, uint8_t u2xn)
{
if (u2xn) // If using double speed
{
switch (baud)
{
case 2400: return 832;
case 4800: return 416;
case 9600: return 207;
case 14400: return 138;
case 19200: return 103;
case 28800: return 68;
case 38400: return 51;
case 57600: return 34;
case 76800: return 25;
case 115200: return 16;
case 230400: return 8;
case 250000: return 7;
case 500000: return 3;
case 1000000: return 1;
default: return 16; // 115200
}
}
switch (baud)
{
case 2400: return 416;
case 4800: return 207;
case 9600: return 103;
case 14400: return 68;
case 19200: return 51;
case 28800: return 34;
case 38400: return 25;
case 57600: return 16;
case 76800: return 12;
case 115200: return 8;
case 230400: return 3;
case 250000: return 3;
case 500000: return 1;
case 1000000: return 0;
default: return 8; // 115200
}
}
void uart_init(uint8_t uartN, uint32_t baud, uint8_t doubleSpeed)
{
if (uartN > UART_MEGA_COUNT)
return;
switch (uartN)
{
case 0:
// set baud rate
UBRR0 = uart_ubrrn_from_baud(baud, doubleSpeed);
// enable receiver and transmitter
UCSR0B |= _BV(RXEN0) | _BV(TXEN0);
// enable interrupts
UCSR0B |= _BV(RXCIE0) | _BV(TXCIE0);
// double speed
if (doubleSpeed)
UCSR0A |= _BV(U2X0);
// 8bit, 2bit stop
UCSR0C |= _BV(USBS0) | _BV(UCSZ02);
break;
case 1:
// set baud rate
UBRR1 = uart_ubrrn_from_baud(baud, doubleSpeed);
// enable receiver and transmitter
UCSR1B |= _BV(RXEN1) | _BV(TXEN1);
// enable interrupts
UCSR1B |= _BV(RXCIE1) | _BV(TXCIE1);
// double speed
if (doubleSpeed)
UCSR1A |= _BV(U2X1);
// 8bit, 2bit stop
UCSR1C |= _BV(USBS1) | _BV(UCSZ12);
break;
case 2:
// set baud rate
UBRR2 = uart_ubrrn_from_baud(baud, doubleSpeed);
// enable receiver and transmitter
UCSR2B |= _BV(RXEN2) | _BV(TXEN2);
// enable interrupts
UCSR2B |= _BV(RXCIE2) | _BV(TXCIE2);
// double speed
if (doubleSpeed)
UCSR2A |= _BV(U2X2);
// 8bit, 2bit stop
UCSR2C |= _BV(USBS2) | _BV(UCSZ22);
break;
case 3:
// set baud rate
UBRR3 = uart_ubrrn_from_baud(baud, doubleSpeed);
// enable receiver and transmitter
UCSR3B |= _BV(RXEN3) | _BV(TXEN3);
// enable interrupts
UCSR3B |= _BV(RXCIE3) | _BV(TXCIE3);
// double speed
if (doubleSpeed)
UCSR3A |= _BV(U2X3);
// 8bit, 2bit stop
UCSR3C |= _BV(USBS3) | _BV(UCSZ32);
break;
default:
break;
}
}
TUart_Mega::TUart_Mega(uint8_t uartN, uint32_t baud, uint8_t doubleSpeed) :
m_uartN(uartN), m_txflag(0),
m_txbuf(m_txbuf_, UART_MEGA_BUFSIZE),
m_rxbuf(m_rxbuf_, UART_MEGA_BUFSIZE)
{
switch (uartN)
{
case 1:
m_uart1 = this;
uart_init(1, baud, doubleSpeed);
break;
case 2:
m_uart2 = this;
uart_init(2, baud, doubleSpeed);
break;
case 3:
m_uart3 = this;
uart_init(3, baud, doubleSpeed);
break;
case 0:
default:
m_uartN = 0;
m_uart0 = this;
uart_init(0, baud, doubleSpeed);
break;
}
}
void TUart_Mega::isr_send()
{
uint8_t c;
if (m_txbuf.pop(&c))
{
m_txflag = 1;
switch (m_uartN)
{
case 1: UDR1 = c; break;
case 2: UDR2 = c; break;
case 3: UDR3 = c; break;
default: UDR0 = c; break;
}
}
else m_txflag = 0;
}
void TUart_Mega::isr_receive(uint8_t v) { m_rxbuf.push(v); }
uint8_t TUart_Mega::read(uint8_t* out) { return m_rxbuf.pop(out); }
void TUart_Mega::send(const char* str)
{
while (*str != '\0')
m_txbuf.push(*str++);
if (!m_txflag)
isr_send();
}
void TUart_Mega::send(uint8_t c)
{
m_txbuf.push(c);
if (!m_txflag)
isr_send();
}
void TUart_Mega::send(const uint8_t* data, uint16_t len)
{
for (uint8_t i = 0; i < len; i++)
m_txbuf.push(data[i]);
if (!m_txflag)
isr_send();
}
void TUart_Mega::sendNumber(uint32_t v, uint16_t base)
{
char tmp[11] = {0};
str_fromint(tmp, 11, v, base);
send(tmp);
}
void TUart_Mega::newline() { send((uint8_t)'\n'); }
TUart_Mega* TUart_Mega::m_uart0 = nullptr;
TUart_Mega* TUart_Mega::m_uart1 = nullptr;
TUart_Mega* TUart_Mega::m_uart2 = nullptr;
TUart_Mega* TUart_Mega::m_uart3 = nullptr;
TUart_Mega* TUart_Mega::getUart0() { return m_uart0; }
TUart_Mega* TUart_Mega::getUart1() { return m_uart1; }
TUart_Mega* TUart_Mega::getUart2() { return m_uart2; }
TUart_Mega* TUart_Mega::getUart3() { return m_uart3; }
// ISR
// ========== ========== ========== ========== ========== ==========
ISR(USART0_TX_vect)
{
TUart_Mega* u = TUart_Mega::getUart0();
if (u) u->isr_send();
}
ISR(USART1_TX_vect)
{
TUart_Mega* u = TUart_Mega::getUart1();
if (u) u->isr_send();
}
ISR(USART2_TX_vect)
{
TUart_Mega* u = TUart_Mega::getUart2();
if (u) u->isr_send();
}
ISR(USART3_TX_vect)
{
TUart_Mega* u = TUart_Mega::getUart3();
if (u) u->isr_send();
}
ISR(USART0_RX_vect)
{
TUart_Mega* u = TUart_Mega::getUart0();
if (u) u->isr_receive(UDR0);
}
ISR(USART1_RX_vect)
{
TUart_Mega* u = TUart_Mega::getUart1();
if (u) u->isr_receive(UDR1);
}
ISR(USART2_RX_vect)
{
TUart_Mega* u = TUart_Mega::getUart2();
if (u) u->isr_receive(UDR2);
}
ISR(USART3_RX_vect)
{
TUart_Mega* u = TUart_Mega::getUart3();
if (u) u->isr_receive(UDR3);
}
On main, you can now initialize Uart0, send an hello message and on the infinite loop you can also send a message like the one below:
// ./src/main.cpp
#include <avr/interrupt.h>
#include "tavr/mega/pins.h"
#include "tavr/mega/uart.h"
#include <util/delay.h>
int main()
{
TUart_Mega uart(0, 250000, 1);
LED_BUILTIN.mode(OUTPUT);
sei(); // enable global interrupts
uart.send("Hello\n");
while (1)
{
LED_BUILTIN.set(HIGH);
_delay_ms(200);
LED_BUILTIN.set(LOW);
_delay_ms(800);
uart.send("Blink\n");
}
}
5 - Time
Because we will need to use delays of variable value, we won’t be able to use the delay functions from ‘util/delay.h’, because of that, we will implement our own time ‘driver’. This ‘driver’ will lets us get the current time since the code started, and also to wait for a given ammount of time.
// ./src/tavr/time.h
#ifndef _tavr_time_h_
#define _tavr_time_h_
#include <inttypes.h>
class TTime
{
static TTime* m_instance; /* timer */
public:
/* Returns number of ms since the program started */
virtual uint64_t millis() = 0;
/* Waits for a number of ms */
virtual void waitms(uint16_t ms) = 0;
/* Waits for a number of seconds */
virtual void wait(uint16_t sec) = 0;
/* Initializes timer */
static TTime* init(TTime* instance);
/* Gets timer */
static TTime* getInstance();
};
#endif
This interface will also have it’s own cpp file, on which we implement the static part of it:
// ./src/tavr/time.cpp
#include "time.h"
TTime* TTime::m_instance = nullptr;
TTime* TTime::init(TTime* instance) { return m_instance = instance; }
TTime* TTime::getInstance() { return m_instance; }
The time implementation for the mega simply takes in a number of interrupts per millisecond, which can either be one or ten:
// ./src/tavr/mega/time.h
#ifndef _tavr_mega_time_h_
#define _tavr_mega_time_h_
#include <inttypes.h>
#include "../time.h"
#define TTIMEMEGA_10INTMS 10
#define TTIMEMEGA_1INTMS 1
class TTime_Mega : public TTime
{
uint8_t m_intms;
public:
/* @brief Creates a timer with the given accuracy (interrupts per millisecond)
* @param intms Can be 10 or 1, any other value will default to 1
*/
TTime_Mega(uint8_t intms);
uint64_t millis() override;
void waitms(uint16_t ms) override;
void wait(uint16_t sec) override;
};
#endif
On the cpp file, we declare an integer that will keep track of the ammount of time that has elapsed since our code started. As for functions, on the constructor, we initialize timer 0 and enable interrupts, the millis function calculates the ammount of milliseconds that has elapsed, and the wait functions make the Arduino wait before continuing to run code:
// ./src/tavr/mega/time.cpp
#include "time.h"
#include <avr/interrupt.h>
static volatile uint64_t time_ticks = 0;
ISR(TIMER0_COMPA_vect) { time_ticks++; }
TTime_Mega::TTime_Mega(uint8_t intms)
{
switch (intms)
{
case 10:
// 10 interrupt per ms
OCR0A = 25;
m_intms = 10;
break;
default:
// 1 interrupt per ms
OCR0A = 250;
m_intms = 1;
break;
}
// Enable CTC
TCCR0A = _BV(WGM01);
// Prescaler 64
TCCR0B = _BV(CS01) | _BV(CS00);
// Enable interrupt
TIMSK0 |= _BV(OCIE0A);
}
uint64_t TTime_Mega::millis() { return time_ticks / m_intms; }
void TTime_Mega::waitms(uint16_t ms)
{
uint64_t start = millis();
while ((millis() - start) <= ms);
}
void TTime_Mega::wait(uint16_t sec)
{
uint64_t start = millis();
uint32_t ms = sec * 1000;
while ((millis() - start) <= ms);
}
On the main file, intialize the timer, switch the ‘_delay_ms’ functions with ‘time->waitms’ and test your code:
// ./src/main.cpp
#include <avr/interrupt.h>
#include "tavr/mega/pins.h"
#include "tavr/mega/uart.h"
#include "tavr/mega/time.h"
int main()
{
TTime_Mega time_mega((uint8_t)TTIMEMEGA_1INTMS);
TTime* time = TTime::init(&time_mega);
TUart_Mega uart(0, 250000, 1);
LED_BUILTIN.mode(OUTPUT);
sei(); // enable global interrupts
uart.send("Hello\n");
while (1)
{
LED_BUILTIN.set(HIGH);
time->waitms(200);
LED_BUILTIN.set(LOW);
time->waitms(800);
uart.sendNumber((uint32_t)time->millis(), 10);
uart.send('\n');
}
}
6 - Logger
Now that we can send messages to a console on our computer, let’s make the logger interface, this class is part of our chip8 code, and it will let us send messages easely and with various levels of importance.
Our Logger interface will have a function that sends a message to the uart directly but also a couple of functions that let us build a message before sending it, those functions are the ‘beginLog’, ‘endLog’ and the ‘beLog’ functions.
// ./src/chip8/logger.h
#ifndef __chip8_logger_H__
#define __chip8_logger_H__
#include <inttypes.h>
enum class ELogLevel
{
NONE = 0,
ERROR = 1,
WARN = 2,
INFO = 3,
DEBUG = 4
};
class TLogger
{
public:
/* @brief Logs a message via uart
* @param message message to log
* @param logLevel log level of the message */
virtual void log(const char* message, ELogLevel logLevel) = 0;
/* @brief Starts logging
* @param logLevel log level of the message */
virtual bool beginLog(ELogLevel logLevel) = 0;
/* @brief Adds a message to the buffer
* @param message message to log */
virtual void beLog(const char* message) = 0;
/* @brief Adds a number to the buffer
* @param value number to log
* @param base number base */
virtual void beLog(uint32_t value, uint32_t base = 10) = 0;
/* @brief Sends the message in the buffer */
virtual void endLog() = 0;
/* @brief Sets the log level
* @param logLevel log level to set */
virtual void setLogLevel(ELogLevel logLevel) = 0;
};
#endif
Our implementation of the interface will store a reference to the uart, a loglevel and then it will also contain a buffer with it’s index, this let’s us build messages before sending them.
// ./src/logger.h
#ifndef _logger_h_
#define _logger_h_
#include "../chip8/logger.h"
#include "../tavr/uart.h"
class TLogger_Avr : public TLogger
{
TUart& m_uart; /* uart to use */
ELogLevel m_logLevel; /* max log level to print */
char m_bebuf[128]; /* buffer for begin end */
uint8_t m_bebufi; /* buffer position */
public:
TLogger_Avr(TUart& uart);
TLogger_Avr(TUart& uart, ELogLevel logLevel);
/* @brief Logs a message via uart
* @param message message to log
* @param logLevel log level of the message */
void log(const char* message, ELogLevel logLevel) override;
/* @brief Starts logging
* @param logLevel log level of the message */
bool beginLog(ELogLevel logLevel) override;
/* @brief Adds a message to the buffer
* @param message message to log */
void beLog(const char* message) override;
/* @brief Adds a number to the buffer
* @param value number to log
* @param base number base */
void beLog(uint32_t value, uint32_t base = 10) override;
/* @brief Sends the message in the buffer */
void endLog() override;
/* @brief Sets the log level
* @param logLevel log level to set */
void setLogLevel(ELogLevel logLevel) override;
};
#endif
On the cpp file, we implement the begin, end and log functions. The begin function appends the message log level and checks if the message should be printed, the ‘beLog’ function adds messages into the buffer and ‘endLog’ appends new line and null characters and then it sends the message via uart.
// ./src/logger.cpp
#include "logger.h"
#include "tavr/utils/strings.h"
TLogger_Avr::TLogger_Avr(TUart& uart) : m_uart(uart), m_logLevel(ELogLevel::DEBUG), m_bebuf(), m_bebufi(0) {}
TLogger_Avr::TLogger_Avr(TUart& uart, ELogLevel logLevel) : m_uart(uart), m_logLevel(logLevel), m_bebuf(), m_bebufi(0) {}
void TLogger_Avr::log(const char* message, ELogLevel logLevel)
{
if (!beginLog(logLevel))
return;
beLog(message);
endLog();
}
bool TLogger_Avr::beginLog(ELogLevel logLevel)
{
if (logLevel > m_logLevel) // check the log level
return false;
m_bebufi = 0; // reset buffer position
switch (logLevel) // add log level title to buffer
{
case ELogLevel::ERROR: m_bebufi += str_append(m_bebuf, 128, "\033[31m[ERROR] "); break;
case ELogLevel::WARN: m_bebufi += str_append(m_bebuf, 128, "\033[33m[WARN] "); break;
case ELogLevel::INFO: m_bebufi += str_append(m_bebuf, 128, "\033[32m[INFO] "); break;
case ELogLevel::DEBUG: m_bebufi += str_append(m_bebuf, 128, "\033[0m[DEBUG] "); break;
default: return false;
}
return true;
}
void TLogger_Avr::beLog(const char* message)
{ // add message to log buffer
m_bebufi += str_append(m_bebuf + m_bebufi, 128 - m_bebufi, message);
}
void TLogger_Avr::beLog(uint32_t value, uint32_t base)
{ // add number to log buffer
m_bebufi += str_fromint(m_bebuf + m_bebufi, 128 - m_bebufi, value, 10);
}
void TLogger_Avr::endLog()
{ // add newline and send message
m_bebufi += str_append(m_bebuf + m_bebufi, 128 - m_bebufi, "\n\0");
m_uart.send(m_bebuf);
}
void TLogger_Avr::setLogLevel(ELogLevel logLevel) { m_logLevel = logLevel; }
On the main file, remove the uart messages, initialize the logger after the uart and then use the logger to send various levels of messages:
// ./src/main.cpp
#include <avr/interrupt.h>
#include "tavr/mega/pins.h"
#include "tavr/mega/uart.h"
#include "tavr/mega/time.h"
#include "logger.h"
int main()
{
TTime_Mega time_mega((uint8_t)TTIMEMEGA_1INTMS);
TTime* time = TTime::init(&time_mega);
TUart_Mega uart(0, 250000, 1);
TLogger_Avr logger(uart);
sei(); // enable global interrupts
while (1)
{
logger.log("Hello", ELogLevel::DEBUG);
logger.log("Hello", ELogLevel::INFO);
logger.log("Hello", ELogLevel::WARN);
logger.log("Hello", ELogLevel::ERROR);
time->waitms(1000);
}
}
7 - Random
The chip8 has an instruction that gets a random number and saves it to a register, because of that we will also need to implement it in our Arduino.
Our interface will simply contain four functions, two will get a random number between 0 and 4,294,967,295, and the other two will scale this value to a given interval:
// ./src/tavr/rand.h
#ifndef _tavr_rand_h_
#define _tavr_rand_h_
#include <inttypes.h>
class TRandom
{
public:
/* Get next random number */
virtual uint32_t next() = 0;
/* Get next random number between values */
virtual uint32_t next(uint32_t min, uint32_t max) = 0;
/* Get a true random number using the ADC */
virtual uint32_t truerandom() = 0;
/* Get a true random number using the ADC */
virtual uint32_t truerandom(uint32_t min, uint32_t max) = 0;
};
#endif
Our pseudo random generator will also need two constants, which you can get from this Wikipedia page, besides the constants, you will also need a state which is a number used to calculate the next pseudo random value. On the constructor, we will set, by default, the seed to 0, which makes a call to get a true random to use as the seed:
// ./src/tavr/mega/rand.h
#ifndef _tavr_mega_rand_h_
#define _tavr_mega_rand_h_
#include <inttypes.h>
#include "../rand.h"
class TRandom_Mega : public TRandom
{
private:
// Parameters for the generator
static constexpr uint32_t a = 1664525; // Multiplier
static constexpr uint32_t c = 1013904223; // Increment
uint32_t state;
public:
TRandom_Mega(uint32_t seed = 0);
/* Get next random number */
virtual uint32_t next() override;
/* Get next random number between values */
virtual uint32_t next(uint32_t min, uint32_t max) override;
/* Get a true random number using the ADC */
virtual uint32_t truerandom() override;
/* Get a true random number using the ADC */
virtual uint32_t truerandom(uint32_t min, uint32_t max) override;
};
#endif
On the cpp file, before implementing the class, we will need to create some code to control the ADC of the Mega. The ADC let’s us get true random values from noise generated by floating (not connected) analog pins. We start by making a variable to know if the ADC as been initialized already, then the function to initialize it, one to read a value from a channel (analog pin) and finally, the function to generate a true random value.
The ‘adc_truerandom’ function makes two ‘adc_read’ calls, from the last two pins of the ADC, then it combines both numbers into an ‘uint32_t’ and returns it.
After implementing the ADC code, we can now create the functions for our class, the constructor either sets the state with the seed, or gets a seed from a true random. The next functions use the state to generate a new value and then saving it, and the true random functions make a call to the adc function.
The reason we don’t use ‘adc_truerandom’ for everything is because it is not ‘fast’. Each time you want to read a pin from the ADC you need to wait until the ADC responds, and that takes more time than a simple calculation like we do in the ‘next’ functions.
// ./src/tavr/mega/rand.cpp
#include "rand.h"
#include <avr/io.h>
volatile static uint8_t adc_initalized = 0;
void adc_init()
{
// Initialize the ADC
adc_initalized = 1;
ADMUX = _BV(REFS0);
ADCSRA = _BV(ADEN) | _BV(ADPS2) | _BV(ADPS1) | _BV(ADPS0);
}
uint16_t adc_read(uint8_t channel)
{
if (channel > 15) // check if channel is valid
return 0;
if (!adc_initalized) // initialize adc if needed
adc_init();
// channels 0 - 7
ADMUX = (ADMUX & 0xF8) | (channel & 0x07);
// 8 - 15
if (channel >= 8) ADCSRB |= _BV(MUX5);
else ADCSRB &= ~_BV(MUX5);
// Enable ADC and wait for read to complete
ADCSRA |= _BV(ADSC);
while (ADCSRA & _BV(ADSC));
return ADC;
}
uint32_t adc_truerandom()
{
uint16_t n1 = adc_read(14); // Read A14
uint16_t n2 = adc_read(15); // Read A15
return ((uint32_t)n1 << 16) | n2; // return combined uint32
}
TRandom_Mega::TRandom_Mega(uint32_t seed)
{
if (!seed) // seed = 0, use true random seed
state = adc_truerandom();
else
state = seed; // set state as given seed
}
uint32_t TRandom_Mega::next()
{
state = (uint32_t)(a * state + c); // calculate next
return state;
}
uint32_t TRandom_Mega::truerandom()
{
return adc_truerandom();
}
uint32_t TRandom_Mega::next(uint32_t min, uint32_t max) { return min + (next() % (max - min + 1)); }
uint32_t TRandom_Mega::truerandom(uint32_t min, uint32_t max) { return min + (truerandom() % (max - min + 1)); }
On the main file, initialize the random with a given seed and then print a couple of values, both from next and truerandom:
// ./src/main.cpp
#include <avr/interrupt.h>
#include "tavr/mega/pins.h"
#include "tavr/mega/uart.h"
#include "tavr/mega/time.h"
#include "tavr/mega/rand.h"
#include "logger.h"
int main()
{
TTime_Mega time_mega((uint8_t)TTIMEMEGA_1INTMS);
TTime* time = TTime::init(&time_mega);
TUart_Mega uart(0, 250000, 1);
TLogger_Avr logger(uart);
TRandom_Mega random(69);
sei(); // enable global interrupts
if (logger.beginLog(ELogLevel::INFO))
{
logger.beLog("Seed = 69: ");
logger.beLog(random.next());
logger.endLog();
}
if (logger.beginLog(ELogLevel::WARN))
{
logger.beLog("Truerandom: ");
logger.beLog(random.truerandom());
logger.endLog();
}
while (1)
{
if (logger.beginLog(ELogLevel::INFO))
{
logger.beLog("Pseudorandom: ");
logger.beLog(random.next());
logger.endLog();
}
if (logger.beginLog(ELogLevel::WARN))
{
logger.beLog("Truerandom: ");
logger.beLog(random.truerandom());
logger.endLog();
}
time->waitms(2000);
}
}
By running this code a couple of times, you will notice that our pseudo random generator will always generate the same numbers, as long as we give it the same seed, but our truerandom function will always give us different numbers.
8 - Keyboard Module
With the basics done, we can now start to implement the platform specific code of our emulator. Lets start with the keyboard, as it is the easiest one to implement.
From our previous projects, paste the following interface:
// ./src/chip8/keyboard.h
#ifndef __chip8_keyboard_H__
#define __chip8_keyboard_H__
#include <inttypes.h>
#define TKEY_COUNT 16
class TKeyboard
{
public:
virtual void init() = 0;
virtual void update(uint8_t* key_map) = 0;
virtual void deinit() = 0;
};
#endif
This class won’t need anything special, just a reference to the 8 pins we will use:
// ./src/chip8/modules/keyboard.h
#ifndef _chip8_modules_keyboard_h_
#define _chip8_modules_keyboard_h_
#include <inttypes.h>
#include "../keyboard.h"
#include "../../tavr/pins.h"
#include "../../tavr/uart.h"
class TKeyboard_Mega : public TKeyboard
{
private:
TPin &o1, &o2, &o3, &o4;
TPin &i1, &i2, &i3, &i4;
public:
TKeyboard_Mega(TPin& o1, TPin& o2, TPin& o3, TPin& o4, TPin& i1, TPin& i2, TPin& i3, TPin& i4);
void init() override;
void update(uint8_t* key_map) override;
void deinit() override;
void debug_print(TUart& uart, uint8_t* key_map);
};
#endif
On the constructor, set the first four pins to output and the other four to input, all to low, on update read the pin states one column at a time and then our debug print function let’s us check if everything is working properly:
// ./src/chip8/modules/keyboard.cpp
#include "keyboard.h"
TKeyboard_Mega::TKeyboard_Mega(TPin& o1, TPin& o2, TPin& o3, TPin& o4, TPin& i1, TPin& i2, TPin& i3, TPin& i4) :
o1{o1}, o2{o2}, o3{o3}, o4{o4}, i1{i1}, i2{i2}, i3{i3}, i4{i4}
{
o1.mode(1); o1.set(0);
o2.mode(1); o2.set(0);
o3.mode(1); o3.set(0);
o4.mode(1); o4.set(0);
i1.mode(0); i1.set(0);
i2.mode(0); i2.set(0);
i3.mode(0); i3.set(0);
i4.mode(0); i4.set(0);
}
void TKeyboard_Mega::init() {}
void TKeyboard_Mega::deinit() {}
void TKeyboard_Mega::update(uint8_t* key_map)
{
o1.set(1);
key_map[0x1] = i1.read();
key_map[0x2] = i2.read();
key_map[0x3] = i3.read();
key_map[0xC] = i4.read();
o1.set(0);
o2.set(1);
key_map[0x4] = i1.read();
key_map[0x5] = i2.read();
key_map[0x6] = i3.read();
key_map[0xD] = i4.read();
o2.set(0);
o3.set(1);
key_map[0x7] = i1.read();
key_map[0x8] = i2.read();
key_map[0x9] = i3.read();
key_map[0xE] = i4.read();
o3.set(0);
o4.set(1);
key_map[0xA] = i1.read();
key_map[0x0] = i2.read();
key_map[0xB] = i3.read();
key_map[0xF] = i4.read();
o4.set(0);
}
void TKeyboard_Mega::debug_print(TUart& uart, uint8_t* key_map)
{
uart.send(key_map[0x1] ? '1' : '0'); uart.send(key_map[0x2] ? '1' : '0'); uart.send(key_map[0x3] ? '1' : '0'); uart.send(key_map[0xC] ? "1\n" : "0\n");
uart.send(key_map[0x4] ? '1' : '0'); uart.send(key_map[0x5] ? '1' : '0'); uart.send(key_map[0x6] ? '1' : '0'); uart.send(key_map[0xD] ? "1\n" : "0\n");
uart.send(key_map[0x7] ? '1' : '0'); uart.send(key_map[0x8] ? '1' : '0'); uart.send(key_map[0x9] ? '1' : '0'); uart.send(key_map[0xE] ? "1\n" : "0\n");
uart.send(key_map[0xA] ? '1' : '0'); uart.send(key_map[0x0] ? '1' : '0'); uart.send(key_map[0xB] ? '1' : '0'); uart.send(key_map[0xF] ? "1\n\n" : "0\n\n");
}
On main, create an array to store the key states, intialize the keyboard and on the infinite loop, update and print its state:
// ./src/main.cpp
#include <avr/interrupt.h>
#include "tavr/mega/pins.h"
#include "tavr/mega/uart.h"
#include "tavr/mega/time.h"
#include "tavr/mega/rand.h"
#include "chip8/modules/keyboard.h"
#include "logger.h"
int main()
{
TTime_Mega time_mega((uint8_t)TTIMEMEGA_1INTMS);
TTime* time = TTime::init(&time_mega);
TUart_Mega uart(0, 250000, 1);
TLogger_Avr logger(uart);
TRandom_Mega random;
sei(); // enable global interrupts
uint8_t keys[TKEY_COUNT];
TKeyboard_Mega keyboard(D22, D23, D24, D25, D26, D27, D28, D29);
while (1)
{
keyboard.update(keys);
keyboard.debug_print(uart, keys);
time->waitms(1000);
}
}
9 - Sound
Sound on the Arduino is a little more complicated than the keyboard, as it depends on what hardware you have available. If you have a passive buzzer like I do, you will need the code below, if you have an active buzzer, simply toggling the pin on and off is enough.
Like before, we copy our interface:
// ./src/chip8/sound.h
#ifndef __chip8_SOUND_H__
#define __chip8_SOUND_H__
class TSound
{
public:
virtual void init() = 0;
virtual void play() = 0;
virtual void pause() = 0;
virtual void deinit() = 0;
};
#endif
On the header file, we will declare on an enum the notes our Arduino is capable of playing, then in it’s class, we set the clock pin, the state and it’s note:
// ./src/chip8/modules/sound.h
#ifndef _chip8_modules_sound_h_
#define _chip8_modules_sound_h_
#include <inttypes.h>
#include "../chip8/sound.h"
#include "../mega/pins.h"
enum ENote : uint8_t
{
O2_C = 0, O2_CS, O2_D, O2_DS, O2_E, O2_F,
O2_FS, O2_G, O2_GS, O2_A, O2_AS, O2_B,
O4_C, O4_CS, O4_D, O4_DS, O4_E, O4_F,
O4_FS, O4_G, O4_GS, O4_A, O4_AS, O4_B,
O5_C, O5_CS, O5_D, O5_DS, O5_E, O5_F,
O5_FS, O5_G, O5_GS, O5_A, O5_AS, O5_B,
O6_C, O6_CS, O6_D, O6_DS, O6_E, O6_F,
O6_FS, O6_G, O6_GS, O6_A, O6_AS, O6_B,
O7_C, O7_CS, O7_D, O7_DS, O7_E, O7_F,
O7_FS, O7_G, O7_GS, O7_A, O7_AS, O7_B
};
class TSound_Mega : public TSound
{
private:
TPin& m_pin;
uint8_t m_state;
ENote m_note;
public:
TSound_Mega();
TSound_Mega(ENote note);
uint8_t isPlaying();
void setNote(ENote note);
void init() override;
void play() override;
void pause() override;
void deinit() override;
};
#endif
On the cpp file, we have some constants with the OCR2A values I calculated for each note, then a function to get the correct Timer settings from a note.
Note, the sound module will generate a PWM signal with the help of Timer2 so you won’t be able to use it for other parts of your code.
Then to play and pause the sound, you simply connect the Timer to a source and the output pin, or disconnect them:
// ./src/chip8/modules/sound.cpp
#include "sound.h"
#include <avr/io.h>
static constexpr uint8_t ocr2a_vals[] = { 239, 225, 213, 201, 190, 179, 169, 159, 150, 142, 134, 127};
static constexpr uint8_t ocr2a_vcount = 12;
void tmrcfg_fromnote(ENote n)
{
TCCR2B &= ~(_BV(CS22) | _BV(CS21) | _BV(CS20)); // remove source
uint8_t octave = (uint8_t)n / ocr2a_vcount; // get octave
uint8_t note = (uint8_t)n % ocr2a_vcount; // get note
OCR2A = ocr2a_vals[note < ocr2a_vcount ? note : ocr2a_vcount - 1]; // clamp assign note
switch (octave) // set prescaler (octave)
{
case 1: TCCR2B |= _BV(CS22) | _BV(CS21);
case 2: TCCR2B |= _BV(CS22) | _BV(CS20); break;
case 3: TCCR2B |= _BV(CS22); break;
case 4: TCCR2B |= _BV(CS21) | _BV(CS20); break;
default: TCCR2B |= _BV(CS22) | _BV(CS21) | _BV(CS20); break;
}
}
TSound_Mega::TSound_Mega() :
m_pin(D10), m_state(0), m_note(ENote::O2_C)
{
m_pin.mode(1);
// Mode 3, fast pwm
TCCR2A |= _BV(WGM21) | _BV(WGM20);
pause();
}
TSound_Mega::TSound_Mega(ENote note) :
m_pin(D10), m_state(0), m_note(note)
{
m_pin.mode(1);
// Mode 3, fast pwm
TCCR2A |= _BV(WGM21) | _BV(WGM20);
pause();
}
uint8_t TSound_Mega::isPlaying() { return m_state; }
void TSound_Mega::setNote(ENote note) { m_note = note; }
void TSound_Mega::play()
{
m_state = 1; // update state
TCNT2 = 0x00; // reset counter
tmrcfg_fromnote(m_note); // set note
TCCR2A |= _BV(COM2A1); // connect timer to oc2a
}
void TSound_Mega::pause()
{
TCCR2A &= ~_BV(COM2A1); // disconnect oc2a
TCCR2B &= ~(_BV(CS22) | _BV(CS21) | _BV(CS20)); // remove source
m_pin.set(0); // set d10 to low (left on high can damage passive buzzers)
m_state = 0; // update state
}
void TSound_Mega::init() {}
void TSound_Mega::deinit() { pause(); }
On the main file, we create the sound driver, give it a note and, on the loop, we make it beep once a second:
// ./src/main.cpp
#include <avr/interrupt.h>
#include "tavr/mega/pins.h"
#include "tavr/mega/uart.h"
#include "tavr/mega/time.h"
#include "tavr/mega/rand.h"
#include "chip8/modules/keyboard.h"
#include "chip8/modules/sound.h"
#include "logger.h"
int main()
{
TTime_Mega time_mega((uint8_t)TTIMEMEGA_1INTMS);
TTime* time = TTime::init(&time_mega);
TUart_Mega uart(0, 250000, 1);
TLogger_Avr logger(uart);
TRandom_Mega random;
sei(); // enable global interrupts
TKeyboard_Mega keyboard(D22, D23, D24, D25, D26, D27, D28, D29);
TSound_Mega sound(ENote::O6_E);
while (1)
{
sound.play();
time->waitms(50);
sound.pause();
time->waitms(950);
}
}
10 - Display
The display module will be one of the most important ones as it will be the one that will cause the most delays in our emulation. If you have access to an SPI oled, I recommend that you use it in place of an I2C one.
Because, at the time of writting, I just burned my only SPI oled, I was forced to use the I2C version of it in this article.
The main problem with the I2C display is that the maximum framerate I can extract out of it, is about 35 frames and because the chip8 was meant for 60Hz displays, we will always be behind.
On the display’s interface, we will make some slight changes to help us save ram, one of which will be to add a read function:
// ./src/chip8/display.h
#ifndef __chip8_DISPLAY_H__
#define __chip8_DISPLAY_H__
#include <inttypes.h>
#define TDISP_HEIGHT 32
#define TDISP_WIDTH 64
class TDisplay
{
public:
virtual void init() = 0;
virtual void draw(uint8_t x, uint8_t y, uint8_t col) = 0;
virtual uint8_t read(uint8_t x, uint8_t y) = 0;
virtual void clear() = 0;
virtual void update() = 0;
virtual void deinit() = 0;
};
#endif
On our implementation header file, we will declare the screen size and page count:
// ./src/chip8/modules/display.h
#ifndef __chip8_modules_DISPLAY_H__
#define __chip8_modules_DISPLAY_H__
#include <inttypes.h>
#include "../display.h"
#define TDISPMEGA_W 128
#define TDISPMEGA_H 64
#define TDISPMEGA_P 8
class TDisplay_Mega : public TDisplay
{
public:
TDisplay_Mega();
void init() override;
void draw(uint8_t x, uint8_t y, uint8_t col) override;
uint8_t read(uint8_t x, uint8_t y) override;
void clear() override;
void update() override;
void deinit() override;
};
#endif
On the cpp file, we declare a couple more display values, create the I2C functions, the display command, init update, clear, draw and read functions and finally we join it all in our class:
// ./src/chip8/modules/display.cpp
#include "display.h"
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include "../../tavr/mega/pins.h"
#define F_SCL 400000L
#define SSD1306_ADDR 0x3C
#define SSD1306_BUFSIZE 1024
#ifndef F_CPU
#define F_CPU 16000000L
#endif
void i2c_init()
{
D20.mode(EPinMode::OUTPUT);
D21.mode(EPinMode::OUTPUT);
TWSR = 0x00; // prescaler 1
TWBR = ((F_CPU / F_SCL) - 16) / 2; // scl frequency
TWCR = _BV(TWEN); // enable i2c
}
void i2c_start()
{
TWCR = (1 << TWSTA) | (1 << TWEN) | (1 << TWINT);
while (!(TWCR & (1 << TWINT)));
}
void i2c_stop()
{
TWCR = (1 << TWSTO) | (1 << TWEN) | (1 << TWINT);
while (TWCR & (1 << TWSTO));
}
void i2c_write(uint8_t data)
{
TWDR = data;
TWCR = (1 << TWEN) | (1 << TWINT);
while (!(TWCR & (1 << TWINT)));
}
uint8_t ssd1306_buf[SSD1306_BUFSIZE];
void SSD1306_command(uint8_t cmd)
{
i2c_start();
i2c_write((SSD1306_ADDR << 1) | 0x00); // Write mode
i2c_write(0x00); // Co = 0, D/C# = 0
i2c_write(cmd);
i2c_stop();
}
void SSD1306_init()
{
_delay_ms(100); // Wait for the display to power on
// Initialization sequence
SSD1306_command(0xAE); // Display off
SSD1306_command(0xD5); // Set display clock divide ratio
SSD1306_command(0x80); // Suggested ratio
SSD1306_command(0xA8); // Set multiplex ratio
SSD1306_command(0x3F); // 1/64 duty
SSD1306_command(0xD3); // Set display offset
SSD1306_command(0x00); // No offset
SSD1306_command(0x40); // Set start line to 0
SSD1306_command(0x8D); // Charge pump
SSD1306_command(0x14); // Enable
SSD1306_command(0x20); // Memory addressing mode
SSD1306_command(0x00); // Horizontal addressing mode
SSD1306_command(0xA1); // Set segment re-map
SSD1306_command(0xC8); // COM output scan direction
SSD1306_command(0xDA); // COM pins hardware configuration
SSD1306_command(0x12);
SSD1306_command(0x81); // Set contrast control
SSD1306_command(0x7F);
SSD1306_command(0xD9); // Set pre-charge period
SSD1306_command(0xF1);
SSD1306_command(0xDB); // Set VCOMH deselect level
SSD1306_command(0x40);
SSD1306_command(0xA4); // Entire display ON
SSD1306_command(0xA6); // Normal display
SSD1306_command(0xAF); // Display ON
}
void SSD1306_clear()
{
for (uint16_t i = 0; i < SSD1306_BUFSIZE; i++)
ssd1306_buf[i] = 0;
}
void SSD1306_send_buffer()
{
for (uint8_t page = 0; page < 8; page++)
{
SSD1306_command(0xB0 + page); // Set page address
SSD1306_command(0x00); // Set column low nibble
SSD1306_command(0x10); // Set column high nibble
i2c_start();
i2c_write((SSD1306_ADDR << 1) | 0x00); // Write mode
i2c_write(0x40); // Co = 0, D/C# = 1 (data)
for (uint8_t col = 0; col < 128; col++)
i2c_write(ssd1306_buf[page * 128 + col]);
i2c_stop();
}
}
void SSD1306_draw_pixel(uint8_t x, uint8_t y, uint8_t color)
{
if (x >= TDISPMEGA_W || y >= TDISPMEGA_H) return; // Out of bounds
uint16_t index = (y / TDISPMEGA_P) * TDISPMEGA_W + x; // Calculate buffer index
uint8_t bit_position = y % TDISPMEGA_P;
if (color)
ssd1306_buf[index] |= _BV(bit_position); // Set pixel
else
ssd1306_buf[index] &= ~_BV(bit_position); // Clear pixel
}
uint8_t SSD1306_read_pixel(uint8_t x, uint8_t y)
{
if (x >= TDISPMEGA_W || y >= TDISPMEGA_H) return 0; // Out of bounds
uint16_t index = (y / TDISPMEGA_P) * TDISPMEGA_W + x; // Calculate buffer index
uint8_t bit_position = y % TDISPMEGA_P;
return (ssd1306_buf[index] & _BV(bit_position));
}
TDisplay_Mega::TDisplay_Mega()
{
i2c_init();
SSD1306_init();
}
void TDisplay_Mega::init() { SSD1306_clear(); }
void TDisplay_Mega::draw(uint8_t x, uint8_t y, uint8_t col)
{ // 1 pixel C8 = 2x2 pixels on oled
uint8_t xx = x * 2;
uint8_t yy = y * 2;
SSD1306_draw_pixel(xx, yy, col);
SSD1306_draw_pixel(xx + 1, yy, col);
SSD1306_draw_pixel(xx, yy + 1, col);
SSD1306_draw_pixel(xx + 1, yy + 1, col);
}
uint8_t TDisplay_Mega::read(uint8_t x, uint8_t y) { return SSD1306_read_pixel(x * 2, y * 2); }
void TDisplay_Mega::clear() { SSD1306_clear(); }
void TDisplay_Mega::update() { SSD1306_send_buffer(); }
void TDisplay_Mega::deinit()
{
SSD1306_clear();
SSD1306_send_buffer();
}
On the main file, we can test the display by initializing it and creating a pixel that moves filling the path behind it:
// ./src/main.cpp
#include <avr/interrupt.h>
#include "tavr/mega/pins.h"
#include "tavr/mega/uart.h"
#include "tavr/mega/time.h"
#include "tavr/mega/rand.h"
#include "chip8/modules/keyboard.h"
#include "chip8/modules/display.h"
#include "chip8/modules/sound.h"
#include "logger.h"
int main()
{
TTime_Mega time_mega((uint8_t)TTIMEMEGA_1INTMS);
TTime* time = TTime::init(&time_mega);
TUart_Mega uart(0, 250000, 1);
TLogger_Avr logger(uart);
TRandom_Mega random;
sei(); // enable global interrupts
TKeyboard_Mega keyboard(D22, D23, D24, D25, D26, D27, D28, D29);
TSound_Mega sound(ENote::O6_E);
TDisplay_Mega display;
display.init();
uint8_t x = 0;
while (1)
{
if (x >= 64)
{
display.clear();
x = 0;
}
else
{
display.draw(x, 15, 1);
x++;
}
display.update();
time->waitms(1);
}
}
11 - Rom Loader
The Rom loader on the Arduino, instead of loading roms from file, it will load them from the Program Mem. This class will be able to take in a number and load a rom into ram.
On the header file, we will include the ‘pgmspace’ header, then we create our rom arrays and their size and on the class, we create the static ‘loadRom’ function:
// ./src/chip8/modules/romloader.h
#ifndef _modules_romloader_h_
#define _modules_romloader_h_
#include <inttypes.h>
#include <avr/pgmspace.h>
#include "../logger.h"
// https://notisrac.github.io/FileToCArray/
static const uint8_t rom_Maze1[] PROGMEM = {
0x60, 0x00, 0x61, 0x00, 0xa2, 0x22, 0xc2, 0x01, 0x32, 0x01, 0xa2, 0x1e, 0xd0, 0x14, 0x70, 0x04,
0x30, 0x40, 0x12, 0x04, 0x60, 0x00, 0x71, 0x04, 0x31, 0x20, 0x12, 0x04, 0x12, 0x1c, 0x80, 0x40,
0x20, 0x10, 0x20, 0x40, 0x80, 0x10};
static const uint16_t rom_Maze1_size = sizeof(rom_Maze1);
static const uint8_t rom_Breakout1[] PROGMEM = {
0x6e, 0x05, 0x65, 0x00, 0x6b, 0x06, 0x6a, 0x00, 0xa3, 0x0c, 0xda, 0xb1, 0x7a, 0x04, 0x3a, 0x40,
0x12, 0x08, 0x7b, 0x02, 0x3b, 0x12, 0x12, 0x06, 0x6c, 0x20, 0x6d, 0x1f, 0xa3, 0x10, 0xdc, 0xd1,
0x22, 0xf6, 0x60, 0x00, 0x61, 0x00, 0xa3, 0x12, 0xd0, 0x11, 0x70, 0x08, 0xa3, 0x0e, 0xd0, 0x11,
0x60, 0x40, 0xf0, 0x15, 0xf0, 0x07, 0x30, 0x00, 0x12, 0x34, 0xc6, 0x0f, 0x67, 0x1e, 0x68, 0x01,
0x69, 0xff, 0xa3, 0x0e, 0xd6, 0x71, 0xa3, 0x10, 0xdc, 0xd1, 0x60, 0x04, 0xe0, 0xa1, 0x7c, 0xfe,
0x60, 0x06, 0xe0, 0xa1, 0x7c, 0x02, 0x60, 0x3f, 0x8c, 0x02, 0xdc, 0xd1, 0xa3, 0x0e, 0xd6, 0x71,
0x86, 0x84, 0x87, 0x94, 0x60, 0x3f, 0x86, 0x02, 0x61, 0x1f, 0x87, 0x12, 0x47, 0x1f, 0x12, 0xac,
0x46, 0x00, 0x68, 0x01, 0x46, 0x3f, 0x68, 0xff, 0x47, 0x00, 0x69, 0x01, 0xd6, 0x71, 0x3f, 0x01,
0x12, 0xaa, 0x47, 0x1f, 0x12, 0xaa, 0x60, 0x05, 0x80, 0x75, 0x3f, 0x00, 0x12, 0xaa, 0x60, 0x01,
0xf0, 0x18, 0x80, 0x60, 0x61, 0xfc, 0x80, 0x12, 0xa3, 0x0c, 0xd0, 0x71, 0x60, 0xfe, 0x89, 0x03,
0x22, 0xf6, 0x75, 0x01, 0x22, 0xf6, 0x45, 0x60, 0x12, 0xde, 0x12, 0x46, 0x69, 0xff, 0x80, 0x60,
0x80, 0xc5, 0x3f, 0x01, 0x12, 0xca, 0x61, 0x02, 0x80, 0x15, 0x3f, 0x01, 0x12, 0xe0, 0x80, 0x15,
0x3f, 0x01, 0x12, 0xee, 0x80, 0x15, 0x3f, 0x01, 0x12, 0xe8, 0x60, 0x20, 0xf0, 0x18, 0xa3, 0x0e,
0x7e, 0xff, 0x80, 0xe0, 0x80, 0x04, 0x61, 0x00, 0xd0, 0x11, 0x3e, 0x00, 0x12, 0x30, 0x12, 0xde,
0x78, 0xff, 0x48, 0xfe, 0x68, 0xff, 0x12, 0xee, 0x78, 0x01, 0x48, 0x02, 0x68, 0x01, 0x60, 0x04,
0xf0, 0x18, 0x69, 0xff, 0x12, 0x70, 0xa3, 0x14, 0xf5, 0x33, 0xf2, 0x65, 0xf1, 0x29, 0x63, 0x37,
0x64, 0x00, 0xd3, 0x45, 0x73, 0x05, 0xf2, 0x29, 0xd3, 0x45, 0x00, 0xee, 0xf0, 0x00, 0x80, 0x00,
0xfc, 0x00, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00};
static const uint16_t rom_Breakout1_size = sizeof(rom_Breakout1);
static const uint8_t rom_Pong1[] PROGMEM = {
0x6a, 0x02, 0x6b, 0x0c, 0x6c, 0x3f, 0x6d, 0x0c, 0xa2, 0xea, 0xda, 0xb6, 0xdc, 0xd6, 0x6e, 0x00,
0x22, 0xd4, 0x66, 0x03, 0x68, 0x02, 0x60, 0x60, 0xf0, 0x15, 0xf0, 0x07, 0x30, 0x00, 0x12, 0x1a,
0xc7, 0x17, 0x77, 0x08, 0x69, 0xff, 0xa2, 0xf0, 0xd6, 0x71, 0xa2, 0xea, 0xda, 0xb6, 0xdc, 0xd6,
0x60, 0x01, 0xe0, 0xa1, 0x7b, 0xfe, 0x60, 0x04, 0xe0, 0xa1, 0x7b, 0x02, 0x60, 0x1f, 0x8b, 0x02,
0xda, 0xb6, 0x8d, 0x70, 0xc0, 0x0a, 0x7d, 0xfe, 0x40, 0x00, 0x7d, 0x02, 0x60, 0x00, 0x60, 0x1f,
0x8d, 0x02, 0xdc, 0xd6, 0xa2, 0xf0, 0xd6, 0x71, 0x86, 0x84, 0x87, 0x94, 0x60, 0x3f, 0x86, 0x02,
0x61, 0x1f, 0x87, 0x12, 0x46, 0x02, 0x12, 0x78, 0x46, 0x3f, 0x12, 0x82, 0x47, 0x1f, 0x69, 0xff,
0x47, 0x00, 0x69, 0x01, 0xd6, 0x71, 0x12, 0x2a, 0x68, 0x02, 0x63, 0x01, 0x80, 0x70, 0x80, 0xb5,
0x12, 0x8a, 0x68, 0xfe, 0x63, 0x0a, 0x80, 0x70, 0x80, 0xd5, 0x3f, 0x01, 0x12, 0xa2, 0x61, 0x02,
0x80, 0x15, 0x3f, 0x01, 0x12, 0xba, 0x80, 0x15, 0x3f, 0x01, 0x12, 0xc8, 0x80, 0x15, 0x3f, 0x01,
0x12, 0xc2, 0x60, 0x20, 0xf0, 0x18, 0x22, 0xd4, 0x8e, 0x34, 0x22, 0xd4, 0x66, 0x3e, 0x33, 0x01,
0x66, 0x03, 0x68, 0xfe, 0x33, 0x01, 0x68, 0x02, 0x12, 0x16, 0x79, 0xff, 0x49, 0xfe, 0x69, 0xff,
0x12, 0xc8, 0x79, 0x01, 0x49, 0x02, 0x69, 0x01, 0x60, 0x04, 0xf0, 0x18, 0x76, 0x01, 0x46, 0x40,
0x76, 0xfe, 0x12, 0x6c, 0xa2, 0xf2, 0xfe, 0x33, 0xf2, 0x65, 0xf1, 0x29, 0x64, 0x14, 0x65, 0x00,
0xd4, 0x55, 0x74, 0x15, 0xf2, 0x29, 0xd4, 0x55, 0x00, 0xee, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00};
static const uint16_t rom_Pong1_size = sizeof(rom_Pong1);
static const uint8_t rom_KeypadTest[] PROGMEM = {
0x12, 0x4e, 0x08, 0x19, 0x01, 0x01, 0x08, 0x01, 0x0f, 0x01, 0x01, 0x09, 0x08, 0x09, 0x0f, 0x09,
0x01, 0x11, 0x08, 0x11, 0x0f, 0x11, 0x01, 0x19, 0x0f, 0x19, 0x16, 0x01, 0x16, 0x09, 0x16, 0x11,
0x16, 0x19, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0x00, 0xa2, 0x02, 0x82, 0x0e, 0xf2, 0x1e,
0x82, 0x06, 0xf1, 0x65, 0x00, 0xee, 0xa2, 0x02, 0x82, 0x0e, 0xf2, 0x1e, 0x82, 0x06, 0xf1, 0x55,
0x00, 0xee, 0x6f, 0x10, 0xff, 0x15, 0xff, 0x07, 0x3f, 0x00, 0x12, 0x46, 0x00, 0xee, 0x00, 0xe0,
0x62, 0x00, 0x22, 0x2a, 0xf2, 0x29, 0xd0, 0x15, 0x70, 0xff, 0x71, 0xff, 0x22, 0x36, 0x72, 0x01,
0x32, 0x10, 0x12, 0x52, 0xf2, 0x0a, 0x22, 0x2a, 0xa2, 0x22, 0xd0, 0x17, 0x22, 0x42, 0xd0, 0x17,
0x12, 0x64};
static const uint16_t rom_KeypadTest_size = sizeof(rom_KeypadTest);
class TRom_Loader
{
public:
static void loadRom(uint8_t n, uint8_t* ram, TLogger* logger);
};
#endif
On the cpp file, we simply make a switch with all the roms, and then load the chosen one and print it’s name and size:
// ./src/chip8/modules/romloader.cpp
#include "romloader.h"
void TRom_Loader::loadRom(uint8_t n, uint8_t* ram, TLogger* logger)
{
uint16_t size;
const uint8_t* rom;
switch (n)
{
case 1:
rom = rom_Maze1;
size = rom_Maze1_size;
logger->log("Loading Maze.ch8...", ELogLevel::INFO);
break;
case 2:
rom = rom_Breakout1;
size = rom_Breakout1_size;
logger->log("Loading Breakout.ch8...", ELogLevel::INFO);
break;
case 3:
rom = rom_Pong1;
size = rom_Pong1_size;
logger->log("Loading Pong.ch8...", ELogLevel::INFO);
break;
case 0:
default:
rom = rom_KeypadTest;
size = rom_KeypadTest_size;
logger->log("Loading KeypadTester.ch8...", ELogLevel::INFO);
break;
}
for (uint16_t i = 0; i < size; i++)
ram[i] = pgm_read_byte(rom + i); // copy from program mem to ram
if (logger->beginLog(ELogLevel::INFO))
{
logger->beLog("Loaded ");
logger->beLog(size);
logger->beLog(" bytes");
logger->endLog();
}
}
12 - Emulator
Now we are reaching the end, the chip8 file in here will be the merge between the chip8 and cpu files in the other projects. This was because we can’t use ‘new’ and ‘delete’ and i thought it would be easier and better to use static memory instead of using malloc and free.
The chip8 header file is very similar to the previous ones we made, you just need to change the rom loader, time and rand header files:
// ./src/chip8/chip8.h
#ifndef __CHIP8_H__
#define __CHIP8_H__
#include <inttypes.h>
#include "display.h"
#include "keyboard.h"
#include "logger.h"
#include "modules/romloader.h"
#include "../tavr/time.h"
#include "../tavr/rand.h"
#include "sound.h"
#define NUM_KEYS 16
#define TOTAL_RAM 4096
#define STACK_SIZE 16
#define FONTSET_SIZE 80
#define TIMER_MAX 255
#define NUM_V_REGISTERS 16
#define PC_START 0x200
#define CHIP8_RAM_START_ADDR 0x000
#define CHIP8_RAM_END_ADDR 0x1FF
#define PROGRAM_START_ADDR 0x200
#define PROGRAM_END_ADDR 0xFFF
#define SCREEN_WIDTH 64
#define SCREEN_HEIGHT 32
const static uint8_t FONTSET[] = {
0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
0x20, 0x60, 0x20, 0x20, 0x70, // 1
0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
0x90, 0x90, 0xF0, 0x10, 0x10, // 4
0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
0xF0, 0x10, 0x20, 0x40, 0x40, // 7
0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
0xF0, 0x90, 0xF0, 0x90, 0x90, // A
0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
0xF0, 0x80, 0x80, 0x80, 0xF0, // C
0xE0, 0x90, 0x90, 0x90, 0xE0, // D
0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
0xF0, 0x80, 0xF0, 0x80, 0x80 // F
};
class TChip8
{
// keyboard
uint8_t m_keys[NUM_KEYS];
bool m_key_pressed = false;
// memory
uint8_t m_ram[TOTAL_RAM];
uint16_t m_stack[STACK_SIZE];
// timers
uint8_t m_delay_timer;
uint8_t m_sound_timer;
// Control var
bool m_emulatorRunning;
// Logging
TLogger* m_logger;
// Display
TDisplay* m_display;
// Keyboard
TKeyboard* m_keyboard;
// Sound
TSound* m_sound;
// Random
TRandom* m_rand;
// Time
TTime* m_time;
uint16_t m_tickCounter;
// CPU registers
uint8_t m_reg[NUM_V_REGISTERS];
uint16_t m_ireg;
uint16_t m_pcreg;
uint16_t m_sp_reg;
// helper variable
uint16_t m_current_op; // current opcode being executed
uint16_t m_instruction;
// Instructions implementation
void decode_0_instruction(); // 0ZZZ
void clear_screen(); // 0ZE0
void return_from_subroutine(); // 0ZEE
void jump_to(); // 1NNN
void call_subroutine(); // 2NNN
void skip_next_instruction_eq(); // 3XNN
void skip_next_instruction_ne(); // 4XNN
void skip_next_instruction_vx_vy(); // 5XYZ
void register_set(); // 6XNN
void add_reg_imm(); // 7XNN
void decode_8_instruction(); // 8XYZ
void move_vy_to_vx(); // 8XY0
void or_vx_vy(); // 8XY1
void and_vx_vy(); // 8XY2
void xor_vx_vy(); // 8XY3
void add_vx_vy(); // 8XY4
void sub_vx_vy(); // 8XY5
void shift_right_reg(); // 8XY6
void subn_vx_vy(); // 8XY7
void shift_left_reg(); // 8XYE
void skip_next_instruction_vx_vy_ne(); // 9XYZ
void set_index_register(); // ANNN
void jump_with_v0(); // BNNN
void generate_random_number(); // CXKK
void draw_sprite(); // DXYN
void decode_E_instruction(); // EZZZ
void skip_next_inst_if_key_pressed(); // EX9E
void skip_next_inst_if_not_pressed(); // EXA1
void decode_F_instruction(); // FZZZ
void load_reg_with_delay_timer(); // FX07
void wait_key_press(); // FX0A
void load_delay_timer_with_reg(); // FX15
void load_sound_timer_with_reg(); // FX18
void add_ireg_with_reg(); // FX1E
void load_font_from_vx(); // FX29
void store_binary_code_decimal_representation(); // FX33
void load_memory_from_regs(); // FX55
void load_regs_from_memory(); // FX65
// cpu functions
void fetch();
void decode();
void execute();
public:
TChip8(TLogger* logger, TTime* time, TRandom* rand, TDisplay* disp, TKeyboard* keyb, TSound* sound);
~TChip8();
/* Initialize emulator and load rom */
void init(uint8_t rom);
/* Run one instruction */
void run();
/* Stop the emulator */
void deinit();
};
#endif
On the cpp file, we make some changes in the init and run functions, then we copy the ‘cpu.cpp’ contents into it:
// ./src/chip8/chip8.cpp
#include "chip8.h"
#include "logger.h"
#include "../tavr/time.h"
#include "../tavr/rand.h"
TChip8::TChip8(TLogger* logger, TTime* time, TRandom* rand, TDisplay* disp, TKeyboard* keyb, TSound* sound)
{
m_logger = logger;
m_time = time;
m_rand = rand;
m_emulatorRunning = true;
m_key_pressed = false;
m_display = disp;
m_keyboard = keyb;
m_sound = sound;
}
TChip8::~TChip8() {}
void TChip8::init(uint8_t rom)
{
// Clear stack
for(auto i = 0; i < STACK_SIZE; i++)
m_stack[i] = 0;
// Clear RAM
for(auto i = 0; i < TOTAL_RAM; i++)
m_ram[i] = 0;
// Load font set into memory
for(auto i = 0; i < FONTSET_SIZE; i++)
m_ram[i] = FONTSET[i];
// Reset timers
m_delay_timer = 0;
m_sound_timer = 0;
// Start keyboard state as all unpressed
for (auto i = 0; i < NUM_KEYS; i++)
m_keys[i] = 0;
m_key_pressed = false;
// Initialize cpu registers
m_pcreg = PC_START;
m_current_op = 0;
m_sp_reg = 0;
m_ireg = 0;
m_instruction = 0;
// Clear registers
for(auto i = 0; i < NUM_V_REGISTERS; i++)
m_reg[i] = 0;
// Load ROM to the ram
TRom_Loader::loadRom(rom, m_ram + PROGRAM_START_ADDR, m_logger);
// Initialize and clear display
m_display->init();
m_display->clear();
m_display->update();
// Init sound and keyboard
m_sound->init();
m_keyboard->init();
}
void TChip8::run()
{
if (!m_emulatorRunning)
return;
uint64_t stime = m_time->millis();
// CPU Cycle
fetch();
decode();
execute();
// Update button states
m_keyboard->update(m_keys);
// ~60Hz
if (m_tickCounter == 2)
{
if (m_delay_timer > 0)
m_delay_timer--;
if (m_sound_timer > 0)
{
m_sound_timer--;
m_sound->play();
}
else m_sound->pause();
}
// ~30Hz (if you have an SPI display, use the 60Hz)
if (m_tickCounter >= 3)
{
m_display->update(); // upddate display
m_tickCounter = 0; // reset counter
}
m_tickCounter++;
uint64_t etime = m_time->millis();
int64_t sleepTime = 10 - (etime - stime);
if (sleepTime > 0)
m_time->waitms(sleepTime);
}
void TChip8::deinit()
{
m_display->deinit();
m_keyboard->deinit();
m_sound->deinit();
}
void TChip8::fetch()
{
m_current_op = ((uint16_t)m_ram[m_pcreg] << 8) | m_ram[m_pcreg+1];
m_pcreg += 2;
}
void TChip8::decode() { m_instruction = m_current_op >> 12; }
void TChip8::execute()
{
switch(m_instruction)
{
case 0x0: decode_0_instruction(); break;
case 0x1: jump_to(); break;
case 0x2: call_subroutine(); break;
case 0x3: skip_next_instruction_eq(); break;
case 0x4: skip_next_instruction_ne(); break;
case 0x5: skip_next_instruction_vx_vy(); break;
case 0x6: register_set(); break;
case 0x7: add_reg_imm(); break;
case 0x8: decode_8_instruction(); break;
case 0x9: skip_next_instruction_vx_vy_ne(); break;
case 0xA: set_index_register(); break;
case 0xB: jump_with_v0(); break;
case 0xC: generate_random_number(); break;
case 0xD: draw_sprite(); break;
case 0xE: decode_E_instruction(); break;
case 0xF: decode_F_instruction(); break;
default:
if (m_logger->beginLog(ELogLevel::ERROR))
{
m_logger->beLog("Impossible instruction: ");
m_logger->beLog(m_instruction);
m_logger->endLog();
}
break;
}
}
// CPU functions
/*****************************
* 0ZZZ
*****************************/
void TChip8::decode_0_instruction()
{
switch(m_current_op & 0xFF)
{
case 0xE0: clear_screen(); break;
case 0xEE: return_from_subroutine(); break;
default:
if (m_logger->beginLog(ELogLevel::ERROR))
{
m_logger->beLog("Impossible instruction 0: ");
m_logger->beLog(m_instruction);
m_logger->endLog();
}
break;
}
}
/*****************************
* 0ZE0
*****************************/
void TChip8::clear_screen()
{
m_display->clear();
}
/*****************************
* 0ZEE
*****************************/
void TChip8::return_from_subroutine()
{
m_sp_reg--;
m_pcreg = m_stack[m_sp_reg];
}
/*****************************
* 1NNN
*****************************/
void TChip8::jump_to()
{
m_pcreg = m_current_op & 0x0FFF;
}
/*****************************
* 2NNN
*****************************/
void TChip8::call_subroutine()
{
uint16_t nnn = m_current_op & 0x0FFF;
m_stack[m_sp_reg] = m_pcreg;
m_sp_reg++;
m_pcreg = nnn;
}
/*****************************
* 3XNN
*****************************/
void TChip8::skip_next_instruction_eq()
{
uint8_t value = m_current_op & 0xFF;
uint8_t reg = (m_current_op >> 8) & 0x0F;
if(m_reg[reg] == value)
m_pcreg += 2;
}
/*****************************
* 4XNN
*****************************/
void TChip8::skip_next_instruction_ne()
{
uint8_t value = m_current_op & 0xFF;
uint8_t reg = (m_current_op >> 8) & 0x0F;
if(m_reg[reg] != value)
m_pcreg += 2;
}
/*****************************
* 5XYZ
*****************************/
void TChip8::skip_next_instruction_vx_vy()
{
uint8_t reg_x = (m_current_op >> 8) & 0x0F;
uint8_t reg_y = (m_current_op >> 4) & 0x0F;
if(m_reg[reg_x] == m_reg[reg_y])
m_pcreg += 2;
}
/*****************************
* 6XNN
*****************************/
void TChip8::register_set()
{
uint8_t value = m_current_op & 0xFF;
uint8_t reg = (m_current_op >> 8) & 0x0F;
m_reg[reg] = value;
}
/*****************************
* 7XNN
*****************************/
void TChip8::add_reg_imm()
{
uint8_t value = m_current_op & 0xFF;
uint8_t reg = (m_current_op >> 8) & 0x0F;
m_reg[reg] += value;
}
/*****************************
* 8XYZ
*****************************/
void TChip8::decode_8_instruction()
{
switch(m_current_op & 0xF)
{
case 0x0: move_vy_to_vx(); break;
case 0x1: or_vx_vy(); break;
case 0x2: and_vx_vy(); break;
case 0x3: xor_vx_vy(); break;
case 0x4: add_vx_vy(); break;
case 0x5: sub_vx_vy(); break;
case 0x6: shift_right_reg(); break;
case 0x7: subn_vx_vy(); break;
case 0xE: shift_left_reg(); break;
default:
if (m_logger->beginLog(ELogLevel::ERROR))
{
m_logger->beLog("Impossible instruction 8: ");
m_logger->beLog(m_current_op & 0xF);
m_logger->endLog();
}
break;
}
}
/*****************************
* 8XY0
*****************************/
void TChip8::move_vy_to_vx()
{
uint8_t reg_x = (m_current_op >> 8) & 0x0F;
uint8_t reg_y = (m_current_op >> 4) & 0x0F;
m_reg[reg_x] = m_reg[reg_y];
}
/*****************************
* 8XY1
*****************************/
void TChip8::or_vx_vy()
{
uint8_t reg_x = (m_current_op >> 8) & 0x0F;
uint8_t reg_y = (m_current_op >> 4) & 0x0F;
m_reg[reg_x] |= m_reg[reg_y];
}
/*****************************
* 8XY2
*****************************/
void TChip8::and_vx_vy()
{
uint8_t reg_x = (m_current_op >> 8) & 0x0F;
uint8_t reg_y = (m_current_op >> 4) & 0x0F;
m_reg[reg_x] &= m_reg[reg_y];
}
/*****************************
* 8XY3
*****************************/
void TChip8::xor_vx_vy()
{
uint8_t reg_x = (m_current_op >> 8) & 0x0F;
uint8_t reg_y = (m_current_op >> 4) & 0x0F;
m_reg[reg_x] ^= m_reg[reg_y];
}
/*****************************
* 8XY4
*****************************/
void TChip8::add_vx_vy()
{
uint8_t reg_x = (m_current_op >> 8) & 0x0F;
uint8_t reg_y = (m_current_op >> 4) & 0x0F;
uint16_t add = m_reg[reg_x] + m_reg[reg_y];
if(add > 0xFF)
m_reg[0xF] = 1;
else
m_reg[0xF] = 0;
m_reg[reg_x] = add & 0xFF;
}
/*****************************
* 8XY5
*****************************/
void TChip8::sub_vx_vy()
{
uint8_t reg_x = (m_current_op >> 8) & 0x0F;
uint8_t reg_y = (m_current_op >> 4) & 0x0F;
if(m_reg[reg_x] > m_reg[reg_y])
m_reg[0xF] = 1;
else
m_reg[0xF] = 0;
m_reg[reg_x] -= m_reg[reg_y];
}
/*****************************
* 8XY6
*****************************/
void TChip8::shift_right_reg()
{
uint8_t reg = (m_current_op >> 8) & 0x0F;
if(m_reg[reg] % 2 == 1 )
m_reg[0xF] = 1;
else
m_reg[0xF] = 0;
m_reg[reg] >>= 1;
}
/*****************************
* 8XY7
*****************************/
void TChip8::subn_vx_vy()
{
uint8_t reg_x = (m_current_op >> 8) & 0x0F;
uint8_t reg_y = (m_current_op >> 4) & 0x0F;
if(m_reg[reg_y] > m_reg[reg_x])
m_reg[0xF] = 1;
else
m_reg[0xF] = 0;
m_reg[reg_x] = m_reg[reg_y] - m_reg[reg_x];
}
/*****************************
* 8XYE
*****************************/
void TChip8::shift_left_reg()
{
uint8_t reg = (m_current_op >> 8) & 0x0F;
if((m_reg[reg] & 0x80) == 1)
m_reg[0xF] = 1;
else
m_reg[0xF] = 0;
m_reg[reg] <<= 1;
}
/*****************************
* 9XYZ
*****************************/
void TChip8::skip_next_instruction_vx_vy_ne()
{
uint8_t reg_x = (m_current_op >> 8) & 0x0F;
uint8_t reg_y = (m_current_op >> 4) & 0x0F;
if(m_reg[reg_x] != m_reg[reg_y])
m_pcreg += 2;
}
/*****************************
* ANNN
*****************************/
void TChip8::set_index_register()
{
m_ireg = m_current_op & 0x0FFF;
}
/*****************************
* BNNN
*****************************/
void TChip8::jump_with_v0()
{
uint16_t nnn = m_current_op & 0x0FFF;
m_pcreg = nnn + m_reg[0];
}
/*****************************
* CXKK
*****************************/
void TChip8::generate_random_number()
{
uint8_t reg = (m_current_op >> 8) & 0x0F;
uint8_t kk = m_current_op & 0xFF;
uint8_t randNum = (uint8_t)(m_rand->next() % 256);
m_reg[reg] = randNum & kk;
}
/*****************************
* DXYN
*****************************/
void TChip8::draw_sprite()
{
uint8_t v_reg_x = (m_current_op & 0x0F00) >> 8;
uint8_t v_reg_y = (m_current_op & 0x00F0) >> 4;
uint8_t sprite_height = m_current_op & 0x000F;
uint8_t x_location = m_reg[v_reg_x];
uint8_t y_location = m_reg[v_reg_y];
// Reset colision register
m_reg[0xF] = 0;
for(int y_coordinate = 0; y_coordinate < sprite_height; y_coordinate++)
{
uint8_t pixel = m_ram[m_ireg + y_coordinate];
for(int x_coordinate = 0; x_coordinate < 8; x_coordinate++)
{
if((pixel & (0x80 >> x_coordinate)) != 0)
{
uint8_t y_lc = y_location + y_coordinate;
uint8_t x_lc = x_location + x_coordinate;
if(m_display->read(x_lc, y_lc))
{
m_reg[0xF] = 1;
m_display->draw(x_lc, y_lc, 0);
}
else m_display->draw(x_lc, y_lc, 1);
}
}
}
}
/*****************************
* EZZZ
*****************************/
void TChip8::decode_E_instruction()
{
switch(m_current_op & 0xFF)
{
case 0x009E: skip_next_inst_if_key_pressed(); break;
case 0x00A1: skip_next_inst_if_not_pressed(); break;
default:
if (m_logger->beginLog(ELogLevel::ERROR))
{
m_logger->beLog("Impossible instruction E: ");
m_logger->beLog(m_current_op & 0xFF);
m_logger->endLog();
}
break;
}
}
/*****************************
* EX9E
*****************************/
void TChip8::skip_next_inst_if_key_pressed()
{
uint8_t reg = (m_current_op >> 8) & 0x0F;
uint8_t val = m_reg[reg];
if(m_keys[val] != 0)
m_pcreg += 2;
}
/*****************************
* EXA1
*****************************/
void TChip8::skip_next_inst_if_not_pressed()
{
uint8_t reg = (m_current_op >> 8) & 0x0F;
uint8_t val = m_reg[reg];
if(m_keys[val] == 0)
m_pcreg += 2;
}
/*****************************
* FZZZ
*****************************/
void TChip8::decode_F_instruction()
{
switch(m_current_op & 0xFF)
{
case 0x0007: load_reg_with_delay_timer(); break;
case 0x000A: wait_key_press(); break;
case 0x0015: load_delay_timer_with_reg(); break;
case 0x0018: load_sound_timer_with_reg(); break;
case 0x001E: add_ireg_with_reg(); break;
case 0x0029: load_font_from_vx(); break;
case 0x0033: store_binary_code_decimal_representation(); break;
case 0x0055: load_memory_from_regs(); break;
case 0x0065: load_regs_from_memory(); break;
default:
if (m_logger->beginLog(ELogLevel::ERROR))
{
m_logger->beLog("Impossible instruction F: ");
m_logger->beLog(m_current_op & 0xFF);
m_logger->endLog();
}
break;
}
}
/*****************************
* FX07
*****************************/
void TChip8::load_reg_with_delay_timer()
{
uint8_t reg = (m_current_op >> 8) & 0x0F;
m_reg[reg] = m_delay_timer;
}
/*****************************
* FX0A
*****************************/
void TChip8::wait_key_press()
{
uint8_t reg = (m_current_op >> 8) & 0x0F;
bool key_pressed = false;
for(int i=0; i<NUM_KEYS; i++)
{
if(m_keys[i] != 0)
{
m_reg[reg] = i;
key_pressed = true;
}
}
if(!key_pressed)
m_pcreg -= 2;
}
/*****************************
* FX15
*****************************/
void TChip8::load_delay_timer_with_reg()
{
uint8_t reg = (m_current_op >> 8) & 0x0F;
m_delay_timer = m_reg[reg];
}
/*****************************
* FX18
*****************************/
void TChip8::load_sound_timer_with_reg()
{
uint8_t reg = (m_current_op >> 8) & 0x0F;
m_sound_timer = m_reg[reg];
}
/*****************************
* FX1E
*****************************/
void TChip8::add_ireg_with_reg()
{
uint8_t reg = (m_current_op >> 8) & 0x0F;
m_ireg += m_reg[reg];
}
/*****************************
* FX29
*****************************/
void TChip8::load_font_from_vx()
{
uint8_t reg = (m_current_op >> 8) & 0x0F;
m_ireg = m_reg[reg] * 0x5;
}
/*****************************
* FX33
*****************************/
void TChip8::store_binary_code_decimal_representation()
{
uint8_t reg = (m_current_op >> 8) & 0x0F;
m_ram[m_ireg] = m_reg[reg] / 100;
m_ram[m_ireg+1] = (m_reg[reg] / 10) % 10;
m_ram[m_ireg+1] = (m_reg[reg] % 100) % 10;
}
/*****************************
* FX55
*****************************/
void TChip8::load_memory_from_regs()
{
uint8_t reg = (m_current_op >> 8) & 0x0F;
for(int i=0; i<=reg; i++)
m_ram[m_ireg + i] = m_reg[i];
m_ireg += (reg + 1);
}
/*****************************
* FX65
*****************************/
void TChip8::load_regs_from_memory()
{
uint8_t reg = (m_current_op >> 8) & 0x0F;
for(int i=0; i<=reg; i++)
m_reg[i] = m_ram[m_ireg + i];
m_ireg += (reg + 1);
}
On main, you can now include every module we will need, initialize every component and the emulator, and on the loop, run ’emu.run’:
// ./src/main.cpp
#include <avr/interrupt.h>
#include "tavr/mega/pins.h"
#include "tavr/mega/uart.h"
#include "tavr/mega/time.h"
#include "tavr/mega/rand.h"
#include "chip8/modules/keyboard.h"
#include "chip8/modules/display.h"
#include "chip8/modules/sound.h"
#include "chip8/chip8.h"
#include "logger.h"
int main()
{
TTime_Mega time_mega((uint8_t)TTIMEMEGA_1INTMS);
TTime* time = TTime::init(&time_mega);
TUart_Mega uart(0, 250000, 1);
TLogger_Avr logger(uart);
TRandom_Mega random;
sei(); // enable global interrupts
TKeyboard_Mega keyboard(D22, D23, D24, D25, D26, D27, D28, D29);
TSound_Mega sound(ENote::O6_E);
TDisplay_Mega display;
display.init();
TChip8 emu(&logger, time, &random, &display, &keyboard, &sound);
emu.init(random.truerandom(0u, 4u));
while (1)
{
emu.run();
}
}
13 - Measuring Performance
An extra thing we can add to the emulator is a way to know how fast it is running:
// ./src/hz.h
#ifndef _hz_h_
#define _hz_h_
#include <inttypes.h>
#include "logger.h"
#define TFPS_SAMPLES 16
#define TFPS_PRINTMS 5000
class THz
{
uint64_t m_samples[TFPS_SAMPLES];
uint8_t m_samplei;
uint64_t m_lastprint;
TLogger* m_log;
public:
THz(TLogger* log);
void addSample(uint64_t start, uint64_t end);
uint64_t read();
};
#endif
By calculating the median ammount of time it takes for our emulator to run one cycle, we can then calculate how many cycles it can run per second:
// ./src/hz.cpp
#include "hz.h"
THz::THz(TLogger* log) : m_samples{0}, m_samplei{0}, m_lastprint{0}
{
m_log = log;
}
void THz::addSample(uint64_t start, uint64_t end)
{
if (m_samplei >= TFPS_SAMPLES)
m_samplei = 0;
m_samples[m_samplei++] = end - start;
if (end - m_lastprint > TFPS_PRINTMS)
{
m_lastprint = end;
if (m_log->beginLog(ELogLevel::DEBUG))
{
m_log->beLog(read());
m_log->beLog(" Hz");
m_log->endLog();
}
}
}
uint64_t THz::read()
{
uint64_t sum = 0;
for (uint8_t i = 0; i < TFPS_SAMPLES; i++)
sum += m_samples[i];
return 1000 / (sum / TFPS_SAMPLES);
}
On main, initialize our ‘THz’ class with our logger, and then get the millis before and after running the run function, after it, pass the values into the ‘addSample’ function and it will automatically print the results every 5 seconds:
// ./src/main.cpp
#include <avr/interrupt.h>
#include "tavr/mega/pins.h"
#include "tavr/mega/uart.h"
#include "tavr/mega/time.h"
#include "tavr/mega/rand.h"
#include "chip8/modules/keyboard.h"
#include "chip8/modules/display.h"
#include "chip8/modules/sound.h"
#include "chip8/chip8.h"
#include "logger.h"
#include "hz.h"
int main()
{
TTime_Mega time_mega((uint8_t)TTIMEMEGA_1INTMS);
TTime* time = TTime::init(&time_mega);
TUart_Mega uart(0, 250000, 1);
TLogger_Avr logger(uart);
TRandom_Mega random;
sei(); // enable global interrupts
TKeyboard_Mega keyboard(D22, D23, D24, D25, D26, D27, D28, D29);
TSound_Mega sound(ENote::O6_E);
TDisplay_Mega display;
display.init();
TChip8 emu(&logger, time, &random, &display, &keyboard, &sound);
emu.init(random.truerandom(0u, 4u));
THz hz(&logger);
while (1)
{
uint64_t s = time->millis();
emu.run();
uint64_t e = time->millis();
hz.addSample(s, e);
}
}
And now you have a Chip8 emulator on your Arduino. If you feel ready to code and also have a SPI display, i strongly recomend you make it work, as just switching the display takes this emulator from a proof of concept into an actual playable console/device.
But that’s all for today’s article, if you would like to, check our other Chip8 tutorials. Thanks for reading, until next time, and keep exploring the world of tech!