Gross geworden

Der Synthesizer ist gewachsen:

RX

Habe nun das Programm erweitert, so dass es nicht nur Daten senden (TX), sondern auch empfangen kann (RX):

// serial 9600,N,8,1
// led on PB3, tx on PB4, rx on PB0

#include <avr/io.h>
#include <avr/interrupt.h>

#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) // clear bit
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))  // set bit

volatile uint8_t tx_buffer = 0;     // tx buffer to send
volatile uint8_t tx_register = 0;   // tx register

volatile uint8_t rx_buffer = 0;     // rx bufffer to receive
volatile uint8_t rx_register = 0;   // rx register

ISR (PCINT0_vect) {
  if (!(PINB & (1 << PB0))) {       // only trigger if level 1 -> 0
    cbi(GIMSK, PCIE);               // disable pin change interrupt
    rx_register = 1;                // rx has work
  }
}

ISR (TIM0_COMPA_vect) {            
  if (tx_register != 0) {           // skip if nothing to send
    if (!(tx_register & 1)) {       // skip every second step
      tx_register++;                //     next step
    } else if (tx_register == 1) {  // index = work
      cbi(PORTB, PB3);              //     send start bit
      tx_register = 2;              //     next step
    } else if (tx_register <= 17) { // index = data 
      if (tx_buffer & 1) {          //     from lsb to msb 
        sbi(PORTB, PB3);            //     send data bit 
      } else { 
        cbi(PORTB, PB3);            //     send data bit 
      } 
      tx_buffer >>= 1;              //     remove lsb
      tx_register++;                //     next bit
    } else if (tx_register == 19) { // index = end
      sbi(PORTB, PB3);              //     send stop bit
      tx_register = 0;              //     work done
    }
  }

  if (rx_register != 0) {           // only work if triggered
    if (!(rx_register & 1)) {       // skip every second step
    } else if (rx_register == 1) {  // start
      rx_buffer = 0;                //       new rx_buffer
      rx_register = 3;              //       catch the next wave
    } else if (rx_register <= 19) { // receive data
      rx_buffer |= ((PINB & (1 << PB0)) << ((rx_register >> 1) - 2));
    }
    rx_register++;                  // next bit
  }
}

void serial_putc(char c) {
  while(tx_register);               // wait while busy
  tx_buffer = c;                    // data to transmit
  tx_register = 1;                  // tx has work
}

char serial_getc() {
  while(rx_register < 23);          // wait until buffer ready 
  rx_register = 0;                  // reset rx register 
  sbi(GIMSK, PCIE);                 // activate pin change interrupt
  return rx_buffer;                 // return data 
} 

int main(void) { 
  sbi(DDRB, PB4);                   // set led pin as output 
 
  sbi(DDRB, PB3);                   // set tx as output
  sbi(PORTB, PB3);                  // tx level is high 
 
  cbi(DDRB, PB0);                   // rx as input 
  sbi(PORTB, PB0);                  // pull up rx 
 
  sbi(PCMSK, PCINT0);               // enable pin change on pin 0 
  sbi(GIMSK, PCIE);                 // enable pin change interrupt 
 
  sbi(TCCR0A, WGM01);               // CTC mode, clear timer on compare match
  sbi(TCCR0B, CS01);                // prescaler clk/8 -> 1 tic = 1us for 8mhz
  OCR0A = 53;                       // set compare register A 
                                    // 103us from wormfood.net/avrbaudcalc.php
  sbi(TIMSK, OCIE0A);               // enable interrrupt for OCROA==TCNT0

  sei();                            // enable all interrupts

  for (;;) {                        // main loop
    char command = serial_getc();   // read command
    serial_putc(command);           // serial out
    if (command == '1') {      
      PORTB ^= (1 << PB4);          // toggle led
    }
  }
};

Das Programm empfängt ein am Computer eingegebenes Zeichen und schickt es zurück. Wenn es ein „1“ ist, wird zudem die LED umgeschaltet. Es ist rund 430 Byte gross.

Senden funktioniert wie bis anhin: alle 53 * 8  Takte (wobei jedes zweite Mal nichts passiert – dazu später mehr – und eigentlich sollten es 52 * 8 sein, aber der interne Taktgeber ist ungenau) wird abgefragt, ob es etwas zum Senden gibt. Wenn ja, wird zuerst ein Start-, dann alle Daten- und am Ende das Stoppbit am Ausgangspin angelegt.

Zum Empfangen wird ständig geprüft, ob sich der Pegel am Eingangspin ändert. Wenn ja, und es ein Startbit ist, schalten wir die Prüfung aus und beginnen mit dem Lesen der Daten. Da dies nicht zu Beginn, sondern am besten in der Mitte eines Bits geschehen soll, wartet wir zusätzlich 53 * 8  Takte und gehen dann ähnlich wie beim Senden vor. (Damit dies zuverlässig geschieht, und man gleichzeitig Senden und Empfangen kann, haben wir den Timer von 103 auf 53 „halbiert“.) Ganz am Ende schalten wir die Prüfung des Eingangspins wieder ein.

Als nächstes möchte ich den Code weiter verkleinern und robuster machen; dann soll die Kommunikation auf mehrere ATtiny ausgebaut werden.

TX

Der ATtiny ist nun über eine Serielle Schnittstelle mit dem PC verbunden. Vorerst sendet er nur Daten und kann keine empfangen, aber immerhin.

Da der ATtiny diese Schnittstelle nicht von sich aus unterstützt, musste ich sie ausprogrammieren. So sieht der komplette Sourcecode aus:

// serial 9600,N,8,1
// led on PB3, tx on PB4

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) // clear bit
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))  // set bit

volatile uint8_t tx_work = 0;      // work request
volatile uint8_t tx_index;         // loop for on byte
volatile uint8_t tx_buffer;        // bufffer to send

ISR(TIM0_COMPA_vect) {             // interrupt service routine
  if (tx_work) {                   // work has to be done
    if (tx_index == 0) {           // index = 0
      cbi(PORTB, PB3);             //     send start bit
    } else if (tx_index <=8) {     // index = data 
      if (tx_buffer & 1) {         //     from lsb to msb 
        sbi(PORTB, PB3);           //     send data bit 
      } else { 
        cbi(PORTB, PB3);           //     send data bit 
      }
      tx_buffer >>= 1;             //     remove lsb
    } else if (tx_index >= 9) {    // index = 9
      sbi(PORTB, PB3);             //     send stop bit
      tx_work = 0;                 //     work done
    }
    tx_index++;                    // next index
  }
}

void serial_putc(char c) {
  while(tx_work);                  // wait if busy
  tx_index  = 0;                   // reset index
  tx_buffer = c;                   // data to transmit
  tx_work = 1;                     // isr has work
}

void serial_print(const char *str) {
  uint8_t i;
  for (i = 0; str[i]; i++) {
    serial_putc(str[i]);           // write each character
  }
}

int main(void) {
  sbi(DDRB,  PB4);                 // set led pin as output
  sbi(DDRB,  PB3);                 // set tx as output

  sbi(TCCR0A, WGM01);              // CTC mode, clear timer on compare match
  sbi(TCCR0B, CS01);               // prescaler clk/8 -> 1 tic = 1us for 8mhz
  OCR0A = 103;                     // set compare register A
                                   // 103us from wormfood.net/avrbaudcalc.php
  sbi(TIMSK, OCIE0A);              // enable interrrupt for OCROA==TCNT0
  sei();                           // enable interrupts

  uint8_t v;                       // init visual counter
  for(v = 0;; v++) {               // main loop
    PORTB ^= 1<<PB4;               // toggle led
    serial_print("toggle\r\n");    // serial out
    _delay_ms (200);               // wait 200ms
  }
};

Das LED leuchtet dadurch alle 400 ms – so sieht das Ergebnis auf dem PC aus:

toggle
toggle
...

PS: Das Programm verbraucht 324 Bytes. Eine frühere Version welche elegant mit fdevopen(&serial_putc, 0) und printf gearbeitet hat, musste ich verwerfen: sie war mit  2’552 Bytes, also rund 60% des verfügbaren Speichers, viel zu gross.

Winzig

