Source file: /~heha/basteln/Konsumgüter/Kram/2021/fz.zip/fz3.cpp

/* Name: fz.cpp; TABSIZE=8, ENCODING=UTF-8, LINESEP=LF
 * Projekt: Drehzahlmesser
 * Zweck: Ausstattung einer Fräsmaschine: Spindeldrehzahl
 * Autor: Henrik Haftmann
 */

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/fuse.h>
#include <avr/signature.h>

FUSES={
  0b11011101,	// Keramikresonator, schnell hochfahren, kein Taktteiler
  0b01010101,	// RESET als Portpin, BrownOut bei 2,7 V
  0b11111111,	// unverändert
};


/* Pin-Zuordnung, LED-Anzeige gemeinsame Katode im Charlieplex
Pin	Port	Funk.	Verwendung
1	Ucc		5 V über 2 Dioden
2	PB0	XTAL1	Quarz 12 MHz
3	PB1	XTAL2	Quarz 12 MHz
4	PB3		LED h
5	PB2		LED g
6	PA7	ICP	Frequenzmess-Eingang
7	PA6		LED f
8	PA5		LED e
9	PA4		LED d
10	PA3		LED c,K3
11	PA2		LED b,K2,c
12	PA1		LED a,K1,b
13	PA0		LED   K0,a
14	GND		Masse
*/

typedef unsigned char byte;
typedef unsigned short word;
typedef unsigned long dword;
typedef unsigned long long qword;
#define NOINIT __attribute__((section(".noinit")))

static byte seg[4] NOINIT;	// von links nach rechts
register byte t0h asm("r2");	// High-Teil von Timer0 (2 Bit)
register char t1h asm("r3");	// High-Teil von Timer1
#if 1
register word captn asm("r4");	// Anzahl Flanken = Capture-Ereignisse
register word capt1 asm("r6");	// Letzter Capture-Wert
#else
static volatile word captn NOINIT;	// Anzahl Flanken = Capture-Ereignisse
static volatile word capt1 NOINIT;	// Letzter Capture-Wert
#endif
// Register R8 ff. wird stets(!) von __udivmod64 benutzt.
// Register R4..R8 werden in onT1O genutzt. Nur R2..R3 erscheinen frei.

// Multiplexen der Anzeige: Die Charlieplex-Verdrahtung ist hier regelmäßig:
// Das Katodenbit schiebt die Anodenbits ab da nach links.
// Bit:		8  7  6  5  4  3  2  1  0
// Segment D0:	h  g  f  e  d  c  b  a  K
//	   D1:	h  g  f  e  d  c  b  K  a
//	   D2:	h  g  f  e  d  c  K  b  a
//	   D3:	h  g  f  e  d  K  c  b  a
// Ausgang:	B3 B2 A6 A5 A4 A3 A2 A1 A0
// Interruptfrequenz F_CPU/8/256 = 1,9 kHz, 488 Hz Multiplexfrequenz
// Um die Capture-ISR nicht zu beeinträchtigen mit freigegebenen Interrupts,
// Rekursion unwahrscheinlich und (falls doch) unschädlich
ISR(TIM0_OVF_vect,ISR_NOBLOCK) {
  byte b = ++t0h&3;	// Digit
  byte c = seg[b];	// Segmente
  word d = (word)c<<1;	// nach links geschobene Segmente
  byte k = 1<<b;	// Katoden-Bit (1,2,4 oder 8)
  c &= k-1;		// rechts von Katodenbit behalten, sonst 0
  d &= ~((k<<1)-1);	// links von Katodenbit behalten, sonst 0
  d |= c;		// Bits (Anoden) zusammensetzen
  b = (d>>5)&0x0C;	// DDRB vorbereiten: Bit 8 und 7
  byte a = d&0x7F;	// DDRA, PORTA vorbereiten
  DDRA = 0;		// Alles aus: Katoden auf h oder z genügt
  PORTB= b;
  DDRB = b;		// H oder z, niemals h (= Pullup)
  PORTA= a|0x80;	// Pullup an PA7 behalten
  DDRA = a|k;		// Jeweilige Katode auf L
  wdt_reset();
}
// Bei zu hoher Interruptlast durch zu hohe Eingangsfrequenz an ICP
// bleibt der Anzeigemultiplex aus, und der Watchdog beißt zu.
// 200 kHz wurden im Versuch noch sicher verarbeitet,
// was einer Drehzahl von über 2.000.000 U/min entspräche!
// Der Wirbelstromsensor schafft nur garantiert 5 kHz,
// was immerhin (bei 9 Zähnen) 30.000 U/min entspräche.

