erlerobot / smart_motor

4 stars 2 forks source link

PWM module #30

Closed jlamperez closed 8 years ago

jlamperez commented 8 years ago

PWM module

pwm

PWM.h

In this file we have the 4 public implemented functions and the pwm_enable and pwm_disable functions to enable or disable PWM register flag.

void pwm_registers_defaults(void);
void pwm_init(void);
void pwm_update(uint16_t position, int16_t pwm);
void pwm_stop(void);
inline static void pwm_enable(void)
{
    uint8_t flags_lo = registers_read_byte(REG_FLAGS_LO);

    // Enable PWM to the servo motor.
    registers_write_byte(REG_FLAGS_LO, flags_lo | (1<<FLAGS_LO_PWM_ENABLED));
}

inline static void pwm_disable(void)
{
    uint8_t flags_lo = registers_read_byte(REG_FLAGS_LO);

    // Disable PWM to the servo motor.
    registers_write_byte(REG_FLAGS_LO, flags_lo & ~(1<<FLAGS_LO_PWM_ENABLED));

    // Stop now!
    pwm_stop();
}

PWM.c

Implementation of the PWM.h functions.

If it is not the encoder enabled, the maximum position is defined to:

#define MAX_POSITION  (1023)

To determine the top value for timer/counter1 from the frequency divider:

#define PWM_TOP_VALUE(div)      ((uint16_t) div << 4) - 1;
//If div is 0x0040(64) using this in the end we have 1023.

Determines the compare value associated with the duty cycle for timer/counter1.

#define PWM_OCRN_VALUE(div,pwm) (uint16_t) (((uint32_t) pwm * (((uint32_t) div << 4) - 1)) / 255)
#define DELAYLOOP 20 // TODO: This needs to be adjusted to account for the clock rate.
                     //       This value of 20 gives a 5 microsecond delay and was chosen
                     //       by experiment with an "MG995".
inline static void delay_loop(int n)
{
    uint8_t i;
    for(i=0; i<n; i++)
    {
        asm("nop");
    }
}

Determine the duty cycle value for the timer.

uint16_t duty_cycle = PWM_OCRN_VALUE(pwm_div, pwm_duty);

Disable interrupts.

cli();

If we need to reconfigure PWM output for direction A:

// Set SMPLn_B (PD4) and SMPLn_A (PD7) to high.
        PORTD |= ((1<<PD4) | (1<<PD7));

        // Set EN_B (PD3) to low.
        PORTD &= ~(1<<PD3);

        // Disable PWM_A (PB1/OC1A) and PWM_B (PB2/OC1B) output.
        // NOTE: Actually PWM_A should already be disabled...
        TCCR1A = 0;

        // Make sure PWM_A (PB1/OC1A) and PWM_B (PB2/OC1B) are low.
        PORTB &= ~((1<<PB1) | (1<<PB2));

        // Give the H-bridge time to respond to the above, failure to do so or to wait long
        // enough will result in brownouts as the power is "crowbarred" to varying extents.
        // The delay required is also dependant on factors which may affect the speed with
        // which the MOSFETs can respond, such as the impedance of the motor, the supply
        // voltage, etc.
        //
        // Experiments (with an "MG995") have shown that 5microseconds should be sufficient
        // for most purposes.
        //
        delay_loop(DELAYLOOP);

        // Enable PWM_A (PB1/OC1A)  output.
        TCCR1A |= (1<<COM1A1);

        // Set EN_A (PD2) to high.
        PORTD |= (1<<PD2);

        // NOTE: The PWM driven state of the H-bridge should not be switched to b-mode or braking
        //       without a suffient delay.

        // Reset the B direction flag.
        pwm_b = 0;

else

    // Update the A direction flag.  A non-zero value keeps us from
    // recofiguring the PWM output A when it is already configured.
    pwm_a = pwm_duty;

    // Update the PWM duty cycle.
    OCR1A = duty_cycle;
    OCR1B = 0;

    // Restore interrupts.
    sei();

    // Save the pwm A and B duty values.
    registers_write_byte(REG_PWM_DIRA, pwm_a);
    registers_write_byte(REG_PWM_DIRB, pwm_b);

In the application DEFAULT_PWM_FREQ_DIVIDER is set to 0x0040(64).

    // PWM divider is a value between 1 and 1024.  This divides the fundamental
    // PWM frequency (500 kHz for 8MHz clock, 1250 kHz for 20MHz clock)(div=16) by a
    // constant value to produce a PWM frequency suitable to drive a motor.  A
    // small motor with low inductance and impedance such as those found in an
    // RC servo will my typically use a divider value between 16 and 64.  A larger
    // motor with higher inductance and impedance may require a greater divider.
    registers_write_word(REG_PWM_FREQ_DIVIDER_HI, REG_PWM_FREQ_DIVIDER_LO, DEFAULT_PWM_FREQ_DIVIDER);

