Gå til innhold

ZeRKoX leker med AVR


Anbefalte innlegg

Med inspirasjon fra tråden til CoolBeer så tenker jeg at jeg skal prøve meg på ca det samme.

 

Denne tråden tenker jeg derfor å bruke til å poste litt ulike prosjektet jeg sysler med. Det vil nok bli litt diverse prosjekter som kommer men primært blir det elektronikkprosjekter som er basert på mikrokontrollerene til Atmel (AVR).

 

Jeg poster nok litt kodeeksempler og slikt underveis, til de som synes slikt er moro/nyttig. Kommentarer og tilbakemeldinger på hva jeg gjør er jo og alltid kjekt og nyttig, så det er bare å poste i vei om dere ønsker det.

 

Jeg har igrunn synes at elektronikk og slikt har vært spennende siden jeg var veldig liten. (En eldre bibletokar hjemme snakker enda om at jeg som 10-åring lånte elektronikkbøker på biblioteket... :) Jeg var ikke så flink til å levere de tilbake, men det betydde ikke så mye, for jeg var den eneste som lånte de. Merkelig nok. Barn... :p), og har for et godt år siden oppdaget magien med mikrokontrollere.

 

Samtidig som jeg leker litt med elektronikk jobber jeg som lydtekniker (der elektronikkkunskap faktisk er nyttig) og er student. Programmeringsbakgrunnen min er kun det jeg har lært gjennom totalt 30 studiepoeng på skolen, så veldig god å programmere er jeg ikke, men jeg pleier å få ting til å virke slik jeg vil :)

Jeg kommer primært til å prøve å skrive objektorientert kode. Jeg er også lite flink til å sjekke hva som finnes der ute, som folk allerede har laget. Dette er fordi jeg er av typen som "kan selv", og derfor vil skrive selv. Noen av prosjektene kan derfor fint finnes fra før, men det bryr jeg meg ikke så mye om.

 

Nå i starten ser jeg for meg relativt grunnleggende ting, siden jeg trenger å lære mye grunnleggende om mikrokontrollerene og hvordan de oppfører seg. Men det vil nok bli innskudd av mer avanserte ting etterhvert.

Lenke til kommentar
Videoannonse
Annonse

Trykknapper:

Når du bygger kretser, så vil trykkknapper være veldig praktisk. Hva er vel en krets som ikke kan påvirkes ved å trykke på knapper?

Knapper bør jo i teorien være en enkel ting. Den har to stillinger, av eller på. Så å få en utgang til å toggle (gå fra lav til høy, eller motsatt) når en knapp trykkes inn burde kunne ordnes slik: (PORTB, pin1 = knapp, pin2 = LED)

 

