Interfacing with IMU and sending data from one Mega to another

Hey guys,

I’m working on a senior design project with a group of mechanical engineers. Little bit of unnecessary backstory so you understand. Its a solar panel system mounted onto a bicycle, and it has to turn towards the sun so it has optimum radiation all the time, while riding around also. We’re using an IMU and a GPS to orient the system and direct it towards the sun. None of us have much programming knowledge unfortunately. I’m able to get both the IMU and the GPS to send data to the Mega, and can serial print it out to the monitor no problem. The problem arises with the second Mega. We have two, one for a UserInterface system at the base, and the second at the top of the tracker, where it controls the motors and such. The base Mega gets the data from the IMU, and we need to use the serial to send to the second Mega up top. I’ve got some different codes I’ve been toying with, from surfing through the different forums.

This is the send code, which takes in the IMU data, prints it out the to the first Mega Serial monitor, and send its through serial to the second Mega.

#include <Adafruit_Sensor.h>
#include <Adafruit_BNO055.h>

  
Adafruit_BNO055 bno = Adafruit_BNO055(55);

float x,y,z;
void setup(void) 
{
  Serial.begin(2400);
  Serial2.begin(2400);
  Serial.println("Orientation Sensor Test"); Serial.println("");
  
  /* Initialise the sensor */
  if(!bno.begin())
  {
    /* There was a problem detecting the BNO055 ... check your connections */
    Serial.print("Ooops, no BNO055 detected ... Check your wiring or I2C ADDR!");
    while(1);
  }
  
  delay(1000);
    
  bno.setExtCrystalUse(true);
}

void loop(void) 
{
  /* Get a new sensor event */ 
  sensors_event_t event; 
  bno.getEvent(&event);

  x=event.orientation.x;
  y=event.orientation.y;
  z=event.orientation.z;

  Serial.print(x,4);
  Serial.print(" ");
  Serial.print(y,4);
  Serial.print(" ");
  Serial.println(z,4);

  Serial2.print(x,4);
  Serial2.print(y,4);
  Serial2.print(z,4);
  
}

This is the code for the second Mega, which is just supposed to read in the data, and then print it out to a separate Serial monitor.

int i = 0;
float x = 0;

void setup() {

  Serial.begin(2400);       //Initializes serial monitor
  Serial1.begin(2400);      //Initializes transfer serial. Include # of serial (Rx Tx) port used
  Serial.println("Is this on?...");     //Prints out on serial monitor
  
}
void loop() {

  for(i;i<3;i++){
    if (Serial1.available()){
        x= Serial1.parseFloat();
        Serial.print(x,4);
        }
    Serial.print(" ");
  }
  Serial.println("");
  i=0;

}

The ultimate desire would be to save it and and be able to use it in calculations to direct the motors. Currently im just trying to prove that I can send it over in real time and get it to print out the same data appropriately.

This mostly works. I can send the data through to the second Mega. However, it seems to me that the processing of all this takes a bit too much time. Sometimes, what was the X data in the first Mega is printed out to the Y data of the second Mega, and Y to Z, and so on. I think its just because of the loop that I’m using to take it in and print it out, but I’m not positive.

Also, it cuts off parts of the data sometimes. It will be 356.1458 in the first Mega, but it prints out 0.1458 in the second Mega. I’m not sure why. I can post some of the data in the second comment to show that I mean.

Proper data: 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875 354.6250 -1.1875 -1.6875

Second Mega data: -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250 -1.1875 -1.6875 0.6250

What was supposed to be X data(354.6250) became the Zdata, and it cut off the 354 for some reason.

Like I said, I'm a huge rookie on all this, so idk if its the fact that im using floats, or its the parseFloat, or if its the loop, or what else. Any help would be hugely appreciated! Thank you so much, and sorry for the long posts!

You need a delay between the records, so that the second controller is not overrun. In most cases it's sufficient to send another record only if the input values change by some reasonable amount.

You also need some sync mark or channel indicator in the data records, so that the receiver will know at least where a record begins or ends. Often a newline is used to indicate the end of a record. Or the second controller acts as the master, that requests IMU data from the first controller (slave), whenever there is time left for reading the next data record. Most probably a single controller will be sufficient for your project, with no communication problems at all.

I'm not sure what's the use of the IMU for positioning the solar panel. How do you enter the position of the sun? Also the computed IMU angles will run away quickly, pointing the solar panel into almost random directions.

Are you sure that positioning the panel on the bike will take less energy than the solar panel can provide?

We're doing a multiple stage control scheme, where first we use light sensors, and balance the lux input on each sensors to position the solar panel properly. The second uses an IMU and stored solar position data logs to orient the panel, knowing where the sun will be just based off of where it's "supposed" to be. And then if we can get all that working we will try to implement the GPS to give us our exact location. I'm not really the controls guy for the project, so I may have explained that kind of wrong. And really, we only have like 3 weeks left to go, so I doubt we get much beyond just making it work somewhat smoothly with the light sensors, but we wanted an ambitious plan how we would try to do this if we had lots of time to work on it.

In regards to having just a single controller, that was our original plan, but some extenuating circumstances forced us to desire one controller at the top and one at the bottom. Just take my word for it, it's unfortunately going to have to stay this way, and hopefully the communication will work out. The main part of the project was to design and manufacture all of the mechanics of the system, and to ensure that the power and thermal components of it would function properly, so that is what most of the semester was spent on. I think in the end the professor will be happy if it turns on, moves, and produces even just a little extra power than what it takes to run the whole thing. We are using a Fresnel concentrating lens and some of the most efficient CPV cells in industry today, so we should get decent power output (I think 10W is realistic for out 14 inch lens, hopefully more.) But no, I think the actual power production of this system doesn't make the whole thing worth it at all, plus its expensive lol. It was mostly to challenge our design minds to make a system that could be built and function properly.

In regards to your actual coding advice, 1. Could i just implement an if statement checking if Xnew is different than Xold, OR Ynew and Yold, OR Znew and Zold by more than some value? If yes, then send over data.

Xold=Xnew;
Yold=Ynew;
Zold=Znew;

Xnew=event.orientation.x;
Ynew=event.orientation.y;
Znew=event.orientation.z;

If(Xnew != Xold || Ynew != Yold || Znew != Zold){
  Serial2.print(Xnew,4);
  Serial2.print(Ynew,4);
  Serial2.print(Znew,4);
}
  1. Could you tell me a bit more about how to implement that sync mark newline? I'm guessing that means thats how the second controller knows that a single line of data has been passed through, but how does it know that, and differentiate from the rest of the data? I'll look into it some more also.

Thank you so much for the help!

In regards to #2, is Robin2’s Serial Input Basics example #3 what you’re referring to? I’ll put a snippet of the code in here since I’m assuming you havent memorized everything on this forum lol.

// Example 3 - Receive with start- and end-markers

const byte numChars = 32;
char receivedChars[numChars];

boolean newData = false;

void setup() {
    Serial.begin(9600);
    Serial.println("<Arduino is ready>");
}

void loop() {
    recvWithStartEndMarkers();
    showNewData();
}

void recvWithStartEndMarkers() {
    static boolean recvInProgress = false;
    static byte ndx = 0;
    char startMarker = '<';
    char endMarker = '>';
    char rc;
 
    while (Serial.available() > 0 && newData == false) {
        rc = Serial.read();

        if (recvInProgress == true) {
            if (rc != endMarker) {
                receivedChars[ndx] = rc;
                ndx++;
                if (ndx >= numChars) {
                    ndx = numChars - 1;
                }
            }
            else {
                receivedChars[ndx] = '\0'; // terminate the string
                recvInProgress = false;
                ndx = 0;
                newData = true;
            }
        }

        else if (rc == startMarker) {
            recvInProgress = true;
        }
    }
}

void showNewData() {
    if (newData == true) {
        Serial.print("This just in ... ");
        Serial.println(receivedChars);
        newData = false;
    }
}

Assuming yes, then my follow up is if I have to use char as my variable type for that, and if that would be bad for me? The goal isn’t to serial print out all this, like in this code, but to have it accessible/readable/usable for calculations in the second controller.

markwag13: 1. Could i just implement an if statement checking if Xnew is different than Xold, OR Ynew and Yold, OR Znew and Zold by more than some value? If yes, then send over data.

If(Xnew != Xold || Ynew != Yold || Znew != Zold){
  Serial2.print(Xnew,4);
  Serial2.print(Ynew,4);
  Serial2.print(Znew,4);
}

A threshold may be better than a comparison for equality.

if (abs(Xnew-Xold)>Xthreshold || ...

markwag13:
In regards to #2, is Robin2’s Serial Input Basics example #3 what you’re referring to?

That’s one example of such a synchronization, usable in your project.

You also could send binary values, using e.g. Serial1.write(value), but then the end marker may appear as part of the binary data (payload) as well. In this case a fixed number of bytes should be read, following the start character, and only afterwards the next character should be checked for being the end marker. You can use a struct to simplify the decoding:

struct {
 float Xval, Yval, Zval;
 char endMarker;
} myRecord;
...
  if (Serial1.available > sizeof(myRecord) && Serial1.read() == '<') { //wait for start marker
    Serial1.readBytes((char[])&myRecord, sizeof(myRecord));
    if (myRecord.endMarker == '>') {
    //valid record received
    ...
    }
  }

Similar for the sender, send the start marker before writing out the struct.

You also can use I2C instead of Serial, and request a record from the other (slave) node. This will eliminate the need for synchronization, because the slave transmits just one record upon request.

I like all this a lot, it makes sense, thank you. I'm implementing into my code now, but I wont be able to test it until I'm back home again.

What does the (ch) mean in Serial1.read(ch)? Typically the () are just empty aren't they? Also, to clarify how this loop and struct are working: It asks to check if available bytes is larger than the size of myRecord, which would be 4? One for Xval, Yval, Zval, and endMarker? And then varifies that the first one is the startmarker(meaning there should actually be a char startMarker in the struct i think).

How does the serial.read(myRecord) then split the remaining bytes to Xval, Y, and Z? Does it just know that the first byte should go to X, second to Y, and so on?

And lastly, I think, because im a dummy, if I am Serial.printing a float like so:

Serial2.print(Xnew,4); //prints X with four decimal places, ie 3.4650

does it send it over as a byte that will then be read out as float value 3.4650 again, or do I have to convert it back into a float?

Thanks! :)

You are right, Serial.read() does not take an argument. I was a step ahead, where readBytes() should be used. See my updated code above, but it still may contain more errors.

Data can be transmitted in binary form using write() or writeBytes(), just as they reside in RAM, or as text, using print(). The textual representation is made for human reading, and requires encoding and decoding of the transmitted text. The binary form instead is more useful for controllers, because no conversion to/from text is required. For structs with multiple members it's required, of course, that the struct has the same declaration (and memory layout) in both the sender and receiver code.

I’m getting some difficulties with the struct coding, lots of errors popping up.
"
exit status 1
no matching function for call to ‘HardwareSerial::readBytes(myRecord&, unsigned int)’"

float Xval, Yval, Zval;

struct myRecord{
  char startMarker;
  float Xval, Yval, Zval;
  char endMarker;  
  };

myRecord dataIn;

typedef struct record Record;

void setup() {
  Serial.begin(2400);       //Initializes serial monitor
  Serial1.begin(2400);      //Initializes transfer serial. Include # of serial (Rx Tx) port used
  Serial.println("Is this on?...");     //Prints out on serial monitor
 }
 
void loop() {
  if (Serial1.available() > sizeof(dataIn) && Serial1.read() == '<'){
    Serial1.readBytes(dataIn, sizeof(dataIn));
    if (dataIn.endMarker == '>'){
      //valid record received
      Serial.print(Xval);
      Serial.print(Yval);
      Serial.print(Zval);
      }
   }
}

It specifically doesn’t like that readBytes line. If I comment out everything from that line on, it compiles okay. Leaving that line in there gives it the above error. I was getting errors just from your code so I looked up some other stuff and tried to write out the struct structure as best I could, but I’m still getting something wrong obviously

You have to give the address of the myRecord variable (&myRecord) to readBytes(). Why don't you use the declaration I gave in my code snippet?

Thats what I first tried, it looks like this:

//float Xval, Yval, Zval;

struct {
  char startMarker;
  float Xval, Yval, Zval;
  char endMarker;  
  } myRecord;


//typedef struct record Record;

void setup() {
  Serial.begin(2400);       //Initializes serial monitor
  Serial1.begin(2400);      //Initializes transfer serial. Include # of serial (Rx Tx) port used
  Serial.println("Is this on?...");     //Prints out on serial monitor
 }
 
void loop() {
  if (Serial1.available() > sizeof(myRecord) && Serial1.read() == '<'){
    Serial1.readBytes(myRecord, sizeof(myRecord));
    if (myRecord.endMarker == '>'){
      //valid record received
      Serial.print(Xval);
      Serial.print(Yval);
      Serial.print(Zval);
      }
   }
}

but I get the error "no matching function for call to ‘HardwareSerial::readBytes(&, unsigned int)’ "

I feel like something is being left out of the struct declaration. When I compared with other struct declarations, there are other lines that I’m not sure of what their purpose is, but I definitely dont have them. It is certainly possible that I’m missing something glaringly obvious that I just don’t know about. Thanks for staying with me

struct record
{
   int one;
   int two;
   int three;
};

typedef struct record Record;

Record aRec;
aRec.one = 12;

This one does compile at least:

   Serial1.readBytes((char[])&myRecord, sizeof(myRecord));

Much could be said about C and C++ structs and classes. They can be declared as types, with or without tag names, or as variables of an anonymous type. All that by moving around the name in the declaration. I used the latter approach, the shortest one, with only a variable but no type name. In your last example you get a struct with tagname record, typename Record, and a variable named aRec.

If you know what you do, you can declare the type first, then use it to define variables. But even I have problems with the details of the C syntax, because usually I write programs in other languages.