├── .github ├── FUNDING.yml └── workflows │ ├── push-master-pico.yml │ └── push-master.yml ├── .gitignore ├── .gitmodules ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── extra_script.py ├── include ├── base.h ├── calibration.h ├── framestate.h ├── main.h └── statistics.h ├── lib └── README ├── platformio.ini ├── rp2040 ├── .gitignore ├── CMakeLists.txt └── hyperspi.cpp ├── src └── main.cpp └── test ├── test_MultiSegment └── main.cpp ├── test_MultiSegmentReversed └── main.cpp └── test_SingleSegment └── main.cpp /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: awawa-dev 2 | -------------------------------------------------------------------------------- /.github/workflows/push-master-pico.yml: -------------------------------------------------------------------------------- 1 | name: HyperSPI Pico CI Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | 7 | ############################# 8 | #### HyperSPI for Pico ###### 9 | ############################# 10 | 11 | HyperSpiPico: 12 | runs-on: ubuntu-22.04 13 | defaults: 14 | run: 15 | working-directory: ./rp2040 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | submodules: recursive 20 | 21 | - name: Install GNU Arm Embedded Toolchain 22 | uses: carlosperate/arm-none-eabi-gcc-action@v1 23 | with: 24 | release: '12.2.Rel1' 25 | 26 | - name: Build packages 27 | shell: bash 28 | run: | 29 | mkdir build 30 | cd build 31 | cmake .. 32 | cmake --build . --config Release 33 | zip -j ../HyperSerialPico/firmware/hyperspi_pico_rp2040.zip ../HyperSerialPico/firmware/*.uf2 34 | 35 | - uses: actions/upload-artifact@v4 36 | name: Upload artifacts (commit) 37 | if: (startsWith(github.event.ref, 'refs/tags') != true) 38 | with: 39 | name: commit-artifact-pico 40 | path: | 41 | rp2040/HyperSerialPico/firmware/*.zip 42 | 43 | - uses: actions/upload-artifact@v4 44 | name: Upload artifacts (release) 45 | if: startsWith(github.ref, 'refs/tags/') 46 | with: 47 | name: release-artifact-pico 48 | path: | 49 | rp2040/HyperSerialPico/firmware/*.zip 50 | 51 | - name: Build packages for Adafruit Feather RP2040 Scorpio 52 | shell: bash 53 | run: | 54 | cd build 55 | rm *.* 56 | rm ../HyperSerialPico/firmware/* 57 | echo "Neopixel is using GPIO16(OUTPUT_DATA_PIN) on output 0." > ../HyperSerialPico/firmware/Firmware_for_Adafruit_Feather_RP2040_Scorpio.txt 58 | echo "SPI MOSI->MISO is GPIO28 (A2)" >> ../HyperSerialPico/firmware/Firmware_for_Adafruit_Feather_RP2040_Scorpio.txt 59 | echo "SPI SCK is GPIO26 (A0)" >> ../HyperSerialPico/firmware/Firmware_for_Adafruit_Feather_RP2040_Scorpio.txt 60 | echo "SPI CHIP_SELECT is GPIO29 (A3)" >> ../HyperSerialPico/firmware/Firmware_for_Adafruit_Feather_RP2040_Scorpio.txt 61 | echo "SPI interface is spi1" >> ../HyperSerialPico/firmware/Firmware_for_Adafruit_Feather_RP2040_Scorpio.txt 62 | cmake -DOVERRIDE_DATA_PIN=16 -DOVERRIDE_SPI_DATA_PIN=28 -DOVERRIDE_SPI_CLOCK_PIN=26 -DOVERRIDE_SPI_CHIP_SELECT=29 -DOVERRIDE_SPI_INTERFACE=spi1 -DCMAKE_BUILD_TYPE=Release .. 63 | cmake --build . 64 | zip -j ../HyperSerialPico/firmware/Adafruit_Feather_RP2040_Scorpio.zip ../HyperSerialPico/firmware/* 65 | 66 | - uses: actions/upload-artifact@v4 67 | name: Upload artifacts (Adafruit_Feather) 68 | if: (startsWith(github.event.ref, 'refs/tags') != true) 69 | with: 70 | name: commit-artifact-Adafruit_Feather 71 | path: | 72 | rp2040/HyperSerialPico/firmware/*.zip 73 | 74 | - uses: actions/upload-artifact@v4 75 | name: Upload artifacts (release for Adafruit_Feather) 76 | if: startsWith(github.ref, 'refs/tags/') 77 | with: 78 | name: release-artifact-Adafruit_Feather 79 | path: | 80 | rp2040/HyperSerialPico/firmware/*.zip 81 | 82 | - name: Build packages for Adafruit ItsyBitsy RP2040 83 | shell: bash 84 | run: | 85 | cd build 86 | rm *.* 87 | rm ../HyperSerialPico/firmware/* 88 | echo "Neopixel is using GPIO14(OUTPUT_DATA_PIN) on output D5." > ../HyperSerialPico/firmware/Firmware_for_Adafruit_ItsyBitsy_RP2040.txt 89 | echo "SPI MOSI->MISO is GPIO28 (A2)" >> ../HyperSerialPico/firmware/Firmware_for_Adafruit_ItsyBitsy_RP2040.txt 90 | echo "SPI SCK is GPIO26 (A0)" >> ../HyperSerialPico/firmware/Firmware_for_Adafruit_ItsyBitsy_RP2040.txt 91 | echo "SPI CHIP_SELECT is GPIO29 (A3)" >> ../HyperSerialPico/firmware/Firmware_for_Adafruit_ItsyBitsy_RP2040.txt 92 | echo "SPI interface is spi1" >> ../HyperSerialPico/firmware/Firmware_for_Adafruit_ItsyBitsy_RP2040.txt 93 | cmake -DOVERRIDE_DATA_PIN=14 -DOVERRIDE_SPI_DATA_PIN=28 -DOVERRIDE_SPI_CLOCK_PIN=26 -DOVERRIDE_SPI_CHIP_SELECT=29 -DOVERRIDE_SPI_INTERFACE=spi1 -DCMAKE_BUILD_TYPE=Release .. 94 | cmake --build . 95 | zip -j ../HyperSerialPico/firmware/Adafruit_ItsyBitsy_RP2040.zip ../HyperSerialPico/firmware/* 96 | 97 | - uses: actions/upload-artifact@v4 98 | name: Upload artifacts (Adafruit_ItsyBitsy) 99 | if: (startsWith(github.event.ref, 'refs/tags') != true) 100 | with: 101 | name: commit-artifact-Adafruit_ItsyBitsy 102 | path: | 103 | rp2040/HyperSerialPico/firmware/*.zip 104 | 105 | - uses: actions/upload-artifact@v4 106 | name: Upload artifacts (release for Adafruit_ItsyBitsy) 107 | if: startsWith(github.ref, 'refs/tags/') 108 | with: 109 | name: release-artifact-Adafruit_ItsyBitsy 110 | path: | 111 | rp2040/HyperSerialPico/firmware/*.zip 112 | 113 | ################################ 114 | ###### Publish Releases ######## 115 | ################################ 116 | 117 | publish: 118 | name: Publish Releases 119 | if: startsWith(github.event.ref, 'refs/tags') 120 | needs: [HyperSpiPico] 121 | runs-on: ubuntu-latest 122 | permissions: 123 | contents: write 124 | steps: 125 | # generate environment variables 126 | - name: Generate environment variables from version and tag 127 | run: | 128 | echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV 129 | echo "preRelease=false" >> $GITHUB_ENV 130 | 131 | # If version contains alpha or beta, mark draft release as pre-release 132 | - name: Mark release as pre-release 133 | if: contains(env.VERSION, 'alpha') || contains(env.VERSION, 'beta') 134 | run: echo "preRelease=true" >> $GITHUB_ENV 135 | 136 | - name: Download artifacts 137 | uses: actions/download-artifact@v4 138 | with: 139 | path: artifacts 140 | pattern: release-artifact-* 141 | merge-multiple: true 142 | 143 | # create draft release and upload artifacts 144 | - name: Create draft release 145 | uses: softprops/action-gh-release@v1 146 | with: 147 | name: HyperSPI ${{ env.VERSION }} 148 | tag_name: ${{ env.TAG }} 149 | files: "artifacts/**" 150 | draft: true 151 | prerelease: ${{ env.preRelease }} 152 | env: 153 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 154 | -------------------------------------------------------------------------------- /.github/workflows/push-master.yml: -------------------------------------------------------------------------------- 1 | name: HyperSPI ESP32/8266 CI Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | 7 | ###################### 8 | #### PlatformIO ###### 9 | ###################### 10 | 11 | PlatformIO: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Cache pip 16 | uses: actions/cache@v4 17 | with: 18 | path: ~/.cache/pip 19 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 20 | restore-keys: | 21 | ${{ runner.os }}-pip- 22 | - name: Cache PlatformIO 23 | uses: actions/cache@v4 24 | with: 25 | path: ~/.platformio 26 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 27 | - name: Set up Python 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: 3.x 31 | - name: Install PlatformIO 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install --upgrade platformio 35 | - name: Run PlatformIO 36 | run: | 37 | pio run 38 | zip -j .pio/build/recovery_firmware.zip .pio/build/*.factory.bin 39 | rm -f .pio/build/*.factory.bin 40 | 41 | - uses: actions/upload-artifact@v4 42 | name: Upload artifacts (commit) 43 | if: (startsWith(github.event.ref, 'refs/tags') != true) 44 | with: 45 | name: firmware-archive 46 | path: | 47 | .pio/build/*.bin 48 | .pio/build/recovery_firmware.zip 49 | 50 | - uses: actions/upload-artifact@v4 51 | name: Upload artifacts (release) 52 | if: startsWith(github.ref, 'refs/tags/') 53 | with: 54 | name: firmware-release 55 | path: | 56 | .pio/build/*.bin 57 | .pio/build/recovery_firmware.zip 58 | 59 | ################################ 60 | ###### Publish Releases ######## 61 | ################################ 62 | 63 | publish: 64 | name: Publish Releases 65 | if: startsWith(github.event.ref, 'refs/tags') 66 | needs: [PlatformIO] 67 | runs-on: ubuntu-latest 68 | steps: 69 | # generate environment variables 70 | - name: Generate environment variables from version and tag 71 | run: | 72 | echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV 73 | echo "preRelease=false" >> $GITHUB_ENV 74 | 75 | # If version contains alpha or beta, mark draft release as pre-release 76 | - name: Mark release as pre-release 77 | if: contains(env.VERSION, 'alpha') || contains(env.VERSION, 'beta') 78 | run: echo "preRelease=true" >> $GITHUB_ENV 79 | 80 | - uses: actions/download-artifact@v4 81 | with: 82 | name: firmware-release 83 | 84 | # create draft release and upload artifacts 85 | - name: Create draft release 86 | uses: softprops/action-gh-release@v1 87 | with: 88 | name: HyperSPI ${{ env.VERSION }} 89 | tag_name: ${{ env.TAG }} 90 | files: | 91 | *.bin 92 | *.zip 93 | draft: true 94 | prerelease: ${{ env.preRelease }} 95 | env: 96 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rp2040/HyperSerialPico"] 2 | path = rp2040/HyperSerialPico 3 | url = https://github.com/awawa-dev/HyperSerialPico 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 awawa-dev 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 | # HyperSPI 2 | SPI bridge for AWA protocol to control a LED strip from HyperHDR. 3 | Diagnostic and performance data available at the serial port output [read more](#performancedebug-output). 4 | Raspberry Pi acts as a master, ESP8266/ESP32/ESP32-S2/rp2040(Raspberry Pi Pico) is in slave mode. 5 | 6 | | LED strip / Device | rp2040 / Pico | ESP8266
(limited performance) | ESP32 / ESP32-S2 mini 7 | |--------------------------------|:-------:|:-----------:|:-------:| 8 | | SK6812 cold white | yes | yes | yes | 9 | | SK6812 neutral white | yes | yes | yes | 10 | | WS281x | yes | yes | yes | 11 | 12 | 13 | # Why this project was created? 14 | 15 | - SPI is very faster. HyperSPI works best at speed over 20Mb 16 | - SPI doesn't have any data integration check. But AWA protocol does have one 17 | - you don't need to have 2Mb capable serial port on your ESP board 18 | - SPI transmission is much lighter than serial communication 19 | - There is a hardware limitation for the Rpi current design...even if you connect your grabber using USB2.0 mode, working serial port driver (used by Adalight) results in quite a large drop in overall USB transfer. So we can replace Adalight with a pure SPI data transfer as an alternative 20 | - I needed it and I was able to implemented it 😉 21 | 22 | # Hardware connection 23 | 24 | If you are using an ESP board compatible with the Wemos board (ESP8266 Wemos D1/pro, ESP32 MH-ET Live, ESP32-S2 lolin mini), the SPI connection uses the same pinout location on the ESP board! The pin positions of the LED output may vary. Cables (including ground) should not exceed 15-20cm or it may be necessary to lower the SPI speed. 25 | 26 | The photos below use the same home-made adapter throughout, so you can see a repeating pattern and the cable colors should help you locate the correct pins. However, always consult the GPIO diagram for your boards to confirm that you have connected the cables correctly, because if you make a mistake and connect to the 5V GPIO line, it may damage both devices. 27 | 28 | As you can also notice, the pinout of the SPI0 interface is identical for the entire Raspberry Pi SBC family: 3, 4, 5, Zero 2W, etc. 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |

See how easy it is to connect Raspberry Pi Pico (rp2040) to Raspberry Pi 5 using SPI

or if you prefer ESP32/ESP32-S2/Esp8266

53 | 54 | # Example of supported boards 55 | 56 |

57 | Adafruit RP2040 Scorpio and ItsyBitsy with built-in level shifter (recommended!)
58 | 59 |

60 | 61 |

62 | Esp8266 Wemos D1 mini (CH340) and Wemos D1 mini pro (CP2104)
63 | 64 |

65 | 66 |

67 | ESP32 MH-ET Live and ESP32-S2 Lolin mini (CDC)
68 | 69 |

