Reputation: 472
Before anyone shouts at me, I know the WS2812B LED strips demand a very precise timing and that it is more easily done with assembly code (and that great libraries already exist out there).
Still, I'm wondering: is it really not possible to drive the strips using timers and interrupts ? After all, the atmega328p doc says about timers that it "allows accurate program execution timing (event management) and wave generation".
I'm using an arduino nano (atmega328p) with a CPU frequency of 16MHz (so with Fast PWM, we get a cycle of duration 6.25 ns right?)
Here's what I wish to do : I want to use the Timer0 to generate the bit-banging signal to drive the LED Strip. I looked into the light_ws2812 lib for (working) timings and my math gave me the following:
So I set up the timer0 to mode 7 according to the Atmega328p datasheet: Fast PWM mode, with a TOP limit of 14+5=19 defined by OCR0A, and I set a compare match value of either 5 or 14 in OCR0B depending on the bit value to be sent. The change of value in OCR0B is done with interrupt on counter overflow flag
A first try is to light one led in color red ; I wrote the following code to achieve all this :
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#define T0H_t 5
#define T1H_t 14
#define TOTAL_t (T0H_t+T1H_t)
#define GET_TIME(B) (B & 1? T1H_t : T0H_t)
#define MAX_LEDS 10
#define BUFFER_SIZE (3*MAX_LEDS)
/**
* @brief Buffer for GRB values to be sent via bit-banging.
*
* Bytes are sent in MSBF. When all bytes are sent, buffer should get reset.
*/
struct sending_buffer
{
uint8_t buffer [BUFFER_SIZE]; // stores RGB bytes to be sent
uint8_t byte_index; // index to byte being sent
uint8_t bit_index; // index to next bit to be sent
uint8_t last_byte; // index of last byte + 1
};
struct sending_buffer colors_to_send;
struct sending_buffer * init_buffer(struct sending_buffer * sb)
{
sb->byte_index = 0;
sb->bit_index = 7;
sb->last_byte = 0;
return sb;
}
// returns the bit value of next bit to be sent from sb
uint8_t get_next_bit(struct sending_buffer * sb)
{
uint8_t bit = (sb->buffer[sb->byte_index] >> sb->bit_index--) & 1;
sb->bit_index = sb->bit_index > 7 ? 7 : sb->bit_index ; // Bytes are sent in MSBF
if(sb->bit_index == 7)
{
sb->byte_index++;
sb->byte_index %= BUFFER_SIZE;
}
return bit;
}
uint8_t put_byte(struct sending_buffer * sb, uint8_t byte)
{
sb->buffer[sb->last_byte++] = byte;
}
uint8_t send_color(uint8_t r, uint8_t g, uint8_t b)
{
put_byte(&colors_to_send, g);
put_byte(&colors_to_send, r);
put_byte(&colors_to_send, b);
}
void init_send()
{
if(colors_to_send.byte_index < colors_to_send.last_byte)
{
uint8_t next_bit = get_next_bit(&colors_to_send);
OCR0B = GET_TIME(next_bit);
}
sei();
}
void setup_interrupt()
{
/**
* @brief Setting up Fast PWM mode with TOP value defined by OCR0A ; clearing OC0B on c-m between OCR0B and TCNT0.
*
*/
// Set Fast PWM mode with TOP value defined by OCR0A
TCCR0A |= (1 << WGM00) | (1 << WGM01);
TCCR0B |= (1 << WGM02);
// Set COM0B1 to clear OC0B on compare match
TCCR0A |= (1 << COM0B1);
// Set timer without prescaler (CS00)
TCCR0B |= (1 << CS00);
// Set TOP value
OCR0A = TOTAL_t; // Assuming TOTAL_t is correctly calculated elsewhere
// Set initial value of OCR0B to 0 (clear OC0B)
OCR0B = 0;
// Set pin 5 (OC0B) as output
DDRD |= (1 << PD5);
// Enable interrups
TIMSK0 |= (1 << TOIE0);
}
ISR(TIMER0_OVF_vect)
{
if(colors_to_send.byte_index < colors_to_send.last_byte)
{
uint8_t next_bit = get_next_bit(&colors_to_send);
OCR0B = GET_TIME(next_bit);
}
}
int main()
{
_delay_ms(10);
init_buffer(&colors_to_send);
send_color(255,0,0);
setup_interrupt();
init_send();
while(colors_to_send.byte_index < colors_to_send.last_byte)
;
cli();
OCR0B = 0;
_delay_ms(10);
while(1);
return 0;
}
Now obvisously I'm doing something wrong since this is not working. So my question is:
Is my code wrong in some place (and if it is, where?), or is it really undoable to drive the WS2812B that way?
Upvotes: 1
Views: 179
Reputation: 819
You have 1.25 microseconds to send one bit. That's 20 clock cycles. From this, 4 cycles to activate the interrupt (CALL
instruction) and 4 cycles to return from interrupt (RETI
instruction) must be substracted. You have 12 machine cycles left. Do you think the interrupt handler code you created only has 12 instructions?
In my opinion, this cannot be done even in assembler. In C no way. And I agree with the comments. Measure it yourself. There is nothing simpler. If you want to do such things on the edge, you must also have the appropriate equipment (logic analyzer or oscilloscope).
Upvotes: 0