Arm Mbed OS support forum

Issues with PCA9685 module on Nucleo_F446ZE, MBed 6.0

Hi Guys,

I am struggling to get a PCA9685 I2C PWM module to work on a Nucleo_F446ZE board - my first venture into I2C in this world, and the PCA9685 is new to me. Using MBed Studio.

I did get the servo working using PWMOut, so I think servo and board are working okay. The PCA9685 module is untested but I have no reason to think it is faulty.

As this is a learning exercise for me I am doing it ‘on-the-chip’, ‘straight line’ and not invoking any library code classes, but I have cut and pasted from a few classes on this website to help me with learning - Big thanks to Paulo Sanna

I am fairly sure I have the hookup, addressing and I2C setup working as the I2C write()s are returning 0. I did try an invalid I2C address and it hung on the write(), so I think the issue is in what I am actually writing to the module, and not the basic setup.

In the config of the chip it seemed to me I only needed to be concerned about the MODE1 register (Ox00) and the PRE_SCALE register (0xFE).

The configuration seems to me reasonably straightforward and I think I understand most of it, except the sections on sleep mode and restart where I am not 100% comfortable. I am also not sure if the auto increment bit needs to be set for the multi-byte write() to function properly. I suspect not but I set it anyway.

When I run it nothing happens - no servo sweep at all.

Any tips in diagnosing the issues or help as to what could be wrong would be appreciated.

Thanks - Peter

Diagnostic output with notes

====================start================
==Reset==
Write (hex values): 0 0 Write returned: 0
==set PWM==
Write for Read: 0 Write() returned: 0
Read() returned: 0 Value= 0
Write (hex values): 0 10 Write returned: 0 <Set Sleep bit 4>
Sleep 5 ms
Write (hex values): fe 79 Write returned: 0 <Set PRE_SCALE to 121>
Write (hex values): 0 0 Write returned: 0
Sleep 5 ms
Write (hex values): 0 a1 Write returned: 0 <Set restart, AI, All Call>
==Loop== No 1
Write 5 (dec values): 6 0 0 205 0 Write() returned: 0 <Servo 0 on 0 Off 205>
Write 5 (dec values): 6 0 0 154 1 Write() returned: 0 <Servo 0 on 0 off 410>
==Loop== No 2
Write 5 (dec values): 6 0 0 205 0 Write() returned: 0
Write 5 (dec values): 6 0 0 154 1 Write() returned: 0
==Loop== No 3
Write 5 (dec values): 6 0 0 205 0 Write() returned: 0
Write 5 (dec values): 6 0 0 154 1 Write() returned: 0
==Loop== No 4
Write 5 (dec values): 6 0 0 205 0 Write() returned: 0
Write 5 (dec values): 6 0 0 154 1 Write() returned: 0

Code

// test PCA9685 module with servo 
#include "mbed.h"

#define PCA9685_MODE1 0x0
#define PCA9685_PRESCALE 0xFE
#define TIMING_BLOCK_START 0x06

int addr8bit = 0x40 << 1;     // default 8-bit I2C address, 0x80 
uint8_t retVal ; 
I2C i2c(I2C_SDA , I2C_SCL);         // create an I2C object using specified pins 

void write8(uint8_t address, uint8_t data)
{
    char cmd[2];
    cmd[0] = address;
    cmd[1] = data;
    printf ("Write (hex values): %x %x ", cmd[0], cmd[1]) ; 
    retVal = i2c.write(addr8bit, cmd, 2);
    printf (" Write returned: %d \n", retVal) ; 
}
 
char read8(char address)
{
    printf ("Write for Read: %x ", address) ; 
    retVal = i2c.write(addr8bit, &address, 1);
    printf ("  Write() returned: %d \n", retVal) ; 
    char rtn;
    retVal = i2c.read(addr8bit, &rtn, 1);
    printf ("Read() returned: %x Value= %x \n", retVal, rtn) ; 
    return rtn;
}

void reset(void)
{
    write8(PCA9685_MODE1, 0x0);
}

