I. Wstęp.
Jak to zwykle bywa z takimi projektami, często powstają z potrzeby chwili.Również i u mnie pojawiła się potrzeba posiadania zegarka. Po wymianie telewizora na taki, który nie wyświetla czasu na bieżąco, stanąłem przed potrzebą zakupu zegara ściennego.
Ponieważ poradniki na temat "magic ledów" miałem już za sobą, a "ring ledy" były w zakresie moich możliwości i pragnień, postanowiłem zbudować od podstaw taki oto właśnie zegar :)
II. Trochę o konstrukcji.
Na początku w planach był tylko sam ring z 60 diodami, ale pustka w środku zegarka nie dawała mi spać po nocach.W pierwszej fazie zapełniania pustki postawiłem na wyświetlacz ledowy z matryc 8x8, ale trudności z umieszczeniem takich matryc w środku obudowy skłoniły mnie do innego rozwiązania.
Wygrzebałem z czeluści elektronicznego dobytku wyświetlacz OLED, przypasowałem i okazało się, że pasuje jak ulał :)
Planowałem wykonać PCB do zegarka, ale zastosowana płytka uniwersalna pozwoliła na zgrabne złożenie całości. Przy okazji powstał schemat w Eagle, więc tę niedogodność zawsze można poprawić.
III. Nie zapominajmy o obudowie.
Już we wczesnej fazie projektowania sprzętu zacząłem zastanawiać się nad obudową.Na początku oczywistym stało się, że to musi być obudowa po rozwiązaniu fabrycznym, bo rzeźbienie w drewnie czy w glinie zajmuje za dużo czasu.
Zegar w końcu ma to być ozdoba pokoju, a nie jakaś szkarada na ścianie :).
Ostatecznie padło na zegarek wskazówkowy, kwarcowy zakupiony na portalu aukcyjny, w cenie ok 20 zł.
Po wybebeszeniu niepotrzebnych gratów ze środka, podpiłowaniu tu i owdzie plastików, okazało się zakupiony zegar idealnie nadaje się do tego projektu.
W dość estetyczny sposób udało się umieścić płytkę uniwersalną z całą potrzebną elektroniką (zastosowałem gotowy klon Arduino Pro Mini oraz wyświetlacz OLED 2.42 cala ).
Fabrycznie dostarczone szkiełko zegarka przyozdobiłem folią do przyciemniania szyb, żeby nadać głębi kontrastu.
Zastosowane diody RGB cechują się sporą jasnością, dlatego też w normalnym użytkowaniu jasność musi być ustawiona na wartość minimalną.
Fabryczna obudowa pozwala nam na bezpośrednie powieszenie zegarka na ścianie dzięki dedykowanej w obudowie zawieszce.
IV. Teraz skromna prezentacja:
V. Jak obsługiwać to cudo?
Całość zegarka, w obecnej wersji, należy obsługiwać za pomocą, zabudowanego od tyłu, enkodera, choć jak będzie można zobaczyć na zdjęciach, zegarek jest również sprzętowo przygotowany na obsługę pilota. To na razie przyszłość, ale planuję zaimplementować tryby uczenia z dowolnego pilota.Lewo, prawo oraz magic button enkodera (short, medium, large time) pozwala na przełączanie między trybami wyświetlacza, sterowanie jasnością oraz wejściem i wyjściem z trybu ustawiania czasu.
Taki sposób sterowania to celowe założenie, zegar bowiem miał mieć cechy zegara analogowego, ustawianego za pomocą pokrętełka :)
Planowana obsługo pilota niestety będzie wymagała dodatkowej pracy, pewnie niezbyt szybko się to uda, bo w zanadrzu mam już inne, nie mniej ciekawe (tak myślę :) ) projekty.
Dorzucam również szczegółowe zdjęcia dotyczące konstrukcji zegara oraz mam nadzieję, że w przyszłości cały kod, bowiem w założeniu jestem orędownikiem open hardware i open source :).
Z kodem może być tylko taki problem, że ciężko będzie mi chyba wydzielić co moje, a co nie moje, ale na pewno podejmę temat.
VI. Jak pracowałem nad softem?
Podstawą były książki oraz poradniki Mirka Kardasia:1. Wyświetlacze Oled na SSD1306:
https://www.youtube.com/watch?v=IDhnhCp61Ao
2. Seria poradników na temat enkoderów:
https://www.youtube.com/watch?v=IP5t_XzfRRM
3. Poradniki dotyczące magick ledów:
https://www.youtube.com/watch?v=nj_vZTQAO7k
4. Poradnik na temat DS3231:
https://www.youtube.com/watch?v=rPxRkYTtvYg
5. Wpisy na temat obsługi klawiszy:
http://mirekk36.blogspot.com/2012/10/drgania-stykow-to-bajki-wiec-jak-to.html
Warto nabyć wiedzę z tych filmów, żeby lepiej rozumieć działanie zegarka.
Tu jeszcze fragment kodu, plik main.c.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | #include <util/delay.h> #include <avr/io.h> #include <avr/interrupt.h> #include "libs/SW_AVR_PERIPHS/timers_v2.h" #include "libs/SW_INPUT/sw_encoder_v2.h" #include "libs/rtc_manage.h" #include "libs/SW_INPUT/sw_keyboard_v3.h" #include "macros_v2.h" uint8_t brightBlinker, visible; uint8_t refresh = 200; uint8_t lastSettings; void rtc_settings( void ); void clock_brightness( void ); void select_display_mod( void ); void update_buffers ( uint8_t condition ); void refresh_displays( uint8_t condition ); int main(void) { sw_led_init(); /********************** Inicjalizacja podsystemów **************/ clock_init(); timer2_init( t2_Prsclr_256 ); sw_ssd1306_init( SSD1306_EXTERNALVCC, REFRESH_MID ); // Inicjujemy OLEDa sw_keyboard_init(); // Inicjujemy klawisze sw_encoder_init(); // Inicjujemy enkoder /***************************************************************/ register_enc_event_callback( clock_brightness ); register_keyboard_event_callback( select_display_mod ); register_datetime_event_callback( sw_update_datetime ); display_oled_clock( &showDateTime, showDateTime.ss%2, currentSettings); display_oled_date( &showDateTime, showDateTime.ss%2, ¤tSettings, &softTimer5 ); sw_update_datetime( &showDateTime ); // Co 1 s pobieramy czas z RTC sw_ssd1306_ram_to_display (0); // Ładujemy do sterownika sei(); while(1) { currentSettings.visible = HIDDEN; SW_ENCODER_EVENT(); // Sprawdzamy zdarzenie od enkodera SW_KEYBOARD_EVENT(); // Sprawdzamy zdarzenie od klawisza SW_DATETIME_EVENT( &showDateTime ); // Sprawdzamy zdarzenie od układu RTC /*************************** Wyświetlamy aktualnie wybrany tryb pracy ***************************/ if (!softTimer7) { update_buffers ( currentSettings.displayMode ); refresh_displays( currentSettings.displayMode ); softTimer7 = time_base_ms(100); // Odświeżamy co 100 ms } /*************************************************************************************************/ } } //===================================== CIAŁA FUNKCJI ===========================================// /****************************** Aktualizujemy bufory wyświetlaczy ********************************/ void update_buffers( uint8_t condition ) { static uint8_t lastState; uint8_t blink; // Wprowadzamy miganie co 0.5 s, miganie co 1s to trochę za mało ---------- if ( showDateTime.ss%2 != lastState ) { // Nastąpiła zmiana stanu softTimer6 = time_base_ms(500); // Startujemy odliczanie 0.5s } if ( softTimer6 ) blink = 1; if ( !softTimer6 ) blink = 0; lastState = showDateTime.ss%2; switch ( condition ) { //----------------------------------------------------------------------------------------------- case SHOW_TIME_MODE0: if ( ( showDateTime.ss%30 > 0 )&&( showDateTime.ss%30 < 5 ) ) // Przez 5s wyświetla temperaturę. display_oled_temp( currentSettings ); else { display_oled_clock( &showDateTime, blink, currentSettings ); //16Mhz: 79 ms display_oled_date ( &showDateTime, blink, ¤tSettings, &softTimer5 ); //16Mhz: 35 ms } leds_set_colors_RAM( LedsRAM, &showDateTime, currentSettings.bright, brightBlinker, 0 ); blink_diode( &brightBlinker, currentSettings.bright ); break; //----------------------------------------------------------------------------------------------- case SHOW_TIME_MODE1: leds_set_colors_RAM( LedsRAM, &showDateTime, currentSettings.bright, brightBlinker, 0 ); blink_diode( &brightBlinker, currentSettings.bright ); break; //----------------------------------------------------------------------------------------------- case SHOW_TIME_MODE2: display_oled_clock( &showDateTime, blink, currentSettings ); display_oled_date ( &showDateTime, blink, ¤tSettings, &softTimer5 ); break; //----------------------------------------------------------------------------------------------- case SET_TIME_MODE: display_oled_clock( &setDateTime, blink, currentSettings ); display_oled_date ( &setDateTime, blink, ¤tSettings, &softTimer5 ); leds_set_colors_RAM( LedsRAM, &setDateTime, currentSettings.bright, currentSettings.bright, 0 ); break; //----------------------------------------------------------------------------------------------- default: break; } } /***********************************************************************************************/ /************************ Odświeżamy wyświetlacze w zależności od kontekstu **********************/ void refresh_displays( uint8_t condition ) { switch ( condition ) { //----------------------------------------------------------------------------------------------- case SHOW_TIME_MODE0: display_rgb_clock( LedsRAM ); sw_ssd1306_ram_to_display(0); // 16MHz, hard I2C - 600kHz: 13.5 ms break; //----------------------------------------------------------------------------------------------- case SHOW_TIME_MODE1: display_rgb_clock( LedsRAM ); break; //----------------------------------------------------------------------------------------------- case SHOW_TIME_MODE2: sw_ssd1306_ram_to_display(0); break; //----------------------------------------------------------------------------------------------- case SET_TIME_MODE: sw_ssd1306_ram_to_display(0); display_rgb_clock( LedsRAM ); break; //----------------------------------------------------------------------------------------------- default: break; } } /**************************************************************************************************/ /**************************** Funkcja do wybierania trybów pracy *******************************/ void select_display_mod (void ) { switch ( keyboard.switchPressType ) { //----------------------------------------------------------------------------------------------- case SHORT_KEY_PRESS: if ( currentSettings.displayMode != SET_TIME_MODE ) { currentSettings.clockFace++; if (currentSettings.clockFace > CLOCK_FONT6) { currentSettings.clockFace = CLOCK_FONT0; } } else { //displayMode == SET_TIME_MODE currentSettings.settingTimeUnit--; if ( currentSettings.settingTimeUnit == SETTING_SEC - 1) currentSettings.settingTimeUnit = SETTING_YEAR; set_encoder( setDateTime.bytes[ currentSettings.settingTimeUnit ] ); // Ustawiamy enkoder na aktualną jednostkę czasu } break; //----------------------------------------------------------------------------------------------- case MEDIUM_KEY_PRESS: if (currentSettings.displayMode == SET_TIME_MODE) // Wychodzimy z funkcji gdy ustawiamy czas i datę return; currentSettings.displayMode++; if (currentSettings.displayMode == SHOW_TIME_MODE3) { currentSettings.displayMode = SHOW_TIME_MODE0; } if (currentSettings.displayMode == SHOW_TIME_MODE2) { sw_ssd1306_display_OnOff( SSD1306_DISPLAYON ); // W trybie MODE2 włączamy wyświetlacz clear_all_diodes_RAM( 60, LedsRAM ); // Wyłączamy tarczę LED zegara display_rgb_clock( LedsRAM ); } if (currentSettings.displayMode == SHOW_TIME_MODE1) { sw_ssd1306_display_OnOff( SSD1306_DISPLAYOFF ); // W trybie MODE1 wyłączamy wyświetlacz } break; //----------------------------------------------------------------------------------------------- case LONG_KEY_PRESS: if( currentSettings.displayMode != SET_TIME_MODE ) { register_enc_event_callback( rtc_settings ); // rejestrujemy callbacka, enkoder w trybie ustawiania czasu ds3231_get_datetime( &setDateTime ); // Kopiujemy aktualny czas z RTC do bufora ustawień czasu i daty set_encoder( setDateTime.bytes[ currentSettings.settingTimeUnit ] ); // Ustawiamy enkoder na aktualną jednostkę czasu currentSettings.displayMode = SET_TIME_MODE; // Wchodzimy w tryb ustawień czasu i daty }else{ //currentSettings.displayMode == SET_TIME_MODE ds3231_set_time( setDateTime.hh, setDateTime.mm, setDateTime.ss ); ds3231_set_date( setDateTime.year, setDateTime.month, setDateTime.day, setDateTime.dayofweek ); set_encoder( currentSettings.bright ); // Ustawiamy postatnio wybraną jasnośc register_enc_event_callback( clock_brightness ); // rejestrujemy callbacka, enkoder w trybie regulacji jasności currentSettings.displayMode = SHOW_TIME_MODE0; // Przełączmy na tryb wyświetlania currentSettings.settingTimeUnit = SETTING_HOUR; } break; default: break; } } /*************************************************************************************************/ int8_t ranges_ring( int8_t min, int8_t max, int8_t value ) { if ( value > max ) value = min; else if ( value < min ) value = max; return value; } /***************************************************************/ /**** Funkcja zwiększa lub zmniejsza zależną wartośc po przekroczeniu odp. progów ****************/ uint8_t update_dependant( uint8_t min, uint8_t max, uint8_t value, uint8_t *dependant ) { static uint8_t tmp; if ( (tmp == max)&&(value == min) ) { *dependant = *dependant + 1; // Inkrementujemy } else if ( (tmp == min)&&(value == max) ) { *dependant = *dependant - 1; // Dekrementujemy } return tmp = value; } /*************************************************************************************************/ /***************************** Funkcja ustawiająca czas i datę zegarka ***************************/ void rtc_settings( void ) { int8_t liczba = get_encoder(); switch ( currentSettings.settingTimeUnit ) { case SETTING_YEAR: liczba = ranges_ring( 0, 99, liczba ); break; case SETTING_MONTH: liczba = ranges_ring( 1, 12, liczba ); update_dependant( 1, 12, liczba, &setDateTime.year); break; case SETTING_DAY: liczba = ranges_ring( 1, 31, liczba ); break; case SETTING_DAYOW: liczba = ranges_ring( 1, 7, liczba ); break; case SETTING_HOUR: liczba = ranges_ring( 0, 23, liczba ); update_dependant( 0, 23, liczba, &setDateTime.day); break; default: // SETTING_SEC or SETTING_MIN liczba = ranges_ring( 0, 59, liczba ); if ( currentSettings.settingTimeUnit == SETTING_SEC ) update_dependant( 0, 59, liczba, &setDateTime.mm); else update_dependant( 0, 59, liczba, &setDateTime.hh); break; } set_encoder( liczba ); setDateTime.bytes[ currentSettings.settingTimeUnit ] = (uint8_t)liczba; } /*************************************************************************************************/ /******************************* Funkcja ustawiająca jasność zegarka *****************************/ void clock_brightness( void ) { int8_t liczba = get_encoder(); if (liczba > 100) liczba = 100; if (liczba < 0) liczba = 0; set_encoder( liczba ); currentSettings.bright = liczba; graphic_set_current_font( (FONT_INFO *)&MicrosoftSansSerif8ptFontInfo_var, &CurrentFont ); graphic_fill_rect_RAM( 0, 46, 128, 28, BLACK); // Czyścimy oleda o zadanym prostokącie graphic_puts_RAM ( 0, 46, L"Jasność: ", 1, WHITE, BLACK, CurrentFont); graphic_puts_int_RAM( 52, 46, liczba, 1, WHITE, BLACK, CurrentFont); // sw_ssd1306_set_brightness( liczba*2 ); currentSettings.visible = VISIBLE; softTimer5 = time_base_s(2); // Ustawiamy czas na 2s } /*************************************************************************************************/ |
VII. Z czego jestem zadowolony szczególnie:
- własna biblioteka do obsługi klawiatury. Wpleciona obsługa zdarzeń i callbacków, rozwinięcie obsługi przycisków poprzez analizę czasu przyciśnięcia.- pisana od podstaw obsługa polskich znaków, skoncentrowana na kodowaniu znaków UTF-8. Dzięki temu podejściu, liczę na łatwiejszą przenośność w przyszłości kodów z Windows na inne platformy.
- mocno modyfikowana obsługa WS2812, z poradników właściwie nie ruszałem tylko kodów asemblerowych, reszta to już pełna próba pisania kodu od A do Z. Od początku starałem wpleść kod oparty na strukturach.
- właściwe praktycznie nie stosuję delayów, a moimi braćmi od dawna są soft timery :)
- dzięki pracy nad takim, jakby nie było sporym, 22kB kodem, dość dogłębne zrozumienie przyswajanych zagadnień, co oczywiście nie oznacza, że już wszystko umiem, wręcz przeciwnie :) Większość kodu, który napisałem na pewno wymaga sporo pracy, żeby był bardziej przejrzysty i optymalny.
Ważne jest to, że po kilku miesięcznej przerwie w pracy na zegarem, nie miałem większego problemu ze zrozumieniem tego co nabazgrałem :)
PS. Z tym sporym kodem to trochę przesadziłem, większą część zajmują po prostu czcionki do wyświetlacza :)
VIII. Co należy jeszcze poprawić?
- mało optymalna obsługa buforowania OLED. Brak optymalizacji rysowania pojedynczych znaków (pikseli) skutkuje dużym czasem rysowania: 35-70 ms.- dorobić trochę bajeranckich efektów wizualnych, ale to wymaga optymalizacji kodu i przemyślenia struktury projektu
- dołożyć czujnik światła i odległości, żeby można było sterować "ręcznie" zegarem bez użycia pilota oraz zdejmowania zegarka ze ściany.
Mam na podorędziu prawie gotową bibliotekę do obsługi takiego czujnika VCNL4010, może kiedyś go wykorzystam.
- lepiej rozwiązać zasilanie zegarka - dedykowane gniazdo zasilające, najlepiej mikro usb.
- dalej pracować nad kodem, zawsze jest coś co można poprawić :)
Привет! Классный проект! Хочу сделать себе такой же, но при компиляции скетча возникают ошибки, что нет библиотек.
OdpowiedzUsuńГде можно взять эти библиотеки:
"libs/SW_AVR_PERIPHS/timers_v2.h"
"libs/SW_INPUT/sw_encoder_v2.h"
"libs/rtc_manage.h"
"libs/SW_INPUT/sw_keyboard_v3.h"
"macros_v2.h"
Что нужно еще, чтобы проект полностью скомпилировался без ошибок?
Udostępniasz gdzieś cały kod?
OdpowiedzUsuń