DDRB = 0x02;	//Sette LED som utgang
PORTB = 0x01;   //Slå på pullup-motstand for bryteren
for(; { // Hoved-program loop
if(!(PINB & 0x01))
	PORTB ^= 0x02;
}

 

Problemet her blir jo at lysdioden kommer til å blinke (_veldig_ kjapt) så lenge knappen er inntrykka, og det blir derfor veldig random om lysdioden blir værende på eller av. Med mindre du klarer å trykke inn og slippe knappen i løpet av et par mikrosekunder da... (Jeg bruker en hårsbredd mer enn 2-3 uS for å trykke på en knapp...)

Dette kan løses ved å lage en sjekk som lagrer i en variabel at det er registrert at knappen er trykket, og ved slipp av knapp resettes denne variabelen. Da kan vi unngå å oppleve mange trykk for hvert trykk på knappen.

 

Et annet problem er det som på godt norsk kalles "bouncing", og dette løses med det kryptiske navnet "debouncing".

Kort fortalt, bouncing er et fenomen som sker når du trykker på en knapp, som gjør at knappen ikke går rett fra av til på, men i overgangen der hopper den litt frem og tilbake, før den bestemmer seg for å være på. Dette skjer så fort at vi mennesker ikke har sjans til å oppdage det, men en mikrokontroller, som kan sjekke om knappen er trykket mange millioner ganger i sekundet, vil definitivt oppdage det.

 

Et bilde fra et ocilloskop som viser hva som skjer når en knapp blir trykket:

debounce.png

 

Som dere ser, så vil det i overgangen kunne gå fra av-på-av-på-av-på-av før den tilslutt bestemmer seg for å være på. For å unngå dette må vi lage litt kode som sjekker status på knappen flere ganger over tid, og basert på dette avgjør om knappen er av eller på.

 

Nå begynner det å lese av statusen på en knapp å bli så avansert at det er kjedelig å skrive denne koden for hver gang jeg skal lese av knappen. I objektorienteringens ånd bestemte jeg meg for å lage en klasse som kan gi meg et litt lettere interface å jobbe med, og som tillater meg å lage så mange knapper jeg vil (les: så mange det er pinner på mikrokontrolleren til) uten å måtte duplisere hauger med kode. Jeg endte derfor opp med følgende klasse:

 

#ifndef __BUTTON_H
#define __BUTTON_H
class Button {
private:
	char port;
	volatile uint8_t data;
	// databit 0 = not_pressed/pressed (0/1)
	// databit 1 = not_changed/changed (0/1)
	// databit 2-4 = ticks since last change (0-7)
	// databit 5-7 = pin-number (0-7)
public:
	Button();					   //Constructer which should not be used
	Button(char port, uint8_t pin); //Constructor
	uint8_t get_state();	//Returns the switch's state. 1 = clicked (LOW), 0 = not clicked (HIGH)
	uint8_t changed();	  //Returns a boolean telling if the state has changed since last check.
	uint8_t pressed();	  //Returns a boolean telling if the switch has been pushed since last check.
	uint8_t released();	 //Returns a boolean telling if the switch has been released since last check
	void tick();			//Updates the "data" variable. Should be run regulary. Preferebly by an ISR, @~100hz.
};
#endif

 

Jeg lagrer to byte med data til hver knapp. Hva som lagres skriver jeg litt om her:

Det jeg trenger å vite om en knapp for å kunne bruke den er i utgangspunktet hvilken port den står på. I tillegg er det greit å vite pin-nummeret til knappen.

Portnavnet lagrer jeg som en stor bokstav (A, B, C eller D) i en char variabel. Siden portene har 8 pinner, trenger jeg 3 bit for å lagre den verdien (0-7). Da velger jeg de tre høyeste bit-ene i en 8-bit int. (En AVR har ofte lite minne, så greit å ikke sløse...)

I tillegg trenger jeg å vite hva som er status på knappen sist den ble sjekka, og om status har endret seg siden sist gang noen sjekka den. Da har vi 2 bit til. De siste 3 bit-ene i "data" variabelen bruker jeg som en teller, som gjør at bryteren må ha vært trykket inn en stund (5ms) før den blir registrert som trykket. Da får jeg fiksa debouncingen og

 

Knappene har også litt funksjoner som følger med. En constructor som har ansvar for å sette ting rett opp (sette pin til input, og dra høy pinnen med en pull-up). Det er også noen funksjoner du kan bruke for å sjekke om knappen er trykket eller ikke.

 

Til slutt er den en funksjon som jeg kalla "tick". Formålet med denne er å jevnlig sjekke status på knappen, og dersom status endrer seg, og er stabil over lengre tid lagres dette, slik at funksjonene jeg bruker for å si om ting har skjedd returnerer rett verdier.

 

Bruk av klassen blir f.eks slik:

#include <avr/io.h>
#include "button.h"
Button knapp('B', 0);  //PortB, pin 0 har en knapp.
inf main() {
DDRB |= 1 << 1;		//PortB, pin 1 har en LED
//Diverse vas for å sette opp timed interrupt her....

for(; {
	if(knapp.pressed()) {
		PORTB ^= 1 << 1;
	}
}
}
ISR(TIMER0_COMPA_vect) {
knapp.tick();
}

 

Tilslutt limer jeg bare inn selve koden til klassen, for de som er interessert:

 

#include <avr/io.h>
#include "button.h"
// Constructor
Button::Button(char port, uint8_t pin) {
	this->data = 0;				 //Initialize the datavariable
	this->data |= (pin << 5);	   //Store the pin-number
	//Save the portletter as uppercase
	if(port >= 'a' && port <= 'd') port -= 32;
	this->port = port;
	//Initialize the pin. Set it as output, and enable pullup
	// The pre-processor directives assures that this code would
	// run avr-s with and without all the ports.
	switch (this->port) {
	#ifdef PORTA
	case 'A':
			PORTA |= 1 << pin;
			DDRA &= ~(1 << pin);
			break;
	#endif
	#ifdef PORTB
	case 'B':
			PORTB |= 1 << pin;
			DDRB &= ~(1 << pin);
			break;
	#endif
	#ifdef PORTC
	case 'C':
			PORTC |= 1 << pin;
			DDRC &= ~(1 << pin);
			break;
	#endif
	#ifdef PORTD
	case 'D':
			PORTD |= 1 << pin;
			DDRD &= ~(1 << pin);
			break;
	#endif
	};
}
//Returns the switch's state. 1 = clicked (LOW), 0 = not clicked (HIGH)
uint8_t Button::get_state() {
	return (this->data & 0x01);
}
//Returns a boolean telling if the state has changed since last check.
uint8_t Button::changed() {
	if(this->data & 0x02) {		 //If it is changed
			this->data &= ~(0x02);  // Clear the changed_flag
			return 1;
	} else {
			return 0;
	}
}
//Returns a boolean telling if the switch has been pushed since last check.
uint8_t Button::pressed() {
	if((this->data & 0x01) && (this->data & 0x02)) { //If it is pressed, and it is changed since last check
			this->data &= ~(0x02);				  // Clear the changed_flag before return.
			return 1;
	} else {
			return 0;
	}
}
//Returns a boolean telling if the switch has been released since last check
uint8_t Button::released() {
	if(!(this->data & 0x01) && (this->data & 0x02)) { //If it isn't pressed, and it is changed since last check
			this->data &= ~(0x02);				  // Clear the changed_flag before return.
			return 1;
	} else {
			return 0;
	}
}
//Updates the "data" variable. Should be run regulary. Preferebly by an ISR, @~100hz.
// Performs debouncing etc.
void Button::tick() {
	uint8_t pressed;

	//Read the correct port and pin, and check if the button is pressed (LOW) or not.
	switch (this->port) {
			#ifdef PORTA
			case 'A':
					pressed = (PINA & (1 << (this->data >> 5))) ? 0 : 1;
					break;
			#endif
			#ifdef PORTB
			case 'B':
					pressed = (PINB & (1 << (this->data >> 5))) ? 0 : 1;
					break;
			#endif
			#ifdef PORTC
			case 'C':
					pressed = (PINC & (1 << (this->data >> 5))) ? 0 : 1;
					break;
			#endif
			#ifdef PORTD
			case 'D':
					pressed = (PIND & (1 << (this->data >> 5))) ? 0 : 1;
					break;
			#endif
			default:
					return;
	};
	//If the button is pressed, and the state flag is set, or not pressed/not set:
	if(pressed == (this->data & 0x01 != 0)) {
			this->data &= ~(0x1C);				  //Clear the "delay" bit of the data.
	//If the button and flag mismatch (a very recent change)
	} else {
			pressed = (this->data & 0x1C) >> 2;	 //Extract the delay data
			pressed++;							  //Increment it
			this->data &= ~(0x1C);				  //Clear the delay from the dataset
			this->data |= (pressed << 2);		   //And insert the new delay to the dataset
	}
	//If the delay is grater or equal to 5
	// (The switch has had the same state for 5 ticks)
	if((this->data & 0x1C) >> 2 >= 5) {
			this->data &= ~(0x1E);  // Clear all the data, except for pin-info and state.
			this->data |= 0x2;	  // Set the "changed" flag
			this->data ^= 0x1;	  // Toggle the "state" flag
	}
}

Lenke til kommentar
  • 9 måneder senere...

Operativsystemer:

Jeg støter stadig borti utfordringer når det kommer til å multitaske på en AVR. Å skrive programmer som gjør en ting, og som kanskje har ett eller to interrupt på en timer som gjør litt småting, er liksom ganske lett. Men så snart vi kaster inn litt LCD-skjermer, litt RS232, knapper og whatnot, så begynner det å bli vanskelig å få ting til å skje "samtidig". Løsningen på dette problemet blir ett operativsystem, som lar oss kjøre forskjellige oppgaver.

 

 

La oss begynne med litt teori.

En prosessor kan av natur ikke multitaske. Den gjør en ting om gangen. For å skape en illusjon om at det skjer flere ting samtidig på en prosessor, kan vi utnytte teknikker som bytter mellom oppgaver mange ganger i sekundet. Det finnes i prinsippet to måter å gjøre dette:

  • Co-operative - Et co-operative system forutsetter at alle oppgavene som kjører er klar over at de må la andre prosesser kjøre, så fra tid til tid returnerer disse til operativsystemet, slik at en annen oppgave kan få kjøre litt. Oppgavene må da selv sørge for å lagre hvor de tar pause, slik at neste gang de får kjøre, kan de fortsette der de slapp.
  • Preemptive - Et preemtive operativsystem sørger selv for å fra tid til tid stoppe en kjørende oppgave, og så laste en annen oppgave for å la denne få kjøre litt. Da er det operativsystemet sitt ansvar å lagre hvor i programmet en oppgave er, og all inormasjon denne oppgaven trenger for å kjøre. Oppgavene (les: prosessene) vet selv ikke om at det finnes andre prosesser enn dem selv. De vet ikke en gang at de blir stoppet nå og da.

Alle moderne operativsystemer i dag beregnet på PC-er er preemtive. Dette er fordi operativsystemet da kan sørge for å "rettferdig" dele cpu-tiden mellom programmer. I tillegg kan programmer skrives uten å måtte tenke på at de må ta pauser nå og da for å la andre programmer kjøre. Bakdelen med preemtive operativsystemer er at de ofte er mer komplekse å skrive enn ett co-operativt system. Så for eksempel til mikrokontroller ender vi som regel opp med å lage en eller annen form for co-operativ oppgavestyring.

 

Om det er spesifikke deler av "operativsystemer" dere vil at jeg skal skrive litt om/demostrere bruken av, så skrik gjerne ut, så kan jeg se hva jeg får gjort :).

 

Over til AVR-ene:

Jeg har lyst å lage meg ett system som er lite, ikke veldig omfattende, og som lett lar meg kaste sammen prosjekter for å teste/leke med forskjellige kretseer. Jeg vil og at kode som er skrevet til å kjøre på en AVR direkte, uten noe operativsystem, skal være mulig å inkludere i et prosjekt som bruker operativsystem. Derfor er ett co-operativt system utelukket.

 

Med litt søking, så ser du fort at det er flere enn meg som har lyst på et operativsystem på en AVR. To prosjekt som er verdt å nevne er freeRTOS, og femtoOS. Fellesnevneren for disse er at de etterhvert er blitt ganske store prosjekter, og en del jobb å sette opp/sette seg inn i. I tillegg så synes jeg at jeg lærer lite av å bruke andre sin kode. :)

 

