Building an ATtiny Cluster

The thought process was as follows. It’s Christmas: time for the annual Christmas project, also known as: Hiding-from-the-in-laws-in-my-lab. So it went:

  • Why don’t I refresh and deepen my understanding of microprocessor architecture.
  • Build my own? Yay!
  • No, too much effort.
  • Simulate it? Too lame.
  • Use serial bus and save some wire? Perhaps…

And so I thought of a serial bus to connect the main microprocessor components: ALU, registers, controller etc. Why not use an off the shelf 2-wire bus? Yes, I2C. Why not use ATtiny85’s so I can simulate ALU’s, Registers, Controllers etc.? Great, that’s easy, cheap and doable. Hence the idea of an ATTiny85 cluster was born.

The idea of creating a ATtiny85 cluster is both silly and not so. Almost certainly a Teensy 4.1 at 600Mhz or a Raspberry Pi 4 at 1.5 gigs and all the other amazing specs can do anything and more than a cluster of tiny’s. But there is so much to learn from building a cluster of microcontrollers that makes it all worth it. Think about it, once you’ve figured out how to orchestrate tasks, balancing resources etc, the step from upgrading a cluster of ATtiny85’a to a cluster of Teensy’s or Raspberry Pi’s is minor. But a cluster of Teensy’s or Raspberry Pi’s can do things that are currently at the edge of what can be done with microprocessors or microcontrollers. So an ATtiny85 cluster it is. And besides, a cluster of 16 tiny’s can be built for 20 bucks.

Artist rendering of a 16 processor ATtiny85 cluster (4 x 5 cm)

Sample application: microprocessor architecture

As mentioned in the intro, I wanted to mess around with a microprocessor architecture. So, let’s build one using the ATtiny Cluster. In the 80’s (ouch) I worked through the Z80 architecture at uni. One of my first jobs was a M6809 outfit and I cut my teeth on that, both on the hardware and software.

Googling both, as well as the popular 8080, 8086 and 6502, I concluded that for my purposes, I didn’t need to choose. I don’t intend to fully replicate the complete architecture, just enough to have a “microprocessor” running on the cluster with some basic instruction set that allows me to write some assembler, compile by hand and load the byte code onto the cluster. A simple program will do. I’m big on fractals, so perhaps a fractal calculation application.

As you can see from the diagram, this microprocessor is quite simple. A controller will fetch instructions from the program memory, decode these and determine where to fetch data, what to do with it and then do that, using the services of the Arithmetic Logic Unit (ALU), Registers and the IO.

You can also see that these functions will be implemented on 5 nodes. Ideally all on the Tiny Cluster. Most likely, I will use an Arduino Nano for the controller and possibly the IO as well. In theory, I’m sure all these nodes could run on the ATtiny85’s but their limited memory resources will be a constraint.

To make things even more interesting, we could deploy the ALU on multiple nodes and perhaps make it more of a maths coprocessor. We can also crank up the memory by using multiple nodes as well. So you see, I definitely need a 16 processor cluster.

A simple 2-Nano test rig

To create a Attiny85 cluster to experiment with multi core systems, we require a master-slave data exchange. I’ve chosen to use I2C which is built into the ATtiny85 chip. For simplicity, I’m working with two Arduino Nanos. It’s enough connect the ground pins (pin 4) and the SDA (pin 23) and SCL (pin 24), and hook up the Nanos to your laptop.

Arduino nano I2CF master slave test set up

