diff pwm.cpp @ 0:2b5078b37907

Working servo PWM
author Radomir Dopieralski <sheep-devel@sheep.art.pl>
date Tue, 12 Jan 2016 19:33:31 +0100
parents
children 7b2d0a758e63
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pwm.cpp	Tue Jan 12 19:33:31 2016 +0100
@@ -0,0 +1,396 @@
+#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;
+}
+*/