sobota, 28 października 2017

Magic Clock

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, &currentSettings, &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, &currentSettings, &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, &currentSettings, &softTimer5 );
  break;
//-----------------------------------------------------------------------------------------------
  case SET_TIME_MODE:
   display_oled_clock( &setDateTime,  blink, currentSettings );
   display_oled_date ( &setDateTime,  blink, &currentSettings, &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ć :)




IX. Teraz jeszcze schemat oraz trochę zdjęć:







2 komentarze:

  1. Привет! Классный проект! Хочу сделать себе такой же, но при компиляции скетча возникают ошибки, что нет библиотек.
    Где можно взять эти библиотеки:
    "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"
    Что нужно еще, чтобы проект полностью скомпилировался без ошибок?

    OdpowiedzUsuń
  2. Udostępniasz gdzieś cały kod?

    OdpowiedzUsuń