Communication between 2 microcontrollers via GPIOs

I would like to communicate between two microcontrollers (Attiny202/402 and a ESP32/ESP32S2/ESP32C3). The easiest way would be using a serial communication or something like i2C or similar. But they consume too mutch flashmemory so I am not able to flash them.

So I just thought why not do the communication via GPIO pins.

I decided to use 3 pins for that on each microcontroller (Clock-,Sender- and Receiverpin). To get a stable signal do I need to use Pullup/Pulldown resistors for that or how can I remove the floating state ( which you got on buttons, when you don“t use and 10k resistor)

If I understand right I need to connect the grounds of the controllers together so they have the same reference?(If I am wrong please correct me)

Sender code:


#include <Arduino.h>

#ifdef ESP32
#pragma message "using ESP32"
#define CLOCKPIN 1
#define SENDDATAPIN 42
#define RECEIVEDATAPIN 2
#else
#define CLOCKPIN 0
#define SENDDATAPIN 1
#define RECEIVEDATAPIN 2
#endif

#define SETTIME B001
#define SETALERT B010
#define DELETEALERT B011
#define GETALERT B100

//TODO build timeot when there is no answer
/**
 * @brief Send a message which is compressed in a 
 * 
 * @param message A message needs to contain following informations, so it can get processed:
 * (Data)   (Data)   (Data)    (Data)   (Type of Data) (More Data Bit)
 * XXXXXXXX XXXXXXXX XXXXXXXX  XXXX     TTT            D
 */               
bool sendMessage(uint32_t message)
{
     #ifdef DEBUGGING
    Serial.println("Sendmessage");
    #endif
    // Send transmission bytes
    digitalWrite(CLOCKPIN, 1);
    digitalWrite(SENDDATAPIN, 1);
    #ifdef DEBUGGING
    Serial.println(digitalRead(CLOCKPIN));
    Serial.println(digitalRead(SENDDATAPIN));
    Serial.println("Waiting for receiver");
    #endif
    // Wait for answer
    while (digitalRead(RECEIVEDATAPIN) == 0)
    {
        delay(10);
        #ifdef DEBUGGING
    Serial.print(".");

    #endif
    }
    // Sending answer
    digitalWrite(CLOCKPIN, 0);
    digitalWrite(SENDDATAPIN, 0);
    // // Wait alittle bit so the other side can get the answer
    delay(100);
    #ifdef DEBUGGING
    Serial.println("Transmission:");
    #endif

    uint32_t mask = 0x80000000;
    for (uint8_t i = 0; i < 32; i++)
    {
        digitalWrite(SENDDATAPIN, (message&mask)==0?0:1);
        #ifdef DEBUGGING
    Serial.print((message&mask)==0?"0":"1");
    Serial.println(digitalRead(SENDDATAPIN));
    #endif
        message <<= 1;
        digitalWrite(CLOCKPIN, 1);
        delay(100);
        digitalWrite(CLOCKPIN, 0);
        delay(100);
        
    }
    #ifdef DEBUGGING
    Serial.println();

    #endif
}


// Different messages
    uint32_t messages[4]
	{
		0xFFFFFFFF,
		0xAAAAAAAA,
		0x00000001,
		0x10000000
	};

/**
 * @brief Test the message send and receive methodes
 * Wire the pins accoring to the defined pins
 * There are differnet pins needed:
 * ClockIN ClockOut (Connect them together)
 * SendDataPin ReceiveDataPin (Connect them together)
 * 
 * @return * Wire 
 */

bool testSendMessage()
{
    
    sendMessage(messages[1]);
    // for(uint8_t index =0;index<sizeof(messages)/sizeof(uint32_t);index++)
    // {
        // sendMessage(messages[index]);
    // }
    return true;  
}

#include "Wire.h"

void setup()
{
        #ifdef DEBUGGING
    Serial.begin(115200);
    #endif

#ifdef DEBUGGING
    Serial.print("Pin:");
    Serial.println(RECEIVEDATAPIN);
    
    #endif


    delay(2000);
    pinMode(CLOCKPIN, OUTPUT);
    pinMode(SENDDATAPIN, OUTPUT);
    pinMode(RECEIVEDATAPIN, INPUT);
    digitalWrite(CLOCKPIN, LOW);
    digitalWrite(SENDDATAPIN, LOW);
    delay(2000);

}

