JESCAB 
Produkter Tjänster Info Leveransvillkor Kontakt
 Snabb länkar:


PIC MPLAB Relocatable mode MAP fil Exempel kod Datablad

Relocatable mode

 

Allmänt

Här beskrivs helt kort de viktigaste skillnaderna mellan att skriva assembler kod i "absolut mode" jämfört med "relocatable mode".

 

 

Absolute mode Relocatable mode
Allokering av variabler i RAM minnet.

Måste ske "för hand".

 

Sker oftast med EQU direktivet vilket inte alls är någon allokering av minne, om man skall vara korrekt. EQU sätter bara ett värde på en symbol, och MPASM har inte en aning om att värdet råkar motsvara en adress i RAM minnet.

 

Man kan av misstag sätta samma värde på två symboler utan att få någon varning eller felmeddelande, men med ganska konstiga fel senare.

 

Om man flyttar koden mellan olika PIC modeller, måste man se över sina EQU eftersom olika processorer kan ha RAM minnet på olika adresser. D.v.s mer job vid flytt mellan modeller.

Sker av MPLINK via RES direktivet. RES allokerar en minnesaddress och MPLINK ser till att ingen annan variablel i applikationen kan allokeras till samma RAM adress.

 

I källkoden anges aldrig en fysisk adress till RAM minnet. Adresserna finns i "Linker script", d.v.s LNK filen.

 

När man flyttar koden till en annan PIC modell, byter man LNK fil i projektet och därmed flyttas alla variabler till adresser som stämmer med den nya processorn. Även portningar av kod mellan PIC16 och PIC18 serierna underlättas detta sätt, eftersom hanteringen av RAM minnet sker automatiskt (mer eller mindre).

 

Så, använd aldrig EQU (Eller CBLOCK) för "allokering" av variabler !

Återanvändning av kod. Man måste bl.a själv kontrollare så olika delar av applikationer inte använder variabler på samma adress. Gör det svårt att skriva generella runiner som enkelt kan användas i olika projekt.