static void numout(word n, char nk=0) {
// 10 Siebensegmentkodes 0..9
  static const PROGMEM byte seg7[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};
  for (char i=4; --i>=0;--nk) {
    byte r = n%10;
    seg[i] = n || nk>=0 ? pgm_read_byte(seg7+r) | (nk?0:0x80) : 0;
    n /= 10;
  }
}

// Diese beiden ISRs begrenzen die maximale Eingangsfrequenz.
// Diese liegt bei 40 CPU-Takten = 100 kHz.
// Diese ISR braucht (vom sleep bis reti) gar 50 Takte.
// Mit Registervariablen 41 Takte.
// In Assembler 21 Takte.
// Bis 1 MHz könnte man mithilfe Zählereingang lösen, alles schnellere
// zusätzlich mit externem Vorteiler (74HC93 o.ä.)
ISR(TIM1_CAPT_vect,ISR_NAKED) {
#if 0
  ++captn;
  capt1=ICR1;
  if (TIFR1&1) GPIOR0|=2;
#else
 asm(
"	in	r6,__SREG__\n"	// capt1L zum Retten von SREG missbrauchen
"	inc	r4	\n"
"	brne	1f	\n"
"	inc	r5	\n"	// captn inkrementiert
"1:	out	__SREG__,r6\n"
"	in	r6,%A0	\n"
"	in	r7,%B0	\n"	// capt1 eingelesen
"	sbic	%1,0	\n"	// TIM1_OVF anhängig?
"	 sbi	%2,1	\n"	// Gleichzeitigkeit erfassen
"	reti		\n"
::"I"(_SFR_IO_ADDR(ICR1)),"I"(_SFR_IO_ADDR(TIFR1)),"I"(_SFR_IO_ADDR(GPIOR0)));
#endif
}
// Folgende Fälle des zeitlichen Aufeinandertreffens von TIM1_CAPT und TIM1_OVF:
// (1) CAPT (weit) vor OVF: Sequenzielle Abarbeitung, R7:R6 != 0
// (2) CAPT kurz vor OFV: Bit 1 von GPIOR0 wird gesetzt aber R7:R6 != 0 (bspw. 0xFFFF)
// (3) Gleichzeitig: Bit 1 von GPIOR0 wird gesetzt und R7:R6 == 0 (als 0x10000 ansehen!)
// (4) CAPT kurz nach OFV: Bit 1 von GPIOR0 wird gesetzt und R7:R6 == 0 oder == 0x0001
//	(da die CPU zum Interruptstart aus Sleep 8 Takte braucht kann sich CAPT vordrängeln)
// (5) CAPT nach OFV: Interrupts werden in der richtigen Reihenfolge abgearbeitet,
//	Bit 1 von GPIOR0 wird (noch) nicht gesetzt, R7:R6 != 0
// (Interruptpriorität klärt nur den Interruptstart und ist unabhängig von
//  gegenseitiger Unterbrechung. Die beiden ISRs unterbrechen sich nicht gegenseitig,
//  aber die Anzeigemultiplex-ISR darf unterbrochen werden.)
// Damit erweist sich lückenlose Frequenzmessung mit AVR doch noch als schwieriges Unterfangen

word capt0,	// Vorheriger Capture-Wert
     savea,saveb,savec NOINIT;
// Wettlaufsituation: Capture kann zeitgleich passieren, weil in Hardware
// Deshalb wird ICR1 bei TIM1_CAPT_vect eingelesen und damit
// die beiden RAM-Variablen capt1 und captn synchron gehalten
// Assembler: 40 Takte (von sleep bis reti)
ISR(TIM1_OVF_vect,ISR_NAKED) {
#if 0
  savec = captn;
  savea = capt0;
  capt0 = saveb = capt1;
  GPIOR0= GPIOR0<<1|1;
  captn = 0;
#else
 asm(
"	sts	savec,r4	\n"
"	sts	savec+1,r5	\n"
"	lds	r4,capt0	\n"
"	lds	r5,capt0+1	\n"
"	sts	savea,r4	\n"
"	sts	savea+1,r5	\n"
"	sts	saveb,r6	\n"
"	sts	saveb+1,r7	\n"
"	sts	capt0,r6	\n"
"	sts	capt0+1,r7	\n"
"	in	r6,__SREG__	\n"
"	in	r4,%0		\n"
"	sec			\n"
"	rol	r4		\n"
"	out	%0,r4		\n"
"	clr	r4		\n"
"	clr	r5		\n"
"	out	__SREG__,r6	\n"
"	reti			\n"
::"I"(_SFR_IO_ADDR(GPIOR0)));
#endif
}

