Need help with AVR C integer overflow behaviour - Page 1
Forum section: electronics / general

author: Lomax
author_rank: Frequent Contributor
timestamp: September 17, 2023, 03:20:37 pm
content: Hi all,Although this is not my first attempt at writing C for an ATMega MCU I am stumped by the odd behaviour I see when trying to map a signed 16-bit int containing degrees of rotation (-1 to 359) to an 8-bit integer (0 to 255), using linear interpolation:Code: (c)#include <avr/io.h>volatile int16_t angle;volatile uint8_t duty;int rotate(int degrees) {angle = (angle + degrees) % 360;if (angle < 0) angle += 360;return (angle * 255) / 360;}int main(void) {while(1) {duty = rotate(1);}}If I watchangleanddutyeverything seems fine untilanglereaches 129, wheredutysuddenly jumps to 166:As you can see, I'm forcing angle to positive before the mapping. I'm sure this is caused by something simple, and probably a fundamental misunderstanding on my part, but what?

author: ataradov
author_rank: Super Contributor
timestamp: September 17, 2023, 03:42:03 pm
content: angle multiplication overflows in the return statement. Something like this should help "return ((int32_t)angle * 255) / 360;"

author: Lomax
author_rank: Frequent Contributor
timestamp: September 17, 2023, 05:03:39 pm
content: Quote from: ataradov on September 17, 2023, 03:42:03 pmDoh! 129 x 255 > 32,767Declaringangleasint32_tfixes the problem.

