ESP32-C3 Digital Watch using LVGL, Squareline Studio, and ESP32-C3 CrowPanel
Table of Contents
ESP32-C3 Digital Watch:
ESP32-C3 Digital Watch using LVGL, Squareline Studio, and ESP32-C3 CrowPanel- This is my 3rd article in the smartwatch programming series using the CrowPanel ESP32-C3 1.28-inch IPS Capacitive Touch Display.
In Part 2, we designed a simple counter using buttons and labels. We will continue with the same project, add another screen, and make a digital watch. After reading this article, you will learn how to add multiple screens, how to switch between screens, how to use an image as the background, how to create custom fonts, and how to fix errors.
Note: if you want anything confusing; you can watch the video tutorial given at the end of this article.
So, without any further delay, let’s get started!!!
This is the Counter GUI we designed in Part 2. For the digital watch, we are going to add a second screen. Go to the Widgets Tab, scroll down, and click on Screen.
On the Inspect tab, go to STYLE (MAIN) and set the background radius to 240 and set the background color. After this, you have to decide whether you want to make a simple digital watch or use some kind of watch face to make your digital watch look attractive.
While making this digital watch, I only used simple labels.
So, that’s why it’s not very attractive. If you search on Google, you will find thousands of watch faces that you can use as background images to take your digital smartwatch to the next level.
In my case, to avoid any copyright issues, I designed this simple watch face in Figma and saved it as a PNG file.
Now, to use this image as the watch face in SquareLine Studio, go to the Assets Tab, click on ADD FILE INTO ASSETS, and then select the image you want to use as the watch face.
As you can see, the image has been added to the Assets. Now, to use this image as the background, look at the right side where you can see the Background Image section. Expand it by clicking on the arrow, check the box to activate the background image, and then select the image. You can see the image has been set as the watch face.
If you want to change the opacity, simply check the box and try different values to see how it looks.
You can see the watch face border doesn’t look good at all. To hide this, I can make use of the Outline.
Now, it looks good.
To switch between the two screens; let’s say when I am on Screen1 and swipe my finger to the left side, Screen2 should show up, and when I swipe my finger to the right side, Screen1 should show up. Let’s do it.
While Screen1 is selected, click the ADD EVENT button.
Set the Trigger type to GESTURE_LEFT.
Next, set the Action type to CHANGE SCREEN.
Then click on the ADD button.
Select Screen2.
Now, repeat the same steps for Screen2.
Set the Trigger type to GESTURE_RIGHT.
Next, set the Action type to CHANGE SCREEN.
Then click on the ADD button.
Select Screen1.
Let’s play the simulation.
Great! It’s working.
Now, let’s add three labels for displaying Hours, Minutes, and Seconds.
Let’s name these labels so we can easily find them in the variables list on the Arduino side.
lblHour
lblminutes
lblseconds
After naming all the 3 labels, next, we have to select the fonts and set their size.
While designing the Counter GUI, I used the default text, but for the digital watch, I am going to create custom fonts. For this, I have downloaded the Seven Segment font. You can use any font as per your choice, you can search on Google and download any font you like. To import the font, simply click the ADD FILE INTO ASSETS button, then select the font. That’s it.
Now, let’s go to the Font Tab and create a font for the Hours label.
Name the font.
Select the Seven Segment font.
Set the font size.
Finally, Click the CREATE button.
You can see the font has been created.
Now, let’s go back to the Inspect Tab and change the Text Font to the one we just created. For this, simply go back to the Inspector Tab and on the SYLE(MAIN), check the Text Font and then click on the arrow head and select the HourFont we created.
You can see the font has been changed to Seven Segment.
I can assign the same font to Minutes and Seconds as well, but it’s good to have separate fonts for each one so we can individually modify any font. So, using the same method, create two more fonts for the Minutes and Seconds. Adjust their size and color. You can play with all the properties to check their effect on the text.
Our GUI is ready. Now, let’s save the project and generate the UI files.
As I explained in my previous article and video, each time you generate the UI files, you have to copy all the files and paste them into the same Arduino project folder.
After copying and pasting the UI files, then you can go ahead and open the Arduino main file.
This time, you can see some new files have been added. You can see Screen2 and the three fonts we created.
If you open the ui_Screen2, you will find the properties of all the widgets used on Screen2. And if you go to the ui.h file, you will find all the variables.
Last time, we used the ui_lblCounter variable, and this time we will use ui_lblseconds, ui_lblminutes, and ui_lblHours for displaying the hour, minutes, and seconds.
In order to use custom large fonts, you will have to go to this file “lv_conf.h” and change the value of LV_FONT_FMT_TXT_LARGE from 0 to 1.
#define LV_FONT_FMT_TXT_LARGE 1
Then go to the ui.c file and change LV_COLOR_16_SWAP from 0 to 1. You will have to change this every time you generate the UI files. So, that’s all about the necessary changes to avoid any errors. Now, let’s start the programming.
In the previous article, I mentioned that it’s a basic template, and this template already has code for all the onboard components. So, we only need to access those values and display them on the screen. If you want to make it easier for yourself, you can download the template folder from my Patreon page. That template folder consists of all the files.
ESP32-C3 Digital Watch Code:
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 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 |
#define LGFX_USE_V1 #include <WiFi.h> #include "Arduino.h" #include <lvgl.h> #include <LovyanGFX.hpp> #include <Ticker.h> #include "CST816D.h" #include "do_mian.h" #include "ui.h" #include <Preferences.h> #include "I2C_BM8563.h" #define I2C_SDA 4 #define I2C_SCL 5 #define TP_INT 0 #define TP_RST -1 int counter = 0; //encoder #define ENCODER_A_PIN 19 #define ENCODER_B_PIN 18 #define SWITCH_PIN 8 //Custom key pins #define Custom_PIN 1 long position = 0; long position_tmp = 0; bool switchPressed = false; #define PI4IO_I2C_ADDR 0x43 I2C_BM8563 rtc(I2C_BM8563_DEFAULT_ADDRESS, Wire); I2C_BM8563_DateTypeDef dateStruct; I2C_BM8563_TimeTypeDef timeStruct; #define off_pin 35 #define buf_size 120 //Alarm switch sign int fal = 0; //Indicates whether the alarm has gone off int fal1 = 0; uint32_t hourValue = 0; uint32_t minuteValue = 0; class LGFX : public lgfx::LGFX_Device { lgfx::Panel_GC9A01 _panel_instance; lgfx::Bus_SPI _bus_instance; public: LGFX(void) { { auto cfg = _bus_instance.config(); cfg.spi_host = SPI2_HOST; cfg.spi_mode = 0; cfg.freq_write = 80000000; cfg.freq_read = 20000000; cfg.spi_3wire = true; cfg.use_lock = true; cfg.dma_channel = SPI_DMA_CH_AUTO; cfg.pin_sclk = 6; cfg.pin_mosi = 7; cfg.pin_miso = -1; cfg.pin_dc = 2; _bus_instance.config(cfg); _panel_instance.setBus(&_bus_instance); } { auto cfg = _panel_instance.config(); cfg.pin_cs = 10; cfg.pin_rst = -1; cfg.pin_busy = -1; cfg.memory_width = 240; cfg.memory_height = 240; cfg.panel_width = 240; cfg.panel_height = 240; cfg.offset_x = 0; cfg.offset_y = 0; cfg.offset_rotation = 0; cfg.dummy_read_pixel = 8; cfg.dummy_read_bits = 1; cfg.readable = false; cfg.invert = true; cfg.rgb_order = false; cfg.dlen_16bit = false; cfg.bus_shared = false; _panel_instance.config(cfg); } setPanel(&_panel_instance); } }; LGFX tft; CST816D touch(I2C_SDA, I2C_SCL, TP_RST, TP_INT); /*Change to your screen resolution*/ static const uint32_t screenWidth = 240; static const uint32_t screenHeight = 240; static lv_disp_draw_buf_t draw_buf; static lv_color_t buf[2][screenWidth * buf_size]; #if LV_USE_LOG != 0 /* Serial debugging */ void my_print(lv_log_level_t level, const char *file, uint32_t line, const char *fn_name, const char *dsc) { Serial.printf("%s(%s)@%d->%s\r\n", file, fn_name, line, dsc); Serial.flush(); } #endif /* Display flushing */ void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) { if (tft.getStartCount() == 0) { tft.endWrite(); } tft.pushImageDMA(area->x1, area->y1, area->x2 - area->x1 + 1, area->y2 - area->y1 + 1, (lgfx::swap565_t *)&color_p->full); lv_disp_flush_ready(disp); /* tell lvgl that flushing is done */ } /*Read the touchpad*/ void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data) { bool touched; uint8_t gesture; uint16_t touchX, touchY; touched = touch.getTouch(&touchX, &touchY, &gesture); if (!touched) { data->state = LV_INDEV_STATE_REL; } else { data->state = LV_INDEV_STATE_PR; /*Set the coordinates*/ data->point.x = touchX; data->point.y = touchY; } } Ticker ticker; //Extended IO function void init_IO_extender() { Wire.beginTransmission(PI4IO_I2C_ADDR); Wire.write(0x01); // test register Wire.endTransmission(); Wire.requestFrom(PI4IO_I2C_ADDR, 1); uint8_t rxdata = Wire.read(); Serial.print("Device ID: "); Serial.println(rxdata, HEX); Wire.beginTransmission(PI4IO_I2C_ADDR); Wire.write(0x03); // IO direction register Wire.write((1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4)); // set pins 0, 1, 2 as outputs Wire.endTransmission(); Wire.beginTransmission(PI4IO_I2C_ADDR); Wire.write(0x07); // Output Hi-Z register Wire.write(~((1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4))); // set pins 0, 1, 2 low Wire.endTransmission(); } void set_pin_io(uint8_t pin_number, bool value) { Wire.beginTransmission(PI4IO_I2C_ADDR); Wire.write(0x05); // test register Wire.endTransmission(); Wire.requestFrom(PI4IO_I2C_ADDR, 1); uint8_t rxdata = Wire.read(); Serial.print("Before the change: "); Serial.println(rxdata, HEX); Wire.beginTransmission(PI4IO_I2C_ADDR); Wire.write(0x05); // Output register if (!value) Wire.write((~(1 << pin_number)) & rxdata); // set pin low else Wire.write((1 << pin_number) | rxdata); // set pin high Wire.endTransmission(); Wire.beginTransmission(PI4IO_I2C_ADDR); Wire.write(0x05); // test register Wire.endTransmission(); Wire.requestFrom(PI4IO_I2C_ADDR, 1); rxdata = Wire.read(); Serial.print("after the change: "); Serial.println(rxdata, HEX); } //RTC function void RTC_init() { rtc.begin(); // Set custom time // I2C_BM8563_TimeTypeDef timeStruct; // timeStruct.hours = 11; // Hour (0 - 23) // timeStruct.minutes = 59; // Minute (0 - 59) // timeStruct.seconds = 0; // Second (0 - 59) // rtc.setTime(&timeStruct); // // I2C_BM8563_DateTypeDef dateStruct; // dateStruct.weekDay = 3; // Weekday (0 - 6, where 0 is Sunday) // dateStruct.month = 1; // Month (1 - 12) // dateStruct.date = 24; // Day of the month (1 - 31) // dateStruct.year = 2024; // Year // rtc.setDate(&dateStruct); } //Encoder function void updateEncoder() { static int previousState = 0; static int flag_A = 0; static int flag_C = 0; int currentState = (digitalRead(ENCODER_A_PIN) << 1) | digitalRead(ENCODER_B_PIN); if ((currentState == 0b00 && previousState == 0b01) || (currentState == 0b01 && previousState == 0b11) || (currentState == 0b11 && previousState == 0b10) || (currentState == 0b10 && previousState == 0b00)) { // foreward // if (switchPressed) { flag_A++; if (flag_A == 50) { flag_A = 0; flag_C = 0; // position++; // position_tmp=position; position_tmp = 1; } // flag_C=0; // } } else if ((currentState == 0b01 && previousState == 0b00) || (currentState == 0b11 && previousState == 0b01) || (currentState == 0b10 && previousState == 0b11) || (currentState == 0b00 && previousState == 0b10)) { // reversal // if (switchPressed) { flag_C++; if (flag_C == 50) { // position--; flag_C = 0; flag_A = 0; // position_tmp=position; position_tmp = 0; } // flag_A=0; // } } previousState = currentState; } void switchPressedInterrupt() { switchPressed = !switchPressed; } void setup() { Serial.begin(115200); /* prepare for possible serial debug */ Serial.println("I am LVGL_Arduino"); Wire.begin(4, 5); init_IO_extender(); delay(100); set_pin_io(3, true); set_pin_io(4, true); pinMode(ENCODER_A_PIN, INPUT_PULLUP); pinMode(ENCODER_B_PIN, INPUT_PULLUP); pinMode(SWITCH_PIN, INPUT_PULLUP); pinMode(Custom_PIN, INPUT); attachInterrupt(digitalPinToInterrupt(ENCODER_A_PIN), updateEncoder, CHANGE); attachInterrupt(digitalPinToInterrupt(ENCODER_B_PIN), updateEncoder, CHANGE); attachInterrupt(digitalPinToInterrupt(SWITCH_PIN), switchPressedInterrupt, FALLING); // ticker.attach(1, tcr1s); tft.init(); tft.initDMA(); tft.startWrite(); tft.setColor(0, 0, 0); tft.fillScreen(TFT_BLACK); delay(200); if (is_touch == 1) { touch.begin(); } lv_init(); #if LV_USE_LOG != 0 //lv_log_register_print_cb(my_print); /* register print function for debugging */ #endif lv_disp_draw_buf_init(&draw_buf, buf[0], buf[1], screenWidth * buf_size); /*Initialize the display*/ static lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); /*Change the following line to your display resolution*/ disp_drv.hor_res = screenWidth; disp_drv.ver_res = screenHeight; disp_drv.flush_cb = my_disp_flush; disp_drv.draw_buf = &draw_buf; lv_disp_drv_register(&disp_drv); /*Initialize the (dummy) input device driver*/ if (is_touch == 1) { static lv_indev_drv_t indev_drv; lv_indev_drv_init(&indev_drv); indev_drv.type = LV_INDEV_TYPE_POINTER; indev_drv.read_cb = my_touchpad_read; lv_indev_drv_register(&indev_drv); } #if 0 /* Create simple label */ lv_obj_t *label = lv_label_create( lv_scr_act() ); lv_label_set_text( label, "Hello Arduino! (V8.0.X)" ); lv_obj_align( label, LV_ALIGN_CENTER, 0, 0 ); #else /* Try an example from the lv_examples Arduino library make sure to include it as written above. lv_example_btn_1(); */ // uncomment one of these demos // lv_demo_widgets(); // OK // lv_demo_benchmark(); // OK // lv_demo_keypad_encoder(); // works, but I haven't an encoder // lv_demo_music(); // NOK // lv_demo_printer(); // lv_demo_stress(); // seems to be OK ui_mian(); // watch #endif Serial.println("Setup done"); // delay(200); set_pin_io(2, true); pinMode(3, OUTPUT); digitalWrite(3, LOW); // pinMode(0, INPUT); // Get RTC RTC_init(); rtc.getDate(&dateStruct); rtc.getTime(&timeStruct); } //void Watch_Function(void *param) void loop() { lv_timer_handler(); /* let the GUI do its work */ lv_label_set_text(ui_lblCounter, String(counter).c_str()); rtc.getTime(&timeStruct); lv_label_set_text(ui_lblHour, String(timeStruct.hours).c_str()); lv_label_set_text(ui_lblminutes, String(timeStruct.minutes).c_str()); lv_label_set_text(ui_lblseconds, String(timeStruct.seconds).c_str()); } void ResetCounter(lv_event_t * e) { counter = 0; } void incrementCounter(lv_event_t * e) { counter++; } |
Output:
On the first screen, we have a fully functional Counter, and on the second screen, we have a Digital Watch. So, that’s all for now!