Come faccio a TDD su dispositivi integrati?


17

Non sono nuovo nella programmazione e ho anche lavorato con alcuni C e ASM di basso livello su AVR, ma non riesco davvero a pensare a un progetto C integrato su larga scala.

Essendo degenerato dalla filosofia di Ruby di TDD / BDD, non riesco a capire come le persone scrivono e testano codice come questo. Non sto dicendo che è un brutto codice, non capisco come possa funzionare.

Volevo approfondire la programmazione di basso livello, ma non ho davvero idea di come affrontarlo, dal momento che sembra una mentalità completamente diversa a cui sono abituato. Non ho problemi a capire l'aritmetica dei puntatori o il modo in cui funziona l'allocazione della memoria, ma quando vedo quanto sia complesso il codice C / C ++ rispetto a Ruby, sembra semplicemente impossibile.

Dal momento che mi sono già ordinato una scheda Arduino, mi piacerebbe molto di più in qualche C di basso livello e capire davvero come fare le cose correttamente, ma sembra che nessuna delle regole delle lingue di alto livello si applichi.

È anche possibile fare TDD su dispositivi integrati o quando si sviluppano driver o cose come bootloader personalizzato, ecc.?


3
Ciao Darth, non possiamo davvero aiutarti a superare la tua paura di C, ma la domanda su TDD sui dispositivi incorporati è in argomento qui: ho modificato la tua domanda per caratterizzarlo invece.

Risposte:


18

Prima di tutto, dovresti sapere che cercare di capire il codice che non hai scritto è 5 volte più difficile che scriverlo da solo. Puoi imparare C leggendo il codice di produzione, ma ci vorrà molto più tempo di imparare facendo.

Essendo degenerato dalla filosofia di Ruby di TDD / BDD, non riesco a capire come le persone scrivono e testano codice come questo. Non sto dicendo che è un brutto codice, non capisco come possa funzionare.

È un'abilità; ci si migliora. La maggior parte dei programmatori C non capisce come le persone usano Ruby, ma ciò non significa che non possano.

È anche possibile fare TDD su dispositivi integrati o quando si sviluppano driver o cose come bootloader personalizzato, ecc.?

Bene, ci sono libri sull'argomento:

inserisci qui la descrizione dell'immagine Se un calabrone può farlo, puoi farlo anche tu!

Tieni presente che l'applicazione di pratiche da altre lingue di solito non funziona. TDD è piuttosto universale però.


2
Ogni TDD che ho visto per i miei sistemi embedded ha trovato solo errori nei sistemi che avevano errori facili da risolvere che avrei trovato facilmente da solo. Non avrebbero mai trovato ciò di cui ho bisogno di aiuto, le interazioni dipendenti dal tempo con altri chip e le interazioni di interruzione.
Kortuk,

3
Questo dipende dal tipo di sistema su cui stai lavorando. Ho scoperto che l'uso di TDD per testare il software, unito a una buona astrazione dell'hardware, mi consente in realtà di deridere quelle interazioni dipendenti dal tempo molto più facilmente. L'altro vantaggio che le persone spesso guardano è che i test, essendo automatizzati, possono essere eseguiti in qualsiasi momento e non richiedono che qualcuno sieda sul dispositivo con un analizzatore logico per assicurarsi che il software funzioni. TDD mi ha salvato settimane di debug solo nel mio attuale progetto. Spesso sono gli errori che riteniamo facili da individuare a causare errori che non ci aspettiamo.
Nick Pascucci,

Inoltre, consente lo sviluppo e il test off-target.
cp.engr,

Posso seguire questo libro per comprendere TDD per C non incorporato? Per qualsiasi spazio utente programmazione C?
Scambio eccessivo il

15

Una grande varietà di risposte qui ... principalmente affrontando il problema in vari modi.

Scrivo software e firmware integrati di basso livello da oltre 25 anni in una varietà di lingue, principalmente C (ma con deviazioni in Ada, Occam2, PL / M e vari assemblatori lungo la strada).

Dopo un lungo periodo di riflessione, tentativi ed errori, ho optato per un metodo che ottiene risultati abbastanza rapidamente ed è abbastanza facile creare involucri di test e imbracature (dove AGGIUNGONO VALORE!)

Il metodo va in questo modo:

  1. Scrivi un driver o un'unità di codice di astrazione hardware per ogni periferica principale che desideri utilizzare. Scrivere anche uno per inizializzare il processore e ottenere tutto impostato (questo rende l'ambiente amichevole). In genere su piccoli processori integrati, ad esempio il tuo AVR, potrebbero esserci 10-20 unità di questo tipo, tutte piccole. Potrebbero essere unità per l'inizializzazione, conversione A / D in buffer di memoria non scalati, uscita bit a bit, ingresso pulsante (nessun debounce appena campionato), driver di modulazione della larghezza di impulso, UART / driver seriali semplici l'uso interrompe e piccoli buffer I / O. Potrebbero essercene alcuni altri, ad esempio driver I2C o SPI per EEPROM, EPROM o altri dispositivi I2C / SPI.

  2. Per ciascuna unità di astrazione hardware (HAL) / driver, scrivo quindi un programma di test. Questo si basa su una porta seriale (UART) e sull'iniziatore del processore, quindi il primo programma di test utilizza solo quelle 2 unità e fa solo input e output di base. Questo mi permette di provare che posso avviare il processore e che ho un I / O seriale di supporto per il debug funzionante. Una volta che funziona (e solo allora) sviluppo gli altri programmi di test HAL, costruendoli sopra le note unità UART e INIT. Quindi potrei avere programmi di test per leggere gli input bit per bit e visualizzarli in una bella forma (esadecimale, decimale, qualunque cosa) sul mio terminale di debug seriale. Posso quindi passare a cose più grandi e complesse come i programmi di test EEPROM o EPROM - faccio guidare la maggior parte di questi menu in modo da poter selezionare un test da eseguire, eseguirlo e vedere il risultato. Non riesco a scriverlo ma di solito non lo faccio

  3. Una volta che ho tutti i miei HAL in esecuzione, trovo quindi un modo per ottenere un normale tick del timer. Questo è in genere a una velocità compresa tra 4 e 20 ms. Questo deve essere regolare, generato in un interrupt. Il rollover / overflow dei contatori è di solito come si può fare. Il gestore di interrupt quindi INCREMENTA una dimensione di byte "semaforo". A questo punto puoi anche giocherellare con la gestione dell'alimentazione, se necessario. L'idea del semaforo è che se il suo valore è> 0 è necessario eseguire il "ciclo principale".

  4. EXECUTIVE esegue il loop principale. Praticamente aspetta solo che quel semaforo diventi diverso da 0 (l'astratto questo dettaglio è lontano). A questo punto, puoi giocare con i contatori per contare questi tick (perché conosci la frequenza di tick) e quindi puoi impostare flag che mostrano se l'attuale tick esecutivo è per un intervallo di 1 secondo, 1 minuto e altri intervalli comuni potrebbe voler usare. Una volta che il dirigente sa che il semaforo è> 0, esegue un singolo passaggio attraverso ogni funzione di "aggiornamento" dei processi "applicazione".

  5. I processi dell'applicazione siedono efficacemente uno accanto all'altro e vengono eseguiti regolarmente da un segno di spunta "aggiornamento". Questa è solo una funzione chiamata dall'esecutivo. Questo è in realtà un multi-tasking per le persone povere con un RTOS casalingo molto semplice che si basa su tutte le applicazioni che entrano, fanno un piccolo lavoro ed escono. Le applicazioni devono mantenere le proprie variabili di stato e non possono eseguire calcoli a lungo termine poiché non esiste un sistema operativo preventivo per forzare l'equità. OVVIAMENTE il tempo di esecuzione delle applicazioni (cumulativamente) dovrebbe essere inferiore al periodo di tick maggiore.

L'approccio sopra è facilmente esteso in modo da poter aggiungere elementi come stack di comunicazione che vengono eseguiti in modo asincrono e che i messaggi di comunicazione possono quindi essere recapitati alle applicazioni (si aggiunge una nuova funzione a ciascuna che è il "rx_message_handler" e si scrive un dispatcher di messaggi che figure a quale domanda spedire).

Questo approccio funziona praticamente per qualsiasi sistema di comunicazione che ti interessa nominare: può (e ha fatto) funzionare per molti sistemi proprietari, sistemi di comunicazione standard aperti, funziona anche per stack TCP / IP.

Ha anche il vantaggio di essere costruito in pezzi modulari con interfacce ben definite. Puoi estrarre e estrarre pezzi in qualsiasi momento, sostituire pezzi diversi. Ad ogni punto lungo il percorso è possibile aggiungere imbracature di prova o manipolatori che si basano sulle parti dello strato inferiore buone note (le cose di seguito). Ho scoperto che all'incirca dal 30% al 50% di un progetto può trarre vantaggio dall'aggiunta di test di unità appositamente scritti che di solito sono abbastanza facilmente aggiunti.

Ho fatto un ulteriore passo avanti (un'idea che ho preso da qualcun altro che lo ha fatto) e ho sostituito il livello HAL con un equivalente per PC. Ad esempio, è possibile utilizzare C / C ++ e winforms o simili su un PC e scrivendo ATTENTAMENTE il codice è possibile emulare ciascuna interfaccia (ad esempio EEPROM = un file del disco letto nella memoria del PC) e quindi eseguire l'intera applicazione integrata su un PC. La possibilità di utilizzare un ambiente di debug intuitivo può far risparmiare molto tempo e fatica. Solitamente solo progetti veramente grandi possono giustificare questo sforzo.

La descrizione sopra è qualcosa che non è unico nel modo in cui faccio le cose su piattaforme incorporate - mi sono imbattuto in numerose organizzazioni commerciali che fanno simili. Il modo in cui è fatto è di solito molto diverso nell'attuazione, ma i principi sono spesso più o meno gli stessi.

Spero che quanto sopra dia un po 'di sapore ... questo approccio funziona per piccoli sistemi embedded che funzionano in pochi kB con una gestione aggressiva della batteria fino a mostri di 100K o più linee sorgente che funzionano permanentemente. Se si esegue "incorporato" su un sistema operativo di grandi dimensioni come Windows CE o simili, tutto quanto sopra è completamente irrilevante. Ma questa non è una vera programmazione embedded, comunque.


2
La maggior parte delle periferiche hardware non è possibile testare tramite un UART, perché molto spesso si è interessati principalmente alle caratteristiche di temporizzazione. Se vuoi controllare una frequenza di campionamento ADC, un duty cycle PWM, il comportamento di qualche altra periferica seriale (SPI, CAN ecc.) O semplicemente il tempo di esecuzione di una parte del tuo programma, non puoi farlo attraverso un UART. Qualsiasi serio test del firmware incorporato include un oscilloscopio: non è possibile programmare i sistemi incorporati senza uno.

1
Oh sì, assolutamente. Ho appena dimenticato di menzionarlo. Ma una volta che hai UART installato e funzionante, è molto facile impostare test o casi di test (che è la domanda di cui trattasi), stimolare le cose, consentire l'input dell'utente, ottenere risultati e visualizzare in modo amichevole. Questo + tuo CRO semplifica la vita.
Velocemente, il

2

Il codice che ha una lunga storia di sviluppo incrementale e ottimizzazioni per più piattaforme, come gli esempi che hai scelto, è solitamente più difficile da leggere.

La cosa positiva di C è che è effettivamente in grado di estendere le piattaforme su una vasta gamma di ricchezza API e prestazioni hardware (e la loro mancanza). MacVim ha funzionato in modo reattivo su macchine con oltre 1000 volte meno memoria e prestazioni del processore rispetto a un tipico smartphone oggi. Il tuo codice Ruby può? Questo è uno dei motivi per cui potrebbe sembrare più semplice degli esempi C maturi che hai scelto.


2

Sono nella posizione opposta di aver trascorso la maggior parte degli ultimi 9 anni come programmatore C, e di recente ho lavorato su alcuni front-end di Ruby on Rails.

Le cose su cui lavoro in C sono principalmente sistemi personalizzati di medie dimensioni per il controllo di magazzini automatici (costo tipico di alcune centinaia di migliaia di sterline, fino a un paio di milioni). La funzionalità di esempio è un database in-memory personalizzato, che si interfaccia ai macchinari con alcuni tempi di risposta brevi e una gestione di livello superiore del flusso di lavoro del magazzino.

Posso dire innanzitutto che non facciamo alcun TDD. In diverse occasioni ho provato a introdurre unit test, ma in C è più un problema di quanto non valga la pena, almeno quando si sviluppa software personalizzato. Ma direi che TDD è molto meno necessario in C di Ruby. Principalmente, questo è solo perché C è compilato e se si compila senza avvisi, hai già eseguito una quantità abbastanza simile di test ai test di impalcatura generati automaticamente rspec in Rails. Ruby senza test unitari non è fattibile.

Ma quello che direi è che C non deve essere duro come alcune persone lo fanno. Gran parte della libreria C standard è un casino di incomprensibili nomi di funzioni e molti programmi C seguono questa convenzione. Sono contento di dire che non lo facciamo, e in effetti abbiamo molti wrapper per la funzionalità di libreria standard (ST_Copy invece di strncpy, ST_PatternMatch invece di regcomp / regexec, CHARSET_Convert invece di iconv_open / iconv / iconv_close e così via). Il nostro codice C interno mi legge meglio della maggior parte delle altre cose che ho letto.

Ma quando dici che le regole di altre lingue di livello superiore non sembrano applicarsi, non sono d'accordo. Un buon codice C "sente" orientato agli oggetti. Si vede spesso un modello di inizializzazione di un handle a una risorsa, chiamando alcune funzioni che passano l'handle come argomento e infine rilasciano la risorsa. In effetti, i principi di progettazione della programmazione orientata agli oggetti derivavano in gran parte dalle cose buone che la gente faceva nei linguaggi procedurali.

I tempi in cui C diventa davvero complicato sono spesso quando si fanno cose come driver di dispositivo e kernel del sistema operativo che sono fondamentalmente di livello molto basso. Quando si scrive un sistema di livello superiore, è anche possibile utilizzare le funzionalità di livello superiore di C ed evitare la complessità di basso livello.

Una cosa molto interessante che potresti voler dare un'occhiata è il codice sorgente C per Ruby. Nei documenti dell'API Ruby (http://www.ruby-doc.org/core-1.9.3/) puoi fare clic e vedere il codice sorgente per i vari metodi. La cosa interessante è che questo codice sembra abbastanza carino ed elegante - non sembra così complesso come potresti immaginare.


" ... puoi anche usare le funzionalità di livello superiore di C ... ", come ci sono? ;-)
alk il

Intendo livello superiore rispetto alla manipolazione dei bit e alla procedura guidata puntatore a puntatore che si tende a vedere nel codice del tipo di driver di dispositivo! E se non sei preoccupato per il sovraccarico di un paio di chiamate di funzione, puoi creare un codice C che sembra davvero di alto livello.
asc99c,

" ... puoi creare un codice C che sembra davvero di alto livello. " Assolutamente, sono pienamente d'accordo. Ma anche se " ... le funzionalità di livello superiore ... " non sono di C, ma nella tua testa, vero?
alk il

2

Quello che ho fatto è separare il codice dipendente dal dispositivo dal codice indipendente dal dispositivo, quindi testare il codice indipendente dal dispositivo. Con buona modularità e disciplina, finirai con una base di codice per lo più ben collaudata.


2

Non c'è motivo per cui non puoi. Il problema è che potrebbero non esserci simpatici framework "unit of the shelf" come quelli che hai in altri tipi di sviluppo. Va bene. Significa solo che devi adottare un approccio "roll-your-own" ai test.

Ad esempio, potresti dover programmare la strumentazione per produrre "input falsi" per i tuoi convertitori A / D o forse dovrai generare un flusso di "dati falsi" per la risposta del tuo dispositivo incorporato.

Se incontri resistenza all'uso della parola "TDD" chiamalo "DVT" (test di verifica del progetto) che renderà le EE più a suo agio con l'idea.


0

È anche possibile fare TDD su dispositivi integrati o quando si sviluppano driver o cose come bootloader personalizzato, ecc.?

Qualche tempo fa dovevo scrivere un bootloader di primo livello per una CPU ARM. In realtà ce n'è uno dei ragazzi che vendono questa CPU. E abbiamo usato uno schema in cui il loro bootloader avvia il nostro bootloader. Ma questo è stato lento, dato che dovevamo eseguire il flashing di due file in NOR flash anziché uno, dovevamo creare le dimensioni del nostro bootloader nel primo bootloader e ricostruirlo ogni volta che cambiavamo il nostro bootloader e così via.

Così ho deciso di integrare le funzioni del loro bootloader nel nostro. Poiché era un codice commerciale, dovevo assicurarmi che tutto funzionasse come previsto. Quindi ho modificato QEMU per emulare i blocchi IP di quella CPU (non tutti, solo quelli che toccano il bootloader), e aggiungo codice a QEMU per "stampare" tutti i registri che controllano cose come PLL, UART, controller SRAM e presto. Quindi ho aggiornato il nostro bootloader per supportare questa CPU e, successivamente, ho confrontato l'output che fornisce al nostro bootloader e al loro emulatore, questo mi aiuta a rilevare diversi bug. È stato scritto in parte in assemblatore ARM, in parte in C. Anche dopo che QEMU modificato mi ha aiutato a catturare un bug, che non riuscivo a catturare usando JTAG e una vera CPU ARM.

Quindi anche con C e assemblatore puoi usare i test.


-2

Sì, è possibile eseguire TDD su software incorporato. Le persone che lo dicono non sono possibili, non rilevanti o non applicabili non sono corrette. C'è un grande valore da guadagnare da TDD in embedded come con qualsiasi software.

Il modo migliore per farlo, tuttavia, non è quello di eseguire i test sulla destinazione ma di astrarre le dipendenze hardware e compilare ed eseguire sul PC host.

Quando fai TDD, creerai ed eseguirai molti test. Hai bisogno di software per aiutarti a farlo. Volete un framework di test che lo renda facile e veloce, con scoperta automatica dei test e generazione di simulazioni.

L'opzione migliore per C in questo momento è Ceedling. Ecco un post che ho scritto al riguardo:

http://www.electronvector.com/blog/try-embedded-test-driven-development-right-now-with-ceedling

Ed è costruito in Ruby! Non è necessario conoscere alcun Ruby per usarlo però.


le risposte sono attese da sole. Costringere i lettori ad accedere a risorse esterne per scoprire la sostanza è disapprovato allo Stack Exchange ("leggi l'articolo o dai un'occhiata a Ceedling"). Prendi in considerazione la modifica per adattarla alle norme sulla qualità del sito
moscerino del

Ceedling ha qualche meccanismo per supportare eventi asincroni? Uno degli aspetti più impegnativi delle applicazioni embedded in tempo reale è che si occupano di ricevere input da sistemi molto complessi che sono essi stessi difficili da modellare ...
Jay Elston,

@Jay Non ha nulla di specifico per supportarlo. Tuttavia, ho avuto successo testando questo genere di cose con derisione e impostando un'architettura per supportarla. Ad esempio, di recente ho lavorato a un progetto in cui gli eventi guidati da interrupt sono stati inseriti in una coda e quindi consumati in una macchina a stati "gestore eventi". Questa era essenzialmente solo una funzione che veniva chiamata ogni volta che si verificava un evento. Durante il test di tale funzione, ho potuto deridere la chiamata di funzione che ha estratto gli eventi dalla coda e quindi simulare qualsiasi evento che si verifica nel sistema. Test di guida aiuta anche qui.
Cherno,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.