Wokwi e Custom Chip
Quando tra i componenti di wokwi non c'è il chip che vogliamo includere nella
simulazione, ci tocca crearne uno. Wokwi espone al programmatore la api
wokwi-api.h
tramite la quale modelliamo il comportamento del
nostro chip custom.
Oltre alla api in C possiamo modellare il nostro chip custom con il linguaggio rust o in verilog, ma lo stato di sviluppo attualmente di questi due linguaggi è alpha.
Per iniziare, dalla lista dei componenti di wokwi (pulsante +), scorriamo la
lista verso il basso incontrando la categoria Misc
(Miscelanea),
selezioniamo Custom Chip
. La finestra di dialogo (Create a Custom
Chip) che compare ci permette di scegliere il linguaggio tra quelli elencati
(scegliamo il C poiché è raccomandato) e assegniamo un nome al nuovo chip.
Confermiamo la scelta premendo il pulsante CREATE CHIP
, in
conseguenza di ciò due nuovi file sono aggiunti al progetto, uno ha estensione
.json
e l'altro .c
.
Graficamente il chip si presenta alla destra come una PCB con 4 terminali, due
sono l'alimentazione, e due uscita e ingresso. Il numero di pin I/O può essere
aumentato modificando il file .json
. L'immagine sotto mostra
quanto descritto prima, cioè sono stati aggiunti al progetto i file
test-mychip.chip.json e test-mychip.chip.c, il termine chip
viene aggiunto automaticamente per cui evitiamo di specificarlo nel nome del
componente, limitandoci a scrivere la sigla del componente o il nome generico,
ad esempio anemometro.
Nell'area editor è visibile il contenuto del file .json
, in
ordine dall'alto verso il basso abbiamo il nome del chip, l'autore, un array
di nome pins
contenente i nomi dei 4 pins. Per ultimo, l'array
controls
, grazie al quale durante la simulazione cliccando sul
componente compare un cursore, che useremo ad esempio per variare la velocità
del vento o altre grandezze come temperatura, umidità ecc. Possiamo
specificare di aggiungere più cursori, senza esagerare poiché prendono spazio
e quando compaiono coprono i componenti sottostanti. Attualmente
controls
è vuoto e quindi non comparirà nessun cursore.
Nell'area destra, stazionando con il puntatore del mouse su uno dei 4 pin comparirà il suo nome. Già che ci siamo avviamo la simulazione per vedere come muta l'interfaccia grafica.
Se non abbiamo commesso errori la simulazione si avvia e nell'area in basso a destra (area terminale) compare la tab CHIP CONSOLE
nella quale possiamo stampare stringhe di debug, come in effetti avviene osservando il file .c
. Nel file .c
compare la seguente chiamata a funzione printf
. Ricordate sempre di aggiungere il carattere terminatore '\n'
alla fine, diversamente non comparirà nulla nel terminale.
Il file .c
contiene già una implementazione di base. La funzione void chip_init()
verrà chiamata per ogni istanza presente nell'area grafica. Cioè se il nostro progetto richiede due o più chip custom, lo selezioniamo e con CTRL+C
e CTRL+V
copiamo e incolliamo. Questa operazione modifica il file diagram.json
, aggiungendo una istanza a quella già presente, vediamo le due righe interessate di seguito.
Il campo attrs
risulta vuoto, al suo interno possiamo specificare più variabili e il loro valore, il valore di ognuna può essere acquisito da codice C attraverso la api di wokwi. Una delle prime cose da fare nella funzione chip_init()
è proprio questa acquisizione. Come saranno usati questi valori determina il modo in cui procedere, ci sono i seguenti 3 modi di procedere:
- Valori usa e getta: Quando il valore acquisito serve solo all'interno della funzione
chip_init()
salviamo il valore in una variabile locale. - Valori in variabili globali: Il valore verrà salvato in una variabile dichiarata globale, per cui dobbiamo prima dichiarare questa variabile. Attenzione che il nome della variabile potrebbe anche essere usato altrove dalla api, stessa cosa per le macro.
- Valori in user_data: Il tipo di dato
chip_state_t
è attualmente una struttura dati priva di membri, possiamo specificare tutti i membri che necessitiamo dentro questa struttura. Molte delle funzioni di wokwi prendono come argomento un puntatore a tipo genericovoid *user_data
a queste possiamo passare come argomento la variabile puntatorechip
dichiarata locale dentro la funzionechip_init()
. Attualmente ho verificato che la variabilechip
può essere anche dichiarata globale e quindi visibile da tutte le funzioni del chip custom.
Occupiamoci della struttura chip_state_t
. Decidiamo per adesso di inserire i 4 pins del chip, la struttura si presenta cosi:
Per definire un pin serve usare il tipo pin_t
seguito dal nome della variabile. Spostiamoci nella funzione chip_init()
per inizializzare i pins. Ogni pin deve essere inizializzato e tra le api troviamo la funzione pin_t pin_init(const char *name, uint32_t mode)
L'argomento mode
può assumere i seguenti valori:
INPUT 0
INPUT_PULLUP 2
INPUT_PULLDOWN 3
OUTPUT_LOW 16
OUTPUT_HIGH 17
ANALOG 4
Non usate le costanti numeriche manifeste (cioè 0, 2, 3 ecc, ma i loro nomi.
Le chiamate a pin_init()
devono trovarsi dentro la funzione chip_init()
o in un funzione da questa chiamata. Se è necessario riconfigurare il pin durante la simulazione useremo la funzione void pin_mode(pin_t pin, uint32_t mode)
. Di seguito vediamo un esempio di inizializzazione:
Certe volte è necessario inizializzare anche il pin VCC, altre volte non serve. Mentre non ho trovato alcuna utilità ad inizializzare il pin GND. Per cui rimuovete i pin che non vi serve inizializzare. Ho trovato utile inizializzare VCC quando il chip viene alimentato durante la simulazione o da un relay o da arduino, ad esempio delle fotocellule che dopo avere ricevuto alimentazione portano il pin OUT
da LOW
ad HIGH
per 100ms per poi tornare LOW
.
Attributi
Un attributo è una coppia nome:valore
specificato in uno dei file .json
. Ogni attributo per il codice C è un parametro di ingresso. Prima di potere leggere il valore di un attributo dobbiamo inizializzarlo. La funzione attr_init() si occupa della inizializzazione. Vediamo un esempio per acquisire da C il valore di un attributo di nome initOutState
specificato nel file diagram.json seguente
:
Considerate anche la possibilità che l'attributo non sia presente, in tal caso da codice C dobbiamo prevedere un valore di default. Di seguito la funzione chip_init() che inizializza l'attributo, legge il valore con attr_read()
e lo stampa con printf()
:
In questo caso stamperà initOutState = 0
, mentre se rimuovete l'attributo "initOutState": "0"
da diagram.json
stamperà
initOutState = 1
.
Attributi del chip
Nel file name.chip.json
possiamo specificare alcuni attributi speciali.
Uno di questi ci fa comparire un cursore regolabile durante la simulazione se clicchiamo sul chip grafico. Nel file in questione esiste la proprietà controls
la quale funge da contenitore dove specificare un array di oggetti. Attualmente il solo tipo di oggetto valido è un cursore con 6 proprietà che lo descrivono. Di seguito due oggetti cursore usati per un generatore di frequenza pwm specificati nel file name.chip.json
:
Ciò che compare durante la simulazione lo possiamo vedere nell'immagine qui sotto.
Entrambe i cursori possono essere regolati durante la simulazione, ciò ci permette di sperimentare applicazioni che fanno uso ad esempio di interrupt
, come un frequenzimetro, un anemometro e persino controllare la posizione di servo RC direttamente collegato al chip. Per acquisire i valori di questi cursori usiamo le stesse due funzioni viste prima, cioè attr_init()
e attr_read()
. L'implementazione del chip richiede l'acquisizione del valore di questi cursori, attualmente non mi risulta ci siano delle funzioni specifiche per agganciare l'evento ad una funzione utente (callback), per cui ho usato un timer software che chiama una funzione ogni 100ms, al suo interno c'è il codice per leggere i cursori attraverso la funzione attr_read()
. La prossima sezione riguarda appunto l'uso dei timer software, con un esempio che mostra come leggere periodicamente i cursori.
Software Timer
La gestione dei timer è affidata a quattro sole funzioni, di cui una conta il tempo in microsecondi e l'altra in nanosecondi. Per non mortificare le prestazioni, si consiglia di usare la funzione void timer_start(uint32_t timer_id, uint32_t micros, bool repeat)
la quale opera in microsecondi. Prima che un timer possa essere usato è necessario inizializzarlo, ciò richiede la compilazione di una struttura composta da 2 campi. Il codice sotto dichiara e inizializza la variabile tcfg_controls
, inizializza il timer e lo avvia. Al membro .user_data
viene assegnato il puntatore chip
, mentre al membro .callback
viene assegnato il nome della funzione da chiamare.
La funzione tcb_controls
(nome a piacere) deve prendere un argomento puntatore a tipo generico e non restituire nulla, segue un esempio:
Vero che il nome può essere scelto a piacere, ma per evitare confusione il nome della funzione inizia con tcb che mi fa capire che si tratta di una timer callback così posso distinguerla facilmente da altre funzioni generiche. Stessa cosa per tid che sta per timer id.
Il puntatore u_data
non può essere usato direttamente, per cui il cast esplicito lo trasforma in un puntatore di nome chip
a tipo chip_state_t
. Questa funzione viene chiamata periodicamente durante la simulazione ed è arrivato il momento di vedere e provare il codice. Il codice seguente ha l'obbiettivo di mostrare il timer in azione. Nella funzione chip_init()
inizializzo gli attributi assegnando loro un valore di default, attr_freq
e attr__dc
sono membri di chip
. In base al valore dell'attributo "initOutState"
inizializzo il pin chip->out
. Alla fine configuro il timer e lo avvio con timer_
start
()
.
Il link sopra punta direttamente al progetto da simulare, potrete constatare voi stessi come la CHIP CONSOLE
viene inondata di messaggi molte volte con stessi valori dei cursori.
Freq Hz: 3032
DutyCycle % : 32
Freq Hz: 3032
DutyCycle % : 32
Freq Hz: 3032
DutyCycle % : 32
Questo non è certo un problema per un test veloce, ma vogliamo stampare il valore dei cursori solo quando l'utente sposta il cursore. Risolviamo questo comportamento aggiungendo una struttura dove salvare i vecchi valori di frequenza e duty cycle e modifichiamo la funzione aggiungendo due condizioni if
come mostrato di seguito:
Ci manca qualche rifinitura è il nostro generatore pwm è pronto per essere impiegato in uno sketch arduino. La versione a seguire aggiunge un timer e una callback che è quella che genera il segnale di uscita pwm. Vediamo le modifiche dall'inizio.
Abbiamo definito un nuovo tipo pwm_t
che usiamo dentro chip_state_t
dove abbiamo eliminato i pin di alimentazione che non ci servono. La funzione tcb_pwm()
viene chiamata dal timer tid_pwm
. Questa funzione legge lo stato di chip->out
ne inverte lo stato per poi avviare lo stesso timer. Il tempo chip->pwm.t01[out_state]
cambia in relazione alla posizione del cursore Duty Cycle, ma anche in relazione allo stato della uscita out.
Nella funzione chip_init()
abbiamo agginto il timer tid_pwm
e lo abbiamo avviato. Quindi il pwm viene generato dall'inizio della simulazione, ma si potrebbe desiderare di avviarlo a comando ad esempio tramite il pin INP
. Prima di implementare questa funzionalità è bene prendere confidenza con le api I/O perché la funzione pin_watch ci permette di agganciare un callback al cambiamento di stato di un pin.
Questa introduzione sui chip custom si ferma qui, penso che sia sufficiente per farsi una idea di base, per il momento non c'è nulla di complicato, ma sappiate che ci sono le api per SPI, i2c ecc, clicca qui per la documentazione ufficiale.
Prosegui la lettura per scoprire la api pin_watch.
Quest'opera è distribuita con Licenza Creative Commons Attribuzione - Condividi allo stesso modo 4.0 Internazionale
Nessun commento:
Posta un commento