/*
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;
}
| Vorgefundene Kodierung: UTF-8 | 0
|