void setPrescale(uint8_t prescale) 
{
    uint8_t oldmode = read8(PCA9685_MODE1);
    uint8_t newmode = (oldmode & 0x7F) | 0x10; // sleep
    write8(PCA9685_MODE1, newmode); // go to sleep
    printf ("Sleep 5 ms \n") ; 
    ThisThread::sleep_for(chrono::milliseconds(5)) ; 
    write8(PCA9685_PRESCALE, prescale); // set the prescaler
    write8(PCA9685_MODE1, oldmode);
    printf ("Sleep 5 ms \n") ; 
    ThisThread::sleep_for(chrono::milliseconds(5)) ; 
    write8(PCA9685_MODE1, oldmode | 0xa1);
}

void setPWMFreq(float freq)
{
    float prescaleval = 25000000;
    prescaleval /= 4096;
    prescaleval /= freq;
    uint8_t prescale = floor(prescaleval  + 0.5) - 1;
    setPrescale(prescale);
}

// function to set servo number (0-15) on and off count values 
// Range 0-4095 count for 1 ms = 205 count for 2 ms = 410
void setServoOnOff(uint8_t num, uint16_t on, uint16_t off)
{
    char cmd[5] ;   // 

    cmd[0] = TIMING_BLOCK_START + (4 * num) ; // address of the low byte of servo 
    cmd[1] = on;                     // low byte of on value 
    cmd[2] = (on >> 8);              // high byte of on 
    cmd[3] = off;                    // low byte of off  
    cmd[4] = (off >> 8);             // high byte of off 
    // note: passing cmd with no index passes an array pointer - required 
    printf ("Write 5 (dec values): %d %d %d %d %d ", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4]) ; 
    retVal = i2c.write(addr8bit, cmd, 5);     // write the address and 4 bytes   
    printf ("Write() returned: %d \n", retVal) ; 
}

int main() 
{   
    printf("====================start================ \n") ; 
    i2c.frequency(400000) ; // set the bus frequency 
    printf("==Reset== \n") ; 
    reset() ; 
    printf("==set PWM== \n") ;
    setPWMFreq(50.0) ; 
   
    uint8_t loopCount = 1 ; 
    while (true) 
    {
        printf("==Loop== No  %d \n", loopCount ) ; 
        setServoOnOff(0, 0, 205) ; 
        ThisThread::sleep_for(chrono::milliseconds(300)) ;  // let servo move 
        
        setServoOnOff(0, 0, 410) ; 
        ThisThread::sleep_for(chrono::milliseconds(300)) ;  // let servo move 

        loopCount ++ ; 
        if (loopCount > 4) 
            return 0 ; 
    }
} 

I thought I found something with setPWMFrequency, but it looks ok.

sleep_for(chrono::milliseconds(5))

can be simplified to:

sleep_for(5ms)

The appendix ms is a predefied literal.

But that is not the problem.

Thanks for the help.
That bit of code that sets the prescale value was copied from Paolo Sanna’s class, and I did have a good look at it in conjunction with the part of the manual that deals with setting the PWM frequency, and I think it is correct. Here :-

uint8_t newmode = (oldmode & 0x7F) | 0x10; // sleep
    write8(PCA9685_MODE1, newmode); // go to sleep

I think that “| 0x10” sets the sleep flag so that the pre-scale can be set with the oscillator off

But I am sure the issue is somewhere in this section - the manual says this about restarting the oscillator:-

1. Read MODE1 register.
2. Check that bit 7 (RESTART) is a logic 1. If it is, clear bit 4 (SLEEP). Allow time for
oscillator to stabilize (500 s).
3. Write logic 1 to bit 7 of MODE1 register. All PWM channels will restart and the
RESTART bit will clear.
Remark: The SLEEP bit must be logic 0 for at least 500 s, before a logic 1 is written into
the RESTART bit.

It’s a bit strange. What if you check bit 7 and it is NOT 1 - what do you do ? And then when you write a 1 there it seems that the H/w flips it back to 0 after restart. And they talk as if it were possible to set/clear a single bit, but as far as I know there is no way to do this with i2c - Hence the code doing the read-bitwise operation- write sequence.

I think maybe I should write a function to perform bitwise operations on the registers and then follow the manual step by step and see what happens.

Imagine if after a few hours struggling I find the module is faulty - I would REALLY feel stupid :grinning:

. . . but we push on . . . I think I took a foolish approach here. I should have taken the published class code, implemented ver 6.0 changes/enhancements and then checked it works - at least that way I would have had working code to work from. I should probably still do that.

