Skip to the content.

Torna all’indice generazione tempi >versione in C++

SCHEDULAZIONE CON I TIMERS HW

La schedulazione dei task normalmente riguarda quei compiti che devono essere ripetuti in maniera periodica, infatti si parla di loop() principale e di loop secondari eventualmente sullo stesso thread (protothread) o su altri thread. Lo stesso scopo può essere ottenuto con dei timer HW che realizzano un loop su base evento. L’evento è l’interrupt di un timer HW, il loop è rappresentato dalla calback associata al timer e che viene viene da esso periodicamente richiamata.

Ma un timer si può impostare per generare:

Per cui un timer HW offre una versatilità in più se, nello stesso progetto, si vogliono combinare insieme eventi periodici con eventi aperiodici triggerati da eventi su un ingresso o da altri eventi (scadenza di timeout o contatori).

La stessa cosa è in realtà possibile realizzarla anche con i timer SW basati sul polling nel loop principale della funzione millis(). La loro versatilità è uguale se non superiore, ma sono soggetti ad alcune limitazioni che potrebbero renderli non adatti in certi contesti. L’ambito che penalizza di più i timer SW è quello delle applicazioni mission-critical (o critical-time). In questo tipo di applicazioni si deve prevedere che l’esecuzione di certi compiti avvenga in maniera estremamente puntuale, pena l’introduzione di instabilità nel sistema o di perdita di sicurezza per chi lo adopera.

Rispetto agli altri metodi di creazione di base dei tempi (polling della millis(), thread e protothread), è tendenzialmente più legato ad uno specifico vendor di HW e ad una specifica linea di prodotti. Le API dei timer, pur esendo molto simili tra loro, non sono standardizzate e la portabilità del SW nel tempo potrebbe non essere garantita. In ogni caso semplificano parecchio la gestione delle ISR associate a timer HW che altrimenti, eseguita a basso livello, richiede una impostazione di registri interni della CPU che necessita di conoscenze di dettaglio molto specifiche.

I TIMERS HW DI ESP32

Il microcontrollore esp32 ha 4 timer HW. Tutti i timer sono divisi in 2 gruppi, 2 timer in ciascun gruppo. Ogni timer HW può avere una propria configurazione indipendente.

Sebbene siano disponibili timer software di FreeRTOS (il sistema operativo di ESP32) questi timer presentano alcune limitazioni:

Similmente ad Arduino, ESP32 permette l’accesso diretto ai timer HW in almeno tre modi:

I timer hardware però, sebbene siano liberi da entrambe le limitazioni precedenti, spesso sono meno convenienti da usare. Ad esempio, i componenti dell’applicazione potrebbero richiedere che gli eventi del timer si attivino in determinati momenti nel futuro, ma il timer hardware contiene un unico valore di “confronto” utilizzato per la generazione di un interrupt.

Ciò significa che è necessario costruire una astrazione in cima ai timer hardware che, nonostante utilizzi ancora il meccanismo delle interruzioni, implementi:

In ESP32, le callback del timer possono essere inviate con due metodi:

Modalità di utilizzo

La modalità di utilizzo è simile a quella di una usuale ISR. Una funzione di callback viene associata ad un evento di un timer tramite un metodo init(). La dissociazione si fa con il metodo contrario deinit(). Gli eventi possibili sono:

I timer HW possono essere:

La possibilità di poter instanziare un timer logico per ogni task, a sua volta definito all’interno di una certa callback, rende l’utilizzo dei timer una strada effettivamente percorribile per la realizzazione di uno schedulatore di compiti sia periodici che one shot (esecuzione singola non ripetuta). La schedulazione dei compiti inoltre rimane molto precisa perchè collegata a ISR eseguite da segnali di interrupt lanciati da timer fisici.

Limitazioni nell’utilizzo

