Source file: /~heha/enas/Kleingeräte/ika-rkt.zip/main.cpp

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <util/delay.h>
#include "PMSM_tables.h"
#include <avr/fuse.h>
#include <avr/eeprom.h>

/* Hardware entsprechend AVR449
1	PB0	!OC1A	UL	Gate-Treiber U High-Side
2	PB1	 OC1A	UH	Gate-Treiber U Low-Side
3	PB2	!OC1B	VL	Gate-Treiber V High-Side
4	PB3	 OC1B	VL	Gate-Treiber V Low-Side
5	Ucc			5 V
6	GND
7	PB4	!OC1D	WL	Gate-Treiber W High-Side
8	PB5	 OC1D	WL	Gate-Treiber W Low-Side
9	PB6		L/T	V40511 transparent und dunkelgetastet wenn Low, Latch und aktiv wenn High
10	PB7	RESET		Debug-Ausgang
11	PA7	AIN1		Shunt zur Strombegrenzung
12	PA6		D3	Datenbus
13	PA5		D2	Datenbus, Inkrementalgeber-Drehung (Low)
14	PA4		D1	Datenbus, Inkrementalgeber-Drehung (Low)
15	AUcc			5 V
16	AGND
17	PA3		D0	Datenbus, Inkrementalgeber-Taste (Low)
18	PA2		H3	Positionssignal vom Hallsensor
19	PA1		H2	Positionssignal vom Hallsensor
20	PA0		H1	Positionssignal vom Hallsensor
U,V,W = gängige Bezeichnung für Anschlüsse eines Dreiphasenmotors
Der 4-Bit-Datenbus füttert das Latch des Siebensegmentdekoders,
steuert die Anzeige-Stellen und fragt die 3 Kontakte des Inkrementalgebers ab.
Die vierstellige Siebensegmentanzeige hat gemeinsame Katoden.
Der V40511 entspricht CD4511 mit Hexadezimalausgabe.
Diese wird nur für die Ausgabe von " OFF" benutzt.
Dezimalpunkte werden nicht angesteuert.
*/

enum{
 SOLL_MIN=30,	// U/min
 SOLL_MAX=1500
};

FUSES={
 0b11000001,	// HF PLL = 16 MHz, ungeteilt, schneller Hochlauf
 0b01010100,	// Kein RESET, EEPROM behalten, Brownout bei 4,3 V
 0b11111111	// Kein SPM
};

typedef uint8_t byte;
typedef uint16_t word;

struct CFG{	// Persistente Daten
 word soll;	// Soll-Drehzahl in U/min, für Display
 byte press;	// Gezählte Tastendrücke, Run = Bit 1, Richtung = Bit 2
}cfg;

byte t0h;	// Softwarezähler für Timer0, Bit 1:0 = aktuell angezeigtes Digit
byte nibbles[4];	// Auszugebende 4-Bit-Werte, von rechts nach links; Bit 4 = 1: Dunkel
byte blink;	// !=0 wenn ganze Anzeige blinkt (Anzeige Sollwert), Abwärtszähler
byte igstate;	// Bit 5 = Inkrementalgeber-Druckknopf-Schaltzustand
char igdiff;	// vzb. Inkrementalgeber-Verdrehung, ±4 = 1 Rastung (bei diesem Geber)
word ist;	// Ist-Drehzahl in U/min
// zur Drehzahlmessung
static volatile word puls;	// Anzahl Impulse
static word tick;		// Zeitpunkt des letzten Pulses der vorherigen Zeitnahme
static volatile word tack;	// Zeitpunkt des letzten Pulses

EEMEM CFG eecfg/*={50,1}*/;

// Asynchrones Update: Wartet nicht sondern stößt EEPROM-Schreibvorgang nur an
static void eeprom_update() {
 if (EECR&0x02) return;
 const byte*src=(const byte*)&cfg;
 byte*dst=(byte*)&eecfg, len=sizeof cfg;
 for (;len;src++,dst++,len--) {
  EEAR=(word)dst;
  EECR|=1;
  if (EEDR!=*src) {
   EEDR=*src;	// Adresse steht noch in EEAR
   cli();
   EECR|=0x04;
   sei();
   EECR|=0x02;	// immer noch mit gesperrten Interrupts (wird zu SBI compiliert)
   return;
  }
 }
}