Thanks for the tip on sleep_for(5ms) - much neater. :+1:

have you compared your methods to


? It may give also some clues. E.g., after writing the reset they wait for 10 ms.

And what about the ~OE pin, is it connected to GND or a port pin?

your reset() is different, you are writing a zero to the command reg instead of
#define MODE1_RESTART 0x80
writing a 0 does nothing:

User writes logic 1 to this bit to clear it to logic 0. A user write of logic 0 will have no
effect. See Section 7.3.1.1 for detail.

good luck!

Yup - I saw that writing bit 7 does nothing :grinning:

On the module I am using the ~OE pin has a pullup (or is it pulldown with the line above it?) resistor on the board, so the board doc says it can be left open. But I will double check. Not sure who made the module. No-Name from Bangood probably. I suppose it would do no harm to tie it.

I think I have a dusty Arduino in a box somewhere. If all else fails I can confirm the module is working by hooking it up and using Ms Limor Fried’s code to check.

Tied the ~OE down - Didn’t help.

Sherlock Holmes said: “Once you have removed the impossible, what remains, no matter how improbable, must be the truth.” :grinning: I am starting to think it may be the module.

But I will sharpen the code and diagnostic pencil just a little more. Plan B is another module. Seems the dusty Arduino in the junk box was donated to an Arduinoite.

I suppose the good news is I’m learning, but not sure what.

Yes, I have seen a pull down in the Adafruit documentation too. Seems to be true.

I’m also interested in the PCA9685 for dimming a bunch of LED spots. So I have ordered two modules for testing, they should arrive on friday and I can join the game also.

I see some mosfets and gamma correction tables in your future. :grinning:

// Gamma brightness lookup table <https://victornpb.github.io/gamma-table-generator>
// gamma = 2.00 steps = 100 range = 0-100
const uint8_t gamma_lut[100] = {
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2,
3, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 7, 8, 9, 9, 10,
10, 11, 12, 12, 13, 14, 15, 16, 16, 17, 18, 19, 20, 21, 22, 23,
24, 24, 26, 27, 28, 29, 30, 31, 32, 33, 34, 36, 37, 38, 39, 40,
42, 43, 44, 46, 47, 49, 50, 51, 53, 54, 56, 57, 59, 60, 62, 64,
65, 67, 69, 70, 72, 74, 75, 77, 79, 81, 83, 84, 86, 88, 90, 92,
94, 96, 98, 100,};

I built a 4-channel 12v dimmer/motor speed controller from two ATTiny45s a while ago. Would have been so much easier with an ARM MBed board - one tenth of the code.

There is a pic of it driving two vibrating motors in my notes in doc no 07 here:-

https://drive.google.com/drive/folders/1TE2zPuoRhSrlRVjCaqT6ffAX3j7rIHkm?usp=sharing

Thanks!
I have played with LED dimming several years ago, and found also the exponential intensity curve. It was calculated by some Excel sheet to create a table for the AVR. But now I will use also a STM32F4, and this guy has enough power and resources to calc a dimming curve on the fly, otherwise he will feel boring :slight_smile:

Whew! I am still not winning here. Still no wiggling in my servo-farm.

I wrote a function (setClearBitInRegister) to allow me to set or clear individual bits in the PCA9685’s registers so I can make my code look a little bit more like the user manual. It does a read-modify-write.

Then I wrote a little register content write function (diagRegDump) so I can look at the registers I am interested in at any stage. It logs a number, the elapsed time in milliseconds and some register values. I am interested only in the registers from 0x00 - 0x0A (MODE1 - PWM00 OFF) and the Pre_Scale register 0xFE.

When I look at the output log I think it is doing what I want. But if that is wrong then I have a problem !

This is from before the reset, so registers are from the previous run.
No= 1 - Time= 0 [0]=21 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=9a [9]=1 PS=79

After the reset. MODE1 has Auto Inc and All Call set. 6-9 set to 0
No= 2 - Time= 109 [0]=21 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=0 [9]=0 PS=79

Now we are setting the prescale. First stop the oscillator by setting bit 4 - MODE1 reflects this
No= 10 - Time= 215 [0]=31 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=0 [9]=0 PS=79

Sleep for a bit
No= 11 - Time= 373 [0]=31 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=0 [9]=0 PS=79

