I2C tastatur

Fra Holstebro HTX Wiki
Skift til: navigering, søgning
Billede af I2C tastaturet

Denne side handler om konstruktionen af et I2C tastatur der fungerer som I2C slave og kan tilsluttes sammen med andre I2C Moduler til en I2C master som vist i følgende skitse.

Struktur for I2C-kommunikation med I2C tastaturet
Struktur for I2C-kommunikation med I2C tastaturet

Princippet i I2C kommunikation er beskrevet under I2C.

Alle resourcer til I2C tastaturet er Zippet sammen i en ZIP-fil.

I2C tastature kan f.x. anvendes til en I2C Lommeregner sammen med en Arduino og et I2C Display.

Princippet i I2C tastaturet

I2C tastaturet er opbygget som en selvstændig enhed der kan opfange de indtastninger der er foretaget på tastaturet, og disse indtastninger kan hentes via I2C ud fra den dokumenterede kommunikation.

Man kan anvende I2C tastaturet ved at opbygge hardwaren og lægge softwaren på som beskrevet i Atmel udviklingsmiljø. Herefter kan man kommunikere med tastaturet via I2C som dokumenteret i kommunikationen.

Ideen i at lave et selvstændigt tastatur er, at man ikke hele tiden skal servicere et tastatur, men at det kan fungere autonomt, så det selv opsamler indtastninger, og man kan så hente dem når ens kode har tid til det - her ved hjælp af I2C kommunikation.

Strukturen i I2C tastaturet

I2C tastaturet er opbygget omkring en microcontroller af typen ATTiny45 som vist i følgende blokdiagram:

Blokdiagram over I2C tastaturet
Blokdiagram over I2C tastaturet

På tastaturet sidder et stik til at programmere microcontrolleren mens den sidder i printet. Det er fysisk det samme stik som anvendes til I2C kommunikationen.

Når man anvender stikket til I2C kommunikation, så skal man være opmærksom på at de to ekstra ben der anvendes til programmring ikke må forbindes, da det ene kan resette mikrocontrolleren og det andet anvendes til at angive I2C adressen på I2C tastaturet.

Dette valg af adresse sidder som en jumper hvor man kan vælge mellem en lige og en ulige adresse på I2C tastaturet.

Så er der selvfølgelig det vigtigste, nemlig selve tastaturet, der er opbygget ud fra ideen til AD-tastaturet konstrueret til PIC-mikrocontrollere. Princippet er nærmere beskrevet under beskrivelsen af AD tastaturet.

Endelig er der en lydgiver, som giver muligheden for at få en auditiv respons fra tastaturet når man indtaster.

I2C tastatur hardware

I2C tastaturet er lagt ud ved hjælpe af Eagle og layoutet med en PDF til at lave print efter kan hentes i denne ZIP-fil.

Det samlede diagram ser ud som følger:

Total-diagram over I2C tastatur
Total-diagram over I2C tastatur

Printet kan monteres efter følgende layout:

Komponent-layout over I2C tastatur
Komponent-layout over I2C tastatur

Komponentlisten til I2C tastaturet er som følger:

Komponent Type Værdi
U1 MikroController ATTiny 45 (vendes rigtigt - i sokkel)
U1 IC Sokkel 8 bens IC-sokkel (vendes rigtigt)
SP1 Lydgiver AL60P 5V lydgiver (vendes rigtigt)
D1 Småsignal diode 1N4148 (vendes rigtigt)
LED1 Lysdiode 5mm LED Grøn (vendes rigtigt)
S1 - S16 Trykknap 16 stk trykknapper
SV1 Jumper stik 2 polet pin-række med Jumper
PR1 Molex stik 2x3 polet pin-række til Moles fladkabel-stik
R1 Modstand 10k ohm
R2 Modstand 680 ohm
R3 - R18 Modstande 16 stk. 1k ohm
R19 Modstand 220k ohm
R21 Modstand 4k7 ohm
R24 Modstand 220k ohm
C1 Polyester kondensator 100nF
C2 Elektrolyt kondensator 10uF (vendes rigtigt)
H1 - H4 Monteringshul 10mm M3 gevindstag med M3 skrue
Principskitse af AD-tastaturet

AD tast

Tastatur modellen brugt i I2C tastaturet er taget fra skolens wiki side om AD-tast.

Modulet består af 16 ens modstande sat i en serie forbindelse hvor 16 knapper vælger en af spændingerne.

Spændingerne vil være jævnt fordelt fra 1/16 af forsyningen op til selve forsyningen, så man på enkel vis kan beregne sig frem til tastenummeret ud fra måling af spændingen.

