Till KTH:s startsida Till KTH:s startsida

Visa version

Version skapad av William Sandqvist 2015-01-27 12:32

Visa < föregående | nästa >
Jämför < föregående | nästa >

Music player

Den musikaliska kretsen

Demonstrationslaboration om programmering av PIC-kretsar

Denna demonstrationslaboration visar, steg för steg, hur man skriver ett kort program i programspråket C, och hur man "laddar ned det" det till en PIC-krets (Perpherial Interface Computer ) med en kretsprogrammerare. Om PIC-kretsen förses med några elektroniska kringkomponenter kan den sedan utföra den uppgift den programmerats för.

Som programmeringsuppgift har vi valt att låta PIC-kretsen spela upp en melodi. Detta är inte bara en "lek", utan det kan faktiskt vara en viktig funktion hos en datorstyrd konsumentprodukt av idag. Möjligheten att få en personlig ringsignal till sin mobiltelefon har varit ett av de viktigaste konkurensmedlen mellan mobiltelefontillverkarna!
Uppgiften är tillräckligt komplex för att visa PIC-kretsens möjligheter, och för den delen, egenheter.


Musik, ton och not

ton

Den intresserade kan finna mycket information om musikens beståndsdelar och uppbyggnad på internet. Där finns även många exempel på hur ringsignalmelodier för olika mobiltelefoner är uppbyggda ( RTTTL-formatet för ringsignaler ). De kan eventuellt skrivas om och utnyttjas i denna "melodispelare".

Vi börjar med att göra en "stämgaffel", det vill säga låta PIC-kretsen avge en kontinuerlig 440 Hz ton.

För att få en stämton ska man "byta" värde "1"/"0" på kretsens utgång varje halvperiod, vilket blir var 1140 ms. Denna tid kan man få genom att genomlöpa en 10ms programslinga 114 ggr. Man börjar därför med att designa en programslinga som tar exakt 10 ms att exekvera.

Antalet varv i slingan för skalans övriga toner finns i en tabell.

Ett bra hjälpmedel för att ställa upp sin C-kod är en JSP-editor. Alla datorprogram kan byggas upp av beståndsdelarna sekvens, selektion, och iteration som ingår i JSP-diagrammet.

När man byggt upp sitt blockdiagram kan man välja menyvalet Generate, Generate C, för att få ett programskelett. Programskelettet visar programstrukturen, blockens rubriker blir till "C-kommentarer" som visar var man ska lägga in sin kod.

int main()
{
   while (I1){
      while (I2)
         /* note times - 10 us instructions */
      /* Toggle Output */
   }
}

Så här kan programmet se ut (för kompilering med B Knudsen's CC5X). Programskelettets andra while-slinga har bytts ut mot en (bekvämare) for-slinga. Man kompilerar programmet "på försök" för att se hur assemblerkoden blir och kan då beräkna hur många nop()-instruktioner som behövs för att slingan ska ta exakt 10ms. Om PIC-processorn har klockfrekvensen 4 MHz tar instruktionerna 1 ms (med några få undantag som tar 2 ms ) - detta innebär att det räcker med huvudräkning!

/* playtone.c   Generates a 440 Hz "A4" tone                          */
/* Use processor 16F690                                               */
#include "16F690.h"
#pragma config |= 0x00D4

void  main(void )
{
  char note = 114;  /*  gives standard pitch A4 440 Hz    */
  bit out;
  OPTION = 0b111; /* Timer0 Prescaler divide by 256         */
   while (1)         /*  forever                           */
    { 
       char i;     /*  local variable                      */
       for(i = note; i > 0; i--) 
           {       /* Delay. Loop + nop()'s totals 10 us   */
            nop(); nop(); nop(); nop();
           }
        /* Toggle Output */
       PORTB.0 = !PORTB.0;   /* Toggle bit B.0             */
    }
}

Genomgång av assemblerkodens aktuella avsnitt tillsammans med huvudräkning visar att det är fyra nop() som behövs!

; The 10 us delay loop.
m002	MOVF  i,1         ; 1 us
	BTFSC 0x03,Zero_  ; 2 us (When skipped)
	GOTO  m003        ;
	NOP               ; 1 us
	NOP               ; 1 us
	NOP               ; 1 us
	NOP               ; 1 us
	DECF  i,1         ; 1 us
	GOTO  m002        ; 2 us
; Total of 10 us!

Efter kompileringen är det bara att "ladda ned" koden till sin krets med en krets-programmerare och jämföra ljudet från den med en stämgaffel för att höra om man träffat rätt ton!


not

För att spela noter (helnot, halvnot, fjärdedelsnot, åttondelsnot) behöver man hålla reda på tonernas varaktighet. Enklast är att bestämma ett fixt tidsintervall för den kortaste tonen (åttondelsnoterna), och att låta längre toner bestå av samma ton upprepad i flera intervall.

TIMER0 inställningar i PIC-processorns OPTION-register.

PIC-kretsen innehåller en 8-bitars räknare, TIMER0. Den kan "nollställas" och "läsas av" från programmet, och det är lämpligt att använda denna för att hålla reda på tonernas varaktighet. Med ett så kallat OPTION-register kan man koppla in en prescaler (frekvensdelare) så att timern räknar "lagom fort" för den aktuella tillämpningen. ( OPTION = 0b11000111 ).

I programmet med den eviga stämtonen står vilkoret while(1). För att få en åttondelsnot byter man i princip ut det vilkoret till while(TMR0 < 250).


Musik

För att musik ska uppstå behöver man spela ett antal noter i följd. Vi antar att noterna finns lagrade i en funktion LookUpNote(i), som när den anropas med notens nummer i returnerar motsvarande tabellvärde för den noten. Två tabellvärden har speciell betydelse. Tabellvärdet "0" markerar slutet på melodin, och tabellvärdet "1" markerar en pausnot ( = tystnad ).

JSP-diagram

Programskelett

int main()
{
   while (I1){
      /* Look up note - note = LookUpNote( i ) */
      if (S1)
         /* Output off */
      else if (S2)
         /* Break */
      else 
         /* Output on */
      /* Start "1/8"-time  - Timer0 = 0 */
      while (I2){
         while (I3)
            /* note times - 10 us instructions */
         /* Toggle Output */
      }
      /* next note - i++ */
   }
}

Program (för kompilering med B Knudsen's CC5X).

/* mel.c Play a melody */
#include "16F690.h"
#pragma config |= 0x00D4
#include "lookup.c"
#define EIGHT_NOTE 250
char LookUpNote(char);  /* function prototype      */

  while(1)
   {
     char i;   
     for(i=0;;i++)
     {
       note = LookUpNote(i);
       if( note == 0 ) break;
       if( note == 1 ) TRISA.4 = 1;  /* pause note is silent */
       else TRISA.4 =  0;            /* RA4 is output        */
          TMR0 = 0;                  /* Reset timer0         */
          while (TMR0 < EIGHT_NOTE)  /* "1/8"-note duration  */
      	    {
              char j;
              for(j = note; j > 0; j--) 
                { /* Delay. Loop + 4 nop()'s totals 10 us  */
                  nop(); nop(); nop(); nop();
                }
              /* Toggle Output bit RA4 On/Off */
              out = !out; 
              PORTA.4 = out;
            }
     }
    while(PORTA.3 == 1){ /* wait */ } /* SW1 to play again */ 
   }

PIC16F628 är en mycket liten processor med små resurser - egentligen omöjlig (?) att programmera i C! Många gånger stöter man på att kompilatorn "ber" om förenklad kod. I detta exempel innehöll slingan for(i=0;;i++) från början ett avbrottsvilkor, men kompilatorn "klagade" på för komplicerad kod. Avbrottsvilkoret placerades i stället inuti slingan som satsen if( note == 0 ) break; och med denna omskrivning blir det inte så sammansatta vilkor för kompilatorn att reda ut.
Ett annat knep för att förenkla programkoden för kompilatorn är att använda goto (även om mången van C-programmerare nog skulle rynka på näsan åt detta).


Noternas tabellvärden

Melodin, noternas tabellvärden är lagras i funktionen LookUpNote(i) som placerats fristående från resten av programmet i filen lookup.c. Filen ska placeras i samma mapp som melody.c så att kompilatorn hittar den.

PIC-processorns instruktioner är 12 bitar långa. Konstanter, sk "literals", lagras i programminnet inbakade i instruktioner. Det finns flera instruktioner av denna typ. Tabellvärden lagras i allmänhet med assemblerinstruktionen RETLW k ( RETurn with Litteral in W ), där k är den 8 bitars konstant som ska bakas in i instruktionen. Om processorn "hoppar" till en sådan instruktion, skickas den tillbaks med talet k i arbetsregistret, w-registret. Vid assemblerprogrammering används detta tillsammans med indexerad adressering så att man med hjälp av ett index kan styra vilken av tabellens konstanter man ska komma tillbaks med. Detta kallas för Computed GOTO.

Exempel assemblerprogram. k1 k2 k3 är tabellens tre konstanter.
         CALL TABLE ; W contains table offset
         ... 
TABLE    ADDWFPC    ; W = offset, go to offset table entry
         RETLW k1   ; Begin table,  return with k1 in w
         RETLW k2   ;               return with k2 in w
         RETLW k3   ; End of table, return with k3 in w

Det finns inget "självklart" sätt att utrycka detta med C-språket. B Knutsen har infört en (inbyggd) funktion skip(i) som ska lösa detta. Denna betyder att man hoppar över det antal C-instruktioner som anges med "i".

Exempel C-funktion (enligt CC5X). k1 k2 k3 är tabellens tre konstanter.
char table(char i)
{
  skip(i);   /* internal function in CC5X  */
  return k1;
  return k2;
  return k3;
}

Med detta skrivsätt får man samma kompakta kod som vid assemblerprogrammering med Computed GOTO, med en C-funktion som är "nästan" korrekt.

Så här ser vår melodifunktion ut med detta skrivsätt ( den återfinns i filen lookup.c ):

char LookUpNote(char W)
{
skip(W); /* internal function to CC5X compiler */
return 1;   /* Pause */
return 76;  /* E5 */
return 85;  /* D5 */
return 76;  /* E5 */
return 76;
return 76;
return 76;
return 114; /* A4 */
return 114;
return 114;
return 114;
return 114;
return 114;
return 114;
return 114;
return 72;  /* F5 */
return 76;  /* E5 */
return 72;  /* F5 */
return 72;
return 76;  /* E5 */
return 76;
return 85;  /* D5 */
return 85;
return 85;
return 85;
return 85;
return 85;
return 85;
return 85;
return 1;   /* Pause */
return 1;
return 72;  /* F5 */
return 76;  /* E5 */
return 72;  /* F5 */
return 72;
return 72;
return 72;
return 114; /* A4 */
return 114;
return 114;
return 114;
return 114;
return 114;
return 114;
return 114;
return 85;  /* D5 */
return 96;  /* C5 */
return 85;  /* D5 */
return 85;
return 96;  /* C5 */
return 96;
return 101; /* B4 */
return 101;
return 85;  /* D5  */
return 85;
return 96;  /* C5  */
return 96;
return 96;
return 96;
return 1;   /* Pause */
return 0;   /* End */
}

[ mel.c ]   [ lookup.c ]   [ lookup2.c ]   [ lookup3.c ]