void loop()
{
   testSendMessage();
   delay(5000);
}

Receivercode:


#include <Arduino.h>

#ifdef ESP32
#pragma message "using ESP32"
#define CLOCKPIN 1
#define SENDDATAPIN 2
#define RECEIVEDATAPIN 42
#define OUTPUTPIN 10
#else
#define CLOCKPIN 0
#define SENDDATAPIN 1
#define RECEIVEDATAPIN 2
#define OUTPUTPIN 3
#endif

#define SETTIME B001
#define SETALERT B010
#define DELETEALERT B011
#define GETALERT B100

// Different messages
uint32_t messages[4]{
	0xFFFFFFFF,
	0xAAAAAAAA,
	0x00000001,
	0x10000000};

uint32_t receiveMessage()
{
	uint32_t buffer;
	byte index = 0;
	// Check if transmission started
	if (digitalRead(CLOCKPIN) == 1 && digitalRead(RECEIVEDATAPIN) == 1)
	{
#ifdef DEBUGGING
		Serial.println("Incomming message");
		// Send answer
		Serial.println("Sending answer");
#endif
		digitalWrite(SENDDATAPIN, 1);

		Serial.println("Waiting for data ");

		// Wait For answer
		while (digitalRead(CLOCKPIN) != 0 && digitalRead(RECEIVEDATAPIN) != 0)
		{
#ifdef DEBUGGING
			Serial.print(".");
#endif
			delay(10);
		}
#ifdef DEBUGGING
		Serial.println();
		Serial.print("Data: ");
#endif
		bool newData = true;
		while (index < 32)
		{
			if (digitalRead(CLOCKPIN) == 1 && newData)
			{
				buffer <<= 1;
				buffer += digitalRead(RECEIVEDATAPIN);
#ifdef DEBUGGING
				Serial.print(digitalRead(RECEIVEDATAPIN));
#endif
				newData = false;
				index++;
			}
			else if (digitalRead(CLOCKPIN) == 0)
			{
				newData = true;
			}
		}
#ifdef DEBUGGING
		Serial.println();
#endif
		return buffer;
	}
	else
	{
		return 0;
	}
}
bool testReceiveMessage()
{
	uint8_t index = 1;
	// for (uint8_t index = 0; index < sizeof(messages) / sizeof(uint32_t); index++)
	// {
	uint32_t message = receiveMessage();
	while (message == 0)
	{
		message = receiveMessage();
	}
	if (messages[index] == message)
	{
#ifdef DEBUGGING
		Serial.print("Received message ");
		Serial.print(index);
		Serial.println("successful");
#endif
		digitalWrite(OUTPUTPIN, 1);
	}
	else
	{
#ifdef DEBUGGING
		Serial.print("Received message ");
		Serial.print(index);
		Serial.println(" failed.");
		Serial.print("Expected: ");
		Serial.println(messages[index], BIN);
		Serial.print("Received: ");
		Serial.print(message, BIN);
#endif
	}
	// }
	return true;
}

void setup()
{
#ifdef DEBUGGING
	Serial.begin(115200);
#endif

#ifdef DEBUGGING
	Serial.print("Pin:");
	Serial.println(RECEIVEDATAPIN);

#endif

	delay(2000);
	pinMode(CLOCKPIN, INPUT);
	pinMode(SENDDATAPIN, OUTPUT);
	pinMode(RECEIVEDATAPIN, INPUT);
	pinMode(OUTPUTPIN, OUTPUT);

	digitalWrite(SENDDATAPIN, LOW);
	digitalWrite(OUTPUTPIN, LOW);
	delay(2000);
}

void loop()
{
	testReceiveMessage();
}

