/* Programm für Punktschweißzange „TZ 5.0“ (VEB Elektroschweißtechnik Dresden)
* für ATtiny13
* Hardware:
* Die Zange hat zwei Einstellpotenziometer:
* - Schweißstrom, 1 % bis 100 % (neu; per Phasenanschnitt)
* - Schweißdauer, 10 ms bis 1200 ms (½ bis 60 Netzperioden, bei 60 Hz geringere Zeiten!)
* einen Schalter in der Zündstromleitung sowie einen Taster zum Auslösen
* am Griff. Eine Piezokapsel wurde zusätzlich eingebaut (Fehlermeldung).
* Die Phasenmessung erfolgt per Spannungsteiler und A/D-Wandler!
* Betrieben wird die Schweißzange mit 400 V einphasiger Wechselspannung.
* Die Schaltung und Firmware ist auch für 230 V Wechselspannung
* (dann natürlich mit bedeutend reduzierter Schweißleistung)
* sowie 50 bis 60 Hz ausgelegt.
* „h#s“ Henrik Haftmann, TU Chemnitz, 12. Januar 2009
*
* tabsize = 8, encoding = utf-8 (ohne Windows-Header BOM)
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/eeprom.h>
#include <util/delay_basic.h>
#define DEBUG
/************
* Hardware *
************/
/*
Verwendung der Ports:
PB0 (5) OC0A Taster (über Widerstand, low-aktiv); Piezokapsel
PB1 (6) - Triac-Zündimpuls low-aktiv
PB2 (7) ADC1 Potenziometer (Spannungsteiler) Schweißdauer
PB3 (2) ADC3 Triac-Anodenspannung (250tel), mittensymmetrisch
PB4 (3) ADC2 Potenziometer (Spannungsteiler) Schweißstrom
PB5 (1) - Reset (frei für In-System-Programmierung)
*/
#define CPUCLK 1200000 // Hz: 1,2 MHz (Standard)
#define MAXCT0 (12*CPUCLK/1000/64) // Halbperiodendauer max. 12 ms,
// sonst Trafo im Leerlauf?
typedef unsigned char uchar, bool;
typedef unsigned short ushort;
typedef unsigned long ulong;
enum {false, true};
typedef struct{
signed char Remanenz; // Remanenz des Trafokerns für knallfreies Einschalten*
uchar SWinkel; // Mindeststartwinkel (Trafokenngröße)
uchar MaxZeit; // Maximale Schweißzeit in Netzperioden
uchar MinKuehl; // Minimale Abkühlzeit in Netzperioden (sonst piept's)
uchar LastError; // Letzter Fehler (Piepcode)
uchar unused[3];
ulong usage; // Anzahl Tastenbetätigungen
ulong welds; // Anzahl erfolgreiche Schweißungen
}persistent_t;
//* vorerst nur als Vorzeichen gespeichert
EEMEM persistent_t eeState;
persistent_t ramState;
register uchar PosPegel asm("r7"); // Darüber ist „positive Spannung“
register uchar NegPegel asm("r6"); // Darunter ist „negative Spannung“
register bool NegWelle asm("r5"); // 0 wenn positiv, ~0 wenn negativ
register uchar T2 asm("r4"); // halbe Periodendauer in Timerticks
register uchar ZWinkel asm("r3"); // tatsächlicher Zündwinkel, in Timerticks
/**************************
* ATtiny-interner EEPROM *
**************************/
// entnommen aus h#s USB2LPT
// Updates an ATmega-internal EEPROM location when different
static void ActualizeEepromByte(uchar *ee_addr, uchar b) {
if (eeprom_read_byte(ee_addr)==b) return;
EEDR =b;
cli();
EECR |= 1<<EEMPE;
EECR |= 1<<EEPE; // don't wait here!
sei();
}
// Updates EEPROM out of RAM area, must be called periodically, does not block
static void ActualizeEepromB(const uchar *pRam, uchar *pEe, uchar n) {
if (!n) return;
do{
if (!eeprom_is_ready()) return;
ActualizeEepromByte(pEe++,*pRam++);
}while(--n);
}
#define ActualizeEeprom(r,e,l) ActualizeEepromB((const uchar*)r,(uchar*)e,l)
/***********
* Piepser *
***********/
// Haltebedingung (nur) für Beep
// Die zusätzlichen Umladevorgänge sorgen für einen schrillen Ton
// (es ginge ja auch eleganter)
static bool Taste(void) {
DDRB &= ~0x01;
_delay_loop_1(100);
if (PINB&0x01) return false;
DDRB |= 0x01;
return true;
}
// Piep ausgeben (Anzahl Perioden (0=∞) oder bis Taster losgelassen)
static void Beep(uchar perilen, uchar periods, bool KeyReleased) {
DDRB |= 0x01; // als Ausgang setzen (keine Tastenabfrage)
OCR0A = perilen;
TCCR0A = 0x42; // Timer0: CTC-Modus, OC0A toggelt bei Überlauf
do{
do; while (!TIFR0); // warten bis Interrupt-Flag
TIFR0 = 0xFF; // Bit löschen
if (!KeyReleased && !Taste()) break;
do; while (!TIFR0); // noch einmal (ganze Periode)
TIFR0 = 0xFF;
}while (!periods || --periods);
TCCR0A = 0; // Timer0: normaler Modus
DDRB &= ~0x01;
_delay_loop_1(100);
}
/***********************************
* Nulldurchgang und Zündzeitpunkt *
***********************************/
static uchar GetADC(void) {
ADCSRA |= 0x40; // A/D-Wandler starten
do; while (ADCSRA&0x40); // warten bis fertig (ca. 166 µs)
return ADCH; // Wert liefern
}
// Sucht Minimal- und Maximalwert der Spannung und „schätzt“ daraus den
// Mittelwert (also den Nullpegel bei nicht gezündetem Triac)
static void SuchNullpegel(void) {
uchar z = 0, min = 255, max = 0, val, n, b;
do{ // 256 Runden genügen, sind mindestens 42 ms
val = GetADC()>>1; // 7 Bit reichen
if (min>val) min = val;
if (max<val) max = val;
}while(--z);
n = max+min; // sollte ca. 80h sein
b = (max-min)>>6; // 1/64 der Gesamtspannung
if (!b) b=1; // Nullbereich mindestens 2
PosPegel = n+b;
NegPegel = n-b;
}
// Wartet Nulldurchgang ab; liefert FALSE bei Zählerüberlauf
// Außerdem wird eine EEPROM-Aktualisierung angeschubst
static bool ZeroCross(void) {
static uchar tp;
ActualizeEeprom(&ramState,&eeState,sizeof(persistent_t));
do{
if (TCNT0 >= MAXCT0) return false;
}while ((NegWelle && (GetADC()<=PosPegel)) || (!NegWelle && (GetADC()>=NegPegel)));
if (NegWelle) {
T2 = ((ushort)tp+TCNT0)>>1;
}else{
tp = TCNT0;
}
TCNT0 = 0;
#ifdef DEBUG
DDRB |= 0x08; // in Oszibild sichtbar machen
_delay_loop_1(8);
DDRB &= ~0x08;
#endif
NegWelle = ~NegWelle;
return true;
}
// Triac zünden: 5 Zündimpulse (je 1,6 µs, alle 6 µs) ausgeben
static void FireNow(void) {
uchar z = 5;
PORTB &= ~0x02; // Low vorbereiten
#ifdef DEBUG
DDRB |= 0x08; // Marker setzen
PORTB |= 0x08;
#endif
do {
DDRB |= 0x02; // als Ausgang (Low)
asm volatile ("nop");
DDRB &= ~0x02; // als Eingang (nicht unnötig Kapazitäten umladen)
_delay_loop_1(4);
}while (--z);
#ifdef DEBUG
DDRB &= ~0x08;
PORTB &= ~0x08;
#endif
PORTB |= 0x02; // als Eingang mit Pull-Up
}
// warten per Timer und dann Triac zünden
// liefert FALSE wenn keine Zündung (i.d.R. Hauptschalter ausgeschaltet)
// oder bei unzureichender/falscher Triac-Spannung vor der Zündung
static uchar Fire(void) {
uchar val;
do; while (TCNT0<ZWinkel);
val = GetADC();
if (NegWelle && val>NegPegel) return 5;
if (!NegWelle && val<PosPegel) return 5;
FireNow();
val = GetADC();
if (val>PosPegel || val<NegPegel) return 4;
ramState.Remanenz = NegWelle;
return 0;
}
/********************
* Analoge Eingaben *
********************/
static uchar LeseStrom(void) {
uchar val;
ADMUX &= 0xFE; // Potenziometerspannung „Strom“ messen
val = ~GetADC(); // 0 = 10% (Zündwinkel 90%), 255 = 100% (Zündwinkel 0)
ADMUX |= 0x01; // zurück: Anodenspannung messen
return (ushort)T2*val/283; // 283 = 255/0.9
} // 10 % Leistung (nicht Strom) ist bei 75 % Anschnittwinkel!
// Bei 90 % Anschnittwinkel ist die Leistung 0,6 %!
static uchar LeseDauer(void) {
uchar val;
ADMUX &= 0xFD; // Potenziometerspannung „Dauer“ messen
val = GetADC();
ADMUX |= 0x02; // zurück: Anodenspannung messen
return 1+((ushort)(ramState.MaxZeit-1)*(ushort)val)/255;
}
/*************************************
* Initialisierung und Hauptschleife *
*************************************/
static void hardwareInit(void) {
ACSR |= 0x80; // Analogvergleicher ausschalten
PORTB = 0x03; // Port-Pullups EIN
ADMUX = 0x23; // Anodenspannung messen, 8 bit linksbündig
ADCSRA = 0x83; // A/D-Wandler aktivieren (noch nicht starten)
DIDR0 = 0x3C; // digitale Eingänge nicht nutzen
eeprom_read_block(&ramState,&eeState,sizeof(persistent_t));
if (ramState.SWinkel==0xFF) ramState.SWinkel=0; // unprogrammiert
if (ramState.MaxZeit==0xFF) ramState.MaxZeit=120; // 1,2 Sekunden
if (ramState.MinKuehl==0xFF) ramState.MinKuehl=50; // ½ Sekunde
TCCR0B = 0x03; // Vorteiler 64; Arbeitsfrequenz ab 37 Hz
}
int __attribute__((noreturn)) main(void) {
uchar t, NWinkel; // nächster Zündwinkel
hardwareInit();
mainloop: // Hauptschleife: hier als Sprungziel
// Warten bis Taste losgelassen (sonst geht's nicht weiter)
do{
ActualizeEeprom(&ramState,&eeState,sizeof(persistent_t));
}while (!(PINB&0x01));
// Nullpegel und Unsicherheitsbereich abschätzen
SuchNullpegel();
// warte positive Halbwelle ab und starte Timer
TCNT0 = 0;
NegWelle = 0;
ZeroCross(); // könnte T2=0 liefern
ZeroCross(); // könnte T2 zu klein liefern
// messe Periodendauer
ZeroCross();
ZeroCross(); // jetzt sollte T2 stimmen
// Warte auf Taster
do ZeroCross(); while (PINB&0x01);
ramState.usage++; // Anzahl Tastenbetätigungen
t = LeseDauer();
ZWinkel = ramState.SWinkel; // mit Startwinkel beginnen
if (ramState.Remanenz!=NegWelle) ZeroCross();
do{
NWinkel = LeseStrom();
if (ZWinkel<NWinkel) ZWinkel = NWinkel; // Größtwert
if (!ZeroCross()) {
Beep(10,80,false); // Unregelmäßigkeiten der Netzspannung oder Trafo-Leerlauf?
ramState.LastError = 3; // Phasenwinkel-Problem (meist: Leerlauf)
goto mainloop;
}
if (PINB&0x01) { // Taste vorzeitig losgelassen (im Schweißvorgang)
Beep(15,250,true);
ramState.LastError = 1;
goto mainloop;
}
switch (Fire()) {
case 5: Beep(40,80,false); // Zündung ohne Spannung (Logikfehler)
ramState.LastError = 5;
goto mainloop;
case 4: Beep(20,0,false); // Zündung erfolglos (Hauptschalter)
ramState.LastError = 4;
goto mainloop;
}
ZWinkel = NWinkel;
}while (--t);
t = ramState.MinKuehl; // ½ Sekunde
do{
ZeroCross();
if (PINB&0x01) { // Taste vorzeitig losgelassen (im Abkühlvorgang)
Beep(15,100,true);
ramState.LastError = 2;
goto mainloop;
}
}while (--t);
ramState.welds++; // Anzahl „ordentliche“ Schweißungen
goto mainloop; // Ruhe wenn Schweißvorgang OK
}
Detected encoding: UTF-8 | 0
|