Variables changing at random | Digispark ATTiny85

I have some code, which does not have any issues running on ESP32. If I run this on ATTiny85, it does have issues.

What happens is, I will run the program and move the "sender" (potentiometer) across its range a few times. Eventually, seemingly at random, the "gauge" (pwm) output changes. It clamps to 0 or 1.

Somehow, one of the variables (sender_min/max, or gauge_min/max) is getting changed, it seems.

I commented out all the code which can change those variables other than at program start.

This never happens if I hardcode the gauge/pwm output to a set value, so the PWM timer seems to be working fine and I am left with the variables getting changed somehow.

The program is not resetting, as the blinkLED() at the start of the program (or anywhere) is not firing.

Since there isn't a great way to Serial write with attiny85 I am having a bit of trouble debugging.

I feel like what is happening is maybe some out of bounds memory access, but I am not seeing it in the code. Maybe I am missing it.

Also attached the circuit diagram. Thanks for the help.

#include <EEPROM.h>

#define SENDER_PIN 3
#define GAUGE_PIN 4

#define SENDER_SET_PIN 0
#define LED_PIN 1
#define GAUGE_SET_PIN 2

// 100 times per second
#define EXECUTION_RATE_MS 10

typedef enum ButtonPressResult {
    ButtonPressNone = 0,
    ButtonPressShort = 1,
    ButtonPressLong = 2
} ButtonPressResult;

typedef struct Button {
    int state;
    unsigned long start_time;
} Button;

Button SENDER_SET_BUTTON = { .state = HIGH, .start_time = 0 };
Button GAUGE_SET_BUTTON = { .state = HIGH, .start_time = 0 };

int sender_min = 0;
int sender_max = 1023;
int gauge_min = 0;
int gauge_max = 255;

int averageGaugeVal[80];
int averageGaugeValSorted[80];

unsigned long lastSampleTime = 0;

void blinkLED(int count) {
    for (int i = 0; i < count; i++) {
        digitalWrite(LED_PIN, HIGH);
        delay(200);
        digitalWrite(LED_PIN, LOW);
        delay(200);
    }
}

void writeInt(int val, int offset) {
    // Serial.print("writeInt: ");
    // Serial.println(val, DEC);
    EEPROM.write(offset, highByte(val));
    EEPROM.write(offset + 1, lowByte(val));
}

int readInt(int offset) {
    byte high = EEPROM.read(offset);
    byte low = EEPROM.read(offset + 1);
    // Serial.print("readInt: ");
    // Serial.println(word(high,low), DEC);
    return word(high,low);
}

int getSender() {
    return analogRead(SENDER_PIN);
}

void setGauge(int val) {
    // Serial.print("setGauge: ");
    // Serial.println(val);
    
    analogWrite(GAUGE_PIN, val);

    // analogWrite(GAUGE_PIN, 220);
}

void setSenderMin(int val) {
    // Serial.print("setSenderMin: ");
    // Serial.println(val);

    sender_min = val;
    writeInt(val, 0);
}

void setSenderMax(int val) {
    // Serial.print("setSenderMax: ");
    // Serial.println(val);

    sender_max = val;
    writeInt(val, 2);
}

// void setGaugeMax(int val) {
//     // Serial.print("setGaugeMax: ");
//     // Serial.println(val);

//     gauge_max = val;
//     writeInt(val, 4);
// }

void setGaugeMin(int val) {
    // Serial.print("setGaugeMin: ");
    // Serial.println(val);

    gauge_min = val;
    writeInt(val, 4);
}

int senderToGauge(int v) {
    // If for some reason they get set to the same value, just return 0 since it's invalid anyways
    if (sender_min == sender_max) {
        return 0;
    }

    return (long)(v - sender_min) * (gauge_max - gauge_min) / (sender_max - sender_min) + gauge_min;
}