static void outOFF() {	// wird (nur) beim Ausschalten des Rührers angezeigt
 nibbles[0]=0x0F;
 nibbles[1]=0x0F;
 nibbles[2]=0;
 nibbles[3]=0x10;
}

static void outErr() {	// Überstrom-Fehler; " Err" kann nicht angezeigt werden
 nibbles[0]=0x0E;
 nibbles[1]=0x0E;
 nibbles[2]=0x0E;
 nibbles[3]=0x0E;
}

static void outdez(word n) {
 if (n>9999) n=9999;
 byte*p=nibbles;
 do{
  *p++=n%10U;		// Dezimalzahl ausspucken
  n/=10U;
 }while(n);
 while (p!=nibbles+4) *p++=0x10;	// voreilende Stellen dunkeltasten
}

static word limit_soll(word soll) {
 if ((int)soll<SOLL_MIN) soll=SOLL_MIN;
 if ((int)soll>SOLL_MAX) soll=SOLL_MAX;
 return soll;
}

static void outsoll() {
 outdez(cfg.soll);
 blink=2*30;		// 2 s blinken
}

static void incsoll(char by) {
 cfg.soll=limit_soll(cfg.soll+by);
 outsoll();
}

// Aufruf erst mal mit lahmen 64 Hz
ISR(TIMER0_OVF_vect,ISR_NOBLOCK) {
 PORTA = 0x7F;	// Alle Katoden aus
 PORTB&=~0x40;	// V40511 dunkeltasten und transparent machen
 DDRA  = 0;	// auf Einlesen schalten
 byte k=++t0h&3;
 byte n=nibbles[k];	// (Zeit vertun)
 GPIOR0|=0x80;	// Marker für Hauptprogramm
 byte p=igstate;	// Vorhergehenden Zustand merken
 byte q=~PINA<<2;	// Bit 7:6 = Drehzustand, Bit 5 = Taste (1 = gedrückt)
 if (q&0x80) q^=0x40;	// Graykode (00-01-11-10-00) in Binärkode (00-01-10-11-00) wandeln
// if ((p^q)&0x20) q|=0x10;	// Veränderung des Tastenzustandes merken in Bit 4
 igstate=q;		// Als vorhergehenden Zustand speichern
 igdiff+=(char((q&0xC0)-(p&0xC0)))>>6;	// Verdrehung in Binärkodeschritten aufsummieren — das wird so prellfrei
 DDRA  = 0x78;	// Datenbus auf Ausgabe
 PORTA = byte((n&0x0F)<<3)|7;	// neues Nibble ausgeben
 k=n&0x10||blink&&t0h&0x20?0x7F:(~(8<<k))&0x7F;	// Katode vorbereiten
 PORTB|= 0x40;	// V40511 Nibble übernehmen und (umgehend) helltasten
 PORTA = k;	// Katode aktivieren
}

register word sineTableIncrement	asm("r10");
register word sineTableIndex		asm("r8");
register word commutationTicks		asm("r6");
register word amplitude			asm("r4");
register byte sineTableNextSectorStart	asm("r3");
register byte t1h			asm("r2");
byte advanceCommutationSteps;

static void SpeedController(word speedReference) {
 cli();
 amplitude = speedReference;
 sei();
}


static void DisablePWMOutputs()	{DDRB=0xC0;PORTB|=0x15;}
static void EnablePWMOutputs()	{DDRB=0xFF;}

static void TimerSetModeSinusoidal() {
  //Set PWM pins to input (Hi-Z) while changing modes.
 DisablePWMOutputs();
  //Set Timer/counter1 in phase and frequency correct mode.
 TCCR1A = 0xA3;		// High-Side nicht steuern!!
 TCCR1C = 0xA9;
 TCCR1D = 0xE9;		// Zentrierte PWM

 OCR1D=OCR1B=OCR1A=TC1H=0;

 GPIOR0|= 0x20;
 GPIOR0&=~0x40;		//WAVEFORM_SINUSOIDAL;
  //Wait for the PWM cycle to complete.
 TIFR = TIFR;
 while (!(TIFR & 1<<TOV1));
  //Change PWM pins to output again to allow PWM control.
 EnablePWMOutputs();
}


