Friday 7 March 2014

Arduino analogWrite with higher resolution

The other day I was troubleshooting a project that did not work as intended, so I eventually put down the problem as inadequate resolution. Even after averaging analogReads, reducing truncation errors and optimising everything it did not work much better.

Of course, ultimately the PWM analogWrite resolution is 8 bit on the Arduino UNO and my project did not even use it's full range, which limited it even more.

After some looking around I actually found out the atmega328 chip supports much higher PWM accuracy, up to 16 bit if necessary, but only on pins controlled by timer1. So I came up with a modified PWM routine that uses 10 bit resolution. (the 16 bit is a bit overkill once considering supply variations)


This is used by calling analogWrite10bit( pin , value); just like the original one but with 0 - 1023 range; Only timer1 pins can be used. That makes for pins 9 , 10 on the UNO and other atmega328 Arduinos. Once called even for one pin you can't use normal pwm on EITHER pins, so you have to use this routine for both.

Not tested on mega, leonardo but might work on corresponding timer1 pins.

//
//      GNU LESSER GENERAL PUBLIC LICENSE
//        Version 2.1, February 1999
//
//
// this was modified (slightly) from the arduino source code
//
// works on pins 9 , 10 on Arduino UNO
// once used *both* pins are set to 10 bit mode and "normal" analogWrite will not give correct results.
// uses timer one

#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

void analogWrite10bit(uint8_t pin, long val)
{
  // We need to make sure the PWM output is enabled for those pins
  // that support it, as we turn it off when digitally reading or
  // writing with them. Also, make sure the pin is in output mode
  // for consistenty with Wiring, which doesn't require a pinMode
  // call for the analog output pins.

  // set 10 bit mode

  sbi (TCCR1A , WGM11 );
 
  pinMode(pin, OUTPUT);
  if (val == 0)
  {
    digitalWrite(pin, LOW);
  }
  else if (val >= 1023)
  {
    digitalWrite(pin, HIGH);
  }
  else
  {
    switch(digitalPinToTimer(pin))
    {

#if defined(TCCR1A) && defined(COM1A1)
    case TIMER1A:
      // connect pwm to pin on timer 1, channel A
      sbi(TCCR1A, COM1A1);
      OCR1A = val;
      break;
#endif

#if defined(TCCR1A) && defined(COM1B1)
    case TIMER1B:
      // connect pwm to pin on timer 1, channel B
      sbi(TCCR1A, COM1B1);
      OCR1B = val;
      break;
#endif

//    case NOT_ON_TIMER:
//    break;
  
    default:
    analogWrite(pin , val / 4);
 
    }
  }
}