Using both i2c busses on a Raspberry Pi Pico2

I'm working on a fairly complex project, a peripheral used to communicate analog values to and from the plugdata patching language. It uses both i2c busses (i2c0 and i2c1) on the Raspberry Pi Pico2 board to communicate with 8x i2c MCP4725 DACs. Channels 1-4 are on bus0, channels 5-8 are on bus1.

i2c0 is acting as you'd expect, with values coming through, generating a voltage and lighting lights. i2c1, though, seems to not be communicating. The odds of an electronic error here are pretty small (the circuit is built with KiCAD and manufactured at JLCPCB), so I think it's something about the way I'm trying to use the second bus.

I'll post the whole code at the bottom, but here are the parts that call i2c:

Array of DACs:

Adafruit_MCP4725 DAC[numOutputChannels];                                                        // All DACs
const byte DACAddress[numOutputChannels] = { 0x60, 0x61, 0x62, 0x63, 0x60, 0x61, 0x62, 0x63 };  // Their respective (duplicate) i2c addresses
const byte DACi2cBus[numOutputChannels] = { 0, 0, 0, 0, 1, 1, 1, 1 };                           // Which bus each DAC is on respectively

Then, inside setup():

void setup() {

  ...

  Wire.setSDA(0);  // Data line for DACs 0-3
  Wire.setSCL(1);  // Serial clock line for DACs 0-3
  Wire.begin();

  Wire1.setSDA(10);  // Data line for DACs 4-7
  Wire1.setSCL(11);  // Serial clock line for DACs 4-7
  Wire1.begin();

  // begin all DACs on both i2c busses
  for (byte thisDAC = 0; thisDAC < numOutputChannels; thisDAC++) {
    if (DACi2cBus[thisDAC] == 0) {
      DAC[thisDAC].begin(DACAddress[thisDAC]);          // begin each DAC on i2c bus 0
    } else {                                            //
      DAC[thisDAC].begin(DACAddress[thisDAC], &Wire1);  // begin each DAC on i2c bus 1
    }
  }

Then, in the loop:

void loop() {
  lastTime = micros() - lastTime;  // Time for the whole loop. Use for diagnostics if you gotta.

  // Receive Serial values into array
  receiveDACs();

...

  // Update DAC values
  updateAllDACs();
  // Update all servo DAC values
  updateAllServos();

...
}

These are the functions being called:

void receiveDACs() {
  for (byte thisDACChannel = 0; thisDACChannel < numOutputChannels; thisDACChannel++) {
    DACSamplesFromEasel[thisDACChannel] = Serial.parseInt();
  }
}

// Scroll through all DACs to update them with data from the Easel
void updateAllDACs() {
  for (byte thisi2cDACChannel = 0; thisi2cDACChannel < numOutputChannels; thisi2cDACChannel++) {  // Write to each DAC
    DAC[thisi2cDACChannel].setVoltage(DACSamplesFromEasel[thisi2cDACChannel], false, 400000);
  }
}

The full code, in case I'm missing something

// Oblique Palette
// Communication of values in ASCII between the Easel and the Palette.
// The Palette (this firmware) speaks with the Easel abstraction patch in plugdata, running on a separate GP computer.
// It announces its presence with its name, version number, and hardware configuration.
// The Easel responds with output values whenever it gets DAC values from the Palette.
// The Palette responds back ADC values, (later, followed by the measured time between communications, to the Easel)
// The Palette updates all DACs
// ( The Palette adjusts its sample rate according to the round trip time)
// ( The Easel adjusts it sample rate according to the round trip time)

// #include <Adafruit_TinyUSB.h>// High efficiency USB serial communication
#include <Arduino.h>           // Implicit in the Arduino IDE
#include <Mux.h>               // Multiplexer for analog inputs
#include <Servo.h>             // For servo-range PWM controls
#include <Wire.h>              // i2c communication for DACs
#include <Adafruit_MCP4725.h>  // i2c DACs
#include <pico/stdlib.h>       // To access Pico-specific libraries
#include <hardware/adc.h>      // Direct ADC read access for high speed reads
#include <hardware/gpio.h>     // Direct GPIO access

using namespace admux;  // for ADC multiplexer


/**********************************
Hardware configuration of the Palette
***********************************/
const char modelName[] = "Palette";       // Model name
const char firmwareVersion[] = "v0.5.3";  // Version number of this firmware
const int DACBitDepth = 12;               // Output/DAC resolution
const int numOutputChannels = 8;          // Number of output channels on the Palette
const int ADCBitDepth = 12;               // Input/ADC resolution
const int numInputChannels = 8;           // Number of input channels on the Palette
char specsheet[32];                       // Buffer to carry the specsheet string to the Easel

const int numSamplesPerChannel = 1;  // How many samples to pack for transmission (for increasing sample rate later)

const int maxDACValue = (pow(2, DACBitDepth) - 1);  // The maximum DAC value
const int maxADCValue = (pow(2, ADCBitDepth) - 1);  // The maximum ADC value

const int numValuesFromEaselPerPacket = numOutputChannels * numSamplesPerChannel;
const int numValuesToEaselPerPacket = numInputChannels * numSamplesPerChannel;

const byte numi2cBusses = 2;  // Using i2c busses 0 and 1

/*********************************
Analytics
*********************************/
long lastTime;                          // For holding the time to measure the amount of time processing takes
const int communicationTimeout = 1000;  // How long to wait to hear back
long communicationTimeoutStart;         // If communication goes down, start a timer
bool ledState = 0;

/*********************************
ADCs and DACs
*********************************/
#define ADCPin 34                                                     // The µC ADC pin
Mux ADCMux(Pin(ADCPin, INPUT, PinType::Analog), Pinset(21, 20, 17));  // For addressing ADC channels with a CD4051 multiplexer

Adafruit_MCP4725 DAC[numOutputChannels];                                                        // All DACs
const byte DACAddress[numOutputChannels] = { 0x60, 0x61, 0x62, 0x63, 0x60, 0x61, 0x62, 0x63 };  // Their respective (duplicate) i2c addresses
const byte DACi2cBus[numOutputChannels] = { 0, 0, 0, 0, 1, 1, 1, 1 };                           // Which bus each DAC is on respectively
                                                                                                //
byte servoPin[numOutputChannels] = { 2, 3, 4, 5, 6, 7, 8, 9 };                                  // Servo pins mirror the output of their associated DAC channel
Servo servoChannel[numOutputChannels];                                                          // All output servos

/********************************************************************
Serial buffer and the values to parse between Easel and Palette & back
********************************************************************/
int DACSamplesFromEasel[numValuesFromEaselPerPacket] = { 511, 1023, 1535, 2047, 2559, 3071, 3583, 4095 };  // The buffer of values received from the Easel
int ADCSamplesToEasel[numValuesToEaselPerPacket];                                                          // The values to send to the Easel



/****************************************************************************************************************************************
Main code
****************************************************************************************************************************************/

void setup() {

  analogReadResolution(ADCBitDepth);  // Use the full bit depth of the ADCs

  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(ADCPin, INPUT);

  digitalWrite(LED_BUILTIN, !ledState);  // Switch whenever something happens

  Serial.begin(12000000);                // Baud setting is ignored, as it's handled by USB peripherals & Easel/OS
  digitalWrite(LED_BUILTIN, !ledState);  // Switch whenever something happens

  Wire.setSDA(0);  // Data line for DACs 0-3
  Wire.setSCL(1);  // Serial clock line for DACs 0-3
  Wire.begin();

  Wire1.setSDA(10);  // Data line for DACs 4-7
  Wire1.setSCL(11);  // Serial clock line for DACs 4-7
  Wire1.begin();

  // begin all DACs on both i2c busses
  for (byte thisDAC = 0; thisDAC < numOutputChannels; thisDAC++) {
    if (DACi2cBus[thisDAC] == 0) {
      DAC[thisDAC].begin(DACAddress[thisDAC]);          // begin each DAC on i2c bus 0
    } else {                                            //
      DAC[thisDAC].begin(DACAddress[thisDAC], &Wire1);  // begin each DAC on i2c bus 1
    }
  }
  testBlink();

  for (byte servoToSetup = 0; servoToSetup < numOutputChannels; servoToSetup++) {  // Make a servo object for each DAC channel
    servoChannel[servoToSetup].attach(servoPin[servoToSetup]);                     // Attach each servo to its pin
  }
  digitalWrite(LED_BUILTIN, HIGH);  // Switch whenever something happens

  while (!findEasel())  // Look for the Easel on boot until the Palette gets back real values
    ;
}


/******************************************** LOOP ********************************************/
void loop() {
  lastTime = micros() - lastTime;  // Time for the whole loop. Use for diagnostics if you gotta.

  // Receive Serial values into array
  receiveDACs();

  // Read ADC values into array
  readAllADCs();
  // Send ADC values
  sendADCToEasel();

  // Update DAC values
  updateAllDACs();
  // Update all servo DAC values
  updateAllServos();

  testBlink();
}
/******************************************************************************************
Functions
******************************************************************************************/

/*************************** DAC/output functions ***************************/

// Receive all DAC values from the Easel
void receiveDACs() {
  for (byte thisDACChannel = 0; thisDACChannel < numOutputChannels; thisDACChannel++) {
    DACSamplesFromEasel[thisDACChannel] = Serial.parseInt();
  }
}

// Scroll through all DACs to update them with data from the Easel
void updateAllDACs() {
  for (byte thisi2cDACChannel = 0; thisi2cDACChannel < numOutputChannels; thisi2cDACChannel++) {  // Write to each DAC
    DAC[thisi2cDACChannel].setVoltage(DACSamplesFromEasel[thisi2cDACChannel], false, 400000);
  }
}

// Map analog values to servo outputs
void updateAllServos() {
  for (byte thisServoToUpdate = 0; thisServoToUpdate < numOutputChannels; thisServoToUpdate++) {
    servoChannel[thisServoToUpdate].writeMicroseconds(map(DACSamplesFromEasel[thisServoToUpdate], 0, maxDACValue, 2000, 1000));  // Map the DAC value to the servos within their range
  }
}


/*************************** ADC/input functions ***************************/
void readAllADCs() {
  for (byte thisADCChannel = 0; thisADCChannel < numInputChannels; thisADCChannel++) {
    ADCSamplesToEasel[thisADCChannel] = ADCMux.read(thisADCChannel);  // use regular ol' analogRead() max sample rate: ~ 177Hz

    // ADCSamplesToEasel[thisADCChannel] = (thisADCChannel * 512) - 1; // Test signal bypassing analogRead()

    // if (!adc_fifo_is_empty()) { // Look at the ADC register, and if it's got anything in it, read it
    //   ADCSamplesToEasel[thisADCChannel] = adc_fifo_get(); //
    // }
  }
}

void sendADCToEasel() {
  for (byte thisADCChannel = 0; thisADCChannel < numInputChannels; thisADCChannel++) {
    Serial.print(ADCSamplesToEasel[thisADCChannel]);
    Serial.write(32); // Space delineated values
  }
  Serial.write(13);  // Carriage return to end the transmission
}

// Insert this f() before sending to the Easel to just repeat back input channels so we can make sure the Easel is sending/Palette is receiving.
void testRepeater() {
  for (int thisChannelToRepeat = 1; thisChannelToRepeat < numInputChannels; thisChannelToRepeat++) {
    ADCSamplesToEasel[thisChannelToRepeat] = DACSamplesFromEasel[thisChannelToRepeat];
  }
  ADCSamplesToEasel[0] = lastTime / 100;
}


// Call out for the Easel and blink the onboard LED if it's not found yet
bool findEasel() {
  if (Serial.available()) {  //If the Palette is receiving data from the Easel, start talking!
    digitalWrite(LED_BUILTIN, HIGH);
    communicationTimeoutStart = micros();
    return (1);

  } else {  // When the Palette hasn't heard from the Easel yet, ping out with identifying information
    sprintf(specsheet, "%s %s %u %u %u %u", modelName, firmwareVersion, numOutputChannels, DACBitDepth, numInputChannels, ADCBitDepth);
    Serial.println(specsheet);

    // Error code/waiting blinky for not hearing back from the Easel is ._ repeating
    digitalWrite(LED_BUILTIN, LOW);
    delay(100);
    digitalWrite(LED_BUILTIN, HIGH);
    delay(100);
    digitalWrite(LED_BUILTIN, LOW);
    delay(100);
    digitalWrite(LED_BUILTIN, HIGH);
    delay(200);

    return (0);
  }
}

void testBlink() {
  digitalWrite(LED_BUILTIN, !ledState);  // Switch whenever something happens
}

Try adding:

#define PIN_WIRE1_SDA  (10u)
#define PIN_WIRE1_SCL  (11u)

before:

#include <Wire.h>

1 Like

This much seems reasonable after a quick test with an SSD1306 on Wire and another on Wire1.

  Wire.setSDA(0);  // Data line for DACs 0-3
  Wire.setSCL(1);  // Serial clock line for DACs 0-3
  Wire.begin();

  Wire1.setSDA(10);  // Data line for DACs 4-7
  Wire1.setSCL(11);  // Serial clock line for DACs 4-7
  Wire1.begin();

It might be worthwhile trying to use just 1 DAC on each of Wire and Wire1. My eyebrows shot up when I saw that the Adafruit_I2CDevice.begin call does a device detect each time it's called; which will be 4 times per I2C interface in your sketch. Why that should make a difference only on Wire1 is not immediately obvious, but the results of reducing that to once per interface would be interesting.

Tried it to see, but no dice. But also, I'm using both busses, which means that I need to be able to define 2x SDA and 2x SCL pins, which I've already done with the Wire and Wire1 methods.

Hhhhuh... Performance is an issue (my same rate is just over 100Hz, which is pretty crappy) and I wonder if that's a factor. There are other libraries for these DACs and maybe I should look into them.

That said, what I'm doing should work. I don't see where I'm making a mistake.

(I should note that this isn't the only problem — I'm just concentrating on one at a time. My analog multiplexer isn't multiplexing, so all channels are currently duplicates of in1. But one thing at a time.)

My completely unsubstantiated hunch at this point is that you aren't doing anything wrong per se. You're just doing something one of the libraries isn't set up handle properly. But it's just a hunch. That's why I suggested trying 1 DAC on each bus and see if the second bus suddenly springs to life.

Oh, I see. Interesting. I thought you suggesting I change the spec of the project!

That should be pretty easy, actually, because of the way those arrays of DACs work.

I’ll give it a shot!

No spec changes; just debugging a simpler case to see if you can get that to work and then gradually adding stuff back in to see when it breaks.

1 Like

Uuuhhh oh.

I just realized that I'm having a weird IDE problem.

Says it's flashed, but nothing has changed. Tried it with a fresh µC to no avail. So that might explain why nothing I'm doing is fixing anything. Gonna scrub the IDE and try again.

Edit: OK, well, had to redownload Earlephilhower's RP2040 library, so now it's actually uploading, but sadly that hasn't fixed anything. In fact, the ADCs have stopped working.

:roll_eyes:

I have a feeling the ADC problem is simple (since fixing the code broke it, and now there are at least different values for different channels, even if they aren't actually listening to the pots).

So, now I can try just using one DAC per bus and see what happens.

The answer might be that I say "fuck this" and go to bed.

OK, that was instructive. I used the same address on bus0 and bus1, and both signals went to bus0. So, somehow Wire1 isn't working.

(And, along the way, so have the 8 ADC channels)

So, that's enough destroying my hard work for the evening!

Edit: Well, now 2 ADC channels are working. Which is a weird number because it means the mux is muxing, but only the first bit. It is definitely bedtime.

Progress. When you feel like breaking some more stuff, it might be interesting to look at the return value from Adafruit_MCP4725::begin.

OK, I'll check in on that in the morning!

It seems like wasting cycles opening and closing all the DACs, when performance is an issue, might be a problem. I really don't want to write my own DAC library.

Something to keep in mind is that the general consensus is that the Adafruit libraries are... not exactly optimized. Others would put that more bluntly. Much more bluntly in some cases. :slight_smile: "Everything AND the kitchen sink." "Bloatware!" I think, trying to be fair, that their libraries are written for the general case. An RV rather than a sports car.

But sometimes, you really need a sports car. And not an RV with a kitchen sink.

Given the simplicity of the Adafruit_MCP4725 class (there's only two methods), writing your own library wouldn't be a huge deal. And it would get rid of the overhead of having the Adafruit_I2CDevice class in there as well.

I'll be interested to hear if you get any false returns from the begin method in the morning.

Tells the library to use pins 10 + 11 for Wire1 and should also configure those pins for I2C.

is supposed to configure the Pico to provide the second I2C bus on GPIO 10 + 11 instead of the default pins, which I believe are GPIO 26 & 27, but I suppose is another way of accomplishing the same thing.

As an experiment what happens of you connect the second bus to GPIO 26+27 and configure Wire1.setSDA and Wire1.setSCL to use the same?

Assumption based on this in pins_Arduino_h file in the Variant/rpipico directory of the board package:

// Wire
#define PIN_WIRE0_SDA  (4u)
#define PIN_WIRE0_SCL  (5u)

#define PIN_WIRE1_SDA  (26u)
#define PIN_WIRE1_SCL  (27u)

#define SERIAL_HOWMANY (3u)
#define SPI_HOWMANY    (2u)
#define WIRE_HOWMANY   (2u)

Weirdly, what I discovered last night is that the data being sent to Wire1 is, in fact, going to Wire. That is, it’s going to pins 0 and 1.

I was kinda hoping this was a now-fixed bug in earlephilhower’s library (I had to update it to get flashing to work) but apparently not.

So what I have to do is figure out what I’m doing wrong that lets Wire1 compile, but then allows it to keep using the pins for Wire.

There’s a lot to be said for having accessible layers of abstraction, particularly for tools made to be introductory.

But if I want to absolutely saturate USB2’s bandwidth with data, I’ll have to make it sleek.

Since I had no trouble using Wire and Wire1 simultaneously, I don't think the problem is in earlephilhower's rp2040 platform. It could be; but that's not first place I'd look. I'm still leaning towards something in one of Adafruit's libraries. Either that or something very strange with your array of DAC objects.

Either way, you've got to simplify things. Reduce to some simple, trivial even, code that works. If that's 1 DAC on Wire1, and no arrays at all, so be it. Find something that works. Then build on that until it breaks again. Debugging 101.

I wish you luck with your project, but I'm done here.

1 Like

I agree, very unlikely that it's in the earlephilhower library. It's so incredibly slick.

Thanks for the help!