// Interruptfrequenz F_CPU/8/65536 = 7,5 Hz
// Einstieg mit gesperrten Interrupts
static void onT1O() {
  word a=savea,b=saveb,c=savec;
// Je nachdem ob im gerade vergangenen Intervall 1 Puls festgestellt wurde
// verlangsamt sich entsprechend die Messfrequenz bis max. 8 Interrupts
// Hier leiste ich mir mal eine "unsigned long long"-Arithmetik
  if (c) {	// Puls(e) gekommen?
    if (t1h>0) {// Gültige Zeitmessung?
      if (GPIOR0&8 && a<0x8000) --t1h;	// a muss als 0x1aaaa gesehen werden
      if (GPIOR0&4 && b<0x8000) ++t1h;	// b muss als 0x1bbbb gesehen werden
      dword T = ((dword)t1h<<16) + b - a;
		//Dauer zwischen erstem und letztem Capture in Zählertakten
      qword F = (qword)F_CPU
	*60	//Sekunden pro Minute (Anzeige in U/min, nicht U/s)
	*10	//Zehntel-Anzeige
	/8	//Zähler-Vorteiler
	/9;	//Zähne am Zählrad = 0x13DE4355
      dword f = F * c / T;
      char nk=1;
      while (f>9999) {
        f/=10;	// Dezimalstelle verwerfen
        if (--nk<0) nk+=3;
      }
      numout(f,nk);
    }
    t1h = 1;	// Ganzperiodenzähler neu starten
  }else if (t1h>0 && ++t1h==8) {	// 7 Interrupts = knapp 1 s kein Puls?
    t1h = 0;	// stillsetzen, beim nächsten Puls ungültige Zeitmessung
    numout(0);	// 0. ausgeben
// Obwohl 0.00 naheliegender erscheint ist das unrichtig,
// denn Frequenzen unter 1 Hz kann diese Anzeige nicht verarbeiten,
// ist nicht dafür ausgelegt.
// Beispielsweise 0,50 Hz würde fälschlich mit 0.00 angezeigt werden.
// (Man könnte auch 0.-- anzeigen aber das dürfte mehr verwirren.)
// Hingegen Frequenzen über 1 Hz können mit über 16 Bit aufgelöst werden.
  }else if (t1h<0 && !++t1h) {	// Lampentest-Zeit
    numout(0);	// ebenfalls 0. ausgeben
  }
}

int main() {
  for (byte i=0; i<4; i++) seg[i]=0xFF;	// Lampentest
  t1h = -2;		// Lampentest-Zeit sowie ungültige Zeitmessung
  ACSR |= 0x80;		// Analogvergleicher abschalten: –70µA
  MCUCR = 0x20;		// CPU-Schlafmodus
  TCCR0B= 0x02;		// ÷8 ⇒ 2 kHz ⇒ 500 Hz Bildwiederholfrequenz
  TIMSK0= 0x01;		// Überlauf-Interrupt: Anzeige-Multiplex
  TCCR1B= 0x82;		// Capture H-L-Flanke mit Störunterdrückung, ÷8 Taktfrequenz, CTC mit OCR1A
  TIMSK1= 0x21;		// Capture- und Überlauf-Interrupt
// Der Watchdog kümmert sich darum, dass das Display nicht einbrennt
  WDTCSR= 0x18;
  WDTCSR= 0x18;		// Watchdog mit 16 ms
  sei();
  for(;;) {
    sleep_cpu();
    if (GPIOR0&0x01) {
      GPIOR0&=~0x01;
      onT1O();	// Rechenaufwendige Routine in Hauptschleife
    }
  }
}
Detected encoding: UTF-80