Pages: [1]   Go Down
Author Topic: I2C based 8 Channel Servo Controller - Looking for feedback  (Read 927 times)
0 Members and 1 Guest are viewing this topic.
Offline Offline
God Member
*****
Karma: 27
Posts: 829
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

I am putting together a standalone I2C to 8 channel Servo Controller board and am looking for some feedback. Currently, I am not doing anything magical. Just combining the wire library and servo library. I will be programming it into an ATmega8 (minimum).

The way I have it set up is to have jumpers on the A0-A3 pins to set the I2C address of the board. This way, multiple boards could be chained on the bus. I could of course expand that to use A0-A5 for even more boards. Current code just receives 8 bytes addressed to the board and updates a position array. The loop then updates the servo postions every 20ms. If no new position is given, it just resends the last position given.

I am looking for suggestions of useful features to expand it. The 'speed' of a servo can be changed by altering the refresh rate from what I understand. I am not sure how useful this would be. I have thought about parsing the first byte in the packet to have commands or program defaults, like the initial position of the servos. I would then store those defaults in the EEPROM.

Also, this is my first time actually using the wire library and servo library, so I wouldn't mind if someone looked over my code and pointed out any mistakes or more efficient ways of doing it.

Here is my code:

Code:
#include <EEPROM.h>

#include <Wire.h>
#include <Servo.h>

//#define thisBoard 0x01 //address of this board

Servo CHAN[8]; //Use: CHAN[i].write(val)
byte pos[8];

unsigned long lastMillis;

void setup()
{
 
byte thisBoard = PINC & B00001111; //Read Jumpers on A0-A3 to set address for I2C slave

Wire.begin(thisBoard); // Set the address of this board determined by jumpers
Wire.onReceive(Command); //Function to be called when I2C commands are received

//Set up 8 servos
CHAN[0].attach(3);
CHAN[1].attach(4);
CHAN[2].attach(5);
CHAN[3].attach(6);
CHAN[4].attach(7);
CHAN[5].attach(8);
CHAN[6].attach(9);
CHAN[7].attach(10);

lastMillis = millis(); //Get first sample of millis for comparison
}

void loop()
{

if(millis() >= lastMillis + 20){ //Update Servo positions every 20ms (50Hz)

for(int i = 0; i < 8; i++){
CHAN[i].write(pos[i]);
}
}

}

void Command(int howMany)
{
  int i = 0;
  while(Wire.available()) // loop through all but the last
  {
    char c = Wire.read(); // receive byte as a character
    pos[i] = c;        // print the character
    i++;
  }
}
Logged

Offline Offline
Edison Member
*
Karma: 58
Posts: 2078
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

You could take a look at my code and the link to Nick Gammon his code.
http://arduino.cc/forum/index.php/topic,156639.msg1173834.html#msg1173834

If both the Wire.onReceive() and Wire.onRequest() are implemented, you can request the status from the I2C slave.

In my code I use registers which can be written and read. The first byte written is the address of the registers.
In Nick Gammon his code he uses the first byte (in write mode) as a command.

About your sketch, I would do it differently.
Why update the servos every 10ms ?
You only have to know when the I2C did update it.
You could add a global boolean variable flagUpdate and set it 'true' in your Command() function.
In the loop you can test the flag, and if it is true, clear it and updates the servos.

In Command() you check Wire.available(). I would use the howMany variable. But I don't know what is better.

The most usefull addition would be to know the current of the servos. But that needs extra hardware. Perhaps a single hall-current-sensor to measure the current for all servos.
What if you could read the 5V or 6V voltage of the servos via I2C ?

If you want to expand it more. You could add the possibility to detach (and attach again) the servos. I learned this day on this forum that some servos go into idle mode after detach.
What if the ATmega8 checks the time of the commands from the Master. If nothing is received for some time, the master must be wrong and the ATmega8 turnes itself off.
Or a blinking led as a status ? A simple led can be helpful, you would only have to glance at the led to see that everyting is okay.
And perhaps a test mode ? The ATmega8 would test the servos one by one (perhaps measuring by the current). But I'm not sure a test of servos can be done.
Logged

