/* Fernbedien-Adapter für Muldenkipper und Radlader.
+ Verarbeitet Fernbedienbefehle von:
- Original-Fernbedienungsempfänger via IBUS am UART
- Raspberry via I²C-Slave
- Pampelmuse mit RFM69 = 433-MHz-Transceiver via SPI
+ Leitet Fernbedienbefehle weiter an
- PWM-Ausgänge am 4017 und in Folge den Servos
- Raspberry via I²C
- Pampelmuse via SPI
+ Vermittelt Sensorsignale (Potenziometerstände, Tacho, Batteriespannug) an
- Original-Fernbedienungsempfänger via IBUS am UART
- Raspberry via I²C
- Pampelmuse via SPI
- Ultraschall-Kollisionsdetektor via Input Capture
+ PI-Regler für Position/Geschwindigkeit sowie 2-3 Hydraulikventile
+ Not-Aus (lokal und von Pampelmuse)
+ Rückwärtsfahrschalter?
+ Odometrische Positionsberechnung
Hardware: ATmega88 oder größer
╔════════════════════╗
ISP ─╢PC6/!RESET SCL/PC5╟─ I²C-Slave Raspi
IBUS-Servo ─╢PD0/RxD SDA/PC4╟─ I²C-Slave Raspi
IBUS-Sensor ─╢PD1/TxD ADC3/PC3╟─ Bordspannung
Not-Aus nach Lo ─╢PD2/INT0 ADC2/PC2╟─ Poti Löffel / Verwindung
Drehimpuls ─╢PD3/INT1 ADC1/PC1╟─ Poti Hubarm / Ladefläche
Debug ─╢PD4 ADC0/PC0╟─ Poti Lenkung
<3P3> ─╢Ucc GND╟─
─╢GND AREF╟─ Kondensator nach GND
Baudratenquarz ─╢PB6/XTO AUcc╟─ <3P3> über Drossel
Baudratenquarz ─╢PB7/XTI SCK/PB5╟─ RFM69 und ISP
SR04:Puls ─╢PD5/OC0B MISO/PB4╟─ RFM69 und ISP
SR04:Senden ─╢PD6/OC0A/A0 MOSI/PB3╟─ RFM69 und ISP
4017:15 Reset ─╢PD7/A1 SS/OC1B/PB2╟─ RFM69 Slave-Select
SR04:Empfang ─╢PB0/ICP OC1A/PB1╟─ 4017:14 Zählen
╚════════════════════╝
Der 4017 dient als Portexpander für bis zu 9 PWM-Ausgänge.
Daran sind angeschlossen:
Q0: muss frei bleiben
Q1: Lenkservo [links ↔]
Q2: Fahrmotor [links ↕] (nicht mehr über Magic Controller, um geräuschlos fahren zu können)
Q3: Hubarm / Ladefläche [rechts ↕]
Q4: Löffel / frei [rechts ↔]
Q5: Hydraulikpumpe [Knopf Rückseite links]
Q6: Magic Controller gelber Draht (Anlassen; Hupe) [Drehrad links]
Q7: Magic Controller grüner Draht (Licht) [Drehrad rechts]
Q8: Rundumleuchte [Knopf Rückseite rechts]
Q9: Magic Controller blauer Draht (Motor- und Fahrgeräusch)
Verwendung der Timer:
Timer0: 8 Bit, Vorteiler 1, CTC
OC0A: 153, Zeitbasis für Ultraschall-Impulsgenerator: F_CPU/48kHz, Interrupt
OC0B: 77, PWM-Ausgang (für 8 Impulse 1:1)
Timer1: 16 bit, Vorteiler 8, Messumfang 71 ms, für Ultraschall 12 m
OC1A: Zum 4017, Interrupt
OC1B: Timeout für Ultraschallmessung
ICP1: Für Ultraschallmessung
Timer2: 8 bit, Vorteiler 1024, Takt 7,2 kHz (138µs), Messumfang 35,55 ms
OC2A: Inter-Character-Timeout für RxD
OC2B: Timeout für I²C
Watchdog: frei
TEMPORÄR DEBUG
PD5 = -
PD6 = Debugausgang I²C-Verklemmer
*/
#include "fba.h"
#include <avr/sleep.h> // sleep_cpu
#include <math.h> // lround
#include <string.h> // memset
#ifndef EEAR
# define EEAR EEARL // für ATmega48
#endif
Uhr uhr NOINIT;
Eedata eedata NOINIT;
/***************
* Zeitmessung *
***************/
// Zur Kommunikation mit ISRs (max. 16 Bit)
struct Load{
byte max,cnt;
unsigned time;
void reset() {time=cnt=max=0;}
}sleepload;
volatile Load isrload; // cli()/sei() zum Auslesen und Löschen erforderlich
// Zur Kommunikation mit I²C (max. 32 Bit)
void Loadavg::prepare() {
t1l+=TCNT1;
}
void Loadavg::reset() {
memset(this,0,sizeof*this-4);
cli();
t1l = -TCNT1; // unsauber aber genau genug
t1h = 0;
sei();
}
// Zur Load-LoadLong-Kommunikation (Bitbreiten aufblasend)
void Loadavg::LoadLong::on20ms(Load&src) {
byte m1=max, m2=src.max; if (m1<m2) max=m2;
cnt+=src.cnt;
time+=src.time;
src.reset();
}
inline void Loadavg::LoadLong::on20ms(volatile Load&src) {
cli();
on20ms(const_cast<Load&>(src));
sei();
}
void Loadavg::on20ms() {
isr.on20ms(isrload); // mit cli()
sleep.on20ms(sleepload); // ohne cli()
}
Loadavg loadavg;
/******************
* EEPROM-Zugriff *
******************/
void Eedata::read() {
byte l=sizeof*this,*p=(byte*)this;
byte a=0;
do{
EEAR=a;
EECR|=1; // Lesen auslösen
*p++=EEDR;
a++;
}while(--l);
}
void Eedata::update() {
if (EECR&2) return; // beschäftigt
byte l=sizeof*this;
byte a = EEAR;
do{
if (a>=sizeof*this) a=0;
EEAR=a;
EECR|=1; // Lesen auslösen
byte o=EEDR, n=((byte*)this)[a];
if (o!=n) {
EECR&=~0x10; EECR&=~0x20; // löschen+schreiben (3,4 ms)
if (n==0xFF) EECR|=0x10; // Nur löschen (1,8 ms)
if (!(byte(~o)&n)) EECR|=0x20;// Nur schreiben (1,8 ms)
EEDR=n;
cli();
EECR|=4;
EECR|=2;
sei();
EECR|=8; // Interrupt am Ende auslösen lassen
return;
}
a++;
}while(--l);
}
int main() {
byte mcusr = MCUSR; // Reset-Ursache einfangen
MCUSR = 0;
// Ports initialisieren
ACSR = 0x80;
SMCR = 0b00000001; // Sleep-Freigabe: Stoppt CPU
PORTB = 0b010100; // RFM69 noch nicht ansprechen; kein Pullup an ICP (vom LM324)
DDRB = 0b101111;
PORTC = 0b1110000; // Pullups an I²C (zusätzlich zu R4 und R5)
DDRC = 0b0000000;
PORTD = 0b10010111; // Freie Pins mit Pullups, ansonsten gibt's externe Pulldowns
DDRD = 0b11110000; // TxD inaktiv
DIDR0 = 0b00001111; // Analogeingänge ohne Digitalfunktion
eedata.read();
if (mcusr&0x01) { // Spannung unter 1,3 V?
// Vieles ist in .noinit, muss nur bei PowerOn-Reset gelöscht werden
l4.kmz = 0; // Daher bleibt bei BrownOut der Speicherinhalt erhalten
uhr.reset(); // … und die Uhr stehen;
l4.lamps = eedata.lamps;// Die anderen drei Lampenzustände werden vom Magic Controller gemerkt
memset(&odo,0,sizeof odo);
} // bei PowerOn ist der Inhalt von .noinit zufällig.
// Bei Reset-Ursache: Reset-Pin ändert sich der Zustand der Rundumleuchte nicht.
// Ansonsten wird l4.flags initialisiert und l4.lamps vom EEPROM geladen
if (mcusr&0x05) { // PowerOn oder BrownOut = Spannung unter 2,7 V?
l4.flags = 0x20; // Rundumleuchte-Zustand: ein, rotierend (beim Einschalten)
// Zustand der Zündung: ausgeschaltet (Bit 3 = 0)
// Zustand des Blinkgeräusches: Wird ausgeschaltet angenommen
}
// Reset-Zähler (4 Bytes) nutzen
for (byte i=0; i<4; i++,mcusr>>=1) eedata.debug[i]+=mcusr&1;
// Timer1 vorbereiten (TODO: Capture-Interrupt auswerten)
TCCR1A= 0b01000000; // OC1A aktivieren: Toggeln
TIMSK1= 0b00000011; // Interruptfreigabe OC1A und bei Überlauf
TCCR1B= 0b00000010; // Kein CTC-Modus, Vorteiler 8; fallende Flanke an ICP triggert, ohne Filter
// Serielle Schnittstelle für IBUS-Servo aktivieren
UBRR0 = F_CPU/115200/16-1; // kommt 3 heraus
UCSR0B= 0b10010000; // Empfänger aktivieren mit Interrupt
UCSR0C= 0b00001110; // 2 Stoppbit, 8 Datenbits
// Impulszähler und Abstandsmessung aktivieren (an PD3 = INT1)
spdCnt= 0;
EICRA = 0b00001100; // INT1 steigende Flanke
EIMSK|= 1<<1; // aktivieren
// Ultraschall-Sendeimpulsgenerator initialisieren (noch nicht starten)
OCR0A = F_CPU/40000; // abgerundet: 184
OCR0B = F_CPU/40000/2; // abgerundet: 92
TCCR0A= 0b00110011; // invertierte PWM am Ausgang OC0B=PD5 (erstmal Low)
TIMSK0= 0b00000010; // OCIE0A: Pulse zählen per Software
// Timer2 laufen lassen
OCR2A = F_CPU/1024/1000;// (7) 1 ms Inaktivität bei TxD feststellen lassen
TCCR2B= 0b00000111; // Laufen lassen mit Vorteiler 1024
TIMSK2= 1<<1; // Interrupt bei Inaktivität = Lücke
#if 0
WDTCSR= 1<<4; // WDCE setzen
WDTCSR= 0b11000100; // Watchdog: Interrupt nach 250 ms
#endif
// A/D-Wandler starten
l4.adcstart();
L2::t1init(); // Sofort zur CompareA-Interruptbehandlung (Assembler)
I2C::init();
char ticks = 0; // vzb. Ticks pro 20 ms (nicht gemittelt)
byte stage = 0;
for(;;) {
if (!ADCSRA) { // A/D-Wandler fertig? (ISR setzt ADCSRA auf Null)
ADCSRA = 0b00011110; // Immer noch aus, Hauptsache ≠0
// assert(!stage);
ticks = l4.save();
// Da der A/D-Wandler während der Reset-Phase des 4017 fertig wird,
// werden hier die neuen PWM-Werte gesetzt.
// Möglicherweise keine gute Idee; besser: Reset-Phase gesondert in ISR erfassen!
L2::k7hold_handler(); // Wirklich 50 Hz?
L2::autopuls_handler();
L2::countdown();
}
if (GPIOR0&1<<1) { // Simulierter CTC-Interrupt Timer1? (50 Hz)
GPIOR0&=~(1<<1);
++uhr; // Mit exakt 50 Hz weiterzählen
l4.adcstart(); // Start der Akkumulierung
byte f=l4.flags&0xF8;
if (l4.lin[2]<-200) f|=1<<0; // Bit 0: Rückfahrtaster
if (!notaus::released()) f|=1<<1; // Bit 1: Notaus-Stellung (Pin ist Lo)
if (notaus::forced()) f|=1<<2; // Bit 2: Notaus-Forcierung (Pin ist Lo-Ausgang)
l4.flags=f;
stage = 3;
}
l3.checkIbusRx();
I2C::poll();
// Die Rechenlast der weiteren Verarbeitungsschritte ist aufgeteilt.
// So als ob jedesmal dazwischen die Flags abgecheckt werden.
switch (stage) {
case 3: l4.on20ms(ticks); ++stage; break; // Linearisierung
case 4: odo.lenkTo(l4.lin[0]); ++stage; break;
case 5: Regler::on20ms(); ++stage; break; // Regler
case 6: odo.on20ms(ticks); ++stage; break; // Odometrie
case 7: L7::on20ms(ticks); ++stage; break; // Job-Abarbeitung
case 8: eedata.update(); stage=9; break; // EEPROM aktualisieren (niedrig priorisiert)
case 9: loadavg.on20ms(); stage=0; break;
}
if (!stage) {
cli();
sleepload.time-=TCNT1;
GPIOR0|=1<<3; // Aufweckende ISR stoppt die Zeit
PORTD&=~(1<<4); // Rechenlast an PD4 anzeigen
sei();
sleep_cpu();
byte c=sleepload.cnt++;
if (c) sleepload.cnt=c; // nicht (zu Null) überlaufen lassen
}
}
}
Detected encoding: UTF-8 | 0
|