static void TimerSetModeBlockComm() {
 DisablePWMOutputs();	//Set PWM pins to input (Hi-Z) while changing modes.
 TCCR1A= 0x03;
 TCCR1C= 0x01;
 TCCR1D= 0xE9;
 OCR1D=OCR1B=OCR1A=TC1H=0;	// Keine Bestromung

 GPIOR0|= 0x20;
 GPIOR0|= 0x40;	//WAVEFORM_BLOCK_COMMUTATION;
 EnablePWMOutputs();
}


static void TimerSetModeBrake() {
  //Set PWM pins to input (Hi-Z) while changing modes.
 DisablePWMOutputs();
  //Set Timer/counter1 in phase and frequency correct mode.
 //TCCR1A = 0x53;
 //TCCR1C = 0x55;
 //TCCR1D = 0xE9;
  //All output duty cycles at 0 creates a braking force. (Low side braking)
 OCR1D=OCR1B=OCR1A=TC1H=0;

 GPIOR0&=~0x20;
 GPIOR0|= 0x40;	// WAVEFORM_BRAKING;

 EnablePWMOutputs();
}

static void BlockCommutate(byte hall) {
 byte t=pgm_read_byte((GPIOR0&WANTDIR_REVERSE
   ?blockCommutationTableReverse
   :blockCommutationTableForward)+hall);
 PORTB|=0x15;	// keine High-Side-Treiber
 TCCR1C=0x01;	// keine Low-Side-Treiber
 if (t&0x01) PORTB&=~0x01;
 if (t&0x02) TCCR1C|=0x80;
 if (t&0x04) PORTB&=~0x04;
 if (t&0x08) TCCR1C|=0x20;
 if (t&0x10) PORTB&=~0x10;
 if (t&0x20) TCCR1C|=0x08;
}


static byte GetHall() { return PINA&7;}


static void DesiredDirectionUpdate() {
 if (cfg.press&4) GPIOR0|=WANTDIR_REVERSE;
 else GPIOR0&=~WANTDIR_REVERSE;
}

// Flanke zählen (in <puls>) und Zeitpunkt festhalten (in <tack>)
// Interrupts gesperrt bei Aufruf!
inline void countpuls() {
 puls++;
 union{
  byte b[2];
  word w;
 }t={{TCNT0L,t0h}};
 // Überlaufproblem droht! Ggf. 16-Bit-Zähler nutzen!
 if (t.b[0]<0x80 && TIFR&2) t.b[1]++;	// ISR nachbilden
 tack=t.w;
}


static void ActualDirectionUpdate(byte lastHall, byte newHall) {
  //Make sure that lastHall is within bounds of table. If not, set to an
  //illegal hall value, but legal table index.
 if (lastHall > 7) lastHall = 0;
 if (pgm_read_byte(HallSequenceForward+lastHall) == newHall) {
  GPIOR0&=~DIR_REVERSE;
  GPIOR0|= DIR_KNOWN;
  countpuls();
 }else if (pgm_read_byte(HallSequenceReverse+lastHall) == newHall) {
  GPIOR0|= DIR_REVERSE;
  GPIOR0|= DIR_KNOWN;
  countpuls();
 }else{
  GPIOR0&=~DIR_KNOWN;
  GPIOR0&=~MOTOR_SYNCHRONIZED;
  puls=0;
 }
}

static word SineTableIncrementCalculate(word ticks) {
  return word((SINE_TABLE_LENGTH/6 << 8) / ticks);
}


static void AdjustSineTableIndex(word 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;
  sineTableNextSectorStart -= SINE_TABLE_LENGTH;
 }

  //Make copy of sineNextSectorStart to specify order of volatile access.
 byte nextSectorStart = sineTableNextSectorStart;
 if ((sineTableIndex >> 8) > nextSectorStart) {
  sineTableIndex = (nextSectorStart << 8);
 }
}


