├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── boards ├── README ├── esp32-2432S028R.json ├── esp32-2432S028Rv2.json ├── esp32-2432S028Rv3.json └── esp32-8048S043C.json ├── platformio.ini ├── projects ├── calibration-8048S043C │ ├── README.md │ ├── lgfx_8048S043C.h │ └── main.cpp ├── fluid-simulation │ ├── Field.h │ ├── README.md │ ├── Vector.h │ ├── iram_float.h │ ├── main.cpp │ └── operations.h ├── paint │ ├── README.md │ └── main.cpp ├── particles │ ├── README.md │ └── main.cpp ├── sand-multi-task-4_3inch │ ├── PixelState.h │ ├── README.md │ ├── colorChangeRoutine.h │ ├── lgfx_8048S043C.h │ └── main.cpp ├── sand-multi-task │ ├── PixelState.h │ ├── README.md │ └── main.cpp └── sand │ ├── PixelState.h │ ├── README.md │ └── main.cpp └── test └── README /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "type_traits": "cpp", 4 | "new": "cpp", 5 | "random": "cpp", 6 | "unordered_map": "cpp", 7 | "unordered_set": "cpp", 8 | "array": "cpp", 9 | "atomic": "cpp", 10 | "*.tcc": "cpp", 11 | "cctype": "cpp", 12 | "clocale": "cpp", 13 | "cmath": "cpp", 14 | "cstdarg": "cpp", 15 | "cstddef": "cpp", 16 | "cstdint": "cpp", 17 | "cstdio": "cpp", 18 | "cstdlib": "cpp", 19 | "cstring": "cpp", 20 | "ctime": "cpp", 21 | "cwchar": "cpp", 22 | "cwctype": "cpp", 23 | "deque": "cpp", 24 | "vector": "cpp", 25 | "exception": "cpp", 26 | "algorithm": "cpp", 27 | "functional": "cpp", 28 | "iterator": "cpp", 29 | "map": "cpp", 30 | "memory": "cpp", 31 | "memory_resource": "cpp", 32 | "numeric": "cpp", 33 | "optional": "cpp", 34 | "string": "cpp", 35 | "string_view": "cpp", 36 | "system_error": "cpp", 37 | "tuple": "cpp", 38 | "utility": "cpp", 39 | "fstream": "cpp", 40 | "initializer_list": "cpp", 41 | "iomanip": "cpp", 42 | "iosfwd": "cpp", 43 | "iostream": "cpp", 44 | "istream": "cpp", 45 | "limits": "cpp", 46 | "ostream": "cpp", 47 | "sstream": "cpp", 48 | "stdexcept": "cpp", 49 | "streambuf": "cpp", 50 | "cinttypes": "cpp", 51 | "typeinfo": "cpp" 52 | } 53 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Projects using the CYD (Cheap Yellow Display) 2 | 3 | These are projects I made* to run on the [ESP32 Arduino LVGL WIFI&Bluetooth Development Board 2.8 " 240*320 Smart Display Screen 2.8inch LCD TFT Module With Touch WROOM](https://www.aliexpress.us/item/3256805849164942.html) board as sold on AliExpress. This is the ESP32-2432S028 board. Based on the ESP32-D0WDQ6. 4 | 5 | For more information on this CYD and its variants, more example projects, mods, and more; check out [Brian Lough's ESP32-Cheap-Yellow-Display GitHub repo](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display). 6 | 7 | ### Other useful links: 8 | 9 | [LVGL drivers for Chinese Sunton Smart display boards, aka CYD (Cheap Yellow Display)](https://github.com/rzeldent/esp32-smartdisplay) 10 | 11 | [Sunton CYD (Cheap Yellow Display) PlatformIo Board definitions](https://github.com/rzeldent/platformio-espressif32-sunton/) 12 | 13 | --- 14 | 15 | # The Projects 16 | 17 | - [Sand](projects/sand) A falling sand with new sand/pixels changing color over time, falling from the touchscreen inpout point. 18 | - [Sand (Multi-Task)](projects/sand-multi-task) I copied the sand project above and then modified it with parallelization to take advantage of the dual-core ESP32 with a performance boost. 19 | - [Sand (Multi-Task) 4.3 Inch](projects/sand-multi-task-4_3inch) Modified from the prior [Sand (Multi-Task) project](../sand-multi-task). This code targets the 4.3 inch display, with capacitive touch, version of the CYD - the ESP32-8048S043C. 20 | - [Paint](projects/paint) A simple spray paint program with the new paint changing color over time. 21 | - [Fluid Simulation *](projects/fluid-simulation) A simple (though not state-of-the-art) C++ implementation of Jos Stam's fluid simulation method 22 | - [Particles **](projects/particles) Particles orbiting around a point of center mass. 23 | 24 | --- 25 | 26 | \* The "Fluid Simulation" project was copied from [github.com/colonelwatch/ESP32-fluid-simulation/](https://github.com/colonelwatch/ESP32-fluid-simulation/). I modified it to work on my CYD device and converted it to a Platform.io project. 27 | 28 | \*\* The "Particles" project was copied from [github.com/doctorpartlow/esp32s3lilygoanimations](https://github.com/doctorpartlow/esp32s3lilygoanimations/blob/main/particles.ino). I fixed a couple minor bugs, modified it to work on the CYD, and converted it to a Platform.io project. 29 | 30 | --- 31 | 32 | ## Some example videos 33 | 34 | [Sand](projects/sand-multi-task) 35 | 36 | [![Sand Simulation on the Cheap Yellow Display (CYD) ESP32 MCU + Display](https://img.youtube.com/vi/j8XRMEEZ0gM/0.jpg)](https://www.youtube.com/watch?v=j8XRMEEZ0gM) 37 | -------------------------------------------------------------------------------- /boards/README: -------------------------------------------------------------------------------- 1 | These board configs were copied from: 2 | 3 | https://github.com/rzeldent/platformio-espressif32-sunton/tree/eb25caec8b15f951c2beb743111efa043d2d0285 -------------------------------------------------------------------------------- /boards/esp32-2432S028R.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "arduino": { 4 | "ldscript": "esp32_out.ld" 5 | }, 6 | "core": "esp32", 7 | "extra_flags": [ 8 | "'-D ARDUINO_ESP32_DEV'", 9 | "'-D ESP32_2432S028R'", 10 | "'-D LCD_WIDTH=240'", 11 | "'-D LCD_HEIGHT=320'", 12 | "'-D LVGL_BUFFER_PIXELS=(LCD_WIDTH*LCD_HEIGHT/4)'", 13 | "'-D LVGL_BUFFER_MALLOC_FLAGS=(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT)'", 14 | "'-D GPIO_BCKL=21'", 15 | "'-D LCD_ILI9341_SPI'", 16 | "'-D ILI9341_SPI_HOST=SPI2_HOST'", 17 | "'-D ILI9341_SPI_DMA_CHANNEL=SPI_DMA_CH_AUTO'", 18 | "'-D ILI9341_SPI_BUS_MOSI_IO_NUM=13'", 19 | "'-D ILI9341_SPI_BUS_MISO_IO_NUM=12'", 20 | "'-D ILI9341_SPI_BUS_SCLK_IO_NUM=14'", 21 | "'-D ILI9341_SPI_BUS_QUADWP_IO_NUM=GPIO_NUM_NC'", 22 | "'-D ILI9341_SPI_BUS_QUADHD_IO_NUM=GPIO_NUM_NC'", 23 | "'-D ILI9341_SPI_BUS_MAX_TRANSFER_SZ=0'", 24 | "'-D ILI9341_SPI_BUS_FLAGS=0'", 25 | "'-D ILI9341_SPI_BUS_INTR_FLAGS=0'", 26 | "'-D ILI9341_SPI_CONFIG_CS_GPIO_NUM=15'", 27 | "'-D ILI9341_SPI_CONFIG_DC_GPIO_NUM=2'", 28 | "'-D ILI9341_SPI_CONFIG_SPI_MODE=SPI_MODE0'", 29 | "'-D ILI9341_SPI_CONFIG_PCLK_HZ=24000000'", 30 | "'-D ILI9341_SPI_CONFIG_TRANS_QUEUE_DEPTH=10'", 31 | "'-D ILI9341_SPI_CONFIG_LCD_CMD_BITS=8'", 32 | "'-D ILI9341_SPI_CONFIG_LCD_PARAM_BITS=8'", 33 | "'-D ILI9341_SPI_CONFIG_FLAGS_DC_AS_CMD_PHASE=false'", 34 | "'-D ILI9341_SPI_CONFIG_FLAGS_DC_LOW_ON_DATA=false'", 35 | "'-D ILI9341_SPI_CONFIG_FLAGS_OCTAL_MODE=false'", 36 | "'-D ILI9341_SPI_CONFIG_FLAGS_LSB_FIRST=false'", 37 | "'-D ILI9341_DEV_CONFIG_RESET_GPIO_NUM=GPIO_NUM_NC'", 38 | "'-D ILI9341_DEV_CONFIG_COLOR_SPACE=ESP_LCD_COLOR_SPACE_BGR'", 39 | "'-D ILI9341_DEV_CONFIG_BITS_PER_PIXEL=16'", 40 | "'-D ILI9341_DEV_CONFIG_FLAGS_RESET_ACTIVE_HIGH=false'", 41 | "'-D ILI9341_DEV_CONFIG_VENDOR_CONFIG=NULL'", 42 | "'-D LCD_SWAP_XY=false'", 43 | "'-D LCD_MIRROR_X=true'", 44 | "'-D LCD_MIRROR_Y=false'", 45 | "'-D BOARD_HAS_TOUCH'", 46 | "'-D TOUCH_XPT2046_SPI'", 47 | "'-D XPT2046_SPI_HOST=SPI3_HOST'", 48 | "'-D XPT2046_SPI_DMA_CHANNEL=SPI_DMA_CH_AUTO'", 49 | "'-D XPT2046_SPI_BUS_MOSI_IO_NUM=32'", 50 | "'-D XPT2046_SPI_BUS_MISO_IO_NUM=39'", 51 | "'-D XPT2046_SPI_BUS_SCLK_IO_NUM=25'", 52 | "'-D XPT2046_SPI_BUS_QUADWP_IO_NUM=GPIO_NUM_NC'", 53 | "'-D XPT2046_SPI_BUS_QUADHD_IO_NUM=GPIO_NUM_NC'", 54 | "'-D XPT2046_SPI_CONFIG_CS_GPIO_NUM=33'", 55 | "'-D XPT2046_SPI_CONFIG_DC_GPIO_NUM=GPIO_NUM_NC'", 56 | "'-D XPT2046_SPI_CONFIG_SPI_MODE=SPI_MODE0'", 57 | "'-D XPT2046_SPI_CONFIG_PCLK_HZ=2000000'", 58 | "'-D XPT2046_SPI_CONFIG_TRANS_QUEUE_DEPTH=3'", 59 | "'-D XPT2046_SPI_CONFIG_LCD_CMD_BITS=8'", 60 | "'-D XPT2046_SPI_CONFIG_LCD_PARAM_BITS=8'", 61 | "'-D XPT2046_SPI_CONFIG_FLAGS_DC_AS_CMD_PHASE=false'", 62 | "'-D XPT2046_SPI_CONFIG_FLAGS_DC_LOW_ON_DATA=false'", 63 | "'-D XPT2046_SPI_CONFIG_FLAGS_OCTAL_MODE=false'", 64 | "'-D XPT2046_SPI_CONFIG_FLAGS_LSB_FIRST=false'", 65 | "'-D XPT2046_TOUCH_CONFIG_X_MAX=LCD_WIDTH'", 66 | "'-D XPT2046_TOUCH_CONFIG_Y_MAX=LCD_HEIGHT'", 67 | "'-D XPT2046_TOUCH_CONFIG_RST_GPIO_NUM=GPIO_NUM_NC'", 68 | "'-D XPT2046_TOUCH_CONFIG_INT_GPIO_NUM=36'", 69 | "'-D XPT2046_TOUCH_CONFIG_LEVELS_RESET=0'", 70 | "'-D XPT2046_TOUCH_CONFIG_LEVELS_INTERRUPT=0'", 71 | "'-D TOUCH_SWAP_XY=false'", 72 | "'-D TOUCH_SWAP_X=true'", 73 | "'-D TOUCH_SWAP_Y=false'", 74 | "'-D BOARD_HAS_TF'", 75 | "'-D TF_CS=5'", 76 | "'-D TF_SPI_MOSI=23'", 77 | "'-D TF_SPI_SCLK=18'", 78 | "'-D TF_SPI_MISO=19'", 79 | "'-D BOARD_HAS_RGB_LED'", 80 | "'-D RGB_LED_R=4'", 81 | "'-D RGB_LED_G=16'", 82 | "'-D RGB_LED_B=17'", 83 | "'-D BOARD_HAS_CDS'", 84 | "'-D CDS=34'", 85 | "'-D BOARD_HAS_SPEAK'", 86 | "'-D SPEAK=26'" 87 | ], 88 | "f_cpu": "240000000L", 89 | "f_flash": "40000000L", 90 | "flash_mode": "dio", 91 | "mcu": "esp32", 92 | "variant": "esp32" 93 | }, 94 | "connectivity": [ 95 | "wifi", 96 | "bluetooth", 97 | "ethernet", 98 | "can" 99 | ], 100 | "debug": { 101 | "openocd_board": "esp-wroom-32.cfg" 102 | }, 103 | "frameworks": [ 104 | "arduino", 105 | "espidf" 106 | ], 107 | "name": "esp32-2432S028R", 108 | "upload": { 109 | "flash_size": "4MB", 110 | "maximum_ram_size": 327680, 111 | "maximum_size": 4194304, 112 | "require_upload_port": true, 113 | "speed": 460800 114 | }, 115 | "url": "https://www.aliexpress.com/item/1005004502250619.html", 116 | "vendor": "Sunton" 117 | } -------------------------------------------------------------------------------- /boards/esp32-2432S028Rv2.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "arduino": { 4 | "ldscript": "esp32_out.ld" 5 | }, 6 | "core": "esp32", 7 | "extra_flags": [ 8 | "'-D ARDUINO_ESP32_DEV'", 9 | "'-D ESP32_2432S028Rv2'", 10 | "'-D LCD_WIDTH=240'", 11 | "'-D LCD_HEIGHT=320'", 12 | "'-D LVGL_BUFFER_PIXELS=(LCD_WIDTH*LCD_HEIGHT/4)'", 13 | "'-D LVGL_BUFFER_MALLOC_FLAGS=(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT)'", 14 | "'-D GPIO_BCKL=21'", 15 | "'-D LCD_ILI9341_SPI'", 16 | "'-D ILI9341_SPI_HOST=SPI2_HOST'", 17 | "'-D ILI9341_SPI_DMA_CHANNEL=SPI_DMA_CH_AUTO'", 18 | "'-D ILI9341_SPI_BUS_MOSI_IO_NUM=13'", 19 | "'-D ILI9341_SPI_BUS_MISO_IO_NUM=12'", 20 | "'-D ILI9341_SPI_BUS_SCLK_IO_NUM=14'", 21 | "'-D ILI9341_SPI_BUS_QUADWP_IO_NUM=GPIO_NUM_NC'", 22 | "'-D ILI9341_SPI_BUS_QUADHD_IO_NUM=GPIO_NUM_NC'", 23 | "'-D ILI9341_SPI_BUS_MAX_TRANSFER_SZ=0'", 24 | "'-D ILI9341_SPI_BUS_FLAGS=0'", 25 | "'-D ILI9341_SPI_BUS_INTR_FLAGS=0'", 26 | "'-D ILI9341_SPI_CONFIG_CS_GPIO_NUM=15'", 27 | "'-D ILI9341_SPI_CONFIG_DC_GPIO_NUM=2'", 28 | "'-D ILI9341_SPI_CONFIG_SPI_MODE=SPI_MODE0'", 29 | "'-D ILI9341_SPI_CONFIG_PCLK_HZ=24000000'", 30 | "'-D ILI9341_SPI_CONFIG_TRANS_QUEUE_DEPTH=10'", 31 | "'-D ILI9341_SPI_CONFIG_LCD_CMD_BITS=8'", 32 | "'-D ILI9341_SPI_CONFIG_LCD_PARAM_BITS=8'", 33 | "'-D ILI9341_SPI_CONFIG_FLAGS_DC_AS_CMD_PHASE=false'", 34 | "'-D ILI9341_SPI_CONFIG_FLAGS_DC_LOW_ON_DATA=false'", 35 | "'-D ILI9341_SPI_CONFIG_FLAGS_OCTAL_MODE=false'", 36 | "'-D ILI9341_SPI_CONFIG_FLAGS_LSB_FIRST=false'", 37 | "'-D ILI9341_DEV_CONFIG_RESET_GPIO_NUM=GPIO_NUM_NC'", 38 | "'-D ILI9341_DEV_CONFIG_COLOR_SPACE=ESP_LCD_COLOR_SPACE_BGR'", 39 | "'-D ILI9341_DEV_CONFIG_BITS_PER_PIXEL=16'", 40 | "'-D ILI9341_DEV_CONFIG_FLAGS_RESET_ACTIVE_HIGH=false'", 41 | "'-D ILI9341_DEV_CONFIG_VENDOR_CONFIG=\"(ili9341_vendor_config_t[]){{.init_cmds=(ili9341_lcd_init_cmd_t[]){{.cmd=0xCF,.data=(uint8_t[]){0x00,0xC1,0x30},.data_bytes=3},{.cmd=0xED,.data=(uint8_t[]){0x64,0x03,0x12,0x81},.data_bytes=4},{.cmd=0xE8,.data=(uint8_t[]){0x85,0x00,0x78},.data_bytes=3},{.cmd=0xCB,.data=(uint8_t[]){0x39,0x2C,0x00,0x34,0x02},.data_bytes=5},{.cmd=0xF7,.data=(uint8_t[]){0x20},.data_bytes=1},{.cmd=0xEA,.data=(uint8_t[]){0x00,0x00},.data_bytes=2},{.cmd=0xC0,.data=(uint8_t[]){0x10},.data_bytes=1},{.cmd=0xC1,.data=(uint8_t[]){0x00},.data_bytes=1},{.cmd=0xC5,.data=(uint8_t[]){0x30,0x30},.data_bytes=2,},{.cmd=0xC7,.data=(uint8_t[]){0xB7},.data_bytes=1},{.cmd=0x3A,.data=(uint8_t[]){0x55},.data_bytes=1},{.cmd=0x36,.data=(uint8_t[]){0x08},.data_bytes=1},{.cmd=0xB1,.data=(uint8_t[]){0x00,0x1A},.data_bytes=2},{.cmd=0xB6,.data=(uint8_t[]){0x08,0x82,0x27},.data_bytes=3},{.cmd=0xF2,.data=(uint8_t[]){0x00},.data_bytes=1},{.cmd=0x26,.data=(uint8_t[]){0x01},.data_bytes=1},{.cmd=0xE0,.data=(uint8_t[]){0x0F,0x2A,0x28,0x08,0x0E,0x08,0x54,0xA9,0x43,0x0A,0x0F,0x00,0x00,0x00,0x00},.data_bytes=15},{.cmd=0xE1,.data=(uint8_t[]){0x00,0x15,0x17,0x07,0x11,0x06,0x2B,0x56,0x3C,0x05,0x10,0x0F,0x3F,0x3F,0x0F},.data_bytes=15},{.cmd=0x2B,.data=(uint8_t[]){0x00,0x00,0x01,0x3F},.data_bytes=4},{.cmd=0x2A,.data=(uint8_t[]){0x00,0x00,0x00,0xEF},.data_bytes=4},{.cmd=0x21},{.cmd=0x11,.delay_ms=120},{.cmd=0x29,.delay_ms=1}},.init_cmds_size=23}}\"'", 42 | "'-D LCD_SWAP_XY=false'", 43 | "'-D LCD_MIRROR_X=true'", 44 | "'-D LCD_MIRROR_Y=false'", 45 | "'-D BOARD_HAS_TOUCH'", 46 | "'-D TOUCH_XPT2046_SPI'", 47 | "'-D XPT2046_SPI_HOST=SPI3_HOST'", 48 | "'-D XPT2046_SPI_DMA_CHANNEL=SPI_DMA_CH_AUTO'", 49 | "'-D XPT2046_SPI_BUS_MOSI_IO_NUM=32'", 50 | "'-D XPT2046_SPI_BUS_MISO_IO_NUM=39'", 51 | "'-D XPT2046_SPI_BUS_SCLK_IO_NUM=25'", 52 | "'-D XPT2046_SPI_BUS_QUADWP_IO_NUM=GPIO_NUM_NC'", 53 | "'-D XPT2046_SPI_BUS_QUADHD_IO_NUM=GPIO_NUM_NC'", 54 | "'-D XPT2046_SPI_CONFIG_CS_GPIO_NUM=33'", 55 | "'-D XPT2046_SPI_CONFIG_DC_GPIO_NUM=GPIO_NUM_NC'", 56 | "'-D XPT2046_SPI_CONFIG_SPI_MODE=SPI_MODE0'", 57 | "'-D XPT2046_SPI_CONFIG_PCLK_HZ=2000000'", 58 | "'-D XPT2046_SPI_CONFIG_TRANS_QUEUE_DEPTH=3'", 59 | "'-D XPT2046_SPI_CONFIG_LCD_CMD_BITS=8'", 60 | "'-D XPT2046_SPI_CONFIG_LCD_PARAM_BITS=8'", 61 | "'-D XPT2046_SPI_CONFIG_FLAGS_DC_AS_CMD_PHASE=false'", 62 | "'-D XPT2046_SPI_CONFIG_FLAGS_DC_LOW_ON_DATA=false'", 63 | "'-D XPT2046_SPI_CONFIG_FLAGS_OCTAL_MODE=false'", 64 | "'-D XPT2046_SPI_CONFIG_FLAGS_LSB_FIRST=false'", 65 | "'-D XPT2046_TOUCH_CONFIG_X_MAX=LCD_WIDTH'", 66 | "'-D XPT2046_TOUCH_CONFIG_Y_MAX=LCD_HEIGHT'", 67 | "'-D XPT2046_TOUCH_CONFIG_RST_GPIO_NUM=GPIO_NUM_NC'", 68 | "'-D XPT2046_TOUCH_CONFIG_INT_GPIO_NUM=36'", 69 | "'-D XPT2046_TOUCH_CONFIG_LEVELS_RESET=0'", 70 | "'-D XPT2046_TOUCH_CONFIG_LEVELS_INTERRUPT=0'", 71 | "'-D TOUCH_SWAP_XY=false'", 72 | "'-D TOUCH_SWAP_X=true'", 73 | "'-D TOUCH_SWAP_Y=false'", 74 | "'-D BOARD_HAS_TF'", 75 | "'-D TF_CS=5'", 76 | "'-D TF_SPI_MOSI=23'", 77 | "'-D TF_SPI_SCLK=18'", 78 | "'-D TF_SPI_MISO=19'", 79 | "'-D BOARD_HAS_RGB_LED'", 80 | "'-D RGB_LED_R=4'", 81 | "'-D RGB_LED_G=16'", 82 | "'-D RGB_LED_B=17'", 83 | "'-D BOARD_HAS_CDS'", 84 | "'-D CDS=34'", 85 | "'-D BOARD_HAS_SPEAK'", 86 | "'-D SPEAK=26'" 87 | ], 88 | "f_cpu": "240000000L", 89 | "f_flash": "40000000L", 90 | "flash_mode": "dio", 91 | "mcu": "esp32", 92 | "variant": "esp32" 93 | }, 94 | "connectivity": [ 95 | "wifi", 96 | "bluetooth", 97 | "ethernet", 98 | "can" 99 | ], 100 | "debug": { 101 | "openocd_board": "esp-wroom-32.cfg" 102 | }, 103 | "frameworks": [ 104 | "arduino", 105 | "espidf" 106 | ], 107 | "name": "esp32-2432S028Rv2", 108 | "upload": { 109 | "flash_size": "4MB", 110 | "maximum_ram_size": 327680, 111 | "maximum_size": 4194304, 112 | "require_upload_port": true, 113 | "speed": 460800 114 | }, 115 | "url": "https://www.aliexpress.com/item/1005004502250619.html", 116 | "vendor": "Sunton" 117 | } -------------------------------------------------------------------------------- /boards/esp32-2432S028Rv3.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "arduino": { 4 | "ldscript": "esp32_out.ld" 5 | }, 6 | "core": "esp32", 7 | "extra_flags": [ 8 | "'-D ARDUINO_ESP32_DEV'", 9 | "'-D ESP32_2432S028Rv3'", 10 | "'-D LCD_WIDTH=240'", 11 | "'-D LCD_HEIGHT=320'", 12 | "'-D LVGL_BUFFER_PIXELS=(LCD_WIDTH*LCD_HEIGHT/4)'", 13 | "'-D LVGL_BUFFER_MALLOC_FLAGS=(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT)'", 14 | "'-D GPIO_BCKL=21'", 15 | "'-D LCD_ST7789_SPI'", 16 | "'-D ST7789_SPI_HOST=SPI2_HOST'", 17 | "'-D ST7789_SPI_DMA_CHANNEL=SPI_DMA_CH_AUTO'", 18 | "'-D ST7789_SPI_BUS_MOSI_IO_NUM=13'", 19 | "'-D ST7789_SPI_BUS_MISO_IO_NUM=GPIO_NUM_NC'", 20 | "'-D ST7789_SPI_BUS_SCLK_IO_NUM=14'", 21 | "'-D ST7789_SPI_BUS_QUADWP_IO_NUM=GPIO_NUM_NC'", 22 | "'-D ST7789_SPI_BUS_QUADHD_IO_NUM=GPIO_NUM_NC'", 23 | "'-D ST7789_SPI_BUS_MAX_TRANSFER_SZ=0'", 24 | "'-D ST7789_SPI_BUS_FLAGS=0'", 25 | "'-D ST7789_SPI_BUS_INTR_FLAGS=0'", 26 | "'-D ST7789_SPI_CONFIG_CS_GPIO_NUM=15'", 27 | "'-D ST7789_SPI_CONFIG_DC_GPIO_NUM=2'", 28 | "'-D ST7789_SPI_CONFIG_SPI_MODE=SPI_MODE3'", 29 | "'-D ST7789_SPI_CONFIG_PCLK_HZ=24000000'", 30 | "'-D ST7789_SPI_CONFIG_TRANS_QUEUE_DEPTH=10'", 31 | "'-D ST7789_SPI_CONFIG_LCD_CMD_BITS=8'", 32 | "'-D ST7789_SPI_CONFIG_LCD_PARAM_BITS=8'", 33 | "'-D ST7789_SPI_CONFIG_FLAGS_DC_AS_CMD_PHASE=false'", 34 | "'-D ST7789_SPI_CONFIG_FLAGS_DC_LOW_ON_DATA=false'", 35 | "'-D ST7789_SPI_CONFIG_FLAGS_OCTAL_MODE=false'", 36 | "'-D ST7789_SPI_CONFIG_FLAGS_LSB_FIRST=false'", 37 | "'-D ST7789_DEV_CONFIG_RESET_GPIO_NUM=GPIO_NUM_NC'", 38 | "'-D ST7789_DEV_CONFIG_COLOR_SPACE=ESP_LCD_COLOR_SPACE_RGB'", 39 | "'-D ST7789_DEV_CONFIG_BITS_PER_PIXEL=16'", 40 | "'-D ST7789_DEV_CONFIG_FLAGS_RESET_ACTIVE_HIGH=false'", 41 | "'-D ST7789_DEV_CONFIG_VENDOR_CONFIG=NULL'", 42 | "'-D LCD_SWAP_XY=false'", 43 | "'-D LCD_MIRROR_X=false'", 44 | "'-D LCD_MIRROR_Y=false'", 45 | "'-D BOARD_HAS_TOUCH'", 46 | "'-D TOUCH_XPT2046_SPI'", 47 | "'-D XPT2046_SPI_HOST=SPI3_HOST'", 48 | "'-D XPT2046_SPI_DMA_CHANNEL=SPI_DMA_CH_AUTO'", 49 | "'-D XPT2046_SPI_BUS_MOSI_IO_NUM=32'", 50 | "'-D XPT2046_SPI_BUS_MISO_IO_NUM=39'", 51 | "'-D XPT2046_SPI_BUS_SCLK_IO_NUM=25'", 52 | "'-D XPT2046_SPI_BUS_QUADWP_IO_NUM=GPIO_NUM_NC'", 53 | "'-D XPT2046_SPI_BUS_QUADHD_IO_NUM=GPIO_NUM_NC'", 54 | "'-D XPT2046_SPI_CONFIG_CS_GPIO_NUM=33'", 55 | "'-D XPT2046_SPI_CONFIG_DC_GPIO_NUM=GPIO_NUM_NC'", 56 | "'-D XPT2046_SPI_CONFIG_SPI_MODE=SPI_MODE0'", 57 | "'-D XPT2046_SPI_CONFIG_PCLK_HZ=2000000'", 58 | "'-D XPT2046_SPI_CONFIG_TRANS_QUEUE_DEPTH=3'", 59 | "'-D XPT2046_SPI_CONFIG_LCD_CMD_BITS=8'", 60 | "'-D XPT2046_SPI_CONFIG_LCD_PARAM_BITS=8'", 61 | "'-D XPT2046_SPI_CONFIG_FLAGS_DC_AS_CMD_PHASE=false'", 62 | "'-D XPT2046_SPI_CONFIG_FLAGS_DC_LOW_ON_DATA=false'", 63 | "'-D XPT2046_SPI_CONFIG_FLAGS_OCTAL_MODE=false'", 64 | "'-D XPT2046_SPI_CONFIG_FLAGS_LSB_FIRST=false'", 65 | "'-D XPT2046_TOUCH_CONFIG_X_MAX=LCD_WIDTH'", 66 | "'-D XPT2046_TOUCH_CONFIG_Y_MAX=LCD_HEIGHT'", 67 | "'-D XPT2046_TOUCH_CONFIG_RST_GPIO_NUM=GPIO_NUM_NC'", 68 | "'-D XPT2046_TOUCH_CONFIG_INT_GPIO_NUM=36'", 69 | "'-D XPT2046_TOUCH_CONFIG_LEVELS_RESET=0'", 70 | "'-D XPT2046_TOUCH_CONFIG_LEVELS_INTERRUPT=0'", 71 | "'-D TOUCH_SWAP_XY=false'", 72 | "'-D TOUCH_SWAP_X=true'", 73 | "'-D TOUCH_SWAP_Y=false'", 74 | "'-D BOARD_HAS_TF'", 75 | "'-D TF_CS=5'", 76 | "'-D TF_SPI_MOSI=23'", 77 | "'-D TF_SPI_SCLK=18'", 78 | "'-D TF_SPI_MISO=19'", 79 | "'-D BOARD_HAS_RGB_LED'", 80 | "'-D RGB_LED_R=4'", 81 | "'-D RGB_LED_G=16'", 82 | "'-D RGB_LED_B=17'", 83 | "'-D BOARD_HAS_CDS'", 84 | "'-D CDS=34'", 85 | "'-D BOARD_HAS_SPEAK'", 86 | "'-D SPEAK=26'" 87 | ], 88 | "f_cpu": "240000000L", 89 | "f_flash": "40000000L", 90 | "flash_mode": "dio", 91 | "mcu": "esp32", 92 | "variant": "esp32" 93 | }, 94 | "connectivity": [ 95 | "wifi", 96 | "bluetooth", 97 | "ethernet", 98 | "can" 99 | ], 100 | "debug": { 101 | "openocd_board": "esp-wroom-32.cfg" 102 | }, 103 | "frameworks": [ 104 | "arduino", 105 | "espidf" 106 | ], 107 | "name": "esp32-2432S028Rv3", 108 | "upload": { 109 | "flash_size": "4MB", 110 | "maximum_ram_size": 327680, 111 | "maximum_size": 4194304, 112 | "require_upload_port": true, 113 | "speed": 460800 114 | }, 115 | "url": "https://www.aliexpress.com/item/1005004502250619.html", 116 | "vendor": "Sunton" 117 | } -------------------------------------------------------------------------------- /boards/esp32-8048S043C.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "arduino": { 4 | "ldscript": "esp32s3_out.ld", 5 | "partitions": "default_16MB.csv", 6 | "memory_type": "qio_opi" 7 | }, 8 | "core": "esp32", 9 | "extra_flags": [ 10 | "'-D ARDUINO_ESP32S3_DEV'", 11 | "'-D BOARD_HAS_PSRAM'", 12 | "'-D ARDUINO_USB_MODE=1'", 13 | "'-D ARDUINO_RUNNING_CORE=1'", 14 | "'-D ARDUINO_EVENT_RUNNING_CORE=1'", 15 | "'-D ARDUINO_USB_CDC_ON_BOOT=0'", 16 | "'-D ESP32_8048S043C'", 17 | "'-D LCD_WIDTH=800'", 18 | "'-D LCD_HEIGHT=480'", 19 | "'-D LVGL_BUFFER_PIXELS=(LCD_WIDTH*LCD_HEIGHT)'", 20 | "'-D LVGL_BUFFER_MALLOC_FLAGS=(MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT)'", 21 | "'-D GPIO_BCKL=2'", 22 | "'-D LCD_ST7262_PAR'", 23 | "'-D ST7262_PANEL_CONFIG_CLK_SRC=LCD_CLK_SRC_PLL160M'", 24 | "'-D ST7262_PANEL_CONFIG_TIMINGS_PCLK_HZ=(8*1000000)'", 25 | "'-D ST7262_PANEL_CONFIG_TIMINGS_H_RES=LCD_WIDTH'", 26 | "'-D ST7262_PANEL_CONFIG_TIMINGS_V_RES=LCD_HEIGHT'", 27 | "'-D ST7262_PANEL_CONFIG_TIMINGS_HSYNC_PULSE_WIDTH=4'", 28 | "'-D ST7262_PANEL_CONFIG_TIMINGS_HSYNC_BACK_PORCH=8'", 29 | "'-D ST7262_PANEL_CONFIG_TIMINGS_HSYNC_FRONT_PORCH=8'", 30 | "'-D ST7262_PANEL_CONFIG_TIMINGS_VSYNC_PULSE_WIDTH=4'", 31 | "'-D ST7262_PANEL_CONFIG_TIMINGS_VSYNC_BACK_PORCH=8'", 32 | "'-D ST7262_PANEL_CONFIG_TIMINGS_VSYNC_FRONT_PORCH=8'", 33 | "'-D ST7262_PANEL_CONFIG_TIMINGS_FLAGS_HSYNC_IDLE_LOW=false'", 34 | "'-D ST7262_PANEL_CONFIG_TIMINGS_FLAGS_VSYNC_IDLE_LOW=false'", 35 | "'-D ST7262_PANEL_CONFIG_TIMINGS_FLAGS_DE_IDLE_HIGH=false'", 36 | "'-D ST7262_PANEL_CONFIG_TIMINGS_FLAGS_PCLK_ACTIVE_NEG=true'", 37 | "'-D ST7262_PANEL_CONFIG_TIMINGS_FLAGS_PCLK_IDLE_HIGH=false'", 38 | "'-D ST7262_PANEL_CONFIG_DATA_WIDTH=16'", 39 | "'-D ST7262_PANEL_CONFIG_SRAM_TRANS_ALIGN=4'", 40 | "'-D ST7262_PANEL_CONFIG_PSRAM_TRANS_ALIGN=64'", 41 | "'-D ST7262_PANEL_CONFIG_HSYNC_GPIO_NUM=39'", 42 | "'-D ST7262_PANEL_CONFIG_VSYNC_GPIO_NUM=41'", 43 | "'-D ST7262_PANEL_CONFIG_DE_GPIO_NUM=40'", 44 | "'-D ST7262_PANEL_CONFIG_PCLK_GPIO_NUM=42'", 45 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_R0=8'", 46 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_R1=3'", 47 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_R2=46'", 48 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_R3=9'", 49 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_R4=1'", 50 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_G0=5'", 51 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_G1=6'", 52 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_G2=7'", 53 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_G3=15'", 54 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_G4=16'", 55 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_G5=4'", 56 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_B0=45'", 57 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_B1=48'", 58 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_B2=47'", 59 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_B3=21'", 60 | "'-D ST7262_PANEL_CONFIG_DATA_GPIO_B4=14'", 61 | "'-D ST7262_PANEL_CONFIG_DISP_GPIO_NUM=GPIO_NUM_NC'", 62 | "'-D ST7262_PANEL_CONFIG_FLAGS_DISP_ACTIVE_LOW=false'", 63 | "'-D ST7262_PANEL_CONFIG_FLAGS_RELAX_ON_IDLE=false'", 64 | "'-D ST7262_PANEL_CONFIG_FLAGS_FB_IN_PSRAM=true'", 65 | "'-D BOARD_HAS_TOUCH'", 66 | "'-D TOUCH_GT911_I2C'", 67 | "'-D GT911_I2C_HOST=0'", 68 | "'-D GT911_I2C_CONFIG_SDA_IO_NUM=19'", 69 | "'-D GT911_I2C_CONFIG_SCL_IO_NUM=20'", 70 | "'-D GT911_I2C_CONFIG_SDA_PULLUP_EN=GPIO_PULLUP_ENABLE'", 71 | "'-D GT911_I2C_CONFIG_SCL_PULLUP_EN=GPIO_PULLUP_ENABLE'", 72 | "'-D GT911_I2C_CONFIG_MASTER_CLK_SPEED=400000'", 73 | "'-D GT911_I2C_CONFIG_CLK_FLAGS=0'", 74 | "'-D GT911_IO_I2C_CONFIG_DEV_ADDR=ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS'", 75 | "'-D GT911_IO_I2C_CONFIG_CONTROL_PHASE_BYTES=1'", 76 | "'-D GT911_IO_I2C_CONFIG_DC_BIT_OFFSET=0'", 77 | "'-D GT911_IO_I2C_CONFIG_LCD_CMD_BITS=16'", 78 | "'-D GT911_IO_I2C_CONFIG_LCD_PARAM_BITS=0'", 79 | "'-D GT911_IO_I2C_CONFIG_FLAGS_DC_LOW_ON_DATA=false'", 80 | "'-D GT911_IO_I2C_CONFIG_FLAGS_DISABLE_CONTROL_PHASE=true'", 81 | "'-D GT911_TOUCH_CONFIG_X_MAX=LCD_WIDTH'", 82 | "'-D GT911_TOUCH_CONFIG_Y_MAX=LCD_HEIGHT'", 83 | "'-D GT911_TOUCH_CONFIG_RST_GPIO_NUM=38'", 84 | "'-D GT911_TOUCH_CONFIG_INT_GPIO_NUM=18'", 85 | "'-D GT911_TOUCH_CONFIG_LEVELS_RESET=0'", 86 | "'-D GT911_TOUCH_CONFIG_LEVELS_INTERRUPT=0'", 87 | "'-D TOUCH_SWAP_XY=false'", 88 | "'-D TOUCH_SWAP_X=false'", 89 | "'-D TOUCH_SWAP_Y=false'", 90 | "'-D BOARD_HAS_TF'", 91 | "'-D TF_CS=10'", 92 | "'-D TF_SPI_MOSI=11'", 93 | "'-D TF_SPI_SCLK=12'", 94 | "'-D TF_SPI_MISO=13'" 95 | ], 96 | "f_cpu": "240000000L", 97 | "f_flash": "80000000L", 98 | "flash_mode": "qio", 99 | "hwids": [ 100 | [ 101 | "0x303A", 102 | "0x1001" 103 | ] 104 | ], 105 | "mcu": "esp32s3", 106 | "variant": "esp32s3" 107 | }, 108 | "connectivity": [ 109 | "wifi" 110 | ], 111 | "debug": { 112 | "openocd_target": "esp32s3.cfg" 113 | }, 114 | "frameworks": [ 115 | "arduino", 116 | "espidf" 117 | ], 118 | "name": "esp32-8048S043C", 119 | "upload": { 120 | "flash_size": "16MB", 121 | "maximum_ram_size": 327680, 122 | "maximum_size": 16777216, 123 | "use_1200bps_touch": true, 124 | "wait_for_upload_port": true, 125 | "require_upload_port": true, 126 | "speed": 460800 127 | }, 128 | "url": "https://www.aliexpress.com/item/1005006110360174.html", 129 | "vendor": "Sunton" 130 | } -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [platformio] 12 | 13 | ;;; esp32-2432S028R (2.8 inch resistive touch) 14 | ;default_env = particles 15 | ;default_env = paint 16 | ;default_env = sand 17 | ;default_env = sand-multi-task 18 | ;default_env = fluid-simulation 19 | 20 | ;;; 8048S043C (4.3 inch capacitive touch) 21 | default_env = sand-multi-task-4_3inch 22 | ;default_env = calibration-8048S043C 23 | 24 | src_dir = projects/${platformio.default_env} 25 | 26 | [env] 27 | framework = arduino 28 | monitor_speed = 115200 29 | 30 | [env:particles] 31 | platform = espressif32 32 | board = esp32-2432S028R 33 | lib_deps = 34 | bodmer/TFT_eSPI@^2.5.33 35 | https://github.com/PaulStoffregen/XPT2046_Touchscreen.git#v1.4 36 | build_flags = 37 | -Iinclude/ 38 | -DTFT_INVERSION_ON 39 | -DUSER_SETUP_LOADED 40 | -DILI9341_2_DRIVER 41 | -DTFT_WIDTH=240 42 | -DTFT_HEIGHT=320 43 | -DUSE_HSPI_PORT 44 | -DTFT_MISO=12 45 | -DTFT_MOSI=13 46 | -DTFT_SCLK=14 47 | -DTFT_CS=15 48 | -DTFT_DC=2 49 | -DTFT_RST=-1 50 | -DTFT_BL=21 51 | -DTFT_BACKLIGHT_ON=HIGH 52 | -DTFT_BACKLIGHT_OFF=LOW 53 | -DLOAD_GLCD 54 | -DSPI_FREQUENCY=55000000 55 | -DSPI_READ_FREQUENCY=20000000 56 | -DSPI_TOUCH_FREQUENCY=2500000 57 | -DLOAD_FONT2 58 | -DLOAD_FONT4 59 | -DLOAD_FONT6 60 | -DLOAD_FONT7 61 | -DLOAD_FONT8 62 | -DLOAD_GFXFF 63 | 64 | [env:paint] 65 | platform = espressif32 66 | board = esp32-2432S028R 67 | lib_deps = 68 | bodmer/TFT_eSPI@^2.5.33 69 | https://github.com/PaulStoffregen/XPT2046_Touchscreen.git#v1.4 70 | build_flags = 71 | -Iinclude/ 72 | -DTFT_INVERSION_ON 73 | -DUSER_SETUP_LOADED 74 | -DILI9341_2_DRIVER 75 | -DTFT_WIDTH=240 76 | -DTFT_HEIGHT=320 77 | -DUSE_HSPI_PORT 78 | -DTFT_MISO=12 79 | -DTFT_MOSI=13 80 | -DTFT_SCLK=14 81 | -DTFT_CS=15 82 | -DTFT_DC=2 83 | -DTFT_RST=-1 84 | -DTFT_BL=21 85 | -DTFT_BACKLIGHT_ON=HIGH 86 | -DTFT_BACKLIGHT_OFF=LOW 87 | -DLOAD_GLCD 88 | -DSPI_FREQUENCY=55000000 89 | -DSPI_READ_FREQUENCY=20000000 90 | -DSPI_TOUCH_FREQUENCY=2500000 91 | -DLOAD_FONT2 92 | -DLOAD_FONT4 93 | -DLOAD_FONT6 94 | -DLOAD_FONT7 95 | -DLOAD_FONT8 96 | -DLOAD_GFXFF 97 | 98 | [env:fluid-simulation] 99 | platform = espressif32@~6.3.2 100 | ; The fluid-simulation crashes on Arduino Core 2.0.10+, per the following readme: 101 | ; https://github.com/colonelwatch/ESP32-fluid-simulation/blob/0a4906ab6106901e7790403f01d6db964ebfd569/README.md 102 | ; espressif32@~6.3.2 uses Arduino Core v2.0.9 103 | ; https://github.com/platformio/platform-espressif32/releases/tag/v6.3.2 104 | board = esp32-2432S028R 105 | lib_deps = 106 | bodmer/TFT_eSPI@^2.5.33 107 | https://github.com/PaulStoffregen/XPT2046_Touchscreen.git#v1.4 108 | build_flags = 109 | -Iinclude/ 110 | -DTFT_INVERSION_ON 111 | -DUSER_SETUP_LOADED 112 | -DILI9341_2_DRIVER 113 | -DTFT_WIDTH=240 114 | -DTFT_HEIGHT=320 115 | -DUSE_HSPI_PORT 116 | -DTFT_MISO=12 117 | -DTFT_MOSI=13 118 | -DTFT_SCLK=14 119 | -DTFT_CS=15 120 | -DTFT_DC=2 121 | -DTFT_RST=-1 122 | -DTFT_BL=21 123 | -DTFT_BACKLIGHT_ON=HIGH 124 | -DTFT_BACKLIGHT_OFF=LOW 125 | -DLOAD_GLCD 126 | -DSPI_FREQUENCY=55000000 127 | -DSPI_READ_FREQUENCY=20000000 128 | -DSPI_TOUCH_FREQUENCY=2500000 129 | -DLOAD_FONT2 130 | -DLOAD_FONT4 131 | -DLOAD_FONT6 132 | -DLOAD_FONT7 133 | -DLOAD_FONT8 134 | -DLOAD_GFXFF 135 | 136 | [env:sand] 137 | platform = espressif32 138 | board = esp32-2432S028R 139 | lib_deps = 140 | bodmer/TFT_eSPI@^2.5.33 141 | https://github.com/PaulStoffregen/XPT2046_Touchscreen.git#v1.4 142 | build_flags = 143 | -Iinclude/ 144 | -DTFT_INVERSION_ON 145 | -DUSER_SETUP_LOADED 146 | -DILI9341_2_DRIVER 147 | -DTFT_WIDTH=240 148 | -DTFT_HEIGHT=320 149 | -DUSE_HSPI_PORT 150 | -DTFT_MISO=12 151 | -DTFT_MOSI=13 152 | -DTFT_SCLK=14 153 | -DTFT_CS=15 154 | -DTFT_DC=2 155 | -DTFT_RST=-1 156 | -DTFT_BL=21 157 | -DTFT_BACKLIGHT_ON=HIGH 158 | -DTFT_BACKLIGHT_OFF=LOW 159 | -DLOAD_GLCD 160 | -DSPI_FREQUENCY=55000000 161 | -DSPI_READ_FREQUENCY=20000000 162 | -DSPI_TOUCH_FREQUENCY=2500000 163 | -DLOAD_FONT2 164 | -DLOAD_FONT4 165 | -DLOAD_FONT6 166 | -DLOAD_FONT7 167 | -DLOAD_FONT8 168 | -DLOAD_GFXFF 169 | 170 | [env:sand-multi-task] 171 | platform = espressif32 172 | board = esp32-2432S028R 173 | lib_deps = 174 | bodmer/TFT_eSPI@^2.5.33 175 | https://github.com/PaulStoffregen/XPT2046_Touchscreen.git#v1.4 176 | build_flags = 177 | -Iinclude/ 178 | -DTFT_INVERSION_ON 179 | -DUSER_SETUP_LOADED 180 | -DILI9341_2_DRIVER 181 | -DTFT_WIDTH=240 182 | -DTFT_HEIGHT=320 183 | -DUSE_HSPI_PORT 184 | -DTFT_MISO=12 185 | -DTFT_MOSI=13 186 | -DTFT_SCLK=14 187 | -DTFT_CS=15 188 | -DTFT_DC=2 189 | -DTFT_RST=-1 190 | -DTFT_BL=21 191 | -DTFT_BACKLIGHT_ON=HIGH 192 | -DTFT_BACKLIGHT_OFF=LOW 193 | -DLOAD_GLCD 194 | -DSPI_FREQUENCY=55000000 195 | -DSPI_READ_FREQUENCY=20000000 196 | -DSPI_TOUCH_FREQUENCY=2500000 197 | -DLOAD_FONT2 198 | -DLOAD_FONT4 199 | -DLOAD_FONT6 200 | -DLOAD_FONT7 201 | -DLOAD_FONT8 202 | -DLOAD_GFXFF 203 | 204 | [env:sand-multi-task-4_3inch] 205 | platform = espressif32 206 | board = esp32-8048S043C 207 | uild_flags = 208 | ; Don't use lv_conf.h. Tweak params via platfom.ini. 209 | -D LV_CONF_SKIP 210 | -D LV_CONF_INCLUDE_SIMPLE 211 | -DLV_LVGL_H_INCLUDE_SIMPLE ;Simple includes for image maps 212 | lib_deps = 213 | https://github.com/lovyan03/LovyanGFX.git#1.1.12 214 | 215 | [env:calibration-8048S043C] 216 | platform = espressif32 217 | board = esp32-8048S043C 218 | build_flags = 219 | ; Don't use lv_conf.h. Tweak params via platfom.ini. 220 | -D LV_CONF_SKIP 221 | -D LV_CONF_INCLUDE_SIMPLE 222 | -DLV_LVGL_H_INCLUDE_SIMPLE ;Simple includes for image maps 223 | lib_deps = 224 | https://github.com/lovyan03/LovyanGFX.git#1.1.12 225 | -------------------------------------------------------------------------------- /projects/calibration-8048S043C/README.md: -------------------------------------------------------------------------------- 1 | # Calilbration calibration-8048S043C 2 | 3 | Code from: 4 | 5 | https://github.com/Zer0-bit/8048S043C-test 6 | -------------------------------------------------------------------------------- /projects/calibration-8048S043C/lgfx_8048S043C.h: -------------------------------------------------------------------------------- 1 | //===================================================================== 2 | // https://github.com/lovyan03/LovyanGFX/tree/master/src/lgfx/v1/platforms/esp32s3 3 | //===================================================================== 4 | #include 5 | #include 6 | #include 7 | 8 | class LGFX : public lgfx::LGFX_Device{ 9 | lgfx::Bus_RGB _bus_instance; 10 | lgfx::Panel_RGB _panel_instance; 11 | lgfx::Light_PWM _light_instance; 12 | lgfx::Touch_GT911 _touch_instance; 13 | 14 | public:LGFX(void){ 15 | auto cfg = _bus_instance.config(); 16 | cfg.panel = &_panel_instance; 17 | cfg.pin_d0 = 8; // B0 18 | cfg.pin_d1 = 3; // B1 19 | cfg.pin_d2 = 46; // B2 20 | cfg.pin_d3 = 9; // B3 21 | cfg.pin_d4 = 1; // B4 22 | cfg.pin_d5 = 5; // G0 23 | cfg.pin_d6 = 6; // G1 24 | cfg.pin_d7 = 7; // G2 25 | cfg.pin_d8 = 15; // G3 26 | cfg.pin_d9 = 16; // G4 27 | cfg.pin_d10 = 4; // G5 28 | cfg.pin_d11 = 45; // R0 29 | cfg.pin_d12 = 48; // R1 30 | cfg.pin_d13 = 47; // R2 31 | cfg.pin_d14 = 21; // R3 32 | cfg.pin_d15 = 14; // R4 33 | cfg.pin_henable = 40; 34 | cfg.pin_vsync = 41; 35 | cfg.pin_hsync = 39; 36 | cfg.pin_pclk = 42; 37 | cfg.freq_write = 14000000; 38 | cfg.hsync_polarity = 0; 39 | cfg.hsync_front_porch = 8; 40 | cfg.hsync_pulse_width = 4; 41 | cfg.hsync_back_porch = 16; 42 | cfg.vsync_polarity = 0; 43 | cfg.vsync_front_porch = 4; 44 | cfg.vsync_pulse_width = 4; 45 | cfg.vsync_back_porch = 4; 46 | cfg.pclk_idle_high = 1; 47 | _bus_instance.config(cfg); 48 | _panel_instance.setBus(&_bus_instance); 49 | 50 | { auto cfg = _panel_instance.config(); 51 | cfg.memory_width = 800; 52 | cfg.memory_height = 480; 53 | cfg.panel_width = 800; 54 | cfg.panel_height = 480; 55 | cfg.offset_x = 1000; 56 | cfg.offset_y = 2000; 57 | _panel_instance.config(cfg); 58 | } 59 | 60 | { auto cfg = _panel_instance.config_detail(); 61 | cfg.use_psram = 1; 62 | _panel_instance.config_detail(cfg); 63 | } 64 | 65 | { auto cfg = _light_instance.config(); 66 | cfg.pin_bl = 2; 67 | cfg.freq = 44100; 68 | cfg.pwm_channel = 7; 69 | _light_instance.config(cfg); 70 | } 71 | _panel_instance.light(&_light_instance); 72 | 73 | { auto cfg = _touch_instance.config(); 74 | cfg.x_min = 0; // タッチスクリーンから得られる最小のX値(生の値) 75 | cfg.x_max = 480; // タッチスクリーンから得られる最大のX値(生の値) 76 | cfg.y_min = 0; // タッチスクリーンから得られる最小のY値(生の値) 77 | cfg.y_max = 272; // タッチスクリーンから得られる最大のY値(生の値) 78 | cfg.pin_int = -1; // INTが接続されているピン番号 18 79 | cfg.bus_shared = false; // 画面と共通のバスを使用している場合 trueを設定 80 | cfg.offset_rotation = 0; // 表示とタッチの向きの調整 0~7の値で設定 81 | // I2C接続 82 | cfg.i2c_port = 1; // I2C(0 = SPI or 1 = Wire) 83 | cfg.pin_sda = 19; // SDA 84 | cfg.pin_scl = 20; // SCL 85 | cfg.pin_rst = 38; 86 | cfg.freq = 800000; // I2C 87 | cfg.i2c_addr = 0x5D; // I2C 0x5D or 0x14 88 | _touch_instance.config(cfg); 89 | _panel_instance.setTouch(&_touch_instance);//タッチスクリーンをパネルにセット 90 | } 91 | 92 | setPanel(&_panel_instance); // 使用するパネルをセットします。 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /projects/calibration-8048S043C/main.cpp: -------------------------------------------------------------------------------- 1 | #include "lgfx_8048S043C.h" 2 | 3 | LGFX display; 4 | 5 | void setup(void) 6 | { 7 | Serial.begin(115200); 8 | // 9 | display.init(); 10 | 11 | display.setTextSize((std::max(display.width(), display.height()) + 255) >> 8); 12 | 13 | // 14 | if (display.touch()) 15 | { 16 | if (display.width() < display.height()) display.setRotation(display.getRotation() ^ 1); 17 | 18 | // 19 | display.setTextDatum(textdatum_t::middle_center); 20 | display.drawString("touch the arrow marker.", display.width()>>1, display.height() >> 1); 21 | display.setTextDatum(textdatum_t::top_left); 22 | 23 | // 24 | std::uint16_t fg = TFT_WHITE; 25 | std::uint16_t bg = TFT_BLACK; 26 | if (display.isEPD()) std::swap(fg, bg); 27 | uint16_t calibrationData[8]; 28 | display.calibrateTouch(calibrationData, fg, bg, std::max(display.width(), display.height()) >> 3); 29 | 30 | for (short i=0;i<8;i++) { 31 | Serial.print('Calibration data: '); 32 | Serial.print(calibrationData[i]); 33 | Serial.print(', '); 34 | } 35 | } 36 | 37 | display.fillScreen(TFT_BLACK); 38 | } 39 | 40 | uint32_t count = ~0; 41 | void loop(void) 42 | { 43 | display.startWrite(); 44 | display.setRotation(++count & 7); 45 | display.setColorDepth((count & 8) ? 16 : 24); 46 | 47 | display.setTextColor(TFT_WHITE); 48 | display.drawNumber(display.getRotation(), 16, 0); 49 | 50 | display.setTextColor(0xFF0000U); 51 | display.drawString("R", 30, 16); 52 | display.setTextColor(0x00FF00U); 53 | display.drawString("G", 40, 16); 54 | display.setTextColor(0x0000FFU); 55 | display.drawString("B", 50, 16); 56 | 57 | display.drawRect(30,30,display.width()-60,display.height()-60,count*7); 58 | display.drawFastHLine(0, 0, 10); 59 | 60 | display.endWrite(); 61 | 62 | int32_t x, y; 63 | if (display.getTouch(&x, &y)) { 64 | display.fillRect(x-2, y-2, 5, 5, count*7); 65 | } 66 | } -------------------------------------------------------------------------------- /projects/fluid-simulation/Field.h: -------------------------------------------------------------------------------- 1 | #ifndef FIELD_H 2 | #define FIELD_H 3 | 4 | #include 5 | #include 6 | 7 | enum BoundaryCondition {DONTCARE, CLONE, NEGATIVE}; 8 | 9 | template 10 | class Field{ 11 | public: 12 | int N_i, N_j; 13 | BoundaryCondition bc; 14 | 15 | Field(int N_i, int N_j, BoundaryCondition bc); 16 | ~Field(); 17 | 18 | // Boundary exists at i = -1, i = N_i, j = -1, j = N_j 19 | T& index(int i, int j); 20 | T index(int i, int j) const; 21 | void update_boundary(); // Make sure to call this after updating values! 22 | 23 | Field& operator=(const T *rhs); 24 | Field& operator=(const Field &rhs); 25 | 26 | std::string toString(int precision = -1, bool inside_only = true) const; 27 | private: 28 | T *_arr; 29 | int _inside_elems, _total_elems; 30 | }; 31 | 32 | template 33 | Field::Field(int N_i, int N_j, BoundaryCondition bc){ 34 | this->N_i = N_i; 35 | this->N_j = N_j; 36 | this->_inside_elems = N_i*N_j; 37 | this->_total_elems = (N_i+2)*(N_j+2); 38 | this->_arr = new T[_total_elems]; 39 | this->bc = bc; 40 | } 41 | 42 | template 43 | Field::~Field(){ 44 | delete[] this->_arr; 45 | } 46 | 47 | template 48 | T& Field::index(int i, int j){ 49 | return this->_arr[1+(i+1)*(this->N_j+2)+j]; 50 | } 51 | 52 | template 53 | T Field::index(int i, int j) const{ 54 | return this->_arr[1+(i+1)*(this->N_j+2)+j]; 55 | } 56 | 57 | template 58 | void Field::update_boundary(){ 59 | if(this->bc == DONTCARE) return; 60 | else if(this->bc == CLONE){ 61 | // corners 62 | this->index(-1, -1) = this->index(0, 0); 63 | this->index(N_i, -1) = this->index(N_i-1, 0); 64 | this->index(-1, N_j) = this->index(0, N_j-1); 65 | this->index(N_i, N_j) = this->index(N_i-1, N_j-1); 66 | 67 | // top and bottom sides 68 | for(int i = 0; i < N_i; i++){ 69 | this->index(i, -1) = this->index(i, 0); 70 | this->index(i, N_j) = this->index(i, N_j-1); 71 | } 72 | for(int j = 0; j < N_j; j++){ 73 | this->index(-1, j) = this->index(0, j); 74 | this->index(N_i, j) = this->index(N_i-1, j); 75 | } 76 | } 77 | else{ // this->bc == NEGATIVE 78 | // corners (negative of a negative!) 79 | this->index(-1, -1) = this->index(0, 0); 80 | this->index(N_i, -1) = this->index(N_i-1, 0); 81 | this->index(-1, N_j) = this->index(0, N_j-1); 82 | this->index(N_i, N_j) = this->index(N_i-1, N_j-1); 83 | 84 | // top and bottom sides 85 | for(int i = 0; i < N_i; i++){ 86 | this->index(i, -1) = -this->index(i, 0); 87 | this->index(i, N_j) = -this->index(i, N_j-1); 88 | } 89 | for(int j = 0; j < N_j; j++){ 90 | this->index(-1, j) = -this->index(0, j); 91 | this->index(N_i, j) = -this->index(N_i-1, j); 92 | } 93 | } 94 | } 95 | 96 | template 97 | Field& Field::operator=(const T *rhs){ 98 | for(int i = 0; i < this->N_i; i++) 99 | for(int j = 0; j < this->N_j; j++) 100 | this->index(i, j) = rhs[i*this->N_j+j]; 101 | this->update_boundary(); 102 | return *this; 103 | } 104 | 105 | template 106 | Field& Field::operator=(const Field &rhs){ 107 | for(int i = 0; i < this->N_i; i++) 108 | for(int j = 0; j < this->N_j; j++) 109 | this->index(i, j) = rhs.index(i, j); 110 | this->update_boundary(); 111 | return *this; 112 | } 113 | 114 | template 115 | std::string Field::toString(int precision, bool inside_only) const{ 116 | std::stringstream ss; 117 | if(precision != -1){ 118 | ss << std::fixed << std::setprecision(precision); 119 | } 120 | if(inside_only){ 121 | for(int i = 0; i < N_i; i++){ 122 | for(int j = 0; j < N_j; j++){ 123 | ss << this->index(i, j); 124 | if(j != N_j-1) ss << " "; 125 | } 126 | if(i != N_i-1) ss << "\n"; 127 | } 128 | } 129 | else{ 130 | for(int i = -1; i < N_i+1; i++){ 131 | for(int j = -1; j < N_j+1; j++){ 132 | ss << this->index(i, j); 133 | if(j != N_j) ss << " "; 134 | } 135 | if(i != N_i) ss << "\n"; 136 | } 137 | } 138 | return ss.str(); 139 | } 140 | 141 | #endif -------------------------------------------------------------------------------- /projects/fluid-simulation/README.md: -------------------------------------------------------------------------------- 1 | # ESP32-fluid-simulation 2 | 3 | Project from: 4 | 5 | https://github.com/colonelwatch/ESP32-fluid-simulation/ 6 | 7 | I made the following changes: 8 | - I renamed ESP32-fluid-simulation.ino to main.cpp 9 | - In the main cpp source file, I changed: 10 | - SPIClass mySpi = SPIClass(HSPI); 11 | - to: 12 | - SPIClass mySpi = SPIClass(VSPI); 13 | - Because HSPI did not work for the board I have. 14 | - I converted this to a Platform.io project for easy dependency management. 15 | -------------------------------------------------------------------------------- /projects/fluid-simulation/Vector.h: -------------------------------------------------------------------------------- 1 | #ifndef VECTOR_H 2 | #define VECTOR_H 3 | 4 | #include 5 | 6 | template 7 | class Vector{ 8 | public: 9 | T x; 10 | T y; 11 | 12 | Vector& operator=(const Vector &rhs){ 13 | this->x = rhs.x; 14 | this->y = rhs.y; 15 | return *this; 16 | } 17 | Vector& operator+=(const Vector &rhs){ 18 | this->x += rhs.x; 19 | this->y += rhs.y; 20 | return *this; 21 | } 22 | Vector& operator-=(const Vector &rhs){ 23 | this->x -= rhs.x; 24 | this->y -= rhs.y; 25 | return *this; 26 | } 27 | Vector& operator*=(const float &rhs){ 28 | this->x *= rhs; 29 | this->y *= rhs; 30 | return *this; 31 | } 32 | Vector& operator/=(const float &rhs){ 33 | this->x /= rhs; 34 | this->y /= rhs; 35 | return *this; 36 | } 37 | 38 | Vector operator-() const{ return {-this->x, -this->y}; } 39 | Vector operator+(const Vector &rhs) const{ return {this->x+rhs.x, this->y+rhs.y}; } 40 | Vector operator-(const Vector &rhs) const{ return {this->x-rhs.x, this->y-rhs.y}; } 41 | Vector operator*(const float &rhs) const{ return {this->x*rhs, this->y*rhs}; } 42 | Vector operator/(const float &rhs) const{ return {this->x/rhs, this->y/rhs}; } 43 | }; 44 | 45 | // Scalar multiplication is commutative, so this fulfills that requirement 46 | template 47 | inline Vector operator*(const float &lhs, const Vector &rhs){ return rhs*lhs; } 48 | 49 | template 50 | std::ostream& operator<<(std::ostream &os, const Vector &rhs){ 51 | os << '(' << rhs.x << ',' << rhs.y << ')'; 52 | return os; 53 | } 54 | 55 | #endif -------------------------------------------------------------------------------- /projects/fluid-simulation/iram_float.h: -------------------------------------------------------------------------------- 1 | // iram_float_t 2 | // A float that must be stored in IRAM as an integer. The result is forcing the compiler 3 | // to use the l32i/s32i instructions (using other instructions cause a LoadStoreError). 4 | // Original issue (and assembly solution): https://github.com/espressif/esp-idf/issues/3036 5 | 6 | #ifndef IRAM_FLOAT_H 7 | #define IRAM_FLOAT_H 8 | 9 | #include 10 | 11 | class iram_float_t{ 12 | public: 13 | iram_float_t(float value = 0) 14 | : _value(*reinterpret_cast(&value)) {} 15 | 16 | #ifdef ESP32 17 | void* operator new[] (size_t size){ // Allows allocation from IRAM 18 | return heap_caps_malloc(size, MALLOC_CAP_32BIT); 19 | } 20 | #endif 21 | 22 | operator float() const { 23 | uint32_t a_raw = _value; 24 | return *reinterpret_cast(&a_raw); 25 | } 26 | private: 27 | volatile uint32_t _value; 28 | }; 29 | 30 | #endif -------------------------------------------------------------------------------- /projects/fluid-simulation/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include // WARNING: before uploading, acquire custom User_Setup.h (see monorepo) 5 | #include 6 | 7 | #include "iram_float.h" 8 | #include "Vector.h" 9 | #include "Field.h" 10 | #include "operations.h" 11 | 12 | // configurables 13 | #define N_ROWS 60 // size of sim domain 14 | #define N_COLS 80 // size of sim domain 15 | #define SCALING 4 // integer scaling of domain -> screen size is inferred from this 16 | #define TILE_HEIGHT 60 // multiple of SCALING and a factor of (N_ROWS*SCALING) 17 | #define TILE_WIDTH 80 // multiple of SCALING and a factor of (N_COLS*SCALING) 18 | #define DT 1/12.0 // s, size of time step in sim time (should roughly match real FPS) 19 | #define POLLING_PERIOD 20 // ms, for the touch screen 20 | // #define DIVERGENCE_TRACKING // if commented out, disables divergence tracking for some extra FPS 21 | 22 | 23 | // touch resources 24 | struct touch{ 25 | Vector coords; 26 | Vector velocity; 27 | }; 28 | QueueHandle_t touch_queue = xQueueCreate(10, sizeof(struct touch)); 29 | const int XPT2046_IRQ = 36; 30 | const int XPT2046_MOSI = 32; 31 | const int XPT2046_MISO = 39; 32 | const int XPT2046_CLK = 25; 33 | const int XPT2046_CS = 33; 34 | 35 | // The original project used HSPI instead of HSPI. HSPI did not work on the board I am using, but VSPI does. 36 | SPIClass ts_spi = SPIClass(VSPI); 37 | XPT2046_Touchscreen ts(XPT2046_CS, XPT2046_IRQ); 38 | 39 | // essential sim resources 40 | // TODO: allocation here causes a crash, AND runtime allocation of the 41 | // velocity field AFTER the color fields causes a crash? 42 | Field> *velocity_field; 43 | Field *red_field, *green_field, *blue_field; 44 | 45 | // draw resources 46 | SemaphoreHandle_t color_consumed = xSemaphoreCreateBinary(), // read preceded by a write, and vice versa 47 | color_produced = xSemaphoreCreateBinary(); 48 | TFT_eSPI tft = TFT_eSPI(); 49 | TFT_eSprite tiles[2] = {TFT_eSprite(&tft), TFT_eSprite(&tft)}; // we'll use the two tiles for double-buffering 50 | uint16_t *foo[2] = { // TODO: related to above todo, only used to init memory early 51 | (uint16_t*)tiles[0].createSprite(TILE_WIDTH, TILE_HEIGHT), 52 | (uint16_t*)tiles[1].createSprite(TILE_WIDTH, TILE_HEIGHT)}; 53 | const int SCREEN_HEIGHT = N_ROWS*SCALING, SCREEN_WIDTH = N_COLS*SCALING; 54 | const int N_TILES = SCREEN_HEIGHT/TILE_HEIGHT, M_TILES = SCREEN_WIDTH/TILE_WIDTH; 55 | 56 | // stats resources 57 | SemaphoreHandle_t stats_consumed = xSemaphoreCreateBinary(), 58 | stats_produced = xSemaphoreCreateBinary(); 59 | struct stats{ 60 | unsigned long point_timestamps[6]; 61 | float current_abs_pct_density; // "current" -> worst over domain at current time 62 | float max_abs_pct_density; // "max" -> worst over domain and all time 63 | int refresh_count; 64 | }; 65 | struct stats global_stats; 66 | 67 | 68 | void touch_routine(void *args){ 69 | ts.setRotation(1); // landscape rotation 70 | ts_spi.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS); 71 | ts.begin(ts_spi); 72 | 73 | Vector last_coords, current_coords; // used for calculating the velocity 74 | bool last_touched = false, touched; // used for detecting when to send a touch struct 75 | 76 | while(1){ 77 | // call ts.touched() and store it's result 78 | touched = ts.touched(); 79 | 80 | // get the touch coordinates if we're supposed to 81 | if(touched){ 82 | last_coords = current_coords; // current_coords is now history 83 | 84 | // We need to map from a 4096x4096 domain to a N_ROWSxN_COLS one 85 | TS_Point raw_coords = ts.getPoint(); 86 | current_coords = (Vector){ 87 | .x = (uint16_t)(raw_coords.x * N_COLS / 4096), 88 | .y = (uint16_t)(raw_coords.y * N_ROWS / 4096)}; 89 | } 90 | // else current_coords should never end up being used 91 | 92 | // we're supposed to send a touch struct only if we have a previous touch 93 | // to calculate velocity with, or else the velocity is undefined 94 | bool send_touch = touched && last_touched; 95 | last_touched = touched; // update memory 96 | 97 | // send the touch struct if we're supposed to 98 | if(send_touch){ 99 | // calculate and send the velocity and location 100 | Vector current_velocity = { 101 | .x = ((float)current_coords.x - (float)last_coords.x) * 1000 / POLLING_PERIOD, 102 | .y = ((float)current_coords.y - (float)last_coords.y) * 1000 / POLLING_PERIOD}; 103 | struct touch current_touch = { .coords = current_coords, .velocity = current_velocity }; 104 | xQueueSend(touch_queue, ¤t_touch, 0); // TODO: don't just use send and pray 105 | } 106 | 107 | vTaskDelay(POLLING_PERIOD / portTICK_PERIOD_MS); 108 | } 109 | } 110 | 111 | 112 | void sim_routine(void* args){ 113 | // local stats and timing the reporting of those stats 114 | unsigned long now, last_reported = millis(); 115 | struct stats local_stats = (struct stats){ .max_abs_pct_density = 0, .refresh_count = 0 }; 116 | 117 | while(1){ 118 | local_stats.point_timestamps[0] = millis(); // holds the millis() for when calculating the time step started 119 | 120 | 121 | // Swap the velocity field with the advected one 122 | Field> *to_delete_vector = velocity_field, 123 | *temp_vector_field = new Field>(N_ROWS, N_COLS, NEGATIVE); 124 | semilagrangian_advect(temp_vector_field, velocity_field, velocity_field, DT); 125 | velocity_field = temp_vector_field; 126 | delete to_delete_vector; 127 | 128 | local_stats.point_timestamps[1] = millis(); 129 | 130 | 131 | // Apply the captured drag (encoded as a sequence of touch structs) to the 132 | // velocity field 133 | // However, the yielded .coords and .velocity follows the coodinate system 134 | // defined in AdafruitGFX, which is a rename of matrix indexing. Their 135 | // "x" is j, and their "y" is i. On the other hand, the simulation 136 | // uses Cartesian indexing where x is i and y is j 137 | // 138 | // Cartesian AdafruitGFX 139 | // Λ y i.e. j ─┼───> j i.e. "x" 140 | // │ │ 141 | // ─┼───> x i.e. i V i i.e. "y" 142 | // 143 | // However, if we take the simulation domain to be *rotated about i=0, j=0* 144 | // relative to the actual domain, then the correct transform of the i and 145 | // j from the screen to the simulation is *to do nothing*. In terms of x, 146 | // "x", y, and "y" though, we swap them. 147 | struct touch current_touch; 148 | while(xQueueReceive(touch_queue, ¤t_touch, 0) == pdTRUE){ // empty the queue 149 | velocity_field->index(current_touch.coords.y, current_touch.coords.x) = { 150 | .x = current_touch.velocity.y, .y = current_touch.velocity.x}; 151 | } 152 | velocity_field->update_boundary(); // in case the dragging went near the boundary, we need to update it 153 | 154 | 155 | // Get a divergence-free projection of the velocity field 156 | // SOR: I found the spectral radius (60x80 grid, dx=dy=1, pure Neumann, 157 | // ignoring +1 and -1(!?) eigvals) to be 0.9996, therefore omega is 1.96 158 | // https://en.wikipedia.org/wiki/Successive_over-relaxation#Convergence_Rate 159 | const float sor_omega = 1.96; 160 | Field *divergence_field = new Field(N_ROWS, N_COLS, DONTCARE), 161 | *pressure_field = new Field(N_ROWS, N_COLS, CLONE); 162 | divergence(divergence_field, velocity_field); 163 | sor_pressure(pressure_field, divergence_field, 10, sor_omega); 164 | gradient_and_subtract(velocity_field, pressure_field); 165 | delete divergence_field; 166 | delete pressure_field; 167 | 168 | local_stats.point_timestamps[2] = millis(); 169 | 170 | 171 | // Wait for the color field to be read/consumed already, and time this wait 172 | xSemaphoreTake(color_consumed, portMAX_DELAY); 173 | local_stats.point_timestamps[3] = millis(); 174 | 175 | 176 | // Replace the color field with the advected one, but do so by rotating the memory used 177 | Field *temp, *temp_color_field = new Field(N_ROWS, N_COLS, CLONE); 178 | 179 | semilagrangian_advect(temp_color_field, red_field, velocity_field, DT); 180 | temp = red_field; 181 | red_field = temp_color_field; 182 | temp_color_field = temp; 183 | 184 | semilagrangian_advect(temp_color_field, green_field, velocity_field, DT); 185 | temp = green_field; 186 | green_field = temp_color_field; 187 | temp_color_field = temp; 188 | 189 | semilagrangian_advect(temp_color_field, blue_field, velocity_field, DT); 190 | temp = blue_field; 191 | blue_field = temp_color_field; 192 | temp_color_field = temp; 193 | 194 | delete temp_color_field; // drop the memory that got rotated out 195 | 196 | // Signal that the color field has been written/produced as is ready to be read/consumed 197 | xSemaphoreGive(color_produced); 198 | 199 | local_stats.point_timestamps[4] = millis(); 200 | 201 | 202 | #ifdef DIVERGENCE_TRACKING 203 | // Assuming density is constant over the domain in the current time (which 204 | // is only a correct assumption if the divergence is equal to zero for all 205 | // time because the density is obviously constant over the domain at t=0), 206 | // I'd think that the Euler equations say that the change in density over 207 | // time is equal to the divergence of the velocity field times the density 208 | // because the advection term is therefore zero. 209 | // Furthermore, I'd argue that "expected density error in pct" is equal to 210 | // the divergence times the time step. This is a thing we can track. 211 | // TODO: research this and find a source? 212 | float current_abs_divergence = 0; // "current" -> worst over domain at current time 213 | Field *new_divergence_field = new Field(N_ROWS, N_COLS, DONTCARE); // "new" divergence after projection 214 | divergence(new_divergence_field, velocity_field); 215 | for(int i = 0; i < N_ROWS; i++) 216 | for(int j = 0; j < N_COLS; j++) 217 | if(abs(new_divergence_field->index(i, j)) > current_abs_divergence) 218 | current_abs_divergence = abs(new_divergence_field->index(i, j)); 219 | local_stats.current_abs_pct_density = 100*current_abs_divergence*DT; 220 | if(local_stats.current_abs_pct_density > local_stats.max_abs_pct_density) 221 | local_stats.max_abs_pct_density = local_stats.current_abs_pct_density; 222 | delete new_divergence_field; 223 | #endif 224 | 225 | local_stats.point_timestamps[5] = millis(); 226 | 227 | 228 | // Update the global stats 229 | now = millis(); 230 | local_stats.refresh_count++; 231 | if(now - last_reported > 5000){ 232 | xSemaphoreTake(stats_consumed, portMAX_DELAY); 233 | global_stats = local_stats; 234 | xSemaphoreGive(stats_produced); 235 | 236 | // don't reset max_abs_pct_density because it's a running max 237 | last_reported = now; 238 | local_stats.refresh_count = 0; 239 | } 240 | 241 | vTaskDelay(1); // give a tick to lower-priority tasks (including the IDLE task?) 242 | } 243 | } 244 | 245 | 246 | void draw_routine(void* args){ 247 | // As mentioned earlier, the simulation operates on a rotated view of the 248 | // screen, so draw_routine needs to account for that 249 | tft.setRotation(1); // landscape rotation 250 | tft.init(); 251 | tft.fillScreen(TFT_BLACK); 252 | tft.initDMA(); 253 | 254 | // pointers to tiles to be used for double-buffering 255 | TFT_eSprite *write_tile = &tiles[0], *read_tile = &tiles[1]; 256 | 257 | while(1){ 258 | xSemaphoreTake(color_produced, portMAX_DELAY); 259 | int buffer_select = 0; 260 | 261 | tft.startWrite(); // start a single transfer for all the tiles 262 | 263 | for(int xx = 0; xx < M_TILES; xx++){ 264 | int x_start = xx*TILE_WIDTH, x_end = (xx+1)*TILE_WIDTH; 265 | for(int yy = 0; yy < N_TILES; yy++){ 266 | int y_start = yy*TILE_HEIGHT, y_end = (yy+1)*TILE_HEIGHT; 267 | 268 | int x_cell_start = x_start/SCALING, x_cell_end = x_end/SCALING; 269 | for(int x_cell = x_cell_start; x_cell < x_cell_end; x_cell++){ 270 | int y_cell_start = y_start/SCALING, y_cell_end = y_end/SCALING; 271 | for(int y_cell = y_cell_start; y_cell < y_cell_end; y_cell++){ 272 | // see above about the coordinate transform 273 | int r = red_field->index(y_cell, x_cell)*255, 274 | g = green_field->index(y_cell, x_cell)*255, 275 | b = blue_field->index(y_cell, x_cell)*255; 276 | 277 | int y_local = y_cell*SCALING-y_start, x_local = x_cell*SCALING-x_start; 278 | write_tile->fillRect(x_local, y_local, SCALING, SCALING, tft.color565(r, g, b)); 279 | } 280 | } 281 | 282 | // pushImageDMA also spin-waits until the previous transfer is done 283 | tft.pushImageDMA(x_start, y_start, TILE_WIDTH, TILE_HEIGHT, (uint16_t*)write_tile->getPointer()); 284 | 285 | TFT_eSprite *temp = write_tile; 286 | write_tile = read_tile; 287 | read_tile = temp; 288 | } 289 | } 290 | 291 | tft.endWrite(); 292 | 293 | xSemaphoreGive(color_consumed); 294 | } 295 | } 296 | 297 | 298 | void stats_routine(void* args){ 299 | struct stats local_stats; 300 | unsigned long now, last_reported = millis(), elapsed; 301 | while(1){ 302 | xSemaphoreTake(stats_produced, portMAX_DELAY); 303 | local_stats = global_stats; 304 | global_stats.refresh_count = 0; 305 | xSemaphoreGive(stats_consumed); 306 | 307 | now = millis(); 308 | elapsed = now - last_reported; 309 | last_reported = now; 310 | 311 | float refresh_rate = 1000*(float)local_stats.refresh_count/elapsed; 312 | float time_taken[5], total_time, pct_taken[5]; 313 | for(int i = 0; i < 5; i++) 314 | time_taken[i] = (local_stats.point_timestamps[i+1]-local_stats.point_timestamps[i])/1000.0; 315 | total_time = (local_stats.point_timestamps[5]-local_stats.point_timestamps[0])/1000.0; 316 | for(int i = 0; i < 5; i++) 317 | pct_taken[i] = 100*time_taken[i]/total_time; 318 | 319 | Serial.print("FPS: "); 320 | Serial.print(refresh_rate, 1); 321 | Serial.print(", "); 322 | Serial.print("Pct times: ("); 323 | for(int i = 0; i < 5; i++){ 324 | Serial.print(pct_taken[i], 1); 325 | Serial.print("%"); 326 | if(i < 4) Serial.print(", "); 327 | } 328 | Serial.print(")"); 329 | Serial.print(", "); 330 | 331 | #ifdef DIVERGENCE_TRACKING 332 | Serial.print("Err now: +/- "); 333 | Serial.print(local_stats.current_abs_pct_density, 1); 334 | Serial.print("%"); 335 | Serial.print(", "); 336 | Serial.print("Err max: +/- "); 337 | Serial.print(local_stats.max_abs_pct_density, 1); 338 | Serial.print("%"); 339 | Serial.print(", "); 340 | #endif 341 | 342 | Serial.print("Touch queue sz: "); 343 | Serial.print(uxQueueMessagesWaiting(touch_queue)); 344 | Serial.println(); 345 | } 346 | } 347 | 348 | 349 | void setup(void) { 350 | Serial.begin(115200); 351 | pinMode(0, INPUT_PULLUP); 352 | 353 | 354 | Serial.println("Initializing velocity field..."); 355 | velocity_field = new Field>(N_ROWS, N_COLS, NEGATIVE); 356 | for(int i = 0; i < N_ROWS; i++) 357 | for(int j = 0; j < N_COLS; j++) 358 | velocity_field->index(i, j) = {0, 0}; 359 | velocity_field->update_boundary(); 360 | 361 | 362 | // Init the raw fields using rules, then smooth them with the kernel for the final color fields 363 | 364 | Serial.println("Initializing color fields..."); 365 | float kernel[3][3] = {{1/16.0, 1/8.0, 1/16.0}, {1/8.0, 1/4.0, 1/8.0}, {1/16.0, 1/8.0, 1/16.0}}; 366 | red_field = new Field(N_ROWS, N_COLS, CLONE); 367 | green_field = new Field(N_ROWS, N_COLS, CLONE); 368 | blue_field = new Field(N_ROWS, N_COLS, CLONE); 369 | 370 | const int center_i = N_ROWS/2, center_j = N_COLS/2; 371 | for(int i = 0; i < N_ROWS; i++){ 372 | for(int j = 0; j < N_COLS; j++){ 373 | // From matrix indexing of the actual domain... 374 | float x = i-center_i, y = j-center_j; // ...to Cartesian indexing of the rotated domain... 375 | float x_rotated = y, y_rotated = -x; // ...to Cartesian indexing of the actual domain 376 | float angle = atan2(y_rotated, x_rotated); 377 | 378 | red_field->index(i, j) = (angle < -PI/3)? 1 : 0; 379 | green_field->index(i, j) = (angle >= -PI/3 && angle < PI/3)? 1 : 0; 380 | blue_field->index(i, j) = (angle >= PI/3)? 1 : 0; 381 | } 382 | } 383 | red_field->update_boundary(); 384 | green_field->update_boundary(); 385 | blue_field->update_boundary(); 386 | 387 | for(int i = 0; i < N_ROWS; i++){ 388 | for(int j = 0; j < N_COLS; j++){ 389 | float smoothed_red = 0, smoothed_green = 0, smoothed_blue = 0; 390 | 391 | for(int di = 0; di < 3; di++){ 392 | for(int dj = 0; dj < 3; dj++){ 393 | int ii = i+di, jj = j+dj; 394 | 395 | // extend the edge of the field by repeating the last row/column 396 | if(ii > N_ROWS-1) ii = N_ROWS-1; 397 | if(jj > N_COLS-1) jj = N_COLS-1; 398 | 399 | smoothed_red += kernel[di][dj]*red_field->index(ii, jj); 400 | smoothed_green += kernel[di][dj]*green_field->index(ii, jj); 401 | smoothed_blue += kernel[di][dj]*blue_field->index(ii, jj); 402 | } 403 | } 404 | 405 | red_field->index(i, j) = smoothed_red; 406 | green_field->index(i, j) = smoothed_green; 407 | blue_field->index(i, j) = smoothed_blue; 408 | } 409 | } 410 | red_field->update_boundary(); 411 | green_field->update_boundary(); 412 | blue_field->update_boundary(); 413 | 414 | 415 | Serial.println("Launching tasks..."); 416 | xSemaphoreGive(color_consumed); // start with a write not a read 417 | xSemaphoreGive(stats_consumed); 418 | xTaskCreate(draw_routine, "draw", 2000, NULL, configMAX_PRIORITIES-1, NULL); 419 | xTaskCreate(touch_routine, "touch", 2000, NULL, configMAX_PRIORITIES-2, NULL); 420 | xTaskCreate(sim_routine, "sim", 2000, NULL, configMAX_PRIORITIES-3, NULL); 421 | xTaskCreate(stats_routine, "stats", 2000, NULL, configMAX_PRIORITIES-4, NULL); 422 | 423 | 424 | vTaskDelete(NULL); // delete the setup-and-loop task 425 | } 426 | 427 | 428 | void loop(void) { 429 | // Not actually used 430 | } -------------------------------------------------------------------------------- /projects/fluid-simulation/operations.h: -------------------------------------------------------------------------------- 1 | #ifndef OPERATIONS_H 2 | #define OPERATIONS_H 3 | 4 | #include "Vector.h" 5 | #include "Field.h" 6 | 7 | #define FLOOR(x) ( x < 0 ? int(x)-1 : int(x) ) 8 | 9 | // The below operations assume that the input and output have the same shape 10 | // SCALAR_T and VECTOR_T are self-evident template args, but T means here that either a scalar or vector can be used 11 | 12 | template 13 | T billinear_interpolate(float di, float dj, T p11, T p12, T p21, T p22) 14 | { 15 | T x1, x2, interpolated; 16 | x1 = p11*(1-dj)+p12*dj; // interp between lower-left and upper-left 17 | x2 = p21*(1-dj)+p22*dj; // interp between lower-right and upper-right 18 | interpolated = x1*(1-di)+x2*di; // interp between left and right 19 | return interpolated; 20 | } 21 | 22 | template 23 | void semilagrangian_advect(Field *new_property, const Field *property, const Field *velocity, float dt){ 24 | int N_i = new_property->N_i, N_j = new_property->N_j; 25 | for(int i = 0; i < N_i; i++){ 26 | for(int j = 0; j < N_j; j++){ 27 | VECTOR_T displacement = dt*velocity->index(i, j); 28 | VECTOR_T source = {i-displacement.x, j-displacement.y}; 29 | 30 | // Clamp the source location within the boundaries 31 | if(source.x < -0.5f) source.x = -0.5f; 32 | if(source.x > N_i-0.5f) source.x = N_i-0.5f; 33 | if(source.y < -0.5f) source.y = -0.5f; 34 | if(source.y > N_j-0.5f) source.y = N_j-0.5f; 35 | 36 | // Get the source value with billinear interpolation 37 | int i11 = FLOOR(source.x), j11 = FLOOR(source.y), 38 | i12 = i11, j12 = j11+1, 39 | i21 = i11+1, j21 = j11, 40 | i22 = i11+1, j22 = j11+1; 41 | float di = source.x-i11, dj = source.y-j11; 42 | T p11 = property->index(i11, j11), p12 = property->index(i12, j12), 43 | p21 = property->index(i21, j21), p22 = property->index(i22, j22); 44 | T interpolated = billinear_interpolate(di, dj, p11, p12, p21, p22); 45 | new_property->index(i, j) = interpolated; 46 | } 47 | } 48 | new_property->update_boundary(); 49 | } 50 | 51 | template 52 | void divergence(Field *del_dot_velocity, const Field *velocity){ 53 | int N_i = del_dot_velocity->N_i, N_j = del_dot_velocity->N_j; 54 | 55 | for(int i = 0; i < N_i; i++){ 56 | for(int j = 0; j < N_j; j++){ 57 | SCALAR_T leftflow, rightflow, downflow, upflow; 58 | leftflow = -velocity->index(i-1, j).x; 59 | rightflow = velocity->index(i+1, j).x; 60 | downflow = -velocity->index(i, j-1).y; 61 | upflow = velocity->index(i, j+1).y; 62 | 63 | del_dot_velocity->index(i, j) = (upflow+downflow+leftflow+rightflow)/2; 64 | } 65 | } 66 | 67 | del_dot_velocity->update_boundary(); 68 | } 69 | 70 | template 71 | void sor_pressure(Field *pressure, const Field *divergence, int iterations, float omega){ 72 | int N_i = pressure->N_i, N_j = pressure->N_j; 73 | 74 | for(int i = 0; i < N_i; i++) 75 | for(int j = 0; j < N_j; j++) 76 | pressure->index(i, j) = 0; 77 | 78 | pressure->update_boundary(); 79 | 80 | for(int k = 0; k < iterations; k++){ 81 | for(int i = 0; i < N_i; i++){ 82 | for(int j = 0; j < N_j; j++){ 83 | SCALAR_T div = divergence->index(i, j); 84 | SCALAR_T left, right, down, up; 85 | left = pressure->index(i-1, j); 86 | right = pressure->index(i+1, j); 87 | down = pressure->index(i, j-1); 88 | up = pressure->index(i, j+1); 89 | 90 | pressure->index(i, j) = (1-omega)*pressure->index(i, j) + omega*(div-left-right-down-up)/(-4); 91 | } 92 | } 93 | 94 | pressure->update_boundary(); 95 | } 96 | } 97 | 98 | template 99 | void gradient_and_subtract(Field *velocity, const Field *pressure){ 100 | int N_i = velocity->N_i, N_j = velocity->N_j; 101 | 102 | for(int i = 0; i < N_i; i++){ 103 | for(int j = 0; j < N_j; j++){ 104 | SCALAR_T left, right, down, up; 105 | left = pressure->index(i-1, j); 106 | right = pressure->index(i+1, j); 107 | down = pressure->index(i, j-1); 108 | up = pressure->index(i, j+1); 109 | 110 | velocity->index(i, j).x -= (right-left)/2; 111 | velocity->index(i, j).y -= (up-down)/2; 112 | } 113 | } 114 | 115 | velocity->update_boundary(); 116 | } 117 | 118 | #endif -------------------------------------------------------------------------------- /projects/paint/README.md: -------------------------------------------------------------------------------- 1 | # Paint 2 | 3 | A project I made to "spray paint" on the touch point of the touchscreen. New spray paint color cycles over time. -------------------------------------------------------------------------------- /projects/paint/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #define XPT2046_IRQ 36 7 | #define XPT2046_MOSI 32 8 | #define XPT2046_MISO 39 9 | #define XPT2046_CLK 25 10 | #define XPT2046_CS 33 11 | 12 | // The following min/max axis values were gathered through empirical data gathering on the specific board I have. 13 | #define TS_MINX 280 14 | #define TS_MAXX 3750 15 | #define TS_MINY 280 16 | #define TS_MAXY 3750 17 | 18 | // Params you can easily play with 19 | unsigned long colorChangeIntervalMs = 250; 20 | uint8_t percentInputFill = 15; 21 | uint8_t inputWidth = 8; 22 | int8_t drawPixelWidth = 2; 23 | 24 | static const int16_t NATIVE_ROWS = 240; 25 | static const int16_t NATIVE_COLS = 320; 26 | 27 | static const int16_t SCALED_ROWS = NATIVE_ROWS / drawPixelWidth; 28 | static const int16_t SCALED_COLS = NATIVE_COLS / drawPixelWidth; 29 | 30 | int16_t BACKGROUND_COLOR = TFT_BLACK; 31 | 32 | SPIClass mySpi = SPIClass(VSPI); 33 | XPT2046_Touchscreen ts(XPT2046_CS, XPT2046_IRQ); 34 | TFT_eSPI tft = TFT_eSPI(); 35 | 36 | byte red = 31; 37 | byte green = 0; 38 | byte blue = 0; 39 | byte colorState = 0; 40 | uint16_t color = red << 11; 41 | unsigned long colorChangeTime = 0; 42 | 43 | int16_t inputX = -1; 44 | int16_t inputY = -1; 45 | 46 | bool withinScaledCols(uint16_t value) 47 | { 48 | return value >= 0 && value <= SCALED_COLS - 1; 49 | } 50 | 51 | bool withinScaledRows(uint16_t value) 52 | { 53 | return value >= 0 && value <= SCALED_ROWS - 1; 54 | } 55 | 56 | // Color changing state machine 57 | void setNextColor() 58 | { 59 | switch (colorState) 60 | { 61 | case 0: 62 | green += 2; 63 | if (green == 64) 64 | { 65 | green = 63; 66 | colorState = 1; 67 | } 68 | break; 69 | case 1: 70 | red--; 71 | if (red == 255) 72 | { 73 | red = 0; 74 | colorState = 2; 75 | } 76 | break; 77 | case 2: 78 | blue++; 79 | if (blue == 32) 80 | { 81 | blue = 31; 82 | colorState = 3; 83 | } 84 | break; 85 | case 3: 86 | green -= 2; 87 | if (green == 255) 88 | { 89 | green = 0; 90 | colorState = 4; 91 | } 92 | break; 93 | case 4: 94 | red++; 95 | if (red == 32) 96 | { 97 | red = 31; 98 | colorState = 5; 99 | } 100 | break; 101 | case 5: 102 | blue--; 103 | if (blue == 255) 104 | { 105 | blue = 0; 106 | colorState = 0; 107 | } 108 | break; 109 | } 110 | 111 | color = red << 11 | green << 5 | blue; 112 | 113 | if (color == BACKGROUND_COLOR) 114 | color++; 115 | } 116 | 117 | void drawScaledPixel(uint16_t x, uint16_t y, uint16_t color) 118 | { 119 | if (!withinScaledCols(x) || !withinScaledRows(y)) 120 | return; 121 | 122 | uint16_t scaledXCol = x * drawPixelWidth; 123 | uint16_t scaledYRow = y * drawPixelWidth; 124 | 125 | // TODO: The below is hard-coded for a drawPixelWidth of 4. Make dynaminc. 126 | tft.drawPixel(scaledXCol, scaledYRow, color); 127 | tft.drawPixel(scaledXCol, scaledYRow + 1, color); 128 | tft.drawPixel(scaledXCol, scaledYRow + 2, color); 129 | tft.drawPixel(scaledXCol, scaledYRow + 3, color); 130 | tft.drawPixel(scaledXCol + 1, scaledYRow, color); 131 | tft.drawPixel(scaledXCol + 1, scaledYRow + 1, color); 132 | tft.drawPixel(scaledXCol + 1, scaledYRow + 2, color); 133 | tft.drawPixel(scaledXCol + 1, scaledYRow + 3, color); 134 | tft.drawPixel(scaledXCol + 2, scaledYRow, color); 135 | tft.drawPixel(scaledXCol + 2, scaledYRow + 1, color); 136 | tft.drawPixel(scaledXCol + 2, scaledYRow + 2, color); 137 | tft.drawPixel(scaledXCol + 2, scaledYRow + 3, color); 138 | tft.drawPixel(scaledXCol + 3, scaledYRow, color); 139 | tft.drawPixel(scaledXCol + 3, scaledYRow + 1, color); 140 | tft.drawPixel(scaledXCol + 3, scaledYRow + 2, color); 141 | tft.drawPixel(scaledXCol + 3, scaledYRow + 3, color); 142 | } 143 | 144 | // Maximum frames per second. 145 | unsigned long maxFps = 30; 146 | long lastMillis = 0; 147 | 148 | void setup() 149 | { 150 | Serial.begin(115200); 151 | Serial.println("Hello, starting..."); 152 | 153 | // Start the SPI for the touch screen and init the TS library 154 | Serial.println("Init display..."); 155 | mySpi.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS); 156 | ts.begin(mySpi); 157 | ts.setRotation(1); 158 | 159 | // Start the tft display and set it to black 160 | tft.init(); 161 | tft.setRotation(1); 162 | tft.fillScreen(BACKGROUND_COLOR); 163 | 164 | colorChangeTime = millis() + 1000; 165 | 166 | delay(500); 167 | } 168 | 169 | void loop() 170 | { 171 | unsigned long currentMillis = millis(); 172 | 173 | // Throttle FPS 174 | unsigned long diffMillis = currentMillis - lastMillis; 175 | if ((1000 / maxFps) > diffMillis) 176 | { 177 | return; 178 | } 179 | 180 | lastMillis = currentMillis; 181 | 182 | // Handle touch. 183 | if (ts.tirqTouched() && ts.touched()) 184 | { 185 | TS_Point p = ts.getPoint(); 186 | 187 | inputX = map(p.x, TS_MINX, TS_MAXX, 0, SCALED_COLS); 188 | inputY = map(p.y, TS_MINY, TS_MAXY, 0, SCALED_ROWS); // Needs flipping this axis for some reason. 189 | // tft.drawCircle(inputY, inputX, 6, TFT_SKYBLUE); 190 | } 191 | else 192 | { 193 | inputX = -1; 194 | inputY = -1; 195 | } 196 | 197 | if (!withinScaledCols(inputX) || !withinScaledRows(inputY)) 198 | return; 199 | 200 | // Randomly add an area of pixels, centered on the input point. 201 | int16_t halfInputWidth = inputWidth / 2; 202 | for (int16_t i = -halfInputWidth; i <= halfInputWidth; ++i) 203 | { 204 | for (int16_t j = -halfInputWidth; j <= halfInputWidth; ++j) 205 | { 206 | if (random(100) < percentInputFill) 207 | { 208 | uint16_t xCol = inputX + i; 209 | uint16_t yRow = inputY + j; 210 | 211 | drawScaledPixel(xCol, yRow, color); 212 | } 213 | } 214 | } 215 | 216 | // Change the color of the pixels over time 217 | if (colorChangeTime < millis()) 218 | { 219 | colorChangeTime = millis() + colorChangeIntervalMs; 220 | setNextColor(); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /projects/particles/README.md: -------------------------------------------------------------------------------- 1 | # Particles Example 2 | 3 | The particles example was "manually forked" from the following GitHub repo: 4 | https://github.com/doctorpartlow/esp32s3lilygoanimations/blob/main/particles.ino 5 | 6 | I only applied a couple minor bug fixes, optimizations, and formatting. 7 | 8 | In addition, for running on the CYD, I added touchscreen input support to move the attractor. 9 | 10 | Here is a Youtube video posted by the original author of the their "7000 non normalized particles on the esp32-s3 t-display from lilygo" code: 11 | 12 | [![7000 non normalized particles on the esp32-s3 t-display from lilygo](https://img.youtube.com/vi/vLZECfKSX04/0.jpg)](https://www.youtube.com/watch?v=vLZECfKSX04) 13 | -------------------------------------------------------------------------------- /projects/particles/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define XPT2046_IRQ 36 8 | #define XPT2046_MOSI 32 9 | #define XPT2046_MISO 39 10 | #define XPT2046_CLK 25 11 | #define XPT2046_CS 33 12 | 13 | // The following min/max axis values were gathered through empirical data gathering on the specific board I have. 14 | #define TS_MINX 280 15 | #define TS_MAXX 3750 16 | #define TS_MINY 280 17 | #define TS_MAXY 3750 18 | 19 | // Maximum frames per second. 20 | unsigned long maxFps = 30; 21 | 22 | char fpsStringBuffer[16]; 23 | unsigned long lastMillis = 0; 24 | int fps = 0; 25 | 26 | static const uint16_t NUM_PARTICLES = 2000; 27 | // static const float PARTICLE_MASS = 3; 28 | static const float PARTICLE_MASS = 5; 29 | // static const float GRAVITY = 0.5; 30 | static const float GRAVITY = 2; 31 | static const uint16_t ROWS = 240; 32 | static const uint16_t COLS = 320; 33 | 34 | float inputX = COLS / 2; 35 | float inputY = ROWS / 2; 36 | 37 | SPIClass mySpi = SPIClass(VSPI); 38 | XPT2046_Touchscreen ts(XPT2046_CS, XPT2046_IRQ); 39 | 40 | TFT_eSPI tft = TFT_eSPI(); 41 | boolean loadingflag = true; 42 | 43 | template 44 | class vec2 45 | { 46 | public: 47 | T x, y; 48 | 49 | vec2() : x(0), y(0) {} 50 | vec2(T x, T y) : x(x), y(y) {} 51 | vec2(const vec2 &v) : x(v.x), y(v.y) {} 52 | 53 | vec2 &operator=(const vec2 &v) 54 | { 55 | x = v.x; 56 | y = v.y; 57 | return *this; 58 | } 59 | 60 | bool operator==(vec2 &v) 61 | { 62 | return x == v.x && y == v.y; 63 | } 64 | 65 | bool operator!=(vec2 &v) 66 | { 67 | return !(x == y); 68 | } 69 | 70 | vec2 operator+(vec2 &v) 71 | { 72 | return vec2(x + v.x, y + v.y); 73 | } 74 | 75 | vec2 operator-(vec2 &v) 76 | { 77 | return vec2(x - v.x, y - v.y); 78 | } 79 | 80 | vec2 &operator+=(vec2 &v) 81 | { 82 | x += v.x; 83 | y += v.y; 84 | return *this; 85 | } 86 | 87 | vec2 &operator-=(vec2 &v) 88 | { 89 | x -= v.x; 90 | y -= v.y; 91 | return *this; 92 | } 93 | 94 | vec2 operator+(double s) 95 | { 96 | return vec2(x + s, y + s); 97 | } 98 | 99 | vec2 operator-(double s) 100 | { 101 | return vec2(x - s, y - s); 102 | } 103 | 104 | vec2 operator*(double s) 105 | { 106 | return vec2(x * s, y * s); 107 | } 108 | 109 | vec2 operator/(double s) 110 | { 111 | return vec2(x / s, y / s); 112 | } 113 | 114 | vec2 &operator+=(double s) 115 | { 116 | x += s; 117 | y += s; 118 | return *this; 119 | } 120 | 121 | vec2 &operator-=(double s) 122 | { 123 | x -= s; 124 | y -= s; 125 | return *this; 126 | } 127 | 128 | vec2 &operator*=(double s) 129 | { 130 | x *= s; 131 | y *= s; 132 | return *this; 133 | } 134 | 135 | vec2 &operator/=(double s) 136 | { 137 | x /= s; 138 | y /= s; 139 | return *this; 140 | } 141 | 142 | void set(T x, T y) 143 | { 144 | this->x = x; 145 | this->y = y; 146 | } 147 | 148 | void rotate(double deg) 149 | { 150 | double theta = deg / 180.0 * M_PI; 151 | double c = cos(theta); 152 | double s = sin(theta); 153 | double tx = x * c - y * s; 154 | double ty = x * s + y * c; 155 | x = tx; 156 | y = ty; 157 | } 158 | 159 | vec2 &normalize() 160 | { 161 | if (length() == 0) 162 | return *this; 163 | *this *= (1.0 / length()); 164 | return *this; 165 | } 166 | 167 | float dist(vec2 v) const 168 | { 169 | // vec2 d(v.x - x, v.y - y); 170 | // return d.length(); 171 | return sqrt(v.x * x + v.y * y); 172 | } 173 | 174 | float length() const 175 | { 176 | return sqrt(x * x + y * y); 177 | } 178 | 179 | void truncate(double length) 180 | { 181 | double angle = atan2f(y, x); 182 | x = length * cos(angle); 183 | y = length * sin(angle); 184 | } 185 | 186 | vec2 ortho() const 187 | { 188 | return vec2(y, -x); 189 | } 190 | 191 | static float dot(vec2 v1, vec2 v2) 192 | { 193 | return v1.x * v2.x + v1.y * v2.y; 194 | } 195 | 196 | static float cross(vec2 v1, vec2 v2) 197 | { 198 | return (v1.x * v2.y) - (v1.y * v2.x); 199 | } 200 | 201 | float mag() const 202 | { 203 | return length(); 204 | } 205 | 206 | float magSq() 207 | { 208 | return (x * x + y * y); 209 | } 210 | 211 | void limit(float max) 212 | { 213 | if (magSq() > max * max) 214 | { 215 | normalize(); 216 | *this *= max; 217 | } 218 | } 219 | }; 220 | 221 | typedef vec2 PVector; 222 | typedef vec2 vec2d; 223 | 224 | class Boid 225 | { 226 | public: 227 | PVector location; 228 | PVector velocity; 229 | PVector acceleration; 230 | int hue; 231 | 232 | float mass; 233 | boolean enabled = true; 234 | 235 | Boid() {} 236 | 237 | Boid(float x, float y) 238 | { 239 | acceleration = PVector(0, 0); 240 | velocity = PVector(randomf(), randomf()); 241 | location = PVector(x, y); 242 | hue = random(0x0A0A0A0A, 0xFFFFFFFF); 243 | } 244 | 245 | static float randomf() 246 | { 247 | return mapfloat(random(0, 255), 0, 255, -.5, .5); 248 | } 249 | 250 | static float mapfloat(float x, float in_min, float in_max, float out_min, float out_max) 251 | { 252 | return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; 253 | } 254 | 255 | void run(Boid boids[], uint8_t boidCount) 256 | { 257 | update(); 258 | } 259 | 260 | // Method to update location 261 | void update() 262 | { 263 | velocity += acceleration; 264 | location += velocity; 265 | acceleration *= 0; 266 | } 267 | 268 | void applyForce(PVector force) 269 | { 270 | // We could add mass here if we want A = F / M 271 | acceleration += force; 272 | } 273 | }; 274 | 275 | class Attractor 276 | { 277 | public: 278 | float mass; 279 | float G; 280 | PVector location; 281 | 282 | Attractor() 283 | { 284 | resetAttractor(); 285 | } 286 | 287 | void resetAttractor() 288 | { 289 | location = PVector(inputX, inputY); 290 | mass = PARTICLE_MASS; 291 | G = GRAVITY; 292 | } 293 | 294 | PVector attract(Boid m) 295 | { 296 | PVector force = location - m.location; // Calculate direction of force 297 | float d = force.mag(); // Distance between objects 298 | d = constrain(d, 6.0, 9.0); // Limiting the distance to eliminate "extreme" results for very close or very far objects 299 | force.normalize(); 300 | float strength = (G * mass * 2) / (d * d); // Calculate gravitional force magnitude 301 | force *= strength; // Get force vector --> magnitude * direction 302 | return force; 303 | } 304 | }; 305 | 306 | Attractor attractor; 307 | bool loadingFlag = true; 308 | 309 | Boid boidss[NUM_PARTICLES]; // this makes the boids 310 | uint16_t x; 311 | uint16_t y; 312 | uint16_t huecounter = 1; 313 | 314 | void start() 315 | { 316 | int direction = random(0, 2); 317 | if (direction == 0) 318 | direction = -1; 319 | 320 | uint16_t ROW_CENTER = ROWS / 2; 321 | uint16_t COL_CENTER = COLS / 2; 322 | 323 | for (int i = 0; i < NUM_PARTICLES; i++) 324 | { 325 | Boid boid = Boid(random(1, COLS - 1), random(1, ROWS - 1)); // Full screen 326 | // Boid boid = Boid(random(COL_CENTER - ROW_CENTER, COL_CENTER + ROW_CENTER), random(1, ROWS - 1)); // Square in middle 327 | boid.velocity.x = ((float)random(40, 50)) / 14.0; 328 | boid.velocity.x *= direction; 329 | boid.velocity.y = ((float)random(40, 50)) / 14.0; 330 | boid.velocity.y *= direction; 331 | boid.hue = huecounter; 332 | huecounter += 0xFABCDE; 333 | boidss[i] = boid; 334 | } 335 | } 336 | 337 | void printTouchToSerial(TS_Point p) 338 | { 339 | Serial.print("Pressure = "); 340 | Serial.print(p.z); 341 | Serial.print(", x = "); 342 | Serial.print(p.x); 343 | Serial.print(", y = "); 344 | Serial.print(p.y); 345 | Serial.println(); 346 | } 347 | 348 | void printTouchToDisplay(TS_Point p) 349 | { 350 | // Clear screen first 351 | tft.fillScreen(TFT_BLACK); 352 | tft.setTextColor(TFT_WHITE, TFT_BLACK); 353 | 354 | int x = 320 / 2; // center of display 355 | int y = 100; 356 | int fontSize = 2; 357 | 358 | String temp = "Pressure = " + String(p.z); 359 | tft.drawCentreString(temp, x, y, fontSize); 360 | 361 | y += 16; 362 | temp = "X = " + String(p.x); 363 | tft.drawCentreString(temp, x, y, fontSize); 364 | 365 | y += 16; 366 | temp = "Y = " + String(p.y); 367 | tft.drawCentreString(temp, x, y, fontSize); 368 | } 369 | 370 | void printConvertedTouchToDisplay(float inX, float inY, int16_t z) 371 | { 372 | // Clear screen first 373 | tft.fillScreen(TFT_BLACK); 374 | tft.setTextColor(TFT_WHITE, TFT_BLACK); 375 | int fontSize = 2; 376 | 377 | int x = 320 / 2; // center of display 378 | int y = 100; 379 | 380 | String temp = "Pressure = " + String(z); 381 | tft.drawCentreString(temp, x, y, fontSize); 382 | 383 | y += 16; 384 | temp = "X = " + String(inX); 385 | tft.drawCentreString(temp, x, y, fontSize); 386 | 387 | y += 16; 388 | temp = "Y = " + String(inY); 389 | tft.drawCentreString(temp, x, y, fontSize); 390 | } 391 | 392 | void setup() 393 | { 394 | // Start the SPI for the touch screen and init the TS library 395 | mySpi.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS); 396 | ts.begin(mySpi); 397 | ts.setRotation(1); 398 | 399 | // Start the tft display and set it to black 400 | tft.init(); 401 | tft.fillScreen(TFT_BLACK); 402 | 403 | lastMillis = millis(); 404 | 405 | delay(2000); 406 | } 407 | 408 | void loop() 409 | { 410 | if (loadingFlag) 411 | { 412 | loadingFlag = false; 413 | start(); 414 | } 415 | 416 | unsigned long currentMillis = millis(); 417 | 418 | // Get frame rate. 419 | fps = 1000 / max(currentMillis - lastMillis, (unsigned long)1); 420 | sprintf(fpsStringBuffer, "fps: %lu", fps); 421 | 422 | // Display frame rate 423 | tft.fillRect(1, 1, 55, 10, TFT_BLACK); 424 | tft.setTextColor(TFT_WHITE, TFT_BLACK); 425 | tft.drawString(fpsStringBuffer, 1, 1); 426 | 427 | // Throttle FPS 428 | unsigned long diffMillis = currentMillis - lastMillis; 429 | if ((1000 / maxFps) > diffMillis) 430 | { 431 | return; 432 | } 433 | 434 | lastMillis = currentMillis; 435 | 436 | // Handle touch. 437 | if (ts.tirqTouched() && ts.touched()) 438 | { 439 | TS_Point p = ts.getPoint(); 440 | 441 | inputX = map(p.x, TS_MINX, TS_MAXX, 0, COLS); 442 | inputY = std::abs(ROWS - map(p.y, TS_MINY, TS_MAXY, 0, ROWS)); // Needs flipping this axis for some reason. 443 | 444 | tft.drawCircle(inputY, inputX, 6, TFT_SKYBLUE); 445 | 446 | attractor.resetAttractor(); 447 | } 448 | 449 | // Move particles. 450 | for (int i = 0; i < NUM_PARTICLES; i++) 451 | { 452 | Boid boid = boidss[i]; 453 | tft.drawPixel(boid.location.y, boid.location.x, 0x0000); 454 | PVector force = attractor.attract(boid); 455 | boid.applyForce(force); 456 | boid.update(); 457 | tft.drawPixel(boid.location.y, boid.location.x, boid.hue); 458 | boidss[i] = boid; 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /projects/sand-multi-task-4_3inch/PixelState.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static const uint8_t GRID_STATE_NEW = 1; 4 | static const uint8_t GRID_STATE_FALLING = 2; 5 | static const uint8_t GRID_STATE_LANDED = 3; 6 | 7 | struct PointState 8 | { 9 | uint32_t XCol; 10 | uint32_t YRow; 11 | uint8_t State; 12 | uint8_t ColorState; 13 | uint8_t Velocity; 14 | uint8_t Shape; 15 | 16 | uint8_t RgbValues[3]; 17 | 18 | PointState() {} 19 | 20 | PointState(uint32_t x, uint32_t y, uint8_t state, uint8_t colorState, uint8_t velocity, uint8_t shape) 21 | { 22 | XCol = x; 23 | YRow = y; 24 | State = state; 25 | ColorState = colorState; 26 | Velocity = velocity; 27 | Shape = shape; 28 | } 29 | }; 30 | 31 | struct Point 32 | { 33 | uint32_t XCol; 34 | uint32_t YRow; 35 | 36 | Point() {} 37 | 38 | Point(uint32_t x, uint32_t y) 39 | { 40 | XCol = x; 41 | YRow = y; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /projects/sand-multi-task-4_3inch/README.md: -------------------------------------------------------------------------------- 1 | # Sand (Multi-Task) 4.3 Inch 2 | 3 | Modified from the prior [Sand (Multi-Task) project](../sand-multi-task). This code targets the 4.3 inch display, with capacitive touch, version of the CYD - the ESP32-8048S043C. 4 | 5 | I also messed around a little with this version and added an option to drop a random seleciton of a few shapes supported by the [LovyanGFX](https://github.com/lovyan03/LovyanGFX) graphics library. I also added a routine that color cycles the fallen pixels over time. 6 | 7 | ### Description from the prior [Sand (Multi-Task) Project](../sand-multi-task) 8 | 9 | I created this project as a copy of my [Sand project](../sand) initially and then added multi-tasking to take advantage of the dual-core CPU in the ESP32. This was my first time trying out parallelization programming on an MCU and with FreeRTOS. 10 | 11 | The performance was indeed improved, but not by quite as much as I was hoping. I haven't done any performance analysis, but I suspect there is some overhead with the constant semaphore context switching. Still, a win is a win. 12 | 13 | You can see a Youtube video of the code in action here: 14 | 15 | [![Sand Simulation on the Cheap Yellow Display (CYD) ESP32 MCU + Display](https://img.youtube.com/vi/j8XRMEEZ0gM/0.jpg)](https://www.youtube.com/watch?v=j8XRMEEZ0gM) 16 | -------------------------------------------------------------------------------- /projects/sand-multi-task-4_3inch/colorChangeRoutine.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Arrays for sine fade technique array color cycling, as seen from: 4 | // https://arduino.stackexchange.com/questions/35734/better-cycling-through-the-rgb-colors 5 | 6 | const uint8_t sins1[360] = { 7 | 127, 129, 131, 134, 136, 138, 140, 143, 145, 147, 149, 151, 154, 156, 158, 160, 162, 164, 166, 169, 171, 173, 175, 177, 179, 181, 183, 185, 187, 189, 191, 193, 195, 196, 198, 200, 8 | 202, 204, 205, 207, 209, 211, 212, 214, 216, 217, 219, 220, 222, 223, 225, 226, 227, 229, 230, 231, 233, 234, 235, 236, 237, 239, 240, 241, 242, 243, 243, 244, 245, 246, 247, 248, 9 | 248, 249, 250, 250, 251, 251, 252, 252, 253, 253, 253, 254, 254, 254, 254, 254, 254, 254, 255, 254, 254, 254, 254, 254, 254, 254, 253, 253, 253, 252, 252, 251, 251, 250, 250, 249, 10 | 248, 248, 247, 246, 245, 244, 243, 243, 242, 241, 240, 239, 237, 236, 235, 234, 233, 231, 230, 229, 227, 226, 225, 223, 222, 220, 219, 217, 216, 214, 212, 211, 209, 207, 205, 204, 11 | 202, 200, 198, 196, 195, 193, 191, 189, 187, 185, 183, 181, 179, 177, 175, 173, 171, 169, 166, 164, 162, 160, 158, 156, 154, 151, 149, 147, 145, 143, 140, 138, 136, 134, 131, 129, 12 | 127, 125, 123, 120, 118, 116, 114, 111, 109, 107, 105, 103, 100, 98, 96, 94, 92, 90, 88, 85, 83, 81, 79, 77, 75, 73, 71, 69, 67, 65, 63, 61, 59, 58, 56, 54, 13 | 52, 50, 49, 47, 45, 43, 42, 40, 38, 37, 35, 34, 32, 31, 29, 28, 27, 25, 24, 23, 21, 20, 19, 18, 17, 15, 14, 13, 12, 11, 11, 10, 9, 8, 7, 6, 14 | 6, 5, 4, 4, 3, 3, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 15 | 6, 6, 7, 8, 9, 10, 11, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 23, 24, 25, 27, 28, 29, 31, 32, 34, 35, 37, 38, 40, 42, 43, 45, 47, 49, 50, 16 | 52, 54, 56, 58, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 88, 90, 92, 94, 96, 98, 100, 103, 105, 107, 109, 111, 114, 116, 118, 120, 123, 125}; 17 | 18 | const uint8_t sins2[360] = { 19 | 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 15, 17, 18, 20, 22, 24, 26, 28, 30, 32, 35, 37, 39, 20 | 42, 44, 47, 49, 52, 55, 58, 60, 63, 66, 69, 72, 75, 78, 81, 85, 88, 91, 94, 97, 101, 104, 107, 111, 114, 117, 121, 124, 127, 131, 134, 137, 21 | 141, 144, 147, 150, 154, 157, 160, 163, 167, 170, 173, 176, 179, 182, 185, 188, 191, 194, 197, 200, 202, 205, 208, 210, 213, 215, 217, 220, 222, 224, 226, 229, 22 | 231, 232, 234, 236, 238, 239, 241, 242, 244, 245, 246, 248, 249, 250, 251, 251, 252, 253, 253, 254, 254, 255, 255, 255, 255, 255, 255, 255, 254, 254, 253, 253, 23 | 252, 251, 251, 250, 249, 248, 246, 245, 244, 242, 241, 239, 238, 236, 234, 232, 231, 229, 226, 224, 222, 220, 217, 215, 213, 210, 208, 205, 202, 200, 197, 194, 24 | 191, 188, 185, 182, 179, 176, 173, 170, 167, 163, 160, 157, 154, 150, 147, 144, 141, 137, 134, 131, 127, 124, 121, 117, 114, 111, 107, 104, 101, 97, 94, 91, 25 | 88, 85, 81, 78, 75, 72, 69, 66, 63, 60, 58, 55, 52, 49, 47, 44, 42, 39, 37, 35, 32, 30, 28, 26, 24, 22, 20, 18, 17, 15, 13, 12, 26 | 11, 9, 8, 7, 6, 5, 4, 3, 2, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; 30 | 31 | void setNextColor_sin1(uint8_t *rgbValues, uint8_t &kValue) 32 | { 33 | rgbValues[0] = sins1[kValue]; 34 | rgbValues[1] = sins1[(kValue + 120) % 360]; 35 | rgbValues[2] = sins1[(kValue + 240) % 360]; 36 | kValue++; 37 | } 38 | 39 | void setNextColor_sin2(uint8_t *rgbValues, uint8_t &kValue) 40 | { 41 | rgbValues[0] = sins2[(kValue + 120) % 360]; 42 | rgbValues[1] = sins2[kValue]; 43 | rgbValues[2] = sins2[(kValue + 240) % 360]; 44 | kValue++; 45 | } 46 | 47 | // Color changing state machine 48 | void setNextColor(uint8_t *rgbValues, uint8_t &kValue) 49 | { 50 | switch (kValue) 51 | { 52 | case 0: 53 | rgbValues[1] += (uint8_t)2; // Increment green 54 | if (rgbValues[1] == (uint8_t)64) 55 | { 56 | rgbValues[1] = (uint8_t)63; 57 | kValue = (uint8_t)1; 58 | } 59 | break; 60 | case 1: 61 | rgbValues[0]--; // Decrement red 62 | if (rgbValues[0] == (uint8_t)255) 63 | { 64 | rgbValues[0] = (uint8_t)0; 65 | kValue = (uint8_t)2; 66 | } 67 | break; 68 | case 2: 69 | rgbValues[2]++; // Increment blue 70 | if (rgbValues[2] == (uint8_t)32) 71 | { 72 | rgbValues[2] = (uint8_t)31; 73 | kValue = (uint8_t)3; 74 | } 75 | break; 76 | case 3: 77 | rgbValues[1] -= (uint8_t)2; // Decrement green 78 | if (rgbValues[1] == (uint8_t)255) 79 | { 80 | rgbValues[1] = (uint8_t)0; 81 | kValue = (uint8_t)4; 82 | } 83 | break; 84 | case 4: 85 | rgbValues[0]++; // Increment red 86 | if (rgbValues[0] == (uint8_t)32) 87 | { 88 | rgbValues[0] = (uint8_t)31; 89 | kValue = (uint8_t)5; 90 | } 91 | break; 92 | case 5: 93 | rgbValues[2]--; // Increment blue 94 | if (rgbValues[2] == (uint8_t)255) 95 | { 96 | rgbValues[2] = (uint8_t)0; 97 | kValue = (uint8_t)0; 98 | } 99 | break; 100 | } 101 | } 102 | 103 | uint8_t dr = 0; 104 | uint8_t dg = 0; 105 | uint8_t db = 0; 106 | 107 | void setNextColor2(uint8_t *rgbValues) 108 | { 109 | rgbValues[0] += dr; 110 | rgbValues[1] += dg; 111 | rgbValues[2] += db; 112 | 113 | if (rgbValues[0] == 255 && rgbValues[1] == 0 && rgbValues[2] == 0) 114 | { 115 | dr = 0; 116 | dg = 1; 117 | db = 0; 118 | } 119 | 120 | if (rgbValues[0] == 255 && rgbValues[1] == 255 && rgbValues[2] == 0) 121 | { 122 | dr = -1; 123 | dg = 0; 124 | db = 0; 125 | } 126 | 127 | if (rgbValues[0] == 0 && rgbValues[1] == 255 && rgbValues[2] == 0) 128 | { 129 | dr = 0; 130 | dg = 0; 131 | db = 1; 132 | } 133 | 134 | if (rgbValues[0] == 0 && rgbValues[1] == 255 && rgbValues[2] == 255) 135 | { 136 | dr = 0; 137 | dg = -1; 138 | db = 0; 139 | } 140 | 141 | if (rgbValues[0] == 0 && rgbValues[1] == 0 && rgbValues[2] == 255) 142 | { 143 | dr = 1; 144 | dg = 0; 145 | db = 0; 146 | } 147 | 148 | if (rgbValues[0] == 255 && rgbValues[1] == 0 && rgbValues[2] == 255) 149 | { 150 | dr = 0; 151 | dg = 0; 152 | db = -1; 153 | } 154 | } -------------------------------------------------------------------------------- /projects/sand-multi-task-4_3inch/lgfx_8048S043C.h: -------------------------------------------------------------------------------- 1 | //===================================================================== 2 | // https://github.com/lovyan03/LovyanGFX/tree/master/src/lgfx/v1/platforms/esp32s3 3 | //===================================================================== 4 | #include 5 | #include 6 | #include 7 | 8 | class LGFX : public lgfx::LGFX_Device{ 9 | lgfx::Bus_RGB _bus_instance; 10 | lgfx::Panel_RGB _panel_instance; 11 | lgfx::Light_PWM _light_instance; 12 | lgfx::Touch_GT911 _touch_instance; 13 | 14 | public:LGFX(void){ 15 | auto cfg = _bus_instance.config(); 16 | cfg.panel = &_panel_instance; 17 | cfg.pin_d0 = 8; // B0 18 | cfg.pin_d1 = 3; // B1 19 | cfg.pin_d2 = 46; // B2 20 | cfg.pin_d3 = 9; // B3 21 | cfg.pin_d4 = 1; // B4 22 | cfg.pin_d5 = 5; // G0 23 | cfg.pin_d6 = 6; // G1 24 | cfg.pin_d7 = 7; // G2 25 | cfg.pin_d8 = 15; // G3 26 | cfg.pin_d9 = 16; // G4 27 | cfg.pin_d10 = 4; // G5 28 | cfg.pin_d11 = 45; // R0 29 | cfg.pin_d12 = 48; // R1 30 | cfg.pin_d13 = 47; // R2 31 | cfg.pin_d14 = 21; // R3 32 | cfg.pin_d15 = 14; // R4 33 | cfg.pin_henable = 40; 34 | cfg.pin_vsync = 41; 35 | cfg.pin_hsync = 39; 36 | cfg.pin_pclk = 42; 37 | cfg.freq_write = 14000000; 38 | cfg.hsync_polarity = 0; 39 | cfg.hsync_front_porch = 8; 40 | cfg.hsync_pulse_width = 4; 41 | cfg.hsync_back_porch = 16; 42 | cfg.vsync_polarity = 0; 43 | cfg.vsync_front_porch = 4; 44 | cfg.vsync_pulse_width = 4; 45 | cfg.vsync_back_porch = 4; 46 | cfg.pclk_idle_high = 1; 47 | _bus_instance.config(cfg); 48 | _panel_instance.setBus(&_bus_instance); 49 | 50 | { auto cfg = _panel_instance.config(); 51 | cfg.memory_width = 800; 52 | cfg.memory_height = 480; 53 | cfg.panel_width = 800; 54 | cfg.panel_height = 480; 55 | cfg.offset_x = 1000; 56 | cfg.offset_y = 2000; 57 | _panel_instance.config(cfg); 58 | } 59 | 60 | { auto cfg = _panel_instance.config_detail(); 61 | cfg.use_psram = 1; 62 | _panel_instance.config_detail(cfg); 63 | } 64 | 65 | { auto cfg = _light_instance.config(); 66 | cfg.pin_bl = 2; 67 | cfg.freq = 44100; 68 | cfg.pwm_channel = 7; 69 | _light_instance.config(cfg); 70 | } 71 | _panel_instance.light(&_light_instance); 72 | 73 | { auto cfg = _touch_instance.config(); 74 | cfg.x_min = 0; // タッチスクリーンから得られる最小のX値(生の値) 75 | cfg.x_max = 480; // タッチスクリーンから得られる最大のX値(生の値) 76 | cfg.y_min = 0; // タッチスクリーンから得られる最小のY値(生の値) 77 | cfg.y_max = 272; // タッチスクリーンから得られる最大のY値(生の値) 78 | cfg.pin_int = -1; // INTが接続されているピン番号 18 79 | cfg.bus_shared = false; // 画面と共通のバスを使用している場合 trueを設定 80 | cfg.offset_rotation = 0; // 表示とタッチの向きの調整 0~7の値で設定 81 | // I2C接続 82 | cfg.i2c_port = 1; // I2C(0 = SPI or 1 = Wire) 83 | cfg.pin_sda = 19; // SDA 84 | cfg.pin_scl = 20; // SCL 85 | cfg.pin_rst = 38; 86 | cfg.freq = 800000; // I2C 87 | cfg.i2c_addr = 0x5D; // I2C 0x5D or 0x14 88 | _touch_instance.config(cfg); 89 | _panel_instance.setTouch(&_touch_instance);//タッチスクリーンをパネルにセット 90 | } 91 | 92 | setPanel(&_panel_instance); // 使用するパネルをセットします。 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /projects/sand-multi-task-4_3inch/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "lgfx_8048S043C.h" 6 | #include "PixelState.h" 7 | #include "colorChangeRoutine.h" 8 | 9 | ///////////////////////////////////////////////////// 10 | // You can adjust the following "subjective" params: 11 | // static const int8_t PIXEL_WIDTH = 3; 12 | // uint8_t percentInputFill = 20; 13 | // uint8_t inputWidth = 6; 14 | static const int32_t PIXEL_WIDTH = 8; 15 | static const bool randomShapesAsPixels = true; 16 | static const uint8_t pixelPadding = 1; 17 | static const uint8_t percentInputFill = 15; 18 | static const int32_t inputWidth = 5; 19 | static const uint8_t gravity = 1; 20 | static const bool changeFallenPixelColors = true; 21 | static const unsigned long maxFps = 30; 22 | static const unsigned long millisToChangeInputColor = 60; 23 | static const unsigned long millisToChangeAllColors = 30; 24 | // End "subjective" params. 25 | ///////////////////////////////////////////////////// 26 | 27 | static const int32_t NATIVE_ROWS = LCD_HEIGHT; 28 | static const int32_t NATIVE_COLS = LCD_WIDTH; 29 | static const int32_t SCALED_ROWS = NATIVE_ROWS / PIXEL_WIDTH; 30 | static const int32_t SCALED_COLS = NATIVE_COLS / PIXEL_WIDTH; 31 | 32 | int16_t BACKGROUND_COLOR = TFT_BLACK; 33 | 34 | static LGFX display; // Instance of LGFX 35 | 36 | // 16-bit color representation: 37 | //------------------------------------ 38 | // Red ⏐ Green ⏐ Blue 39 | // 1 1 1 1 1 ⏐ 1 1 1 1 1 0 ⏐ 0 0 0 0 0 40 | // = 31 ⏐ = 62 ⏐ = 0 41 | // 42 | // ((31 << 11) | (62 << 5) | 0) = 65472 = 0xffc0 43 | 44 | // uint8_t newRgbValues[3] = { 0x31, 0x00, 0x00 }; // red, green, blue 45 | uint8_t newRgbValues[3] = {0x1F, 0x00, 0x00}; // red, green, blue 46 | uint8_t newKValue = 4; 47 | 48 | unsigned long colorChangeTime = 0; 49 | unsigned long allColorChangeTime = 0; 50 | 51 | int32_t inputX = -1; 52 | int32_t inputY = -1; 53 | 54 | // pixelStates key is a concat of the 16 bit x/y values into a single 32 bit value for more efficient storage. 55 | std::unordered_map pixelStates; // [X,Y]:[STATE_DATA] 56 | std::unordered_map landedPixelStates; // [X,Y]:[STATE_DATA] 57 | std::unordered_map landedPixelsColumnTops; // [X]:[Y] // For each column (X), what is the highest row (Y) where a pixel stopped. 58 | 59 | static std::unordered_map pixelStatesToAdd; 60 | static std::unordered_set pixelsToErase; 61 | 62 | SemaphoreHandle_t xDisplayMutex = NULL; 63 | SemaphoreHandle_t xStateMutex = NULL; 64 | SemaphoreHandle_t xSemaphore1 = NULL; 65 | SemaphoreHandle_t xSemaphore2 = NULL; 66 | SemaphoreHandle_t xSemaphore3 = NULL; 67 | SemaphoreHandle_t xSemaphore4 = NULL; 68 | 69 | long lastMillis = 0; 70 | int fps = 0; 71 | char fpsStringBuffer[32]; 72 | 73 | bool withinNativeCols(int32_t value) 74 | { 75 | return value >= 0 && value <= NATIVE_COLS - 1; 76 | } 77 | 78 | bool withinNativeRows(int32_t value) 79 | { 80 | return value >= 0 && value <= NATIVE_ROWS - 1; 81 | } 82 | 83 | bool withinScaledCols(int32_t value) 84 | { 85 | return value >= 0 && value <= SCALED_COLS - 1; 86 | } 87 | 88 | bool withinScaledRows(int32_t value) 89 | { 90 | return value >= 0 && value <= SCALED_ROWS - 1; 91 | } 92 | 93 | void setColor(uint8_t *rgbValues, uint8_t _red, uint8_t _green, uint8_t _blue) 94 | { 95 | rgbValues[0] = _red; /// * `rgbValues[0]` is the red value 96 | rgbValues[1] = _green; /// * `rgbValues[1]` is the green value 97 | rgbValues[2] = _blue; /// * `rgbValues[2]` is the blue value 98 | } 99 | 100 | Point getXYIndividualValues(uint64_t xy) 101 | { 102 | // The 16 bit x/y values are stored as one 32 bit concatenation. Get the individual x/y values. 103 | uint32_t pixelXCol = (uint32_t)((xy & 0xFFFF0000) >> 16); 104 | uint32_t pixelYRow = (uint32_t)(xy & 0x0000FFFF); 105 | 106 | return Point(pixelXCol, pixelYRow); 107 | } 108 | 109 | uint64_t getXYCombinedValue(uint32_t x, uint32_t y) 110 | { 111 | return ((uint64_t)x << 16) | (uint64_t)(y); 112 | } 113 | 114 | void clearScaledPixel(int32_t x, int32_t y) 115 | { 116 | // Scale 117 | int32_t scaledXCol = x * PIXEL_WIDTH; 118 | int32_t scaledYRow = y * PIXEL_WIDTH; 119 | 120 | if (xSemaphoreTake(xDisplayMutex, portMAX_DELAY)) 121 | { 122 | display.fillRect(scaledXCol, scaledYRow, PIXEL_WIDTH, PIXEL_WIDTH, BACKGROUND_COLOR); 123 | xSemaphoreGive(xDisplayMutex); 124 | } 125 | } 126 | 127 | uint8_t getRandomShape() 128 | { 129 | return random(0, 8); 130 | } 131 | 132 | uint8_t getPixelShape() 133 | { 134 | return randomShapesAsPixels ? getRandomShape() : 0; 135 | } 136 | 137 | // Convert scaled pixel to native pixel area and then draw it. 138 | void drawScaledPixel(int32_t x, int32_t y, uint8_t *rgbValues, uint8_t shape) 139 | { 140 | // Serial.printf("drawScaledPixel: x: %d, y: %d, shape: %d, rgbValues: %02hhX%02hhX%02hhX", x, y, shape, rgbValues[0], rgbValues[1], rgbValues[2]); 141 | // Serial.println(); 142 | 143 | // Scale 144 | int32_t scaledXCol = x * PIXEL_WIDTH; 145 | int32_t scaledYRow = y * PIXEL_WIDTH; 146 | 147 | // long map(long x, long in_min, long in_max, long out_min, long out_max) 148 | uint8_t r = map(rgbValues[0], 0, 31, 0, 255); 149 | uint8_t g = map(rgbValues[1], 0, 63, 0, 255); 150 | uint8_t b = map(rgbValues[2], 0, 31, 0, 255); 151 | 152 | if (xSemaphoreTake(xDisplayMutex, portMAX_DELAY)) 153 | { 154 | display.setColor(r, g, b); 155 | 156 | // Serial.printf("drawScaledPixel: color: %02hhX %02hhX %02hhX", r, g, b); 157 | // Serial.println(); 158 | 159 | // Prevent edges of shapres overlapping. 160 | auto pixelWidthAdjustment = max(1, PIXEL_WIDTH - pixelPadding); 161 | 162 | if (shape == 0) 163 | { 164 | // Filled (square) rectangle. 165 | display.fillRect(scaledXCol, scaledYRow, pixelWidthAdjustment, pixelWidthAdjustment); 166 | } 167 | else if (shape == 1) 168 | { 169 | // Rectangle (square) outline. 170 | display.drawRect(scaledXCol, scaledYRow, pixelWidthAdjustment, pixelWidthAdjustment); 171 | } 172 | else if (shape == 2) 173 | { 174 | // Circle outline. 175 | display.drawCircle(scaledXCol + (pixelWidthAdjustment / 2), scaledYRow + (pixelWidthAdjustment / 2), pixelWidthAdjustment / 2); 176 | } 177 | else if (shape == 3) 178 | { 179 | // Filled circle. 180 | display.fillCircle(scaledXCol + (pixelWidthAdjustment / 2), scaledYRow + (pixelWidthAdjustment / 2), pixelWidthAdjustment / 2); 181 | } 182 | else if (shape == 4) 183 | { 184 | // Triangle outline "pointing" down. 185 | display.drawTriangle(scaledXCol, scaledYRow, 186 | scaledXCol + pixelWidthAdjustment, scaledYRow, 187 | scaledXCol + (pixelWidthAdjustment / 2), 188 | scaledYRow + pixelWidthAdjustment); 189 | } 190 | else if (shape == 5) 191 | { 192 | // Filled triangle "pointing" down. 193 | display.fillTriangle(scaledXCol, scaledYRow, 194 | scaledXCol + pixelWidthAdjustment, scaledYRow, 195 | scaledXCol + (pixelWidthAdjustment / 2), 196 | scaledYRow + pixelWidthAdjustment); 197 | } 198 | else if (shape == 6) 199 | { 200 | // Triangle outline "pointing" up. 201 | display.drawTriangle(scaledXCol, scaledYRow + pixelWidthAdjustment, 202 | scaledXCol + pixelWidthAdjustment, scaledYRow + pixelWidthAdjustment, 203 | scaledXCol + (pixelWidthAdjustment / 2), 204 | scaledYRow); 205 | } 206 | else if (shape == 7) 207 | { 208 | // Filled triangle "pointing" up. 209 | display.fillTriangle(scaledXCol, scaledYRow + pixelWidthAdjustment, 210 | scaledXCol + pixelWidthAdjustment, scaledYRow + pixelWidthAdjustment, 211 | scaledXCol + (pixelWidthAdjustment / 2), 212 | scaledYRow); 213 | } 214 | else 215 | { 216 | // Filled (square) rectangle. 217 | display.fillRect(scaledXCol, scaledYRow, pixelWidthAdjustment, pixelWidthAdjustment); 218 | } 219 | 220 | xSemaphoreGive(xDisplayMutex); 221 | } 222 | } 223 | 224 | void setNextColorAll(std::unordered_map::iterator landedPixelStatesBegin, 225 | std::unordered_map::iterator landedPixelStatesEnd, 226 | std::unordered_map::iterator pixelStatesBegin, 227 | std::unordered_map::iterator pixelStatesEnd) 228 | { 229 | for (auto iter = landedPixelStatesBegin; iter != landedPixelStatesEnd; iter++) 230 | { 231 | setNextColor(iter->second.RgbValues, iter->second.ColorState); 232 | drawScaledPixel(iter->second.XCol, iter->second.YRow, iter->second.RgbValues, iter->second.Shape); 233 | } 234 | 235 | for (auto iter = pixelStatesBegin; iter != pixelStatesEnd; iter++) 236 | { 237 | setNextColor(iter->second.RgbValues, iter->second.ColorState); 238 | drawScaledPixel(iter->second.XCol, iter->second.YRow, iter->second.RgbValues, iter->second.Shape); 239 | } 240 | } 241 | 242 | bool isPixelSlotAvailable(uint64_t xy) 243 | { 244 | auto point = getXYIndividualValues(xy); 245 | return withinScaledRows(point.YRow) && withinScaledCols(point.XCol) && pixelStates.find(xy) == pixelStates.end() && point.YRow < landedPixelsColumnTops[point.XCol]; 246 | } 247 | 248 | void updateLandedPixelsColumnTops(uint64_t xyLanded) 249 | { 250 | // landedPixelsColumnTops stores the highest row (Y) where a pixel stopped for each column (X). 251 | 252 | auto point = getXYIndividualValues(xyLanded); 253 | 254 | landedPixelsColumnTops[point.XCol] = std::min(landedPixelsColumnTops[point.XCol], point.YRow); 255 | } 256 | 257 | bool canPixelFall(uint64_t xyKey) 258 | { 259 | // The 16 bit x/y values are stored as one 32 bit concatenation. Get the individual x/y values. 260 | uint32_t pixelYRow = (uint32_t)(xyKey & 0x0000FFFF); 261 | 262 | uint32_t pixelYRowNext = pixelYRow + 1; 263 | 264 | if (!withinScaledRows(pixelYRowNext)) 265 | return false; 266 | 267 | uint32_t pixelXCol = (uint32_t)((xyKey & 0xFFFF0000) >> 16); 268 | uint32_t pixelXColMinus = pixelXCol - 1; 269 | uint32_t pixelXColPlus = pixelXCol + 1; 270 | 271 | return (withinScaledCols(pixelXColMinus) && landedPixelsColumnTops[pixelXColMinus] > pixelYRowNext) || 272 | (withinScaledCols(pixelXCol) && landedPixelsColumnTops[pixelXCol] > pixelYRowNext) || 273 | (withinScaledCols(pixelXColPlus) && landedPixelsColumnTops[pixelXColPlus] > pixelYRowNext); 274 | } 275 | 276 | void movePixels(std::unordered_map::iterator _pixelStatesIteratorBegin, std::unordered_map::iterator _pixelStatesIteratorEnd) 277 | { 278 | // Iterate moving pixels and move them as needed. 279 | for (auto iter = _pixelStatesIteratorBegin; iter != _pixelStatesIteratorEnd; iter++) 280 | { 281 | auto keyVal = *iter; 282 | auto pixelKey = keyVal.first; 283 | auto thisPixelState = keyVal.second; 284 | 285 | // The 16 bit x/y values are stored as one 32 bit concatenation. Get the individual x/y values. 286 | auto pixelPoint = getXYIndividualValues(pixelKey); 287 | uint32_t pixelXCol = pixelPoint.XCol; 288 | uint32_t pixelYRow = pixelPoint.YRow; 289 | 290 | auto pixelState = keyVal.second.State; 291 | if (pixelState == GRID_STATE_NEW) 292 | { 293 | pixelStates[pixelKey].State = GRID_STATE_FALLING; 294 | continue; 295 | } 296 | 297 | uint8_t *thisPixelRgbValues = thisPixelState.RgbValues; 298 | 299 | auto pixelColorState = thisPixelState.ColorState; 300 | auto pixelVelocity = thisPixelState.Velocity; 301 | auto pixelShape = thisPixelState.Shape; 302 | 303 | bool moved = false; 304 | 305 | uint32_t newMaxYRowPos = uint32_t(pixelYRow + pixelVelocity); 306 | for (int32_t yRowPos = newMaxYRowPos; yRowPos > pixelYRow; yRowPos--) 307 | { 308 | if (!withinScaledRows(yRowPos)) 309 | { 310 | continue; 311 | } 312 | 313 | uint64_t belowXY = getXYCombinedValue(pixelXCol, yRowPos); 314 | 315 | int32_t direction = 1; 316 | if (random(100) < 50) 317 | { 318 | direction *= -1; 319 | } 320 | 321 | uint32_t belowXY_A = -1; 322 | uint32_t belowXY_A_XCol = pixelXCol + direction; 323 | uint32_t belowXY_B = -1; 324 | uint32_t belowXY_B_XCol = pixelXCol - direction; 325 | 326 | if (withinScaledCols(belowXY_A_XCol)) 327 | { 328 | belowXY_A = getXYCombinedValue(belowXY_A_XCol, yRowPos); 329 | } 330 | if (withinScaledCols(belowXY_B_XCol)) 331 | { 332 | belowXY_B = getXYCombinedValue(belowXY_B_XCol, yRowPos); 333 | } 334 | 335 | if (xSemaphoreTake(xStateMutex, portMAX_DELAY)) 336 | { 337 | if (isPixelSlotAvailable(belowXY) && pixelStatesToAdd.find(belowXY) == pixelStatesToAdd.end()) 338 | { 339 | // This pixel will go straight down. 340 | pixelStatesToAdd[belowXY] = PointState(pixelXCol, yRowPos, GRID_STATE_FALLING, pixelColorState, pixelVelocity + gravity, pixelShape); 341 | setColor(&(pixelStatesToAdd[belowXY].RgbValues[0]), thisPixelRgbValues[0], thisPixelRgbValues[1], thisPixelRgbValues[2]); 342 | 343 | clearScaledPixel(pixelXCol, pixelYRow); // Out with the old. 344 | drawScaledPixel(pixelXCol, yRowPos, thisPixelRgbValues, pixelShape); // In with the new. 345 | 346 | pixelsToErase.insert(pixelKey); 347 | pixelsToErase.erase(belowXY); 348 | 349 | moved = true; 350 | xSemaphoreGive(xStateMutex); 351 | break; 352 | } 353 | else if (isPixelSlotAvailable(belowXY_A) && pixelStatesToAdd.find(belowXY_A) == pixelStatesToAdd.end()) 354 | { 355 | // This pixel will fall to side A (right) 356 | pixelStatesToAdd[belowXY_A] = PointState(belowXY_A_XCol, yRowPos, GRID_STATE_FALLING, pixelColorState, pixelVelocity + gravity, pixelShape); 357 | setColor(&(pixelStatesToAdd[belowXY_A].RgbValues[0]), thisPixelRgbValues[0], thisPixelRgbValues[1], thisPixelRgbValues[2]); 358 | 359 | clearScaledPixel(pixelXCol, pixelYRow); // Out with the old. 360 | drawScaledPixel(belowXY_A_XCol, yRowPos, thisPixelRgbValues, pixelShape); // In with the new. 361 | 362 | pixelsToErase.insert(pixelKey); 363 | pixelsToErase.erase(belowXY_A); 364 | 365 | moved = true; 366 | xSemaphoreGive(xStateMutex); 367 | break; 368 | } 369 | else if (isPixelSlotAvailable(belowXY_B) && pixelStatesToAdd.find(belowXY_B) == pixelStatesToAdd.end()) 370 | { 371 | // This pixel will fall to side B (left) 372 | pixelStatesToAdd[belowXY_B] = PointState(belowXY_B_XCol, yRowPos, GRID_STATE_FALLING, pixelColorState, pixelVelocity + gravity, pixelShape); 373 | setColor(&(pixelStatesToAdd[belowXY_B].RgbValues[0]), thisPixelRgbValues[0], thisPixelRgbValues[1], thisPixelRgbValues[2]); 374 | 375 | clearScaledPixel(pixelXCol, pixelYRow); // Out with the old. 376 | drawScaledPixel(belowXY_B_XCol, yRowPos, thisPixelRgbValues, pixelShape); // In with the new. 377 | 378 | pixelsToErase.insert(pixelKey); 379 | pixelsToErase.erase(belowXY_B); 380 | 381 | moved = true; 382 | xSemaphoreGive(xStateMutex); 383 | break; 384 | } 385 | 386 | xSemaphoreGive(xStateMutex); 387 | } 388 | } 389 | 390 | if (!moved) 391 | { 392 | thisPixelState.Velocity += gravity; 393 | } 394 | 395 | if (xSemaphoreTake(xStateMutex, portMAX_DELAY)) 396 | { 397 | if (!moved && !canPixelFall(pixelKey)) 398 | { 399 | thisPixelState.State = GRID_STATE_LANDED; 400 | landedPixelStates[pixelKey] = thisPixelState; 401 | 402 | pixelsToErase.insert(pixelKey); 403 | updateLandedPixelsColumnTops(pixelKey); 404 | } 405 | 406 | xSemaphoreGive(xStateMutex); 407 | } 408 | } 409 | } 410 | 411 | // Task for moving the falling "pixels". 412 | void task1(void *pvParameters) 413 | { 414 | // auto coreId = xPortGetCoreID(); 415 | 416 | while (1) 417 | { 418 | if (xSemaphoreTake(xSemaphore1, portMAX_DELAY)) 419 | { 420 | auto pixelStatesBegin = pixelStates.begin(); 421 | auto pixelStatesMid = std::next(pixelStatesBegin, (pixelStates.size() / 2)); 422 | auto pixelStatesEnd = pixelStates.end(); 423 | 424 | movePixels(pixelStatesMid, pixelStatesEnd); 425 | 426 | xSemaphoreGive(xSemaphore2); // release the mutex 427 | } 428 | } 429 | } 430 | 431 | // Task for changing all "pixel" colors. 432 | void task2(void *pvParameters) 433 | { 434 | // auto coreId = xPortGetCoreID(); 435 | 436 | while (1) 437 | { 438 | if (xSemaphoreTake(xSemaphore3, portMAX_DELAY)) 439 | { 440 | auto landedPixelStatesBegin = landedPixelStates.begin(); 441 | auto landedPixelStatesMid = std::next(landedPixelStatesBegin, (landedPixelStates.size() / 2)); 442 | auto landedPixelStatesEnd = landedPixelStates.end(); 443 | 444 | auto pixelStatesBegin = pixelStates.begin(); 445 | auto pixelStatesMid = std::next(pixelStatesBegin, (pixelStates.size() / 2)); 446 | auto pixelStatesEnd = pixelStates.end(); 447 | 448 | setNextColorAll(landedPixelStatesMid, landedPixelStatesEnd, pixelStatesMid, pixelStatesEnd); 449 | 450 | xSemaphoreGive(xSemaphore4); // release the mutex 451 | } 452 | } 453 | } 454 | 455 | void setup() 456 | { 457 | // Serial.begin(115200); 458 | 459 | Serial.println("Init display..."); 460 | display.init(); 461 | 462 | if (display.touch()) 463 | { 464 | if (display.width() < display.height()) 465 | display.setRotation(display.getRotation() ^ 1); 466 | 467 | // display.setTextDatum(textdatum_t::middle_center); 468 | // display.drawString("touch the arrow marker.", display.width()>>1, display.height() >> 1); 469 | // display.setTextDatum(textdatum_t::top_left); 470 | 471 | // std::uint16_t fg = TFT_WHITE; 472 | // std::uint16_t bg = TFT_BLACK; 473 | // if (display.isEPD()) std::swap(fg, bg); 474 | // uint16_t calibrationData[8]; 475 | // display.calibrateTouch(calibrationData, fg, bg, std::max(display.width(), display.height()) >> 3); 476 | 477 | // for (short i=0;i<8;i++) { 478 | // Serial.print('Calibration data: '); 479 | // Serial.print(calibrationData[i]); 480 | // Serial.print(', '); 481 | // } 482 | } 483 | 484 | colorChangeTime = millis() + 1000; 485 | 486 | // Set the tallest pixel column to be 1 slot bellow the "bottom" (0,0 coordinate being top left.) 487 | for (int32_t xCol = 0; xCol < SCALED_COLS; xCol++) 488 | landedPixelsColumnTops[xCol] = SCALED_ROWS; 489 | 490 | auto viewportWidth = display.width(); 491 | auto viewportHeight = display.height(); 492 | 493 | // Serial.printf("Viewport dimensions (W x H): %i x %i\n", viewportWidth, viewportHeight); 494 | 495 | xDisplayMutex = xSemaphoreCreateMutex(); 496 | xStateMutex = xSemaphoreCreateMutex(); 497 | xSemaphore1 = xSemaphoreCreateCounting(1, 0); 498 | xSemaphore2 = xSemaphoreCreateCounting(1, 0); 499 | xSemaphore3 = xSemaphoreCreateCounting(1, 0); 500 | xSemaphore4 = xSemaphoreCreateCounting(1, 0); 501 | 502 | xTaskCreatePinnedToCore( 503 | task1, // Function to implement the task 504 | "task1", // Name of the task 505 | 4096, // Stack size in words 506 | NULL, // Task input parameter 507 | 1, // Priority of the task 508 | NULL, // Task handle 509 | 0 // CPU core to pin to 510 | ); 511 | 512 | xTaskCreatePinnedToCore( 513 | task2, // Function to implement the task 514 | "task2", // Name of the task 515 | 4096, // Stack size in words 516 | NULL, // Task input parameter 517 | 1, // Priority of the task 518 | NULL, // Task handle 519 | 0 // CPU core to pin to 520 | ); 521 | 522 | delay(500); 523 | } 524 | 525 | void loop() 526 | { 527 | unsigned long currentMillis = millis(); 528 | 529 | // Throttle FPS 530 | unsigned long diffMillis = currentMillis - lastMillis; 531 | if ((1000 / maxFps) > diffMillis) 532 | { 533 | return; 534 | } 535 | 536 | // Serial.println("Looping..."); 537 | 538 | // Get frame rate. 539 | fps = 1000 / max(currentMillis - lastMillis, 1UL); 540 | sprintf(fpsStringBuffer, "fps:%4lu", fps); 541 | 542 | // Display frame rate 543 | display.setTextColor(TFT_WHITE, TFT_BLACK); 544 | display.drawString(fpsStringBuffer, 0, 0); 545 | 546 | // uint32_t psramSize = ESP.getPsramSize(); 547 | // uint32_t freePsram = ESP.getFreePsram(); 548 | // sprintf(fpsStringBuffer, "psram: %6u / %6u", freePsram, psramSize); 549 | // display.drawString(fpsStringBuffer, 0, 10); 550 | 551 | // uint32_t heapSize = ESP.getHeapSize(); 552 | // uint32_t freeHeap = ESP.getFreeHeap(); 553 | // sprintf(fpsStringBuffer, "heap: %6u / %6u", freeHeap, heapSize); 554 | // display.drawString(fpsStringBuffer, 0, 20); 555 | 556 | // uint32_t minFreeHeap = ESP.getMinFreeHeap(); 557 | // sprintf(fpsStringBuffer, "min free heap: %6u", minFreeHeap); 558 | // display.drawString(fpsStringBuffer, 0, 30); 559 | 560 | lastMillis = currentMillis; 561 | 562 | // Change the color of the fallen pixels over time 563 | if (changeFallenPixelColors && allColorChangeTime < millis()) 564 | { 565 | allColorChangeTime = millis() + millisToChangeAllColors; 566 | 567 | auto landedPixelStatesBegin = landedPixelStates.begin(); 568 | auto landedPixelStatesMid = std::next(landedPixelStatesBegin, (landedPixelStates.size() / 2)); 569 | 570 | auto pixelStatesBegin = pixelStates.begin(); 571 | auto pixelStatesMid = std::next(pixelStatesBegin, (pixelStates.size() / 2)); 572 | 573 | // Start task and proceed. 574 | xSemaphoreGive(xSemaphore3); 575 | 576 | setNextColorAll(landedPixelStatesBegin, landedPixelStatesMid, pixelStatesBegin, pixelStatesMid); 577 | 578 | // Wait for task to complete. 579 | xSemaphoreTake(xSemaphore4, portMAX_DELAY); 580 | 581 | setNextColor(newRgbValues, newKValue); 582 | } 583 | 584 | // Change the color of the pixels over time 585 | if (colorChangeTime < millis()) 586 | { 587 | colorChangeTime = millis() + millisToChangeInputColor; 588 | setNextColor(newRgbValues, newKValue); 589 | } 590 | 591 | // Handle touch. 592 | int32_t touchX, touchY; 593 | if (display.getTouch(&touchX, &touchY)) 594 | { 595 | // Scale 596 | inputX = map(touchX, 0, NATIVE_COLS, 0, SCALED_COLS); 597 | inputY = map(touchY, 0, NATIVE_ROWS, 0, SCALED_ROWS); 598 | } 599 | else 600 | { 601 | inputX = -1; 602 | inputY = -1; 603 | } 604 | 605 | if (withinNativeCols(inputX) && withinNativeRows(inputY)) 606 | { 607 | // Randomly add an area of pixels 608 | int32_t halfInputWidth = inputWidth / 2; 609 | for (int32_t i = -halfInputWidth; i <= halfInputWidth; ++i) 610 | { 611 | for (int32_t j = -halfInputWidth; j <= halfInputWidth; ++j) 612 | { 613 | if (random(100) < percentInputFill) 614 | { 615 | uint32_t xCol = inputX + i; 616 | uint32_t yRow = inputY + j; 617 | 618 | // Concat the 16 bit x/y values into a single 32 bit value for more efficient storage. 619 | uint64_t xy = ((uint64_t)xCol << 16) | (uint64_t)yRow; 620 | 621 | if (withinNativeCols(xCol) && withinNativeRows(yRow) && pixelStates.find(xy) == pixelStates.end()) 622 | { 623 | pixelStates[xy] = PointState(xCol, yRow, GRID_STATE_NEW, newKValue, 1, getPixelShape()); 624 | setColor(pixelStates[xy].RgbValues, newRgbValues[0], newRgbValues[1], newRgbValues[2]); 625 | drawScaledPixel(xCol, yRow, pixelStates[xy].RgbValues, pixelStates[xy].Shape); 626 | } 627 | } 628 | } 629 | } 630 | } 631 | 632 | // Split up the work. 633 | 634 | auto pixelStatesBegin = pixelStates.begin(); 635 | auto pixelStatesMid = std::next(pixelStatesBegin, (pixelStates.size() / 2)); 636 | 637 | pixelStatesToAdd.clear(); 638 | pixelsToErase.clear(); 639 | 640 | // Start task and proceed. 641 | xSemaphoreGive(xSemaphore1); 642 | 643 | movePixels(pixelStatesBegin, pixelStatesMid); 644 | 645 | // Wait for task to complete. 646 | xSemaphoreTake(xSemaphore2, portMAX_DELAY); 647 | 648 | for (const auto &key : pixelsToErase) 649 | { 650 | if (pixelStatesToAdd.find(key) != pixelStatesToAdd.end()) 651 | continue; // Do not erase pixel if it is being back-filled. 652 | 653 | pixelStates.erase(key); 654 | } 655 | 656 | for (const auto &keyVal : pixelStatesToAdd) 657 | { 658 | pixelStates[keyVal.first] = PointState(keyVal.second.XCol, keyVal.second.YRow, keyVal.second.State, keyVal.second.ColorState, keyVal.second.Velocity, keyVal.second.Shape); 659 | setColor(pixelStates[keyVal.first].RgbValues, keyVal.second.RgbValues[0], keyVal.second.RgbValues[1], keyVal.second.RgbValues[2]); 660 | } 661 | } 662 | -------------------------------------------------------------------------------- /projects/sand-multi-task/PixelState.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static const uint8_t GRID_STATE_NEW = 1; 4 | static const uint8_t GRID_STATE_FALLING = 2; 5 | 6 | struct PointState 7 | { 8 | uint16_t XCol; 9 | uint16_t YRow; 10 | uint16_t State; 11 | uint16_t Color; 12 | uint8_t Velocity; 13 | 14 | PointState() {} 15 | 16 | PointState(uint16_t x, uint16_t y, uint16_t state, uint16_t color, uint8_t velocity) 17 | { 18 | XCol = x; 19 | YRow = y; 20 | State = state; 21 | Color = color; 22 | Velocity = velocity; 23 | } 24 | }; 25 | 26 | struct Point 27 | { 28 | uint16_t XCol; 29 | uint16_t YRow; 30 | 31 | Point() {} 32 | 33 | Point(uint16_t x, uint16_t y) 34 | { 35 | XCol = x; 36 | YRow = y; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /projects/sand-multi-task/README.md: -------------------------------------------------------------------------------- 1 | # Sand (Multi-Task) 2 | 3 | I created this project as a copy of my [Sand project](../sand) initially and then added multi-tasking to take advantage of the dual-core CPU in the ESP32. This was my first time trying out parallelization programming on an MCU and with FreeRTOS. 4 | 5 | The performance was indeed improved, but not by quite as much as I was hoping. I haven't done any performance analysis, but I suspect there is some overhead with the constant semaphore context switching. Still, a win is a win. 6 | 7 | You can see a Youtube video of the code in action here: 8 | 9 | [![Sand Simulation on the Cheap Yellow Display (CYD) ESP32 MCU + Display](https://img.youtube.com/vi/j8XRMEEZ0gM/0.jpg)](https://www.youtube.com/watch?v=j8XRMEEZ0gM) 10 | -------------------------------------------------------------------------------- /projects/sand-multi-task/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "PixelState.h" 8 | 9 | #define XPT2046_IRQ 36 10 | #define XPT2046_MOSI 32 11 | #define XPT2046_MISO 39 12 | #define XPT2046_CLK 25 13 | #define XPT2046_CS 33 14 | 15 | // The following min/max axis values were gathered through empirical data gathering on the specific board I have. 16 | #define TS_MINX 280 17 | #define TS_MAXX 3750 18 | #define TS_MINY 280 19 | #define TS_MAXY 3750 20 | 21 | ///////////////////////////////////////////////////// 22 | // You can adjust the following "subjective" params: 23 | // static const int8_t PIXEL_WIDTH = 3; 24 | // uint8_t percentInputFill = 20; 25 | // uint8_t inputWidth = 6; 26 | static const int8_t PIXEL_WIDTH = 2; 27 | static const uint8_t percentInputFill = 10; 28 | static const uint8_t inputWidth = 10; 29 | static const uint8_t gravity = 1; 30 | static const unsigned long maxFps = 30; 31 | static const unsigned long colorChangeFrequencyMs = 250; 32 | // End "subjective" params. 33 | ///////////////////////////////////////////////////// 34 | 35 | static const int16_t NATIVE_ROWS = 240; 36 | static const int16_t NATIVE_COLS = 320; 37 | static const int16_t SCALED_ROWS = NATIVE_ROWS / PIXEL_WIDTH; 38 | static const int16_t SCALED_COLS = NATIVE_COLS / PIXEL_WIDTH; 39 | 40 | int16_t BACKGROUND_COLOR = TFT_BLACK; 41 | 42 | SPIClass mySpi = SPIClass(VSPI); 43 | XPT2046_Touchscreen ts(XPT2046_CS, XPT2046_IRQ); 44 | TFT_eSPI tft = TFT_eSPI(); 45 | 46 | // 16-bit color representation: 47 | //------------------------------------ 48 | // Red ⏐ Green ⏐ Blue 49 | // 1 1 1 1 1 ⏐ 1 1 1 1 1 0 ⏐ 0 0 0 0 0 50 | // = 31 ⏐ = 62 ⏐ = 0 51 | // 52 | // ((31 << 11) | (62 << 5) | 0) = 65472 = 0xffc0 53 | 54 | byte red = 31; 55 | byte green = 0; 56 | byte blue = 0; 57 | byte colorState = 0; 58 | uint16_t color = red << 11; 59 | 60 | unsigned long colorChangeTime = 0; 61 | 62 | int16_t inputX = -1; 63 | int16_t inputY = -1; 64 | 65 | // pixelStates key is a concat of the 16 bit x/y values into a single 32 bit value for more efficient storage. 66 | std::unordered_map pixelStates; // [X,Y]:[STATE_DATA] 67 | std::unordered_map landedPixelsColumnTops; // [X]:[Y] // For each column (X), what is the highest row (Y) where a pixel stopped. 68 | 69 | static std::unordered_map pixelStatesToAdd; 70 | static std::unordered_set pixelsToErase; 71 | 72 | SemaphoreHandle_t xStateMutex = NULL; 73 | SemaphoreHandle_t xSemaphore1 = NULL; 74 | SemaphoreHandle_t xSemaphore2 = NULL; 75 | 76 | long lastMillis = 0; 77 | int fps = 0; 78 | char fpsStringBuffer[32]; 79 | 80 | bool withinNativeCols(int16_t value) 81 | { 82 | return value >= 0 && value <= NATIVE_COLS - 1; 83 | } 84 | 85 | bool withinNativeRows(int16_t value) 86 | { 87 | return value >= 0 && value <= NATIVE_ROWS - 1; 88 | } 89 | 90 | bool withinScaledCols(int16_t value) 91 | { 92 | return value >= 0 && value <= SCALED_COLS - 1; 93 | } 94 | 95 | bool withinScaledRows(int16_t value) 96 | { 97 | return value >= 0 && value <= SCALED_ROWS - 1; 98 | } 99 | 100 | // Color changing state machine 101 | void setNextColor() 102 | { 103 | switch (colorState) 104 | { 105 | case 0: 106 | green += 2; 107 | if (green == 64) 108 | { 109 | green = 63; 110 | colorState = 1; 111 | } 112 | break; 113 | case 1: 114 | red--; 115 | if (red == 255) 116 | { 117 | red = 0; 118 | colorState = 2; 119 | } 120 | break; 121 | case 2: 122 | blue++; 123 | if (blue == 32) 124 | { 125 | blue = 31; 126 | colorState = 3; 127 | } 128 | break; 129 | case 3: 130 | green -= 2; 131 | if (green == 255) 132 | { 133 | green = 0; 134 | colorState = 4; 135 | } 136 | break; 137 | case 4: 138 | red++; 139 | if (red == 32) 140 | { 141 | red = 31; 142 | colorState = 5; 143 | } 144 | break; 145 | case 5: 146 | blue--; 147 | if (blue == 255) 148 | { 149 | blue = 0; 150 | colorState = 0; 151 | } 152 | break; 153 | } 154 | 155 | color = red << 11 | green << 5 | blue; 156 | 157 | if (color == BACKGROUND_COLOR) 158 | color++; 159 | } 160 | 161 | Point getXYIndividualValues(uint32_t xy) 162 | { 163 | // The 16 bit x/y values are stored as one 32 bit concatenation. Get the individual x/y values. 164 | uint16_t pixelXCol = (uint16_t)((xy & 0xFFFF0000) >> 16); 165 | uint16_t pixelYRow = (uint16_t)(xy & 0x0000FFFF); 166 | 167 | return Point(pixelXCol, pixelYRow); 168 | } 169 | 170 | uint32_t getXYCombinedValue(uint16_t x, uint16_t y) 171 | { 172 | return ((uint32_t)x << 16) | (uint32_t)(y); 173 | } 174 | 175 | // Convert scaled pixel to native pixel area and then draw it. 176 | void drawScaledPixel(int16_t x, int16_t y, uint16_t color) 177 | { 178 | // Scale 179 | int16_t scaledXCol = x * PIXEL_WIDTH; 180 | int16_t scaledYRow = y * PIXEL_WIDTH; 181 | 182 | for (uint16_t i = 0; i < PIXEL_WIDTH; i++) 183 | { 184 | for (uint16_t j = 0; j < PIXEL_WIDTH; j++) 185 | { 186 | tft.drawPixel(scaledXCol + i, scaledYRow + j, color); 187 | } 188 | } 189 | } 190 | 191 | bool isPixelSlotAvailable(uint32_t xy) 192 | { 193 | auto point = getXYIndividualValues(xy); 194 | return withinScaledRows(point.YRow) && withinScaledCols(point.XCol) && pixelStates.find(xy) == pixelStates.end() && point.YRow < landedPixelsColumnTops[point.XCol]; 195 | } 196 | 197 | void updateLandedPixelsColumnTops(uint32_t xyLanded) 198 | { 199 | // landedPixelsColumnTops stores the highest row (Y) where a pixel stopped for each column (X). 200 | 201 | auto point = getXYIndividualValues(xyLanded); 202 | 203 | landedPixelsColumnTops[point.XCol] = std::min(landedPixelsColumnTops[point.XCol], point.YRow); 204 | } 205 | 206 | bool canPixelFall(uint32_t xyKey) 207 | { 208 | // The 16 bit x/y values are stored as one 32 bit concatenation. Get the individual x/y values. 209 | uint16_t pixelYRow = (uint16_t)(xyKey & 0x0000FFFF); 210 | 211 | uint16_t pixelYRowNext = pixelYRow + 1; 212 | 213 | if (!withinScaledRows(pixelYRowNext)) 214 | return false; 215 | 216 | uint16_t pixelXCol = (uint16_t)((xyKey & 0xFFFF0000) >> 16); 217 | uint16_t pixelXColMinus = pixelXCol - 1; 218 | uint16_t pixelXColPlus = pixelXCol + 1; 219 | 220 | return (withinScaledCols(pixelXColMinus) && landedPixelsColumnTops[pixelXColMinus] > pixelYRowNext) || 221 | (withinScaledCols(pixelXCol) && landedPixelsColumnTops[pixelXCol] > pixelYRowNext) || 222 | (withinScaledCols(pixelXColPlus) && landedPixelsColumnTops[pixelXColPlus] > pixelYRowNext); 223 | } 224 | 225 | void movePixels(std::unordered_map::iterator _pixelStatesIteratorBegin, std::unordered_map::iterator _pixelStatesIteratorEnd) 226 | { 227 | // Iterate moving pixels and move them as needed. 228 | for (auto iter = _pixelStatesIteratorBegin; iter != _pixelStatesIteratorEnd; iter++) 229 | { 230 | auto keyVal = *iter; 231 | auto pixelKey = keyVal.first; 232 | auto thisPixelState = keyVal.second; 233 | 234 | // The 16 bit x/y values are stored as one 32 bit concatenation. Get the individual x/y values. 235 | auto pixelPoint = getXYIndividualValues(pixelKey); 236 | uint16_t pixelXCol = pixelPoint.XCol; 237 | uint16_t pixelYRow = pixelPoint.YRow; 238 | 239 | auto pixelState = keyVal.second.State; 240 | if (pixelState == GRID_STATE_NEW) 241 | { 242 | pixelStates[pixelKey].State = GRID_STATE_FALLING; 243 | continue; 244 | } 245 | 246 | auto pixelColor = thisPixelState.Color; 247 | auto pixelVelocity = thisPixelState.Velocity; 248 | 249 | bool moved = false; 250 | 251 | uint16_t newMaxYRowPos = uint16_t(pixelYRow + pixelVelocity); 252 | for (int16_t yRowPos = newMaxYRowPos; yRowPos > pixelYRow; yRowPos--) 253 | { 254 | if (!withinScaledRows(yRowPos)) 255 | { 256 | continue; 257 | } 258 | 259 | uint32_t belowXY = getXYCombinedValue(pixelXCol, yRowPos); 260 | 261 | int16_t direction = 1; 262 | if (random(100) < 50) 263 | { 264 | direction *= -1; 265 | } 266 | 267 | uint32_t belowXY_A = -1; 268 | uint16_t belowXY_A_XCol = pixelXCol + direction; 269 | uint32_t belowXY_B = -1; 270 | uint16_t belowXY_B_XCol = pixelXCol - direction; 271 | 272 | if (withinScaledCols(belowXY_A_XCol)) 273 | { 274 | belowXY_A = getXYCombinedValue(belowXY_A_XCol, yRowPos); 275 | } 276 | if (withinScaledCols(belowXY_B_XCol)) 277 | { 278 | belowXY_B = getXYCombinedValue(belowXY_B_XCol, yRowPos); 279 | } 280 | 281 | if (isPixelSlotAvailable(belowXY) && pixelStatesToAdd.find(belowXY) == pixelStatesToAdd.end()) 282 | { 283 | // This pixel will go straight down. 284 | pixelStatesToAdd[belowXY] = PointState(pixelXCol, yRowPos, GRID_STATE_FALLING, pixelColor, pixelVelocity + gravity); 285 | 286 | if (xSemaphoreTake(xStateMutex, portMAX_DELAY)) 287 | { 288 | drawScaledPixel(pixelXCol, pixelYRow, BACKGROUND_COLOR); // Out with the old. 289 | drawScaledPixel(pixelXCol, yRowPos, pixelColor); // In with the new. 290 | 291 | pixelsToErase.insert(pixelKey); 292 | pixelsToErase.erase(belowXY); 293 | 294 | xSemaphoreGive(xStateMutex); 295 | } 296 | 297 | moved = true; 298 | break; 299 | } 300 | else if (isPixelSlotAvailable(belowXY_A) && pixelStatesToAdd.find(belowXY_A) == pixelStatesToAdd.end()) 301 | { 302 | // This pixel will fall to side A (right) 303 | pixelStatesToAdd[belowXY_A] = PointState(belowXY_A_XCol, yRowPos, GRID_STATE_FALLING, pixelColor, pixelVelocity + gravity); 304 | 305 | if (xSemaphoreTake(xStateMutex, portMAX_DELAY)) 306 | { 307 | drawScaledPixel(pixelXCol, pixelYRow, BACKGROUND_COLOR); // Out with the old. 308 | drawScaledPixel(belowXY_A_XCol, yRowPos, pixelColor); // In with the new. 309 | 310 | pixelsToErase.insert(pixelKey); 311 | pixelsToErase.erase(belowXY_A); 312 | xSemaphoreGive(xStateMutex); 313 | } 314 | 315 | moved = true; 316 | break; 317 | } 318 | else if (isPixelSlotAvailable(belowXY_B) && pixelStatesToAdd.find(belowXY_B) == pixelStatesToAdd.end()) 319 | { 320 | // This pixel will fall to side B (left) 321 | pixelStatesToAdd[belowXY_B] = PointState(belowXY_B_XCol, yRowPos, GRID_STATE_FALLING, pixelColor, pixelVelocity + gravity); 322 | 323 | if (xSemaphoreTake(xStateMutex, portMAX_DELAY)) 324 | { 325 | drawScaledPixel(pixelXCol, pixelYRow, BACKGROUND_COLOR); // Out with the old. 326 | drawScaledPixel(belowXY_B_XCol, yRowPos, pixelColor); // In with the new. 327 | 328 | pixelsToErase.insert(pixelKey); 329 | pixelsToErase.erase(belowXY_B); 330 | 331 | xSemaphoreGive(xStateMutex); 332 | } 333 | 334 | moved = true; 335 | break; 336 | } 337 | } 338 | 339 | if (!moved) 340 | { 341 | thisPixelState.Velocity += gravity; 342 | } 343 | 344 | if (!moved && !canPixelFall(pixelKey)) 345 | { 346 | if (xSemaphoreTake(xStateMutex, portMAX_DELAY)) 347 | { 348 | pixelsToErase.insert(pixelKey); 349 | updateLandedPixelsColumnTops(pixelKey); 350 | xSemaphoreGive(xStateMutex); 351 | } 352 | } 353 | } 354 | } 355 | 356 | void task1(void *pvParameters) 357 | { 358 | auto coreId = xPortGetCoreID(); 359 | 360 | while (1) 361 | { 362 | if (xSemaphoreTake(xSemaphore1, portMAX_DELAY)) 363 | { 364 | auto pixelStatesBegin = pixelStates.begin(); 365 | auto pixelStatesMid = std::next(pixelStatesBegin, (pixelStates.size() / 2)); 366 | auto pixelStatesEnd = pixelStates.end(); 367 | 368 | movePixels(pixelStatesMid, pixelStatesEnd); 369 | 370 | xSemaphoreGive(xSemaphore2); // release the mutex 371 | } 372 | } 373 | } 374 | 375 | void setup() 376 | { 377 | // Serial.begin(115200); 378 | 379 | // Start the SPI for the touch screen and init the TS library 380 | // Serial.println("Init display..."); 381 | mySpi.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS); 382 | ts.begin(mySpi); 383 | ts.setRotation(1); 384 | 385 | // Start the tft display and set it to black 386 | tft.init(); 387 | tft.setRotation(1); 388 | tft.fillScreen(TFT_BLACK); 389 | 390 | colorChangeTime = millis() + 1000; 391 | 392 | // Set the tallest pixel column to be 1 slot bellow the "bottom" (0,0 coordinate being top left.) 393 | for (int16_t xCol = 0; xCol < SCALED_COLS; xCol++) 394 | landedPixelsColumnTops[xCol] = SCALED_ROWS; 395 | 396 | auto viewportWidth = tft.getViewportWidth(); 397 | auto viewportHeight = tft.getViewportHeight(); 398 | 399 | // Serial.printf("Viewport dimensions (W x H): %i x %i\n", viewportWidth, viewportHeight); 400 | 401 | xStateMutex = xSemaphoreCreateMutex(); 402 | xSemaphore1 = xSemaphoreCreateCounting(1, 0); 403 | xSemaphore2 = xSemaphoreCreateCounting(1, 0); 404 | 405 | xTaskCreatePinnedToCore( 406 | task1, // Function to implement the task 407 | "task1", // Name of the task 408 | 4096, // Stack size in words 409 | NULL, // Task input parameter 410 | 1, // Priority of the task 411 | NULL, // Task handle. 412 | 0 // Core where the task should run 413 | ); 414 | 415 | delay(500); 416 | } 417 | 418 | void loop() 419 | { 420 | unsigned long currentMillis = millis(); 421 | 422 | // Throttle FPS 423 | unsigned long diffMillis = currentMillis - lastMillis; 424 | if ((1000 / maxFps) > diffMillis) 425 | { 426 | return; 427 | } 428 | 429 | // Get frame rate. 430 | fps = 1000 / max(currentMillis - lastMillis, 1UL); 431 | sprintf(fpsStringBuffer, "fps:%4lu", fps); 432 | 433 | // Display frame rate 434 | tft.setTextColor(TFT_WHITE, TFT_BLACK); 435 | tft.drawString(fpsStringBuffer, 0, 0); 436 | 437 | // uint32_t psramSize = ESP.getPsramSize(); 438 | // uint32_t freePsram = ESP.getFreePsram(); 439 | // sprintf(fpsStringBuffer, "psram: %6u / %6u", freePsram, psramSize); 440 | // tft.drawString(fpsStringBuffer, 0, 10); 441 | 442 | // uint32_t heapSize = ESP.getHeapSize(); 443 | // uint32_t freeHeap = ESP.getFreeHeap(); 444 | // sprintf(fpsStringBuffer, "heap: %6u / %6u", freeHeap, heapSize); 445 | // tft.drawString(fpsStringBuffer, 0, 20); 446 | 447 | // uint32_t minFreeHeap = ESP.getMinFreeHeap(); 448 | // sprintf(fpsStringBuffer, "min free heap: %6u", minFreeHeap); 449 | // tft.drawString(fpsStringBuffer, 0, 30); 450 | 451 | lastMillis = currentMillis; 452 | 453 | // Handle touch. 454 | if (ts.tirqTouched() && ts.touched()) 455 | { 456 | TS_Point p = ts.getPoint(); 457 | 458 | inputX = map(p.x, TS_MINX, TS_MAXX, 0, SCALED_COLS); 459 | inputY = map(p.y, TS_MINY, TS_MAXY, 0, SCALED_ROWS); 460 | } 461 | else 462 | { 463 | inputX = -1; 464 | inputY = -1; 465 | } 466 | 467 | if (withinNativeCols(inputX) && withinNativeRows(inputY)) 468 | { 469 | // Randomly add an area of pixels 470 | int16_t halfInputWidth = inputWidth / 2; 471 | for (int16_t i = -halfInputWidth; i <= halfInputWidth; ++i) 472 | { 473 | for (int16_t j = -halfInputWidth; j <= halfInputWidth; ++j) 474 | { 475 | if (random(100) < percentInputFill) 476 | { 477 | uint16_t xCol = inputX + i; 478 | uint16_t yRow = inputY + j; 479 | 480 | // Concat the 16 bit x/y values into a single 32 bit value for more efficient storage. 481 | uint32_t xy = ((uint32_t)xCol << 16) | (uint32_t)yRow; 482 | 483 | if (withinNativeCols(xCol) && withinNativeRows(yRow) && pixelStates.find(xy) == pixelStates.end()) 484 | { 485 | pixelStates[xy].Color = color; 486 | pixelStates[xy].State = GRID_STATE_NEW; 487 | pixelStates[xy].Velocity = (uint8_t)1; 488 | 489 | drawScaledPixel(xCol, yRow, color); 490 | } 491 | } 492 | } 493 | } 494 | } 495 | 496 | // Change the color of the pixels over time 497 | if (colorChangeTime < millis()) 498 | { 499 | colorChangeTime = millis() + colorChangeFrequencyMs; 500 | setNextColor(); 501 | } 502 | 503 | // Split up the work. 504 | 505 | auto pixelStatesBegin = pixelStates.begin(); 506 | auto pixelStatesMid = std::next(pixelStatesBegin, (pixelStates.size() / 2)); 507 | 508 | pixelStatesToAdd.clear(); 509 | pixelsToErase.clear(); 510 | 511 | // Start task and proceed. 512 | xSemaphoreGive(xSemaphore1); 513 | 514 | movePixels(pixelStatesBegin, pixelStatesMid); 515 | 516 | // Wait for task to complete. 517 | xSemaphoreTake(xSemaphore2, portMAX_DELAY); 518 | 519 | for (const auto &key : pixelsToErase) 520 | { 521 | if (pixelStatesToAdd.find(key) != pixelStatesToAdd.end()) 522 | continue; // Do not erase pixel if it is being back-filled. 523 | 524 | pixelStates.erase(key); 525 | } 526 | 527 | for (const auto &keyVal : pixelStatesToAdd) 528 | { 529 | pixelStates[keyVal.first] = PointState(keyVal.second.XCol, keyVal.second.YRow, keyVal.second.State, keyVal.second.Color, keyVal.second.Velocity); 530 | } 531 | } 532 | -------------------------------------------------------------------------------- /projects/sand/PixelState.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static const uint8_t GRID_STATE_NEW = 1; 4 | static const uint8_t GRID_STATE_FALLING = 2; 5 | 6 | struct PointState 7 | { 8 | uint16_t XCol; 9 | uint16_t YRow; 10 | uint16_t State; 11 | uint16_t Color; 12 | uint8_t Velocity; 13 | 14 | PointState() {} 15 | 16 | PointState(uint16_t x, uint16_t y, uint16_t state, uint16_t color, uint8_t velocity) 17 | { 18 | XCol = x; 19 | YRow = y; 20 | State = state; 21 | Color = color; 22 | Velocity = velocity; 23 | } 24 | }; 25 | 26 | struct Point 27 | { 28 | uint16_t XCol; 29 | uint16_t YRow; 30 | 31 | Point() {} 32 | 33 | Point(uint16_t x, uint16_t y) 34 | { 35 | XCol = x; 36 | YRow = y; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /projects/sand/README.md: -------------------------------------------------------------------------------- 1 | # Sand 2 | 3 | A project I made to drop sand (or "pixels") randomly from the touch point of the touchscreen and watch them fall into piles. New pixels color cycles over time. 4 | 5 | You can see a Youtube video of the code in action here (This is the [multi-task version](../sand-multi-task)): 6 | 7 | [![Sand Simulation on the Cheap Yellow Display (CYD) ESP32 MCU + Display](https://img.youtube.com/vi/j8XRMEEZ0gM/0.jpg)](https://www.youtube.com/watch?v=j8XRMEEZ0gM) 8 | -------------------------------------------------------------------------------- /projects/sand/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "PixelState.h" 8 | 9 | #define XPT2046_IRQ 36 10 | #define XPT2046_MOSI 32 11 | #define XPT2046_MISO 39 12 | #define XPT2046_CLK 25 13 | #define XPT2046_CS 33 14 | 15 | // The following min/max axis values were gathered through empirical data gathering on the specific board I have. 16 | #define TS_MINX 280 17 | #define TS_MAXX 3750 18 | #define TS_MINY 280 19 | #define TS_MAXY 3750 20 | 21 | ///////////////////////////////////////////////////// 22 | // You can adjust the following "subjective" params: 23 | // static const int8_t PIXEL_WIDTH = 3; 24 | // uint8_t percentInputFill = 20; 25 | // uint8_t inputWidth = 6; 26 | static const int8_t PIXEL_WIDTH = 2; 27 | static const uint8_t percentInputFill = 10; 28 | static const uint8_t inputWidth = 10; 29 | static const uint8_t gravity = 1; 30 | static const unsigned long maxFps = 30; 31 | static const unsigned long colorChangeFrequencyMs = 250; 32 | // End "subjective" params. 33 | ///////////////////////////////////////////////////// 34 | 35 | static const int16_t NATIVE_ROWS = 240; 36 | static const int16_t NATIVE_COLS = 320; 37 | static const int16_t SCALED_ROWS = NATIVE_ROWS / PIXEL_WIDTH; 38 | static const int16_t SCALED_COLS = NATIVE_COLS / PIXEL_WIDTH; 39 | 40 | int16_t BACKGROUND_COLOR = TFT_BLACK; 41 | 42 | SPIClass mySpi = SPIClass(VSPI); 43 | XPT2046_Touchscreen ts(XPT2046_CS, XPT2046_IRQ); 44 | TFT_eSPI tft = TFT_eSPI(); 45 | 46 | // 16-bit color representation: 47 | //------------------------------------ 48 | // Red ⏐ Green ⏐ Blue 49 | // 1 1 1 1 1 ⏐ 1 1 1 1 1 0 ⏐ 0 0 0 0 0 50 | // = 31 ⏐ = 62 ⏐ = 0 51 | // 52 | // ((31 << 11) | (62 << 5) | 0) = 65472 = 0xffc0 53 | 54 | byte red = 31; 55 | byte green = 0; 56 | byte blue = 0; 57 | byte colorState = 0; 58 | uint16_t color = red << 11; 59 | 60 | unsigned long colorChangeTime = 0; 61 | 62 | int16_t inputX = -1; 63 | int16_t inputY = -1; 64 | 65 | // pixelStates key is a concat of the 16 bit x/y values into a single 32 bit value for more efficient storage. 66 | std::unordered_map pixelStates; // [X,Y]:[STATE_DATA] 67 | std::unordered_map landedPixelsColumnTops; // [X]:[Y] // For each column (X), what is the highest row (Y) where a pixel stopped. 68 | 69 | long lastMillis = 0; 70 | int fps = 0; 71 | char fpsStringBuffer[32]; 72 | 73 | bool withinNativeCols(int16_t value) 74 | { 75 | return value >= 0 && value <= NATIVE_COLS - 1; 76 | } 77 | 78 | bool withinNativeRows(int16_t value) 79 | { 80 | return value >= 0 && value <= NATIVE_ROWS - 1; 81 | } 82 | 83 | bool withinScaledCols(int16_t value) 84 | { 85 | return value >= 0 && value <= SCALED_COLS - 1; 86 | } 87 | 88 | bool withinScaledRows(int16_t value) 89 | { 90 | return value >= 0 && value <= SCALED_ROWS - 1; 91 | } 92 | 93 | // Color changing state machine 94 | void setNextColor() 95 | { 96 | switch (colorState) 97 | { 98 | case 0: 99 | green += 2; 100 | if (green == 64) 101 | { 102 | green = 63; 103 | colorState = 1; 104 | } 105 | break; 106 | case 1: 107 | red--; 108 | if (red == 255) 109 | { 110 | red = 0; 111 | colorState = 2; 112 | } 113 | break; 114 | case 2: 115 | blue++; 116 | if (blue == 32) 117 | { 118 | blue = 31; 119 | colorState = 3; 120 | } 121 | break; 122 | case 3: 123 | green -= 2; 124 | if (green == 255) 125 | { 126 | green = 0; 127 | colorState = 4; 128 | } 129 | break; 130 | case 4: 131 | red++; 132 | if (red == 32) 133 | { 134 | red = 31; 135 | colorState = 5; 136 | } 137 | break; 138 | case 5: 139 | blue--; 140 | if (blue == 255) 141 | { 142 | blue = 0; 143 | colorState = 0; 144 | } 145 | break; 146 | } 147 | 148 | color = red << 11 | green << 5 | blue; 149 | 150 | if (color == BACKGROUND_COLOR) 151 | color++; 152 | } 153 | 154 | Point getXYIndividualValues(uint32_t xy) 155 | { 156 | // The 16 bit x/y values are stored as one 32 bit concatenation. Get the individual x/y values. 157 | uint16_t pixelXCol = (uint16_t)((xy & 0xFFFF0000) >> 16); 158 | uint16_t pixelYRow = (uint16_t)(xy & 0x0000FFFF); 159 | 160 | return Point(pixelXCol, pixelYRow); 161 | } 162 | 163 | uint32_t getXYCombinedValue(uint16_t x, uint16_t y) 164 | { 165 | return ((uint32_t)x << 16) | (uint32_t)(y); 166 | } 167 | 168 | // Convert scaled pixel to native pixel area and then draw it. 169 | void drawScaledPixel(int16_t x, int16_t y, uint16_t color) 170 | { 171 | // Scale 172 | int16_t scaledXCol = x * PIXEL_WIDTH; 173 | int16_t scaledYRow = y * PIXEL_WIDTH; 174 | 175 | for (uint16_t i = 0; i < PIXEL_WIDTH; i++) 176 | { 177 | for (uint16_t j = 0; j < PIXEL_WIDTH; j++) 178 | { 179 | tft.drawPixel(scaledXCol + i, scaledYRow + j, color); 180 | } 181 | } 182 | } 183 | 184 | bool isPixelSlotAvailable(uint32_t xy) 185 | { 186 | auto point = getXYIndividualValues(xy); 187 | return withinScaledRows(point.YRow) && withinScaledCols(point.XCol) && pixelStates.find(xy) == pixelStates.end() && point.YRow < landedPixelsColumnTops[point.XCol]; 188 | } 189 | 190 | void updateLandedPixelsColumnTops(uint32_t xyLanded) 191 | { 192 | // landedPixelsColumnTops stores the highest row (Y) where a pixel stopped for each column (X). 193 | 194 | auto point = getXYIndividualValues(xyLanded); 195 | 196 | landedPixelsColumnTops[point.XCol] = std::min(landedPixelsColumnTops[point.XCol], point.YRow); 197 | } 198 | 199 | bool canPixelFall(uint32_t xyKey) 200 | { 201 | // The 16 bit x/y values are stored as one 32 bit concatenation. Get the individual x/y values. 202 | uint16_t pixelYRow = (uint16_t)(xyKey & 0x0000FFFF); 203 | 204 | uint16_t pixelYRowNext = pixelYRow + 1; 205 | 206 | if (!withinScaledRows(pixelYRowNext)) 207 | return false; 208 | 209 | uint16_t pixelXCol = (uint16_t)((xyKey & 0xFFFF0000) >> 16); 210 | uint16_t pixelXColMinus = pixelXCol - 1; 211 | uint16_t pixelXColPlus = pixelXCol + 1; 212 | 213 | return (withinScaledCols(pixelXColMinus) && landedPixelsColumnTops[pixelXColMinus] > pixelYRowNext) || 214 | (withinScaledCols(pixelXCol) && landedPixelsColumnTops[pixelXCol] > pixelYRowNext) || 215 | (withinScaledCols(pixelXColPlus) && landedPixelsColumnTops[pixelXColPlus] > pixelYRowNext); 216 | } 217 | 218 | void setup() 219 | { 220 | // Serial.begin(115200); 221 | 222 | // Start the SPI for the touch screen and init the TS library 223 | Serial.println("Init display..."); 224 | mySpi.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS); 225 | ts.begin(mySpi); 226 | ts.setRotation(1); 227 | 228 | // Start the tft display and set it to black 229 | tft.init(); 230 | tft.setRotation(1); 231 | tft.fillScreen(TFT_BLACK); 232 | 233 | colorChangeTime = millis() + 1000; 234 | 235 | // Set the tallest pixel column to be 1 slot bellow the "bottom" (0,0 coordinate being top left.) 236 | for (int16_t xCol = 0; xCol < SCALED_COLS; xCol++) 237 | landedPixelsColumnTops[xCol] = SCALED_ROWS; 238 | 239 | auto viewportWidth = tft.getViewportWidth(); 240 | auto viewportHeight = tft.getViewportHeight(); 241 | 242 | Serial.printf("Viewport dimensions (W x H): %i x %i\n", viewportWidth, viewportHeight); 243 | 244 | delay(500); 245 | } 246 | 247 | void loop() 248 | { 249 | unsigned long currentMillis = millis(); 250 | 251 | // Throttle FPS 252 | unsigned long diffMillis = currentMillis - lastMillis; 253 | if ((1000 / maxFps) > diffMillis) 254 | { 255 | return; 256 | } 257 | 258 | // Get frame rate. 259 | fps = 1000 / max(currentMillis - lastMillis, 1UL); 260 | sprintf(fpsStringBuffer, "fps:%4lu", fps); 261 | 262 | // Display frame rate 263 | tft.setTextColor(TFT_WHITE, TFT_BLACK); 264 | tft.drawString(fpsStringBuffer, 0, 0); 265 | 266 | // uint32_t psramSize = ESP.getPsramSize(); 267 | // uint32_t freePsram = ESP.getFreePsram(); 268 | // sprintf(fpsStringBuffer, "psram: %6u / %6u", freePsram, psramSize); 269 | // tft.drawString(fpsStringBuffer, 0, 10); 270 | 271 | // uint32_t heapSize = ESP.getHeapSize(); 272 | // uint32_t freeHeap = ESP.getFreeHeap(); 273 | // sprintf(fpsStringBuffer, "heap: %6u / %6u", freeHeap, heapSize); 274 | // tft.drawString(fpsStringBuffer, 0, 20); 275 | 276 | // uint32_t minFreeHeap = ESP.getMinFreeHeap(); 277 | // sprintf(fpsStringBuffer, "min free heap: %6u", minFreeHeap); 278 | // tft.drawString(fpsStringBuffer, 0, 30); 279 | 280 | lastMillis = currentMillis; 281 | 282 | // Handle touch. 283 | if (ts.tirqTouched() && ts.touched()) 284 | { 285 | TS_Point p = ts.getPoint(); 286 | 287 | inputX = map(p.x, TS_MINX, TS_MAXX, 0, SCALED_COLS); 288 | inputY = map(p.y, TS_MINY, TS_MAXY, 0, SCALED_ROWS); 289 | } 290 | else 291 | { 292 | inputX = -1; 293 | inputY = -1; 294 | } 295 | 296 | if (withinNativeCols(inputX) && withinNativeRows(inputY)) 297 | { 298 | // Randomly add an area of pixels 299 | int16_t halfInputWidth = inputWidth / 2; 300 | for (int16_t i = -halfInputWidth; i <= halfInputWidth; ++i) 301 | { 302 | for (int16_t j = -halfInputWidth; j <= halfInputWidth; ++j) 303 | { 304 | if (random(100) < percentInputFill) 305 | { 306 | uint16_t xCol = inputX + i; 307 | uint16_t yRow = inputY + j; 308 | 309 | // Concat the 16 bit x/y values into a single 32 bit value for more efficient storage. 310 | uint32_t xy = ((uint32_t)xCol << 16) | (uint32_t)yRow; 311 | 312 | if (withinNativeCols(xCol) && withinNativeRows(yRow) && pixelStates.find(xy) == pixelStates.end()) 313 | { 314 | pixelStates[xy].Color = color; 315 | pixelStates[xy].State = GRID_STATE_NEW; 316 | pixelStates[xy].Velocity = (uint8_t)1; 317 | 318 | drawScaledPixel(xCol, yRow, color); 319 | } 320 | } 321 | } 322 | } 323 | } 324 | 325 | // Change the color of the pixels over time 326 | if (colorChangeTime < millis()) 327 | { 328 | colorChangeTime = millis() + colorChangeFrequencyMs; 329 | setNextColor(); 330 | } 331 | 332 | std::unordered_set pixelsToErase; // [X,Y] 333 | std::unordered_map pixelStatesToAdd; // [X,Y]:[STATE_DATA] 334 | 335 | // Iterate moving pixels and move them as needed. 336 | for (const auto &keyVal : pixelStates) 337 | { 338 | auto pixelKey = keyVal.first; 339 | 340 | // The 16 bit x/y values are stored as one 32 bit concatenation. Get the individual x/y values. 341 | auto pixelPoint = getXYIndividualValues(pixelKey); 342 | uint16_t pixelXCol = pixelPoint.XCol; 343 | uint16_t pixelYRow = pixelPoint.YRow; 344 | 345 | auto pixelState = keyVal.second.State; 346 | if (pixelState == GRID_STATE_NEW) 347 | { 348 | pixelStates[pixelKey].State = GRID_STATE_FALLING; 349 | continue; 350 | } 351 | 352 | auto pixelColor = pixelStates[pixelKey].Color; 353 | auto pixelVelocity = pixelStates[pixelKey].Velocity; 354 | 355 | bool moved = false; 356 | 357 | uint16_t newMaxYRowPos = uint16_t(pixelYRow + pixelVelocity); 358 | for (int16_t yRowPos = newMaxYRowPos; yRowPos > pixelYRow; yRowPos--) 359 | { 360 | if (!withinScaledRows(yRowPos)) 361 | { 362 | continue; 363 | } 364 | 365 | uint32_t belowXY = getXYCombinedValue(pixelXCol, yRowPos); 366 | 367 | int16_t direction = 1; 368 | if (random(100) < 50) 369 | { 370 | direction *= -1; 371 | } 372 | 373 | uint32_t belowXY_A = -1; 374 | uint16_t belowXY_A_XCol = pixelXCol + direction; 375 | uint32_t belowXY_B = -1; 376 | uint16_t belowXY_B_XCol = pixelXCol - direction; 377 | 378 | if (withinScaledCols(belowXY_A_XCol)) 379 | { 380 | belowXY_A = getXYCombinedValue(belowXY_A_XCol, yRowPos); 381 | } 382 | if (withinScaledCols(belowXY_B_XCol)) 383 | { 384 | belowXY_B = getXYCombinedValue(belowXY_B_XCol, yRowPos); 385 | } 386 | 387 | if (isPixelSlotAvailable(belowXY) && pixelStatesToAdd.find(belowXY) == pixelStatesToAdd.end()) 388 | { 389 | // This pixel will go straight down. 390 | pixelStatesToAdd[belowXY] = PointState(pixelXCol, yRowPos, GRID_STATE_FALLING, pixelColor, pixelVelocity + gravity); 391 | 392 | drawScaledPixel(pixelXCol, pixelYRow, BACKGROUND_COLOR); // Out with the old. 393 | drawScaledPixel(pixelXCol, yRowPos, pixelColor); // In with the new. 394 | 395 | pixelsToErase.insert(pixelKey); 396 | pixelsToErase.erase(belowXY); 397 | 398 | moved = true; 399 | break; 400 | } 401 | else if (isPixelSlotAvailable(belowXY_A) && pixelStatesToAdd.find(belowXY_A) == pixelStatesToAdd.end()) 402 | { 403 | // This pixel will fall to side A (right) 404 | pixelStatesToAdd[belowXY_A] = PointState(belowXY_A_XCol, yRowPos, GRID_STATE_FALLING, pixelColor, pixelVelocity + gravity); 405 | 406 | drawScaledPixel(pixelXCol, pixelYRow, BACKGROUND_COLOR); // Out with the old. 407 | drawScaledPixel(belowXY_A_XCol, yRowPos, pixelColor); // In with the new. 408 | 409 | pixelsToErase.insert(pixelKey); 410 | pixelsToErase.erase(belowXY_A); 411 | 412 | moved = true; 413 | break; 414 | } 415 | else if (isPixelSlotAvailable(belowXY_B) && pixelStatesToAdd.find(belowXY_B) == pixelStatesToAdd.end()) 416 | { 417 | // This pixel will fall to side B (left) 418 | pixelStatesToAdd[belowXY_B] = PointState(belowXY_B_XCol, yRowPos, GRID_STATE_FALLING, pixelColor, pixelVelocity + gravity); 419 | 420 | drawScaledPixel(pixelXCol, pixelYRow, BACKGROUND_COLOR); // Out with the old. 421 | drawScaledPixel(belowXY_B_XCol, yRowPos, pixelColor); // In with the new. 422 | 423 | pixelsToErase.insert(pixelKey); 424 | pixelsToErase.erase(belowXY_B); 425 | 426 | moved = true; 427 | break; 428 | } 429 | } 430 | 431 | if (!moved) 432 | { 433 | // Give new pixels a chance to fall. 434 | pixelStates[pixelKey].Velocity += gravity; 435 | } 436 | 437 | if (!moved && !canPixelFall(pixelKey)) 438 | { 439 | pixelsToErase.insert(pixelKey); 440 | updateLandedPixelsColumnTops(pixelKey); 441 | } 442 | } 443 | 444 | for (const auto &key : pixelsToErase) 445 | { 446 | if (pixelStatesToAdd.find(key) != pixelStatesToAdd.end()) 447 | continue; // Do not erase pixel if it is being back-filled. 448 | 449 | pixelStates.erase(key); 450 | } 451 | 452 | for (const auto &keyVal : pixelStatesToAdd) 453 | { 454 | pixelStates[keyVal.first] = PointState(keyVal.second.XCol, keyVal.second.YRow, keyVal.second.State, keyVal.second.Color, keyVal.second.Velocity); 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------