Using the Arduino IDE, select the board type (Arduino Nano), the processor (ATmega 328p or ATMega328p (old bootloader). You can now program each Nano by selecting the associated USB port.

You can now upload one of the example “wire” programs, for example Master Writer and Slave Reader to each of the Nanos respectively, check the serial monitor and you’ll the I2C communication in action. Arduino has some sparse documentation on the simple wire library (https://www.arduino.cc/en/reference/wire).

Using this test rig, I wrote some master and slave code, where the slave is a calculator and the master a controller. Below is the master code.

// Tiny Cluster - Master-Slave test rig
// Henk Mulder
// eDesign Extended
// d1x1.com
//
// This is code for the master. It requests a series of calculations from the slave, 
// which is a i2c calculator module.

#include <Wire.h>

const int SLAVE = 7;            // address of slave node
const int OP_ADD = 1;           // opcode for addition
const int OP_SUB = 2;           // opcode for subtraction
const int OP_MUL = 3;           // opcode for multiplication
const int OP_DIV = 4;           // opcode for division

void setup() {
  Wire.begin(); // connect to i2c
  Serial.begin(115200); 
}


void loop() {
  Serial.print("8 + 101 = ");
  Serial.println(requestCalculation(OP_ADD, 8,101));
  Serial.print("72 - 8 = ");
  Serial.println(requestCalculation(OP_SUB, 72 ,8));
  Serial.print("7 * 11 = ");
  Serial.println(requestCalculation(OP_MUL, 7,110));
  Serial.print("12 / 2 = ");
  Serial.println(requestCalculation(OP_DIV, 12,2));
  Serial.println();
  
  delay(500);
}


byte requestCalculation(byte opcode, byte a,byte b){
  // send request for addition of two bytes
  Wire.beginTransmission(SLAVE);   // transmit to slave
  Wire.write(opcode);              // opcode for multiplication
  Wire.write(a);                   // first value
  Wire.write(b);                   // second value
  Wire.endTransmission();          // end transmission with slave

  Wire.requestFrom(SLAVE, 1);      // request 1 byte response from the slave

  byte answer;      
  while (Wire.available()) {  
    answer = Wire.read();          // receive a response     
  }
  return answer;
}

And the slave is here.

// Tiny Cluster - Master-Slave test rig
// Henk Mulder
// eDesign Extended
// d1x1.com
//
// This is code for the slave. it receives operands and an opcode 
// (type of operation required= from the master and returns the 
// response when requested.
//
// Note that although the masters in this example sends new operands 
// and requests the answers immediately, the reception of operands and
// the request for the answers are asynchronous.
//
// Also note that no boundary checks are done on the data. All operations
// are "modulo 256", i.e. if the answers are outside the 0-255 range, 
// than multiples of 256 are added or subtracted until it is. Division also 
// rounds as per the rules of integer division. Divide by zero returns 255.

#include <Wire.h>

const int SLAVE = 7;              // address of slave node
int a;
int b;
int opcode;

void setup() {
  Wire.begin(SLAVE);              // join i2c bus with address #8
  Wire.onReceive(receiveEvent);   // set up the receive event
  Wire.onRequest(requestEvent);   // set up the request event
  Serial.begin(115200);           // start serial for output

}

void loop() {
  delay(100);                     // listen for data and requests
}

// The slave has received data from the master
// The data bytes received are: opcode, operand a and operand b

void receiveEvent(int howMany) {
  opcode = Wire.read();           // receive opcode
  Serial.print("opcode: ");       // print opcode
  Serial.println(opcode);         

  while (1 <= Wire.available()) { // loop through all but the last
    a = Wire.read();              // receive byte as a int
    b = Wire.read();              // receive byte as a int
    Serial.print("a = ");         // print opertand a
    Serial.println(a);            
    Serial.print("b = ");          // print opertand b
    Serial.println(b);             // print the int
  }
}


// the master requests data dfrom the slave
// i.e. the answer of the last calculation request

void requestEvent() {
  switch( opcode )
  {
    case 1:                      // addition
        Wire.write(a+b);
        break;
    case 2:                      // subtraction
        Wire.write(a-b);
        break;
    case 3:                      // multiplication
        Wire.write(a*b);
        break;
    case 4:                      // division
        Wire.write(a/b);
        break;
  }
}

Below you can see the output on the master and slave serial terminals.

Master Serial Monitor
Slave Serial Monitor