I2C DMAC with GPS on Sparkfun SAMD21 Dev

I am working on an autonomous sailboat with an I2C port that supports GPS, magnetometer,. accelerometer, gyro, current and voltage. I have had trouble with the I2C locking up, and after reading a bit on this forum, decided to change to MartinL’s I2C DMAC library. I have had success with the exception of the GPS.

The GPS is an odd device to put on I2C. It is clearly a native UART device which has been shoehorned into I2C. It has no registers, and sends large text strings up the I2C whenever it feels like, without prompting from the master. Text string commands are sent from the master to the GPS, and the GPS responds with text string acknowledgments. My device is an Adafruit mini GPS PA1010D, but SparfFun makes I2C GPSs too.

I have tried to use the library functions that don’t include register addresses. I have had success with initReadBytes(), but I have had trouble with initWriteBytes().

#include <I2C_DMAC.h>
#define GPS_ADDRESS 0x10                         // Device address when ADO = 0

char c[100];

void setup()
{
  SerialUSB.begin(115200);                           // Activate the native USB port
  while (!SerialUSB);                                // Wait for the native USB to be ready

  I2C.begin(100000);                                 // Start I2C bus at 400kHz
  delay(100);

  //None of the following initWriteBytes lines seem to work
  I2C.initWriteBytes(GPS_ADDRESS, (uint8_t*)"$PMTK314,0,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0*29", sizeof("$PMTK314,0,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0*29"));  
  // while (I2C.writeBusy);  Tried 
  //I2C.write();                   these
  //while (I2C.writeBusy);               with
  //delay(100);                               no benefit
  
  I2C.initWriteBytes(GPS_ADDRESS, (uint8_t*)"$PMTK220,1000*1F", strlen("$PMTK220,1000*1F"));
  
  I2C.initWriteBytes(GPS_ADDRESS, (uint8_t*)"$PMTK300,1000,0,0,0,0*1C", strlen("$PMTK300,1000,0,0,0,0*1C"));
  
  I2C.initWriteBytes(GPS_ADDRESS, (uint8_t*)"$PGCMD,33,1*6C", strlen("$PGCMD,33,1*6C"));
  
  I2C.initWriteBytes(GPS_ADDRESS, (uint8_t*)"$PMTK605*31" , strlen("$PMTK605*31" ));
  
  I2C.initReadBytes(GPS_ADDRESS, (uint8_t*)c, sizeof(c));        // Initialise DMAC read transfer: This works!

}

void loop() {
  //Everything here works fine.
  delay(1000);  //check for GPS data once a second.  Successfull about once every 5 seconds

  I2C.read();                                      // Initiate DMAC read transmission on the I2C bus
  //put code here
  while (I2C.readBusy);    // Wait for synchronization
  if (c[0] == 0) {
    SerialUSB.println("nothing this pass ");             // Output the result
    return;
  }
  for (int i = 0; i < sizeof(c); i++) {
    switch (c[i]) {
      case 0x0a:
        //SerialUSB.print('~');  //uncomment to see how many LFs are being thrown away.
        break;
      case 0x0d:
        SerialUSB.println();
        break;
      default:
        SerialUSB.print(c[i]);             // Output the result byte by byte:  ends with CR and one or more LF
    }
  }
}

Understandably, there are no examples showing initWriteBytes() used in this way. I have tried a few variations, but have had no luck sending commands to the GPS. I never get an acknowledgement, and I can never change the behavior of the GPS, it continues to run on its default settings. Please look at my code.

I also have two questions about reading from this device:

  1. Is there a way I can see the number of bytes waiting for me in DMA and only read that many? I have been reading 100 characters every second, but I am discarding a lot of white space. As I write this, 100 chars doesn’t seem excessive, but I feel like I am doing unnecessary work.

  2. All my white space characters are 0x0a (line feed). This doesn’t present a problem, but it seems odd to me. Are these coming from the DMA or the GPS?

Hi Doug,

I just ran a test with the initWriteBytes() function and it appears to be working correctly:

#include <I2C_DMAC.h>

void setup()
{
  I2C.begin(100000);                                              // Start I2C bus at 100kHz
  I2C.initWriteBytes(0x68, (uint8_t*)"Hello", sizeof("Hello"));   // Initialise DMAC with "Hello" string
  I2C.write();                                                    // Send the string
  while (I2C.writeBusy);                                          // Wait for write flag
}

void loop(){}

On my oscilloscope’s I2C decoder, I can see the hex character codes: 0x68, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x00 on the SDA line, in other words the “Hello\0” string, (including the trailling null ‘\0’).

In lieu of an I2C GPS, I’m using the address of my gyroscope at I2C device address: 0x68. In this way, it at least acknowledges what I’m sending it, even if it doesn’t understand the string that’s being sent.

Martin,

It is good to know I was using the software correctly. I will try some more things and get back to you.

Clarification: When the GPS receives a good command, it sends a 20-30 char text string ack. This is what I am not receiving.

You have done a great job of giving your functions meaningful names. I think we all would benefit from 3 or 4 paragraphs about what is going on. I understand what DMA is, but have never tinkered with it, and don’t have a great desire to go up that learning curve. This is my understanding. Can you comment?

initxxxxx() sets up I2C – DMA interface for upcoming write or read.

read() initiates transfer from I2C to DMA

write() initiates transfer from DMA to I2C, Not exactly the mirror image of read()

getData() gets one byte from DMA memory

readByte(s) and writeByte(s) various combinations of above functions to simplify coding for standard cases.

while(readBusy()) and while(writeBusy()) While I understand that the underlying transfer takes some time, I need some assurance that these “while” loops will never block, and why. It is a bit frightening to see so many of them.

Various callbacks: These seem like a great idea, but there could be more explanation. I see sercom3errorCallback() used in your MPU6050_Temperature_V2.ino example, and I am totally going to use it, even though I don’t really know what it does.

Again, Thanks! I will get back to you after more GPS testing.

Doug

Ahhh..... I found my problem. I added carriage returns to the ends of my messages, and they went through as they should. I was unable to find this detail in the datasheet, nor in the 50 or so files in the Adafruit Sensors libraries.

Thanks for your help.

Doug

Another followup: What the GPS really wants is a \r\n a carriage return and line feed.

Another comment about Martin's MPU6050_Temperature_V2.ino example code: I see one line you have commented out:

//I2C.attachSercom3ErrorCallback(sercom3errorCallback);

I uncommented it and changed it to I2C.attachSercomErrorCallback(sercom3errorCallback); and it seems to do no harm. Is this just an oversight?

Again Thanks.

Doug

Hi Doug,

There are 3 levels of funnctions in the I2C_DMAC library.

The top (3rd level) are the readByte() and readBytes() functions with a register address (regAddress) parameter. These perform a combined write to set the register address pointer of the device, blocking wait, to wait for the write to complete, followed by a read:

I2C.readByte(MPU6050_ADDRESS, WHO_AM_I);           // Read the WHO_AM_I register 
while(I2C.readBusy);                               // Wait for synchronization

Note that the blocking wait for the read is handled outside of this funtion with while(I2C.readBusy).

In middle (2nd level) the readByte() and readBytes() functions do not have the register address (regAddress) as a parameter. This is instead performed by separate write functions: writeByte(), writeBytes() and writeRegAddress() and overloaded read functions readByte() and readBytes():

I2C.writeRegAddr(MPU6050_ADDRESS, WHO_AM_I);       // Write the register address
// Add concurrent code here....
while(I2C.writeBusy);
I2C.readByte(MPU6050_ADDRESS);                     // Read the WHO_AM_I register
// Add concurrent code here....
while(I2C.readBusy);                               // Wait for synchronization

This allows for the insertion of concurrently running code, as the DMAC goes off and requests or sends data to the I2C device.

The bottom (1st level) breaks the procedure is broken down even further, allowing separate DMAC configuration and send/receive routines. This can be advantageous for repeated operation, as it allows the DMAC to be configured just once and repeated reads and write of the I2C device to be made. This uses the functions: initWriteByte(), initWriteBytes(), initWriteRegAddress(), initReadByte() and initReadBytes() to initialise the DMAC and the functions write() and read() to execute the data transfer.

I2C.initWriteRegAddr(MPU6050_ADDRESS, WHO_AM_I);   // Initialise the DMAC write
I2C.initReadByte(MPU6050_ADDRESS);                 // Initialise the DMAC read
I2C.write();                                       // Write the register address
// Add concurrent code here...
while(I2C.writeBusy);                              // Wait for synchronization
I2C.read();                                        // Read the WHO_AM_I register
// Add concurrent code here...
while(I2C.readBusy);                               // Wait for synchronization

In the temperature example uncommenting the line activates the SERCOM error function. I can remember why I left it commented out, perhaps it was an oversight. Revisiting my old code I often find myself thinking, ."hmm...now why did I do that?".

Martin,

The reason I switched to the non-blocking I2C library was because my "wire" based code would block occasionally, causing me to have to power down my unit to recover. Please explain writeBusy and readBusy, specifically can they hang and under what circumstances.

Doug

Hi Doug,

Specifically, after the microcontroller’s CPU calls on the DMA to transfer data on the I2C bus, they’re working in parallel. The DMA is left to control the transfer, while the CPU free to do other tasks.

At some point however these two separate threads of execution need to be sychronised. In this instance, the CPU needs to know when the DMA has finished transferring data. This can be achieved either by the CPU simply polling the appropriate writeBusy or readBusy flag once its task has completed, or alternatively calling on the DMA’s transfer complete callback function, to run additional code asynchronously before returing to the main loop(). These are two standard synchronisation methods.

While the Arduino Wire library will block and wait for an I2C device to return data, it shouldn’t be crashing your system. To me this indicates a possible problem with a device on the I2C bus itself.

As you might imagine, I am trying to make this application bulletproof. I have had a few cases where my system has locked up during I2C access (using wire). I did some reading which indicated that interrupts running at exactly the wrong time can interfere with I2C timing and cause one end or the other to wait for an event that will never occur. I switched to this DMAC I2C library to minimize this.