70 | 71 | ## Default pinout (can be changed for esp32, esp32-s2 and rp2040 Pico) 72 | 73 | | PINOUT | ESP8266 | ESP32 | ESP32-S2 | Pico (rp2040) | Adafruit rp2040
Scorpio / ItsyBitsy 74 | |-------------|-----------|-----------|-----------|-----------|-----------| 75 | | Clock (SCK) | GPIO 14 | GPIO 18 | GPIO 7 | GPIO 2 | GPIO 26 (A0) | 76 | | Data (MOSI) | GPIO 13 | GPIO 23 | GPIO 11 | GPIO 4 | GPIO 28 (A2) | 77 | | SPI Chip Select(e.g. CE0) | not used | GPIO 5 | GPIO 12 | GPIO 5 | GPIO 29 (A3) | 78 | | GROUND | mandatory | mandatory | mandatory | mandatory | mandatory | 79 | | LED output | GPIO 2 | GPIO 2 | GPIO 2 | GPIO 14 | GPIO16 / GPIO14 | 80 | 81 | > [!CAUTION] 82 | > The ground connection between both GPIOs is as important as the other SPI data connections. The ground cable should be of a similar length as them and run directly next to them. 83 | 84 | # Flashing the firmware 85 | 86 | There are two versions of the firmware for ESP32 and ESP32-S2. The 'factory' (in the `recovery_firmware.zip` archive) and the 'base' one. Factory firmware should be flashed to offset 0x0, base firmware to offset 0x10000. 87 | 88 | ## Flashing ESP32-S2 Lolin mini 89 | 90 | Requires using `esptool.py` to flash the firmware e.g. 91 | - `esptool.py write_flash 0x10000 hyperspi_esp32_s2_mini_SK6812_RGBW_COLD.bin` or 92 | - `esptool.py write_flash 0x0 hyperspi_esp32_s2_mini_SK6812_RGBW_COLD.factory.bin` 93 | 94 | Troubleshooting: ESP32-S2 Lolin mini recovery procedure if the board is not detected or is malfunctioning. 95 | 1. Put the board into dfu mode using board buttons: press `Rst` + `0` buttons, then release `Rst`, next release `0` 96 | Do not reset or disconnect the board until the end of the recovery procedure. 97 | 2. Execute `esptool.py erase_flash` 98 | 3. Flash 'factory' version of the firmware e.g. 99 | `esptool.py write_flash 0x0 hyperspi_esp32_s2_mini_SK6812_RGBW_COLD.factory.bin` 100 | 4. **`esptool.py` is not able to automatically reset esp32-s2 when in dfu mode. Reconnect or hard reset it manually.** The board should be detected as a COM port in the system. 101 | 102 | ## Flashing generic Esp8266/ESP32 103 | 104 | Recommend to use [esphome-flasher](https://github.com/esphome/esphome-flasher/releases) 105 | Or use `esptool.py` e.g. 106 | - `esptool.py write_flash 0x10000 hyperspi_esp32_SK6812_RGBW_COLD.bin` or 107 | - `esptool.py write_flash 0x0 hyperspi_esp32_SK6812_RGBW_COLD.factory.bin` 108 | 109 | For **RGBW LED strip** like RGBW SK6812 NEUTRAL white choose: *hyperspi_..._SK6812_RGBW_NEUTRAL.bin* 110 | For **RGBW LED strip** like RGBW SK6812 COLD white choose: *hyperspi_..._SK6812_RGBW_COLD.bin* 111 | For **RGB LED strip** like WS8212b or RGB SK6812 variant choose: *hyperspi_..._WS281x_RGB.bin* 112 | 113 | ## Flashing Pico boards 114 | 115 | It's very easy and you don't need any special flasher. 116 | 117 | Use firmware from the `hyperspi_pico_rp2040.zip` archive. Adafruit boards have their own custom firmware package inside the archive: `Adafruit_ItsyBitsy_RP2040.zip` and `Adafruit_Feather_RP2040_Scorpio.zip` 118 | 119 | Put your Pico board into DFU mode: 120 | * If your Pico board has only one button (`boot`) then press & hold it and connect the board to the USB port. Then you can release the button. 121 | * If your Pico board has two buttons, connect it to the USB port. Then press & hold `boot` and `reset` buttons, then release `reset` and next release `boot` button. 122 | 123 | In the system file explorer you should find new drive (e.g. called `RPI-RP2` drive) exposed by the Pico board. Drag & drop (or copy) the selected firmware to this drive. 124 | The Pico will reset automaticly after the upload and after few seconds it will be ready to use. 125 | 126 | # Software configuration (HyperHDR v17 and above) 127 | 128 | **In HyperHDR `Image Processing→Smoothing→Update frequency` you should do not exceed the maximum capacity of the device. Read more here: [testing performance](https://github.com/awawa-dev/HyperSPI#performance-output)** 129 | 130 | Select `esp8266` protocol for ESP proprietary SPI protocol, `esp32` for ESP32 boards, `rp2040 (Pico)` for Pico boards or `standard` for other devices. 131 | Make sure you set "Refresh time" to zero, "Baudrate" should be set to high but realistic value like ```25 000 000```. 132 | Enabling "White channel calibration" is optional, if you want to fine tune the white channel balance of your sk6812 RGBW LED strip. 133 | 134 | 135 | 136 | # Benchmark results 137 | 138 | ## ESP32 & ESP32-S2 parallel multi-segment mode 139 | 140 | | sk6812 LED strip / Device | ESP32 MH-ET LIVE mini
HyperSPI v9 | ESP32-S2 Lolin mini
HyperSPI v9 | 141 | |-----------------------------------------------------------------------------------------|-----------------------|----------------------| 142 | | 300 RGBW LEDs
Refresh rate/continues output=100Hz
SECOND_SEGMENT_START_INDEX=150 | 100 | 100 | 143 | | 600 RGBW LEDs
Refresh rate/continues output=83Hz
SECOND_SEGMENT_START_INDEX=300 | 83 | 83 | 144 | | 900 RGBW LEDs
Refresh rate/continues output=55Hz
SECOND_SEGMENT_START_INDEX=450 | 54-55 | 55 | 145 | 146 | ## SP8266 / ESP32 147 | 148 | | sk6812 LED strip / Device | ESP32 MH ET Live
HyperSPI v9 | ESP32-S2 Lolin mini
HyperSPI v9 | ESP8266 Wemos D1 Pro
HyperSPI v9 | 149 | |------------------------------------------------|-----------------|--------------------|---------------------| 150 | | 300 RGBW LEDs
continues output=83/70Hz | 83 | 83 | 70 | 151 | | 600 RGBW LEDs
continues output=43/33Hz | 42-43 | 42 | 33 | 152 | | 900 RGBW LEDs
continues output=28/22Hz | 28 | 28 | 22 | 153 | 154 | # Compiling 155 | 156 | ## ESP8266 / ESP32 157 | 158 | Currently we use PlatformIO to compile the project. Install [Visual Studio Code](https://code.visualstudio.com/) and add [PlatformIO plugin](https://platformio.org/). 159 | This environment will take care of everything and compile the firmware for you. Low-level LED strip support is provided by my highly optimizated (pre-fill I2S DMA modes, turbo I2S parallel mode for up to 2 segments etc) version of Neopixelbus library: [link](https://github.com/awawa-dev/NeoPixelBus). 160 | 161 | ## Pico rp2040 162 | 163 | Use Pico SDK and Visual Code to open ```rp2040``` folder. Edit ```rp2040\CMakeLists.txt``` configuration file if you need to apply changes. 164 | 165 | ## Github Action 166 | 167 | But there is also an alternative and an easier way. Just fork the project and enable its Github Action. Use the online editor to make changes: 168 | - esp8266/ESP32 boards: to the ```platformio.ini``` file 169 | - rp2040 Pico boards: to the ```rp2040\CMakeLists.txt``` file 170 | 171 | for example change default pin-outs or enable multi-segments support, and save it. Github Action will compile new firmware automatically in the Artifacts archive. It has never been so easy! **Just remember to follow the steps in the correct order otherwise the Github Action may not be triggered the first time after saving the changes.** 172 | 173 | Tutorial: https://github.com/awawa-dev/HyperSPI/wiki 174 | 175 | # Multi-Segment Wiring (ESP32, ESP32-S2 and Pico rp2040 boards only) 176 | 177 | ## ESP32 178 | 179 | Using parallel multi-segment allows you to double your Neopixel (e.g. sk6812 RGBW) LED strip refresh rate by dividing it into two smaller equal parts. Both smaller segments are perfectly in sync so you don't need to worry about it. Proposed example of building a multisegment: 180 | - Divide a long or dense strip of LEDs into 2 smaller equal parts. So `SECOND_SEGMENT_START_INDEX` in the HyperSPI firmware is the total number of LEDs divided by 2. 181 | - Build your first segment traditional way e.g. clockwise, so it starts somewhere in middle of the bottom of frame/TV and ends in the middle of the top of frame/TV 182 | - Start the second segment in the opposite direction to the first one e.g. counterclockwise (`SECOND_SEGMENT_REVERSED` option in the HyperSPI firmware configuration must be enabled). So it starts somewhere in the middle of the bottom of the frame/TV and ends in the middle of the top of the TV/frame. Both segments could be connected if possible at the top but only 5v and ground ( NOT the data line). 183 | - The data line starts for both segments somewhere in the middle of the bottom of the TV/frame (where each of the LED strips starts) 184 | - Configuration in HyperHDR does not change! It's should be configured as one, single continues segment. All is done in HyperSPI firmware transparently and does not affect LED strip configuration in HyperHDR. 185 | 186 | You also must configure data pin in the `platformio.ini`. Review the comments at the top of the file: 187 | * `SECOND_SEGMENT_DATA_PIN` - These is data pin for your second strip 188 | 189 | You add these to your board's config. Be sure to put `-D` in front of each setting. 190 | 191 | Examples of final build_flags for 288 LEDs divided into 2 equal segments in the `platformio.ini`: 192 | ``` 193 | [env:SK6812_RGBW_COLD] 194 | build_flags = -DNEOPIXEL_RGBW -DCOLD_WHITE -DDATA_PIN=2 ${env.build_flags} -DSECOND_SEGMENT_START_INDEX=144 -DSECOND_SEGMENT_DATA_PIN=4 -DSECOND_SEGMENT_REVERSED 195 | ... 196 | [env:WS281x_RGB] 197 | build_flags = -DNEOPIXEL_RGB -DDATA_PIN=2 ${env.build_flags} -DSECOND_SEGMENT_START_INDEX=144 -DSECOND_SEGMENT_DATA_PIN=4 -DSECOND_SEGMENT_REVERSED 198 | ... 199 | ``` 200 | Implementation example: 201 | - The diagram of the board for WS2812b/SK6812 including ESP32 and the SN74AHCT125N 74AHCT125 [level shifter](https://github.com/awawa-dev/HyperHDR/wiki/Level-Shifter). 202 | 203 | ## Pico rp2040 204 | 205 | Edit ```rp2040\CMakeLists.txt``` file and recompile the project. 206 | 207 | ![HyperSPI](https://user-images.githubusercontent.com/85223482/222923979-f344349a-1f8b-4195-94ca-51721923359e.png) 208 | 209 | # Performance/debug output 210 | 211 | **The output is only available when HyperHDR is not using the device at the moment, so it should be disabled in the app for a while.** Stores the last result when HyperHDR was running in the current session. You can read it from the serial port at a speed of 115200. 212 | 213 | On Linux you can `screen` command. 214 | First install it: `sudo apt install screen`. Adjust USB port if necessary and connect to the serial port: 215 | `screen /dev/ttyACM0 115200` 216 | If you want to exit screen press `Ctrl-a` then `k` and confirm exit. 217 | 218 | You can also use `Putty` on Windows 219 | ![obraz](https://user-images.githubusercontent.com/69086569/216762783-0ce47e57-98a7-474d-aa84-7e5afb42d294.png) 220 | 221 | For testing maximum performance in HyperHDR enable `Image Processing→Smoothing→Continuous output`, high `Update frequency` in the same tab and set any color in the `Remote control` tab as an active effect. After testing you need to disable `Continuous output`and set `Update frequency` according to your results. 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | -------------------------------------------------------------------------------- /extra_script.py: -------------------------------------------------------------------------------- 1 | Import("env") 2 | platform = env.PioPlatform() 3 | 4 | import sys 5 | from os.path import join 6 | from pathlib import Path 7 | import shutil 8 | 9 | sys.path.append(join(platform.get_package_dir("tool-esptoolpy"))) 10 | import esptool 11 | 12 | def post_program_action(source, target, env): 13 | program_path = target[0].get_abspath() 14 | path = Path(program_path) 15 | shutil.copy(program_path, path.parent.parent.absolute()) 16 | 17 | # Combine separate bin files with their respective offsets into a single file 18 | # This single file must then be flashed to an ESP32 node with 0 offset. 19 | # Copied from: https://github.com/letscontrolit/ESPEasy/blob/mega/tools/pio/post_esp32.py 20 | def esp32_create_combined_bin(source, target, env): 21 | print("Generating combined binary for serial flashing") 22 | 23 | # The offset from begin of the file where the app0 partition starts 24 | # This is defined in the partition .csv file 25 | app_offset = 0x10000 26 | 27 | new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin") 28 | sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) 29 | firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") 30 | chip = env.get("BOARD_MCU") 31 | flash_size = env.BoardConfig().get("upload.flash_size") 32 | flash_freq = env.BoardConfig().get("build.f_flash", '40m') 33 | flash_freq = flash_freq.replace('000000L', 'm') 34 | flash_mode = env.BoardConfig().get("build.flash_mode", "dio") 35 | memory_type = env.BoardConfig().get("build.arduino.memory_type", "qio_qspi") 36 | if flash_mode == "qio" or flash_mode == "qout": 37 | flash_mode = "dio" 38 | if memory_type == "opi_opi" or memory_type == "opi_qspi": 39 | flash_mode = "dout" 40 | cmd = [ 41 | "--chip", 42 | chip, 43 | "merge_bin", 44 | "-o", 45 | new_file_name, 46 | "--flash_mode", 47 | flash_mode, 48 | "--flash_freq", 49 | flash_freq, 50 | "--flash_size", 51 | flash_size, 52 | ] 53 | 54 | print(" Offset | File") 55 | for section in sections: 56 | sect_adr, sect_file = section.split(" ", 1) 57 | print(f" - {sect_adr} | {sect_file}") 58 | cmd += [sect_adr, sect_file] 59 | 60 | print(f" - {hex(app_offset)} | {firmware_name}") 61 | cmd += [hex(app_offset), firmware_name] 62 | 63 | print('Using esptool.py arguments: %s' % ' '.join(cmd)) 64 | 65 | esptool.main(cmd) 66 | path = Path(new_file_name) 67 | shutil.copy(new_file_name, path.parent.parent.absolute()) 68 | 69 | env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", post_program_action) 70 | if env.GetProjectOption("custom_prog_version").find("esp32_") != -1: 71 | env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) 72 | env.Replace(PROGNAME="hyperspi_%s" % env.GetProjectOption("custom_prog_version")) 73 | 74 | -------------------------------------------------------------------------------- /include/base.h: -------------------------------------------------------------------------------- 1 | /* base.h 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2025 awawa-dev 6 | * 7 | * https://github.com/awawa-dev/HyperSPI 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | #ifndef BASE_H 29 | #define BASE_H 30 | 31 | #if defined(ARDUINO_ARCH_ESP32) 32 | #include "freertos/semphr.h" 33 | #endif 34 | 35 | #if defined(SECOND_SEGMENT_START_INDEX) 36 | #if !defined(SECOND_SEGMENT_DATA_PIN) 37 | #error "Please define SECOND_SEGMENT_DATA_PIN for second segment" 38 | #elif !defined(SECOND_SEGMENT_CLOCK_PIN) && !defined(NEOPIXEL_RGBW) && !defined(NEOPIXEL_RGB) 39 | #error "Please define SECOND_SEGMENT_CLOCK_PIN and SECOND_SEGMENT_DATA_PIN for second segment" 40 | #endif 41 | #endif 42 | 43 | class Base 44 | { 45 | // LED strip number 46 | int ledsNumber = 0; 47 | // NeoPixelBusLibrary primary object 48 | LED_DRIVER* ledStrip1 = nullptr; 49 | // NeoPixelBusLibrary second object 50 | LED_DRIVER2* ledStrip2 = nullptr; 51 | // frame is set and ready to render 52 | bool readyToRender = false; 53 | 54 | public: 55 | // static data buffer for the loop 56 | uint8_t buffer[MAX_BUFFER + 1] = {0}; 57 | // handle to tasks 58 | TaskHandle_t processDataHandle = nullptr; 59 | TaskHandle_t processSerialHandle = nullptr; 60 | 61 | // semaphore to synchronize them 62 | xSemaphoreHandle i2sXSemaphore; 63 | 64 | // current queue position 65 | volatile int queueCurrent = 0; 66 | // queue end position 67 | volatile int queueEnd = 0; 68 | 69 | inline int getLedsNumber() 70 | { 71 | return ledsNumber; 72 | } 73 | 74 | inline LED_DRIVER* getLedStrip1() 75 | { 76 | return ledStrip1; 77 | } 78 | 79 | inline LED_DRIVER2* getLedStrip2() 80 | { 81 | return ledStrip2; 82 | } 83 | 84 | void initLedStrip(int count) 85 | { 86 | if (ledStrip1 != nullptr) 87 | { 88 | delete ledStrip1; 89 | ledStrip1 = nullptr; 90 | } 91 | 92 | if (ledStrip2 != nullptr) 93 | { 94 | delete ledStrip2; 95 | ledStrip2 = nullptr; 96 | } 97 | 98 | ledsNumber = count; 99 | 100 | #if defined(SECOND_SEGMENT_START_INDEX) 101 | if (ledsNumber > SECOND_SEGMENT_START_INDEX) 102 | { 103 | #if defined(NEOPIXEL_RGBW) || defined(NEOPIXEL_RGB) 104 | ledStrip1 = new LED_DRIVER(SECOND_SEGMENT_START_INDEX, DATA_PIN); 105 | ledStrip1->Begin(); 106 | ledStrip2 = new LED_DRIVER2(ledsNumber - SECOND_SEGMENT_START_INDEX, SECOND_SEGMENT_DATA_PIN); 107 | ledStrip2->Begin(); 108 | #else 109 | ledStrip1 = new LED_DRIVER(SECOND_SEGMENT_START_INDEX); 110 | ledStrip1->Begin(CLOCK_PIN, 12, DATA_PIN, 15); 111 | ledStrip2 = new LED_DRIVER2(ledsNumber - SECOND_SEGMENT_START_INDEX); 112 | ledStrip2->Begin(SECOND_SEGMENT_CLOCK_PIN, 12, SECOND_SEGMENT_DATA_PIN, 15); 113 | #endif 114 | } 115 | #endif 116 | 117 | if (ledStrip1 == nullptr) 118 | { 119 | #if defined(ARDUINO_ARCH_ESP32) 120 | ledStrip1 = new LED_DRIVER(ledsNumber, DATA_PIN); 121 | ledStrip1->Begin(); 122 | #else 123 | ledStrip1 = new LED_DRIVER(ledsNumber); 124 | ledStrip1->Begin(); 125 | #endif 126 | } 127 | } 128 | 129 | /** 130 | * @brief Check if there is already prepared frame to display 131 | * 132 | * @return true 133 | * @return false 134 | */ 135 | inline bool hasLateFrameToRender() 136 | { 137 | return readyToRender; 138 | } 139 | 140 | inline void dropLateFrame() 141 | { 142 | readyToRender = false; 143 | } 144 | 145 | inline void renderLeds(bool newFrame) 146 | { 147 | if (newFrame) 148 | readyToRender = true; 149 | 150 | if (readyToRender && 151 | (ledStrip1 != nullptr && ledStrip1->CanShow()) && 152 | !(ledStrip2 != nullptr && !ledStrip2->CanShow())) 153 | { 154 | statistics.increaseShow(); 155 | readyToRender = false; 156 | 157 | // display segments 158 | ledStrip1->Show(false); 159 | if (ledStrip2 != nullptr) 160 | ledStrip2->Show(false); 161 | } 162 | } 163 | 164 | inline bool setStripPixel(uint16_t pix, ColorDefinition &inputColor) 165 | { 166 | if (pix < ledsNumber) 167 | { 168 | #if defined(SECOND_SEGMENT_START_INDEX) 169 | if (pix < SECOND_SEGMENT_START_INDEX) 170 | ledStrip1->SetPixelColor(pix, inputColor); 171 | else 172 | { 173 | #if defined(SECOND_SEGMENT_REVERSED) 174 | ledStrip2->SetPixelColor(ledsNumber - pix - 1, inputColor); 175 | #else 176 | ledStrip2->SetPixelColor(pix - SECOND_SEGMENT_START_INDEX, inputColor); 177 | #endif 178 | } 179 | #else 180 | ledStrip1->SetPixelColor(pix, inputColor); 181 | #endif 182 | } 183 | 184 | return (pix + 1 < ledsNumber); 185 | } 186 | } base; 187 | 188 | #endif -------------------------------------------------------------------------------- /include/calibration.h: -------------------------------------------------------------------------------- 1 | /* calibration.h 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2025 awawa-dev 6 | * 7 | * https://github.com/awawa-dev/HyperSPI 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | #ifdef NEOPIXEL_RGBW 29 | typedef RgbwColor ColorDefinition; 30 | #else 31 | typedef RgbColor ColorDefinition; 32 | #endif 33 | 34 | 35 | #if !defined(CALIBRATION_H) && (defined(NEOPIXEL_RGBW) || defined(HYPERSERIAL_TESTING)) 36 | #define CALIBRATION_H 37 | 38 | #include 39 | #include 40 | 41 | #define ROUND_DIVIDE(numer, denom) (((numer) + (denom) / 2) / (denom)) 42 | 43 | struct 44 | { 45 | uint8_t white[256]; 46 | uint8_t red[256]; 47 | uint8_t green[256]; 48 | uint8_t blue[256]; 49 | } channelCorrection; 50 | 51 | class CalibrationConfig 52 | { 53 | // calibration parameters 54 | uint8_t gain = 0xFF; 55 | uint8_t red = 0xA0; 56 | uint8_t green = 0xA0; 57 | uint8_t blue = 0xA0; 58 | 59 | /** 60 | * @brief Build the LUT table using provided parameters 61 | * 62 | */ 63 | void prepareCalibration() 64 | { 65 | // prepare LUT calibration table, cold white is much better than "neutral" white 66 | for (uint32_t i = 0; i < 256; i++) 67 | { 68 | // color calibration 69 | uint32_t _gain = gain * i; // adjust gain 70 | uint32_t _red = red * i; // adjust red 71 | uint32_t _green = green * i; // adjust green 72 | uint32_t _blue = blue * i; // adjust blue 73 | 74 | channelCorrection.white[i] = (uint8_t)std::min(ROUND_DIVIDE(_gain, 0xFF), (uint32_t)0xFF); 75 | channelCorrection.red[i] = (uint8_t)std::min(ROUND_DIVIDE(_red, 0xFF), (uint32_t)0xFF); 76 | channelCorrection.green[i] = (uint8_t)std::min(ROUND_DIVIDE(_green,0xFF), (uint32_t)0xFF); 77 | channelCorrection.blue[i] = (uint8_t)std::min(ROUND_DIVIDE(_blue, 0xFF), (uint32_t)0xFF); 78 | } 79 | } 80 | 81 | public: 82 | CalibrationConfig() 83 | { 84 | prepareCalibration(); 85 | } 86 | 87 | /** 88 | * @brief Compare base calibration settings 89 | * 90 | */ 91 | bool compareCalibrationSettings(uint8_t _gain, uint8_t _red, uint8_t _green, uint8_t _blue) 92 | { 93 | return _gain == gain && _red == red && _green == green && _blue == blue; 94 | } 95 | 96 | /** 97 | * @brief Set the parameters that define RGB to RGBW transformation 98 | * 99 | * @param _gain 100 | * @param _red 101 | * @param _green 102 | * @param _blue 103 | */ 104 | void setParamsAndPrepareCalibration(uint8_t _gain, uint8_t _red, uint8_t _green, uint8_t _blue) 105 | { 106 | if (gain != _gain || red != _red || green != _green || blue != _blue) 107 | { 108 | gain = _gain; 109 | red = _red; 110 | green = _green; 111 | blue = _blue; 112 | prepareCalibration(); 113 | } 114 | } 115 | 116 | /** 117 | * @brief print RGBW calibration parameters when no data is received 118 | * 119 | */ 120 | void printCalibration() 121 | { 122 | #ifdef SerialPort 123 | char output[128]; 124 | snprintf(output, sizeof(output),"RGBW => Gain: %i/255, red: %i, green: %i, blue: %i\r\n", gain, red, green, blue); 125 | SerialPort.print(output); 126 | #endif 127 | } 128 | } calibrationConfig; 129 | #endif 130 | 131 | -------------------------------------------------------------------------------- /include/framestate.h: -------------------------------------------------------------------------------- 1 | /* framestate.h 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2025 awawa-dev 6 | * 7 | * https://github.com/awawa-dev/HyperSPI 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | #ifndef FRAMESTATE_H 29 | #define FRAMESTATE_H 30 | 31 | /** 32 | * @brief my AWA frame protocol definition 33 | * 34 | */ 35 | enum class AwaProtocol 36 | { 37 | HEADER_A, 38 | HEADER_w, 39 | HEADER_a, 40 | HEADER_HI, 41 | HEADER_LO, 42 | HEADER_CRC, 43 | VERSION2_GAIN, 44 | VERSION2_RED, 45 | VERSION2_GREEN, 46 | VERSION2_BLUE, 47 | RED, 48 | GREEN, 49 | BLUE, 50 | FLETCHER1, 51 | FLETCHER2, 52 | FLETCHER_EXT 53 | }; 54 | 55 | /** 56 | * @brief Contains current state of the incoming frame 57 | * 58 | */ 59 | class 60 | { 61 | volatile AwaProtocol state = AwaProtocol::HEADER_A; 62 | bool protocolVersion2 = false; 63 | uint8_t CRC = 0; 64 | uint16_t count = 0; 65 | uint16_t currentLed = 0; 66 | uint16_t fletcher1 = 0; 67 | uint16_t fletcher2 = 0; 68 | uint16_t fletcherExt = 0; 69 | uint8_t position = 0; 70 | 71 | public: 72 | ColorDefinition color; 73 | 74 | /** 75 | * @brief Reset statistics for new frame 76 | * 77 | * @param input 78 | */ 79 | inline void init(byte input) 80 | { 81 | currentLed = 0; 82 | count = input * 0x100; 83 | CRC = input; 84 | fletcher1 = 0; 85 | fletcher2 = 0; 86 | fletcherExt = 0; 87 | position = 0; 88 | base.dropLateFrame(); 89 | } 90 | 91 | /** 92 | * @brief get computed CRC 93 | * 94 | * @return uint8_t 95 | */ 96 | inline uint8_t getCRC() 97 | { 98 | return CRC; 99 | } 100 | 101 | /** 102 | * @brief Get the color count reported by the frame 103 | * 104 | * @return uint16_t 105 | */ 106 | inline uint16_t getCount() 107 | { 108 | return count; 109 | } 110 | 111 | /** 112 | * @brief Get the Fletcher1 total sum 113 | * 114 | * @return uint16_t 115 | */ 116 | inline uint16_t getFletcher1() 117 | { 118 | return fletcher1; 119 | } 120 | 121 | /** 122 | * @brief Get the Fletcher2 total sum 123 | * 124 | * @return uint16_t 125 | */ 126 | inline uint16_t getFletcher2() 127 | { 128 | return fletcher2; 129 | } 130 | 131 | /** 132 | * @brief Get the FletcherExt total sum 133 | * 134 | * @return uint16_t 135 | */ 136 | inline uint16_t getFletcherExt() 137 | { 138 | return (fletcherExt != 0x41) ? fletcherExt : 0xaa; 139 | } 140 | 141 | /** 142 | * @brief Get and increase the current Led index 143 | * 144 | * @return uint16_t 145 | */ 146 | inline uint16_t getCurrentLedIndex() 147 | { 148 | return currentLed++; 149 | } 150 | 151 | /** 152 | * @brief Set if frame protocol version 2 (contains calibration data) 153 | * 154 | * @param newVer 155 | */ 156 | inline void setProtocolVersion2(bool newVer) 157 | { 158 | protocolVersion2 = newVer; 159 | } 160 | 161 | /** 162 | * @brief Verify if frame protocol version 2 (contains calibration data) 163 | * 164 | * @return true 165 | * @return false 166 | */ 167 | inline bool isProtocolVersion2() 168 | { 169 | return protocolVersion2; 170 | } 171 | 172 | /** 173 | * @brief Set new AWA frame state 174 | * 175 | * @param newState 176 | */ 177 | inline void setState(AwaProtocol newState) 178 | { 179 | state = newState; 180 | } 181 | 182 | /** 183 | * @brief Get current AWA frame state 184 | * 185 | * @return AwaProtocol 186 | */ 187 | inline AwaProtocol getState() 188 | { 189 | return state; 190 | } 191 | 192 | /** 193 | * @brief Update CRC based on current and previuos input 194 | * 195 | * @param input 196 | */ 197 | inline void computeCRC(byte input) 198 | { 199 | count += input; 200 | CRC = CRC ^ input ^ 0x55; 201 | } 202 | 203 | /** 204 | * @brief Update Fletcher checksumn for incoming input 205 | * 206 | * @param input 207 | */ 208 | inline void addFletcher(byte input) 209 | { 210 | fletcher1 = (fletcher1 + (uint16_t)input) % 255; 211 | fletcher2 = (fletcher2 + fletcher1) % 255; 212 | fletcherExt = (fletcherExt + (input ^ (position++))) % 255; 213 | } 214 | 215 | /** 216 | * @brief Check if the calibration data was updated and calculate new one 217 | * 218 | */ 219 | inline void updateIncomingCalibration() 220 | { 221 | #ifdef NEOPIXEL_RGBW 222 | if (protocolVersion2) 223 | { 224 | calibrationConfig.setParamsAndPrepareCalibration(calibration.gain, calibration.red, calibration.green, calibration.blue); 225 | } 226 | #endif 227 | } 228 | 229 | 230 | #ifdef NEOPIXEL_RGBW 231 | /** 232 | * @brief Compute && correct the white channel 233 | * 234 | */ 235 | inline void rgb2rgbw() 236 | { 237 | color.W = min(channelCorrection.red[color.R], 238 | min(channelCorrection.green[color.G], 239 | channelCorrection.blue[color.B])); 240 | color.R -= channelCorrection.red[color.W]; 241 | color.G -= channelCorrection.green[color.W]; 242 | color.B -= channelCorrection.blue[color.W]; 243 | color.W = channelCorrection.white[color.W]; 244 | } 245 | #endif 246 | 247 | /** 248 | * @brief Incoming calibration data 249 | * 250 | */ 251 | struct 252 | { 253 | uint8_t gain = 0; 254 | uint8_t red = 0; 255 | uint8_t green = 0; 256 | uint8_t blue = 0; 257 | } calibration; 258 | 259 | } frameState; 260 | 261 | #endif -------------------------------------------------------------------------------- /include/main.h: -------------------------------------------------------------------------------- 1 | /* main.h 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2025 awawa-dev 6 | * 7 | * https://github.com/awawa-dev/HyperSPI 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | #ifndef MAIN_H 29 | #define MAIN_H 30 | 31 | #if defined(ARDUINO_ARCH_ESP32) 32 | #define MAX_BUFFER (3013 * 3 + 1) 33 | #else 34 | #define MAX_BUFFER (4096) 35 | #endif 36 | #define HELLO_MESSAGE "\r\nWelcome!\r\nAwa driver 10." 37 | 38 | #include "calibration.h" 39 | #include "statistics.h" 40 | #include "base.h" 41 | #include "framestate.h" 42 | 43 | /** 44 | * @brief separete thread on core 1 for handling serial communication using cyclic buffer 45 | * 46 | */ 47 | 48 | bool serialTaskHandler() 49 | { 50 | uint16_t incomingSize = min(SerialPort.available(), MAX_BUFFER - 1); 51 | 52 | if (incomingSize > 0) 53 | { 54 | if (base.queueEnd + incomingSize < MAX_BUFFER) 55 | { 56 | SerialPort.read(&(base.buffer[base.queueEnd]), incomingSize); 57 | base.queueEnd += incomingSize; 58 | } 59 | else 60 | { 61 | int left = MAX_BUFFER - base.queueEnd; 62 | SerialPort.read(&(base.buffer[base.queueEnd]), left); 63 | SerialPort.read(&(base.buffer[0]), incomingSize - left); 64 | base.queueEnd = incomingSize - left; 65 | } 66 | } 67 | 68 | return (incomingSize > 0); 69 | } 70 | 71 | void updateMainStatistics(unsigned long currentTime, unsigned long deltaTime, bool hasData) 72 | { 73 | if (hasData && deltaTime >= 1000 && deltaTime <= 1025 && statistics.getGoodFrames() > 3) 74 | statistics.update(currentTime); 75 | else if (deltaTime > 1025) 76 | statistics.lightReset(currentTime, hasData); 77 | } 78 | 79 | /** 80 | * @brief process received data on core 0 81 | * 82 | */ 83 | void processData() 84 | { 85 | // update and print statistics 86 | unsigned long currentTime = millis(); 87 | unsigned long deltaTime = currentTime - statistics.getStartTime(); 88 | 89 | updateMainStatistics(currentTime, deltaTime, base.queueCurrent != base.queueEnd); 90 | 91 | if (statistics.getStartTime() + 5000 < millis()) 92 | { 93 | frameState.setState(AwaProtocol::HEADER_A); 94 | } 95 | 96 | // render waiting frame if available 97 | if (base.hasLateFrameToRender()) 98 | base.renderLeds(false); 99 | 100 | // process received data 101 | while (base.queueCurrent != base.queueEnd) 102 | { 103 | byte input = base.buffer[base.queueCurrent++]; 104 | 105 | if (base.queueCurrent >= MAX_BUFFER) 106 | { 107 | base.queueCurrent = 0; 108 | yield(); 109 | } 110 | 111 | switch (frameState.getState()) 112 | { 113 | case AwaProtocol::HEADER_A: 114 | // assume it's protocol version 1, verify it later 115 | frameState.setProtocolVersion2(false); 116 | if (input == 'A') 117 | frameState.setState(AwaProtocol::HEADER_w); 118 | break; 119 | 120 | case AwaProtocol::HEADER_w: 121 | if (input == 'w') 122 | frameState.setState(AwaProtocol::HEADER_a); 123 | else 124 | frameState.setState(AwaProtocol::HEADER_A); 125 | break; 126 | 127 | case AwaProtocol::HEADER_a: 128 | // detect protocol version 129 | if (input == 'a') 130 | frameState.setState(AwaProtocol::HEADER_HI); 131 | else if (input == 'A') 132 | { 133 | frameState.setState(AwaProtocol::HEADER_HI); 134 | frameState.setProtocolVersion2(true); 135 | } 136 | else 137 | frameState.setState(AwaProtocol::HEADER_A); 138 | break; 139 | 140 | case AwaProtocol::HEADER_HI: 141 | // initialize new frame properties 142 | statistics.increaseTotal(); 143 | frameState.init(input); 144 | frameState.setState(AwaProtocol::HEADER_LO); 145 | break; 146 | 147 | case AwaProtocol::HEADER_LO: 148 | frameState.computeCRC(input); 149 | frameState.setState(AwaProtocol::HEADER_CRC); 150 | break; 151 | 152 | case AwaProtocol::HEADER_CRC: 153 | // verify CRC and create/update LED driver if neccesery 154 | if (frameState.getCRC() == input) 155 | { 156 | uint16_t ledSize = frameState.getCount() + 1; 157 | 158 | // sanity check 159 | if (ledSize > 4096) 160 | frameState.setState(AwaProtocol::HEADER_A); 161 | else 162 | { 163 | if (ledSize != base.getLedsNumber()) 164 | base.initLedStrip(ledSize); 165 | 166 | frameState.setState(AwaProtocol::RED); 167 | } 168 | } 169 | else if (frameState.getCount() == 0x2aa2 && (input == 0x15 || input == 0x35)) 170 | { 171 | statistics.print(currentTime, base.processDataHandle, base.processSerialHandle); 172 | 173 | if (input == 0x15) 174 | SerialPort.println(HELLO_MESSAGE); 175 | delay(10); 176 | 177 | currentTime = millis(); 178 | statistics.reset(currentTime); 179 | frameState.setState(AwaProtocol::HEADER_A); 180 | } 181 | else 182 | frameState.setState(AwaProtocol::HEADER_A); 183 | break; 184 | 185 | case AwaProtocol::RED: 186 | frameState.color.R = input; 187 | frameState.addFletcher(input); 188 | 189 | frameState.setState(AwaProtocol::GREEN); 190 | break; 191 | 192 | case AwaProtocol::GREEN: 193 | frameState.color.G = input; 194 | frameState.addFletcher(input); 195 | 196 | frameState.setState(AwaProtocol::BLUE); 197 | break; 198 | 199 | case AwaProtocol::BLUE: 200 | frameState.color.B = input; 201 | frameState.addFletcher(input); 202 | 203 | #ifdef NEOPIXEL_RGBW 204 | // calculate RGBW from RGB using provided calibration data 205 | frameState.rgb2rgbw(); 206 | #endif 207 | 208 | // set pixel, increase the index and check if it was the last LED color to come 209 | if (base.setStripPixel(frameState.getCurrentLedIndex(), frameState.color)) 210 | { 211 | frameState.setState(AwaProtocol::RED); 212 | } 213 | else 214 | { 215 | if (frameState.isProtocolVersion2()) 216 | frameState.setState(AwaProtocol::VERSION2_GAIN); 217 | else 218 | frameState.setState(AwaProtocol::FLETCHER1); 219 | } 220 | 221 | break; 222 | 223 | case AwaProtocol::VERSION2_GAIN: 224 | frameState.calibration.gain = input; 225 | frameState.addFletcher(input); 226 | 227 | frameState.setState(AwaProtocol::VERSION2_RED); 228 | break; 229 | 230 | case AwaProtocol::VERSION2_RED: 231 | frameState.calibration.red = input; 232 | frameState.addFletcher(input); 233 | 234 | frameState.setState(AwaProtocol::VERSION2_GREEN); 235 | break; 236 | 237 | case AwaProtocol::VERSION2_GREEN: 238 | frameState.calibration.green = input; 239 | frameState.addFletcher(input); 240 | 241 | frameState.setState(AwaProtocol::VERSION2_BLUE); 242 | break; 243 | 244 | case AwaProtocol::VERSION2_BLUE: 245 | frameState.calibration.blue = input; 246 | frameState.addFletcher(input); 247 | 248 | frameState.setState(AwaProtocol::FLETCHER1); 249 | break; 250 | 251 | case AwaProtocol::FLETCHER1: 252 | // initial frame data integrity check 253 | if (input != frameState.getFletcher1()) 254 | frameState.setState(AwaProtocol::HEADER_A); 255 | else 256 | frameState.setState(AwaProtocol::FLETCHER2); 257 | break; 258 | 259 | case AwaProtocol::FLETCHER2: 260 | // initial frame data integrity check 261 | if (input != frameState.getFletcher2()) 262 | frameState.setState(AwaProtocol::HEADER_A); 263 | else 264 | frameState.setState(AwaProtocol::FLETCHER_EXT); 265 | break; 266 | 267 | case AwaProtocol::FLETCHER_EXT: 268 | // final frame data integrity check 269 | if (input == frameState.getFletcherExt()) 270 | { 271 | statistics.increaseGood(); 272 | 273 | base.renderLeds(true); 274 | 275 | #ifdef NEOPIXEL_RGBW 276 | // if received the calibration data, update it now 277 | if (frameState.isProtocolVersion2()) 278 | { 279 | frameState.updateIncomingCalibration(); 280 | } 281 | #endif 282 | 283 | currentTime = millis(); 284 | deltaTime = currentTime - statistics.getStartTime(); 285 | updateMainStatistics(currentTime, deltaTime, true); 286 | 287 | yield(); 288 | } 289 | 290 | frameState.setState(AwaProtocol::HEADER_A); 291 | break; 292 | } 293 | } 294 | } 295 | 296 | #endif 297 | -------------------------------------------------------------------------------- /include/statistics.h: -------------------------------------------------------------------------------- 1 | /* stats.h 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2025 awawa-dev 6 | * 7 | * https://github.com/awawa-dev/HyperSPI 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | #ifndef STATISTICS_H 29 | #define STATISTICS_H 30 | 31 | // statistics (stats sent only when there is no communication) 32 | class 33 | { 34 | unsigned long startTime = 0; 35 | uint16_t goodFrames = 0; 36 | uint16_t showFrames = 0; 37 | uint16_t totalFrames = 0; 38 | uint16_t finalGoodFrames = 0; 39 | uint16_t finalShowFrames = 0; 40 | uint16_t finalTotalFrames = 0; 41 | 42 | public: 43 | /** 44 | * @brief Get the start time of the current period 45 | * 46 | * @return unsigned long 47 | */ 48 | inline unsigned long getStartTime() 49 | { 50 | return startTime; 51 | } 52 | 53 | /** 54 | * @brief Detected new frame 55 | * 56 | */ 57 | inline void increaseTotal() 58 | { 59 | totalFrames++; 60 | } 61 | 62 | /** 63 | * @brief The frame is received and shown 64 | * 65 | */ 66 | inline void increaseShow() 67 | { 68 | showFrames++; 69 | } 70 | 71 | /** 72 | * @brief The frame is received correctly (not yet displayed) 73 | * 74 | */ 75 | inline void increaseGood() 76 | { 77 | goodFrames++; 78 | } 79 | 80 | /** 81 | * @brief Get number of correctly received frames 82 | * 83 | * @return uint16_t 84 | */ 85 | inline uint16_t getGoodFrames() 86 | { 87 | return goodFrames; 88 | } 89 | 90 | /** 91 | * @brief Period restart, save current statistics ans send them later if there is no incoming communication 92 | * 93 | * @param currentTime 94 | */ 95 | void update(unsigned long currentTime) 96 | { 97 | if (totalFrames > 0) 98 | { 99 | finalShowFrames = showFrames; 100 | finalGoodFrames = std::min(goodFrames, totalFrames); 101 | finalTotalFrames = totalFrames; 102 | } 103 | 104 | startTime = currentTime; 105 | goodFrames = 0; 106 | totalFrames = 0; 107 | showFrames = 0; 108 | } 109 | 110 | /** 111 | * @brief Print last saved statistics to the serial port 112 | * 113 | * @param curTime 114 | * @param taskHandle 115 | */ 116 | void print(unsigned long curTime, TaskHandle_t taskHandle1, TaskHandle_t taskHandle2) 117 | { 118 | char output[128]; 119 | 120 | startTime = curTime; 121 | goodFrames = 0; 122 | totalFrames = 0; 123 | showFrames = 0; 124 | 125 | snprintf(output, sizeof(output), "HyperHDR frames: %u (FPS), receiv.: %u, good: %u, incompl.: %u, mem1: %i, mem2: %i, heap: %i\r\n", 126 | finalShowFrames, finalTotalFrames,finalGoodFrames,(finalTotalFrames - finalGoodFrames), 127 | (taskHandle1 != nullptr) ? uxTaskGetStackHighWaterMark(taskHandle1) : 0, 128 | (taskHandle2 != nullptr) ? uxTaskGetStackHighWaterMark(taskHandle2) : 0, 129 | ESP.getFreeHeap()); 130 | SerialPort.print(output); 131 | 132 | #if defined(NEOPIXEL_RGBW) 133 | calibrationConfig.printCalibration(); 134 | #endif 135 | } 136 | 137 | /** 138 | * @brief Reset statistics 139 | * 140 | */ 141 | void reset(unsigned long currentTime) 142 | { 143 | startTime = currentTime; 144 | 145 | finalShowFrames = 0; 146 | finalGoodFrames = 0; 147 | finalTotalFrames = 0; 148 | 149 | goodFrames = 0; 150 | totalFrames = 0; 151 | showFrames = 0; 152 | } 153 | 154 | void lightReset(unsigned long curTime, bool hasData) 155 | { 156 | if (hasData) 157 | startTime = curTime; 158 | 159 | goodFrames = 0; 160 | totalFrames = 0; 161 | showFrames = 0; 162 | } 163 | 164 | } statistics; 165 | 166 | #endif -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; Configurable parameters: 2 | ; SERIALCOM_SPEED = speed of the serial port (baud), performance/debug data only, global [env] section 3 | ; DATA_PIN = pin/GPIO for the LED strip data channel, specific [board] section 4 | ; 5 | ; MULTI-SEGMENT SUPPORT 6 | ; You can define second segment to handle. Add following parameters (with -D prefix to the build_flags sections). 7 | ; SECOND_SEGMENT_START_INDEX = start index of the second segment 8 | ; SECOND_SEGMENT_DATA_PIN = pin/GPIO for the second segment LED strip clock channel 9 | ; SECOND_SEGMENT_REVERSED = if defined, the segment is reversed, the first LED of the second segment becomes the last 10 | ; For example: we have first LED strip with 512 leds and the second LED strip with 400 leds. 11 | ; For such configuration you need to set SECOND_SEGMENT_START_INDEX to 512 here, compile and upload the firmware. 12 | ; In HyperHDR you should have a single LED strip that contains 912 leds. 13 | ; SECOND_SEGMENT_REVERSED could be helpful if you start both segments in the middle of the bottom edge and join again at the top (both ends). 14 | ; Example build string for the second segment (append it to the global [env] or to the specific [board] section): 15 | ; build_flags = ... -DSECOND_SEGMENT_START_INDEX=450 -DSECOND_SEGMENT_DATA_PIN=4 -DSECOND_SEGMENT_REVERSED 16 | 17 | 18 | [platformio] 19 | default_envs = SK6812_RGBW_COLD, SK6812_RGBW_NEUTRAL, WS281x_RGB, s2_mini_SK6812_RGBW_COLD, s2_mini_SK6812_RGBW_NEUTRAL, s2_mini_WS281x_RGB, esp8266_SK6812_RGBW_COLD, esp8266_SK6812_RGBW_NEUTRAL, esp8266_WS281x_RGB 20 | 21 | [env] 22 | framework = arduino 23 | extra_scripts = pre:extra_script.py 24 | build_flags = -DSERIALCOM_SPEED=115200 25 | 26 | ;========================================================== 27 | ; ESP32 board 28 | ;========================================================== 29 | [esp32] 30 | platform = espressif32@6.0.0 31 | lib_deps = https://github.com/awawa-dev/NeoPixelBus.git#HyperSerialESP32, hideakitai/ESP32DMASPI@0.2.0 32 | test_ignore = 33 | 34 | [env:SK6812_RGBW_COLD] 35 | build_flags = -DNEOPIXEL_RGBW -DCOLD_WHITE -DDATA_PIN=2 ${env.build_flags} 36 | custom_prog_version = esp32_SK6812_RGBW_COLD 37 | 38 | board = esp32dev 39 | platform = ${esp32.platform} 40 | lib_deps = ${esp32.lib_deps} 41 | test_ignore = ${esp32.test_ignore} 42 | 43 | [env:SK6812_RGBW_NEUTRAL] 44 | build_flags = -DNEOPIXEL_RGBW -DDATA_PIN=2 ${env.build_flags} 45 | custom_prog_version = esp32_SK6812_RGBW_NEUTRAL 46 | 47 | board = esp32dev 48 | platform = ${esp32.platform} 49 | lib_deps = ${esp32.lib_deps} 50 | test_ignore = ${esp32.test_ignore} 51 | 52 | [env:WS281x_RGB] 53 | build_flags = -DNEOPIXEL_RGB -DDATA_PIN=2 ${env.build_flags} 54 | custom_prog_version = esp32_WS281x_RGB 55 | 56 | board = esp32dev 57 | platform = ${esp32.platform} 58 | lib_deps = ${esp32.lib_deps} 59 | test_ignore = ${esp32.test_ignore} 60 | 61 | ;========================================================== 62 | ; ESP32-S2 board 63 | ;========================================================== 64 | [esp32_lolin_s2_mini] 65 | platform = espressif32@6.0.0 66 | lib_deps = https://github.com/awawa-dev/NeoPixelBus.git#HyperSerialESP32, hideakitai/ESP32DMASPI@0.2.0 67 | test_ignore = * 68 | 69 | [env:s2_mini_SK6812_RGBW_COLD] 70 | build_flags = -DNEOPIXEL_RGBW -DCOLD_WHITE -DDATA_PIN=2 ${env.build_flags} 71 | custom_prog_version = esp32_s2_mini_SK6812_RGBW_COLD 72 | 73 | board = lolin_s2_mini 74 | board_build.mcu = esp32s2 75 | platform = ${esp32_lolin_s2_mini.platform} 76 | lib_deps = ${esp32_lolin_s2_mini.lib_deps} 77 | test_ignore = ${esp32_lolin_s2_mini.test_ignore} 78 | 79 | [env:s2_mini_SK6812_RGBW_NEUTRAL] 80 | build_flags = -DNEOPIXEL_RGBW -DDATA_PIN=2 ${env.build_flags} 81 | custom_prog_version = esp32_s2_mini_SK6812_RGBW_NEUTRAL 82 | 83 | board = lolin_s2_mini 84 | board_build.mcu = esp32s2 85 | platform = ${esp32_lolin_s2_mini.platform} 86 | lib_deps = ${esp32_lolin_s2_mini.lib_deps} 87 | test_ignore = ${esp32_lolin_s2_mini.test_ignore} 88 | 89 | [env:s2_mini_WS281x_RGB] 90 | build_flags = -DNEOPIXEL_RGB -DDATA_PIN=2 ${env.build_flags} 91 | custom_prog_version = esp32_s2_mini_WS281x_RGB 92 | 93 | board = lolin_s2_mini 94 | board_build.mcu = esp32s2 95 | platform = ${esp32_lolin_s2_mini.platform} 96 | lib_deps = ${esp32_lolin_s2_mini.lib_deps} 97 | test_ignore = ${esp32_lolin_s2_mini.test_ignore} 98 | 99 | ;========================================================== 100 | ; ESP8266 board 101 | ;========================================================== 102 | [esp8266] 103 | platform = espressif8266@4.1.0 104 | lib_deps = https://github.com/awawa-dev/NeoPixelBus.git#HyperSerialESP32 105 | test_ignore = * 106 | 107 | [env:esp8266_SK6812_RGBW_COLD] 108 | build_flags = -DNEOPIXEL_RGBW -DCOLD_WHITE ${env.build_flags} 109 | custom_prog_version = esp8266_SK6812_RGBW_COLD 110 | 111 | board = d1_mini 112 | platform = ${esp8266.platform} 113 | lib_deps = ${esp8266.lib_deps} 114 | test_ignore = ${esp8266.test_ignore} 115 | 116 | [env:esp8266_SK6812_RGBW_NEUTRAL] 117 | build_flags = -DNEOPIXEL_RGBW ${env.build_flags} 118 | custom_prog_version = esp8266_SK6812_RGBW_NEUTRAL 119 | 120 | board = d1_mini 121 | platform = ${esp8266.platform} 122 | lib_deps = ${esp8266.lib_deps} 123 | test_ignore = ${esp8266.test_ignore} 124 | 125 | [env:esp8266_WS281x_RGB] 126 | build_flags = -DNEOPIXEL_RGB ${env.build_flags} 127 | custom_prog_version = esp8266_WS281x_RGB 128 | 129 | board = d1_mini 130 | platform = ${esp8266.platform} 131 | lib_deps = ${esp8266.lib_deps} 132 | test_ignore = ${esp8266.test_ignore} 133 | 134 | -------------------------------------------------------------------------------- /rp2040/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | vscode 3 | -------------------------------------------------------------------------------- /rp2040/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(CMAKE_SYSTEM_NAME Generic) 2 | # User configuration section starts here 3 | 4 | # Some boards, such as the first Adafruit revisions, may have trouble booting properly 5 | # due to bad componets used in the design. 6 | # Turn this setting to ON if your rp2040 is not detected after firmware upload and reset 7 | set(BOOT_WORKAROUND ON) 8 | 9 | # Default output data pin for the non-SPI LED strips (only for sk6812/ws2812b) 10 | set(OUTPUT_DATA_PIN 14) 11 | 12 | # Use multi-segment, starting index of second led strip or OFF to disable 13 | set(SECOND_SEGMENT_INDEX OFF) 14 | 15 | # If multi-segment is used and it's reversed, set this option to ON to enable reversing 16 | set(SECOND_SEGMENT_REVERSED OFF) 17 | 18 | # HyperSPI communication interface with Raspberry Pi 19 | set(OUTPUT_SPI_DATA_PIN 4) 20 | set(OUTPUT_SPI_CLOCK_PIN 2) 21 | set(OUTPUT_SPI_CHIP_SELECT 5) 22 | set(OUTPUT_SPI_INTERFACE spi0) 23 | 24 | # User configuration section ends here 25 | # Usually you don't need to change anything below this section 26 | 27 | if(NOT CMAKE_HOST_WIN32) 28 | string(ASCII 27 EscChar) 29 | set(ColorReset "${EscChar}[m") 30 | set(GreenColor "${EscChar}[32m") 31 | set(YellowColor "${EscChar}[33m") 32 | endif() 33 | 34 | if (OVERRIDE_DATA_PIN) 35 | set(OUTPUT_DATA_PIN ${OVERRIDE_DATA_PIN}) 36 | message( STATUS "${YellowColor}Overriding output Neopixel Data GPIO: ${OUTPUT_DATA_PIN}${ColorReset}") 37 | endif() 38 | 39 | if (OVERRIDE_SPI_DATA_PIN) 40 | set(OUTPUT_SPI_DATA_PIN ${OVERRIDE_SPI_DATA_PIN}) 41 | message( STATUS "${YellowColor}Overriding input SPI Data GPIO: ${OUTPUT_SPI_DATA_PIN}${ColorReset}") 42 | endif() 43 | 44 | if (OVERRIDE_SPI_CLOCK_PIN) 45 | set(OUTPUT_SPI_CLOCK_PIN ${OVERRIDE_SPI_CLOCK_PIN}) 46 | message( STATUS "${YellowColor}Overriding input SPI Clock GPIO: ${OUTPUT_SPI_CLOCK_PIN}${ColorReset}") 47 | endif() 48 | 49 | if (OVERRIDE_SPI_CHIP_SELECT) 50 | set(OUTPUT_SPI_CHIP_SELECT ${OVERRIDE_SPI_CHIP_SELECT}) 51 | message( STATUS "${YellowColor}Overriding input SPI chip select GPIO: ${OUTPUT_SPI_CHIP_SELECT}${ColorReset}") 52 | endif() 53 | 54 | if (OVERRIDE_SPI_INTERFACE) 55 | set(OUTPUT_SPI_INTERFACE ${OVERRIDE_SPI_INTERFACE}) 56 | message( STATUS "${YellowColor}Overriding input SPI Interface: ${OUTPUT_SPI_INTERFACE}${ColorReset}") 57 | endif() 58 | 59 | cmake_minimum_required(VERSION 3.13) 60 | 61 | set(PICO_SDK_PATH ${CMAKE_CURRENT_SOURCE_DIR}/HyperSerialPico/sdk/pico) 62 | set(FREERTOS_KERNEL_PATH ${CMAKE_CURRENT_SOURCE_DIR}/HyperSerialPico/sdk/freertos) 63 | include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake) 64 | include(${FREERTOS_KERNEL_PATH}/portable/ThirdParty/GCC/RP2040/FreeRTOS_Kernel_import.cmake) 65 | 66 | project(HyperSPI_Pico C CXX ASM) 67 | pico_sdk_init() 68 | 69 | set(PICO_PROGRAM_MAIN_ENTRY "../hyperspi.cpp") 70 | set(DISABLE_SPI_LEDS ON) 71 | add_definitions(-DSPI_INTERFACE=${OUTPUT_SPI_INTERFACE} 72 | -DSPI_DATA_PIN=${OUTPUT_SPI_DATA_PIN} 73 | -DSPI_CLOCK_PIN=${OUTPUT_SPI_CLOCK_PIN} 74 | -DSPI_CHIP_SELECT=${OUTPUT_SPI_CHIP_SELECT}) 75 | add_subdirectory(HyperSerialPico) 76 | -------------------------------------------------------------------------------- /rp2040/hyperspi.cpp: -------------------------------------------------------------------------------- 1 | /* hyperspi.cpp 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2025 awawa-dev 6 | * 7 | * https://github.com/awawa-dev/HyperSPI 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | #define TUD_OPT_HIGH_SPEED 29 | 30 | 31 | #include "FreeRTOS.h" 32 | #include "task.h" 33 | #include 34 | #include 35 | #include "pico/stdio/driver.h" 36 | #include "pico/stdlib.h" 37 | #include "pico/stdio.h" 38 | #include "pico/stdio_usb.h" 39 | #include "pico/multicore.h" 40 | #include "pico/sem.h" 41 | #include "leds.h" 42 | 43 | #define SPI_FRAME_SIZE 1536 44 | #define SPI_SPEED 20833333 45 | 46 | uint8_t spiBuffer[SPI_FRAME_SIZE] {}; 47 | 48 | /////////////////////////////////////////////////////////////////////////// 49 | // DO NOT EDIT THIS FILE. ADJUST THE CONFIGURATION IN THE CmakeList.txt // 50 | /////////////////////////////////////////////////////////////////////////// 51 | 52 | #define _STR(x) #x 53 | #define _XSTR(x) _STR(x) 54 | #define VAR_NAME_VALUE(var) #var " = " _XSTR(var) 55 | #define _XSTR2(x,y) _STR(x) _STR(y) 56 | #define VAR_NAME_VALUE2(var) #var " = " _XSTR2(var) 57 | 58 | #if defined(BOOT_WORKAROUND) && defined(PICO_XOSC_STARTUP_DELAY_MULTIPLIER) 59 | #pragma message("Enabling boot workaround") 60 | #pragma message(VAR_NAME_VALUE(PICO_XOSC_STARTUP_DELAY_MULTIPLIER)) 61 | #endif 62 | 63 | 64 | #ifdef NEOPIXEL_RGBW 65 | #pragma message(VAR_NAME_VALUE(NEOPIXEL_RGBW)) 66 | #endif 67 | #ifdef NEOPIXEL_RGB 68 | #pragma message(VAR_NAME_VALUE(NEOPIXEL_RGB)) 69 | #endif 70 | #ifdef COLD_WHITE 71 | #pragma message(VAR_NAME_VALUE(COLD_WHITE)) 72 | #endif 73 | 74 | #ifdef NEOPIXEL_RGBW 75 | #define LED_DRIVER sk6812 76 | #elif NEOPIXEL_RGB 77 | #define LED_DRIVER ws2812 78 | #endif 79 | 80 | #pragma message(VAR_NAME_VALUE(DATA_PIN)) 81 | #pragma message(VAR_NAME_VALUE(SPI_INTERFACE)) 82 | #pragma message(VAR_NAME_VALUE(SPI_DATA_PIN)) 83 | #pragma message(VAR_NAME_VALUE(SPI_CLOCK_PIN)) 84 | #pragma message(VAR_NAME_VALUE(SPI_CHIP_SELECT)) 85 | 86 | #if defined(SECOND_SEGMENT_START_INDEX) 87 | #pragma message("Using parallel mode for segments") 88 | 89 | #ifdef NEOPIXEL_RGBW 90 | #undef LED_DRIVER 91 | #define LED_DRIVER sk6812p 92 | #define LED_DRIVER2 sk6812p 93 | #elif NEOPIXEL_RGB 94 | #undef LED_DRIVER 95 | #define LED_DRIVER ws2812p 96 | #define LED_DRIVER2 ws2812p 97 | #else 98 | #error "Parallel mode is unsupportd for selected LEDs configuration" 99 | #endif 100 | 101 | #pragma message(VAR_NAME_VALUE(LED_DRIVER)) 102 | #pragma message(VAR_NAME_VALUE(SECOND_SEGMENT_START_INDEX)) 103 | #pragma message(VAR_NAME_VALUE(LED_DRIVER2)) 104 | #pragma message(VAR_NAME_VALUE(SECOND_SEGMENT_REVERSED)) 105 | #else 106 | #pragma message(VAR_NAME_VALUE(LED_DRIVER)) 107 | 108 | typedef LedDriver LED_DRIVER2; 109 | #endif 110 | 111 | ///////////////////////////////////////////////////////////////////////// 112 | #define delay(x) sleep_ms(x) 113 | #define yield() busy_wait_us(100) 114 | #define millis xTaskGetTickCount 115 | 116 | #include "main.h" 117 | 118 | static void core1() 119 | { 120 | for( ;; ) 121 | { 122 | if (sem_acquire_timeout_us(&base.receiverSemaphore, portMAX_DELAY)) 123 | { 124 | processData(); 125 | } 126 | } 127 | } 128 | 129 | static uint initSpi(uint baudrate, spi_inst_t* _spi, uint32_t spiMosipin, uint32_t spiClockpin, uint32_t spiSelectPin) 130 | { 131 | uint selectedSpeed = spi_init(_spi, baudrate); 132 | printf("Using baudrate: %i\n", selectedSpeed); 133 | spi_set_format(_spi, 8, SPI_CPOL_1, SPI_CPHA_1, SPI_MSB_FIRST); 134 | hw_set_bits(&spi_get_hw(_spi)->dmacr, SPI_SSPDMACR_TXDMAE_BITS | SPI_SSPDMACR_RXDMAE_BITS); 135 | spi_set_slave(_spi, true); 136 | hw_set_bits(&spi_get_hw(_spi)->cr1, SPI_SSPCR1_SSE_BITS); 137 | gpio_set_function(spiClockpin, GPIO_FUNC_SPI); 138 | gpio_set_function(spiMosipin, GPIO_FUNC_SPI); 139 | gpio_set_function(spiSelectPin, GPIO_FUNC_SPI); 140 | gpio_set_function(3, GPIO_FUNC_SPI); 141 | bi_decl(bi_4pins_with_func(spiMosipin, 3, spiClockpin, spiSelectPin, GPIO_FUNC_SPI)); 142 | 143 | uint dmaChannelNumber = dma_claim_unused_channel(true); 144 | dma_channel_config channelConfig = dma_channel_get_default_config(dmaChannelNumber); 145 | channel_config_set_transfer_data_size(&channelConfig, DMA_SIZE_8); 146 | channel_config_set_dreq(&channelConfig, spi_get_dreq(_spi, false)); 147 | channel_config_set_read_increment(&channelConfig, false); 148 | channel_config_set_write_increment(&channelConfig, true); 149 | dma_channel_configure(dmaChannelNumber, &channelConfig, 150 | spiBuffer, 151 | &spi_get_hw(_spi)->dr, 152 | sizeof(spiBuffer), 153 | false); 154 | return dmaChannelNumber; 155 | } 156 | 157 | static void core0( void *pvParameters ) 158 | { 159 | uint dmaChannelNumber = initSpi(SPI_SPEED, SPI_INTERFACE, SPI_DATA_PIN, SPI_CLOCK_PIN, SPI_CHIP_SELECT); 160 | 161 | while(true) 162 | { 163 | dma_channel_set_write_addr(dmaChannelNumber, spiBuffer, true); 164 | dma_channel_wait_for_finish_blocking(dmaChannelNumber); 165 | 166 | int remains = SPI_FRAME_SIZE; 167 | int wanted, received; 168 | do 169 | { 170 | wanted = std::min(MAX_BUFFER - base.queueEnd, MAX_BUFFER - 1); 171 | received = std::min(remains, wanted); 172 | memcpy((void*)&(base.buffer[base.queueEnd]), &(spiBuffer[SPI_FRAME_SIZE - remains]), received); 173 | base.queueEnd = (base.queueEnd + received) % (MAX_BUFFER); 174 | remains -= received; 175 | }while(remains); 176 | 177 | sem_release(&base.receiverSemaphore); 178 | } 179 | } 180 | 181 | int main(void) 182 | { 183 | stdio_init_all(); 184 | 185 | sem_init(&base.receiverSemaphore, 0, 1); 186 | 187 | multicore_launch_core1(core1); 188 | 189 | xTaskCreate(core0, 190 | "HyperSPI:core0", 191 | configMINIMAL_STACK_SIZE * 2, 192 | NULL, 193 | (configMAX_PRIORITIES - 1), 194 | &base.processSerialHandle); 195 | 196 | vTaskStartScheduler(); 197 | panic_unsupported(); 198 | } 199 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /* main.cpp 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2025 awawa-dev 6 | * 7 | * https://github.com/awawa-dev/HyperSPI 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | #include 29 | 30 | #if defined(ARDUINO_ARCH_ESP32) 31 | #if defined(CONFIG_IDF_TARGET_ESP32S2) 32 | #define VSPI FSPI 33 | #define VSPI_HOST FSPI_HOST 34 | #endif 35 | #include 36 | #else 37 | #include 38 | #define TaskHandle_t void* 39 | #define xSemaphoreHandle void* 40 | #define uxTaskGetStackHighWaterMark(x) 0 41 | #endif 42 | 43 | #include 44 | 45 | /////////////////////////////////////////////////////////////////////////// 46 | // DO NOT EDIT THIS FILE. ADJUST THE CONFIGURATION IN THE platformio.ini // 47 | /////////////////////////////////////////////////////////////////////////// 48 | 49 | #define _STR(x) #x 50 | #define _XSTR(x) _STR(x) 51 | #define VAR_NAME_VALUE(var) #var " = " _XSTR(var) 52 | #define _XSTR2(x,y) _STR(x) _STR(y) 53 | #define VAR_NAME_VALUE2(var) #var " = " _XSTR2(var) 54 | 55 | #ifdef NEOPIXEL_RGBW 56 | #pragma message(VAR_NAME_VALUE(NEOPIXEL_RGBW)) 57 | #endif 58 | #ifdef NEOPIXEL_RGB 59 | #pragma message(VAR_NAME_VALUE(NEOPIXEL_RGB)) 60 | #endif 61 | #ifdef COLD_WHITE 62 | #pragma message(VAR_NAME_VALUE(COLD_WHITE)) 63 | #endif 64 | 65 | #pragma message(VAR_NAME_VALUE(SERIALCOM_SPEED)) 66 | 67 | #if defined(ARDUINO_ARCH_ESP32) 68 | #if defined(CONFIG_IDF_TARGET_ESP32S2) 69 | #ifdef NEOPIXEL_RGBW 70 | #define LED_DRIVER NeoPixelBus 71 | #elif NEOPIXEL_RGB 72 | #define LED_DRIVER NeoPixelBus 73 | #endif 74 | #else 75 | #ifdef NEOPIXEL_RGBW 76 | #define LED_DRIVER NeoPixelBus 77 | #elif NEOPIXEL_RGB 78 | #define LED_DRIVER NeoPixelBus 79 | #endif 80 | #endif 81 | 82 | #pragma message(VAR_NAME_VALUE(DATA_PIN)) 83 | #ifdef CLOCK_PIN 84 | #pragma message(VAR_NAME_VALUE(CLOCK_PIN)) 85 | #endif 86 | 87 | #if defined(SECOND_SEGMENT_START_INDEX) 88 | #if defined(NEOPIXEL_RGBW) || defined(NEOPIXEL_RGB) 89 | #define PARALLEL_MODE 90 | #endif 91 | 92 | #if defined(PARALLEL_MODE) 93 | #pragma message("Using parallel mode for segments") 94 | #endif 95 | 96 | #if defined(CONFIG_IDF_TARGET_ESP32S2) 97 | #ifdef NEOPIXEL_RGBW 98 | #ifdef PARALLEL_MODE 99 | #undef LED_DRIVER 100 | #define LED_DRIVER NeoPixelBus 101 | #define LED_DRIVER2 NeoPixelBus 102 | #else 103 | #define LED_DRIVER2 NeoPixelBus 104 | #endif 105 | #elif NEOPIXEL_RGB 106 | #ifdef PARALLEL_MODE 107 | #undef LED_DRIVER 108 | #define LED_DRIVER NeoPixelBus 109 | #define LED_DRIVER2 NeoPixelBus 110 | #else 111 | #define LED_DRIVER2 NeoPixelBus 112 | #endif 113 | #endif 114 | #else 115 | #ifdef NEOPIXEL_RGBW 116 | #ifdef PARALLEL_MODE 117 | #undef LED_DRIVER 118 | #define LED_DRIVER NeoPixelBus 119 | #define LED_DRIVER2 NeoPixelBus 120 | #else 121 | #define LED_DRIVER2 NeoPixelBus 122 | #endif 123 | #elif NEOPIXEL_RGB 124 | #ifdef PARALLEL_MODE 125 | #undef LED_DRIVER 126 | #define LED_DRIVER NeoPixelBus 127 | #define LED_DRIVER2 NeoPixelBus 128 | #else 129 | #define LED_DRIVER2 NeoPixelBus 130 | #endif 131 | #endif 132 | #endif 133 | #pragma message(VAR_NAME_VALUE2(LED_DRIVER)) 134 | #pragma message(VAR_NAME_VALUE(SECOND_SEGMENT_START_INDEX)) 135 | #pragma message(VAR_NAME_VALUE(SECOND_SEGMENT_DATA_PIN)) 136 | #ifdef SECOND_SEGMENT_CLOCK_PIN 137 | #pragma message(VAR_NAME_VALUE(SECOND_SEGMENT_CLOCK_PIN)) 138 | #endif 139 | #pragma message(VAR_NAME_VALUE2(LED_DRIVER2)) 140 | #else 141 | #pragma message(VAR_NAME_VALUE2(LED_DRIVER)) 142 | 143 | class LED_DRIVER2 { 144 | public: 145 | bool CanShow() {return true;} 146 | void Show(bool safe) {} 147 | }; 148 | #endif 149 | #else 150 | 151 | #ifdef NEOPIXEL_RGBW 152 | #define LED_DRIVER NeoPixelBus 153 | #elif NEOPIXEL_RGB 154 | #define LED_DRIVER NeoPixelBus 155 | #elif SPILED_APA102 156 | #define LED_DRIVER NeoPixelBus 157 | #elif SPILED_WS2801 158 | #define LED_DRIVER NeoPixelBus 159 | #endif 160 | 161 | #pragma message(VAR_NAME_VALUE2(LED_DRIVER)) 162 | 163 | class LED_DRIVER2 { 164 | public: 165 | bool CanShow() {return true;} 166 | void Show(bool safe) {} 167 | }; 168 | #endif 169 | 170 | #define SerialPort Serial 171 | #include "main.h" 172 | 173 | 174 | 175 | static const uint32_t REAL_BUFFER = 1536; 176 | static const uint32_t BUFFER_SIZE = REAL_BUFFER + 8; 177 | 178 | #if defined(ARDUINO_ARCH_ESP32) 179 | ESP32DMASPI::Slave slave; 180 | uint8_t *spi_slave_tx_buf; 181 | uint8_t *spi_slave_rx_buf; 182 | constexpr uint8_t CORE_TASK_SPI_SLAVE{0}; 183 | constexpr uint8_t CORE_TASK_PROCESS_BUFFER{0}; 184 | static TaskHandle_t task_handle_wait_spi = 0; 185 | static TaskHandle_t task_handle_process_buffer = 0; 186 | portMUX_TYPE spiMutex = portMUX_INITIALIZER_UNLOCKED; 187 | 188 | void readSpi() 189 | { 190 | taskENTER_CRITICAL(&spiMutex); 191 | 192 | if (spi_slave_rx_buf[REAL_BUFFER] == 0xAA) 193 | { 194 | if (base.queueEnd + REAL_BUFFER < MAX_BUFFER) 195 | { 196 | memcpy(&(base.buffer[base.queueEnd]), spi_slave_rx_buf, REAL_BUFFER); 197 | base.queueEnd += REAL_BUFFER; 198 | } 199 | else 200 | { 201 | int left = MAX_BUFFER - base.queueEnd; 202 | memcpy(&(base.buffer[base.queueEnd]), spi_slave_rx_buf, left); 203 | memcpy(&(base.buffer[0]), spi_slave_rx_buf + left, REAL_BUFFER - left); 204 | base.queueEnd = REAL_BUFFER - left; 205 | } 206 | spi_slave_rx_buf[REAL_BUFFER] = 0; 207 | taskEXIT_CRITICAL(&spiMutex); 208 | 209 | if (base.processDataHandle != nullptr) 210 | xSemaphoreGive(base.i2sXSemaphore); 211 | } 212 | else 213 | { 214 | taskEXIT_CRITICAL(&spiMutex); 215 | } 216 | } 217 | 218 | void task_wait_spi(void *pvParameters) 219 | { 220 | while (1) 221 | { 222 | ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 223 | 224 | slave.wait(spi_slave_rx_buf, spi_slave_tx_buf, BUFFER_SIZE); 225 | 226 | xTaskNotifyGive(task_handle_process_buffer); 227 | } 228 | } 229 | 230 | void task_process_buffer(void *pvParameters) 231 | { 232 | while (1) 233 | { 234 | ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 235 | 236 | readSpi(); 237 | 238 | slave.pop(); 239 | 240 | xTaskNotifyGive(task_handle_wait_spi); 241 | } 242 | } 243 | 244 | /** 245 | * @brief separete thread for handling incoming data using cyclic buffer 246 | * 247 | * @param parameters 248 | */ 249 | void processDataTask(void * parameters) 250 | { 251 | for(;;) 252 | { 253 | xSemaphoreTake(base.i2sXSemaphore, portMAX_DELAY); 254 | processData(); 255 | } 256 | } 257 | #endif 258 | 259 | 260 | 261 | 262 | void setup() 263 | { 264 | // Init serial port 265 | Serial.setTimeout(50); 266 | Serial.begin(SERIALCOM_SPEED); 267 | 268 | #if defined(NEOPIXEL_RGBW) || defined(NEOPIXEL_RGB) 269 | #ifdef NEOPIXEL_RGBW 270 | #ifdef COLD_WHITE 271 | calibrationConfig.setParamsAndPrepareCalibration(0xFF, 0xA0, 0xA0, 0xA0); 272 | #else 273 | calibrationConfig.setParamsAndPrepareCalibration(0xFF, 0xB0, 0xB0, 0x70); 274 | #endif 275 | #endif 276 | #endif 277 | 278 | #if !defined(CONFIG_IDF_TARGET_ESP32S2) 279 | // Display config 280 | Serial.println(HELLO_MESSAGE); 281 | #if defined(SECOND_SEGMENT_START_INDEX) 282 | SerialPort.write("SECOND_SEGMENT_START_INDEX = "); 283 | SerialPort.println(SECOND_SEGMENT_START_INDEX); 284 | #endif 285 | 286 | // Colorspace/Led type info 287 | #if defined(NEOPIXEL_RGBW) || defined(NEOPIXEL_RGB) 288 | #ifdef NEOPIXEL_RGBW 289 | #ifdef COLD_WHITE 290 | Serial.println("NeoPixelBus SK6812 cold GRBW. "); 291 | #else 292 | Serial.println("NeoPixelBus SK6812 neutral GRBW. "); 293 | #endif 294 | calibrationConfig.printCalibration(); 295 | #else 296 | Serial.println("NeoPixelBus ws281x type (GRB)."); 297 | #endif 298 | #endif 299 | 300 | //Serial.flush(); 301 | delay(50); 302 | #endif 303 | 304 | 305 | 306 | // spi stuff 307 | #if defined(ARDUINO_ARCH_ESP32) 308 | bool multicore = false; 309 | 310 | if (multicore) 311 | { 312 | // create a semaphore to synchronize threads 313 | base.i2sXSemaphore = xSemaphoreCreateBinary(); 314 | 315 | // create new task for handling received serial data on core 0 316 | xTaskCreatePinnedToCore( 317 | processDataTask, 318 | "processDataTask", 319 | 5096, 320 | NULL, 321 | 1, 322 | &base.processDataHandle, 323 | 0); 324 | } 325 | 326 | spi_slave_tx_buf = slave.allocDMABuffer(BUFFER_SIZE); 327 | spi_slave_rx_buf = slave.allocDMABuffer(BUFFER_SIZE); 328 | 329 | slave.setDataMode(SPI_MODE0); 330 | slave.setMaxTransferSize(BUFFER_SIZE); 331 | slave.setDMAChannel(1); 332 | slave.setQueueSize(1); 333 | 334 | #if defined(CONFIG_IDF_TARGET_ESP32S2) 335 | // sck: 7, miso: 34 (no important), MOSI: 11, spi select: 12 336 | slave.begin(VSPI, 7, 34, 11, 12); 337 | #else 338 | slave.begin(VSPI); 339 | #endif 340 | 341 | xTaskCreatePinnedToCore(task_wait_spi, "task_wait_spi", 2048, NULL, 2, &task_handle_wait_spi, CORE_TASK_SPI_SLAVE); 342 | xTaskNotifyGive(task_handle_wait_spi); 343 | xTaskCreatePinnedToCore(task_process_buffer, "task_process_buffer", 2048, NULL, 2, &task_handle_process_buffer, CORE_TASK_PROCESS_BUFFER); 344 | #else 345 | SPISlave.onData([](uint8_t *data, size_t len) 346 | { 347 | if (base.queueEnd + len < MAX_BUFFER) 348 | { 349 | memcpy(&(base.buffer[base.queueEnd]), data, len); 350 | base.queueEnd += len; 351 | } 352 | else 353 | { 354 | int left = MAX_BUFFER - base.queueEnd; 355 | memcpy(&(base.buffer[base.queueEnd]), data, left); 356 | memcpy(&(base.buffer[0]), data + left, len - left); 357 | base.queueEnd = len - left; 358 | } 359 | }); 360 | 361 | SPISlave.begin(); 362 | 363 | SPISlave.setStatus(millis()); 364 | #endif 365 | } 366 | 367 | void loop() 368 | { 369 | if (base.processDataHandle == nullptr) 370 | { 371 | processData(); 372 | } 373 | 374 | // print 375 | unsigned long currentTime = millis(); 376 | unsigned long deltaTime = currentTime - statistics.getStartTime(); 377 | if (Serial && deltaTime > 3000) 378 | statistics.print(currentTime, base.processDataHandle, base.processSerialHandle); 379 | } 380 | 381 | -------------------------------------------------------------------------------- /test/test_MultiSegment/main.cpp: -------------------------------------------------------------------------------- 1 | /* test_MultiSegment/main.cpp 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2025 awawa-dev 6 | * 7 | * https://github.com/awawa-dev/HyperSPI 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | #define NO_GLOBAL_SERIAL 29 | #define HYPERSERIAL_TESTING 30 | 31 | #include 32 | #include 33 | #include 34 | #include "calibration.h" 35 | 36 | /////////////////////////////////////////////////////////////////////////////////// 37 | /////////////////////////////////////////////////////////////////////////////////// 38 | /////////////////////// AWA PROTOCOL CORRECTNESS TEST ///////////////////////////// 39 | /////////////////////////////////////////////////////////////////////////////////// 40 | /////////////////////////////////////////////////////////////////////////////////// 41 | 42 | #define TEST_LEDS_NUMBER 1025 43 | uint8_t _ledBuffer[TEST_LEDS_NUMBER * 3 + 6 + 8]; 44 | 45 | #define LED_DRIVER ProtocolTester 46 | #define LED_DRIVER2 ProtocolTester 47 | #define SECOND_SEGMENT_START_INDEX 513 48 | #define SECOND_SEGMENT_CLOCK_PIN 100 49 | #define SECOND_SEGMENT_DATA_PIN 101 50 | 51 | /** 52 | * @brief Mockup Serial class to simulate the real communition 53 | * 54 | */ 55 | 56 | class SerialTester 57 | { 58 | int frameSize = 0; 59 | int sent = 0; 60 | 61 | public: 62 | 63 | void createTestFrame(bool _white_channel_calibration, uint8_t _white_channel_limit = 0, 64 | uint8_t _white_channel_red = 0, uint8_t _white_channel_green = 0, 65 | uint8_t _white_channel_blue = 0) 66 | { 67 | _ledBuffer[0] = 'A'; 68 | _ledBuffer[1] = 'w'; 69 | _ledBuffer[2] = (_white_channel_calibration) ? 'A' : 'a'; 70 | _ledBuffer[4] = (TEST_LEDS_NUMBER-1) & 0xff; 71 | _ledBuffer[3] = ((TEST_LEDS_NUMBER-1) >> 8) & 0xff; 72 | _ledBuffer[5] = _ledBuffer[3] ^ _ledBuffer[4] ^ 0x55; 73 | 74 | uint8_t* writer = &(_ledBuffer[6]); 75 | uint8_t* hasher = writer; 76 | 77 | for(int i=0; i < TEST_LEDS_NUMBER; i++) 78 | { 79 | *(writer++)=random(255); 80 | *(writer++)=random(255); 81 | *(writer++)=random(255); 82 | } 83 | 84 | 85 | if (_white_channel_calibration) 86 | { 87 | *(writer++) = _white_channel_limit; 88 | *(writer++) = _white_channel_red; 89 | *(writer++) = _white_channel_green; 90 | *(writer++) = _white_channel_blue; 91 | } 92 | 93 | uint16_t fletcher1 = 0, fletcher2 = 0, fletcherExt = 0; 94 | uint8_t position = 0; 95 | while (hasher < writer) 96 | { 97 | fletcherExt = (fletcherExt + (*(hasher) ^ (position++))) % 255; 98 | fletcher1 = (fletcher1 + *(hasher++)) % 255; 99 | fletcher2 = (fletcher2 + fletcher1) % 255; 100 | } 101 | *(writer++) = (uint8_t)fletcher1; 102 | *(writer++) = (uint8_t)fletcher2; 103 | *(writer++) = (uint8_t)((fletcherExt != 0x41) ? fletcherExt : 0xaa); 104 | 105 | frameSize = (int)(writer - _ledBuffer); 106 | sent = 0; 107 | } 108 | 109 | 110 | inline size_t write(const char * s) 111 | { 112 | return 0; 113 | } 114 | 115 | inline size_t print(unsigned char, int = DEC) 116 | { 117 | return 0; 118 | } 119 | 120 | int available(void) 121 | { 122 | if (sent < frameSize) 123 | { 124 | return std::min(std::max((int)(random(64)), 1), frameSize - sent); 125 | } 126 | 127 | return 0; 128 | } 129 | 130 | int toSend(void) 131 | { 132 | return frameSize - sent; 133 | } 134 | 135 | int getFrameSize() 136 | { 137 | return frameSize; 138 | } 139 | 140 | size_t read(uint8_t *buffer, size_t size) 141 | { 142 | int max = std::min(frameSize - sent, (int)size); 143 | if (max > 0) 144 | { 145 | memcpy(buffer, &(_ledBuffer[sent]), max); 146 | sent += max; 147 | return max; 148 | } 149 | return 0; 150 | } 151 | 152 | void println(const String &s) 153 | { 154 | 155 | } 156 | } SerialPort; 157 | 158 | 159 | /** 160 | * @brief Mockup LED driver to verify correctness of the received LEDs color values 161 | * 162 | */ 163 | 164 | class ProtocolTester { 165 | int ledCount; 166 | int currentIndex; 167 | int lastCount; 168 | bool first; 169 | 170 | public: 171 | ProtocolTester(int _count, int _pin) : ProtocolTester(_count) 172 | { 173 | if (_pin == SECOND_SEGMENT_DATA_PIN) 174 | first = false; 175 | } 176 | 177 | ProtocolTester(int _count) 178 | { 179 | first = true; 180 | ledCount = _count; 181 | currentIndex = 0; 182 | lastCount = 0; 183 | } 184 | 185 | bool CanShow() 186 | { 187 | return true; 188 | } 189 | 190 | void Show(bool safe = true) 191 | { 192 | lastCount = currentIndex; 193 | currentIndex = 0; 194 | } 195 | 196 | void Begin() 197 | { 198 | 199 | } 200 | 201 | void Begin(int _pin1, int _pin2, int _pin3, int _pin4) 202 | { 203 | if (_pin1 == SECOND_SEGMENT_CLOCK_PIN) 204 | first = false; 205 | } 206 | 207 | int getLastCount() 208 | { 209 | return lastCount; 210 | } 211 | 212 | /** 213 | * @brief Very important: verify LED color, compare it to the origin 214 | * 215 | * @param indexPixel 216 | * @param color 217 | */ 218 | #ifdef NEOPIXEL_RGBW 219 | void SetPixelColor(uint16_t indexPixel, RgbwColor color) 220 | { 221 | TEST_ASSERT_EQUAL_INT_MESSAGE(currentIndex, indexPixel, "Unexpected LED index"); 222 | TEST_ASSERT_LESS_THAN_MESSAGE(TEST_LEDS_NUMBER, indexPixel, "LED index out of scope"); 223 | 224 | uint8_t *c = (first) ? &(_ledBuffer[6 + indexPixel * 3]) : &(_ledBuffer[6 + (indexPixel + SECOND_SEGMENT_START_INDEX) * 3]); 225 | uint8_t r = *(c++); 226 | uint8_t g = *(c++); 227 | uint8_t b = *(c++); 228 | 229 | uint8_t w = min(channelCorrection.red[r], 230 | min(channelCorrection.green[g], 231 | channelCorrection.blue[b])); 232 | r -= channelCorrection.red[w]; 233 | g -= channelCorrection.green[w]; 234 | b -= channelCorrection.blue[w]; 235 | w = channelCorrection.white[w]; 236 | 237 | TEST_ASSERT_EQUAL_UINT8(r, color.R); 238 | TEST_ASSERT_EQUAL_UINT8(g, color.G); 239 | TEST_ASSERT_EQUAL_UINT8(b, color.B); 240 | TEST_ASSERT_EQUAL_UINT8(w, color.W); 241 | 242 | currentIndex = indexPixel + 1; 243 | lastCount = 0; 244 | } 245 | #else 246 | void SetPixelColor(uint16_t indexPixel, RgbColor color) 247 | { 248 | TEST_ASSERT_EQUAL_INT_MESSAGE(currentIndex, indexPixel, "Unexpected LED index"); 249 | TEST_ASSERT_LESS_THAN_MESSAGE(TEST_LEDS_NUMBER, indexPixel, "LED index out of scope"); 250 | 251 | uint8_t *c = (first) ? &(_ledBuffer[6 + indexPixel * 3]) : &(_ledBuffer[6 + (indexPixel + SECOND_SEGMENT_START_INDEX) * 3]); 252 | uint8_t r = *(c++); 253 | uint8_t g = *(c++); 254 | uint8_t b = *(c++); 255 | 256 | TEST_ASSERT_EQUAL_UINT8(r, color.R); 257 | TEST_ASSERT_EQUAL_UINT8(g, color.G); 258 | TEST_ASSERT_EQUAL_UINT8(b, color.B); 259 | 260 | currentIndex = indexPixel + 1; 261 | lastCount = 0; 262 | } 263 | #endif 264 | }; 265 | 266 | #include "main.h" 267 | 268 | 269 | 270 | /** 271 | * @brief Send RGBW calibration data and verify it all (including proper colors rendering) 272 | * 273 | */ 274 | void MultiSegmentTest_SendRgbwCalibration() 275 | { 276 | // set all calibration values to test 277 | SerialPort.createTestFrame(true, 10, 20, 30, 40); 278 | base.queueCurrent = 0; 279 | base.queueEnd = 0; 280 | statistics.update(0); 281 | 282 | while(SerialPort.toSend() > 0) 283 | { 284 | serialTaskHandler(); 285 | } 286 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 287 | processData(); 288 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 289 | TEST_ASSERT_EQUAL_INT_MESSAGE(SECOND_SEGMENT_START_INDEX, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up (segment1)"); 290 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER - SECOND_SEGMENT_START_INDEX, base.getLedStrip2()->getLastCount(), "Not all LEDs were set up(segment2)"); 291 | TEST_ASSERT_EQUAL_MESSAGE(true, calibrationConfig.compareCalibrationSettings(10,20,30,40), "Incorrect calibration result"); 292 | 293 | // should not change if the frame doesnt contain calibration data 294 | SerialPort.createTestFrame(false); 295 | statistics.update(0); 296 | 297 | while(SerialPort.toSend() > 0) 298 | { 299 | serialTaskHandler(); 300 | } 301 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 302 | processData(); 303 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 304 | TEST_ASSERT_EQUAL_INT_MESSAGE(SECOND_SEGMENT_START_INDEX, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up (segment1)"); 305 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER - SECOND_SEGMENT_START_INDEX, base.getLedStrip2()->getLastCount(), "Not all LEDs were set up(segment2)"); 306 | TEST_ASSERT_EQUAL_MESSAGE(true, calibrationConfig.compareCalibrationSettings(10,20,30,40), "Incorrect calibration result"); 307 | 308 | // last test 309 | SerialPort.createTestFrame(true, 255, 128, 128, 128); 310 | statistics.update(0); 311 | 312 | while(SerialPort.toSend() > 0) 313 | { 314 | serialTaskHandler(); 315 | } 316 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 317 | processData(); 318 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 319 | TEST_ASSERT_EQUAL_INT_MESSAGE(SECOND_SEGMENT_START_INDEX, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up (segment1)"); 320 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER - SECOND_SEGMENT_START_INDEX, base.getLedStrip2()->getLastCount(), "Not all LEDs were set up(segment2)"); 321 | TEST_ASSERT_EQUAL_MESSAGE(true, calibrationConfig.compareCalibrationSettings(255,128,128,128), "Incorrect calibration result"); 322 | } 323 | 324 | /** 325 | * @brief Send 100 RGB/RGBW frames and verify it all (including proper colors rendering) 326 | * 327 | */ 328 | void MultiSegmentTest_Send100Frames() 329 | { 330 | base.queueCurrent = 0; 331 | base.queueEnd = 0; 332 | 333 | for(int i = 0; i < 100; i++) 334 | { 335 | SerialPort.createTestFrame(false); 336 | statistics.update(0); 337 | 338 | while(SerialPort.toSend() > 0) 339 | { 340 | serialTaskHandler(); 341 | } 342 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 343 | processData(); 344 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 345 | TEST_ASSERT_EQUAL_INT_MESSAGE(SECOND_SEGMENT_START_INDEX, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up (segment1)"); 346 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER - SECOND_SEGMENT_START_INDEX, base.getLedStrip2()->getLastCount(), "Not all LEDs were set up(segment2)"); 347 | } 348 | } 349 | 350 | /** 351 | * @brief Send 200 RGB/RGBW valid/invalid frames and verify it all (including proper colors rendering) 352 | * 353 | */ 354 | void MultiSegmentTest_Send200UncertainFrames() 355 | { 356 | base.queueCurrent = 0; 357 | base.queueEnd = 0; 358 | 359 | for(int i = 0; i < 200; i++) 360 | { 361 | SerialPort.createTestFrame(false); 362 | statistics.update(0); 363 | 364 | bool damaged = (random(255) % 2) == 0; 365 | int index; 366 | uint8_t backup; 367 | if (damaged) 368 | { 369 | index = random(SerialPort.getFrameSize()); 370 | backup = _ledBuffer[index]; 371 | _ledBuffer[index] = backup ^ 0xff; 372 | } 373 | 374 | while(SerialPort.toSend() > 0) 375 | { 376 | serialTaskHandler(); 377 | } 378 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 379 | processData(); 380 | if (damaged) 381 | { 382 | char buffer[128]; 383 | snprintf(buffer, sizeof(buffer), "Damaged frame was received: [%d]=>%d", index, backup); 384 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), buffer); 385 | base.getLedStrip1()->Show(); 386 | base.getLedStrip2()->Show(); 387 | frameState.setState(AwaProtocol::HEADER_A); 388 | } 389 | else 390 | { 391 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 392 | TEST_ASSERT_EQUAL_INT_MESSAGE(SECOND_SEGMENT_START_INDEX, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up (segment1)"); 393 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER - SECOND_SEGMENT_START_INDEX, base.getLedStrip2()->getLastCount(), "Not all LEDs were set up(segment2)"); 394 | } 395 | } 396 | } 397 | 398 | /////////////////////////////////////////////////////////////////////////////////// 399 | /////////////////////////////////////////////////////////////////////////////////// 400 | ///////////////////////////// UNIT TEST ROUTINES ////////////////////////////////// 401 | /////////////////////////////////////////////////////////////////////////////////// 402 | /////////////////////////////////////////////////////////////////////////////////// 403 | 404 | void setup() 405 | { 406 | delay(1000); 407 | randomSeed(analogRead(0)); 408 | UNITY_BEGIN(); 409 | #ifdef NEOPIXEL_RGBW 410 | RUN_TEST(MultiSegmentTest_SendRgbwCalibration); 411 | #endif 412 | RUN_TEST(MultiSegmentTest_Send100Frames); 413 | RUN_TEST(MultiSegmentTest_Send200UncertainFrames); 414 | UNITY_END(); 415 | } 416 | 417 | void loop() 418 | { 419 | } 420 | -------------------------------------------------------------------------------- /test/test_MultiSegmentReversed/main.cpp: -------------------------------------------------------------------------------- 1 | /* test_MultiSegmentReversed/main.cpp 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2025 awawa-dev 6 | * 7 | * https://github.com/awawa-dev/HyperSPI 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | #define NO_GLOBAL_SERIAL 29 | #define HYPERSERIAL_TESTING 30 | 31 | #include 32 | #include 33 | #include 34 | #include "calibration.h" 35 | 36 | /////////////////////////////////////////////////////////////////////////////////// 37 | /////////////////////////////////////////////////////////////////////////////////// 38 | /////////////////////// AWA PROTOCOL CORRECTNESS TEST ///////////////////////////// 39 | /////////////////////////////////////////////////////////////////////////////////// 40 | /////////////////////////////////////////////////////////////////////////////////// 41 | 42 | #define TEST_LEDS_NUMBER 1025 43 | uint8_t _ledBuffer[TEST_LEDS_NUMBER * 3 + 6 + 8]; 44 | 45 | #define LED_DRIVER ProtocolTester 46 | #define LED_DRIVER2 ProtocolTester 47 | #define SECOND_SEGMENT_START_INDEX 513 48 | #define SECOND_SEGMENT_CLOCK_PIN 100 49 | #define SECOND_SEGMENT_DATA_PIN 101 50 | #define SECOND_SEGMENT_REVERSED 51 | 52 | /** 53 | * @brief Mockup Serial class to simulate the real communition 54 | * 55 | */ 56 | 57 | class SerialTester 58 | { 59 | int frameSize = 0; 60 | int sent = 0; 61 | 62 | public: 63 | 64 | void createTestFrame(bool _white_channel_calibration, uint8_t _white_channel_limit = 0, 65 | uint8_t _white_channel_red = 0, uint8_t _white_channel_green = 0, 66 | uint8_t _white_channel_blue = 0) 67 | { 68 | _ledBuffer[0] = 'A'; 69 | _ledBuffer[1] = 'w'; 70 | _ledBuffer[2] = (_white_channel_calibration) ? 'A' : 'a'; 71 | _ledBuffer[4] = (TEST_LEDS_NUMBER-1) & 0xff; 72 | _ledBuffer[3] = ((TEST_LEDS_NUMBER-1) >> 8) & 0xff; 73 | _ledBuffer[5] = _ledBuffer[3] ^ _ledBuffer[4] ^ 0x55; 74 | 75 | uint8_t* writer = &(_ledBuffer[6]); 76 | uint8_t* hasher = writer; 77 | 78 | for(int i=0; i < TEST_LEDS_NUMBER; i++) 79 | { 80 | *(writer++)=random(255); 81 | *(writer++)=random(255); 82 | *(writer++)=random(255); 83 | } 84 | 85 | 86 | if (_white_channel_calibration) 87 | { 88 | *(writer++) = _white_channel_limit; 89 | *(writer++) = _white_channel_red; 90 | *(writer++) = _white_channel_green; 91 | *(writer++) = _white_channel_blue; 92 | } 93 | 94 | uint16_t fletcher1 = 0, fletcher2 = 0, fletcherExt = 0; 95 | uint8_t position = 0; 96 | while (hasher < writer) 97 | { 98 | fletcherExt = (fletcherExt + (*(hasher) ^ (position++))) % 255; 99 | fletcher1 = (fletcher1 + *(hasher++)) % 255; 100 | fletcher2 = (fletcher2 + fletcher1) % 255; 101 | } 102 | *(writer++) = (uint8_t)fletcher1; 103 | *(writer++) = (uint8_t)fletcher2; 104 | *(writer++) = (uint8_t)((fletcherExt != 0x41) ? fletcherExt : 0xaa); 105 | 106 | frameSize = (int)(writer - _ledBuffer); 107 | sent = 0; 108 | } 109 | 110 | 111 | inline size_t write(const char * s) 112 | { 113 | return 0; 114 | } 115 | 116 | inline size_t print(unsigned char, int = DEC) 117 | { 118 | return 0; 119 | } 120 | 121 | int available(void) 122 | { 123 | if (sent < frameSize) 124 | { 125 | return std::min(std::max((int)(random(64)), 1), frameSize - sent); 126 | } 127 | 128 | return 0; 129 | } 130 | 131 | int toSend(void) 132 | { 133 | return frameSize - sent; 134 | } 135 | 136 | int getFrameSize() 137 | { 138 | return frameSize; 139 | } 140 | 141 | size_t read(uint8_t *buffer, size_t size) 142 | { 143 | int max = std::min(frameSize - sent, (int)size); 144 | if (max > 0) 145 | { 146 | memcpy(buffer, &(_ledBuffer[sent]), max); 147 | sent += max; 148 | return max; 149 | } 150 | return 0; 151 | } 152 | 153 | void println(const String &s) 154 | { 155 | 156 | } 157 | } SerialPort; 158 | 159 | 160 | /** 161 | * @brief Mockup LED driver to verify correctness of the received LEDs color values 162 | * 163 | */ 164 | 165 | class ProtocolTester { 166 | int ledCount; 167 | int currentIndex; 168 | int lastCount; 169 | bool first; 170 | 171 | public: 172 | ProtocolTester(int _count, int _pin) : ProtocolTester(_count) 173 | { 174 | if (_pin == SECOND_SEGMENT_DATA_PIN) 175 | first = false; 176 | } 177 | 178 | ProtocolTester(int _count) 179 | { 180 | first = true; 181 | ledCount = _count; 182 | currentIndex = 0; 183 | lastCount = 0; 184 | } 185 | 186 | bool CanShow() 187 | { 188 | return true; 189 | } 190 | 191 | void Show(bool safe = true) 192 | { 193 | lastCount = currentIndex; 194 | currentIndex = 0; 195 | } 196 | 197 | void Begin() 198 | { 199 | 200 | } 201 | 202 | void Begin(int _pin1, int _pin2, int _pin3, int _pin4) 203 | { 204 | if (_pin1 == SECOND_SEGMENT_CLOCK_PIN) 205 | first = false; 206 | } 207 | 208 | int getLastCount() 209 | { 210 | return lastCount; 211 | } 212 | 213 | /** 214 | * @brief Very important: verify LED color, compare it to the origin 215 | * 216 | * @param indexPixel 217 | * @param color 218 | */ 219 | #ifdef NEOPIXEL_RGBW 220 | void SetPixelColor(uint16_t indexPixel, RgbwColor color) 221 | { 222 | if (!first) 223 | TEST_ASSERT_EQUAL_INT_MESSAGE(ledCount - currentIndex - 1, indexPixel, "Unexpected LED index"); 224 | TEST_ASSERT_LESS_THAN_MESSAGE(TEST_LEDS_NUMBER, indexPixel, "LED index out of scope"); 225 | if (!first) 226 | indexPixel = TEST_LEDS_NUMBER - 1 - indexPixel; 227 | uint8_t *c = &(_ledBuffer[6 + indexPixel * 3]); 228 | uint8_t r = *(c++); 229 | uint8_t g = *(c++); 230 | uint8_t b = *(c++); 231 | 232 | uint8_t w = min(channelCorrection.red[r], 233 | min(channelCorrection.green[g], 234 | channelCorrection.blue[b])); 235 | r -= channelCorrection.red[w]; 236 | g -= channelCorrection.green[w]; 237 | b -= channelCorrection.blue[w]; 238 | w = channelCorrection.white[w]; 239 | 240 | TEST_ASSERT_EQUAL_UINT8(r, color.R); 241 | TEST_ASSERT_EQUAL_UINT8(g, color.G); 242 | TEST_ASSERT_EQUAL_UINT8(b, color.B); 243 | TEST_ASSERT_EQUAL_UINT8(w, color.W); 244 | 245 | currentIndex++; 246 | lastCount = 0; 247 | } 248 | #else 249 | void SetPixelColor(uint16_t indexPixel, RgbColor color) 250 | { 251 | if (!first) 252 | TEST_ASSERT_EQUAL_INT_MESSAGE(ledCount - currentIndex - 1, indexPixel, "Unexpected LED index"); 253 | TEST_ASSERT_LESS_THAN_MESSAGE(TEST_LEDS_NUMBER, indexPixel, "LED index out of scope"); 254 | if (!first) 255 | indexPixel = TEST_LEDS_NUMBER - 1 - indexPixel; 256 | uint8_t *c = &(_ledBuffer[6 + indexPixel * 3]); 257 | uint8_t r = *(c++); 258 | uint8_t g = *(c++); 259 | uint8_t b = *(c++); 260 | 261 | TEST_ASSERT_EQUAL_UINT8(r, color.R); 262 | TEST_ASSERT_EQUAL_UINT8(g, color.G); 263 | TEST_ASSERT_EQUAL_UINT8(b, color.B); 264 | 265 | currentIndex++; 266 | lastCount = 0; 267 | } 268 | #endif 269 | }; 270 | 271 | #include "main.h" 272 | 273 | 274 | 275 | /** 276 | * @brief Send RGBW calibration data and verify it all (including proper colors rendering) 277 | * 278 | */ 279 | void MultiSegmentReversedTest_SendRgbwCalibration() 280 | { 281 | // set all calibration values to test 282 | SerialPort.createTestFrame(true, 10, 20, 30, 40); 283 | base.queueCurrent = 0; 284 | base.queueEnd = 0; 285 | statistics.update(0); 286 | 287 | while(SerialPort.toSend() > 0) 288 | { 289 | serialTaskHandler(); 290 | } 291 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 292 | processData(); 293 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 294 | TEST_ASSERT_EQUAL_INT_MESSAGE(SECOND_SEGMENT_START_INDEX, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up (segment1)"); 295 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER - SECOND_SEGMENT_START_INDEX, base.getLedStrip2()->getLastCount(), "Not all LEDs were set up(segment2)"); 296 | TEST_ASSERT_EQUAL_MESSAGE(true, calibrationConfig.compareCalibrationSettings(10,20,30,40), "Incorrect calibration result"); 297 | 298 | // should not change if the frame doesnt contain calibration data 299 | SerialPort.createTestFrame(false); 300 | statistics.update(0); 301 | 302 | while(SerialPort.toSend() > 0) 303 | { 304 | serialTaskHandler(); 305 | } 306 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 307 | processData(); 308 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 309 | TEST_ASSERT_EQUAL_INT_MESSAGE(SECOND_SEGMENT_START_INDEX, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up (segment1)"); 310 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER - SECOND_SEGMENT_START_INDEX, base.getLedStrip2()->getLastCount(), "Not all LEDs were set up(segment2)"); 311 | TEST_ASSERT_EQUAL_MESSAGE(true, calibrationConfig.compareCalibrationSettings(10,20,30,40), "Incorrect calibration result"); 312 | 313 | // last test 314 | SerialPort.createTestFrame(true, 255, 128, 128, 128); 315 | statistics.update(0); 316 | 317 | while(SerialPort.toSend() > 0) 318 | { 319 | serialTaskHandler(); 320 | } 321 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 322 | processData(); 323 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 324 | TEST_ASSERT_EQUAL_INT_MESSAGE(SECOND_SEGMENT_START_INDEX, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up (segment1)"); 325 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER - SECOND_SEGMENT_START_INDEX, base.getLedStrip2()->getLastCount(), "Not all LEDs were set up(segment2)"); 326 | TEST_ASSERT_EQUAL_MESSAGE(true, calibrationConfig.compareCalibrationSettings(255,128,128,128), "Incorrect calibration result"); 327 | } 328 | 329 | /** 330 | * @brief Send 100 RGB/RGBW frames and verify it all (including proper colors rendering) 331 | * 332 | */ 333 | void MultiSegmentReversedTest_Send100Frames() 334 | { 335 | base.queueCurrent = 0; 336 | base.queueEnd = 0; 337 | 338 | for(int i = 0; i < 100; i++) 339 | { 340 | SerialPort.createTestFrame(false); 341 | statistics.update(0); 342 | 343 | while(SerialPort.toSend() > 0) 344 | { 345 | serialTaskHandler(); 346 | } 347 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 348 | processData(); 349 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 350 | TEST_ASSERT_EQUAL_INT_MESSAGE(SECOND_SEGMENT_START_INDEX, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up (segment1)"); 351 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER - SECOND_SEGMENT_START_INDEX, base.getLedStrip2()->getLastCount(), "Not all LEDs were set up(segment2)"); 352 | } 353 | } 354 | 355 | /** 356 | * @brief Send 200 RGB/RGBW valid/invalid frames and verify it all (including proper colors rendering) 357 | * 358 | */ 359 | void MultiSegmentReversedTest_Send200UncertainFrames() 360 | { 361 | base.queueCurrent = 0; 362 | base.queueEnd = 0; 363 | 364 | for(int i = 0; i < 200; i++) 365 | { 366 | SerialPort.createTestFrame(false); 367 | statistics.update(0); 368 | 369 | bool damaged = (random(255) % 2) == 0; 370 | int index; 371 | uint8_t backup; 372 | if (damaged) 373 | { 374 | index = random(SerialPort.getFrameSize()); 375 | backup = _ledBuffer[index]; 376 | _ledBuffer[index] = backup ^ 0xff; 377 | } 378 | 379 | while(SerialPort.toSend() > 0) 380 | { 381 | serialTaskHandler(); 382 | } 383 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 384 | processData(); 385 | if (damaged) 386 | { 387 | char buffer[128]; 388 | snprintf(buffer, sizeof(buffer), "Damaged frame was received: [%d]=>%d", index, backup); 389 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), buffer); 390 | base.getLedStrip1()->Show(); 391 | base.getLedStrip2()->Show(); 392 | frameState.setState(AwaProtocol::HEADER_A); 393 | } 394 | else 395 | { 396 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 397 | TEST_ASSERT_EQUAL_INT_MESSAGE(SECOND_SEGMENT_START_INDEX, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up (segment1)"); 398 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER - SECOND_SEGMENT_START_INDEX, base.getLedStrip2()->getLastCount(), "Not all LEDs were set up(segment2)"); 399 | } 400 | } 401 | } 402 | 403 | /////////////////////////////////////////////////////////////////////////////////// 404 | /////////////////////////////////////////////////////////////////////////////////// 405 | ///////////////////////////// UNIT TEST ROUTINES ////////////////////////////////// 406 | /////////////////////////////////////////////////////////////////////////////////// 407 | /////////////////////////////////////////////////////////////////////////////////// 408 | 409 | void setup() 410 | { 411 | delay(1000); 412 | randomSeed(analogRead(0)); 413 | UNITY_BEGIN(); 414 | #ifdef NEOPIXEL_RGBW 415 | RUN_TEST(MultiSegmentReversedTest_SendRgbwCalibration); 416 | #endif 417 | RUN_TEST(MultiSegmentReversedTest_Send100Frames); 418 | RUN_TEST(MultiSegmentReversedTest_Send200UncertainFrames); 419 | UNITY_END(); 420 | } 421 | 422 | void loop() 423 | { 424 | } 425 | -------------------------------------------------------------------------------- /test/test_SingleSegment/main.cpp: -------------------------------------------------------------------------------- 1 | /* test_SingleSegment/main.cpp 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2025 awawa-dev 6 | * 7 | * https://github.com/awawa-dev/HyperSPI 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | #define NO_GLOBAL_SERIAL 29 | #define HYPERSERIAL_TESTING 30 | 31 | #include 32 | #include 33 | #include 34 | #include "calibration.h" 35 | 36 | /////////////////////////////////////////////////////////////////////////////////// 37 | /////////////////////////////////////////////////////////////////////////////////// 38 | /////////////////////// WHITE CHANNEL CALIBRATION TEST //////////////////////////// 39 | /////////////////////////////////////////////////////////////////////////////////// 40 | /////////////////////////////////////////////////////////////////////////////////// 41 | 42 | 43 | // old calibration params 44 | float whiteLimit; 45 | uint8_t rCorrection; 46 | uint8_t gCorrection; 47 | uint8_t bCorrection; 48 | 49 | uint8_t wChannel[256]; 50 | uint8_t rChannel[256]; 51 | uint8_t gChannel[256]; 52 | uint8_t bChannel[256]; 53 | 54 | /** 55 | * @brief Old calibration procedure 56 | * 57 | * @return void 58 | */ 59 | void oldCalibration() 60 | { 61 | for (uint32_t i = 0; i < 256; i++) 62 | { 63 | // color calibration 64 | float red = rCorrection * i; // adjust red 65 | float green = gCorrection * i; // adjust green 66 | float blue = bCorrection * i; // adjust blue 67 | 68 | wChannel[i] = (uint8_t)round(std::min(whiteLimit * i, 255.0f)); 69 | rChannel[i] = (uint8_t)round(std::min(red / 0xFF, 255.0f)); 70 | gChannel[i] = (uint8_t)round(std::min(green / 0xFF, 255.0f)); 71 | bChannel[i] = (uint8_t)round(std::min(blue / 0xFF, 255.0f)); 72 | } 73 | } 74 | 75 | /** 76 | * @brief Compare old and new calibration result 77 | * 78 | * @return void 79 | */ 80 | void compareLut() 81 | { 82 | for (uint32_t i = 0; i < 256; i++) 83 | { 84 | int w = std::abs(int(wChannel[i]) - int(channelCorrection.white[i])); 85 | int r = std::abs(int(rChannel[i]) - int(channelCorrection.red[i])); 86 | int g = std::abs(int(gChannel[i]) - int(channelCorrection.green[i])); 87 | int b = std::abs(int(bChannel[i]) - int(channelCorrection.blue[i])); 88 | TEST_ASSERT_LESS_THAN(1, (int)(std::max(w, std::max(r, std::max(g, b))))); 89 | } 90 | } 91 | 92 | /** 93 | * @brief Calculate the calibration table using old and new method and compare the result 94 | * 95 | * @return void 96 | */ 97 | void CommonTest_OldAndNedCalibrationAlgorithm() 98 | { 99 | // cold white old calibration (full range) 100 | whiteLimit = 1.0f; 101 | rCorrection = 0xA0; 102 | gCorrection = 0xA0; 103 | bCorrection = 0xA0; 104 | oldCalibration(); 105 | // new procedure 106 | calibrationConfig.setParamsAndPrepareCalibration(255, 0xA0, 0xA0, 0xA0); 107 | compareLut(); 108 | 109 | // neutral white old calibration (full range) 110 | whiteLimit = 1.0f; 111 | rCorrection = 0xB0; 112 | gCorrection = 0xB0; 113 | bCorrection = 0x70; 114 | oldCalibration(); 115 | // new procedure 116 | calibrationConfig.setParamsAndPrepareCalibration(255, 0xB0, 0xB0, 0x70); 117 | compareLut(); 118 | 119 | // cold white old calibration (medium range) 120 | whiteLimit = 0.5019607843137255f; 121 | rCorrection = 0xA0; 122 | gCorrection = 0xA0; 123 | bCorrection = 0xA0; 124 | oldCalibration(); 125 | // new procedure 126 | calibrationConfig.setParamsAndPrepareCalibration(128, 0xA0, 0xA0, 0xA0); 127 | compareLut(); 128 | 129 | // neutral white old calibration (medium range) 130 | whiteLimit = 0.5019607843137255f; 131 | rCorrection = 0xB0; 132 | gCorrection = 0xB0; 133 | bCorrection = 0x70; 134 | oldCalibration(); 135 | // new procedure 136 | calibrationConfig.setParamsAndPrepareCalibration(128, 0xB0, 0xB0, 0x70); 137 | compareLut(); 138 | } 139 | 140 | /////////////////////////////////////////////////////////////////////////////////// 141 | /////////////////////////////////////////////////////////////////////////////////// 142 | /////////////////////// AWA PROTOCOL CORRECTNESS TEST ///////////////////////////// 143 | /////////////////////////////////////////////////////////////////////////////////// 144 | /////////////////////////////////////////////////////////////////////////////////// 145 | 146 | #define TEST_LEDS_NUMBER 801 147 | uint8_t _ledBuffer[TEST_LEDS_NUMBER * 3 + 6 + 8]; 148 | 149 | /** 150 | * @brief Mockup Serial class to simulate the real communition 151 | * 152 | */ 153 | 154 | class SerialTester 155 | { 156 | int frameSize = 0; 157 | int sent = 0; 158 | 159 | public: 160 | 161 | void createTestFrame(bool _white_channel_calibration, uint8_t _white_channel_limit = 0, 162 | uint8_t _white_channel_red = 0, uint8_t _white_channel_green = 0, 163 | uint8_t _white_channel_blue = 0) 164 | { 165 | _ledBuffer[0] = 'A'; 166 | _ledBuffer[1] = 'w'; 167 | _ledBuffer[2] = (_white_channel_calibration) ? 'A' : 'a'; 168 | _ledBuffer[4] = (TEST_LEDS_NUMBER-1) & 0xff; 169 | _ledBuffer[3] = ((TEST_LEDS_NUMBER-1) >> 8) & 0xff; 170 | _ledBuffer[5] = _ledBuffer[3] ^ _ledBuffer[4] ^ 0x55; 171 | 172 | uint8_t* writer = &(_ledBuffer[6]); 173 | uint8_t* hasher = writer; 174 | 175 | for(int i=0; i < TEST_LEDS_NUMBER; i++) 176 | { 177 | *(writer++)=random(255); 178 | *(writer++)=random(255); 179 | *(writer++)=random(255); 180 | } 181 | 182 | if (_white_channel_calibration) 183 | { 184 | *(writer++) = _white_channel_limit; 185 | *(writer++) = _white_channel_red; 186 | *(writer++) = _white_channel_green; 187 | *(writer++) = _white_channel_blue; 188 | } 189 | 190 | uint16_t fletcher1 = 0, fletcher2 = 0, fletcherExt = 0; 191 | uint8_t position = 0; 192 | while (hasher < writer) 193 | { 194 | fletcherExt = (fletcherExt + (*(hasher) ^ (position++))) % 255; 195 | fletcher1 = (fletcher1 + *(hasher++)) % 255; 196 | fletcher2 = (fletcher2 + fletcher1) % 255; 197 | } 198 | *(writer++) = (uint8_t)fletcher1; 199 | *(writer++) = (uint8_t)fletcher2; 200 | *(writer++) = (uint8_t)((fletcherExt != 0x41) ? fletcherExt : 0xaa); 201 | 202 | frameSize = (int)(writer - _ledBuffer); 203 | sent = 0; 204 | } 205 | 206 | 207 | inline size_t write(const char * s) 208 | { 209 | return 0; 210 | } 211 | 212 | inline size_t print(unsigned char, int = DEC) 213 | { 214 | return 0; 215 | } 216 | 217 | int available(void) 218 | { 219 | if (sent < frameSize) 220 | { 221 | return std::min(std::max((int)(random(64)), 1), frameSize - sent); 222 | } 223 | 224 | return 0; 225 | } 226 | 227 | int toSend(void) 228 | { 229 | return frameSize - sent; 230 | } 231 | 232 | int getFrameSize() 233 | { 234 | return frameSize; 235 | } 236 | 237 | size_t read(uint8_t *buffer, size_t size) 238 | { 239 | int max = std::min(frameSize - sent, (int)size); 240 | if (max > 0) 241 | { 242 | memcpy(buffer, &(_ledBuffer[sent]), max); 243 | sent += max; 244 | return max; 245 | } 246 | return 0; 247 | } 248 | 249 | void println(const String &s) 250 | { 251 | 252 | } 253 | } SerialPort; 254 | 255 | 256 | /** 257 | * @brief Mockup LED driver to verify correctness of the received LEDs color values 258 | * 259 | */ 260 | 261 | class ProtocolTester { 262 | int ledCount; 263 | int currentIndex = 0; 264 | int lastCount = 0; 265 | 266 | public: 267 | ProtocolTester(int count, int b) 268 | { 269 | ledCount = count; 270 | 271 | } 272 | 273 | ProtocolTester(int count) 274 | { 275 | ledCount = count; 276 | } 277 | 278 | bool CanShow() 279 | { 280 | return true; 281 | } 282 | 283 | void Show(bool safe = true) 284 | { 285 | lastCount = currentIndex; 286 | currentIndex = 0; 287 | } 288 | 289 | void Begin() 290 | { 291 | 292 | } 293 | 294 | void Begin(int a, int b, int c, int d) 295 | { 296 | 297 | } 298 | 299 | int getLastCount() 300 | { 301 | return lastCount; 302 | } 303 | 304 | /** 305 | * @brief Very important: verify LED color, compare it to the origin 306 | * 307 | * @param indexPixel 308 | * @param color 309 | */ 310 | #ifdef NEOPIXEL_RGBW 311 | void SetPixelColor(uint16_t indexPixel, RgbwColor color) 312 | { 313 | TEST_ASSERT_EQUAL_INT_MESSAGE(currentIndex, indexPixel, "Unexpected LED index"); 314 | TEST_ASSERT_LESS_THAN_MESSAGE(TEST_LEDS_NUMBER, indexPixel, "LED index out of scope"); 315 | uint8_t *c = &(_ledBuffer[6 + indexPixel * 3]); 316 | uint8_t r = *(c++); 317 | uint8_t g = *(c++); 318 | uint8_t b = *(c++); 319 | 320 | uint8_t w = min(channelCorrection.red[r], 321 | min(channelCorrection.green[g], 322 | channelCorrection.blue[b])); 323 | r -= channelCorrection.red[w]; 324 | g -= channelCorrection.green[w]; 325 | b -= channelCorrection.blue[w]; 326 | w = channelCorrection.white[w]; 327 | 328 | TEST_ASSERT_EQUAL_UINT8(r, color.R); 329 | TEST_ASSERT_EQUAL_UINT8(g, color.G); 330 | TEST_ASSERT_EQUAL_UINT8(b, color.B); 331 | TEST_ASSERT_EQUAL_UINT8(w, color.W); 332 | 333 | currentIndex = indexPixel + 1; 334 | lastCount = 0; 335 | } 336 | #else 337 | void SetPixelColor(uint16_t indexPixel, RgbColor color) 338 | { 339 | TEST_ASSERT_EQUAL_INT_MESSAGE(currentIndex, indexPixel, "Unexpected LED index"); 340 | TEST_ASSERT_LESS_THAN_MESSAGE(TEST_LEDS_NUMBER, indexPixel, "LED index out of scope"); 341 | uint8_t *c = &(_ledBuffer[6 + indexPixel * 3]); 342 | uint8_t r = *(c++); 343 | uint8_t g = *(c++); 344 | uint8_t b = *(c++); 345 | 346 | TEST_ASSERT_EQUAL_UINT8(r, color.R); 347 | TEST_ASSERT_EQUAL_UINT8(g, color.G); 348 | TEST_ASSERT_EQUAL_UINT8(b, color.B); 349 | 350 | currentIndex = indexPixel + 1; 351 | lastCount = 0; 352 | } 353 | #endif 354 | }; 355 | 356 | #define LED_DRIVER ProtocolTester 357 | #define LED_DRIVER2 ProtocolTester 358 | #include "main.h" 359 | 360 | 361 | 362 | /** 363 | * @brief Send RGBW calibration data and verify it all (including proper colors rendering) 364 | * 365 | */ 366 | void SingleSegmentTest_SendRgbwCalibration() 367 | { 368 | // set all calibration values to test 369 | SerialPort.createTestFrame(true, 10, 20, 30, 40); 370 | base.queueCurrent = 0; 371 | base.queueEnd = 0; 372 | statistics.update(0); 373 | 374 | while(SerialPort.toSend() > 0) 375 | { 376 | serialTaskHandler(); 377 | } 378 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 379 | processData(); 380 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 381 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up"); 382 | TEST_ASSERT_EQUAL_MESSAGE(true, calibrationConfig.compareCalibrationSettings(10,20,30,40), "Incorrect calibration result"); 383 | 384 | // should not change if the frame doesnt contain calibration data 385 | SerialPort.createTestFrame(false); 386 | statistics.update(0); 387 | 388 | while(SerialPort.toSend() > 0) 389 | { 390 | serialTaskHandler(); 391 | } 392 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 393 | processData(); 394 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 395 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up"); 396 | TEST_ASSERT_EQUAL_MESSAGE(true, calibrationConfig.compareCalibrationSettings(10,20,30,40), "Incorrect calibration result"); 397 | 398 | // last test 399 | SerialPort.createTestFrame(true, 255, 128, 128, 128); 400 | statistics.update(0); 401 | 402 | while(SerialPort.toSend() > 0) 403 | { 404 | serialTaskHandler(); 405 | } 406 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 407 | processData(); 408 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 409 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up"); 410 | TEST_ASSERT_EQUAL_MESSAGE(true, calibrationConfig.compareCalibrationSettings(255,128,128,128), "Incorrect calibration result"); 411 | } 412 | 413 | /** 414 | * @brief Send 100 RGB/RGBW frames and verify it all (including proper colors rendering) 415 | * 416 | */ 417 | void SingleSegmentTest_Send100Frames() 418 | { 419 | base.queueCurrent = 0; 420 | base.queueEnd = 0; 421 | 422 | for(int i = 0; i < 100; i++) 423 | { 424 | SerialPort.createTestFrame(false); 425 | statistics.update(0); 426 | 427 | while(SerialPort.toSend() > 0) 428 | { 429 | serialTaskHandler(); 430 | } 431 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 432 | processData(); 433 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 434 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up"); 435 | } 436 | } 437 | 438 | /** 439 | * @brief Send 200 RGB/RGBW valid/invalid frames and verify it all (including proper colors rendering) 440 | * 441 | */ 442 | void SingleSegmentTest_Send200UncertainFrames() 443 | { 444 | base.queueCurrent = 0; 445 | base.queueEnd = 0; 446 | 447 | for(int i = 0; i < 200; i++) 448 | { 449 | SerialPort.createTestFrame(false); 450 | statistics.update(0); 451 | 452 | bool damaged = (random(255) % 2) == 0; 453 | int index; 454 | uint8_t backup; 455 | if (damaged) 456 | { 457 | index = random(SerialPort.getFrameSize()); 458 | backup = _ledBuffer[index]; 459 | _ledBuffer[index] = backup ^ 0xff; 460 | } 461 | 462 | while(SerialPort.toSend() > 0) 463 | { 464 | serialTaskHandler(); 465 | } 466 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), "Unexpected initial stats value"); 467 | processData(); 468 | if (damaged) 469 | { 470 | char buffer[128]; 471 | snprintf(buffer, sizeof(buffer), "Damaged frame was received: [%d]=>%d", index, backup); 472 | TEST_ASSERT_EQUAL_INT_MESSAGE(0, statistics.getGoodFrames(), buffer); 473 | base.getLedStrip1()->Show(); 474 | frameState.setState(AwaProtocol::HEADER_A); 475 | } 476 | else 477 | { 478 | TEST_ASSERT_EQUAL_INT_MESSAGE(1, statistics.getGoodFrames(), "Frame is not received"); 479 | TEST_ASSERT_EQUAL_INT_MESSAGE(TEST_LEDS_NUMBER, base.getLedStrip1()->getLastCount(), "Not all LEDs were set up"); 480 | } 481 | } 482 | } 483 | 484 | /////////////////////////////////////////////////////////////////////////////////// 485 | /////////////////////////////////////////////////////////////////////////////////// 486 | ///////////////////////////// UNIT TEST ROUTINES ////////////////////////////////// 487 | /////////////////////////////////////////////////////////////////////////////////// 488 | /////////////////////////////////////////////////////////////////////////////////// 489 | 490 | void setup() 491 | { 492 | delay(1000); 493 | randomSeed(analogRead(0)); 494 | UNITY_BEGIN(); 495 | #ifdef NEOPIXEL_RGBW 496 | RUN_TEST(CommonTest_OldAndNedCalibrationAlgorithm); 497 | RUN_TEST(SingleSegmentTest_SendRgbwCalibration); 498 | #endif 499 | RUN_TEST(SingleSegmentTest_Send100Frames); 500 | RUN_TEST(SingleSegmentTest_Send200UncertainFrames); 501 | UNITY_END(); 502 | } 503 | 504 | void loop() 505 | { 506 | } 507 | --------------------------------------------------------------------------------