Reading RC input and generating 1KHz PWM output on Attiny85 (Trinket)

Righto, so someone on an electric skateboard forum asked for some help controlling an LED controller from and RC receiver channel. The LED controller wants a 1KHz PWM signal to set the brightness of the LED. I offered to help, but not having an Attiny85 myself I had to do a bit of googling and guessing. Eventually by going back and forth a few times (I send some new code, he tells me what error he gets) we got it compiling and working to some extent. It does successfully control the brightness of the LED, but for some reason it goes in 4 distinct steps instead of smoothly. We're not sure why but we just decided to ignore that for the time being.

Then the guy asked if we could implement a way read in the throttle and control a brake light. I thought that was a great idea, so I modified the code to support two channels. Obviously it will need some more code to check if the throttle is at less than half to enable the brake light, but we're just starting with getting it working on two channels first. The guy tested the 2-channel code and found that the signal is now only working on about 5% of the range of his servo tester, so when he changes the "map" from 1000=0, 2000=100 to 1500=0, 1600=100 the LED changes brightness smoothly and across its whole brightness range, though the servo tester still only moves over about 5% of its range. It's strange that a) it no longer changes brightness in 4 distinct steps, and b) only 5% of the servo tester's range is usable. I'm fairly confused at this point. Here's the 2-channel code:

#include "avr/interrupt.h"

volatile unsigned long rc_timeStart1;
volatile unsigned long rc_timeStart2;
volatile int rc_value1;
volatile int rc_value2;
volatile int rcPin1LastState = 0;
volatile int rcPin2LastState = 0;

int percentage1 = 0;
int percentage2 = 0;
unsigned long lastPwmStart1 = 0;
unsigned long lastPwmStart2 = 0;
const byte pwmPin1 = 0;
const byte pwmPin2 = 1;
const byte rcPin1 = 2;
const byte rcPin2 = 3;

void setup() {
  pinMode(rcPin1, INPUT);
  pinMode(rcPin2, INPUT);
  pinMode(pwmPin1, OUTPUT);
  pinMode(pwmPin2, OUTPUT);

  GIMSK = 0b00100000; //turn on pin change interrupts
  PCMSK = 0b00001100; //turn on interrupts on pin PB2 and PB3 (physical pins 2 & 3)
  sei();
}

void loop() {
  percentage1 = map(rc_value1, 1000, 2000, 0, 100); //assume the receiver is outputting exactly in spec between 1000 and 2000; it's probably not but should be close enough
  percentage2 = map(rc_value2, 1000, 2000, 0, 100);
  percentage1 = constrain(percentage1, 0, 100);
  percentage2 = constrain(percentage2, 0, 100);
  doPWM1();
  doPWM2();
}

ISR(PCINT0_vect)
{
  if(digitalRead(rcPin1) == HIGH) {
    if(rcPin1LastState == 0) {
      rc_timeStart1 = micros();
      rcPin1LastState = 1;
    }
  } else {
    if(rcPin1LastState == 1) {
      rc_value1 = micros() - rc_timeStart1;
      rcPin1LastState = 0;
    }
  }
  if(digitalRead(rcPin2) == HIGH) {
    if(rcPin2LastState == 0) {
      rc_timeStart2 = micros();
      rcPin2LastState = 1;
    }
  } else {
    if(rcPin2LastState == 1) {
      rc_value2 = micros() - rc_timeStart2;
      rcPin2LastState = 0;
    }
  }
}

void doPWM1() {
  //1KHz PWM signal means beginning a new pulse every 1000 microseconds or 1 millisecond
  //then we just vary the length of the pulse between 0 and 1000 microseconds
  unsigned long microseconds = micros();
  if(microseconds - lastPwmStart1 >= 1000) { //begin new pulse
    lastPwmStart1 = microseconds;
    digitalWrite(pwmPin1, HIGH);
  } else {
    int targetLength = percentage1 * 10; //0 - 100 times 10 will be 0 - 1000
    if(microseconds - lastPwmStart1 >= targetLength) { //reached the target pulse length, time to turn off the pin
      digitalWrite(pwmPin1, LOW);
    }
  }
}

void doPWM2() {
  unsigned long microseconds = micros();
  if(microseconds - lastPwmStart2 >= 1000) { //begin new pulse
    lastPwmStart2 = microseconds;
    digitalWrite(pwmPin2, HIGH);
  } else {
    int targetLength = percentage2 * 10; //0 - 100 times 10 will be 0 - 1000
    if(microseconds - lastPwmStart2 >= targetLength) { //reached the target pulse length, time to turn off the pin
      digitalWrite(pwmPin2, LOW);
    }
  }
}

