aboutsummaryrefslogtreecommitdiff
path: root/firmware/lib/ws2812b.c
blob: e481730c1f4f73eaf1b4f105afa396749409697c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/* 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 = low | *(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)
	
}