Alla olika delar av applikationen (även de delar som har "återanvänts" använder RES och därför kommer MPLINK att allokera varabler med hänsyn till hela applikationen. Inga adresser kan allokeras till mer än en variabel.

 

D.v.s att samma generella rutin kan ha sina variabler på olika adresser beroende på vilken applikation den används i. Helt OK.

Segmentering av kod. Hela applikationen är ett enda segment. Alla adresser måste anges "för hand".

Man använer CODE direktivet för att segmentera koden i mindre bitar. Detta gör det enkelt för MPLINK att sprida koden runt speciella delar som man vill ha på speciella adresser (som t.ex lookup-tabeller).

 

Att dela koden i många mindre segment underlättar för MPLINK att fylla "hålen" i programminnet. Normalt anges bara enstaka adresser direkt i källkoden, resten får MPLINK räkna ut...

 

Alla JESCAB's exempel använder rellocatable mode, och det hela blir helt naturligt om man gör "rätt" från början.


Konvertering av kod från abs till relloc mode.

Vid konvertera en gammal kod som är skriven i absolute mode finns det ett par saker som kan behöver göras :

  • I det nya projektet skall ett "Linker script" läggas till. Därmed kommer MPASM att ge felmeddelanden för det som inte är giltigt i relocatable mode.
  • Byt EQU till RES (se MPASM manulen eller programexempel för syntax).
  • Segmentera gärna koden lite med CODE (inte nödvändigt, men kan göra det överskådligare)
  • För ett stort projekt, fundera på att dela upp applikationen på flera ASM filer.

Övrigt

Observera att om man kodar för de större PIC modellerna (dsPIC30, dsPIC33 och PIC24) så är rellocatable mode det enda alternativet.


Exempel på användningen av RES direktivet

Eftersom många tycker det är lite svårt att följa med i exemplen i manualen till MPASM/MPLINK så kommer här några korta/enkla kodexempel som visar olika sätt att allokera utrymme i RAM (GPR) för variabler.

Ex1: Enkelt exempel med variabler i "shared memory"
(enbart PIC16 !)

Shared memory (eller "unbanked memory") är den del av GPR som är gemensam för alla banker. D.v.s att samma minne accessas oavsett hur bank-bitarna i STATUS registret råkar vara ställda. I t.ex en 16F628A är detta de sista 16 adresserna i varje bank. Detta minnesutrymme har två huvudsyften, dels för lagring av register under ett interrupt, dels för andra "snabba" variabler som man alltid vill ha tillgång till utan extra "bank-switching".

I exemplet tänker vi oss två variabler som ska allokeras till "unbanked memory", det skulle kunna se ut så här :

; Allokerar två variabler i "unbanked memory".
;
my_variables      UDATA_SHR             ; Starta ny data-section. Namnet "my_variables" kommer att
                                        ; finnas med i MAP filen så att man (om man vill) kan
                                        ; se vilka fysiska adresser som de hamnade på. Normalt
                                        ; är det inget som man varken vill eller behöver veta...
;
myvar1            RES 1                 ; Allokerar (skapar) en variabel med namnet "myvar1".
myvar2            RES 1                 ; Allokerar (skapar) en variabel med namnet "myvar2".
                                        ; De två symbolerna "myvar1" och "myvar2" används sedan
                                        ; som "vanligt" i koden där en adress till en varabel
                                        ; kan användas.
;
; Slut på variabler, här kommer koden...
;
my_code           CODE                  ; Starta ny code-section.
;
; ...
; ...

Eftersom myvar1 och myvar2 är allokerade i "unbanked memory", så behöver man inte bekymra sig om BANKSEL innan man använder dom i koden.

 

Notera att PIC12F629/675 *enbart* har "unbanked memory", så på dom *måste* man använda UDATA_SHR !

Notera även att detta inte gäller (den för övrigt mycket snarlika) PIC12F683, den har sitt "unbanked memory" mer likt övriga PIC16 modeller.

Ex2: Allokering av variabler i övriga delen av minnet.

När man har mer normala variabler lägger man dom vanligtsvis i "banked memory" (fast om de ändå inte

är fler än 16, så kan ju alla vara "unbanked") m.h.a UDATA. Ett litet exempel skulle kunna se ut så här:

; Allokerar variabler i "banked memory".
;
my_variables2      UDATA                ; Starta ny data-section. Namnet "my_variables2" kommer att
                                        ; finnas med i MAP filen så att man (om man vill) kan
                                        ; se vilka fysiska adresser som de hamnade på. Normalt
                                        ; är det inget som man varken vill eller behöver veta...
;
myvar21            RES 1                ; Allokerar (skapar) en variabel med namnet "myvar1".
myvar22            RES 1                ; Allokerar (skapar) en variabel med namnet "myvar2".
                                        ; De två symbolerna "myvar1" och "myvar2" används sedan
                                        ; som "vanligt" i koden där en adress till en varabel
                                        ; kan användas.
;
                                        ; Notera nu att man inte vet *var* de två variablerna
                                        ; har allokerats eller i vilken bank, så BANKSEL måste
                                        ; användas innan man använder dom!
;
; Slut på variabler, här kommer koden...
;
my_code           CODE                  ; Starta ny code-section.
;
                                        ; Så här klan det t.ex se ut i koden
                                        ; Vi vill räkna ner myvar21 med 1 och kolla
                                        ; om resultetet blev 0.
;
;
                  BANKSEL  myvar21      ; Se till att rätt bank är vald.
                  DECFSZ   myvar21, f   ; Minska myvar21 och "skippa" nästa instruktion om = 0.
                  GOTO     not_zero     ; Det blev *inte* 0, gå och gör något annat...
                  ...                   ; Det *blev* 0, så fortsätt här...
;
; ...
; ...

Så skillnaden mellan UDATA_SHR och UDATA är i princip bara "bankningen".

Ex3: Allokeringen av temorära variabler som kan "delas" mellan olika delar av koden.

Ofta har man delar av koden (t.ex en delay rutin) där man har temorära variabler (t.ex "varv-räknarna" i en delay rutin) vars värde inte är intressant efter att man har lämnat rutinen. De adresser som dessa variabler upptar skulle ju då kunna delas av andra delar av koden för att spara RAM minne. Till detta används UDATA_OVR. Vi tittar på ett exempel igen...

 

; Allokerar variabler som "overlayed" från två olika subrutiner.
;                                      
;********************************************************************************
; SUB1, som har 2 lokala temp-variabler och 2 andra variabler.
;
shr_vars           UDATA_OVR            ; Starta ny data-section för temp variablerna
                                        ; Vi väljer att kalla den för "shr_vars".  
;
sub1_tmp1          RES 1                ; Allokera de två temp variablerna...
sub1_tmp2          RES 1
;
sub1_vars          UDATA                ; Starta ny data-section för de "normala" variablerna.
;
sub1_var1          RES 1                ; Allokera de två variablerna...
sub1_var2          RES 1
;
;
sub1_code          CODE                 ; Starta code-section för SUB1.
;                  ...
;
;********************************************************************************
; SUB2, som har 3 lokala temp-variabler och 1 vanlig variabel.
;
shr_vars           UDATA_OVR            ; Starta ny data-section för temp variablerna¨
                                        ; notera att vi sätter samma namn som i SUB1
                                        ; för att dela minnesadresser!
;
sub2_tmp1          RES 1                ; Allokera de tre temp variablerna...
sub2_tmp2          RES 1
sub2_tmp3          RES 1
;
sub2_vars          UDATA                ; Starta ny data-section för de "normala" variablerna.
;
sub2_var1          RES 1                ; Allokera de två variablerna...
sub2_var2          RES 1
;
;
sub2_code          CODE                 ; Starta code-section för SUB2.
;                  ...
;                  ...

Om vi nu tittar i MAP filen för att se hur detta allokerades så kan det se ut så här (endast de intressanta delarna):

...
...
                                 Section Info
                  Section       Type    Address   Location Size(Bytes)
                ---------  ---------  ---------  ---------  ---------
                   .cinit    romdata   0x000005    program   0x000004
                SUB1_CODE       code   0x000007    program   0x000002
                 SHR_VARS      udata   0x000120       data   0x000003
                SUB2_VARS      udata   0x000123       data   0x000002
                SUB1_VARS      udata   0x000125       data   0x000002
...
...
                              Symbols - Sorted by Address
                     Name    Address   Location    Storage File                    
                ---------  ---------  ---------  --------- ---------               
                SUB1_TMP1   0x000120       data     static C:\DATA\proj\test\Untitled.asm
                SUB2_TMP1   0x000120       data     static C:\DATA\proj\test\Untitled.asm
                SUB1_TMP2   0x000121       data     static C:\DATA\proj\test\Untitled.asm
                SUB2_TMP2   0x000121       data     static C:\DATA\proj\test\Untitled.asm
                SUB2_TMP3   0x000122       data     static C:\DATA\proj\test\Untitled.asm
                SUB2_VAR1   0x000123       data     static C:\DATA\proj\test\Untitled.asm
                SUB2_VAR2   0x000124       data     static C:\DATA\proj\test\Untitled.asm
                SUB1_VAR1   0x000125       data     static C:\DATA\proj\test\Untitled.asm
                SUB1_VAR2   0x000126       data     static C:\DATA\proj\test\Untitled.asm

Notera hur tmp1 opch tmp2 från både SUB1 och SUB2 delar adress medan var1 och var2 är separat allokerade.

 

När man använder overlay variabler så måste man naturligtsvis se upp! SUB1 får ju t.ex inte göra ett CALL till SUB2!

Man kan inte heller låta SUB1 avbrytas av ett interrupt som i sin tur gör CALL till SUB2.

Men rätt använt är det ett sätt att hushålla med minnet i processorn.

 

Genom att man själv anger namnet på den "overlay section" som man vill använda, så kan man skapa flera stycken om man t.ex har olika grupper av subrutiner som normalt inte anropas från varandra. D.v.s en grupp av 2-3 subrutiner kan dela på en "overlay section" medan 3-4 andra subrutiner kan dela på en helt annan "overlay section".

Ex4: Allokering av variabler i "access bank" (endast PIC18 !)

"Access bank" i PIC18 motsvaras på sätt och vis av (men ska inte förväxlas med) "unbanked memory" i PIC16. Det är en del av RAM minnet som man alltid "kommer åt" oavsett hur bank-bitarna är satta. Access bank är också betydligt större än "unbanked memory" i PIC16, normalt 128 bytes (varierar dock mellan PIC18 modellerna, se respektive datablad!).

 

Kodexempel är lite onödigt, det ser i princip likadant ut som ovan, fast UDATA_ACS används istället.

 

Det finns även en specialvariant av UDATA_OVR för PIC18, den heter då ACCESS_OVR, och fungerar i princip som UDATA_OVR ovan, men att de "delade" sektionerna allokeras i just "access bank". UDATA och UDATA_OVR fungerar även till PIC18, men då allokeras utrymmet i det övriga RAM minnet och BANKSEL kan behövas (eller så kör man indexerad läsning via index registren FSR0, FSR1 och FSR2, vilket sannolikt är vanligare på PIC18).

En liten titt i en LKR fil...

Inte för att fördjupa det för långt, men vi gör i alla fall en liten koll i en LKR fil, i detta fall för PIC16F886.
Först filen så kommer kommentarerna efteråt...

// Sample linker command file for 16F886

LIBPATH  .

CODEPAGE   NAME=vectors  START=0x0      END=0x4      PROTECTED
CODEPAGE   NAME=page0    START=0x5      END=0x7FF
CODEPAGE   NAME=page1    START=0x800    END=0xFFF
CODEPAGE   NAME=page2    START=0x1000   END=0x17FF
CODEPAGE   NAME=page3    START=0x1800   END=0x1FFF
CODEPAGE   NAME=.idlocs  START=0x2000   END=0x2003   PROTECTED
CODEPAGE   NAME=.config  START=0x2007   END=0x2008   PROTECTED
CODEPAGE   NAME=eedata   START=0x2100   END=0x21FF   PROTECTED

DATABANK   NAME=sfr0     START=0x0      END=0x1F     PROTECTED
DATABANK   NAME=sfr1     START=0x80     END=0x9F     PROTECTED
DATABANK   NAME=sfr2     START=0x100    END=0x10F    PROTECTED
DATABANK   NAME=sfr3     START=0x180    END=0x18F    PROTECTED

DATABANK   NAME=gpr0     START=0x20     END=0x6F
DATABANK   NAME=gpr1     START=0xA0     END=0xEF
DATABANK   NAME=gpr2     START=0x110    END=0x16F
DATABANK   NAME=gpr3     START=0x190    END=0x1EF

SHAREBANK  NAME=gprnobnk START=0x70     END=0x7F
SHAREBANK  NAME=gprnobnk START=0xF0     END=0xFF
SHAREBANK  NAME=gprnobnk START=0x170    END=0x17F
SHAREBANK  NAME=gprnobnk START=0x1F0    END=0x1FF

SECTION    NAME=STARTUP  ROM=vectors    // Reset and interrupt vectors
SECTION    NAME=PROG1    ROM=page0      // ROM code space - page0
SECTION    NAME=PROG2    ROM=page1      // ROM code space - page1
SECTION    NAME=PROG3    ROM=page2      // ROM code space - page2
SECTION    NAME=PROG4    ROM=page3      // ROM code space - page3
SECTION    NAME=IDLOCS   ROM=.idlocs    // ID locations
SECTION    NAME=DEEPROM  ROM=eedata     // Data EEPROM

Allt som heter xxxPAGE eller xxxBANK definierar de *fysiska* adresserna för bl.a programminne (flash) och RAM-minne (GPR).

(Dessutom anges var EEPROM minnet ligger m.m, men det hoppar vi över just nu...)

 

Allokeringen av minne sker i princip så här för olika direktiv:

 

CODE 

Letar efter ledigt minne i en CODEPAGE (som inte är "PROTECTED" !).
Man kan styra sin kod till en viss "page" genom att skriva t.ex "PROG2" före CODE direktivet.
Notera hur PROG2 "pekar" mot den CODEPAGE som heter "page1". Sen kan man fråga sig varför de inte heter PROG0-PROG3, men det är en helt annan sak och det har inget praktisk betydelse... :-)


