how to use millis() instead of for loop in this function

1.2k Views Asked by At

I use Neopixels (64 LEDs), and I have a function called level_up that gets different led_num each time. Generally, it is a level bar; level[1] will light all the LEDs in a sequence from 0 to 28, level[2] all the LEDs from 29 to 48, etc. The function I attached works fine, but I need to change the delay to millis() and not sure how. Any thoughts?

uint8_t level[] = {0, 28, 48, 60, 64};  //levels 0 to 4


void level_up(uint8_t wait, uint8_t led_num) {
    uint8_t start_point;
    if (led_num == level[1]) start_point = 0;   //up from level 0 to 1
    if (led_num == level[2]) start_point = 28;  //up from level 1 to 2
    if (led_num == level[3]) start_point = 48;  //up from level 2 to 3
    if (led_num == level[4]) start_point = 60;  //...

    for (uint8_t i = start_point; i < led_num; i++) {
        strip.setPixelColor(i, strip.Color(0, 0, 255));
        strip.show();
        delay(wait);  //TODO: change it to timer
    }
}

void loop() {
    if (plus_btn.pressed()) {
        score++;
        if (score >= 4) {
            score = 4;
        }
    }
    if (minus_btn.pressed()) {
        score--;
        if (score <= 0) {
            score = 0;
        }
    }

switch (score) {
    case 0:
        if (last_score == 1) level_down(50, level[0]);
        last_score = 0;
        break;
    case 1:
        // if last_score was 0 make the blue effect because level is up
        if (last_score == 0) level_up(50, level[1]);
        // if last_score was 2 make the red effect because level is down
        if (last_score == 2) level_down(50, level[1]);
        last_score = 1;
        break;
    case 2:
        if (last_score == 1) level_up(50, level[2]);
        if (last_score == 3) level_down(50, level[2]);
        last_score = 2;
        break;
    case 3:
        if (last_score == 2) level_up(50, level[3]);
        if (last_score == 4) level_down(50, level[3]);
        last_score = 3;
        break;
    case 4:
        winning_timer.start();
        winning();
        digitalWrite(WINNING_SENSOR_PIN, HIGH);
        break;
}

Serial.println(score);

}

3

There are 3 best solutions below

3
On

Using millis() will not block the for-loop like delay(). Therefore I think you will have to adapt the code that is calling your method, because at the moment it looks like your code depends on being blocked in the for-loop. But generally you would use millis() like in the code sample below. You store the start-timestamp and then do something after your wait-period is over.

uint8_t level[] = {0, 28, 48, 60, 64};  //levels 0 to 4

uint8_t counter;
uint8_t end_point;
bool show_level;

void level_up(uint8_t wait, uint8_t led_num) {
    if (led_num == level[1]) counter = 0;   //up from level 0 to 1
    if (led_num == level[2]) counter = 28;  //up from level 1 to 2
    if (led_num == level[3]) counter = 48;  //up from level 2 to 3
    if (led_num == level[4]) counter = 60;  //...
    show_level =true;
    end_point = led_num;
}

bool set_pixel_color(uint8_t wait) 
{
    if(timestamp - millis() == wait)
    {
        strip.setPixelColor(counter, strip.Color(0, 0, 255));
        strip.show();
        timestamp = millis();
        return true; // incremented 
    }    
    return false;
}

void show_level_led_strip()
{
    if(show_level)
    {
        if(counter > end_point) // escape when the counter gets bigger then the current led_num
        {
            show_level = false;
        }
        else
        {
            if(set_pixel_color(50))
            {
                counter++;
            }
        }
    }
}

void loop() {
    if (plus_btn.pressed()) {
        score++;
        if (score >= 4) {
            score = 4;
        }
    }
    if (minus_btn.pressed()) {
        score--;
        if (score <= 0) {
            score = 0;
        }
    }

    switch (score) {
        case 0:
            if (last_score == 1) level_down(level[0]);
            last_score = 0;
            break;
        case 1:
            // if last_score was 0 make the blue effect because level is up
            if (last_score == 0) level_up(level[1]);
            // if last_score was 2 make the red effect because level is down
            if (last_score == 2) level_down(level[1]);
            last_score = 1;
            break;
        case 2:
            if (last_score == 1) level_up(level[2]);
            if (last_score == 3) level_down(level[2]);
            last_score = 2;
            break;
        case 3:
            if (last_score == 2) level_up(level[3]);
            if (last_score == 4) level_down(level[3]);
            last_score = 3;
            break;
        case 4:
            winning_timer.start();
            winning();
            digitalWrite(WINNING_SENSOR_PIN, HIGH);
            break;
    }

    show_level_led_strip();
}

Serial.println(score);
0
On

This doesn't answer your question directly, but the strategy that I use gives me any number of timed events without my program blocking in millis().

Set a deadline in the future and enclose the delayed action in an if statement that polls millis() until that deadline is reached. It's not perfect because the software timing loses time due to processing, and because of the millis() overflow and wrap-around issue (look it up on arduino.cc).

/* Global variables (constexpr creates a compile time only constant) */
constexpr uint32_t WAIT_INTERVAL = 10;  // interval is 10ms
uint32_t deadline = 0;  // when to run next

// inside loop()
    uint32_t now = millis();  // capture the current millis() value
    if(now >= deadline)
    {
        deadline = now + WAIT_INTERVAL;  // push the next deadline into the future
        // perform timed periodic operations here (call function or whatever)
    }
9
On

After reading your post and all comments, I think I know what you want. You just want that the loop continues without stay in this function during the delay right?

millis() does not sleep or delay, it just gives you the time in milliseconds, since the Arduino is running.

So, you can just add this to your code and it will work:

uint8_t level[] = {0, 28, 48, 60, 64};  //levels 0 to 4
unsigned long lastTime = 0;  // << add this
uint8_t start_point = 0; // << move here

void update_leds(uint16_t wait, uint8_t led_num) {
    if(start_point >= led_num) return;
    if(millis() - lastTime > wait) {  // << add this
        //uint8_t start_point;
        lastTime = millis(); // << add this

        //for (uint8_t i = start_point; i < led_num; i++) {
            strip.setPixelColor(start_point, strip.Color(0, 0, 255));
            strip.show();
            //delay(wait);  // << remove this
        //}
        start_point++;
    }
}

void level_up(uint8_t led_num) {
    if (led_num == level[1]) start_point = 0;   //up from level 0 to 1
    if (led_num == level[2]) start_point = 28;  //up from level 1 to 2
    if (led_num == level[3]) start_point = 48;  //up from level 2 to 3
    if (led_num == level[4]) start_point = 60;  //...
}

change wait from uint8_t to uint16_t, since 255 could be too little.

Now, you can call this function many times, but the leds are updated only when the timeout finished.

There is only one problem: if inside your loop you have other delays, maybe the leds are updated some milliseconds later than expected... If you understand what I mean.

Edit: some times, you want also be notified if the leds was updated. So you can return a bool to say if the function updated the leds or not (maybe you need it in the loop, to check if the it "leveled up".