hur organisera C-program?

PIC, AVR, Arduino, Raspberry Pi, Basic Stamp, PLC mm.
Användarvisningsbild
jesse
Inlägg: 9240
Blev medlem: 10 september 2007, 12:03:55
Ort: Alingsås

hur organisera C-program?

Inlägg av jesse »

jag håller på med några olika projekt med AVR som jag programmerar i C. Ett av dem är ganska omfattande, och det börjar bli lite rörigt, beroende på att jag inte har någon riktig "standard" på var jag ska lägga olika data och funktioner.

Nu ska jag inte gå in i detalj på röran i mitt projekt, utan efterlyser istället hur man allmänt bör lägga upp program som är lite större än "blinka lysdiod". Syftet är att jag ska kunna återanvända så mycket av koden som möjligt vid nya projekt, även om hårdvaran byts ut.

Vi tar ett exempel:

jag ska läsa in text från UART och/eller från annan enhet och skicka ut som text till en 128x64 grafisk LCD och/eller till en 4 x 20 raders textbaserad LCD. ena LCD:n har SPI-interface, den andra kopplas parallellt.

Jag vill då ha följande filer:
main.c - starta allt och hålla det rullande.
spi.c - grundläggande funktioner för SPI
graflcd.c - grundläggande kommandon för grafisk lcd.
txtlcd.c - fuktioner för textbaserad lcd.
uart.c - funktioner för UART

sen har varje fil en motsvarande headerfil (.h)
kanske jag även borde ha en egen c-fil (eller bara h-fil) med definitioner för just den hårdvara jag använder för tillfället. Så om jag ska skriva ett program för "kretskort x" så inkluderar jag bara "x.c" eller "x.h" så har jag det mesta konfigurerar för just den designen (ex.. LEDPORT = PORTB, RED_LED = PB4)

Tanken är väl att funktionerna i "graflcd" ska kunna användas oavsett *hur* jag sköter min SPI-kommunikation. Därför bör jag ha generella SPI-funktioner i separat fil. Och main.c i sin tur ska kunna anropa generella "skriva ut" rutiner utan att behöva veta *vilken* display (eller annan enhet) den skriver till.. t.ex. putChar('A'); eller print(string);

Frågan är hur jag lämpligast arrangerar möjligheten att byta enhet... just nu har jag en global variabel som heter put. och jag kan sätta den till olika värden: t.ex.

Kod: Markera allt

put = LCD;
print(helptext);
put = TFT2;
print(annan_text);
put = LCD|TFT2|UART;
print(skriver_ut_till_alla_enheter);
put = 0;
antag att uart.c (med tillhörande uart.h) ska vara en generell funktion för uart så ska man bland annat kunna ställa in vilken kanal man använder, baudrate osv... då ska man väl lämpligen ha UART_BAUDRATE och UART_KANAL definierade - bör då dessa definitioner alltid ligga i main.h eller var lägger jag dem? Och i så fall - då behöver jag väl alltid ha #include "main.h" i uart.h-filen, för annars hittar inte kompilatorn definitionerna? (jag vill hellre använda fasta definitioner på t.ex. BAUDRATE istället för funktioner, såvida det inte behöver ändras under programmets gång. Ett alternativ till funktioner är kanske att använda globala variabler, men det verkar som om det kan bli ganska rörigt.

Lite allmän "systemeringskunskap" för enklare processorsystem skulle vara intressant att få del av, gärna på svenska.
Användarvisningsbild
vfr
EF Sponsor
Inlägg: 3515
Blev medlem: 31 mars 2005, 17:55:45
Ort: Kungsbacka

Re: hur organisera C-program?

Inlägg av vfr »

Det där med att använda olika "drivrutiner" till olika hårdvara, är beroende på när och hur det skall vara olika. Det enklaste är att göra olika objektfiler med ett gemensamt interface och sedan länka ihop dom grejor man behöver till just den hårdvaran.

Skall man kunna byta hårdvara under drift i samma system, t.ex olika serieportar med olika drivrutiner, så krävs det lite mer. Då måste båda vara länkade samtidigt i samma applikation och det krävs en inställning av något slag. Då får man göra det lite mer flexibelt. Jag jobbar lite på båda sätten, beroende på tillfälle och behov.
blueint
Inlägg: 23238
Blev medlem: 4 juli 2006, 19:26:11
Kontakt:

Re: hur organisera C-program?

Inlägg av blueint »

Gå från detta:

Kod: Markera allt

put = LCD;
print(helptext);
put = TFT2;
print(annan_text);
put = LCD|TFT2|UART;
print(skriver_ut_till_alla_enheter);
put = 0;
Till (pseudodokod):

Kod: Markera allt

print(LCD, helptext);
print(TFT2, annan_text);
put[0]=LCD; put[1]=TFT2; put[3]=UART; put[4]=NULL;
print(put, skriver_ut_till_alla_enheter);
Förslagsvis kan man koppla ihop ett "filhandtag" med UART parametrar om man t.ex har 2x SPI + 1x Bitbang SPI osv.. Så att olika drivrutiner och I/O addresser till dito kopplas samman korrekt.
Huvudprincipen bör vara generalisering. Men med hänsyn till kodstorlek, och responstid som krävs i projektet. En stor "struct" KAN ta för lång tid att hantera osv.. I en del fall kan man gömma specialfallet med lite smidiga omskrivningar.
Användarvisningsbild
stekern
Inlägg: 453
Blev medlem: 2 november 2008, 08:24:18
Ort: Esbo, Finland

Re: hur organisera C-program?

Inlägg av stekern »

jesse skrev: antag att uart.c (med tillhörande uart.h) ska vara en generell funktion för uart så ska man bland annat kunna ställa in vilken kanal man använder, baudrate osv... då ska man väl lämpligen ha UART_BAUDRATE och UART_KANAL definierade - bör då dessa definitioner alltid ligga i main.h eller var lägger jag dem? Och i så fall - då behöver jag väl alltid ha #include "main.h" i uart.h-filen, för annars hittar inte kompilatorn definitionerna? (jag vill hellre använda fasta definitioner på t.ex. BAUDRATE istället för funktioner, såvida det inte behöver ändras under programmets gång. Ett alternativ till funktioner är kanske att använda globala variabler, men det verkar som om det kan bli ganska rörigt.
Du kan ju ha en egen .h fil med just konfigurationsinställningar, t.ex. en uart_config.h, eller en projektövergripande config.h
Användarvisningsbild
jesse
Inlägg: 9240
Blev medlem: 10 september 2007, 12:03:55
Ort: Alingsås

Re: hur organisera C-program?

Inlägg av jesse »

hmm... (funderar)...


hittade den här sidan som jag ska läsa noga: Organizing Code Files in C and C++

exempel på saker jag behöver lära mig mer om:

Bild

blueint: ditt förslag ser ut att ge större programkod, särskilt om putChar ska gå igenom en array med attribut...

mer tänkvärt (hittatde nyss denna :)
Highly embedded (limited code and ram size) projects pose unique challenges for code organization.

I have seen quite a few projects with no organization at all. (Mostly by hardware engineers who, in my experience are not typically concerned with non-functional aspects of code.)

However, I have been trying to organize my code accordingly:

1. hardware specific (drivers, initialization)
2. application specific (not likely to be reused)
3. reusable, hardware independent

For each module I try to keep the purpose to one of these three types.
I've written and maintained 30 embedded products on a varity of target micros (including MSP430's). The "rules of thumb" I have been most successful with are:

* Try to modularize generic concepts as much as possible. It makes for easier maintenance and reuse/porting of a project to another target micro in the future. (e.g. separate driver code from application code)
* DO NOT start by worrying about optimized code at the very beginning. Try to solve the domain's problem first and optimize second. -- Your target micro can handle a lot more "stuff" than you might expect.
* Work to ensure readability. Although most embedded projects seem to have short development-cycles, the projects often live longer than you might expect and another developer will undoubtedly have to work with your codebase.
blueint
Inlägg: 23238
Blev medlem: 4 juli 2006, 19:26:11
Kontakt:

Re: hur organisera C-program?

Inlägg av blueint »

Det sista citatet är utmärkt!
Nerre
Inlägg: 27257
Blev medlem: 19 maj 2008, 07:51:04
Ort: Upplands väsby

Re: hur organisera C-program?

Inlägg av Nerre »

Det där sista, med läsbarhet, är det som jag tycker är viktigast.

Hela vitsen med att använda en kompilator är ju just att den fixar optimeringar och ersättningar så man behöver inte bry sig så mycket om det.

Om man utgår från pseudo-kod i main och helt enkelt skriver vad programmet skall göra så får man en bra stomme. Sen gör man om pseudokoden till (största delen) funktioner (som man inte skrivit ännu). Och så skriver man funktionerna. Och om de är komplexa så skriver man de på samma sätt som huvudprogrammet.
dangraf
Inlägg: 530
Blev medlem: 9 juni 2003, 15:30:56
Ort: göteborg

Re: hur organisera C-program?

Inlägg av dangraf »

Jag har skrivit en hel del program till större applikationer och tror för tillfället på följande struktur för att få en dynamisk uppsättning objekt att leka med.
Det kommer bli mycket kod, är inte alltid effektivt men fördelarna är att man får ett väldigt tydligt api att arbeta med samt att man har möjlighet att "byta" drivrutin" i runtime om man så vill. Det är ganska lätt att porta om kod och bygga om moduler t.ex om du kommer på att du vill prata paralellt med den grafiska display eller om du vill kunna koppla in både en text och en grafisk display på samma kretskort.

Själva grund-uppbyggnaden på hur jag löser det hela är följande:
* Öppnar en SPI port och skaffar ett "handtag" av typ "ReadWriteApi.
* Öppnar en Paralell port och skaffar ett "handtag"av typ "ReadWriteApi"
* Öppnar t.ex din grafiska display med hjälp av en init-funktion där jag slänger in SPI porten som inparameter.
* Öppnar text-displayen med hjälp av en init-funktion där jag släner in Paralell-porten som inparameter.


ReadWriteApi:et skulle kunna se ut som följande i en .h fil

Kod: Markera allt

// prototype for common "write" function
  typedef uint16 (*comAdrApiWriteFnPtr)( void *pDrv, uint16 addr, uint8 *buffer, uint16 numData );

// prototype for common "read" function
  typedef uint16 (*comAdrApiReadFnPtr)( void *pDrv, uint16 addr, uint8 *buffer, uint16 numData);

  #define RW_API_MEMBERS         \
        comAdrApiWriteFnPtr                Write;  \
        comAdrApiReadFnPtr                 Read;  

typedef struct __RWApi
{
   RW_API_MEMBERS	
} RWApiBaseT; 

// help macro for functions to get a more readable code when casting pointers.
#define ComAdrApiWrite( pdrv, addr, buffer, numData )    			((RWApiBaseT *)(pdrv))->Write( pdrv, addr, buffer, numData )
#define ComAdrApiRead( pdrv,addr, buffer, numData )             	((RWApiBaseT *)(pdrv))->Read( pdrv, addr, buffer, numData )

Om du vill använda APIet i till t.ex din SPI port skulle du kunna skriva följande:

spi.h

Kod: Markera allt

typedef struct __SpiCom
{
	RW_API_MEMBERS 
	uint8 spiNbr;
}SpiComT;

spiInit( SpiComT *pThis, uartNbr);

spi.c

Kod: Markera allt

// static gör att funktionen blir "privat" för enbart filen "spi.c" vilket gör att även andra filer kan ha funktioner med samma namn utan att de "krockar"
static uint16 spiRead( void *pDrv, uint16 addr, uint8 *buffer, uint16 numData )
{
   // some  implementation.
}
static uint16 spiWrite( void *pDrv, uint16 addr, uint8 *buffer, uint16 numData)
{
	SpiComT *pThis = pDrv;
	switch(pThis->SpiNbr)
	{
		case SPI_NR_1: // do something
		break;
		case SPI_NR_2: // do something elese
		break
		default:
	}
}
spiInit( SpiComT *pThis, spiNbr)
{
	pThis->spiNbr = spiNbr;
	pThis->Write = spiWrite;
	pThis->Read = spiRead;
}
Det som händer är att alla "medlemma i "RW_API_MEMBERS" kommer läggas först i din strukten"SpiComT" och därefter läggs alla privata variabler som är specifika för just den drivrutinen. t.ex paralell porten borde inte behöva några parametrar alls medan en UART kanske även vill hålla koll på baud-raten beroende på applikation eller annat konstigt som man kommer på. Den applikation som vill använda SPI porten eller UART porten kör en "cast" på strukten till "RWApiBaseT" där read och write funktionen alltid ligger på samma "adress-offset" från grund-adressen.

De delar som är hårdvaruspecifika och alltid fasta brukar jag lägga i en separat fil som heter t.ex spi_config.h så att man lät kan kopiera och modifiera och är specifik för just det projektet/kretskortet du arbetar med.


Om man bygger vidare på detta sätt skulle man även kunna göra ett "terminalApi" som har hand om text-hantering så att man har sitt api för "printf" eller "moveto" kommandon oberoende av om man kör en grafisk eller text display.

Det finns möjlighet att "ärva" från andra api-er t.ex om man vill göra ett lite mer avancerat API för just grafiska displayen då man vill rita linjer eller t.ex cirklar. Det är viktigt att om man börjar ge sig på detta på olika klasser så måste dessa alltid hamna i rätt ordning i strukten. Alltså att t.ex ett GraphApi alltid har ett TERMINAL_MEMBERS först i strukten innan "GRAPHICAL_MEMBERS" läggs in.

Ett vidare exempel på en main-funktion:

Kod: Markera allt

int main(void)
{
        char textString[] = "hejsan hej";
        SpiComT spiHandle;
        UartComT uartHandle;

        Terminal terminalHandle;
	
	spiInit( &spiHandle, SPI_NR_1);
       
       // exempel på hur man skriver en sträng till spi-porten med hjälp av hjälp-macrot.
        ComAdrApiWrite( &spiHandle,
                               0x1234, 
                              textString,
                              sizeof( textString ) );

        uartInit( &uartHandle, 19200, UART_2);

        graphDisplayInit( &terminalHandle, &spiHandle);

        ApplicationInit( &teminalHandle, uartHandle );

       while(1)
       {
           spiExe();        // hanterar SPI, tex bit-banginig.
           uart(Exe();     // hanterar Uart,
           graphExe();    // hanterar grafiska displayen
           ApplicationExe();  // hanterar applicationen.
        }
}
Koden jag skrivit är inte kompilerad och säkert full av fel, men hoppas du hänger med på principen :-)
Det är lätt att virra bort sig i funktions-pekar träsket men får du till det så finns det många fördelar. T.ex så har jag bytt ut de hårdvarunära delarna i mina projekt så att applikationerna kan kompileras och debuggas i t.ex visual-studio innan man kör vidare och testar på riktig hårdvara.

Lycka till! ;-)
Skriv svar