view pwm.cpp @ 2:9f41bcaa3cee

Handle servo updates in the main loop
author Radomir Dopieralski <sheep-devel@sheep.art.pl>
date Wed, 27 Jan 2016 11:59:41 +0100
parents 7b2d0a758e63
children
line wrap: on
line source

#include "pwm.h"

typedef struct pwm_pin_t {
	volatile uint8_t *port;
	uint8_t pin;
	uint16_t compare_value;
} pwm_pin_t;

typedef struct pwm_event_t {
	//Only used in OCR1B; ignored for high event in OCR1A.  This is the value which TCNT1 is firing 
	// on *now*; you need to reference the next element in the array to get the value which is 
	// to be reset for next time.
	uint16_t compare_value;

	//Port masks
#ifndef PWM_PORTA_UNUSED
	uint8_t porta_mask;
#endif
#ifndef PWM_PORTB_UNUSED
	uint8_t portb_mask;
#endif
#ifndef PWM_PORTC_UNUSED
	uint8_t portc_mask;
#endif
#ifndef PWM_PORTD_UNUSED
	uint8_t portd_mask;
#endif
} pwm_event_t;


volatile void* _pwm_events_low_ptr;								//Pointer to current value in _pwm_events_low.  Reset in OCR1A, incremented in OCR1B.


static uint8_t _set_phase_batch = 0;							//Set to 1 when set_phase_batch is called with a changed value.
static volatile uint8_t _set_phase = 0;							//Set to 1 when phase is re-calculated; signals that the ISR COMPA needs to re-load phase values.
static volatile uint8_t _set_phase_lock = 0; 					//Set to 1 when we are in set_phase function.  Prevents OCR1A from copying the double buffered pwm_events when this is 0.
static volatile uint8_t _set_stop = 0;							//Set to 1 when stop is requested.  ISR COMPA will turn off timer.

static uint16_t _set_period = 0; 								//New period defined; set in set_period, and updated to OCR1A in changed in COMPA interrupt

//Variables used to store config data
static pwm_pin_t _pwm_pins[PWM_MAX_PINS];						//Array of pins.  Index is important here, as that is how we refer to the pins from the outside world.
static uint8_t _count;											//How many pins should be used

static pwm_event_t _pwm_event_high;								//PWM event to set all pins high at start of period.  Calculated on a non-zero set_phase.
static pwm_event_t _pwm_event_high_new;							//Double buffer of pwm high event; copied to pwm_events in OCR1A when _set_phase is non-zero.

static pwm_event_t _pwm_events_low[PWM_MAX_PINS + 1];			//Array of pwm events.  Each event will set one or more pins low.
static pwm_event_t _pwm_events_low_new[PWM_MAX_PINS + 1];		//Double buffer of pwm events.  Calculated in each set_phase call; copied to pwm_events in OCR1A when _set_phase is non-zero.

static uint16_t _prescaler = 0x0;								//Numeric prescaler (1, 8, etc).  Required for _pwm_micros_to_clicks calls.
static uint8_t _prescaler_mask = 0x0;							//Prescaler mask corresponding to _prescaler


static uint16_t _pwm_micros_to_clicks(uint32_t micros){
	//There is a potential for an overflow here if micros * (F_CPU in MHz) does not fit into
	// a 32 bit variable.  If this happens, we can divide micros by prescaler before multiplying
	// with F_CPU, although this will give a loss of precision.  Leave as-is for now.
	return (((F_CPU / 1000000L) * micros) / _prescaler) & 0xFFF0;

	//TODO Rounding, based on 10MHz clock and various prescalers:
	//Prescaler: 1, AND with 0xFF80
	//Prescaler: 8, AND with 0xFFF0
	//Prescaler: 64, AND with 0xFFFE
	//Other prescalers should be fine as-is...
	//More testing at different F_CPU values and prescalers are still required.
}

/*
 * Note: We extrapolate the DDR registers based off of the associated PORT 
 * register.  This assumes that the DDR registers come directly after the PORT
 * equivalents, with DDRX at the next address after PORTX.  This is valid for 
 * all the chips I have looked at; however, it is highly recommended that you 
 * check any new chips which you want to use this library with.
 */