I timer richiamano particolari funzioni di callback , le ISR, che sono attivate a seguito di un interrupt HW (timeout del timer). Nei sistemi multi-core questi interrupt possono essere chiamati solo dal core che ha eseguito il timer. Ciò significa che se si utilizza un timer per attivare un interrupt, è necessario assicurarsi che l’interrupt venga chiamato solo dal core che sta eseguendo il timer.

le ISR di base non sono interrompibili a meno di impostare apposite istruzioni che lo consentono. Interrompere una ISR potrebbe causare inconsistenze nella gestione delle risorse condivise con il loop o altri thread. D’altra parte, una ISR che non si interrompe per molto tempo impedisce la tempestiva esecuzione delle altre ISR dovute ad interruzioni simultanee o temporalmente vicine.

Esistono limitazioni speciali su ciò che può e non può essere fatto all’interno delle ISR nella maggior parte dei controllori:

Esempio di dichiarazione e instanziazione di un oggetto timer:

from machine import Timer 

Esempio delle due modalità di impostazione:

# Create physical timers 
tim1 = Timer(1)
tim1.init(mode=Timer.PERIODIC, period=100, callback=mycallback)
tim2 = Timer(2, mode=Timer.PERIODIC, period=500, callback=mycallback)
tim3 = Timer(3, mode=Timer.ONE_SHOT)
# Create a virtual timer with period 500ms
tim = Timer(-1)
tim.init(period=500, callback = mycallback)

La funzione Timer.deinit() può essere utilizzata per rimuovere un timer dal sistema. Ciò è utile se si desidera arrestare un timer attualmente in esecuzione. Se ci sono molti timer che utilizzano ciascuno la memoria, è una buona pratica rimuoverli quando il loro compito è terminato.

Esempio completo:

import time
from machine import Pin, Timer

def blink(led):
     led.value(not led.value())

def periodicPrint(t):
    global executionsCount
    global led
    print("printing in periodic function. Exec nr: ", (executionsCount+1));
    executionsCount += 1
    blink(led)

    if executionsCount >= maxExecutionsCount:
        tim.deinit()

led = Pin(13, Pin.OUT)
tim = Timer(-1)
tim.init(period=500, callback = periodicPrint)	
maxExecutionsCount = 10
executionsCount = 0

while True:
    time.sleep_ms(1)

Simulazione su Esp32 con Wowki: https://wokwi.com/projects/371694109619973121

L’esp_timer è un set di APIs che fornisce timer one-shot e periodici, con risoluzione di tempo dei microsecondo e 64-bit di range di conteggio. In ogni caso è bene tenere presente che:

import time
from machine import Pin, Timer

def blink(led):
    led.value(not led.value())

led1 = Pin(12, Pin.OUT)
led2 = Pin(18, Pin.OUT)
tim1 = Timer(2)
tim1.init(period=500, callback = lambda t: blink(led1))	
tim2 = Timer(3)
tim2.init(period=1000, callback = lambda t: blink(led2))	
count = 0

while True:
    print("Doing stuff... ", count)
    count += 1
    if count >= 10:
        break
    time.sleep_ms(1000)

print("Ending timers...")
tim1.deinit()
tim2.deinit()

Simulazione su Esp32 con Wowki: https://wokwi.com/projects/371695217789720577

PROBLEMA DEL NUMERO LIMITATO DI TIMER HW

Esistono dei limiti nel numero dei timer HW a bordo di un sistema a microcontrollore. ESP32, ad esempio ne ha solo 4, virtualizzabili in un numero indefinito in C++, non virtualizzabili in python. Arduino, nelle varie versioni (come prortotipo di microcontrollori più semplici) ne ha un numero ridotto e non virtualizzabile.

Se i task da mandare in esecuzione in parallelo sono in numero maggiore dei timer allocabili (HW o virtuali) allora bisogna condividere un timer tra più task e per questo scopo si possono usare le solite tecniche di schedulazione che permettono, a fronte di un tempo comune (tempo base), di generare i tempi propri di ciascun task. Invocando lo schedulatore in corrispondenza del momento dello scadere (elapsed) di questi tempi, viene invocata la funzione (o il blocco di codice) del task.