Derfor har jeg begynt jobben med å skrive ett co-operativt system som kjører på AVR-brikker. Dette kommer primært til lages for å være lettfattelig, og lærerikt. Ikke så mye for å optimalisere minnebruk og kodestørrelse. Du vil derfor slite med å kjøre det på en Attiny. Men det skal være lett å porte/sette opp på en hvilket som helst AtMega. Fordelen blir at koden skal være lettlest, og lett å forstå. (Forhåpentligvis) :)

 

La oss begynne å kikke på kode. En prosess har jeg definert til å være ett "frittstående" program, som har egen stack (minneområde til lokale variabler), og eget kjøremiljø (prosessorregistre). En prosess skal være mulig å starte/stoppe, og vi skal kunne gi en prosess høyere prioritet enn en annen. Jeg har derfor følgende "struct" for å samle informasjonen om en prosess:

 

struct kernelProcess {
	uint8_t running;		// Is the process running? 0=NO, 1=YES
	uint16_t sleep;  // The number of ticks befor this process can run again
	uint8_t runValue;	   // (Priority << 4) + no_ticks waiting to run.
	uint8_t priority;	   // Priority. 1 == very low, 15 = very high
	uint8_t initialized;	// 0 = no, 1 = allocated stack, 2 = ready to run
	uint8_t* stack;  // Pointer to the bottom of the stack
	uint16_t stacksize;	 // Size of the allocated stack, in bytes
	uint16_t stackpointer;  // Stackpointer. Initialized at the top, and works its
							//   way down when used.
	int (*main) (void);	 // Pointer to this processes main function
	int returnValue;		// Value returned, if this main returns.
};

 