author: SiliconWizard
author_rank: Super Contributor
timestamp: September 17, 2023, 08:15:25 pm
content: Quote from: Lomax on September 17, 2023, 05:03:39 pmBut it's wasteful as you don't need to store the angle on 32 bits. That wouldn't matter much on a 32-bit MCU, but you're dealing with a 8-bit MCU here.So I would favor the solution ataradov suggested, with the cast just for the multiplication. C arithmetic rules are not trivial and I recommend learning them (it's a trap for many).

author: Infraviolet
author_rank: Super Contributor
timestamp: September 17, 2023, 10:08:09 pm
content: As you're dealing with angles, I'll throw in a little warning here. Remember that 360 wraps round to zero, it seems obvious but it gives some troublesome behaviour if you try to average or low pass filter angles which are separated across that discontinuity. You need to take sin and co first, process the, then recombine using atan2 to get the angle back.It can cause an MCU to waste more RAM and more instructions to an operation than ideal, but if you write as(angle * 255.5 ) / 360.0the compiler will do this with float mathematics, then return the result as the type of integer you want.You could tryfloat Tempvariable=(angle * 255.0) / 360.0;return (uint8_t)Tempvariable;Note that rotate() might as well return a uint8_t rather than an int, as your value is always in the 0 to 255 range.

author: brucehoult
author_rank: Super Contributor
timestamp: September 17, 2023, 10:46:36 pm
content: 360 * 255 = 91800, so your multiplication result can exceed what an "int" can store by a factor of 2.8.You could use 32 bit arithmetic but simply dividing both 255 and 360 by 5 would give the same effect and be a lot faster.(angle * 51) / 72Or, for that matter...(angle * 17) / 24

author: T3sl4co1l
author_rank: Super Contributor
timestamp: September 17, 2023, 11:41:27 pm
content: It's not clear what you're doing, if this applies, but notice an unsigned byte 2s-complement is modular in 256, or a short out of 65k, so can be used to represent angles without any additional logic.(Note also that short, int, etc. sizes are minimums, and their actual size is platform dependent; use uint16_t, etc. where this matters. Or add otherwise-redundant masking (unsigned short foo = (expression) & 0xffffwill be optimized out on most platforms anyway) to guarantee the desired result. Even byte is minimum IIRC, though there are very few platforms indeed where the byte/word size isn't 8 bits, aside from very old platforms.)Most things you'd use angles for, you can tweak to use fixed point fractions of a circle. Polynomial approximation of sin/cos for example. Or use the bits directly in a CORDIC, etc..Also, avoid using % and /, they're very slow, particularly for variable arguments (that can't be streamlined by the compiler, but IIRC integer division isn't optimized anyway?, and modulo not at all*). Not that this necessarily matters for whatever you're doing right now -- but keep an eye out for low-hanging optimizations like this when it does matter.*You can craft a multiplication method for division (with remainder) by a constant, but I don't think GCC uses it. Div/mod by powers of 2 generallyareoptimized to the respective logical operations.Tim

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 18, 2023, 12:44:27 am
content: ((int32_t)angle * 255) / 360is wrong, because it maps 0..359 to 0..254.The correct formula is(value*out_range) /in_range, i.e. 256/360 = 32/45, when rounding towards zero.Rounding halfway away from zero, you can use(value*out_range+half) /in_range, wherehalfis+in_range/2ifvalueis positive, and-in_range/2ifvalueis negative. (Either one is fine whenvalueis zero.)If you want the same results as((int)(roundf(angle*32.0f/45.0f)))&255, tryCode:uint8_t degrees_to_byte(int16_t deg){uint32_t pdeg = (deg < 0) ? (uint32_t)((int32_t)deg + 33120) : deg;return ((pdeg * 32 + 22) / 45) & 255;}which works correctly for all angles between -32768 and +32767 degrees, the full range anint16_tin C can represent. For negative angles, we add the smallest multiple of 360 not smaller than 32768, to make sure the angle is positive: 33120. Then, we cast it to unsigned 32-bit number. (Because of C integer promotion rules, the casts in thepdeginitialization are not necessary, and(deg < 0) ? deg + 33120 : deg;would do the exact same thing.)Thus,pdegcan range from 0 to 33119, inclusive.Since 256/360 = 32/45, we multiplypdegby 32, add half of 45 for rounding (+22), and do an integer division by 45. Note that the value being divided is always between 22 and 1059830, inclusive, ie. a 21-bit positive integer.As 256/360 = 32/45 = 0.7111..., we could open-code the multiplication and division into a fixed-point multiplication by 32/45, since division is quite slow on AVRs. Because (32×2¹⁹)/45 = 372827.0222... –– 19 being the smallest power of two that yields exactly correct values here ––, this yields us the dense but efficient formCode:uint8_t degrees_to_byte(int16_t deg){return ((((uint32_t)((int32_t)deg * 372827) >> 16) + 4) >> 3) & 255;}For negativedeg, the multiplication will overflow, but we're not interested in the high bits anyway (part of full turns in the angle), so we can simply cast the result to unsigned 32-bit integer so we retain all significant bits of the result (and do not actually care about the most significant bit, whether it is correct result bit or copied from the sign bit). Again, we rely here on the fact that with these integer types, negative values use two's complement format, which allows us to do this "yeah, we don't care about those highest bits at all, really" trick. On AVR, this is a simple int-int multiplication, implemented by the__mulsi3()avr-libc function or equivalent, so this multiplication is quite fast.After the multiplication, we drop the least significant 16 bits, because on AVR this is just two register moves. Since we have a division by 8 left, we add the +4 for rounding (with half away from zero), and do the three bit shifts, and finally use only the 8 least significant bits left.Both above forms return the exact same value as((int)(roundf((float)deg*32.0f/45.0f))) & 255for all possibleint16_tvalues ofdeg, -32768 to +32767, inclusive. I verified this with a test program, using x86-64 double-precision floating point (i.e.((int)(round(deg*32.0/45.0)))&255to test all possible values ofdeg).

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 18, 2023, 02:08:45 am
content: If OP maps 0..360 to 0..255, 360 will correspond to 255 and not 256, and 180 will correspond to 127.5 and not 128 as one would expect for cyclical 8-bit angle units.When linearly mappingin_min ≤input≤ in_maxtoout_min ≤output≤ out_maxusing the standard formula,output= out_min + (input- in_min) * (out_max - out_min) / (in_max - in_min)the input and output ranges are split into (min(in_max - in_min + 1, out_max - out_min + 1)) equal-width bins, andin_minandout_mincorrespond to the minimum values in the first bin.The error many programmers make is believing thatin_maxandout_maxcorrespond to the maximum values in the last bin.This is not so; they too correspond to the minimum values in the last bin!If we use the rounding variant,output= out_min + ( (input- in_min) * (out_max - out_min) + in_half ) / (in_max - in_min)wherein_half = (in_max - in_min) / 2, thenin_minandout_mincorrespond to the center value in the first bin, andout_minandout_maxcorrespond to the center value in the last bin. Thus, first and last bins have technically half the width of the rest of the bins.If we usein_firstandout_firstfor the first values in the first bin, andin_lastandout_lastfor thelastvalues in the last bin, then we need to useoutput= out_first + (input- in_first) * (out_last - out_first + 1) / (in_last - in_first + 1)because we are mapping(in_last - in_first + 1)different input values to(out_last - out_first + 1)different output values.For cyclical values, we want the first and last bins to have just half the width of the other bins (so that the "zero" bin is not double-wide when we consider values that wrap around our cyclical range), and thus need to use the rounding variant,output= out_first + ( (input- in_first) * (out_last - out_first + 1) + in_half) / (in_last - in_first + 1)wherein_half = (in_last - in_first + 1) / 2. This wayin_firstandout_firstcorrespond to themiddlevalues in the first bin, andin_lastandout_lastto the middle values in the last bin.This works, and you can use the above for both integer and floating-point mapping. Just make sure the multiplicands either have sufficient range, or at least one of them is cast to a type having sufficient range, for the product.When implemented using integer arithmetic, the ones within_halfabove round halfway towards last/max. Cround(),roundf(), androundl()should use the same logic, but some implementations round exact halfway towards odd or even.

author: Lomax
author_rank: Frequent Contributor
timestamp: September 18, 2023, 08:43:02 am
content: Crikey, that opened a can of wormsI'm sure you all know what it's like when you're staring at something that should be obvious but your brain is stuck at the wrong angle. The AVR and Atmel C is just one of many tools in my kit and not one I reach for very often, so please don't be too hard on me. I understand well the inefficiency in using unnecessarily large integers, performing floating point operations, etc, but efficiency is not important at this stage. I'm trying to come up with a way to convert a numerical angle (0-359) into analogue sine/cosine voltages (by means of R/C filtering), and the snippet I posted was just a stepping stone on that path - the 0-359 to 0-255 mapping was a temporary test to get a linear PWM duty cycle representing the angle and it's long gone. Therotatefunction now looks like this, and angle is defined as anint16_t:Code:void rotate(int degrees) {angle = (angle + degrees) % 360;if (angle < 0) angle += 360;radian = angle * (M_PI / 180.0);sine = (sin(radian) + 1) * 127;cosine = (cos(radian) + 1) * 127;OCR1A = sine;OCR1B = cosine;}I'm sure there are many more n00b errors in this - my trig is even worse than my C - but it seems to do what I want and loads the AVR ~15% when running at 2 MHz. It would be interesting to hear what my errors are if/how the code could be further optimised.Edit:My current test simulation looks like this:The input angle will eventually come over the serial port, so the rotation and associated modulo operation will not be used. It's only there so I can do proof-of concept testing using the encoder input. Final code might look something like this:Code:void output(int angle) {float radian = angle * (M_PI / 180.0);uint8_t sine = (sin(radian) + 1) * 127;uint8_t cosine = (cos(radian) + 1) * 127;OCR1A = sine;OCR1B = cosine;}

author: ledtester
author_rank: Super Contributor
timestamp: September 18, 2023, 09:21:37 am
content: Quote from: Lomax on September 18, 2023, 08:43:02 amA lookup table for the sine and cosine values would be a lot faster than calling sin() and cos().This project write-up shows how to store such a lookup table in flash memory on an Arduino Uno so it doesn't take up space in RAM:https://home.csulb.edu/~hill/ee470/Lab%202d%20-%20Sine_Wave_Generator.pdfThe key points are:- include <avr/pgmspace.h>- use the PROGMEM directive, e.g.:const uint8_t sinewave[] PROGMEM= { ... }- use the pgm_read_byte() function to access the array, e.g.:OCR1A=pgm_read_byte(&sinewave);The cosine lookup can also use the sine table by just shifting the input by 90 degrees:OCR1B=pgm_read_byte(&sinewave[(450-angle)%360])(Noteangleshould be between 0 and 359.)

author: Doctorandus_P
author_rank: Super Contributor
timestamp: September 18, 2023, 11:48:58 am
content: Quote from: Nominal Animal on September 18, 2023, 02:08:45 amIf you want to map a circle into a byte, then you should not map 360 to 255.For a circle, 360 degrees is a full turn, so it maps back to zero.

author: Lomax
author_rank: Frequent Contributor
timestamp: September 18, 2023, 12:03:24 pm
content: Quote from: Doctorandus_P on September 18, 2023, 11:48:58 amQuote from: Lomax on September 18, 2023, 08:43:02 am

author: Doctorandus_P
author_rank: Super Contributor
timestamp: September 18, 2023, 04:08:10 pm
content: Quote from: Lomax on September 18, 2023, 12:03:24 pmThat would make the overflow from 255 to 0 the same as an overflow from 359 to zero. The difference is small, and depending on the allowed rounding errors it may or may not be acceptable.The correct way is to map 360 to 256. Note that on the circumference of a circle there are 360 equal distances when divided in degrees, and 256 distances when divided over a byte.

author: Infraviolet
author_rank: Super Contributor
timestamp: September 19, 2023, 02:51:34 pm
content: "convert a numerical angle (0-359) into analogue sine/cosine voltages"Why?In a lot of circumstances it would be easier to measure the angle, hold it as a digital value then pass it over serial or something, as digital numerical data, to whatever requires the angle reading. Calculating sine and cos components is pretty simple, but properly generating analog voltages from an atmega328p is HARD, if you try it with PWM and RC filtering your output levels will fluctuate whenever a timing tick or other interrupt fires and makes a PWM pulse come late or last a little longer than normal. If you really need to generate voltages you'd likely do better to have a proper DAC as an I2C slave to the 328p. Is there an analogue thing you are trying to control from cos and sine voltages, explain what and a simpler way might be possible.

author: peter-h
author_rank: Super Contributor
timestamp: September 19, 2023, 05:15:32 pm
content: I know of some avionics (my hobby etc) products which map 0-359 to 0-255 and the result is horrible! You get various weird effects, not to mention accumulating errors of 1-2 degrees which create havoc down the line.But if you are just feeding the voltage into an 8 bit DAC to generate a sinewave (the X/Y/400Hz "synchro" system used in avionics) fair enough. It just won't be quite accurate and people will notice that. Same if you are doing LVDT emulation (done that too, but with 12 bit DACs).PWM is OK if it runs at a proper high speed so the lowpass filtering doesn't cause problems.Honestly, in 2023, I would not waste my time with these cheap chips unless the product is ultra price sensitive. I am using a 32F417 which runs at 168MHz and you can just do the whole lot with floats. It does a float mult in 1 cycle (7ns). An "int" is 32 bits and of course everything also takes 7ns. The chip costs 10 quid in low qtys, 5 quid 1k+. You can do waveform synthesis in hardware on it using timers, dma, dacs, so no software actually running to make it all work, after the set-up.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: September 19, 2023, 11:19:12 pm
content: Or even if you're tied to the AVR ecosystem for...reasons, the newer e.g. AVR-DA and etc. have a number of compelling features that keep them... if not competitive, then generally useful at least. There are significant differences in hardware, of course.Working in C, there's not really a huge deal going between mainstream MCUs, at least if you write in a reasonably portable manner (this does take some experience, granted). STM32s and ATSAMs are quite popular, competitive, and very powerful in comparison to the old ATMEGAs.Tim

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 20, 2023, 02:49:54 am
content: Never underestimate the power ofvector algebra.For example, if you have a 2D unit vector \$(x, y)\$ (unit meaning length is 1, \$x^2 + y^2 = 1^2 = 1\$), then \$x = \cos \varphi\$ and \$y = \sin \varphi\$ where \$\varphi\$ is the angle, positive counter-clockwise, the vector makes with the positive \$x\$ axis.To rotate any 2D vector \$(u, v)\$ by angle \$\varphi\$ represented by unit vector \$(x, y)\$, you do$$\begin{aligned}u^\prime &= u x - v y \\v^\prime &= v x + u y \\\end{aligned}$$If a vector \$(x^\prime, y^\prime)\$ should be an unit vector –– rounding errors tend to creep in, causing a bit of shrinking/expansion ––, it can always benormalizedto an unit vector \$(x, y)\$ via$$\begin{aligned}L^\prime &= \sqrt{ {x^\prime}^2 + {y^\prime}^2 } \\x &= \frac{x^\prime}{L^\prime} \\y &= \frac{y^\prime}{L^\prime} \\\end{aligned}$$Since division tends to be much slower than multiplication, often the division by square root is replaced by multiplication with the reciprocal square root, \$1 / \sqrt{{x^\prime}^2 + {y^\prime}^2}\$. This may sound like a slow operation, but there are tricks that one can use to speed it up significantly, even when using say fixed-point numbers with no hardware square root or division operations. One is to first calculate \${L^\prime}^2\$. If it is less than one half, multiply it by four, and double both \$x^\prime\$ and \$y^\prime\$; repeat until it it is at least one half. If it is two or greater, divide it by four, and halve both \$x^\prime\$ and \$y^\prime\$. This simplifies to finding the reciprocal of square root of \$z\$, with \$1/2 \le z \lt 2\$. As \$1/\sqrt{z} = \sqrt{1/z}\$ is a nice smooth monotonically decreasing curve, one can use a binary search, a polynomial approximation, or even Newton-Raphson iteration via \$r \gets r ( 3 - z r^2 ) / 2\$. Binary search will give one additional bit of precision per iteration but requires fewer elementary operations per Newton-Raphson iteration, but Newton-Raphson converges faster requiring fewer iterations (after the first one or two) to reach the same precision.If you use a polynomial approximation for the first sine octant, you can construct an unit vector given only the angle. The above normalization will be useful then too, to minimise the approximation error; then, the approximation error shows up as "noise" in the angle parameter \$\varphi\$, but not in the unit vector length per se. That is, the sine-cosine pari constructed thus will have \$\sin^2 \varphi + \cos^2 \varphi = 1\$.With this unit vector approach, if you don't have hardware floating point or division operations, sooner or later you will end up withCORDIC: an algorithm to calculate trigonometric functions with bit shifts, additions, multiplications, and some look-up tables.With the above, and using quaternions or bivectors for representing 3D orientation and rotations, even an 8-bit AVR has easily enough computational power to do real-time 3D position and orientation tracking with sensor fusion from accelerometers and magnetic field sensors using standard software floating-point operations; much more so if using fixed-point arithmetic with 8, 16, 24, or 32 fractional bits (and 7 integral bits, if using unit vectors and unit quaternions/bivectors) and extended inline assembly implementations for their efficient multiplication.

author: Siwastaja
author_rank: Super Contributor
timestamp: September 20, 2023, 05:34:09 am
content: Quote from: peter-h on September 19, 2023, 05:15:32 pmWell, quite obviously, if 8 bits of angular resolution is not enough, then it's not enough. The idea of using self-wrapping numbers to represent angles is sound, though, not only because of the performance, but also not having to write wrapping code (which can go wrong). If 8 bits is not enough, and especially if one is worried about accumulating errors, why not use 32 bits to represent the full circle. You can easily mask any number of bits (e.g., if you decide to use a 1024-element sine LUT, then just use the 10 highest bit of the angle), and you actually havesignificantly moreresolution than a 32-bit float, with a huge performance gain over floating point implementation (which also has to range-check and wrap everywhere).

author: Lomax
author_rank: Frequent Contributor
timestamp: September 23, 2023, 11:04:41 am
content: Quote from: Infraviolet on September 19, 2023, 02:51:34 pmBecause I want to drive an analogue dial instrument that shows wind direction. The instrument has sin/cos inputs. My wind direction sensor only has a resolution of 16 steps, which I read over a 1-Wire bus, so required accuracy is not demanding; I just want a rough indication of the direction the wind is blowing. Appreciate the advanced discussion though!

author: mikerj
author_rank: Super Contributor
timestamp: September 23, 2023, 11:59:09 am
content: For only 16 positions a table to lookup the sin and cos values would be practical (and very fast).

author: Nominal Animal
author_rank: Super Contributor
timestamp: September 23, 2023, 12:12:20 pm
content: Quote from: Lomax on September 23, 2023, 11:04:41 amThe point is you are complicating your task by unnecessarily using angular degrees.If you used 0-255 instead – with say 0 East, 64 North, 128 West, 192 South – you would simplify your code, because(direction& 255)correctly maps 256 to 1, -1 to 255, -128 to 128, -64 to 192, and so on.To convert from 0..15 or -8..7 wind direction, just multiply by 16 and bitwise-and with 255, or use a look-up table for the 16 wind directions.As to generating the sin/cos outputs for the indicator, the exact indicator inputs matter a lot: are the inputs voltage or current? What voltage or current range they use?Most microcontrollers only support I/O voltages between 0 and Vcc. With four control pins – two differential pairs – you could use four output pins, as two pairs, with one of each pair always being ground, and the other pin used for either full output, or PWM'd and low-pass filtered for a lower voltage.If the current draw of the indicator is higher than say 10mA, then you might use a dual full bridge (with optional low-pass filters).For easier control of the indicator, you might use a four-channel DAC that can source and sink sufficient current.The simplest direction indicator might be just a magnetic needle with four coils, wired in north-south and east-west pairs, inside a magnetically shielded enclosure (so that external magnetic fields do not affect the indicator reading).

author: Lomax
author_rank: Frequent Contributor
timestamp: September 23, 2023, 01:12:29 pm
content: My original question, which I know was pretty stupid, has been answered. I did not see the need to provide every other detail about what I'm trying to do, but since many point out the performance downside of floating point math calculation of the sin/cos values I guess I'll have to clarify things a little.First of all, I know and understand how a lookup table would work, and why it would be a lot less CPU intensive to use one, particularly with only 16 cardinal directions.But I have no need for this performance boost, because the update frequency will only be 1 Hz. The AVR will only have to perform the sin/cos calculationsonce a second. I would gainnothingby switching to a lookup table.Secondly, just because my current wind sensor only has 16 directions that does not exclude the possibility that a future replacement may provide far higher resolution. I do not want to restrict myself to any arbitrary resolution - though 255 steps per revolution seems plenty, particularly when the internal damping algorithm of the instrument is taken into account. Should this prove unsatisfactory in practice it ought to be trivial to switch to a 16-bit resolution for the PWM. But I am pretty sure this will not be needed.I am basically trying to convert an old analogue wind instrument to a (slightly less outdated) NMEA 0183 wind instrument. Because I prefer its appearance over modern digital instruments. Specifically, the AVR will be listening to theMWVNMEA 0183 sentence, transmitted to it once per second over RS422/385:1 2 3 4 5 6| | | | | |$IIMWV,x.x,a,x.x,a*hh<CR><LF>Wind Angle, 0 to 359 degreesReference, R = Relative, T = TrueWind SpeedWind Speed Units, K/M/NStatus, A = Data Valid, V = InvalidChecksumFor example:$IIMWV,214.8,R,5.1,K,A*33When a message arrives (once per second) and triggers the USART interrupt the AVR should parse the NMEA sentence and update the sin/cos voltages representing wind direction, as well as the frequency of a square wave representing wind speed (~0-4 kHZ IIRC) with the received values. I think this should be well within the capabilities of an ATMega328, even clocked at 2 MHz. In fact, some quick checking reveals the following number of clock cycles spent on each of the calculations:radian = angle * (M_PI / 180.0);227sine = (sin(radian) + 1) * 127;2086cosine = (cos(radian) + 1) * 127;2005That's a total of 4318 clock cycles spent calculating the sin/cos values. While this might seem excessive, at 2 MHz the AVR could perform these calculations 463 times per second (provided it did nothing else). Or in other words, with a 1 Hz update rate these calculations use less than 0.25% of the available time on a 2 MHz AVR.