logo_elektromys.eu

/ WS2812 |

"Inteligentní RGB LEDky" WS2812 a jejich různé ekvivalenty a varianty asi není třeba představovat. Jsou velmi rozšířené a běžně jsou k dostání moduly různých tvarů složené z těchto programovatelných LED. V principu jednoduchý komunikační protokol umožňuje kaskádovat za sebe teoreticky neomezené množství LEDek. Jeho dokumentace je, jak to jen říct, velmi nejednoznačná. Každá LED tvoří posuvný registr pro 24bitů (3Byty pro intenzity tří barev). Bit se zapisuje jako pulz různé délky. Pracovně řekněme že bit s hodnotou "0" se zapisuje jako kladný pulz v trvání přibližně 400ns následovaný 850ns mezerou (nízkou úrovní). Bit s hodnotou "1" jako přibližně 800ns kladný pulz a 450ns mezera (0V). Mezera delší jak 50us pak způsobí "latch", tedy LED převezme aktuální obsah posuvného registru a nastaví podle toho intenzity všech tří LED - tedy jas a barvu. Jen pro zajímavost si dovolím shrnout do tabulky různé údaje z různých datasheetů.

\
Název "0"
(H / L)
"1"
(H / L)
Reset
(Latch)
zdroj / odkaz
WS2812 200~500ns / 650~950ns 550~850ns / 450~750ns >50us https://cdn-shop.adafruit.com/datasheets/WS2812.pdf
WS2812C 220~380ns / 580~1000ns 580~1000ns / 220~420ns >280us https://www.tme.eu/Document/4a8239e52df5e0b5cc2eb96834c5d8b6/WS2812B-2020.pdf
WS2813 300~450ns / 300ns~100us 750~1000ns / 300ns~100us >300us https://www.tme.eu/Document/cf443473af21167b0bdd177fb1a7e90a/WS2813.pdf
WS2813C 220~380ns / 580~1000ns 580~1000ns / 220~420ns >280us https://www.tme.eu/Document/7d2748d1482e2b3eda43d8d98894faca/WS2813C.pdf
OST34020C1A 150~450ns / 750~1050ns 750~1050ns / 150~450ns ~80us https://www.tme.eu/Document/51bb5ba94ddb7397e2c58834d9bef250/OST34020C1A.pdf
OSTW3535C1A
(RGBW)
220~380ns / 580~1600ns 580 ~ 1600ns / 220~420ns >280us https://www.tme.eu/Document/93097ceeb1802c05dbd38868f2abee01/OSTW3535C1A.pdf
WS2811 350~650ns / 1850~2150ns 1050~1350ns / 1150~1450ns >50us https://www.tweaking4all.com/wp-content/uploads/2014/01/WS2811.pdf

Proč to všechno uvádím. Časové parametry těchto komunikačních protokolů jsou různé. Z části to bude tím že jde o různá zařízení a z části to bude faktem, že čínské datasheety stojí za prd. Čísla jsou v nich dost často vycucaná z prstů (nebo okopírovaná z dokumentace podobných zařízení). Navíc nad většinou těch údajů se vznáší jeden velký otazník. Vysílání každého bitu je zakončeno "mezerou" (tedy minimálním časem po který datová linka setrvává v úrovni 0V). Tato mezera musí mít definované minimální trvání než se začne vysílat další bit (kladný pulz) avšak její maximální délka je už poněkud volnější. Příliš dlouhá mezera vede k vyvolání "latch" události - zakončuje datový rámec a dává LEDkám pokyn aby nastavily svou barvu. Jenže maximální mezera mezi bity se dle datasheetu pohybuje v řádu stovek nanosekund a "latch" pulz v řádu desítek až stovek us. Jak LED reaguje na mezery z tohoto intervalu ? Co se stane když mezeru mezi znaky trošku natáhneme, třeba na jednotky us ? Například pokud uděláme 2us mezeru mezi bity pochopí to zařízení jako příkaz "latch" ? A pokud ano, proč je v dokumentaci řečeno že to musí být minimálně 50us ? Že by tak velká výrobní tolerance ? To asi ne. Osobně se domnívám, a případně mě opravte, že mezera mezi bity může být mnohem širší, klidně jednotky us. A uváděná "maximální" délka mezery je v dokumentaci spíš jako "pomůcka" kolik by to asi mělo být pokud by měl být datový tok například 800kb/s.

