APA102 individually addressable LEDs

Posted on
Page
of 2
/ 2
Next
  • Has anyone else written any code for these bad boys?
    If you haven't heard the news - the APA102 is like a WS2812B on steroids.
    They use data and clock lines, not just data, so the timing isn't critical (and you control them with normal SPI, albeit in a weird mode). They have 4 bytes per pixel. 3 1's, 5 bit global brightness, then 8 bits per color. The per-color brightness is PWM'ed at 20khz, while the global brightness is PWMed separately at ~600hz. So yeah - these are like WS2812B's, only you can do dim colors better (0/255 to 1/255 to 2/255 is a very visible change in brightness - you need more than 255 steps to smoothly dim all the way down to nothing), or you can use them in fast-moving things without getting the dashed-tracers that you get from WS2812B's or things PWMed at low frequencies.

    I've got these working on the bench (ie, controlling them by hand) and will be writing usable code to control them, but I wanted to check that I wasn't going to reinvent the wheel.

    If nobody else has, I'll write up what I did to control them and submit to the docs.

  • Since they are clocked and not timed, higher switch frequencies can be applied... adafruit sells them under the term DotStar LEDs https://www.adafruit.com/products/2241.

  • I got a whole page written up from them ready to submit to the docs.

    Click "Commit"
    "Internet disconnected" (no, no it's not, it's working fine).
    Page reloads.
    All work gone.

    Anyway - the pingpong ball light project lives:

  • Maybe you want to check this video as well APA102 RGB Pixel Addressable LED

    , Julian Ilett is doing great stuff !

    Or check this site digging-into-the-apa102-serial-led-proto­col on HACKADAY

  • 
    pinMode(A7,"af_opendrain");
    pinMode(A5,"af_opendrain");
    SPI1.setup({sck:A5,mosi:A7,mode:1,order:­"msb"});
    
    anibuff=[];
    anibuff[0]=new Uint8Array([0,0,0,0,0xff,0x00,0x5c,0xb9,­0xff,0x00,0x80,0xff,0xff,0x52,0x5c,0x0a,­0xff,0x51,0x00,0x00,0xff,0x00,0x40,0x80,­0xff,0x01,0x4b,0xa7,0xff,0xff,0xff,0x00]­);
    anibuff[1]=new Uint8Array([0,0,0,0,0xff,0xff,0xff,0x00,­0xff,0x00,0x80,0xff,0xff,0x52,0x5c,0x0a,­0xff,0x51,0x00,0x00,0xff,0x00,0x40,0x80,­0xff,0x01,0x4b,0xa7,0xff,0x00,0x4f,0x4f]­);
    anibuff[2]=new Uint8Array([0,0,0,0,0xff,0x00,0x5c,0xb9,­0xff,0xff,0xff,0x00,0xff,0x52,0x5c,0x0a,­0xff,0x51,0x00,0x00,0xff,0x00,0x40,0x80,­0xff,0x01,0x4b,0xa7,0xff,0x00,0x4f,0x4f]­);
    anibuff[3]=new Uint8Array([0,0,0,0,0xff,0x00,0x5c,0xb9,­0xff,0x00,0x80,0xff,0xff,0xd0,0xd0,0x00,­0xff,0x51,0x00,0x00,0xff,0x00,0x40,0x80,­0xff,0x01,0x4b,0xa7,0xff,0x00,0x4f,0x4f]­);
    anibuff[4]=new Uint8Array([0,0,0,0,0xff,0x00,0x5c,0xb9,­0xff,0x00,0x80,0xff,0xff,0x52,0x5c,0x0a,­0xff,0xff,0xff,0x00,0xff,0x00,0x40,0x80,­0xff,0x01,0x4b,0xa7,0xff,0x00,0x4f,0x4f]­);
    anibuff[5]=new Uint8Array([0,0,0,0,0xff,0x00,0x5c,0xb9,­0xff,0x00,0x80,0xff,0xff,0x52,0x5c,0x0a,­0xff,0x51,0x00,0x00,0xff,0xff,0xff,0x00,­0xff,0x01,0x4b,0xa7,0xff,0x00,0x4f,0x4f]­);
    anibuff[6]=new Uint8Array([0,0,0,0,0xff,0x00,0x5c,0xb9,­0xff,0x00,0x80,0xff,0xff,0x52,0x5c,0x0a,­0xff,0x51,0x00,0x00,0xff,0x00,0x40,0x80,­0xff,0xff,0xff,0x00,0xff,0x00,0x4f,0x4f]­);
    currentframe = 0;
    framecount=7;
    
    function APA102Lite(data){
      SPI1.send(data);
      SPI1.send([0xFF,0xFF,0xFF,0xFF]);
    }
    
    function animate() {
      currentframe++;
      if (currentframe >= framecount) {
        currentframe=0;
      }
      APA102Lite(anibuff[currentframe]);
      timer=setTimeout("animate()",100);
    }
    
    
    
  • NICE !

    Question: shouldn't setTimeout("animate()",100); be outside of function animate() {} ?

  • animate() sets the timer to call itself again for the next frame ;-)

  • On an ESP8266:

    
    SPI1.setup({sck:14,mosi:13,mode:1,order:­"msb",baud:1000000});
    
    
    numleds=5;
    
    var leds = {};
    leds.spi=SPI1;
    leds.num=numleds;
    leds.fbuf=new Uint8Array(numleds*4);
    leds.buff=new Uint8Array(numleds*3);
    leds.tbuf=new Uint8Array(numleds*3);
    leds.twinkle=new Int8Array(numleds*3);
    leds.twinklemin=new Int8Array(numleds*3);
    leds.twinklemax=new Int8Array(numleds*3);
    leds.overlay=new Uint8Array(numleds*3);
    leds.gdim=new Uint8Array(numleds);
    for (var tem=0;tem<numleds;tem++){
    	leds.gdim[tem]=31; 
    	for (var j=0;j<3;j++){
    		leds.twinklemin[tem*3+j]=-10;
    		leds.twinklemax[tem*3+j]=10;
    	}
    }
    
    
    leds.dotwinkle = function () {
    	for (var i=0;i<numleds*3;i++){
    		var n=Math.random();
          this.twinkle[i]=E.clip(this.twinkle[i]+(­n<0.75?(n>0.25?0:-1):1),this.twinklemin[­i],this.twinklemax[i]);
    		if (this.buff[i] != this.tbuf[i]){ //fade
              this.buff[i]=this.buff[i]+(this.tbuf[i]>­this.buff[i]?1:-1);
    		}
    
    	}
    };
    
    leds.setPixel = function (x, y, color) {
    	this.tbuf[x*3]=color[0];
    	this.tbuf[x*3+1]=color[1];
    	this.tbuf[x*3+2]=color[2];
    };
    
    leds.setPixel2 = function (x, y, color,mintwi,maxtwi) {
    	this.tbuf[x*3]=color[0];
    	this.tbuf[x*3+1]=color[1];
    	this.tbuf[x*3+2]=color[2];
    	this.twinklemax[x*3]=maxtwi[0];
    	this.twinklemax[x*3+1]=maxtwi[1];
    	this.twinklemax[x*3+2]=maxtwi[2];
    	this.twinklemin[x*3]=mintwi[0];
    	this.twinklemin[x*3+1]=mintwi[1];
    	this.twinklemin[x*3+2]=mintwi[2];
    };
    
    leds.flip = function () {
    	var tclb=new Uint8ClampedArray(this.num*3);
    	for (var i=0;i<(this.num*3);i++) {
          tclb[i]=leds.buff[i]+(leds.buff[i]?leds.­twinkle[i]:0)+leds.overlay[i];
    	}
      //console.log(tclb);
    	for (var i=0;i<numleds;i++) {
    		//var x = new Uint16Array([tclb[i*3]<<5,tclb[(i*3)+1]<­<5,tclb[(i*3)+2]<<5]);
    		var x = new Uint16Array([gtab[tclb[i*3]],gtab[tclb[(­i*3)+1]],gtab[tclb[(i*3)+2]]]);
    		
    		var ma = Math.max(x[0],x[1],x[2]);
    		var mi = Math.min(x[0],x[1],x[2]);
    		var mult=1;
    		if (this.gdim[i] == 31) {
    			if (ma <700 && mi < 200) {
    				this.gdim[i]=7;
    				mult=4.4;
    
    			} else if (ma <1700 && mi < 500) {
    				this.gdim[i]=15;
    				mult=2.06;
    			} 
    		} else if (this.gdim[i] == 15) {
    			if (ma <700 && mi < 200) {
    				this.gdim[i]=7;
    				mult=4.4;
    			} else if (ma >1980) {
    				this.gdim[i]=31;
    			} else {
    				mult=2.06;
    			} 
    		} else if (this.gdim[i] == 7) {
    			if (ma > 1980 ) {
    				this.gdim[i]=31;
    			} else if (ma >924) {
    				this.gdim[i]=15;
    				mult=2.06;
    			} else {
    				mult=4.4;
    			}
    		} 
    		x[0]=x[0]*mult;
    		x[1]=x[1]*mult;
    		x[2]=x[2]*mult;
    
    		this.fbuf[i*4]=(this.on?(this.gdim[i]|22­4):224);
    		this.fbuf[1+i*4]=(x[0]==0?0:Math.max(x[0­]>>4,1));
    		this.fbuf[2+i*4]=(x[1]==0?0:Math.max(x[1­]>>4,1));
    		this.fbuf[3+i*4]=(x[2]==0?0:Math.max(x[2­]>>4,1));
    
    		if (i==-1) {
    
          console.log(x);
          console.log(mult);
          console.log(this.fbuf[i*4]+" "+this.fbuf[1+i*4]+" "+this.fbuf[2+i*4]+" "+this.fbuf[3+i*4]);
    		}
    	}
    
    	this.spi.send([0,0,0,0]);
    	this.spi.send(this.fbuf);
        this.spi.send([0xFF,0xFF,0xFF,0xFF]);
    };
    
    ![](http://drazzy.com/e/espruino/etc/IMG­_20160125_0128587.jpg)
    
    
    
    function animate() {
      leds.flip();
      leds.dotwinkle();
      setTimeout("animate()",20);
    }
    //setBusyIndicator(LED1);
    leds.setPixel(0,0,[255,255,0]);
    
    gtab=new Uint16Array(256);
    gtab=new Uint16Array([0,1,3,5,8,12,15,19,23,28,33­,37,43,48,54,59,65,72,78,85,91,98,105,11­2,120,127,135,143,151,159,167,175,184,19­3,201,210,219,228,238,247,257,266,276,28­6,296,306,316,326,337,347,358,369,380,39­1,402,413,424,435,447,459,470,482,494,50­6,518,530,542,554,567,579,592,605,617,63­0,643,656,669,683,696,709,723,736,750,76­4,777,791,805,819,833,848,862,876,891,90­5,920,935,949,964,979,994,1009,1024,1039­,1055,1070,1085,1101,1116,1132,1148,1164­,1179,1195,1211,1227,1244,1260,1276,1292­,1309,1325,1342,1359,1375,1392,1409,1426­,1443,1460,1477,1494,1511,1528,1546,1563­,1581,1598,1616,1634,1651,1669,1687,1705­,1723,1741,1759,1777,1796,1814,1832,1851­,1869,1888,1906,1925,1944,1963,1981,2000­,2019,2038,2057,2077,2096,2115,2134,2154­,2173,2193,2212,2232,2252,2271,2291,2311­,2331,2351,2371,2391,2411,2431,2451,2472­,2492,2512,2533,2553,2574,2595,2615,2636­,2657,2678,2699,2720,2741,2762,2783,2804­,2825,2846,2868,2889,2911,2932,2954,2975­,2997,3019,3040,3062,3084,3106,3128,3150­,3172,3194,3216,3238,3261,3283,3305,3328­,3350,3373,3395,3418,3441,3463,3486,3509­,3532,3555,3578,3601,3624,3647,3670,3693­,3716,3740,3763,3786,3810,3833,3857,3881­,3904,3928,3952,3975,3999,4023,4047,4071­,4095]);
    
    
    

    The table needs to be adjusted a bit more still, I think.

    The "twinkle" effect is a slow drift of the LED color around the set point - in this way, a bunch of LEDs that are set to the "same" color won't all look identical, and their colors relative to eachother will subtly shift over time, giving a more organic effect.

    The diffusers look great now - I'm using a single halved pingpong ball for each, with the halves inside eachother (with the help of a bit of glue).

  • Looks nice! I guess it doesn't actually add much to the cost vs. a WS2812B if you're using LEDs on a PCB. I guess the LED strings with wires will still have a slight premium though.

    What's the power-on state? I know I bought a bunch of APA104s (WS2812B clones) and the power on state was full blue, which really sucked for pretty much everything - the under counter lights in my kitchen have this annoying 100ms blue flash when turned on now!

    By the way, you can change:

    this.spi.send([0,0,0,0]);
        this.spi.send(this.fbuf);
        this.spi.send([0xFF,0xFF,0xFF,0xFF]);
    

    to:

    this.spi.write(0,0,0,0,this.fbuf,0xFF,0x­FF,0xFF,0xFF);
    

    SPI.write doesn't try and receive any data, so it doesn't allocate an array for the data it gets back - it should also be a bit faster.

  • 
    SPI1.setup({sck:14,mosi:13,mode:1,order:­"msb",baud:4000000});
    I2C1.setup({scl:5,sda:4});
    
    var http = require("http");
    var eeprom=require("AT24").connect(I2C1, 32, 32);
    
    setBusyIndicator(2);
    require("ESP8266").logDebug(0);
    require("ESP8266").setLog(0);
    
    
    // Parameters:
    numleds=5;
    
    //global functions
    function animate() {
      setTimeout("animate()",20);
      leds.flip();
      leds.dotwinkle();
    }
    
    
    // Network
    
    function onPageRequest(req, res) {
      var a = url.parse(req.url, true);
      if (a.pathname=="/setAll.cmd") {
        //lreq=a.query;
        leds.setAll(eval(a.query.color),eval(a.q­uery.mode),eval(a.query.max),eval(a.quer­y.min));
      } else if (a.pathname=="/setPixel.cmd") {
        //lreq=a.query;
        leds.setPixel2(a.query.led,0,eval(a.quer­y.color),eval(a.query.mode),eval(a.query­.max),eval(a.query.min));
      }
      res.writeHead(200);
      res.end("Hello World"); 
    }
    require("http").createServer(onPageReque­st).listen(80);
    
    // LEDS
    var leds = {};
    leds.spi=SPI1;
    leds.num=numleds;
    leds.afr=0;
    leds.fbuf=new Uint8Array(numleds*4);
    leds.buff=new Uint8Array(numleds*3);
    leds.tbuf=new Uint8Array(numleds*3);
    leds.twinkle=new Int8Array(numleds*3);
    leds.twimode=new Uint8Array(numleds*3);
    leds.twinklemin=new Int8Array(numleds*3);
    leds.twinklemax=new Int8Array(numleds*3);
    leds.overlay=new Uint8Array(numleds*3);
    leds.gdim=new Uint8Array(numleds);
    for (var tem=0;tem<numleds;tem++){
    	leds.gdim[tem]=31; 
    	for (var j=0;j<3;j++){
    		leds.twinklemin[tem*3+j]=-10;
    		leds.twinklemax[tem*3+j]=10;
    	}
    }
    leds.ison=1;
    leds.dotwinkle = function () {
    	for (var i=0;i<numleds*3;i++){
    		if (this.buff[i] != this.tbuf[i]){ //fade
              		this.buff[i]=this.buff[i]+(this.tbuf[i]>­this.buff[i]?1:-1);
    		}
    		var mode=this.twimode[i];
    		var mo=mode&0x0F;
    		var pr=mode>>4;
    		if (mo==1) { //0x01 - high nybble is chance to change, from 0 (1/16) to 15 (16/16 chance to change)
    			var n=Math.random();
    			var th=(pr+1)/32;
          			this.twinkle[i]=E.clip(this.twinkle[i]+(­n<(0.5+th)?(n>(0.5-th)?0:-1):1),this.twi­nklemin[i],this.twinklemax[i]);
    		} else if (mo==2) { //fade/pulse. 
              if (this.afr%((1+pr)&7)==0){
                this.twinkle[i]=E.clip(this.twinkle[i]+(­pr&8?1:-1),this.twinklemin[i],this.twink­lemax[i]);
    			if (this.twinkle[i] == this.twinklemin[i] || this.twinkle[i] == this.twinklemax[i]) {
    				this.twimode[i]=mode^128;
    			}
              }
    		}
    
    	}
      this.afr=this.afr==255?0:this.afr+1;
    };
    
    leds.pr = function (p,n) {
    	
    };
    leds.setAll= function (color,tmode,tmax,tmin) {
    	for (var i=0;i<this.num;i++) {
    		for (j=0;j<3;j++){
    			this.twinkle[3*i+j]=0;
    			this.tbuf[3*i+j]=color[j];
    			if (tmode) {
    				this.twimode[3*i+j]=tmode[j];
    				this.twinklemin[3*i+j]=tmin[j];
    				this.twinklemax[3*i+j]=tmax[j];
    			}
    		}
    	}
    };
    leds.loadBase = function (eep,addr,len) {
      len=len?len:this.num; 
    	this.tbuf=eep.read(addr,this.num*3);
    	this.twinkle=new Int8Array(this.num*3);
    	this.twimode=new Uint8Array(eep.read((addr+len*3),this.nu­m*3));
    	this.twinklemin=new Int8Array(eep.read((addr+len*6),this.num­*3));
    	this.twinklemax=new Int8Array(eep.read((addr+len*9),this.num­*3));
    };
    
    leds.saveBase = function (eep,addr,base,twim,mint,maxt) {
    	var n=base.length;
    	eep.write(addr,base);
    	eep.write(addr+n*3,twim);
    	eep.write(addr+n*6,mint);
    	eep.write(addr+n*9,maxt);
    };
    
    
    leds.setPixel = function (x, y, color) {
    	this.tbuf[x*3]=color[0];
    	this.tbuf[x*3+1]=color[1];
    	this.tbuf[x*3+2]=color[2];
    };
    
    leds.setPixel2 = function (x, y, color,mode,mintwi,maxtwi) {
    	this.tbuf[x*3]=color[0];
    	this.tbuf[x*3+1]=color[1];
    	this.tbuf[x*3+2]=color[2];
    	this.twimode[x*3]=mode[0];
    	this.twimode[x*3+1]=mode[1];
    	this.twimode[x*3+2]=mode[2];
    	this.twinklemax[x*3]=maxtwi[0];
    	this.twinklemax[x*3+1]=maxtwi[1];
    	this.twinklemax[x*3+2]=maxtwi[2];
    	this.twinklemin[x*3]=mintwi[0];
    	this.twinklemin[x*3+1]=mintwi[1];
    	this.twinklemin[x*3+2]=mintwi[2];
    };
    
    leds.flip = function () {
    	var tclb=new Uint8ClampedArray(this.num*3);
    	for (var i=0;i<(this.num*3);i++) {
          tclb[i]=leds.buff[i]+(leds.buff[i]?leds.­twinkle[i]:0)+leds.overlay[i];
    	}
      //console.log(tclb);
    	for (var i=0;i<numleds;i++) {
    		//var x = new Uint16Array([tclb[i*3]<<5,tclb[(i*3)+1]<­<5,tclb[(i*3)+2]<<5]);
    		var x = new Uint16Array([gtab[tclb[i*3]],gtab[tclb[(­i*3)+1]],gtab[tclb[(i*3)+2]]]);
    		
    		var ma = Math.max(x[0],x[1],x[2]);
    		var mi = Math.min(x[0],x[1],x[2]);
    		var mult=1;
    		if (this.gdim[i] == 31) {
    			if (ma <390 && mi < 300) {
    				this.gdim[i]=3;
    				mult=10.33;
    
    			} else if (ma <700 && mi < 500) {
    				this.gdim[i]=7;
    				mult=4.4;
    
    			} else if (ma <1700 && mi < 1000) {
    				this.gdim[i]=15;
    				mult=2.06;
    			} 
    		} else if (this.gdim[i] == 15) {
    			if (ma <390 && mi < 300) {
    				this.gdim[i]=3;
    				mult=10.33;
    			} else if (ma <700 && mi < 500) {
    				this.gdim[i]=7;
    				mult=4.4;
    			} else if (ma >1980) {
    				this.gdim[i]=31;
    			} else {
    				mult=2.06;
    			} 
    		} else if (this.gdim[i] == 7) {
    			if (ma <390 && mi < 300) {
    				this.gdim[i]=3;
    				mult=10.33;
    			} else if (ma > 1980 ) {
    				this.gdim[i]=31;
    			} else if (ma >924) {
    				this.gdim[i]=15;
    				mult=2.06;
    			} else {
    				mult=4.4;
    			}
    		} else if (this.gdim[i] == 3) {
    			if (ma > 1980 ) {
    				this.gdim[i]=31;
    			} else if (ma >924) {
    				this.gdim[i]=15;
    				mult=2.06;
    			} else if (ma>390) {
    				this.gdim[i]=7;
    				mult=4.4;
    			} else {
    				mult=10.33;
    			}
    		} 
    		x[0]=x[0]*mult;
    		x[1]=x[1]*mult;
    		x[2]=x[2]*mult;
    
    		this.fbuf[i*4]=(this.ison?(this.gdim[i]|­224):224);
    		this.fbuf[1+i*4]=(x[2]==0?0:Math.max(x[2­]>>4,1));
    		this.fbuf[2+i*4]=(x[1]==0?0:Math.max(x[1­]>>4,1));
    		this.fbuf[3+i*4]=(x[0]==0?0:Math.max(x[0­]>>4,1));
    
    	}
    
    	this.spi.write(0,0,0,0,this.fbuf,0xFF,0x­FF,0xFF,0xFF);
    };
    
    gtab=new Uint16Array(256);
    gtab=new Uint16Array([0,1,2,3,4,5,6,7,8,9,11,13,1­5,17,19,21,23,25,27,30,33,36,39,42,45,48­,51,54,58,62,66,70,74,78,82,86,91,96,101­,106,111,116,121,126,132,138,144,150,156­,162,168,174,181,188,195,202,209,216,223­,230,238,246,254,262,270,278,286,294,303­,312,321,330,339,348,357,366,376,386,396­,406,416,426,436,446,457,468,479,490,501­,512,523,534,546,558,570,582,594,606,618­,630,643,656,669,682,695,708,721,734,748­,762,776,790,804,818,832,846,861,876,891­,906,921,936,951,966,982,998,1014,1030,1­046,1062,1078,1094,1111,1128,1145,1162,1­179,1196,1213,1230,1248,1266,1284,1302,1­320,1338,1356,1374,1393,1412,1431,1450,1­469,1488,1507,1526,1546,1566,1586,1606,1­626,1646,1666,1686,1707,1728,1749,1770,1­791,1812,1833,1854,1876,1898,1920,1942,1­964,1986,2008,2030,2053,2076,2099,2122,2­145,2168,2191,2214,2238,2262,2286,2310,2­334,2358,2382,2406,2431,2456,2481,2506,2­531,2556,2581,2606,2631,2657,2683,2709,2­735,2761,2787,2813,2839,2866,2893,2920,2­947,2974,3001,3028,3055,3083,3111,3139,3­167,3195,3223,3251,3279,3308,3337,3366,3­395,3424,3453,3482,3511,3541,3571,3601,3­631,3661,3691,3721,3751,3782,3813,3844,3­875,3906,3937,3968,3999,4031,4063,4095])­;
    
    
    
    setBusyIndicator(2);
    leds.setPixel(0,0,[0,255,255]);
    
    

    Added web control, selectable twinkle modes, and pulse/fade.

    Takes URLs like this:

    
    http://192.168.2.136/setAll.cmd?color=[1­28,128,128]&mode=[2,2,2]&min=[-128,-128,­-128]&max=[127,127,127]
    
    

    New gtab that seems to do a better job of linearizing the light output vs brightness and smooths dimming at low brightness, and another level of global dimming use to take advantage of this.

  • Anyone have any thoughts on improving performance of flip() and dotwinkle()? I realize they're inefficient, but I'm not sure I know what the right approach is towards making them more efficient.

    I'm discovering that if I want even 10 LEDs in the string, the frame rate is going to be terrible.... it's about 70ms per flip() and 50ms per dotwinkle()

  • I'm not 100% sure what it's doing, but I'd say lookup tables are the way to go. Also allocating a new Uint16Array each time around the loop isn't great - it'd be faster as a normal array.

    Even something like:

    var gdimlookup = {
      15: function(i) {
                if (ma <390 && mi < 300) {
                    this.gdim[i]=3;
                    return 10.33;
                } else if (ma <700 && mi < 500) {
                    this.gdim[i]=7;
                    return 4.4;
                } else if (ma <1700 && mi < 1000) {
                    this.gdim[i]=15;
                    return 2.06;
                } 
            },
      ... 
    };
    // ...
            
            var f = gdimlookup[this.gdim[i]];
            var mult = f?f(i):1;
    
    

    would help, as it's not having to parse a whole bunch of stuff each time around the loop.

  • I've reworked these.... but it's still slow.

    
    
    
    
    SPI1.setup({sck:14,mosi:13,mode:1,order:­"msb",baud:4000000});
    I2C1.setup({scl:5,sda:4});
    
    var http = require("http");
    var eeprom=require("AT24").connect(I2C1, 32, 32);
    
    setBusyIndicator(2);
    require("ESP8266").logDebug(0);
    require("ESP8266").setLog(0);
    
    
    // Parameters:
    numleds=5;
    
    //global functions
    function animate() {
      setTimeout("animate()",20);
      var x=getTime();
      leds.flip();
      leds.dotwinkle();
      console.log(getTime()-x);
    }
    
    
    // Network
    
    function onPageRequest(req, res) {
      var a = url.parse(req.url, true);
      if (a.pathname.split(".")[1]=="cmd"){
      	if (handleCmd(a.pathname,a.query)) {
      		res.writeHead(200,{'Access-Control-Allow­-Origin':'*'});
      		res.end("OK");
      	} else {
      		res.writeHead(400,{'Access-Control-Allow­-Origin':'*'});
      		res.end("ERROR");
      	}
      } else {
    	res.writeHead(404);
    	res.end("ERROR");	
      }
    }
    require("http").createServer(onPageReque­st).listen(80);
    
    function handleCmd(pn,q) {
      if (pn=="/setAll.cmd") {
        //lreq=a.query;
        leds.setAll(eval(q.color),eval(q.mode),e­val(q.max),eval(q.min));
        return 1;
      } else if (pn=="/setPixel.cmd") {
        leds.setPixel2(q.led,0,eval(q.color),eva­l(q.mode),eval(q.max),eval(q.min));
        return 1;
      }	
    }
    
    
    // LEDS
    gtab=new Uint16Array(256);
    gtab=new Uint16Array([0,1,2,3,4,5,6,7,8,9,11,13,1­5,17,19,21,23,25,27,30,33,36,39,42,45,48­,51,54,58,62,66,70,74,78,82,86,91,96,101­,106,111,116,121,126,132,138,144,150,156­,162,168,174,181,188,195,202,209,216,223­,230,238,246,254,262,270,278,286,294,303­,312,321,330,339,348,357,366,376,386,396­,406,416,426,436,446,457,468,479,490,501­,512,523,534,546,558,570,582,594,606,618­,630,643,656,669,682,695,708,721,734,748­,762,776,790,804,818,832,846,861,876,891­,906,921,936,951,966,982,998,1014,1030,1­046,1062,1078,1094,1111,1128,1145,1162,1­179,1196,1213,1230,1248,1266,1284,1302,1­320,1338,1356,1374,1393,1412,1431,1450,1­469,1488,1507,1526,1546,1566,1586,1606,1­626,1646,1666,1686,1707,1728,1749,1770,1­791,1812,1833,1854,1876,1898,1920,1942,1­964,1986,2008,2030,2053,2076,2099,2122,2­145,2168,2191,2214,2238,2262,2286,2310,2­334,2358,2382,2406,2431,2456,2481,2506,2­531,2556,2581,2606,2631,2657,2683,2709,2­735,2761,2787,2813,2839,2866,2893,2920,2­947,2974,3001,3028,3055,3083,3111,3139,3­167,3195,3223,3251,3279,3308,3337,3366,3­395,3424,3453,3482,3511,3541,3571,3601,3­631,3661,3691,3721,3751,3782,3813,3844,3­875,3906,3937,3968,3999,4031,4063,4095])­;
    var leds = {};
    leds.spi=SPI1;
    leds.num=numleds;
    leds.afr=0;
    leds.fbuf=new Uint8Array(numleds*4);
    leds.buff=new Uint8Array(numleds*3);
    leds.tbuf=new Uint8Array(numleds*3);
    leds.t=new Int8Array(numleds*3);
    leds.tm=new Uint8Array(numleds*3);
    leds.ti=new Int8Array(numleds*3);
    leds.ta=new Int8Array(numleds*3);
    leds.overlay=new Uint8Array(numleds*3);
    leds.tclb=new Uint8ClampedArray(numleds*3);
    for (var tem=0;tem<numleds;tem++){
    	//leds.gdim[tem]=31; 
    	for (var j=0;j<3;j++){
    		leds.ti[tem*3+j]=-10;
    		leds.ta[tem*3+j]=10;
    	}
    }
    leds.ison=1;
    leds.animode=0;
    leds.aniframe=0;
    leds.anilast=0;
    
    leds.dotwinkle = function () {
      var t=this.t;
      var tm= this.tm;
      var ta=this.ta;
      var ti=this.ti;
      var b=this.buff;
      var z=this.tbuf;
      var o=this.overlay;
    	if (this.animode) {
    		if (this.aniframe > this.anilast) {
    			this.animode=0;
    			this.anilast=0;
    			this.aniframe=0;
    			this.overlay=new Uint8Array(this.num*3);
    		} else {
    			this.overlay=this.animation[this.anifram­e++];
    		}
    	}
    	for (var i=0;i<this.num*3;i++){
    		if (b[i] != z[i]){ //fade
              		b[i]=b[i]+(z[i]>b[i]?1:-1);
    		}
    		var mode=tm[i];
    		var mo=mode&0x0F;
    		var pr=mode>>4;
    		if (mo==1) { //0x01 - high nybble is chance to change, from 0 (1/16) to 15 (16/16 chance to change)
    			var n=Math.random();
    			var th=(pr+1)/32;
          			t[i]=E.clip(t[i]+(n<(0.5+th)?(n>(0.5-th)­?0:-1):1),ti[i],ta[i]);
    		} else if (mo==2) { //fade/pulse. 
              if (this.afr%((1+pr)&7)==0){
                t[i]=t[i]+(pr&8?1:-1);
    			if (t[i] == ti[i] || t[i] == ta[i]) {
    				tm[i]=mode^128;
    			}
              }
    		}
    		leds.tclb[i]=b[i]+(b[i]?t[i]:0)+o[i];
    	}
      this.afr=this.afr==255?0:this.afr+1;
    };
    
    leds.setAll= function (color,tmode,tmax,tmin) {
    	for (var i=0;i<this.num;i++) {
    		for (j=0;j<3;j++){
    			this.t[3*i+j]=0;
    			this.tbuf[3*i+j]=color[j];
    			if (tmode) {
    				this.tm[3*i+j]=tmode[j];
    				this.ti[3*i+j]=tmin[j];
    				this.ta[3*i+j]=tmax[j];
    			}
    		}
    	}
    };
    leds.loadBase = function (eep,addr,len) {
      len=len?len:this.num; 
    	this.tbuf=eep.read(addr,this.num*3);
    	this.t=new Int8Array(this.num*3);
    	this.tm=new Uint8Array(eep.read((addr+len*3),this.nu­m*3));
    	this.ti=new Int8Array(eep.read((addr+len*6),this.num­*3));
    	this.ta=new Int8Array(eep.read((addr+len*9),this.num­*3));
    };
    
    leds.saveBase = function (eep,addr,len) {
    	
    	eep.write(addr,this.tbuf);
    	eep.write(addr+len*3,this.tm);
    	eep.write(addr+len*6,this.ti);
    	eep.write(addr+len*9,this.ta);
    };
    
    
    leds.setPixel = function (x, y, color) {
    	this.tbuf[x*3]=color[0];
    	this.tbuf[x*3+1]=color[1];
    	this.tbuf[x*3+2]=color[2];
    };
    
    leds.setPixel2 = function (x, y, color,mode,mintwi,maxtwi) {
    	this.tbuf[x*3]=color[0];
    	this.tbuf[x*3+1]=color[1];
    	this.tbuf[x*3+2]=color[2];
    	this.tm[x*3]=mode[0];
    	this.tm[x*3+1]=mode[1];
    	this.tm[x*3+2]=mode[2];
    	this.ta[x*3]=maxtwi[0];
    	this.ta[x*3+1]=maxtwi[1];
    	this.ta[x*3+2]=maxtwi[2];
    	this.ti[x*3]=mintwi[0];
    	this.ti[x*3+1]=mintwi[1];
    	this.ti[x*3+2]=mintwi[2];
    };
    
    
    leds.flip = function () {
    	for (var i=0;i<numleds;i++) {
    		//var x = new Uint16Array([tclb[i*3]<<5,tclb[(i*3)+1]<­<5,tclb[(i*3)+2]<<5]);
    		
            
          var rch=gtab[leds.tclb[i*3]];
    		var gch=gtab[leds.tclb[i*3+1]];
    		var bch=gtab[leds.tclb[i*3+2]];
    		//var x = new Uint16Array([gtab[tclb[i*3]],gtab[tclb[(­i*3)+1]],gtab[tclb[(i*3)+2]]]);
    		
    		var ma = Math.max(rch,gch,bch);
    		//var mi = Math.min(x[0],x[1],x[2]);
    		//var mi = Math.min(rch,gch,bch);
    		var mult=1;
            gdim=31;
    		//if (this.gdim[i] == 31) {
    			if (ma <390) {
    				gdim=3;
    				mult=10.33;
    			} else if (ma <700) {
    				gdim=7;
    				mult=4.4;
    			} else if (ma <1700) {
    				gdim=15;
    				mult=2.06;
    			} 
    		/*} else if (this.gdim[i] == 15) {
    			if (ma <390) {
    				this.gdim[i]=3;
    				mult=10.33;
    			} else if (ma <700) {
    				this.gdim[i]=7;
    				mult=4.4;
    			} else if (ma >1980) {
    				this.gdim[i]=31;
    			} else {
    				mult=2.06;
    			} 
    		} else if (this.gdim[i] == 7) {
    			if (ma <390) {
    				this.gdim[i]=3;
    				mult=10.33;
    			} else if (ma > 1980 ) {
    				this.gdim[i]=31;
    			} else if (ma >924) {
    				this.gdim[i]=15;
    				mult=2.06;
    			} else {
    				mult=4.4;
    			}
    		} else if (this.gdim[i] == 3) {
    			if (ma > 1980 ) {
    				this.gdim[i]=31;
    			} else if (ma >924) {
    				this.gdim[i]=15;
    				mult=2.06;
    			} else if (ma>390) {
    				this.gdim[i]=7;
    				mult=4.4;
    			} else {
    				mult=10.33;
    			}
    		} 
            */
    		rch*=mult;
    		gch*=mult;
    		bch*=mult;
    
    		this.fbuf[i*4]=(this.ison?(gdim|224):224­);
    		this.fbuf[1+i*4]=(bch?Math.max(bch>>4,1)­:0);
    		this.fbuf[2+i*4]=(gch?Math.max(gch>>4,1)­:0);
    		this.fbuf[3+i*4]=(rch?Math.max(rch>>4,1)­:0);
    
    	}
    
    	this.spi.write(0,0,0,0,this.fbuf,0xFF,0x­FF,0xFF,0xFF);
    };
    
    leds.dotwinkle = function () {
      var t=this.t;
      var tm= this.tm;
      var ta=this.ta;
      var ti=this.ti;
      var b=this.buff;
      var z=this.tbuf;
      var o=this.overlay;
    	if (this.animode) {
    		if (this.aniframe > this.anilast) {
    			this.animode=0;
    			this.anilast=0;
    			this.aniframe=0;
    			this.overlay=new Uint8Array(this.num*3);
    		} else {
    			this.overlay=this.animation[this.anifram­e++];
    		}
    	}
    	for (var i=0;i<this.num*3;i++){
    		if (b[i] != z[i]){ //fade
              		b[i]=b[i]+(z[i]>b[i]?1:-1);
    		}
    		var mode=tm[i];
    		var mo=mode&0x0F;
    		var pr=mode>>4;
    		if (mo==1) { //0x01 - high nybble is chance to change, from 0 (1/16) to 15 (16/16 chance to change)
    			var n=Math.random();
    			var th=(pr+1)/32;
          			t[i]=E.clip(t[i]+(n<(0.5+th)?(n>(0.5-th)­?0:-1):1),ti[i],ta[i]);
    		} else if (mo==2) { //fade/pulse. 
              if (this.afr%((1+pr)&7)==0){
                t[i]=t[i]+(pr&8?1:-1);
    			if (t[i] == ti[i] || t[i] == ta[i]) {
    				tm[i]=mode^128;
    			}
              }
    		}
    		leds.tclb[i]=b[i]+(b[i]?t[i]:0)+o[i];
    	}
      this.afr=this.afr==255?0:this.afr+1;
    };
    
    
    setBusyIndicator(2);
    leds.setPixel(0,0,[0,255,255]);
    
    

    flip() is taking about 16ms. doTwinkle() (with animode=0) is taking 30-45ms (depending on animation modes - mo=1 is the worst, mo=2 is only a little better) with 5 LEDs.

    That means 3ms + 6~9ms per pixel, which is too slow for a string of 10-12 pixels...

    Changing from this.blah (with the longer variable names) only saved a few milliseconds per run.

    I'm sort of wondering what it is that I'm doing that's taking the time. Would reworking it to use map() in dotwinkle() be faster? Er, can I even do that? Maybe I can't...

  • Well, at the moment you have a bunch of commented out code in the FOR loop. Just removing that would help a lot (or you could turn on minification?).

    Also, What happens with (bch?Math.max(bch>>4,1):0);? Would Math.max(bch>>4,0) work?

    If so, make fbuf a Uint8ClampedArray and you can remove the max altogether... Maybe:

            rch*=mult;
            gch*=mult;
            bch*=mult;
    
            this.fbuf[i*4]=(this.ison?(gdim|224):224­);
            this.fbuf[1+i*4]=(bch?Math.max(bch>>4,1)­:0);
            this.fbuf[2+i*4]=(gch?Math.max(gch>>4,1)­:0);
            this.fbuf[3+i*4]=(rch?Math.max(rch>>4,1)­:0);
    

    to:

    var fi = 0;
    
            this.fbuf[fi++]=this.ison?(gdim|224):224­;
            this.fbuf[fi++]=(mult*bch)>>4;
            this.fbuf[fi++]=(mult*gch)>>4;
            this.fbuf[fi++]=(mult*rch)>>4;
    
  • I'm already minifying.

    The point of the Math.max() is to ensure that if bch > 0, the result is >0 - basically, so that if a channel is supposed to be on, but very dim, and the bitshift would make it 0 (off), we instead set it to minimum brightness (1).

    changing to incrementing a variable only shaved off about 1ms. I was playing around with commenting out lines though.

    flip() is now down to 18ms or so, but with mode=1 (twinkle), dotwinkle() takes like 45ms.... There are three lines that will save 11ms if I comment them out (ofc, they're critical lines!). These times are with just 5 LEDs, too...

    
    leds.flip = function () {
    	var j=0;
    	var i=0;
    	var z=leds.num*3;
    	while (i<z) {
          var rch=gtab[leds.tclb[i++]];
    		var gch=gtab[leds.tclb[i++]];
    		var bch=gtab[leds.tclb[i++]];
    		
    		var ma = Math.max(rch,gch,bch);
    		var mult=1;
            var gdim=31;
    		
    			if (ma <390) {
    				gdim=3;
    				mult=10.33;
    			} else if (ma <700) {
    				gdim=7;
    				mult=4.4;
    			} else if (ma <1700) {
    				gdim=15;
    				mult=2.06;
    			} 
    		
    		this.fbuf[j++]=(this.ison?(gdim|224):224­);
    		this.fbuf[j++]=(bch?Math.max((bch*mult)>­>4,1):0);
    		this.fbuf[j++]=(gch?Math.max((gch*mult)>­>4,1):0);
    		this.fbuf[j++]=(rch?Math.max((rch*mult)>­>4,1):0);
    
    	}
    
    	this.spi.write(0,0,0,0,this.fbuf,0xFF,0x­FF,0xFF,0xFF);
    };
    
    
    leds.dotwinkle = function () {
    	var t=this.t;
    	var tm= this.tm;
    	var ta=this.ta;
    	var ti=this.ti;
    	var b=this.buff;
    	var z=this.tbuf;
    	var o=this.overlay;
    	if (this.animode) {
    		if (this.aniframe > this.anilast) {
    			this.animode=0;
    			this.anilast=0;
    			this.aniframe=0;
    			this.overlay=new Uint8Array(this.num*3);
    		} else {
    			this.overlay=this.animation[this.anifram­e++];
    		}
    	}
    	for (var i=0;i<this.num*3;i++){
    		if (b[i] != z[i]){ //fade
              		b[i]=b[i]+(z[i]>b[i]?1:-1); //11ms
    		}
    		var mode=tm[i];
    		var mo=mode&0x0F;
    		var pr=mode>>4;
    		if (mo==1) { //0x01 - high nybble is chance to change, from 0 (1/16) to 15 (16/16 chance to change)
    			var n=Math.random(); //3ms
              //var n=0.8;
    			var th=(pr+1)/32;
          			t[i]=E.clip(t[i]+(n<(0.5+th)?(n>(0.5-th)­?0:-1):1),ti[i],ta[i]); //11ms
    		} else if (mo==2) { //fade/pulse. 
              		if (this.afr%((1+pr)&7)==0){
                			t[i]=t[i]+(pr&8?1:-1);
    				if (t[i] == ti[i] || t[i] == ta[i]) {
    					tm[i]=mode^128;
    				}
            		}
    		}
    		leds.tclb[i]=b[i]+(b[i]?t[i]:0)+o[i]; //11ms
    	}
    	this.afr=this.afr==255?0:this.afr+1;
    };
    
    

    On the Pico, times are 16ms and 38ms - so the pico is only a little faster than the ESP8266.

    It looks like the costly operation may be accessing array members?

  • Are all those arrays typed arrays? Accessing those should be quite quick, but accessing large, normal arrays is slow. Mind you, your arrays really aren't that large.

    It also helps if all the variables you're accessing are defined in the function itself, but it looks like you're doing that.

    Actually even finding a variable based on name is a bit slow, and for an array access you're finding at least 2 variables.

    So something like b[i]=b[i]+(z[i]>b[i]?1:-1); would be faster as b[i]+=z[i]>b[i]?1:-1;

    Compiled code is also potentially an option?

  • Well, compiled code I think is a no-go on the ESP8266 right? ;-) (and that's what I'm hoping to use in my project) I'll try compiled on the pico and see what impact that has on performance.

    Do those speeds for typed array access sound reasonable to you though? 11ms (like 9 on Pico I think? haven't done line-by-line on Pico) or so for 60-75 typed array accesses and some simple math?

  • Yes, compiled code is out on that :)

    I just did:

    function test() {
      var a = new Uint8Array(10000);
      var t = getTime();
      for (var i=0;i<10000;i++)a[i]=i;
      print(getTime()-t);t = getTime();
      for (var i=0;i<10000;i++){a[i]=i;a[i]=i;}
      print(getTime()-t);t = getTime();
      for (var i=0;i<10000;i++){a[i]=i;a[i]=i;a[i]=i;}
      print(getTime()-t);t = getTime();
    }
    

    And it came out at:

    1.89215660095
    2.79538631439
    3.63432979583
    

    So it looks like for a[i]=i it's 0.08ms, so with 75 accesses it's looking like 9ms is in the right kind of ballpark. It's just not that quick I'm afraid.

    It feels like array accesses should be faster - but in fact a lot of that is looking up the variables - for example even doing a;i;i; takes 0.063ms.

    It's something I hope I can make a bit faster soon - maybe by adding some kind of cache... but to be honest Espruino really is never going to be good at flat-out speed.

  • I think a variable lookup cache would be a good thing if it could be pulled off without wasting memory. Maybe even decree that single-letter local variable lookups would be cached or something (that is what the minifier turns variables into, and it's what someone hand optimizing would use too)...?

  • What about...

    For execution, the code is loaded into the RAM - and therefore it is modifiable - is it?. With some JIT compiler mind, the interpreter could after first lookup of a (long enough) variable (or function) referencing name override that name with a prefixed pointer. The prefix is some reserved byte(s) value which is telling the interpreter on subsequent interpretation that the lookup already happened. (May be there is no such special byte(s) left to make the interpreter recognize). Caveat: the delete of a variable becomes a bit tricky... and would thus not be allowed anymore.

  • One issue is that there isn't any kind of JIT in Espruino - it just runs the source code directly. I had wondered about trying to tokenise it and pull out the variable names, but that is quite a big undertaking.

    My worry is that if you have some kind of cache, you have to make sure it's in the right state. I'm struggling to think, but I could imagine that halfway through a function you might be able to call another function which changed which variable something like foo actually pointed to.

    Perhaps I'm wrong about that though - if so then life could be a lot easier.

    I like your variable name suggestion @DrAzzy - that could be very easy to add a special-case for, and it would be very easy for users to take advantage of.

  • Yeah - there's sort of this knee-jerk reaction in my brain "oh that's evil, special cases for certain variable names", but the more I think about it, the more sense I think it makes here. The fact that the minifier is already transmuting local variables into lowercase single letter variables with letters near the start of the alphabet means that people will take advantage of this without even knowing it, assuming they send the code from the right side of the IDE so it gets minified. And we already have to minify the code if we care about it running fast. But if you're hand optimizing code, it's the same thing - single-letter variable names are what we should be using anyway.
    You could probably get a significant performance enhancement with just say, the first 10-16 letters of the alphabet...

  • I did not study the variable handling close enough, but I expect a memory area where actually the metadata about the variable is kept - last but not least for garbage collection and the like - and that part stays put (has fixed address) - I expect or hope... I know: in science and technology, only fools hope :). But anyway...

    The pointer overwriting the variable name in the source I was thinking of would point to the location of the meta data and not the location where the actual value is. Since I do not know how the space of the variable metadata is organized (where var creates the metadata), it may add some challenges. For example, if it is a linked list - just as so many things seem to be in Espruino (for better or worse) - it is still a look up... so no gain, just pain.

    The linked-list concept makes me just now think that the meta data may be a 'memory address in-line' header of the actual variable data, and all is a linked list with explicit (pointers) or with implicit (length) linking for the lookup. Separation of the variable meta data from the actual value data make a partial JIT possible... (That's what I did in my OO VMs in order to have resolved references... that are fast, random - array like - accessible).

    There are usually not infinite places for a 'contiguous, growing/shrinking' memory space... and the separation asks just for one more... In addition, one more pointer has to be hosted per variable which points from the meta data to the value data. Furthermore, more discrete items in memory waste more bytes since they do not all align with the memory block boundaries... which confirms the truth that speed costs space. For some defined data types - such as int, boolean, float (and natively implemented objects of known fixed lengths), the meta data can also include the values and there is no need for pointing into the 'wild' (dynamically) managed memory space.

    While writing this post, I conclude that Espruino does not separate variables' meta-data form variable data, and going for a separation is a major concept change: a new Theory of Operation.

    @DrAzzy's idea is NOT THAT knee-jerk: remember Rockwell's 6502... a processor breath life into Commodore's PET and Apple I and on is still very a-live, and what did this thing do - more precisely - what where the architects of it thinking? Let's treat the first 256 memory locations / (single bit addresses) be handled differently: let's treat them as general registers! A brilliant hybrid choice for a time when (memory and processing) resources weren't commodities yet... and about the same I feel with Espruino. And I used the Wang Computers which used a similar approach: Variable names would always start with an uppercase and be followed by a digit 0 thru 9. Ending with a $ denoted one of the two only available data types: Strings. The other data type was a number (of course - last but not least it was 'a computer' - Wang 2200 T, starting out from a Wang 370 desk calculator). Wang 2200 T was a TTL implementation with 18 bit word format and 7483 based ALU and memory was blankets of blankets of RAM chip 'cards'.

    Usually, i like pure, consistent implementations, but with what is at stake and can be achieved 'here' with very limited resources asks for a bit more flexibility, creativity,... (going for that single letter variable thing, may mess a bit with the minifiers..., but after all it could be quite a boost. It could though start a 'war of claims': who owns which variable... because they are global and exist only once!

  • I conclude that Espruino does not separate variables' meta-data form variable data

    It'd be worth looking at http://www.espruino.com/Performance and http://www.espruino.com/Internals... Espruino has the concept of a 'name' and an actual value.

    So something like hello = 5 gets packed into 2 variable blocks - hello and 5. It's only when you get to some special cases (4 or less chars, plus a <16 bit integer) that it packs the two together. Either way, you do have some unique area of memory you can point to that corresponds to a specific variable.

    I think we might be getting sidetracked from APA102s though. The single-char cache might work, but I'm wary that there may be some nasty edge cases - it'd be a case of trying it when I have some time, and seeing if it broke any of the language tests.

  • I think we might be getting sidetracked from APA102s though.

    Was thinking that too... but did not want to start separate thread (yet).

    Espruino has the concept of a 'name' and an actual value.

    Yes, but they are 'stuck' with one another in 'block contiguous' memory. As soon as they can be / are separated, a JIT as sketched in previous posts can be achieved.

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

APA102 individually addressable LEDs

Posted by Avatar for DrAzzy @DrAzzy

Actions