15

Does anyone know how to setup a Raspberry Pi 3 as an I2C slave (in C++)?

Before someone answers saying that it's not possible, it is possible.

Here is a link that says it is possible (pigpio documentation)

This is the code I'm trying to use (from the link above):

#include <pigpio.h>
#include <iostream>
using namespace std;


int main(){
    bsc_xfer_t xfer;
    gpioInitialise();
    xfer.control = (0x0A<<16) | 0x305; // Set I2C slave Address to 0x0A
    int status = bscXfer(&xfer);
    if (status >= 0)
    {
        xfer.rxCnt = 0;
        while(1){
            if (xfer.rxCnt > 0){
                cout << xfer.rxBuf;
            }
       }    
    }

    return 1;
} 

My connections are SCL to BCM GPIO 18 and SDA to BCM GPIO 19. (This should be the other way round: SDA is GPIO 18, SCL is GPIO 19)

The rest of the devices on the bus are ok (already tested).

I also have connected the SDA1 and SCL1 of the raspberry to this I2C bus so that I can do i2cdetect -y 1 to list I2C devices on the bus.

The problem with this code is that despite correctly Addressing the Raspberry Pi, i2cdetect -y 1 is able to see address 0x0A, I can not receive any message.

If I run a sniffer in other process I'm able to get the message correctly.

2 Answers 2

7

This is almost identical to your previous question. You probably should have edited that rather than asking a new question.

You need the bscXfer to be within the while loop. That is how the xfer structure is updated with new information.

