2015-07-14 09:51:50 +02:00
/* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program . If not , see < http : //www.gnu.org/licenses/>.
*
*/
/* This library allows to control WS2812B LEDs
* it uses timer 0 with interruts
*/
# include <stdint.h> // Standard Integer Types
# include <stdio.h> // Standard IO facilities
# include <stdlib.h> // General utilities
# include <stdbool.h> // boolean type
# include <avr/io.h> // AVR device-specific IO definitions
# include <avr/interrupt.h> // Interrupts
# include "ws2812b.h" // WS2812B header
/* has the WS2812B been initialized correctly */
bool initialized = false ;
/* the green, red, and blue (8 bits each) bit values for each LEDs */
uint8_t color_bits [ 3 * 8 * WS2812B_NB_LEDS ] = { 0 } ;
/* how much to shift for which channel */
uint8_t channel_shifts [ WS2812B_NB_CHANNELS ] = { 0 } ;
/* have the 50 us reset code timeout passed */
volatile bool reset_code = true ;
/* initialize */
bool ws2812b_init ( )
{
initialized = false ;
/* verify we are running with 16 MHz */
if ( F_CPU ! = 16000000 ) {
return initialized ; // the code has been designed for 16 MHz
}
/* verify there are LEDs to use */
if ( WS2812B_NB_LEDS = = 0 ) {
return initialized ; // there in no data to send
}
/* generate the channel shift depending on mask*/
uint8_t channel = 0 ;
for ( uint8_t shift = 0 ; shift < 8 ; shift + + ) {
if ( ( WS2812B_MASK > > shift ) & 0x01 ) {
channel_shifts [ channel + + ] = shift ;
}
}
/* verify the number of channels corresponds to the mask */
if ( WS2812B_NB_CHANNELS ! = channel ) {
return initialized ; // the number of channels does not match the mask
}
WS2812B_DDR | = WS2812B_MASK ; // set pins as output
/* user timer 0 to time the reset code of 50 us */
TCCR0B | = ( 0 < < WGM02 ) ; // set mode 2, Clear Timer on Compare Match
TCCR0A | = ( 1 < < WGM01 ) | ( 0 < < WGM00 ) ; // set mode 2, Clear Timer on Compare Match
TCCR0B & = ~ ( ( 1 < < CS02 ) | ( 1 < < CS01 ) | ( 1 < < CS00 ) ) ; // stop timer 0
OCR0A = 100 ; // corresponds to 50 us when using prescale 8
TIMSK0 | = ( 1 < < OCIE0A ) ; // enable interrupt when OCRA0A is matched
TCNT0 = 0 ; // reset timer 0
initialized = true ;
return initialized ;
}
/* timer 0 OCR0A match interrupt
* 50 us passed
*/
ISR ( TIMER0_COMPA_vect )
{
reset_code = true ; // reset code passed
TCCR0B & = ~ ( ( 1 < < CS02 ) | ( 1 < < CS01 ) | ( 1 < < CS00 ) ) ; // stop timer 0
}
/* set the color of the LED on a channel LED chain, but do not show yet */
bool ws2812b_set_led_color ( uint8_t channel , uint8_t led , uint8_t red , uint8_t green , uint8_t blue )
{
if ( ! initialized ) {
return false ;
}
if ( channel > = WS2812B_NB_CHANNELS ) {
return false ;
}
if ( led > = WS2812B_NB_LEDS ) {
return false ;
}
/* set color values
* composition of 24 bit data : G7 . . G0 , R7 . . R0 , B7 . . B0
* high bit is sent first
* LEDs keep the first values and forward the next
*/
for ( uint8_t i = 0 ; i < 8 ; i + + ) {
uint8_t color_bit = 1 < < ( 7 - i ) ;
if ( green & color_bit ) {
color_bits [ 8 * 3 * led + i ] | = ( 1 < < channel_shifts [ channel ] ) ;
} else {
color_bits [ 8 * 3 * led + i ] & = ~ ( 1 < < channel_shifts [ channel ] ) ;
}
if ( red & color_bit ) {
color_bits [ 8 * 3 * led + 8 + i ] | = ( 1 < < channel_shifts [ channel ] ) ;
} else {
color_bits [ 8 * 3 * led + 8 + i ] & = ~ ( 1 < < channel_shifts [ channel ] ) ;
}
if ( blue & color_bit ) {
color_bits [ 8 * 3 * led + 16 + i ] | = ( 1 < < channel_shifts [ channel ] ) ;
} else {
color_bits [ 8 * 3 * led + 16 + i ] & = ~ ( 1 < < channel_shifts [ channel ] ) ;
}
}
return true ;
}
/* switch off all LEDs, but do not show yet */
void ws2812b_off ( void )
{
for ( uint16_t i = 0 ; i < sizeof ( color_bits ) ; i + + ) {
color_bits [ i ] = 0 ;
}
}
/* send the RGB values to the LEDs */
void ws2812b_show ( void )
{
if ( ! initialized ) {
return ;
}
/* WS2812B LEDs use 1 bit per 1.25 us communication input.
2015-07-23 10:18:38 +02:00
* that corresponds to a 800 kbps data transfer rate .
2015-07-14 09:51:50 +02:00
* on a 16 MHz development board 1 clock cycle needs 62.5 ns .
* this means we have exactly 20 clock cycles to send a single bit .
* this timing constraint is hard to respect with pure C .
* even with interrupt handling routing , this is not fast enough .
* thus we will use inline assembly to handle precise timing constraints .
* a 0 bit is represented with a HIGH signal for 0.4 us ( ~ 6 clock cycles ) , and a LOW signal for 0.85 us ( ~ 14 clock cycles ) .
* this corresponds to the following output form : HHHHHHLLLLLLLLLLLLLL ( in clock cycles ) .
* a 1 bit is represented with a HIGH signal for 0.8 us ( ~ 13 clock cycles ) , and a LOW signal for 0.45 us ( ~ 7 clock cycles ) .
* this corresponds to the following output form : HHHHHHHHHHHHHLLLLLLL ( in clock cycles ) .
* the common output form is : HHHHHHxxxxxxxLLLLLLL .
* to end the communication , at least 50 us must pass between two high signals .
* this implementation will handle the communication the following way :
* - put the signal high
* - decrement the bit counter
* - verify if there is a bit to transfer
* - if not , start reset procedure : put the signal low and wait for 50 us ( 800 cycles )
* - if yes , set the bit value a T = 6 , using a precomputed tables
* - prepare the next bit value
* - set the signal to low at T = 13
* - restart this loop
* before starting this procedure , the table of bit values need to be precomputed in a table .
* this table will hold the state of the pin of a port at T = 6 : low for 0 , high for 1.
* the disadvantage is that this one pin bit value needs to be stored in a port byte variable , which would waste 7 bit of memory .
* this can be turned into an advantage by storing in this byte the value for the 8 pins of this port .
* thus we can drive in parallel 8 WS2812B chains , with no time loss , by using all the bits in this byte
*/
uint16_t count = WS2812B_NB_LEDS * 24 + 1 ; // the number of bits to transfer: R+G+B (8+8+8=24) per LED on a channel, plus one because of the loop implementation
uint8_t high = WS2812B_PORT | WS2812B_MASK ; // set all WS2812B pins to high
uint8_t low = WS2812B_PORT & ~ WS2812B_MASK ; // set all WS2812B pins to low
uint8_t * next = color_bits ; // a pointer to the next color bit
2015-07-23 10:45:15 +02:00
uint8_t out = * ( next + + ) ; // the next bit to send
2015-07-14 09:51:50 +02:00
while ( ! reset_code ) ; // wait for previous reset code to finish
2015-07-23 10:44:15 +02:00
// use in-line assembly to handle precise timing constraints, and volatile to prevent optimisation of the code
cli ( ) ; // disable interrupt for time critical code
2015-07-14 09:51:50 +02:00
__asm__ __volatile__ (
" 0: " " \n \t " // clock start,duration,stop instruction (use local label = number)
" out %[port], %[high] " " \n \t " // -1,1,0 start bit, set signal to high
2015-07-23 10:44:15 +02:00
" sbiw %[count], 1 " " \n \t " // 0,2,2 decrement the number of bits to send (do it here to be able to compare immediately)
2015-07-14 09:51:50 +02:00
" breq 1f " " \n \t " // 2,1-2,3-4 if this was the last bit, start the reset procedure (go "f"orward to 1)
" or %[out], %[low] " " \n \t " // 3,1,4 combine bit and port values
" nop " " \n \t " // 4,1,5 wait before outputing bit value
" out %[port], %[out] " " \n \t " // 5,1,6 output bit value
" ld %[out], %a[next]+ " " \n \t " // 6,2,8 load next bit value
" nop " " \n \t " // 8,1,9 wait before outputing low
" nop " " \n \t " // 9,1,10 wait before outputing low
" nop " " \n \t " // 10,1,11 wait before outputing low
" nop " " \n \t " // 11,1,12 wait before outputing low
" out %[port], %[low] " " \n \t " // 12,1,13 ensure signal is low
" nop " " \n \t " // 13,1,14 wait before going to next bit
" nop " " \n \t " // 14,1,15 wait before going to next bit
" nop " " \n \t " // 15,1,16 wait before going to next bit
" nop " " \n \t " // 16,1,17 wait before going to next bit
" rjmp 0b " " \n \t " // 17,2,19 start next bit (go "b"ack to 0)
" 1: " " \n \t " // start sending reset code (use local label = number)
: // output operands
[ count ] " +w " ( count ) ,
[ out ] " +r " ( out )
: // input operands
[ port ] " I " ( _SFR_IO_ADDR ( WS2812B_PORT ) ) ,
[ high ] " r " ( high ) ,
[ low ] " r " ( low ) ,
[ next ] " e " ( next )
) ;
sei ( ) ; // re-enable interrupts
/* send reset code */
WS2812B_PORT = low ; // set to low (for at least 50 us)
reset_code = false ; // block further transmission
TCCR0B | = ( 0 < < CS02 ) | ( 1 < < CS01 ) | ( 0 < < CS00 ) ; // start reset code waiting time (with a prescale of 8)
}