Extracting individual digits of a multi digit decimal - Page 1
Forum section: electronics / general

author: Dan Moos
author_rank: Frequent Contributor
timestamp: November 07, 2016, 01:23:18 am
content: I want to control a multi digit seven segment display. The numbers 0 to 9 are easy. It's when I get into numbers requiring multiple digits that I'm needing help.I don't want a simple sequential count. That too would be easy. I want to take an arbitrary 4 digit number, and display it.Seems to me some method of extracting the individual digits is in order. I considered converting the number to BCD, and going from there, but I think that might be an unnecessary step.I imagine I want to do something along the lines of successively dividing by ten and taking what's on the left of the decimal point. Basically like a bitwise shift, but with decimals.Am I on the right track?

author: amyk
author_rank: Super Contributor
timestamp: November 07, 2016, 01:33:45 am
content: Yes. Repeatedly divide by 10 and the remainders will be the digits.That assumes the platform you're using has a divide instruction, or doing it in software (shift+subtract) is not too slow. Otherwise there are other more complex methods.

author: Dan Moos
author_rank: Frequent Contributor
timestamp: November 07, 2016, 02:07:04 am
content: Ok. So I'm on an 8 bit avr running at 8 mHz. My project isn't very computationally intensive though.So basically I want a modulus function rather than an actual divide, right? I strongly suspect my hardware supports neither, so I'm curious what clever software methods there are ( besides just doing plain divide or modulus). You mentioned some sort of shift and subtract. Care to elaborate? I'm pretty uneducated when it comes to clever math tricks on micros.

author: IanB
author_rank: Super Contributor
timestamp: November 07, 2016, 02:39:49 am
content: You have a four digit decimal number in the range 0 to 9999?The brute force approach is to break it down digit by digit.First find the thousands digit. Repeatedly subtract 1000 until the number goes negative and then add back the last one. That's your first digit.Now repeatedly subtract 100 until the number goes negative and add back the last one. That gives the hundreds digit.Follow the same process for the tens and units digits. At worst you will have made 40 subtractions.If you are really keen you could try to optimize it using powers of two, but it may not really be worth it.

author: rstofer
author_rank: Super Contributor
timestamp: November 07, 2016, 02:42:40 am
content: Google is full of BCD to binary conversion algorithms. Here's a pretty neat onehttp://www.eng.utah.edu/~nmcdonal/Tutorials/BCDTutorial/BCDConversion.htmlThis has the advantage of only using shifts, no multiplication or division. It is also for an 8 bit binary but it probably works for larger values. You need to see how it responds to signed char as the example is using unsigned char. If it doesn't work for signed char (or signed short), I would save the sign and convert to a positive value. You would do that anyway for display.The C function is itoa() - integer to ASCII in this case.

author: GK
author_rank: Super Contributor
timestamp: November 07, 2016, 02:55:27 am
content: Sometimes efficiency isn't a priority and you just want the code function to be easy to interpret.Code:yh = three_digit_decimal_number / 100;yt = three_digit_decimal_number - (yh * 100);yu = yt - 10 * (yt / 10);yt = yt / 10;

author: Dan Moos
author_rank: Frequent Contributor
timestamp: November 08, 2016, 01:08:27 am
content: So GK's method honestly is probably the way I'll go since I think I can spare the cycles, but...How does one go about figuring out how much a particular arithmetic operation costs? I don't believe I have a profiler (running Atmel Studio with an avr Dragon using debug Wire.

author: IanB
author_rank: Super Contributor
timestamp: November 08, 2016, 01:26:20 am
content: Quote from: Dan Moos on November 08, 2016, 01:08:27 amNormally you should find in the data sheet a table of processor instructions and the number of CPU cycles required for each. You can look at the generated assembly and add them up.

author: Someone
author_rank: Super Contributor
timestamp: November 08, 2016, 01:51:23 am
content: Quote from: Dan Moos on November 08, 2016, 01:08:27 amAtmel studio does include debugging and simulation tools to do this, you typically add breakpoints at the entry/exit and check the elapsed time for a series of different values. The documentation is top notch:http://www.atmel.com/webdoc/atmelstudio/atmelstudio.Debug.Views.ProcessorView.html

author: snarkysparky
author_rank: Frequent Contributor
timestamp: November 08, 2016, 01:04:50 pm
content: void MakeDigits(unsigned int inval){DigVals = 0x000a; DigVals = 0x000a; DigVals=0x000a; DigVals = 0x0000; // clear digitsif(inval > 9999)inval = 9999; // max displayif(inval > 999 ) // extract thousands digit{if(inval > 4999) //{if(inval > 8999){DigVals = 9;inval = inval - 9000;}else if(inval > 7999){DigVals = 8;inval = inval - 8000;}else if(inval > 6999){DigVals = 7;inval = inval - 7000;}else if(inval > 5999){DigVals = 6;inval = inval - 6000;}else{DigVals = 5;inval = inval - 5000;}} // if(inval > 4999)else // inval between 4999 and 1000{if(inval > 3999) // 4000 - 4999{DigVals = 4;inval = inval - 4000;}else if(inval > 2999) // 3000 - 3999{DigVals = 3;inval = inval - 3000;}else if(inval > 1999) // 2000 - 2999{DigVals = 2;inval = inval - 2000;}else // 1000 - 1999{DigVals = 1;inval = inval - 1000;}}DigVals = 0x0000; DigVals=0x0000; DigVals = 0x0000; // fill with zeros}// if(inval >999)if(inval > 99 ) // extract the hundreds digit{if(inval > 499) //{if(inval > 899){DigVals = 9;inval = inval - 900;}else if(inval > 799){DigVals = 8;inval = inval - 800;}else if(inval > 699){DigVals = 7;inval = inval - 700;}else if(inval > 599){DigVals = 6;inval = inval - 600;}else{DigVals = 5;inval = inval - 500;}} // if(inval > 499)else // inval between 499 and 100{if(inval > 399) // 400 - 499{DigVals = 4;inval = inval - 400;}else if(inval > 299) // 300 - 399{DigVals = 3;inval = inval - 300;}else if(inval > 199) // 200 - 299{DigVals = 2;inval = inval - 200;}else // 100 - 199{DigVals = 1;inval = inval - 100;}}DigVals=0x0000; DigVals = 0x0000; // fill tens and ones dec with zeros}// if(inval > 99)if( inval > 9 ) // get the tens digit{if(inval > 49) // 50 - 99{if(inval > 89) // 90 - 99{DigVals = 9;inval = inval - 90;}else if(inval > 79){DigVals = 8;inval = inval - 80;}else if(inval > 69){DigVals = 7;inval = inval - 70;}else if(inval > 59){DigVals = 6;inval = inval - 60;}else{DigVals = 5;inval = inval - 50;}}// if(inval > 49)else // 10 - 49{if(inval > 39) // 10 - 40{DigVals = 4;inval = inval - 40;}else if(inval > 29){DigVals = 3;inval = inval - 30;}else if(inval > 19){DigVals = 2;inval = inval - 20;}else{DigVals = 1;inval = inval - 10;}}}DigVals = inval; // at this point inval should be less than 10}

author: Kalvin
author_rank: Super Contributor
timestamp: November 08, 2016, 02:31:47 pm
content: Quote from: snarkysparky on November 08, 2016, 01:04:50 pmTotally awful!

author: T3sl4co1l
author_rank: Super Contributor
timestamp: November 08, 2016, 02:36:37 pm
content: For an example of an efficient approach, here's how the divide-by-10 might be implemented:http://www.seventransistorlabs.com/uDivTen.asm.txtNaturally, it's cryptic assembly, but hey, you asked.Long-hand division (not shown here) is done the same way you do long division by hand, with the simplification that, since the running remainder can only possibly have a one or a zero in the relevant place (conveniently arranged so it's the sign bit), the actual operation is a shift and conditional subtraction. On AVR, it takes about 220 CPU cycles.The above method is accurate for a constant divisor. It uses the precomputed reciprocal, and multiplies the dividend by it. The extra (fraction) bits are the remainder. Because AVR has a hardware multiply, this operation can be very fast, even though it's a roundabout method, conceptually.A compiler probably won't output a function like this, for constant division, but will use the long-hand function instead (built into a library). A compiler will use an in-line operation where the division is a power of 2, however: those reduce to a shift-right operation. You will often see programmers write "x = y / 16" as "x = y >> 4", explicitly.Completing an operation such as "sprintf("%n", var_is_uint16_t)", in standard C and libraries, should take on the order of several thousand clock cycles to finish. (There's also a lot of wasted cycles and code space, spent jumping over the very complicated parts of sprintf, if it's a fully C-compliant sprintf.)Tim

author: snarkysparky
author_rank: Frequent Contributor
timestamp: November 08, 2016, 02:49:41 pm
content: Quote from: Kalvin on November 08, 2016, 02:31:47 pmOw now it ain't that bad. I happen to like it, and am quite proud of it. My code frequently gets similar responses though. I would really appreciate an in depth critique from anyone who has the time. I will only reply once.thanks

author: Kalvin
author_rank: Super Contributor
timestamp: November 08, 2016, 02:55:21 pm
content: This thread has some solutions:https://www.eevblog.com/forum/microcontrollers/memory-efficient-int-to-chars-without-division-(bitshift-ok)-for-binary-bases/The thread has some requirements for the formatting and different bases, but removing the extra options will give a nice solution.

author: AndyC_772
author_rank: Super Contributor
timestamp: November 08, 2016, 03:06:00 pm
content: Quote from: snarkysparky on November 08, 2016, 02:49:41 pmI'm afraid I agree with Kalvin here, your solution could be a lot better in many different ways. Here's a few metrics which you might like to consider.- how much space does your solution take up?- how easy is it to see how it's meant to work?- how easy is it to see that it actuallydoeswork, without silly mistakes (such as typos) which could stop it from working under some conditions?- how fast is it? Has it been optimised to an appropriate degree given how often it'll be called, and the time constraints which might apply when it is called?- how easy is it to modify in order to, say, add additional digits, or support for negative numbers, or decimal places, or leading zero suppression, or... whatever?

author: newbrain
author_rank: Super Contributor
timestamp: November 08, 2016, 03:13:46 pm
content: Quote from: snarkysparky on November 08, 2016, 02:49:41 pm"Ogni scarrafone è bell'a mamma soja" (literally: every cockroach is beautiful to its mother").I will not go deep into the code, apart from the first and last line:- is there a reason to init some digits to 0x000A? That seems wrong. Also, is DigVals a global?- how's the value returned? (see above)And leave these two notes:- think of the poor guy that will see this "scarrafone" some months or years from now (and it could be you).- even worse, think of the tests needed to prove that this code works, with respect to the straightforward one.

author: snarkysparky
author_rank: Frequent Contributor
timestamp: November 08, 2016, 03:20:01 pm
content: - how much space does your solution take up?Well it has lots of int comparisons and int assignments. All of which are single cycle on most machines.- how easy is it to see how it's meant to work?It seems to me much easier to comprehend than some other solutions posted here. Custom assembler routines for division- how easy is it to see that it actually does work, without silly mistakes (such as typos) which could stop it from working under some conditions?See above answer.- how fast is it? Has it been optimised to an appropriate degree given how often it'll be called, and the time constraints which might apply when it is called?Without hardware division ( the situation posed by the OP ) I would ball park it to be 20X faster. Worst case is 21 integer comparisons and branches. Even on AVR theseare single cycle.- how easy is it to modify in order to, say, add additional digits, or support for negative numbers, or decimal places, or leading zero suppression, or... whatever?Not so easy I admit.Do note that IanB ( a supercontributor ) advocated this method. His code would have been a lot nicer I am sure.Given that this is how I would code the thing in the workplace ( with some cleanup ) should I give up on working as a programmer. I ask seriously because I usually really really really don't get the basis for complaints against my code. I have been thinking of this for some time. Fear not hurt feelings let me have it. ( I have other things I can do to put food on table )

author: snarkysparky
author_rank: Frequent Contributor
timestamp: November 08, 2016, 03:24:53 pm
content: Quote from: newbrain on November 08, 2016, 03:13:46 pm0x000a flags the digit as invalid if not modified. I know the global variable sucks and the lack of documentation. I accept your criticisms, this code I snipped from a home project thermocouple display. Still it isn't the most useless post in the thread. Thanks for the feedback.

author: FrankBuss
author_rank: Unknown
timestamp: November 08, 2016, 03:28:14 pm
content: Quote from: Dan Moos on November 08, 2016, 01:08:27 amWhat I do to measure the runtime of a function on hardware is usually to use a high resolution timer on the hardware. If this is not available, I set a GPIO high on start and low on end of the function, then measure it with an oscilloscope. Much easier than counting cycles. But be careful, it doesn't give you a worst case runtime, just the tests you are doing. But you could run all possible values for your case and turn on persistence on the scope.

author: rstofer
author_rank: Super Contributor
timestamp: November 08, 2016, 09:41:08 pm
content: Quote from: snarkysparky on November 08, 2016, 03:20:01 pmThe OP wants to do the conversion on an 8 bit AVR. Int comparisons are expensive.What I didn't see mentioned was the range of values that needed conversion.

author: IanB
author_rank: Super Contributor
timestamp: November 08, 2016, 09:50:23 pm
content: Quote from: rstofer on November 08, 2016, 09:41:08 pmFour digits, from 0000 to 9999.

author: Someone
author_rank: Super Contributor
timestamp: November 09, 2016, 12:49:05 am
content: Quote from: snarkysparky on November 08, 2016, 03:20:01 pmComparison might be single cycle, but conditional branching is most certainly not!

author: rstofer
author_rank: Super Contributor
timestamp: November 09, 2016, 01:16:57 am
content: Quote from: IanB on November 08, 2016, 09:50:23 pmSo the data is stored in two bytes... That's regrettable because even the shift algorithm gets ugly. I would still try to work with the shift algorithm I posted in Reply 4. Shifts are still faster than multiply and divide and probably faster than repeated add/subtract.I wonder if the data should have been carried in BCD from the beginning. That may not apply here but I have done it in the past. In fact, the 8080/z80 have a DAA (Decimal Adjust Accumulator) for this very reason. I have seen code (just now) to implement a DAA subroutine on the AVR. I don't know if it will help.

author: hamster_nz
author_rank: Super Contributor
timestamp: November 09, 2016, 01:24:06 am
content: If you wanted to do cascaded 'if's (which is somewhat silly, but maybe you are loop adverse) at least do it like this:Code:d2 = d1 = d0 = '0';/* Hundreds */if(val > 500) {val -= 500;d2 += 5;}if(val > 200) {val -= 200;d2 += 2;}if(val > 200) {val -= 200;d2 += 2;}if(val > 100) {val -= 100;d2++;}/* Tens */if(val > 50) {val -= 50;d1 += 5;}if(val > 20) {val -= 20;d1 += 2;}if(val > 20) {val -= 20;d1 += 2;}if(val > 10) {val -= 10;d1 ++;}/* Units */d0 += val;It should actually be quicker than looping code like this:Code:d2 = d1 = d0 = '0';while(val > 100) {val -= 100;d2++;}while(val > 10) {val -= 10;d1 ++;}d0 += val;But maybe 3x the code size.The cascaded 'if's has an advantage that it gives predictable results with over-range values too...

author: Someone
author_rank: Super Contributor
timestamp: November 09, 2016, 02:35:19 am
content: There are good examples here:http://www.avrfreaks.net/forum/smallest-and-fastest-binary-bcd-conversion?page=all

author: JPortici
author_rank: Super Contributor
timestamp: November 09, 2016, 06:01:40 am
content: Quote from: Dan Moos on November 08, 2016, 01:08:27 amfour methods:1) Write the code, compile, take the disassembly and count instructions. keep track of jumps etc. easier if you write the routine directly in ASM2) simulator with a stopwatch3) debugger with stopwatch4) hardware, toggle an output before and after and measure time between the two events3 and 4 are real world results so external interrupts and other routines which exectution time depends on the input value will affect the result of the measure