Hver modstand er 1 kilo ohm, så en belastning af spændingsdeleren ikke vil trække den skævt.

Hvis der ikke er trykket på en tast, så vil indgangen på mikrocontrolleren i princippet svæve, så for at man har styr på hvad der måles når der ikke er trykket en tast nede, så er der en pull-down modstand til stel. På I2C tastaturet er den valgt til 220k.

Hvis man regner på pull-down modstanden, så er det mest kritisk ved den midterste tast, hvor pull-down modstanden har størst mulighed for at trække spændingsdeleren skævt (der er størst modstand mod stel og størst modstand mod forsyningen). I dette tilfælde må pull-down ikke trække mere skævt end halvdelen af et trin ubelastet. Grænsen for dette er mellem 56k og 68 k, så valget af en 200k er ret konservativt.

Kommunikationsport til I2C

Kommunikationsporten til I2C

Som vist på diagrammet indeholder kommunikationsporten PR1 til I2C flere ting, da portbenene anvendes til flere forskellige funktioner.

Stikket er 6-polet selvom det kun er de 4 ben der anvendes til I2C-kommunikationen.

Til I2C har porten følgende funktion:

Ben nr Signal-navn Specielle forhold
1 MISO Anvendes ikke til I2C - skal svæve ved reset
2 Vcc (+ 5V) Forsyning til I2C modulet
3 SCK Serial Clock til I2C
4 SDA Serial Data til I2C
5 Reset Mikrocontrollerens Reset - skal svæve når modulet er i funktion
6 GND Stel-forbindelse til I2C modulet

PR1 porten kan også anvendes til at programmere Mikrocontrolleren igennem, hvor porten får følgende funktion:

Ben nr Signal-navn Specielle forhold
1 MISO En del af ICSP programmeringen
2 Vcc (+ 5V) Forsyning til I2C modulet
3 SCK En del af ICSP programmeringen
4 MOSI En del af ICSP programmeringen
5 Reset Mikrocontrollerens Reset - Anvendes til at initiere ICSP
6 GND Stel-forbindelse til I2C modulet
Adresse-jumper I2C tastaturet

Adresse på I2C tastaturet med Jumper

Ud over dette anvendes ben 1 i PR1 til at angive om det er en lige eller en ulige adresse I2C-tastaturet skal reagere på. Dette angives ved hjælp af jumperen SV1, hvor den er trukket høj af R24 når jumperen ikke er monteret og bliver trukket lav gennem R21 når man sætter jumperen på. Niveauet læses ved reset af mikrocontrolleren.

Som det kan ses på billederne til højre, så er det blot en jumper der kan sættes på for at skifte adressen. Når jumperen ikke er på, så bliver adressen (0x22) en højere (0x23), end hvis den er på.

Ideen med dette er at man kan have to ens tastaturer på samme I2C, og endda have samme program liggende i dem, men at man kan henvende sig til hvert af dem, uden det giver konflikt.

Yderligere hardware på I2C tastaturet

Der er yderligere komponeter på I2C tastaturet som har en mindre rolle at spille.

SP1 er en lydgiver, så man kan få tastaturet til at give små bip hver gang man taster - det kan være en funktion man ønsker at anvende for at give brugere en bedre fornemmelse af at der er tastet. Denne lydfunktion kan slås til og fra via I2C kommunikationen.

C1 og C2 er blot monteret for at fastholde forsyningen, hvis I2C modulet bliver forsynet gennem et længere kabel. Det er for at gøre modulet miondre støjfølsomt.

R1 og D1 er en del af reset-kredsløbet, som ellers håndteres internt i Mikrocontrolleren.

R2 og LED1 er blot en power-indikation der kan være praktisk at anvende i opstillinger hvor man kobler tingene sammen med kabler og løse ledninger. Hvis man ønsker at spare strøm i sin opstilling, så kan man spare 5 mA ved at undlade at montere disse to komponenter.

Taste overlæg til tastaturet

Et layout placeret over tastaturet til tolkning af funktionen

Når man anvender tastaturet, så kan det være en god ide at have en tolkning af hvad knappernes funktion er.

Til dette formål kan man anvende et layout som vist her.

Layoutet til knapperne kan hentes som en PhotoShop fil eller PDF-fil.

I2C Kommandoer

Alle kommandoer skal selvfølgelig sendes til den adresse I2C tastaturet har.

Efter adressen sendes en byte der bestemmer hvilken kommando man vil udføre, og alt efter hvilken kommando det er skal der sendes en byte mere, modtages en byte eller evt. modtages to byte - det afhænger naturligvis af hvilken funktion kommandoen skal udføre.

I2C tastaturet har følgende kommandoer:

