├── .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 |
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 | 
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 | 
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 |
--------------------------------------------------------------------------------