Set the prescale - PS=79 - expected although val is unchanged
No= 12 - Time= 480 [0]=31 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=0 [9]=0 PS=79

Now we clear bit 4 in MODE1 to exit sleep and then we sleep for a bit.
No= 13 - Time= 589 [0]=21 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=0 [9]=0 PS=79

after sleep we write 1 to bit 7 in MODE1 to 1. Hardware sets it back to 0. Osc & PWM should be running
No= 14 - Time= 746 [0]=21 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=0 [9]=0 PS=79

==Loop== No 1
Set On time for servo 0 to 0 and Off time to 205 registers 6-9 reflect correctly
No= 20 - Time= 872 [0]=21 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=cd [9]=0 PS=79

Set On time for servo 0 to 0 and Off time to 410 registers 6-9 reflect correctly
No= 30 - Time= 1280 [0]=21 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=9a [9]=1 PS=79

repeat
==Loop== No 2
No= 20 - Time= 1706 [0]=21 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=cd [9]=0 PS=79
No= 30 - Time= 2115 [0]=21 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=9a [9]=1 PS=79
==Loop== No 3
No= 20 - Time= 2541 [0]=21 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=cd [9]=0 PS=79
No= 30 - Time= 2950 [0]=21 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=9a [9]=1 PS=79
==Loop== No 4
No= 20 - Time= 3376 [0]=21 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=cd [9]=0 PS=79
No= 30 - Time= 3785 [0]=21 [1]=4 [2]=e2 [3]=e4 [4]=e8 [5]=e0 [6]=0 [7]=0 [8]=9a [9]=1 PS=79

So what I am seeing is that I am doing what I understand from the manual I need to do, but still no working. Whew !

So either my brain or the module are faulty. :grinning: (Possibly both?)

Where from here ? I’ll get another module next Monday and see.

Frustrating . . .

Peter
:south_africa: :south_africa: :south_africa:

The code now looks like this :-

// test PCA9685 module with servo 
#include "mbed.h"

#define PCA9685_MODE1 0x0
#define PCA9685_PRESCALE 0xFE
#define TIMING_BLOCK_START 0x06

// Forward Declarations
void reset() ; 
void setPWMFreq(float )  ; 
void setPrescale(uint8_t) ; 
void setServoOnOff(uint8_t , uint16_t , uint16_t ) ; 
void write8(uint8_t, uint8_t) ;
char read8(char) ; 
void setClearBitInRegister(char, uint8_t , uint8_t ) ; 
void diagRegDump(uint8_t logNo) ; 

// Global Vars 
int addr8bit = 0x40 << 1;     // default 8-bit I2C address, 0x80 
uint8_t retVal ; 
I2C i2c(I2C_SDA , I2C_SCL);         // create an I2C object using specified pins 
Timer t ; 

int main() 
{   
   t.start() ; // start the timer 
   i2c.frequency(400000) ; // set the bus frequency 
   reset() ; 
   setPWMFreq(50.0) ; 
  
   uint8_t loopCount = 1 ; 
   while (true) 
   {
       printf("==Loop== No  %d \n", loopCount ) ; 
       setServoOnOff(0, 0, 205) ; 
       diagRegDump(20) ; 
       ThisThread::sleep_for(chrono::milliseconds(300)) ;  // let servo move 
       
       setServoOnOff(0, 0, 410) ; 
       diagRegDump(30) ; 
       ThisThread::sleep_for(chrono::milliseconds(300)) ;  // let servo move 

       loopCount ++ ; 
       if (loopCount > 4) 
           return 0 ; 
   }
}

// ========================================

void reset(void)
{
   // Init set Auto Inc and respond to all call 
   diagRegDump(1) ; 
   write8(PCA9685_MODE1, 0x21); // auto-inc and all-call 
   for (uint8_t i = 0x06; i <= 0x45; i++) 
       write8(i, 0x00) ; // clear all the on-off registers
   diagRegDump(2) ; 
}

void setPWMFreq(float freq)
{
   float prescaleval = 25000000;
   prescaleval /= 4096;
   prescaleval /= freq;
   uint8_t prescale = floor(prescaleval  + 0.5) - 1;
   setPrescale(prescale) ; 
}

