Kurs esp32/esp8266 | #4 – UART

UART | esp-idf

Uart, to interfejs oferujący sposób szeregowej wymiany danych. Do jego realizacji używamy dwóch pinów mikrokontrolera – RX oraz TX, odpowiedzialnych kolejno za odbiór i wysyłanie danych. Uart z pewnością nie raz ci się przyda podczas programowania mikrokontrolerów. Niektóre moduy/układy wykorzystują go do komunikacji z mikrokontrolerem, stosuje się go do skomunikowania dwóch mikrokontrolerów, czy komunikacji mikrokontroler-komputer. Dzisiaj w komputerach nie mamy już złącz typu rs232 więc komunikacja odbywa się poprzez osobny układ scalony. Jeżeli korzystasz z płytki prototypowej to możesz się komunikować z esp za pomocą przewodu USB, ponieważ taki układ się na niej znajduję.

Dokumentacje:

esp8266 rtos sdk uart

esp32 uart


uart – Wysyłanie danych

Posługuje się tutaj linux’ową aplikacją minicom do odczytywania danych nadchodzących od mikrokontrolera. W przypadku systemu Windows możesz użyć programu „putty”, lub wbudowany w program arduino IDE, monitor portu szeregowego.

Naszym pierwszym celem będzie napisanie programu, który co sekundę będzie wysyłał tekst „Hello, world.”. Jest to bardzo proste w realizacji.

Importujemy bibliotekę obsługującą uart:

#include "driver/uart.h"

Powołujemy zmienną typu „uart_config_t”, w której konfigurujemy opcje interfejsu:

uart_config_t uart_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity    = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE
    };

Omówię więc co tutaj się dzieje.

  • baud_rate – prędkość transmisji, najbardziej typowe to 9600, 57600, 115200
  • data_bits – ilość bitów z jakich składa się pojedynczy bufor danych
  • parity – bit służący do sprawdzania poprawności transmisji
  • stop_bits – ilość bitów kończących transferowany bufor
  • flow_ctrl – sprzętowe kontrolowanie transmisji

Taka konfiguracja wystarczy ci w większości zastosowań. Jeżeli jednak interesuje cię czym się różni przykładowo transmisja, w której pojedynczy bufor danych składa się z 6-ciu, a nie 8 bitów musisz bardziej się zaznajomić ze specyfikacją standardu UART. Tego tutaj opisywać nie będę.

Włączamy uart oraz zatwierdzamy naszą konfiguracje poniższymi poleceniami:

uart_driver_install(UART_NUM_0, 2048, 0, 0, NULL, 0);
uart_param_config(UART_NUM_0, &uart_config);

Użyłem tutaj makra „UART_NUM_0” ponieważ mikrokontrolery esp nie mają tylko jednego uarta. Esp32 ma ich aż trzy, natomiast esp8266 – dwa, z czego drugi (UART_NUM_1) może tylko transmitować dane (nie może odbierać).

Pierwsze polecenie uruchamia uart, przekazujemy mu nazwę uarta, wielkość buforu na odebrane dane (rx), wielkość buforu na dane do wysłania (tx), kolejne dwa argumenty odpowiadają za funkcję „quene” pochodzącą z freeRTOS’a. Ostatni argument w przypadku esp8266 nie ma znaczenia, natomiast w esp32 odpowiada za alokacje flagi przerwań, co na razie nas nie interesuje – wstawiamy tam 0.

Drugie polecenie zatwierdza naszą konfigurację dla uarta UART_NUM_0.

Teraz, stwórzmy sobie zmienną zawierającą ciąg znaków:

char tekst[] = "Hello, world.\n\r";

Znak ‘\n’ to przejście do kolejnej linii, natomiast znak ‘\r’ sprawia, że kursor terminala wróci na pozycje startową.

Wystarczy już tylko w głównej pętli programu umieścić funkcje wysyłającą ten tekst po uarcie i delay. Więc główna pętla naszego programu będzie wyglądała następująco:

while (1)
{
    uart_write_bytes(UART_NUM_0, tekst, sizeof(tekst));
    vTaskDelay(1000 / portTICK_RATE_MS);
}