inline void CommutationTicksUpdate() {
 if (commutationTicks < COMMUTATION_TICKS_STOPPED) commutationTicks++;
 else{
  GPIOR0|= MOTOR_STOPPED;
  GPIOR0&=~MOTOR_SYNCHRONIZED;
  sineTableIncrement = 0;
  if ((GPIOR0&0x60)!=WF_BLOCK_COMMUTATION) {
   TimerSetModeBlockComm();
   BlockCommutate(GetHall());
  }
 }
}


inline void MotorSynchronizedUpdate() {
 static uint8_t synchCount = 0;

 if (GPIOR0&DIR_KNOWN
 && !((GPIOR0>>2^GPIOR0)&DIR_REVERSE) /*desiredDirection == actualDirection*/
 && !(GPIOR0&MOTOR_STOPPED)
 && !(GPIOR0&MOTOR_SYNCHRONIZED)) {
  synchCount++;
  if (synchCount >= SYNCHRONIZATION_COUNT) GPIOR0|=MOTOR_SYNCHRONIZED;
 }else synchCount = 0;
}


inline byte getSine(byte index) {
  //Im letzten Drittel 0 liefern
 if (index>=128) return 0;
  //Im zweiten Drittel gespiegelten Funktionswert liefern
 if (index>=64) index = 127-index;
 return pgm_read_byte(sineTable+index);
}


/*! Fast unsigned multiply of a 15 bit number with an 8 bit number with 15 bit result.
 *  50 CPU clock cycles.
 */
inline word MultiplyUS15x8(const word m15, const byte m8) {
  unsigned int result = 0;
  if (m8 & 1<<0)  result += m15;  result >>= 1;
  if (m8 & 1<<1)  result += m15;  result >>= 1;
  if (m8 & 1<<2)  result += m15;  result >>= 1;
  if (m8 & 1<<3)  result += m15;  result >>= 1;
  if (m8 & 1<<4)  result += m15;  result >>= 1;
  if (m8 & 1<<5)  result += m15;  result >>= 1;
  if (m8 & 1<<6)  result += m15;  result >>= 1;
  if (m8 & 1<<7)  result += m15;  result >>= 1;
  return result;
}

inline void SineOutputUpdate() {
 uint16_t temp;
 uint8_t iU,iV,iW;

  //Calculate U phase output duty cycle.
 iU = (uint8_t)(sineTableIndex >> 8);
 temp = getSine(iU);
 if (temp) {
  temp = MultiplyUS15x8(amplitude,temp);
  PORTB|=0x01;			// High-Side-Treiber ausschalten
 }
 TC1H = uint8_t(temp>>8);
 OCR1A=uint8_t(temp);
 if (!temp) PORTB&=~0x01;	// High-Side-Treiber aktivieren

 iV = iU+64;			// Index für V
 if (iV>=192) iV -= 192;
 iW = iV+64;			// Index für W
 if (iW>=192) iW -= 192;
 if (GPIOR0&WANTDIR_REVERSE) {iU=iV;iV=iW;iW=iU;}

 temp = getSine(iV);
 if (temp) {
  temp = MultiplyUS15x8(amplitude,temp);
  PORTB|=0x04;
 }
 TC1H = uint8_t(temp>>8);
 OCR1B=uint8_t(temp);
 if (!temp) PORTB&=~0x04;

 temp = getSine(iW);
 if (temp) {
  temp = MultiplyUS15x8(amplitude,temp);
  PORTB|=0x10;
 }
 TC1H = uint8_t(temp>>8);
 OCR1D=uint8_t(temp);
 if (!temp) PORTB&=~0x10;
}


/*! \brief Hall sensor change interrupt service routine.
 *
 *    - Synchronize sine wave generation to hall sensors.
 *    - Block commutation.
 *    - Direction control.
 *    - Synchronization control.
 *    - Sine table increment calculation.
 *    - Stop detection.
 */
ISR(PCINT_vect) {
 static uint8_t lastHall = 0xff;
 uint8_t hall;

 hall = GetHall();

 if (hall == lastHall) return;

 MotorSynchronizedUpdate();
 if (((GPIOR0&0x60)!=WF_SINUSOIDAL) && GPIOR0&MOTOR_SYNCHRONIZED) {
    TimerSetModeSinusoidal();
 }

  //If sinusoidal driving is used, synchronize sine wave generation to the
  //current hall sensor value. Advance commutation (lead angle) is also
  //added in the process.
 switch (GPIOR0&0x60) {
  case WF_SINUSOIDAL: {
   uint16_t tempIndex;
   tempIndex = (pgm_read_byte((GPIOR0&WANTDIR_REVERSE
     ?CSOffsetsReverse
     :CSOffsetsForward)+hall)
     + advanceCommutationSteps) << 8;
   sineTableIndex = tempIndex;

    //Adjust next sector start index. It might be set to a value larger than
    //SINE_TABLE_LENGTH at this point. This is adjusted in AdjustSineTableIndex
    //and should not be done here, as it will cause problems when advance
    //commutation is used.
   sineTableNextSectorStart = (tempIndex>>8) + SINE_TABLE_LENGTH/6;

  }break;
  //If block commutation is used. Commutate according to hall signal.
  case WF_BLOCK_COMMUTATION: {
   BlockCommutate(hall);
  }break;
 }

  //Update the actual direction flag.
 ActualDirectionUpdate(lastHall, hall);

 lastHall = hall;

  //Calculate new step size for sine wave generation and reset commutation
  //timer.
 sineTableIncrement = SineTableIncrementCalculate(commutationTicks);
 commutationTicks = 0;

  //Since the hall sensors are changing, the motor can not be stopped.
 GPIOR0&=~MOTOR_STOPPED;
}


/*  32 kHz, PWM-Ausgänge wurden aktualisiert
 *
 *  The responsibilities of this ISR are:
 *    - Sine wave modulation update.
 *    - Direction command input.
 *    - Stop detection.
 *    - Speed controller timing.
 */
ISR(TIMER1_OVF_vect) {
 PORTB|= 0x80;
 switch (GPIOR0&0x60) {
  case WF_SINUSOIDAL: {
   AdjustSineTableIndex(sineTableIncrement);
   SineOutputUpdate();
  }break;
  case WF_BLOCK_COMMUTATION: {
   uint16_t blockCommutationDuty = amplitude * BLOCK_COMMUTATION_DUTY_MULTIPLIER;
   if (blockCommutationDuty > 0x3FF) blockCommutationDuty = 0x3FF;
   TC1H=byte(blockCommutationDuty>>8);
   OCR1D=OCR1B=OCR1A=byte(blockCommutationDuty);
  }break;
 }
  //Update desired direction flag.
 DesiredDirectionUpdate();

 static uint8_t lastDesiredDirection = 0xff;
 if ((GPIOR0&WANTDIR_REVERSE)!=lastDesiredDirection) {
    //Set motor in brake mode. The motor will automatically start once it is
    //synchronized or stopped.
  GPIOR0&=~MOTOR_SYNCHRONIZED;
  if (GPIOR0&DIR_KNOWN)
    TimerSetModeBrake(); // Automatically sets driveWaveform.

  lastDesiredDirection = GPIOR0&WANTDIR_REVERSE;
 }

 CommutationTicksUpdate();

 if (t1h) --t1h;
 PORTB&=~0x80;
}


/*! \brief Hardware fault protection interrupt.
 *
 *  This ISR will be run every time the hardware fault protection disables the
 *  PWM outputs. The required action to this event will be different from
 *  application to application. Here, a delay is inserted, before the PWM
 *  outputs are once again enabled.
 */
ISR(FAULT_PROTECTION_vect,ISR_NOBLOCK) {
 outErr();
 _delay_ms(1000);
 TCCR1D |= 1<<FPEN1;
}