8
  • Thanks for the answer, it worked! (I don't have enough reputation to make a +1 on your answer)
    – Sebastião
    Commented Dec 6, 2017 at 12:20
  • @Sebastião It is good to know it worked. I usually only find out when something doesn't work :(
    – joan
    Commented Dec 6, 2017 at 12:25
  • Also is worth mention that we should clear the buffer after reading it because otherwise it have trash from older messages. memset( xfer.rxBuf, '\0', sizeof(char)*BSC_FIFO_SIZE ); will do it
    – Sebastião
    Commented Dec 6, 2017 at 12:32
  • Do you know how to set the I2C bus speed and witch is the default and maximum speed?
    – Sebastião
    Commented Dec 6, 2017 at 12:46
  • The default speed is 100kbps. I think the maximum speed is 3.8Mps. See (from memory) /boot/overlays/README for details.
    – joan
    Commented Dec 6, 2017 at 12:58
16

Since this topic is very poorly covered and Sebastião's snippet and helped me to solve this problem I want add a complete solution on how to setup a RaspberryPi right here (tested on a RPi 3 and Zero W)!

Setting up a working slave:

Preparations

Be sure to have commented out this line in your /boot/config.txt:

dtparam=i2c_arm=on

Dependencies

Next, install g++ and pigpio using this command:

sudo apt install g++ pigpio

Pins

As stated in the documentation, the pins are GPIO 18 (SDA) and 19 (SCL). This site helps locating them on your RaspberryPi (layout should be the same for RaspberryPi 2, 3, Zero and Zero W)

This schematic from the site will help: RaspberryPi GPIO Schematic

Software

Explanation

As already, said this solution is based on Sebastião's code snippet. I modified it with the help of joan's solution.

I also tried to make sense of the code using the documentation for the function bscXfer.

In the source code, the data in the bsc_xfer_t struct are used add or receive messages but those are only applied when bscXfer is executed with the address to the struct (as joan pointed out in his solution).

The bsc_xfer_t.control integer has a fairly special role which states multiple things like slave I²C address, and various other states that are well documented in the documentation.

To understand this better, I copied the important parts of the documentation into the source code and changed some things or outsourced them into separate functions.

Source code

The address can be changed to whatever you want (as long as it is not above 127 (aka 7F(16) or 1111111(2)).

Since I'm not good at C++, you have to comment out, what you want you wan't to do. It is recommended to run the closeSlave function upon finishing your testing.

Here the slaveTest.cpp file:

#include <pigpio.h>
#include <iostream>

using namespace std;

void runSlave();
void closeSlave();
int getControlBits(int, bool);

const int slaveAddress = 0x03; // <-- Your address of choice
bsc_xfer_t xfer; // Struct to control data flow

int main(){
    // Chose one of those two lines (comment the other out):
    runSlave();
    //closeSlave();

    return 0;
}

void runSlave() {
    gpioInitialise();
    cout << "Initialized GPIOs\n";
    // Close old device (if any)
    xfer.control = getControlBits(slaveAddress, false); // To avoid conflicts when restarting
    bscXfer(&xfer);
    // Set I2C slave Address to 0x0A
    xfer.control = getControlBits(slaveAddress, true);
    int status = bscXfer(&xfer); // Should now be visible in I2C-Scanners
    
    if (status >= 0)
    {
        cout << "Opened slave\n";
        xfer.rxCnt = 0;
        while(1){
            bscXfer(&xfer);
            if(xfer.rxCnt > 0) {
                cout << "Received " << xfer.rxCnt << " bytes: ";
                for(int i = 0; i < xfer.rxCnt; i++)
                    cout << xfer.rxBuf[i];
                cout << "\n";
            }
            //if (xfer.rxCnt > 0){
            //    cout << xfer.rxBuf;
            //}
    }
    }else
        cout << "Failed to open slave!!!\n";
}

void closeSlave() {
    gpioInitialise();
    cout << "Initialized GPIOs\n";

    xfer.control = getControlBits(slaveAddress, false);
    bscXfer(&xfer);
    cout << "Closed slave.\n";

    gpioTerminate();
    cout << "Terminated GPIOs.\n";
}


int getControlBits(int address /* max 127 */, bool open) {
    /*
    Excerpt from http://abyz.me.uk/rpi/pigpio/cif.html#bscXfer regarding the control bits:

    22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
    a  a  a  a  a  a  a  -  -  IT HC TF IR RE TE BK EC ES PL PH I2 SP EN

    Bits 0-13 are copied unchanged to the BSC CR register. See pages 163-165 of the Broadcom 
    peripherals document for full details. 

    aaaaaaa defines the I2C slave address (only relevant in I2C mode)
    IT  invert transmit status flags
    HC  enable host control
    TF  enable test FIFO
    IR  invert receive status flags
    RE  enable receive
    TE  enable transmit
    BK  abort operation and clear FIFOs
    EC  send control register as first I2C byte
    ES  send status register as first I2C byte
    PL  set SPI polarity high
    PH  set SPI phase high
    I2  enable I2C mode
    SP  enable SPI mode
    EN  enable BSC peripheral
    */

    // Flags like this: 0b/*IT:*/0/*HC:*/0/*TF:*/0/*IR:*/0/*RE:*/0/*TE:*/0/*BK:*/0/*EC:*/0/*ES:*/0/*PL:*/0/*PH:*/0/*I2:*/0/*SP:*/0/*EN:*/0;

    int flags;
    if(open)
        flags = /*RE:*/ (1 << 9) | /*TE:*/ (1 << 8) | /*I2:*/ (1 << 2) | /*EN:*/ (1 << 0);
    else // Close/Abort
        flags = /*BK:*/ (1 << 7) | /*I2:*/ (0 << 2) | /*EN:*/ (0 << 0);

    return (address << 16 /*= to the start of significant bits*/) | flags;
}

Note that in some cases, you want the first byte to be the command byte rather than part of your general data.

EDIT: Also note, that while this works fine for testing purposes, @crasic pointed out (first comment), that there is a better (but also poorly documented) way of doing it with events rather than using an endless loop. That should be better when used with multiple applications.

Compile & Execute

You can compile this with

g++ slaveTest.cpp -lpthread -lpigpio -o slaveTest

and execute with

sudo ./slaveTest

Test with a master

To shortly test it a master, a popular option is to use smbus, which is far easier and can be found by simply searching with an search engine of your choice.

My chosen option in short:

  • Look up the SDA, SCL pins (they differ as a master!)
  • Run sudo apt install python3 python3-smbus
  • Copy the snippet below to as e.g. masterI2C.py onto your RPi
  • Open a python shell with this snippet using python3 -i masterI2C.py
  • Run sendData(0x03, 'Hello World of I2C!') to send data

Master python snippet:

import smbus

bus = smbus.SMBus(1)

def sendData(slaveAddress, data):
    intsOfData = list(map(ord, data))
    bus.write_i2c_block_data(slaveAddress, intsOfData[0], intsOfData[1:])

Image of testing this:

Testing it myself


I hope, that I could clarify this topic for other people.

(When experiencing sudden problems, restarting my slave raspberry pi usually helped me.)

4
  • 3
    Great Answer. Two suggestions for academic completeness. Using a while loop is OK for low computation load and slow communications, and replicates what one would do on many simple microcontrollers, but a better way is to be event driven by using an interrupt on the TxStatus. This is poorly documented on BCM, but is available. #2 Just as poorly documented, is to use DMA. If you are interested in developing a complete I2C Slave solution that plays nice with other applications on the linux system, that would be the next place to start digging.
    – crasic
    Commented Nov 30, 2018 at 23:11
  • 1
    @crasic That is nice to hear. Since I'm not into C++ (right now), I don't really know how to do that. But I'll leave a note in the solution for other people who know how to do it properly.
    – LinusCDE
    Commented Dec 5, 2018 at 18:43
  • Because pigpio says it supports all raspi models, I tried this on Raspi B rev 2. It doesn't work because GPIO 19 is not exposed as a pin on the board even if I think it exists in the CPU. pigpio docs imply GPIO 18 and 19 can't be changed because they are linked to a hardware I2C slave circuit in the CPU. GPIO 18/19 are available on Model A+, B+, Pi Zero, Pi Zero W, Pi2B, Pi3B, Pi4B but the docs for bsc_xfr() say you need to change to GPIO 10/11 on Pi4B because it uses a different MCU: BCM2711. Commented Nov 17, 2020 at 23:08
  • After calling gpioTerminate(), i2cdetect -y 1 still reports the slave as active, thus potentially confusing an I2C_Master device. Is there a way of totally terminating the I2C_Slave mode?
    – gone
    Commented Jun 24 at 10:23

Not the answer you're looking for? Browse other questions tagged or ask your own question.