So basically I would just like someone who knows a bit more about interrupts and generating PWM signals (and has some free time) to give my code a quick once-over and see if there's any obvious flaws. It's pretty simple code but I can't see anything wrong with it. I got it working on a couple of Unos last night (one had the above code and was hooked up to a regular old LED and the other was generating a servo signal) and it seemed to work perfectly. I'm just not sure how I can go about debugging it when I'm in Australia and the guy with the RC receiver, servo tester, Attiny85 and oscilloscope is in the US.

I realise a Trinket isn't an Arduino, but given that the register names and pinouts are really the only differences I'm hoping no one minds :wink:

You do know that pin 0 and 1 support hardware PWM, don’t you? I would change the PWM pins to pin 0 and 4 so you can use timer 1 as timer 0 is used for the Arduino timing support (micros(), millis() and the like).

Regarding you two questions, I would attach a scope and check the input signal. It looks to me that you don’t get a standard RC PWM signal but that’s just a guess. If possible, post a picture of the signal at 0%, 25% and 100% RC input value.

Change

volatile int rcPin1LastState = 0;
volatile int rcPin2LastState = 0;

to

volatile byte rcPin1LastState = 0;
volatile byte rcPin2LastState = 0;

That save a few cycles when assigning values to the variable.

I know about the hardware PWM, but I don't think that helps me, does it? It has to be a 1KHz PWM signal and I don't think it's possible to generate a hardware 1KHz signal. Correct me if I'm wrong.

Not sure what you mean about using timer 1 as timer 0 or what the goal of that would be.

I don't have a scope or an Attiny85, but the esk8 guy luckily does have a scope and already sent me some photos demonstrating the signals. The RC receiver signal appears correct, and he said it slides smoothly from min to max. Minimum on RC receiver:

Maximum on the RC receiver:

He also said that the PWM signal only works in 4 steps: 25%, 50%, 75% and 100%. As in, the signal moves in jumps without sliding smoothly. 25% PWM output:

50% PWM output:

75% PWM output:

Good point on using a byte for the last pin state too, thanks.

I don't think the limitation to four steps is caused by accumulated time error, but it would not hurt to change this

//lastPwmStart1 = microseconds;
lastPwmStart1 += 1000;
//lastPwmStart2 = microseconds;
lastPwmStart2 += 1000;

If you do this, ensure that the start times begin at the first micros() value, and not at 0, or it may take awhile to catch up.

cattledog:
I don't think the limitation to four steps is caused by accumulated time error, but it would not hurt to change this

//lastPwmStart1 = microseconds;

lastPwmStart1 += 1000;
//lastPwmStart2 = microseconds;
lastPwmStart2 += 1000;




If you do this, ensure that the start times begin at the first micros() value, and not at 0, or it may take awhile to catch up.

I'm confused, surely that would remove the guarantee of a 1KHz signal? The only way it would accumulate error is if the uC couldn't keep up (which I would be pretty surprised about, I can't imagine it would be too slow for this code). With your change it would be more inclined to generate a slightly-slower-than-1KHz signal. Actually, to make it as close to 1KHz as possible I would want to change this:

lastPwmStart1 = microseconds;
digitalWrite(pwmPin1, HIGH);

...to this:

digitalWrite(pwmPin1, HIGH);
lastPwmStart1 = micros();

That way the recorded start time of the last pulse is as close to the actual start of the last pulse as possible. With your way, you assume that the code runs at exactly 1000 microseconds since the start of the last pulse and take 0 time to run. For instance, it might only happen to run the doPwm(); function at 1084 microseconds-

wait

You're absolutely right, I'm an idiot. Good catch. The new code looks like this:

#include "avr/interrupt.h"

volatile unsigned long rc_timeStart1;
volatile unsigned long rc_timeStart2;
volatile int rc_value1;
volatile int rc_value2;
volatile byte rcPin1LastState = 0;
volatile byte rcPin2LastState = 0;

int percentage1 = 0;
int percentage2 = 0;
unsigned long lastPwmStart1 = 0;
unsigned long lastPwmStart2 = 0;
const byte pwmPin1 = 0;
const byte pwmPin2 = 1;
const byte rcPin1 = 2;
const byte rcPin2 = 3;

void setup() {
  pinMode(rcPin1, INPUT);
  pinMode(rcPin2, INPUT);
  pinMode(pwmPin1, OUTPUT);
  pinMode(pwmPin2, OUTPUT);

  GIMSK = 0b00100000; //turn on pin change interrupts
  PCMSK = 0b00001100; //turn on interrupts on pin PB2 and PB3 (physical pins 2 & 3)
  sei();
  
  lastPwmStart1 = micros();
  lastPwmStart2 = micros();
}

void loop() {
  percentage1 = map(rc_value1, 1000, 2000, 0, 100); //assume the receiver is outputting exactly in spec between 1000 and 2000; it's probably not but should be close enough
  percentage2 = map(rc_value2, 1000, 2000, 0, 100);
  percentage1 = constrain(percentage1, 0, 100);
  percentage2 = constrain(percentage2, 0, 100);
  doPWM1();
  doPWM2();
}

ISR(PCINT0_vect)
{
  if(digitalRead(rcPin1) == HIGH) {
    if(rcPin1LastState == 0) {
      rc_timeStart1 = micros();
      rcPin1LastState = 1;
    }
  } else {
    if(rcPin1LastState == 1) {
      rc_value1 = micros() - rc_timeStart1;
      rcPin1LastState = 0;
    }
  }
  if(digitalRead(rcPin2) == HIGH) {
    if(rcPin2LastState == 0) {
      rc_timeStart2 = micros();
      rcPin2LastState = 1;
    }
  } else {
    if(rcPin2LastState == 1) {
      rc_value2 = micros() - rc_timeStart2;
      rcPin2LastState = 0;
    }
  }
}

void doPWM1() {
  //1KHz PWM signal means beginning a new pulse every 1000 microseconds or 1 millisecond
  //then we just vary the length of the pulse between 0 and 1000 microseconds
  unsigned long microseconds = micros();
  if(microseconds - lastPwmStart1 >= 1000) { //begin new pulse
    lastPwmStart1 += 1000;
    digitalWrite(pwmPin1, HIGH);
  } else {
    int targetLength = percentage1 * 10; //0 - 100 times 10 will be 0 - 1000
    if(microseconds - lastPwmStart1 >= targetLength) { //reached the target pulse length, time to turn off the pin
      digitalWrite(pwmPin1, LOW);
    }
  }
}

void doPWM2() {
  unsigned long microseconds = micros();
  if(microseconds - lastPwmStart2 >= 1000) { //begin new pulse
    lastPwmStart2 += 1000;
    digitalWrite(pwmPin2, HIGH);
  } else {
    int targetLength = percentage2 * 10; //0 - 100 times 10 will be 0 - 1000
    if(microseconds - lastPwmStart2 >= targetLength) { //reached the target pulse length, time to turn off the pin
      digitalWrite(pwmPin2, LOW);
    }
  }
}

I know about the hardware PWM, but I don't think that helps me, does it? It has to be a 1KHz PWM signal and I don't think it's possible to generate a hardware 1KHz signal. Correct me if I'm wrong.

If you set the prescaler config to 1110 (register TCCR1) you get about 980Hz (given the internal oscillator is precise enough) which probably would be accepted by you LED controller. It's most probably much more precise than your code. With that you get a resolution of 256 which is much more than your solution will ever reach.

The RC signal on the scope doesn't show enough resolution unfortunately so it's quite difficult to tell the timing from the photos. I hoped to see picture from a DSO where the timing would be clearly visible.

Sorry, I should have mentioned, the timebase was ~5ms/div for the RC and ~500us/div for the PWM. It looks like a correct RC signal to me.

I honestly have no idea how I would use the timer once I set its prescaler. Sounds like it's time for me to go and learn more low-level AVR chip stuff! :wink:

I honestly have no idea how I would use the timer once I set its prescaler

Once you set the prescaler, you should be able to use analogWrite(pin,value) with the two output pins for Timer1. (PB1,PB4) and values 0-255.

Wait so analogWrite uses Timer1? How would I find that kind of information? Oh nvm I found it in the datasheet for the Attiny85. And in reading that I also understand where @pylon got the 1110 prescaler bits. Does the Arduino micros() function use Timer0? This seems like a way better solution than software PWM.

Does the Arduino micros() function use Timer0?

Yes.

Alrighty so the latest code looks like this. Let's see if that works :slight_smile:

#include "avr/interrupt.h"

volatile unsigned long rc_timeStart1;
volatile unsigned long rc_timeStart2;
volatile int rc_value1;
volatile int rc_value2;
volatile byte rcPin1LastState = 0;
volatile byte rcPin2LastState = 0;

int percentage1 = 0;
int percentage2 = 0;
const byte pwmPin1 = 1;
const byte pwmPin2 = 4;
const byte rcPin1 = 0;
const byte rcPin2 = 2;

void setup() {
  pinMode(rcPin1, INPUT);
  pinMode(rcPin2, INPUT);
  pinMode(pwmPin1, OUTPUT);
  pinMode(pwmPin2, OUTPUT);

  GIMSK = 0b00100000; //turn on pin change interrupts
  PCMSK = 0b00000101; //turn on interrupts on pin PB0 and PB2 (physical pins 0 & 2)
  sei();
  
  TCCR1 = 0b00001110; //set the clock select bits of the Timer1 control register to make it run at ~980Hz so that our PWM output is close to 1KHz
}

void loop() {
  percentage1 = map(rc_value1, 1000, 2000, 0, 255); //assume the receiver is outputting exactly in spec between 1000 and 2000; it's probably not but should be close enough
  percentage2 = map(rc_value2, 1000, 2000, 0, 255);
  percentage1 = constrain(percentage1, 0, 255);
  percentage2 = constrain(percentage2, 0, 255);
  analogWrite(pwmPin1, percentage1);
  analogWrite(pwmPin2, percentage2);
}

ISR(PCINT0_vect)
{
  if(digitalRead(rcPin1) == HIGH) {
    if(rcPin1LastState == 0) {
      rc_timeStart1 = micros();
      rcPin1LastState = 1;
    }
  } else {
    if(rcPin1LastState == 1) {
      rc_value1 = micros() - rc_timeStart1;
      rcPin1LastState = 0;
    }
  }
  if(digitalRead(rcPin2) == HIGH) {
    if(rcPin2LastState == 0) {
      rc_timeStart2 = micros();
      rcPin2LastState = 1;
    }
  } else {
    if(rcPin2LastState == 1) {
      rc_value2 = micros() - rc_timeStart2;
      rcPin2LastState = 0;
    }
  }
}
const byte pwmPin1 = 0;
const byte pwmPin2 = 4;

Check the data sheets and the pin outs, but I think that the non inverted (OC1A,OC1B) pwm output pins for timer1 are PB1 and PB4.

Oh whoops you're right, I'll edit my post with the corrected code (although I think I have to wait 5 minutes between posts and edits count as posts, which is a bit dumb).

 PCMSK = 0b00001100; //turn on interrupts on pin PB2 and PB3 (physical pins 2 & 3)

Not consistent with

const byte rcPin1 = 0;
const byte rcPin2 = 2;

Why did you change from the interrupts which appeared to be working ok

const byte rcPin1 = 2;
const byte rcPin2 = 3;

Geez I'm so bad at this. I changed the interrupt pins because I want to avoid using the USB pins if possible. I have to use one of them (PB4) but don't have to use PB3, so I changed it. Didn't think to change the interrupt register though.

Ok, I will fix the code in 5 minutes (that post limit is getting rather annoying).

Boom! That latest code works perfectly! Thanks so much for the help @pylon and @cattledog! I've got a much better understanding of AVR timers too, so that's good.

Boom! That latest code works perfectly!

Nice job working your way through this.

Do you have an electric skateboard as well as the guy you were coding for? From what I've seen of them on YouTube, the long boards with the remotes look pretty nifty.

Thanks, yeah I do have an esk8 but it's very unfinished, still in development. Waiting on some new pulleys from Canada. Mine's not a longboard, it's just a cheapo cruiser (or shortboard as some people call them). I rode it once on the paths around town and it was heaps of fun but the belt was slipping (for some reason I went with a 9mm wide belt, way too narrow when I have a single powerful motor and a 110kg guy riding) and the wheels were too small (63mm.... bought some shiny new 90mm wheels). Then I haven't ridden it again since, waiting for parts. You can pretty much stick a motor and some batteries on any kind of skateboard.

Still need to put a capacitor on the power input to the receiver because it's just an Arduino Pro Mini with an NRF radio module (I forget exactly which one) and I think it browned out under constant acceleration up a hill (it threw me off but I managed to stay upright).

If you're interested in esk8s you should check out this forum: http://www.electric-skateboard.builders/