martedì 12 dicembre 2023

Wokwi custom chip 1

API pin watch

Se proviamo a tradurre watch in italiano, la prima traduzione sarà orologio, le altre, guardare e osservare sono più calzanti in questo caso. Osservare lo stato di un pin e agire in relazione allo stato di questo. La funzione pin_watch() abilita l'osservazione di un pin, mentre pin_watch_stop() la disabilità. Sotto vediamo il prototipo delle due funzioni che ci serve per capire come usarle.

bool pin_watch(pin_t pin, pin_watch_config_t *config);
void pin_watch_stop(pin_t pin);

L'argomento pin_t pin sappiamo cosa essere, per cui concentriamoci sull'argomento di tipo pin_watch_config_t, il quale è una struttura così composta:

typedef struct {
void *user_data;
uint32_t edge;
void (*pin_change)(void *user_data, pin_t pin, uint32_t value);
} pin_watch_config_t;

user_data deve contenere un puntatore che verrà usato come argomento nella funzione di callback pin_change.

edge ci permette di specificare per quale fronte del segnale vogliamo che sia chiamata la callback pin_change. Il campo edge può assumere i seguenti valori:

  • FALLING
    Una transizione High -> Low determina la chiamata a funzione callback specificata nel campo pin_change.
  • RISING
    Una transizione Low -> High determina la chiamata a funzione callback specificata nel campo pin_change.
  • BOTH
    Una transizione High -> Low o Low -> High determina la chiamata a funzione callback specificata nel campo pin_change.

Sembra complicato, ma come vedrete risulta abbastanza semplice configurare e abilitare l'osservazione di un pin. Attenzione però ad abilitare più osservazioni sullo stesso pin, vedremo dopo cosa possiamo fare e cosa no, per adesso lo usiamo nel modo più utile e frequente. Vediamo subito quanto semplice sia l'inizializzazione e la chiamata a pin_watch() presente nella funzione chip_init():

void chip_init() {
setvbuf(stdout, NULL, _IOLBF, 1024); // Limit output buffering to a single line
chip_state_t *chip = malloc(sizeof(chip_state_t));
printf("Hello from custom chip!\n");
// inizializza il pin INP come ingresso con pull-up
chip->inp = pin_init("INP", INPUT_PULLUP);

// dichiara e inizializza winp_cfg (watch inp config)
pin_watch_config_t winp_cfg = {
.edge = FALLING,
.pin_change = wcb_inp,
.user_data = chip
};
// abilita l'osservazione
pin_watch(chip->inp, &winp_cfg);
// pin__^ ^__watch config
}

Aggiungiamo l'implementazione della seguente callback dopo la funzione chip_init() e proviamo a compilare:

void wcb_inp(void *u_data, pin_t pin, uint32_t value) {
chip_state_t *chip = (chip_state_t*)u_data;
chip->on_off = !chip->on_off;
if (chip->on_off) {
printf("on\n");
} else {
printf("off\n");
}
}

Scopriamo che il compilatore esce con un messaggio di errore per niente chiaro, il messaggio mette in risalto il nome del simbolo wcb_inp che è il nome che abbiamo dato alla funzione di callback. Risolviamo questo errore nel modo standard che prevede di inserire sempre i prototipi delle funzioni in testa dopo le include, oppure all'interno di un header file. Il codice seguente in effetti ora compila correttamente. Ricordiamoci che stiamo scrivendo codice C da dare in pasto al compilatore C e questo dovrebbe farci apprezzare la semplificazione di cui godiamo usando arduino.

#include "wokwi-api.h"
#include <stdio.h>
#include <stdlib.h>

// prototipi delle funzioni
void wcb_inp(void *u_data, pin_t pin, uint32_t value);
// se commenti la riga sopra ottieni errore

typedef struct {
// TODO: Put your chip variables here
pin_t out;
pin_t inp;
bool on_off;
} chip_state_t;

void chip_init() {
setvbuf(stdout, NULL, _IOLBF, 1024); // Limit output buffering to a single line
chip_state_t *chip = malloc(sizeof(chip_state_t));
printf("Hello from custom chip!\n");
// inizializza il pin INP come ingresso con pull-up
chip->inp = pin_init("INP", INPUT_PULLUP);

// configura winp_cfg (watch inp config)
pin_watch_config_t winp_cfg = {
.edge = FALLING,
.pin_change = wcb_inp,
.user_data = chip
};
// abilita l'osservazione
pin_watch(chip->inp, &winp_cfg);
// pin__^ ^__watch config
}

void wcb_inp(void *u_data, pin_t pin, uint32_t value) {
chip_state_t *chip = (chip_state_t*)u_data;
chip->on_off = !chip->on_off;
if (chip->on_off) {
printf("on\n");
} else {
printf("off\n");
}
}

Simulando il codice e premendo il pulsante, la CHIP CONSOLE viene inondata di messaggi, il motivo di ciò è legato alla simulazione del pulsante vero, che è sempre afflitto da rimbalzi dei contatti elettromeccanici, difatti cliccando sul pulsante (vedi Fig.2) possiamo disabilitare i rimbalzi e ad ogni pressione avremo una sola stampa.

Fig.1: Collegamenti di test pin_watch

Risulta sicuramente utile osservare questi rimbalzi dei contatti e wokwi ci permette di farlo in modo abbastanza semplice. Quindi riabilitiamo il rimbalzo (Fig.2), aggiungiamo l'analizzatore logico pigiando il pulsante blu (+), alla comparsa della lista dei componenti, scorriamo verso la fine fino ad incontrare l'analizzatore. Colleghiamo GND con il GND e il pin D7 con il pin INP. Avviamo la simulazione e premiamo il pulsante, proviamo a fare il tutto nell'arco di 1-2 secondi e fermiamo la simulazione. Wokwi ci chiede di salvare il file wokwi-logic.vcd, salviamolo. Questo file può essere visualizzato dal programma PulseView (sigrok) o da GtkWave. Possiamo osservare i rimbalzi dei contatti in Fig.3.

Fig.3: Primo rimbalzo alla pressione a 435ms. Rimbalzo al rilascio dopo 465ms
Fig.2: Disabbilita/abilita il rimbalzo.

Per fortuna i segnali digitali non rimbalzano, per tale motivo ci dobbiamo preoccupare dei rimbalzi solo quando colleghiamo pulsante, interruttore o relay ad un ingresso digitale.

Adesso vediamo come possiamo risolvere il problema del rimbalzo dei contatti con il solo fine di mostrare l'uso di pin_watch(), pin_watch_stop() e timer_start(). Il programma è lo stesso del precedente a cui aggiungiamo dei campi alla struttura chip_state_t e una funzione callback di un timer. Vediamo di seguito le modifiche alla struttura. Tre campi, dove gcnt è una variabile da 8-bit che useremo come contatore generico. wcfg_inp è la variabile di tipo pin_watch_config_t già usata in precedenza ma non faceva parte della struttura. tid_deb ci serve che sia nella struttura perché nella funzione tcb_debounce chiamiamo timer_stop(chip->tid_deb).

typedef struct {
// TODO: Put your chip variables here
pin_t out;
pin_t inp;
bool on_off;
uint8_t gcnt; // generic counter
pin_watch_config_t wcfg_inp;
timer_t tid_deb; // timer debounce
} chip_state_t;

Passiamo alla funzione chip_init() dove inizializziamo il timer chip->tid_deb senza avviarlo, a seguire configuriamo chip->wcfg_inp e abilitiamo l'osservazione del pin chiamando pin_watch(). Nota che edge è impostato a FALLING, quindi ad ogni transizione da HIGH -> LOW verrà eseguita la funzione wcb_inp().

void chip_init() {
setvbuf(stdout, NULL, _IOLBF, 1024); // Limit output buffering to a single line
chip_state_t *chip = malloc(sizeof(chip_state_t));
printf("Hello from custom chip!\n");
// inizializza il pin INP come ingresso con pull-up
chip->inp = pin_init("INP", INPUT_PULLUP);

timer_config_t tcfg_debounce = {
.user_data = chip,
.callback = tcb_debounce // timer callback
};
chip->tid_deb = timer_init(&tcfg_debounce);

// configura chip->wcfg_inp (watch config inp)
chip->wcfg_inp.edge = FALLING;
chip->wcfg_inp.pin_change = wcb_inp; // watch callback
chip->wcfg_inp.user_data = chip;
pin_watch(chip->inp, &chip->wcfg_inp);
}

Passiamo ad analizzare la funzione wcb_inp():

void wcb_inp(void *u_data, pin_t pin, uint32_t value) {
chip_state_t *chip = (chip_state_t*)u_data;
pin_watch_stop(pin); // <--- stop watch
timer_start(chip->tid_deb, CVT_msTOus(5), AUTO_REPEAT);
chip->on_off = !chip->on_off;
if (chip->on_off) {
printf("on\n");
} else {
printf("off\n");
}
}

Notate subito la chiamata a pin_watch_stop(), seguendo c'è l'avvio del timer ciclico che chiama la funzione tcb_debounce() ogni 5 millesimi di secondo. In questo momento l'osservazione del pin è disabilitata per cui il treno di impulsi del pulsante non ha conseguenze, allora invertiamo lo stato di chip->on_off e stampiamo on o off. Il timer da 5ms ci serve per riabilitare l'osservazione del pin ma solo se il suo stato è HIGH per 2 volte consecutive il che significa che il pulsante è stato rilasciato. Vediamo tutto ciò dentro la funzione seguente:

void tcb_debounce(void *u_data) {
chip_state_t *chip = (chip_state_t*)u_data;
bool s_inp = pin_read(chip->inp);
if (s_inp) {
chip->gcnt++;
if (chip->gcnt >= 2) {
chip->gcnt = 0;
timer_stop(chip->tid_deb);
pin_watch(chip->inp, &chip->wcfg_inp);
}
} else {
chip->gcnt = 0;
}
}

Tiriamo le somme: abbiamo imparato che quando ci serve avviare/abilitare timer/pin_watch durante la simulazione ci serve aggiungere nella struttura chip_state_t una variabile di tipo timer_t e pin_watch_config_t. Mentre non ha alcuna utilità aggiungere una variabile di tipo timer_config_t nella struttura perché timer_init() non deve essere eseguita durante la simulazione ma soltanto all'inizio dentro la funzione chip_init(). Questo vuole dire che il timer una volta configurato non può essere modificato, mentre una pin_watch_config_t può essere modificata in ogni momento.

Adesso possiamo prenderci la briga di introdurre questa funzionalità per accendere/spegnere il generatore di segnale pwm desritto nell'articolo precendete con il solo scopi di vedere quanto abbiamo imparato e se questo è sufficiente da permetterci di integrare questa funzionalità.

Link alla simulazione.

Limiti di pin_watch

Osservando il prototipo di pin_watch() notiamo che restituisce un valore di tipo bool. Restituisce true se l'osservazione del pin ha avuto successo, altrimenti restituisce false. Quando restituisce false vuole dire che c'è già abilitata l'osservazione dello stesso pin. Ad esempio:

// configura chip->wcfg_inp (watch config inp)
chip->wcfg_inp.edge = FALLING;
chip->wcfg_inp.pin_change = wcb_inp; // watch callback
chip->wcfg_inp.user_data = chip;
pin_watch(chip->inp, &chip->wcfg_inp);
//pin_watch_stop(chip->inp);

pin_watch_config_t wcfg = {
.edge = RISING,
.pin_change = wcb_due, // watch callback
.user_data = chip
};
bool res = pin_watch(chip->inp, &wcfg);
printf("res: %u\n", res);

L'ultima chiamata restituisce false. Se rimuoviamo il commento alla pin_watch_stop(chip->inp), l'ultima chiamata restituisce true. Quindi possiamo avere più configurazioni da abilitare con pin_watch, ma quella abilitata deve essere fermata prima di potere chiamare pin_watch sullo stesso pin. In altre parole non ci possono essere più osservazioni attive sullo stesso pin.

Licenza Creative Commons
Quest'opera è distribuita con Licenza Creative Commons Attribuzione - Condividi allo stesso modo 4.0 Internazionale

martedì 5 dicembre 2023

Wokwi custom chip

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.

Create a Custom Chip

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.

area di lavoro dopo la creazione del chip

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.

Terminale chip console

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.

printf("Hello from custom chip!\n");

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.

{ "type": "chip-test-mychip", "id": "chip1", "top": -66.18, "left": -446.4, "attrs": {} },
{ "type": "chip-test-mychip", "id": "chip2", "top": -96.62, "left": -488.32, "attrs": {} }

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 generico void *user_data a queste possiamo passare come argomento la variabile puntatore chip dichiarata locale dentro la funzione chip_init(). Attualmente ho verificato che la variabile chip 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: 

typedef struct {
// TODO: Put your chip variables here
pin_t out;
pin_t inp;
pin_t vcc;
pin_t gnd;
} chip_state_t;

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:

void chip_init() {
// alloca lo spazio con malloc che restituisce il puntatore chip
chip_state_t *chip = malloc(sizeof(chip_state_t));
chip->out = pin_init("OUT", OUTPUT_LOW);
chip->inp = pin_init("INP", INPUT);
chip->vcc = pin_init("VCC", INPUT);
chip->gnd = pin_init("GND", INPUT);
}

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:

{
"type": "chip-test-mychip",
"id": "chip1",
"top": 20.22,
"left": -244.8,
"attrs": { "initOutState": "0" }
}

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():

void _chip_init() {
chip_state_t *chip = malloc(sizeof(chip_state_t));
uint32_t attr_ios = attr_init("initOutState", 1);
// valore di default ^
printf("initOutState = %u\n", attr_read(attr_ios));
}

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

"controls": [
{
"id": "ctrlFreq",
"label": "Frequency",
"type": "range",
"min": 1,
"max": 10000,
"step": 1
},
{
"id": "ctrlDcycle",
"label": "Duty Cycle",
"type": "range",
"min": 1,
"max": 99,
"step": 1
}
]

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.

// tid_controls is periodic 100ms timer
timer_config_t tcfg_controls = {
.user_data = chip,
.callback = tcb_controls
};
timer_t tid_controls = timer_init(&tcfg_controls);
timer_start(tid_controls, 100000, true);
// time__^ ^__auto repeat

La funzione tcb_controls (nome a piacere) deve prendere un argomento puntatore a tipo generico e non restituire nulla, segue un esempio:

void tcb_controls(void *u_data) {
chip_state_t *chip = (chip_state_t*)u_data;
}

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().

// SPDX-License-Identifier: MIT
// Copyright 2023 Maurilio Pizzurro

// .abbreviazioni
// tid timer_id
// attr attribute
// cnt counter
// tcfg timer_config
// cb callback
// tcb timer callback
// CVT CONVERT

#include "wokwi-api.h"
#include <stdio.h>
#include <stdlib.h>

#define AUTO_REPEAT true
#define NO_REPEAT false
#define DFLT_FREQ 1
#define DFLT_DC 50
#define DLFT_IOS HIGH

#define CVT_msTOus(m) (m * 1000)

typedef struct {
// TODO: Put your chip variables here
pin_t out;
pin_t inp;
pin_t vcc;
pin_t gnd;
uint32_t attr_freq;
uint32_t attr_dc;
} chip_state_t;