void pwm_init(volatile uint8_t *ports[],
              uint8_t pins[],
              uint8_t count,
              uint32_t period) {

	_count = (count <= PWM_MAX_PINS ? count : PWM_MAX_PINS);

	//Store values
	for (uint8_t i = 0; i < _count; i++){
		_pwm_pins[i].port = ports[i];
		_pwm_pins[i].pin = pins[i];
		*(_pwm_pins[i].port - 0x1) |= _BV(_pwm_pins[i].pin);
	}

	//This is calculated by the focumula:
	// CUTOFF_VALUE = PRESCALER * MAX_VALUE / (F_CPU / 1000000)
	// where CUTOFF_VALUE is the period comparison for each if block,
	// PRESCALER is the prescaler in the datasheet for a given CSx selection,
	// and MAX_VALUE is the largest integer of the selected bit depth (256 / 65536).

#ifdef PWM_8_BIT
	uint32_t max_value = 255;
#else
	uint32_t max_value = 65535L;
#endif

	if (period < (1 * max_value / (F_CPU / 1000000L))){
		_prescaler = 1;
		_prescaler_mask = _BV(CS10);
	}
	else if (period < (8 * max_value / (F_CPU / 1000000L))){
		_prescaler = 8;
		_prescaler_mask = _BV(CS11);
	}
	else if (period < (64 * max_value / (F_CPU / 1000000L))){
		_prescaler = 64;
		_prescaler_mask = _BV(CS11) | _BV(CS10);
	}
	else if (period < (256 * max_value / (F_CPU / 1000000L))){
		_prescaler = 256;
		_prescaler_mask = _BV(CS12);
	}
	else if (period < (1024 * max_value / (F_CPU / 1000000L))){
		_prescaler = 1024;
		_prescaler_mask = _BV(CS12) | _BV(CS10);
	}
	else {
		period = (1024 * max_value / (F_CPU / 1000000L)); //The largest possible period
		_prescaler = 1024;
		_prescaler_mask = _BV(CS12) | _BV(CS10);
	}

	TCCR1A = 0x00;
	TCCR1B &= ~(_BV(CS12) | _BV(CS11) | _BV(CS10));
	TCCR1B |= _prescaler_mask;

        uint16_t clicks = _pwm_micros_to_clicks(period);
	//OCR1A controls the PWM period
	OCR1A = clicks;
	//OCR1B controls the PWM phase.  It is initialized later.

	//Enable compare interrupt on both channels
	TIMSK1 = _BV(OCIE1A) | _BV(OCIE1B);

	//Enable interrupts if the NO_INTERRUPT_ENABLE define is not set.  If it is, you need to call sei() elsewhere.
#ifndef NO_INTERRUPT_ENABLE
	sei();
#endif

	//Force interrupt on compare A initially
	TCCR1C |= _BV(FOC1A);
}

void pwm_start(){
	TCNT1 = 0x00;	//Restart timer counter
	TIMSK1 = _BV(OCIE1A) | _BV(OCIE1B);	//Enable output compare match interrupt enable
	TCCR1B |= _prescaler_mask;	//Enable
}

void pwm_stop(){
	_set_stop = 1;
}

//The comparison method used to sort pwm_pin variables
static int16_t _compare_values(const void *pin1, const void *pin2){
	 const pwm_pin_t *pwm1 = (const pwm_pin_t*) pin1;
	 const pwm_pin_t *pwm2 = (const pwm_pin_t*) pin2;
	 if (pwm1->compare_value < pwm2->compare_value) return -1;
	 else if (pwm1->compare_value > pwm2->compare_value) return 1;
	 else return 0;
}

void pwm_set_phase(uint8_t index, uint32_t phase){
	pwm_set_phase_batch(index, phase);
	pwm_apply_batch();
}

