Geïntegreerd Practicum

Laurent Segers - laurent.segers@vub.be

Het Geïntegreerd Practicum omvat een aantal begeleide labo's waarin de studenten worden geconfronteerd met diverse elektronische componenten en schakelingen van gevorderd niveau, aangevuld met een pakket zelfstandig werk. De bedoeling is om verschillende bestaande elektronische componenten in een groter project in te bouwen. Een belangrijk aspect hierbij is het kunnen ontwerpen en implementeren van diverse schakelingen en software, waarbij debug-technieken en -tools gebruikt worden. Tijdens de labo's zullen we een remote LED-controller bouwen op basis van PIC-microcontrollers, FPGA's en draadloze modules. Dit project mag eenvoudig lijken, maar de samenhang van de verschillende onderdelen moet ervoor kunnen zorgen dat de eindgebruiker er maximaal comfort van kan hebben. Om dit tot een goed einde te brengen zullen er tijdens de labo's een aantal onderwerpen aan bod komen:

  • ontwerpen van elektronisch systemen op basis van bestaande technologiën,
  • gebruik kunnen maken van ingebedde microcontrollers en de mogelijke functionaliteiten,
  • het gebruik van FPGA's,
  • debugtechnieken voor ingebedde software,
  • debugtechnieken voor VHDL code (FGPA),
  • debuggen van diverse protocollen teneinde een goede communicatie tussen de verschillende ingebedde controllers/FPGA te kunnen garanderen,
  • project management,
  • het gebruik sensoren en actuatoren, en
  • draadloze communicatietechnologie en draadloze aansturingen,
  • het kunnen tekenen van PCB's in Altium Designer.

Beoordeling

Tijdens het laatste labo zal elke student afzonderlijk zijn project presenteren. De presentatie omvat:

  • een korte uiteenzetting (5 min.) van de topologie van het ingebed systeem (FPGA, microcontroller, hardware, enz.) tesamen met de werking ervan,
  • een overtuigende demo (5 min.) van het systeem waarbij alle functionaliteiten worden gedemonstreerd,
  • vragenronde (5 min.) waarbij de assistenten en collega-studenten vragen kunnen stellen.

Remote LED controller driver

Als we aan een remote LED controller driver denken, is de kans groot dat we we ons aan een LEDstrip controller denken die aangestuurd wordt met een kleine afstandsbediening. Hoewel dit over het algmeen het geval is, kan een remote LED controller ook andere toepassingen vinden:

  • LED lichtkrant.
  • slimme verlichting die overdag de kleuren van LED's aanpast. Hiermee wordt dan zoveel mogelijk het natuurlijke bioritme gevolgd (zie ook F.Lux).
  • LED cubes
  • ambilight voor computerschermen
  • dynamische richtingsaanwijzers voor auto's, fietsers, enz. Hierbij kunnen de LED's afzonderlijk aangestuurd worden via o.a. I2C of one-wire
  • dynamische verkeersborden
  • hoog vermogen elektronica die toelaat om 4 verschillende RGB-LEDstrips aan te sturen aan elk 2A. Dit is het equivalent van 600 RGB LED's!

In totaal vinden er 8 labo's plaats. Hiervan worden er 6 ingericht om de verschillende onderdelen van het project aan te reiken. Elke student krijgt dan ook de kans zelf om op de leerstof te kunnen experimenteren om zo een grondig inzicht te verwerven. De laatst 2 worden aan het project besteed zodat elke student een werkend protpype heeft op het einde van de labo's. Merk hierbij op de aangereikte leerstof soms niet de meest optimale oplossing is voor een probleem. De methoden zijn generiek en kunnen in een groot aantal projecten gebruikt worden. Elke student is vrij om een onderwerp te kiezen zolang er FPGA's en PIC-microcontrollers in het ontwerp gebruikt worden en het geheel op een finale PCB wordt afgeleverd. In de labo's worden een aantal generieke bouwstenen aangeleverd, maar de student is vrij om andere elementen te gebruiken zolang deze gelijkwaardig zijn aan de voorgestelde microcontrollers/FPGA's. Om de installatie van de nodige tools te vergemakkelijken, wordt een Linux virtuele machine aangeboden waarop zowel Xilinx ISE als gcc/g++ erop geïnstalleerd staan. Deze virtuele machine draait onder VMware player 12.0.

Labo 1

in labo 1 zal er met de PIC18F2455 gewerkt worden. Deze microcontroller van Microchip biedt een aantal mogelijkheden aan zoals de ingebouwde I2C, SPI en UART modules. Daarnaast biedt deze microcontroller ook analoog naar digitaal converters (ADC) aan. Deze microcontroller heeft ook ingebouwde PWM modules zodat men er LED's mee kan dimmen zoals gewenst. Bovendien is deze microcontroller heel goed gedocumenteerd in een uitgebreide datasheet en wordt deze door veel mensen gebruikt. Vooral dit laatste zorgt ervoor dat deze microcontroller goed te gebruiken is. We zullen deze microcontroller dan ook gebruiken als startpunt voor het project. In dit eerste labo zullen we volgende zaken behandelen:

  • LED-blink applicatie (hello world equivalent),
  • Een LED laten PWM'en. Hiervoor worden bepaalde timers gebruikt tesamen met interrupt-routines. Voor een enkele LED kan je de ingebouwde PWM modules gebruiken. Indien je meer dan 2 PWM-kanalen nodig hebt, kan je de PWM beter softwarematig implementeren.
  • Bidirectionele UART-communicatie tussen de microcontroller en een Linux machine.

Opdracht

Als opdracht voor dit wpo schrijf je zelf ingebedde software die toelaat om een RGB LED te PWM'en met de commando's doorgestuurd vanuit een Linux machine. Hierbij mag je 3 bytes doorsturen (R,G,B) waarbij de waarden de intensiteit van elke kleur weergeven. om bytes te kunnen doorsturen over UART in de terminal van Linux kan je onderstaande code gebruiken:

stty -F /dev/ttyUSB0 speed 115200 cs8 -cstopb -parenb -echo
echo -en '\xff\x00\xff' > /dev/ttyUSB0

Bovenstaande code zou met R = 255, G = 0 en B = 255 moeten overeenkomen. Merk op dat we het device ttyUSBx gebruiken en de snelheid hier ingesteld is op 115200 bps.

Tijdens dit eerste labo hebben we de code van de UART op de microcontroller (PIC18F2455) behandeld. De code van deze les kan hieronder gevonden worden.

/* 20 MHz crystal connected to the primary oscillator circuit -> divide by 5 to
have 4MHz */
#pragma config PLLDIV = 5 // now 5, because of 20 MHz crystal
/* We select to have the clk directly from the primary oscillator */
#pragma config FOSC = 0xF
/* Select the 20MHz if on HS (0xFC) (without USB multiplier), or 48MHz on
OSC1_PLL2 (0xFE) (with USB multiplier )*/
#pragma config CPUDIV = OSC1_PLL2
/* Disable watch dog timer */
#pragma config WDT = OFF
/* Disable USB voltage regulator */
#pragma config VREGEN = OFF
#pragma config MCLRE = OFF
#pragma config LVP = OFF
//------------------------------------------------------------------------------
#include 
#include 
//------------------------------------------------------------------------------
#define LEDpin          PORTCbits.RC0     //Define LEDPin as PORTC 0
#define LEDpin_Tris     TRISCbits.TRISC0  //Define LEDTris as TRISC Pin 0
#define UART_RX_tris    TRISCbits.TRISC7     
#define UART_TX_tris    TRISCbits.TRISC6   
//------------------------------------------------------------------------------
void UART_config();
void uart_write(char* stream,uint16_t len);
void uart_putc(char c);
volatile uint8_t UART_byte_received;
volatile uint8_t data_byte;

//------------------------------------------------------------------------------
void main(void) 
{
    // config LED as output
    LEDpin_Tris = 0; // output
    LEDpin = 1;
    
    UART_config();
    while(1)
    {
        if (UART_byte_received)
        {
            UART_byte_received = 0;
            uart_putc(data_byte);
        }
    }
    //uart_write("Dit is een test\r\n",18);
}
//------------------------------------------------------------------------------
//-- send byte per byte over UART
void uart_putc(char c)
{
    TXREG = c;
    while(!TXSTAbits.TRMT);
    while(PIR1bits.TXIF==0);   
} 
//------------------------------------------------------------------------------
//-- send a stream of bytes by using the above function
void uart_write(char* stream,uint16_t len)
{
    uint16_t i;
    for (i=0;i<len;i++)
    {
        uart_putc(stream[i]);
    }
}
//------------------------------------------------------------------------------
// interrupts (high priority interrupt routine))
void interrupt tc_int(void)         // High priority interrupt
{ 
	// check if it was the interrupt caused by UART RX pin
	// check both the IF and IE flags!!!
	if (PIR1bits.RCIF && PIE1bits.RCIE) // UART receive interrupt
    { 
    	data_byte = RCREG;
    	UART_byte_received = 1;
    	PIR1bits.RCIF=0;
        LEDpin=~LEDpin; // use LED to confirm we received a byte
    } 	
}
//------------------------------------------------------------------------------
void UART_config()
{
    // config for 9600bps
    UART_RX_tris = 1;
    UART_TX_tris = 1;     
    BAUDCON = 0b00001000;				// clear all except BRG16
    RCSTAbits.SPEN = 1;                 
    RCSTAbits.CREN = 1;                                    
    RCSTAbits.RX9 = 0;
    RCSTAbits.FERR = 0;
    RCSTAbits.OERR = 0;
    TXSTAbits.BRGH = 1;                 
    TXSTAbits.TXEN = 1;                 
    TXSTAbits.TX9  = 0;                 
    TXSTAbits.TX9D = 0;
    TXSTAbits.SYNC = 0; 
    // SPBRG = 1249 --> 4*256 + 225;
    SPBRG = 225;
    SPBRGH = 4;
    /* enable interrupt when RX (UART) gets data */
    PIE1bits.RCIE = 1;
    PIE1bits.TXIE = 0;
    /* enable global interrupt */
    INTCONbits.GIE = 1;
    /* enable peripheral interrupts (UART,...)*/
    INTCONbits.PEIE = 1;
}
//------------------------------------------------------------------------------

Labo 2

Een remote LED-controller is niet "remote" als er geen communicatiemedium en/of protocol aanwezig zijn om data naar de eindbestemming te kunnen brengen. Er bestaan heel veel verschillende manieren om data naar de remote controller te brengen, zoals bluethooth, 3G, wifi, ethernet, USB, ingebedde radiocontrollers, enz. Veelal berusten deze technologiën op een gelijkaardig principe. Tijdens de labo's wordt een eenvoudige maniere aangewend dat gebruik maakt van UART naar radio frequentie (RF) transceivers: de HM-TRP modules van HopeRF. Deze modules vertalen bytes die over UART worden overgebracht naar een radiosignaal dat door een andere RF-module opgepikt kan worden. De andere modules vertalen dit RF-signaal terug naar een UART signaal. Buiten het feit dat er geen speciale commando's (bv. AT commando's) naar de modules gestuurd hoeven te worden, verloopt dus de hele communicatie volledig transparent.

Naar wie stuur ik?

Het voordeel van de HopeRF modules is de eenvoudig van gebruik. Een neveneffect is echter dat deze modules enkel broadcasten, en dus niet naar een specifieke andere module data versturen. Hierdoor weten de ontvangers niet of de verzonden data al dan niet voor heen bedoeld was. Om dit te verhelpen zullen we een eenvoudig protocol schrijven dat toelaat om berichten gericht naar ëën enkele ontvanger te sturen of naar een doelgroep. We zullen m.a.w. eenvoudige addressering toepassen. Dit protocol zal volledig in C geschreven worden, en zal als dusdanig compatibel zijn voor zowel kleine microcontrollers als voor een computer die uitgerust is met een HopeRF-module.

Het protocol dat we zullen ontwikkelen is heel analoog aan het UDP-protocol dat gevonden kan worden in gewone computers. Omdat wij in open lucht (RF) werken, moet ons protocol eenvoudig maar robuust zijn. De vereisten zijn:

  • Een volledig datapakket moet van zender naar bestemming op een veilige manier toekomen, geen enkele byte mag verkeerd zijn (integriteit).
  • Een pakket bevat zowel de zender-ID als de ontvanger-ID. Op die kan de ontvanger eventueel een reply sturen naar de originele zender.
  • Een datapakket bevat zowel de effectieve data (payload) als de informatie over het pakket (beide ID's, enz.). De lengte van het pakket moet ook opgegeven zijn zodat een correcte afhandeling mogelijk is.
  • Het verwerken (opmaken en decoderen) van een pakket is eenvoudig: zowel een kleine microcontroller als een computer moeten erme kunnen omgaan.

In onderstaand codefragment wordt een voorbeeld van een pakket weergeven. Merk op dat dit enkel C-commentaar is.

/**********************************************************************
 * packet frame format
 *
 * packetType | messagelength |   CRC   | empty bytes | senderID |  receiverID |    Data
 *
 *   1 byte        1 byte        1 byte     1 bytes     2 bytes      2 bytes      n bytes
 **********************************************************************/

De verschillende elementen zijn:

  • packetType: de eerste byte van het hele pakket en en dient als herkenningspunt om een pakket te decoderen. Een typische waarde is bv. 0x04.
  • messagelength: de lengte van het bericht. hier zullen we de totale lengte van het pakket beschouwen: payload + informatie.
  • CRC: cyclic redundancy check: is een byte waari nwe de correctheid van het pakket mee zullen nagaan. Een voudig voorbeeld is het gebruik van een XOR functie die serieel over alle bytes heen gaat. Bij het opstellen wordt deze byte als laatste toegevoegd aan het pakket. Bij het decoderen wordt dezelfde XOR toegepast (deze keer CRC inclusief). Op het einde van het bericht (messagelength) moet het resultaat altijd 0 zijn. IS dit het geval, dan is het pakket wel degelijk correct. In het andere geval is er een byte verkeerd toegekomen en moet het pakket worden weggegooid.
  • empty bytes: zijn lege bytes die enkel dienen om de rest van het pakket weer te aligneren in het geheugen. Dit aligneren maakt achteraf mogelijk om alles via structs en unions (C) te coderen en decoderen zonder het gebruik van complexe algoritmes. Als gouden regel worden alignaties toegepast op veelvouden van 4 bytes (2 kan soms ook). Alignatie is enkel nodig indien de volgende informatie van het pakket meer dan 1 byte lang is en het einde ervan net voorbij een veelvoud van 4 bytes terechtkomt.
  • sender/receiver-ID: de addressen van respectievelijk de zender en de ontvanger (bestemming). De ontvanger mag het bericht pas decoderen indien het adres overeenkomt met het eigen adres (in te stellen in de software) of met een broadcast adres (0xffff). Beide adressen zijn telkens 2 bytes groot.
  • Data: de effectieve payload bestaande uit n bytes. Hierbij speelt de messagelength een belangrijke rol voor de lengtebepaling.

Bovenstaande informatie is enkel een beschrijving van hoe een pakket bekeken moet worden. In wat komt, volgt er een uiteenzetting van een mogelijke datastructuur om een pakket te construëren. Een pittig detail is de informatie die we wensen bloot te stellen aan de programmeur die dit protocol wenst te gebruiken. Alle pakket informatie of enkel een deel ervan? De programmeur zal waarschijnlijk enkel interesse hebben in het eigen ID en de ID van de tegenpartij. Om deze reden wordt een aparte datastructuur voor de ID's aangemaakt.

volatile uint16_t HOST_ID;
typedef struct 
{
	uint16_t senderID;
	uint16_t receiverID;
}
Socket_t;

De totale datastructuur wordt dan.

#define PACKET_TYPE 0x04
typedef struct
{
    uint8_t packetType;
    uint8_t messageLength;
    uint8_t CRC_Check;
    uint8_t empty_byte1;
    Socket_t IP_info;
} Socket_info_t;

Als laatste wordt deze datastructuur gemapped op een array. De array heeft hierbij 2 functies: het tijdelijk opslaan van data tijdens het ontvangen en het terugvinden van de payload eenmaal het pakket compleet is. De mapping van de bovenstaande structuren op een array gebeurt a.d.h.v. C-unions.

#define HEADER_LENGTH 8
typedef union
{
    Socket_info_t socket_info;
    char datastream[HEADER_LENGTH];
} IP_info_t;

Pakket opmaken en versturen

Eenmaal de structuren aangemaakt zijn is het relatief evident om een pakket op te maken en te versturen. Dit opmaken gebeurt als volgt:

  • voorzie een array van 64 bytes,
  • vul alle velden van de union/struct in,
  • kopieer de array van de union naar de eerste array,
  • kopieer de gewenste data naar de array.
/* 	Arguments:
 * 	socket: the information about the receiver (and sender)
 * 	data: the data which has to be copied into the stream
 *	datalength: the length of the data array	
 */
void IP_Write(Socket_t* socket, char* data, int datalength)
{
	char stream[64];
	uint8_t CRC = 0;
	IP_info_t s_info;
	socket->senderID = HOST_ID;
	s_info.socket_info.packetType = PACKET_TYPE;
	s_info.socket_info.messageLength = datalength;
	s_info.socket_info.CRC_Check = 0; // set temporarely to 0
	s_info.socket_info.empty_byte1=0; // set to 0
	s_info.socket_info.IP_info = *socket;
	// copy the data into the stream array
	// calculate also the CRC check
	for (i=0;i<datalength;i++)
	{
		// note the offset (header_length +i)
		stream[HEADER_LENGTH+i]=data[i];
		CRC = CRC^data[i];		
	}
	// calc CRC of the header
	for (i=0;i<HEADER_LENGTH;i++) 
	{		
		CRC = CRC^stream[i];
	}
	s_info.socket_info.CRC_Check = CRC;
	// copy the header into the array
	for (i=0;i<HEADER_LENGTH;i++) 
	{
		stream[i] = s_info.socket_info.datastream[i];
	}
	//-- stream our data to the underlying UART
	message_length = datalength + HEADER_LENGTH;
	uart_write(stream,message_length);
}

Om het gebruik ervan te vergemakkelijken hebben we het geheel dus in een functie geprogrammeerd. Op het einde roept de functie de UART write methode aan die de bytes een voor een zal versturen over UART (naar de HopeRF-module). Merk op dat er geen ingebouwde check is die toelaat om na te gaan of de datalength kleiner is dan de maximaal toegestane lengte, ttz. 64-HEADER_LENGTH.

Pakket ontvangen en decoderen

Een pakket ontvangen is daarentegen een grotere uitdaging. Men moet de startpositie van een pakket terugvinden, en nadien het pakket kunnen decoderen indien het de CRC check doorstaat. Om een pakket correct te kunnen decoderen, worden een aantal stappen ondernomen:

  • Voorzie een datastructuur waarmee het mogelijk is om een ringbuffer te programmeren.
  • Schrijf een functie die toelaat om data byte per byte in de ringbuffer op te nemen. Deze functie is zo kort mogelijk zodat het in de interrupt-routine van de UART-receiver opgenomen kan worden.
  • Via de main-loop wordt een functie opgeroepen die de inhoud van de ringbuffer nakijkt. Indien er voldoende bytes zijn ontvangen, wordt overgegaan tot het decoderen van de inhoud.
  • Retourneer zowel de data als de informatie van de zender/ontvanger indien een correct pakket is ontvangen.

De ringbuffer die hier gebruikt wordt ziet er als volgt uit:

typedef struct
{
	// where to write new data
	uint16_t writeIndex;
	// where to start reading data drom
	uint16_t readIndex;
	// how many bytes do we already have (unprocessed)
	uint16_t bytes_available;
	// the buffer of bytes
	uint8_t datastream[BUF_LENGTH]; // mimic a ring buffer
} IP_Ringbuffer_t;
volatile IP_Ringbuffer_t IP_buffer;

De functie die het mogelijk maakt om een byte in de buffer te plaatsen is als volgt:

inline void IP_BufferDataByte(char databyte)
{
    IP_buffer.datastream[IP_buffer.writeIndex] = databyte;
	IP_buffer.bytes_available++;
    //-- continue writing in the buffer until the end, then return
    if (IP_buffer.writeIndex<BUF_LENGTH-1) IP_buffer.writeIndex++;
    else IP_buffer.writeIndex=0;    
}

Deze functie wordt als inline geschreven zodat de compiler de hint krijgt om deze functie letterlijk in de interrupt-routine (UART) te schrijven zodat er geen function-call naar deze functie uitgevoerd wordt.

Het decoderen van de pakketten wordt in een aparte functie uitgevoerd. Deze functie zal eerst zoeken naar de startbyte van een pakket, nagaan of het aantal ontvangen bytes minstens even lang is als de header (vast gedeelde) en of we verder kunnen met het decoderen van de data. Het voorgestelde schema is hier sterk vereenvoudigd.

uint16_t IP_Read(Socket_t* socket, char* data)
{
	IP_info_t IP_info;
	unsigned int start_byte_found;
	int16_t i,readIndex;
	int16_t message_len;
	uint8_t CRC_DATA = 0;
    
	// first check if we have new data
	if (IP_buffer.bytes_available>0)
	{
		readIndex = IP_buffer.readIndex;
		start_byte_found=0;
		//-- prepare parsing for incoming databyte
		while((IP_buffer.writeIndex!=readIndex)&&(start_byte_found==0))
		{
			IP_info.datastream[0] = IP_buffer.datastream[readIndex];
			if (IP_info.socket_info.packetType==PACKET_TYPE)
			{
				start_byte_found=1;
				// copy the header info
				for (i=1;i<HEADER_LENGTH;i++)
				{
					IP_info.datastream[i] = IP_buffer.datastream[readIndex+i];
				}
			}
			else
			{
				// increment the real readindex when no packet has been found
				IP_buffer.readIndex++; 
				IP_Buffer.bytes_available--;
				if (IP_buffer.readIndex==BUF_LENGTH) IP_buffer.readIndex = 0;
			}
		}		
        
		if (start_byte_found==1)
		{
			message_len = IP_buffer.writeIndex-readIndex;
			message_len = (message_len<0)? message_len+BUF_LENGTH:message_len;
			// parse if we have at least one byte of data aside of our header
			if (message_len>HEADER_LENGTH)
			{
				// are we the destination of the packet? 
				if (IP_info.socket_info.IP_info.receiverID==HOST_ID)
				{
					// do we at least have the amount of bytes expected for the packet? 
					if (message_len>=IP_info.socket_info.messageLength+HEADER_LENGTH)
					{
						readIndex = IP_buffer.readIndex+HEADER_LENGTH;
						// XOR is commutative
						for(i=0;i<HEADER_LENGTH;i++)
						{
							CRC_DATA = CRC_DATA ^ data[i];
						}						
						for(i=0;i<IP_info.socket_info.messageLength;i++)
						{
							if (readIndex>BUF_LENGTH-1) readIndex=0;
							data[i] = IP_buffer.datastream[readIndex];
							CRC_DATA = CRC_DATA ^ data[i];
							readIndex++;
						}
						if (CRC_DATA==IP_info.socket_info.CRC_Check)
						{
							*socket = IP_info.socket_info.IP_info;
							// increment our pointer to the next location
							RingBufferIncrementReadPointer(IP_info.socket_info.messageLength+HEADER_LENGTH);
							return IP_info.socket_info.messageLength;
						}
						else // CRC not OK, packet is destroyed, start from next position
							RingBufferIncrementReadPointer(1);						
					}
				}
				else // packet is not for us, start from next position 
					RingBufferIncrementReadPointer(1);
			}
		}		
	}
	return 0;
}

Deze functie maakt gebruik van de functie RingBufferIncrementReadPointer. Deze functie update de read pointer van de ringbuffer zodat deze altijd naar een juiste locatie in de array wijst.

inline void RingBufferIncrementReadPointer(short int jump)
{
	IP_buffer.readIndex = IP_buffer.readIndex + jump;
	if (IP_buffer.readIndex>BUF_LENGTH-1) IP_buffer.readIndex = IP_buffer.readIndex - BUF_LENGTH;
	IP_buffer.bytes_available-=jump;
}

Bovenstaande codefragmenten zijn niet exhaustief en bevatten nog een aantal tekortkomingen. Ze liggen echter wel aan de basis van heel wat protocollen. De code wordt weliswar in een bibliotheek ingekapseld (IP.h en IP.c) zodat ze zowel in een microcontroller als een gewone computer gebruikt kunnen worden. Het is daarom ook goed om deze praktijk door te trekken naar alle onderdelen van het programma.

Op een Linux host machine kan dezelfde C-code zonder meer aangewend worden. De UART-code is daarentegen wel verschillend. De UART-code om op Linux te kunnen werken kan hier gevonden worden.

Om te kunnen achterhalen of een pakket verzonden of toegekomen is, is het interessant om een aantal print statements te declareren. Zo is mogelijk om te zien of een pakket toekomt, vertrekt of dat er fouten in de communicatie is. Je kan hierbij verschillende levels van printen declareren: enkel correcte pakketten, correcte pakketten in hexadecimaal formaat en alle binnenkomende en uitgaande byte in hexadecimaal formaat. Dit laatste is interessant om de code te debuggen en te zien waar er zich fouten voordoen. Pas dit eventueel toe in je code.

Opdrachten

  • Implementeer bovenstaande code en zorg ervoor dat je enkel je eigen LED's kan laten branden indien jouw adres in het pakket aanwezig is. De sterkte van de LED wordt in het bericht in een eigen formaat meegestuurd.
  • Kan je code uitbreiden zodat je ook een acknowledge kan terugsturen? Een acknowledge (ACK) is nuttig om te weten dat een pakket goed is toegekomen.
  • Sommige berichten kunnen van toepassing zijn op alle controllers in een netwerk. Voorzie de mogelijk om een broadcast te doen naar alle nodes. Maak hierbij een adres aan waarnaar alle nodes moeten luisteren.

Labo 3

De PIC microcontroller die we tot nu toe gebruikt hebben beschikt niet over het aantal vereiste pinnen om bv. een RGB lichtkrant te laten oplichten. Tenzij men met multiplexers zou werken, zijn de 28 pinnen van deze MCU onvoldoende om bv. 256 LED's aan te sturen. Een chip die wel over voldoende pinnen beschikt is een field programmable gate array (FPGA). Het lijkt hierdoor aantrekkelijk om een FPGA in alle mogelijke designs te implementeren waardat veel IO-pinnen nodig zijn. Vaak bestaan er (veel) goedkopere alternatieven om aan veel IO-pinnen te geraken dan een FPGA te gebruiken. In wat volgt wordt duidelijk wanneer men een microcontroller en/of een FPGA gebruikt.

FGPA

Een microcontroller kan gezien worden als een miniatuur computer waarbij één of meerdere processorkernen de taken op een sequentiële wijze uitvoeren. Interruptroutines geven ons hierbij de illusie dat de processor verschillende taken tegelijk zou kunnen doen, maar processor voert de taken (instructies) enkel sequentieel uit. Dankzij ingebouwde modules (UART, I2C, SPI, PWM, enz.) kan de processor met de buitenwereld communiceren.

Een FPGA darentegen bevat standaard geen processor die instructies uitvoert. Een FPGA bevat digitale logica die door de gebruiker geconfigureerd kan worden om combinatorische schakelingen en/of finite state machines (FSM) te implementeren. Waardat bij een microcontroller alles sequentieel uitgevoerd wordt, wordt op een FPGA alle logica in parallel uigevoerd. Men moet dus in termen van hardware denken i.p.v. software.

Op een FPGA wordt logica getriggered op een welgegeven tijdsinterval. Dit laat toe om o.a. FSM's te laten evolueren in de tijd. Dit triggeren wordt gedaan a.d.h.v. een klok. De klok heeft een bepaalde frequentie en wordt gegeven door het kristal dat zich naast de FPGA op het bordje bevindt. Doorgaans wordt dit door de fabrikant van het bordje opgegeven. In de komende labo's zullen de Mojo-borden van Embedded Micro gebruikt worden. De FGPA's (Spartan 6) draaien hierbij aan een frequentie van 50MHz.

Het mojo bordje beschikt verder over 8 LED's en over een reeks IO-poorten. In wat volgt zullen eenvoudige voorbeelden gedemonstreerd worden om een counter, een kleine PWM-module en de basis van de UART-module te implementeren.

De taal waarin we deze modules zullen schrijven is VHDL en de implementatie zal gebeuren a.d.h.v. de Xilinx ISE tools. Het geheel kan gevonden worden in de virtuele machine die voor dit vak aangemaakt is.

Het eerste voorbeeld dat hier aangehaald zal worden is het aanmaken van een eenvoudige counter die op LED's weergegeven zal worden. De 8 aanwezige LED's zorgen ervoor dat men een counter van 8 bit dient aan te maken zodat men 256 verschillende waarden kan weergeven. De LED's tonen in oplopende wijze de verschillende getallen (van 0 t.e.m. 256). In onderstaand codefragment wordt getoond hoe men dit implementeert.

LED_counter:process(clk)
	variable counter:unsigned(7 downto 0):=(others=>'0');
begin
	if (rising_edge(clk)) then
		counter:=counter+1;
		LEDs<=STD_LOGIC_VECTOR(counter);
	end if;
end process LED_counter;

Bovenstaade code bevat een aantal belangrijke elementen:

  • De input clock (clk): dit is het signaal waarop het process LED_counter zal triggeren. Bij elke trigger wordt het proces als ware opgeroepen en wordt de hele code van het proces uitgevoerd.
  • Een klok bevat doorgaans 2 staten: high of low. Bij elke periode worden dus 2 overgangen gemaakt waardoor het proces 2 keren per periode opgeroepen zal worden. Om dit te vermijden wordt telkens getriggered op de stijgende flank van de klok. Dit wordt aangeduid als rising_edge. De tegenvariant, falling_edge bestaat ook.
  • Binnen het proces worden variabelen aangemaakt. Merk op dat een veriabele in VHDL iets helemaal anders betekent dan in een gewone programmeertaal. Een variabele is in feit een buffertje waarin een aantal bits (vector) worden bijgehouden. Het aantal bits kan vrij gekozen worden. Dus hier kan men een variabele aanmaken van 17 bits. In een gewone programmeertaal zoals C, C++, enz. is het echter enkel mogelijk om variabelen te alloceren die een veelvoud van 8 bits hebben.
  • VHDL is een sterk getypeerde taal. Dit betekent dus dat alle variabelen (en signalen) van een datatype zijn. Dit is nog niet duidelijk het huidige voorbeeld, maar komt later zeker terug.

Het voorbeeld van hierboven laat toe om tussen 0 en 255 te tellen. Wat het voorbeeld echter niet toont is hoe deze counter de waarde op LED's zal kunnen weergeven. Om dit te kunnen doen moeten we de counter (het proces) embedden in een VHDL module. Dit wordt in onderstaande codefragment gedaan.

----------------------------------------------------
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
----------------------------------------------------
entity LEDcounter is
	port(
		clk: 		IN 		STD_LOGIC;
		LEDs:		OUT		STD_LOGIC_VECTOR(7 downto 0)
	);
end LEDcounter;
----------------------------------------------------
architecture Behavioral of LEDcounter is
----------------------------------------------------
begin
	------------------------------------------------
	LED_counter:process(clk)
		variable counter:unsigned(7 downto 0):=(others=>'0');
	begin
		if (rising_edge(clk)) then
			counter:=counter+1;
			LEDs<=STD_LOGIC_VECTOR(counter);
		end if;
	end process LED_counter;
----------------------------------------------------
end Behavioral;
----------------------------------------------------

Poorten en gedrag

Elke VHDL code vertrekt vanuit een aantal voorafbepaalde datatypen en operatoren. Deze worden in bestaande bibliotheken meegeleverd en kunnen door de VHDL-codeur gebruikt worden. Bibliotheken gebruiken gebeurt door het woordje "use" gevolgd door de naam van de bibliotheek. Over het algmeen geldt dat de bibliotheken zoals hierboven getoond voldoende zijn voor veel basisprojecten.

Een VHDL-module bestaat uit 2 grote delen: de beschrijving van de input-output (poorten) en de beschrijving van het gedrag van de module (de behavioral).

De poorten zijn de letterlijke verbindingen van de module met de buitenwereld. De declaratie ervan is altijd als volgt:

  • de naam van de poort gevolgd door een dubbele punt,
  • of het een input, output of beide is,
  • hoeveel bits de poort in beslag neemt (STD_LOGIC) of (STD_LOGIC_VECTOR).

Bij het declareren van poorten wordt per conventie steeds met STD_LOGIC of met STD_LOGIC_VECTOR gewerkt. Dit maakt de potentiële communicatie met andere modules eenvoudiger omdat er dan tussen de modules geen lastige typeconversies moet plaatsvinden.

Naast de beschrijving van de poorten wordt ook het gedrag van de module geschreven. In een vorig codefragment is het gewenste gedrag reeds beschreven en dit kan rechtreeks hierin geïmplementeerd worden. Merk op dat klok van het gedrag overeenkomt met de klokbeschrijving van in de poorten. Ook het "LEDs"-signaal is reeds bij de poorten beschreven. Het aantal bits (en type) van de poorten en de aanspreking ervan in de code moet exact overeenkomen voor het gewenste resultaat.

Signalen vs. variables

In VHDL worden regelmatig signalen en variables door elkaar gebruikt. Beide lijken hetzelfde te doen, maar hun gebruik is totaal verschillend. Variabelen worden enkel lokaal in een proces gebruikt worden. Het toewijzen van een waarden aan een variabele gebeurt door het ":=" teken. Een variabele wordt in digitale logica vertaald naar een latch. Dit betekent dus dat een variabele onmiddelijk van waarde verandert na de toewijzing. Een signaal darentegen wordt over het algemeen vertaald naar een flipflop. Dit betekent dus dat een signaal zijn toewijzing pas ziet na de volgende (klok)cyclus. Tot deze cyclus blijft een signaal zijn oude waarde behouden. Hierdoor wordt een signaal gebruikt als communicatielijn tussen verschillende modules. De syntax van toewijzing is dan ook verschillend dan die van een variabele, nl. "<=".

In VHDL is het ook mogelijk om commentaar te schrijven. Dit wordt gedaan door de commentaar door 2 "-" te laten voorafgaan. Dit symbool heeft enkel effect op de daaropvolgende tekst tot het einde van de huidige regel.

Verbinding met de fysieke IO-poorten van de FPGA

Hoewel we aan afgewerkte module hebben geschreven, is het nog niet mogelijk om de code op een FPGA te implementeren. Een belangrijke stap is mappen van de poorten van de module naar de fysieke pinnen van de FPGA. Dit kunnen we aan de tools duidelijk maken door gebruik te maken van een user constraints file (UCF). Dit wordt hieronder voorgedaan.

net 	"clk"		LOC=P56;
net 	"LEDs<0>"	LOC=P134;
net 	"LEDs<1>"	LOC=P133;
net 	"LEDs<2>"	LOC=P132;
net 	"LEDs<3>"	LOC=P131;
net 	"LEDs<4>"	LOC=P127;
net 	"LEDs<5>"	LOC=P126;
net 	"LEDs<6>"	LOC=P124;
net 	"LEDs<7>"	LOC=P123;

Het geheel (VHDL + UCF) synthetiseren en compileren naar een *.bin bestand levert de "firmware" op die op de FPGA gedownload kan worden.

Klokdeler

De LED counter die we hiernet hebben geschreven zal op de LED's van de FPGA weldegelijk het gewenste resultaat opleveren. Het enige lastige eraan is wel dat de LED's zodanig snel van toestand zullen wisselen dat het menselijke oog het tellen niet zal kunnen waarnemen. Om dit toch nog te kunnen zien zal men de frequentie van het tellen moeten verlagen. Dti kan men doen door de klokfrequentie te delen. Om het comfortabel te maken zullen we de frequentie van het tellen verlagen naar 1Hz. Hierdoor moet men de originele klok delen door 50 miljoen. Men moet dus 50 miljoen klokticks wachten vooraleer men het getal met 1 eenheid mag ophogen. Dit kan men doen a.d.h.v. een nieuwe teller.

Zoals reeds vermeld kunnen we in VHDL zelf variabelen definiëren van een bepaalde bitlengte (vector). Om een vector aan te maken waarin we minimaal 50 miljoen waarden in kunnen bijhouden, moeten we een vector aanmaken van 26 bits (67 miljoen waarden). Het volstaat dan om deze vector bij elke kloktick te laten ophogen en te vergelijken of er 50 miljoen ticks verstreken zijn (zie onderstaand codefragment).

LED_counter:process(clk)
	variable counter:unsigned(7 downto 0):=(others=>'0');
	variable clk_counter:unsigned(25 downto 0):=(others=>'0');
begin
	if (rising_edge(clk)) then
		clk_counter:=clk_counter+1;
		if (clk_counter=50000000) then -- compare if equal to 50*10^6
			counter:=counter+1;
			LEDs<=STD_LOGIC_VECTOR(counter);
			clk_counter:=(others=>'0'); -- reset counter
		end if;
	end if;
end process LED_counter;

Nieuw hierbij is dat men een variabele kan vergelijken met een voorafbepaalde waarde. Algemeen geldt dat men een variabele kan vergelijken met een waarde die van hetzelfde datatype kan zijn. Een vergelijking vindt steeds plaats met het symbool "=".

UART TX

In principe kan men om het even welke controller in VHDL implementeren. Een ander voorbeeld van "gemakkelijk" te implementeren modules is de UART-module. Wij zullen hierbij de TX-kant van de UART implementeren. Het RX-gedeelte wordt als oefening gelaten. UART is een point-to-point communicatie medium met 2 signaallijnen (full duplex). Dit laatste maakt de implementatie van UART eenvoudig. De UART-communicatie gebeurt als volgt:

  • Wanneer er geen data verstuurd wordt, is de datalijn passief hoog. Men laat de lijn zweven en de pull-up weerstand doet hier zijn werk.
  • Bij het versturen van data, wordt er eerst een startbit berstuurd. Deze startbit is altijd actief laag (logisch 0).
  • Na de startbit volgen de 7,8 of 9 databits. De datalijn kan dus hoog of laag zijn.
  • Als laatste volgt de stopsequentie. Deze bestaat doorgaans uit 1, 1.5 of 2 stopbits. Een stopbit is actief hoog (logisch 1).
  • Na de stopsequentie wordt de lijn weer passief hoog gelaten.

Heel de operatie kan samengevat worden in een toestandsmachine (FSM). De FSM van de UART-module zou er als volgt kunnen uitzien:

  • Blijf in IDLE zolang dat er geen data (send_data = 0) verstuurd moet worden.
  • Ga over naar de STARTBIT indien send_data = 1.
  • Na de startbit wordt overgegaan naar SENDDATA. Blijf in SENDDATA zolang dat niet alle databits verzonden zijn.
  • Zijn alle databits verzonden, ga dan over naar de STOPBITS.
  • Als laatste mag men terugkeren naar de IDLE toestand, waarop men opnieuw wacht op een nieuwe te verzenden byte.

De beschreven toestanden zijn dus: IDLE, STARTBIT, SENDDATA en STOPBIT. De UART-module (enkel TX) in VHDL kan in onderstaand codefragment gevonden worden. Merk op dat we de baudrate op 9600 hebben gezet.

----------------------------------------------------
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
----------------------------------------------------
entity UART is
	port(
		clk: 		IN 		STD_LOGIC;
		TX:			OUT		STD_LOGIC;
		TX_enable:	IN		STD_LOGIC;
		TX_data:	IN		STD_LOGIC_VECTOR(7 downto 0)
	);
end UART;
----------------------------------------------------
architecture Behavioral of UART is
----------------------------------------------------
UART_TX:process(clk)
	-- declare a type for our statemachine
	type UART_TX_STATE is (IDLE,STARTBIT,SENDDATA, STOPBIT);
	variable state:UART_TX_STATE:=IDLE;
	variable data:STD_LOGIC_VECTOR(7 downto 0);
	variable databit_counter:unsigned(3 downto 0); -- count the amount of bits sent + 1
	variable clk_divider:unsigned(13 downto 0):=(others=>'0'); -- count up to 5208 ticks for 9600 baud per second 
begin
	if (rising_edge(clk)) then
		clk_divider:=clk_divider+1;
		if (clk_divider=5208) then
			clk_divider:=(others=>'0');
			case state is
				when IDLE =>
					TX<='1';
					databit_counter:=(others=>'0');
					if (TX_enable='1') then
						state:=STARTBIT;
						data:=TX_data;
					end if;
				when STARTBIT =>
					TX<='0';
					state:=SENDDATA;
				when SENDDATA =>
					TX<=data(to_integer(databit_counter));
					databit_counter:=databit_counter+1;
					if (databit_counter=8) then
						state:=STOPBIT;
					end if;
				when STOPBIT =>
					TX<='1';
					if (TX_enable='0') then
						state:=IDLE;
					end if;
			end case;	
		end if;
	end if;
end process UART_TX;
----------------------------------------------------
end Behavioral;
----------------------------------------------------

Voor de eenvoud zijn we hier uitgegaan van 1 start- en stopbit en 8 databits. De module kan met een logic state analyser (digitale oscilloscoop) bemonsterd worden om te zien of het gewenste effect bereikt is.

Opdrachten

  • Schrijf een PWM module die een refreshrate van 200kHz heeft en een waarde tussen 0 en 256 als input kan aannemen. De input zijn 8 IO-pinnen die je met breadboardkabels hoog/laag trekt.
  • Naast het TX-gedeelte beschikt de UART ook over een RX-gedeelte. Implementeer deze ook voor een baudrate van 9600. Pro-tip: tracht elke bit in het midden van duurtijd te bemonsteren. Daar is de kans zeer klein dat men verkeerdelijk een andere bit zou inlezen.

Labo 4

In dit labo zullen we uitgebreider ingaan op de basisconcepten van VHDL. Vaak is het zo dat een VHDL-implementatie niet bestaat uit een enkele functionaliteit, maar uit verschillende elementen die samen een geheel vormen. VHDL laat hierbij toe om verschillende functionaliteiten apart te coderen en nadien samen te voegen. Deze stappen verschillen niet heel t.o.v. het coderen van een enkele module. Echter zullen we vanaf nu in verschillende lagen modules aan elkaar koppelen, ttz. in een hiƫrarchie. Om dit te kunnen doen schrijven we in VHDL onze modules eerst apart, waarna we ze zullen doorkoppelen in een toplevel. ALs voorbeeld zullen we een eenvoudig RGB-PWM module maken die zelf uit 3 afzonderlijke PWM-modules bestaat. De interne werking van een PWM-module wordt hierbij aan de leze overgelaten. Een PWM-module kan onderstaande poortdefinitie hebben.

entity PWM is
	port(
		clk: 		IN 		STD_LOGIC;
		value:		IN		STD_LOGIC_VECTOR(7 downto 0);
		PWM_out:	OUT		STD_LOGIC;
		enable:		IN		STD_LOGIC
	);
end PWM;

Deze module kan zelf geïmplementeerd worden op de FPGA doro de poorten in deze definitie te koppelen aan de fysieke pinnen van de FPGA (via de UCF-file). Hierdoor wordt deze module de hoogste module in de hiërarchie en kan men dus met de FPGA niets anders meer doen. Om een RGB-module te kunnen maken hebben we een PWM-module per kanaal nodig, ttz. 3 PWM-modules. Deze 3 modules zullen we dus in parallel schakelen in een hoger niveau. Dit hoger niveau heet dan ook een toplevel. Het toevoegen van een module in een toplevel gebeurt in 3 fasen:

  • declaratie van de betrokken module,
  • instantiatie van de betrokken module en
  • de nodige "bekabeling" van de module naar de rest van de toplevel en of buitenpoorten.

Het geheel wordt gedemonstreerd in onderstaand codefragment.

----------------------------------------------------
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
----------------------------------------------------
entity RGB_MODULE is
	port(
		clk: 		IN 		STD_LOGIC;
		channel_r:	OUT		STD_LOGIC;
		channel_g:	OUT		STD_LOGIC;
		channel_b:	OUT		STD_LOGIC;
		en:			IN		STD_LOGIC
	);
end RGB_MODULE;
----------------------------------------------------
architecture Behavioral of RGB_MODULE is
	-- declare our PWM module here. Please note that we replaced
	-- entity (from port declaration) with component
	component PWM is
	port(
		clk: 		IN 		STD_LOGIC;
		value:		IN		STD_LOGIC_VECTOR(7 downto 0);
		PWM_out:	OUT		STD_LOGIC;
		enable:		IN		STD_LOGIC
	);
	end component;
	-- if you have other modules to declare, put them 
	-- also here before the begin keyword
	component RGB_master is
	port(
		clk:		IN		STD_LOGIC;
		R:			OUT		STD_LOGIC_VECTOR(7 downto 0);
		G:			OUT		STD_LOGIC_VECTOR(7 downto 0);
		B:			OUT		STD_LOGIC_VECTOR(7 downto 0)
	);
	end component;
----------------------------------------------------
-- we need to declare the signals which we will use 
-- in order to interconnect all the modules properly
-- note: only signals which do not belong to the port 
-- definition of the toplevel must be declared here 
	signal PWM_value_r, PWM_value_g, PWM_value_b: STD_LOGIC_VECTOR(7 downto 0);	
----------------------------------------------------
begin
	-- instantiate the modules by giving them a name 
	-- all names shoud be unique. 
	RGB_R PWM 
	port map(
		clk=>clk,
		value=>PWM_value_r,
		PWM_out=>channel_r,
		enable=>en
	);
	------------------------------------------------
	-- we declare the same module with a different 
	-- name and different nets
	RGB_G PWM 
	port map(
		clk=>clk,
		value=>PWM_value_g,
		PWM_out=>channel_g,
		enable=>en
	);
	------------------------------------------------
	RGB_B PWM 
	port map(
		clk=>clk,
		value=>PWM_value_b,
		PWM_out=>channel_b,
		enable=>en
	);
	------------------------------------------------
	RGB:controller: RGB_master
	port map(
		clk=>clk,
		R=>PWM_value_r,
		G=>PWM_value_g,
		B=>PWM_value_b
	);
----------------------------------------------------
end Behavioral;
----------------------------------------------------

In bovenstaande code valt op dat we een extra module hebben toegevoegd die de waarde van de PWM-kanalen aangeeft. Deze module kan naar hartelust ingevuld worden om het gewenste resultaat te bereiken. Deze module kan bijgevolg dus ook aangepast worden om een waarde vanuit een remote locatie aan te nemen en door te geven naar het gepaste PWM-kanaal.

Bij het uitschrijven van een toplevel is het belangrijk om 2 typen signalen te onderscheiden: de signalen die intern in de module zelf blijven en de signalen die rechtstreeks aan de input/output van de toplevel gekoppeld zijn. Signalen die aan de IO van de toplevel gekoppeld zijn mogen in geen geval opnieuw gedeclareerd worden als signaal binnen de toplevel. Deze sorgen er immers voor dat de interne onderdelen met de buitenwereld klappen. De poortdefinitie van een module/toplevel declareert deze signalen dus al. De andere (interne) signalen moeten wel aangemaakt worden zodat de module gekoppeld kunnen worden. Voor de interne signalen geldt dat er steeds maximaal 1 aansturende module is. Het aantal lezende modules is voor elk signaal onbeperkt. Met een aansturende module wordt bedoeld dat deze module het signaal actief hoog en laag trekt. Moesten 2 modules eenzelfde signaal willen aansturen zou men immers kortsluiting kunnen veroorzaken. Bij de signalen die extern gekoppeld worden geldt dat men een output signaal door 1 enkele interne module mag aansturen. Een inkomend signaal mag door alle modules geraadpleegd worden.

UCF

De pinmapping naar de fysieke FPGA gebeurt door enkel de poorten van de toplevel in de UCF-file op te nemen. Alle andere poorten van de submodules opnemen in de UCF-file zal tot fouten leiden en zal ervoor zorgen dat de VHDL code niet te implementeren zal zijn. In ISE kan men een module als toplevel aanduiden er erop te rechterklikken en te selecteren als topmodule.

Opgaven

  • Zorg ervoor dat het mogelijk is om via 3 waarden (telkens 1 byte) die over UART verzonden worden om een RGB-patroon te genereren op 1 of meerdere LED's.
  • Kan je de PIC-microcontroller van voorgaande lessen koppelen aan de FPGA om ditzeldde te kunnen bereiken?

Opencores

FPGA's zijn reeds sinds enige tijd beschikbaar op de markt en hierdoor is het (net zoals voor andere ingebedde sytemen) mogelijk om beschikbare modules van andere nover te nemen en te implementeren in een eigen design. Veel codeurs stellen hun code dan ook publiekelijk beschikbaar op allerhande websites. Een belangrijke website die niet mag ontbreken tijdens het ontwikkelen van een VHDL-code is opencores.org. Ga gerust op deze website kijken om eventueel een gepaste module over tenemen in je eigen design. Vergeet hierbij dan ook niet om correct naar de plaats van vinden te refereren.