alt text

TIMERS HW SCHEDULATO TRAMITE CONTEGGIO DEI TIMES TICK

Si tratta della stessa situazione dell’esempio precedente in cui ci stanno tre task da eseguire con precisione e soltanto due timer HW per farlo. I task complessivamente in esecuzione sono quattro:

import time
from machine import Pin, Timer
import random

def blink(led):
    led.value(not led.value())

def scheduleAll(leds):
    global step
    step = (step + 1) % nstep      # conteggio circolare arriva al massimo a nstep-1
    # il codice eseguito al tempo base va quì	
    # ..........
    # task 1
    if not(step % 2):      # schedulo eventi al multiplo del tempo stabilito (1 sec)
        blink(leds[0])                       
    # task 2
    if not(step % 3):      # schedulo eventi al multiplo del tempo stabilito (1,5 sec)
        blink(leds[1])      
    # task 3
    if not(step % 4):      # schedulo eventi al multiplo del tempo stabilito (2 sec)
        blink(leds[2])      
    # task 4
    if not(step % 6):      # schedulo eventi al multiplo del tempo stabilito (3 sec)
        blink(leds[3])      
    # il codice eseguito al tempo base va quì	
    # ..........

led1 = Pin(12, Pin.OUT)
led2 = Pin(14, Pin.OUT)
led3 = Pin(27, Pin.OUT)
led4 = Pin(5, Pin.OUT)
led5 = Pin(4, Pin.OUT)
led6 = Pin(2, Pin.OUT)
leds1 = [led1, led2, led3, led4]
leds2 = [led5, led6]
tim1 = Timer(3)
tim1.init(period=500, callback = lambda t: scheduleAll(leds1))	
tim2 = Timer(4)
tim2.init(period=1000, callback = lambda t: blink(led5))	
step = 0
nstep = 1000

while True:
    print("task 6 pesante nel loop")
    blink(led6)
    randomDelay = random.randint(500,800)
    print("delay: ", randomDelay)
    time.sleep_ms(randomDelay)
    #time.sleep_ms(500)

Simulazione su Arduino con Wowki: https://wokwi.com/projects/371769605396662273

TIMERS HW SCHEDULATI TRAMITE AGGIORNAMENTO DEL PERIODO DEL TASK

Gli schedulatori utilizzati sono due:

Entrambi possono essere utilizzati a partire da una generazione di tempi costante (delay, millis(), timer HW). Per una dissertazione più accurata sul loro utilizzo vedi Schedulatore di compiti basato sul polling della millis()

import time
from machine import Pin, Timer

def blink(led):
    led.value(not led.value())

def scheduleAll(leds):
    global tbase1
    global elapsedTime
    global period
    #task3
    if elapsedTime[0] >= period[0]:
        blink(leds[0])
        elapsedTime[0] = 0
    elapsedTime[0] += tbase1
    #task4
    if elapsedTime[1] >= period[1]:
        blink(leds[1])
        elapsedTime[1] = 0
    elapsedTime[1] += tbase1
    #task5
    if elapsedTime[2] >= period[2]:
        blink(leds[2])
        elapsedTime[2] = 0
    elapsedTime[2] += tbase1

led1 = Pin(12, Pin.OUT)
led2 = Pin(14, Pin.OUT)
led3 = Pin(27, Pin.OUT)
led4 = Pin(5, Pin.OUT)
led5 = Pin(4, Pin.OUT)
led6 = Pin(2, Pin.OUT)
leds1 = [led1, led2, led3]
leds2 = [led4, led5]
#parametri dello sheduler 1
period2 = [500, 3000]
precs= [0, 0]
precm = 0
#inizializzazione dello scheduler 1
for i in range(2):
    precs[i] = precm -period2[i];
