/*! \file *********************************************************************
*
* \brief
* VFD = Variable Frequency Drive
* This file contains the full implementation of the VFD control,
* except the PID-controller.
* To successfully compile it and run on the AVR,
* select -Os optimization in your compiler options.
* Fuse your controller to no CLKDIV8, external fullswing high frequency crystal
* and if you wish enable EESAVE to keep your EEPROM settings.
* Alternatively, you could use the internal RC Oscillator with its 8 Mhz
* but the PWM Frequency is well in hearing range then.
*
* \par Roughly based on Application note:
* AVR447: Sinusoidal driving of three-phase motor using
* ATmega48/88/168/328
*
* \author
* Matthias Schoeldgen - DC7UR
*
* Hardware and LCD
* ------------------
* A HD44780 Display is connected to
* LCD Data DB4-7 on PC0-PC3
* LCD E to PB0
* LCD RS to PB4
* LCD RW to PB5
* Connections have been chosen to not interfere with the six PWM outputs.
* Peter Fleury's library is used to drive the LCD and it has been modified
* See lcd.h for the settings. Make sure to apply pin and XTAL settings
* to LCD.h and set DIVISIONEER in vfd.h according to the formula there
* The default settings are for a 16MHz Crystal.
*
* Three buttons are connected to PD0 to PD2 (grounding the resp. pin)
* Button 0 black cycles through parameters
* Button 1 red decreases selected parameter
* Button 2 green increases selected parameter
* Holding Button 0 and pressing 1 select the extended menu
* where the PID parameters and the Deadtime are set
* In this menu holding button 0 and pressing button 1 will store current values to the EEPROM
* Button 0 and 2 pressed simultaneously in this menu exits and returns to main menu
* The EEPROM set is run on power up
*
* Frequency and Amplitude control:
* when jumper on PD4 is set (grounded) control of Frequency and V/Hz is done
* by analog values from ADC inputs 4 and 5 resp.
*
* Direction control is provided through PD7:
* Attention!
* This drive doesn't measure motor rotation and can't determine wether
* the motor is stopped or not. To change direction you should stop the motor first.
*
* The variable Inco is the width between table values
* resulting frequency is:
*
* (F_CPU/512/256)/(SIN_TABLE_LENGTH/Inco) = Fout
*
* The 'V/Hz' parameter controls the PWM Power according to the frequency
* Adjust it for the power of the motor at its working frequency
* Those values and the Deadtime can be stored in EEPROM
* The PID parameters are stored there,too
* Contact me: mikrocontroller@schoeldgen.de
* this project compiles with AVR studio 4 and WinAVR
*
* Diagnostic outputs on startup. If everything goes well, you'll probably never see those
* as they are thrown on the LCD very quickly on start.
* 0 - Port & LCD init done
* 1 - Timer init done
* 2 - Pinchange Interrupt init done
* 3 - ADC init done
* 4 - PID Controller init done
* 5 - init fastFlags done
* 6 - init Timer IRQs done
* 7 - EEPROM init done and keys not stuck
*
******************************************************************************/
// I really don't know if its necessary to include all this here, but if it is not
// needed it won't eat up flash ROM anyway
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <avr/eeprom.h>
#include <avr/wdt.h>
#include <avr/interrupt.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <util/delay.h>
// project headers
#include "vfd.h"
#include "vfdtables.h"
// PID controller headers
#include "pid.h"
// Peter Fleury's LCD Lib
#include "lcd.h"
/* ------------- Global variables ----*/
/*! \brief Flags for Waveform and Direction
*
* This variable contains all the flags used for VF control.
* Note that we use direct registers to hold some of the more time-critical
* variables, although it probably isn't necessary. It is a nice example of
* using this compiler feature, though.
* The fastFlags register holds a bit set of some control parameters. Its declaration is in vfd.h
*/
register volatile VFDflags_t fastFlags asm ("r9");
/*! \brief Increment used for sine table iteration.
*
* This variable holds the increment used for sine table iteration.
* It is stored in an 8.8 fixed point format.
*/
register volatile uint16_t sineTableIncrement asm ("r2");
/*! \brief Index into sine table
*
* This variable is used as an index into the sine table. It is
* stored in a 8.8 fixed point format, so it can be directly incremented by
* sineTableIncrement. Only the high byte is used as table index.
*/
volatile uint16_t sineTableIndex ;
/*! \brief The amplitude of the generated sine waves.
*
* This variable controls the amplitude of the generated sine waves.
* The range is 0-255. Calculated in SpeedController from frequency and V/Hz
*/
register volatile uint8_t amplitude asm("r6");
// This is the frequency to voltage ratio
register volatile uint8_t VperHz asm("r7");
// This is the main frequency control
register volatile uint8_t Inco asm("r8") ;
/*! \brief current selected parameter
*
*/
volatile uint8_t parameter = 0;
// calculated frequency * 10
uint16_t freq;
//! The most recent speed input from ADC
volatile uint8_t speedInput;
// desired dead time from eeprom
volatile uint8_t DEAD_TIME_HALF = 20;
/*! \brief Speed controller run flag.
*
* This variable is set to TRUE every time the speed controller should be run.
*/
volatile uint8_t SpeedControllerRun = FALSE;
//! Struct used to hold PID controller parameters and variables.
volatile pidData_t pidParameters;
// EEPROM values preset for generating an .eep file
// adjusting only necessary if the very initial values are not what you want.
// normally you set EEPROM values from the running system
//
uint8_t ee_DEAD_TIME_HALF __attribute__((section(".eeprom"))) = 20;
// sinTableIncrement where 79 is about 50 Hz
uint8_t ee_Inco __attribute__((section(".eeprom"))) = 79;
// desired V/f ratio
uint8_t ee_VperHz __attribute__((section(".eeprom"))) = 45;
// PID params
uint16_t ee_pid_P __attribute__((section(".eeprom"))) = PID_K_P;
uint16_t ee_pid_I __attribute__((section(".eeprom"))) = PID_K_I;
uint16_t ee_pid_D __attribute__((section(".eeprom"))) = PID_K_D;
/* ----- SUB Programs ---*/
/*! \brief Initializes I/O port directions and pull-up resistors
*
* This function initializes all I/O ports with correct
* direction and pull-up resistors, if needed.
*/
static void PortsInit(void)
{
// Port C is LCD Data Output and ADC Input
// the LCD lib does this on its own, but here for completeness
DDRC = 0b00001111;
// PORTB, PORTD outputs
// clr pullups but do nothing else here. The sine driver will take care of the output states
PORTB &= ~(PWM_PATTERN_PORTB);
PORTD &= ~(PWM_PATTERN_PORTD);
// Set the inputs on Port D for the three buttons
DDRD &= ~BUTTON_MASK;
// Enable pull-up on input signals.
PORTD |= _BV(DIRECTION_COMMAND_PIN) | _BV(EXTERNAL_CONTROL_PIN) | BUTTON_MASK ;
}
/*! \brief Initializes and synchronizes Timers
*
* This function sets the correct prescaler and starts all
* three timers. The timers are synchronized to ensure that
* all PWM signals are aligned.
*/
static void TimersInit(void)
{
//Set all timers in "Phase correct mode". Do not enable outputs yet.
TCCR0A = (1 << WGM00);
TCCR1A = (1 << WGM11);
TCCR2A = (1 << WGM20);
//Set top value of Timer/counter1.
ICR1 = 0xff;
//Synchronize timers. These values were determined by running the simulator
TCNT0 = 0;
TCNT1 = 3;
TCNT2 = 5;
// Start all 3 timers.
TCCR0B = (0 << CS01) | (1 << CS00);
TCCR1B = (1 << WGM13) | (0 << CS11) | (1 << CS10);
TCCR2B = (0 << CS21) | (1 << CS20);
}
/*! \brief Initialize pin change interrupts.
*
* This function initializes pin change interrupt on
* motor direction control input.
*/
static void PinChangeIntInit(void)
{
// Initialize pin change interrupt on direction command pin
// PD7 is PCINT23
PCMSK2 = _BV(PCINT23);
// Enable pin change interrupt on ports with pin change signals
PCICR = (1 << PCIE2);
}
/* This really does nothing except restarting the ADC with IRQ enabled
* If you want to calibrate, do it here.
*/
static void CalibADC(void)
{
DisablePWMOutputs();
_delay_ms(200);
//Initialize ADC and restart, now with IRQ
ADCSRA = (1 << ADEN) | (1 << ADSC) | (0 << ADATE) | (1 << ADIF) | (1 << ADIE) | (ADC_PRESCALER);
}
/*!
* This function initializes the ADC for frequency and amplitude control
* The ADC is triggered once and then relies on
* the ADC complete interrupt service routine to get restarts
* Declaration of ADMUX and the other constants can be found in vfd.h
*/
static void ADCInit(void)
{
// Initialize ADC, enable first. Purists probably ask why i shift zeros into the byte
// but its making it all clearer and more easy to change
// remember this is init only and will only be run ONCE...
ADCSRA = (1 << ADEN) | (0 << ADSC) | (0 << ADATE) | (1 << ADIF) | (0 << ADIE) | (ADC_PRESCALER);
// disable digital inputs on analog channels
DIDR0 = ( 1 << ADC_CHANNEL_FREQUENCY) | (1 << ADC_CHANNEL_AMPLITUDE );
//Select initial AD conversion channel.
ADMUX = ADMUX_FREQUENCY;
//Set trigger source to free run
ADCSRB = (0 << ADTS2) | (0 << ADTS1) | (0 << ADTS0);
//Initialize ADC and start once, no IRQ
ADCSRA = (1 << ADEN) | (1 << ADSC) | (0 << ADATE) | (0 << ADIF) | (0 << ADIE) | (ADC_PRESCALER);
CalibADC();
}
// now lets initialize the EEPROM and read in our variables
// as the LCD is now up and running, we can let the user know of things
static void EEPROMInit(void)
{
lcd_gotoxy(0,1);
lcd_puts_P( "Reading EEPROM " );
DEAD_TIME_HALF = eeprom_read_byte(&ee_DEAD_TIME_HALF);
/* a fail here would be fatal to the PA. So if somehow a wrong value is read
* we set to MIN_DEAD_TIME to save the life of the PA.
*/
if (DEAD_TIME_HALF < MIN_DEAD_TIME) DEAD_TIME_HALF = MIN_DEAD_TIME;
// Working parameters
Inco = eeprom_read_byte(&ee_Inco);
VperHz = eeprom_read_byte(&ee_VperHz);
// PID values
pidParameters.P_Factor = eeprom_read_word(&ee_pid_P);
pidParameters.I_Factor = eeprom_read_word(&ee_pid_I);
pidParameters.D_Factor = eeprom_read_word(&ee_pid_D);
_delay_ms(500);
lcd_gotoxy(0,1);
lcd_puts_P( "Read Done " );
_delay_ms(500);
// wait for button release
// Don't ask why this is here --- ok, you may ask: Its here if some fool powers
// up the VFD and keeps the buttons pressed or the buttons somehow are stuck.
// That would probly lead to undesired operation and so - - - lets wait
if (keyin() > 0){};
}
/* brief: button routines
* three buttons return hex 1,2,4 resp.
* note that debouncing is not really necessary here as the polling routine
* just happens to pass by.
*/
char keyin(void){
uint8_t i = 0;
i = ~BUTTON_PORT;
i &= BUTTON_MASK;
return i;
}
// Console//LCD utilities
static void printspc(void)
{
lcd_putc(' ');
}
/* Some basic routines to print hecadecimal and
* decimal numbers plus some conversion stuff
*/
// table unfortunately needs to reside in RAM for now
char hextable[18] = "0123456789ABCDEF";
// very simple output to hex
static void printbyte(const unsigned char data)
{
lcd_putc(hextable[data >> 4]);
lcd_putc(hextable[data & 0x0f]);
}
static void printword(const unsigned int data)
{
printbyte(data >> 8);
printbyte(data);
}
// output a number in decimal style
char dectable[12] = "000000000000";
char a;
static void printdec(int16_t number) {
uint8_t n,i;
/* yeah, yeah warning "pointer to integer without a cast"
* Am i tired of this or what
*/
a = itoa(number,dectable,10);
i = strlen(dectable);
for (n=0;n < i;n++) {
if (n == i-1) lcd_putc('.');
lcd_putc(dectable[n]);
}
}
static void printnum(int16_t number) {
uint8_t n,i;
a = itoa(number,dectable,10);
i = strlen(dectable);
for (n=0;n < i;n++) {
lcd_putc(dectable[n]);
}
}
/* write values to EEPROM
* IRQs must be disabled before writing and are re-enabled afterwards
* therefore disabling the PWM is safest. Reestablishing sine driving is done
* by resetting the waveform to undefined. The timer IRQ will handle the remains.
*/
void wrEEPROM(void) {
lcd_gotoxy(0,1);
lcd_puts_P( "Storing EEPROM " );
cli();
DisablePWMOutputs();
// real writing is here
eeprom_busy_wait();
eeprom_write_byte(&ee_DEAD_TIME_HALF,DEAD_TIME_HALF);
eeprom_busy_wait();
eeprom_write_byte(&ee_Inco,Inco);
eeprom_busy_wait();
eeprom_write_byte(&ee_VperHz,VperHz);
eeprom_busy_wait();
// write PID values
eeprom_write_word(&ee_pid_P,pidParameters.P_Factor);
eeprom_busy_wait();
eeprom_write_word(&ee_pid_I,pidParameters.I_Factor);
eeprom_busy_wait();
eeprom_write_word(&ee_pid_D,pidParameters.D_Factor);
eeprom_busy_wait();
// tell the interrupt to reestablish Sine Driving
fastFlags.driveWaveform = WAVEFORM_UNDEFINED;
sei();
// tell the user
_delay_ms(500);
lcd_gotoxy(0,1);
lcd_puts_P( "Stored " );
_delay_ms(500);
// wait for button release
if (keyin() > 0){};
}
// Cursorpositions for selectable parameters
PROGMEM uint8_t cursorextpos[EXTNUMPARAMS+1] = { 0,4,8,13 };
static void showExtendedPars(void)
{
uint8_t *p = cursorextpos;
lcd_gotoxy(0,1);
printword(pidParameters.P_Factor);
printword(pidParameters.I_Factor);
printword(pidParameters.D_Factor);printspc();
printbyte(DEAD_TIME_HALF);printspc();
lcd_gotoxy(pgm_read_byte(p+parameter),1); // set cursor below active parameter
_delay_ms(5);
}
/* The extended menu offers access to the PID parameters and the Deadtime setting
* The deadtime is in cpu cycles. For a PA with IR2112/2110 and the IRFGP40BC a setting of 20 seems to be good
* note that a high dead time setting costs resolution and a setting too low will burn the Fuse
* or otherwise cause overcurrent.
* Adjust in a safe environment with current monitoring.
* Place on the LCD is limited, thus PID and DT is displayed as Hex.
* Wrap around on these values should be avoided, thus the range check
*/
static void execExtendedCommand(void)
{
char n=0;
uint8_t j=0;
lcd_home();
// this line is only written once, actual values are always displayed in line 2
lcd_puts_P( "P I D DT " );
parameter = 0;
// wait for button release
while (keyin()!=0){};
// pressing black and green button simultaneously exits this menu
while (n != 5) {
n = keyin();
switch (n)
{
// cycle through parameters
case 1 : parameter++; if (parameter > EXTNUMPARAMS) parameter = 0;
break;
// red button decrements parameter
case 2 :
switch (parameter) {
case 0 : if (pidParameters.P_Factor < 1) pidParameters.P_Factor = 1;
pidParameters.P_Factor--;
break;
case 1 : if (pidParameters.I_Factor < 1) pidParameters.I_Factor = 1;
pidParameters.I_Factor--;
break;
case 2 : if (pidParameters.D_Factor < 1) pidParameters.D_Factor = 1;
pidParameters.D_Factor--;
break;
case 3 : DEAD_TIME_HALF--;
if (DEAD_TIME_HALF < MIN_DEAD_TIME) DEAD_TIME_HALF = MIN_DEAD_TIME;
break;
default : break;
}
break;
// green button increments parameter
case 4 :
switch (parameter) {
case 0 : if (pidParameters.P_Factor > 65534) pidParameters.P_Factor = 65534;
pidParameters.P_Factor++;
break;
case 1 : if (pidParameters.I_Factor > 65534) pidParameters.I_Factor = 65534;
pidParameters.I_Factor++;
break;
case 2 : if (pidParameters.D_Factor > 65534) pidParameters.D_Factor = 65534;
pidParameters.D_Factor++;
break;
case 3 : DEAD_TIME_HALF++;
if (DEAD_TIME_HALF > 200) DEAD_TIME_HALF = 200;
break;
default : break;
}
break;
// pressing black (1) and red (2) button simultaneously writes EEPROM
case 3 : wrEEPROM(); break;
// pressing black and green(4) button simultaneously exits this menu
case 5 : break;
default: j = 0; break;
} // switch
PID_Init(pidParameters.P_Factor, pidParameters.I_Factor, pidParameters.D_Factor,(pidData_t *) &pidParameters);
// includes a little autorepeat accelerator
if (keyin() > 0) {
j++;
if (j<10) _delay_ms(60);
else {
_delay_ms(10);
j = 11;
}
}
showExtendedPars();
} // while
parameter = 0;
lcd_clrscr();
// restore original diplay
lcd_puts_P( "Freq V/Hz Amp " );
// wait for button release
while (keyin()!=0){};
}
// Cursorpositions for selectable parameters
PROGMEM uint8_t cursorpos[NUMPARAMS+1] = { 0,6 };
/* print a line with the most important variables
* resulting frequency is:
*
* (F_CPU/512/256*Steps)/(SINE_TABLE_LENGTH/Inco)
* though for displaying we multiply by 10
* to show accurate frequency in integer math
* amplitude is shown in percent
*/
static void showPars(void)
{
uint8_t *p = cursorpos;
lcd_gotoxy(0,1);
printdec(freq);printspc();lcd_gotoxy(6,1);
printdec(VperHz);printspc();printspc();lcd_gotoxy(12,1);
printnum((uint16_t)(amplitude*100)/255);lcd_putc('%');printspc();
lcd_gotoxy(pgm_read_byte(p+parameter),1); // set cursor below active parameter
_delay_ms(5);
}
/* repeatedly execute this code part. Here we scan the buttons, set params and display stuff
* Although we blast to LCD quite often, it won't interfere with the
* sine drive as this is completely interrupt driven
* if the "external control" jumper is set only the extended menu option will work
*/
static void execCommand(void)
{
char n;
static uint8_t i;
n = keyin();
// first make sure we get inputs from buttons and not from analog inputs
if (!fastFlags.externalControl){
switch (n)
{
// black button - cycle through parameters
case 1 : parameter++; if (parameter > NUMPARAMS) parameter = 0;
break;
// red button - decrement parameter
case 2 :
switch (parameter) {
case 0 : if (Inco < 1) Inco = 1;
Inco--;
break;
case 1 : if (VperHz < 1) VperHz = 1;
VperHz--;
break;
default : break;
}
break;
// green button - increment parameter
case 4 :
switch (parameter) {
case 0 :if (Inco > 254) Inco = 254;
Inco++;
break;
case 1 :if (VperHz > 254) VperHz = 254;
VperHz++;
break;
default : break;
}
break;
// pressing first black (1) and then red (2) button simultaneously goes to extended menu
case 3 : execExtendedCommand();
break;
default: i = 0; break;
} // switch
} else { // external control
// with external control we only can go to the extended menu
switch (n)
{
// pressing black (1) and red (2) button simultaneously goes to sub menu
case 3 : execExtendedCommand();
break;
default: i = 0; break;
} // switch
} // external control
// simple autorepeater
if (keyin() > 0) {
i++;
if (i<10) _delay_ms(60);
else {
_delay_ms(10);
i = 11;
}
}
showPars();
}
/*! \brief Updates global desired direction flag.
*
* Running this function triggers a reading of the direction
* input pin. The desiredDirection flag is set accordingly.
*/
static void DesiredDirectionUpdate(void)
{
if ( bit_is_clear(PIND,DIRECTION_COMMAND_PIN) )
{
fastFlags.desiredDirection = DIRECTION_REVERSE;
}
else
{
fastFlags.desiredDirection = DIRECTION_FORWARD;
}
}
/* select control method by examining the EXTERNAL_CONTROL_PIN
* if it is jumpered to ground, we derive V/Hz and Inco from analog signals
*/
static void DesiredControlUpdate(void)
{
if ( bit_is_clear(PIND,EXTERNAL_CONTROL_PIN) )
{
fastFlags.externalControl = TRUE;
}
else
{
fastFlags.externalControl = FALSE;
}
}
/* ***************************************************************************************
*! \brief Main function / initialization
*
* The main function initializes all subsystems needed for sine control
* and enables interrupts, which kicks off the fully interrupt-driven
* PWM generator. The main function goes into an loop where it
* does the LCD and button work.
* During startup we throw some numbers on the display to help when an error in
* any initialization occurs
* Lets initialize all variables here - even if its only for completeness
*******************************************************************************************+
*/
int main(void)
{
cli();
// variables
amplitude = 0 ;
speedInput = 0 ;
Inco = 1;
parameter = 0;
//Initialize peripherals.
PortsInit();
/* initialize display, cursor off */
lcd_init(LCD_DISP_ON);
lcd_home();
lcd_puts_P( "VF Sinus Drive\n" );
lcd_putc('0'); // diagnostic outputs
// timers
TimersInit();
lcd_putc('1');
// jumpers
PinChangeIntInit();
lcd_putc('2');
// analog
ADCInit();
lcd_putc('3');
PID_Init(PID_K_P, PID_K_I, PID_K_D,(pidData_t *) &pidParameters);
lcd_putc('4');
//Initialize fastflags. Use temporary variable to avoid several accesses to
//volatile variable.
{
VFDflags_t fastFlagsInitial;
// Set motorSyncronized to FALSE at startup. This will prevent the motor from being
// driven until the motor is in synch or stopped.
fastFlagsInitial.externalControl = FALSE;
fastFlagsInitial.desiredDirection = DIRECTION_FORWARD;
fastFlagsInitial.driveWaveform = WAVEFORM_UNDEFINED;
fastFlags = fastFlagsInitial;
}
lcd_putc('5');
// read the port pins and set fastFlags accordingly
DesiredDirectionUpdate();
DesiredControlUpdate();
// Enable Timer1 capture event interrupt.
TIFR1 = _BV(ICF1) | _BV(OCF1B) | _BV(OCF1A) | _BV(TOV1) ;
TIMSK1 = _BV(ICIE1);
lcd_putc('6');
_delay_ms(500);
// read in stored values
EEPROMInit();
lcd_putc('7');
lcd_clrscr();
lcd_command(LCD_DISP_ON_CURSOR);
// this line is only written once, actual values are always displayed in line 2
lcd_puts_P( "Freq V/Hz Amp " );
// activate the engine
sei();
/*************************** MAIN LOOP *************************/
//Enable interrupts globally and let sine driver take over.
//the main loop handles user interface
for(;;)
{
execCommand();
}
}
/*************************** END MAIN LOOP *************************/
/*
* From here on most routines are only run from the interrupt driven VFD
*
*/
/*! \brief Speed regulator loop.
*
* This function is called every SPEED_REGULATOR_TIME_BASE ticks. In this
* implementation, a simple PID controller loop is called if we run from external control inputs
* note the internal representation is 10 times the real values.
*/
static void SpeedController(void)
{
int32_t outputValue, amplitudeValue;
// calculate the frequency
if (fastFlags.externalControl) {
//Calculate an increment setpoint from the analog speed input.
int16_t incrementSetpoint = ((uint32_t)speedInput * SPEED_CONTROLLER_MAX_INCREMENT) / SPEED_CONTROLLER_MAX_INPUT;
//PID regulator with feed forward from speed input.
outputValue = (uint32_t)speedInput;
outputValue += PID_Controller(incrementSetpoint, ((uint16_t)Inco),(pidData_t *) &pidParameters);
Inco = (uint16_t)outputValue;
// clamp
}
freq = DIVISIONEER/(SINE_TABLE_DISP/Inco);
amplitudeValue = (freq/10)*VperHz;
if (amplitudeValue < 1)
{
amplitudeValue = 0;
}
else if (amplitudeValue > 2549)
{
amplitudeValue = 2550;
}
amplitude = amplitudeValue / 10;
}
/*! \brief Waits for the start of the next PWM cycle.
*
* Can be used to make sure that a shoot-through
* does not occur in the transition between two output waveform generation modes.
*/
static void TimersWaitForNextPWMCycle(void)
{
//Clear Timer1 Capture event flag.
TIFR1 = (1 << ICF1);
//Wait for new Timer1 Capture event flag.
while ( !(TIFR1 & (1 << ICF1)) )
{
}
}
/*! \brief Enables PWM output pins
*
* This function enables PWM outputs by setting the port direction for
* all PWM pins as output. The PWM configuration itself is not altered
* in any way by running this function.
*/
static void EnablePWMOutputs(void)
{
DDRB |= PWM_PATTERN_PORTB;
DDRD |= PWM_PATTERN_PORTD;
}
/*! \brief Disables PWM output pins
*
* This function disables PWM outputs by setting the port direction for
* all PWM pins as inputs, thus overriding the PWM. The PWM configuration
* itself is not altered in any way by running this function.
*/
static void DisablePWMOutputs(void)
{
DDRB &= ~PWM_PATTERN_PORTB;
DDRD &= ~PWM_PATTERN_PORTD;
}
/*! \brief Configures timers for sine wave generation
*
* This function is called every time sine wave generation is
* needed. PWM outputs are safely disabled while configuration
* registers are changed to avoid unintended driving or shoot-
* through.
*/
static void TimersSetModeSinusoidal(void)
{
//Set PWM pins to input (Hi-Z) while changing modes.
DisablePWMOutputs();
//Sets all 3 timers in inverted pair mode.
TCCR0A = (1 << COM0A1) | (0 << COM0A0) | (1 << COM0B1) | (1 << COM0B0) | (1 << WGM00);
TCCR1A = (1 << COM1A1) | (0 << COM1A0) | (1 << COM1B1) | (1 << COM1B0) | (1 << WGM11);
TCCR2A = (1 << COM2A1) | (0 << COM2A0) | (1 << COM2B1) | (1 << COM2B0) | (1 << WGM20);
//Make sure all outputs are turned off before PWM outputs are enabled.
OCR0A = OCR1AL = OCR2A = 0;
OCR0B = OCR1BL = OCR2B = 0xff;
//Wait for next PWM cycle to ensure that all outputs are updated.
TimersWaitForNextPWMCycle();
fastFlags.driveWaveform = WAVEFORM_SINUSOIDAL;
//Change PWM pins to output again to allow PWM control.
EnablePWMOutputs();
}
/*! \brief Returns the high and low values with deadband for a given compare value.
*
* This function takes as argument a desired compare value and inserts a symmetric
* deadband. The compare values for high and low side with deadband are returned
* through the two supplied pointers.
* The variable \ref DEAD_TIME_HALF is used as deadband, and the resulting deadtime
* will be DEAD_TIME_HALF clock cycles times 2.
*
* \param compareValue desired compare value
* \param compareHighPtr Pointer used to return high side compare value with dead band.
* \param compareLowPtr Pointer used to return low side compare value with dead band.
*/
static void InsertDeadband(const uint8_t compareValue, uint8_t * compareHighPtr, uint8_t * compareLowPtr)
{
if (compareValue <= DEAD_TIME_HALF)
{
*compareHighPtr = 0x00;
*compareLowPtr = compareValue;
}
else if (compareValue >= (0xff - DEAD_TIME_HALF))
{
*compareHighPtr = 0xff - (2 * DEAD_TIME_HALF);
*compareLowPtr = 0xff;
}
else
{
*compareHighPtr = compareValue - DEAD_TIME_HALF;
*compareLowPtr = compareValue + DEAD_TIME_HALF;
}
}
/*! \brief Adjusts the sine table index according to the current increment.
*
* This function increases the sine table index with the given increment
* The index is then adjusted to be within the table length.
*
* \param increment The increment (in 8.8 format) added to the sine table index.
*/
static void AdjustSineTableIndex(const uint16_t increment)
{
sineTableIndex += increment ;
// If the table index is out of bounds, wrap the index around the table end
// to continue from the beginning. Also wrap the next sector start index.
if ((sineTableIndex >> 8) >= SINE_TABLE_LENGTH)
{
sineTableIndex -= (SINE_TABLE_LENGTH << 8);
}
}
void ChangeDirection(void)
{
DesiredDirectionUpdate();
//Disable driver signals
DisablePWMOutputs();
fastFlags.driveWaveform = WAVEFORM_UNDEFINED;
}
/*! \brief Direction input change interrupt service routine.
*
* This ISR is called every time the direction input pin changes
* state. The desired direction flag is updated accordingly.
*/
ISR(PCINT2_vect)
{
//Call the routine to actually stop the motor and reverse
ChangeDirection();
}
//
EMPTY_INTERRUPT(TIMER0_OVF_vect);
// ISR stub for unused irqs. The AVRs sometimes need to fire an interupt and
// execute it before OC registers are updated
EMPTY_INTERRUPT(TIMER0_COMPB_vect);
ISR(TIMER0_COMPA_vect,ISR_ALIASOF (TIMER0_COMPB_vect));
/*! \brief Timer1 Capture Event interrupt service routine.
*
* This interrupt service routine is run everytime the up-down counting timer1
* reaches TOP (0xff). New sinusoidal output values are calculated and the
* timers are updated to reflect the new values.
* The core routine of the VF Drive
*/
ISR(TIMER1_CAPT_vect)
{
static uint8_t speedRegTicks = 0;
uint8_t tempU, tempV, tempW;
{
if (fastFlags.driveWaveform != WAVEFORM_SINUSOIDAL) TimersSetModeSinusoidal() ;
uint8_t *sineTablePtr = sineTable ; // set to start of sine table
sineTableIncrement = Inco; // stepwidth = frequency selection
AdjustSineTableIndex(sineTableIncrement); // call the routine to update the pointer
//Add sine table offset to pointer. Must be multiplied by 3, since one
//value for each phase is stored in the table.
sineTablePtr += (sineTableIndex >> 8) * 3;
tempU = pgm_read_byte(sineTablePtr++);
if (fastFlags.desiredDirection == DIRECTION_FORWARD)
{
tempW = pgm_read_byte(sineTablePtr++);
tempV = pgm_read_byte(sineTablePtr);
}
else
{
tempV = pgm_read_byte(sineTablePtr++);
tempW = pgm_read_byte(sineTablePtr);
}
}
//Scale sine modulation values to the current amplitude.
tempU = ((uint16_t)(amplitude * tempU) >> 8);
tempV = ((uint16_t)(amplitude * tempV) >> 8);
tempW = ((uint16_t)(amplitude * tempW) >> 8);
{
uint8_t compareHigh, compareLow;
InsertDeadband(tempU, &compareHigh, &compareLow);
OCR0A = compareHigh;
OCR0B = compareLow;
InsertDeadband(tempV, &compareHigh, &compareLow);
OCR1AL = compareHigh;
OCR1BL = compareLow;
InsertDeadband(tempW, &compareHigh, &compareLow);
OCR2A = compareHigh;
OCR2B = compareLow;
}
// regular task calls the SpeedController
speedRegTicks++;
if (speedRegTicks >= SPEED_CONTROLLER_TIME_BASE)
{
SpeedController();
speedRegTicks = 0;
}
}
/*! \brief AD conversion complete interrupt service routine
*
* This interrupt service routine is automatically executed every time
* an AD conversion is finished and the converted result is available
* in the ADC data register.
*
* The switch/case construct makes sure the converted value is stored in the
* variable corresponding to the selected channel and changes the channel for
* the next ADC measurement.
*
* More ADC measurements can be added to the cycle by extending the switch/
* case construct.
* to avoid interfering with the main generator routine,
* this is declared as non-blocking
*
* Only the 8 most significant bits of the ADC result are used.
*/
ISR(ADC_vect, ISR_NOBLOCK)
{
if (fastFlags.externalControl) {
switch (ADMUX)
{
case ADMUX_FREQUENCY:
speedInput = (speedInput + ADCH) >> 1; // do some averaging
ADMUX = ADMUX_AMPLITUDE;
break;
case ADMUX_AMPLITUDE:
VperHz = (VperHz + ADCH) >> 1;
ADMUX = ADMUX_FREQUENCY;
break;
default:
//This is probably an error and should be handled. Instead we set the multiplexer and just run away
ADMUX = ADMUX_FREQUENCY;
break;
}
// restart the ADC
ADCSRA = (1 << ADEN) | (1 << ADSC) | (0 << ADATE) | (1 << ADIF) | (1 << ADIE) | ADC_PRESCALER;
}
}
PROGMEM char VersionandCopyright[36] = "1.1 (c)2009-2013 Matthias Schoeldgen";
PROGMEM char EndofFile[22] = "Ende der Fahnenstange ";
ไม่มีความคิดเห็น:
แสดงความคิดเห็น