Is this library more resistant to this kind of problem? Is there any event that can cause writeBusy or readBusy to never return? I am hoping that the sercom callback function will catch this kind of problem so I can call I2C.end() and start all my I2C devices again, and maybe power cycle the I2C power. Am I being too optomistic?

Doug

Hi Doug,

I did some reading which indicated that interrupts running at exactly the wrong time can interfere with I2C timing and cause one end or the other to wait for an event that will never occur.

Might I ask where you read this?

Is this library more resistant to this kind of problem?

As I never experienced a similar issue with the Arduino Wire library, I’m probably not be best person to answer this question.

I have however experience problems with the I2C bus hanging. In my case, due to an external magnetometer breakout board locking up, while connected to my SAMD51 based flight controller board. Unfortunately, the flight controller’s gyroscope and accelerometer happened to share the same I2C port, instantly turning my tricopter drone into a flying brick. Saying that, I’ve never experienced any issues with the SAMD21.

The solution turned out to be adding 120R series resistors to the SCL and SDA lines, to reduce the harsh signal edges and ringing.

It also taught me the importance activating the microcontroller’s watchdog timer, to reset the board, just in case such unforseen circumstances occur.

Martin,

Here it is: https://spellfoundry.com/2020/06/25/reliable-embedded-systems-recovering-arduino-i2c-bus-lock-ups/

I believe my I2C bus hangs, and causes my processor to wait for an event that never occurs. I have four interrupts going on: 1) 16 ms timer, 2) pin driven interrupt from INA260 12C current/voltage board (8 seconds), 3) pin driven interrupt to read PPM signal from RC remote. (16 every 20 ms) 4) Your sercom3errorCallback(). I am also using IridiumSBD which may have an interrupt as well. I believe these may be messing up the I2C timing, causing the I2C to hang.

My approach is to first do whatever I can to prevent, or fix these problems, and then introduce a watchdog. I am in the fixing stage.

Evidence: Most significant is that when my system hangs and I reset it, it will continuously hang at the same place during initialization. I can install unrelated example code and that code will hang too if it uses the I2C. Power cycling the i2C gives mixed results.

I was optimistic that a nonblocking I2C library would allow the system to continue so I could identify the problem and take measures to reset the bus (like power cycle it). I was especially hopeful of your sercom3errorCallback(), but so far, it hasn't fired off.

One solution that occurs to me would be to disable interrupts during all critical I2C events. I don't think any of them would be troubled by a 50-100 us delay.

It would seem to me that a watchdog reset would take a couple seconds to get back running. Not too bad in my case, except it takes the AHRS as much as a minute to get it's magnetic heading info synchronized with the gyro. I would love to see a video of a copter going through a watchdog reset in the air.

I have spent the last week porting over to your library, and I think it has been time well spent. I have had to go through all my I2C devices and actually understand their registers instead of mindlessly accepting Adafruit defaults. Nevertheless, I wonder if your idea of "nonblocking" doesn't differ from mine.

Your comments?

Doug

Martin,

I have worked past this problem. Yes, I believe my problem is due to the I2C blocking, but my problem is how to deal with this wth a watchdog as the last resort. This is what I have done:

I reconfigured my GPS for serial; This makes a lot of sense. The GPS just spews out data. It doesn't really want to be asked for it. Its native interface is clearly serial.

I switched back to wire. I did find one case where I2C DMAC blocked (due to an I2C block I am sure) at a write(), not at a writeBusy as I would have expected. In the code below, I got the first print: "> 2 ", but not the second: "> 3 ".

 SerialUSB.print("> 2 ");   SerialUSB.println(PMTK_SET_NMEA_OUTPUT_RMCGGA);
 I2C.write();                        //starting up, this function someties blocks when the GPS was shut down while asleep
 SerialUSB.print("> 3 ");   SerialUSB.println(PMTK_SET_NMEA_OUTPUT_RMCGGA);
 while (I2C.writeBusy);

Doug

Hi Doug,

The I2C.write() will only block if the I2C bus is currently busy:

while (sercom->I2CM.STATUS.bit.BUSSTATE == 0x2);   // Wait while the I2C bus is BUSY

It does this as a failsafe, to prevent a new data transfer from being initiated while the DMAC is busy with the current one. As the microcontroller is the single bus master and in control of events, this should never actually occur, provided each communication is transfer completed before the next is initiated.

The issue here is that because the CPU and DMAC are operating in parallel, it's possible for the CPU to initiate another transfer before the DMAC has completed the previous one. The line of code above simply blocks until the previous DMAC transfer has finished.

If the I2C.write() function is blocking, my questions would be:

  • How often is this occuring and is it repeatable or intermittent?
  • Are there any other devices on the I2C bus, other than the GPS?
  • Does the system work when only the GPS and no other devices are accessed?
  • Do these devices free the I2C bus, after communication has taken place? In other words, is a "stop condition" generated at the end of the transfer?
  • Are these devices correctly synchronised with the writeBusy/readBusy flags, before initiating a data transfer on other device?
  • Is a single data transfer limited to 256 bytes?
  • Does this issue also occur with the Arduino Wire library?
    Kind regards,
    Martin

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