Jak widać, jest to bardzo proste. Używamy funckji „uart_write_bytes()”, której przekazujemy, że korzystamy z uarta nr 0, kolejno podajemy zmienną zawierającą nasz tekst oraz wielkość tej zmiennej (otrzymaliśmy ją z wykorzystania wbudowanej w języku C, funkcji sizeof()).

uart – przykład dla esp32/esp8266

#include <stdio.h>
#include <stdlib.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"


void app_main(void)
{
     uart_config_t uart_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity    = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE
    };
    uart_driver_install(UART_NUM_0, 2048, 0, 0, NULL, 0);
    uart_param_config(UART_NUM_0, &uart_config);
    char tekst[] = "Hello, world.\n\r";

    while (1) 
    {
       uart_write_bytes(UART_NUM_0, tekst,sizeof(tekst));
       vTaskDelay(1000 / portTICK_RATE_MS);
    }
}

A jego efekt powinien być następujący:

esp32_uart

Zamiast „uart_write_bytes()” moglibyśmy także użyć polecenia typowego dla języka C – printf(„Hello, wolrd.\n”), lub ets_printf(„Hello, wolrd.\n”). Do zwykłego debugowania, czy wysyłania prostych danych na terminal polecam ci właśnie z tych poleceń korzystać. Dlaczego więc „uart_write_bytes”? Jeżeli robimy urządzenie, które będzie komunikować się z innym mikrokontrolerem, czy programem PC to mamy większą kontrolę nad tym co przelatuje po linii TX, i jest szybsze.

W obsługach przerwań polecenie printf() może powodować reset procesora – skorzystaj wtedy z ets_printf().

Polecenie printf, w przypadku esp32 powinno zadziałać bez konieczności inicjalizacji uarta. Z moich obserwacji wynika że, inaczej jest w przypadku esp8266 – tutaj musimy go zainicjalizować, gdyż bez tego odczytamy tylko jakieś „krzaki”.


uart – Odbiór danych

Skoro wiemy już jak wysyłać dane po uarcie teraz może je odbierzmy… Pokażę to na przykładzie komunikacji między dwoma mikrokontrolerami, jako drugi wykorzystam arduino UNO. Więc w skrócie, na arduino zrobiłem prosty program, który pod wpływem wciśnięcia przycisku wysyła po uarcie tekst „ABC” do esp. W tym celu połączyłem pin 1(TX) arduino i RXD0(RX-nodemcu8266) na esp. Celem programu na esp będzie po prostu wypisane w terminalu komputerowym tekstu, który otrzymał od arduino. Wystarczy lekko zmodyfikować poprzedni przykład.

Arduino zasiliłem napięciem 3.3 V (na takim działa esp). W innym przypadku pojawiałyby się błędy w transmisji.

kod arduino:

void setup() {
  Serial.begin(9600);

}

void loop() {
  if (digitalRead(7))
  {
    Serial.print("ABC\n\r");
    while (digitalRead(7));
  }
}

Ustawiłem w arduino prędkość transmisji na 9600, na taką też będziemy musieli zmienić w kodzie esp. Zrobiłem to dlatego gdyż atmega328p, która jest zainstalowana na arduino UNO obsłużyłaby transmisję o prędkości 115200, ale przy zastosowanym kwarcu na UNO (16MHz) mogłyby się często pojawiać błędy. W przypadku prędkości 9600 i kwarcu 16MHz wartość błędu to 0.2%, gdzie przy 115200 – 3.5%.

Wracamy do kodu na esp. Dodajmy do niego zmienną, której będziemy przekazywać zawartość bufora rx.

uint8_t *data = (uint8_t*)malloc(1024);

malloc to funkcja języka C służąca do dynamicznej alokacji pamięci, w tym przypadku dla zmiennej data rezerwujemy 1024 bajty.

I w taki sposób modyfikujemy pętlę główną:

while (1)
{
    const uint16_t len = uart_read_bytes(UART_NUM_0, data, 1024, 20/portTICK_RATE_MS);
    if (len>0)
    {
        uart_write_bytes(UART_NUM_0, (const char *)data, len);
        data[len] = 0;
    }
}

Funkcja „ uart_read_bytes” zwraca rozmiar w bajtach, danych zapisanych w buforze odbiorczym (rx). Wystarczy więc sprawdzić czy ta wartość jest większa od zera by dowiedzieć się czy coś w tym buforze się pojawiło. Bufor sprzętowy zostaje opróżniony po wywołaniu tej funkcji.

