Introduction: Coding Assembler on ATmega328p

In this instructable I will show you how you can code Assembler on an Atmega328p which is actually like an Arduino in another form. I used the Atmega328p instead of a normal Arduino Uno or Nano because you only have to load the code once and then you can reprogram it yourself again and again.

This is a cool project because with this Assembler programming tool you can learn how to program Assembler and use the Atmega328p as a simple computer. You can change the code and define the OP-Codes by yourself.

It acts like an 8-bit Computer with a RAM of 256 bytes.

Functions:

  • Programming
  • Running
  • Showing the current value of the switches

Supplies

  • 1x Atmega328p
  • 1x IC-base for arduino
  • 1x LCD (16 x 2) which can be controlled by SCL and SDA
  • 1x 4-pin DIP switches
  • 1x 8-pin DIP switches
  • 1x 9V battery
  • 1x 7805 voltage regulator
  • 1x on-off-switch
  • 12x 1k ohm resistor
  • 1x 10k ohm resistor
  • 1x button
  • 2x 20pF condensator
  • 16'000 MHz crystal
  • lots of jumper wires
  • Some boards where you can place the electronic parts on
  • Some screws that you can screw the parts on the case
  • FTDI programmer

A 3D printer is not necessarily required because you can also screw everything on wood or even something third but I think it's the easiest way to generate the case.

Step 1: Printing Out the Parts

In this step you can create the case. Like I said earlier: It doesn't need to be 3D printed. It's up to you which material you use.

If you are printing out the case, you can find the stl file under the step. The yellow soldering board measures 2 by 8 cm, the red one 4 by 6 cm and the white one 5 by 7 cm.

Step 2: The Circuit

This step is about the circuit, how it is built and how I broke it down modularly. The colored areas on the picture are chosen in the same color as the boards on the cover picture.

The gray area contains the heart of the circuit: the ATmega328p, which acts like an Arduino Uno with the help of the surrounding components (capacitor, crystal, resistors). So you can replace the gray part with an Arduino Uno without any problems. If you choose this option, there is no need for a voltage regulator and the yellow part is therefore omitted.

The LCD I used has an I2C connector. This saves many pins on the Arduino. The SDA pin of the display is connected to A4, while SCL is connected to A5 of the Arduino.

In the red or orange part you can see that there are many switches connected to the ATmega328p. The eight switches, which are drawn below each other, are used to enter the code later. The other four switches are for switching between the modes, but I will only really need two of them. Maybe you can think of a few more modes and use them as well. Maybe you don't find eight bits enough and want to expand to ten bits with the two unused switches. Who knows :)
The single switch is there to make an input.

When I soldered the circuit together, it was important to me that it was modular so that I could quickly replace parts in the event of a defect. For this reason it became so many boards with me. But you can feel free to solder it on only one board.

Step 3: Functions

There are three main functions: Programming, Running and Showing the current value of the switches. In this step I am going to explain what these "modes" of out Assambler project are doing.

Programming: In this mode you can "type in your code" which means that you have a set of instructions and you can choose from them which one you take. You can take any of them but not every sequence of code will work properly because it has to make sense. There is a counter which shows you how many commands you have already entered. When you are coding something I recommend you to usa a piece of paper to write down all the commands in the right order, because in the end it's way easier to find a mistake if there is one.

Running: In the running mode you can run the code that you have written in the programming mode. If you don't get a value or the wrong one, you should check your code for any mistakes.

Showing the current value of the switches: In this mode you see the value of the switches. Especially in the beginning of working with binary numbers they can be confusing. That's why I made a mode, so that you can see what the binary number of your switches are in decimal numbers. The value that the running mode returns is also in binary numbers. If you want to know what it is in decimal you can just change the value switches to the same thing as shown on the screen (0 = switch down = off; 1 = switch up = on). And then you switch into the Showing the current value of the switches mode.

Step 4: Programmer

The ATmega328p has 28 pins, but you can't connect it directly to a computer. For this you need a chip like the YP-05 development board.

On the picture above you can see how to connect the chip. For this I wrote down the pin number and named the function behind it. To use the chip, you need the setup of the ATmega328p, which consists of the crystal and the two corresponding capacitors. I also added a LED to know better when the program was uploaded or if it is uploading at all, because the software unfortunately shows this very late. It is important to make sure that the ATmega328p is not connected to a power source anywhere else, otherwise the components could be damaged. A capacitor of 100 nF is to be connected to the reset pin, the rest is connected directly with a cable.

With this setup you can program the ATmega328p like an Arduino with the Arduino IDE. However, when uploading the program it is important to see which Arduino board you select. For me it only worked via a direct connection to the computer - not via a USB distributor - and with the board Arduino Nano. It can take a while until it works. Also you have to see if certain drivers are needed to access the YT-05 or whatever FTDI programmer you are using.

Step 5: OP-Codes

OP-codes are very important in assembler. They represent abbreviations of the commands. Assembler is a programming language which is very close to the hardware. For this reason, I found the switches to be a cool way of representing rather than just writing a computer program. Each OP-code has a specific number. In a computer, these numbers are then stored via the instruction register and the appropriate control signals are set to execute that particular instruction. In the document attached below I have defined a few OP-codes. But there are many more than these. Also, I have added a delay of one second so that you can count down, for example, as shown in the opening video.

Step 6: Coding

In the next few steps I am going to explain the code that I've written. It has many functions and I think it's very interesting to know how it works in detail. To make it clear, I have marked in each paragraph what it is about. My whole code is attached to this step.

Step 7: Preparing the LCD

#include <Wire.h>
#include "rgb_lcd.h"

rgb_lcd lcd;

const int colorR = 200;
const int colorG = 55;
const int colorB = 0;

The first step in a program is to consider what libraries you need for the project. The code is later processed chronologically, which means that everything you want to use later must be defined beforehand. For this project you need the LCD-library, which can be found here. Because the LC-Display runs over an I2C connection, you need the appropriate library to access it easily. This library is already preinstalled in the Arduino IDE and is called Wire.

rgb_lcd is an LCD object with the name "lcd". But you can choose this name as you like.

The next three variables are there to define the colors of the display. The background color is controlled by three variables for RGB which stand for the colors red, green and blue. I have chosen orange as the background color, which is why the relationship of the variables looks like in the code above.Together, the numbers should come to 255. The "const" is there because I will not change the colors in the course of the code and the display will remain constant orange.

Step 8: Define the Pins

//define the pins<br>int button = A0;
int mode = 11;
int address_data = 10;
int instructionNr = 12; //for completeness
int led = 13;
int one = 9;
int two = 8;
int three = 7;
int four = 6;
int five = 5;
int six = 4;
int seven = 3;
int eight = 2;

The pins which I connected to the ATmega328p have to be defined to work with them. The pins are all integer values and you write it so that you name the data type (in this case integer), behind it the name you give it, an equal sign which means defined and finally the corresponding value. This is how it is done with "normal" variables and also with the pin definition - like we do right now. The first pins which are defined are the control pins. Then comes an LED, which I added to see if the ATmega328p turns on. Then come the pins for programming.

Step 9: A List for the Output

//make a list for the output
int list[256];
int counter = 0;<br>

An 8-bit computer has only a certain amount of memory. This means that you can only enter 2^8 or 256 commands in a row. So to store the commands easily, you can create a list of 256 places that stores integers. In this program "binary numbers" are stored as integers which works because integers are quite large data types and there is a manageable number of OP-Codes.

To know at which place you are, you need a counter. Since a list starts at the place 0, you can set the counter to 0.

Step 10: Other Variables

//other variables
int mode2;
int nr; //number
int ad; //address

int space = 0;
String instruction;

int registerA;
int registerB;

int registerC[8];
int registerD[8];


//variables for jumping
int jump = 0;
int jumpe = 0;
int jumpa = 0;
int jumpb = 0;
int j = 0; //counter for the later program

int loopa = 0; //loop but written differently because of the Arduino syntax

int lastState = 0; //saves the last state of the button

int registerAS = 0;
int registerBS = 0;

Already at the beginning of the program I define the other variables, because you can access them later from all positions ("global variables"). If they were defined in a function, they would be "local variables". You can find more about this under this link. I wrote some comments behind the variables which should explain them.

Step 11: The Setup() Function

void setup() {  
  lcd.begin(16, 2);
  lcd.setRGB(colorR, colorG, colorB);
  
  pinMode(led, OUTPUT);

  //display welcome
  lcd.setCursor(0, 0);
  lcd.print("Welcome to");
  lcd.setCursor(0, 1);
  lcd.print("Assembler!");
  digitalWrite(led, HIGH);
  delay(2000);
  digitalWrite(led, LOW);

  //define input pins
  pinMode(one, INPUT);
  pinMode(two, INPUT);
  pinMode(three, INPUT);
  pinMode(four, INPUT);
  pinMode(five, INPUT);
  pinMode(six, INPUT);
  pinMode(seven, INPUT);
  pinMode(eight, INPUT);
  pinMode(address_data, INPUT);
  pinMode(mode, INPUT);
  pinMode(button, INPUT);

  lcd.clear();
}

The setup() function in an Arduino program runs exactly once - namely when the program starts - and there the pins are defined and the connection to the components is established via the libraries.

First, the lcd object is told that the display is 16 x 2 characters. Then the background color is set.

The LED is defined as an output, as it is to be lit.

To write something on the display, you have to tell the cursor exactly where to go. This happens with "lcd.setCursor(x, y)". Then you can write something on it with "lcd.print("whatever")". You just have to be careful not to reach the end of the display. As long as the display shows the message "Welcome to Assembler!" I want the LED to be on. To address the port I write "digitalWrite(port, HIGH)". After two seconds all this should stop. I do this with the command "delay(milliseconds)". After the time has expired, the LED should go off and the display should be blank. The display is cleared with "lcd.clear()".

The switches are to be set as input. For this purpose use the command "pinMode(pinNumber, INPUT)". I did this for all pins which are connected to the ATmega328p except the LED.

Step 12: Read Out the Pins