Většina dnešních MCU nemá periferii jejímž primárním účelem je komunikace tímto protokolem. Mnohdy nám tedy při psaní driverů nezbývá než sáhnout k asembleru a počítat instrukce. Což není pro každého úplně příjemná práce. Ve vánoční nudě jsem se tedy rozhodl vyzkoušet pár alternativních cest jak realizovat vysílání pomocí běžných periferií na různých platformách. A o ty bych se s vámi rád podělil. Možná pro vás budou zajímavé už jen kvůli schopnosti snadné adaptace na časování konkrétní LEDky, LED driveru či modulku.

/ Rozcestník |

/ STM8S - SPI |

První periferie, která se nabízí je SPI. Signál na MOSI může mít vcelku jemné časové rozlišení. SPI umí běžet na poloviční frekvenci jádra. STM8S20x může běžet až na 24MHz, ostatní STM8S pak na 16MHz. Díky tomu máme k dispozici časové rozlišení 1/12 respektive 1/8 mikrosekundy (62.5ns respektive 125ns). Takové rozlišení stačí. Princip funkce je jednoduchý. Výstup clocku nás nezajímá, takže ho necháme nezapojený. Vstup MISO taktéž. Jediné co nás zajímá je datový výstup MOSI. Po SPI odešleme binární zprávu odpovídající několika po sobě jdoucích "jedniček". Jejich počet spolu s frekvencí SPI určuje délku kladného pulzu, který bude na MOSI výstupu. Například budeme-li mít SPI clock 8MHz a budeme-li chtít vygenerovat kladný pulz délky 450ns, musíme odeslat byte s trojicí "jedniček". Tedy například 0b00001110, nebo třeba 0b01110000. Důležité je aby poslední i první bit zprávy byl nulový. Na MOSI totiž po skončení vysílání zůstává úroveň odpovídající úrovni posledního bitu (a my chceme mít záruku že tam bude nízká úroveň). No a to je vše. Celý byte se vysílá 8*125ns=1000ns. Nelze tedy překročit přenosovou rychlost 1Mb/s. To ale není žádoucí. Navíc můj program má drobné prodlevy mezi jednotlivými bajty (tedy z pohledu WS2812 bity), takže skutečná přenosová rychlost bude nižší. Protože nepoužíváme MISO vstup, dovolil jsem si v ukázce využít "Bi-directional Mode", který by měl uvolnit MISO pin k jiným účelům.

// A) STM8 -> WS2812B (STM8S208, 16MHz internal RC)
// test driver for WS2812B using SPI (MOSI used as data ouput on PC6, SCK left unconnected)
// Possible release MISO using BIDIrectional mode

#include "stm8s.h"
#include "milis.h"
//#include "stdio.h"
//#include "spse_stm8.h"
//#include "stm8_hd44780.h"

void init_spi(void);
void test(uint8_t* data, uint16_t delka);

// test pattern for (12 RGB LED ring)
uint8_t colors[36]={
0xff,0x00,0x00, // B
0x00,0xff,0x00, // R
0x00,0x00,0xff, // G
0x00,0x00,0x00, // black
0x2f,0x2f,0x2f  // light white
};

#define L_PATTERN 0b01110000 // 3x125ns (8MHZ SPI)
#define H_PATTERN 0b01111100 // 5x125ns (8MHZ SPI), first and last bit must be zero (to remain MOSI in Low between frames/bits)

void main(void){
CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1); // 16MHz from internal RC
init_milis(); // millis using TIM4 - not necessary
init_spi();

while (1){
  test(colors,sizeof(colors)); 	// transfer image into RGB LED string
  delay_ms(2);  								// wait some time
 }
}

// takes array of LED_number * 3 bytes (RGB per LED)
void test(uint8_t* data, uint16_t length){
 uint8_t mask;
 disableInterrupts(); // can be omitted if interrupts do not take more then about ~25us
 while(length){   // for all bytes from input array
  length--;
  mask=0b10000000; // for all bits in byte
  while(mask){
   while(!(SPI->SR & SPI_SR_TXE)); // wait for empty SPI buffer
   if(mask & data[length]){ // send pulse with coresponding length ("L" od "H")
    SPI->DR = H_PATTERN;
   }else{
    SPI->DR = L_PATTERN;
   }
   mask = mask >> 1;
  }
 }
 enableInterrupts();
while(SPI->SR & SPI_SR_BSY); // wait until end of transfer - there should come "reset" (>50us in Low)
}

void init_spi(void){
// Software slave managment (disable CS/SS input), BiDirectional-Mode release MISO pin to general purpose
SPI->CR2 |= SPI_CR2_SSM | SPI_CR2_SSI | SPI_CR2_BDM | SPI_CR2_BDOE; 
SPI->CR1 |= SPI_CR1_SPE | SPI_CR1_MSTR; // Enable SPI as master at maximum speed (F_MCU/2, there 16/2=8MHz)
}

Oscilogram části komunikace ze kterého jsou patrné správné délky kladných pulzů a mírné (pravděpodobně legální) překročení délky mezer

Pokud se nám poštěstí do té míry že budeme mít k dispozici clock, který lze podělit tak aby přenosová rychlost SPI dosáhla přibližně 3MHz, můžeme náš postup drobně zdokonalit. Při Taktu 3MHz odpovídá délka jednoho bitu 333ns a do jednoho bytu se nám tak vlezou dva bity. Každý ze dvou bitů pro WS2812 odešleme pomocí čtveřice SPI bitů, které budou tvořit rámce o délce 4*333=1332ns. Kombinací 0b0100 odešleme pulz o délce 333ns a mezeře 999ns (tedy bit s hodnotou "0") a kombinací 0b0110 pošleme pulz o délce 666ns a mezeře taktéž 666ns což WS2812 pochopí jako bit s hodnotou "1". Pro tuto ukázku jsem MCU vybavil 24MHz krystalem (k tomu je nutné aktivovat 1 wait state pro flash paměť). Pro přehlednost zveřejňuji jen ty části předchozí ukázky, které jsou odlišné.

// A2) STM8 -> WS2812B (STM8S208, 25MHz Xtal)
// test driver for WS2812B using SPI (MOSI used as data ouput on PC6, SCK left unconnected, MISO free)
// variant with 2bits in 1 SPI byte

#define L_PATTERN 0b0100 // 333ns pulse, 999ns space
#define H_PATTERN 0b0110 // 666ns pulse, 666ns space

void main(void){
CLK_SYSCLKConfig(CLK_PRESCALER_CPUDIV1);
CLK_ClockSwitchConfig(CLK_SWITCHMODE_AUTO, CLK_SOURCE_HSE, DISABLE, CLK_CURRENTCLOCKSTATE_ENABLE); // 24MHz Xtal oscillator
init_milis(); // millis using TIM4 - not necessary
init_spi();

 while (1){
  test(colors,sizeof(colors)); 	// transfer image into RGB LED string
  delay_ms(2);  								// wait some time
 }
}

// takes array of LED_number * 3 bytes (RGB per LED)
void test(uint8_t* data, uint16_t length){
 uint8_t mask,tmp;
 disableInterrupts(); // can be omitted if interrupts do not take more then about ~25us
 while(length){   // for all bytes from input array
  length--;
  mask=0b10000000; // for all bits in byte
  tmp=0; // combine two bit patterns into one byte there
  while(mask){
   if(mask & data[length]){ // set high nibble with corresponding bit pattern
    tmp |= H_PATTERN<<4;
   }else{
    tmp |= L_PATTERN<<4;
   }
   mask = mask >> 1;
   if(mask & data[length]){ // set low nibble with corresponding bit pattern
    tmp |= H_PATTERN;
   }else{
    tmp |= L_PATTERN;
   }
   mask = mask >> 1;
   while(!(SPI->SR & SPI_SR_TXE)); // wait for empty SPI buffer
   SPI->DR = tmp; // send pattern with two pulses / two bits for WS2812
  }
 }
 enableInterrupts();
while(SPI->SR & SPI_SR_BSY); // wait until end of transfer - now should come latch/reset (>50us in Low)
}

void init_spi(void){
// Software slave managment (disable CS/SS input), BiDirectional-Mode release MISO pin to general purpose
SPI->CR2 |= SPI_CR2_SSM | SPI_CR2_SSI | SPI_CR2_BDM | SPI_CR2_BDOE; 
SPI->CR1 |= SPI_CR1_SPE | SPI_CR1_MSTR | 2<<3; // Enable SPI as master at maximum speed (F_MCU/8, 24/2=3MHz)
}

Jen pro dokreslení oscilogram stejné části komunikace jako v předchozím příkladu

/ STM8S - TIMer |

Když jde o to generovat pulzy specifické délky, první co člověka napadne je použít k tomu timer. Čtenáře zvyklé na STM32 asi napadne jako první "One-pulse" mód. A protože STM8 mají velice podobné časovače, mají tuto možnost také. V tomto režimu časovač běží jen jednorázově a po přetečení se zastaví. U toho může vygenerovat jednu periodu PWM - tedy jeden pulz. Jehož šířku lze nastavovat s rozlišením timeru - tedy jednoho taktu procesoru. Na STM8 máme (opět podobně jako na STM32) Advanced timer (TIM1) a pak general purpose timery (TIM2,TIM3 atd.). Datasheet překvapivě uvádí že one-pulse mód nelze použít na TIM2 a TIM3. ST by mělo tuto poznámku asi více rozvést, neboť mě TIM3 v tomto režimu úspěšně pracoval. Kostra programu zůstane shodná s první ukázkou, jen namísto vysílání pomocí SPI budeme generovat pulzy/data pomocí timeru. Ukázku předvedu na TIM1 (abych nikoho nedráždil ukázkou pomocí TIMeru, který to oficielně neumí) a zdrojový kód pro variantu využívající TIM3 si dovolím dát jen do odkazu. Shodné části zdrojového kódu s předchozí ukázkou si dovolím přeskočit.