void setup() {
    // Serial.begin(9600);

    blinkLED(5);

    sender_min = readInt(0);
    sender_max = readInt(2);
    gauge_min = readInt(4);

    // Serial.print("sender_min: ");
    // Serial.println(sender_min);

    // Serial.print("sender_max: ");
    // Serial.println(sender_max);

    // Serial.print("gauge_max: ");
    // Serial.println(gauge_max);

    pinMode(SENDER_SET_PIN, INPUT_PULLUP);
    pinMode(GAUGE_SET_PIN, INPUT_PULLUP);
    pinMode(SENDER_PIN, INPUT);
    pinMode(GAUGE_PIN, OUTPUT);

    // Fill out avg
    int senderVal = getSender();
    for (int i = 0; i < 80; i++) {
        averageGaugeVal[i] = senderVal;
        averageGaugeValSorted[i] = senderVal;
    }

    setGauge(senderToGauge(senderVal));

    // Sample timer
    lastSampleTime = millis();
}

int sortAsc(const void *v1, const void *v2) {
    return *((int *)v1) - *((int *)v2);
}

// Shift everything over
void addGaugeVal(int val) {
    // Serial.print("addGaugeVal: ");
    // Serial.println(val);

    // Add it
    for (int i = 1; i < 80; i++) {
        averageGaugeVal[i - 1] = averageGaugeVal[i];
    }
    averageGaugeVal[79] = val;

    // Copy vals to sorted version
    for (int i = 0; i < 80; i++) {
        averageGaugeValSorted[i] = averageGaugeVal[i];
    }

    // Sort it
    qsort(averageGaugeValSorted, 80, sizeof(averageGaugeValSorted[0]), sortAsc);
}

// Return the median value
int getGaugeVal() {
    return averageGaugeValSorted[39];
}

ButtonPressResult buttonPressed(Button *button, int newState) {
    // Press down
    if (button->state == HIGH && newState == LOW) {
        button->state = LOW;
        button->start_time = millis();
        return ButtonPressNone;
    }

    // Press up
    if (button->state == LOW && newState == HIGH) {
        button->state = HIGH;

        if (millis() - button->start_time >= 50) {
            if (millis() - button->start_time >= 1000) {
                return ButtonPressLong;
            } else {
                return ButtonPressShort;
            }
        }
    }

    return ButtonPressNone;
}

void loop() {
    int senderVal = getSender();
    unsigned long sampleTime = millis();

    // Update gauge val every 100ms
    if (sampleTime - lastSampleTime >= 100) {
        lastSampleTime = sampleTime;

        // Add it to the dampened array
        addGaugeVal(senderVal);

        // Update gauge with latest dampened value
        int gaugeVal = senderToGauge(getGaugeVal());

        setGauge(gaugeVal);
        // setGauge(senderVal);

        // Serial.print("s: ");
        // Serial.println(senderVal);
        // Serial.print("g: ");
        // Serial.println(gaugeVal);
        // Serial.print("gA: ");
        // Serial.println(getGaugeVal());
    }


    // detect if any buttons are pressed which updates eeprom
    // if (ButtonPressResult result = buttonPressed(&SENDER_SET_BUTTON, digitalRead(SENDER_SET_PIN))) {
    //     if (result == ButtonPressShort) {
    //         setSenderMin(senderVal);
    //         // blinkLED(1);
    //     }

    //     if (result == ButtonPressLong) {
    //         setSenderMax(senderVal);
    //         // blinkLED(2);
    //     }
    // }
    
    // if (ButtonPressResult result = buttonPressed(&GAUGE_SET_BUTTON, digitalRead(GAUGE_SET_PIN))) {
    //     if (result == ButtonPressShort) {
    //         setGaugeMin(min(240, gauge_min + 20));
    //         // blinkLED(3);
    //     }

    //     if (result == ButtonPressLong) {
    //         setGaugeMin(0);
    //         // blinkLED(4);
    //     }
    // }

    // Execute at 100hz by sleeping the difference between loop execution time and desired execution rate
    unsigned long elapsedTime = millis() - sampleTime;
    
    if (elapsedTime <= EXECUTION_RATE_MS) {
        delay(EXECUTION_RATE_MS - elapsedTime);
    }
}