Funktion Kommando nr. Byte til tastatur Byte 1 fra tastatur Byte 2 fra tastatur
Hent tast i buffer 1 - tast (0-15) / 255 -
Hent antal taster i buffer 2 - antal i bufferen -
Slet indholdet i bufferen 3 - - -
Sæt Beep mode (lyd) 4 0 / 1 - -
Sæt Repeat mode 5 0 / 1 - -
Sæt tid før godkendelse 6 antal 10ms perioder - -
Sæt tid før repeat starter 7 antal 10ms perioder - -
Sæt tid mellem hver tast i repeat 8 antal 10ms perioder - -
Hent nedtrykket tast 0 - tast (0-15) / 255 -
Hent timer0 værdi 9 - TCNT0 -
Hent pins in Port B (PINB) 10 - decimal value -
Hent Buffer start og end 11 - Buffer start pointer Buffer end pointer

Disse kommandoer bliver implementeret ved hjælp af den efterfølgende software.

I2C tastatur software

Koden til I2C tastaturet inde i ATTiny45'en ligger i denne ZIP-fil. Koden som den er kan brændes ned i en ATTiny45 ved at man redigerer den makefile-fil der ligger i mappen til den COM-port som Atmel udviklingsmiljøet skal bruge for at brænde HEX-filen ned i mikrocontrolleren. Hvis det er en ny ATTiny der ikke har fået brændt fuses, så skal man også huske at brænde dem ned i den.

Softwaren til tastaturet er skrevet i C++ til AVR og kan compileres med AVR Compileren. Koden er opbygget som de fleste andre koder til Mikrocontrollere med en initialiseringsdel hvor det hele sættes op så det er klart og en udførselsdel, hvor Mikrocontolleren tjekker alle ting løbende. Dette princip følger ideen i polling, hvor man dybest set looper så hurtigt rundt som muligt og tjekker om der skal laves noget på de forskellige ting.

Filer til softwaren

Koden er modulariseret i forskellige C++ (.cpp) filer med tilhørende include-filer (.h).

Den grundlæggende kode ligger i mail.cpp hvor initialiseringen foretages og det uendelige loop til polling ligger.

I main.cpp håndteres timingen og der kaldes ud til AD-aflæsning og I2C håndtering. Ud fra AD-aflæsningen beregnes tasteværdierne og når der registreres en indtastning placeres indtastningen i bufferen. Desuden håndteres lydgiveren.

AD-konverteringen er lavet som et simpelt modul adc.cpp der kan aflæse AD-konverteren ud fra en valg om hvilken kanal der skal aflæses og som venter på at konverteringen sker.

I2C kommunikationen løses på første niveau med i2c.cpp, som angiver hvordan I2C kommunikationen skal opføre sig på det overliggende plan, altså hvilke muligheder man stiller til rådighed for masteren

Forløbet af softwaren

Som illustreret i det viste flowchart, så sker der en initialisering af softwaren hvorefter der loopes med prioritering af I2C-kommunikationen. For hver 10. millisekund bliver tastaturet tjekket om der er sket nyt.

Flowchart over I2C-tastaturets software
Flowchart over I2C-tastaturets software

Flowchartet illustrer ikke alle detaljer i hvordan tastaturet aflæses, da der både er indbygget lyd og repeat-funktioner i aflæsningen.

Koden til softwaren

Forklaringen til koden opdeles i de forskellige funktioner der løses, så tingene kan forklares samlet.

AD aflæsning

Selv om der kun anvendes en kanal til I2C tastaturet, så anvendes et generel modul til dette formål.

Modulet includes i main.cpp med:

#include "adc.h"

I starten af main-funktionen initialiseres AD-konverteren med følgende kald:

	adc_init();

Initialiseringen foregår på følgende måde:

// Initializing AD module
void adc_init() {
	// The AD converter is set to single conversions
    ADCSRA = 0;
	ADCSRA |= (1<<ADPS2) | (1<<ADPS1);	// Set prescaler to div64 --> 125kHz ADC clock
	ADCSRA |= (1<<ADEN);				// Enable the AD-converter
}

Det gør at AD-værdien kan aflæses på følgende måde:

			sensorValue = adc_read(2);		// Read AD input of ADC2 (pin PB4)

Det retrunerede tal har en værdi fra 0 til 1023 repræsenterende 0-5V

Selve konverteringen sker på følgende måde:

// Routine for all AD-channels
int adc_read(uint8_t channel) {
  int res;
  ADMUX = ADC_REF | channel;	// Select the channel for conversion
  switch (channel) {			// Set the selected channel as input
    case 0: set_input(DDRB, PB5);
	   break;
    case 1: set_input(DDRB, PB2);
	   break;
    case 2: set_input(DDRB, PB4);
	   break;
    case 3: set_input(DDRB, PB3);
	   break;
  }
  //start a conversion
  ADCSRA |= (1<<ADSC);
  //wait for end of conversion
  while (ADCSRA & (1<<ADSC)) {
    //do nothing
  }
  res = ADCL;		// Read the converted value from two bytes
  res |= ADCH << 8;
  return res;		// Return it from the routine
}

Bemærk at koden venter på at konverteringen sker

Styring af tiden i softwaren

Da koden har forskellig gennemløbstid alt efter hvad der sker i koden, så vælges der her at sætte en fast timing op, så det er til at regne med hvilke tider der gælder for tasterne. Til dette formål vælges at arbejde med 10 ms enheder, så alle de tidsmæssige indstillinger der laves er i denne enhed (altså fra 10 ms op til 2.55 sekund, da det rummes i en byte).

De tider tastaturet arbejder med er skitseret herunder:
Tidsdiagram der illustrerer indtastninger på tastaturet
Tidsdiagram der illustrerer indtastninger på tastaturet

De svagt grå tids-enheder svarer til 10ms inddelinger.

Der er to tastetryk illustreret i tidsdiagrammet, med følgende noter tilknyttet:
1) Første tast nedtrykkes.
2) Her registreres første tast, og den lagres i bufferen. Samtidigt starter et 50 ms beep.
3) Her slippes den første tast.
4) Anden tast nedtrykkes.
5) Her registreres anden tast, og den lagres i bufferen. Samtidigt starter et 50 ms beep.
6) Her starter repeat af anden tast, den lagres i bufferen og der beepes.
7) Anden tast repeteres igen, den lagres i bufferen og der beepes.
8) Anden tast repeteres for tredje gang, den lagres i bufferen og der beepes. Det kan fortsætte så længe tasten holdes nede eller bufferen fyldes.
9) Anden tast slippes igen.

For at kunne opnå denne 10 ms opdeling anvendes Timer0, der i sin fulde udstrækning er beskrevet i databladet[1] side 65-82, hvor register-indholdet er beskrevet side 77-82.

Timeren indstilles til at fungere i CTC-mode, hvor timeren TCNT0 tæller fra 0 til indholdet i OCR0A og resetter. Der indstilles en prescaler til at dele system-clocken på 8MHz med 1024, og ved at skrive 78 i OCR0A, så nulstilles hvert 9.984 ms, hvilket er fint acceptabelt som 10 ms i denne sammenhæng.

Disse instillinger laves i starten af main-funktionen i main.cpp:

TCCR0A = 0x02;	// Limit to OCR0A
TCCR0B = 0x05; 	// 1024 prescaler
OCR0A = 78;	// time period of 9.984 ms