// B) STM8 -> WS2812B (STM8S208, 16MHz internal RC)
// test driver for WS2812B using TIM1 (CH1 used as data ouput, using One-Pulse mode)
// edit to use another channel

#define L_PULSE 6 // 6*1/16MHz = 6*62.5 = 375ns (~400ns)
#define H_PULSE 12 // 12*1/16MHz = 12*62.5 = 750ns (~800ns)

void main(void){
CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1); // 16MHz from internal RC
init_milis(); // millis using TIM4 - not necessary
init_tim();

 while (1){
  test(colors,sizeof(colors));
  delay_ms(2); 
 }
}

void init_tim(void){
 GPIO_Init(GPIOC,GPIO_PIN_1,GPIO_MODE_OUT_PP_LOW_FAST); // PC1 (TIM1_CH1)
 TIM1_TimeBaseInit(0, TIM1_COUNTERMODE_UP, 15, 0); // Upcounting, prescaler 0, dont care period/ARR value
 // OC1 as output with Polarity High in PWM2 mode (OC1N not used)
 TIM1_OC1Init(TIM1_OCMODE_PWM2, TIM1_OUTPUTSTATE_ENABLE, TIM1_OUTPUTNSTATE_DISABLE,
   1, TIM1_OCPOLARITY_HIGH, TIM1_OCNPOLARITY_HIGH, TIM1_OCIDLESTATE_SET,
   TIM1_OCNIDLESTATE_RESET); 
 TIM1_CtrlPWMOutputs(ENABLE); // Timer output global enable
 TIM1_SelectOnePulseMode(TIM1_OPMODE_SINGLE); // Selecting One Pulse Mode
}

// takes array of LED_number * 3 bytes (RGB per LED)
void test(uint8_t* data, uint16_t length){
 uint8_t mask;
 disableInterrupts(); // can be omitted if interrupts do not take more then about ~25us
 while(length){   // for all bytes from input array
  length--;
  mask=0b10000000; // for all bits in byte
  while(mask){
   while(TIM1->CR1 & TIM1_CR1_CEN); // wait if timer run (transmitting last bit)
   if(mask & data[length]){ // send pulse with coresponding length ("L" od "H")
    TIM1->ARRL = H_PULSE; // set pulse width for "H" bit
   }else{
    TIM1->ARRL = L_PULSE; // set pulse width for "L" bit
   }
   TIM1->CR1 |= TIM1_CR1_CEN; // Start timer (start single pulse generation)
   mask = mask >> 1;
  }
 }
 enableInterrupts();
}

Oscilogram opět pro ilustraci jak vypadají pulzy a mezery

Slibovanou variantu pro TIM3 najdete na konci článku v odkazech. Ukázky je možné snadno upravit na jiné kanály timeru, či využít "option bytes" a remapovat si výstupy na jiné piny (což u SPI možné nebylo). Zdatnější programátoři možná dokážou využít "preload" funkce na ARR registru timeru a zkrátit tak mezery mezi pulzy - tedy zvýšit komunikační rychlost.

/ STM8S - UART? |

Další periferie v řadě která by mohla zvládnout náš úkol je UART. Čtenáři familiérní s STM32 by po něm možná sáhli jako po první volbě. Na STM8 je ale situace nepříjemná. UART smí používat přenosové rychlosti maximálně 1/16 frekvence jádra. Takže v našem případě 1Mb/s (či při 24MHz teoreticky 1.5Mb/s). To je ale pro naše účely nedostatečné. Trocha pátrání ukáže že UART1 má režim IrDA, který generuje pulzy o délce 3/16 taktu jádra. To už naše požadavky splňuje. Nejkratší pulz může trvat 188ns a změnou "baudrate" můžeme měnit šířku pulzu. K odeslání jednoho bitu je ale potřeba celý paket a ten trvá příliš dlouho. Prodlevy mezi pulzy jsou tak velké (>20us) že už je WS2812 vyhodnocuje jako "latch". Takže tato cesta je na STM8 neprůchodná. Na jiných platformách, jak snad snad brzy uvidíte, bude situace lepší.

| Odkazy /

Home
| V1.00 26.12.2019 /
| By Michal Dudka (m.dudka@seznam.cz) /