├── .gitmodules ├── examples └── platformio │ ├── .gitignore │ ├── etc │ ├── platformio │ │ └── middleware.py │ └── esp32 │ │ └── partitions.csv │ ├── test │ └── README │ ├── platformio.ini │ ├── lib │ └── README │ ├── include │ └── main.hpp │ └── src │ └── main.cpp ├── docs ├── pcb-layout.jpg ├── soc-pinout.png └── esp32-wroom-32_datasheet_en.pdf ├── .gitignore ├── library.properties ├── LICENSE ├── library.json ├── README.md └── include └── ESP323248S035.hpp /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/platformio/.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | -------------------------------------------------------------------------------- /docs/pcb-layout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardnew/ESP32-3248S035/HEAD/docs/pcb-layout.jpg -------------------------------------------------------------------------------- /docs/soc-pinout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardnew/ESP32-3248S035/HEAD/docs/soc-pinout.png -------------------------------------------------------------------------------- /docs/esp32-wroom-32_datasheet_en.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardnew/ESP32-3248S035/HEAD/docs/esp32-wroom-32_datasheet_en.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | examples/*/* 2 | !examples/arduino/**/*.{c,cpp,h,hpp} 3 | !examples/arduino/**/Makefile 4 | !examples/platformio/** 5 | 6 | **/.vscode/ 7 | **/*.code-workspace 8 | 9 | **/*.o 10 | -------------------------------------------------------------------------------- /examples/platformio/etc/platformio/middleware.py: -------------------------------------------------------------------------------- 1 | Import("env") 2 | 3 | def cflags(env, node): 4 | return env.Object( 5 | node, 6 | CFLAGS=env['CFLAGS'] + 7 | ['-std=gnu99'] 8 | ) 9 | 10 | def cxxflags(env, node): 11 | return env.Object( 12 | node, 13 | CXXFLAGS=env['CXXFLAGS'] + 14 | ['-std=gnu++17'] 15 | ) 16 | 17 | env.AddBuildMiddleware(cflags, "*.c") 18 | env.AddBuildMiddleware(cxxflags, "*.cpp") 19 | -------------------------------------------------------------------------------- /examples/platformio/etc/esp32/partitions.csv: -------------------------------------------------------------------------------- 1 | # vim: noet:ts=12 2 | # 3 | # (Bootloader) 0x1000 # -- 4 | # (Partition table) 0x8000 0x1000 # 4K 5 | #nvs, data, nvs, 0x9000, 0x5000 # 20K 6 | #otadata, data ota, 0xE000, 0x2000 # 8K 7 | #app0, app, factory, 0x10000, 0x3E0000 # 3968K 8 | #coredump data, coredump, 0x3F0000, 0x10000 # 64K 9 | 10 | nvs, data, nvs, , 0x5000 11 | otadata, data, ota, , 0x2000 12 | app0, app, factory, , 0x3E0000 13 | coredump, data, coredump, , 0x10000 14 | 15 | -------------------------------------------------------------------------------- /examples/platformio/test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PlatformIO Test Runner and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PlatformIO Unit Testing: 11 | - https://docs.platformio.org/en/latest/advanced/unit-testing/index.html 12 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=ESP32-3248S035 2 | version=0.2.0 3 | author=ardnew 4 | maintainer=ardnew 5 | sentence=Board support package for Sunton ESP32-3248S035 6 | paragraph=This library provides comprehensive peripheral support for the low-cost Sunton ESP32-3248S035C featuring an ESP32-WROOM-32 MCU (NRND), 4MB (32Mbit) embedded QSPI flash, 3.5" 320x480 ST7796 SPI LCD, GT911 I²C capacitive touch, CH340C USB (Micro-B) UART bridge, MicroSD card slot, 3-pin RGB LED, and SC8002B 3W audio amplifier 7 | category=Device Control 8 | url=https://github.com/ardnew/ESP32-3248S035 9 | architectures=esp32 10 | includes=ESP323248S035.h 11 | depends=cronos, lvgl 12 | -------------------------------------------------------------------------------- /examples/platformio/platformio.ini: -------------------------------------------------------------------------------- 1 | [platformio] 2 | default_envs = esp32-3248s035 3 | description = PlatformIO project demo using ESP32-3248S035 BSP 4 | 5 | [lvgl] 6 | build_flags = 7 | -DLV_CONF_INCLUDE_SIMPLE=1 8 | -DLV_CONF_SKIP=1 9 | -DLV_COLOR_DEPTH=16 10 | -DLV_USE_LOG=1 11 | -DLV_LOG_LEVEL=LV_LOG_LEVEL_INFO 12 | 13 | [env:esp32-3248s035] 14 | platform = espressif32 15 | framework = arduino 16 | board = esp32dev 17 | board_build.partitions = /$PROJECT_DIR/etc/esp32/partitions.csv 18 | build_flags = 19 | ${lvgl.build_flags} 20 | ; -D CORE_DEBUG_LEVEL=5 21 | lib_deps = 22 | ESP32-3248S035=symlink://../../ 23 | monitor_speed = 115200 24 | extra_scripts = 25 | pre:etc/platformio/middleware.py 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ardnew 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ESP32-3248S035", 3 | "keywords": "ESP32-3248S035, ESP32-3248S035C, ESP32, ST7796, GT911", 4 | "description": "Board support package for Sunton ESP32-3248S035", 5 | "homepage": "https://github.com/ardnew/ESP32-3248S035", 6 | "license": "MIT", 7 | "version": "0.2.1", 8 | "frameworks": [ 9 | "arduino" 10 | ], 11 | "platforms": [ 12 | "espressif32" 13 | ], 14 | "dependencies": { 15 | "ardnew/cronos": "^0.2.1", 16 | "lvgl/lvgl": "^9.1.0" 17 | }, 18 | "headers": [ 19 | "ESP323248S035.hpp" 20 | ], 21 | "build": { 22 | "unflags": [ 23 | "-std=gnu++11", "-std=gnu++14" 24 | ], 25 | "flags": [ 26 | "-std=gnu++17", 27 | "-DLV_CONF_INCLUDE_SIMPLE=1", 28 | "-DLV_CONF_SKIP=1", 29 | "-DLV_COLOR_DEPTH=16" 30 | ] 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/ardnew/ESP32-3248S035" 35 | }, 36 | "authors": [ 37 | { 38 | "name": "ardnew", 39 | "email": "andrew@ardnew.com", 40 | "url": "https://github.com/ardnew", 41 | "maintainer": "true" 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /examples/platformio/lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /examples/platformio/include/main.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // The ESP32-3248S035 library defines View as a pure virtual class, which acts 6 | // as an interface between the library and application GUI code. 7 | // 8 | // We must define a concrete implementation of View to act as the root window 9 | // of the target device's GUI. The content of View is application-defined. 10 | // 11 | // NOTE: Do NOT make any lvgl API calls from the View constructor. These must be 12 | // performed in the View::init() method. 13 | class Main: public bsp::View { 14 | public: 15 | Main() = default; 16 | ~Main() = default; 17 | 18 | bool init(lv_obj_t *root) override { 19 | if (nullptr == root) { 20 | // Use the default screen if no root view provided. 21 | root = lv_scr_act(); 22 | } 23 | lv_obj_t *label = lv_label_create(root); 24 | lv_obj_center(label); 25 | lv_label_set_text(label, "Hello, Arduino!"); 26 | return true; 27 | } 28 | 29 | void update(msecu32_t const now) override { 30 | // Update any subviews here. 31 | } 32 | 33 | std::string title() override { 34 | return "Main"; 35 | } 36 | }; 37 | 38 | Main root; 39 | //bsp::ESP323248S035C target(root); // C++17 can deduce template parameter. 40 | bsp::ESP323248S035C
target(root); // Pre-C++17 requires explicit type. 41 | -------------------------------------------------------------------------------- /examples/platformio/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "main.hpp" 2 | 3 | uint8_t wheel(lv_color32_t &rgb, uint8_t const i, int8_t const step = 1) { 4 | uint8_t curr = i; 5 | uint8_t next = i + step; 6 | curr = 0xFF - curr; 7 | if (curr < 0x55) { 8 | rgb.red = 0x03 * curr; 9 | rgb.green = 0xFF - curr * 0x03; 10 | rgb.blue = 0x00; 11 | } 12 | else if (curr < 0xAA) { 13 | curr -= 0x55; 14 | rgb.red = 0xFF - curr * 0x03; 15 | rgb.green = 0x00; 16 | rgb.blue = 0x03 * curr; 17 | } 18 | else { 19 | curr -= 0xAA; 20 | rgb.red = 0x00; 21 | rgb.green = 0x03 * curr; 22 | rgb.blue = 0xFF - curr * 0x03; 23 | } 24 | return next; 25 | } 26 | 27 | template 28 | bool can_refresh(T const now = msecu32().count()) { 29 | if constexpr (Freq != 0) { 30 | static T last = 0; 31 | if (now - last < Freq) { 32 | return false; 33 | } 34 | last = now; 35 | } 36 | return true; 37 | } 38 | 39 | // The Arduino API requires a setup() and loop(). These are where you initiate 40 | // and refresh the GUI elements and all other hardware peripherals. 41 | void setup() { 42 | target.init(); 43 | } 44 | 45 | void loop() { 46 | static uint8_t i_wheel = 0; 47 | if (can_refresh()) { 48 | // The individual hardware peripherals can be accessed via template 49 | // parameter on method hw(), where T is the peripheral type: 50 | lv_color32_t rgb = target.hw().get(); 51 | i_wheel = wheel(rgb, i_wheel); 52 | 53 | target.hw().set(rgb); 54 | target.update(); 55 | } 56 | } 57 | 58 | // If using the lvgl logging system (LV_USE_LOG), you must: 59 | // 1. Define a tpc_type::log_type function, and 60 | // 2. Assign it to the static log member of the tpc_type class. 61 | // Both steps are shown here: 62 | #if (LV_USE_LOG) 63 | template<> const bsp::tpc_type::log_type bsp::tpc_type::log = 64 | [](lv_log_level_t ll, const char *msg) { 65 | static const char *pre[_LV_LOG_LEVEL_NUM] = { 66 | "[-] ", // [0] LV_LOG_LEVEL_TRACE 67 | "[=] ", // [1] LV_LOG_LEVEL_INFO 68 | "[~] ", // [2] LV_LOG_LEVEL_WARN 69 | "[!] ", // [3] LV_LOG_LEVEL_ERROR 70 | "[+] " // [4] LV_LOG_LEVEL_USER 71 | }; 72 | unsigned level = static_cast(ll); 73 | if (level < LV_LOG_LEVEL_NONE) { 74 | Serial.print(pre[level]); 75 | } 76 | Serial.println(msg); 77 | }; 78 | #endif 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32-3248S035 2 | 3 | #### [ESP-WROOM-32 datasheet (EN)](docs/esp32-wroom-32_datasheet_en.pdf) 4 | 5 | This repository contains a barebones board support package (BSP) for the Sunton ESP32-3248S035(C) with a minimal API that supports all onboard peripherals: 6 | 7 | |Peripheral|Interface| 8 | |:--------:|:--------| 9 | |LCD (ST7796)|SPI| 10 | |Capacitive touch (GT911)|I²C| 11 | |Audio amplifier|PWM| 12 | |RGB LED|PWM| 13 | |Photoresistor|ADC| 14 | 15 | The LCD graphics and touch support are provided directly by [lvgl](https://github.com/lvgl/lvgl) version 9.0. To minimize dependencies and overhead, it drives the SoC peripherals directly instead of using intermediate driver libraries (e.g., TFT_eSPI or LoyvanGFX). 16 | 17 | [Example projects](examples) exist for PlatformIO. It uses the `arduino` framework and `espressif32` platform, so it should be trivial to use in either of those environments. 18 | 19 | ## Layout 20 | 21 | The following image is sourced from [macsbug](https://macsbug.wordpress.com/2022/10/02/esp32-3248s035/). Note this image is taken of the board variant with resistive touch (XPT2046) instead of capacitive touch (GT911). 22 | 23 | ![ESP32-3248S035R](docs/pcb-layout.jpg) 24 | 25 | Some of the notable differences I've observed between resistive and capacitive touch variants: 26 | 27 | |Symbol|Resistive (pictured, unsupported)|Capacitive (supported)| 28 | |:----:|:-------------------------------:|:--------------------:| 29 | | `U3` | XPT2046 resistive touch | Unpopulated | 30 | | `U4` | 4MB SPI flash | Unpopulated | 31 | |`CN1.3` | Not connected (`NC`) | IO22 | 32 | 33 | ## Pinout 34 | 35 | ![ESP-WROOM-32](docs/soc-pinout.png) 36 | 37 | The following table summarizes GPIO functionality. Note that this **does not** convey all constraints or restrictions. [Refer to the datasheet](docs/esp32-wroom-32_datasheet_en.pdf) before assuming a pin is available. 38 | 39 | | GPIO | Input | Output | Strapping | Function | 40 | |:------:|:-----:|:------:|:-----:|:---------| 41 | | 0 | ☒ | ☒ | ☒ | ADC2_CH1, TOUCH1, RTC_GPIO11, CLK_OUT1, EMAC_TX_CLK | 42 | | 1 | ⚠ | ⚠ | ☐ | U0TXD, CLK_OUT3, EMAC_RXD2 | 43 | | 2 | ☒ | ☒ | ☒ | ADC2_CH2, TOUCH2, RTC_GPIO12, HSPIWP, HS2_DATA0, SD_DATA0 | 44 | | 3 | ⚠ | ⚠ | ☐ | U0RXD, CLK_OUT2 | 45 | | 4 | ☒ | ☒ | ☐ | ADC2_CH0, TOUCH0, RTC_GPIO10, HSPIHD, HS2_DATA1, SD_DATA1, EMAC_TX_ER | 46 | | 5 | ☒ | ☒ | ☒ | VSPICS0, HS1_DATA6, EMAC_RX_CLK | 47 | | 6 | ⚠ | ⚠ | ☐ | SD_CLK, SPICLK, HS1_CLK, U1CTS | 48 | | 7 | ⚠ | ⚠ | ☐ | SD_DATA0, SPIQ, HS1_DATA0, U2RTS | 49 | | 8 | ⚠ | ⚠ | ☐ | SD_DATA1, SPID, HS1_DATA1, U2CTS | 50 | | 9 | ⚠ | ⚠ | ☐ | SD_DATA2, SPIHD, HS1_DATA2, U1RXD | 51 | | 10 | ⚠ | ⚠ | ☐ | SD_DATA3, SPIWP, HS1_DATA3, U1TXD | 52 | | 11 | ⚠ | ⚠ | ☐ | SD_CMD, SPICS0, HS1_CMD, U1RTS | 53 | | 12 | ☒ | ☒ | ☒ | ADC2_CH5, TOUCH5, RTC_GPIO15, MTDI, HSPIQ, HS2_DATA2, SD_DATA2, EMAX_TXD3 | 54 | | 13 | ☒ | ☒ | ☐ | ADC2_CH4, TOUCH4, RTC_GPIO14, MTCK, HSPID, HS2_DATA3, SD_DATA3, EMAX_RX_ER | 55 | | 14 | ☒ | ☒ | ☐ | ADC2_CH6, TOUCH6, RTC_GPIO16, MTMS, HSPICLK, HS2_CLK, SD_CLK, EMAC_TXD2 | 56 | | 15 | ☒ | ☒ | ☒ | ADC2_CH3, TOUCH3, MTDO, HSPICS0, RTC_GPIO13, HS2_CMD, SD_CMD, EMAC_RXD3 | 57 | | 16 | ☒ | ☒ | ☐ | HS1_DATA4, U2RXD, EMAC_CLK_OUT | 58 | | 17 | ☒ | ☒ | ☐ | HS1_DATA5, U2TXD, EMAC_CLK_OUT_180 | 59 | | 18 | ☒ | ☒ | ☐ | VSPICLK, HS1_DATA7 | 60 | | 19 | ☒ | ☒ | ☐ | VSPIQ, U0CTS, EMAC_TXD0 | 61 | | 21 | ☒ | ☒ | ☐ | VSPIHD, EMAC_TX_EN | 62 | | 22 | ☒ | ☒ | ☐ | VSPIWP, U0RTS, EMAC_TXD1 | 63 | | 23 | ☒ | ☒ | ☐ | VSPID, HS1_STROBE | 64 | | 25 | ☒ | ☒ | ☐ | DAC_1, ADC2_CH8, RTC_GPIO6, EMAC_RX_D0 | 65 | | 26 | ☒ | ☒ | ☐ | DAC_2, ADC2_CH9, RTC_GPIO7, EMAC_RX_D1 | 66 | | 27 | ☒ | ☒ | ☐ | ADC2_CH7, TOUCH7, RTC_GPIO17, EMAC_RX_DV | 67 | | 32 | ☒ | ☒ | ☐ | XTAL_32K_P (32.768 kHz crystal input), ADC1_CH4, TOUCH9, RTC_GPIO9 | 68 | | 33 | ☒ | ☒ | ☐ | XTAL_32K_N (32.768 kHz crystal output), ADC1_CH5, TOUCH8, RTC_GPIO8 | 69 | | 34 | ☒ | ☐ | ☐ | ADC1_CH6, RTC_GPIO4 | 70 | | 35 | ☒ | ☐ | ☐ | ADC1_CH7, RTC_GPIO5 | 71 | | 36 | ☒ | ☐ | ☐ | ADC1_CH0, RTC_GPIO0 | 72 | | 39 | ☒ | ☐ | ☐ | ADC1_CH3, RTC_GPIO3 | 73 | -------------------------------------------------------------------------------- /include/ESP323248S035.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Arduino-ESP32 core 4 | #include 5 | #include 6 | #include 7 | #include 8 | // ESP-IDF SDK 9 | #include 10 | #include 11 | #include 12 | // External libraries 13 | #include 14 | #include 15 | // C++ stdlib 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | namespace bsp { 22 | 23 | // The sole template parameter msecu32_t N defines the frequency (miilliseconds) 24 | // of which the given function func is called. 25 | // 26 | // Note that func is not required to run in the same, fixed time duration every 27 | // iteration. The intervening delay before func is called again will be recalcu- 28 | // lated to ensure consistent frequency (this is the purpose and responsibility 29 | // of FreeRTOS function xTaskDelayUntil). 30 | // 31 | // The only constraint on N is that it is greater than the maximum _possible_ 32 | // duration of func. Macro TASK_SCHED_GREEDY configures the behavior when this 33 | // constraint is not met: 34 | // 35 | // If func takes longer than N milliseconds and: 36 | // Defined(TASK_SCHED_GREEDY): 37 | // The subsequent func is invoked immediately without delay. 38 | // If this condition continues, it will starve the IDLE task and trigger 39 | // the IWDT to reset the system. This will ensure func runs as frequently 40 | // as possible, but it might cause system resets. 41 | // NotDefined(TASK_SCHED_GREEDY): 42 | // The subsequent func is deferred to the following period. 43 | // This means the func whose deadline was missed will never execute, which 44 | // reduces the frequency of func but gives the IDLE task more opportunity 45 | // to run and reset the IWDT. 46 | // This behavior should improve system stability. 47 | // Therefore, it is the default. 48 | // 49 | template void run(std::function func) { 50 | // xTaskDelayUntil arguments are expressed in units of system "ticks". 51 | // Conventionally, 1 tick = 1 millisecond regardless of CPU frequency. 52 | static constexpr TickType_t freq = N / portTICK_PERIOD_MS; 53 | TickType_t last = xTaskGetTickCount(); 54 | for (;;) { 55 | #ifndef TASK_SCHED_GREEDY 56 | if /* conditionally run func() iff time has elapsed */ 57 | #endif 58 | (xTaskDelayUntil(&last, freq)) 59 | { func(); } 60 | } 61 | } 62 | 63 | using pin_type = int8_t; 64 | 65 | // Atomic provides a mutual exclusion mechanism for shared resources. 66 | template > 68 | struct Atomic: MutexType { 69 | using type = Atomic; 70 | using mutex_type = MutexType; 71 | using guard_type = GuardType; 72 | // Take ownership of this mutex until the returned value is destroyed. 73 | virtual inline guard_type make() { return guard_type(*this); } 74 | }; 75 | 76 | struct Periodic { virtual void update(msecu32_t const) = 0; }; 77 | struct Controller: Periodic { virtual bool init() = 0; }; 78 | 79 | struct View : Periodic { 80 | virtual bool init(lv_obj_t *) = 0; 81 | virtual std::string title() = 0; 82 | }; 83 | 84 | template struct SPIImpl; 85 | template <> struct SPIImpl : SPIClass { 86 | using type = SPIClass; 87 | using type::type; 88 | inline bool init() { type::begin(); return true; } 89 | }; 90 | 91 | template struct I2CImpl; 92 | template <> struct I2CImpl : TwoWire { 93 | using type = TwoWire; 94 | using type::type; 95 | inline bool init() { return type::begin(); } 96 | }; 97 | 98 | template , 99 | typename SPIType = SPIImpl<>, typename I2CType = I2CImpl<>> 100 | class TPC_LCD: public Controller, AtomicType, SPIType, I2CType { 101 | public: 102 | using type = TPC_LCD; 103 | using atomic_type = AtomicType; 104 | using spi_type = SPIType; 105 | using i2c_type = I2CType; 106 | 107 | // C callback signatures required by lvgl 108 | // (note: omit tick() because it's a static method we can use directly) 109 | struct Callback { 110 | using log_type = void (*)(lv_log_level_t, const char *); 111 | using flush_type = void (*)(lv_display_t *, const lv_area_t *, uint8_t *); 112 | using read_type = void (*)(lv_indev_t *, lv_indev_data_t *); 113 | inline static type *instance; // must be public for access from C context 114 | static void flush(lv_display_t *disp, const lv_area_t *area, uint8_t *data) { 115 | instance->flush(disp, area, data); 116 | } 117 | static void read(lv_indev_t *drv, lv_indev_data_t *data) { 118 | instance->read(drv, data); 119 | } 120 | }; 121 | 122 | using log_type = typename Callback::log_type; 123 | using flush_type = typename Callback::flush_type; 124 | using read_type = typename Callback::read_type; 125 | 126 | #if (LV_USE_LOG) 127 | // User is required to provide log whenever the LV_USE_LOG macro is defined. 128 | // It will be registered with lvgl automatically in method TPC_LCD::init(). 129 | static const log_type log; 130 | #endif 131 | 132 | protected: 133 | struct __attribute__((packed)) point_t { 134 | // attributes of an individual touch event, this structure is repeated 135 | // contiguously in the following memory-mapped registers, which implements 136 | // 5-point multi-touch tracking (in the hardware). 137 | // however, the LCD graphics library (lvgl or TFT_eSPI/LoyvanGFX) currently 138 | // supports single-touch only. 139 | // 1. 0x814F—0x8156 140 | // 2. 0x8157—0x815E 141 | // 3. 0x815F—0x8166 142 | // 4. 0x8167—0x816E 143 | // 5. 0x816F—0x8176 144 | uint8_t track; 145 | uint16_t x; 146 | uint16_t y; 147 | uint16_t area; 148 | // 1 pad byte makes 64 bits in (packed) struct 149 | uint8_t _u8[1]; // Value is irrelevant 150 | }; 151 | 152 | enum class cmd_t : uint8_t { 153 | swreset = 0x01, // Software Reset 154 | slpin = 0x10, // Sleep in 155 | slpout = 0x11, // Sleep out 156 | noron = 0x13, // Normal Display Mode On 157 | invoff = 0x20, // Display Inversion Off 158 | dispon = 0x29, // Display On 159 | caset = 0x2A, // Column Address Set 160 | raset = 0x2B, // Row Address Set 161 | ramwr = 0x2C, // Memory Write 162 | madctl = 0x36, // Memory Data Access Control 163 | colmod = 0x3A, // Interface Pixel Format 164 | pgc = 0xE0, // Positive Gamma Control 165 | ngc = 0xE1, // Negative Gamma Control 166 | cscon = 0xF0, // Command Set wqaControl 167 | }; 168 | enum class madctl_t : uint8_t { 169 | my = 0x80, // Row Addr Order: 0=Inc(↓), 1=Dec(↑) 170 | mx = 0x40, // Column Addr Order: 0=Inc(→), 1=Dec(←) 171 | mv = 0x20, // Row/Col exchange: 0=Norm, 1=Exchange 172 | ml = 0x10, // Vertical Refresh Order 173 | bgr = 0x08, // Subpixel rendering: BGR order 174 | mh = 0x10, // Horizontal Refresh Order 175 | rgb = 0x00, // Subpixel rendering: RGB order 176 | // MADCTL rotation bitmasks 177 | // We can conveniently translate these bitmasks to unique values 0..3, 178 | // AND preserve the ordering for computing degrees of rotation, as follows: 179 | tall = my, // ((128 >> 5) & 3) * 90 == 0 * 90 = 0° 180 | wide = mv, // ((32 >> 5) & 3) * 90 == 1 * 90 = 90° 181 | tall_inverted = mx, // ((64 >> 5) & 3) * 90 == 2 * 90 = 180° 182 | wide_inverted = my | mv | mx, // ((224 >> 5) & 3) * 90 == 3 * 90 = 270° 183 | }; 184 | // Like the enum definitions above, these color mode constants are used by 185 | // the MIPI TFT init sequence (n.b., unrelated to lvgl (LV_COLOR_*)). 186 | enum class colmod_t : uint8_t { 187 | rgb16 = 0x50, 188 | ctrl16 = 0x05, 189 | rgb656 = rgb16 | ctrl16, 190 | }; 191 | 192 | // GPIO pins 193 | static pin_type constexpr _pin_sdi = 12U; // SPI MISO 194 | static pin_type constexpr _pin_sdo = 13U; // SPI MOSI 195 | static pin_type constexpr _pin_sck = 14U; // SPI SCLK 196 | static pin_type constexpr _pin_sel = 15U; // SPI SS/CS 197 | static pin_type constexpr _pin_sdc = 2U; // LCD DC/RS 198 | static pin_type constexpr _pin_blt = 27U; // delicious BLT 199 | static pin_type constexpr _pin_scl = 32U; // I²C SCL 200 | static pin_type constexpr _pin_sda = 33U; // I²C SDA 201 | static pin_type constexpr _pin_int = 21U; // TPC INT 202 | static pin_type constexpr _pin_rst = 25U; // TPC RST 203 | // SPI interfaces 204 | static uint8_t constexpr _spi_bus = 2U; // HSPI 205 | static uint32_t constexpr _bus_freq = 80000000U; // 80 MHz 206 | static uint8_t constexpr _word_ord = SPI_MSBFIRST; 207 | static uint8_t constexpr _sig_mode = SPI_MODE0; 208 | // I²C interfaces 209 | static uint8_t constexpr _i2c_bus = 1U; // Wire1 210 | static uint8_t constexpr _dev_addr = 0x5D; // I²C touch device address 211 | // Backlight PWM 212 | static uint8_t constexpr _pwm_blt_chan = 12U; 213 | static uint32_t constexpr _pwm_blt_freq = 5000U; // 5 kHz 214 | static uint8_t constexpr _pwm_blt_bits = 8U; 215 | static uint8_t constexpr _pwm_blt_hres = (1U << _pwm_blt_bits) - 1U; 216 | // TFT configuration 217 | static msecu32_t constexpr _tft_refresh = msecu32_t{10}; // milliseconds 218 | static uint16_t constexpr _tft_width = 320U; // Physical dimensions, does 219 | static uint16_t constexpr _tft_height = 480U; // not consider rotations. 220 | static uint8_t constexpr _tft_colmod = static_cast(colmod_t::rgb656); 221 | static uint8_t constexpr _tft_bpp = 16; // bits per pixel 222 | static uint8_t constexpr _tft_pxsize = (_tft_bpp + 7U) >> 3U; // bytes 223 | static size_t constexpr _tft_bufcount = 2U; 224 | static size_t constexpr _tft_bufline = 10U; 225 | static size_t constexpr _tft_bufsize = _tft_width * _tft_bufline * _tft_pxsize; 226 | static uint8_t constexpr _tft_subpixel = static_cast(madctl_t::rgb); 227 | static uint8_t constexpr _tft_aspect = static_cast(madctl_t::wide_inverted); 228 | // Touch MMIO 229 | static uint16_t constexpr _reg_prod_id = 0x8140; // product ID (byte 1/4) 230 | static size_t constexpr _size_prod_id = 4U; // bytes 231 | static uint16_t constexpr _reg_stat = 0x814E; // buffer/track status 232 | static uint16_t constexpr _reg_base_point = 0x814F; 233 | static size_t constexpr _touch_max = 5U; // simultaneous touch points 234 | 235 | // C callback signature of timebase required by lvgl. 236 | static inline void tick() { lv_tick_inc(_tft_refresh.count()); } 237 | 238 | void flush(lv_display_t *disp, const lv_area_t *area, uint8_t *data) { 239 | tx(cmd_t::caset, 240 | static_cast(area->x1 >> 8), 241 | static_cast(area->x1), 242 | static_cast(area->x2 >> 8), 243 | static_cast(area->x2)); 244 | tx(cmd_t::raset, 245 | static_cast(area->y1 >> 8), 246 | static_cast(area->y1), 247 | static_cast(area->y2 >> 8), 248 | static_cast(area->y2)); 249 | 250 | px(cmd_t::ramwr, data, lv_area_get_size(area) * pxsize()); 251 | lv_display_flush_ready(disp); 252 | } 253 | 254 | void read(lv_indev_t *drv, lv_indev_data_t *data) { 255 | static int16_t _x = 0, _y = 0; 256 | point_t pt; 257 | switch (touch_count()) { 258 | case 1: 259 | if (map(&pt, 1)) { 260 | data->state = LV_INDEV_STATE_PR; 261 | _x = data->point.x = pt.x; 262 | _y = data->point.y = pt.y; 263 | } 264 | break; 265 | default: 266 | data->point.x = _x; 267 | data->point.y = _y; 268 | data->state = LV_INDEV_STATE_REL; 269 | break; 270 | } 271 | } 272 | 273 | void px(cmd_t const code, const uint8_t data[], size_t const size) { 274 | digitalWrite(_pin_sdc, LOW); // D/C ⇒ command 275 | spi_type::beginTransaction(SPISettings(_bus_freq, _word_ord, _sig_mode)); 276 | digitalWrite(_pin_sel, LOW); // C/S ⇒ enable 277 | spi_type::write(static_cast(code)); 278 | if (size > 0) { 279 | digitalWrite(_pin_sdc, HIGH); // D/C ⇒ data 280 | spi_type::writeBytes(data, size); 281 | } 282 | digitalWrite(_pin_sel, HIGH); // C/S ⇒ disable 283 | spi_type::endTransaction(); 284 | } 285 | 286 | template void tx(cmd_t const code, E... e) { 287 | constexpr size_t size = sizeof...(e); 288 | uint8_t data[size] = {static_cast(e)...}; 289 | px(code, data, size); 290 | } 291 | 292 | inline bool r16(uint16_t const reg) { 293 | i2c_type::beginTransmission(_dev_addr); 294 | return i2c_type::write(static_cast(reg >> 8)) && // MSB (first) 295 | i2c_type::write(static_cast(reg & 0xFF)); // LSB 296 | } 297 | 298 | inline bool tx(uint16_t const reg, uint8_t *const buf, size_t const len) { 299 | size_t sent = 0U; 300 | if (r16(reg)) { 301 | sent = i2c_type::write(buf, len); 302 | } 303 | i2c_type::endTransmission(); 304 | return sent == len; 305 | } 306 | 307 | inline bool rx(uint16_t const reg, uint8_t *const buf, size_t const len) { 308 | uint8_t *dst = buf; 309 | size_t rem = 0U; 310 | bool ok = r16(reg); 311 | i2c_type::endTransmission(false); 312 | if (ok && (len == (rem = i2c_type::requestFrom(_dev_addr, len)))) { 313 | while (i2c_type::available() && rem--) { 314 | *(dst++) = i2c_type::read(); 315 | } 316 | } 317 | return rem == 0 && dst != buf; 318 | } 319 | 320 | inline bool rx(uint16_t const reg, point_t *const buf, size_t const len) { 321 | return rx(reg, (uint8_t *)buf, len); 322 | } 323 | 324 | // template 325 | // static inline constexpr uint16_t track() { 326 | // return _reg_base_point + N * sizeof(point_t) + offsetof(point_t, track); 327 | // } 328 | 329 | template 330 | inline bool map(point_t *const pt, size_t count) { 331 | bool ok = rx(_reg_base_point, pt, sizeof(point_t) * count); 332 | ((std::function[]){ 333 | [pt, count]() { 334 | for (uint8_t i = 0; i < count; ++i) { 335 | pt[i].x = width() - pt[i].x; 336 | pt[i].y = height() - pt[i].y; 337 | } 338 | }, 339 | [pt, count]() { 340 | uint16_t swap; 341 | for (uint8_t i = 0; i < count; ++i) { 342 | swap = pt[i].x; 343 | pt[i].x = pt[i].y; 344 | pt[i].y = height() - swap; 345 | } 346 | }, 347 | [pt, count]() { 348 | for (uint8_t i = 0; i < count; ++i) { 349 | pt[i].x = pt[i].x; 350 | pt[i].y = pt[i].y; 351 | } 352 | }, 353 | [pt, count]() { 354 | uint16_t swap; 355 | for (uint8_t i = 0; i < count; ++i) { 356 | swap = pt[i].x; 357 | pt[i].x = width() - pt[i].y; 358 | pt[i].y = swap; 359 | } 360 | }}[(R >> 5) & 3])(); 361 | return ok; 362 | } 363 | 364 | inline size_t touch_count() { 365 | static uint8_t stat[1] = {0}; 366 | size_t count = 0; 367 | if (rx(_reg_stat, stat, sizeof(stat))) { 368 | count = stat[0]; 369 | } 370 | memset(stat, 0, sizeof(stat)); 371 | if (((count & 0x80) > 0) && ((count & 0x0F) < _touch_max)) { 372 | tx(_reg_stat, stat, sizeof(stat)); 373 | count &= 0x0F; 374 | } 375 | return count; 376 | } 377 | 378 | private: 379 | static inline lv_display_t *_disp; 380 | static inline lv_indev_t *_inpt; 381 | static inline uint8_t _fb[_tft_bufcount][_tft_bufsize] __attribute__((aligned(4))); 382 | 383 | Ticker _tick; 384 | View &_view; 385 | 386 | public: 387 | TPC_LCD() = delete; // display driver must have a root view! 388 | explicit TPC_LCD(View &root, log_type log = nullptr) 389 | : spi_type(_spi_bus), i2c_type(_i2c_bus), _view(root) { 390 | Callback::instance = this; 391 | } 392 | virtual ~TPC_LCD() {} 393 | 394 | bool init() override { 395 | #if (LV_USE_LOG) 396 | lv_log_register_print_cb(log); 397 | #endif 398 | 399 | lv_init(); 400 | spi_type::begin(_pin_sck, _pin_sdi, _pin_sdo); 401 | 402 | pinMode(_pin_sdc, OUTPUT); // D/C 403 | pinMode(_pin_sel, OUTPUT); // CS 404 | digitalWrite(_pin_sel, HIGH); 405 | 406 | pinMode(_pin_blt, OUTPUT); // LITE (PWM) 407 | ledcSetup(_pwm_blt_chan, _pwm_blt_freq, _pwm_blt_bits); 408 | ledcAttachPin(_pin_blt, _pwm_blt_chan); 409 | 410 | // ST7796 initialization sequence 411 | tx(cmd_t::swreset); 412 | delay(100); 413 | tx(cmd_t::cscon, 0xC3); // Enable extension command 2 part I 414 | tx(cmd_t::cscon, 0x96); // Enable extension command 2 part II 415 | tx(cmd_t::colmod, _tft_colmod); // 16 bits R5G6B5 416 | tx(cmd_t::madctl, _tft_aspect | _tft_subpixel); 417 | tx(cmd_t::pgc, 0xF0, 0x09, 0x0B, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B); 418 | tx(cmd_t::ngc, 0xE0, 0x09, 0x0B, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B); 419 | tx(cmd_t::cscon, 0x3C); // Disable extension command 2 part I 420 | tx(cmd_t::cscon, 0x69); // Disable extension command 2 part II 421 | tx(cmd_t::invoff); // Inversion off 422 | tx(cmd_t::noron); // Normal display on 423 | tx(cmd_t::slpout); // Out of sleep mode 424 | tx(cmd_t::dispon); // Main screen turn on 425 | 426 | set_backlight(_pwm_blt_hres); 427 | 428 | _disp = lv_display_create(width(), height()); 429 | // Callback::fn = 430 | // std::bind(&type::flush, this, 431 | // std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); 432 | // lv_display_set_flush_cb(_disp, static_cast( 433 | // Callback::callback)); 434 | //auto flush = static_cast(std::bind(&type::flush, this, 435 | // std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); 436 | lv_display_set_flush_cb(_disp, Callback::flush); 437 | lv_display_set_buffers(_disp, _fb[0], _fb[1], size(), LV_DISPLAY_RENDER_MODE_PARTIAL); 438 | lv_obj_clean(lv_scr_act()); 439 | 440 | // --------------------------------------------------------------------------- 441 | // TODO: calibrate touch? 442 | // --------------------------------------------------------------------------- 443 | 444 | bool ok; 445 | if ((ok = i2c_type::begin(_pin_sda, _pin_scl))) { 446 | static uint8_t pid[_size_prod_id]; 447 | ok = rx(_reg_prod_id, pid, sizeof(pid)); 448 | } 449 | 450 | _inpt = lv_indev_create(); 451 | // Callback::fn = 452 | // std::bind(&type::read, this, 453 | // std::placeholders::_1, std::placeholders::_2); 454 | lv_indev_set_type(_inpt, LV_INDEV_TYPE_POINTER); 455 | lv_indev_set_read_cb(_inpt, Callback::read); 456 | // lv_indev_set_read_cb(_inpt, static_cast( 457 | // Callback::callback)); 458 | 459 | if ((ok = ok && _view.init(lv_scr_act()))) { 460 | _tick.attach_ms(_tft_refresh.count(), tick); 461 | } 462 | 463 | return ok; 464 | } 465 | void update(msecu32_t const now) override { 466 | lv_timer_handler(); 467 | _view.update(now); 468 | } 469 | 470 | View &view() { return _view; } 471 | 472 | template 473 | static void set_backlight(uint16_t duty) { 474 | ledcWrite(C, duty); 475 | } 476 | 477 | static constexpr size_t depth(void) noexcept { return _tft_bpp; } 478 | static constexpr size_t pxsize(void) noexcept { return _tft_pxsize; } 479 | static constexpr size_t size(void) noexcept { return _tft_bufsize; } 480 | static constexpr size_t buffers(void) noexcept { return _tft_bufcount; } 481 | 482 | static constexpr size_t wide(void) noexcept { 483 | return (_tft_aspect & static_cast(madctl_t::mv)) > 0; // transpose? 484 | } 485 | static constexpr size_t width(void) noexcept { 486 | return wide() ? _tft_height : _tft_width; 487 | } 488 | static constexpr size_t height(void) noexcept { 489 | return wide() ? _tft_width : _tft_height; 490 | } 491 | }; 492 | 493 | template > 494 | class RGB_PWM: Controller, AtomicType { // RGB 3-pin analog LR 495 | public: 496 | using type = RGB_PWM; 497 | using atomic_type = AtomicType; 498 | 499 | protected: 500 | static pin_type const _pin_r = 4U; 501 | static pin_type const _pin_g = 16U; 502 | static pin_type const _pin_b = 17U; 503 | 504 | static uint8_t const _pwm_r_chan = 13U; 505 | static uint8_t const _pwm_g_chan = 14U; 506 | static uint8_t const _pwm_b_chan = 15U; 507 | 508 | static uint32_t const _pwm_freq = 5000U; // 5 kHz 509 | static uint8_t const _pwm_bits = 8U; 510 | static uint8_t const _pwm_hres = (1U << _pwm_bits) - 1U; 511 | 512 | lv_color32_t _rgb, _set; // _rgb is last set value, _set is buffer for next 513 | 514 | public: 515 | RGB_PWM() { 516 | } 517 | bool init() override { 518 | // Red 519 | pinMode(_pin_r, OUTPUT); 520 | digitalWrite(_pin_r, true); 521 | ledcSetup(_pwm_r_chan, _pwm_freq, _pwm_bits); 522 | ledcAttachPin(_pin_r, _pwm_r_chan); 523 | // Green 524 | pinMode(_pin_g, OUTPUT); 525 | digitalWrite(_pin_g, true); 526 | ledcSetup(_pwm_g_chan, _pwm_freq, _pwm_bits); 527 | ledcAttachPin(_pin_g, _pwm_g_chan); 528 | // Blue 529 | pinMode(_pin_b, OUTPUT); 530 | digitalWrite(_pin_b, true); 531 | ledcSetup(_pwm_b_chan, _pwm_freq, _pwm_bits); 532 | ledcAttachPin(_pin_b, _pwm_b_chan); 533 | set({}); 534 | update(msecu32_t{0}); 535 | return true; 536 | } 537 | void update(msecu32_t const now) override { 538 | if (now.count() > 0) { 539 | auto scope = atomic_type::make(); 540 | if (lv_color32_eq(_rgb, _set)) { 541 | return; 542 | } 543 | } 544 | write(_set); 545 | } 546 | lv_color32_t get() noexcept { 547 | auto scope = atomic_type::make(); 548 | return _rgb; 549 | } 550 | void set(lv_color32_t const rgb) noexcept { 551 | auto scope = atomic_type::make(); 552 | _set = rgb; 553 | } 554 | void write(lv_color32_t const rgb) noexcept { 555 | { 556 | auto scope = atomic_type::make(); 557 | _rgb = rgb; 558 | } 559 | ledcWrite(_pwm_r_chan, _pwm_hres - rgb.red); 560 | ledcWrite(_pwm_g_chan, _pwm_hres - rgb.green); 561 | ledcWrite(_pwm_b_chan, _pwm_hres - rgb.blue); 562 | } 563 | }; 564 | 565 | template > 566 | class AMP_PWM: Controller, AtomicType { // Autio amplifier 567 | public: 568 | using type = AMP_PWM; 569 | using atomic_type = AtomicType; 570 | 571 | protected: 572 | static pin_type const _pin = 26U; 573 | 574 | public: 575 | bool init() override { 576 | pinMode(_pin, INPUT); // High impedence 577 | update(msecu32_t{0}); 578 | return true; 579 | } 580 | void update(msecu32_t const now) override { 581 | } 582 | void set(uint32_t const hz, msecu32_t const ms) { 583 | tone(_pin, hz, ms.count()); 584 | } 585 | void mute() { 586 | noTone(_pin); 587 | } 588 | }; 589 | 590 | template > 591 | class CDS_ADC: Controller, AtomicType { // Light sensitive photo-resistor 592 | public: 593 | using type = CDS_ADC; 594 | using atomic_type = AtomicType; 595 | 596 | protected: 597 | static adc_attenuation_t const _att = ADC_0db; // 0dB (1.0x): 0~800mV 598 | 599 | static pin_type const _pin = 34U; // A0 600 | 601 | int _val; // _val is last read value, _get is buffer for next 602 | 603 | public: 604 | bool init() override { 605 | analogSetAttenuation(_att); // 0dB(1.0x) 0~800mV 606 | pinMode(_pin, INPUT); 607 | update(msecu32_t{0}); 608 | return true; 609 | } 610 | void update(msecu32_t const now) override { 611 | read(); 612 | } 613 | int get() { 614 | auto scope = atomic_type::make(); 615 | return _val; 616 | } 617 | int read() { 618 | auto scope = atomic_type::make(); 619 | _val = analogRead(_pin); 620 | return _val; 621 | } 622 | }; 623 | 624 | template > 625 | class SDC_SPI: Controller, AtomicType { // MicroSD card 626 | public: 627 | using type = SDC_SPI; 628 | using atomic_type = AtomicType; 629 | 630 | protected: 631 | static pin_type const _pin_sdi = 19U; 632 | static pin_type const _pin_sdo = 23U; 633 | static pin_type const _pin_sck = 18U; 634 | static pin_type const _pin_sel = 5U; 635 | 636 | public: 637 | bool init() override { 638 | update(msecu32_t{0}); 639 | return true; 640 | } 641 | void update(msecu32_t const now) override { 642 | } 643 | }; 644 | 645 | using tpc_type = TPC_LCD<>; 646 | using rgb_type = RGB_PWM<>; 647 | using amp_type = AMP_PWM<>; 648 | using cds_type = CDS_ADC<>; 649 | using sdc_type = SDC_SPI<>; 650 | 651 | class ESP323248S035 : Controller {}; 652 | 653 | template class ESP323248S035C : public ESP323248S035 { 654 | // You must be careful to not make any lvgl API calls from any constructor, 655 | // since that library is initialized indirectly via method init(), and the 656 | // user can only call init() on a fully constructed object. 657 | // UI initialization and layout should be performed in the init(lv_obj_t *) 658 | // method of template parameter V, which we verify here derives from View. 659 | static_assert(std::is_base_of::value, 660 | "Template parameter V must derive from View."); 661 | 662 | protected: 663 | static constexpr msecu32_t _refresh = msecu32_t{20}; 664 | 665 | // All hardware peripherals found on-board, external to the ESP32, that have 666 | // class drivers implemented in this library. 667 | // This is a singleton class, with a single instance named _hw, which is 668 | // instantiated in the outer/parent class's member initializer list. 669 | struct hw_t { 670 | tpc_type tpc; 671 | rgb_type rgb; 672 | amp_type amp; 673 | cds_type cds; 674 | sdc_type sdc; 675 | hw_t(V &view) : tpc(view) {} 676 | } _hw; 677 | 678 | public: 679 | static inline constexpr msecu32_t refresh() { return _refresh; } 680 | 681 | // Accessor for members of the hardware peripherals (hw_t) singleton instance. 682 | // 683 | // Instead of using separate methods for each member, we use a template and 684 | // the SFINAE pattern to select the correct member based on class type of the 685 | // template argument. 686 | // 687 | // Note that _hw must be resolved at runtime in the body of a method, not at 688 | // compile time in a method's signature (e.g., as default argument). 689 | template inline C &hw() { 690 | union { 691 | tpc_type &cast(hw_t &p, tpc_type *) { return p.tpc; } 692 | rgb_type &cast(hw_t &p, rgb_type *) { return p.rgb; } 693 | amp_type &cast(hw_t &p, amp_type *) { return p.amp; } 694 | cds_type &cast(hw_t &p, cds_type *) { return p.cds; } 695 | sdc_type &cast(hw_t &p, sdc_type *) { return p.sdc; } 696 | } u; 697 | return u.cast(_hw, static_cast(0)); 698 | } 699 | 700 | public: 701 | ESP323248S035C() = delete; 702 | explicit ESP323248S035C(V &view) : _hw(view) {} 703 | 704 | virtual ~ESP323248S035C() {} 705 | 706 | bool init() { 707 | // Due to short-circuiting, if any member's init method returns false, the 708 | // remaining member init methods are never called. 709 | // So be sure the ordering is correct. 710 | return _hw.rgb.init() && _hw.amp.init() && _hw.cds.init() && 711 | _hw.tpc.init() && _hw.sdc.init(); 712 | } 713 | 714 | virtual inline void update(msecu32_t const = msecu32()) override { 715 | dispatch(&_hw.sdc, &_hw.cds, &_hw.rgb, &_hw.tpc, &_hw.amp); 716 | } 717 | 718 | // This will call msecu32() on each peripheral in the order they are listed 719 | // so that the runtime of each peripheral can be calculated correctly: 720 | // |<-- e[0] -->|<-- e[1] -->| ... |<-- e[n] -->| 721 | // ^t[0] ^t[1] ^t[n] 722 | // 723 | // Otherwise, if we were to call msecu32() only once at the beginning, the 724 | // update frequency of the peripherals would be skewed: 725 | // 726 | // |<-- e[0] -->| 727 | // |<-- e[1] -->| 728 | // ... 729 | // |<-- e[n] -->| 730 | template void dispatch(E... e) { 731 | (int[]){(e->update(msecu32()), 0)...}; 732 | } 733 | }; 734 | 735 | } // namespace bsp 736 | --------------------------------------------------------------------------------