void setPrescale(uint8_t prescale)
{
   setClearBitInRegister(0x00, 4, 1) ; // set sleep bit 
   diagRegDump(10) ; 
   ThisThread::sleep_for(50ms); // allow all PWM to stop 
   diagRegDump(11) ;     
   write8(PCA9685_PRESCALE, prescale); // set the prescaler value 
   diagRegDump(12) ; 
   setClearBitInRegister(0x00, 4, 0) ; // clear sleep bit 
   diagRegDump(13) ; 
   ThisThread::sleep_for(50ms); // allow osc to stabilize 
   setClearBitInRegister(0x00, 7, 1) ; // write 1 to reset 
   diagRegDump(14) ; 
}

// function to set servo number (0-15) on and off count values 
// Range 0-4095 count for 1 ms = 205 count for 2 ms = 410
void setServoOnOff(uint8_t num, uint16_t on, uint16_t off)
{
   char cmd[5] ; 
   cmd[0] = TIMING_BLOCK_START + (4 * num) ;  
   cmd[1] = on ;                        
   cmd[2] = on >> 8 ; 
   cmd[3] = off ; 
   cmd[4] = off >> 8 ;                   
   // note: passing cmd with no index passes an array pointer - required 
   retVal = i2c.write(addr8bit, cmd, 5);     // write the address and 4 bytes   
}

char read8(char address)
{
   retVal = i2c.write(addr8bit, &address, 1);
   char rtn;
   retVal = i2c.read(addr8bit, &rtn, 1);
   return rtn;
}

void write8(uint8_t address, uint8_t data)
{
   char cmd[2];
   cmd[0] = address;
   cmd[1] = data;
   retVal = i2c.write(addr8bit, cmd, 2);
}

// clears or sets a bit in a register at address 
// pass setTo1or0 = 0 to clear or > 0 to set 
void setClearBitInRegister(char address, uint8_t bitNumber, uint8_t setTo1or0)
{
   i2c.write(addr8bit, &address, 1); // load the control register 
   char oRegVal ; 
   char nRegVal ; 
   i2c.read(addr8bit, &oRegVal, 1);  // read existing value 
   if (setTo1or0 == 0)
       nRegVal = (oRegVal & ~(1 << bitNumber)) ;  // clear the bit 
   else 
       nRegVal = (oRegVal | (1 << bitNumber)) ;   // set the bit 
   char add_Val[] = {address, nRegVal}  ; 
   i2c.write(addr8bit, add_Val, 2); // replace 
}

void diagRegDump(uint8_t logNo) 
{
   printf ("No= %d - Time= %d  ", logNo, (int)chrono::duration_cast<chrono::milliseconds>(t.elapsed_time()).count()) ;
   char address = 0 ; 
   i2c.write(addr8bit, &address, 1) ; 
   char regvals[10] ;  
   i2c.read(addr8bit, regvals, 10) ; 
   
   for (char i = 0; i < 10; i++)
      printf("[%x]=%x  ", i, regvals[i] ) ; 
   
   address = 0xFE ; 
   i2c.write(addr8bit, &address, 1) ; 
   char prescale ;  
   i2c.read(addr8bit, &prescale, 1) ; 
   printf("PS=%x \n ", prescale) ;  
}

today, I received my PCA9685 module. Connected also some old servo and tried your code, also nothing.
I checked the I2C communication with my Saleae logic analyzer, I2C looked fine.
Conneted the LA to the PWM0 pin, and it worked! So in my case it was just a broken cord in the servo cable. Another servo worked fine.
So on the analyzer you can see the pulse with is changing from about 1 to 2 ms.


grafik

have you used also the V+ pin or the extra terminals for the servo power supply?

Thanks so much. This looks encouraging, but I have never seen a logic analyser output, so I am no 100% sure what I am looking at.

I have a scope, so if I simply connect it up to my PWM out, and I don’t see the PWM out that you get does that point to a possibly faulty board? Board I can get another. New 67 year old brain is not so easy.

And yes. I have a 6v battery hooked up to the extra power terminals. I drive the Vcc, which I think is the chips power. from the Nucleo’s 3.3v out.

Thanks - I really appreciate your help here. Do I read your post correctly in that it seems the code is working on your board ?