Przekazujemy jej numer UART’a, którym się posługujemy, nazwę naszego bufora, któremu ma przekazać dane, wielkość danych jakie maksymalnie może przekazać, oraz timeout w ms.

Warto tutaj zaznaczyć, przekazywanie otrzymanych danych do buforu „sprzętowego” dzieje się automatycznie. Więc my jedynie musimy sprawdzić czy coś się w nim pojawiło i sobie to skopiować do naszego buforu.

Dla pewności wyjaśnie tutaj znaczenie drugiego argumentu funkcji „uart_write_bytes()”, a mianowicie „(const char *)data”. Jako, że funkcja ta wymaga od nas przekazania argumentu o typie const char *, wykonałem operację rzutowania czyli zamiany typów danych.

Teraz po otwarciu podglądu portu szeregowego na komputerze, za każdym razem gdy wcisnę przycisk podpięty pod arduino, na podglądzie danych nadchodzących od esp widzę wysyłany przez arduino tekst. Dokładnie to chciałem osiągnąć.

Korzystam tutaj z jednego UARTA – UART_NUM_0, zarówno do skomunikowania arduino-esp i esp-komputer. Mogę tak zrobić ponieważ pin TX esp i tak byłby nieużywany w transmisji esp-arduino, tak samo jak pin RX w przypadku esp-komputer.

Jeżeli chciałbym przykładowo skomunikować ze sobą esp-arduino w taki sposób, że komunikacja następywałaby w obydwie strony to już nie mógłbym połączyć w obrębie jednego uarta więcej niż dwóch urządzeń (bez dodatkowej, programowej implementacji magistrali).


Uart freertos – błyskawiczne odbieranie danych

Czasami chcemy by nasz procesor błyskawicznie zareagował na jakieś dane pochodzące z uarta, skorzystamy w tym przykładzie z funkcji freertos’a. Radzę się więc zapoznać teraz z częścią kursu dotyczącą freertos.

Kurs esp-idf freeRTOS

Co zrobimy? Utworzymy zadanie, które cały czas monitorować będzie stan bufora rx (received). Tak będzie wyglądać funkcja zadnia:

void print_uart(void *arg)
{
    uint8_t *data = (uint8_t *) malloc(1024);
    while (1)
    {
        uint16_t len = uart_read_bytes(UART_NUM_0, data, 1024, 20 / portTICK_RATE_MS);
        if (len>0)
        {
            uart_write_bytes(UART_NUM_0, (const char *)data, len);
            uart_flush(UART_NUM_0);
            data[len] = 0;
        }
    }
    free(data);
}

Na samym początku zaalokowałem sobie 1024 bajty, które posłużą za bufor na dane, następnie wywołuję znaną nam już funkcje „uart_read_bytes”, która zwraca wielkość w bajtach otrzymanych danych. Jeżeli ta wartość jest większa od 0 (czyli coś się znajduje w buforze „sprzętowym”) to wysyłamy dane, które otrzymaliśmy i czyścimy bufor („sprzętowy” i nasz tymczasowy). Funkcja „uart_flush” służy do czyszczenia buforów sprzętowych. Bez jej użycia nie powinno być problemów, jednak wolałem ją zastosować.

Na końcu wstawiłem funkcję free, a w niej nazwę naszego bufora. Jest to funkcja wbudowana w standardową bibliotekę C służąca do zwalniania zaalokowanej pamięci. Jej użycie w naszym przypadku jest bezcelowe – nasze zadanie nigdy nie opuści pętli while. Gdy jednak będziesz chciał w swoim programie przerwać jakieś zadanie, a na jego potrzeby zaalokowałeś dynamicznie jakieś zmienne – zawsze pamiętaj by zwolnić później tę pamięć.

By program zadziałał pozostało tylko utworzyć zadanie:

xTaskCreate(print_uart, "print_uart", 2048, NULL, 1, NULL);

Myślę, że nie trzeba tutaj więcej tłumaczyć. Jeśli przeczytałeś część kursu dotyczącą freertos’a, to powinieneś wiedzieć co tutaj się właściwie dzieje.


Kolejna część: #5 – freeRTOS

Kurs esp32 i esp8266 – spis treści