void pwm_set_phase_batch(uint8_t index, uint32_t phase){
	//Bounds checking
	if (index >= _count) {
		return;
	}

	//The new compare value, in clock ticks
	uint16_t new_clicks = _pwm_micros_to_clicks(phase);
	pwm_pin_t *p = &(_pwm_pins[index]);

	if (p->compare_value != new_clicks){
		_set_phase_batch = 1;
	}
	//Set the new compare value
	p->compare_value = new_clicks;
}

void pwm_apply_batch(){
	if (_set_phase_batch == 0){
		return;	//No need to re-calculate if there was no changed phases.
	}

	_set_phase_lock = 1;

	//Copy _pwm_pins to _pwm_pins_sorted, and then sort it by value
	pwm_pin_t _pwm_pins_sorted[PWM_MAX_PINS];
	for (uint8_t i = 0; i < _count; i++){
		_pwm_pins_sorted[i] = _pwm_pins[i];
	}
	qsort(_pwm_pins_sorted, _count, sizeof _pwm_pins_sorted[0], _compare_values);

	//Populate the _pwm_events_high variable and _pwm_events_low_new array, used in 
	// OCR1A and OCR1B respectively to turn pins on / off.
	//First we reset everything in this array...
#ifndef PWM_PORTA_UNUSED
	_pwm_event_high_new.porta_mask = 0x00;
#endif
#ifndef PWM_PORTB_UNUSED
	_pwm_event_high_new.portb_mask = 0x00;
#endif
#ifndef PWM_PORTC_UNUSED
	_pwm_event_high_new.portc_mask = 0x00;
#endif
#ifndef PWM_PORTD_UNUSED
	_pwm_event_high_new.portd_mask = 0x00;
#endif
	for (uint8_t i = 0; i < _count + 1; i++){
		_pwm_events_low_new[i].compare_value = 0xFFFF;
#ifndef PWM_PORTA_UNUSED
		_pwm_events_low_new[i].porta_mask = 0xFF;
#endif
#ifndef PWM_PORTB_UNUSED
		_pwm_events_low_new[i].portb_mask = 0xFF;
#endif
#ifndef PWM_PORTC_UNUSED
		_pwm_events_low_new[i].portc_mask = 0xFF;
#endif
#ifndef PWM_PORTD_UNUSED
		_pwm_events_low_new[i].portd_mask = 0xFF;
#endif
	}
	//... then we look through the sorted _pwm_pins list, and collect all of the ports / pins which are
	// set at the same compare values into a single event.
	uint16_t last_compare_value = _pwm_pins_sorted[0].compare_value;
	uint8_t last_index = 0;

	for (uint8_t i = 0; i < _count; i++){
		pwm_pin_t *p = &(_pwm_pins_sorted[i]);
		if (p->compare_value > last_compare_value){
			//We don't want an empty pwm_event at the beginning of the array, so verify whether last_compare_value was zero before incrementing
			if (last_compare_value > 0) {
				last_index++;
			}
			last_compare_value = p->compare_value;
		}

		if (p->compare_value > 0){
			//Set pins to high
#ifndef PWM_PORTA_UNUSED
			if (p->port == &PORTA) _pwm_event_high_new.porta_mask |= _BV(p->pin);
			else
#endif
#ifndef PWM_PORTB_UNUSED
			if (p->port == &PORTB) _pwm_event_high_new.portb_mask |= _BV(p->pin);
			else
#endif
#ifndef PWM_PORTC_UNUSED
			if (p->port == &PORTC) _pwm_event_high_new.portc_mask |= _BV(p->pin);
			else
#endif
#ifndef PWM_PORTD_UNUSED
			if (p->port == &PORTD) _pwm_event_high_new.portd_mask |= _BV(p->pin);
			else
#endif
			if (0);	//Nop so that ignored ports dont break things

			//Set pins to low
			pwm_event_t *e = &(_pwm_events_low_new[last_index]);
			e->compare_value = last_compare_value;
#ifndef PWM_PORTA_UNUSED
			if (p->port == &PORTA) e->porta_mask &= ~_BV(p->pin);
			else
#endif
#ifndef PWM_PORTB_UNUSED
			if (p->port == &PORTB) e->portb_mask &= ~_BV(p->pin);
			else
#endif
#ifndef PWM_PORTC_UNUSED
			if (p->port == &PORTC) e->portc_mask &= ~_BV(p->pin);
			else
#endif
#ifndef PWM_PORTD_UNUSED
			if (p->port == &PORTD) e->portd_mask &= ~_BV(p->pin);
			else
#endif
			if (0);	//Nop so that ignored ports dont break things
		}
	}

	_set_phase_batch = 0;

	//Signal OCR1A that we are ready to load new values
	_set_phase = 1;
	_set_phase_lock = 0;
}