Som du kanskje ser, så har jeg i tillegg til en "priority" noe jeg kaller for "runValue". Denne har som oppgave å gi en prosess høyere prioritet, dersom den ikke har fått kjøre på en stund. For hver "tick" økes denne med 1. Grunn til at denne også blandes med prioriteten er at vi ikke skal stoppe en oppgave helt, selv om det er en oppgave med høyere prioritet som konstant bruker CPU-en. Hver gang en oppgave får litt CPU-tid resettes denne verdien til prioritet * 16.

 

Resultatet av dette gjør at dersom vi har to oppgaver (med hhv. prioritet 4 og 5) som begge krever så mye CPU-tid de kan få, så vil den med høyest prioritet få kjøre 16 ganger så mye som den som har lavere prioritet. Det er mulig dette systemet endres i fremtiden, men frem til nå synes jeg det virker ganske bra.

 

Men hvordan virker egentlig et bytte mellom to prosesser? Hvis du er som meg, og stort sett kun har skrevet enkle C-programmer til en mikrokontroller, så er det vanskelig å se for seg koden til å stoppe en oppgave, og starte en annen. For å gjøre dette, så trenger du nemlig litt assembly. Du må nemlig manuelt lagre alle registrene som tilhører en oppgave til oppgavens's stack, før du lagrer unna stackpointeren til denne oppgaven. Deretter henter du en annen stackpointer (til oppgaven du nå vil kjøre) slik at du i prinsippet bruker en stack i ett annet område av minnet. Da kan du hente av alle registrene fra stacken du nå står på, som da er registrene som tilhører den nye oppgaven.

For en mye bedre forklaring, (dog på engelsk), så anbefaler jeg å lese litt HER.

 

I kode, ser byttet ca slik ut:

// Save the processor state to the stack:
asm volatile ("push r0");
asm volatile ("push r1");
asm volatile ("push r2");
asm volatile ("push r3");
asm volatile ("push r4");
  .
  .  Fjernet noen linjer for lesbarhet. Kikk på linja over og under, så klarer du
  .  nok å tippe hva som er fjernet... 
  .
asm volatile ("push r30");
asm volatile ("push r31");
asm volatile ("in r0, 0x3B");
asm volatile ("push r0");
asm volatile ("in r0, 0x3F");
asm volatile ("push r0");
// Save the stackpointer:
processes[runningPid].stackpointer = SP;


//
// Her trenger vi kode som velger hva som skal være neste prosess som får kjøre,
// og lagrer id på denne inn i variabelen "runningPid"
//


// Restore new stackpointer:
SP = processes[runningPid].stackpointer;
asm volatile ("pop r0");
asm volatile ("out 0x3F, r0");//SREG
asm volatile ("pop r0");
asm volatile ("out 0x3B, r0");//RAMPZ
asm volatile ("pop r31");
asm volatile ("pop r30");
  .
  .
  .
asm volatile ("pop r4");
asm volatile ("pop r3");
asm volatile ("pop r2");
asm volatile ("pop r1");
asm volatile ("pop r0");
asm volatile ("reti");

 

"asm volatile ("ettellerannetfancy");" betyr at kompilatoren skal sette inn assemblykoden "ettellerannetfancy" når den kompilerer programmet. Vi må gjøre det slik siden det ikke finnes noen måte i C til å lagre unna/laste inn prosessorregistrene. Henger du enda med?

 

Koden over virker fint til å bytte mellom to prosesser som allerede kjører. Helt til slutt i koden har vi instruksjonen "reti", som returnerer til forrige funksjon. Siden vi har byttet stack, betyr det den funksjonen som kjørte sist gang prosessen som eide denne stacken kjørte.

 

