Week 11

★ ★ ★ Networking and Communications ★ ★ ★

This week at Fab Academy, we dived into how to connect multiple microcontrollers so they can work together. Either through wired or wireless networks. This is a key step in designing more complex, distributed, and modular systems.


## ꩜꩜꩜ Making Two Attiny412 Have a Conversation ꩜꩜꩜

Let's make an Attiny412 have a friend to talk to.

Since it's tiny I created a board to surface-mount it and place it over the breadboard:

Block Diagram Pin Layout Pin Definition


Because I knew this was not going to be easy, the goal was to freely try the connections without the compromise of re-doing a full board.

The idea is simple; connect two Attiny412 via I2C, one would be the master who would tell the other, the slave.

Master would have a tilt switch and some leds.

Tilt-Switch Leds

The slave on the other hand, would be wired to a DFPlayer Mini, which I had tried during last week.

Tilt-Switch

When not tilted ---> DFPlayer Mini plays song until done.

When tilted -------> DFPlayer would stops and the leds turn on.

So before going deep into I2C, I thougth it would be better to try the connection of the components within one board. And since Attiny412 doesn't have UART communication, for the purpose of possible debugging, I tried the code in the Barduino.

With ChatGPT's help we got it working.

#include <SoftwareSerial.h>
#include <DFRobotDFPlayerMini.h>

#define TILT_PIN 3      // Tilt switch is connected to PA3 (pin 3)
#define LED_PIN 7       // LED is connected to PA7 (pin 7)
#define DFPLAYER_RX 1   // DFPlayer TX goes to this pin (PA1)
#define DFPLAYER_TX 2   // DFPlayer RX goes to this pin (PA2)

SoftwareSerial dfSerial(DFPLAYER_RX, DFPLAYER_TX);  // RX, TX for software serial
DFRobotDFPlayerMini dfplayer;

bool playing = false;   // Keeps track of whether audio is currently playing

void setup() {
  pinMode(TILT_PIN, INPUT_PULLUP);  // Set tilt pin as input with internal pull-up resistor
  pinMode(LED_PIN, OUTPUT);         // Set LED pin as output

  dfSerial.begin(9600);             // Start SoftwareSerial at 9600 baud
  if (!dfplayer.begin(dfSerial)) {  // Try to connect to DFPlayer
    // If connection fails, stop here (optional)
    while (true);
  }

  dfplayer.volume(20); // Set initial volume (0 to 30)
}

void loop() {
  bool tilted = digitalRead(TILT_PIN) == LOW;  // Check if tilt switch is tilted

  if (tilted && !playing) {
    digitalWrite(LED_PIN, LOW);    // Turn off LED
    dfplayer.play(1);              // Start playing track 001.mp3
    playing = true;                // Remember that it's playing
  } 
  else if (!tilted && playing) {
    digitalWrite(LED_PIN, HIGH);   // Turn on LED
    dfplayer.stop();               // Stop playback
    playing = false;               // Remember that it's not playing
  }

  delay(200);  // Wait a bit to avoid spamming the DFPlayer or reacting to noise
}
Prompt was: "Can you help me write a sketch for an ATtiny412 that:

  • Uses a tilt switch on pin PA3

  • Turns an LED on/off on PA7 based on the tilt state

  • Plays or stops audio using a DFPlayer Mini

  • Connects the DFPlayer via SoftwareSerial on PA1 (RX) and PA2 (TX)

  • Makes sure the audio only plays once per tilt and stops if tilt returns"


Understanding the code

I tried to understand the code from scratch to truly internalize it.

First I declare the variables.

Then this part was a little confusing. DFRobotDFPlayerMini dfplayer;

Aparently, this line creates an object named dfplayer from the DFRobotDFPlayerMini class.

Other examples of "SomeLibraryClass nameYouChoose;" that you might recognize:

SoftwareSerial mySerial; Adafruit_NeoPixel strip; Servo myServo;

dfplayer.begin(...) is a function. That means it requires some input (called "arguments") in parentheses, just like when you write:

Serial.begin(9600);

It's telling Serial what speed to use. So, for the DFPlayer:

dfplayer.begin(dfSerial);

This is telling the DFPlayer object: "Start using this Serial connection to talk to the actual DFPlayer module."

Why dfSerial? Earlier in the code (or at least in most DFPlayer setups), you create a serial connection like this:

SoftwareSerial dfSerial(0, 1); // Or whatever pins you're using

So dfSerial is the software serial connection between your microcontroller (Attiny or Barduino) and the DFPlayer board. That’s how they talk.

When the library function .begin() runs, it needs to know what connection to use, so we give it dfSerial.

Functions use parentheses to take input arguments.

dfplayer.begin(...) expects a serial connection as input.

You pass it dfSerial, because that’s the connection you made to the DFPlayer.

**You learn what to pass in by reading the library documentation or examples.**

while (true); is an infinite loop that does nothing. It’s a clean way to halt a program if something goes wrong, like a hardware failure. It's the same as writing while (true){}

bool playing = false;

I'm creating a variable named playing that will keep track of whether music is currently playing. Right now, I’m saying it’s not playing. It's a flag that can be fliped later in the code and I can use it to decide when to start or stop the DFPlayer.

It helps prevent triggering the same action over and over again when the switch doesn’t change.

Hey! I didn't show you how it worked! Look ;)

First Code Working

Two Attiny412 Blinking At Each Other

For the purpose of this program's schedule, I’ve decided to keep things simple: I'll just connect two Attiny(s) and leave the DFPlayer and tilt switch for another time.

Simplicity is part of the learning process. Especially when you're working under tight deadlines... hehe.

After a lot of effort and debugging, we finally got the master to successfully command the slave to blink an LED.

I lost quite a bit of time because I didn’t realize that the SDA and SCL lines needed pull-up resistors (10kΩ). We tried everything, checking solder joints with a multimeter, inspecting signals on the oscilloscope, and even using a logic analyzer that showed the following readings of the SDA and SCL while ON.

Surgery

And this was OFF:

Surgery

The whole setup:

Surgery

But once we figured that out and uploaded this code to the master.


#include 

void setup() {
  Wire.begin();
  delay(1000);

  // Step 1: Send 0xAA to prepare slave
  Wire.beginTransmission(0x10);
  Wire.write(0xAA);
  Wire.endTransmission();
  delay(10);

}

void loop() {
  // Nothing here
  // Step 2: Send 0x01 to turn on LED
  Wire.beginTransmission(0x10);
  Wire.write(0x01);
  Wire.endTransmission();

  delay(100);


    // Nothing here
  // Step 2: Send 0x01 to turn on LED
  Wire.beginTransmission(0x10);
  Wire.write(0x02);
  Wire.endTransmission();

  delay(100);
}

And this code to the slave.


#include 

const int ledPin = 1;  // LED pin, make sure to connect an LED here for debugging

void setup() {
  Wire.begin(0x10);  // Set slave address to 0x10
  Wire.onReceive(receiveEvent);  // Register the event handler for I2C receive

  pinMode(ledPin, OUTPUT);  // Set LED pin as output
}

void loop() {

  // Nothing needed here, we just handle I2C data in the background
}

void receiveEvent(int numBytes) {

  while (Wire.available()) {
    byte command = Wire.read();  // Read the received byte

    if (command == 0x01) {
      // Turn on the LED for debugging
      digitalWrite(ledPin, HIGH);
    } else {
      // If another command is received, turn off the LED
      digitalWrite(ledPin, LOW);
    }
  }
}

It finally worked!

Working Communication

Yipiii. May not be rocket science, but I've learned A LOT during the process and now I can confidently use the oscilloscope and the rest of the components I've experimented with. I also feel very comfortable with the Attiny412 and incorporating it into the rest of my projects. But for the moment, I'll leave them to rest and chat about their toxic master/slave relationship.

Let's give them space.

See you next week.