Man kan själv lägga till flera SECTIONS med "ROM=" för att styra olika delar av koden till olika pages.


Eftersom det krävs lite extra "jobb" i koden när man gör CALL över page-gränserna, så kan det vara praktiskt att samla subrutiner som hör ihop (och som gör många anrop till varandra) på samma CODEPAGE. För att garantera att så sker, måste man skapa en speciell SECTION för dessa och ange vilken CODEPAGE som den nya sektionen "hör till". Låter man MPLINK allokera helt själv, så kan subrutinerna hamna på olika CODEPAGE's.

 

Notera att det finns en SECTION som heter STARTUP och som pekar mot "vectors" på adress "noll" d.v.s där man hamnar efter reset eller power-on. I mina exempelkoder används STARTUP för att placera de instruktioner som ska köras vid reset eller power-on på rätt ställe.

UDATA

Allokerar minne från ledigt minne i *någon* DATABANK. Vilken som helst som har ledigt minne (kvar).
Man kan styra UDATA till en speciell bank genom att skapa en ny SECTION med ett nytt namn och med t.ex "RAM=gpr3" för att styra denna nya sektion till bank3. Sedan sätter man detta nya namn före sin UDATA och länkaren kommer då att ta hand om det hela. 

 

Notera också att FSR minnet är definierat som DATABANK'er, men detta är PROTECTED, så länkaren kommer aldrig att försöka allokera variabler där.

UDATA_SHR  Allokerar minne från SHAREBANK.
Notera att alla 4 är definierade i LKR filen, men de avser ju i praktiken samma fysiska minne. 
UDATA_OVR  Samma som för UDATA, för övrigt se ovan om UDATA_SHR... 

Notera slutligen att det står "*Sample* linker command file". För större projekt är det vanligt att man gör anpassningar av LKR filen, t.ex av det slag som beskrevs ovan. Anledningen kan t.ex vara att göra placeringen av t.ex lookup-tabeller transparent för applikationen (källkoden) så att när man flyttar koden mellan olika PIC modeller så byter man bara LKR fil. 


Denna sida senast uppdaterad 26-Jan-2013