Men hva gjør vi dersom vi vil starte en ny prosess? Da har vi sannsynligvis en tom stack, som ikke inneholder prosessorregistre, eller en returverdi slik at vi kan returnere til noe kode. Jeg har løst dette med å legge inn en sjekk før jeg laster inn prosessen (siste halvdel av koden over) som ser om stacken er tom, eller om prosessen har kjørt før. Dersom prosessen har kjørt før, kan vi laste inn koden fra eksempelet over. Dersom den ikke har kjørt før, gjør jeg følgende:

 

processes[runningPid].initialized = 2;
asm volatile ("lds r0, t0_PCL");
asm volatile ("push r0");
asm volatile ("lds r0, t0_PCH");
asm volatile ("push r0");

 

Jeg bruker variabelen "initialized" som en indikator på at en prosess er initialisert. Siden jeg nå initialiserer prosessen, så setter jeg denne som 2. Deretter legger jeg adressen til funksjonen som er ansvarlig for å starte en prosess på stacken, slik at når jeg senere kaller "reti", så returnerer jeg til den funksjonen.

 

Hva er t0_PC*? Enkelt og greit, så har jeg hentet adressen på en c-funksjon slik:

 

t0_PCL = (uint8_t)((uint16_t)(*_kernelStartProcess) % 256);
t0_PCH = (uint8_t)((uint16_t)(*_kernelStartProcess) >> 8);

 

Funksjonen "_kernelStartProcess()" er forholdsvis enkel, og ser slik ut:

void _kernelStartProcess() {
	processes[runningPid].returnValue = processes[runningPid].main();
	kernelStopProcess(runningPid);
	kernelYield();
}

 

Alt det den funksjonen gjør, er å kalle funksjonen som "main" oppi prosess-info structen peker på. Dersom den av en eller annen grunn skulle finne på å returnere, så lagres returverdien i prosess-info structen, før prosessen stoppes, og vi ber operativsystemet om å bytte til en annen tråd (Yield).

Nå har vi grunnstenene av ett operativsystem klart. Det vi mangler er å jevnlig bytte hvilken oppgave som kjører. Dette er så enkelt som å sette opp en av AVR-brikken's timere til CTC-mode, og sette opp ett interrupt som kaller os-et sin Yield funksjon.

Koden for dette ser slik ut:

 

//Initialize timer0 to 1khz CTC for the OS-Tick
TCCR0A |= (1 << WGM01);
TIMSK0 |= (1 << OCIE0A);
OCR0A = 250;
TCCR0B = (1 << CS01) | (1 << CS00);

 

og:

 

ISR(TIMER0_COMPA_vect) {
	_kernelYield(0);
}

 

Hvorfor en 0 som parameter på funksjonen? For å kunne skille mellom oppgavebytter som skjer pga. interrupt fra manuelle oppgavebytter. Funksjonen for manuelle oppgavebytter ser nemlig slik ut:

 

void kernelYield(void) {
	_kernelYield(1);
}

 

Nå har vi sett på en hel del kode, og fryktelige mengder teori. Det finnes mange steder på Internett fra før, men jeg har aldri sett ett fullt eksempel på kode som kan kompileres og kjøres før.. La oss derfor nå til slutt se på ett eksempel av fungerende kode.

 

På følgende link finner du en tar-ball som inneholder kodefiler, og en makefile, for å kompilere og overføre koden til en AVR, så du selv kan se at det virker:

http://filer.rothaug...Alpha0.1.tar.gz

 

Koden er på totalt 485 linjer, fordelt på 3 filer. Det skal nevnes at det er mye kommentarer der, så det er nok nærmere 300 linjer som faktisk er kode. Jeg håper og tror at du synes det er interessant å få ett så lite eksempel, slik at det er mulig å forstå hva som skjer :)

 

Forklaring av eksempelet finner du i "main.cpp" oppi tarballen. Og om du lurer på noe, så er det lov å spørre :)

Lenke til kommentar

Minnehåndtering

 

En av oppgavene til ett operativsystem er å administrere minnet. La oss kikke på hva som er i minnet på en standard AVR, dersom det ikke er noe operativsystem innblandet:

 

malloc-std.png

 

Vi kan i all hovedsak dele minnet opp i 4 områder:

  • .data: Dette området inneholder alle statiske variabler (f.eks tekststrenger) og globale variabler som er initialisert.
  • .bss: Dette området inneholder alle globale variabler som ikke er forhåndsinitialisert.Grunnen for at dette er ett seperat område i forhold til .data, er at initialiserte variabler trenger verdien lagret i programminnet (og tar derfor flashspace), mens uinitialiserte variabler er det nok å lagre total størrelse av, for å få reservert stort nok område.
  • heap: Dette er området som dynamisk tildeles vha. funksjonen "malloc". Det er område som kan allokeres og frigis etter behov, slik at funksjoner ikke trenger å reservere store bufre, uten at de egentlig trenger de. Etterhvert som dette området vokser, vokser det oppover mot høyere minneadresser.
  • stack: Stacken brukes til å lagre lokale variabler, og til å lagre returadresser fra funksjonskall. Hver gang du kaller en funksjon, lagres adressen programkoden er på, før programmet hopper til funksjonen. På den måten kan programmet finne tilbake til hvor det var når funksjonen returnerer. Stacken starter i toppen av minnet, og vokser nedover, mot heapen. Det er et eget register som hele tiden peker på toppen av stacken (laveste adresse), slik at programmet hele tiden vet hvor på stacken den er. Dette registeret er kalt en "stack-pointer"

