denna artikel är inriktad på att lära sig hur en microcontroller kärna är utformad, och är endast avsedd för utbildningsbruk. Besök gärna www.zilog.com och kolla tillverkarens produktlinje för att välja en mikrokontroller som passar dina projektbehov (från åtta bitars Z8-Encores! och eZ80 Acclaims till 32-bitars ARM Cortex – M3 baserad ZNEO32! som inkluderar avancerade motorstyrfunktioner).
min kärleksaffär med mikrokontroller och mikroprocessorer började redan 1988 när jag arbetade mot en teknisk examen vid CEFET-PR (en fyraårig Brasiliansk sekundär/teknisk skola och universitet i Curitiba). Jag började med att lära mig grunderna medan jag utforskade den klassiska Zilog Z-80 (Figur 1a).
Figur 1A. Zilog Z-80A (med tillstånd av Wikimedia Commons).
Snabbspolning framåt genom en karriär inom programmering som inkluderade författande av några böcker om mikrokontrollerprogrammering (se resurser), starta ett litet designhus (ScTec) och avsluta ett post-examensprogram vid CEFET-SC (ett annat Brasilianskt universitet i Florianopolis). Detta var 2008, då jag hade mer kontakt med programmerbar logik och VHDL och min nyfikenhet toppade. År senare i 2016 hittade jag ett mycket prisvärt FPGA-kit (Field-Programmable Gate Array) och bestämde mig för att ge det en chans och började lära mig mer om FPGA-tekniken.
vad skulle vara bättre än att designa en mjukporr för att lära sig mer om VHDL (VHSIC hardware description language), FPGA: er och mikroprocessorkärnor själva? Jag slutade välja en modern Z-80-släkting: Zilog Z8 Encore! (Alias eZ8; Figur 1b).
Figur 1b.Zilog eZ8.
det är en åtta-bitars mikrokontroller kärna med en enkel — men kraftfull — instruktionsuppsättning och en mycket trevlig on-chip debugger. Med sin lätta IDE (integrated development environment) och gratis ANSI C-kompilator är det ett utmärkt projekt att lära sig (och även lära) om inbyggda system.
innan vi dyker in i djupet av kärnoperationen, VHDL och FPGA, låt oss ta en titt på Zilog Z8 Encore! funktion.
figur 1C. FPz8 på en FPGA.
Zilog Z8 Encore!
eZ8 är en åtta-bitars mikrokontrollerfamilj baserad på Zilogs framgångsrika Z8-familj och på det stora Z-80-arvet. Den har en Harvard CISC-maskin med upp till 4 096 byte RAM (filregister och specialfunktionsregisterområde), upp till 64 KB programminne (vanligtvis flashminne) och upp till 64 KB dataminne (RAM). Ez8-kärnan innehåller också en vektorerad avbrottskontroller med programmerbar prioritet och en on-chip debugger som kommunicerar med värddatorn med asynkron seriell kommunikation. Dessa mikrokontroller är packade med en mycket fin kringutrustning, allt från mångsidiga 16-bitars timers till motorstyrningstimers, från flera UARTs (IrDA ready) till USB-enheter och mycket mer (besök www.zilog.com för att kontrollera hela produktlinjen).
ett viktigt inslag i ez8-programmeringsmodellen är bristen på en fast ackumulator. Istället kan någon av de 4 096 möjliga RAM-adresserna fungera som ackumulatorer. CPU behandlar sin huvudsakliga RAM (filen och SFRs — special function registers — area) som en stor uppsättning CPU-register. För att uppnå detta delas RAM upp i registergrupper (det finns 256 grupper med 16 arbetsregister vardera). En instruktion fungerar vanligtvis inom en enda arbetsregistergrupp, som väljs av en SFR som heter RP (registerpekare). Observera att alla SFR finns på den sista sidan av RAM (adresser från 0xF00 upp till 0xFFF).
när det gäller instruktionsuppsättningen finns det 83 olika instruktioner uppdelade i två opcode-sidor. Den består av vanliga instruktioner för grundläggande operationer som addition, subtraktion, logiska operationer, instruktioner för datamanipulation, skiftande instruktioner, instruktioner för flödesändring, några 16-bitars instruktioner, bittestning och manipulation, 8×8 multiplicera, etc.
programminnesområdet är organiserat så att de första adresserna är avsedda för speciella ändamål. Adresser 0x0000 och 0x0001 är dedikerade till konfigurationsalternativen; adresser 0x0002 och 0x0003 lagrar återställningsvektorn; och så vidare. Tabell 1 visar programminne organisation.
0x0000 | Option bytes |
0x0002 | Reset vector |
0x0004 | WDT vector |
0x0006 | Illegal instruction vector |
0x0008 to 0x0037 | Interrupt vectors |
0x0038 to 0xFFFF | User program memory area |
TABLE 1. Simplified program memory organization.
vissa enheter innehåller också ett andra datautrymme (upp till 65 536 adresser) som endast kan nås med hjälp av lde/LDEI-instruktioner. Detta område kan användas för att lagra mindre använda data (eftersom läsning/skrivning till det är långsammare än RAM/SFR-området).
FPz8
den första implementeringen av FPz8 använder en mycket konservativ och hårdkodad designmetod med två huvudbussar: en för programminne och en annan för registerminne. Eftersom jag valde att inte inkludera ett dataminneområde, är lde/LDEI-instruktionerna inte implementerade.
programminnesbussarna består av en 16-bitars instruktionsadressbuss (IAB), en åtta-bitars instruktionsdatabuss (IDB för att läsa data från programminnet), en åtta-bitars instruktionsdatabuss (iwdb för att skriva data till programminnet) och en pgm_wr-signal som styr skrivning till programminnet. FPz8 innehåller 16 384 byte programminne implementerat med synkron block RAM (vilket innebär att programminneinnehållet går förlorat när enheten är avstängd).
de fem registerområdesbussarna består av tre för filregisterområdet (användarram) och ytterligare två dedikerade till specialfunktionsregister. Det finns en huvud 12-bitars file register address bus (FRAB), en åtta-bitars file register input data bus (FRIDB), en åtta-bitars file register output data bus (FRODB), en åtta-bitars register input data bus (RIDB), och slutligen en åtta-bitars register output data bus (RODB) för att skriva in SFRs. FPz8 innehåller 2,048 byte av användarram-minne implementerat med synkron block RAM.
Figur 2 visar ett blockschema över FPz8; du kan se CPU, två minnesenheter (en för programlagring och den andra för datalagring), och även en extern timermodul.
figur 2. FPz8 blockschema.
Observera att jag inte använder Dubbelriktade bussar för några sammankopplingar i detta projekt. Enkelriktade bussar är enklare att använda, även om de är mindre utrymmeseffektiva.
VHDL-beskrivningen av fpz8 är stor och lite komplex, så jag kommer att dela upp sin funktion i vissa moduler för att underlätta förståelsen:
- instruktion kö motor
- instruktion avkodning
- avbryta bearbetning
- Debugger
instruktion kö Motor
hämta instruktioner är en primär uppgift för alla CPU. Fpz8: s Harvard-arkitektur möjliggör samtidig hämtning och dataåtkomst (på grund av separata bussar för instruktion och data). Det betyder att CPU kan hämta en ny instruktion medan en annan läser eller skriver i dataminnet.
eZ8 har ett instruktionsord med variabel längd (instruktionslängden varierar från en byte upp till fem byte); vissa instruktioner är långa men går snabbare än andra. På så sätt har en BRK-instruktion en längd på en byte och körs i två cykler, medan en LDX IM,ER1 är fyra byte lång och körs i två klockcykler.
så, hur kan vi framgångsrikt avkoda alla dessa instruktioner? Med en instruktionskö; det vill säga en mekanism som fortsätter att hämta byte från programminnet och lagra dem i en åtta byte-array:
if (CAN_FETCH=’1′) sedan
if (IQUEUE.FETCH_STATE=F_ADDR) sedan
FETCH_ADDR := PC;
IAB <= PC;
IQUEUE.WRPOS: = 0;
IQUEUE.RDPOS: = 0;
IQUEUE.CNT := 0;
IQUEUE.FETCH_STATE: = F_READ;
annat
om (IQUEUE.FULL= ’0’) sedan
IQUEUE.KÖ (IQUEUE.WRPOS): = IDB;
FETCH_ADDR: = FETCH_ADDR + 1;
IAB <= FETCH_ADDR;
IQUEUE.WRPOS: = IQUEUE.WRPOS + 1;
I KÖ.CNT: = IQUEUE.CNT + 1;
avsluta om;
avsluta om;
avsluta om;
om (IQUEUE.CNT = 7) sedan IQUEUE.FULL: = ’1’; annars IQUEUE.FULL:= ’0’;
avsluta om;
lista 1. Instruktion kö motor.
hämtning styrs av en huvudaktiveringssignal (CAN_FETCH) som kan inaktiveras i vissa speciella fall (avbryt bearbetning, av LDC/LDCI-instruktioner eller felsökningsåtkomst). Det finns också en struktur (IQUEUE) som lagrar flera interna parametrar (hämta tillstånd, läsa och skriva pekare, kö array själv, en räknare, och en fullständig indikator).
köräknaren (CNT) används för att identifiera antalet byte som är tillgängliga för användning (läsning) i kön. Avkodningssteget använder detta nummer för att verifiera att önskat antal byte för instruktionen redan finns i kön.
Instruktionsavkodning
det är här den faktiska magin händer. Instruktionsavkodaren läser opkoder från instruktionskön och översätter dem till motsvarande operationer.
Instruktionsavkodare design började med att räkna ut förhållandet mellan alla instruktioner och adresseringslägen. Vid första anblicken är det lätt att se att vissa instruktioner (Figur 3) är grupperade efter kolumn (DJNZ, JR cc,X, LD r1,IM, JP cc,DA och INC r1). Avkodning av en INC r1-instruktion är enkel: på dessa instruktioner med en byte anger den höga nibble käll-/destinationsregistret och den nedre nibble anger själva instruktionen (0xE).
figur 3. Opcodes av grupper.
de flesta instruktioner kan klassificeras enligt vissa grundläggande regler:
- kolumner (den nedre nibble av en opcode) anger vanligtvis ett adresseringsläge: Kolumn 0X9 instruktioner,till exempel, använder mestadels im, ER1 adresseringsläge och är fyra byte Långa (den andra byten är den omedelbara operanden och de två sista byte är destinationens utökade adress).
- rader (den högre nibble av en opkod) anger vanligtvis en operation: rad 0x0 instruktioner är mestadels additionsoperationer; rad 0x2 instruktioner är mestadels subtraktionsoperationer, och så vidare.
om vi tittar på rad 0x1 kan vi se att kolumnerna 0x0 och 0x1 är RLC-instruktioner och kolumnerna 0x2 upp till 0x9 är ADC-instruktioner. Så vi kan designa en ALU som tar en nibble som inmatning (den högre nibble från opcode) och avkodar den i enlighet därmed. Även om detta skulle fungera för kolumnerna 0x2 till 0x9, skulle vi behöva ett annat tillvägagångssätt för de två första kolumnerna.
det är därför jag slutade skriva två enheter: en ALU som koncentrerar sig på de flesta aritmetiska och logiska instruktioner; och en andra enhet (logisk enhet 2 eller LU2) som utför andra operationer som visas i kolumnerna 0x0 och 0x1 (inte alla operationer som ses på dessa kolumner utförs av LU2). Operationskoderna för både ALU och LU2 valdes för att matcha opkodrader som visas i Figur 3.
en annan viktig detalj är att alla instruktioner inom samma kolumn och grupp har samma storlek i byte, vilket kan avkodas i samma avkodningsavsnitt.
avkodardesignen använder sig av en stor finite state machine (FSM) som går framåt på varje klockmarkering. Varje instruktion börjar i CPU_DECOD stat. Det är här avkodaren faktiskt avkodar opkoderna, förbereder bussar och interna stödsignaler och steg till andra exekveringstillstånd. Bland alla dessa stater används två ofta av många instruktioner: CPU_OMA och CPU_OMA2. Kan du gissa varför? Om du sa att de är relaterade till ALU och LU2, du har helt rätt!
OMA är kort för One Memory Access och det är det sista tillståndet för alla Alu relaterade instruktioner (ADD, ADC, ADTX, ADCX, SUB, SBC, SUBX, SBCX, OR, ORX, och, ANDX, XOR, XORX, CP, CPC, TCM, TCMX, TM, TMX, och vissa varianter av LD och LDX). Å andra sidan är CPU_OMA2 det sista tillståndet för alla LU2-relaterade instruktioner (RLC, INC, DEC, DA, COM, RL, CLR, RRC, sra, SRL, RR och SWAP).
Låt oss nu ta en titt inuti cpu_decod-tillståndet. Se Figur 4.
figur 4. CPU_DECOD tillstånd.
inom CPU_DECOD-tillståndet kan vi se att mycket action äger rum. I början initieras vissa tillfälliga variabler till ett standardtillstånd. Observera att NUM_BYTES är mycket viktigt eftersom det styr hur många byte som konsumeras av instruktionsavkodaren. Dess värde används i den sista delen av detta steg för att öka datorn (programräknaren), avancera köläsningspekaren och minska antalet tillgängliga byte i kön.
efter initialiseringsavsnittet kan vi se avsnittet avbrytningsbehandling. Det är ansvarigt för att upptäcka eventuella pågående avbrott och förbereder CPU i enlighet därmed. Jag täcker detta i nästa avsnitt.
det faktiska instruktionsavkodningsblocket kontrollerar om ett lågeffektläge inte är aktivt och även om felsökningsläget är avstängt (OCDCR.DBGMODE = 0). Eller, i felsökningsläge, utfärdades ett enda steg felsökningskommando (OCDCR.DBGMODE=1 och OCD.SINGLE_STEP=1). Den kontrollerar sedan tillgängliga byte i kön och fortsätter med avkodning.
vissa instruktioner (mestadels singlebyte) är slutförda inom CPU_DECOD-tillståndet, medan andra behöver flera tillstånd tills de är helt färdiga.
Observera att vissa instruktioner avkodning kan använda sig av flera funktioner och procedurer skrivna speciellt för FPz8:
- DATAWRITE-denna procedur förbereder bussar för en skrivoperation. Den väljer om destinationen är en intern SFR, en extern SFR eller en ram-plats för användare.
- DATAREAD-detta är en ömsesidig funktion för DATAWRITE. Den används för att läsa en källadress och väljer automatiskt om det är en intern SFR, en extern SFR eller en användarram-plats.
- CONDITIONCODE-används för villkorliga instruktioner (som JR och JP). Det tar en fyra-bitars tillståndskod, testar den och returnerar resultatet.
- ADDRESSER4, ADDRESSER8 och ADDRESSER12-dessa funktioner returnerar en 12-bitars adress från en fyra-, åtta-eller 12-bitars källa. De använder innehållet i Rp-registret för att generera den slutliga 12-bitarsadressen. ADDRESSER8 och ADDRESSER12 kontrollerar också om det finns något undgått adresseringsläge.
- ADDER16-detta är en 16-bitars adder för adress offset beräkning. Det tar en åtta bitars signerad operand, sign förlänger den, lägger till den i 16-bitarsadressen och returnerar resultatet.
- ALU och LU2-dessa diskuterades tidigare och utför de flesta aritmetiska och logiska operationer.
Avbryt bearbetning
som jag sa tidigare har eZ8 en vektorerad avbrottskontroller med programmerbar prioritet. Först trodde jag att det här avsnittet inte skulle vara så svårt eftersom avbrott inte är så stora, eller hur? Tja, när jag började räkna ut hur man gör alla nödvändiga uppgifter (spara sammanhang, vektorering, hantera prioriteringar etc.), Insåg jag att det skulle vara tuffare än jag först trodde. Efter ett par timmar kom jag fram till den nuvarande designen.
Fpz8s avbrottssystem slutade vara enkelt. Den har åtta ingångar (INT0 till INT7); en global avbrottsaktivering (IRQE-bit i IRQCTL-registret); två register för prioritetsinställning (IRQ0ENH och IRQ0ENL); och ett register för avbrottsflaggor (IRQ0). Designen använder sig av en kapslad IF-kedja som genererar en vektoradress vid detektering av en avbrottshändelse angående ett aktiverat avbrott.
Figur 5 visar en komprimerad vy av avbrottssystemet. Det finns ett första IF-uttalande med en symbol ATM_COUNTER. Detta är en enkel räknare som används av ATM-instruktionen (den inaktiverar avbrott i tre instruktionscykler, vilket möjliggör atomoperationer).
figur 5. Fpz8 avbryta systemet.
en sista kommentar om avbrott: Interrupt flag register (IRQ0) prover avbryter ingångar varje stigande kant av systemklockan. Det finns också två buffertvariabler (IRQ0_LATCH och OLD_IRQ0) som lagrar flaggans nuvarande och sista tillstånd. Detta möjliggör avbrottskantdetektering och synkroniserar även de externa ingångarna till den interna klockan (FPGA fungerar inte bra med asynkrona interna signaler).
On-Chip Debugger
detta är förmodligen den coolaste funktionen i denna mjukporr eftersom det tillåter en kommersiell integrerad utvecklingsmiljö (IDE; som Zilogs ZDS-II) för att kommunicera, programmera och felsöka programvara som körs på FPz8. On-chip debugger (OCD) består av en UART med autobaud-kapacitet och en kommandoprocessor ansluten till den. UART utför seriell kommunikation med en värddator och levererar kommandon och data till felsökningstillståndsmaskinen som behandlar felsökningskommandon (felsökningskommandot FSM finns i CPU_DECOD-tillståndet).
figur 6. On-chip debugger UART (notera dbg_rx synchronizer).
min OCD-design implementerar nästan alla kommandon som är tillgängliga på den verkliga hårdvaran, förutom de som är relaterade till dataminne (felsökningskommandon 0x0C och 0x0D); read runtime counter (0x3); och read-programminnet CRC (0x0E).
en sak som jag vill lyfta fram är att försiktighet behövs när man hanterar asynkrona signaler inuti FPGA. Min första design tog inte hänsyn till det när jag behandlade dbg_rx-ingångssignalen. Resultatet var helt konstigt. Min design hade fungerat felfritt i simulering. Jag laddade ner den till en FPGA och började leka med debug serial interface med hjälp av en seriell terminal (min FPGA-kort har en inbyggd seriell USB-omvandlare).
till min förvåning, medan jag för det mesta kunde skicka kommandon och få de förväntade resultaten, ibland skulle designen helt enkelt frysa och sluta svara. En mjuk återställning skulle få saker att gå tillbaka till sin korrekta funktion, men det var spännande mig. Vad hände?
efter många tester och lite Googling, räknade jag ut att det möjligen var relaterat till de asynkrona kanterna på den seriella ingångssignalen. Jag inkluderade sedan en kaskad spärr för att synkronisera signalen till min interna klocka och alla problem var borta! Det är ett tufft sätt att lära sig att du alltid måste synkronisera externa signaler innan de matas in i komplex logik!
jag måste säga att felsökning och raffinering av felsökningskoden var den svåraste delen av detta projekt; mest för att det interagerar med alla andra delsystem inklusive bussar, avkodaren och instruktionskön.
syntetisering och testning
en gång fullständigt kompilerad (jag använde Quartus II v9.1 sp2) använde fpz8-kärnan 4 900 logiska element, 523 register, 147 456 bitar on-chip-minne och en inbäddad nio-bitars multiplikator. Sammantaget använder FPz8 80% av EP4CE6: s tillgängliga resurser. Även om det här är mycket, finns det fortfarande några 1200 logiska element tillgängliga för kringutrustning (min enkla 16-bitars timer lägger till cirka 120 logiska element och 61 register). Den passar till och med på den minsta Cyclone IV FPGA — EP4CE6 — som är den som är monterad på det billiga minikortet jag använde här (Figur 7).
figur 7. Altera Cyclone IV EP4CE6 mini styrelse.
minikortet har (tillsammans med EP4CE6-enheten): ett EPCS4-seriellt konfigurationsminne (monterat på undersidan); ett FTDI-seriellt till USB-omvandlarchip samt en 50 MHz kristalloscillatormodul; några knappar; lysdioder; och stifthuvuden för att komma åt FPGA-stift. Det finns ingen integrerad USB-Blaster (för FPGA-programmering), men paketet jag köpte inkluderade också en extern programmeringsdongle.
när det gäller den verkliga världen tester, naturligtvis, fpz8 fungerade inte första gången! Efter att ha tänkt lite och läst kompilatorutgångsmeddelanden, tänkte jag på att det förmodligen var en tidsfråga. Detta är ett mycket vanligt dilemma när man utformar med programmerbar logik, men eftersom det här var min andra FPGA-design någonsin, betalade jag inte tillräckligt med uppmärksamhet åt det.
genom att kontrollera tidsanalysmeddelandena kunde jag se en varning om att den maximala klockan skulle vara runt 24 MHz. Först försökte jag använda en divider-by-2 för att generera en 25 MHz CPU-klocka, men det var inte tillförlitligt. Jag använde sedan en divider-by-3. Allt började fungera perfekt!
det är därför FPz8 körs för närvarande vid 16.666 MHz. Det är möjligt att uppnå högre hastigheter genom att använda en av de interna PLL: erna för att multiplicera/dela huvudklockan för att få en resulterande klocka lägre än 24 MHz, men högre än 16.666 MHz.
programmering och felsökning
använda FPz8 är mycket enkel och okomplicerad. När designen har laddats ner till FPGA börjar CPU att köra alla program som laddas i minnet. Du kan leverera en hex-fil och använda MegaWizard Plug-In Manager för att ändra programminnesinitieringsfilen. På så sätt börjar din applikationskod springa efter en återställningssignal.
du kan använda Zilog ZDS-II IDE för att skriva montering eller C-kod och generera nödvändiga hex-filer (jag brukar välja Z8F1622 som min målenhet eftersom den också har 2 KB RAM och 16 KB programminne). Tack vare on-chip debugger är det också möjligt att använda ZDS-II IDE för att ladda ner kod till FPz8 med en seriell debug-anslutning (USB, i vårt fall).
innan du ansluter, se till att felsökningsinställningarna är desamma som i Figur 8. Avmarkera alternativet ” Använd sidradering innan du blinkar ”och välj” SerialSmartCable ” som det aktuella felsökningsverktyget. Glöm inte att också kontrollera om FTDI: s virtuella COM-port är korrekt vald som felsökningsport (använd inställningsknappen). Du kan också ställa in önskad kommunikationshastighet; 115 200 bps fungerar mycket bra för mig.
figur 8. Debugger inställningar.
Observera att när du ansluter till FPz8 kommer ZDS-II IDE att visa ett varningsmeddelande som informerar dig om att målenheten inte är densamma som projektet. Det händer eftersom jag inte implementerade några ID-minnesområden. Ignorera bara varningen och fortsätt med felsökningssessionen.
när koden har laddats ner kan du starta programmet (GO-knappen), steginstruktioner, inspektera eller redigera register, ställa in brytpunkter etc. Som med alla andra bra debugger kan du till exempel välja PAOUT-registret (under PORTS group) och till och med ändra tillståndet för de lysdioder som är anslutna till PAOUT.
några enkla C-kodexempel finns i nedladdningarna.
tänk bara på att FPz8 har ett flyktigt programminne. Således förloras något program som laddas ner till det när FPGA stängs av.
stängning
detta projekt tog mig ett par veckor att slutföra, men det var härligt att undersöka och designa en mikrokontroller kärna.
jag hoppas att detta projekt kan vara användbart för alla som vill lära sig om beräkningsgrunder, mikrokontroller, inbäddad programmering och/eller VHDL. Jag tror att FPz8 — om det är parat med en låg kostnad FPGA-styrelse-kan ge ett fantastiskt lärande (och undervisning) verktyg. Ha så kul! NV
CEFET-PR:
www.utfpr.edu.br
ScTec:
www.sctec.com.br
HCS08 Unleashed:
https://www.amazon.com/HCS08-Unleashed-Designers-Guide-Microcontrollers/dp/1419685929
Zilog eZ8 CPU Manual (UM0128):
www.zilog.com/docs/UM0128.pdf
Zilog Z8F64xx Product Specification (PS0199):
www.zilog.com/docs/z8encore/PS0199.pdf
Zilog ZDS II IDE User Manual (UM0130):
www.zilog.com/docs/devtools/UM0130.pdf
Zilog ZDS-II Software Download:
https://www.zilog.com/index.php?option=com_zcm&task=view&soft_id=7&Itemid=74
Zilog Microcontroller Product Line:
http://zilog.com/index.php?option=com_product&task=product&businessLine=1&id=2&parent_id=2&Itemid=56
Project Files available at:
https://github.com/fabiopjve/VHDL/tree/master/FPz8
FPz8 at Opencores.org:
http://opencores.org/project,fpz8