ATMega64A Odd PWM behaviors

Go To Last Post
17 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Hello, I've run into a problem that I can't seem to solve using PWM on an ATMega64A.

 

I initialize PWM using the following code:

//Setup servo PWM outputs
DDRB |= SERVO_SIGNAL_B | SERVO_SIGNAL_A; //Enable outputs for servo signal lines
TCCR1A |= _BV(COM1A1) | _BV(COM1A0) | _BV(COM1B1) | _BV(COM1B0) | _BV(WGM11); //Inverting mode fast pwm
TCCR1B |= _BV(WGM13) | _BV(WGM12) | _BV(CS11); //Clock pre-scaler to 8
//Set 20ms period for servo PWM
ICR1H = 0x9C;
ICR1L = 0x40;

Then I set the duty cycle using the following code (it's translating radians to the correct pulse width:

float servoA = 1.3f;
float servoB = 3.0f;

uint16_t pos = (uint16_t)(40000 - (1800 + (servoA / 18000) * 2400));
OCR1AH = pos >> 8;
OCR1AL = pos & 0xFF;
    
pos = (uint16_t)(40000 - (1800 + (servoB / 18000) * 2400));
OCR1BH = pos >> 8;
OCR1BL = pos & 0xFF;

This works fine, but when I put the same exact code into a separate function:

void setServoPositions(float servoA, float servoB)
{
    uint16_t pos = (uint16_t)(40000 - (1800 + (servoA / 18000) * 2400));
    OCR1AH = pos >> 8;
    OCR1AL = pos & 0xFF;
        
    pos = (uint16_t)(40000 - (1800 + (servoB / 18000) * 2400));
    OCR1BH = pos >> 8;
    OCR1BL = pos & 0xFF;
}

It does not work. Using this function with any input causes the PWM pins to be high all the time. Why is the function different than the code in the main body?

This topic has a solution.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I ran your calculations for servo=1.3 and servo=3.0.  I get 38119.8267 and 38199.6 (the same integer value!)

 

Did you add or lose a few zeros in the editor along the way?

 

And I have to say I hate all the float math you're doing here.  It's not illegal, but it seems grossly inefficient for an 8-bit processor.  To be fair, others will argue the other way, so I want you to understand this is just one opinion.  But what I think everyone would agree with is that you should mark constants that get promoted to float __as__ float.  I don't want to have to sit and think - let's see, servo converts 18000 to float, then the float result converts 2400, then...

 

For that matter, you should replace magic numbers with understandable defines or constant names.

 

Where are the radian values coming from in the first place?  That's an important consideration.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The 1.3 and 3.0 values are just random sample values that I chose to see if the PWM worked. When I sent the same values over the function that does the same thing it didn't work.

 

I had written an integer version of this code, but ran into a different odd issue. When dividing by a non-power of 2, the PWM signal goes high forever (when you use that value for OCRnA/B).

 

My mistake, that should have been

(uint16_t)(40000 - (1800 + (servoA / M_PI) * 2400));

 

Last Edited: Sun. Nov 9, 2014 - 02:32 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

OK, let's back up and start over.  Where do the radian values come from?  Do they have to be float, or are you converting integer calculations to float along the way?

 

What is the valid range of radian values?

 

I presume from your comments that you are controlling an RC servo.  Is this correct?  What are the max and min pulse widths you want to achieve (these will be tied to the max and min radian values given above).

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Yes, I am controlling an RC servo. The radian values is for range 0 to PI, but could be specified in anything. Pulsewidth is from 0.9ms to 2.1ms which which works correctly when I specify the angle directly in initPins(), but when I use the function setServoAngle() to do it, it makes the PWM pins always high.

 

The count 40000 comes from 16MHz/8(prescaler) / 50Hz. 1800 counts equals to 0.9ms and 4200 counts equals 2.1ms.

 

In a variant of this code, I did this in an integer representing 0.01 degree units resulting in the following code:

    uint16_t pos = (uint16_t)(40000 - (1800 + (servoA / 18000) * 2400));
    OCR1AH = pos >> 8;
    OCR1AL = pos & 0xFF;
    
    pos = (uint16_t)(40000 - (1800 + (servoB / 18000) * 2400));
    OCR1BH = pos >> 8;
    OCR1BL = pos & 0xFF;

For range 0 to 180 degrees, but the division here by 18000, not a power of 2, resulted in the same issue as the floating point version.

Last Edited: Sun. Nov 9, 2014 - 03:07 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

OK, your hardware resolution is 4200-1800, or 2400 counts, for 180 degrees (correct?).  That means essentially a working resolution of about 1/10 of a degree.  So what if you had an input from 0 to 1800 (180 degrees, in tenths)?  Would that be acceptable?

 

Next, why are you using inverted PWM and starting the pulse near the end of the PWM cycle?  I don't understand that.  Why not just have normal PWM that goes to 1 at count=0, and goes to 0 at some value between 1800 and 4200?

 

If so, I would do

 

uint16_t tenths = 900U; //90 degrees
uint16_t pos = (1800U + (uint16_t)((uint32_t)tenths * 2400UL)/1800UL);

... but seeing that 2400UL/1800UL is just 4/3, and that 4*1800 is < 65536...

uint16_t pos = 1800U + ((4*tenths)/3);

The first version promotes 'tenths' to unsigned long so the multiply by 2400 does not overflow, then divides by 1800.  This is your basic scale equation:

pos = 1800 + (2400*tenths)/1800;

 

But 2400/1800 is just 4/3, so we can reduce.  Now, the maximum value for tenths is 1800, and 1800 * 4 is only 7200, far below any 16-bit unsigned overflow (65536).  So the second version does not need to bring in any 32-bit values or math operations.  Short and fast!

 

Thoughts?

Last Edited: Sun. Nov 9, 2014 - 03:24 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

the radian values is for range 0 to PI

...

For range 0 to 180 degrees,

So which are you really using, radians or degrees? If you use radians, then (servoA / 18000) * 2400 will always result in a value  less than 1, so pos will always be 38200. And even with a range of 0 - 180,  pos will be in the range of 38200 to 38224, so you are really not going to see much change in the duty cycle.

Regards,
Steve A.

The Board helps those that help themselves.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Note that if you are in charge of the radian scaling throughout the program, you could just scale to 2400 counts = 180 degrees immediately, and stick the resulting value into the PWM register without any further math.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Let me clarify, in the case where I am using 18000 as a denominator, servoA takes a value of 0 to 18000 representing 0.01 degrees.

 

My main concern is not how I am calculating the OCR values. It is about the bug that I am experiencing.

 

Why does having the same exact code in a separate function cause PWM to always be high?

Why does doing integer division by a non-power of 2 and then assigning the result to OCR result in PWM being always high?

Last Edited: Sun. Nov 9, 2014 - 03:52 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I would run the code in the simulator and break down the math into one operation per line:

 

float temp = servo / PI;

temp *= 2400;

temp += 1800;

temp = 40000 - temp;

 

I suspect it has something to do with when int values are promoted to float, though I can't see it offhand.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Going off of your code, I am hitting the same issue. The function that sets the PWM is now:

void setServoPosition(uint16_t servoA, uint16_t servoB)
{
    uint16_t pos = 1800U + (servoA * 4)/3;
    OCR1AH = pos >> 8;
    OCR1AL = pos & 0xFF;

    pos = 1800U + (servoB * 4)/3;;
    OCR1BH = pos >> 8;
    OCR1BL = pos & 0xFF;
}

Where servoA and servoB are tenths of an inch. I've also switched to non-inverting mode.

 

Using this function with any angle causes the PWM signal to always be low. Writing the same code in the body of my init function gives correct behavior however. Why is this? Even stranger, dividing by 2 works, but not dividing by 3.

Last Edited: Sun. Nov 9, 2014 - 05:57 AM
This reply has been marked as the solution. 
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Okay, I figured it out. Setting the entire 16bit register directly solves the problem. My guess is that there is some weird timing issues that occur when you set the high then low bits.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Wil.7ang wrote:

Okay, I figured it out. Setting the entire 16bit register directly solves the problem. My guess is that there is some weird timing issues that occur when you set the high then low bits.

It is always best to set the entire 16-bit register directly, if your language allows it.  This is both for clarity (IMO) and because many AVR 16-bit registers require a particular order of reading and writing.  Most that require a particular order (but check!) require reads in the order L,H, but writes in the opposite order H,L.

 

So you were writing in the correct order (OCR1A,B do require the correct order of access), according to the code you posted.  Did you not post the same code you were testing?

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The problem was also compounded by the fact that my clock fuses were not setup properly for my external crystal. I noticed that anything called after the first function call did not have any effect.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Wil.7ang wrote:

The problem was also compounded by the fact that my clock fuses were not setup properly for my external crystal. I noticed that anything called after the first function call did not have any effect.

How much SRAM does your compiler report being used?  This sounds like you're out of SRAM.  Or maybe you're compiling for the wrong AVR?

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Okay, I figured it out.

No you didn't. Neither the setting of the register or the clock fuses have any bearing whatsoever on your problem. Look deeper.

Regards,
Steve A.

The Board helps those that help themselves.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Using the same exact code and switching to internal oscillator, it worked. So I tried the external crystal with a shorter startup time and now everything is working properly.