Legg merke til at dersom stacken eller heapen blir for stor, så vil de overskrive hverandre. Resultatet av dette blir, spennende :) avr-libc sin versjon av malloc prøver å unngå dette, med å ikke allokere minneområder som er for nærme stack-pointeren.

 

Dette er ganske rett frem dersom det er en oppgave som kjører, eventuelt med litt interruptrutiner, men de fungerer ca som ett funksjonskall, så det blir ikke en egen tråd utav det.

 

Men, når vi kjører ett preemtive operativsystem, så vet vi ikke når prosesser blir byttet, og vi må derfor la prosessene ha sitt eget minneområde. data/bss er statiske, så de kan deles mellom prosessene. Heapen er tildelt i blokker, til den som spør etter den, og vil derfor deles ut til prosessene automatisk. Så den delen av minnet vi må dedikere til hver enkelt prosess er stacken.

I første versjon av operativsystemet brukte jeg avr-libc sin versjon av malloc til å allokere områder for stacken til hver prosess. Dette funker helt fint om det gøres fra idle-thread-en, siden den har sin stack i toppen av minnet. Men, siden avr-libc har lagt inn den beskyttelsen som ikke gir ut minne om det ikke har en viss avstand til stackpointeren, så vil den ikke virke når stacken ligger i ett allokert minneområde. Vi må derfor skrive egen malloc, slik at den virker fra alle prosesser.

 

La oss snakke funksjonsdesign. Vi vil ha to funksjoner; "malloc" som skal allokere minne, og free() som skal frigi tidligere allokert minne. Vi må ha en måte å lagre hvilke deler av minnet som ikke er allokert, og vi trenger ett sted å notere hvor store allokeringer er, til da vi senere skal frigi dem. Utfordringen blir hvordan vi kan lagre dette, med minst mulig minneavtrykk. Siden vi allerede har lite minne, vil vi ikke sløse det bort på store lister over allokerte/ledige minneområder. Trikset avr-libc bruker, og som også jeg vil bruke er en linket liste, som befinner seg i det ledige minneområdet. I starten av hvert område legger vi en struct, som inneholder størrelsen på området, og adressen til neste område. Slik kan vi holde styr på området som ikke er i bruk, uten å bruke minne :)

For å lagre hvor store minneområder er, lagrer vi størrelsen på minneområdet på de to bytene som er rett før det allokerte området. F.eks: dersom du kaller malloc(10), reserveres det ett 12byte stort område. De to første bytene brukes til å størrelsen på allokeringen. Deretter returneres adressen til byte 3.

 

Skaper det mening? Spør om jeg er så dårlig å forklare, at du ikke forstår :)

 

Til listen over ledige minneområder, bruker jeg følgende konstruksjon:

struct OosFreeList {
uint16_t size;
OosFreeList* next;
}

 

Denne konstruksjonen kan også brukes til å lagre størrelsen på minneallokeringer. Når vi allokerer minne, lagrer vi størrelsen i "size". Deretter kan vi bare returnere adressen til next.

 

Malloc er implementert slik:

void* malloc(uint16_t size) {
OosFreeList* p = heapStatus->freeList, *best = 0;
// We need to make sure nothin unforseen happens while
//   working with memory allocation, as we are modifying
//   the freeList. Interrupts is therfore turned off.
unsigned char _sreg = SREG;
cli();
// To prevent too small holes, use a minimum size of 4
if(size < 4) size = 6;
else size += 2;
//Traverse trough the freeList, and find the best hit.
while(p) {
 if(p->size >= size) {
  if(!best || best->size >= p->size) {
best = p;
  }
 }
 p = p->next;
}
// If we found a suitable match in the freeList:
if(best) {
 //If the best area is more than 6 bytes larger than needed:
 if(best->size > size + 6) {
  //Slize the area into two. The last past is the one wee
  //  need, and the first part should bee the rest.
  best->size -= size;
  // Put a pointer to the part we need
  p = (OosFreeList*)(((char*)best) + best->size);
  // Marks it size;
  p->size = size;
 //If the best area is a near perfect hit, and the first item in
 //  the freelist, make the list start at the next item, and
 //  return the first item.
 } else if(best == heapStatus->freeList) {
  heapStatus->freeList = heapStatus->freeList->next;
  p = best;
 //If the best area is ~  perfect, but not in the start of the list
 } else {
  //Search for the item right before the best item
  p = heapStatus->freeList;
  while(p->next != best) p = p->next;
  // When found, make this item skip the best item, and
  //   return the pointer.
  p->next = best->next;
  p = best;
 }
 heapStatus->freeBytes -= size;
//If we did not find a suitable match
} else {
 //Get a pointer to the very end of the heap
 p = (OosFreeList*)(((char*)heapStatus) + heapStatus->totalSize);
 //Set this area's size
 p->size = size;
 //Extend the heap
 heapStatus->totalSize += size;
}
SREG = _sreg;

return (void*)&p->next;
}

 