Offline Offline
God Member
*****
Karma: 27
Posts: 829
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Changed the code to handle the EEPROM and parsing of commands. I have it so that it checks a byte in EEPROM to determine if it is the first time the board is powered up. If so, it writes the initial servo positions to the first 8 bytes in EEPROM. It then writes the flag in EEPROM so this will not be run the next time. Next, it reads the first 8 bytes of the EEPROM and puts those values into the position array. And finally it updates the servo positions with the initial values. So, until the initial positions are reprogrammed, the servos will always start up at position 90.

In the receive section, I changed it to read the first byte and start parsing that. If command is 0, then it interprets the next 8 bytes as servo position data. If it is 1, it interprets the next 8 bytes as initial positions and writes them to the EEPROM.

I also expanded the address jumpers to A0-A5, so now 64 different addresses can set (allowing up to 64 boards * 8 servos = 512 servos)

I will be using an Uno to do the initial development, then laying out a board using the DIP version and the SMD versions of the ATMEGA8. I will post all source code and files here. Final version may include a regular serial version. I chose I2C because it has built in addressing. But that could be implemented in serial as well. We are going to be using 2 wires either way, so I didn't see any real advantage to doing it as serial and complicating the code to handle addressing.

Code:
#include <EEPROM.h>

#include <Wire.h>
#include <Servo.h>

//#define thisBoard 0x01 //address of this board

Servo CHAN[8]; //Use: CHAN[i].write(val)
byte pos[8];

byte firstRun;

unsigned long lastMillis;

void setup()
{

  firstRun = EEPROM.read(8);

  if(firstRun = 1){ //first time board is powered up
    for(int i = 0; i < 8; i++){
      EEPROM.write(i,90); //so write intial position as 90 (center)
    }
     EEPROM.write(8,0);
  }

  for(int i = 0; i < 8; i++){ //Get initial values for Servos from EEPROM
    pos[i] = EEPROM.read(i);
  }


  byte thisBoard = PINC & B00111111; //Read Jumpers on A0-A5 to set address for I2C slave

  Wire.begin(thisBoard); // Set the address of this board determined by jumpers
  Wire.onReceive(Command); //Function to be called when I2C commands are received

    //Set up 8 servos
  CHAN[0].attach(3);
  CHAN[1].attach(4);
  CHAN[2].attach(5);
  CHAN[3].attach(6);
  CHAN[4].attach(7);
  CHAN[5].attach(8);
  CHAN[6].attach(9);
  CHAN[7].attach(10);

  for(int i = 0; i < 8; i++){
    CHAN[i].write(pos[i]);
  }

  lastMillis = millis(); //Get first sample of millis for comparison
}

void loop()
{

  if(millis() >= lastMillis + 20){ //Update Servo positions every 20ms (50Hz)

    for(int i = 0; i < 8; i++){
      CHAN[i].write(pos[i]);
    }
  }

}

void Command(int howMany)
{

  char cmd;
  int i = 0;
  if(Wire.available()){
    cmd = Wire.read();
  }

  switch(cmd){

  case 0: //Command Servos

    for(int u = 0; u > 8;u++){
      char c = Wire.read(); // receive byte as a character

      pos[u] = c;        // print the character
    }
    break;

  case 1: //Program Initial servo position
    for(int u = 0; u > 8;u++){
      char c = Wire.read(); // receive byte as a character
      EEPROM.write(u,c);
    }
    break;

  }
}


Logged

Offline Offline
God Member
*****
Karma: 27
Posts: 829
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

...
Why update the servos every 10ms ?
...
The most usefull addition would be to know the current of the servos. But that needs extra hardware. Perhaps a single hall-current-sensor to measure the current for all servos.
...
If you want to expand it more. You could add the possibility to detach (and attach again) the servos. I learned this day on this forum that some servos go into idle mode after detach.
What if the ATmega8 checks the time of the commands from the Master. If nothing is received for some time, the master must be wrong and the ATmega8 turnes itself off.