Yes, I used your code, only the board has a F401 MCU. But that should not matter, the Mbed I2C Code is the same for all F4.
In the Screenshots you see the positive Puls with 1 or 2 ms and the period with about 20 ms, that is what the servo needs.
Have you already tried some other of the 16 pwm channels? Maybe only output 0 is broken?

Yes. I tried a number of the outputs and changed the code accordingly. None worked.

I am wondering if it may have something to do with the powering of the chip from +3.3v and the servos from the extra power terminals, but I think that is the way it is designed to work. I put a meter on the board and saw 3.3v on the Vcc and 6v on the extra power terminal.

When I tested the servo from the Nucleo board’s PWMOut I powered the servo directly from a 6v battery pack and it worked fine.

I am wondering if I should disconnect all servos and power the V+ from the nucleo board and put the scope on the PWMout. I was nervous in powering the servo from the Nucleo, which in turn gets it’s power from USB. I didn’t want to power servos from my laptop’s USB.

But I think plan A is to get a new PCA9685 module. The local guy here in Durban only has the no-name brand module like I already have, but I can get a branded Adafruit one couriered from Johannesburg.

But at last I am seeing light at the end of the dark tunnel, and I thank you. :+1:

I hope in some way my code may help you with your project in return for your input. :grinning:
:south_africa: :south_africa:

No problem, you’re welcome.
I use also VCC 3.3 V from the MCU Board, GND and SDA/SCL. And 6 V Batterypack +/- at screw terminals.
My PCA board is also some clone.

Yee Haa - it works !

I’ll tell the story if you promise not to laugh. Okay ?

I get a new PCA9685 module. Get it home. Plug it in. Run. Nothing happens.

Back to square one. Check the pins. Check the Vcc voltage 3.3v. Check the battery pack voltage. 6.1v Try again. Nothing. But as I touch the battery pack the servo twitches. Huh? I pull out the batteries, Clean the contacts, put them back try again.

It works. !!! How can this be ?

But I live in Durban, 120m from the warm Indian Ocean - Lots of Surfers, Nibbly Sharks, Nice Dolphins and nasty corrosion. So I think that the contacts on the battery pack were good enough to register 6v on my multi meter, but not good enough to drive the servos.

Hard to believe?

So I put the OLD PCA9685 module back - It works as well !! I have two working modules.

But nothing lost. I learned, and have working code. I will neaten the code up and stick it back here as test and study material in case anyone else has issues.

Peter
:south_africa: :south_africa:

This was my code that now works on both my modules.

It sweeps each servo in a sine wave pattern at 1Hz frequency. Each servo’s sine wave is shifted by 22.5 degrees (360/16) from it’s neighbour, so servo 0 and servo 7 are synchronised, but 180 degrees phase-shifted.

// test PCA9685 module with servos 
#include "mbed.h"

#define PCA9685_MODE1 0x0
#define PCA9685_PRESCALE 0xFE
#define TIMING_BLOCK_START 0x06
#define SDA_PIN I2C_SDA
#define SCL_PIN I2C_SCL

// Forward Declarations
void reset() ; 
void setPWMFreq(float )  ; 
void setPrescale(uint8_t) ; 
void setServoOnOff(uint8_t , uint16_t , uint16_t ) ; 
void write8(uint8_t, uint8_t) ;
char read8(char) ; 
void setClearBitInRegister(char, uint8_t , uint8_t ) ; 
// void diagRegDump(uint8_t logNo) ; 

// Global Vars 
int addr8bit = 0x40 << 1;     // default 8-bit I2C address, 0x80 
uint8_t retVal ; 
I2C i2c(SDA_PIN, SCL_PIN);         // create an I2C object using specified pins 
Timer t ;                            // stopwatch 

int main() 
{   
    t.start() ; // start the timer 
    i2c.frequency(400000) ; // set the bus frequency 
    reset() ;  // 
    setPWMFreq(50.0) ; 
    float phaseAngle = 0.0 ;  // this will go from 0 - 2*pi() at 0.5Hz 
    uint16_t millsecondCount = 0 ; 
       
    // sweep all the servos sinusoidally at 1Hz between 1ms(205) and 2ms (410)
    // servo num ofset by num * 22.5 degrees from servo 0    
    while (true) 
    {  
        millsecondCount = chrono::duration_cast<chrono::milliseconds>(t.elapsed_time()).count() ; 
        if (millsecondCount > 999)
            t.reset() ; 

        phaseAngle = 6.2831853 * ((float)millsecondCount / 1000.0 )  ; // (2 * pi) * (t / 2000)  

        for (int num = 0; num < 16; num++ )         // loop the servos 
        {
            float servoPhaseAngle =  phaseAngle + ((float)num * 0.785398) ;   // ofset phase angle  
            uint16_t pulseForServo = 307.5 + 105.0 * sin(servoPhaseAngle) ;   // calc pulse
            setServoOnOff(num, 0, pulseForServo) ;                            // set servo 
        }
    }
}

