/* Program to demonstrate and test PWM as DAC uses ARDUINO UNO, Nano, Micro or similar board; ADS1115 to measure results - not needed just to generate PWM. EITHER a simple RC filter R=47k on pin 9, C = 0.22uF to ground Time constant is 10msec. so need to allow at least 100 msec for change to establish fully to 16 bit resolution OR an active fiter based on Butterworth Sallen & Key design with a similar cut off frequency reads the filter voltage on A0 AND/OR uses ADS1115 to measure filtered voltage. PWM is proportional to Vcc so use Vcc as analog reference if using internal adc Starts by measuring Vcc to support calculation of expected value function adjustPWM provides correction for calculation of the expected value to match the observed results. Voltage error is at 1LSB level */ #include Adafruit_ADS1115 ads; /* Use this for the 16-bit version */ //Adafruit_ADS1015 ads; /* Use this for the 12-bit version */ //Analog input and pin assignments const byte v0Pin = A0; //Filter voltage const byte vOutPin = 9; // use pin 9 for pwm out //control String string0 = " \n"; //null string for start of new run String string01 = "Choose divider for PWM: Options 1 8 64 256 1024: "; String string1 = "Choose PWM type: 0 for PCPWM (default) or 1 for Fast PWM: "; String string2 = "Choose fixed value (0) (default) Step (1) or ramp (2): "; String string3 = "Choose nMin: integer value between 0 and 255 : "; String string4 = "Choose nMax: integer value greater then nMin and less than 256 : "; String string5 = "Choose step interval in milliseconds for ramp else duration in tenths of a second for fixed value or step: typically 100 for step or >100 for ramp : "; //generating PWM signal - *** select required values here *** int divider = 1; //PWM: for pin 9 divider=1 sets pwm freq at 31,250Hz; =8 sets it to 3,906Hz - or double that for fast PWM int stepSize = 1; //chosen by input in program, values here are just defaults int pMode = 0; // 0 - pcPWM; 1 - fastPWM; else "normal" no PWM int pShape = 0; // 0 = Fixed, 1 = Step, 2= ramp int nMin = 0; int nMax = 255; // value is the duty cycle of the pwm ie 255 = always 1, 0 = always off. long interval = 240; // time in msec before each new output for ramp long duration = 240; //time in seconds to continue step or fixed output long t; //current time for fixed or step long stepInterval; //time between samples - 1/240 of duration //variables used in calculations int nOut; //number to be output via PWM; value is the duty cycle of the pwm ie 255 = always 1, 0 = always off. 2v5=128 int nOutOld; //previous value float mVoltsOut; //expected pwm average output voltage in mV float vSlope; //range calibration float vOffset; // zero offset error correction for PCPWM or FPWM //reading ADS1115 and converting to mV int16_t nVcc; //number reading of Vcc float mVcc = 4930; //mV - actual measured value of Vcc; *** corrected in setup if using ADS1115 *** int16_t nVolts; //adc reading as number float mVolts; //reading converted to millivolts int nTimes = 16; //ADC averaguing; only if not using ADS1115 int rDelay = 7; //msec delat between ADC readings; only if not using ADS1115 //function prototypes int getSerInt(String myString); //get integer value to choose PWM mode, range etc void setPwmFrequency(int pin, int divisor); void setPwmMode(int pin, int mode); void readVcc(); //check and print supply voltage float readADS(int which); //reads the voltage on the selected ADC as millivolts; "which" selects which pin to read float convADS(int16_t adcReading); //convert reading to voltage float readADC(int which); // alternative to using ADS1115 //main part of loop: void printHeader(); //print header lines so we have a record of the settings: print as comma seperated values (CSV) for later analysis if required void getSettings(); //choose pMode, pShape, nMin, nMax, interval/duration void printSettings(); //print on one line, csv void printResults(); //print one set of output values and voltages void adjustPWM(); //corrections to match expected and measured values // diagnostic void printBits(byte myByte); //to print binary value showing all bits even leading zeros. void setup() { Serial.begin(57600); Serial.print(F("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n")); //scroll previous off screen delay(5000); //time to restart serial monitor or other app pinMode(vOutPin, OUTPUT); // Serial.print("Readings from ADS1115: ,"); Serial.println("ADC Range: +/- 6.144V , (1 bit = 3mV/ADS1015, 0.1875mV/ADS1115)"); if (!ads.begin()) { Serial.println("Failed to initialize ADS."); while (1) ; } ads.setDataRate(RATE_ADS1115_8SPS); // nice and slow to filter out HF } void loop() { Serial.print("Enter a character to start a new run"); int temp = getSerInt(string0); Serial.println("**********************"); // mark start of new run Serial.println(); //adc0 reads Vcc; adc1 reads vFilter readVcc(); //check and print supply voltage getSettings(); adjustPWM(); //ReadVcc() sets default values, this allows slope and offset to be changed to suit observed results printSettings(); //use fastest conversion for step if (pShape == 1) { ads.setDataRate(RATE_ADS1115_860SPS); // error correction is a little different for this rate. Perhaps still seeing a little HF } else { ads.setDataRate(RATE_ADS1115_8SPS); } //settling time esp for step analogWrite(vOutPin, nMin); //allow output to settle to minimum value chosen delay(3000); //time to clear the monitor display if wanted //diagnostic print String str1; Serial.println(str1 + "pShape: " + pShape + " time t: " + t + " duration: " + duration); printHeader(); nOut = nMin; //set start value nOutOld = -1; //allows pwm update t = 0; // start "time" counter //*** generating pwm values and incrementing counter *** while (((pShape == 2) && (nOut <= nMax)) || ((pShape <= 1) && (t <= duration))) { // //identify required shape and prepare next value to be sent to pwm switch (pShape) { case 0: { //Serial.println("fixed"); nOut = nMin; delay(stepInterval); break; } case 1: { //Serial.println("step"); int fifth = duration / 5; if (t <= fifth) { nOut = nMin; } else { //step starts at half duration nOut = nMax; } delay(stepInterval); break; } case 2: { //Serial.println("ramp"); delay(interval); break; } } //activate pwm and print results if (nOut != nOutOld) { //if its unchanged dont repat the write. //Serial.print("*"); analogWrite(vOutPin, nOut); //generates the PWM nOutOld = nOut; } printResults(); //Update nOut and/or t at end of run if (pShape == 2) { nOut++; //t++; //just increment the loop count as nOut may not step by 1: or t += interval; //show actual milliseconds time } //increment at end to allow starting from nMin else { t += stepInterval; // t will display in msec } } //end of run analogWrite(vOutPin, 0); // stops the PWM } float readADS(int which) { //read chosen ADC, return result int16_t adcReading; adcReading = ads.readADC_SingleEnded(which); //int return (adcReading); //average adc reading of voltage as a number } float convADS(int16_t adcReading) { //convert reading to voltage float fsRange = 6.144f; //m_gain = GAIN_TWOTHIRDS; /* +/- 6.144V range (limited to VDD +0.3V max!) */ int m_bitShift = 0; float nVolts = 1000 * ads.computeVolts(adcReading); return (nVolts); //adc reading of voltage as a number of mV } void readVcc() { //check and print supply voltage ads.setDataRate(RATE_ADS1115_8SPS); // nice and slow to filter out HF; changed for step // read and print actual supply voltage nVcc = readADS(0); //read the actual supply voltage Serial.println("ADC reading of Vcc: , Vcc mV"); Serial.print(nVcc); mVcc = convADS(nVcc); Serial.print(", "); Serial.println(mVcc); Serial.println(); //set "uncorrected" values for slope and offse in case "adjustPWM" not required vSlope = mVcc / 256; //pwm step size in mV vOffset = 0; //add to mVolts for low end correction } void printHeader() { //print header lines so we have a record of the settings: print as comma seperated values (CSV) for later analysis if required setPwmMode(vOutPin, pMode); setPwmFrequency(vOutPin, divider); Serial.println(); //seperate results from settings recorded above Serial.println("t, nOut, mVoltsOut, nFilter, vFilter, vError"); } int getSerInt(String myString) { //get integer value to choose PWM mode, range etc Serial.print(myString); // Serial.print("enter an integer value with return : "); while (Serial.available() == 0) { } int value = Serial.parseInt(); Serial.println(value); return (value); } void getSettings() { //choose divider, pMode, pShape, nMin, nMax, interval/duration divider = 0; while (divider < 1) { divider = getSerInt(string01); switch (divider) { //handle incorrect entry case 1: break; case 8: break; case 64: break; case 256: break; case 1024: break; default: divider = 0; } } //here onwards only basic error correction inserting suitable default for incorrect entry pMode = getSerInt(string1); if (pMode != 1) pMode = 0; //allow for false entry, 0 is default pShape = getSerInt(string2); if ((pShape < 0) || (pShape > 2)) pShape = 0; if (pShape == 1) { Serial.println("*** Expect small errors (0.5%) due to high data rate setting on ADC ***"); } nMin = getSerInt(string3); if ((nMin < 0) || (nMin > 255)) nMin = 0; //allow nMin=255 for fixed or step if (pShape != 0) { //only need 2 values for step or ramp nMax = getSerInt(string4); if ((nMax < nMin) || (nMax > 255)) nMax = 255; ///ensure nMax > nMin } interval = getSerInt(string5); if (interval < 1) interval = 1; //prevent 0 error duration = 100 * interval; //so words make sense for fixed and step, and make duration in tenths of seconds // Serial.println(duration); stepInterval = interval; // make 100 points for fixed or step } void printSettings() { Serial.println(); //separate from lines above Serial.println("clock divider, pMode, pShape, nMin, nMax, interval or duration, step mV, Zero offset"); String str; Serial.println(str + divider + ", " + pMode + ", " + pShape + ", " + nMin + ", " + nMax + ", " + interval + ", " + vSlope + ", " + vOffset); } void printResults() { //for all shapes: Serial.print(t); Serial.print(", "); Serial.print(nOut); Serial.print(", "); // print expected results mVoltsOut = (vSlope * nOut) + vOffset; // adjusted ( step number * Step size) + zero offset Serial.print(mVoltsOut, 1); Serial.print(", "); //measure and print actual results nVolts = readADS(1); Serial.print(nVolts); Serial.print(", "); mVolts = convADS(nVolts); Serial.print(mVolts, 1); Serial.print(", "); Serial.println(mVolts - mVoltsOut); //end of one line of data } void adjustPWM() //corrections to match expected and measured values { // arrange step size and end adjustment for PCPWM or FPWM. if (pMode == 0) { //pcpwm vSlope = mVcc / 256; //pwm step size in mV vOffset = 0; //add to mVolts for low end correction } else { //fast pwm vSlope = mVcc / 256; //pwm step size in mV vOffset = vSlope; //add to mVolts for low end correction. NB there is a zero discontinuity - in fastPWM } Serial.println("PWM Error correction completed"); } float readADC(int which) { //ONLY IF NOT USING ADS1115: code will need changs. read chosen ADC, return average of nTimes readings float nVolts = 0; for (int i = 0; i < nTimes; i++) { nVolts += analogRead(which); delay(rDelay); } nVolts = nVolts / (float)nTimes; //find average return (nVolts); //average adc reading of voltage as a number } void printBits(byte myByte) { for (byte mask = 0x80; mask; mask >>= 1) { if (mask & myByte) Serial.print('1'); else Serial.print('0'); } } void setPwmFrequency(int pin, int divisor) { byte fmode; if (pin == 5 || pin == 6 || pin == 9 || pin == 10) { switch (divisor) { case 1: fmode = 0x01; break; case 8: fmode = 0x02; break; case 64: fmode = 0x03; break; case 256: fmode = 0x04; break; case 1024: fmode = 0x05; break; default: return; } if (pin == 5 || pin == 6) { TCCR0B = TCCR0B & 0b11111000 | fmode; } else { TCCR1B = TCCR1B & 0b11111000 | fmode; } } //ide doesnt recognise TCCR2 for Micro } void setPwmMode(int pin, int mode) { // mode: 0=pcpwm, 1=fastPWM, otherwise "normal" (return that timer to default) if (pin == 5 || pin == 6) { //Timer 0: CAUTION may affect delay and millis timing. if (mode == 0) { //pcpwm TCCR0A = TCCR0A | 0b00000001; // set WGM0 - 8 bit PWM TCCR0B = TCCR0B & 0b11100111; // clear WGM03, WGM02 to set pcpwm } else if (mode == 1) { //fast pwm TCCR0A = TCCR0A | 0b00000001; // set WGM0 - 8 bit PWM TCCR0B = TCCR0B | 0b00001000; //set WGM02 for fastpwm } else { // normal TCCR0A = TCCR0A & 0b11111110; // clear WGM0 - normal TCCR0B = TCCR0B & 0b11100111; // clear WGM03, WGM02 } } else { //Timer 1 if (mode == 0) { //pcpwm //Serial.println("timer 1 PC pwm"); TCCR1A = TCCR1A | 0b00000001; // set WGM11 - 8 bit PWM TCCR1B = TCCR1B & 0b11100111; // clear WGM12, WGM13 to set pcpwm } else if (mode == 1) { //fast pwm //Serial.println("timer 1 fast pwm"); TCCR1A = TCCR1A | 0b00000001; // set WGM11 - 8 bit PWM TCCR1B = TCCR1B | 0b00001000; //set fast pwm } else { // normal //Serial.println("timer 1 no pwm"); TCCR1A = TCCR1A & 0b11111110; // clear WGM11 - normal TCCR1B = TCCR1B & 0b11100111; // clear WGM12, WGM13 } /* // print TCCR1 values as binary Serial.print("TCCR1A is: "); printBits(TCCR1A); Serial.print(" TCCR1B is: "); printBits(TCCR1B); Serial.println(); */ } } // **** information on the ads1115 settings **** // The ADC input range (or gain) can be changed via the following // functions, but be careful never to exceed VDD +0.3V max, or to // exceed the upper and lower limits if you adjust the input range! // Setting these values incorrectly may destroy your ADC! // ADS1015 ADS1115 // ------- ------- // ads.setGain(GAIN_TWOTHIRDS); // 2/3x gain +/- 6.144V 1 bit = 3mV 0.1875mV (default) // ads.setGain(GAIN_ONE); // 1x gain +/- 4.096V 1 bit = 2mV 0.125mV // ads.setGain(GAIN_TWO); // 2x gain +/- 2.048V 1 bit = 1mV 0.0625mV // ads.setGain(GAIN_FOUR); // 4x gain +/- 1.024V 1 bit = 0.5mV 0.03125mV // ads.setGain(GAIN_EIGHT); // 8x gain +/- 0.512V 1 bit = 0.25mV 0.015625mV // ads.setGain(GAIN_SIXTEEN); // 16x gain +/- 0.256V 1 bit = 0.125mV 0.0078125mV /** Data rates */ /* #define RATE_ADS1015_128SPS (0x0000) ///< 128 samples per second #define RATE_ADS1015_250SPS (0x0020) ///< 250 samples per second #define RATE_ADS1015_490SPS (0x0040) ///< 490 samples per second #define RATE_ADS1015_920SPS (0x0060) ///< 920 samples per second #define RATE_ADS1015_1600SPS (0x0080) ///< 1600 samples per second (default) #define RATE_ADS1015_2400SPS (0x00A0) ///< 2400 samples per second #define RATE_ADS1015_3300SPS (0x00C0) ///< 3300 samples per second #define RATE_ADS1115_8SPS (0x0000) ///< 8 samples per second #define RATE_ADS1115_16SPS (0x0020) ///< 16 samples per second #define RATE_ADS1115_32SPS (0x0040) ///< 32 samples per second #define RATE_ADS1115_64SPS (0x0060) ///< 64 samples per second #define RATE_ADS1115_128SPS (0x0080) ///< 128 samples per second (default) #define RATE_ADS1115_250SPS (0x00A0) ///< 250 samples per second #define RATE_ADS1115_475SPS (0x00C0) ///< 475 samples per second #define RATE_ADS1115_860SPS (0x00E0) ///< 860 samples per second */ /** Divides a given PWM pin frequency by a divisor. The resulting frequency is equal to the base frequency divided by the given divisor: - Base frequencies: o The base frequency for pins 3, 9, 10, and 11 is 31250 Hz. o The base frequency for pins 5 and 6 is 62500 Hz. - Divisors: o The divisors available on pins 5, 6, 9 and 10 are: 1, 8, 64, 256, and 1024. o The divisors available on pins 3 and 11 are: 1, 8, 32, 64, 128, 256, and 1024. PWM frequencies are tied together in pairs of pins. If one in a pair is changed, the other is also changed to match: - Pins 5 and 6 are paired on timer0 - Pins 9 and 10 are paired on timer1 - Pins 3 and 11 are paired on timer2 Note that this function will have side effects on anything else that uses timers: - Changes on pins 3, 5, 6, or 11 may cause the delay() and millis() functions to stop working. Other timing-related functions may also be affected. - Changes on pins 9 or 10 will cause the Servo library to function incorrectly. Thanks to macegr of the Arduino forums for his documentation of the PWM frequency divisors. His post can be viewed at: https://forum.arduino.cc/index.php?topic=16612#msg121031 */