author: T3sl4co1l
author_rank: Super Contributor
timestamp: November 09, 2016, 07:28:07 am
content: The nice thing about my AVR example is, AVR is very simple and deterministic, so one can count the cycles very easily, using the datasheet timings. And it's in ASM to begin with, so you get what you write. No worries about what the compiler will make of it.A tree of IF statements is... one way to do it, I suppose. But if that's all you can think of, you have much to learn about the richness of programming!Also, if you're absolutely concerned about one concern alone -- like clock cycles, or code and data size, or memory usage, there are many optimizations that can be applied there. Loop unrolling is one time saver; when you're doing transfer functions (i.e., one parameter comes in, a value goes out), a memory map (lookup table) can save tremendously. But lookup tables aren't very practical on embedded systems (with limited memory), so you can't go too crazy there.Taking advantage of your available resources is always the best path. AVR has hardware multiply, which makes some things fantastically easy, even though the math works out worse. One 16/16 division is worth about six 16x16 multiplies!Tim

author: westfw
author_rank: Super Contributor
timestamp: November 11, 2016, 06:48:17 am
content: There was recently a long and detailed discussion along these lines over in avrfreaks:http://www.avrfreaks.net/forum/optimizing-libc-integer-conversion-routines

author: tggzzz
author_rank: Super Contributor
timestamp: November 11, 2016, 10:38:23 am
content: Quote from: IanB on November 08, 2016, 01:26:20 amOnly for very simple processors, those that don't have caches and don't have overlapped processing. Even on a 486 there could be an 8:1 (IIRC) variation in interrupt routine latency, depending on what was in the cache.If you want to get accurate timings, use the XMOS processors, where the compiler/IDE willguaranteemaximum timings.