Reading of PWM-Driven Voltage Amplitude

Hello everyone- long-time visitor, first time poster here.

Firstly, thank you to all members of this forum for their attentive replies to those who have posted before me. I have only made it this far due to your kindness. I am having some final issues with my first Arduino project, and was hoping somebody could suggest some workarounds.

Sorry if my descriptions are poor- I am new to electronics and still have a lot to learn!

Description of circuits:

I have fabricated a microfluidic device with two platinum film heaters and three platinum RTD sensors (blue components in the attached schematic). The Arduino has four main functions:

1- Independent control of power to heaters:

The Arduino drives two MOSFETs using PWM pins, which allows an external (65 V) power supply to be throttled for each of the two heaters. This is the red part of the attached schematic, and works fine.

2- Measurement of heater power:

By measuring the voltage drop across the heater and across a shunt resistor, the resistance of the heater can be inferred. This is needed for calculation of heater power using V2/R. Since the power is supplied in pulsing format, the amplitude of the square wave can be multiplied by duty cycle to get the effective voltage across each component. The only way I could think to get the square wave amplitude was by taking a burst of measurements on each pin and using the maximum of these values as the square wave’s top. As the measured voltage will have a 65V max, a voltage divider (200k:15k) is used at each point to give signals in the range 0-4.9 V.

3- Measurement of RTD resistance:

The RTDs (resistance ranging from 10-20 ohms) were placed in series with 50-ohm resistors, and supplied with a 3.3V reference voltage. Measuring the voltage division here allows the resistance of the RTDs to be calculated.

4- Multiplexing of these signals:

I have 9 signals and will possibly increase the number of heaters and sensors in future devices, so the signals are multiplexed using a series of DPDT relay switches (green in the attached schematic). The schematic software I used did not have these relays, so they are represented by inductors and switches.

Using a control relay (MUX Control 1), the lower-tier relays (MUX Switch 1/2/3) are switched twice as frequently as the higher-tier relays (MUX Output 1/2). Readings are taken every time the low-tier relays are switched.

All voltages are measured using the internal (1.1V) bandgap, so should be very accurate. All connections are to a common ground.

Sketch:

// --------- CONSTANTS -----------------

// Heater parameters
const int nHeaters = 2;
const int gatePins[] = {9, 10}; // control pins for MOSFET gates
const long stepUpPeriods[] = {180000, 180000}; // time between voltage steps
const float stepUpFraction[] = {0.1, 0.1}; // fraction of overall voltage per step

// Relay parameters
const byte relayPins[] = {2, 3, 4}; // Relay control pins
const byte relayIn[] = {0, 1, 2, 5}; // Analog signals
const float readingPeriod = 1000.0; // Time between consecutive readings
const int internalSkipCount[] = {2, 2, 1}; // Change of relay happens after this number of internal periods.
const byte relayCount = 3;
byte relayTrigger[relayCount]; // Used to detect when all relays have been read and start printing on a new line.
float internalPeriod = readingPeriod / 4; // Used to determine how frequently the relays are updated.

// V measurement parameters
const int numMeasurements = 20; // Number of measurements in a burst
const int burstTime = 2; // ms between measurements in a burst

// ----------- VARIABLES --------------

// Heater variables
int PWMfreq[] = {1, 1}; // PWM frequency (will run up to 255)

// Relay variables
byte relayState[relayCount];
byte trigger = 0; // Turns to 1 when internal timer is triggered


// V measurements variables
float instReading = 0; // number of instantaneous readings
byte newInstReading = 0; // slightly delayed reading for comparison
float Vref = 5.0; // reference voltage of Arduino. Will modify using internal band gap for greater accuracy
int readIndex = 0;

// Timer variables
unsigned long currentMillis = 0; // stores curernt time in each loop iteration
unsigned long previousPWMMillis[] = {0, 0}; // stores time of last power step up
unsigned long previousRelayMillis = 0; // stores time of last relay trigger
unsigned long previousReadingMillis = 0; // stores time of last reading
// ==========================================

void setup() {
  Serial.begin(9600);

  // set all heaters to their initial power.
  for (byte n = 0; n < nHeaters; n++) {
    pinMode(gatePins[n], OUTPUT); // set PWM outputs to beginning
    analogWrite(gatePins[n], PWMfreq[n]); // set all PWM rates to beginning
  }

  // set relays to their initial positions.
  for (byte n = 0; n < relayCount; n++) {
    pinMode(relayPins[n], OUTPUT); // set relay digital pins as output
    digitalWrite(relayPins[n], LOW); // set relay digital pins to low
    relayState[n] = LOW; // set relay state counter to low
    relayTrigger[n] = 0;
  }

  // designate analog inputs
  for (byte n = 0; n < (sizeof(relayIn)); n++) {
    pinMode(relayIn[n], INPUT);
  }
  
}

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

void loop() {
  currentMillis = millis(); // capture current time

  // call each function sequentially
  updatePWMRate();
  relayChange();
}
// ========================================