...
Or a blinking led as a status ? A simple led can be helpful, you would only have to glance at the led to see that everyting is okay.
And perhaps a test mode ? The ATmega8 would test the servos one by one (perhaps measuring by the current). But I'm not sure a test of servos can be done.


Some good suggestions there. You mean 'current' as in electrical current? How would this be useful to know at all times? I suppose it could be possible by tapping into the servo voltage lines, but then I would need those analog pins back. I guess I am not really sure how it would be useful. A servo has it's own built-in controls to handle this kind of stuff. Do you mean current position? If so, I agree that would be useful, but not really feasible.

As far as updating every 20ms, that's just because that's how it is done in a radio control environment. True, it would really only need to be updated only when the positions have changed. If I were receiving commands for only 1 servo at a time, I would have just wrote them directly in the I2C receive function. Since I am expecting all 8 positions to be sent at once, this was just a simple way to handle it. And since this is the only thing that this controller will be doing, I didn't think it mattered that I was keeping it busy doing this. Can you think of an advantage to only updating on new commands? Keep in mind that I am expecting to receive all 8 positions with every I2C transaction, not one at a time.

Hmm... idle mode. That would help save battery life, wouldn't it? I guess I can also peg that as a reason for above. However, even if I don't send a new command to the servos every 20ms, they will hold their position anyway which would take up current. Well, without this idle mode you mention. I will look into that. Sounds useful.

Blinking status LED is a good idea. I have a couple extra pins... A test mode. I'll have to give that some thought. Sounds useful if possible.

Off to look at the code you linked to...

Thanks!
Logged

Offline Offline
Edison Member
*
Karma: 58
Posts: 2078
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Electrical current.

What I want to able to do, is to read information from it.
I want to read at least the firmware version of the sketch in the slave. For me that would not be optional, but a must !

If something is wrong, it would be nice if the Master can read status information from it.
If for example the servo voltage would 4.5, something is wrong. Or the electrical current is zero or too high, that would be nice to know.
Perhaps an unique identifier stored in EEPROM, or the number of seconds since power on.

If you have the possibility to use a lot of those slave devices, trust me, any information can be helpful.

You could use other pins (spare digital pins) for the address. You have to use seperate digitalRead() and bitSet() instead of using PORTC, but that would make the sketch more flexible.

Did you look at the code ? It is very similar to what you are doing.
Logged

Offline Offline
God Member
*****
Karma: 27
Posts: 829
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Did you look at the code ? It is very similar to what you are doing.

Yes. Still parsing it. I will implement the firmware version. That will add the reading code into my sketch so adding other feedback would be simple. It would be simple to test the voltage on the servo lines and would only need 1 ADC pin. I plan to use the QFN version of the ATMEGA8 and that actually has two extra ADC-only pins on it. I can easily add that there. Or for compatibility, I can give up on of the analog pins used for the address. Not a big deal. I doubt many people would really need 512 servos.

Given the way I am designing the board, the VCC for the micro is regulated from the servo voltage, so if it were to meaure zero current, well... it wouldn't because the micro wouldn't be able to respond. smiley So a simple test of that would be to simple check for a response from all of the I2C devices.

Being able to sense how many of these are connected would also be very useful. And that just got me thinking about something else. It might be a nice feature to report back what function that servo controller node is on (for me, these would be used in animatronics, so I might have one in the head, one in the torso, etc...) I can see how it might be useful to know where it is located and what it is controlling.)

This all leads me to another question. In the Arduino, some of the EEPROM is used by the main code already. So I won't have access to all of it. What areas are reserved?
Logged

Offline Offline
Edison Member
*
Karma: 58
Posts: 2078
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

There are no reserved areas in the EEPROM of the Arduino.
It is only what you use.

I think it would be better to give an unique number to the servo controller, and have that combined with head and torso in the Master Arduino. That makes it easier to understand (for me) and easier to replace a servo controller.

This is also someone with a project with a lot of servos, http://arduino.cc/forum/index.php/topic,154561.0.html
Logged

Pages: [1]   Go Up
Jump to: