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