Source file: /~heha/basteln/m/servo/servo.zip/servo15/main.cpp

/*
Quasianaloge Servosteuerung für 15 Türen
mit analog einstellbaren Anschlägen
und Nachführung der Türposition im EEPROM

Hardware: ATtinyX8, ATmegaX8, ATmega8
Pin	Port	Funk.
1	(PC6)	!RESET	ISP
2	PD0	RxD	Tastenmatrix 0
3	PD1	TxD	Tastenmatrix 1
4	PD2		Tastenmatrix 2
5	PD3		Tastenmatrix 3
6	PD4		Tastenmatrix 4
7	Ucc
8	GND
9	PB6		PWM(B) Servo 5
10	PB7		PWM(B) Servo 6
11	PD5		Tastenmatrix 5
12	PD6		PWM(A) Servo 7
13	PD7		PWM(A) Servo 8
14	PB0		PWM(B) Servo 9
15	PB1		PWM(B) Servo 11
16	PB2		PWM(B) Servo 10
17	PB3	MOSI	PWM(B) Servo 14, ISP
18	PB4	MISO	PWM(B) Servo 13, ISP
19	PB5	SCK	PWM(B) Servo 12, ISP
20	AUcc
21	AREF		unbeschaltet
22	GND
23	PC0	ADC0	Einstellregler
24	PC1		PWM(A) Servo 4
25	PC2		PWM(A) Servo 3
26	PC3		PWM(A) Servo 2
27	PC4		PWM(A) Servo 1
28	PC5		PWM(A) Servo 0

15 Standard-Servos mit 0,5 .. 1,5 .. 2,5 ms Pulsdauer bei 50 Hz
PWM(A) bedeutet: Die Pulslänge wird durch Timer1 CompareA definiert.
Die o.g. Zuordnung vereinfacht die softwaremäßige Endebehandlung.

Funktion:
Der Tastendruck (nach Low) schließt oder öffnet die Tür.
Betätigung während der Bewegung stoppt die Tür
und dreht die nächste Bewegungsrichtung um.
Mit einigermaßen Beschleunigungs- und Bremsfunktion.
Vorbild: Eintasten-Rollladensteuerung oder Garagentorsteuerung.

Einsatzerfahrung:
Die Inbetriebnahme mit dem Oszi ist erforderlich, da die Servos
nicht ratiometrisch zur Pulsfrequenz arbeiten, sie messen nur die Pulslänge.
Beim Einschalten springen sie Servos, obwohl die Pulsausgabe des Controllers
sofort korrekt erscheint.
Wie zu erwarten arbeiten die Servos ziemlich ruckelig und geräuschvoll.
Das Absenken der Speisespannung (um das Motordrehmoment zu senken)
bringt bloß noch die Servos noch mehr durcheinander.
Stromaufnahme der Servos bei lastfreier Verstellung: 200 mA.
Die Vorgabewerte für a, e, vmax und amax ergeben eine Durchlaufzeit von 5 s.

Um nur /ein/ Potenziometer zur Endwertkalibrierung verwenden zu können,
wird dieses wie folgt abgefragt:
* Die jeweils zuletzt gedrückte Taste weist den Servo zu
* Ein „gültiger“ Analogwert wird erst nach Durchkreuzen der Mittelstellung angenommen
  (Ein Inkrementalgeber wäre zweifellos besser, lohnt sich aber erst bei noch mehr PWM-Ausgängen)
* Während der Bewegung: Einstellen der Geschwindigkeit
* Stehend: Stetiges Einstellen der Endlage (je nach „innerer Laufrichtung“)
* Nur nach dem Flashen des EEPROM (noch kein Tastendruck erfolgt)
  bewirkt das Poti die Einstellung der PWM-Frequenz
Die Beschleunigung ist nicht einstellbar.
(Das könnte man realisieren, wenn der Servo zwischen den Endlagen steht.
Das Feedback ist dabei schwierig zu gestalten, hm.)
Zeitversetztes Betätigen einer Doppeltür mit /einer/ Taste erfordert Software-Änderung.

Stellpräzision: 16000 Schritte; PWM-Stufung: 0,2 µs; PWM-Jitter: max. 2 µs (Software-PWM)
Interne Positionsdarstellung: PWM in Mikrosekunden mal 16 (füllt 16-Bit-Register gut aus)

Im Versuch beeinflussten sich die Servos merklich gegenseitig.
Ursache sind Einbrüche bei der Spannungsversorgung beim verwendeten Steckernetzteil
und die sich dabei ändernde Taktfrequenz des internen Oszillators.
Eine sehr stramme oder entkoppelte Stromversorgung ist also wichtig beim Einsatz.
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <avr/eeprom.h>
#include <avr/fuse.h>
#include <avr/signature.h>	// gepatcht: "const" weg!

#ifdef __AVR_ATmega8__
FUSES={
 0b11100100,	// BOD, SUT, CKSEL (8 MHz)
 0b11011001,	// RSTDISBL, WDTON, SPIEN, CKOPT, EESAVE, BOOTSZ
};
#elif defined(__AVR_ATmega48__)
FUSES={
 0b11100010,	// CLKDIV8, CKOUT, SUT, CKSEL
 0b11011111,	// RSTDISBL, DWEN, SPIEN, WDTON, EESAVE, BODLEVEL
 0b11111111,	// SELFPRGEN
};
#else	// ATmega88, 168, 328
FUSES={
 0b11100010,	// CLKDIV8 (nein), CKOUT, SUT, CKSEL
 0b11011111,	// RSTDISBL, DWEN, SPIEN, WDTON, EESAVE, BODLEVEL
 0b11111001,	// BOOTSZ, BOOTSRT
};
#endif

typedef unsigned char byte;
typedef unsigned word;
#define NOINIT __attribute__((section(".noinit")))
#define N 15	// Anzahl Servos

struct servo{	// 10 Bytes
 word a,e;	// Anfangs- und Endlage<<4, 500 ≤ a < e ≤ 2499
 word x;	// Position<<4, 499 = 0,5 ms, 2499 = 2,5 ms, Periode 20 ms
 byte vmax;	// Geschwindigkeitsvorgabe (einstellbar während Fahrt)
 byte amax;	// Beschleunigungsvorgabe (nicht mit Poti einstellbar)
 void keypress();	// Aufruf bei festgestelltem Tastendruck
 void change(int);	// Bei festgestellter Wertänderung, vom A/D-Wandler oder Inkrementalgeber
 void tick();		// nach 20 ms (50 Hz) neue Position ermitteln
 byte f;	// Bit 0: Richtung der Verfahrbewegung, Bit 1: In Bewegung
 byte v;	// aktuelle Geschwindigkeit (vorzeichenlos)
 word ocr() const;	// Ohne die Subschritte
private:
 void calc_v(unsigned);
};

struct eedata{	// Vom EEPROM „gebackupte“ Daten
 servo s[N];
 byte i;	// „aktiver“ Kanal (für die Wirkung des Potenziometers an ADC0)
 byte cal;	// Oszillator-Kalibrierbyte
};

#define A ( 500U<<4)	// Anfang Stellweg (Dezimalzahlen in µs Pulslänge)
#define M (1500U<<4)	// Mitte Stellweg
#define E ((2500U<<4)-2)// Ende Stellweg (4 Bits „unten“ sind für Subschritte)

#define DEFSERVO {A+2048,E-2048,M,128,2}

EEMEM eedata e={	// Vorgabewerte nach dem Programmieren des Chips
 {DEFSERVO,
  DEFSERVO,
  DEFSERVO,
  DEFSERVO,
  DEFSERVO,
  DEFSERVO,
  DEFSERVO,
  DEFSERVO,
  DEFSERVO,
  DEFSERVO,
  DEFSERVO,
  DEFSERVO,
  DEFSERVO,
  DEFSERVO,
  DEFSERVO},
 0xFF,
 0xFF		// Kode für unprogrammiertes OSCCAL
};

eedata d NOINIT;

register byte zero asm("r2");
register byte defportd asm("r3");	// Konstante für schnelle ISR
register byte killint asm("r4");
register int adcprev asm("r6");		// vorhergehender A/D-Wert (0..1023)

#ifdef __AVR_ATmega8__
ISR(TIMER1_COMPA_vect,ISR_NAKED) {	// beendet Software-Impuls an PortD + PortC
 asm volatile(
"	out	0x12,r3	\n"	// PORTD=defportd;
"	out	0x15,r2	\n"	// PORTC=zero;
"	reti		\n");
}
ISR(TIMER1_COMPB_vect,ISR_NAKED) {	// beendet Software-Impuls an PortB
 asm volatile(
"	out	0x18,r2	\n"	// PORTB=zero;
"	reti		\n");
}

// nominell mit 50 Hz × 8 = 400 Hz
ISR(TIMER1_CAPT_vect,ISR_NAKED) {
 asm volatile(
"	sbi	0x08,0	\n"	// ACSR|=0x01, Bit für „echte“ Interruptmeldung missbrauchen
"	out	0x12,r3	\n"	// PORTD=defportd
"	out	0x15,r2	\n"	// PORTC=zero
"	out	0x18,r2	\n"	// PORTB=zero
"	out	0x38,r4	\n"	// TIFR=killint, Compare-Interrupts sind erledigt
"	reti		\n");
}
#else	// ATmegaX8
ISR(TIMER1_COMPA_vect,ISR_NAKED) {	// beendet Software-Impuls an PortD + PortC
 asm volatile(
"	out	0x0B,r3	\n"	// PORTD=defportd;
"	out	0x08,r2	\n"	// PORTC=zero;
"	reti		\n");
}
ISR(TIMER1_COMPB_vect,ISR_NAKED) {	// beendet Software-Impuls an PortB
 asm volatile(
"	out	0x05,r2	\n"	// PORTB=zero;
"	reti		\n");
}

// nominell mit 50 Hz × 8 = 400 Hz
ISR(TIMER1_CAPT_vect,ISR_NAKED) {
 asm volatile(
"	sbi	0x1E,0	\n"	// GPIOR0|=0x01, Bit für „echte“ Interruptmeldung
"	out	0x0B,r3	\n"	// PORTD=defportd
"	out	0x08,r2	\n"	// PORTC=zero
"	out	0x05,r2	\n"	// PORTB=zero
"	out	0x16,r4	\n"	// TIFR1=killint, Compare-Interrupts sind erledigt
"	reti		\n");
}
#endif

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

// Lesen der Charliplex-Tastatur an D5..D0, Bitzuordnung:
//  0 = D1-D0,  1 = D2-D0,  2 = D2-D1,  3 = D3-D0,
//  4 = D3-D1,  5 = D3-D2,  6 = D4-D0,  7 = D4-D1,
//  8 = D4-D2,  9 = D4-D3, 10 = D5-D0, 11 = D5-D1,
// 12 = D5-D2, 13 = D5-D3, 14 = D5-D4, 15 = 0.
// Mehrere gleichzeitig gedrückte Tasten führen zu Phantomtasten: Pech!
// Ist keine Taste gedrückt, liefert die Funktion 0
static word readkeys() {
#if 0
 word ret=0;
// Die Schleife liefert 5+4+3+2+1 = 15 Bits aneinandergehängt
 for(byte mask=0x20;!(mask&1);mask>>=1){
  DDRD^=mask;		// eine Leitung zu Low aktivieren
  PORTD^=mask;		// (alle übrigen Leitungen bleiben Eingang mit Pullup)
  ret*=mask;		// warten und Bits freimachen
  ret|=~PIND&mask-1;	// Gedrückte Tasten liefern hier Low-Level
  PORTD^=mask;
  DDRD^=mask;
 }
#else
 word ret;
 asm volatile(
"	clr	%A0	\n"	// Nur Bit 0 wird zu Bit 15, alle anderen Bits schieben sich aus
"	ldi	r20,0x1F\n"
"1:	inc	r20	\n"
"	in	r0,%2	\n"
"	in	r1,%3	\n"
"	movw	r18,r0	\n"
"	eor	r0,r20	\n"
"	eor	r1,r20	\n"
"	out	%3,r1	\n"
"	out	%2,r0	\n"
"	mul	%B0,r20	\n"
"	mov	%B0,r1	\n"
"	mul	%A0,r20	\n"
"	add	r1,%B0	\n"
"	movw	%0,r0	\n"
"	dec	r20	\n"
"	in	r0,%1	\n"
"	out	%3,r19	\n"
"	out	%2,r18	\n"
"	com	r0	\n"
"	and	r0,r20	\n"
"	or	%A0,r0	\n"
"	lsr	r20	\n"
"	brne	1b	\n"
"	clr	r1	\n"
 :"=&r"(ret)
 :"I"(_SFR_IO_ADDR(PIND)),"I"(_SFR_IO_ADDR(DDRD)),"I"(_SFR_IO_ADDR(PORTD))
 :"r18","r19","r20"
 );
#endif
 return ret;
}

#ifdef __AVR_ATmega8__
# define GPIOR0 ACSR		// Missbrauch geht
#endif

static void wait400() {
 do sleep_cpu(); while (!(GPIOR0&1));
 GPIOR0&=~1;	// Compare-Interrupts müssen als Aufweckgrund ignoriert werden
}

int main() {
 MCUCR =0x80;	// Sleep aktivieren
 DDRB  =0xFF;	// PWM als Ausgänge
 DDRC  =0x3E;
 DDRD  =0xC0;
 PORTD =defportd=0x3F;
 killint=0x06;
 zero ^=zero;	// zero=0 würde wegoptimiert werden!
 TCCR1B=0x19;	// loszählen mit Vorteiler 1, CTC via ICR1
 ICR1  =F_CPU/400-1;	// 400 Hz: Zählt bis 19999 = 0x4E1F (15 Bit)
#ifdef __AVR_ATmega8__
 TIMSK =0x38;	// Interrupt bei Überlauf (via ICR1) und beide Compare
#else
 TIMSK1=0x26;
 DIDR0 =0x01;	// Analogeingang nicht abfragen
#endif
 ACSR |=0x80;	// Analogvergleicher aus
 ADMUX =0x40;	// 10 Bit, ADC0 = PC0, ratiometrisch zu Ucc
// adcactive=false;
 eeprom_read_block(&d,&e,sizeof d);
 if ((byte)~d.cal) OSCCAL=d.cal; else d.cal=OSCCAL;
 word keys=readkeys();
 sei();
// Funktionsprinzip: Pro Zeitschlitz von 2,5 ms (400 Hz)
// müssen zwei Servos mit unterschiedlicher Pulsbreite bedient werden.
// Die PWM-Werte liegen sicher so, dass diese auch erreicht werden.
 for(;;) {
  wait400();
  PORTB|=0x01;
  PORTC|=0x20;
  OCR1B=d.s[9].ocr();
  OCR1A=d.s[0].ocr();
  d.s[0].tick();
  d.s[8].tick();

  wait400();
  PORTB|=0x02;
  PORTC|=0x10;
  OCR1B=d.s[11].ocr();
  OCR1A=d.s[1].ocr();
  d.s[1].tick();
  d.s[9].tick();

  wait400();
  PORTB|=0x04;
  PORTC|=0x08;
  OCR1B=d.s[10].ocr();
  OCR1A=d.s[2].ocr();
  d.s[2].tick();
  d.s[10].tick();

  wait400();
  PORTB|=0x08;
  PORTC|=0x04;
  OCR1B=d.s[14].ocr();
  OCR1A=d.s[3].ocr();
  d.s[3].tick();
  d.s[11].tick();

  wait400();
  PORTB|=0x10;
  PORTC|=0x02;
  OCR1B=d.s[14].ocr();
  OCR1A=d.s[4].ocr();
  d.s[4].tick();
  d.s[12].tick();

  wait400();
  PORTB|=0x20;
  PORTD|=0x40;
  OCR1B=d.s[13].ocr();
  OCR1A=d.s[7].ocr();
  d.s[5].tick();
  d.s[13].tick();

  wait400();
  PORTB|=0x40;
  PORTD|=0x80;
  OCR1B=d.s[12].ocr();
  OCR1A=d.s[8].ocr();
  d.s[6].tick();
  d.s[14].tick();
  ADCSRA=0xC1;		// A/D-Wandler starten

  wait400();
  PORTB|=0x80;
  OCR1B=d.s[10].ocr();
  OCR1A=0xFFFF;		// Nicht auslösen lassen
  d.s[7].tick();
  for (const servo*p=d.s;p<d.s+N;p++) {
   if (p->f&2) goto noupdate;	// Nur speichern wenn alle Servos stehen
  }
  eeprom_update(&d,&e,sizeof d);
noupdate:
  int adc=ADC;		// Analogwert abholen
  ADCSRA=0;		// A/D-Wandler stoppen
  if (GPIOR0&2) {		// Poti-Mittelstellung erfasst
   int delta=adc-adcprev;
   adcprev=adc;
   if (d.i<N) d.s[d.i].change(delta);	// Endlagen oder Geschwindigkeit verstellen
   else{
    delta>>=2;
    delta+=d.cal;
    if (delta<0) delta=0;	// Unterlauf
    else if (delta>255) delta=255;	// Überlauf
    OSCCAL=d.cal=delta;	// OSCCAL und damit PWM-Frequenz einstellen (mit Oszi auf 50 Hz einstellen!)
   }
  }else if (512-16<=adc && adc<512+16) {
   GPIOR0|=2;
   adcprev=adc;
  }
  word b=readkeys();	// Tasten erneut abfragen
  word c=b&~keys;	// Übergänge (von losgelassen auf gedrückt) erfassen
  keys=b;
  if (c) {
   for (byte bit=0; bit<N; bit++) {
    if (c&1<<bit) d.s[d.i=bit].keypress();
   }
  }
 }
}

static word Bremsweg(byte v, byte a) {
 return (word(v*v)+v)/a>>1;	// der Term läuft nicht über
}
 
/* Objekt-Funktionen */

