Skip to content

Instantly share code, notes, and snippets.

@slintak
Last active April 20, 2023 11:34
Show Gist options
  • Save slintak/7c822c8d5bbbfff6df885770371682fd to your computer and use it in GitHub Desktop.
Save slintak/7c822c8d5bbbfff6df885770371682fd to your computer and use it in GitHub Desktop.
Příklady použití "sscanf" v Arduino.

Následující příklady ukazují zpracování řetězců přijatých po sériové lince (UART) na platformě Arduino. Vše bylo vyzkoušeno s deskou Arduino Uno, ovšem příklady budou fungovat na všech deskách, které podporují platformu Arduino.

Příklady jsou koncipovány tak, aby čtenáře postupně seznámili s příjmem řetězců od uživatele a jejich převodem na číslo. K převodu řetězce na číslo nejdříve používáme vlastní implementaci, později pak funkci atoi() a na závěr pak funkci sscanf().

/**
* Tento příklad čte příchozí znaky ze sériové linky (UART)
* a obratem je posílá zpět po sério lince.
* Každý příchozí znak se vytiskne jako "Znak: x"
*/
void setup() {
// Inicializace sériové komunikace (UART) na rychlost 9600 baudů.
Serial.begin(9600);
}
void loop() {
// Je v příchozí frontě nějaký znak?
if(Serial.available() > 0) {
// Přecti jeden znak z příchozí fronty a ulož do proměnné `c`.
char c = Serial.read();
// Vytiskni řetězec "Znak: " na sériovou linku.
Serial.print("Znak: ");
// Vytiskni přijatý znak.
Serial.println(c);
}
}
/**
* Tento příklad ukazuje jednoduchý způsob, jak převést
* dva příchozí znaky v kódování ASCII na celé dvouciferné
* číslo.
*
* Na sériovou linku stačí poslat dva znaky reprezentující
* číslo, například "4" a "2", tedy "42". Tyto znaky pak
* budou převedeny na celé číslo typu `int`.
*
* Příklad bude fungovat _pouze_ pro dvouciferná čísla.
* Příklad chybně zpracuje i jiné znaky, než "0" až "9".
*/
void setup() {
Serial.begin(9600);
}
void loop() {
// Počkej, až budou v příchozí frontě alespoň dva znaky.
if(Serial.available() >= 2) {
// Přečti z fronty dva znaky.
char digit1 = Serial.read();
char digit2 = Serial.read();
// Od obou přijatých znaků odečteme hodnotu '0', tedy
// ASCII hodnotu 48 reprezentující znak "nula".
digit1 -= '0'; // '0' == 48
digit2 -= '0';
// Nyní převedeme přijaté znaky na číslo.
// První přijatý znak 'digit1' reprezentuje v čísle
// řád desítek a druhý přijatý znak 'digit2'
// reprezentuje řád jednotek.
int number = (digit1 * 10) + digit2;
Serial.print("Cislo: ");
Serial.println(number);
}
}
/**
* Tento příklad ukazuje jednoduchý způsob, jak převést
* dva příchozí znaky v kódování ASCII na celé číslo v
* rozsahu 0 až 32767.
*
* Na sériovou linku stačí poslat dva znaky reprezentující
* číslo, například "4" a "2", tedy "42". Tyto znaky pak
* budou převedeny na celé číslo typu `int`.
*
* Příklad bude fungovat _pouze_ pro kladná celá čísla.
* Příklad chybně zpracuje i jiné znaky, než "0" až "9".
*/
void setup() {
Serial.begin(9600);
}
void loop() {
int length = Serial.available();
int number = 0;
// Počkej, až budou v příchozí frontě alespoň nějaké znaky.
if(length > 0) {
// Postupně projdeme všechny přijaté znaky z fronty.
// Celkový počet znaků je v proměnné `length`.
for(int i = 0; i < length; i++) {
// Přečti jeden znaky z fronty a odečti od něj
// ASCII hodnotu pro znak 0. Tím si převedeme
// přijatý znak na jednociferné číslo.
char digit = Serial.read();
digit -= '0'; // '0' == 48
// Do proměnné `number` postupně ukládáme přijaté číslo.
// Doposud přijatá hodnota je nejdříve vynásobena 10, tím
// se všechny přijaté čísla "posunou" o jeden řád doleva.
// Následně k číslu přičteme aktuální číslo, tím přidáme
// řád jednotek.
number *= 10;
number += digit;
// Pokud tedy ve frontě čeká např. řetězec "123", pak se "for"
// cyklus vykoná přesně 3x a v proměnné `number` bude postupně
// tato hodnota:
// 1
// 12
// 123
}
Serial.print("Cislo: ");
Serial.println(number);
}
// Počkáme 100 ms před dalším čtením fronty.
delay(100);
}
/**
* Tento příklad ukazuje použití funkce `atoi()` ze standardní
* knihovny jazyka C, která umožňuje převod řetězce na číslo v
* rozsahu -32767 až 32767.
*
* Na sériovou linku stačí poslat dva znaky reprezentující
* číslo, například "4" a "2", tedy "42". Tyto znaky pak
* budou převedeny na celé číslo typu `int`.
*
* Příklad bude fungovat _pouze_ pro řetězce ukončené znakem
* nového řádku, tedy `\n`.
* Příklad ignoruje jakékoliv jiné znaky, než "0" až "9".
* Pokud dojde při převodu k chybě (například pokud řetězec
* neobsahuje vůbec čísla), pak je výstupem funkce `atoi()`
* nulová hodnota a o chybě se nedozvíme.
*/
// Globální proměnné. Tyto použijeme pro uložení přijatého
// řetězce a jeho délky.
char buffer[128];
int buffer_length = 0;
void setup() {
Serial.begin(9600);
}
void loop() {
// Počkej, až budou v příchozí frontě alespoň nějaké znaky.
if(Serial.available()) {
// Přečti jeden znak z fronty do proměnné `c`.
char c = Serial.read();
// Ulož přijatý znak do paměti `buffer`, pouze pokud v proměnné
// `c` není znak nového řádku, tedy `\n` v ASCII kódování se
// jedná o hodnotu 10.
if(c != '\n') {
// Znak ulož to paměti a inkrementuj délku přijatého řetězce.
buffer[buffer_length] = c;
buffer_length++;
// Klíčové slovo `return` přeruší aktálně vykonávanou funkci,
// v tomto případě se přeruší funkce `loop()` a začne opět
// od začátku.
return;
}
// V proměnné `c` byl znak nového řádku. Přijali jsme tedy celý
// řetězec a můžeme jej zpracovat.
buffer[buffer_length] = '\0';
buffer_length = 0;
// Funkce `atoi()` ze standardní knihovny umí převést retězec na
// celé číslo v rozsahu -32767 až 32767. Zároveň ignoruje všechny
// znaky, které nejsou čísla. Vstup "123ahoj" se tedy převede na
// číslo "123" a následující znaky "ahoj" jsou ignorovány.
int number = atoi(buffer);
Serial.print("Prijato: \"");
Serial.print(buffer);
Serial.print("\", cislo: ");
Serial.println(number);
}
}
/**
* Tento příklad ukazuje použití funkce `sscanf()` ze standardní
* knihovny jazyka C, která umožňuje "parsování" libovolného řetězce
* v předem daném formátu. My funkci použijeme na převod řětězce na
* číslo rozsahu -32767 až 32767.
*
* Na sériovou linku stačí poslat dva znaky reprezentující
* číslo, například "4" a "2", tedy "42". Tyto znaky pak
* budou převedeny na celé číslo typu `int`.
*
* Příklad bude fungovat _pouze_ pro řetězce ukončené znakem
* nového řádku, tedy `\n`.
* Pokud dojde při převodu k chybě (například pokud řetězec
* neobsahuje vůbec čísla), pak se tuto informaci dozvíme
* z návratové hodnoty funkce `sscanf()`.
*/
// Globální proměnné. Tyto použijeme pro uložení přijatého
// řetězce a jeho délky.
char buffer[128];
int buffer_length = 0;
bool read_until(char end) {
// Počkej, až budou v příchozí frontě alespoň nějaké znaky.
if(Serial.available()) {
// Přečti jeden znak z fronty do proměnné `c`.
char c = Serial.read();
// Ulož přijatý znak do paměti `buffer`, pouze pokud v proměnné
// `c` není ukončující znak `end`. To může být například znak
// nového řádku '\n', v ASCII hodnota 10.
if(c != end) {
// Znak ulož to paměti a inkrementuj délku přijatého řetězce.
buffer[buffer_length] = c;
buffer_length++;
// Klíčové slovo `return` přeruší aktálně vykonávanou funkci,
// v tomto případě se přeruší funkce `read_loop()` a vrátí se
// hodnota `false`, která uživateli říká, že jsme ještě nenačetli
// celý řádek.
return false;
}
// V proměnné `c` byl ukončující znak. Přijali jsme tedy celý
// řetězec a můžeme jej zpracovat.
buffer[buffer_length] = '\0';
buffer_length = 0;
// Návratová hodnota `true` říká uživateli, že byl do proměnné
// buffer načten celý očekávaný řetězec.
return true;
}
return false;
}
void setup() {
Serial.begin(9600);
}
void loop() {
// Počkej, až načteme celý jeden řádek ukončený znakem '\n'.
if(read_until('\n')) {
Serial.print("Prijato: \"");
Serial.print(buffer);
Serial.println("\".");
int number = 0;
// Funkce `sscanf()` zpracuje celý řetězec z paměti `buffer`
// od začátku do konce podle daného formátu (druhý parametr).
// Formát "%d" znamená, že chceme z řetězce načíst celá čísla
// se znaménkem.
// Návratová hodnota funkce `sscanf()` je nula, pokud se nepovedlo
// zpracovat retězec podle zadaného formátu, jinak bude návratová
// hodnota nenulová.
if(sscanf(buffer, "%d", &number) == 1) {
Serial.print("Cislo: ");
Serial.println(number);
} else {
Serial.println("Cislo: nelze prevest.");
}
}
}
/**
* Tento příklad ukazuje použití funkce `sscanf()` ze standardní
* knihovny jazyka C, která umožňuje "parsování" libovolného řetězce
* v předem daném formátu. My funkci použijeme na převod jednoduché
* matematické operace ve formátu "číslo operátor číslo", například
* validní vstupy jsou:
* "42-2"
* "80*2"
* "1+1"
*
* Tento příklad dokáže zpracovat i vstup, který bychom v matematice
* rozhodně nenašli: "1+-1". Program takovýto řetězec zpracuje a
* výsledkem bude operace sčítání dvou čísel "1" a "-1", tedy "0".
* Úpravu kódu tak, aby toto zadání považoval za chybné necháme jako
* cvičení čtenáři.
*
* Na sériovou linku stačí poslat dva znaky reprezentující
* číslo, například "4" a "2", tedy "42" následované znakem matematické
* operace "+", "-" nebo "*" následované dalším číslem.
*
* Příklad bude fungovat _pouze_ pro řetězce ukončené znakem
* nového řádku, tedy `\n`.
*
* Pokud dojde při převodu k chybě (například pokud řetězec
* neobsahuje vůbec čísla), pak se tuto informaci dozvíme
* z návratové hodnoty funkce `sscanf()`.
*/
// Globální proměnné. Tyto použijeme pro uložení přijatého
// řetězce a jeho délky.
char buffer[128];
int buffer_length = 0;
bool read_until(char end) {
// Počkej, až budou v příchozí frontě alespoň nějaké znaky.
if(Serial.available()) {
// Přečti jeden znak z fronty do proměnné `c`.
char c = Serial.read();
// Ulož přijatý znak do paměti `buffer`, pouze pokud v proměnné
// `c` není ukončující znak `end`. To může být například znak
// nového řádku '\n', v ASCII hodnota 10.
if(c != end) {
// Znak ulož to paměti a inkrementuj délku přijatého řetězce.
buffer[buffer_length] = c;
buffer_length++;
// Klíčové slovo `return` přeruší aktálně vykonávanou funkci,
// v tomto případě se přeruší funkce `read_loop()` a vrátí se
// hodnota `false`, která uživateli říká, že jsme ještě nenačetli
// celý řádek.
return false;
}
// V proměnné `c` byl ukončující znak. Přijali jsme tedy celý
// řetězec a můžeme jej zpracovat.
buffer[buffer_length] = '\0';
buffer_length = 0;
// Návratová hodnota `true` říká uživateli, že byl do proměnné
// buffer načten celý očekávaný řetězec.
return true;
}
return false;
}
void setup() {
Serial.begin(9600);
}
void loop() {
// Počkej, až načteme celý jeden řádek ukončený znakem '\n'.
if(read_until('\n')) {
Serial.print("Prijato: \"");
Serial.print(buffer);
Serial.println("\".");
int number1;
int number2;
char operation;
// Funkce `sscanf()` zpracuje celý řetězec z paměti `buffer`
// od začátku do konce podle daného formátu (druhý parametr).
// Formát "%d" znamená, že chceme z řetězce načíst celé číslo
// se znaménkem.
// Formát "%c" znamená, že chceme načíst jeden znak v ASCII.
// Dohromady "%d%c%d" tedy znamená, že chceme přijmout řetězec
// ve tvaru "cislo" "znak" "cislo", například "123+456" nebo "42-2".
// Návratová hodnota funkce `sscanf()` je nula, pokud se nepovedlo
// zpracovat retězec podle zadaného formátu, jinak bude návratová
// hodnota nenulová.
if(sscanf(buffer, "%d%c%d", &number1, &operation, &number2) != 3) {
Serial.println("Chybny format!");
return;
}
if(operation == '+') {
// Načtená čísla budeme sčítat.
Serial.print("Scitani: ");
Serial.println(number1 + number2);
} else if(operation == '-') {
// Načtená čísla budeme odčítat.
Serial.print("Odcitani: ");
Serial.println(number1 - number2);
} else if(operation == '*') {
// Načtená čísla budeme násobit.
Serial.print("Nasobeni: ");
Serial.println(number1 * number2);
} else {
Serial.println("Chybny operator!");
}
}
}
/**
* Příklad, který ukazuje jak zobrazit text na LCD displeji
* o velikosti 16x2 znaků připojeném na sběrnici I2C.
*
* Tento příklad zobrazí na první řádek displeje vycentrovaný
* text "Hello World!" a na druhý řádek pak pravidelně tiskne
* počet sekund od startu.
*/
// Tuto knihovnu je potřeba nejdříve nainstalovat.
// Jedná se o LiquidCrystal I2C od Frank de Brabander.
// V Arduino IDE stačí otevřít "Library Manager",
// knihovnu vyhledat a nainstalovat.
#include <LiquidCrystal_I2C.h>
// Tento řádek slouží k nastavení základních parametrů
// knihovny pro LCD.
// Proměnná `lcd` bude obsahovat "instanci objektu", který
// nám umožní komunikovat s displejem. Ten je na I2C adrese
// 0x27 a má 16 znaků na řádek a 2 řádky.
LiquidCrystal_I2C lcd(0x27, 16, 2);
char buffer[33];
unsigned int counter = 0;
void setup() {
// Inicializace hardware displeje, smazání jeho obsahu
// a zapnutí podsvícení.
lcd.init();
lcd.clear();
lcd.backlight();
// Posuň kurzor na 3. znak prvního řádku (počítáno od nuly).
lcd.setCursor(2, 0);
// Zobraz řetězec "Hello World!".
lcd.print("Hello world!");
}
void loop() {
// Posuň kurzor na 6. znak druhého řádku (počítáno od nuly).
lcd.setCursor(5, 1);
// Zobraz řetězec obsahující počítadlo (counter).
sprintf(buffer, "%06u", counter);
lcd.print(buffer);
counter++;
delay(1000);
}
/**
* V tomto příkladu obsluhujeme dvě tlačítka připojené k digitálním
* pinům 2 a 3 na desce Arduino UNO. K zaznamenání stisku a ošetření
* kmitání (dedouncing) používáme přerušení (interrupt).
*
* Tato základní ukázka vyžaduje dva pull-up rezistory o hodnotě 10kOhm
* a dvě tlačítka (puss button NC).
*
* Kód je dostatečně jednoduchý a pochopitelný, takže ho není třeba
* popisovat nijak podrobně. Za povšimnutí stojí použití klíčových
* slov `volatile` pro stavové proměnné tlačítek a `static` u
* proměnných `last_interrupt_time` uvnitř rutiny přerušení.
*/
// Stavové proměnné obou tlačítek A a B.
// Hodnota `true` znamená, že tlačítko bylo aktivováno.
volatile bool button_A_state = false;
volatile bool button_B_state = false;
// Obsluha přerušení pro tlačítko A.
void button_A_handler() {
static unsigned long last_interrupt_time = 0;
unsigned long interrupt_time = millis();
// Pokud přerušení přijde v kratší době jak 200 ms od
// posledního přerušení, považujeme takovou událost za
// zákmit a ignorujeme.
if (interrupt_time - last_interrupt_time > 200) {
button_A_state = true;
}
last_interrupt_time = interrupt_time;
}
// Obsluha přerušení pro tlačítko B.
// Tento kód je téměř identický s obsluhou tlačítka A.
void button_B_handler() {
static unsigned long last_interrupt_time = 0;
unsigned long interrupt_time = millis();
// Pokud přerušení přijde v kratší době jak 200 ms od
// posledního přerušení, považujeme takovou událost za
// zákmit a ignorujeme.
if (interrupt_time - last_interrupt_time > 200) {
button_B_state = true;
}
last_interrupt_time = interrupt_time;
}
void setup() {
Serial.begin(9600);
// Tímto aktivujeme obsluhu přerušení pro piny 2 a 3, na kterých máme
// připojeny tlačítka A a B.
// Obsluha bude reagovat pouze na sestupnou hranu (falling edge)
// signálu tlačítka.
attachInterrupt(digitalPinToInterrupt(2), button_A_handler, FALLING);
attachInterrupt(digitalPinToInterrupt(3), button_B_handler, FALLING);
}
void loop() {
if(button_A_state) {
Serial.println("Button A");
button_A_state = false;
}
if(button_B_state) {
Serial.println("Button B");
button_B_state = false;
}
}
/**
* Jednoduchá meteo stanice, která měří aktuální teplotu
* pomocí termistoru na analogovém pinu A0.
*
* Výsledky měření se zobrazují na 2x16 LCD displeji
* připojeném přes I2C sběrnici.
*
* Pomocí dvou tlačítek lze měnit hodnoty, které se na
* displeji zobrazují.
*/
#include <LiquidCrystal_I2C.h>
#define THERMISTOR_PIN A0
#define BUTTON_A_PIN 2
#define BUTTON_B_PIN 3
#define R1 (9.78) // Rezistor R1 na napěťovém děliči, kOhm
#define R0 (10.0) // Rezistivita termistoru při teplotě T0, kOhm
#define T0 298.15 // Teplota T0 termistoru, Kelvin
#define B 4050.0 // Beta parametr použitého termistoru, Kelvin
// Speciální symboly pro LCD displej.
// Pro potřeby naší meteo stanice definujeme 3 speciální
// znaky: znak stupně, "e" s čárkou, "u" s kroužkem a
// řecké písmeno Omega.
// Tyto znaky budou uloženy v paměti LCD pod čísly 1 až 4.
// Pomůcka: https://maxpromer.github.io/LCD-Character-Creator/
byte stupen[] = {
B01110, B01010, B01110, B00000,
B00000, B00000, B00000, B00000
};
byte e_dlouhe[] = {
B01010, B00100, B01110, B10001,
B11110, B10000, B01110, B00000
};
byte u_krouzek[] = {
B00110, B00110, B10001, B10001,
B10001, B10011, B01101, B00000
};
byte omega[] = {
B00000, B00000, B01110, B10001,
B10001, B01010, B11011, B00000
};
// Inicializace knihovny LiquidCrystal pro ovládání
// 2x16 LCD displeje přes I2C sběrnici.
LiquidCrystal_I2C lcd(0x27, 16, 2);
static bool display_update = false;
static int8_t display_value = 0;
static uint16_t temperature_measurement = 0;
static uint16_t temperature_adc = 0;
static float temperature_resistivity = 0;
static float temperature_current = 0;
static float temperature_average = 0;
// Stavové proměnné obou tlačítek A a B.
// Hodnota `true` znamená, že tlačítko bylo aktivováno.
volatile bool button_A_state = false;
volatile bool button_B_state = false;
static uint32_t last_temperature_measurement = 0;
static uint32_t last_display_update = 0;
// Obsluha přerušení pro tlačítko A.
void button_A_handler() {
static unsigned long last_interrupt_time = 0;
unsigned long interrupt_time = millis();
// Pokud přerušení přijde v kratší době jak 200 ms od
// posledního přerušení, považujeme takovou událost za
// zákmit a ignorujeme.
if (interrupt_time - last_interrupt_time > 200) {
button_A_state = true;
}
last_interrupt_time = interrupt_time;
}
// Obsluha přerušení pro tlačítko B.
// Tento kód je téměř identický s obsluhou tlačítka A.
void button_B_handler() {
static unsigned long last_interrupt_time = 0;
unsigned long interrupt_time = millis();
// Pokud přerušení přijde v kratší době jak 200 ms od
// posledního přerušení, považujeme takovou událost za
// zákmit a ignorujeme.
if (interrupt_time - last_interrupt_time > 200) {
button_B_state = true;
}
last_interrupt_time = interrupt_time;
}
void setup() {
Serial.begin(9600);
// Inicializace LCD knihovny, zapnutí podsvícení
// a uložení speciálních znaků do paměti LCD.
lcd.init();
lcd.clear();
lcd.backlight();
lcd.createChar(1, stupen);
lcd.createChar(2, e_dlouhe);
lcd.createChar(3, u_krouzek);
lcd.createChar(4, omega);
// Tímto aktivujeme obsluhu přerušení pro piny 2 a 3, na kterých máme
// připojeny tlačítka A a B.
// Obsluha bude reagovat pouze na sestupnou hranu (falling edge)
// signálu tlačítka.
attachInterrupt(digitalPinToInterrupt(BUTTON_A_PIN), button_A_handler, FALLING);
attachInterrupt(digitalPinToInterrupt(BUTTON_B_PIN), button_B_handler, FALLING);
}
void loop() {
// Do proměnné `now` si uložíme aktuální počet milisekund
// od posledního restartu. Tímto si počítáme čas potřebný
// k měření teploty.
uint32_t now = millis();
if(button_A_state) {
display_value++;
display_value = constrain(display_value, 0, 3);
Serial.print("Zobrazuji "); Serial.println(display_value);
display_update = true;
button_A_state = false;
}
if(button_B_state) {
display_value--;
display_value = constrain(display_value, 0, 3);
Serial.print("Zobrazuji "); Serial.println(display_value);
display_update = true;
button_B_state = false;
}
// Jednou za 1000 ms provedeme měření teploty a její
// zobrazení na displeji.
if((now - last_temperature_measurement) > 1000) {
last_temperature_measurement = now;
// Převod analogové hodnoty napětí na termistoru
// na rezistivitu.
uint16_t adc = analogRead(THERMISTOR_PIN);
float R_temp = (R1 * adc) / (1024 - adc);
// Sheinhart-Hart výpočet teploty z rezistivity
// termistoru.
float steinhart = R_temp / R0;
steinhart = log(steinhart); // Přirozený logaritmus
steinhart /= B;
steinhart += (1 / T0);
steinhart = 1 / steinhart;
steinhart -= 273.15;
temperature_adc = adc;
temperature_resistivity = R_temp;
temperature_current = steinhart;
temperature_average += steinhart;
temperature_measurement++;
Serial.print("T1:");
Serial.println(steinhart);
}
// Hodnoty na displeji budeme aktualizovat jednou za 1000 ms
// nebo pokud je nastavena proměnná `display_update`.
if((display_update) || ((now - last_display_update) > 1000)) {
display_update = false;
if(display_value == 0) {
lcd.setCursor(0, 0);
lcd.print("Aktualni teplota");
lcd.setCursor(0, 1);
lcd.print(temperature_current);
lcd.print("\1C ");
} else if(display_value == 1) {
lcd.setCursor(0, 0);
lcd.print("Pr\3m\2rna teplota");
lcd.setCursor(0, 1);
lcd.print(temperature_average / temperature_measurement);
lcd.print("\1C ");
} else if(display_value == 2) {
lcd.setCursor(0, 0);
lcd.print("Rezistivita ");
lcd.setCursor(0, 1);
lcd.print(temperature_resistivity);
lcd.print(" k\4 ");
} else if(display_value == 3) {
lcd.setCursor(0, 0);
lcd.print("Hodnota ADC ");
lcd.setCursor(0, 1);
lcd.print(temperature_adc);
lcd.print(" ");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment