Bit Banging an ATTiny85 and a Pi (Part 1)

The ATTiny85 is truly an amazing little device. In my current project, I’ve used it to create a rotary encoder peripheral which I mean to hook up to the raspberry pi. The peripheral on its own took me weeks to design correctly, but now that it works beautifully I have to ask… How can I get those readings back to the raspberry pi? The answer is, of course, bit banging!

Some people may scoff at the idea of wasting software cycles to do the menial tasks required to communicate between devices. But to me, it is a fun challenge with very achievable results. Don’t get me wrong, I love reading data-sheets as much as the next… But I’m ready to see some results! And with the awesome power of a raspberry pi, I don’t feel too bad utilizing some of those cycles if it means a quick and easy communication protocol. So here’s how I managed implement simplex binary encoding between an ATTiny85 and a raspberry pi…

In this example, I ended up writing my own bit transmission protocol. Perhaps a smidge overkill and admittedly a very slow implementation due to the need for syncing the cycles often. That being said, it’s also a great starting point so that you can further improve the design (or write some other protocol entirely). Someday, I’d really love to implement this with Manchester Encoding. But today is not that day.

Before we get further, let’s define some project objectives:

  • One-way communication from the peripheral to a raspberry pi
  • A maximum of 10 bits are required per “packet” (maximum number you can send: 1023)
  • Ideally it will only require 1 GPIO wire
  • The library must be non-blocking

It would be easy to change this to support more bits. Or bytes. Or whatever requirements your peripheral has. Though my encoder really only goes up to 500. So 10 bits seemed more than enough for my purposes.

This blog post will document my very light-weight library that I’ve created. You can see the full source code on my github repo here: https://github.com/SharpCoder/attiny85-bit-banging

The Protocol

Let’s define the specifics of the protocol we will be implementing. I chose a naiive approach which is very very simple to implement, unfortunately it’s also very slow. Here’s why…

  • Each bit will be either ON or OFF
  • We will use a static timing defined in milliseconds. So for the duration of this time sequence, we will pull the receiving pin either HIGH or LOW and then move on to the next bit
  • To verify our sequence, we will have a synchronization stage which pulls the output pin HIGH for an entire cycle

Here’s an example of the number 170 encoded. You can see why this protocol is very slow. More than half of it is used creating a synchronization signal! But that’s fine. It’s still fast enough for our needs and incredibly easy to implement.

data signal

You may notice that we pull the receive pin LOW before the true sync signal occurs. This is to make sure that we know when the data signal ended and the synchronization signal begins. I believe there’s still an edge case that could cause momentary confusion if you are sending all 1’s intentionally.

CommConfig.h

Now that we’ve defined the protocol. Let’s start implementing it! The following code will be written in c++ and is intended for the ATTiny85 chip.

This first file that we create will be a simple place to define the library variables. I decided to use a config file so these were very explicitly defined. They are tightly bound to the receiving hardware as well as the physical device you are programming.

#ifndef COMM_CONFIG_H
#define COMM_CONFIG_H

#define COMM_TIMING    32 // Timing (in ms) between each bit
#define COMM_WIRE      3  // Which pin to use for transmission

#endif

This should be relatively straightforward.

Comm1W.h

Next we will define the actual .h file for our library. There are only 3 publicly defined methods that we will need to expose.

  • void commInit() - This will initialize the output pin. It MUST be called in the arduino setup(){ } method.
  • void commSet() - You can invoke this method each time you want to send a new value. It’s important to note that this does NOT queue up messages. It simply updates the value that will be used in the next communication cycle.
  • void commLoop() - This method MUST be called in arduino loop(){ } It is non-blocking and will handle all communication over the specified output wire.

Another important thing to note about this library is that you should avoid using delay() anywhere. It will mess up the timing of the data signals.

#ifndef COMM_1_WIRE
#define COMM_1_WIRE

void commInit();
void commSet(short value);
void commLoop();

#endif

Comm1W.cpp

In the first part of Comm1W.cpp we will define all the variables necessary to run the library. Since this is an even smaller arduino than common ones, it’s best to code as “defensively” as possible.

#include "Arduino.h"
#include "CommConfig.h"
#include "Comm1W.h"

