Spindicator means spinning indicator, Apple calls it “spinners”. It is a very common animation used to show the loading process progress in many software including Windows 10 and 11. This project only contains the code which will create the animation (which is essentially the chase effect). In this basic guide, we have not shown PWM to control the progress of the indicator. This guide is the building block of more complicated projects. You can add logic to start, pause and stop the spinning effect.
This is an application of LED spindicator created with IC:
1 | http://www.pcbheaven.com/userpages/hdd_led_spindicator/ |
Controlling NeoPixel with PWM can be done both with Arduino and Raspberry Pi. For this guide, you need an Arduino UNO or some equivalent development board which works with Arduino IDE, such as ESP32. The second thing you’ll need is a 24-bit NeoPixel Ring. It can be from Adafruit (or equivalent which works and is cheaper). The circuit diagram will be like this one:
---

The GND of the NeoPixel ring will be connected to the GND of Arduino.
The Data IN of the NeoPixel ring will be connected to GPIO number 6 of Arduino.
The VCc or Vin of the NeoPixel ring will be connected to a 3.3v Arduino.
The result will be like this simulation on TinkerCAD:
This is the main .ino
file (or sketch) for the above result:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | /** Copyright (C) 2018 Robert Ulbricht Modified by Abhishek Ghosh License: GNU General Public License version 3 */ #include <Adafruit_NeoPixel.h> #include "hsv.h" // data pin #define PIN 6 // led count #define CNT 24 // max Hue #define MAXHUE 256*6 int position = 0; Adafruit_NeoPixel strip = Adafruit_NeoPixel(CNT, PIN, NEO_GRB + NEO_KHZ800); void setup() { strip.begin(); } void loop() { // hue - red // saturation - max // value - 0-255 for (int i = 0; i < CNT; i++) strip.setPixelColor((i + position) % CNT, getPixelColorHsv(i, 0, 255, strip.gamma8(i * (255 / CNT)))); strip.show(); position++; position %= CNT; delay(50); } |
You need to include the below header file with the name hsv.h
in Arduino IDE:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | uint32_t getPixelColorHsv( uint16_t n, uint16_t h, uint8_t s, uint8_t v) { uint8_t r, g, b; if (!s) { // Monochromatic, all components are V r = g = b = v; } else { uint8_t sextant = h >> 8; if (sextant > 5) sextant = 5; // Limit hue sextants to defined space g = v; // Top level // Perform actual calculations /* Bottom level: --> (v * (255 - s) + error_corr + 1) / 256 */ uint16_t ww; // Intermediate result ww = v * (uint8_t)(~s); ww += 1; // Error correction ww += ww >> 8; // Error correction b = ww >> 8; uint8_t h_fraction = h & 0xff; // Position within sextant uint32_t d; // Intermediate result if (!(sextant & 1)) { // r = ...slope_up... // --> r = (v * ((255 << 8) - s * (256 - h)) + error_corr1 + error_corr2) / 65536 d = v * (uint32_t)(0xff00 - (uint16_t)(s * (256 - h_fraction))); d += d >> 8; // Error correction d += v; // Error correction r = d >> 16; } else { // r = ...slope_down... // --> r = (v * ((255 << 8) - s * h) + error_corr1 + error_corr2) / 65536 d = v * (uint32_t)(0xff00 - (uint16_t)(s * h_fraction)); d += d >> 8; // Error correction d += v; // Error correction r = d >> 16; } // Swap RGB values according to sextant. This is done in reverse order with // respect to the original because the swaps are done after the // assignments. if (!(sextant & 6)) { if (!(sextant & 1)) { uint8_t tmp = r; r = g; g = tmp; } } else { if (sextant & 1) { uint8_t tmp = r; r = g; g = tmp; } } if (sextant & 4) { uint8_t tmp = g; g = b; b = tmp; } if (sextant & 2) { uint8_t tmp = r; r = b; b = tmp; } } return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; } |
As for TinkerCAD, I had to make it a single .ino
file (that is one reason behind mentioning the license):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | /** Copyright (C) 2018 Robert Ulbricht Modified by Abhishek Ghosh License: GNU General Public License version 3 */ uint32_t getPixelColorHsv( uint16_t n, uint16_t h, uint8_t s, uint8_t v) { uint8_t r, g, b; if (!s) { // Monochromatic, all components are V r = g = b = v; } else { uint8_t sextant = h >> 8; if (sextant > 5) sextant = 5; // Limit hue sextants to defined space g = v; // Top level // Perform actual calculations /* Bottom level: --> (v * (255 - s) + error_corr + 1) / 256 */ uint16_t ww; // Intermediate result ww = v * (uint8_t)(~s); ww += 1; // Error correction ww += ww >> 8; // Error correction b = ww >> 8; uint8_t h_fraction = h & 0xff; // Position within sextant uint32_t d; // Intermediate result if (!(sextant & 1)) { // r = ...slope_up... // --> r = (v * ((255 << 8) - s * (256 - h)) + error_corr1 + error_corr2) / 65536 d = v * (uint32_t)(0xff00 - (uint16_t)(s * (256 - h_fraction))); d += d >> 8; // Error correction d += v; // Error correction r = d >> 16; } else { // r = ...slope_down... // --> r = (v * ((255 << 8) - s * h) + error_corr1 + error_corr2) / 65536 d = v * (uint32_t)(0xff00 - (uint16_t)(s * h_fraction)); d += d >> 8; // Error correction d += v; // Error correction r = d >> 16; } // Swap RGB values according to sextant. This is done in reverse order with // respect to the original because the swaps are done after the // assignments. if (!(sextant & 6)) { if (!(sextant & 1)) { uint8_t tmp = r; r = g; g = tmp; } } else { if (sextant & 1) { uint8_t tmp = r; r = g; g = tmp; } } if (sextant & 4) { uint8_t tmp = g; g = b; b = tmp; } if (sextant & 2) { uint8_t tmp = r; r = b; b = tmp; } } return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; } #include <Adafruit_NeoPixel.h> // data pin #define PIN 6 // led count #define CNT 24 // max Hue #define MAXHUE 256*6 int position = 0; Adafruit_NeoPixel strip = Adafruit_NeoPixel(CNT, PIN, NEO_GRB + NEO_KHZ800); void setup() { strip.begin(); } void loop() { // hue - red // saturation - max // value - 0-255 for (int i = 0; i < CNT; i++) strip.setPixelColor((i + position) % CNT, getPixelColorHsv(i, 0, 255, strip.gamma8(i * (255 / CNT)))); strip.show(); position++; position %= CNT; delay(50); } |
I found the original code on Robo Ulbricht’s repo on GitHub. He worked for a long time behind NeoPixel ring.