Source file: /~heha/ewa/Kram/smfg.zip/Firmware/main.cpp

/* Frequenzgenerator (Rechteck) für Schrittmotor-Endstufen
   für ATtiny2313A @ 8 MHz, für Johannes Rudolph.
   „h#s“ Henrik Haftmann, TU Chemnitz, 23. Oktober 2019
   tabsize = 8, encoding = utf-8 (ohne Windows-Header BOM)
Hardware:
1	PA2	RESET	ISP
2	PD0	(RxD)	frei
3	PD1	(TxD)	Ausgang !ENA
4	PA1	XTAL1	(Quarz) - frei
5	PA0	XTAL2	(Quarz) - frei
6	PD2	(INT0)	K1 = 3-stellige 7segment-Anzeige gemeinsame Katode links über 100 Ω
7	PD3	(INT1)	Segment e
8	PD4	(T0)	Segment b; Inkrementalgeber (10 kΩ gegen Masse)
9	PD5	(OC0B)	K3 = Katode rechts über 100 Ω
10	GND		0 V
11	PD6	(ICP1)	Segment h (Dezimalpunkt)
12	PB0	(AIN0)	Segment c
13	PB1	(AIN1)	K2 = Katode Mitte über 100 Ω
14	PB2	(OC0A)	Segment d
15	PB3	OC1A	Ausgang STEP
16	PB4	OC1B	Ausgang !DIR
17	PB5	MOSI	ISP; Segment g; Inkrementalgeber (10 kΩ gegen Masse)
18	PB6	MISO	ISP; Segment a; Inkrementalgeber: Tastfunktion (10 kΩ gegen Masse)
19	PB7	SCK	ISP; Segment f
20	Ucc		5 V

Timer0: Anzeige-Multiplex-Takt 800 Hz
Timer1: Frequenzgenerator im CTC-Modus, Output-Compare-Ausgänge im Toggelbetrieb
EEPROM: Die einstellbare Sollfrequenz, die zuletzt gewählte Richtung sowie Ein/Aus
wird darin gespeichert.
Alle anderen Werte (bspw. Getriebefaktor) können nur per EEPROM-Editor
oder Neuerstellung geändert werden.
*/

#include <avr/io.h>
#include <avr/pgmspace.h>
#include <avr/eeprom.h>
#include <avr/fuse.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <avr/signature.h>
#include <util/delay.h>
#include <string.h>	// memset

FUSES={
 0b11100100,	// kein Taktteiler, RC-Oszillator 8 MHz
 0b11011111,	// (unverändert)
 0b11111111	// (unverändert)
};

typedef uint8_t byte;
typedef uint16_t word;
typedef uint32_t dword;
#define NOINIT __attribute__((section(".noinit")))

struct Config{
 word f_soll;	// Zieldrehzahl in 1/100 U/min (Anzeigewert, Default = 10 U/min = 1/6 U/s)
 word fac16;	// Frequenzfaktor: F_CPU/16 / fac16 / Drehzahl / 2 = Timer-Teilerwert
 word f_min;	// Startdrehzahl in 1/100 U/min (Default = 0,1 U/min = 1/600 U/s)
 word f_max;	// Maximalfrequenz (Vorgabe = 600 U/min = 10 U/s = irre schnell)
 word accel;	// Relative Beschleunigungszeit (Vorgabe = 20)
 word deccel;	// Relative Bremszeit (Vorgabe = 10)
 union{
  struct{
   byte dir:1;	// Richtung
   byte ena:1;	// Motor dreht (1) oder steht (0)
   byte alt:1;	// Mit jedem Stopp wird die Drehrichtung gewechselt
   byte sin:1;	// Sin+Cos-Ausgabe statt Step+Dir
   byte rsv:2;	// frei für Erweiterung
   byte ok:2;	// muss 0b10 sein wenn gültig
  };
  byte b2;
 };
 bool valid() {return ok==2;}
};

static Config c NOINIT;
static EEMEM Config eedata;
static const PROGMEM Config ee_defaults = {
 /* f_soll */	1000,		// 10 U/min = 1/6 U/s
 /* fac16 */	48*6*8>>4,	// Schritte pro Umlauf, Getriebe, Mikroschritt, ÷16
 /* f_min */	10,		// 0,1 U/min
 /* f_max */	60000,		// 600 U/min
 /* accel */	20,		// ≈ 1 s
 /* deccel */	10,{		// ≈ ½ s
 /* dir */	0,
 /* ena */	0,
 /* alt */	0,
 /* sin */	0,
 /* rsv */	0,
 /* ok */	2
}};
// Die meisten Werte können nur durch Ändern des EEPROMs eingestellt werden!
// Für eine Menüführung fehlt dem ATtiny2313A Programmspeicher → ATtiny4313