void servo::calc_v(word d) {	// d = vzl. Entfernung zum Ziel
// Bremsweg nach Gaußscher Summenformel s = (v²+v)/2a
 if (d<Bremsweg(v,amax)) {	// abbremsen?
  if (v>amax) v-=amax;		// minimal 1 belassen
 }
 else if (v<vmax) {		// beschleunigen
  if (vmax-v<amax) v=vmax;
  else v+=amax;
 }
}

// Aufruf mit 50 Hz (pro Servo)
void servo::tick() {
 if (f&2) {	// in Bewegung?
  word p=x;
  if (f&1) {	// rückwärts
   calc_v(p-a);	// je nach Entfernung zum Ziel
   p-=v;
   if (p<=a) {
    p=a;
    f=0;			// Stop, nächste Richtung: vorwärts
    v=0;
    GPIOR0&=~2;
   }
  }else{	// vorwärts
   calc_v(e-p);
   p+=v;
   if (p>=e) {
    p=e;
    f=1;			// Stop, nächste Richtung: rückwärts
    v=0;
    GPIOR0&=~2;
   }
  }
  x=p;	// ablegen
 }
}
  
void servo::keypress() {
 if (f&2) {	// in Bewegung?
  v=0;		// Vollbremsung (abrupt)
  f=f&1^1;	// Stop, nächste Richtung umkehren
 }else{
  f|=2;		// Bewegung starten
 }
 GPIOR0&=~2;
}

void servo::change(int d) {
 if (f&2) {	// in Bewegung?
  d>>=1;
  d+=vmax;
  if (d<=8) d=8;
  else if (d>=256) d=255;	// nicht 0 oder negativ werden lassen
  vmax=d;	// v passt sich automatisch an, in calc_v()
 }else{		// Stillstand
  word w=d<<4;
  if (f&1) {	// x sollte auf e stehen
   w+=e;
   if (w<=a) w=a+1;
   if (w>E) w=E;
   x=e=w;	// Position nachführen zur Kontrolle
  }else{
   w+=a;
   if (w<A) w=A;
   if (w>=e) w=e-1;
   x=a=w;
  }
 }
}

word servo::ocr() const{
 word ret=x>>1;
// if (ret< 499U) ret= 499U;	// DEBUG: Notbremse!
// if (ret>2499U) ret=2499U;
 return ret;
}
Detected encoding: UTF-80