Free er implementert slik:

void free(void* address) {
	OosFreeList* p = (OosFreeList*)((char*)address - sizeof(uint16_t));
	OosFreeList *q, *r;
	// If someone  tries to free an invalid address, do nothing
	if(address <= &heapStatus)
			return;
	//If the freelist is empty, just put this entry as the list.
	if(heapStatus->freeList == 0) {
			p->next = 0;
			heapStatus->freeList = p;
			heapStatus->freeBytes += p->size;
	} else {
			q = heapStatus->freeList;
			r = q->next;
			while(r != 0 && r < p) {
					q = r;
					r = r->next;
			}
			//Attach the area in the right place of the list
			if(p < q) {
  heapStatus->freeList = p;
  p->next = q;
  r = q;
 } else {
  p->next = r;
  q->next = p;
 }
			heapStatus->freeBytes += p->size;
			//If this area are right before another freeList-entry, merge
			//  them.
			if(r != 0 && (OosFreeList*)(((char*) p) + p->size) == r) {
					p->size += r->size;
					p->next = r->next;
			}
			//If this area is right after another freeList-entry, merge
			//  them.
			if((OosFreeList*)(((char*) q) + q->size) == p) {
					q->size += p->size;
					q->next = p->next;
			}
	}
}

 

Og helt til slutt bonusen, en link til ett fungerende eksempel der jeg demonstrerer at malloc fungerer, til å starte en prosess fra en annen prosess.

 

http://filer.rothaug...Alpha0.2.tar.gz (NB! Dette eksempelet er til en atmega1284p. Men det er ingenting i veien å bruke en atmega328p,du må bare endre en linje i makefile-a. Pass og på at du kobler shiftregistrene rett sted. Jeg har de koblet til PORTB pinne 4/5 og 6/7. Om du kobler de ett annet sted, bytt det i main-funksjonene til oppgavene.)

 

Nevner til slutt at jeg fom. denne versjonen har strukturert koden til operativsystemet i mapper. Ting er delt opp i følgende mapper:

  • drivers - Skal i fremtiden brukes til kodefiler som har med drivere til diverse hardware.
  • oos - Kodefiler som har med operativsystemets kjernefunksjonalitet. Til nå har vi "kernel.cpp/.h" for selve scheduleren/context-switcheren, og vi har "memory.h/.cpp" for malloc/free.
  • util - Diverse verktøy/utilities. Kommer tilbake til denne senere.
  • obj - Objektfiler, brukt av kompilatoren når den kompilerer koden. Greier å stappe de i en mappe, enn å ha de flytende rundt ved resten av kodefilene :)

Lenke til kommentar
  • 4 måneder senere...

Objektorientering!

Jeg innså at det å påstå at jeg skal skrive objektorientert kode, men så å starte å skrive et operativsystem, der kjernen, minnehåndtering, prosesser og lignende ikke er objektorientert ble for dumt. Jeg har derfor begynt sånn halvveis fra scratch, og skrevet hele operativsystemet objektorientert.

Noen vil nok kalle meg gal som drar inn objektorientering, med arv, polimorfisme mm på en mikrokontroller, og for all del, jeg kan jo være enig i at det kanskje er overengineering, overkill, og ikke minst litt ineffektivt. Men på en annen side, så vil jeg påstå at avr-gcc sine optimaliseringer er veldig bra, så så ineffektivt blir det ikke. I tillegg vil jeg påstå at objektorientert kode generellt sett er pen, og lettere å vedlikeholde. Jeg skriver og dette prosjektet for å lære, så om det blir ineffektivt, then so be it. :)

 

Selve kjernen på operativsystemet er nå en klasse med det klingende navnet "Kernel". Denne inneholder en array med pekere til prosessobjekter, som i praksis er prosesstabellen. Klassen "Kernel" inneholder funksjoner for å starte og drepe prosesser, i tillegg til funksjoner for å få ulik statusinfo, som minnebruk, eller cpu-bruk. Klassen inneholder også funksjonen for context-switches. Fordelen med å legge hele kjernen i klassen, er blandt annet at jeg nå kan hindre tilgang til prosesstabell og lignende fra andre prosesser. (Ok, siden jeg ikke har en MMU, så vil alltid alt være tilgjendelig om du virkelig vil, men jeg er så nærme jeg kan få til).

 

Prosesser er nå objekter av klasser, som arver fra klassen "Process". "Process" er en klasse, som inneholder data som er spesifikk for en gitt prosess. Dette er peker til stacken, samt litt statusflagg og lignende. Den inneholder også funksjoner for å pause prosessen, blokkere for en eller annen grunn (F.eks pga en mutex, eller io-wait), og funksjon for å varsle at den kan starte igjen. Vi har også funksjoner for å få litt statusinfo, som minneforbruk. Vi har også en virtuell funksjon til "main", som blir funksjonen hos barneklassene, som vi starter når en gitt prosess blir satt i gang.