static void eeprom_update(const void*ram, const void*eeprom, byte len) {
 if (EECR&2) return;
 const byte*a=static_cast<const byte*>(ram);
 EEAR=(word)eeprom;
 do{
  EECR|=1;
  byte b=*a++;
  if (b!=EEDR) {
   byte c=4;			// löschen und schreiben (3,4 ms)
   if (b==0xFF) c|=0x10;	// nur löschen (1,8 ms)
   if (EEDR==0xFF) c|=0x20;	// nur schreiben (1,8 ms)
   EEDR=b;
   cli();
   EECR=c;
   EECR|=2;
   sei();
   return;
  }
  EEAR++;
 }while(--len);
}

static byte digi[3] NOINIT;	// Ganz normal Bit 0 = Segment a bis Bit 7 = Segment h
static byte attr[3] NOINIT;	// 0: Hell, 5-7: Blinkphasen
register byte segment asm("r2");// aktuell gemultiplextes Segment (0..7); Blinkphase
register byte tccr1b asm("r3");
register word ocr1a asm("r4");
static byte keystate;

// liefert geänderte Bits sowie Vor/Zurück-Info für Inkrementalgeber:
// Bit 3 = Flanke an Inkrementalgeber-Taste
// Bit 4 = Inkrementalgeber-Aktivität
// Bit 5 = Richtung der Inkrementalgeber-Aktivität, sonst undefiniert
static byte getKeys() {
 byte k=0;
 if (!(PINB&0x40)) k|=0x08;	// Inkrementalgeber-Taste
 if (!(PIND&0x10)) k|=0x10;	// Inkrementalgeber
 if (!(PINB&0x20)) {k|=0x20; k^=0x10;}	// Graykode zu Binärkode
 byte ksh=keystate&0xF0;	// High-Nibble = Inkrementalgeber-Zähler
 ksh+=(char)((k&0xF0)-ksh<<2)>>2;	// Differenz (-2, -1, 0 oder +1) dazu
 byte ks=(k&0x0F)+ksh;
 byte kss=(ks^keystate)&0x0F;	// geänderte Bits (nur Tasten)
 byte diff=(ks&0xF0)-(keystate&0xF0);	// Differenz per Subtraktion
 keystate=ks;			// neuer Tastenstatus
 return kss|diff;
}

// Um Widerstände zu sparen werden die Anoden 1-aus-n durchgeschaltet
// und bei LED ein die jeweilige Katode aktiviert
// Die Pins sind wild verteilt um 1. an OC1A und OC1B heranzukommen
// und 2. die Platine einfacher zu verdrahten
// Um für Charlieplexing gewappnet zu sein
// werden die Treiberleitungen in Tristate gehalten.
static byte multiplex() {
 DDRB  = 0b00011000;	// Alle 8 Anoden auf Eingang (für bis zu 8 Tasten),
 DDRD  = 0b00000010;	// alle 3 Katoden aus
 PORTB|= 0b11100101;
 PORTD|= 0b01011000;	// Alle 8 Anoden mit Pullups
 _delay_us(10);
 byte kss=getKeys();
 byte m,seg=segment&7;
 PORTB&= 0b00011000;	// Anoden f,a,g,d,c aus, Katode K2 aus
 PORTD&= 0b10000011;	// Anoden h,b,e aus, Katode K1 + K3 aus
 static const PROGMEM byte anodeB[8]={1<<6,  0 ,1<<0,1<<2,  0 ,1<<7,1<<5, 0  };
 static const PROGMEM byte anodeD[8]={ 0  ,1<<4, 0  , 0  ,1<<3, 0  , 0  ,1<<6};
 m=pgm_read_byte(anodeB+seg); PORTB|=m; DDRB|=m;
 m=pgm_read_byte(anodeD+seg); PORTD|=m; DDRD|=m;
 m=1<<seg; seg=segment&0xE0;
 if (digi[0]&m && !(attr[0]&seg)) DDRD|=1<<2;	// Katode K1 ein
 if (digi[1]&m && !(attr[1]&seg)) DDRB|=1<<1;	// Katode K2 ein
 if (digi[2]&m && !(attr[2]&seg)) DDRD|=1<<5;	// Katode K3 ein
 return kss;
}
// Zeitgeber für Display-Multiplex (Multiplex in Hauptschleife)
ISR(TIMER0_COMPA_vect) {
 byte s=++segment;
 s&=0x3F; if (!s) GPIOR0|=4;	// Triggersignal mit 12 Hz
 s&=0x0F; if (!s) GPIOR0|=2;	// Triggersignal mit 50 Hz
 GPIOR0|=1;			// Triggersignal mit 800 Hz
}
// Helligkeitsreduktion (per Digit)
ISR(TIMER0_COMPB_vect) {
 if (!(attr[0]&1)) DDRD&=~(1<<2);	// Katode K1 (vorzeitig) aus
 if (!(attr[1]&1)) DDRB&=~(1<<1);	// Katode K2 (vorzeitig) aus
 if (!(attr[2]&1)) DDRD&=~(1<<5);	// Katode K3 (vorzeitig) aus
}
// Neuen Endwert und neuen Vorteiler laden
// Wegen umzuschaltenden Vorteiler kann kein doppelt gepufferter PWM-Modus verwendet werden.
ISR(TIMER1_COMPA_vect) {
 TCCR1B= tccr1b;
 OCR1A = ocr1a;
 OCR1B = ocr1a>>1;	// 90° phasenverschoben
 TIMSK = 0x05;
}