Initialize the pwm frequency divider value.

pwm_div = registers_read_word(REG_PWM_FREQ_DIVIDER_HI, REG_PWM_FREQ_DIVIDER_LO);

Set EN_A (PD2) and EN_B (PD3) to low, putting them to 1 then doing a one complement and an and operation.

PORTD &= ~((1<<PD2) | (1<<PD3));

Set SMPLn_B (PD4) and SMPLn_A (PD7) to high for Back_EMF measure.

PORTD |= ((1<<PD4) | (1<<PD7));

Enable PD2, PD3, PD4 and PD7 as outputs.

DDRD |= ((1<<DDD2) | (1<<DDD3) | (1<<DDD4) | (1<<DDD7));

Set PWM_A (PB1/OC1A) and PWM_B (PB2/OC1B) to low.

PORTB &= ~((1<<PB1) | (1<<PB2));

Enable PB1/OC1A and PB2/OC1B as outputs.

DDRB |= ((1<<DDB1) | (1<<DDB2));

Reset the timer1 configuration.

    TCNT1 = 0;          // Timer/Counter1 1,2,...X
    TCCR1A = 0;         // Timer/Counter1 Control Register A
    TCCR1B = 0;         // Timer/Counter1 Control Register B
    TCCR1C = 0;         // Timer/Counter1 Control Register C
    TIMSK1 = 0;         // Timer/Counter1 Interrupt Mask

Set timer top value.

ICR1 = PWM_TOP_VALUE(pwm_div);  //Input Capture Register1 // Top Value will be 1024 

Set the PWM duty cycle to zero.

    OCR1A = 0;  //Output Compare Register1 A
    OCR1B = 0;  //Output Compare Register1 B

Configure timer 1 for PWM, Phase and Frequency Correct operation mode, but leave outputs disabled. This operation mode is number 8 in page 132 datasheet and the TOP value is managed with ICR1.

    TCCR1A = (0<<COM1A1) | (0<<COM1A0) |                    // Disable OC1A output.
             (0<<COM1B1) | (0<<COM1B0) |                    // Disable OC1B output.
             (0<<WGM11) | (0<<WGM10);                       // PWM, Phase and Frequency Correct, TOP = ICR1
    TCCR1B = (0<<ICNC1) | (0<<ICES1) |                      // Input on ICP1 disabled.
             (1<<WGM13) | (0<<WGM12) |                      // PWM, Phase and Frequency Correct, TOP = ICR1
             (0<<CS12) | (0<<CS11) | (1<<CS10);             // No prescaling.

Update the pwm values.

    registers_write_byte(REG_PWM_DIRA, 0);
    registers_write_byte(REG_PWM_DIRB, 0);

First quick check to see if the frequency divider changed. If so we need to configure a new top value for timer/counter1. This value should only change infrequently so we aren't too elegant in how we handle updating the value. However, we need to be careful that we don't configure the top to a value lower than the counter and compare values.

if (registers_read_word(REG_PWM_FREQ_DIVIDER_HI, REG_PWM_FREQ_DIVIDER_LO) != pwm_div)

Hold EN_A (PD2) and EN_B (PD3) low.

        PORTD &= ~((1<<PD2) | (1<<PD3));

        // Give the H-bridge time to respond to the above, failure to do so or to wait long
        // enough will result in brownouts as the power is "crowbarred" to varying extents.
        // The delay required is also dependant on factors which may affect the speed with
        // which the MOSFETs can respond, such as the impedance of the motor, the supply
        // voltage, etc.
        //
        // Experiments (with an "MG995") have shown that 5microseconds should be sufficient
        // for most purposes.
        //
        delay_loop(DELAYLOOP);

Disable, disconnect OC1A and OC1B outputs.

        TCCR1A &= ~((1<<COM1A1) | (1<<COM1A0));
        TCCR1A &= ~((1<<COM1B1) | (1<<COM1B0));

Make sure that PWM_A (PB1/OC1A) and PWM_B (PB2/OC1B) are held low.

PORTB &= ~((1<<PB1) | (1<<PB2));

Reset the A and B direction flags.

        pwm_a = 0;
        pwm_b = 0;

Update the pwm frequency divider value.

pwm_div = registers_read_word(REG_PWM_FREQ_DIVIDER_HI, REG_PWM_FREQ_DIVIDER_LO);

Update the timer top value.

ICR1 = PWM_TOP_VALUE(pwm_div);

Reset the counter and compare values to prevent problems with the new top value.

        TCNT1 = 0;
        OCR1A = 0;
        OCR1B = 0;

