[173] Arduino i oscyloskop cz. 3
W pewnym litewskim oscyloskopie z początków lat dziewięćdziesiątych widziałem kiedyś wyświetlane napięcia. Bardzo mi się to spodobało i dziś będę chciał ten pomysł odtworzyć. Zaczniemy więc od wyświetlania cyfr. Na początku – w ramach ćwiczeń – jednej, która będzie się zmieniać co sekundę od zera do dziewięciu. Cały układ połączeniowy będzie identyczny jak w poprzednich artykułach. A więc: wejście Y podłączymy pod pin 9, wejście X – pod pin 10. Oczywiście pozostawimy filtr dolnoprzepustowy, zbudowany poprzednio, by mieć czyste linie bez zakłócających przebiegów PWM.
Skoro da się wyświetlać okręgi i różne figury, dlaczego by nie wyświetlać kształtów cyfr? Zwłaszcza jeśli przyjmiemy koncepcję z wyświetlaczy siedmiosegmentowych, a więc siatkę z sześcioma węzłami i łączącymi je odcinkami w charakterystycznym kształcie ósemki. I tak właśnie zrobimy, ale po kolei.
const byte inputY = 9; // Port wejścia Y oscyloskopu (Kanał 2).
const byte inputX = 10; // Port wejścia X oscyloskopu (Kanał 1).
const byte xL = 80; // Adresy punktów węzłowych segmentów cyfr.
const byte xR = 176;
const byte yD = 32;
const byte yM = 128;
const byte yU = 224;
const byte sBeginX[7] = { xL, xR, xR, xL, xL, xL, xL }; // Początki X segmentów od A do G
const byte sBeginY[7] = { yU, yU, yM, yD, yM, yU, yM }; // Początki Y segmentów od A do G
const byte sEndX[7] = { xR, xR, xR, xR, xL, xL, xR }; // Końce X segmentów od A do G
const byte sEndY[7] = { yU, yM, yD, yD, yD, yM, yM }; // Końce Y segmentów od A do G
const byte fonts[10][7] = {
{ 1, 1, 1, 1, 1, 1, 0 }, // Wzory cyfr według standardu siedmiosegmentowego, 0
{ 0, 1, 1, 0, 0, 0, 0 }, // 1
{ 1, 1, 0, 1, 1, 0, 1 }, // 2
{ 1, 1, 1, 1, 0, 0, 1 }, // 3
{ 0, 1, 1, 0, 0, 1, 1 }, // 4
{ 1, 0, 1, 1, 0, 1, 1 }, // 5
{ 1, 0, 1, 1, 1, 1, 1 }, // 6
{ 1, 1, 1, 0, 0, 0, 0 }, // 7
{ 1, 1, 1, 1, 1, 1, 1 }, // 8
{ 1, 1, 1, 1, 0, 1, 1 } // 9
};
byte digit = 0; // Wyświetlana cyfra.
unsigned long time = 0; // Zmienna do odliczania sekund.
const byte steps = 160; // Liczba kroków pośrednich rysowania jednej linii.
void setup() {
TCCR1A = _BV(COM1A1) | _BV(COM1B1) | _BV(WGM10); // Zmiana częstotliwości pracy PWM na 62,5 kHz
TCCR1B = _BV(WGM12) | _BV(CS10);
pinMode(inputY, OUTPUT); // Inicjuj port wejścia Y oscyloskopu.
pinMode(inputX, OUTPUT); // Inicjuj port wejścia X oscyloskopu.
time = millis(); // Załaduj wartość pokładowego zegara.
}
void loop() {
if (millis() - time >= 1000) { // Jeśli minęła sekunda...
time += 1000; // Przesuń punkt odniesienia o kolejną sekundę.
digit = (digit + 1) % 10; // Zwiększ liczbę do wyświetlenia, zeruj ją jeśli przekroczyła 9
}
for (byte segment = 0; segment < 7; segment++) { // Analizuj po kolei wszystkie segmenty.
if (fonts[digit][segment] == 1) { // Jeśli segment ma się świecić...
byte beginX = sBeginX[segment]; // Pobierz współrzędną X początku segmentu
byte beginY = sBeginY[segment]; // Pobierz współrzędną Y początku segmentu
byte endX = sEndX[segment]; // Pobierz współrzędną X końca segmentu
byte endY = sEndY[segment]; // Pobierz współrzędną Y końca segmentu
for (byte nextStep = 0; nextStep <= steps; nextStep++) { // Rysuj wybrany segment krok po kroku.
byte x = beginX + ((int)(endX - beginX) * nextStep) / steps; // Oblicz napięcie dla kolejnego punktu osi X
byte y = beginY + ((int)(endY - beginY) * nextStep) / steps; // Oblicz napięcie dla kolejnego punktu osi Y
analogWrite(inputX, x); // Wystaw napięcie dla osi X
analogWrite(inputY, y); // Wystaw napięcie dla osi Y
}
}
}
}
Na początek ustalimy adresy owych punktów na hipotetycznej siatce o rozdzielczości 256x256, bo taką określają napięcia generowane przez PWM.
const byte xL = 80; // Adresy punktów węzłowych segmentów cyfr.
const byte xR = 176;
const byte yD = 32;
const byte yM = 128;
const byte yU = 224;
Punkty te wybrałem w ten sposób i tworzą poziome i pionowe linie, pozostawiając niezbędny margines. Następnie definiujemy cztery tablice adresów początków odcinków, parami X i Y oraz ich końców.
const byte sBeginX[7] = { xL, xR, xR, xL, xL, xL, xL }; // Początki X segmentów od A do G
const byte sBeginY[7] = { yU, yU, yM, yD, yM, yU, yM }; // Początki Y segmentów od A do G
const byte sEndX[7] = { xR, xR, xR, xR, xL, xL, xR }; // Końce X segmentów od A do G
const byte sEndY[7] = { yU, yM, yD, yD, yD, yM, yM }; // Końce Y segmentów od A do G
Te wszystkie symbole określają liczby zdefiniowane wyżej. Zauważmy, że możemy tutaj zamiast zebranych współrzędnych węzłów wpisać dowolne liczby, otrzymując na przykład cyfry pochylone albo przedzielone odstępami.
const byte fonts[10][7] = {
{ 1, 1, 1, 1, 1, 1, 0 }, // Wzory cyfr według standardu siedmiosegmentowego, 0
{ 0, 1, 1, 0, 0, 0, 0 }, // 1
{ 1, 1, 0, 1, 1, 0, 1 }, // 2
{ 1, 1, 1, 1, 0, 0, 1 }, // 3
{ 0, 1, 1, 0, 0, 1, 1 }, // 4
{ 1, 0, 1, 1, 0, 1, 1 }, // 5
{ 1, 0, 1, 1, 1, 1, 1 }, // 6
{ 1, 1, 1, 0, 0, 0, 0 }, // 7
{ 1, 1, 1, 1, 1, 1, 1 }, // 8
{ 1, 1, 1, 1, 0, 1, 1 } // 9
};
W końcu mamy dużą tablicę fonts zawierającą typowe wzory cyfr. Jedynka – segment się świeci, zero – nie świeci. Ściślej: segment będzie rysowany albo ignorowany. Potem mamy jeszcze zmienną rysowanego stopera digit, sekundowego interwału time i liczbę kroków pośrednich rysowania każdego segmentu steps. Im mniej, tym szybciej będą rysowane cyfry, ale tym gorzej będzie sobie z tym radził taki stary oscyloskop. Dobrałem wartość 160, przy której ósemka już troszkę mruga, ale nie aż tak bardzo, za to całość jest w miarę ostra.
W części wstępnej przyspieszamy PWM do 62 i pół kiloherca i inicjujemy porty sterujące oscyloskopem. A także zaczytujemy wartość pokładowego zegara. Główną pętlę zaczynamy od stwierdzenia faktu przyrostu sekundy. Digit to zmienna, która jest zwiększana co sekundę właśnie i reprezentuje cyfrę do wyświetlenia. Dzięki temu będziemy mogli widzieć zwiększające się cyfry. Następnie analizujemy każdy z segmentów cyfry, którą mamy wyświetlić. Jeśli w tablicy kształtu cyfry znajdziemy jedynkę – należy pobrać adresy początków i końców tego segmentu na ekranie. Następnie będziemy go rysować w owych krokach, o których mówiłem, dzieląc cały odcinek na fragmenty. Wyliczone w ten sposób napięcia, odzwierciedlające położenie plamki światła będziemy wystawiać na wejścia oscyloskopu. Jak widać, nie ma tu żadnych pętli opóźniających, bo liczenia jest tyle, że ono samo nieźle opóźnia działanie algorytmu.
Cyfry pojawiają się, nieco kosmate i z zakłóceniami, ale – jak mówiłem – to już urok tego grata, do którego mam sentyment. Prawdziwie użyteczne będą dopiero co najmniej cztery cyfry, które mogą wyświetlać jakąś wartość mierzoną albo zegar – w wypadku efekciarskiego wykorzystania oscyloskopu. Taki zegar będzie się jednak nadawał najwyżej na jakieś wystawy czy zloty, do codziennego używania będzie zupełnie niepraktyczny.
const byte inputY = 9; // Port wejścia Y oscyloskopu (Kanał 2).
const byte inputX = 10; // Port wejścia X oscyloskopu (Kanał 1).
const byte xL = 32; // Lewy punkt pierwszej cyfry.
const byte xR = 64; // Prawy punkt pierwszej cyfry.
const byte yD = 96; // Dolny punkt siatki segmentów.
const byte yM = 128; // Środkowy punkt siatki segmentów.
const byte yU = 160; // Górny punkt siatki segmentów.
const byte offset = 48; // Przesunięcie cyfr.
const byte steps = 65; // Liczba kroków pośrednich rysowania jednej linii.
const byte sBeginX[7] = { xL, xR, xR, xL, xL, xL, xL }; // Początki X segmentów od A do G
const byte sBeginY[7] = { yU, yU, yM, yD, yM, yU, yM }; // Początki Y segmentów od A do G
const byte sEndX[7] = { xR, xR, xR, xR, xL, xL, xR }; // Końce X segmentów od A do G
const byte sEndY[7] = { yU, yM, yD, yD, yD, yM, yM }; // Końce Y segmentów od A do G
const byte fonts[10][7] = {
{ 1, 1, 1, 1, 1, 1, 0 }, // Wzory cyfr według standardu siedmiosegmentowego, 0
{ 0, 1, 1, 0, 0, 0, 0 }, // 1
{ 1, 1, 0, 1, 1, 0, 1 }, // 2
{ 1, 1, 1, 1, 0, 0, 1 }, // 3
{ 0, 1, 1, 0, 0, 1, 1 }, // 4
{ 1, 0, 1, 1, 0, 1, 1 }, // 5
{ 1, 0, 1, 1, 1, 1, 1 }, // 6
{ 1, 1, 1, 0, 0, 0, 0 }, // 7
{ 1, 1, 1, 1, 1, 1, 1 }, // 8
{ 1, 1, 1, 1, 0, 1, 1 } // 9
};
int value = 0; // Wyświetlana liczba 0...9999
void setup() {
TCCR1A = _BV(COM1A1) | _BV(COM1B1) | _BV(WGM10); // Zmiana częstotliwości pracy PWM na 62,5 kHz
TCCR1B = _BV(WGM12) | _BV(CS10);
pinMode(inputY, OUTPUT); // Inicjuj port wejścia Y oscyloskopu.
pinMode(inputX, OUTPUT); // Inicjuj port wejścia X oscyloskopu.
}
void loop() {
value = (value + 1) % 10000; // Zwiększ liczbę do wyświetlenia, zeruj ją jeśli przekroczyła 9999
drawDigit(value / 1000, 0 * offset); // Wyciągnij z liczby poszczególne cyfry, przekaż wraz z ich przesunięciem.
drawDigit((value / 100) % 10, 1 * offset);
drawDigit((value / 10) % 10, 2 * offset);
drawDigit(value % 10, 3 * offset);
}
void drawDigit(byte digit, byte xOffset) { // Odzyskaj cyfrę oraz pozycję rysowania.
for (byte segment = 0; segment < 7; segment++) { // Analizuj po kolei wszystkie segmenty.
if (fonts[digit][segment] == 1) { // Jeśli segment ma się świecić...
byte beginX = xOffset + sBeginX[segment]; // Początek odcinka w osi X.
byte beginY = sBeginY[segment]; // Początek odcinka w osi Y.
byte endX = xOffset + sEndX[segment]; // Koniec odcinka w osi X.
byte endY = sEndY[segment]; // Koniec odcinka w osi Y.
for (byte nextStep = 0; nextStep <= steps; nextStep++) { // Rysuj wybrany segment krok po kroku.
byte x = beginX + ((int)(endX - beginX) * nextStep) / steps; // Oblicz napięcie dla kolejnego punktu osi X
byte y = beginY + ((int)(endY - beginY) * nextStep) / steps; // Oblicz napięcie dla kolejnego punktu osi Y
analogWrite(inputX, x); // Wystaw napięcie dla osi X
analogWrite(inputY, y); // Wystaw napięcie dla osi Y
}
}
}
}
Jak zrealizować wyświetlacz czterocyfowy? Bierzmy się do pracy. Definicje zmienią się: musimy określić lewy xL i prawy xR punkt pierwszej cyfry oraz przesunięcie między cyframi offset, czyli sumę szerokości i odstępu. Kolejne będą rysowane na podstawie wartości dla pierwszej, z uwzględnieniem przesunięć. Trzeba też zdefiniować poziomy dla segmentów dolnych, środkowych i górnych. Tutaj także mamy liczbę kroków rysowania steps, im wyższa, tym wolniejsze będzie rysowanie, ale czytelniejsze na kiepskich oscyloskopach. Kolejne tabele, podobnie jak w poprzednim projekcie, zawierają mapę segmentów na wzór wyświetlaczy siedmiosegmentowych oraz mapę segmentów aktywnych i wygaszonych. Przyda nam się jeszcze pomocniczy licznik demonstracyjny value.
W głównej pętli zabawę zaczniemy od owego licznika. Będzie on w każdym przebiegu zwiększał wartość do dziesięciu tysięcy bez jednego, po czym wyzeruje się i tak bez końca. Liczba ta oczywiście będzie się nam wyświetlać. Cztery kolejne linie będą wyciągać cztery kolejne cyfry z tej liczby i wysyłać je do podprogramu wyświetlającego je na ekranie.
Będą tam przekazywane dwa parametry: cyfra do wydrukowania i jej pozycja, już przeliczona jako przesunięcie względem pierwszej cyfry. Teraz nastąpi analiza wszystkich siedmiu segmentów po kolei. Jeśli w tablicy uwzględniono świecenie konkretnego segmentu, obliczymy współrzędne jego początku i końca w formacie siatki 255x255. W pętli określonej liczbą zdefiniowanych na początku kroków będziemy wyświetlać segment, czyli odcinek łączący wyliczone punkty. Te dwie linie wystawiają obliczone współrzędne na wyjścia PWM, które sterują już bezpośrednio oscyloskopem.
Po kompilacji możemy cieszyć się takim efekciarskim licznikiem. Mój stary oscyloskop ledwie tu zipie, wszak jego pasmo przekraczało w praktyce niewiele ponad 10 kHz. Ale coś tam widać, a te wszystkie niedokładności dodają tylko uroku. Wyświetlacz taki możemy zatrudnić do czegoś użytecznego i tak właśnie teraz zrobię. W programie pojawi się niewielka zmiana: będziemy wyświetlać napięcie mierzone na wejściu A1. Na płytce edukacyjnej TME podłączono tu potencjometr, więc podepnę go do programu, zastępując mierzonymi wartościami wcześniejszy licznik. Cała reszta wygląda dokładnie tak samo. Możemy sobie teraz przeglądać dowolne liczby, kręcąc gałką. Jak widać, napięcie podawane jest w umownych jednostkach, będących podziałem napięcia odniesienia przez dwa do potęgi dziesiątej. Jeśli chcielibyśmy prawdziwych woltów, należy przekonwertować tę wartość.
const byte inputY = 9; // Port wejścia Y oscyloskopu (Kanał 2).
const byte inputX = 10; // Port wejścia X oscyloskopu (Kanał 1).
const byte pot = A1; // Port wejścia potencjometru.
const byte xL = 32; // Lewy punkt pierwszej cyfry.
const byte xR = 64; // Prawy punkt pierwszej cyfry.
const byte yD = 96; // Dolny punkt siatki segmentów.
const byte yM = 128; // Środkowy punkt siatki segmentów.
const byte yU = 160; // Górny punkt siatki segmentów.
const byte offset = 48; // Przesunięcie cyfr.
const byte steps = 65; // Liczba kroków pośrednich rysowania jednej linii.
const byte sBeginX[7] = { xL, xR, xR, xL, xL, xL, xL }; // Początki X segmentów od A do G
const byte sBeginY[7] = { yU, yU, yM, yD, yM, yU, yM }; // Początki Y segmentów od A do G
const byte sEndX[7] = { xR, xR, xR, xR, xL, xL, xR }; // Końce X segmentów od A do G
const byte sEndY[7] = { yU, yM, yD, yD, yD, yM, yM }; // Końce Y segmentów od A do G
const byte fonts[10][7] = {
{ 1, 1, 1, 1, 1, 1, 0 }, // Wzory cyfr według standardu siedmiosegmentowego, 0
{ 0, 1, 1, 0, 0, 0, 0 }, // 1
{ 1, 1, 0, 1, 1, 0, 1 }, // 2
{ 1, 1, 1, 1, 0, 0, 1 }, // 3
{ 0, 1, 1, 0, 0, 1, 1 }, // 4
{ 1, 0, 1, 1, 0, 1, 1 }, // 5
{ 1, 0, 1, 1, 1, 1, 1 }, // 6
{ 1, 1, 1, 0, 0, 0, 0 }, // 7
{ 1, 1, 1, 1, 1, 1, 1 }, // 8
{ 1, 1, 1, 1, 0, 1, 1 } // 9
};
void setup() {
TCCR1A = _BV(COM1A1) | _BV(COM1B1) | _BV(WGM10); // Zmiana częstotliwości pracy PWM na 62,5 kHz
TCCR1B = _BV(WGM12) | _BV(CS10);
pinMode(inputY, OUTPUT); // Inicjuj port wejścia Y oscyloskopu.
pinMode(inputX, OUTPUT); // Inicjuj port wejścia X oscyloskopu.
}
void loop() {
int value = analogRead(pot); // Zmierz napięcie wyznaczone przez ślizgacz potencjometru.
drawDigit(value / 1000, 0 * offset); // Wyciągnij z liczby poszczególne cyfry, przekaż wraz z ich przesunięciem.
drawDigit((value / 100) % 10, 1 * offset);
drawDigit((value / 10) % 10, 2 * offset);
drawDigit(value % 10, 3 * offset);
}
void drawDigit(byte digit, byte xOffset) { // Odzyskaj cyfrę oraz pozycję rysowania.
for (byte segment = 0; segment < 7; segment++) { // Analizuj po kolei wszystkie segmenty.
if (fonts[digit][segment] == 1) { // Jeśli segment ma się świecić...
byte beginX = xOffset + sBeginX[segment]; // Początek odcinka w osi X.
byte beginY = sBeginY[segment]; // Początek odcinka w osi Y.
byte endX = xOffset + sEndX[segment]; // Koniec odcinka w osi X.
byte endY = sEndY[segment]; // Koniec odcinka w osi Y.
for (byte nextStep = 0; nextStep <= steps; nextStep++) { // Rysuj wybrany segment krok po kroku.
byte x = beginX + ((int)(endX - beginX) * nextStep) / steps; // Oblicz napięcie dla kolejnego punktu osi X
byte y = beginY + ((int)(endY - beginY) * nextStep) / steps; // Oblicz napięcie dla kolejnego punktu osi Y
analogWrite(inputX, x); // Wystaw napięcie dla osi X
analogWrite(inputY, y); // Wystaw napięcie dla osi Y
}
}
}
}

