/*****************
 * Zahlenausgabe *
 *****************/

static const PROGMEM byte seg7[10]={
 0b00111111,
 0b00000110,
 0b01011011,
 0b01001111,
 0b01100110,
 0b01101101,
 0b01111101,
 0b00000111,
 0b01111111,
 0b01101111
};

// Dreistellige Zahl ausgeben (n maximal 999)
static void outnumber(word n) {
 const byte base=10;
 for (byte i=0; i<3; i++) {
  byte z=n%base;
  n/=base;
  digi[2-i]=pgm_read_byte(seg7+z);
 }
}

// 3.2-Festkommazahl ausgeben, so dass notfalls Kommastellen abgeschnitten werden
static void show(word n) {
 byte kpos=0;		// (1) Bereich 0,00 .. 9,99 U/min
 while (n>999) {	// (2) Bereich 10,00 .. 99,99 U/min
  n/=10; kpos++;	// (3) Bereich 100,00 .. 655,35 U/min - schneller rast ein Schrittmotor mit Getriebe und Mikroschrittsteuerung nicht!
 }
 outnumber(n);
 digi[kpos]|=0x80;
}

static dword dividend NOINIT;

// Frequenz f auf Timer1 ausgeben, dabei minimalen Vorteiler benutzen
// Ein PWM-Modus (mit Doppelpufferung von TOP) kann nicht verwendet werden,
// weil die Vorteiler-Umschaltung stets ungepuffert ist:
// Die Umschaltung muss unmittelbar nach Zähler-Neustart erfolgen.
static void outfreq(word f) {
 byte ps=0;	// Vorteiler, kommt nach TCCR1B
 dword t0=0;
 if (f) {
  ps=0x09;	// CTC-Modus mit TOP == OCR1A, Vorteiler 1
  t0=dividend/f;
  while (t0 & 0xFFFF0000UL) {		// 2: Vorteiler 8	>>3
   t0>>=++ps<0x0C?3:2;			// 3: Vorteiler 64	>>3
  }					// 4: Vorteiler 256	>>2
  if (ps>=0x0E) ps=0;			// 5: Vorteiler 1024	>>2
 }		// ps==0: Frequenz war kleiner als 0,06 Hz (T > 16 s)
 if (TCCR1B) {		// Timer läuft: Später aktualisieren
  ocr1a = word(t0);	// vorhalten für ISR
  tccr1b= ps;
  TIFR  = 0x40;
  TIMSK = 0x45;		// Interrupt jetzt aktivieren: Laden kurz nach Zähler-Neustart
 }else{			// Timer steht: Starten
  TCNT1 = 0;
  OCR1A = word(t0);	// die o.a. Division rundet sowieso ab
  OCR1B = word(t0)>>1;
  TCCR1B= ps;
 }
}

// exponentielles Inkrementieren/Dekrementieren:
// Finde ein „glatten“ Inkrementwert 1,2,5 * 10^n für die Vorgabe v
// 0..1		1
// 2..4		2
// 5..		5
// 10..		10
// 20..40	20
// 50..		50
// usw.
// 50000..65535	50000
static word calcIncr(word v) {
 word i=1;
 while (v>=10) {v/=10; i*=10;}
 if (v>=5) i*=5;
 else if (v>=2) i*=2;
 return i;
}

// Templates statt Makros: Für einfache Typen; bei komplexen Typen wäre T& besser
template<class T>T min(T a,T b){return a<b?a:b;}
template<class T>T max(T a,T b){return a>b?a:b;}

static bool rampDown(word&v,word i) {
 if (v<i) {v=0; return true;}
 v-=i; return false;
}
static word rampUp(word v,word max,word i) {
 dword w=(dword)v+i;
 return w>max ? max : word(w);
}
static void swapDir() {
 c.b2^=1;
 if (c.sin) TCCR1C=0x40;
 else PINB|=0x10;	// Richtung wechseln
}

static word f_ist;	// in 1/100 U/min = 1/6000 Hz

static void ramp() {
 if (c.ena) {
  PORTD&=~(1<<1);	// lineare(!) Frequenzrampe (für Schrittmotoren günstiger als exponentiell)
			// Das macht die Bezugnahme auf f_soll für die Schrittweitenberechnung
  f_ist = f_ist ? rampUp(f_ist,c.f_soll,calcIncr(c.f_soll/c.accel)) : min(c.f_min,c.f_soll);
 }else{
  rampDown(f_ist,calcIncr(c.f_soll/c.deccel));	// schneller runter als rauf
  if (f_ist < c.f_min) {
   f_ist=0;
   PORTD|= 1<<1;
   if (c.alt) swapDir();
  }
 }
 outfreq(f_ist);	// PWM-Ausgabe nachführen
}

// Zeige Istdrehzahl (während Bewegung) oder blinkende Solldrehzahl (bei Stillstand)
static void show() {
 if (f_ist) {
  attr[0]&=~0xE0;
  attr[1]&=~0xE0;
  attr[2]&=~0xE0;	// nicht blinken
  show(f_ist);
 }else{
  attr[0]|= 0x20;	// blinken
  attr[1]|= 0x20;
  attr[2]|= 0x20;
  show(c.f_soll);
 }
}

static void initHardware() {
 PORTA = 0b00000111;	// Pullups (der offenen Eingänge) aktivieren
 PORTB = 0b00011010;
 DDRB  = 0b00011000;	// Ausgänge aktivieren
 PORTD = 0b00000011;
 DDRD  = 0b00000010;
 ACSR |= 0x80;	// Analogvergleicher deaktivieren
 OCR0A = 155;	// ÷156
 TCCR0A= 0x02; 	// CTC-Modus
 TCCR0B= 0x03;	// mit Vorteiler 64 ergibt 8 MHz / 64 / 78 = 801 Hz Interruptfrequenz
 OCR0B = 38;	// ¼ Helligkeit im Normalfall
 TCCR1A= c.sin ? 0x50 : 0x40;	// Toggle ergibt automatisch 50 % Tastverhältnis
		// Kein PWM-Modus, Ausgang OC1B bei Sin/Cos-Betrieb
 TIMSK = 0x05;	// Interruptroutine: Anzeige-Multiplex mit 100 Hz, dimmbar mit OCR0B
 MCUCR = 0x20;	// sleep aktivieren
 sei();
}

// Inkrementalgeber-Flanken pro Raststufe (es gibt solche mit 1, 2 oder 4 Flanken)
static const char FPR=2;

static void lampentest() {
 memset(digi,0xFF,sizeof digi);	// Alle Segmente ein
 memset(attr,1,sizeof attr);	// Hell
 do{
  sleep_cpu();
  if (!(GPIOR0&1)) continue;
  GPIOR0&=~1;
  multiplex();
 }while (segment);	// für kurze Zeit
 memset(attr,0,sizeof attr);	// Reduzierte Helligkeit
}

int main() {
 eeprom_read_block(&c,&eedata,sizeof c);
 if (c.valid()) {
  if (c.dir) PORTB&=~0x10;
 }else memcpy_P(&c,&ee_defaults,sizeof c);
 initHardware();
 dividend = dword(F_CPU>>5)*6000/c.fac16;
 lampentest();
 attr[2]=0x01;
 char inkmem=0;
 bool pressrotated=false;
 for(;;) {
  sleep_cpu();
  if (GPIOR0&1) {	// 800 Hz
   GPIOR0&=~1;
   byte keychange=multiplex();
   if (keychange&0x10) {
    inkmem+=keychange&0x20?-1:1;
    char inkr=0;
    if (inkmem<=-FPR) {inkr=-1; inkmem+=FPR;}
    if (inkmem>=+FPR) {inkr=+1; inkmem-=FPR;}
    if (inkr) {
     byte stepbase=100;	// macht Einerschritte
     if (keystate&0x08) {	// gleichzeitig gedrückt?
      pressrotated=true;
      stepbase=10;		// Zehnerschritte
     }
     if (c.dir) inkr=-inkr;	// Drehwirkung umdrehen
     word i=calcIncr(c.f_soll/stepbase);
     if (inkr>=0) c.f_soll=rampUp(c.f_soll,c.f_max,i);
     else if (rampDown(c.f_soll,i)) swapDir();
    }
   }
   if (keychange&0x08) {
    if (keystate&0x08) {
     pressrotated=false;
     attr[2]&=~1;
     attr[1]|= 1;	// helle Zehnerstelle
    }else{
     attr[1]&=~1;
     attr[2]|= 1;	// helle Einerstelle
     if (!pressrotated) c.b2^=2;	// ein/aus beim Loslassen-ohne-Drehen
    }
   }
  }
  if (GPIOR0&2) {	// 50 Hz
   GPIOR0&=~2;	  
   ramp();
  }
  if (GPIOR0&4) {	// 12 Hz
   GPIOR0&=~4;
   show();
   eeprom_update(&c,&eedata,sizeof c);
  }
 }
}
Detected encoding: UTF-80