For å lage en prosess, oppretter du nå bare en klasse, som arver fra Process, og i denne klassen må du lage en funksjon som kalles main. Eksempel:

class EksempelProsess : public Process {
  private:
    int data, variabler, og, skit, som, denne;
    char prosessen, har, bruk, for;
  public:
    EksempelProsess() {
      //Constructor, om vi vil gjøre noe spesielt når prosessen settes opp
    }
    uint8_t main() {
      // Main-funksjonen. Koden som skal kjøres i denne prosessen
    }
};

int main() {
  Process* p = new EksempelProsess;
  Kernel::startProcess(p);
}

Enklere, og renere enn dette får vi det vel ikke? Jeg vurderer dog å endre syntaksen på å starte prosessen, fra "Kernel::startProcess(p);" til "p->start()", men jeg er ikke helt sikker på hva jeg synes er mest logisk. På en side er det greit å se at jeg gir en prosess til kjernen, men på den anddre siden er det fint å ikke tenke på kjernen i det hele.

Noen her som tør å mene hva som blir penest?

 

Jeg har og laget en klasse "Memory", som blir grunnlaget for objektet "mem", som jeg bruker til minnehåndtering. mem->malloc(), og mem->free() kan nå brukes til å allokere/frigjøre minne. Jeg har også definert new/delete operatorene, slik at du kan bruke disse til å opprette/slette objekter. Jeg funderer litt på hvordan jeg best kan gjøre malloc/free tilgjengelige mellom ulike kodefiler. Slik det er nå, må jeg definere en "extern Memory* mem" i filer der jeg vil bruke malloc/free, og det føles litt klønete.

 

Objektorientering er veldig fint når vi skal begynne å skrive drivere. Jeg har laget en driver for et lcd-panel (HD44780 basert). Disse kan som kjent kobles på flere ulike måter, litt avhengig av hvor mange pinner du vil bruke, etc. Men samme hvordan de kobles, så vil de ha samme instruksjonssett, så det hadde vært fint å slippe å duplisere all koden, dersom jeg vil koble det på et annet vis. Jeg har derfor laget klassen "Lcd", som ikke har noen referanser til hardwaren, men som tar seg av "brukerinterfacet" til lcd-en. Denne klassen tar seg av å sende kommandoer til den virtuelle funksjonen "write".

Jeg lager deretter barneklasser til "Lcd" som er selve driveren mot displayet. Denne klassen må minimum ha funksjonen "write" for å sende kommandoer til displayet, og funksjonen "initialize" for å sette opp displayet. Jeg har laget classen "LcdShiftreg" i førsteomgang, som er en driveren jeg bruker nå når jeg tester. Som navnet kanskje antyder, har jeg brukt et shiftregister mellom mikrokontrolleren og displayet. Dette har jeg gjort for å spare pinner. D0-D7 på lcd-en er koblet til utgangene på shiftregisteret, som igjen er koblet med to pinner til avr-en (Data + klokke).

 

Fullt eksempel:

Til slutt velger jeg, som vanlig, å avslutte med ett fullt, og fungerende eksempel. Eksempelet er bygget rundt en AtMega1284P, og dersom du vil teste alt trenger du følgende deler:

  • 1*atmega1284p med krystaller og kondensatorer
  • 3* 74*164 shiftregistre
  • 1* HD44780 LCD-display (2*16 char)
  • 3 knapper
  • 16 LED
  • motstander, koblingsbrett, ledninger, og dill.

Eksempelet viser bruken av prosesser, lcd-display, shift-registre og trykknapper sammen med operativsystemet. Vi har:

  • LCD som viser ram/cpu-pruk til avr-en, i tillegg til stack-bruk for hver enkelt prosess
  • 2 stk shiftregistre, med 8 led's hver, som viser en binær teller, som øker verdien med 1 for hver gang en av to knapper trykkes.

Ting er koblet til AVR-en slik:

  • B0 - B2: Tre brytere, som drar pinnen til jord ved klikk
  • B4: data til 1.shiftregister med 8 LED's
  • B5: klokke til overnevnte shiftregister
  • B6: data til 2.shiftregister med 8 LED's
  • B7: klokke til overnevnte shiftregister
  • D0: "RS" på lcd-en
  • D1: "EN" på lcd-en
  • D2: Klokke-pinnen til shiftregisteret til LCD-en's datapinner.
  • D3: Data-pinnene til overnenvte shiftregister.

Om det er interesse for det, så kan jeg tegne koblingsskjema, men siden det kun er prototyping, så gidder jeg ikke gjøre det, med mindre noen vil se det.

 

Eksempelkode, som kan kompileres og programmeres til en mikrokontroller finnes her:

http://filer.rothaugane.com/avr/oos/Alpha_1.0.tar.gz

 

 

Lenke til kommentar

Opprett en konto eller logg inn for å kommentere

Du må være et medlem for å kunne skrive en kommentar

Opprett konto

Det er enkelt å melde seg inn for å starte en ny konto!

Start en konto

Logg inn

Har du allerede en konto? Logg inn her.

Logg inn nå
  • Hvem er aktive   0 medlemmer

    • Ingen innloggede medlemmer aktive
×
×
  • Opprett ny...