void updatePWMRate() {
  for (byte n = 0; n < nHeaters; n++) { // runs through each of the heaters
    if (currentMillis - previousPWMMillis[n] >= stepUpPeriods[n]) { // time's up for this heater. Take action
      previousPWMMillis[n] += stepUpPeriods[n]; // update time of change
      PWMfreq[n] += (255 * stepUpFraction[n]); // update PWM frequency for this heater
      analogWrite(gatePins[n], PWMfreq[n]);

      if (PWMfreq[n] >= 255) {    // resets to beginning if we try to exceed the maximum duty cycle.
        PWMfreq[n] = 1;
      }
    }
  }
}

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

// Code to determine VCC:
long readVcc() {
  // Read 1.1V reference against AVcc
  // set the reference to Vcc and the measurement to the internal 1.1V reference
#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
  ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
  ADMUX = _BV(MUX5) | _BV(MUX0) ;
#else
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#endif

  delay(2); // Wait for Vref to settle
  ADCSRA |= _BV(ADSC); // Start conversion
  while (bit_is_set(ADCSRA, ADSC)); // measuring

  uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH
  uint8_t high = ADCH; // unlocks both

  long result = (high << 8) | low;

  result = 1125300L / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000
  return result; // Vcc in millivolts
}

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

void relayChange() {
  if (currentMillis - previousRelayMillis >= internalPeriod) { // time to update relay trigger
    previousRelayMillis += internalPeriod; // update time of change

    for (byte n = 0; n < relayCount; n++) { // runs through each of the relays
      relayTrigger[n]++; // adds one to the trigger
      if (relayTrigger[n] == internalSkipCount[n]) { // if trigger is at specified value for this relay, relay changes

        if (relayState[n] == LOW) { // update Relays
          relayState[n] = HIGH;
        } else
          relayState[n] = LOW;

        digitalWrite(relayPins[n], relayState[n]);
        relayTrigger[n] = 0;  // reset relay trigger now.
      }
    }
    // we want to measure voltages after each relay change
    voltageReader();
    Vref = readVcc();
    trigger++;
  }
  if (trigger == 4) {
    trigger = 0;
    Serial.print(PWMfreq[0]);
    Serial.print(",");
    Serial.print(PWMfreq[1]);
    Serial.print(",");
    Serial.print(Vref);
    Serial.println();
  }
}

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

void voltageReader() {
  delay(10);
  for (byte m = 0; m < 4; m++) { // return time-maximised values from each of the analog ports
    instReading = analogRead(m);

    while (readIndex < numMeasurements) { // cycle through the values and determine the largest
      delay(burstTime);
      newInstReading = analogRead(m);

      if (newInstReading > instReading) {
        instReading = newInstReading;
      }
      readIndex++;
    }
    readIndex = 0;
    Serial.print(instReading);
    Serial.print(",");
  }
}

Unfortunately, after attaching the code my post is too long. Please see the first reply to this post for my questions.

Thank you all.

My questions:

  • The measured voltages fluctuate slightly, giving large uncertainties since the following heater power is calculated using their ratios. I have attached an image of the Arduino's output, showing the unsteady nature of these values. Perhaps the reason for this is that I am taking the maximum of a burst of values, meaning the signal's ripple is included in my measurements. Is there another way to find the PWM amplitude? I wanted to use RC filters to smooth the readings, as this could possibly reduce the need to take bursts of readings- however, this would also alter the measured peak voltages. Is there a way to compensate for this?

  • RTD measurement is inaccurate. Replacing the RTDs with 220-ohm resistors gives readings between 200-225 ohms. In the attached results, it can be seen that the measured voltages are quite different from each other AND fluctuate. Neither of these should be the case as the test was carried out using identical 50-ohm resistors and with the 5V pin (measured accurately using the 1.1V internal band gap). Is there a way to stabilise the reading, and to tighten up the range? With 10-bit precision it should be possible to read the resistance to within 0.1% of its true value.

  • RTD heating: Joule heating from the 3.3V reference voltage causes the RTD sensors to warm up, which kind of defeats their purpose. How can I place some resistors to reduce the current drawn? Would a small (perhaps 10-ohm) resistor in parallel be an acceptable way to reduce RTD heating?

Many thanks to anybody who can help me. I understand that this is a large post with many questions, so any feedback to even nudge me in the right direction will be highly appreciated.

Firstly to measure the voltage do this:

  digitalWrite (pin, HIGH) ;
  // now take readings
  v = analogRead(...) ;
  i = analogRead (...) ;
  analogWrite (pin, value) ; // restore original PWM

So there's no timing uncertainty - so long as you don't do this too often it shouldn't upset the heating rate
much.

Reading fluctuation suggests the power rail is not stable.

The 3.3V output is low current, not suitable, use an external 3.3V regulator capable of high enough
currents for your references, and switch its input supply off when not measuring to avoid self-heating.

Hi @MarkT,

Thank you for your kind suggestion. Please could you clarify- do you mean I should apply this to the driving pins (D9 and D10) so that the signal becomes constant for the duration of measurement?

Do you have any suggestion for stabilising the power rail? I have access to some more powerful DC supply units so would you recommend I supply excess voltage and use linear regulators to drop them to my 65V target?

And finally- when you say,

use an external 3.3V regulator capable of high enough
currents for your references

do you mean I should draw the RTD voltage from the 5V pin then regulate down to 3.3V using something like a buck converter? Or is an additional external power supply necessary?

Again, thank you for your help. I'll get to work on these suggestions and hopefully make a breakthrough in the next couple of days!