#parametri dello sheduler 2
period = [500, 1000, 2000]
elapsedTime = [0, 0, 0]
tbase1 = 500
#inizializzazione dello scheduler 2
tbase2 = 500
for i in range(2):
     elapsedTime[i] = period[i]
#configurazione timers HW
tim1 = Timer(3)
tim1.init(period=500, callback = lambda t: scheduleAll(leds1))	
tim2 = Timer(4)
tim2.init(period=1000, callback = lambda t: blink(led6))	

while True:
    time.sleep_ms(500)
    precm += tbase2
    #task1
    if precm - precs[0] >= period2[0]:
        precs[0] += period2[0]
        blink(leds2[0])
    #task2
    if precm - precs[1] >= period2[1]:
        precs[1] += period2[1]
        blink(leds2[1])

Simulazione su Arduino con Wowki: https://wokwi.com/projects/371783717482539009

In questo esempio si utilizza un unico timer HW come base dei tempi per uno schedulatore SW che gestisce la tempistica di due task:

Le operazioni benchè semplici vengono considerate come prototipi di task più complessi e magari soggetti a ritardi considerevoli. In questa circostanza la loro esecuzione all’interno di una ISR è sconsigliata per cui essi vengono eseguiti nel loop() principale su segnalazione di un flag asserito dentro la ISR del timer.

import time
from machine import Pin, Timer

class Toggle(object):
    def __init__(self, btn, state = False):
        self.btn = btn
        self.state = state
        self.precval = 0
    def toggle(self):
        changed = False
        val = self.btn.value()
        if self.precval == 0 and val == 1: 
            changed = True
            self.state = not self.state
            print(self.state)
        self.precval = val 
        return changed
    def getState(self):
        return self.state
    def setState(self,state):
        self.state = state

def blink(led):
    led.value(not led.value())

def press(p):
    p.toggle()
    time.sleep_ms(200)# emulazione ritardo del task

def toggleLogic(led):
    global pulsante
    if pulsante.getState():
        blink(led)
        print("Stato ",pulsante.getState())
        time.sleep_ms(100)# emulazione ritardo del task
    else:
        led.off()
         
def  timerISR(timer):
    global timerFlag
    global count
    if timerFlag:
        for i in range(taskNum):
            elapsedTime[i] += tbase
            count[i] +=1
    else:
        timerFlag = True
        for i in range(taskNum):
            if count[i] > 0:
                print("Recuperati ", count[i], " ticks del task ", i)
        for i in range(taskNum):
             count[i] = 0

def scheduleAll():
    global elapsedTime
    global tickFct
    global elapsedTime
    global taskNum
    for i in range(taskNum):
        if elapsedTime[i] >= period[i]:
            tickFct[i](pin[i])
            elapsedTime[i] = 0			
        elapsedTime[i] += tbase

btn1 = Pin(12,Pin.IN)
led1 = Pin(13,Pin.OUT)
led2 = Pin(2,Pin.OUT)
pulsante = Toggle(btn1)
pin = [led1, led2, pulsante]
timerFlag = False
tickFct = [toggleLogic, blink, press]
period = [1000, 500, 50]
elapsedTime = [0, 0, 0]
taskNum = len(period)
tbase = 50
count = [0, 0, 0]
myPerTimer = Timer(3)
myPerTimer.init(period=tbase, mode=Timer.PERIODIC, callback=timerISR)
#inizializzazione dei task
for i in range(taskNum):
     elapsedTime[i] = period[i]

while True:
    if timerFlag:
        scheduleAll()
        #time.sleep_ms(200)
        timerFlag = False
    time.sleep_ms(1)
    # il codice eseguito al tempo massimo della CPU va quì 

Di seguito il link della simulazione online con Tinkercad su Arduino: https://wokwi.com/projects/371662961899688961

Sitografia

Torna all’indice generazione tempi >versione in C++