[102] Radio z wyświetlaczem
![[102] Radio z wyświetlaczem](/_next/image?url=https%3A%2F%2Farduino.pl%2Fimgproxy%2FuV_l1pELesdnEBS6lsT01Hl-mmTXbqsrrbOsbwyr9aA%2Ff%3Awebp%2Fw%3A1200%2FbG9jYWw6Ly8vaW1hZ2VzLzAtNjdlL2IxNzcwLzA0N2RkLzAtNjdlYjE3NzAwNDdkZDc2NTQ1OTk4Ni5wbmc%3D.webp&w=3840&q=75)
Niedawno prezentowałem projekt odbiornika radiowego sterowanego Arduino, wykorzystującego jedną z popularnych płytek tunera, do której można było się dostać za pomocą magistrali I2C Wspomniałem wówczas, że płytka ta może zaoferować nam także tak zwaną usługę RDS, czyli wyświetlania tekstów wplecionych w sygnał radiowy. O szczegółach – za chwilę, gdyż by cokolwiek wyświetlić, potrzebny jest wyświetlacz. Więc zacznijmy najpierw od niego. O tych urządzeniach, zbudowanych na bazie układu HD44780, pisałem już wiele razy. To chyba najliczniejsza rodzina, choć obecnie już nieco ograniczona. Wyświetlacze zawierają od ośmiu do osiemdziesięciu znaków w czterech, dwóch lub w jednym wierszu, a każdy znak dostaje dla siebie pole 5x7 pikseli.
Wyświetlacze te mogą pracować z szyną równoległą ośmiobitową – w praktyce nie używa się tego trybu, czterobitową, a ostatnio popularne są wersje z magistralą I2C, choć nie jest to rozwiązanie natywne, a po prostu dokłada się do takich wyświetlaczy ekspander.

I taki przykład mamy na płytce edukacyjnej TME, której używam, budując kolejne urządzenia. Wyświetlacz o organizacji 16 kolumn na 2 wiersze umieszczono nad ekspanderem MCP23008 i połączono go według schematu dołączonego do dokumentacji płytki. Dołożenie jednego, taniego układu pozwala zaoszczędzić cztery linie, ale istotniejsze jest to, że otrzymujemy układ pracujący na magistrali I2C, która zwykle i tak jest w użyciu. W naszym urządzeniu pracuje na niej płytka radiowego tunera. O samym ekspanderze pisałem już kiedyś.
Zanim do niej przejdziemy, zapoznajmy się z wyświetlaczem sterowanym w ten nowy sposób. Szkic będzie malutki.
#include Wire.h // Biblioteka obsługująca magistralę I2C
#include hd44780.h // Biblioteka obsługująca wyświetlacze 44780
#include hd44780ioClass/hd44780_I2Cexp.h // Dodatek obsługujący wyświetlacze podłączone do ekspandera I2C
hd44780_I2Cexp lcd(0x20, I2Cexp_MCP23008, 7, 6, 5, 4, 3, 2, 1, HIGH); // Konfiguracja połączeń wyświetlacza LCD
void setup() {
lcd.begin(16, 2); // Inicjalizacja wyświetlacza LCD
lcd.setCursor(0, 0); // Ustaw kursor na początku wyświetlacza.
lcd.print("Dzien dobry! :)"); // Wyświetl napis.
}
void loop() {
}
Najpierw należy zaimportować dodatkowe biblioteki: odpowiedzialną za magistralę I2C oraz współpracującą z wyświetlaczem. Należy ją jeszcze uzupełnić dodatkową biblioteką dla tej konkretnej aplikacji wyświetlacza i ekspandera.
Następnie inicjujemy układ wyświetlacza z ekspanderem. Tam także znajduje się adres ekspandera i we własnych rozwiązaniach należy pamiętać o tym, by adresy poszczególnych kostek pracujących razem różniły się.
W końcu mamy już typowe rozkazy współpracy w wyświetlaczem: deklarację rozmiarów, ustawienie kursora na początku oraz wyświetlenie napisu. Na koniec wstępu dodam, że oczywiście można użyć także wyświetlacz pracujący klasycznie, na magistrali czterobitowej. Trzeba wtedy wygospodarować dodatkowe sześć linii i poczynić lekkie zmiany w szkicu, do którego zaraz przejdziemy. Zainteresowanych szczegółami zapraszam do tego artykułu.
Skoro już opanowaliśmy wyświetlacz, czas na napisanie jakiegoś menu do projektu radia, o którym niedawno pisałem. Zakończyliśmy na etapie adaptacji świecącej cyferki, którą można oczywiście pozostawić, ale ja z niej tutaj zrezygnuję, by nie zaciemniać logiki programu – wszak numer programu w pamięci będzie się nam teraz wyświetlał na wyświetlaczu ciekłokrystalicznym. Wykorzystamy wszystko, co do tej pory powstało, a ja się skupię wyłącznie na zmianach i dodatkach. Jeśli coś z pozostałych elementów programu wydaje się niezrozumiałe, zapraszam do poprzednich artykułów. Tym razem przyjmę nieco inny porządek: od razu będziemy operować na wersji ostatecznej, ale będę komentował wszystko zgodnie z kolejnością powstania.