void pwm_set_period(uint32_t period){
	_set_period = _pwm_micros_to_clicks(period);
}



/*
 * The frequency comparison.  When it overflows, we reset the timer to 0.
 */
#ifdef PWM_8_BIT
EMPTY_INTERRUPT(TIM0_OVF_vect)
ISR(TIM0_COMPA_vect){
#else
EMPTY_INTERRUPT(TIMER1_OVF_vect)
ISR(TIMER1_COMPA_vect){
#endif
	//Update values if needed
	if (_set_phase && !_set_phase_lock){
		for (uint8_t i = 0; i < _count; i++){
			_pwm_events_low[i] = _pwm_events_low_new[i];
		}
		_pwm_event_high = _pwm_event_high_new;
		_set_phase = 0;
	}
	if (_set_period){
		OCR1A = _set_period;
		_set_period = 0;
	}
	if (_set_stop){
		TCCR1B = 0x00;

#ifndef PWM_PORTA_UNUSED
		PORTA &= ~_pwm_event_high.porta_mask;
#endif
#ifndef PWM_PORTB_UNUSED
		PORTB &= ~_pwm_event_high.portb_mask;
#endif
#ifndef PWM_PORTC_UNUSED
		PORTC &= ~_pwm_event_high.portc_mask;
#endif
#ifndef PWM_PORTD_UNUSED
		PORTD &= ~_pwm_event_high.portd_mask;
#endif

		_set_stop = 0;
		return;
	}
	//Reset counter after the new compare values are updated (otherwise it affects the phase 
	// of the next execution, causing jitter).
	TCNT1 = 0;

	//Set to the first (sorted) compare value in the pwm_events_low array.  This is calculated 
	// in pwm_apply_batch(), and updated above in this ISR.
	_pwm_events_low_ptr = (void*) _pwm_events_low;
	OCR1B = _pwm_events_low[0].compare_value;

	//Set pins high.  We do this after re-enabling the clock so that we do not artificially increase 
	// the phase.  We turn off the ports (in COMPB) in the same order that we turn them on here,
	// so that the delta of any delay between PORTA and PORTD should be reduced or eliminated.
#ifndef PWM_PORTA_UNUSED
	PORTA |= _pwm_event_high.porta_mask;
#endif
#ifndef PWM_PORTB_UNUSED
	PORTB |= _pwm_event_high.portb_mask;
#endif
#ifndef PWM_PORTC_UNUSED
	PORTC |= _pwm_event_high.portc_mask;
#endif
#ifndef PWM_PORTD_UNUSED
	PORTD |= _pwm_event_high.portd_mask;
#endif
}

/*
 * The phase comparison, implemented in C.  When it overflows, we find the next highest value.
 * For a faster alternative, comment this out and use the ASM version in pwm.S
 *
ISR(TIMER1_COMPB_vect){
	pwm_event_t* e = (pwm_event_t*) _pwm_events_low_ptr;
	_pwm_events_low_ptr = (uint8_t*) _pwm_events_low_ptr + sizeof(pwm_event_t);

#ifndef PWM_PORTA_UNUSED
	PORTA &= e->porta_mask;
#endif
#ifndef PWM_PORTB_UNUSED
	PORTB &= e->portb_mask;
#endif
#ifndef PWM_PORTC_UNUSED
	PORTC &= e->portc_mask;
#endif
#ifndef PWM_PORTD_UNUSED
	PORTD &= e->portd_mask;
#endif

	//Set the timer for the next lowest value.
	OCR1B = ((pwm_event_t*) _pwm_events_low_ptr)->compare_value;
}
*/