Opiszę tutaj proces implementacji wysyłania sygnału IR w oparciu o PWM i timer. Wysłany tak z mikrokontrolera sygnał będzie w stanie kontrolować klimatyzację. Dodatkowo użyty format sygnału będzie zgodny z tym co przechwytuje Flipper Zero, co pozwoli na wykorzystanie przechwyconych w ten sposób sygnałów do wysyłania ich z mojej platformy. Temat zrealizuję w oparciu o MCU na którym robię swój projekt, ale myślę, że pokazane tu kroki będą analogiczne też na innych platformach.
Plan działania
W poprzednim temacie przyjrzeliśmy się sygnałowi IR przechwyconemu przez Flipper Zero:
Budowa sygnału IR, przechwytywanie w Flipper Zero, zapis w formacie raw
Wiemy już zatem, że potrzebne będzie:
- PWM, w tym przypadku 38kHz z możliwością regulacji wypełnienia
- jakaś forma timera aby móc odczekać dany czas by włączyć/wyłączyć PWM
Przypominając, tak wygląda format który chcemy wysłać:
name: Power
type: raw
frequency: 38000
duty_cycle: 0.330000
data: 2204 708 834 1342 882 1344 837 632 836 1392 836 615 723 738 836 633 861 684 835 658 838 710 835 657 838 656 837 641 723 762 838 1397 727 101537 2257 704 761 1417 884 1342 762 737 731 1467 836 612 828 634 760 707 787 760 835 658 761 786 835 658 761 733 836 637 827 660 763 1473 726
Oznacza to, że musimy włączyć PWM 38kHz z wypełnieniem 33%, zostawić je włączone przez 2204 us, potem je wyłączyć i odczekać 708 us, potem je znów włączyć, itd.
A więc musimy mieć jakąś tablicę do której wpiszemy te wartości czasów i będziemy wedle nich operować.
Trzeba jakoś odczekiwać te czasy. Widzę tu dwie opcje:
- z każdym czasem na nowo ustawiać timer na daną wartość, po upływie której nastąpi przerwanie
- na sztywno dobrać sobie jakąś jednostkę precyzji timera
Dla uproszczenie wybrałem drugi sposób. Przy odpowiednio małym okresie przerwania błędy będą pomijalne. Patrząc na te wartości czasów uznałem, że timer będę wywoływać co 50 us.
Uruchomienie timera
A więc uruchamiamy timer. Wygląda to inaczej zależnie od platformy. Implementowałem to na BK7231, więc dokumentacji praktycznie nie było. Musiałem sam wyszukać po chińskim SDK co i jak. Oto uruchomienie timera:
Kod: C / C++
Dodatkowo w celu sprawdzenia czy timer działa postanowiłem "pomachać" jakimś GPIO. Ustawienie pinu w tryb wyjścia:
Kod: C / C++
No i nasze ISR - Interrupt Service Routine - obsługa przerwania od timera:
Kod: C / C++
Wgrywamy, testujemy:
Rzeczywiście, wszystko się zgadza. Całość można by jeszcze znacznie zoptymalizować poprzez zamianę obsługi pinu przez funkcję na bezpośrednie operacje na rejestrach, ale na razie uruchamiamy tylko timer, więc to pominę.
Uruchomienie PWM
Analogicznie, teraz musimy uruchomić PWM. Wszystko na razie jako osobny przykład. U mnie na BK7231 wygląda to tak:
Kod: C / C++
26000000 to częstotliwość zegara MCU, czyli 26MHz. Dzielimy ją na 38kHz, by otrzymać wartość okresu PWM. Potem wypełnienie ustalamy na połowę okresu - 50%. Działa:
Uruchomienie kolejki czasów
Teraz trzeba zastanowić się jak reprezentować będziemy dane raw w pamięci.
Zróbmy to najprościej jak się da - bufor kołowy można wprowadzić później. Utwórzmy tablicę o stałym rozmiarze:
Kod: C / C++
Teraz trzeba pamiętać co jest następne w kolejce do wysłania - powiedzmy, że będzie to wskaźnik na dany element w tablicy. Gdy jest zerem, to oznacza, że wszystko wysłaliśmy.
Kod: C / C++
Trzeba też wiedzieć gdzie się zatrzymać - wprowadźmy po prostu wskaźnik stopu.
Kod: C / C++
Teraz jeszcze ustalmy okres timera oraz zmienną zliczającą czas który upłynął. Przy okazji musimy pamiętać stan (wysyłamy PWM lub nie), by móc go zamieniać na odwrotny po upływie czasu:
Kod: C / C++
A teraz nasz ISR, jednakże na początek bez PWM - tylko machający pinem:
Kod: C / C++
Powyższy kod zlicza ile upłynęło czasu, porównuje czas który upłynął z następnym progiem i jeśli próg został pokonany, to kod go "przeskakuje" i od bieżącego czasu odejmuje jego wartość (by nie akumulować błędu).
Do tego też dodałem parsing komend w formacie:
Send 100,500,300,500,100
Ten temat nie dotyczy parsowania tekstów ASCII do bufora, ale i tak wspomnę, że użyłem strtok - kod do wglądu:
Kod: C / C++
Ważne tutaj tylko by pamiętać, by nie zacząć wysyłania danych przedwcześnie, w trakcie parsowania. Dlatego wskaźnik "cur" ustawiam na pierwszy element tablicy na końcu, już po parsingu.
Testujemy - wysyłamy "dane":
Wszystko się zgadza. Stan wysoki jest około 100us, potem niski 500us i znów wysoki 300us. To jest to, co podałem w pliku.
PWM wkracza do akcji
Skoro machanie pinem wedle czasów już działa, to można podpiąć PWM. Zasadniczo tylko zmieniamy kilka linijek. W zależności od stanu (wysyłanie lub nie) ustawiamy odpowiednie wypełnienie (choć równie dobrze można by wyłączać całe PWM...):
Kod: C / C++
Pora to przetestować. U mnie w środowisku wszystko jest opakowane w komendy tekstowe, które wyglądają mniej więcej tak:
// start the driver
startDriver IR2
// start timer 50us
// arguments: duty_on_fraction, duty_off_fraction
StartTimer 50 0.5 0
// send data
Send 3500, 1500, 500, 1000, 1000, 500,
Przykładowy rezultat (zrzut ekranu z Sigrok):
Po powiększeniu widać PWM (tutaj z oscyloskopu):
Inny przykład:
// start the driver
startDriver IR2
// start timer 50us
// arguments: duty_on_fraction, duty_off_fraction, pin for sending (optional)
StartTimer 50 0.5 0
// send data
Send 3200,1300,950,500,900,1300,900,550,900,650,900
Rezultat - wygląda dobrze.
Można by już testować z klimatyzacją, ale jeszcze moment...
Drobne optymalizacje
Tu można by zakończyć - ale czy na pewno?
Pamiętajmy, że liczy się tu czas i precyzja, a funkcje biblioteczne nie zawsze są optymalne.
Rozważmy jak aktualizujemy PWM:
Kod: C / C++
Wygląda niewinnie? Ale co kryje się pod maską?
Kod: C / C++
To dopiero wywołuje kolejne funkcje... czym jest sddev_control?
Kod: C / C++
No no, nie wygląda to zbyt wydajnie.
Przede wszystkim, tu jest iteracja i porównanie... łańcuchów znaków? Czym jest PWM_DEV_NAME?
Kod: C / C++
Tak, to są napisy.
A jak wygląda ta tablica, którą iterują?
Kod: C / C++
To dość długie. I bez tablicy hashującej? Nie no, tak nie może być, to trzeba zoptymalizować.
Zobaczmy, gdzie trafia ostatecznie wywołanie.
Gdzie jest użyte CMD_PWM_SINGLE_UPDATA_PARAM?
Dopiero w PWM jest wielki switch z wieloma case:
Kod: C / C++
A to ostatecznie wywołuje:
Kod: C / C++
Podsumowując, widzę tu kilka rzeczy do poprawki:
- po pierwsze, można bezpośrednio w sterowniku IR pisać po rejestrach zamiast wołać te wiele funkcji jak w SDK
- po drugie, te zmienne można pobierać raz i trzymać potem w pamięci:
Kod: C / C++
- po trzecie, jak zmieniam tylko pierwszy duty_cycle, to pozostałe REG_WRITE można pominąć:
Kod: C / C++
- po czwarte, jak wiemy że group będzie stałe, to co robi REG_GROUP_PWM0_T1_ADDR? To makro;
Kod: C / C++
Tę finalną wartość adresu też można policzyć raz i potem trzymać w pamięci...
Większość tych optymalizacji wykonałem, resztę będę dodawać. Kto chce, ten znajdzie na moim repozytorium kod: https://github.com/openshwprojects/OpenBK7231T_App
Końcowy test
Do końcowego testu posłużyła nam klimatyzacja Galanz AUS-12HR53FA2/AS-12HR53F2F:
Klimatyzacja ta nie ma WiFi, ale ma posiada pilot obsługujący wystarczającą ilość funkcji do wygodnego użytkowania:
Przechwyciliśmy Flipperem Zero komendę ON:
Filetype: IR signals file
Version: 1
#
name: Galanz_on
type: raw
frequency: 38000
duty_cycle: 0.330000
data: 3665 1547 605 1078 527 1151 507 498 605 415 584 415 584 1098 585 415 507 501 522 1152 507 1175 585 417 528 1154 506 497 502 523 579 1081 524 1160 550 493 584 1080 603 1077 503 502 600 416 583 1105 578 417 582 419 504 1178 582 445 477 503 599 416 583 416 506 500 524 499 579 417 506 492 507 523 579 417 582 415 508 496 553 451 601 416 583 421 501 500 549 468 584 1104 579 418 581 417 506 1176 583 416 507 503 546 1129 528 1156 504 499 550 450 601 417 583 418 505 523 526 469 583 416 507 1176 583 1101 582 417 582 418 505 500 601 417 582 419 504 1176 583 418 506 1179 580 1102 581 416 586 419 502 523 578 418 581 418 505 500 525 494 582 417 506 495 505 502 547 468 582 422 502 499 524 500 526 469 581 420 504 501 549 467 582 418 506 501 575 468 582 418 506 498 502 502 548 468 582 420 504 500 550 467 532 467 507 496 528 494 532 465 534 468 506 500 550 468 531 470 531 1177 531 1155 528 1133 550 1132 524 501 526 469 530 1152 531 467 534
Komendę przekonwertowaliśmy na format dla mojego firmware (czasy oddzielone przecinkami) i... klimatyzacja ruszyła!
[i]Do wysłania użyliśmy 'IR blaster' od Tuya z wgranym OpenBeken.
Podsumowanie
Wygląda na to, że moje wysyłanie IR działa. Jeszcze dodam pewnie wsparcie 1:1 takich samych plików jakie tworzy Flipper Zero, ale to już kwestia parsowania danych, więc nie jest to aż tak ciekawe i ważne jak samo wysyłanie. Dodatkowo myślę, że można by spróbować ustawiać timer na czas impulsu zamiast sprawdzać co 50μs, ale to spróbuję zrealizować później. Na ten moment wszystkie postawione założenia sobie spełniłem, więc jestem zadowolony.
Czy ktoś z czytających implementował sam wysyłanie IR, czy każdy korzysta z gotowych rozwiązań - a jeśli tak, to jakich? Mała sztuka dla sztuki w celach edukacyjno-hobbystycznych raczej nikomu nie zaszkodzi.
Fajne? Ranking DIY Pomogłem? Kup mi kawę.
