|
| 1 | +# SeedStudio SenseCAP Indicator D1 |
| 2 | + |
| 3 | +The device is a 4-Inch Touch Screen IoT development platform powered by ESP32S3 & RP2040. It has variants with LoRa support, and Air Quality support. |
| 4 | + |
| 5 | + |
| 6 | + |
| 7 | +Link to purchase the device [SeedStudio web site](https://www.seeedstudio.com/SenseCAP-Indicator-D1L-p-5646.html) |
| 8 | + |
| 9 | +There are variants, all sharing the following features: |
| 10 | + |
| 11 | +- ESP32S3 with 8MB Flash and 8MB PSRAM |
| 12 | +- 4 inch 480 x 480 pixels display, ST7701 controller, connected in parallel 8 bits mode to ESP32S3 for maximum display speed |
| 13 | +- Capacitive Touchscreen, FT5x06 controller |
| 14 | +- SD Card connector |
| 15 | +- Dual I2C Groove connectors |
| 16 | +- Dual USB-C connectors below and in the back of the device |
| 17 | +- Buzzer MLT-8530, Resonant Frequency:2700Hz |
| 18 | +- Also contains a RP2040 MCU, Dual ARM Cortex-M0+ up to 133MHz, 2MB of Flash |
| 19 | + |
| 20 | +We will focus below on the "SenseCAP Indicator D1L" which includes: |
| 21 | + |
| 22 | +- internal SGP41 tVOC Air Quality Sensor (Range: 0-40000ppm, Accuracy: 400ppm - 5000ppm ±(50ppm+5% of reading)) |
| 23 | +- internal SCD40 CO2 Carbon Dioxid Sensors (Range: 1-500 VOC Index Points) |
| 24 | +- external AHT20 Temperature and Humidity sensor (Range: -40 ~ + 85 ℃/± 0.3 ℃; 0 ~ 100% RH/± 2% RH (25 ℃)) |
| 25 | + |
| 26 | + |
| 27 | +## ESP32S3 build |
| 28 | + |
| 29 | +The device requires a self-compile with the following options: |
| 30 | + |
| 31 | +- Compile with an environment that uses `board = esp32s3-qio_opi_120`, which enables Quad SPI Flash and Octal SPI PSRAM at 120MHz. |
| 32 | +- Enable the following options: `USE_SDCARD`, `USE_I2C_SERIAL`, `USE_AHT2x`, `USE_SGP4X`, `USE_SCD40`, `USE_I2C`, `USE_SPI`, `USE_LVGL`, `USE_DISPLAY_LVGL_ONLY`, `USE_DISPLAY`, `USE_UNIVERSAL_TOUCH`, `USE_UNIVERSAL_DISPLAY` |
| 33 | + |
| 34 | +TODO: have a readu-to-use `platformio_override.ini` template. |
| 35 | + |
| 36 | +## Configure GPIOs and LVGL Display |
| 37 | + |
| 38 | +Use: |
| 39 | + |
| 40 | +``` |
| 41 | +Template {"NAME":"SenseCAP Indicator D1","GPIO":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11488,11520,0,6210,0,0,0,0,32,641,609,0,1,1,1,0,1,0,0],"FLAG":0,"BASE":1} |
| 42 | +Module 0 |
| 43 | +``` |
| 44 | + |
| 45 | +Add the following content in `display.ini` on the device file-system: |
| 46 | + |
| 47 | +``` |
| 48 | +:H,ST7701,480,480,16,RGB,18,17,16,21,45,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,6 |
| 49 | +:V,1,10,8,50,1,10,8,20,0 |
| 50 | +:S,2,1,1,0,40,20 |
| 51 | +:IS,41,48,-1,-1 |
| 52 | +FF,5,77,01,00,00,10 |
| 53 | +C0,2,3B,00 |
| 54 | +C1,2,0D,02 |
| 55 | +C2,2,31,05 |
| 56 | +C7,1,04 |
| 57 | +CD,1,08 |
| 58 | +B0,10,00,11,18,0E,11,06,07,08,07,22,04,12,0F,AA,31,18 |
| 59 | +B1,10,00,11,19,0E,12,07,08,08,08,22,04,11,11,A9,32,18 |
| 60 | +FF,5,77,01,00,00,11 |
| 61 | +B0,1,60 |
| 62 | +B1,1,32 |
| 63 | +B2,1,07 |
| 64 | +B3,1,80 |
| 65 | +B5,1,49 |
| 66 | +B7,1,85 |
| 67 | +B8,1,21 |
| 68 | +C1,1,78 |
| 69 | +C2,A1,78 |
| 70 | +E0,3,00,1B,02 |
| 71 | +E1,B,08,A0,00,00,07,A0,00,00,00,44,44 |
| 72 | +E2,C,11,11,44,44,ED,A0,00,00,EC,A0,00,00 |
| 73 | +E3,4,00,00,11,11 |
| 74 | +E4,2,44,44 |
| 75 | +E5,10,0A,E9,D8,A0,0C,EB,D8,A0,0E,ED,D8,A0,10,EF,D8,A0 |
| 76 | +E6,4,00,00,11,11 |
| 77 | +E7,2,44,44 |
| 78 | +E8,10,09,E8,D8,A0,0B,EA,D8,A0,0D,EC,D8,A0,0F,EE,D8,A0 |
| 79 | +EB,7,02,00,E4,E4,88,00,40 |
| 80 | +EC,2,3C,00 |
| 81 | +ED,10,AB,89,76,54,02,FF,FF,FF,FF,FF,FF,20,45,67,98,BA |
| 82 | +36,1,10 |
| 83 | +FF,5,77,01,00,00,13 |
| 84 | +E1,1,E4 |
| 85 | +FF,5,77,01,00,00,00 |
| 86 | +21,0 |
| 87 | +3A,1,60 |
| 88 | +11,80 |
| 89 | +29,80 |
| 90 | +:B,120,02 |
| 91 | +:UTI,FT5x06,I2,48,-1,-1 |
| 92 | +RD A8 |
| 93 | +CP 11 |
| 94 | +RTF |
| 95 | +RD A3 |
| 96 | +CP 64 |
| 97 | +RTF |
| 98 | +RT |
| 99 | +:UTT |
| 100 | +RDM 00 16 |
| 101 | +MV 2 1 |
| 102 | +RT |
| 103 | +:UTX |
| 104 | +MV 3 2 |
| 105 | +SCL 480 -1 |
| 106 | +RT |
| 107 | +:UTY |
| 108 | +MV 5 2 |
| 109 | +SCL 480 -1 |
| 110 | +RT |
| 111 | +# |
| 112 | +``` |
| 113 | + |
| 114 | +## Using Air Quality Sensors |
| 115 | + |
| 116 | +According to the schematics, ESP32S3 is directly connected in I2C to the `FT5x06` TouchScreen Controller, and to the `PCA8535` IO Expander. The `SCD40`, `SGP41` and `AHT20` are connected in I2C to the `RP2040` MCU so out of reach of Tasmota. For this, we have added the `I2C_SERIAL` interface which allows to access remote I2C devices via a UAR interface using the same Serial protocol as NXP `SC18IM704` chip. |
| 117 | + |
| 118 | +To make it accessible from native I2C drivers, the I2C Serial driver must use bus `1`, and the I2C bus connected to ESP32S3 must use I2C bus `2`. |
| 119 | + |
| 120 | +Now you need to flash the `RP2040` and use a simple Micropython script to bridge the UART to I2C bus. |
| 121 | + |
| 122 | +## Flashing and configuring RP2040 |
| 123 | + |
| 124 | +# Step 1. Flash Micropython |
| 125 | + |
| 126 | +To flash the RP2040, you need to insert a pin in the "reset" small hole, and power-up the device while keeping the Reset button pushed. You can then release the Reset button. |
| 127 | + |
| 128 | +RP2040 boots in flash mode, and shows a USB disk. Simply download the latest RPI Pico Micropython firmware (file ending with `.uf2`) from [the official Micropython site](https://micropython.org/download/RPI_PICO/). This was tested with `RPI_PICO-20241025-v1.24.0.uf2`. |
| 129 | + |
| 130 | +# Step 2. Use Thonny |
| 131 | + |
| 132 | +For easy setup, download and install [Thonny](https://thonny.org/): |
| 133 | + |
| 134 | +- Launch Thonny |
| 135 | +- Connect to the RP2040: click on the lower right corner and select `MicroPython (RP2040)` |
| 136 | +- Copy and paste the Micropython code from below |
| 137 | +- Click on "Save", select "RP2040 Device" and choose "main.py" as a filename |
| 138 | +- You can hit the "Run Current Script" button (green arrow) to see the script running |
| 139 | +- The script will automatically run at power on |
| 140 | + |
| 141 | +Here is how it should look like: |
| 142 | + |
| 143 | + |
| 144 | +# MicroPython code for RP2040 |
| 145 | + |
| 146 | +```python |
| 147 | +# below is an example of Micropython code for Seedstudio SenseCap |
| 148 | +# that allows to bridge the UART on GPIO 16/17 to I2C on GPIO 20/21 |
| 149 | + |
| 150 | +from machine import Pin, I2C |
| 151 | +from machine import Pin |
| 152 | +from machine import UART, Pin |
| 153 | +import time |
| 154 | + |
| 155 | +uart = UART(0, baudrate=115200, tx=Pin(16), rx=Pin(17), timeout=30000, timeout_char=50, txbuf=128, rxbuf=128) |
| 156 | +print(f"CFG: UART initialized") |
| 157 | + |
| 158 | +power_i2c = Pin(18, Pin.OUT) # create output pin on GPIO0 |
| 159 | +power_i2c.on() # set pin to "on" (high) level |
| 160 | + |
| 161 | +i2c = I2C(0, scl=Pin(21), sda=Pin(20), freq=400_000, timeout=1000) |
| 162 | + |
| 163 | +# print(f"I2C: scan {i2c.scan()}") |
| 164 | + |
| 165 | +# i2c_stat: |
| 166 | +# 0: no error |
| 167 | +# 1: I2C_NACK_ON_ADDRESS |
| 168 | +# 2: I2C_NACK_ON_DATA |
| 169 | +# 3: I2C_TIME_OUT |
| 170 | +i2c_stat = 0 |
| 171 | +def set_i2c_stat(v): |
| 172 | + global i2c_stat |
| 173 | + i2c_stat = v |
| 174 | + |
| 175 | +def get_i2c_stat(): |
| 176 | + global i2c_stat |
| 177 | + return i2c_stat |
| 178 | + |
| 179 | + |
| 180 | +def ignore_until_P(): |
| 181 | + # read uart until none left or 'P' reached |
| 182 | + # return last unprocessed char or None |
| 183 | + while True: |
| 184 | + c = uart.read(1) |
| 185 | + if c is None: |
| 186 | + return None # end of receive |
| 187 | + if c == b'P': |
| 188 | + cur_char = None |
| 189 | + return None # end reached |
| 190 | + |
| 191 | +def process_cmd_start(): |
| 192 | + # return last unprocessed char or None |
| 193 | + addr_b = uart.read(1) |
| 194 | + if addr_b is None: print("start: no address sent"); return None |
| 195 | + addr = addr_b[0] >> 1 |
| 196 | + is_write = not bool(addr_b[0] & 1) |
| 197 | + len_b = uart.read(1) |
| 198 | + if len_b is None: print("start: no length sent"); return None |
| 199 | + len_i = len_b[0] |
| 200 | + cmd_next = None |
| 201 | + # dispatch depending on READ or WRITE |
| 202 | + if is_write: |
| 203 | + payload_b = bytes() |
| 204 | + if len_i > 0: |
| 205 | + payload_b = uart.read(len_i) |
| 206 | + if len(payload_b) < len_i: |
| 207 | + print(f"start: payload {payload_b} too small, expected {len_i} bytes") |
| 208 | + return None |
| 209 | + stop_bit = False |
| 210 | + cmd_next = uart.read(1) |
| 211 | + if cmd_next == b'P': |
| 212 | + stop_bit = True |
| 213 | + try: |
| 214 | + set_i2c_stat(0) |
| 215 | + acks_count = i2c.writeto(addr, payload_b, stop_bit) |
| 216 | + #print(f"{acks_count=} {len_i=}") |
| 217 | + if acks_count < len_i: |
| 218 | + set_i2c_stat(2) |
| 219 | + else: |
| 220 | + print(f"I2C: [0x{addr:02X}] W '{payload_b.hex()}'") |
| 221 | + #print(f"{acks_count=} {len_i=} {get_i2c_stat()=}") |
| 222 | + except Exception as error: |
| 223 | + #print(f"{error=}") |
| 224 | + set_i2c_stat(1) # I2C_NACK_ON_ADDRESS |
| 225 | + # if 'S' is followed, return to main loop |
| 226 | + if cmd_next == b'S': |
| 227 | + return cmd_next |
| 228 | + else: |
| 229 | + # read |
| 230 | + payload_b = b'' |
| 231 | + #print(f"read: [0x{addr:02X}] {len_i}") |
| 232 | + try: |
| 233 | + set_i2c_stat(0) |
| 234 | + payload_b = i2c.readfrom(addr, len_i, True) |
| 235 | + print(f"I2C: [0x{addr:02X}] R '{payload_b.hex()}' {len(payload_b)}/{len_i}") |
| 236 | + uart.write(payload_b) |
| 237 | + except Exception as error: |
| 238 | + print(f"I2C: error while reading from 0x{addr:02X} len={len_i} error '{error}'") |
| 239 | + set_i2c_stat(1) # I2C_NACK_ON_ADDRESS |
| 240 | + return None |
| 241 | + return None |
| 242 | + |
| 243 | + |
| 244 | +def process_cmd_stop(): |
| 245 | + # return last unprocessed char or None |
| 246 | + return None # do nothing |
| 247 | + |
| 248 | +def process_cmd_read(): |
| 249 | + # return last unprocessed char or None |
| 250 | + # we accept only 1 register for now |
| 251 | + reg = uart.read(1) |
| 252 | + if reg is None: print("read: no register sent"); return None |
| 253 | + cmd_next = uart.read(1) |
| 254 | + if cmd_next is None or cmd_next != b'P': print("read: unfinished command"); return None |
| 255 | + # |
| 256 | + reg = reg[0] # convert to number |
| 257 | + if reg == 0x0A: # I2CStat |
| 258 | + uart.write(int.to_bytes(get_i2c_stat() | 0xF0)) |
| 259 | + else: |
| 260 | + uart.write(int.to_bytes(0x00)) |
| 261 | + return None |
| 262 | + |
| 263 | +def process_cmd_write(): |
| 264 | + # return last unprocessed char or None |
| 265 | + print("I2C: ignore 'W' commmand") |
| 266 | + return ignore_until_P() |
| 267 | + |
| 268 | +def process_cmd_version(): |
| 269 | + ignore_until_P() |
| 270 | + uart.write(b'Tasmota I2C uart bridge 1.0\x00') |
| 271 | + return None |
| 272 | + |
| 273 | +def process_cmd_ignore(): |
| 274 | + # return last unprocessed char or None |
| 275 | + return ignore_until_P() |
| 276 | + |
| 277 | +def process_discard(): |
| 278 | + # discard all bytes in input |
| 279 | + # return last unprocessed char or None |
| 280 | + while uart.any() > 1: |
| 281 | + uart.read(uart.any()) |
| 282 | + return None |
| 283 | + |
| 284 | +def run(): |
| 285 | + cmd = None |
| 286 | + while True: |
| 287 | + if cmd is None and uart.any() > 0: |
| 288 | + cmd = uart.read(1) |
| 289 | + if cmd is None: |
| 290 | + time.sleep(0.01) |
| 291 | + else: |
| 292 | + #print(f"SER: received cmd {cmd}") |
| 293 | + if cmd == b'S': |
| 294 | + cmd = process_cmd_start() |
| 295 | + elif cmd == b'P': |
| 296 | + cmd = process_cmd_stop() |
| 297 | + elif cmd == b'R': |
| 298 | + cmd = process_cmd_read() |
| 299 | + elif cmd == b'W': |
| 300 | + cmd = process_cmd_write() |
| 301 | + elif cmd == b'V': |
| 302 | + cmd = process_cmd_version() |
| 303 | + elif cmd == b'I' or cmd == b'O' or cmd == b'Z': |
| 304 | + cmd = process_cmd_ignore() |
| 305 | + else: |
| 306 | + cmd = process_discard() |
| 307 | + |
| 308 | +run() |
| 309 | +``` |
| 310 | + |
| 311 | +## Internals |
| 312 | + |
| 313 | +SeedStudio does not provide the detailed schematics, but still provides an overview of GPIO connection: |
| 314 | + |
| 315 | + |
0 commit comments