215 lines
8.3 KiB
C
215 lines
8.3 KiB
C
/* 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 24bit 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.
|
|
* that corresponds to a 800 kbps data transfer rate.
|
|
* 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
|
|
uint8_t out = *(next++); // the next bit to send
|
|
|
|
while (!reset_code); // wait for previous reset code to finish
|
|
|
|
// use in-line assembly to handle precise timing constraints, and volatile to prevent optimisation of the code
|
|
cli(); // disable interrupt for time critical code
|
|
__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
|
|
"sbiw %[count], 1" "\n\t" // 0,2,2 decrement the number of bits to send (do it here to be able to compare immediately)
|
|
"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)
|
|
|
|
}
|