// ========================================

void reset(void)
{
    // Init set Auto Inc and respond to all call 
    write8(PCA9685_MODE1, 0x21); // auto-inc and all-call 
}

void setPWMFreq(float freq)
{
    float prescaleval = 25000000; // default on-chip oscillator 
    prescaleval /= 4096;          // per formula in manual 
    prescaleval /= freq;
    uint8_t prescale = floor(prescaleval  + 0.5) - 1;
    setPrescale(prescale) ;        // set the pre-scale value 
}

void setPrescale(uint8_t prescale)
{
    // put the oscillator to sleep - required for Changing Prescale 
    setClearBitInRegister(PCA9685_MODE1, 4, 1) ; // set sleep bit 
    ThisThread::sleep_for(50ms);        // allow all PWM to stop 
    write8(PCA9685_PRESCALE, prescale); // set the prescaler value 
    setClearBitInRegister(PCA9685_MODE1, 4, 0) ; // clear sleep bit 
    ThisThread::sleep_for(50ms);        // allow osc to stabilize 
    setClearBitInRegister(PCA9685_MODE1, 7, 1) ; // write 1 to reset HW will clear the bit
}

// function to set servo number (0-15) on and off count values 
// Range 0-4095 count for 1 ms = 205 count for 2 ms = 410
void setServoOnOff(uint8_t num, uint16_t on, uint16_t off)
{
    char cmd[5] ; 
    cmd[0] = TIMING_BLOCK_START + (4 * num) ;  
    cmd[1] = on ;                        
    cmd[2] = on >> 8 ; 
    cmd[3] = off ; 
    cmd[4] = off >> 8 ;                   
    // note: passing cmd with no index passes an array pointer - required 
    retVal = i2c.write(addr8bit, cmd, 5);     // write the address and 4 bytes   
}

char read8(char address)
{
    retVal = i2c.write(addr8bit, &address, 1);
    char rtn;
    retVal = i2c.read(addr8bit, &rtn, 1);
    return rtn;
}

void write8(uint8_t address, uint8_t data)
{
    char cmd[2];
    cmd[0] = address;
    cmd[1] = data;
    retVal = i2c.write(addr8bit, cmd, 2);
}

// clears or sets a bit in a register at address 
// pass setTo1or0 = 0 to clear or > 0 to set 
void setClearBitInRegister(char address, uint8_t bitNumber, uint8_t setTo1or0)
{
    i2c.write(addr8bit, &address, 1);               // load the control register 
    char oRegVal ; 
    char nRegVal ; 
    i2c.read(addr8bit, &oRegVal, 1);                // read existing value 
    if (setTo1or0 == 0)
        nRegVal = (oRegVal & ~(1 << bitNumber)) ;  // clear the bit 
    else 
        nRegVal = (oRegVal | (1 << bitNumber)) ;   // set the bit 
    char add_Val[] = {address, nRegVal}  ; 
    i2c.write(addr8bit, add_Val, 2);                // replace 
}

:south_africa: :south_africa:

:+1:
Thanks for feedback and the solution for the puzzle :slight_smile:
And I know the rough climate in SA, I’ve been in some places there, lovely country.

I added a little document (doc 11) on this step up the learning curve in my ‘musings’ :grinning: as Anna Bridge calls them. May help someone:-

https://drive.google.com/drive/folders/1TE2zPuoRhSrlRVjCaqT6ffAX3j7rIHkm?usp=sharing

I like the word ‘musings’ can I have it? :rofl:

Next step up the curve is dual DC motor speed control with a joystick - with the added twist of doing the ADC reading of the joystick in a background thread.

Thanks
:south_africa: :south_africa: