diff --git a/PCA9685.cpp b/PCA9685.cpp index 919a3e1..8563ec6 100644 --- a/PCA9685.cpp +++ b/PCA9685.cpp @@ -2,7 +2,7 @@ // FILE: PCA9685.cpp // AUTHOR: Rob Tillaart // DATE: 24-apr-2016 -// VERSION: 0.2.2 +// VERSION: 0.3.0 // PURPOSE: Arduino library for I2C PCA9685 16 channel PWM // URL: https://github.com/RobTillaart/PCA9685_RT // @@ -12,16 +12,17 @@ // 0.2.0 2020-05-25 refactor; ESP32 begin(sda,scl) // 0.2.1 2020-06-19 fix library.json // 0.2.2 2020-09-21 fix #1 + add getFrequency() +// 0.2.3 2020-11-21 fix digitalWrite (internal version only) +// 0.3.0 2020-11-22 fix setting frequency + -#include #include "PCA9685.h" -// check datasheet for details +// REGISTERS CONFIGURATION - check datasheet for details #define PCA9685_MODE1 0x00 #define PCA9685_MODE2 0x01 - // MODE1 REGISTERS #define PCA9685_RESTART 0x80 #define PCA9685_EXTCLK 0x40 @@ -32,22 +33,30 @@ #define PCA9685_SUB3 0x02 #define PCA9685_ALLCALL 0x01 - // MODE2 REGISTERS (see datasheet) #define PCA9685_INVERT 0x10 #define PCA9685_OCH 0x08 #define PCA9685_OUTDRV 0x04 #define PCA9685_OUTNE 0x03 +// REGISTERS - CHANNELS +#define PCA9685_CHANNEL_0 0x06 // 0x06 + 4*channel is base per channel -// SPECIAL REGISTER - FREQUENCY -#define PCA9685_PRE_SCALE 0xFE +// REGISTERS - FREQUENCY +#define PCA9685_PRE_SCALER 0xFE - -// NOT IMPLEMENTED YET +// REGISTERS - Subaddressing I2C - not implemented #define PCA9685_SUBADR(x) (0x01+(x)) // x = 1..3 #define PCA9685_ALLCALLADR 0x05 -#define PCA9685_TESTMODE 0xFF + +// REGISTERS - ALL_ON ALL_OFF - partly implemented +#define PCA9685_ALL_ON_L 0xFA +#define PCA9685_ALL_ON_H 0xFB +#define PCA9685_ALL_OFF_L 0xFC +#define PCA9685_ALL_OFF_H 0xFD // used for allOFF() + +// NOT IMPLEMENTED YET +#define PCA9685_TESTMODE 0xFF // do not be use. see datasheet. ////////////////////////////////////////////////////////////// @@ -114,7 +123,8 @@ void PCA9685::setPWM(uint8_t channel, uint16_t onTime, uint16_t offTime) _error = PCA9685_ERR_CHANNEL; return; } - uint8_t reg = 0x06 + (channel << 2); + offTime &= 0x0FFFF; // non-doc feature - to easy set figure 8 P.17 + uint8_t reg = PCA9685_CHANNEL_0 + (channel << 2); writeReg2(reg, onTime, offTime); } @@ -134,7 +144,7 @@ void PCA9685::getPWM(uint8_t channel, uint16_t* onTime, uint16_t* offTime) _error = PCA9685_ERR_CHANNEL; return; } - uint8_t reg = 0x06 + (channel << 2); + uint8_t reg = PCA9685_CHANNEL_0 + (channel << 2); Wire.beginTransmission(_address); Wire.write(reg); _error = Wire.endTransmission(); @@ -151,25 +161,53 @@ void PCA9685::getPWM(uint8_t channel, uint16_t* onTime, uint16_t* offTime) // set update frequency for all channels -void PCA9685::setFrequency(uint16_t freq) +void PCA9685::setFrequency(uint16_t freq, int offset) { _freq = freq; if (_freq < 24) _freq = 24; // page 25 datasheet if (_freq > 1526) _freq = 1526; // removed float operation for speed // faster but equal accurate - // uint8_t scaler = round(25e6 / (freq * 4096)) - 1; + // uint8_t scaler = round(25e6 / (_freq * 4096)) - 1; uint8_t scaler = 48828 / (_freq * 8) - 1; - writeReg(PCA9685_PRE_SCALE, scaler); + + uint8_t mode1 = readMode(PCA9685_MODE1); + writeMode(PCA9685_MODE1, mode1 | PCA9685_SLEEP); + scaler += offset; + writeReg(PCA9685_PRE_SCALER, scaler); + writeMode(PCA9685_MODE1, mode1); +} + +int PCA9685::getFrequency(bool cache) +{ + if (cache) return _freq; + uint8_t scaler = readReg(PCA9685_PRE_SCALER); + scaler++; + _freq = 48828 / scaler; + _freq /= 8; + return _freq; } +// datasheet P.18 - fig. 9: +// Note: bit[11-0] ON should NOT equal timer OFF in ON mode +// in OFF mode it doesn't matter. void PCA9685::digitalWrite(uint8_t channel, uint8_t mode) { - if (mode != LOW) setPWM(channel, 0x1000, 0x0000); - else setPWM(channel, 0x0000, 0x1000); + if (channel > 15) + { + _error = PCA9685_ERR_CHANNEL; + return; + } + uint8_t reg = PCA9685_CHANNEL_0 + (channel << 2); + if (mode != LOW) writeReg2(reg, 0x1000, 0x0000); + else writeReg2(reg, 0x0000, 0x0000); } +void PCA9685::allOFF() +{ + writeReg(PCA9685_ALL_OFF_H, 0x10); +} int PCA9685::lastError() { @@ -196,9 +234,9 @@ void PCA9685::writeReg2(uint8_t reg, uint16_t a, uint16_t b) Wire.beginTransmission(_address); Wire.write(reg); Wire.write(a & 0xFF); - Wire.write((a >> 8) & 0x0F); + Wire.write((a >> 8) & 0x1F); Wire.write(b & 0xFF); - Wire.write((b >> 8) & 0x0F); + Wire.write((b >> 8) & 0x1F); _error = Wire.endTransmission(); } diff --git a/PCA9685.h b/PCA9685.h index c058900..5e189bc 100644 --- a/PCA9685.h +++ b/PCA9685.h @@ -3,7 +3,7 @@ // FILE: PCA9685.H // AUTHOR: Rob Tillaart // DATE: 24-apr-2016 -// VERSION: 0.2.2 +// VERSION: 0.3.0 // PURPOSE: Arduino library for I2C PCA9685 16 channel PWM // URL: https://github.com/RobTillaart/PCA9685_RT // @@ -12,8 +12,9 @@ // #include "Arduino.h" +#include "Wire.h" -#define PCA9685_LIB_VERSION "0.2.2" +#define PCA9685_LIB_VERSION "0.3.0" // ERROR CODES #define PCA9685_OK 0x00 @@ -49,8 +50,10 @@ class PCA9685 // set update frequency for all channels // freq = 24 - 1526 Hz - void setFrequency(uint16_t freq); - int getFrequency() { return _freq; }; + // note: as the frequency is converted to an 8 bit prescaler + // the frequency set will seldom be exact, but best effort. + void setFrequency(uint16_t freq, int offset = 0); + int getFrequency(bool cache = true); // set channel HIGH or LOW (effectively no PWM) void digitalWrite(uint8_t channel, uint8_t mode); @@ -59,6 +62,9 @@ class PCA9685 void setON(uint8_t channel) { digitalWrite(channel, HIGH); }; void setOFF(uint8_t channel) { digitalWrite(channel, LOW); }; + // experimental for 0.3.0 + void allOFF(); + int lastError(); private: diff --git a/README.md b/README.md index 9333d06..d9867bb 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,16 @@ Arduino library for I2C PCA9685 16 channel PWM extender # Description This library is to control the I2C PCA9685 PWM extender. -The 16 channels are independently configurable is steps of 1/4096. -this allows for better than 0.1% finetuning of the duty-cycle -of the PWM signal. Furthermore the PWM's of the different signals -have individual start and stop moments. This can be used to distribute the -power more evenly over multiple servo's or give special effects when -used in an RGB LED. +The 16 channels are independently configurable in steps of 1/4096. +This allows for better than 0.1% finetuning of the duty-cycle +of the PWM signal. +The PWM's of the different channels have individual start and stop moments. +This can be used to distribute the power more evenly over multiple servo's +or give special effects when used in an RGB LED. + +The frequency of the PWM can be set from 24 to 1526 according to the datasheet, however in practice not all frequencies are set accurate. +Lower frequencies do better than higher frequencies. ### interface @@ -39,19 +42,37 @@ channels do not need to start at the same moment with HIGH. **getPWM(channel, ontime, offtime)** read back the configuration of the channel. -**setFrequency(freq)** set the update speed of the channels. -This is for all channels at once. -The frequency is constrained to be between 24 and 1526 Hz, +**setFrequency(freq, int offset = 0)** set the update speed of the channels. +This value is set the same for all channels at once. +The frequency is constrained to be between 24 and 1526 Hz. +As the frequency is converted to an 8 bit **prescaler**, +the frequency set will seldom be exact. +After changing the frequency, one must set all channels (again), +so one should set the frequency in **setup()** + +The parameter offset can be used to tune the **prescaler** to get a frequency +closer to the requested value. See **PCA9685_setFrequency_offset** example. +Default the offset = 0. As the prescaler is smaller at higher frequencies +higher frequencies are less accurate. +Making offset too large can result in very incorrect frequencies. -**getFrequency()** get the current update frequency of the channels. -This is same for all channels. +When using offset, the **getFrequency(false)** will return the adjusted prescaler. + +**getFrequency(cache = true)** get the current update frequency of the channels. +This is same for all channels. If cache is false, the frequency is fetched and +calculated from the **prescaler** register and will probably differ from the +value set with **setFrequency()**. **digitalWrite(channel, mode)** mode = HIGH or LOW, just use the PCA9685 as a digitalpin. This single function replaces the setON() and setOFF() that will become obsolete in the future. -**lastERror()** returns **PCA9685_OK = 0** if all is OK, and +**allOFF()** switches all PWM channels OFF. **Experimental** in 0.3.0 +To "undo" the allOFF one can call the **reset()** function and set all +PWM channels again. + +**lastError()** returns **PCA9685_OK = 0** if all is OK, and | Error code | Value | Description | |:----|:----:|:----| @@ -59,7 +80,7 @@ obsolete in the future. | PCA9685_ERROR | 0xFF | generic error | PCA9685_ERR_CHANNEL | 0xFE | Channel out of range | PCA9685_ERR_MODE | 0xFD | Invalid mode register chosen | -| PCA9685_ERR_I2C | 0xFF | PCA9685 I2C communication error +| PCA9685_ERR_I2C | 0xFC | PCA9685 I2C communication error diff --git a/examples/PCA9685_allOFF_test/PCA9685_allOFF_test.ino b/examples/PCA9685_allOFF_test/PCA9685_allOFF_test.ino new file mode 100644 index 0000000..73abfa0 --- /dev/null +++ b/examples/PCA9685_allOFF_test/PCA9685_allOFF_test.ino @@ -0,0 +1,53 @@ +// +// FILE: PCA9685_allOFF_test.ino +// AUTHOR: Rob Tillaart +// DATE: 2020-11-22 +// VERSION: 0.1.0 +// PUPROSE: test PCA9685 library +// + +/* + sets all channels to a PWM + then switches them all off + you can check it by testing all channels. +*/ + +#include "PCA9685.h" + +PCA9685 PCA(0x40); + +const uint8_t PIN = 2; + +void setup() +{ + Wire.begin(); + PCA.begin(); + + Serial.begin(115200); + Serial.print("PCA9685 LIB version: "); + Serial.println(PCA9685_LIB_VERSION); + Serial.println(); + + pinMode(PIN, INPUT_PULLUP); + for (int channel = 0; channel < 16; channel++) + { + PCA.setPWM(channel, 0, 1000); + } + delay(100); // to be sure they started. + PCA.allOFF(); + + // delay(100); + // PCA.reset(); // needed to reset the allOFF() + // for (int channel = 0; channel < 16; channel++) + // { + // PCA.digitalWrite(channel, HIGH); + // } +} + + +void loop() +{ + Serial.println(digitalRead(PIN)); // you can measure all pins +} + +// -- END OF FILE -- diff --git a/examples/PCA9685_digitalWrite_test/PCA9685_digitalWrite_test.ino b/examples/PCA9685_digitalWrite_test/PCA9685_digitalWrite_test.ino new file mode 100644 index 0000000..1f01b24 --- /dev/null +++ b/examples/PCA9685_digitalWrite_test/PCA9685_digitalWrite_test.ino @@ -0,0 +1,66 @@ +// +// FILE: PCA9685_digitalWrite_test.ino +// AUTHOR: Rob Tillaart +// DATE: 2020-11-21 +// VERSION: 0.1.0 +// PUPROSE: test PCA9685 library +// + +/* + sets one channel to max PWM 0..4095 + and connect the output to an interrupt pin 2 + to see the frequency of the PWM +*/ + +#include "PCA9685.h" + +PCA9685 PCA(0x40); + +const uint8_t IRQ_PIN = 2; +volatile uint16_t count = 0; +uint32_t lastTime = 0; + +void setup() +{ + Wire.begin(); + PCA.begin(); + + Serial.begin(115200); + Serial.print("PCA9685 LIB version: "); + Serial.println(PCA9685_LIB_VERSION); + Serial.println(); + + pinMode(IRQ_PIN, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(IRQ_PIN), irq, CHANGE); + + // PCA.setPWM(15, 0, 1000); // works OK - reference to test irq() + // PCA.digitalWrite(15, LOW); // works OK + PCA.digitalWrite(15, HIGH); // works OK +} + + +// INTERRUPT ROUTINE TO COUNT THE PULSES +void irq() +{ + count++; +} + + +void loop() +{ + uint32_t now = millis(); + if (now - lastTime >= 1000) + { + lastTime = now; + // make a copy + noInterrupts(); + uint16_t t = count; + count = 0; + interrupts(); + + Serial.print(t); + Serial.print("\t"); + Serial.println(digitalRead(IRQ_PIN)); + } +} +// -- END OF FILE -- diff --git a/examples/PCA9685_maxPWM_test/PCA9685_maxPWM_test.ino b/examples/PCA9685_maxPWM_test/PCA9685_maxPWM_test.ino new file mode 100644 index 0000000..8d29d54 --- /dev/null +++ b/examples/PCA9685_maxPWM_test/PCA9685_maxPWM_test.ino @@ -0,0 +1,70 @@ +// +// FILE: PCA9685_maxPWM_test.ino +// AUTHOR: Rob Tillaart +// DATE: 2020-11-22 +// VERSION: 0.1.0 +// PUPROSE: test PCA9685 library +// + +/* + sets one channel to max PWM 0..4095 + and connect the output to an interrupt pin 2 + to see the frequency of the PWM +*/ + +#include "PCA9685.h" + +PCA9685 PCA(0x40); + +const uint8_t IRQ_PIN = 2; +volatile uint16_t count = 0; +uint32_t lastTime = 0; + +void setup() +{ + Wire.begin(); + PCA.begin(); + + Serial.begin(115200); + Serial.print("PCA9685 LIB version: "); + Serial.println(PCA9685_LIB_VERSION); + Serial.println(); + + pinMode(IRQ_PIN, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(IRQ_PIN), irq, RISING); // CHANGE + + Serial.println(PCA.getFrequency()); + PCA.setFrequency(200); + Serial.println(PCA.lastError()); + Serial.println(PCA.getFrequency(false)); // do not fetch from cache. + + PCA.setPWM(15, 0, 4095); // gives 2 changes per interval +} + + +// INTERRUPT ROUTINE TO COUNT THE PULSES +void irq() +{ + count++; +} + + +void loop() +{ + uint32_t now = millis(); + if (now - lastTime >= 1000) + { + lastTime += 1000; + // make a working copy of count + noInterrupts(); + uint16_t t = count; + count = 0; + interrupts(); + + Serial.print(t); + Serial.print("\t"); + Serial.println(digitalRead(IRQ_PIN)); + } +} + +// -- END OF FILE -- diff --git a/examples/PCA9685_setFrequency_offset/PCA9685_setFrequency_offset.ino b/examples/PCA9685_setFrequency_offset/PCA9685_setFrequency_offset.ino new file mode 100644 index 0000000..66b55c0 --- /dev/null +++ b/examples/PCA9685_setFrequency_offset/PCA9685_setFrequency_offset.ino @@ -0,0 +1,101 @@ +// +// FILE: PCA9685_setFrequency_offset.ino +// AUTHOR: Rob Tillaart +// DATE: 2020-11-22 +// VERSION: 0.1.0 +// PUPROSE: test PCA9685 library +// + +/* + This sketch is to determine the offset needed to get te best matching + value for offset to match the wanted frequency. + + connect PWM line 15 to IRQ line 2 to monitor the real frequency + set the frequency to the value you want. + use the + and - keys to adjust the frequency to get the wanted frequency. + + Note: the higher the frequency, the more inaccurate the real frequency, + +*/ + +#include "PCA9685.h" + +PCA9685 PCA(0x40); + +const uint8_t IRQ_PIN = 2; +volatile uint16_t count = 0; +uint32_t lastTime = 0; + + +uint16_t freq = 200; // adjust to freq needed (between 24..1526 ) +int offset = 0; +int lines = 0; + +void setup() +{ + Wire.begin(); + PCA.begin(); + + Serial.begin(115200); + Serial.print("PCA9685 LIB version: "); + Serial.println(PCA9685_LIB_VERSION); + Serial.println(); + + pinMode(IRQ_PIN, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(IRQ_PIN), irq, RISING); + + PCA.setFrequency(freq, offset); + PCA.setPWM(15, 0, 4095); // gives 2 changes per interval + + Serial.println("\nSET\tIRQ\tIRQ%\tOFFSET"); +} + + +// INTERRUPT ROUTINE TO COUNT THE PULSES +void irq() +{ + count++; +} + + +void loop() +{ + uint32_t now = millis(); + if (now - lastTime >= 1000) + { + lastTime += 1000; + // make a working copy of count + noInterrupts(); + uint16_t t = count; + count = 0; + interrupts(); + + Serial.print(freq); + Serial.print("\t"); + Serial.print(t); + Serial.print("\t"); + Serial.print(100.0 * t / freq, 1); + Serial.print("\t"); + Serial.print(offset); + Serial.print("\n"); + + lines++; + if (lines == 20) + { + Serial.println("\nSET\tIRQ\tIRQ%\tOFFSET"); + lines = 0; + } + } + + if (Serial.available()) + { + char c = Serial.read(); + if (c == '+') offset++; + if (c == '-') offset--; + PCA.setFrequency(freq, offset); + PCA.setPWM(15, 0, 4095); + } + +} + +// -- END OF FILE -- diff --git a/examples/PCA9685_setFrequency_test/PCA9685_setFrequency_test.ino b/examples/PCA9685_setFrequency_test/PCA9685_setFrequency_test.ino new file mode 100644 index 0000000..ed54127 --- /dev/null +++ b/examples/PCA9685_setFrequency_test/PCA9685_setFrequency_test.ino @@ -0,0 +1,92 @@ +// +// FILE: PCA9685_setFrequency_test.ino +// AUTHOR: Rob Tillaart +// DATE: 2020-11-22 +// VERSION: 0.1.0 +// PUPROSE: test PCA9685 library +// + +/* + sets one channel to max PWM 0..4095 + and connect the output to an interrupt pin 2 + to see the frequency of the PWM +*/ + +#include "PCA9685.h" + +PCA9685 PCA(0x40); + +const uint8_t IRQ_PIN = 2; +volatile uint16_t count = 0; +uint32_t lastTime = 0; +uint16_t freq = 24; + +uint8_t lines = 0; + +void setup() +{ + Wire.begin(); + PCA.begin(); + + Serial.begin(115200); + Serial.print("PCA9685 LIB version: "); + Serial.println(PCA9685_LIB_VERSION); + Serial.println(); + + pinMode(IRQ_PIN, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(IRQ_PIN), irq, RISING); // CHANGE + + PCA.setFrequency(24); + PCA.setPWM(15, 0, 4095); // gives 2 changes per interval + + Serial.println("\nSET\tGET\tGET%\tIRQ\tIRQ%"); +} + + +// INTERRUPT ROUTINE TO COUNT THE PULSES +void irq() +{ + count++; +} + + +void loop() +{ + + + uint32_t now = millis(); + if (now - lastTime >= 1000) + { + lastTime += 1000; + // make a working copy of count + noInterrupts(); + uint16_t t = count; + count = 0; + interrupts(); + + Serial.print(freq); + Serial.print("\t"); + Serial.print(PCA.getFrequency(false)); + Serial.print("\t"); + Serial.print(100.0 * PCA.getFrequency(false) / freq, 1); + Serial.print("\t"); + Serial.print(t); + Serial.print("\t"); + Serial.print(100.0 * t / freq, 1); + Serial.print("\n"); + + freq += 4; + if (freq >= 1526) freq = 24; + PCA.setFrequency(freq); + PCA.setPWM(15, 0, 4095); + + lines++; + if (lines == 20) + { + Serial.println("\nSET\tGET\tGET%\tIRQ\tIRQ%"); + lines = 0; + } + } +} + +// -- END OF FILE -- diff --git a/examples/PCA9685_test02/PCA9685_test02.ino b/examples/PCA9685_test02/PCA9685_test02.ino index 5d366fc..b285814 100644 --- a/examples/PCA9685_test02/PCA9685_test02.ino +++ b/examples/PCA9685_test02/PCA9685_test02.ino @@ -2,12 +2,11 @@ // FILE: PCA9685_test02.ino // AUTHOR: Rob Tillaart // DATE: 24-APR-2016 -// VERSION: 0.1.1 +// VERSION: 0.1.2 // PUPROSE: test PCA9685 library // #include "PCA9685.h" -#include PCA9685 ledArray(0x40); diff --git a/library.json b/library.json index caad882..27276b9 100644 --- a/library.json +++ b/library.json @@ -15,7 +15,7 @@ "type": "git", "url": "https://github.com/RobTillaart/PCA9685_RT.git" }, - "version": "0.2.2", + "version": "0.3.0", "frameworks": "arduino", "platforms": "*" } diff --git a/library.properties b/library.properties index 5e746dd..47a9c1a 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=PCA9685_RT -version=0.2.2 +version=0.3.0 author=Rob Tillaart maintainer=Rob Tillaart sentence=Arduino library for I2C PCA9685 16 channel PWM