cmicrocontrolleravr

Return value not as expected. AVR C calculation


I am trying to perform the following calculation using an ATmega328P MCU.

๐‘๐‘œ๐‘ ๐‘–๐‘ก๐‘–๐‘œ๐‘› = 1000 ยท ๐‘‰๐‘Ž๐‘™0 + 2000 ยท ๐‘‰๐‘Ž๐‘™1 + โ‹ฏ + 8000 ยท ๐‘‰๐‘Ž๐‘™7 / ๐‘‰๐‘Ž๐‘™0+๐‘‰๐‘Ž๐‘™1+โ‹ฏ+๐‘‰๐‘Ž๐‘™7

In the main routine (as shown here):

int main(void)
{
    //variables
    uint16_t raw_values[8];
    uint16_t position = 0;
    uint16_t positions[8];
    char raw[] = " raw";
    char space[] = ", ";
    char channelString[] = "Channel#: ";
    char positionString[] = "Position: ";

    //initialize ADC (Analog)
    initADC();

    //initialize UART
    initUART(BAUD, DOUBLE_SPEED);

    //give time for ADC to perform & finish 1st conversion
    //8us x 25 = 200us
    delay_us(200);

    while(1)
    {
        //get the raw values from the ADC for each channel
        for(uint8_t channel = 0; channel < 8; channel++)
        {
            raw_values[channel] = analog(channel);

            //invert the raw value
            raw_values[channel] = DIVISOR - raw_values[channel];
        }

        for(uint8_t channel = 0; channel < 8; channel++)
        {
            //print the channel#
            transmitString(channelString);
            printDec16bit(channel);
            transmitString(space);

            //print the raw value from the ADC conversion
            printDec16bit(raw_values[channel]);
            transmitString(raw);
            transmitString(space);

            //calculate the position value at each sensor
            transmitString(positionString);
            positions[channel] = (uint16_t)((POSITION_REF/DIVISOR) * raw_values[channel]);
            printDec16bit(positions[channel]);
            printCR();
        }

        printCR();

        //calculate and display 'position'
        position = calculatePosition(positions);
        printDec16bit(position);
        printCR();
        printCR();


        //add a delay
        delay_ms(2000);
    }
}

I am calling the following function, but the return value I am getting is way off.

uint16_t calculatePosition(uint16_t* channel_positions)
{
    uint32_t intermediates[8];
    uint32_t temp_sum = 0;
    uint16_t divisor = 0;
    uint16_t value = 0;

    for(uint8_t i = 0; i < 8; i++)
    {
        intermediates[i] = channel_positions[i] * ((i + 1) * 1000);
    }

    for(uint8_t j = 0; j < 8; j++)
    {
        temp_sum = temp_sum + intermediates[j];
    }

    for(uint8_t k = 0; k < 8; k++)
    {
        divisor = divisor + channel_positions[k];
    }

    value = temp_sum/divisor;

    return value;
}

Alternatively, I have even tried this code, and get a result that is not what I expect.

uint16_t calculatePosition(uint16_t* channel_positions)
{
    uint16_t position;
    position = ((1000 * channel_positions[0]) + 
                (2000 * channel_positions[1]) + 
                (3000 * channel_positions[2]) + 
                (4000 * channel_positions[3]) + 
                (5000 * channel_positions[4]) + 
                (6000 * channel_positions[5]) + 
                (7000 * channel_positions[6]) + 
                (8000 * channel_positions[7])) /
                (channel_positions[0] + 
                 channel_positions[1] + 
                 channel_positions[2] + 
                 channel_positions[3] + 
                 channel_positions[4] + 
                 channel_positions[5] + 
                 channel_positions[6] + 
                 channel_positions[7]);
    return position;
}

What could I be doing wrong? For an array of values such as {15, 12, 5, 16, 11, 35, 964, 76} I expect a result of 6504, but instead I get a value in the 200's (or some other weird value).


Solution

  • Look at your input array: {15, 12, 5, 16, 11, 35, 964, 76}

    Specifically, look at the element that is 964. That element times 7000 is 6748000 which is greater than a uint16_t can handle.

    There are a number of solutions. One of them is changing to uint32_t. If this is not an option, you could extract a factor of 1000, like this:

    position = 1000 *(
                ((1 * channel_positions[0]) + 
                (2 * channel_positions[1]) + 
                (3 * channel_positions[2]) + 
                (4 * channel_positions[3]) + 
                (5 * channel_positions[4]) + 
                (6 * channel_positions[5]) + 
                (7 * channel_positions[6]) + 
                (8 * channel_positions[7])) /
                (channel_positions[0] + 
                 channel_positions[1] + 
                 channel_positions[2] + 
                 channel_positions[3] + 
                 channel_positions[4] + 
                 channel_positions[5] + 
                 channel_positions[6] + 
                 channel_positions[7]));
    

    Note that this will not eliminate the problem, but it could possibly reduce it so that the problem never occurs for reasonable input.

    Taking the same idea to the loop version, we get:

    uint16_t calculatePosition(uint16_t* channel_positions)
    {
        uint16_t temp_sum = 0;
        uint16_t divisor = 0;
    
        for(uint8_t i = 0; i < 8; i++) {
            temp_sum  += (channel_positions[i] * (i+1));
            divisor += channel_positions[i];
        }
    
        return 1000*(temp_sum/divisor);
    }
    

    Note that you will lose some accuracy in the process due to rounding with integer division. Since you have been very careful with specifying the width, I assume you're not willing to change the type of the input array. This code should give you maximum accuracy with minimal extra memory usage. But if you're running this function often on a 16-bit machine it can impact performance quite a bit.

    uint16_t calculatePosition(uint16_t* channel_positions)
    {
        // Use 32 bit for these
        uint32_t temp_sum = 0;
        uint32_t divisor = 0;
    
        for(uint8_t i = 0; i < 8; i++) {
            // Copy the value to a 32 bit number
            uint32_t temp_pos = channel_positions[i];
            temp_sum  += temp_pos * (i+1);
            divisor += temp_pos;
        }
    
        // Moved parenthesis for better accuracy
        return (1000*temp_sum) / divisor;
    }
    

    Provided that the result can fit in a uint16_t there is absolutely zero chance that this version will fail, because the biggest possible value for 1000*temp_sum is 2,359,260,000 and the biggest value it can hold is 4,294,967,295.

    Sidenote about MRE (minimal, reproducible example)

    In this example, a good main function to post in the question would be:

    #include <stdio.h>
    
    int main() 
    { 
        uint16_t positions[] = {15, 12, 5, 16, 11, 35, 964, 76}; 
        uint16_t pos = calculatePosition(positions); 
        printf("%d\n", pos); 
    }
    

    It's enough to demonstrate the problem you had and no more.