Nach dem ATmega328 (steckt im Arduino) und dem ARM Cortex M0 (im Simblee) möchte ich einen neuen Mikrocontroller kennenlernen, den ATtiny45. Wieder ein AVR-Prozessor, sehr ähnlich wie sein grosser Bruder ATmega328, nur kleiner und leichter: Er hat 256 Byte RAM, 4 KB Flash-Speicher, 8 Füsschen und – was schmerzt – von Haus aus kein einfaches, serielles Interface, nur SPI. Und I2C, ok, vielleicht kann ich das irgendwie nutzten? Oder soll ich das serielle Interface mit Software nachbilden? geht das mit nur 4KB? Dazu später mehr.

Trotz seiner Grösse ist der ATtiny ein vollständiger kleiner Mikroprozessor: hat 2 Timer, Analog/Digital-Wandler, einen Stromsparmodus, und ist robust bezüglich der Stromversorgung (2 bis 5.5V). Er kostet etwa 1 EUR/CHF.

Mit diesem Projekt möchte ich weg von der Arduino IDE: es gibt zum Glück eine ausgezeichnetes Paket, das für AVR-Mikrocontroller alles nötige enthält, den C-Compiler und avrdude, um die eigene Software auf den Prozessor zu schreiben. Daneben nutzte ich jetzt den Editor Atom.

So sieht die Hardware aus:

Oben rechts: Der UBSTinyISP um den Chip via SPI zu programmieren, wird von avrdude sehr gut unterstützt. Um den Programmer einfacher mit dem Breadboard zu verbinden, habe ich mir dafür einen Stecker zusammengelötet.

Unten rechts: Ein FTDI-Adapter, der aus einem USB-Ausgang einen seriellen Port macht. Noch nicht im Einsatz.

Links: Das Breadboard mit dem ATtiny45 selbst (winzig, oder?) Zudem eine LED zum Testen und die Verkabelung zum UBSTinyISP, der auch die Stromversorgung übernimmt. Die Taktung erfolgt über den Chip selbst, ich habe noch kein Gefühl dafür, ob diese genau genug ist, oder es einen externen Oszillator braucht.

Die ersten Versuche haben gut geklappt.

  1. Verbindung prüfen
avrdude -p t45 -c usbtiny

avrdude: AVR device initialized and ready to accept instructions
Reading | ################################ | 100% 0.00s
avrdude: Device signature = 0x1e9206
[...]
avrdude done. Thank you.
  1. Die Einstellungen (sogenannte Fuses) des ATtiny richtig setzten, damit er mit 8MHz läuft, ab Werk sind es nur 1Mhz.
avrdude -p t45 -c usbtiny -U lfuse:w:0xe2:m -U hfuse:w:0xdf:m
[...]
  1. Ein kleines Programm schreiben, welches die LED – sie ist an Pin PB4 angeschlossen – ein- und ausschaltet.
#include <avr/io.h>
#include <util/delay.h>

int main(void) {
    DDRB = (1<<4);
    for(;;) {
        _delay_ms(1000);
        PORTB ^= (1<<4);
    }
    return 0;
}
  1. Und dieses kompilieren (umwandeln in eine hex-Datei) und auf den Chip hochladen (hier das Makefile).
make flash

avr-gcc -Wall -Os -DF_CPU=8000000 -mmcu=attiny45 -c main.c -o main.o
avr-gcc -Wall -Os -DF_CPU=8000000 -mmcu=attiny45 -o main.elf main.o
rm -f main.hex
avr-objcopy -j .text -j .data -O ihex main.elf main.hex
avr-size --format=avr --mcu=attiny45 main.elf

AVR Memory Usage
----------------
Program: 84 bytes (2.1% Full)
Data: 0 bytes (0.0% Full)

avrdude -p t45 -c usbtiny -U flash:w:main.hex:i
[...]
avrdude: erasing chip
avrdude: reading input file "main.hex"
avrdude: writing flash (84 bytes):
Writing | ######################################## | 100% 0.19s
avrdude: 84 bytes of flash written
avrdude: verifying flash memory against main.hex:
[...]
avrdude done. Thank you.

Soweit so gut.