Once we have finished if we are reversing the seek sense we do next:

    if (registers_read_byte(REG_REVERSE_SEEK) != 0)
    {
        // Yes. Swap the minimum and maximum position.

        // Get the minimum and maximum seek position.
        min_position = registers_read_word(REG_MAX_SEEK_HI, REG_MAX_SEEK_LO);
        max_position = registers_read_word(REG_MIN_SEEK_HI, REG_MIN_SEEK_LO);

        // Make sure these values are sane 10-bit values.
        if (min_position > MAX_POSITION) min_position = MAX_POSITION;
        if (max_position > MAX_POSITION) max_position = MAX_POSITION;

        // Adjust the values because of the reverse sense.
        min_position = MAX_POSITION - min_position;
        max_position = MAX_POSITION - max_position;
    }

If we are not reversing the sense, we use the maximum and minimum position as is:

        // Get the minimum and maximum seek position.
        min_position = registers_read_word(REG_MIN_SEEK_HI, REG_MIN_SEEK_LO);
        max_position = registers_read_word(REG_MAX_SEEK_HI, REG_MAX_SEEK_LO);

        // Make sure these values are sane 10-bit values.
        if (min_position > MAX_POSITION) min_position = MAX_POSITION;
        if (max_position > MAX_POSITION) max_position = MAX_POSITION;

If the pwm value is above or below the maximum and minimum value we disable pwm.

    // Disable clockwise movements when position is below the minimum position.
    if ((position < min_position) && (pwm < 0)) pwm = 0;

    // Disable counter-clockwise movements when position is above the maximum position.
    if ((position > max_position) && (pwm > 0)) pwm = 0;

Determine if PWM is disabled in the registers. If it is disabled pwm=0.

if (!(registers_read_byte(REG_FLAGS_LO) & (1<<FLAGS_LO_PWM_ENABLED))) pwm = 0;

Determine direction of servo movement or stop. If we set SWAP_PWM_DIRECTION_ENABLED to 1 what we are doing is changing the polarity of the motor. For one hand if pwm is less than 0:

    if (pwm < 0)
    {
        // Less than zero. Turn clockwise.

        // Get the PWM width from the PWM value.
        pwm_width = (uint8_t) -pwm;

        // Turn clockwise.
#if SWAP_PWM_DIRECTION_ENABLED
        pwm_dir_b(pwm_width);
#else
        pwm_dir_a(pwm_width);
#endif
    }

else if pwm > 0:

    else if (pwm > 0)
    {
        // More than zero. Turn counter-clockwise.

        // Get the PWM width from the PWM value.
        pwm_width = (uint8_t) pwm;

        // Turn counter-clockwise.
#if SWAP_PWM_DIRECTION_ENABLED
        pwm_dir_a(pwm_width);
#else
        pwm_dir_b(pwm_width);
#endif

    }

And if pwm =0:

    else
    {
        // Stop all PWM activity to the motor.
        pwm_stop();
    }

Disable interrupts.

cli()

If we are moving in the A or B direction disable the PWM and EN signals.

// Make sure that SMPLn_B (PD4) and SMPLn_A (PD7) are held high.
        PORTD |= ((1<<PD4) | (1<<PD7));

        // Disable PWM_A (PB1/OC1A) and PWM_B (PB2/OC1B) output.
        TCCR1A = 0;

        // Make sure that PWM_A (PB1/OC1A) and PWM_B (PB2/OC1B) are held low.
        PORTB &= ~((1<<PB1) | (1<<PB2));

        // Do we want to enable braking?
        if (1)
        {
           // Before enabling braking (which turns on the "two lower MOSFETS"), introduce
           // sufficient delay to give the H-bridge time to respond to the change of state 
           // that has just been made.
           delay_loop(DELAYLOOP);

            // Hold EN_A (PD2) and EN_B (PD3) high.
            PORTD |= ((1<<PD2) | (1<<PD3));
        }
        else
        {
            // Hold EN_A (PD2) and EN_B (PD3) low.
            PORTD &= ~((1<<PD2) | (1<<PD3));
        }

        // Reset the A and B direction flags.
        pwm_a = 0;
        pwm_b = 0;

Set the PWM duty cycle to zero.

    OCR1A = 0;
    OCR1B = 0;

Restore interrupts.

    sei();

Save the pwm A and B duty values.

    registers_write_byte(REG_PWM_DIRA, pwm_a);
    registers_write_byte(REG_PWM_DIRB, pwm_b);

Phase and Frequency Correct PWM Mode, Timing Diagram. image

ICR1 Register is used to define TOP and this value is fixed and is calculated in code using

#define PWM_TOP_VALUE(div)      ((uint16_t) div << 4) - 1;

In the registers this div value is 0x40. This value if we shifted 4 bits and then substract one unit, we have 1023.

In th datasheet we can find for the Phase and Frequency Correct PWM Mode that the frecuency is:

fpwm = fclk/(2.N.TOP).

In our case fpwm = 20*10^6/ (2x1x1023) =9775.17 Hz So we have a period Tpwm of = 1.023x10^-4.

More information in the datasheet