// timer callback
void tcb_controls(void *u_data) {
chip_state_t *chip = (chip_state_t*)u_data;
uint32_t f = attr_read(chip->attr_freq);
uint8_t dc = attr_read(chip->attr_dc);
printf("Freq Hz: %u\n", f);
printf("DutyCycle %% : %u\n", dc);
}

void chip_init() {
chip_state_t *chip = malloc(sizeof(chip_state_t));
printf("Hello from custom chip!\n");

// !!! inizializazione degli attributi !!!
// inizializza gli attrinuti controls (vedi test-mychip.chip.json)
chip->attr_freq = attr_init("ctrlFreq", 1);
// attribute name_^ ^__default value
chip->attr_dc = attr_init("ctrlDcycle", 50);
// initOutState è nel file diagram.json
uint32_t attr_ios = attr_init("initOutState", DLFT_IOS);

bool ios = (bool)attr_read(attr_ios);
printf("initOutState = %u\n", ios);

// *** inizializazione dei pin I/O ***
// Se ios == true il pin out è in uscita impostato HIGH
if (ios) {
chip->out = pin_init("OUT", OUTPUT_HIGH);
// altrimenti out è uscita impostata LOW
} else {
chip->out = pin_init("OUT", OUTPUT_LOW);
}
chip->inp = pin_init("INP", INPUT_PULLUP);

// *** inizializazione dei timer ***
// tid_controls is periodic 100ms timer
timer_config_t tcfg_controls = {
.user_data = chip,
.callback = tcb_controls
};
timer_t tid_controls = timer_init(&tcfg_controls);
// avvia il timer ciclico
timer_start(tid_controls, CVT_msTOus(100), AUTO_REPEAT);
}

Link al progetto su wokwi,

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:

struct {
int32_t freq;;
int8_t dc;
} oldValue = { .freq=-1, .dc = -1 };

// timer callback
void tcb_controls(void *u_data) {
chip_state_t *chip = (chip_state_t*)u_data;
uint32_t f = attr_read(chip->attr_freq);
uint8_t dc = attr_read(chip->attr_dc);
if (oldValue.freq != f) {
printf("Freq Hz: %u\n", f);
oldValue.freq = f;
}
if (oldValue.dc != dc) {
printf("DutyCycle %%: %u\n", dc);
oldValue.dc = dc;
}
}

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.

#define AUTO_REPEAT true
#define NO_REPEAT false
#define DFLT_FREQ 1
#define DFLT_DC 500
#define DLFT_IOS HIGH

#define T0 0
#define T1 1

#define CVT_msTOus(m) (m * 1000)

typedef struct {
uint32_t period;
uint32_t t01[2];
timer_t tid_pwm;
} pwm_t;

typedef struct {
// TODO: Put your chip variables here
pin_t out;
pin_t inp;
uint32_t attr_freq;
uint32_t attr_dc;
pwm_t pwm;
} chip_state_t;

struct {
int32_t freq;;
int16_t dc;
} oldValue = { .freq=-1, .dc = -1 };

// timer callback
void tcb_pwm(void *u_data) {
chip_state_t *chip = (chip_state_t*)u_data;
uint8_t out_state = !pin_read(chip->out); // Nota !
timer_start(chip->pwm.tid_pwm, chip->pwm.t01[out_state], NO_REPEAT);
pin_write(chip->out, out_state);
//printf("out_state:%u\n", !out_state);
}

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.

// chip->tid_pwm is oneshot timer
timer_config_t tcfg_pwm = {
.user_data = chip,
.callback = tcb_pwm
};
chip->pwm.tid_pwm = timer_init(&tcfg_pwm);
tcb_controls(chip);
timer_start(chip->pwm.tid_pwm, chip->pwm.t01[T0], NO_REPEAT);

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.

Link alla simulazione. 

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.

Licenza Creative Commons
Quest'opera è distribuita con Licenza Creative Commons Attribuzione - Condividi allo stesso modo 4.0 Internazionale