screenlight/firmware/lib/ws2812b.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)
}