// Define all the variables we need to handle non-blocking state
boolean comm_sending_sync = false;
boolean comm_sending_sync_1 = false;
boolean comm_sending_sync_2 = false;

// Define all of the other variables we need to support the protocol
char comm_index = 0;
short comm_value = 0;
short comm_value_next = 0;
unsigned long comm_current_millis = 0;
unsigned long comm_target_millis = 0;

In keeping with our non-blocking requirement, you may notice that we use a bunch of state booleans. These will be used in conjunction with the millis() function to keep track of what portion of the code we should be running at any given time.

// Entry point to initialize the PIN we need.
void commInit() {
  pinMode(COMM_WIRE, OUTPUT);
}

boolean comm_can_execute() {
  if (comm_target_millis > comm_current_millis) {
    return false;
  }
  return true;
}

// Update the target delay
void yield_delay(unsigned long ms)
{
  comm_target_millis = comm_current_millis + (ms * 1);
}

The first crucial functionality is the comm_can_execute() and yield_delay() methods which are the heart of our non-blocking sleep mechanism. Essentially it just sets a target variable equal to the millisecond reading that we should be at or beyond before running the next stage of code.

void commSet(short number) {
  comm_value_next = number;
}

We also need to define the exposed function which allows anyone to set the next number that we will send.

void commLoop() {
  comm_current_millis = millis();
  // return if we need to delay longer.
  if (comm_can_execute()) {
    if (!comm_sending_sync) {
      if ((comm_value >> comm_index) & 0x01 == 1) {
        // High value
        digitalWrite(COMM_WIRE, true);
      } else {
        // Low value
        digitalWrite(COMM_WIRE, false);
      }
      yield_delay(COMM_TIMING);
      comm_index++;

      if (comm_index >= 10) {
        comm_index = 0;
        digitalWrite(COMM_WIRE, true);
        comm_sending_sync_1 = true;
        comm_sending_sync_2 = false;
      }
    } else {
      if (comm_sending_sync_1) {
        digitalWrite(COMM_WIRE, false);
        yield_delay(COMM_TIMING * 1);
        comm_sending_sync_1 = false;
        comm_sending_sync_2 = true;
       } else {
        digitalWrite(COMM_WIRE, true);
        yield_delay(COMM_TIMING * 10);
        comm_sending_sync = false;
        comm_sending_sync_1 = false;
        comm_sending_sync_2 = false;

        // Now that we've completed one full transmission
        // update the next value to send.
        comm_value = comm_value_next;
      }
    }
  }
}

There are three parts to this block of code, so I’ll address each one in turn.

Part 1

If we can execute code, we need to read which state we are currently in. If we’re not sending the synchronization signal, then we’re free to set the next bit in the sequence for our current send value. Check if the bit on our current index is high or low and then set the output accordingly. Simple as that!

Part 2

Okay now it’s time to start the synchronization signal. We know that we’ve transmitted 10 bits already. So we enter the sync mode. Simply pull the output LOW. This will make sure it breaks any continuous HIGH sequences that may have been left off. Just so the receiving hardware knows unequivocally that we are not sending a synchronize signal before this point. Wait the COMM_TIMING amount of milliseconds, and move on to the final point in our transmission.

Part 3

Last thing we need to do is pull the output HIGH for 10 bits. Thus, completing the synchronization sequence.

Conclusion

That’s all there is to the bitbanging library I wrote. Again, if you want to see the code simply go to my github repo https://github.com/SharpCoder/attiny85-bit-banging. In the next article, I’ll explain how to receive this signal using Java and a raspberry pi!

Troubleshooting

Here are some common problems I ran into while getting this setup.

  • The GPIO pins appear to “float” even when they are definitely hooked up correctly. You might assume I had forgotten a pull up/down resistor, but no. While developing the code, I was using an Uno for quick prototyping which happened to be hooked up to my MacBook Pro through the large USB port. Well, turns out that was causing inconsistent voltage drops which made any sort of communication between the arduino and the raspberry pi completely void. Beware of this! My solution was to use VIN on the arduino sourced from the Raspberry Pi. Suddenly my floating GPIO reads went away… -_-