You can configure one unused IO pin as Software TX and send the value of the variables in Serial Monitor for debugging burposes.

Test Sketch:

#include<SoftwareSerial.h>
SoftwareSerial SUART(0, 5);  //SRX, STX

void setup()
{
  SUART.begin(9600);
  while (!Serial)
  {
    ;
  }
}

void loop()
{
  SUART.println(23, DEC);
  delay(1000);
}

What are the memory used values at the end of the compile messages?

Sketch uses 2638 bytes (40%) of program storage space. Maximum is 6586 bytes.
Global variables use 336 bytes (65%) of dynamic memory, leaving 176 bytes for local variables. Maximum is 512 bytes.

Sketch uses 2786 bytes (42%) of program storage space. Maximum is 6586 bytes.
Global variables use 348 bytes (67%) of dynamic memory, leaving 164 bytes for local variables. Maximum is 512 bytes.

I was having trouble communicating with the board even plugged in to my circuit board, I have to unplug it to send it a new program. I'll see if I can get it to communicate while plugged in like that.

Your post #5 clearly indicates that your PC is communicating well with the Board.

1 Like

Where is the code for qsort()? If it is a variant of quick sort it will allocate storage for the partitioning, which may exceed your memory size.

No, hah, I didn't even have the board connected. I just compiled it, and I guess the board definition is where it's getting the memory information.

How do I determine which version of qsort? I am not importing anything specific to it and this is whatever qsort is being resolved by default when using this ATTinyCore board.

Is it not the standard libray function of C?

You are trying to keep the last 80 values read from the pot, then calculate the median of them. That's what the second array and qsort() is for, so that the median value is at averageGaugeValSorted[39]. You then send this value to the output pin as the "dampened" value.

This is an expensive way to do things, in terms of memory, and, like you, I am concerned that values are getting overwritten because the attiny is running out of memory.

Is it important that it should be the median? Would the mean value be just as good? That is less expensive to calculate, in memory terms. It can be calculated without copying or sorting the original values.

What distribution of values are you expecting, where the mean would be significantly different to the median?

This is what is using up most of the attiny's memory.

I think I can see a way to reduce this by half, while still finding the median value, if that is truly important.

These 2 arrays hold int values which require 2 bytes to store them. But once the median value has been found, another int value is calculated by senderToGauge() which is given to analogWrite(). That value can't be higher than 255, so could be stored as a single byte.

So my idea is to use senderToGauge() to calculate the single byte each time the pot is read. Then, the two arrays can be byte instead of int and so require half the memory.

Thank you, but median is an absolute requirement. The arrays are in stack not heap, the compiler should be accounting for them... changing the size of that array changes the size of the program. There is memory to spare in that regard. When I said out of bounds memory access, I meant exactly that and NOT running out of memory. I have purposefully made the arrays bigger to use more memory and indeed the compiler throws an error about not having enough memory.

I would expect even a single function call to reset the program if its stack used up the rest of the memory? But unsure.

No, the compiler can't take into account whatever is allocated at run time on the stack...

Same space, and no, the compiler will not avoid conflicts.

I see. Shows what I know in that regard. That being said, if I change the [80], the compiler changes the memory usage, so it seems to at least know how much memory those variables take?

Yes - when you do

the allocated RAM memory is neither on the stack nor heap but in data segment the compiler builds for global/static variables.

Great, so as long as I am not using that up, which the compiler knows here, I should not be running out of memory. Is that a correct assessment?

Thusly, I don't see the need to try to optimize this out when total memory usage doesn't appear to be the issue.

It might come from your qsort() call. Eventhough it doesn’t perform dynamic heap allocation, it can still cause stack overflows due to recursion depth and local variable use within those recursive calls.

Do you see the issue if you reduce the array size to 40 or 20 ?

the source code for qsort() is available in GitHub

This qsort implementation uses limited recursion on one side of the partition (and a nice goto for the other side!), which can still be enough to overflow the tiny ATTiny85 stack in worst-case conditions.

It seems it uses insertion sort for arrays smaller than 7 elements so if you go for 7 datapoints you might be on the safe side.