Pinball machine’s internal dialogue¶
This week we needed to create a communication between multiple controllers. Originally my plan was to not have a controller on the bumpers, but doing it this way enabled me to create the PCB for the pinball machine while working on the project.
The idea¶
I2C
I2C or inter-integrated circuit is a communication protocol between microcontrollers. As its name suggests, it was originally designed for circuit boards with multiple microcontrollers or other devices. I2C is a controller-peripheral system, where one microcontroller can poll other microcontrollers for read or write permissions based on the id-address of the peripheral. I2C requires two pulled up pins for communcation - clock and data - and it can only communicate with one peripheral at the time.
Initially the idea was to have all bumpers be completely isolated and self sustaining, but then I understood that the bumpers need a way to communicate when they were hit in order to calculate the points. I decided to test, if I2C could be used for this purpose.
The basic idea was to have the main board poll each bumper periodically, like every 100 ms, to see if they had been activated since. The bumpers in this design needs to be extremely stupid, and only know one thing: has the bumper been activated since the previous poll. When the controller polls the peripheral, it will answer with “1” if it had been activated and with “0” if not.
The annoying thing in using I2C is the fact that all peripherals need to have their own addresses, which need to be hard coded into it. There are ways around it, but there never can be two devices with same addresses in one I2C bus. I did not do anything fancy here, I just changed the #defined I2C_ADDRESS constant when programming the each bumper board. I even wrote the address on the board, so that I would remember them.
I2C on a breadboard¶
At first, I tried a simple way to make I2C communication between two microcontrollers. I used a breadboard for this, as it enabled me to test how the wiring should work and make mistakes on the way.
I took two microcontrollers, an RP2040 and an ESP32-C3, and attached them to a breadboard. After a couple of failed attempts, I managed to connected the SDA (data) and SCL (clock) pins from both controllers together in a bus that was pulled up to the 3V pin of the RP2040, which worked as the master for the connection.
I copied the code for the master and the peripheral from this Arduino tutorial. The code from there worked as-is, without any modifications. There was even no need to modify any pins, as I2C pins are predefined in all microcontollers.
Master code
// Wire Controller Reader
// by Nicholas Zambetti [http://www.zambetti.com](http://www.zambetti.com)
// Demonstrates use of the Wire library
// Reads data from an I2C/TWI peripheral device
// Refer to the "Wire Peripheral Sender" example for use with this
// Created 29 March 2006
// This example code is in the public domain.
#include <Wire.h>
void setup() {
Wire.begin(); // join i2c bus (address optional for master)
Serial.begin(9600); // start serial for output
}
void loop() {
Wire.requestFrom(8, 6); // request 6 bytes from peripheral device #8
while (Wire.available()) { // peripheral may send less than requested
char c = Wire.read(); // receive a byte as character
Serial.print(c); // print the character
}
delay(500);
}
Peripheral code
// Wire Peripheral Sender
// by Nicholas Zambetti [http://www.zambetti.com](http://www.zambetti.com)
// Demonstrates use of the Wire library
// Sends data as an I2C/TWI peripheral device
// Refer to the "Wire Master Reader" example for use with this
// Created 29 March 2006
// This example code is in the public domain.
#include <Wire.h>
void setup() {
Wire.begin(8); // join i2c bus with address #8
Wire.onRequest(requestEvent); // register event
}
void loop() {
delay(100);
}
// function that executes whenever data is requested by master
// this function is registered as an event, see setup()
void requestEvent() {
Wire.write("hello "); // respond with message of 6 bytes
// as expected by master
}
I manager to get the microcontrollers to communicate between each other. At this point, I knew how to wire the I2C communication, so now only thing I needed to do was to build one properly.
I2C in bumpers¶
My bumpers needed a way to communicate to the main board if the had been activated.
I2C is bad for this
I2C is really a bad way to do this. What I want is some kind of system where the peripherals can send event to the main board, when the peripheral has been activated. I2C does it backwards, where the master needs to query and poll the peripherals for changes.
I created a PCB for the bumper, which acts as a peripheral for the main controller. Initially I intented to add a relay to the PCB, as the solenoid in the bumper requires 24V and 4-5A of power, and a normal transistor wont cut it with that high amperage requirements. This system would have also required me to design a system around said relay that contains a protective diode and a way to supply 24V to the board. It would have probably also required two different grounds, so that the 24V channel would not interfere with any pulled down lines in the lower voltage circuit.
But I happened to own a couple of relay components that contained all of that in one package. If I attachet that next to my PCB, I would only need to send a wire from the board to the relay component to activate it, and not worry about any other bits.
This of course left my PCB quite bare. There would only be one ATtiny412 and a couple of screw terminals attached to a select pins. To make the board look a bit more proper and built-to-purpose, I labeled all terminals with their intented purpose.
I also added a updi pin to it, in order to program the ATtiny when it was soldered to the PCB.
I probably would need to change the screw terminals to something else in the final project.
I designed the PCB using KiCad with the same process as used in electronics design week and milled it according to what I had been taught in electronics production week.
PCB Files for first bumper design
After milling them, I soldered the bits on it. By bits I mean ATTiny412 and 4 screw terminals with 2 slots.
The I programmed them. I used the previous example code as a starting point and edited them for my use.
Main board
#include <Wire.h>
#define PERIPHERAL_COUNT 2
int peripherals[2] = {8, 9};
void setup() {
Wire.begin(); // join i2c bus (address optional for master)
Serial.begin(9600); // start serial for output
}
void loop() {
bool foundSomething = false;
for (int i=0; i<PERIPHERAL_COUNT; ++i) {
Wire.requestFrom(peripherals[i], 1);
if (Wire.available()) {
foundSomething = true;
Serial.print(peripherals[i]);
Serial.print(":");
while (Wire.available()) { // peripheral may send less than requested
char c = Wire.read(); // receive a byte as character
if (c == 'a') {
Serial.print(1);
} else if (c == 'b') {
Serial.print(0);
}
}
Serial.print(',');
}
delay(100);
}
if (foundSomething == false) {
Serial.println("Silence");
} else {
Serial.println();
}
delay(500);
}
Bumper
#include <Wire.h>
#define SOLENOID_PIN 6
#define I2C_ADDRESS 8
#define BUTTON_PIN 4
bool switchHasToggled = false;
void setup() {
Wire.begin(I2C_ADDRESS); // join i2c bus with address defined in I2C_ADDRESS
Wire.onRequest(requestEvent); // register event
pinMode(BUTTON_PIN, INPUT_PULLUP);
}
void loop() {
if (digitalRead(BUTTON_PIN) == LOW) {
switchHasToggled = true;
}
delay(100);
}
// function that executes whenever data is requested by master
// this function is registered as an event, see setup()
void requestEvent() {
if (switchHasToggled) {
Wire.write('a');
switchHasToggled = false;
} else {
Wire.write('b');
}
//Wire.write("hello "); // respond with message of 6 bytes
// as expected by master
}
To upload this to my PCB, I followed this documentation on how to use the updi programmer that exist was available in our lab.
I also needed the Arduino IDE to support the ATtiny board. To do that I followed these instructions. I needed to add http://drazzy.com/package_drazzy.com_index.json
to File -> preferences -> Additional Board Manager URLs
. There already was a url from previous week assignment, but thankfully Arduino IDE supports multiple urls. I only needed to separate them with commas. After that I installed megaTinyCore from board manager (Tools -> Board: -> Board Manager
).
After that I selected the ATtiny412 as my board, correct USB port, and SerialUPDI - SLOW as my programmer. The I could not upload with the normal arrow button on the top left, instead I had to press Sketch -> Upload using a programmer.
Troubles¶
At this point I hit my first big snag, when Arduino IDE would not let me upload my code. No matter what I did, what settings I changed, which board I used, nothing seemed to work. It just threw some weird error about UPDI initialization failing, After wasting a total of 5 hours on it during vappu, I finally gave up and asked for help. And even they were flummoxed. Until someone noticed that the wiring on the lab’s UPDI programmer was a bit different from the one on the website. For some reason, the pin order in labs programmer is mirrored from what the picture in the documentation shows. So I just swapped the wires around, and it worked.
I had a lot of trouble figuring out how to get the buttons to work in the bumpers, which work as stand-in for the steel ball activated shorted copper plates. For some time, my bumper code would not read button activations from the pull down buttons that I had wired to it. And I looked for different reasons as to why it would not work. I perused the data sheet if the pins support internal pull up (yes they do), I looked through multiple websites on how the pin numbers map between Arduino and ATtiny412, but none of them seemed to work.
I then wrote this simple script, which I used to test all possible pins that the button could possibly use:
Test code
#include <Wire.h>
#define SOLENOID_PIN 6
#define I2C_ADDRESS 9
#define BUTTON_PIN 3
#define BUTTON_2_PIN 4
#define BUTTON_3_PIN 5
#define BUTTON_4_PIN 6
bool switchHasToggled = false;
char lastButton = '0';
void setup() {
Wire.begin(I2C_ADDRESS); // join i2c bus with address defined in I2C_ADDRESS
Wire.onRequest(requestEvent); // register event
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(BUTTON_2_PIN, INPUT_PULLUP);
pinMode(BUTTON_3_PIN, INPUT_PULLUP);
pinMode(BUTTON_4_PIN, INPUT_PULLUP);
}
void loop() {
if (digitalRead(BUTTON_PIN) == LOW) {
switchHasToggled = true;
lastButton = '1';
} else if (digitalRead(BUTTON_2_PIN) == LOW) {
switchHasToggled = true;
lastButton = '2';
} else if (digitalRead(BUTTON_3_PIN) == LOW) {
switchHasToggled = true;
lastButton = '3';
} else if (digitalRead(BUTTON_4_PIN) == LOW) {
switchHasToggled = true;
lastButton = '4';
}
delay(100);
}
// function that executes whenever data is requested by master
// this function is registered as an event, see setup()
void requestEvent() {
if (switchHasToggled) {
Wire.write(lastButton);
switchHasToggled = false;
} else {
Wire.write('b');
}
//Wire.write("hello "); // respond with message of 6 bytes
// as expected by master
}
I had to test different numbers, if the board would even allow for certain pins, and if those pins were tied to something else. But after reprogramming it a couple of times (I probably would have saved time by just testing pin numbers one by one), I could determine that the correct pin number for the ATTiny412 hardware pin 7 in Arduino IDE is 4.
Now everything worked, and I just needed to lift everything from the breadboard and wire them properly.
Completed design¶
The main board.
The bumper board.
Final Files
Wrong pins
In the files, the pin order is different in the main PCB and the bumper PCB. The I2C clock pin and data pin are reversed.
I made some PCBs for the bumpers, and another for the main board. The main board just has two I2C connections (one should be enough), power and ground, plus seven unused pins connected to female jumper headers for future proofing. The main board pulls the I2C data and clock pins up to 3V by connecting the 3V pin to the I2C pins with a pair of 4K resistors.
The bumper PCB is controlled by an ATTiny412 microcontroller. It has two connectors for I2C, one “input” and one “output”, so that the I2C boards could be connected in a series. This is needed as there will be at least three bumpers in the pinball machine, and probably other machines that will use I2C to communicate, so sonnecting them to one bus makes a lot of sense. The bumper PCB also has connectors for the shortable copper plating, another connector for the relay that controls the solenoid. In order to program the ATTiny412, the PCB also has a UPDI pins.
Pin description | Connection name in PCB | ATTiny412 connected pins |
---|---|---|
I2C SDA (data) | I2C d | 4 |
I2C SCL (clock) | I2C c | 5 |
The main power in | I2C + | 1 (vcc) |
The main ground | I2C - | 8 (gnd) |
Ground for pulled up “button” | switch (1) | 8 (gnd) |
Input-pullup pin for “button” | switch (2) | 7 |
5V power for relay | relay + | 1 (vcc) |
Ground connector for relay | relay - | 8 (gnd) |
The signal pin for relay | relay d | 3 |
updi power | updi + | 1 (vcc) |
updi ground | updi - | 8 (gnd) |
updi signal | updi ~ | 6 (updi) |
I connected all the pins and made it work. The main board was connected to Serial Plotter in Arduino IDE, which shows whenever a bumper tells it is active through I2C.
The video shows a simple test, where I connected two bumpers to the main board with I2C, and the main board communicates with the Arduino IDE Serial Plotter to show when each bumper says it has been activated. In the video I “activate” the bumpers by manually shorting two wires that are connected to the button pins (or the ball copper shorting pins).