Physical computing/Signalen en events: verschil tussen versies
(80 tussenliggende versies door dezelfde gebruiker niet weergegeven) | |||
Regel 25: | Regel 25: | ||
In het voorbeeld hierboven zie je het signaal van een drukknop: dit is laag als de drukknop in rust is, en hoog als de drukknop ingedrukt is. | In het voorbeeld hierboven zie je het signaal van een drukknop: dit is laag als de drukknop in rust is, en hoog als de drukknop ingedrukt is. | ||
Daaronder zie je de bijbehorende events die overeenkomen met het indrukken van de drukknop. | Daaronder zie je de bijbehorende events die overeenkomen met het indrukken van de drukknop. | ||
Deze events kun je in het signaal detecteren als de overgang van laag naar hoog. | Deze events kun je in het signaal detecteren als de overgang van laag naar hoog. (In de software: 0->1) | ||
=== Verwerken van signalen === | === Verwerken van signalen === | ||
Regel 40: | Regel 40: | ||
Het detecteren van een event kan op verschillende manieren: in de hardware, in een onderliggende software-laag, of in de eigenlijke besturingssoftware. Voor de verdere verwerking van de events maakt dat geen verschil. | Het detecteren van een event kan op verschillende manieren: in de hardware, in een onderliggende software-laag, of in de eigenlijke besturingssoftware. Voor de verdere verwerking van de events maakt dat geen verschil. | ||
We geven hier alleen voorbeelden van het detecteren van | |||
=== Verwerken van een event: event-handler === | |||
De verwerking van een event bestaat uit een actie die aan deze event gekoppeld is. | |||
Deze actie wordt steeds uitgevoerd als de betreffende event optreedt. | |||
In een programma heeft deze actie vaak de vorm van een ''functie'' (blok code) die als ''event-handler'' aan de betreffende event gekoppeld wordt. | |||
Voorbeeld: met twee drukknoppen kun je een lamp aan- en uitzetten: de event van het indrukken van de ene knop zet de lamp aan, het indrukken van de andere knop zet de lamp uit. In tabelvorm: | |||
{| class="wikitable" | |||
|+ 2-button LED control | |||
|- | |||
! Event !! Action | |||
|- | |||
| Button A pushed (0->1) || LED on | |||
|- | |||
| Button B pushed (0->1) || LED off | |||
|} | |||
=== Events en toestanden === | |||
De actie van een event is soms afhankelijk van de ''toestand''. Bijvoorbeeld: het indrukken van een drukknop geeft als actie om de lamp aan te zetten, als deze uit was; en omgekeerd. In de actie moeten we dan deze toestand ook inspecteren en weer aanpassen. We kunnen een dergelijk gedrag beschrijven door middel van een eindige automaat, in de vorm van een toestandsdiagram. Dit kunnen we dan vervolgens op een systematische manier omzetten in een programma. | |||
[[Bestand:Diagram-1-button-on-off.png|300px]] | |||
Deze eindige automaat kunnen we ook als tabel met overgangen weergeven: | |||
{| class="wikitable" | |||
|+ LED besturing met één drukknop | |||
|- | |||
! Input A !! Toestand !! --> !! Actie !! Nieuwe toestand | |||
|- | |||
| Button A pushed (0->1) || state 0 || --> || led ON || state 1 | |||
|- | |||
| Button A pushed (0->1) || state 1 || --> || led OFF || state 0 | |||
|} | |||
== Signalen == | |||
Een signaal heeft op elk moment een waarde. | |||
Voorbeelden van signalen: | |||
* het signaal van een drukknop (button); | |||
* het signaal van een temperatuursensor; | |||
* het signaal van een hart (electrocardiogram, ECG); | |||
* het signaal van een versnellingsmeter (accelerometer); | |||
<<figuur van een signalen>> | |||
Een drukknop levert een tweewaardig of "logisch" signaal; elektrisch gezien een lage spanning (0V) of een "hoge" spanning (meestal 5V of 3.3V). In de software komt dat overeen met de getallen 0 en 1, of de logische waarden False en True. | |||
Veel andere sensoren leveren een analoog signaal, zoals bijvoorbeeld een temperatuursensor. Dit analoge signaal wordt op discrete momenten in de tijd bemonsterd; de analoge waarde van een monster wordt omgezet in een getal (gediscretiseerd) door een Analoog/Digitaal (A/D) omzetter. | |||
* Opmerking: het resultaat van de A/D omzetting is meestal een geheel getal (integer), in een beperkt bereik; bijvoorbeeld 0..1023 voor een 10-bits A/D omzetter. Deze waarde kun je vervolgens omrekenen naar een fysieke eenheid, bijvoorbeeld "°C"; daarvoor heb je meestal een floating point getal nodig. Soms is het handiger om met de oorspronkelijke gehele getallen te rekenen; soms werk je met de fysieke eenheden, als floating point getal. (Het werken met floating point getallen heeft enkele nadelen: (i) je krijgt mogelijk wat precisie-verlies; (ii) het kost meer rekenkracht - wat vooral voor microcontrollers een punt kan zijn.) | |||
=== Verwerken van signalen === | |||
In principe kun je met een input-signaal twee dingen doen: | |||
# verwerken tot een output-signaal; | |||
# detecteren van events - die verder als event verwerkt worden. | |||
Een signaal kun je op verschillende manieren verwerken tot een output-signaal: | |||
* de huidige signaalwaarde ''direct'' omzetten in een output-signaalwaarde; | |||
* de huidige signaalwaarde combineren met de historie van het signaal tot een output-signaalwaarde. | |||
Deze laatste aanpak wordt gebruikt bij besturingsalgoritmes zoals een PID-regeling (zie https://en.wikipedia.org/wiki/PID_controller). | |||
We beschrijven hier de eerste mogelijkheid, de directe besturing, zonder gebruik van de historie van het signaal. | |||
Het detecteren van events bespreken we later. | |||
=== Voorbeeld: potmeter-regeling === | |||
In dit voorbeeld gebruiken we een potmeter voor analoge invoer, waarmee we het PWM-signaal voor een LED (of voor een motor) sturen. | |||
Het invoer-signaal geven we direct door aan de uitvoer. | |||
Soms moeten we hierbij een schaling toepassen, als het input-bereik verschilt van het output-bereik. | |||
{| class="wikitable" | |||
|+ Potmeter regeling | |||
|- | |||
! Invoersignaal !! -> !! Uitvoersignaal | |||
|- | |||
| Potmeter || -> || LED | |||
|} | |||
(Dit is al behandeld bij analoge invoer en PWM-uitvoer?) | |||
==== Programma: MakeCode blokjes ==== | |||
==== Programma: microbit microPython ==== | |||
Op de microbit zijn de pinnen 0, 1, 2, (3), (4) en (10) beschikbaar als analoge input. | |||
De pinnen 3, 4, en 10 worden ook gebruikt voor het aansturen van het display. | |||
Pinnen 0, 1 2, (...) zijn beschikbaar als analoge (PWM) output. | |||
(Zie: https://tech.microbit.org/hardware/edgeconnector/#pins-and-signals) | |||
We gebruiken hier pin0 voor de potmeter en pin1 voor de LED. | |||
<syntaxhighlight lang=python> | |||
from microbit import * | |||
from utime import sleep | |||
pot = pin0 | |||
led = pin1 | |||
led.set_analog_period(1) # set PWM period (1ms, 1000Hz) | |||
while True: | |||
level = pot.read_analog() | |||
led.write_analog(level) | |||
sleep(0.1) | |||
</syntaxhighlight> | |||
==== Programma: RP-Pico microPython ==== | |||
De uitwerking voor de RP-Pico in microPython: | |||
<syntaxhighlight lang=python> | |||
from machine import Pin, PWM, ADC | |||
from machine import ADC, Pin | |||
pot = ADC(Pin(26)) # create ADC object on ADC pin (GPIO 26, 27, 28) | |||
pot.read_u16() # read value, 0-65535 across voltage range 0.0v - 3.3v | |||
led = PWM(Pin(0)) # create PWM object from a pin | |||
led.freq(1000) # set frequency | |||
while True: | |||
level = pot.read_u16() | |||
led.duty_u16(level) | |||
time.sleep(0.1) | |||
</syntaxhighlight> | |||
==== Programma: Arduino ==== | |||
<syntaxhighlight lang=cpp> | |||
int led = D13; # built-in LED | |||
int pot = A0; | |||
void setup() { | |||
pinMode(pot, INPUT); | |||
pinMode(led, OUTPUT); | |||
} | |||
void loop() { | |||
int level = analogRead(pot); | |||
level = level / 4; # scaling from 10 to 8 bits (1023 -> 255) | |||
analogWrite(led); | |||
} | |||
</syntaxhighlight> | |||
=== Voorbeeld: drukknop-lichtregeling === | |||
Met behulp van twee drukknoppen kunnen we 4 verschillende niveau's bepalen, bijvoorbeeld 0, 1/4, 1/2, 1, voor de lichtsterkte van een LED, of voor de snelheid van een motor. We werken met een directe besturing: de ''huidige stand van de drukknoppen'' bepaalt de lichtsterkte of de snelheid. Als je de knoppen loslaat, is het niveau 0. (Dit is meestal niet praktisch, zie voor een alternatief de drukknop-dimmer: dezelfde schakeling met een ander programma.) | |||
==== Ontwerp ==== | |||
{| class="wikitable" | |||
|+ Lichtsterkte regeling | |||
|- | |||
! KnopA !! Knop B !! -> !! Lichtsterkte | |||
|- | |||
| 0 || 0 || -> || 0 | |||
|- | |||
| 1 || 0 || -> || max_level / 4 | |||
|- | |||
| 1 || 1 || -> || max_level / 2 | |||
|- | |||
| 0 || 1 || -> || max_level | |||
|} | |||
Opmerkingen: | |||
* de verschillende lichtsterktes geven steeds een verdubbeling: dat is omdat ons oog de lichtsterktes logaritmisch ervaart. | |||
* we hebben de volgorde van de knoppen zo gekozen, dat je voor opeenvolgende niveaus maar één knop hoeft te veranderen. Dit heet ook wel: de Hamming-afstand tussen de verschillende standen (codes) is 1. | |||
==== Programma: MakeCode blokken ==== | |||
[[Bestand:Makecode-2-button-pwm-control.png|500px]] | |||
Regeling voor de lichtsterkte (of motorsnelheid) met 2 drukknoppen. | |||
De actuele stand van de drukknoppen bepaalt de actuele lichtsterkte (of snelheid). | |||
Merk op dat het bovenstaande steeds herhaald wordt: er is geen sprake van events. | |||
==== Programma - RP-Pico Python ==== | |||
<syntaxhighlight lang=python> | |||
from machine import Pin, PWM | |||
from utime import sleep | |||
buttonA = Pin(20, Pin.IN) | |||
buttonB = Pin(21, Pin.IN) | |||
led = PWM(Pin(9)) # create PWM object from a pin | |||
led.freq(1000) # set frequency | |||
max_level = 65535 # max PWM level | |||
while True: | |||
if buttonA.value() == 1 and buttonB.value() == 0: | |||
led.duty_u16(max_level // 4) | |||
elif buttonA.value() == 1 and buttonB.value() == 1: | |||
led.duty_u16(max_level // 2) | |||
elif buttonA.value() == 0 and buttonB.value() == 1: | |||
led.duty_u16(max_level) | |||
else: # buttonA == 0, buttonB == 0 | |||
led.duty_u16(0) | |||
sleep(0.1) | |||
</syntaxhighlight> | |||
Opmerkingen: | |||
* voor de Raspberry Pi Pico is het maximale PWM-niveau 65535 (16 bits) | |||
* <code>a // b </code> staat in Python voor geheeltallige (integer) deling, waarbij <code> a </code>, <code> b </code> en het resultaat gehele getallen zijn. | |||
* je kunt deze if met 4 takken ook schrijven als tweemaal twee geneste if-statements. | |||
==== Programma: microbit microPython ==== | |||
<syntaxhighlight lang=python> | |||
from microbit import * | |||
from utime import sleep | |||
led = pin1 | |||
led.set_analog_period(1000) # set PWM period | |||
max_level = 1023 | |||
while True: | |||
if button_a.is_pressed() and not button_b.is_pressed(): | |||
led.write_analog(max_level // 4) | |||
elif button_a.is_pressed() and button_b.is_pressed(): | |||
led.write_analog(max_level // 2) | |||
elif not button_a.is_pressed() and button_b.is_pressed(): | |||
led.write_analog(max_level) | |||
else: # button_a == 0, button_b == 0 | |||
led.write_analog(0) | |||
sleep(0.1) | |||
</syntaxhighlight> | |||
In het geval van de microbit zijn de buttons A en B voorgedefinieerd, met de functies `is_pressed` (voor de actuele signaalwaarde) | |||
en `was_pressed` (voor de events, zie verderop). | |||
== Events == | |||
<< figuur van een event>> | |||
Een event vindt plaats op één enkel ''ondeelbaar'' moment. De waarde van de event is alleen op dat moment gedefinieerd. | |||
Voorbeelden van events: | |||
* het indrukken van een drukknop (of: het loslaten daarvan) | |||
* eem muisklik | |||
* het schudden van de microbit | |||
* een beweging zoals gedetecteerd door een infrarood-sensor | |||
* een hartslag | |||
* een stap | |||
* het ontvangen van een radiobericht | |||
: In de grafische gebruikersinterface (GUI) van een computer spelen events een grote rol: het indrukken van een toets, een muis-klik, het aanraken van een touchscreen, zijn allemaal voorbeelden van input-events die door de interface-software afgehandeld worden. | |||
: Als de duur van een "event" wel een rol speelt dan kun je ook afzonderlijke events gebruiken voor het begin en het einde. Voorbeeld: het indrukken en het loslaten van een drukknop. | |||
=== Detecteren van een event in een signaal === | |||
Als je events nodig hebt, terwijl de sensor alleen maar een signaal levert, dan heb je signaalverwerking nodig om de events in het signaal te detecteren. Dit kan heel eenvoudig zijn, bijvoorbeeld het detecteren van het indrukken van een knop. In andere gevallen, zoals het detecteren van de hartslagen in een ECG, is dit lastiger. Voor het detecteren van een event in een signaal heb je een stukje van de historie van het signaal nodig. Voor het detecteren van het indrukken van een drukknop is dat alleen de waarde van het signaal op het vorige tijdstip. Voor het betrouwbaar detecteren van een "stap" in het signaal van een accellerometer (versnellingsmeter) heb je meer waarden uit de historie nodig. | |||
De microbit-software bevat al de nodige event-detectie. | |||
Je kunt zo voor de versnellingsmeter events gebruiken als "schudden", "logo omhoog", "logo omlaag", "vrije val". | |||
''code voor het detecteren van het indrukken van een drukknop - met figuur'' | |||
''figuur daarbij'' | |||
=== Verwerken van een event: event-handler === | === Verwerken van een event: event-handler === | ||
Regel 71: | Regel 338: | ||
In een programma kun je deze koppeling tussen ''event'' en ''actie'' vormgeven met behulp van een event handler: dat is een functie of een stuk programmacode met daarin de actie die je koppelt aan de event. | In een programma kun je deze koppeling tussen ''event'' en ''actie'' vormgeven met behulp van een event handler: dat is een functie of een stuk programmacode met daarin de actie die je koppelt aan de event. | ||
==== MakeCode blokjestaal ==== | ==== Programma: MakeCode blokjestaal ==== | ||
[[Bestand:Makecode-event-blokken.png|miniatuur]] | [[Bestand:Makecode-event-blokken.png|miniatuur]] | ||
Regel 81: | Regel 348: | ||
[[Bestand:Makecode-2-button-led-control.png|350px]] | [[Bestand:Makecode-2-button-led-control.png|350px]] | ||
==== microPython ==== | ==== Programma: microPython ==== | ||
In Python | In Python definieer je voor een event-handler als een ''functie''. Deze functie koppel je vervolgens aan een event: de functie wordt dan steeds aangeroepen als de event gedetecteerd wordt. | ||
Voorbeeld van deze functie (microPyton RP2): | Voorbeeld van deze functie (microPyton RP2): | ||
Regel 89: | Regel 356: | ||
<syntaxhighlight lang=Python> | <syntaxhighlight lang=Python> | ||
def buttonA_handler(): | def buttonA_handler(): | ||
led.value(1) | |||
def buttonB_handler(): | def buttonB_handler(): | ||
led.value(0) | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Regel 106: | Regel 373: | ||
<syntaxhighlight lang=Python> | <syntaxhighlight lang=Python> | ||
prev_levelA = 0 | |||
prev_levelB = 0 | |||
while True: | while True: | ||
cur_levelA = buttonA.value() | cur_levelA = buttonA.value() | ||
Regel 118: | Regel 388: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Het complete programma wordt dan (hier voor de Raspberry Pi Pico): | |||
<syntaxhighlight lang=python> | |||
from machine import Pin, PWM | |||
from utime import sleep | |||
buttonA = Pin(20, Pin.IN) | |||
buttonB = Pin(21, Pin.IN) | |||
led = Pin(9, Pin.OUT) | |||
: | def buttonA_handler(): | ||
led.value(1) | |||
def buttonB_handler(): | |||
led.value(0) | |||
prev_levelA = 0 | |||
prev_levelB = 0 | |||
: | while True: | ||
cur_levelA = buttonA.value() | |||
if cur_levelA == 1 and prev_levelA == 0: | |||
buttonA_handler() | |||
prev_levelA = cur_levelA | |||
cur_levelB = buttonB.value() | |||
if cur_levelB == 1 and prev_levelB == 0: | |||
buttonB_handler() | |||
prev_levelB = cur_levelB | |||
</syntaxhighlight> | |||
=== Events en toestanden === | |||
De actie van een event is soms afhankelijk van de ''toestand''. Bijvoorbeeld: het indrukken van een drukknop geeft als actie om de lamp aan te zetten, als deze uit was; en omgekeerd. In de actie moeten we dan deze toestand ook inspecteren en weer aanpassen. | |||
We kunnen dit gedrag beschrijven met een ''eindige automaat''. | |||
We beschrijven een dergelijke automaat met behulp van toestandsdiagrammen en tabellen. | |||
Dit ontwerp zetten we vervolgens op een systematische manier om in een programma. | |||
==== Ontwerp: 1-knops aan/uitschakelaar ==== | |||
Als eenvoudigste voorbeeld gebruiken we het aan- en uitzetten van een LED met één enkele drukknop. | |||
De automaat bestaat uit twee toestanden: 0: de lED is uit; en 1: de LED is aan. | |||
Zie het onderstaande toestandsdiagram: | |||
[[Bestand:Diagram-1-button-on-off.png|300px]] | |||
De overgangen kunnen we ook als tabel weergeven: | |||
{| class="wikitable" | |||
|+ LED besturing met één drukknop | |||
|- | |||
! Input A !! Toestand !! --> !! Actie !! Toestand | |||
|- | |||
| button A pushed (0->1) || state 0 || --> || led ON || state 1 | |||
|- | |||
| button A pushed (0->1) || state 1 || --> || led OFF || state 0 | |||
|} | |||
==== Programma (MakeCode blokjes) ==== | |||
Het bovenstaande ontwerp zetten we als volgt om in een programma: | |||
* we combineren alle overgangen rijen met dezelfde input-event | |||
** in het bovenstaande geval is er maar 1 event: <code> button A pushed </code> | |||
* in de event-handler onderscheiden we vervolgens de verschillende toestanden | |||
[[Bestand:Makecode-1-button-led-control.png|400px]] | [[Bestand:Makecode-1-button-led-control.png|400px]] | ||
==== Programma (microPython) ==== | |||
We gebruiken dezelfde regels voor het omzetten van een automaat-ontwerp in een programma: | |||
* combineer alle overgangen (rijen) met dezelfde input-event | |||
** in het bovenstaande geval is er maar 1 event: <code> button A pushed </code> | |||
* onderscheid in de event-handler <code>buttonA_handler</code> de verschillende toestanden: | |||
Het | ** state 0: led is uit; state 1: led is aan | ||
* per toestand - gegeven de input-event van de huidige handler, dus eigenlijk per overgang: | |||
** de actie, bijvoorbeeld <code>led.value(1)</code>, om de led aan te zetten; | |||
** het zetten van de volgende toestand, bijvoorbeeld <code>led_state = 1</code> | |||
Het totale programma wordt dan: | |||
<syntaxhighlight lang=python> | |||
from machine import Pin | |||
from utime import sleep | |||
buttonA = Pin(20, Pin.IN) | |||
== | led = Pin(9, Pin.OUT) | ||
led_state = 0 | |||
def buttonA_handler(): | |||
global led_state | |||
if led_state == 0: | |||
led.value(1) # action | |||
led_state = 1 # next state | |||
else: # led_state == 1 | |||
led.value(0) # action | |||
led_state = 0 # next state | |||
prev_levelA = 0 | |||
while True: | |||
cur_levelA = buttonA.value() | |||
if cur_levelA == 1 and prev_levelA == 0: # detect buttonA pushed | |||
buttonA_handler() # call handler | |||
prev_levelA = cur_levelA | |||
sleep(0.1) | |||
</syntaxhighlight> | |||
Opmerking: | |||
* in Python moet je een globale variabele die je ''aanpast'' in een functie, daar aangeven als <code>global</code>. Anders wordt aangenomen dat het om een lokale variabele gaat. Zie hierboven: <code>global led_state</code>. | |||
=== | === Over eindige automaten en toestandsdiagrammen === | ||
Een eindige automaat heeft een eindig aantal toestanden, en een eindig aantal overgangen tussen deze toestanden. | |||
Elke toestandsovergang is gelabeld met een inputsymbool: als deze overgang gekozen wordt, op grond van de huidige toestand en het huidige invoersymbool, dan wordt het invoersymbool verwerkt en gaat de automaat over naar de bijbehorende volgende toestand. | |||
Eén van de toestanden is de begintoestand; deze gegeven we aan met een binnenkomende pijl (overgang) zonder label of bron-toestand. | |||
In ons geval wordt de rol van de invoersymbolen overgenomen door de input-events. Bovendien kan een toestandsovergang voorzien zijn van een actie, die uitgevoerd wordt als deze overgang gekozen wordt. We noteren deze combinatie bij een overgang als <code> input-event / output-action </code>. | |||
: Een dergelijke automaat, die uitvoer genereert (of acties uitvoert) bij een overgang, heet een Mealy-automaat. Een alternatieve vorm is de Moore-automaat: daar hangt de uitvoer af van de toestand. De Mealy-automaat past beter bij het event-model en de acties die je vanuit een programma kunt uitvoeren. | |||
=== Ontwerp: 1-knops dimmer === | |||
Je kunt de aan-uitschakeling met 1 knop uitbreiden met meerdere toestanden. | |||
Elke volgende toestand komt bijvoorbeeld overeen met een hoger lichtniveau van de LED. | |||
Op die manier kun je een dimmer maken met 1-knops bediening. | |||
(Ik heb een lamp met een aanmaakschakelaar die op deze manier werkt.) | |||
Merk op dat het uitschakelen in 1 keer vanuit de LED op volledige sterkte gaat. | |||
Je moet er steeds voor zorgen dat een toestand maar 1 uitgaande pijl heeft die met "A" gelabeld is: | |||
anders krijg je een dubbelzinnige automaat waarvan je niet weet hoe die zich gedraagt. | |||
: In een deterministische automaat zijn alle uitgaande overgangen van een toestand gelabeld met verschillende inputsymbolen (input-events). | |||
Voor 2 tussenliggende lichtsterktes krijg je het volgende ontwerp: | |||
Toestandsdiagram: | |||
[[Bestand:Diagram-1-button-dimmer.png|500px|Dimmer met 1-knops bediening]] | |||
Opmerking: de lichtsterktes lopen exponentieel op, omdat de gevoeligheid van je oog logaritmisch is (waardoor je oog een heel groot dynamisch bereik heeft). | |||
De bijbehorende tabel: | |||
{| class="wikitable" | {| class="wikitable" | ||
|+ | |+ LED besturing met één drukknop | ||
|- | |- | ||
! | ! Input A !! Toestand !! --> !! Actie !! Toestand | ||
|- | |- | ||
| 0 || 0 || | | button A pushed (0->1) || state 0 || --> || led 1/4 || state 1 | ||
|- | |- | ||
| 1 || | | button A pushed (0->1) || state 1 || --> || led 1/2 || state 2 | ||
|- | |- | ||
| 1 || 1 || | | button A pushed (0->1) || state 2 || --> || led 1 || state 2 | ||
|- | |- | ||
| 0 || | | button A pushed (0->1) || state 3 || --> || led 0 || state 0 | ||
|} | |} | ||
=== Programma: microPython 1-knops dimmer === | |||
<syntaxhighlight lang=python> | |||
from machine import Pin, PWM | |||
from utime import sleep | |||
buttonA = Pin(20, Pin.IN) | |||
led = PWM(Pin(9)) # create PWM object from a pin | |||
led.freq(1000) # set frequency | |||
max_level = 65535 # max PWM level | |||
led_state = 0 | |||
def buttonA_handler(): | |||
global led_state | |||
if led_state == 0: | |||
led.duty_u16(max_level // 4) | |||
led_state = 1 | |||
elif led_state == 1: | |||
led.duty_u16(max_level // 2) | |||
led_state = 2 | |||
elif led_state == 2: | |||
led.duty_u16(max_level) | |||
led_state = 3 | |||
else: # led_state == 3 | |||
led.duty_u16(0) | |||
led_state = 0 | |||
prev_levelA = 0 | |||
while True: | |||
cur_levelA = buttonA.value() | |||
if cur_levelA == 1 and prev_levelA == 0: | |||
buttonA_handler() | |||
prev_levelA = cur_levelA | |||
sleep(0.1) | |||
</syntaxhighlight> | |||
=== | === Opdracht: 2-knops dimmer === | ||
Ontwerp een automaat (toestandsdiagram en overgangen-tabel) voor een dimmer met 4 lichtniveaus (0, 1/4, 1/2, en 1 * de maximale lichtsterkte), | |||
met twee drukknoppen. Indrukken van knop A geeft een hogere lichtsterkte, als dat mogelijk is; indrukken van knop B geeft een lagere lichtsterkte, als dat mogelijk is. | |||
Maak vanuit de overgangen-tabel het bijbehorende Python-programma. | |||
=== Timers en timer-events === | |||
In sommige gevallen wordt de wachttijd in een programma aangegeven door <code> sleep(dt) </code>. | |||
Een voorbeeld is het klassieke Blink-programma: | |||
<syntaxhighlight lang=python> | |||
while True: | while True: | ||
led.value(1) | |||
sleep(0.1) | |||
led.value(0) | |||
sleep(0.1) | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Hiermee laat je een led knipperen, met een frequentie van 1 / 0.2s = 5 Hz. | |||
< | In sommige omgevingen is deze <code>sleep</code>-opdracht een blokkerende functie: er kan tijdens deze actie niets anders gedaan worden. | ||
Dit is bijvoorbeeld al een probleem als je twee LEDs onafhankelijk van elkaar wilt later knipperen, bijvoorbeeld met 3Hz en 5Hz. | |||
Een alternatief voor het gebruik van <code>sleep</code> is dan het gebruik van timers en timer-events. | |||
: In de Arduino-omgeving is <code>sleep</code> een blokkerende actie. In de MakeCode blokjes-omgeving is dat niet het geval: daar kun je elk stukje programma met <code>sleep</code>-acties een eigen "thread" geven. (Zie elders.) | |||
=== Berichten === | |||
Deelsystemen kunnen met elkaar communiceren met behulp van berichten, eventueel via (radio)communicatie. | |||
Een inkomend bericht kun je dan ook weer als een event beschouwen. | |||
Het versturen van een bericht is een actie die dan door een ander (deel)systeem verwerkt wordt. | |||
=== rest === | |||
In dit gedeelte behandelen we een aanpak voor de verwerking, in het bijzonder die van events. | |||
Met deze aanpak kun je op een systematische manier je programma maken. | |||
Voor niet al te complexe problemen levert dit snel een werkend programma. | |||
Een belangrijk uitgangspunt is dat ''elke input-event op elk moment kan plaatsvinden''. | |||
Je programma moet dan ook op elk moment op elk input-event kunnen reageren. | |||
Je kunt er niet vanuit gaan dat de omgeving de events in de gewenste volgorde of op het gewenste moment aanbiedt | |||
: Denk bijvoorbeeld aan een robot die een lijn volgt: deze kan op elk moment een obstakel tegenkomen, of een besturingsbericht ontvangen via de afstandsbediening. | |||
In een Python-programma betekent dit dat we een event koppelen aan een functie: | |||
deze functie wordt telkens aangeroepen als de bijbehorende event optreedt. | |||
Zo'n functie noemen we een ''event-handler''. | |||
: | : De betekenis is dan: <code>WHEN event THEN handler()</code> | ||
: | [[Bestand:Makecode-event-blokken.png|miniatuur]] | ||
In de Makecode blokjestaal voor de microbit heb je apart blokken voor deze event-handlers: "wanneer knop A wordt ingedrukt", "bij schudden", of "wanneer pin0 wordt aangeraakt". De code binnen dit blok is de handler die uitgevoerd wordt als de event optreedt. | |||
Opmerking. Een bekend programma voor regels met deze structuur is "IFTTT" - "if this then that". | |||
Met de "if" hierin wordt niet de "if" van een programmeertaal bedoeld, maar een event of trigger die op elk moment plaats kan vinden. | |||
"when", of eigenlijk "whenever", zou een betere naam geweest zijn. | |||
== Analoge en digitale signalen; bemonstering == | == Analoge en digitale signalen; bemonstering == |
Huidige versie van 4 okt 2021 om 08:45
Inleiding
Een Physical Computing systeem bestaat uit sensoren, actuatoren, een (micro)controller, communicatieverbindingen (en een energiebron). De software van de microcontroller verwerkt de inputs (sensoren, communicatie) en stuurt daarmee de outputs (actuatoren, communicatie) aan.
In dit gedeelte behandelen we de volgende vragen:
- hoe ontwerp je de verwerking van inputs naar outputs?
- hoe zet je dit ontwerp om in een programma?
Signalen en events
We onderscheiden twee soorten inputs: signalen en events.
- Een signaal heeft op elk moment een waarde, zoals het signaal van een microfoon of van een temperatuursensor.
- Een event is een gebeurtenis die op een bepaald, ondeelbaar moment plaatsvindt, zoals het indrukken van een drukknop, of het ontvangen van een bericht.
Fysisch gezien is een event niet ondeelbaar: deze zal altijd een zekere tijd in beslag nemen. Maar in de software beschouwen we een event als "instantaan" en speelt de duur daarvan geen rol.
In het voorbeeld hierboven zie je het signaal van een drukknop: dit is laag als de drukknop in rust is, en hoog als de drukknop ingedrukt is. Daaronder zie je de bijbehorende events die overeenkomen met het indrukken van de drukknop. Deze events kun je in het signaal detecteren als de overgang van laag naar hoog. (In de software: 0->1)
Verwerken van signalen
Een input-signaal kun je op verschillende manieren verwerken:
- je kunt de huidige signaalwaarde direct omzetten in een output-signaal;
- je kunt de huidige signaalwaarde combineren met de geschiedenis van dit signaal, en dat verwerken tot een output-signaal;
- je kunt in het signaal events detecteren (zoals het indrukken van een knop).
We werken deze mogelijkheden verderop uit.
Detecteren van events
Het detecteren van een event kan op verschillende manieren: in de hardware, in een onderliggende software-laag, of in de eigenlijke besturingssoftware. Voor de verdere verwerking van de events maakt dat geen verschil.
Verwerken van een event: event-handler
De verwerking van een event bestaat uit een actie die aan deze event gekoppeld is. Deze actie wordt steeds uitgevoerd als de betreffende event optreedt. In een programma heeft deze actie vaak de vorm van een functie (blok code) die als event-handler aan de betreffende event gekoppeld wordt.
Voorbeeld: met twee drukknoppen kun je een lamp aan- en uitzetten: de event van het indrukken van de ene knop zet de lamp aan, het indrukken van de andere knop zet de lamp uit. In tabelvorm:
Event | Action |
---|---|
Button A pushed (0->1) | LED on |
Button B pushed (0->1) | LED off |
Events en toestanden
De actie van een event is soms afhankelijk van de toestand. Bijvoorbeeld: het indrukken van een drukknop geeft als actie om de lamp aan te zetten, als deze uit was; en omgekeerd. In de actie moeten we dan deze toestand ook inspecteren en weer aanpassen. We kunnen een dergelijk gedrag beschrijven door middel van een eindige automaat, in de vorm van een toestandsdiagram. Dit kunnen we dan vervolgens op een systematische manier omzetten in een programma.
Deze eindige automaat kunnen we ook als tabel met overgangen weergeven:
Input A | Toestand | --> | Actie | Nieuwe toestand |
---|---|---|---|---|
Button A pushed (0->1) | state 0 | --> | led ON | state 1 |
Button A pushed (0->1) | state 1 | --> | led OFF | state 0 |
Signalen
Een signaal heeft op elk moment een waarde. Voorbeelden van signalen:
- het signaal van een drukknop (button);
- het signaal van een temperatuursensor;
- het signaal van een hart (electrocardiogram, ECG);
- het signaal van een versnellingsmeter (accelerometer);
<<figuur van een signalen>>
Een drukknop levert een tweewaardig of "logisch" signaal; elektrisch gezien een lage spanning (0V) of een "hoge" spanning (meestal 5V of 3.3V). In de software komt dat overeen met de getallen 0 en 1, of de logische waarden False en True.
Veel andere sensoren leveren een analoog signaal, zoals bijvoorbeeld een temperatuursensor. Dit analoge signaal wordt op discrete momenten in de tijd bemonsterd; de analoge waarde van een monster wordt omgezet in een getal (gediscretiseerd) door een Analoog/Digitaal (A/D) omzetter.
- Opmerking: het resultaat van de A/D omzetting is meestal een geheel getal (integer), in een beperkt bereik; bijvoorbeeld 0..1023 voor een 10-bits A/D omzetter. Deze waarde kun je vervolgens omrekenen naar een fysieke eenheid, bijvoorbeeld "°C"; daarvoor heb je meestal een floating point getal nodig. Soms is het handiger om met de oorspronkelijke gehele getallen te rekenen; soms werk je met de fysieke eenheden, als floating point getal. (Het werken met floating point getallen heeft enkele nadelen: (i) je krijgt mogelijk wat precisie-verlies; (ii) het kost meer rekenkracht - wat vooral voor microcontrollers een punt kan zijn.)
Verwerken van signalen
In principe kun je met een input-signaal twee dingen doen:
- verwerken tot een output-signaal;
- detecteren van events - die verder als event verwerkt worden.
Een signaal kun je op verschillende manieren verwerken tot een output-signaal:
- de huidige signaalwaarde direct omzetten in een output-signaalwaarde;
- de huidige signaalwaarde combineren met de historie van het signaal tot een output-signaalwaarde.
Deze laatste aanpak wordt gebruikt bij besturingsalgoritmes zoals een PID-regeling (zie https://en.wikipedia.org/wiki/PID_controller). We beschrijven hier de eerste mogelijkheid, de directe besturing, zonder gebruik van de historie van het signaal.
Het detecteren van events bespreken we later.
Voorbeeld: potmeter-regeling
In dit voorbeeld gebruiken we een potmeter voor analoge invoer, waarmee we het PWM-signaal voor een LED (of voor een motor) sturen. Het invoer-signaal geven we direct door aan de uitvoer. Soms moeten we hierbij een schaling toepassen, als het input-bereik verschilt van het output-bereik.
Invoersignaal | -> | Uitvoersignaal |
---|---|---|
Potmeter | -> | LED |
(Dit is al behandeld bij analoge invoer en PWM-uitvoer?)
Programma: MakeCode blokjes
Programma: microbit microPython
Op de microbit zijn de pinnen 0, 1, 2, (3), (4) en (10) beschikbaar als analoge input. De pinnen 3, 4, en 10 worden ook gebruikt voor het aansturen van het display. Pinnen 0, 1 2, (...) zijn beschikbaar als analoge (PWM) output. (Zie: https://tech.microbit.org/hardware/edgeconnector/#pins-and-signals)
We gebruiken hier pin0 voor de potmeter en pin1 voor de LED.
from microbit import *
from utime import sleep
pot = pin0
led = pin1
led.set_analog_period(1) # set PWM period (1ms, 1000Hz)
while True:
level = pot.read_analog()
led.write_analog(level)
sleep(0.1)
Programma: RP-Pico microPython
De uitwerking voor de RP-Pico in microPython:
from machine import Pin, PWM, ADC
from machine import ADC, Pin
pot = ADC(Pin(26)) # create ADC object on ADC pin (GPIO 26, 27, 28)
pot.read_u16() # read value, 0-65535 across voltage range 0.0v - 3.3v
led = PWM(Pin(0)) # create PWM object from a pin
led.freq(1000) # set frequency
while True:
level = pot.read_u16()
led.duty_u16(level)
time.sleep(0.1)
Programma: Arduino
int led = D13; # built-in LED
int pot = A0;
void setup() {
pinMode(pot, INPUT);
pinMode(led, OUTPUT);
}
void loop() {
int level = analogRead(pot);
level = level / 4; # scaling from 10 to 8 bits (1023 -> 255)
analogWrite(led);
}
Voorbeeld: drukknop-lichtregeling
Met behulp van twee drukknoppen kunnen we 4 verschillende niveau's bepalen, bijvoorbeeld 0, 1/4, 1/2, 1, voor de lichtsterkte van een LED, of voor de snelheid van een motor. We werken met een directe besturing: de huidige stand van de drukknoppen bepaalt de lichtsterkte of de snelheid. Als je de knoppen loslaat, is het niveau 0. (Dit is meestal niet praktisch, zie voor een alternatief de drukknop-dimmer: dezelfde schakeling met een ander programma.)
Ontwerp
KnopA | Knop B | -> | Lichtsterkte |
---|---|---|---|
0 | 0 | -> | 0 |
1 | 0 | -> | max_level / 4 |
1 | 1 | -> | max_level / 2 |
0 | 1 | -> | max_level |
Opmerkingen:
- de verschillende lichtsterktes geven steeds een verdubbeling: dat is omdat ons oog de lichtsterktes logaritmisch ervaart.
- we hebben de volgorde van de knoppen zo gekozen, dat je voor opeenvolgende niveaus maar één knop hoeft te veranderen. Dit heet ook wel: de Hamming-afstand tussen de verschillende standen (codes) is 1.
Programma: MakeCode blokken
Regeling voor de lichtsterkte (of motorsnelheid) met 2 drukknoppen. De actuele stand van de drukknoppen bepaalt de actuele lichtsterkte (of snelheid).
Merk op dat het bovenstaande steeds herhaald wordt: er is geen sprake van events.
Programma - RP-Pico Python
from machine import Pin, PWM
from utime import sleep
buttonA = Pin(20, Pin.IN)
buttonB = Pin(21, Pin.IN)
led = PWM(Pin(9)) # create PWM object from a pin
led.freq(1000) # set frequency
max_level = 65535 # max PWM level
while True:
if buttonA.value() == 1 and buttonB.value() == 0:
led.duty_u16(max_level // 4)
elif buttonA.value() == 1 and buttonB.value() == 1:
led.duty_u16(max_level // 2)
elif buttonA.value() == 0 and buttonB.value() == 1:
led.duty_u16(max_level)
else: # buttonA == 0, buttonB == 0
led.duty_u16(0)
sleep(0.1)
Opmerkingen:
- voor de Raspberry Pi Pico is het maximale PWM-niveau 65535 (16 bits)
a // b
staat in Python voor geheeltallige (integer) deling, waarbija
,b
en het resultaat gehele getallen zijn.- je kunt deze if met 4 takken ook schrijven als tweemaal twee geneste if-statements.
Programma: microbit microPython
from microbit import *
from utime import sleep
led = pin1
led.set_analog_period(1000) # set PWM period
max_level = 1023
while True:
if button_a.is_pressed() and not button_b.is_pressed():
led.write_analog(max_level // 4)
elif button_a.is_pressed() and button_b.is_pressed():
led.write_analog(max_level // 2)
elif not button_a.is_pressed() and button_b.is_pressed():
led.write_analog(max_level)
else: # button_a == 0, button_b == 0
led.write_analog(0)
sleep(0.1)
In het geval van de microbit zijn de buttons A en B voorgedefinieerd, met de functies `is_pressed` (voor de actuele signaalwaarde) en `was_pressed` (voor de events, zie verderop).
Events
<< figuur van een event>>
Een event vindt plaats op één enkel ondeelbaar moment. De waarde van de event is alleen op dat moment gedefinieerd. Voorbeelden van events:
- het indrukken van een drukknop (of: het loslaten daarvan)
- eem muisklik
- het schudden van de microbit
- een beweging zoals gedetecteerd door een infrarood-sensor
- een hartslag
- een stap
- het ontvangen van een radiobericht
- In de grafische gebruikersinterface (GUI) van een computer spelen events een grote rol: het indrukken van een toets, een muis-klik, het aanraken van een touchscreen, zijn allemaal voorbeelden van input-events die door de interface-software afgehandeld worden.
- Als de duur van een "event" wel een rol speelt dan kun je ook afzonderlijke events gebruiken voor het begin en het einde. Voorbeeld: het indrukken en het loslaten van een drukknop.
Detecteren van een event in een signaal
Als je events nodig hebt, terwijl de sensor alleen maar een signaal levert, dan heb je signaalverwerking nodig om de events in het signaal te detecteren. Dit kan heel eenvoudig zijn, bijvoorbeeld het detecteren van het indrukken van een knop. In andere gevallen, zoals het detecteren van de hartslagen in een ECG, is dit lastiger. Voor het detecteren van een event in een signaal heb je een stukje van de historie van het signaal nodig. Voor het detecteren van het indrukken van een drukknop is dat alleen de waarde van het signaal op het vorige tijdstip. Voor het betrouwbaar detecteren van een "stap" in het signaal van een accellerometer (versnellingsmeter) heb je meer waarden uit de historie nodig.
De microbit-software bevat al de nodige event-detectie. Je kunt zo voor de versnellingsmeter events gebruiken als "schudden", "logo omhoog", "logo omlaag", "vrije val".
code voor het detecteren van het indrukken van een drukknop - met figuur
figuur daarbij
Verwerken van een event: event-handler
We maken eerst een ontwerp voor de verwerking van de events, en zetten dat daarna om in een programma.
Ontwerp: koppel event aan actie
Een event kun je koppelen aan een actie. Bijvoorbeeld: het indrukken van een drukknop aan het aan- of uitzetten van een lamp. Het volgende schema is dan een ontwerp voor een aan- uit besturing van een lamp met twee knoppen:
Event | Action |
---|---|
Button A pushed | LED on |
Button B pushed | LED off |
Met Button A pushed bedoelen we hier: de event van het indrukken van knop A. Steeds als je A (opnieuw) indrukt, zet je de LED aan. Met het indrukken van knop B zet je de LED uit.
- Merk op dat er geen verschil is of je knop A één keer indrukt, of meerdere keren achter elkaar. Zo'n operatie noemen we ook wel "idempotent". Dit is bijvoorbeeld handig als de knoppen niet helemaal betrouwbaar zijn, of als er een grote vertraging is tussen de event en de actie: als er geen reactie komt, probeer je het nog een keer. Dat levert in dit geval geen problemen op.
Programma: event-handlers
In een programma kun je deze koppeling tussen event en actie vormgeven met behulp van een event handler: dat is een functie of een stuk programmacode met daarin de actie die je koppelt aan de event.
Programma: MakeCode blokjestaal
In de Makecode blokjestaal voor de microbit heb je apart blokken voor deze event-handlers: "wanneer knop A wordt ingedrukt", "bij schudden", of "wanneer pin0 wordt aangeraakt". De code binnen dit blok is de handler die uitgevoerd wordt als de event optreedt.
Het programma voor de bovenstaande 2-knops besturing van een LED ziet er dan als volgt uit:
Programma: microPython
In Python definieer je voor een event-handler als een functie. Deze functie koppel je vervolgens aan een event: de functie wordt dan steeds aangeroepen als de event gedetecteerd wordt.
Voorbeeld van deze functie (microPyton RP2):
def buttonA_handler():
led.value(1)
def buttonB_handler():
led.value(0)
In MakeCode Python koppel je een event-handler op de volgende manier aan een event:
input.on_button_pressed(Button.A, buttonA_handler)
In microPython moet je zelf het detectie van de button-events programmeren, in een niet-eindigende "event loop". Na het detecteren van een event moet de bijbehorende handler aangeroepen worden.
prev_levelA = 0
prev_levelB = 0
while True:
cur_levelA = buttonA.value()
if cur_levelA == 1 and prev_levelA == 0:
buttonA_handler()
prev_levelA = cur_levelA
cur_levelB = buttonB.value()
if cur_levelB == 1 and prev_levelB == 0:
buttonB_handler()
prev_levelB = cur_levelB
Het complete programma wordt dan (hier voor de Raspberry Pi Pico):
from machine import Pin, PWM
from utime import sleep
buttonA = Pin(20, Pin.IN)
buttonB = Pin(21, Pin.IN)
led = Pin(9, Pin.OUT)
def buttonA_handler():
led.value(1)
def buttonB_handler():
led.value(0)
prev_levelA = 0
prev_levelB = 0
while True:
cur_levelA = buttonA.value()
if cur_levelA == 1 and prev_levelA == 0:
buttonA_handler()
prev_levelA = cur_levelA
cur_levelB = buttonB.value()
if cur_levelB == 1 and prev_levelB == 0:
buttonB_handler()
prev_levelB = cur_levelB
Events en toestanden
De actie van een event is soms afhankelijk van de toestand. Bijvoorbeeld: het indrukken van een drukknop geeft als actie om de lamp aan te zetten, als deze uit was; en omgekeerd. In de actie moeten we dan deze toestand ook inspecteren en weer aanpassen. We kunnen dit gedrag beschrijven met een eindige automaat.
We beschrijven een dergelijke automaat met behulp van toestandsdiagrammen en tabellen. Dit ontwerp zetten we vervolgens op een systematische manier om in een programma.
Ontwerp: 1-knops aan/uitschakelaar
Als eenvoudigste voorbeeld gebruiken we het aan- en uitzetten van een LED met één enkele drukknop. De automaat bestaat uit twee toestanden: 0: de lED is uit; en 1: de LED is aan. Zie het onderstaande toestandsdiagram:
De overgangen kunnen we ook als tabel weergeven:
Input A | Toestand | --> | Actie | Toestand |
---|---|---|---|---|
button A pushed (0->1) | state 0 | --> | led ON | state 1 |
button A pushed (0->1) | state 1 | --> | led OFF | state 0 |
Programma (MakeCode blokjes)
Het bovenstaande ontwerp zetten we als volgt om in een programma:
- we combineren alle overgangen rijen met dezelfde input-event
- in het bovenstaande geval is er maar 1 event:
button A pushed
- in het bovenstaande geval is er maar 1 event:
- in de event-handler onderscheiden we vervolgens de verschillende toestanden
Programma (microPython)
We gebruiken dezelfde regels voor het omzetten van een automaat-ontwerp in een programma:
- combineer alle overgangen (rijen) met dezelfde input-event
- in het bovenstaande geval is er maar 1 event:
button A pushed
- in het bovenstaande geval is er maar 1 event:
- onderscheid in de event-handler
buttonA_handler
de verschillende toestanden:- state 0: led is uit; state 1: led is aan
- per toestand - gegeven de input-event van de huidige handler, dus eigenlijk per overgang:
- de actie, bijvoorbeeld
led.value(1)
, om de led aan te zetten; - het zetten van de volgende toestand, bijvoorbeeld
led_state = 1
- de actie, bijvoorbeeld
Het totale programma wordt dan:
from machine import Pin
from utime import sleep
buttonA = Pin(20, Pin.IN)
led = Pin(9, Pin.OUT)
led_state = 0
def buttonA_handler():
global led_state
if led_state == 0:
led.value(1) # action
led_state = 1 # next state
else: # led_state == 1
led.value(0) # action
led_state = 0 # next state
prev_levelA = 0
while True:
cur_levelA = buttonA.value()
if cur_levelA == 1 and prev_levelA == 0: # detect buttonA pushed
buttonA_handler() # call handler
prev_levelA = cur_levelA
sleep(0.1)
Opmerking:
- in Python moet je een globale variabele die je aanpast in een functie, daar aangeven als
global
. Anders wordt aangenomen dat het om een lokale variabele gaat. Zie hierboven:global led_state
.
Over eindige automaten en toestandsdiagrammen
Een eindige automaat heeft een eindig aantal toestanden, en een eindig aantal overgangen tussen deze toestanden. Elke toestandsovergang is gelabeld met een inputsymbool: als deze overgang gekozen wordt, op grond van de huidige toestand en het huidige invoersymbool, dan wordt het invoersymbool verwerkt en gaat de automaat over naar de bijbehorende volgende toestand.
Eén van de toestanden is de begintoestand; deze gegeven we aan met een binnenkomende pijl (overgang) zonder label of bron-toestand.
In ons geval wordt de rol van de invoersymbolen overgenomen door de input-events. Bovendien kan een toestandsovergang voorzien zijn van een actie, die uitgevoerd wordt als deze overgang gekozen wordt. We noteren deze combinatie bij een overgang als input-event / output-action
.
- Een dergelijke automaat, die uitvoer genereert (of acties uitvoert) bij een overgang, heet een Mealy-automaat. Een alternatieve vorm is de Moore-automaat: daar hangt de uitvoer af van de toestand. De Mealy-automaat past beter bij het event-model en de acties die je vanuit een programma kunt uitvoeren.
Ontwerp: 1-knops dimmer
Je kunt de aan-uitschakeling met 1 knop uitbreiden met meerdere toestanden. Elke volgende toestand komt bijvoorbeeld overeen met een hoger lichtniveau van de LED. Op die manier kun je een dimmer maken met 1-knops bediening. (Ik heb een lamp met een aanmaakschakelaar die op deze manier werkt.)
Merk op dat het uitschakelen in 1 keer vanuit de LED op volledige sterkte gaat. Je moet er steeds voor zorgen dat een toestand maar 1 uitgaande pijl heeft die met "A" gelabeld is: anders krijg je een dubbelzinnige automaat waarvan je niet weet hoe die zich gedraagt.
- In een deterministische automaat zijn alle uitgaande overgangen van een toestand gelabeld met verschillende inputsymbolen (input-events).
Voor 2 tussenliggende lichtsterktes krijg je het volgende ontwerp:
Toestandsdiagram:
Opmerking: de lichtsterktes lopen exponentieel op, omdat de gevoeligheid van je oog logaritmisch is (waardoor je oog een heel groot dynamisch bereik heeft).
De bijbehorende tabel:
Input A | Toestand | --> | Actie | Toestand |
---|---|---|---|---|
button A pushed (0->1) | state 0 | --> | led 1/4 | state 1 |
button A pushed (0->1) | state 1 | --> | led 1/2 | state 2 |
button A pushed (0->1) | state 2 | --> | led 1 | state 2 |
button A pushed (0->1) | state 3 | --> | led 0 | state 0 |
Programma: microPython 1-knops dimmer
from machine import Pin, PWM
from utime import sleep
buttonA = Pin(20, Pin.IN)
led = PWM(Pin(9)) # create PWM object from a pin
led.freq(1000) # set frequency
max_level = 65535 # max PWM level
led_state = 0
def buttonA_handler():
global led_state
if led_state == 0:
led.duty_u16(max_level // 4)
led_state = 1
elif led_state == 1:
led.duty_u16(max_level // 2)
led_state = 2
elif led_state == 2:
led.duty_u16(max_level)
led_state = 3
else: # led_state == 3
led.duty_u16(0)
led_state = 0
prev_levelA = 0
while True:
cur_levelA = buttonA.value()
if cur_levelA == 1 and prev_levelA == 0:
buttonA_handler()
prev_levelA = cur_levelA
sleep(0.1)
Opdracht: 2-knops dimmer
Ontwerp een automaat (toestandsdiagram en overgangen-tabel) voor een dimmer met 4 lichtniveaus (0, 1/4, 1/2, en 1 * de maximale lichtsterkte), met twee drukknoppen. Indrukken van knop A geeft een hogere lichtsterkte, als dat mogelijk is; indrukken van knop B geeft een lagere lichtsterkte, als dat mogelijk is.
Maak vanuit de overgangen-tabel het bijbehorende Python-programma.
Timers en timer-events
In sommige gevallen wordt de wachttijd in een programma aangegeven door sleep(dt)
.
Een voorbeeld is het klassieke Blink-programma:
while True:
led.value(1)
sleep(0.1)
led.value(0)
sleep(0.1)
Hiermee laat je een led knipperen, met een frequentie van 1 / 0.2s = 5 Hz.
In sommige omgevingen is deze sleep
-opdracht een blokkerende functie: er kan tijdens deze actie niets anders gedaan worden.
Dit is bijvoorbeeld al een probleem als je twee LEDs onafhankelijk van elkaar wilt later knipperen, bijvoorbeeld met 3Hz en 5Hz.
Een alternatief voor het gebruik van sleep
is dan het gebruik van timers en timer-events.
- In de Arduino-omgeving is
sleep
een blokkerende actie. In de MakeCode blokjes-omgeving is dat niet het geval: daar kun je elk stukje programma metsleep
-acties een eigen "thread" geven. (Zie elders.)
Berichten
Deelsystemen kunnen met elkaar communiceren met behulp van berichten, eventueel via (radio)communicatie. Een inkomend bericht kun je dan ook weer als een event beschouwen. Het versturen van een bericht is een actie die dan door een ander (deel)systeem verwerkt wordt.
rest
In dit gedeelte behandelen we een aanpak voor de verwerking, in het bijzonder die van events. Met deze aanpak kun je op een systematische manier je programma maken. Voor niet al te complexe problemen levert dit snel een werkend programma.
Een belangrijk uitgangspunt is dat elke input-event op elk moment kan plaatsvinden. Je programma moet dan ook op elk moment op elk input-event kunnen reageren. Je kunt er niet vanuit gaan dat de omgeving de events in de gewenste volgorde of op het gewenste moment aanbiedt
- Denk bijvoorbeeld aan een robot die een lijn volgt: deze kan op elk moment een obstakel tegenkomen, of een besturingsbericht ontvangen via de afstandsbediening.
In een Python-programma betekent dit dat we een event koppelen aan een functie:
deze functie wordt telkens aangeroepen als de bijbehorende event optreedt.
Zo'n functie noemen we een event-handler.
- De betekenis is dan:
WHEN event THEN handler()
In de Makecode blokjestaal voor de microbit heb je apart blokken voor deze event-handlers: "wanneer knop A wordt ingedrukt", "bij schudden", of "wanneer pin0 wordt aangeraakt". De code binnen dit blok is de handler die uitgevoerd wordt als de event optreedt.
Opmerking. Een bekend programma voor regels met deze structuur is "IFTTT" - "if this then that".
Met de "if" hierin wordt niet de "if" van een programmeertaal bedoeld, maar een event of trigger die op elk moment plaats kan vinden.
"when", of eigenlijk "whenever", zou een betere naam geweest zijn.
Analoge en digitale signalen; bemonstering
Een analoog signaal is continu: dit heeft op elk moment in de tijd een waarde in een continu domein.
- We noemen een signaal "analoog" omdat de waarde van het signaal analoog is met een fysieke grootheid. Bijvoorbeeld: het elektrische signaal van een microfoon is analoog met de geluidstrillingen in de lucht. Of, de weerstand van een lichtgevoelige weerstand is analoog met de lichtsterkte op elk moment.
- Meestal werken we met elektrische signalen: die zijn het gemakkelijks te verwerken (versterken, filteren, enz.)
Een digitaal signaal is discreet: dit heeft alleen op bepaalde momenten in de tijd een waarde, in een discreet (geheeltallig) domein.
Een digitale computer werkt met eindige getallen;
Getallen van het type "floating point" zijn zwevende-komma getallen met een eindig aantal significate cijfers: in de meeste gevallen (voor 32-bit floats) "slechts" 23 bits, ofwel ongeveer 7 decimalen.
Voor sensoren en actuatoren hebben we vaak aan 16-bits gehele getallen voldoende: vaak is een sensor niet nauwkeuriger dan 1%.
- Hoeveel decimalen heb je nodig voor een temperatuursensor van 0..100 graden, met een nauwkeurigheid van 1/10 graad?
Over nauwkeurigheid en precisie
- toevallige fouten ("ruis") en systematische fouten
- herhaalbaarheid
- doeltreffendheid
Dit betreft vnl. eigenschappen van sensoren.
Sturen met signalen
Soms stuur je een output-signaal direct vanuit een input-signaal: signaal -> bewerking -> signaal.
De eenvoudigste vorm hiervan is een directe besturing, bijvoorbeeld als je een potmeter (analoge input) gebruikt voor het regelen van de snelheid van een motor, of van de lichtsterkte van een lamp.
from machine import ADC, PWM, Pin
potmeter = ADC(Pin(26)) # create potmeter object on ADC pin
motor = PWM(Pin(0)) # create motor object on PWM pin
while True:
level = potmeter.read_u16()
motor.duty_cycle(level)
We kunnen de opdrachten in de lus in dit geval zelfs combineren tot motor.duty_cycle(potmeter.read_u16())
.
Vaak moeten we het input-signaal omrekenen naar een waarde die geschikt is voor de uitvoer. Deze berekening komt dan tussen het inlezen van de input-waarde en het schrijven van de output-waarde.
Het wordt iets complexer als we op basis van de input-waarde de output-waarde bij moeten sturen (proportionele regeling).
from machine import ADC, PWM, Pin
speed = ADC(Pin(26)) # create potmeter object on ADC pin
motor = PWM(Pin(0)) # create motor object on PWM pin
goal = 123 # preset speed
while True:
level = speed.read_u16()
motor.duty_cycle(level )
Een stap verder is het gebruik van een volledige PID-regeling (zie: https://en.wikipedia.org/wiki/PID_controller).
?Detecteren van events
Voor het detecteren van een event in een signaal hebben we, naast de huidige waarde van het signaal, een stukje van de signaal-historie nodig. In het eenvoudige geval van het indrukken van een drukknop hebben we aan de vorige waarde genoeg.
Speciale gevallen van eventdetectie:
- temperatuurregeling: temperatuur komt boven (of onder) een bepaalde grens
- de regeling is vaak een hysterese-regeling: de grens voor het uitschakelen ligt anders dan voor het inschakelen.
- de event-detectie doe je eigenlijk op basis van de toestand van de schakeling (niet van de temperatuur alleen): als de temperatuur onder een bepaalde grens komt, en de CV is nog niet ingeschakeld, dan schakel je deze in.
- bij een CV komt daar nog een effect bij: je compenseert voor een zekere "overshoot", er is nog de nodige warmte in de installatie als je de brander uitschakelt. (Je laat de pomp dan ook langer lopen.) (Voor zover ik weet schakelt de thermostaat alleen de CV; de schakeling van de pomp gebeurt vanuit de ketel.)
- vgl. het koken van melk op een elektrisch fornuis...
Afhandelen van events
De "normale" manier om een event af te handelen is door de bijbehorende handler-functie ("on event") uit te voeren. Dit is het model dat bijvoorbeeld bij grafische gebruikersinterfaces gebruikt wordt, zoals met JavaScript in de browser.
Er zijn een paar zaken om rekening mee te houden:
- elke input-event kan in principe op elk moment optreden: je hebt de buitenwereld niet in de hand. Hooguit kun je op basis van fysieke eigenschappen aangeven met welke snelheid processen plaatsvinden.
- maar: je bent mogelijk niet op elk moment in elke input geïnteresseerd. Afhankelijk van de huidige toestand (mode) zijn sommige inputs relevant, andere niet.
- ook al heb je met de buitenwereld een bepaald protocol afgesproken, je kunt er niet vanuit gaan dat de buitenwereld zich aan deze regels houdt. (En: je kunt er niet op rekenen dan de buitenwereld in andere opzichten betrouwbaar is.)
Events en automaten
Bij besturingstoepassingen maken we vaak gebruik van eindige automaten. De invoer- en uitvoertekens van een automaat komen dan overeen met invoer- en uitvoer-events.
Een eenvoudig voorbeeld is het aan- en uitschakelen van een LED met een drukknop. Bij elke druk op de knop schakelt de LED om: van uit naar aan en omgekeerd.
De automaat hiervoor heeft twee toestanden, 0 (LED uit) en 1 (LED aan). Er is 1 input-symbool: "push". Dit zorgt voor een overgang naar de andere toestand. Bij deze overgang hoort een output-symbol: van 0 naar 1: "led-on", van 1 naar 0: "led-off".
<<fig>>
Dit kunnen we op de volgende manier in een programma omzetten:
Events en messages (berichten)
Een andere manier om met events om te gaan is om deze om te zetten in messages (berichten). Deze berichten kunnen lokaal afgehandeld worden, of ze kunnen gecommuniceerd worden naar andere systemen, om daar af te handelen.
Een voordeel van messages is dat deze compleet asynchroon afgehandeld kunnen worden, zoals in het geval van NodeRed.
Een "blok" (node, thing?) voor het afhandelen van een message kan een (strikte) functie zijn, maar ook een automaat, met een eigen toestand. Een dergelijke automaat-blok kan meerdere event-streams als input hebben, of als output.
NodeRed
NodeRed is een grafisch programmeersysteem voor dataflow/event/messageflow. Een "normaal" blok heeft message/event-inputs en message/event-outputs.
NodeRed is gebaseerd op een message-model; dat verschilt op een aantal punten van een event-model. Messages zijn niet globaal, maar worden alleen via verbindingen tussen knopen gecommuniceerd. Op elk moment kunnen meerdere messages van eenzelfde type onderweg zijn. Bij events is dat niet (altijd) het geval; maar je kunt ook in dat geval een event-queue gebruiken voor het bufferen van events.
Events hebben een globaal karakter, en een "publish/subscribe" karakter. Elk programmaonderdeel kan zich abonneren op een event.
NodeRed is gebaseerd op Node.js.
ThingFlow
- https://thingflow.io
- https://thingflow-python.readthedocs.io/en/latest/
- https://github.com/mpi-sws-rse/thingflow-python
- https://micropython-iot-hackathon.readthedocs.io/en/latest/
Opmerking: de laatste activiteiten rond thingflow lijken van 2017. Dat lijkt niet gunstig voor de continuïteit. Maar: de hele implementatie is niet erg groot, dit is relatief eenvoudig te onderhouden of aan te passen.
Thingflow voor de microbit? De implementatie is niet erg groot of ingewikkeld, het lijkt mij dat dit zou moeten werken.
- Ik vind de naamgeving van ThingFlow in een aantal opzichten niet handig: (i) ik zou (in een IoT-omgeving) nodes voor de verwerking nooit "things" noemen; (ii) ik zou een OutputThing eerder "inputNode" noemen.
- Ik heb nog geen ervaring met het gebruik van deze aanpak voor een microcontroller; ik ben benieuwd welke voor- en nadelen dit heeft.