For at fange denne nulstilling hver gang, så testes på interrupt-flaget, uden at lave en interrupt-rutine. Det gøres ved følgende test:

    if ((TIFR & (1 << OCF0A)) > 0) {    // Timer overflow - comes about every 10 ms
        TIFR = 1 << OCF0A;              // Clear timer overflow

Når man finder interruptet findes, så slettes det med sætningen efter testen.

Hele keyboard-aflæsningen og håndteringen af buzzeren sker inde i denne if-sætning, altså hvert 10. ms.

Registrering af en tast

Når AD-værdien bliver aflæst hvert 10. ms, så testes først om der er holdt en tast nede ud fra at AD-værdien skal være over halvdelen af værdien for den mindste tastespænding. Det sker ved følgende test:

            if (sensorValue > (resolution / (antalTaster * 2))) {   // Test for any key

Ud fra den aflæse AD-værdi beregnes tastenummeret fra 0 til 15 ved at trække et halvt taste-interval fra og dividere med taste-intervallet som vist her:

                // Calculate what key seems to be pressed
                calcTast = (sensorValue - (resolution / (antalTaster * 2))) / (resolution / antalTaster);

Hvis det ikke er den samme om sidste aflæsning, så registreres det at der er den nye tasteværdi, og at den er læst 0 gange ind til videre, samt at den ikke skal være i taste-repeat.

                if (calcTast != tast) {     // Is it a new key that are pressed
                    antalRead = 0;          // Start to check for a new key
                    tast = calcTast;
                    repeat = false;

Hvis vi har samme aflæsning som sidst tjekkes først om vi har læst så mange gange at vi skal begynde at repetere tasten, og hvis det ikke er tilfældet, så registreres at den samme tast er læst en gang til:

                } else {
                    if (antalRead < repStart) { // Are we below the start of repeating
                        antalRead += 1;
                        // The same key has been read a number of times, so it is accepted

Hvis vi så har læst den samme tast 5 gange (defaultværdi i anatalGodkend) så accepteres tasten (det svarer til at tasten accepteres efter 50 ms, hvilket brugeren ikke registrerer).
Hvis det er en godkendt tast, så tjekkes lige om der er plads i bufferen, og hvis der er det, så tændes buzzeren forudsat vi er i beep-mode (det er vi som default), og tiden sættes op til at buzzeren skal lyde i 50 ms.
Når det er gjort lagres den registrerede tast i bufferen og start-pointeren til bufferen flyttes en frem.
På denne måde kommer bufferen til at fungere som en FIFO-buffer, ved at aflæsningen af tastaturet med I2C sker ved at tasteværdierne trækkes ud ved end-pointeren.

                        if (antalRead == antalGodkend) {
                            if (((bufStart + 1) % bufSize) != bufEnd) { // Is the buffer not full
                                if (beepMode) {     // If the beep-mode is set
                                    beepTime = 5;   // Set the beep-time
                                    output_high(PORTB, PB3);    // Turn on the buzzer
                                }
                                taster[bufStart] = tast;    // Save the key that has been accepted
                                bufStart++;                 // Adjust the start of the buffer
                                bufStart %= bufSize;
                            }

Stadigvæk under godkendelsen af tasten kigges på om vi er kommet til at repetere, for så sættes tiden til den næste tast til at være repAntal, der som default er 20 (giver 200 ms mellem repetationerne).

                            if (repeat) {   // If the repeat has been started 
                                antalRead -= repAntal;  // Set the time to the next key-event
                            }
                        }

Når vi når til starten af ventetiden før tasten skal repetere, så kan der ske to ting. Hvis vi ikke er i repeat-mode (hvilket ellers er default), så vil der ikke ske mere - tiden tælles ikke længere frem, og der vil ikke ske noget før der kommer en anden taste-aflæsning. Er vi i repeat-mode, så vil tiden sættes således at der vil går 200 ms inden den næste tast kommer.

                    } else {    // When the start of repeating is reached
                        if (repeatMode) {   // Is repeating allowed
                            repeat = true;  // Start the repeat
                            antalRead = antalGodkend - repAntal;    // and set the time to the next key
                        }
                    }
                }

Hvis AD-aflæsningen indikerer at der ikke er trykket på nogen taster, så sættes tastværdien til 255.

            } else {    // If no key is pressed
                tast = 255; // Indicate that no key is pressed
            }

Til slut i den if-sætning der afvikles hvert 10. ms håndteres tiden for buzzeren, ved at den tælles ned hvis den er over 0, og når den rammer 0, så slukkes der for buzzeren.

            if (beepTime > 0) {     // If the buzzer is ON
                beepTime--;
                if (beepTime == 0) { // Beep till the time has run out
                    output_low(PORTB, PB3);
                }
            }

Håndtering af I2C kommunikationen

For at adskille koden, så er selve håndteringen af I2C kommunikationen lagt ud i en fil for sig, i2c.cpp som includes i starten af main.cpp:

#include "i2c.h"

Lige som AD-modulet, så kaldes der en initialiserings-rutine fra main.cpp:

    i2c_init();     // Initialization of external modules

Denne initialisering indeholder følgende kode, der sætter interrupt, så I2C hardwaremn kan fungere, input og output sættes op, og der indstilles I2C tastaturets adresse ud fra den jumper der kan placeres på SV1. Til sidst kaldes usiTwiSlaveInit, der er en initialisering af det grundlæggende I2C-modul:

// Initializing the I2C module
void i2c_init() {
  sei();    // Set the interrupt
  set_output(DDRB, PB3);    // Buzzer as output
  output_low(PORTB, PB3);   // Buzzer silent
  set_input(DDRB, PB1);     // Input for read address for odd or even
  set_input(DDRB, PB4);     // AD - key analog input
  if ((PINB && 0x02) > 0) { // Tjeck for address setting
      I2C_SLAVE_ADDR++;
  }  
  usiTwiSlaveInit(I2C_SLAVE_ADDR);  // init I2C Slave mode in the usiTwiSlave module
}

I main.cpp der kaldes I2C taste-tjekkes hele tiden (for hver gang i loopet), så I2C kan reagere hurtigst muligt:

        i2c_tast();     // Tjeck for the I2C communication for every loop

Rutinen tjekker hver gang om masteren har henvendt sig, hvilket sker på følgende måde:

// Routine that can access the keyboard variables to get and set keyboard information via I2C
void i2c_tast() {
  uint8_t byteRcvd;
  if (usiTwiDataInReceiveBuffer()){     // got I2C input!
    byteRcvd = usiTwiReceiveByte();     // get the command byte from master

De rutiner der anvendes til at etablere I2C-kommunikationen er hentet i et generelt modul usiTwiSlave[2], der indstiller I2C porten til at reagere på den angivne I2C adresse ved hjælp af usiTwiSlaveInit(I2C_SLAVE_ADDR), og samle bytes op når de modtages og kan sende dem i det rigtige format, så når det modul er initialiseret.
Modultet fungerer interrupt-baseret, så man ikke hele tiden skal tjekke indgangene - opsamlingen udnytter at Atmel mikroconterollere har indbygget hardware til at håndtere en I2C-kommunikation, så kan hele kommunikationen forløbe ved hjælp af følgende 3 rutiner:
usiTwiDataInReceiveBuffer() der kan angive om der er modtaget data.
usiTwiReceiveByte() der henter en byte data, hvis der er modtaget noget.
usiTwiTransmitByte(tast); der sender en byte (her variablen tast)

Den første byte der modtages tolkes i dette tilfælde som en kommando, der siger hvordan resten af kommunikationen skal forløbe for den kommando. Det er stillet op i en længere case-struktur, hvor starten er angivet her:

    switch (byteRcvd) {     // All the different commands
      case 0:   // The actual tast value - will be 255 if no key is pressed - normally for testpurpose
        usiTwiTransmitByte(tast);
        break;
      case 1:   // Get a key from the buffer
        if (bufEnd == bufStart) {
            usiTwiTransmitByte(255);    // Will return 255 meaning no byte, if the buffer is empty
        } else {
            usiTwiTransmitByte(taster[bufEnd]); // Transmit the first in the buffer
            bufEnd++;           // Move buffer pointers to "clear" the transmitted key
            bufEnd %= bufSize;
        }
        break;
      case 2:	// Number of keys in buffer
        if (bufStart >= bufEnd) {
            usiTwiTransmitByte(bufStart - bufEnd);
        } else {
            usiTwiTransmitByte(bufStart + bufSize - bufEnd);
        }
        break;
    .
    .
    .

Kommando 0 er blot til test, hvor man kan læse hvilken tastværdi tatstaturet rent faktisk tolker.
Kommando 1 kan faktisk anvendes til standard aflæsning af tastaturet, hvis man er opmærksom på at der svares 255, hvis der ikke er taster i bufferen.
Kommando 2 kan anvendes til at se om der ligger taster i bufferen, så man ikke behøves at tolke det man læser med kommando 1.

Der er defineret 12 forskellige kommandoer i resten af koden der følger samme struktur, dels til at aflæse tastaturet med, og dels til at stille på tastaturets egenskaber. Koden er ikke vist her, men kan ses i ZIP-filen med koden.

Alle kommandoer er gennemgået under I2C kommandoer.

Test af I2C tastaturet

Udviklings-opstilling til I2C tastaturet

Tastaturet er blevet testet løbende under udviklingen, og der er brugt den viste testopstilling.

Den ene arduino med et AVR-programmer Shield på er til at brænde kode ned i ATTiny45 mikrocontrolleren med.

Den anden arduino er til at teste I2C kommunikationen med, her anvendes også et Shield til I2C kommunikation, blot fordi det er nemmest med de 6 polede Molex stil på et fladkabel.

Fra denne arduino kan man hente information i I2C tastaturet og præsentere det på PC'en skærm. Princippet er at man programmerer arduinoen til at fungere som Master i I2C kommunikationen, og løbende tilretter den til at afteste de funktioner man implementerer i ATTinyen.

Test af tidsmæssige forhold i I2C kommunikationen

Testopstilling til I2C tastaturet

Når man henter informationer i I2C tastaturet, så kan der være problemer med at kommunikationen låser.

I første omgang løstes dette ved blot at smide nogle små delays på 1 ms ind mange steder i Masterens kode.

Dette tolkes ikke som en holdbar løsning, så der oprettedes en ny master-kode til Arduinoen, hvor der blev tilføjet to debug-output, så man kan måle på arduinoen samtidigt med at man måler på I2C kommunikationen. Det blev gjort med den viste testopstillen, hvor man anvender en logik-analysator til at måle med.

Først måles hvordan signalerne ser ud når man måler på kommunikationen når den fungerer:
Måling af en fungerende kommunikation, hvor antal i bufferen hentes og den ønskede tast hentes.
Måling af en fungerende kommunikation, hvor antal i bufferen hentes og den ønskede tast hentes.

Det man specielt skal lægge mærke til her er den lyslilla visning lige efter 05 (taste-nummeret) i den fortolkede I2C kommunikation. Det er en NAK der skal afslutte de hentede data, lige som den ses i slutningen af den første blok hvor antallet 01 hentes.

Man skal også lægge mærke til den ekstra lille puls i slutningen af Digital 2, der er et debug-output som viser forløbet af koden i arduinoen.

Herefter måles hvad der sker når kommunikationen fejler:
Måling af en fejl kommunikation, hvor den ønskede tast ikke bliver hentet.
Måling af en fejl kommunikation, hvor den ønskede tast ikke bliver hentet.

Her er der zoomet ind på hentningen af tasten, da det er her der fejles. Det der sker er at der hvor tasten hentes, der svares der med 00 efterfulgt af en ACK, og ikke en NAK som den skulle. Dette gør at masteren ikke kan hente den ønskede taste-værdi og som det ses, så afspejles det også ved at der ikke er den lille ekstra puls på Digital 2.

Årsagen til dette er at ATTiny'en der laver I2C tastaturet ikke kan nå at lægge pakken klar til afhentning i alle situationer. Løsningen på dette er at lægge en delay ind på 100 mikrosekunder ind mellem den Wire.endTransmission hvor man sender kommandoen og den Wire.requestFrom som beder om en byte fra I2C tastaturet.

Dette gør at koden til getTastBuf skal se ud som følger:

void getTastBuf() {
  Wire.beginTransmission(I2C_Address);
  Wire.write(2);
  error = Wire.endTransmission();

  if (error == 0)
  {
    delayMicroseconds(100);
    Wire.requestFrom(I2C_Address, 1);
    if (Wire.available() == 1) {
      error = Wire.read();
      if (error >= 1) {
        Serial.print("Num in buffer: ");
        Serial.print(error);
        Serial.print("\tKey reading: ");
        Wire.beginTransmission(I2C_Address);
        Wire.write(1);
        error = Wire.endTransmission();
      
        if (error == 0) {
          delayMicroseconds(100);
          Wire.requestFrom(I2C_Address, 1);
          if (Wire.available() == 1) {
            error = Wire.read();
            Serial.println(error);
          }
        }
      }
    }
  } else {
    Serial.print(error);
    Serial.println(" Fail");
  }
}

Arduino Master software

For at kunne anvende I2C tastaturet til noget fornuftigt, så skal der skrives en I2C master der trækker det ud af slaven (tastaturet) som er relevant - her er det indtastninger på tastaturet.

Denne Arduino master er skrevet for at illustrere de forskellige funktioner i I2C tastaturet som man kan få fat i via I2C kommunikationen.

Koden til I2C-masteren kan hentes i denne ZIP-fil.

For at kunne opbygge en I2C master skal modulet wire includeres, og der defineres en adresse man vil kommunikere med:

#include <Wire.h>

#define I2C_Address 0x23

Adressen er her sat til 0x23, hvilket svarer til at der ikke er en jumper på SV1 - hvis der placeres en jumper på SV1, så bliver adressen til 0x22 (læses kun ved reset af tastaturet).

setup()

Initialiseringen af wire-modulet foretages som følger:

void setup()
{
  Wire.begin();

  Serial.begin(9600);
  delay(100);
  help();  
}

Ud over dette indstilles den serielle port, så man i test-programmet kan se hvad der sker, og der udskrives en hjælpeskærm, så man får vist hvilke muligheder der kan afprøves.

loop()

I loopet startes med at kigge efter en seriel karakter, så man kan bestemme hvad man skal kommunikere til modulet:

void loop()
{
  if (Serial.available() > 0) {
    ch = Serial.read();
    if ((ch == 'h') | (ch == 'H')) {
      help();
    }
    if ((ch == 'b') | (ch == 'B')) {
      error = Serial.parseInt();
      setBeep(error);
    }
    if ((ch == 'a') | (ch == 'A')) {
      getTast();
    }
    if ((ch == 'r') | (ch == 'R')) {
      error = Serial.parseInt();
      setRepeat(error);
    }
    .
    .
    .

Der er en del flere muligheder, som det kan ses af følgende hjælpeskærm:

Demo program for I2C AD keyboard
--------------------------------
H       Displays this help-screen
B n     Set Beep mode to n
M       Flips the mode where the keys are presented
A       Gets the actually read keyvalue
G       Tries to read from the key-buffer
C       Clears the buffer
R n     Repeat set to n
N       Number in buffer
S       Start and End pointers to the buffer
K n     Time before a key is accepted as valid
U n     Time before starting repeat
V n     Time between repeating keys
P       Port B as input (decimal)
T       Timer value

Hvis test-softwaren er sat til getMode (default), så kaldes rutinen gerTastBuf() der forsøger at hente en tast i bufferen. Dette mode kan ændres ved at taste M.

  if (getMode) {
    getTastBuf();
  }

Læsning af en tast på tastaturet

getTastBuf er defineret som følger:

void getTastBuf() {
  Wire.beginTransmission(I2C_Address);
  Wire.write(2);        // Kommando der spørger om antallet af taster i bufferen
  error = Wire.endTransmission();

  if (error == 0)
  {
    delay(1);
    Wire.requestFrom(I2C_Address, 1);  // Læs svar fra tastaturet
    delay(1);
    if (Wire.available() == 1) {
      error = Wire.read();             // Læs antallet af taster der er klar i tastaturet
      if (error >= 1) {
        Serial.print("Num in buffer: ");
        Serial.print(error);           // Udskriv resultater
        Serial.print("\tKey reading: ");
        Wire.beginTransmission(I2C_Address);
        Wire.write(1);
        error = Wire.endTransmission();
      
        if (error == 0) {
          delay(1);
          Wire.requestFrom(I2C_Address, 1);  // Læs svar fra tastaturet
          delay(1);
          if (Wire.available() == 1) {
            error = Wire.read();       // Læs hvilken tast det er
            Serial.println(error);     // og skriv den ud
          }
        }
      }
    }
  } else {
    Serial.print(error);     // Visning af fejl-kommunikation
    Serial.println(" Fail");
  }
}

Det ser sker i koden er at der sendes et 2-tal til tastaturet, og hvis det går godt, så hentes der en byte, der angiver hvor mange taster der ligger i bufferen.

Hvis det tal er over 0 (at der ligger taster i bufferen), så udskrives antallet sammen med noget tekst, og der sendes et 1-tal til tastaturet, og går det godt, så hentes den karakter der ligger i bufferen, og den skrives ud.

Dette er en lidt avanceret måde at hente tasterne på.

Man kan også gøre det mere simpelt som vist i getTastRaw():

void getTastRaw() {
  Wire.beginTransmission(I2C_Address);
  Wire.write(1);
  error = Wire.endTransmission();

  if (error == 0)
  {
    Wire.requestFrom(I2C_Address, 1);
    delay(1);
    if (Wire.available() == 1) {
      error = Wire.read();
      Serial.print("Key reading: ");
      Serial.println(error);
    }
  } else {
    Serial.print(error);
    Serial.println(" Fail");
  }
}

Her hentes tasten simpelthen og udskrives. Det man vil se er at hvis der ikke er tastet noget, så svarer tastaturet med 255. Det vil sige at man på denne måde bare kan hente tasten, og hvis den er 255, så betyder det at der ikke er kommet en tast siden man tjekkede sidst.

Det kan lade sig gøre at indstille forskellige ting i tastaturet ved at sende forskellige kommandoer. Dette eksempel viser hvordan man kan slå Beep funktionen til og fra.
Sender man 0 efter kommandoen bliver beep-funktionen slået fra.
Sender man 1 efter kommandoen bliver beep-funktionen slået til.

void setBeep(byte mode) {  // Rutine der kan slå lydgiveren til og fra
  Wire.beginTransmission(I2C_Address);
  Wire.write(4);     // Angiv at det er kommandoen til lydgiveren
  Wire.write(mode);  // Angiv om den skal såes til eller fra
  error = Wire.endTransmission();
  if (error == 0) {
    Serial.print("Beep mode set to: ");  
    Serial.println(mode);   // Vis resultatet hvis det lykkedes
  }
}

Der ligger også forskellige test-kommandoer, hvor man f.x. kan læse hvordan start og end pointer stå. Denne funktion har ikke nogen værdi i forbindelse med anvendelsen af tastaturet, men kan være nyttig hvis man er i tvivl om hvordan tastaturet fungerer.

void getStartEnd() { // Rutine der henter de to buffer-pointere
  Wire.beginTransmission(I2C_Address);
  Wire.write(11);    // Angiv kommando til at hente buffer-pointere med
  error = Wire.endTransmission();

  if (error == 0)
  {
    delay(1);
    Wire.requestFrom(I2C_Address, 2);
    delay(1);
    if (Wire.available() == 2) {  // Tjek om der der er kommet 2 byte
      error = Wire.read();        // læs start-pointer
      Serial.print("Start: ");
      Serial.print(error);        // læs end-pointer
      Serial.print("\tEnd: ");
      error = Wire.read();
      Serial.println(error);      // Det hele skrives ud på terminalen
    }
  }
  else
  {
    Serial.print(error);
    Serial.println(" Fail");
  }
}

Referencer