int main(void) {
 PORTA = 0x7F;		// überall Pullups außer an AIN1
 PORTB = 0x15;		// Invertierte Ausgänge standardmäßig High
 DDRB  = 0xC0;
 eeprom_read_block(&cfg,&eecfg,sizeof cfg);
 cfg.soll=limit_soll(cfg.soll);
 cfg.press=cfg.press&~2|1;	// Motor aus, Taste gelöst beim Einschalten
 outdez(8888);
 TCCR0B= 0x04;		// Vorteiler 256, Überlauf 16 MHz/256/256 = 250 Hz
 TIMSK = 0x06;		// Überlaufinterrupts für beide Timer
 ACSRA = 0x40;		// 1,1 V am +, AIN1 an -: Fallende Flanke bei Überstrom
 ACSRB = 0x80;		// etwas Hysterese
 while (!(PLLCSR & 1<<PLOCK));  //Warte bis PLL eingerastet
 PLLCSR= 0x04;		// Als Timer1-Taktquelle selektieren (64 MHz)
 TCCR1B= 0x01;		// Ohne Vorteiler
 TCCR1D= 0xEC;		// Überstromdetektor aktivieren via Analogvergleicher
// DT1   = DEAD_TIME<<4 | DEAD_TIME;	// Beide Totzeiten einstellen
 TC1H  = 3;
 OCR1C = 0xFF;		// PWM top = 0x3FF, Überlauf bei 2×1023; ~ 32 kHz
 PCMSK0= 0x07;		// Pegelwechselinterrupt an Hall-Eingängen
 PCMSK1= 0;
 GIMSK = 0x20;		// Pegelwechselinterrupt freigeben
 DDRA  = 0x07;		// jetzt auslösen
 DDRA  = 0;


 t1h = SPEED_CONTROLLER_TIME_BASE;
 advanceCommutationSteps=0;

    // Set motorStopped to FALSE at startup. This will make sure that the motor
    // is not started if it is not really stopped. If it is stopped, this variable
    // will quickly be updated.

    // Set motorSyncronized to FALSE at startup. This will prevent the motor from being
    // driven until the motor is in synch or stopped.
 GPIOR0 = 0;

 DesiredDirectionUpdate();

  //Enable interrupts globally and let motor driver take over.
 sei();
 MCUCR=0x20;	// Sleep Enable
 _delay_ms(100);	// 8888 anzeigen (Display-Test 0,1 s)
 outsoll();

 for (;;) {
  sleep_cpu();	// Schlafen bis Interrupt: Timer1, Hall-Sensoren, Fehlereingang
  if (GPIOR0&0x80) {	// Timer0?
   if (!(t0h&0x3F)) {	// Alle 64 Überläufe, 3,81 Hz; 262144 µs (0x40000 µs)
    cli();
    word d=tack-tick;	// Zeitdifferenz
    tick=tack;
    word n=puls;	// Anzahl Flanken (12 pro Umlauf)
    puls=0;
    sei();
/* Berechnung der Istdrehzahl anhand der Flanken von den 3 Hall-Gebern, Terme:
 n	Anzahl detektierter Pulse in den 262 ms = 64*256 = 0x4000 Zählertakten
 F_CPU	Speisetakt für Timer0-Vorteiler
 60	60 Sekunden pro Minute
 256	Vorteiler Timer0
 12	Flanken pro Umdrehung (Flanken aller 3 Hall-Geber)
 ()	Dieser konstante Ausdruck ergibt 312500 = 0x4C4B4, eine 32-Bit-Zahl
 d	Zeit zwischen vergangenem letzten und jetzig letztem Impuls,
	bei Synchronität 64*256 = 0x4000 ergebend
*/
    if (n) n=n*uint32_t(F_CPU*60/256/12)/d;
    ist=n;
    if (!blink) outdez(ist);
   }
   if (!(t0h&7)) {	// Inkremente in geringerer Frequenz (30 Hz) auswerten
    char d=igdiff;
    int s=cfg.soll;
    if (d>=4) {
     incsoll(s>200?s>500?s>1000?-50:-20:-10:-5);
     d-=4;
     igdiff=d;
    }else if (d<=-4) {
     incsoll(s>=200?s>=500?s>=1000?50:20:10:5);
     d+=4;
     igdiff=d;
    }else{
     if (blink&&!--blink) outdez(ist);
     eeprom_update();
    }
    if (!((igstate>>5^cfg.press)&1)	// Taste gedrückt oder losgelassen
    && !(++cfg.press&1)) {	// Taste gedrückt
     if (cfg.press&2) outsoll();
     else outOFF();
     blink=1*30;		// Blinken nur 1 s
    }
   }
   GPIOR0&=~0x80;
  }
  if (!t1h) {
   t1h = SPEED_CONTROLLER_TIME_BASE;
   SpeedController(cfg.press&2?cfg.soll:0);
  }
 }
}
Detected encoding: UTF-80