Currently it does not work (not stable, receiver only get "1" but not "0". Maybe someone could help me or explain, how I can solve that?

Yes, a common ground connection is usually required.

But they consume too mutch flashmemory so I am not able to flash them.

Not true if the MCU has a UART or I2C hardware. You don't need to use the associated libraries, and it takes only a couple of lines of code to send a data byte via the hardware.

Your approach is to "bit bang" digital data transmission connections, which has been done many times for UART-type, I2C and SPI serial transfer. There are plenty of code examples to study. Example search phrase "bitbang I2C arduino".

how can I remove the floating state

Why do you think there is one?

It has an Unified Program and Debug Interface (UPDI) and IC2 arcorrding to the datasheet. I only saw software implementations so far which required to mutch flash memory.
I tryed the "bitbang I2C" lib which I got from the google search but that was the "heaviest" overflow so far (4959 Bytes) for the example for the lib. When I remove the Serial functions it gets down to 2729 bytes overflow, which is still very mutch .

By just using the wire lib (#include "Wire.h") it also took too mutch space.

Is there a more resource saving way?

Yes exactly but I need an implementation which is really basic, all the protocolls are to heavy. I think I need something which I can use without additional lib. (And these are mutch faster as my approach). The speed of the transmission is not really a problem, but it need to work reliable (use of Hamming-Code?).

Because I get none sense values and sometimes in receives messages while the sender does not send.
I forgot to mention that there is no constant messaging it happens maybe all 1min,5 min, 1 hour or 1 day depending on the use case (and the required Responsiveness)

Post links to the ones you have actually looked at, and rejected.

Serial "RS232" is probably the simplest, and takes about a dozen lines of code to transmit or receive a byte. It is extremely reliable for short connections.

Example:

void uart_tx(unsigned char val) {
    unsigned char i;
    Tx_Pin = 0;                         // Start bit
    uart_delay();
    for ( i = 0 ; i < 8 ; i++ ) {
        if (val & 0x01) Tx_Pin = 1;   // send LSB
        else            Tx_Pin = 0;
        val >>= 1;
        uart_delay();
        }
    Tx_Pin = 1;                         // Stop bit
    uart_delay();
}

If you are serious about using the ATtiny202 mentioned in the first post, then of course you would use the built in USART.

In that case it takes one line of code to send a byte of data.

If you code your own serial interface - here with a CLOCK, Rx and Tx signals - it is actually a "synchronous" interface (not like UART not using a clock).

It should work pretty flawlessly. Just to bear in mind:

  • every digital signal (on the receiver) side needs a "setup" and "hold" time.
    It means:
  • you "cannot" change the state of Tx when the clock is changing "at the same time". You need the Tx stable before ("setup") and still stable after ("hold") the CLOCK signal changes the state.

I recommend to implement this as a SDR interface (Single Data Rate):
it means: on one clock edge - the receiver will sample: let's assume on every rising edge, clock goes from 0 to 1 - the Rx will read the Rx GPIO value.
But you change the Tx GPIO on Tx side just with the other CLOCK edge: change and set the Tx when CLOCK toggles from 1 to 0.

This results in a setup and hold time of a half-clock cycle. One CLOCK edge is the sampling edge, the other one the data line change. So you make sure that the Rx will see a stable GPIO signal on Rx.

You have to clock your SW code in half-period steps:

  • when changing CLOCK from 1 to 0 - you update the Tx out
  • wait a half-period
  • CLOCK changes now from 0 to 1 - but do not touch the Tx line (it remains the same)
  • wait a half-period
  • and now you start over with the next bit

The Rx will use the CLOCK transition from 0 to 1 as the indication to read now the Rx line.
But the other edge on CLOCK, the 1 to 0, does not do anything on receiver side (it is "ignored" because it is used by the Tx to setup the data line again.

Just "shift" all by a half-clock period. I mean: toggle with half-clock period and just place the CLOCK edges and the data line edges on different half-periods.

BTW: such a "synchronous" interface is much better as an UART: the Tx decides how fast - the Rx will follow. And the CLOCK can be varying, the Rx still gets all data correct.
It is based on a sampling clock edge, e.g. 0 to 1, and a Tx setup clock edge (0 to 1). You can have a very wide range of CLOCK speed working, even very fast. Just when the clock becomes so fast that Rx sees the clock edge, but Tx changes again (with next half-period) - it becomes too fast.

Just a tiny delicate issue: one MCU generates the CLOCK. But the other side wants to transmit (Tx back). This MCU for Tx on other side has to watch out now for both clock edges: change Tx on 1 to 0 and keep it constant for 0 to 1. It means: the Tx on other side has to see the non-sampling edge, the 1 to 0 transition, to change the Tx line. But the Rx is using the 0 to 1 transition.

Do not try DDR (Double Data Rate): on every clock edge is now a change of the Tx and Rx: this is very timing sensitive and potentially never working as SW implementation.

Just consider the timing relation of Rx and Tx in relation to the CLOCK: have a stable sampling edge and have the other edge to update the data line. This makes sure to have a proper "setup" and "hold" time.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.