/* Frequenzgenerator (Rechteck) für Schrittmotor-Endstufen
für ATtiny2313A @ 8 MHz, für Johannes Rudolph.
„h#s“ Henrik Haftmann, TU Chemnitz, 23. Oktober 2019
tabsize = 8, encoding = utf-8 (ohne Windows-Header BOM)
Hardware:
1 PA2 RESET ISP
2 PD0 (RxD) frei
3 PD1 (TxD) Ausgang !ENA
4 PA1 XTAL1 (Quarz) - frei
5 PA0 XTAL2 (Quarz) - frei
6 PD2 (INT0) K1 = 3-stellige 7segment-Anzeige gemeinsame Katode links über 100 Ω
7 PD3 (INT1) Segment e
8 PD4 (T0) Segment b; Inkrementalgeber (10 kΩ gegen Masse)
9 PD5 (OC0B) K3 = Katode rechts über 100 Ω
10 GND 0 V
11 PD6 (ICP1) Segment h (Dezimalpunkt)
12 PB0 (AIN0) Segment c
13 PB1 (AIN1) K2 = Katode Mitte über 100 Ω
14 PB2 (OC0A) Segment d
15 PB3 OC1A Ausgang STEP
16 PB4 OC1B Ausgang !DIR
17 PB5 MOSI ISP; Segment g; Inkrementalgeber (10 kΩ gegen Masse)
18 PB6 MISO ISP; Segment a; Inkrementalgeber: Tastfunktion (10 kΩ gegen Masse)
19 PB7 SCK ISP; Segment f
20 Ucc 5 V
Timer0: Anzeige-Multiplex-Takt 800 Hz
Timer1: Frequenzgenerator im CTC-Modus, Output-Compare-Ausgänge im Toggelbetrieb
EEPROM: Die einstellbare Sollfrequenz, die zuletzt gewählte Richtung sowie Ein/Aus
wird darin gespeichert.
Alle anderen Werte (bspw. Getriebefaktor) können nur per EEPROM-Editor
oder Neuerstellung geändert werden.
*/
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <avr/eeprom.h>
#include <avr/fuse.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <avr/signature.h>
#include <util/delay.h>
#include <string.h> // memset
FUSES={
0b11100100, // kein Taktteiler, RC-Oszillator 8 MHz
0b11011111, // (unverändert)
0b11111111 // (unverändert)
};
typedef uint8_t byte;
typedef uint16_t word;
typedef uint32_t dword;
#define NOINIT __attribute__((section(".noinit")))
struct Config{
word f_soll; // Zieldrehzahl in 1/100 U/min (Anzeigewert, Default = 10 U/min = 1/6 U/s)
word fac16; // Frequenzfaktor: F_CPU/16 / fac16 / Drehzahl / 2 = Timer-Teilerwert
word f_min; // Startdrehzahl in 1/100 U/min (Default = 0,1 U/min = 1/600 U/s)
word f_max; // Maximalfrequenz (Vorgabe = 600 U/min = 10 U/s = irre schnell)
word accel; // Relative Beschleunigungszeit (Vorgabe = 20)
word deccel; // Relative Bremszeit (Vorgabe = 10)
union{
struct{
byte dir:1; // Richtung
byte ena:1; // Motor dreht (1) oder steht (0)
byte alt:1; // Mit jedem Stopp wird die Drehrichtung gewechselt
byte sin:1; // Sin+Cos-Ausgabe statt Step+Dir
byte rsv:2; // frei für Erweiterung
byte ok:2; // muss 0b10 sein wenn gültig
};
byte b2;
};
bool valid() {return ok==2;}
};
static Config c NOINIT;
static EEMEM Config eedata;
static const PROGMEM Config ee_defaults = {
/* f_soll */ 1000, // 10 U/min = 1/6 U/s
/* fac16 */ 48*6*8>>4, // Schritte pro Umlauf, Getriebe, Mikroschritt, ÷16
/* f_min */ 10, // 0,1 U/min
/* f_max */ 60000, // 600 U/min
/* accel */ 20, // ≈ 1 s
/* deccel */ 10,{ // ≈ ½ s
/* dir */ 0,
/* ena */ 0,
/* alt */ 0,
/* sin */ 0,
/* rsv */ 0,
/* ok */ 2
}};
// Die meisten Werte können nur durch Ändern des EEPROMs eingestellt werden!
// Für eine Menüführung fehlt dem ATtiny2313A Programmspeicher → ATtiny4313
static void eeprom_update(const void*ram, const void*eeprom, byte len) {
if (EECR&2) return;
const byte*a=static_cast<const byte*>(ram);
EEAR=(word)eeprom;
do{
EECR|=1;
byte b=*a++;
if (b!=EEDR) {
byte c=4; // löschen und schreiben (3,4 ms)
if (b==0xFF) c|=0x10; // nur löschen (1,8 ms)
if (EEDR==0xFF) c|=0x20; // nur schreiben (1,8 ms)
EEDR=b;
cli();
EECR=c;
EECR|=2;
sei();
return;
}
EEAR++;
}while(--len);
}
static byte digi[3] NOINIT; // Ganz normal Bit 0 = Segment a bis Bit 7 = Segment h
static byte attr[3] NOINIT; // 0: Hell, 5-7: Blinkphasen
register byte segment asm("r2");// aktuell gemultiplextes Segment (0..7); Blinkphase
register byte tccr1b asm("r3");
register word ocr1a asm("r4");
static byte keystate;
// liefert geänderte Bits sowie Vor/Zurück-Info für Inkrementalgeber:
// Bit 3 = Flanke an Inkrementalgeber-Taste
// Bit 4 = Inkrementalgeber-Aktivität
// Bit 5 = Richtung der Inkrementalgeber-Aktivität, sonst undefiniert
static byte getKeys() {
byte k=0;
if (!(PINB&0x40)) k|=0x08; // Inkrementalgeber-Taste
if (!(PIND&0x10)) k|=0x10; // Inkrementalgeber
if (!(PINB&0x20)) {k|=0x20; k^=0x10;} // Graykode zu Binärkode
byte ksh=keystate&0xF0; // High-Nibble = Inkrementalgeber-Zähler
ksh+=(char)((k&0xF0)-ksh<<2)>>2; // Differenz (-2, -1, 0 oder +1) dazu
byte ks=(k&0x0F)+ksh;
byte kss=(ks^keystate)&0x0F; // geänderte Bits (nur Tasten)
byte diff=(ks&0xF0)-(keystate&0xF0); // Differenz per Subtraktion
keystate=ks; // neuer Tastenstatus
return kss|diff;
}
// Um Widerstände zu sparen werden die Anoden 1-aus-n durchgeschaltet
// und bei LED ein die jeweilige Katode aktiviert
// Die Pins sind wild verteilt um 1. an OC1A und OC1B heranzukommen
// und 2. die Platine einfacher zu verdrahten
// Um für Charlieplexing gewappnet zu sein
// werden die Treiberleitungen in Tristate gehalten.
static byte multiplex() {
DDRB = 0b00011000; // Alle 8 Anoden auf Eingang (für bis zu 8 Tasten),
DDRD = 0b00000010; // alle 3 Katoden aus
PORTB|= 0b11100101;
PORTD|= 0b01011000; // Alle 8 Anoden mit Pullups
_delay_us(10);
byte kss=getKeys();
byte m,seg=segment&7;
PORTB&= 0b00011000; // Anoden f,a,g,d,c aus, Katode K2 aus
PORTD&= 0b10000011; // Anoden h,b,e aus, Katode K1 + K3 aus
static const PROGMEM byte anodeB[8]={1<<6, 0 ,1<<0,1<<2, 0 ,1<<7,1<<5, 0 };
static const PROGMEM byte anodeD[8]={ 0 ,1<<4, 0 , 0 ,1<<3, 0 , 0 ,1<<6};
m=pgm_read_byte(anodeB+seg); PORTB|=m; DDRB|=m;
m=pgm_read_byte(anodeD+seg); PORTD|=m; DDRD|=m;
m=1<<seg; seg=segment&0xE0;
if (digi[0]&m && !(attr[0]&seg)) DDRD|=1<<2; // Katode K1 ein
if (digi[1]&m && !(attr[1]&seg)) DDRB|=1<<1; // Katode K2 ein
if (digi[2]&m && !(attr[2]&seg)) DDRD|=1<<5; // Katode K3 ein
return kss;
}
// Zeitgeber für Display-Multiplex (Multiplex in Hauptschleife)
ISR(TIMER0_COMPA_vect) {
byte s=++segment;
s&=0x3F; if (!s) GPIOR0|=4; // Triggersignal mit 12 Hz
s&=0x0F; if (!s) GPIOR0|=2; // Triggersignal mit 50 Hz
GPIOR0|=1; // Triggersignal mit 800 Hz
}
// Helligkeitsreduktion (per Digit)
ISR(TIMER0_COMPB_vect) {
if (!(attr[0]&1)) DDRD&=~(1<<2); // Katode K1 (vorzeitig) aus
if (!(attr[1]&1)) DDRB&=~(1<<1); // Katode K2 (vorzeitig) aus
if (!(attr[2]&1)) DDRD&=~(1<<5); // Katode K3 (vorzeitig) aus
}
// Neuen Endwert und neuen Vorteiler laden
// Wegen umzuschaltenden Vorteiler kann kein doppelt gepufferter PWM-Modus verwendet werden.
ISR(TIMER1_COMPA_vect) {
TCCR1B= tccr1b;
OCR1A = ocr1a;
OCR1B = ocr1a>>1; // 90° phasenverschoben
TIMSK = 0x05;
}
/*****************
* Zahlenausgabe *
*****************/
static const PROGMEM byte seg7[10]={
0b00111111,
0b00000110,
0b01011011,
0b01001111,
0b01100110,
0b01101101,
0b01111101,
0b00000111,
0b01111111,
0b01101111
};
// Dreistellige Zahl ausgeben (n maximal 999)
static void outnumber(word n) {
const byte base=10;
for (byte i=0; i<3; i++) {
byte z=n%base;
n/=base;
digi[2-i]=pgm_read_byte(seg7+z);
}
}
// 3.2-Festkommazahl ausgeben, so dass notfalls Kommastellen abgeschnitten werden
static void show(word n) {
byte kpos=0; // (1) Bereich 0,00 .. 9,99 U/min
while (n>999) { // (2) Bereich 10,00 .. 99,99 U/min
n/=10; kpos++; // (3) Bereich 100,00 .. 655,35 U/min - schneller rast ein Schrittmotor mit Getriebe und Mikroschrittsteuerung nicht!
}
outnumber(n);
digi[kpos]|=0x80;
}
static dword dividend NOINIT;
// Frequenz f auf Timer1 ausgeben, dabei minimalen Vorteiler benutzen
// Ein PWM-Modus (mit Doppelpufferung von TOP) kann nicht verwendet werden,
// weil die Vorteiler-Umschaltung stets ungepuffert ist:
// Die Umschaltung muss unmittelbar nach Zähler-Neustart erfolgen.
static void outfreq(word f) {
byte ps=0; // Vorteiler, kommt nach TCCR1B
dword t0=0;
if (f) {
ps=0x09; // CTC-Modus mit TOP == OCR1A, Vorteiler 1
t0=dividend/f;
while (t0 & 0xFFFF0000UL) { // 2: Vorteiler 8 >>3
t0>>=++ps<0x0C?3:2; // 3: Vorteiler 64 >>3
} // 4: Vorteiler 256 >>2
if (ps>=0x0E) ps=0; // 5: Vorteiler 1024 >>2
} // ps==0: Frequenz war kleiner als 0,06 Hz (T > 16 s)
if (TCCR1B) { // Timer läuft: Später aktualisieren
ocr1a = word(t0); // vorhalten für ISR
tccr1b= ps;
TIFR = 0x40;
TIMSK = 0x45; // Interrupt jetzt aktivieren: Laden kurz nach Zähler-Neustart
}else{ // Timer steht: Starten
TCNT1 = 0;
OCR1A = word(t0); // die o.a. Division rundet sowieso ab
OCR1B = word(t0)>>1;
TCCR1B= ps;
}
}
// exponentielles Inkrementieren/Dekrementieren:
// Finde ein „glatten“ Inkrementwert 1,2,5 * 10^n für die Vorgabe v
// 0..1 1
// 2..4 2
// 5.. 5
// 10.. 10
// 20..40 20
// 50.. 50
// usw.
// 50000..65535 50000
static word calcIncr(word v) {
word i=1;
while (v>=10) {v/=10; i*=10;}
if (v>=5) i*=5;
else if (v>=2) i*=2;
return i;
}
// Templates statt Makros: Für einfache Typen; bei komplexen Typen wäre T& besser
template<class T>T min(T a,T b){return a<b?a:b;}
template<class T>T max(T a,T b){return a>b?a:b;}
static bool rampDown(word&v,word i) {
if (v<i) {v=0; return true;}
v-=i; return false;
}
static word rampUp(word v,word max,word i) {
dword w=(dword)v+i;
return w>max ? max : word(w);
}
static void swapDir() {
c.b2^=1;
if (c.sin) TCCR1C=0x40;
else PINB|=0x10; // Richtung wechseln
}
static word f_ist; // in 1/100 U/min = 1/6000 Hz
static void ramp() {
if (c.ena) {
PORTD&=~(1<<1); // lineare(!) Frequenzrampe (für Schrittmotoren günstiger als exponentiell)
// Das macht die Bezugnahme auf f_soll für die Schrittweitenberechnung
f_ist = f_ist ? rampUp(f_ist,c.f_soll,calcIncr(c.f_soll/c.accel)) : min(c.f_min,c.f_soll);
}else{
rampDown(f_ist,calcIncr(c.f_soll/c.deccel)); // schneller runter als rauf
if (f_ist < c.f_min) {
f_ist=0;
PORTD|= 1<<1;
if (c.alt) swapDir();
}
}
outfreq(f_ist); // PWM-Ausgabe nachführen
}
// Zeige Istdrehzahl (während Bewegung) oder blinkende Solldrehzahl (bei Stillstand)
static void show() {
if (f_ist) {
attr[0]&=~0xE0;
attr[1]&=~0xE0;
attr[2]&=~0xE0; // nicht blinken
show(f_ist);
}else{
attr[0]|= 0x20; // blinken
attr[1]|= 0x20;
attr[2]|= 0x20;
show(c.f_soll);
}
}
static void initHardware() {
PORTA = 0b00000111; // Pullups (der offenen Eingänge) aktivieren
PORTB = 0b00011010;
DDRB = 0b00011000; // Ausgänge aktivieren
PORTD = 0b00000011;
DDRD = 0b00000010;
ACSR |= 0x80; // Analogvergleicher deaktivieren
OCR0A = 155; // ÷156
TCCR0A= 0x02; // CTC-Modus
TCCR0B= 0x03; // mit Vorteiler 64 ergibt 8 MHz / 64 / 78 = 801 Hz Interruptfrequenz
OCR0B = 38; // ¼ Helligkeit im Normalfall
TCCR1A= c.sin ? 0x50 : 0x40; // Toggle ergibt automatisch 50 % Tastverhältnis
// Kein PWM-Modus, Ausgang OC1B bei Sin/Cos-Betrieb
TIMSK = 0x05; // Interruptroutine: Anzeige-Multiplex mit 100 Hz, dimmbar mit OCR0B
MCUCR = 0x20; // sleep aktivieren
sei();
}
// Inkrementalgeber-Flanken pro Raststufe (es gibt solche mit 1, 2 oder 4 Flanken)
static const char FPR=2;
static void lampentest() {
memset(digi,0xFF,sizeof digi); // Alle Segmente ein
memset(attr,1,sizeof attr); // Hell
do{
sleep_cpu();
if (!(GPIOR0&1)) continue;
GPIOR0&=~1;
multiplex();
}while (segment); // für kurze Zeit
memset(attr,0,sizeof attr); // Reduzierte Helligkeit
}
int main() {
eeprom_read_block(&c,&eedata,sizeof c);
if (c.valid()) {
if (c.dir) PORTB&=~0x10;
}else memcpy_P(&c,&ee_defaults,sizeof c);
initHardware();
dividend = dword(F_CPU>>5)*6000/c.fac16;
lampentest();
attr[2]=0x01;
char inkmem=0;
bool pressrotated=false;
for(;;) {
sleep_cpu();
if (GPIOR0&1) { // 800 Hz
GPIOR0&=~1;
byte keychange=multiplex();
if (keychange&0x10) {
inkmem+=keychange&0x20?-1:1;
char inkr=0;
if (inkmem<=-FPR) {inkr=-1; inkmem+=FPR;}
if (inkmem>=+FPR) {inkr=+1; inkmem-=FPR;}
if (inkr) {
byte stepbase=100; // macht Einerschritte
if (keystate&0x08) { // gleichzeitig gedrückt?
pressrotated=true;
stepbase=10; // Zehnerschritte
}
if (c.dir) inkr=-inkr; // Drehwirkung umdrehen
word i=calcIncr(c.f_soll/stepbase);
if (inkr>=0) c.f_soll=rampUp(c.f_soll,c.f_max,i);
else if (rampDown(c.f_soll,i)) swapDir();
}
}
if (keychange&0x08) {
if (keystate&0x08) {
pressrotated=false;
attr[2]&=~1;
attr[1]|= 1; // helle Zehnerstelle
}else{
attr[1]&=~1;
attr[2]|= 1; // helle Einerstelle
if (!pressrotated) c.b2^=2; // ein/aus beim Loslassen-ohne-Drehen
}
}
}
if (GPIOR0&2) { // 50 Hz
GPIOR0&=~2;
ramp();
}
if (GPIOR0&4) { // 12 Hz
GPIOR0&=~4;
show();
eeprom_update(&c,&eedata,sizeof c);
}
}
}
Detected encoding: UTF-8 | 0
|