void loop() {  
  //query the pins
  int e = digitalRead(one);
  int z = digitalRead(two);
  int d = digitalRead(three);
  int v = digitalRead(four);
  int f = digitalRead(five);
  int se = digitalRead(six);
  int si = digitalRead(seven);
  int a = digitalRead(eight);

  //check mode
  mode2 = digitalRead(mode);

The loop() function repeats as long as the ATmega328p is connected to the power. After the program has gone through the setup() function, it automatically enters the loop() function. The loop function begins here and goes on for quite a long time. For a better reading the lines in the loop function are inserted.

Since the eight programming switches are needed for most modes, I read them first. Because even if the program seems to be long, mostly only a small fraction of it is executed - because it has very many branches. This also means that these switches are read out very often respectively quickly one after the other and the new state - if there is one - is set quickly.

Next, I check the mode I am in. This is the decision basis for the next branch, because only there is defined what exactly the program should do.

Step 13: Programming Mode I

  if(mode2 == HIGH){ //programming mode
    lcd.clear();
    lastState = 0;
    j = 0;    
      
    ad = digitalRead(address_data);

    if(ad == HIGH){
      //print out instruction number
      lcd.setCursor(13, 1);
      lcd.print(counter + 1);
      
      //Decision
      if(e == HIGH && z == LOW && d == LOW && v == LOW && f == LOW && se == LOW && si == LOW && a == LOW){ //ADD
        //print number on lcd
        instruction = "00000001";
        lcd.setCursor(0, 0);
        lcd.print(instruction);
        lcd.setCursor(0, 1);
        lcd.print("ADD");
        if(digitalRead(button) == HIGH){
          list[counter] = 1;
          delay(500);
          counter++;
        }
      }

If the switch for mode is HIGH, then the programming mode is switched on.

Because the display writes new characters over old ones, you want to delete the old characters, otherwise the number of characters in a row must be the same to not see any difference. The variable "lastState" is used to switch from programming mode to run mode and to run everything again. For this reason also "j" is reset, which will run through the loop with the commands later in the program.

If the Address_data control pin is set, one shall be able to program. It is also clear that one wants to see the current command number. That's why this is the first one to be shown on the display. Since a list starts with 0, but it should be easier for the user, the variable counter is temporarily increased by 1 for the output.

Now comes the elaborate part, because the next decisions are based on small changes and are divided into two types of structures. I will discuss one type now and the other in the next step. I used a lot of if() conditions to query the switches. Depending on the current position of the switches, the assembler command should be displayed "live". For this the binary number of the command is output on the top pf the display as well as the abbreviation on the next line. Since one must press the button to enter the command, this is also queried and the integer value is added to the list, if the button is pressed. Thereupon the counter increases by 1.

Step 14: Programming Mode II

      if(e == HIGH && z == LOW && d == HIGH && v == HIGH && f == LOW && se == LOW && si == LOW && a == LOW){ //LDA
        //print number on lcd
        instruction = "00001101";
        lcd.setCursor(0, 0);
        lcd.print(instruction);
        lcd.setCursor(0, 1);
        lcd.print("LDA");
        if(digitalRead(button) == HIGH){
          list[counter] = 1101;
          delay(500);
          counter++;
          lcd.clear();
          lda();
        }
      }

The second structure occurs in the load A command. Here it must also be decided in the programming mode which value is to be loaded into the register A. The change happens after the button has been pressed. Here the function lda() is called, in which one can enter a value. I will explain this function in more detail later.

Step 15: Show Current Value

    if(ad == LOW){ //show current value
      lcd.setCursor(0, 0);
      lcd.print("CURRENT VALUE:");              
      lcd.setCursor(0, 1);
      lcd.print(number());
    }

To display the current value, set the address_data switch to LOW. In the function number() - which I will discuss later in this section - the current position of the switches is converted into a number of the data type integer.

Step 16: Run Mode

  }if(mode2 == LOW){ //Run mode
    if(lastState == 0){
      lcd.clear();
      registerA = registerAS;
      registerB = registerBS;
      lastState = 1;
    }
    
    lcd.setCursor(0, 0);
    lcd.print("OUTPUT:");

    while(j < 256){
      if(digitalRead(mode2) == HIGH){
        j = 0;
        break;
      }
      if(list[j] == 1){ //ADD
        add();
        j++;
      }
      if(list[j] == 10){ //AND
        anda();
        j++;
      }
      if(list[j] == 11){ //JA<B
        if(registerA < registerB){
          j = jumpa;
        }else{
          j++;
        }
      }
      if(list[j] == 100){ //JA>B
        if(registerA > registerB){
          j = jumpb;
        }else{
          j++;
        }
      }
      if(list[j] == 101){ //DECA
        decA();
        j++;
      }
      if(list[j] == 110){ //DECB
        decB();
        j++;
      }
      if(list[j] == 111){ //DIV
        divide();
        j++;
      }
      if(list[j] == 1000){ //HLT
        j = 256;
      }
      if(list[j] == 1001){ //INCA
        incA();
        j++;
      }
      if(list[j] == 1010){ //INCB
        incB();
        j++;
      }
      if(list[j] == 1011){ //JE
        if(registerA == registerB){
          j = jumpe;
        }else{
          j++;
        }
      }
      if(list[j] == 1100){ //JMP
        j = jump;
      }
      if(list[j] == 1101){ //LDA
        j++;
      }
      if(list[j] == 1110){ //LDB
        j++;
      }
      if(list[j] == 1111){ //LOOP
        if(registerA - 1 > 0){
          registerA -= 1;
          j = loopa;
        }else{
          j++;
        }
      }
      if(list[j] == 10000){ //MUL
        mul();
        j++;
      }
      if(list[j] == 10001){ //NOT
        nota();
        j++;
      }
      if(list[j] == 10010){ //OR
        ora();
        j++;
      }
      if(list[j] == 10011){ //OUTA
        outA();
        j++;
      }
      if(list[j] == 10100){ //OUTB
        outB();
        j++;
      }
      if(list[j] == 10101){ //SUB
        sub();
        j++;
      }
      if(list[j] == 10110){ //DELAY
        delay(1000);
        j++;
      }
    }
  }
  delay(300);
}

If the mode switch is set to LOW, you are in run mode. First the variable "lastState" is set to 1, so that the run mode can be called again when switching to programming mode. The register values are therefore stored again in another variable, so that these are reset to the initial position.

Then the text "output" will be shown on the display. This is useful to know which mode you are in, even if you have not yet programmed a line or if there is no output because the program you have written contains an error.

Next, the list of commands is processed. Here mostly functions are called, which I will discuss in the next steps. If two functions are the same, I will only deal with one. The whole code is stored in one of the steps above.

Step 17: Function Loopx()

void loopx(){
  while(true){
    lcd.setCursor(0, 0);
    lcd.print("Line:");
    binary(number(), 7, 0);
    if(digitalRead(button) == HIGH){
      loopa = number() - 1;
      delay(500);
      break;
    }
    delay(200);
  }
} 

In order not to have to enter the same lines in a program x times, there is the loop function with which you can jump to any line in assembler and the code is executed from there. This function is already called in the programming mode. For this the position of the switches is identified and converted into a number. This number is later calculated in the loop -1 - because of the list and the number 0 - and stored in loopa, which is processed in the list in step 16. The current value of the switches is transformed by the function binary from an integer into a list with ones and zeros, which are output on the display.

Step 18: Function Jumpx()

void jumpx(){
  while(true){
    lcd.setCursor(0, 0);
    lcd.print("Line:");
    binary(number(), 7, 0);
    if(digitalRead(button) == HIGH){
      jump = number() - 1;
      delay(500);
      break;
    }
    delay(200);
  }
}

When jumping, the function is only executed if the condition checked in step 16 is also true. So all jump functions in this step look the same, even if the conditions are partly opposite. Again, you have to choose a line of code to jump to. So this works like in the last step.

Step 19: Function IncA(), DecA()

void incA(){
  if(registerA < 255){
    registerA += 1;
  }else{
    registerA = 0;
  }
}

void decA(){
  if(registerA > 0){
    registerA -= 1;
  }else{
    registerA = 255;
  }
}

Increment means that the value of the register goes up by 1. It was important to me to make sure that there are no values that cannot be displayed. Since only the numbers from 0 to 255 can be displayed, after 255 the register is reset to 0 and continues counting.

Decrementing works the same as incrementing, but the value is decreased by 1.

Step 20: Function Nota()

int nota(){
  for(int j = 0; j < 8; j++){
    registerC[j] = registerA % 2;
    registerA = registerA / 2;
  }
  
  int c[8];

  for(int j = 0; j < 8; j++){
    if(registerC[j] == 1){
      c[j] = 0;
    }else{
      c[j] = 1;
    }
  }

  int d = numberAddition(c);
  registerA = d;
  return d;
}

With NOT all values in the register can be reversed. This means that all ones become zeros and vice versa.

Because the current value of the register is stored as an integer, it must first be converted into an array of ones and zeros. For this I use modulo. This function gives the remainder that is left when you divide by a number. So with this I get either a one or a 0.

Next, all the values in the array are flipped.

Finally, the array must be converted to the new number of the integer data type. To convert arrays to integer the function numberAddition() is used. I will introduce this a little later.

Step 21: Function Anda()

int anda(){
  for(int j = 0; j < 8; j++){
    registerC[j] = registerA % 2;
    registerA = registerA / 2;
  }

  for(int j = 0; j < 8; j++){
    registerD[j] = registerB % 2;
    registerB = registerB / 2;
  }
  
  int c[8];

  for(int j = 0; j < 8; j++){
    if(registerC[j] == 1 && registerD[j] == 1){
      c[j] = 1;
    }
  }

  int d = numberAddition(c);
  registerA = d;
  return d;
}

First, the values are converted into an array as in the last step. This time, however, it must be done for both registers (A and B). Then an AND operation is performed for each element of the array and the result is stored again in an array. Finally, the array is converted back to an integer and stored in register A.

Step 22: Function Ora()

int ora(){
  for(int j = 0; j < 8; j++){
    registerC[j] = registerA % 2;
    registerA = registerA / 2;
  }

  for(int j = 0; j < 8; j++){
    registerD[j] = registerB % 2;
    registerB = registerB / 2;
  }
  
  int c[8];

  for(int j = 0; j < 8; j++){
    if(registerC[j] == 1 || registerD[j] == 1){
      c[j] = 1;
    }
  }

  int d = numberAddition(c);
  registerA = d;
  return d;
}

First, the values are converted into an array as in the last two step. This time, however, it must be done for both registers (A and B). Then an OR operation is performed for each element of the array and the result is stored again in an array. Finally, the array is converted back to an integer and stored in register A.

Step 23: Function OutA()

void outA(){
  binary(registerA, 0, 1);
}

The register A is passed into the binary function, which converts the integer value into an array for output.

Step 24: Calculate

int add(){
  int i = registerA + registerB;
  registerA = i;
  return i;
}

int sub(){
  int i = registerA - registerB;
  registerA = i;
  return i;
}

int mul(){
  int i = registerA * registerB;
  registerA = i;
  return i;
}

int divide(){
  int i = registerA / registerB;
  registerA = i;
  return i;
}

For adding, subtracting, multiplying and dividing, the values of the two registers are handled with the corresponding operation and stored in register A.

Step 25: Function Lda()

void lda(){
  while(true){
    lcd.setCursor(0, 0);
    lcd.print("Data:");
    binary(number(), 7, 0);
    if(digitalRead(button) == HIGH){
      registerA = number();
      registerAS = registerA;
      delay(500);
      break;
    }
    delay(200);
  }
}

After pressing the button in the programming mode, it is necessary to determine which value you want to load into the register. To do this, enter a value with the switches, which is output on the display via the binary function. The program is executed in a loop, so that you can have as much time as you need to enter the value. During the loop no other functions are possible. After the button has been pressed again for input, the value is saved and the loop is exited.

Step 26: Function Number()

int number(){
  //scan pins
  int ei = digitalRead(one);
  int zw = digitalRead(two);
  int dr = digitalRead(three);
  int vi = digitalRead(four);
  int fu = digitalRead(five);
  int sec = digitalRead(six);
  int sie = digitalRead(seven);
  int ac = digitalRead(eight);

  //add
  int i = 0;
  if(ei == HIGH){
    i += 1;
  }
  if(zw == HIGH){
    i += 2;
  }
  if(dr == HIGH){
    i += 4;
  }
  if(vi == HIGH){
    i += 8;
  }
  if(fu == HIGH){
    i += 16;
  }
  if(sec == HIGH){
    i += 32;
  }
  if(sie == HIGH){
    i += 64;
  }
  if(ac == HIGH){
    i += 128;
  }
  return i;
}

Here the values of the switches are converted into an integer. For this the switches are read out and the variables are added with a number of the 2^n series depending on the position of the switches.

Step 27: Function NumberAddition()

int numberAddition(int i[8]){
  int k = 0;
  if(i[0] == 1){
    k += 1;
  }
  if(i[1] == 1){
    k += 2;
  }
  if(i[2] == 1){
    k += 4;
  }
  if(i[3] == 1){
    k += 8;
  }
  if(i[4] == 1){
    k += 16;
  }
  if(i[5] == 1){
    k += 32;
  }
  if(i[6] == 1){
    k += 64;
  }
  if(i[7] == 1){
    k += 128;
  }
  return k;
}

The numberAddition function has some similarity to the number() function. Only here an array is entered, which is then converted into an integer. Also here the 2^n series is used.

Step 28: Function Binary()

void binary(int i, int cursorPlace, int line){
  int number[8];
  for(int j = 0; j < 8; j++){
    number[j] = i % 2;
    i = i / 2;
  }
  
  for(int j = 7; j >= 0; j--){
    lcd.setCursor(cursorPlace, line);
    lcd.print(number[j]);
    cursorPlace++;
  }
}

The binary function takes an integer, and two variables for the position on the display, and converts the integer into an array. This is then printed from back to front on the display and looks like a binary number consisting out of 8 bits.