Najpierw założenia: mając dwa rzędy po 16 znaków można menu urządzić na przykład tak. A więc na górze mamy: częstotliwość odbieranej stacji, potem numer programu w pamięci, poniżej poziom ustawionej głośności, a po lewej stronie komunikaty RDS, o których zaraz opowiem. Elementy będziemy wdrażać po kolei, a zaczniemy od numeru programu.
#include <radio.h> // Biblioteka obsługi serii tunerów FM
#include <RDA5807FP.h> // Biblioteka obsługi konkretnej wersji tunera.
RDA5807FP radio; // Nadaj układowi nazwę "radio"
// #include <RDA5807M.h>
// RDA5807M radio;
// #include <SI4705.h>
// SI4705 radio;
// #include <SI47xx.h>
// SI47xx radio;
// #include <TEA5767.h>
// TEA5767 radio;
#include <RDSParser.h> // Biblioteka obsługująca protokół RDS
RDSParser rds;
#include <EEPROM.h> // Biblioteka obsługi pamięci nieulotnej.
#include <Wire.h> // Biblioteka obsługująca magistralę I2C
#include <hd44780.h> // Biblioteka obsługująca wyświetlacze 44780
#include <hd44780ioClass/hd44780_I2Cexp.h> // Dodatek obsługujący wyświetlacze podłączone do ekspandera I2C
hd44780_I2Cexp lcd(0x20, I2Cexp_MCP23008, 7, 6, 5, 4, 3, 2, 1, HIGH); // Konfiguracja połączeń wyświetlacza LCD
const byte poprzednia = 4; // Pstryczek "Poprzednia stacja"
const byte nastepna = 7; // Pstryczek "Następna stacja"
const byte programMinus = 5; // Pstryczek "Zmniejsz numer programu"
const byte programPlus = 6; // Pstryczek "Zwiększ numer programu"
const byte programZapisz = 8; // Pstryczek "Zapisz program"
const byte potencjometr = A1; // Potencjometr głośności.
int napiecieStare = 0; // Poprzednie zmierzone napięcie ustawione potencjometrem.
int napiecieNowe = 1023; // Aktualne zmierzone napięcie ustawione potencjometrem.
byte program = 1; // Numer aktualnego programu (1-29)
int czestotliwosc; // Aktualna częstotliwość stacji.
char czestotliwoscWyswietlana[11]; // Zmienna tekstowa, która reprezentuje częstotliwość pobraną z płytki tunera.
byte glosnosc; // Poziom aktualnej głośności.
void setup() {
radio.initWire(Wire); // Inicjuj tuner.
radio.setup(RADIO_FMSPACING, RADIO_FMSPACING_100); // Ustaw krok strojenia równy 100 kHz.
radio.setup(RADIO_DEEMPHASIS, RADIO_DEEMPHASIS_50); // Ustaw deemfazę obowiązującą w Europie.
radio.setBandFrequency(RADIO_BAND_FM, 9800); // Ustaw częstotliwość początkową na środek zakresu.
radio.setMono(false); // Ustaw tryb stereo.
radio.attachReceiveRDS(rdsOdbierz); // Zdefiniuj podprogram odbierający dane RDS z płytki tunera.
rds.attachServiceNameCallback(rdsWyswietl); // Zdefiniuj podprogram wyświetlający dane RDS
pinMode(poprzednia, INPUT_PULLUP); // Deklaruj linie pstryczków jako wejścia podciągnięte wewnętrznie do wysokiego stanu.
pinMode(nastepna, INPUT_PULLUP);
pinMode(programMinus, INPUT_PULLUP);
pinMode(programPlus, INPUT_PULLUP);
pinMode(programZapisz, INPUT_PULLUP);
lcd.begin(16, 2); // Inicjalizacja wyświetlacza LCD
lcd.clear(); // Wyczyść wyświetlacz i ustaw kursor na początku.
zmienProgram(); // Pobierz częstotliwość pierwszego programu z pamięci EEPROM i wyślij ją do tunera.
}
void loop() {
if (digitalRead(poprzednia) == HIGH) { // Jeśli wciśnięto przycisk "Poprzednia stacja"...
radio.seekDown(0); // Przestrój się w górę o 100 kHz (jedynka skacze do najbliższej stacji).
wyswietlCzestotliwosc(); // Wyświetl częstotliwość.
wyswietlKreski(); // Wyświetl kreski zamiast numeru programu.
while (digitalRead(poprzednia) == HIGH) {} // Czekaj aż przycisk zostanie puszczony.
}
if (digitalRead(nastepna) == HIGH) { // Jeśli wciśnięto przycisk "Następna stacja"...
radio.seekUp(0); // Przestrój się w dół o 100 kHz (jedynka skacze do najbliższej stacji).
wyswietlCzestotliwosc(); // Wyświetl częstotliwość.
wyswietlKreski(); // Wyświetl kreski zamiast numeru programu.
while (digitalRead(nastepna) == HIGH) {} // Czekaj aż przycisk zostanie puszczony.
}
if (digitalRead(programMinus) == HIGH) { // Jeśli wciśnięto przycisk "Zmniejsz numer programu"...
--program; // Zmniejsz numer programu.
if (program < 1) { // Jeśli numer programu jest mniejszy od 1...
program = 29; // Numer programu będzie równy 29
}
zmienProgram(); // Reszta procedury.
while (digitalRead(programMinus) == HIGH) {} // Czekaj aż przycisk zostanie puszczony.
}
if (digitalRead(programPlus) == HIGH) { // Jeśli wciśnięto przycisk "Zwiększ numer programu"...
++program; // Zwiększ numer programu.
if (program > 29) { // Jeśli numer programu jest większy od 29...
program = 1; // Numer programu będzie równy 1
}
zmienProgram(); // Reszta procedury.
while (digitalRead(programPlus) == HIGH) {} // Czekaj aż przycisk zostanie puszczony.
}
if (digitalRead(programZapisz) == HIGH) { // Jeśli wciśnięto przycisk "programZapisz"...
EEPROM.put(program * 2, radio.getFrequency()); // Zapisz w pamięci EEPROM aktualną częstotliwość.
lcd.clear(); // Wyczyść wyświetlacz i ustaw kursor na początku.
lcd.setCursor(1, 0); // Wyświetl komunikat.
lcd.print(F("Stacja zostala"));
lcd.setCursor(4, 1);
lcd.print(F("zapisana"));
delay(1000); // Zaczekaj chwilę...
lcd.clear(); // Odśwież wyświetlacz.
wyswietlCzestotliwosc();
wyswietlProgram();
wyswietlGlosnosc();
while (digitalRead(programZapisz) == HIGH) {} // Czekaj aż przycisk zostanie puszczony.
}
napiecieNowe = analogRead(potencjometr); // Mierzymy napięcie ustawione ślizgaczem potencjometru.
if (abs(napiecieNowe - napiecieStare) > 3) { // Odejmujemy tę wartość od wartości zmierzonej poprzednio i jeśli wartość bezwzględna wyniku jest większa od 3, to...
napiecieStare = napiecieNowe; // Przepisujemy wartość zmierzonego napięcia do zmiennej napiecieStare.
if (napiecieNowe < 4) { // Jeśli napięcie jest mniejsze od 4...
radio.setMute(1); // Wycisz radio.
glosnosc = 0; // Na potrzeby wyświetlacza ustaw wartość głośności równą zero.
} else { // W przeciwnym razie...
radio.setMute(0); // Wyłącz wyciszenie oraz...
glosnosc = map(napiecieNowe, 4, 1020, 0, 15); // Przemapuj pozycję potencjometru do zakresu (0-15)
radio.setVolume(glosnosc); // Wyślij tę wartość do rejestru głośności.
glosnosc++; // Na potrzeby wyświetlacza przesuń wartość głośności o jeden, bo zero to cisza.
}
wyswietlGlosnosc(); // Odśwież wyświetlacz.
}
radio.checkRDS(); // Odśwież dane RDS
}
void zmienProgram() {
EEPROM.get(program * 2, czestotliwosc); // Pobierz z pamięci EEPROM częstotliwość aktualnego kanału.
radio.setBandFrequency(RADIO_BAND_FM, czestotliwosc); // Wyślij tę częstotliwość do tunera.
wyswietlCzestotliwosc(); // Odśwież wyświetlacz.
wyswietlProgram();
}
void wyswietlCzestotliwosc() {
lcd.setCursor(0, 0); // Ustaw kursor.
delay(100); // Niezbędne opóźnienie celem ustabilizowania się układu tunera po zmianie częstotliwości.
radio.formatFrequency(czestotliwoscWyswietlana, 11); // Pobierz wartość częstotliwości z tunera.
lcd.print(czestotliwoscWyswietlana); // Wyświetl ją.
}
void wyswietlProgram() {
lcd.setCursor(11, 0); // Ustaw kursor.
lcd.print(F("Pr:")); // Wyświetl "PR:"
if (program < 10) { // Gdy numer programu jest mniejszy od 10...
lcd.print(F("0")); // Wyświetl zero wiodące.
}
lcd.print(String(program)); // Wyświetl numer programu.
}
void wyswietlGlosnosc() {
lcd.setCursor(10, 1); // Ustaw kursor.
lcd.print(F("Vol:")); // Wyświetl "VOL:"
if (glosnosc < 10) { // Gdy poziom głośności jest mniejszy od 10...
lcd.print(F("0")); // Wyświetl zero wiodące.
}
lcd.print(String(glosnosc)); // Wyświetl wartość głośności.
}
void wyswietlKreski() {
lcd.setCursor(14, 0); // Ustaw kursor.
lcd.print(F("--")); // Wyświetl "--"
}
void rdsOdbierz(unsigned int rds1, unsigned int rds2, unsigned int rds3, unsigned int rds4) { // Procedura pozyskująca dane RDS
rds.processData(rds1, rds2, rds3, rds4);
}
void rdsWyswietl(const char* rdsDane) { // Pozyskaj dane RDS z tunera.
lcd.setCursor(0, 1); // Ustaw kursor na dole wyświetlacza.
lcd.print(rdsDane); // Wyświetl pozyskane dane.
}
Na początek zaimportujemy wszystko to co przed chwilą pokazałem w malutkim programie obsługującym wyświetlacz. A więc biblioteki obsługi I2C i wyświetlacza oraz rozkaz konfiguracji tegoż. Nie zapomnijmy także o zadeklarowaniu rzeczywistych wymiarów wyświetlacza i wyczyszczeniu go na wstępie – choć nie jest to konieczne, ale jest w dobrym tonie.
Teraz możemy już wypisywać teksty. Zatem do dzieła. Zmiana numerów programów zapisanych w pamięci przebiegała podprogramie. Dopiszemy do niego kolejny podprogram.
lcd.setCursor(11, 0); // Ustaw kursor.
lcd.print(F("Pr:")); // Wyświetl "PR:"
Będzie się on zaczynał od ustawienia kursora na pozycji jedenastej. Tam wyświetlimy skrót: „Pr” i dwukropek. Zauważmy, że format tego rozkazu czerpie wzorce z pamięci flash, a nie z ramu – da nam to oszczędność bajta na każdym znaku. To niby niedużo, ale uczy dobrych zwyczajów.
if (digitalRead(programPlus) == HIGH) { // Jeśli wciśnięto przycisk "Zwiększ numer programu"...
++program; // Zwiększ numer programu.
if (program > 29) { // Jeśli numer programu jest większy od 29...
program = 1; // Numer programu będzie równy 1
}
Teraz wyświetlimy numer programu, ale nie tak od razu. Pomyślałem, że mając więcej miejsca na wyświetlaczu, nie muszę się ograniczać do jednego znaku, więc pamięci może być więcej niż dziewięć. W tych miejscach zdjąłem dotąd istniejące ograniczenie na 9 programów, dopisując dwójki. Programów jest teraz 29, ale każdy może sobie wpisać tu dowolną liczbę, nie większą niż 99.
if (program < 10) { // Gdy numer programu jest mniejszy od 10...
lcd.print(F("0")); // Wyświetl zero wiodące.
}
lcd.print(String(program)); // Wyświetl numer programu.
Wróćmy do porzuconej procedury. Przed wyświetleniem numeru musimy go zamienić na reprezentanta znakowego, bowiem inaczej zamiast cyfry ukazywałyby się kody ASCII aktualnej wartości programu. Powyższa funkcja zamienia daną liczbową na string, czyli tekst, który można sobie już wysłać na wyświetlacz bez problemów.
Tylko to będzie wyglądać nieciekawie. Numery mniejsze od dziesięciu będą się przesuwać w lewo, a za nimi będzie dziura. Dlatego powstała pierwsza linia, która identyfikuje takie wartości i najpierw dopisuje zero. Zatem program pierwszy będzie wyświetlany jako „01”, ale tak wygląda to lepiej.

Po skompilowaniu i przełączaniu programów będziemy widzieć aktualny numer na końcu wyświetlacza. Czas zająć się głośnością.
napiecieNowe = analogRead(potencjometr); // Mierzymy napięcie ustawione ślizgaczem potencjometru.
if (abs(napiecieNowe - napiecieStare) > 3) { // Odejmujemy tę wartość od wartości zmierzonej poprzednio i jeśli wartość bezwzględna wyniku jest większa od 3, to...
napiecieStare = napiecieNowe; // Przepisujemy wartość zmierzonego napięcia do zmiennej napiecieStare.
if (napiecieNowe < 4) { // Jeśli napięcie jest mniejsze od 4...
radio.setMute(1); // Wycisz radio.
glosnosc = 0; // Na potrzeby wyświetlacza ustaw wartość głośności równą zero.
} else { // W przeciwnym razie...
radio.setMute(0); // Wyłącz wyciszenie oraz...
glosnosc = map(napiecieNowe, 4, 1020, 0, 15); // Przemapuj pozycję potencjometru do zakresu (0-15)
radio.setVolume(glosnosc); // Wyślij tę wartość do rejestru głośności.
glosnosc++; // Na potrzeby wyświetlacza przesuń wartość głośności o jeden, bo zero to cisza.
}
wyswietlGlosnosc(); // Odśwież wyświetlacz.
}
Zaczniemy od stworzenia zmiennej glosnosc. W procedurze obsługi potencjometru poczynimy pewne zmiany. Tam, gdzie identyfikowało się pełne wyciszenie, zmiennej glosnosc nadamy wartość równą zero. Jeśli potencjometr nie był skręcony w lewo do oporu, głośność będzie przyjmowała mapowane wartości potencjometru, które przedtem wysyłaliśmy bezpośrednio do płytki tunera. Teraz będziemy wysyłać tą zmienną.
Po wysłaniu powiększamy ją o jeden. Dlaczego? Otóż tuner ma pewną wadę: zerowy poziom głośności to nie cisza. Zatem gdybyśmy wyświetlali zero, co intuicyjnie rozumie się jako ciszę, wcale ciszy nie byłoby. Zrobimy tak: zero będzie się pojawiać, gdy naprawdę będzie cisza, a wartości głośności od 0 do 15 przemapujemy na zakres 1 do 16.
void wyswietlGlosnosc() {
lcd.setCursor(10, 1); // Ustaw kursor.
lcd.print(F("Vol:")); // Wyświetl "VOL:"
if (glosnosc < 10) { // Gdy poziom głośności jest mniejszy od 10...
lcd.print(F("0")); // Wyświetl zero wiodące.
}
lcd.print(String(glosnosc)); // Wyświetl wartość głośności.
}
Teraz już możemy wyskoczyć do podprogramu wyświetlającego poziom głośności, który jest bliźniaczy wyświetlającemu numer programu. Jedyne, co się tu zmienia, to miejsce, w którym ma się pojawiać ta wartość no i zamiast „Pr:” będzie wyświetlać się „Vol:”

Czas wyświetlić bieżącą częstotliwość. Będziemy to robić zarówno przy zmianie programów jak i podczas ręcznego strojenia, bowiem w obu tych przypadkach trzeba tę wartość odświeżyć.
void wyswietlCzestotliwosc() {
lcd.setCursor(0, 0); // Ustaw kursor.
delay(100); // Niezbędne opóźnienie celem ustabilizowania się układu tunera po zmianie częstotliwości.
radio.formatFrequency(czestotliwoscWyswietlana, 11); // Pobierz wartość częstotliwości z tunera.
lcd.print(czestotliwoscWyswietlana); // Wyświetl ją.
}
Podprogram ten będzie wyglądał nieco inaczej. Na początku ustawimy kursor, jak zwykle, po czym dodamy opóźnienie – zaraz powiem po co. Wartość częstotliwości już sobie w programie krąży, bo ta zmienna jest używana do zapisywania stacji w pamięci. Ale należałoby ją sformatować, ponieważ składa się z pięciu cyfr bez przecinka. Tymczasem biblioteka związana z obsługą tunera ma instrukcję zwracającą już sformatowany łańcuch znaków. Skorzystajmy z tego, ale trzeba wiedzieć, że ta dana aktualizuje się dopiero kilkadziesiąt milisekund po zmianie częstotliwości. Po to jest funkcja opóźniająca. Na końcu można już śmiało wyświetlić dane, bez przejmowania się przecinkiem i napisem: megaherce.

if (digitalRead(nastepna) == HIGH) { // Jeśli wciśnięto przycisk "Następna stacja"...
radio.seekUp(0); // Przestrój się w dół o 100 kHz (jedynka skacze do najbliższej stacji).
wyswietlCzestotliwosc(); // Wyświetl częstotliwość.
wyswietlKreski(); // Wyświetl kreski zamiast numeru programu.
while (digitalRead(nastepna) == HIGH) {} // Czekaj aż przycisk zostanie puszczony.
}
Wróćmy do obsługi przycisków strojenia. Postanowiłem zmienić działanie tych przycisków: nie będą wyszukiwać najbliższej stacji, tylko przestrajać radio o 100 kHz. Przy 29 programach ma to większy sens. W tym celu zamiast jedynek w funkcję radio.seekUp wpisuję zero. Kto nadal chce mieć wyszukiwarkę, może wpisać jedynki.
void wyswietlKreski() {
lcd.setCursor(14, 0); // Ustaw kursor.
lcd.print(F("--")); // Wyświetl "--"
}
Dopisałem jeszcze podprogram, który zamiast numeru programu wyświetla kreski. Otóż gdy zaczniemy stroić radio, wyświetlanie numeru programu nie ma już sensu. Dlatego po wejściu w tryb strojenia po skrócie „Pr:” pojawią się dwie kreseczki, co odbywa się w tym miejscu.
Pozostała rzecz ostatnia: RDS, czyli wyświetlanie tekstów wplecionych w sygnał radiowy. Ale o tym napiszę w ostatnim na razie artykule radiowym.