Low on program space with pic microcontroller - Page 1
Forum section: electronics / general

author: jerryk
author_rank: Regular Contributor
timestamp: February 14, 2022, 05:59:31 pm
content: I've just started this learning project and my program space is 87% used. I'm using a pic16F18875 and here is the summary at build.Memory Summary:Program space used 1BD0h ( 7120) of 2000h words ( 86.9%)Data space used 102h ( 258) of 400h bytes ( 25.2%)EEPROM space used 0h ( 0) of 100h bytes ( 0.0%)Data stack space used 0h ( 0) of 2ECh bytes ( 0.0%)Configuration bits used 5h ( 5) of 5h words (100.0%)ID Location space used 0h ( 0) of 4h bytes ( 0.0%)The main.c program is as follows:Code:/**Included files*/#include "mcc_generated_files/mcc.h"#include "lcd_jerry.h"#include <stdio.h>#include "app.h"void Delay_Seconds(unsigned char z){unsigned char x,y;for (y = 0; y < z; y++){for (x = 0; x < 100; x++) __delay_ms(10);}}//*************************Declare Global Variables******************float voltage0;float voltage1;float voltage2;float voltage3;float voltage4;char Buffer;int ADC_Read(int channel);float GET_Voltage(float resistorRatio, float convertedValue);void LCD_Display(float cellVoltage, int row, int column);/*Main application*/void main(void){//Initialize functionsSYSTEM_Initialize();Lcd4_Init();ADCC_Initialize();APP_LED_Initialize();// Enable the Global InterruptsINTCONbits.GIE = 1;// Enable the Peripheral InterruptsINTCONbits.PEIE = 1;//Resistor Ratios//Cell1float resistorOneCell1 = 982;float resistorTwoCell1 = 248.2;float resistorRatioCell1 = 0;//Cell2float resistorOneCell2 = 663;float resistorTwoCell2 = 248.3;float resistorRatioCell2 = 0;//Cell3float resistorOneCell3 = 364.8;float resistorTwoCell3 = 248.2;float resistorRatioCell3 = 0;//Cell4float resistorOneCell4 = 56.14;float resistorTwoCell4 = 247.8;float resistorRatioCell4 = 0;float cellVoltage1 = 0;float cellVoltage2 = 0;float cellVoltage3 = 0;float cellVoltage4 = 0;//Resistor RatiosresistorRatioCell1 = (resistorTwoCell1 / (resistorOneCell1 + resistorTwoCell1));resistorRatioCell2 = (resistorTwoCell2 / (resistorOneCell2 + resistorTwoCell2));resistorRatioCell3 = (resistorTwoCell3 / (resistorOneCell3 + resistorTwoCell3));resistorRatioCell4 = (resistorTwoCell4 / (resistorOneCell4 + resistorTwoCell4));// LCD GreetingLcd4_Clear();Lcd4_Set_Cursor(1, 1);Lcd4_Write_String("Battery");Lcd4_Set_Cursor(2, 1);Lcd4_Write_String("Simulation");led4_Toggle();__delay_ms(1500);led4_Toggle();Lcd4_Clear();// Turn on the ADC moduleADCON0bits.ADON = 1;//Main while loopwhile (1){APP_LED_Tasks();//Get Voltage at balance wiresvoltage0 = GET_Voltage(resistorRatioCell1, ADC_Read(5));voltage1 = GET_Voltage(resistorRatioCell2, ADC_Read(4));voltage2 = GET_Voltage(resistorRatioCell3, ADC_Read(3));voltage3 = GET_Voltage(resistorRatioCell4, ADC_Read(2));//Converts readings at the balance wires to individual cell voltagecellVoltage1 = voltage0 - voltage1;cellVoltage2 = voltage1 - voltage2;cellVoltage3 = voltage2 - voltage3;cellVoltage4 = voltage3;//Display the resultsLCD_Display(cellVoltage1, 1, 0);LCD_Display(cellVoltage2, 1, 8);LCD_Display(cellVoltage3, 2, 0);LCD_Display(cellVoltage4, 2, 8);}}//Function Definitionsint ADC_Read(int channel){float convertedValue = 0;// select the A/D channelADPCH = channel;__delay_ms(10);// Start the conversionADCON0bits.ADGO = 1;while (ADCON0bits.ADGO);convertedValue = ((adc_result_t) ((ADRESH << 8) + ADRESL));ADCON0bits.ADGO = 0;return convertedValue;}float GET_Voltage(float resistorRatio, float convertedValue){float voltage = 0;voltage = ((convertedValue) * 5 / 1024) / resistorRatio;return voltage;}void LCD_Display(float cellVoltage, int row, int column){Lcd4_Set_Cursor(row, column);sprintf(Buffer, "%.2f", cellVoltage);Lcd4_Write_String(Buffer);Lcd4_Write_String(" V");}/**End of File*/It's ugly beginner code but it works so far. I'm using MPLABX and MCC. I am heading away from MCC because I don't like being that insulated from what's going on. I'm trying to understand if MCC files add bulk to the program file or is it in the main.c program. I suspect there are problems with my use (or abuse) of variables and math operators in the main program.Please don't be gentle here. I want to learn this stuff and either the controller I'm using does not have enough space for my end goal or I need some more schooling.Thank in advance - Jerry

author: Mechatrommer
author_rank: Super Contributor
timestamp: February 14, 2022, 06:12:49 pm
content: float?! my goodness! how luxury people nowadays...(just quick scroll on the code) otoh you are lucky you still have 13% of space left, i had to split a program into 2 because i have negative space left.

author: NorthGuy
author_rank: Super Contributor
timestamp: February 14, 2022, 06:14:52 pm
content: This is because you use lots of floating point operations on an MCU which doesn't even have hardware multiplier. This produces huge overhead. That is why your code is so big.If you wish, you can re-write with integers/fractionals and the code will be much smaller (and faster if that matters).Or, you can always switch to PIC16F18877 which has 4 times more memory.

author: Siwastaja
author_rank: Super Contributor
timestamp: February 14, 2022, 06:22:31 pm
content: Yes, if you want the luxury (ease of development) of float, get a bigger device. Otherwise, go for fixed point arithmetic. It's not difficult, just a little bit of manual thinking. Chances are, you start liking the "manual ranging" of variables so much you end up using fixed point even if you have hardware floating point available.Fixed point doesn't need to mean binary fractions, if you struggle at that idea. At simplest, you do this by choosing units like mV or even µV instead of V, so that you can just use integers.Even a 32-bit integer on a 8-bit CPU is much more efficient than a float, and the numerical range for int32 is approx +/-2 billion.But you need to think about the ranges, rounding is done as a "floor" function and if any intermediate result is close to zero (just a few counts), that will mean huge quantization error. Also exceeding ranges causes wrap-around.

author: mariush
author_rank: Super Contributor
timestamp: February 14, 2022, 07:03:47 pm
content: You have datasheet for that chip here:https://ww1.microchip.com/downloads/en/DeviceDoc/PIC16(L)F18855-75-Data-Sheet-40001802H.pdfThe microcontroller has a built in voltage reference, which can be set to 1.024 , 2.048v , 4.096vIf you can afford to, set it to 4.096v and then your 10 bit ADC readings will give you 4mV per step ...Look into staying away from floating point, the compiler has to add floating point stuff to your code because the controller doesn't have built in hardware floating point stuff.So... don't use delay_ms 10 in your adc_read... just use a loop until it's not busy or something like that.see 17.4 in your datasheet ... you need at least 200uS for a reading, then 200uS between consecutive readings... no need for 10msSee 23.0 and 23.1 and 23.2 in datasheet for adc configuration ... you could set the adgo to 1 and then do something like while .adgo==1 asm("nop")You can be smarter and figure out how much time each instruction would use and basically do a timeout.For example each instruction uses 1 or more cycles, where a cycle is 4 Hz ... so at 32 Mhz that would be 8,000,000 cycles in a second or 8000 cycles in a ms, or 8 cycles in a uS ... if you want to timeout in 240uS, it would be enough to spend 8 x 240 cycles doing something.See section 36 in datasheet ... you can see there decfsz uses 1 cycle or 2 if condition is met (it's used in while until value is 0) , goto uses 2 cycles so you could do something likeunsigned char n = 240;do {asm("goto $+1"); asm("goto $+1"); asm("nop");// decrease if not zero - decfsz (1 cycle if !=0, 2 cycles otherwise)// goto loop start = 2 cycles} while(--n);/// so that's 5 cycles for the goto commands + nop, plus 2 for the goto start , plus one for the decrement = 8 cycles and that gets repeated 240 times... you have your 240 x 8 cycles.you could add in the do loop something like if (xxx.ADGO ==0) n=0; to exit loop as soon as done, or after 240 loops otherwise .... that would be a comparison and an assignment so you can calculate the cycles (it's probably 3 or 4 cycles but you can view the assembly code and see what instructions the compiler puts there and count the cycles based on instructions chosen by compiler.Going back to the voltage reading... like I said... keep it in mV as much as possible.And you could probably choose those resistor values better if you can, or just not use floating point, multiply everything by 10 for exampleYour values : 982, 248.2 : 0.7982 My values : 9820 , 2482 : 9820 / 12302 keep it like this, don't even bother defining a float ratio value.So let's say the ADC gives you 852 as a measurement ... that's 3408mV if you use a voltage reference of 4.096vYou can now do 3408 x 9820 / 12302 = 2720.416 and just round it to 2720 mV and print 2.720V on screen.If possible, you could try tweaking your ratios to be multiples of 2 and then you can keep it integer and just shift bits...For example, you can rewrite 9820 / 12302 as 817.447 / 1024 or 81744 / 102400 because 12302 = 12.013 x 1024So now your measurement can be 3408 x 816.447 / 1024 = 3408 x 816447 / 1024 / 1000 = (2,782,451,376 >> 10 ) / 1000 = 2,717,237 / 1000 = 2717 mV ( 3mV error, close enough)Same for your second cell ... 663 and 248.3 ... gives you 663 / 911.3 = rewrite it as 6640/9113 or as 745 / 1024 ( so you can shift 10 bits and be done with it)and so on...--sprintf also adds up a lot of code. If you keep your voltage in mV all the way to the end, then it's just a matter of adding a dot before the last 3 digits.

author: hans
author_rank: Super Contributor
timestamp: February 14, 2022, 07:25:05 pm
content: I would argue against assuming 32-bit is more efficient than float, as an universal truth.Itdependsa lot.(I know these MCUs don't match the PIC16, by a long shot for the 32-bit MIPS, but I just want to reference the speed of several math operations on a range of platforms)For sure do floating point routines cost quite a lot of memory space, especially for printf, but it does not have to be a lot slower than 32-bit integers on a 8-bit MCU.Both multiplications and divisions with floats is on the same level compared to int32 (of the PIC18 wiki link), since floats are stored in the logarithmic domain. Float add/subtract is where it primarily suffers, because it has to do conversions between linear and the logarithmic domain. However I don't see a lot of those operations in OP's program. Certainly the program calculates resistorRatioCellX, but that can be done at compile-time if the compiler is smart enough. On the PIC itself it only has to perform a couple of float multiplications/divisions to calculate the voltage.I think there is one major advantage of using floats: you can often avoid the very costly division operation (which is expensive for both int32 and float), by taking the reciprocal of the divisor number as the constant. This doesn't incur a lot of loss of precision in float, while it can be a complete disaster for int32.Probably the compiler is not smart enough to do this for you because it wants to store the resistorRatioCellX verbatim in memory, and then use that value for division. What you could try is help it a little bit and calculate/store the reciprocal ratio value instead, and then use:Code://... start snippet of mainresistorRatioCell1 = (resistorOneCell1 + resistorTwoCell1) / resistorTwoCell1;// (or actually if you do the math, we now have: resistorRatioCell1 = 1+ resistorOneCell1 / resistorTwoCell1; )//etc.//... end snippet of mainfloat GET_Voltage(float resistorRatio, float convertedValue){float voltage = 0;voltage = convertedValue * resistorRatio * 5 / 1024;return voltage;}The compiler should be smart enough to see that *5/1024 is just another float constant, and can just multiply by 0.004883 etc. If you want to go even further, you could also put this scaling factor in your resistorRatioCellX constant, and effectively get a "ADC LSB to voltage"-conversion-factor using only 1 float multiplication. This should at least speed up the program by a bit,but I don't think this is going to save a lot of program space. It's very likely that printf uses float multiplications/divisions anyway to print the numerical representation of the float. Nonetheless, it can be useful to play around with how you write down your computations, especially since on the PIC XC8 compiler you can't expect it the compiler to be too smart.I still recommended trying to worrk with fixed point 32-bit if memory space and printf is of concernBut e.g. then you can also try to use 16-bit, if that has enough resolution.edit:another idea/variation you could try, is to do the ADC-to-voltage conversion in floats calculating in mV, uV 0.1mV counts, or whatever resolution you need. Then go cast that float to an 16 or 32-bit integer, perform the subtraction for the cell voltages, and print those results with printf().

author: Ian.M
author_rank: Super Contributor
timestamp: February 14, 2022, 07:38:33 pm
content: Comment out those sprintf() calls and see how much bloat the printf() functions library is bringing in. Its usually quite large, so rolling your own number to string routines with hard-coded formatting may be worth the pain.I wouldn't recommend it to you *THIS* *TIME*, but its worth knowing that the XC8 compiler 'PRO' license, which enables optimizations that can significantly reduce the binary size can be purchased on what's basically a monthly rental:https://www.microchip.com/en-us/development-tool/sw006021-subIf you ever have a project 95% or better complete, have tested all sections separately, but can't quite get it to fit or have had to disable 'nice to have' features due to lack of program memory, and you are already using the largest memory PIC in the device family and footprint, or are getting paid for the design, it may well be worth buying one month of 'PRO'!N.B. The Microchip XC compilers licenses permit commercial use in free mode, so if you can live with the binary bloat and do without official support, you can be a money-earning cheap-skate.

author: MarkF
author_rank: Super Contributor
timestamp: February 14, 2022, 07:49:28 pm
content: I'm seeing a lot of harping on floating point math here. And I agree it could be optimized by some smart math to avoid doing fractions (i.e. multiply and then divide instead of multiply by a float scale factor). You could look into fixed point math functions.However, everyone is ignoring the elephant in the room. Graphic libraries can consume large amounts of memory. Without being able to see what you're using, try reducing its size. One area I seen in libraries is large storage of fonts. Or eliminate graphic drawing primitives you don't think you will ever use as a last resort.Have you enabled any of the XC8 optimization? There is some even with the 'free' license.What version of XC8 are you using?

author: hans
author_rank: Super Contributor
timestamp: February 14, 2022, 07:57:49 pm
content: I suspect OP is using an alphanumeric LCD display considering it's printing characters at row/cols, so in that case there is not font table on the MCU.But that is just my guess.. I don't see any X/Y pixels, but for all we know that could be hidden in the LCD libraryA quick google for the LCD function names does resolve quite a lot of code/libraries that use alphanumeric displays, though.

author: brucehoult
author_rank: Super Contributor
timestamp: February 14, 2022, 09:34:19 pm
content: Guarantee the sprintf is the big problem.SoftFP routines aren't usually all that big in themselves, just for the arithmetic.

author: mikerj
author_rank: Super Contributor
timestamp: February 14, 2022, 09:51:58 pm
content: Quote from: hans on February 14, 2022, 07:25:05 pmYou can very avoid division with fixed point math as well, if you are dividing by a constant value. The major advantage of floating point is when you really need to deal with very large and very small values, but many applications don't and beginners tend to jump to floats as the first solution to anything that has fractional values.

author: jerryk
author_rank: Regular Contributor
timestamp: February 14, 2022, 10:40:02 pm
content: Just an update: I'm moving through all the comments and redoing the code as fast as I can but it will be a moment. Changing all floats to int dropped program space down to 71% from the original 87%. Commenting out the sprintf() dropped it to 15% from the 71%. I'm going to try @Ian.M's roll your own character to string suggestion. I used to use that display method but switched to sprint() after following one of Microchip's tutorials on State Machine coding. I did not realize the memory implications.As for adjusting resistor sizes, I am stuck with the existing hardware design for now. I did fine tune each divider to the actual resistor values rather that the stated values. This was because of accuracy issues related to very high impedance inputs to the ADC. I'm stuck with that also. As for the display type it's a simple 16x2 LCD.I really appreciate the amazing help. I could have spent a while spinning my wheels on this one.Jerry

author: DavidAlfa
author_rank: Super Contributor
timestamp: February 14, 2022, 11:42:48 pm
content: Are you using compiler optimization? XC8 is free up to O2 (included).If not using floats, useiprintf, but for displaying simple integers, making your own function will produce much smaller code.

author: brucehoult
author_rank: Super Contributor
timestamp: February 14, 2022, 11:51:10 pm
content: The problem with printf() is it doesn't know until runtime what kind of value you will be trying to print, so it needs to include code for ALL integer and floating point sizes, including possibly depending on which libc library you use both 32 bit and 64 bit integers, and 32 bit, 64 bit, and 128 bit floating point. Newlib is smaller than glibc, and Newlib nano (a build option for Newlib) is smaller than Newlib. There are other even more cut down libc implementations for microcontrollers.This is one place where both the Arduino libraries and standard C++ win out, with overloaded C++ functions for Serial.print() or << respectively, that result in the linker being able to include only the code that you are actually using.

author: jerryk
author_rank: Regular Contributor
timestamp: February 15, 2022, 12:07:39 am
content: Quote from: DavidAlfa on February 14, 2022, 11:42:48 pmI just checked the XC8 properties and I amnotusing any optimization. I did not see that part in the properties window when I set it up, or looked at it and sat it out do to lack of knowledge when I first started with MPLABX. There are four optimization selections 1,2,3,S with the last two being supersets of O2. I will try that when I get my code redone.Thanks - Jerry

author: T3sl4co1l
author_rank: Super Contributor
timestamp: February 15, 2022, 12:21:51 am
content: Float isn't necessarily a big deal, but it depends on the platform. Definitely check the listings to see how much space is being taken up by what, how much the compiler is adding to your code versus what's stuff you're more directly responsible for. (I mean, you're responsible for everything past startup code, ultimately; but as far as direct implementation, that is.)I'm not familiar with the platform, so I can't comment on which of those are likely or actual problem points. I wouldguessfloat libs aren'ttooterrible, for example a couple typical functions take up only a couple kwords on avr-gcc; but AVR does have hardware multiply so those can be a bit faster / more streamlined than they'll be on a more limited machine. That shouldn't take up much more (multiplication on Z80 for example is only like a dozen instructions), and if that same operation is shared across the library calls, that's hardly a difference really -- but if it's not shared, it may bloat up much more. Also, PIC-C / XC8 / whatever has been notorious for poor optimization options, so, maybe it's more intentional than you'd prefer... Anyway.As for things you can control -- may or may not be better to use this, but definitely something totry.Repeated lines like the:Code:voltage0 = GET_Voltage(resistorRatioCell1, ADC_Read(5));and etc. Try putting them in a loop. Sometimes iterating over a loop is faster than repeating all the variable/pointer loads/stores and function calls. You could rewrite this as something like,Code:for (uint8_t i = 0; i < 4; i++) {voltage = GET_Voltage(resistorRatioCell, ADC_Read(5 - i));}Or I suppose possibly the whole main() loop could be written in this way, since it's fairly repetitive.More generally this relates to the subject of vectorization, and even parallelization. Vectors, for computing purposes, are arrays where the same operation is performed on every element of the array, or, at least some consistent behavior across elements. Maybe some variables are off-by-one (i.e. thecellVoltages are the forward difference between [ i ] and [i-1]), or if some are in reverse order (like the ADC_Read parameter) but it's still one-to-one (or directly dependent on the index or whatever), that's still fine. And, you can see some of these could be evaluated in parallel, assuming you had a platform that could do that. (On PCs, such operations can be done with SIMD instructions, or even done on the GPU. Fewer MCUs have such features though.)Anyway, the aim of this is, if the function call overhead is relatively large, while the loop overhead is relatively low, this can save space. Since doing it all unrolled, is N function calls, so, it doesn't scale well at some point. For N=4 it's something to try.And, above all! Learn the instruction set, get some idea what the compiler is actually doing, see how your program is divided into functions and calls, see what code is generated by what statements -- and see what statements are prone to taking more space.Tim

author: Mechatrommer
author_rank: Super Contributor
timestamp: February 15, 2022, 12:32:41 am
content: Quote from: MarkF on February 14, 2022, 07:49:28 pmusually the programmer who made the lcd library knows what he's doing. ie the best possible code for the function required. but once the "user" programmer calls the function with float as argument, then we know what we are asking. otherwise not trusting the lcd library, we can (1) not using the library at all, meaning missing lcd feature, or (2) make our own lcd library which is another elephant in the coding task. similarly to using float operations (its just a float library backstage), using it means we know what we are asking, not trusting it? we need to snap our own fixed points algorithm gymnastics. if we dont want unreadable gymnastics, we need to upgrade our mcu. ymmv.

author: MarkF
author_rank: Super Contributor
timestamp: February 15, 2022, 01:28:16 am
content: Quote from: jerryk on February 14, 2022, 05:59:31 pmI've never used the MCC to setup the configuration.I just use the 'Configuration Bits' window of MPLAB to select my configuration and then copy&paste it into the main.c file. Press the 'Generate Source Code to Output' button to get text to put in the main.c

author: brucehoult
author_rank: Super Contributor
timestamp: February 15, 2022, 03:25:15 am
content: Quote from: jerryk on February 15, 2022, 12:07:39 amI don't know what that compiler is like, but if it's anything like gcc and llvm then, basically, never eve turn optimisation off. Always use at least -O1. Otherwise the code is just huge and slow and very very hard to follow.As it's a smallish MCU (8192 instructions, 1 KB SRAM, basically like an ATMega168, but half the EEPROM) I expect you're going to want either 1 or S, and both will make huge code savings.I've only used the ATMega328, which has twice the program space, but I've never come close to using all the program space and I've never tried to economise and I'd think probably everything I've used it for the 168 would have been fine. (I also use the ATTiny85 a lot, but I expect to have to watch code size very carefully there)

author: SiliconWizard
author_rank: Super Contributor
timestamp: February 15, 2022, 03:36:15 am
content: I've used both PIC16F (purely in assembly) and PIC18F (in C, with mcc18, not xc8) MCUs, and never ever used floating point. Integer/fixed point arithmetic was always perfectly fine, never needed anything else. Most of the time (oh yeah you can find exceptions to this to your heart's content), FP on such small targets (on which you'll implement relatively simple stuff) can be avoided without a problem. Many people using FP are doing that out of laziness, or lack of arithmetic skills.Never used printf() on those either, even without FP support. printf() is really bloated.

author: Mechatrommer
author_rank: Super Contributor
timestamp: February 15, 2022, 04:08:54 am
content: Quote from: SiliconWizard on February 15, 2022, 03:36:15 amnot everybody. if the program is simple enough, to reduce development and support/debugging cost, FP is perfectly fine to use on small mcu imho. undoubtedly, a fuller bag of skills is necessary to make wiser decision on application suitability. such as in this case, the OP hits limitation, so FP is among the one that needs attention, it will "propagate"...

author: JPortici
author_rank: Super Contributor
timestamp: February 15, 2022, 06:51:06 am
content: Quote from: Ian.M on February 14, 2022, 07:38:33 pmbut -O2 which is available in free mode does an excellent job already, true there is a diference between -O2 in free and -O2 in pro because some other aggressive optimizations are not available in free mode but...- looking into abandoning MCC completely is a good starter, you can remove one or two layers of calls and some extra convoluted code like the UART and I2C libraries- looking into optimizing printf is a good idea too- looking into abandoning float.. well yes the floating point library takes a lot of space and in this example is not really necessary but i wouldn't take it as a general rule though to be fair i try to avoid 32 bit types as well.. i make an extensive use of the 24 bit types (__int24 and __uint24) since most of the time i deal with 16x8 bit operationswhat i would NOT do es resort to nasty inline assembly tricks, in fact they can confuse the optimizer and put you in a vicious loop of oh i can't touch this comma.Had done it in many places in a project in the past, i couldn't add a single line of code so i decided to rewrite it completely. in the end i got it even faster and saved space over the inline assembly version. In all new projects the only insile assembly instructions i use are TBLRD and TBLWT (only because they don't provide a builtin) otherwise it's all assembly modules included in the code so the optimizer and the linker don't get confused, they are easy to confuse in XC8

author: hans
author_rank: Super Contributor
timestamp: February 15, 2022, 10:45:50 am
content: Quote from: mikerj on February 14, 2022, 09:51:58 pmI agree, but excuse me for being picky, it can still depend what kind of constant divisor you have. A 2^N divisor can be executed with a bit shift. Something else, for example a calibration factor *253/1000, still requires a long-tail division function such as__udivmodsi4(nom, denom)= __udivmodsi4(x*253, 1000).In this example it's easy to modify the calculation to *259/1024 instead, which only has 0.03% error.Thatimplementation will for sure be a lot faster, and the precision for many applications is good enough (if you know the dynamic range of the variable, you can probably get better accuracy with a larger multiplication and 2^N divisor).I did some benchmarks in the MPLAB XC8 simulator (O2 free) for a PIC16F1508 executing the optimized int32 arithmetic, naive int32 calculations, and a straight forward floating point calculation.A naive int32 calculation isn't that much faster than float, as I alluded to before with the PIC18 benchmarks. Only if you optimize to fixed point arithmetic it will be a lot quicker... but otherwise a straight forward float implementation as @Mechatrommer says can save a lot of dev time. (Nonetheless still a good practice to do/learn) Optimizing is still worth it in terms of code size:Fixed point (div1024): 226 bytes (of which 47 bytes is lmul)Naive int32 (div1000): 311 bytes (of which 82 bytes is "lldiv")Float: 1011 bytes (of which 536 bytes is "flmul", and also requires some int-float and float-int conversion functions)Let alone the sprintf implementation for int32 vs float. But honestly, unless you're doing this professionally, spending 50ct on a larger memory part vs the extra dev time needssome(hundreds) production quantity to make it worth it

author: Siwastaja
author_rank: Super Contributor
timestamp: February 15, 2022, 10:53:49 am
content: Indeed, it's important to understand when youactuallyneed a division.For just scaling, it's never needed. You can multiply by any arbitrary constant, then bitshift. Note on bitshifting, simple architectures rarely have an instruction to shift by any arbitrary number of bits; for example, ATTiny CPUs can only shift by 1, so shift-by-3 becomes three instructions. But, shifting by multiples of 8 is always very efficient as that only means shuffling the whole bytes around, or just "ignoring" parts of the variables/registers.In practice, this would mean something like reading a 12-bit voltage measurement into uint32, multiplying it with a constant of 65536 equaling "1.0", then shift by 16 bits. Now you can adjust this large constant to provide finely stepped gain calibration. And if the each ADC step happens to be 1.2345mV, then no problem, just instead of 65536, multiply by 53087, and you get millivolts out directly! No division anywhere.

author: mikerj
author_rank: Super Contributor
timestamp: February 15, 2022, 11:02:57 am
content: Quote from: hans on February 15, 2022, 10:45:50 amNot necessarily as it depends entirely on how much error is acceptable, for instance 253/1000 (0.253) is very close to 259/1024 (0.25293). More accuracy can be achieved by using multiple binary factors e.g. (2^-2 + 2^-9 + 2^-10 + 2^-14) is 0.25299

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: February 15, 2022, 11:09:09 am
content: Quotethere's your problem.Embedded applications very rarely need floating point.FP gives you range, not precision.Real-world variables rarely need enough range to justify the overhead of floating point.Fixed-point maths will usually be far more efficient as all the calculations are the same as for integers.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: February 15, 2022, 12:19:36 pm
content: Quote from: Siwastaja on February 15, 2022, 10:53:49 amNote that C does arithmetic inintby default, so you often need to apply typecasts carefully, within expressions, to make sure the compiler doesn't discard part of your expression (e.g., the 53087 constant above, might be written as long (53087Lor(uint32_t)53087) to force this.Arithmetically speaking, addition adds one bit (the carry), and multiplication sums the number of bits (so, two 16s give a 32-bit result), but in C, these data are discarded and the result is always in the same type as the operands, silently causing overflow (which may also be undefined behavior and platform dependent; mostly, it's modulo-2^N because almost all CPUs use 2s-complement arithmetic). So you need to anticipate and provide extra space for the result, and also tell the compiler that you expect all the bits from that result (by casting one or both operands to the larger size; a 32x16 multiplication gives a 32 bit result in C).Also, with calibration values, no hokey magic-number VREFs are needed -- use whatever is handy, use VCC even (if it's suitable, electrically speaking -- say everything is ratiometric, e.g. DACs, potentiometers, thermistors..). A better way to view the ADC result is as a dimensionless ratio of full scale. You typically need just a few extra bits (on top) to handle these values, to deal with carries on intermediate results. (The reality is, you still tend to use 32-bit data sizes, because 24 is poorly supported if at all, and as mentioned above, byte alignments are easier. And you can use the extra low bits to keep quantization (rounding) noise down.) And you can apply calibration valuesanywherebetween ADC acquisition, and how ever the values get presented to the user (whether digitally accessible registers, or written to a com port or display). In particular, writing back out to a DAC of the same scale (bits and VREF), the calibration istrivial!Tim

author: Ian.M
author_rank: Super Contributor
timestamp: February 15, 2022, 02:28:28 pm
content: XC8 supports 24 bitshort longinteger types and as C99 fixed width typesint24_tanduint24_t, *EXCEPT* if you have the CCI*compiler option enabled. Useful when 16 bits isn't enough, but you don't want the code and data space overheads of 32 bit math.* CCI: Microchip Common C Interface, a fundamentally broken attempt to unify implementation defined extended syntax, features and behavior across the whole XC compiler range by banning HiTech PIC C derived stuff in XC8 that isn't supported in the GCC based XC16 and XC32 without adequately providing equivalent functionality when compiling for 8 bit PICs!

author: errorprone
author_rank: Contributor
timestamp: February 15, 2022, 03:23:54 pm
content: An easier way to check what is using program memory is to look at the .map file. Not sure if it’s generated automatically but it is a text file that shows size and location of symbols.

author: DavidAlfa
author_rank: Super Contributor
timestamp: February 15, 2022, 04:48:08 pm
content: Where's the problem for running multiple bitshifing instrutions?Compare that to 100s when handling floats using software.Fixed point is the way for simple values like these.

author: brucehoult
author_rank: Super Contributor
timestamp: February 15, 2022, 10:11:20 pm
content: Quote from: mikeselectricstuff on February 15, 2022, 11:09:09 amIf you have something where you have to multiply two unknown (e.g. input) values then fixed-point is scarcely any faster than floating point, and you run a very real risk of underflow or overflow.Sure, if you only ever add and subtract, or multiply by constants, then you can do the analysis and decide integer or fixed point will be enough.But you have to actually do the analysis, and not everyone who is a programmer is competent to do that.FP provides a safety net.If I recall correctly, FP add/sub/mul on an ATMega at 16 MHz takes about 80 clock cycles i.e 5µs. That means you can do up to 200,000 of them a second. Even if you're doing closed-loop control at 100 Hz or even 1000 Hz that's quite a lot of FP calculation available in the time budget.

author: westfw
author_rank: Super Contributor
timestamp: February 15, 2022, 11:25:51 pm
content: Hmm. Are there any "modern" tools for analyzing binary size?Linker maps are typically verbose and unfriendly.A disassembly ... even moreso.gcc's "nm -S " tool is very useful, but primitive (CLI input and output.)I'm picturing something GUI with expanding trees of large contributors and the functions they alone require.So if printf is the only function that uses a 32bit divide, the space used by the divide function would get blamed on printf(), but if there were other users, it wouldn't. I *think* all the info needed is available and used for MAP/CREF file production, but not in a convenient fashion...

author: DavidAlfa
author_rank: Super Contributor
timestamp: February 16, 2022, 02:24:25 am
content: In STM32 IDE (Eclipse-based) you have a very nice build analyzer, showing the program/ram usage for every function and variable, also has a stack analyzer.I've been searching for the same functionality on XC8 but found nothing.

author: JPortici
author_rank: Super Contributor
timestamp: February 16, 2022, 12:05:22 pm
content: Quote from: brucehoult on February 15, 2022, 10:11:20 pmPrecisely. If i have such slow control loops i don't even care anymore and just throw floats at the problem. Data for communication is sent in binary format (packed structure) so i let the PC deal with the conversion to human readable data

author: jerryk
author_rank: Regular Contributor
timestamp: February 16, 2022, 04:39:36 pm
content: I have modified the code attempting to follow some of the suggestions. I have listed two versions below.This is the full version that compiles and uploads to the mc fine. It use 17% of program space which is great. Unfortunately it does not work. I can't tell if the error is an overflow issue or if it's hiding in the ADC_Read() function.Code:/**Included files*/#include "mcc_generated_files/mcc.h"#include "lcd_jerry.h"#include <stdio.h>#include "app.h"//*************************Declare Global Variables******************char Buffer;int ADC_Read(uint16_t channel);uint32_t GET_Voltage(uint16_t resistorRatio, uint16_t convertedValue);void LCD_Display(uint32_t cellVoltage, uint8_t row, uint8_t column);void Lcd4_Clear(void);void Lcd4_Init(void);/*Main application*/void main(void){//Initialize functionsSYSTEM_Initialize();Lcd4_Init();ADCC_Initialize();APP_LED_Initialize();// Enable the Global InterruptsINTCONbits.GIE = 1;// Enable the Peripheral InterruptsINTCONbits.PEIE = 1;//Resistor Ratios/**The value of resistorRatioCelln calculated as follows:(1/(resistorTwoCelln / (resistorOneCelln + resistorTwoCelln))) * 100*/uint16_t resistorRatioCell = {496, 367, 245, 123};uint16_t cellVoltage = {0,0,0,0};uint16_t voltage = {0, 0, 0, 0};// LCD GreetingLcd4_Clear();Lcd4_Set_Cursor(1, 1);Lcd4_Write_String("Battery");Lcd4_Set_Cursor(2, 1);Lcd4_Write_String("Simulation");led4_Toggle();__delay_ms(1500);led4_Toggle();Lcd4_Clear();// Turn on the ADC moduleADCON0bits.ADON = 1;//Main while loopwhile (1){APP_LED_Tasks();//Get Voltage at balance wiresfor (uint8_t i = 0; i < 4; i++){voltage = GET_Voltage(resistorRatioCell, ADC_Read(5 - i));}/* Used to confirm loops working*voltage = GET_Voltage(resistorRatioCell, ADC_Read(5));voltage = GET_Voltage(resistorRatioCell, ADC_Read(4));voltage = GET_Voltage(resistorRatioCell, ADC_Read(3));voltage = GET_Voltage(resistorRatioCell, ADC_Read(2));*///Converts readings at the balance wires to individual cell voltagefor (uint8_t i = 0; i < 3; i++){cellVoltage = voltage - voltage[i+1];}cellVoltage = voltage;/*Used to confirm loops working*cellVoltage = voltage - voltage;cellVoltage = voltage - voltage;cellVoltage = voltage - voltage;cellVoltage = voltage;*///Display the resultsLCD_Display(cellVoltage, 1, 0);LCD_Display(cellVoltage, 1, 8);LCD_Display(cellVoltage, 2, 0);LCD_Display(cellVoltage, 2, 8);}}//Function Definitionsint ADC_Read(uint8_t channel){uint16_t convertedValue = 0;// select the A/D channelADPCH = channel;__delay_ms(10);// Start the conversionADCON0bits.ADGO = 1;while (ADCON0bits.ADGO);convertedValue = ((adc_result_t) ((ADRESH << 8) + ADRESL));ADCON0bits.ADGO = 0;return convertedValue;}uint32_t GET_Voltage(uint16_t resistorRatio, uint16_t convertedValue){uint32_t voltage = 0;voltage = ((convertedValue) * 4) * resistorRatio;return voltage;}void LCD_Display(uint32_t cellVoltage, uint8_t row, uint8_t column){char volt[] = "1.11";char *volt_ptr = volt;volt_ptr = look((cellVoltage / 100000) % 10);volt_ptr = look((cellVoltage / 10000) % 10);volt_ptr = look((cellVoltage / 1000) % 10);Lcd4_Set_Cursor(row, column);Lcd4_Write_String(volt);Lcd4_Write_String(" V");}/**End of File*/This is test code that I compiled with gcc and it works just fine. I have to feed it the ADC values and have used a typical voltage read at the ADC pin of 2.6V or convertedValue = 650 using FVR of 4.096V.Code:/**Included files*/#include <stdio.h>#include <stdint.h>//*************************Declare Global Variables******************char Buffer;char look(int a);uint32_t GET_Voltage(uint16_t resistorRatio, uint16_t convertedValue);void LCD_Display(uint32_t cellVoltage);/*Main application*/int main(void){uint16_t resistorRatioCell = {496, 367, 245, 123};uint16_t ADC_Values = {650, 650, 650, 650};uint32_t cellVoltage = {0,0,0,0};uint32_t voltage = {0, 0, 0, 0};for (uint8_t i = 0; i < 4; i++){voltage = GET_Voltage(resistorRatioCell, ADC_Values);}//Converts readings at the balance wires to individual cell voltagefor (uint8_t i = 0; i < 3; i++){cellVoltage = voltage - voltage[i + 1];printf("cellVoltage = %d\n", cellVoltage);}cellVoltage = voltage;//Display the resultsLCD_Display(cellVoltage);LCD_Display(cellVoltage);LCD_Display(cellVoltage);LCD_Display(cellVoltage);}//Function Definitionsuint32_t GET_Voltage(uint16_t resistorRatio, uint16_t convertedValue){int voltage = 0;printf("convertedValue = %d\n", convertedValue);printf("resistorRatio = %d\n", resistorRatio);voltage = ((convertedValue) * 4) * resistorRatio;return voltage;}void LCD_Display(uint32_t cellVoltage){char volt[] = "0.00";char *volt_ptr = volt;//volt_ptr = look(cellVoltage / 10000000);//volt_ptr = look((cellVoltage / 1000000) % 10);volt_ptr = look((cellVoltage / 100000) % 10);printf("%c\n",volt );volt_ptr = look((cellVoltage / 10000) % 10);printf("%c\n",volt );volt_ptr = look((cellVoltage / 1000) % 10);printf("%c\n",volt );//printf("%c\n",volt );//volt_ptr = look(cellVoltage / 100 % 10);//volt_ptr = look(cellVoltage / 10 % 10);//volt_ptr = look(cellVoltage / 1 % 10);printf("volt_ptr = %s\n",volt);}char look(int a){switch(a){case 0:return '0';case 1:return '1';case 2:return '2';case 3:return '3';case 4:return '4';case 5:return '5';case 6:return '6';I have checked the FVR related control register settings and they seem correct and are as follows.Code:ADREF = 0x03;FVRCON = 0x83;I do appreciate the help which has been amazing but if anyone can see how I might have stalled out feel free to give me a kick in the right direction.Jerry

author: DavidAlfa
author_rank: Super Contributor
timestamp: February 16, 2022, 07:16:13 pm
content: In ADC read function, ADRESH is 8 bit, so doing 8 bit shift might cause it to be always 0.Code:convertedValue = ((adc_result_t) ((ADRESH << 8) + ADRESL));Try this:Code:convertedValue = ADRESH;convertedValue = (convertedValue << 8) | ADRESL;In GET voltage function, this will probably overflow:Code:voltage = ((convertedValue) * 4) * resistorRatio;ConvertconvertedValueto uint32 before multiplying:Code:voltage = (uint32_t)convertedValue * 4 * resistorRatio;Can't you debug it? Stepping the code would show the problem very easily.How are the cells connected to the adc? What are the resistor values?Remember that ADC sampling time is greatly affected by the input impedance.Have you ensured it's correctly adjusted?You say ADC reads 650 for 2.6V cell.For cell 0, you do:650*4*123 = 319800.Then the display code makes 3.19V out of that.So what's this result? Shouldn't it be 2.6V?

author: jerryk
author_rank: Regular Contributor
timestamp: February 16, 2022, 08:18:18 pm
content: Quote from: DavidAlfa on February 16, 2022, 07:16:13 pmIt is a BMS for lifepo4 4S batteries and the controller on it is actually a pic16LF1824. I'm stuck with the hardware that has very high impedance voltage dividers. These dividers are stepped in a manner to present a uniform voltage below the 3.3Vdd for the low voltage pic. A typical voltage seen by the 4 ADC inputs is around 2.6V if all cells balance at 3.2V nominal. The reason for the display is to help me reverse engineer the existing BMS code. To do that I want the display to accurately show cell voltage and not the divided voltage at the ADC input. I chose to stick with the same high impedance dividers so I could learn how to adjust ADC acquisition times accordingly.I did get the code to work as there was overflow issues from using uint16_t which you caught.I hope my explanation makes sense.Here is the new code that now functions and the program space is:Memory Summary:Program space used 5F7h ( 1527) of 2000h words ( 18.6%)Data space used B8h ( 184) of 400h bytes ( 18.0%)EEPROM space used 0h ( 0) of 100h bytes ( 0.0%)Data stack space used 0h ( 0) of 32Ch bytes ( 0.0%)Configuration bits used 5h ( 5) of 5h words (100.0%)ID Location space used 0h ( 0) of 4h bytes ( 0.0%)New code that works:Code:/**Included files*/#include "mcc_generated_files/mcc.h"#include "lcd_jerry.h"#include <stdio.h>#include "app.h"//*************************Declare Global Variables******************char Buffer;uint16_t ADC_Read(uint16_t channel);uint32_t GET_Voltage(uint32_t resistorRatio, uint16_t convertedValue);void LCD_Display(uint32_t cellVoltage, uint8_t row, uint8_t column);void Lcd4_Clear(void);void Lcd4_Init(void);/*Main application*/void main(void){//Initialize functionsSYSTEM_Initialize();Lcd4_Init();ADCC_Initialize();APP_LED_Initialize();// Enable the Global InterruptsINTCONbits.GIE = 1;// Enable the Peripheral InterruptsINTCONbits.PEIE = 1;//Resistor Ratios/**The value of resistorRatioCelln calculated as follows:(1/(resistorTwoCelln / (resistorOneCelln + resistorTwoCelln))) * 100*/uint32_t resistorRatioCell = {496, 367, 245, 123};uint32_t cellVoltage = {0,0,0,0};uint32_t voltage = {0, 0, 0, 0};// LCD GreetingLcd4_Clear();Lcd4_Set_Cursor(1, 1);Lcd4_Write_String("Battery");Lcd4_Set_Cursor(2, 1);Lcd4_Write_String("Simulation");led4_Toggle();__delay_ms(1500);led4_Toggle();Lcd4_Clear();// Turn on the ADC moduleADCON0bits.ADON = 1;//Main while loopwhile (1){APP_LED_Tasks();//Get Voltage at balance wiresfor (uint8_t i = 0; i < 4; i++){voltage = GET_Voltage(resistorRatioCell, ADC_Read(5 - i));}//Converts readings at the balance wires to individual cell voltagefor (uint8_t i = 0; i < 3; i++){cellVoltage = voltage - voltage[i+1];}cellVoltage = voltage;LCD_Display(cellVoltage, 1, 0);LCD_Display(cellVoltage, 1, 8);LCD_Display(cellVoltage, 2, 0);LCD_Display(cellVoltage, 2, 8);}}//Function Definitionsuint16_t ADC_Read(uint16_t channel){uint16_t convertedValue = 0;// select the A/D channelADPCH = channel;__delay_ms(10);// Start the conversionADCON0bits.ADGO = 1;while (ADCON0bits.ADGO);convertedValue = ((adc_result_t) ((ADRESH << 8) + ADRESL));ADCON0bits.ADGO = 0;return convertedValue;}uint32_t GET_Voltage(uint32_t resistorRatio, uint16_t convertedValue){uint32_t voltage = 0;voltage = ((convertedValue) * 4) * resistorRatio;return voltage;}void LCD_Display(uint32_t cellVoltage, uint8_t row, uint8_t column){char volt[] = "1.11";char *volt_ptr = volt;volt_ptr = look((cellVoltage / 100000) % 10);volt_ptr = look((cellVoltage / 10000) % 10);volt_ptr = look((cellVoltage / 1000) % 10);Lcd4_Set_Cursor(row, column);Lcd4_Write_String(volt);Lcd4_Write_String(" V");}/**End of File*/I'm still all ears on how to improve the efficiency of this code and will continue to digest all the suggestions thus far.Jerry

author: DavidAlfa
author_rank: Super Contributor
timestamp: February 16, 2022, 08:53:23 pm
content: Oh you meant 2.6V from the ADC input, not from the cellNo need to declare resistor ratios as uint32, it's wasting space. Just use uint32 for the calculation to avoid overflow.You can re-use the same voltage array for computing the cell voltage.What's char Buffer for?Initializing variables which you're going to write over is wasted time!Mopst of them could be global, thus skipping unnecessary initialization on every call. Globals also makes debugging easier (Can be read at any time).Also, you could replace look function by a simple constant table.Finally, declare as constant the values you're not writing to (Ex. resistor ratios)You need some timer to limit the refresh rate to 1-5Hz, faster than that you'll get unreadable dancing numbers when you get near a threashold (ex. 3.199-3.200)Code:/**Included files*/#include "mcc_generated_files/mcc.h"#include "lcd_jerry.h"#include <stdio.h>#include "app.h"//*************************Declare Global Variables******************char Buffer;uint32_t voltage;char volt_str[] = "1.11 V";const char digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };/*Resistor RatiosThe value of resistorRatioCelln calculated as follows:(1/(resistorTwoCelln / (resistorOneCelln + resistorTwoCelln))) * 100*/const uint16_t resistorRatioCell = { 496, 367, 245, 123 };uint16_t ADC_Read(uint16_t channel);uint32_t GET_Voltage(uint32_t resistorRatio, uint16_t convertedValue);void LCD_Display(uint32_t cellVoltage, uint8_t row, uint8_t column);void Lcd4_Clear(void);void Lcd4_Init(void);/*Main application*/void main(void){//Initialize functionsSYSTEM_Initialize();Lcd4_Init();ADCC_Initialize();APP_LED_Initialize();// Enable the Global InterruptsINTCONbits.GIE = 1;// Enable the Peripheral InterruptsINTCONbits.PEIE = 1;// LCD GreetingLcd4_Clear();Lcd4_Set_Cursor(1, 1);Lcd4_Write_String("Battery");Lcd4_Set_Cursor(2, 1);Lcd4_Write_String("Simulation");led4_Toggle();__delay_ms(1500);led4_Toggle();Lcd4_Clear();// Turn on the ADC moduleADCON0bits.ADON = 1;//Main while loopwhile (1){APP_LED_Tasks();//Get Voltage at balance wiresfor (uint8_t i = 0; i < 4; i++) {voltage = GET_Voltage(resistorRatioCell, ADC_Read(5 - i));}//Converts readings at the balance wires to individual cell voltagefor (uint8_t i = 0; i < 3; i++) {voltage = voltage - voltage[i+1];}LCD_Display(voltage, 1, 0);LCD_Display(voltage, 1, 8);LCD_Display(voltage, 2, 0);LCD_Display(voltage, 2, 8);}}//Function Definitionsuint16_t ADC_Read(uint16_t channel){// select the A/D channelADPCH = channel;__delay_ms(10);// Start the conversionADCON0bits.ADGO = 1;while (ADCON0bits.ADGO);return ( ((uint16_t)ADRESH << 8) | ADRESL );}uint32_t GET_Voltage(uint16_t resistorRatio, uint16_t convertedValue) {return ( (uint32_t)convertedValue * 4 * resistorRatio );}void LCD_Display(uint32_t cellVoltage, uint8_t row, uint8_t column) {volt_str = digit[ (cellVoltage / 100000) % 10) ];volt_str = digit[ (cellVoltage / 10000) % 10) ];volt_str = digit[ (cellVoltage / 1000) % 10) ];Lcd4_Set_Cursor(row, column);Lcd4_Write_String(volt_str);}/**End of File*/

author: Picuino
author_rank: Super Contributor
timestamp: February 16, 2022, 08:54:36 pm
content: I find it faster and more efficient to perform the conversion from binary to decimal with this routine (for 32bits):Code: (c)#define BINARY_DIGITS 32#define DECIMAL_DIGITS 10void bin32_to_str(char *str, unsigned long bin) {char maxdig, digcnt, bitcnt;static char *p, carry;// Clear stringp = str;digcnt = DECIMAL_DIGITS + 1;do { *p++ = 0; } while (--digcnt);// Transform binary to BCDbitcnt = BINARY_DIGITS;maxdig = (DECIMAL_DIGITS - 1);str += (DECIMAL_DIGITS - 1);do {// Shift left binary number with carrycarry = 0;if (bin & (1L<<(BINARY_DIGITS-1)))carry |= 1;bin <<= 1;// Shift left decimal number with carryp = str;digcnt = DECIMAL_DIGITS - maxdig;do {carry = (*p<<1) + carry;if (carry >= 10) {*p-- = carry - 10;carry = 1;if (digcnt == 1) {maxdig--;digcnt++;}}else {*p-- = carry;carry = 0;}} while(--digcnt);} while(--bitcnt);// Transform BCD to ASCIIdigcnt = DECIMAL_DIGITS;do *str-- += '0'; while (--digcnt);}void setup() {Serial.begin(9600);}void loop() {unsigned long bin;char strnum[DECIMAL_DIGITS + 1];strnum = 0; // End of charfor(bin=0; bin<100000; bin++) {bin32_to_str(strnum, bin);Serial.println(strnum);}}It's Arduino-ready, but it's easy to port it to the PIC.Another one for 16bits:Code: (c)#define BINARY_DIGITS 16#define DECIMAL_DIGITS 5void bin16_to_str(char *str, unsigned int bin) {char maxdig, digcnt, bitcnt;static char *p, carry;// Clear stringp = str;digcnt = DECIMAL_DIGITS + 1;do { *p++ = 0; } while (--digcnt);// Transform binary to BCDbitcnt = BINARY_DIGITS;maxdig = (DECIMAL_DIGITS - 1);str += (DECIMAL_DIGITS - 1);do {// Shift left binary number with carrycarry = 0;if (bin & (1L<<(BINARY_DIGITS-1)))carry |= 1;bin <<= 1;// Shift left decimal number with carryp = str;digcnt = DECIMAL_DIGITS - maxdig;do {carry = (*p<<1) + carry;if (carry >= 10) {*p-- = carry - 10;carry = 1;if (digcnt == 1) {maxdig--;digcnt++;}}else {*p-- = carry;carry = 0;}} while(--digcnt);} while(--bitcnt);// Transform BCD to ASCIIdigcnt = DECIMAL_DIGITS;do *str-- += '0'; while (--digcnt);}void setup() {Serial.begin(9600);}void loop() {unsigned int bin;char strnum[DECIMAL_DIGITS + 1];strnum = 0; // End of charfor(bin=0; bin<50000; bin++) {bin16_to_str(strnum, bin);Serial.println(strnum);}}

author: jerryk
author_rank: Regular Contributor
timestamp: February 16, 2022, 10:24:59 pm
content: Quote from: DavidAlfa on February 16, 2022, 08:53:23 pmI had to stare at that one for a minute to understand how that workedQuoteIt was a remnant from the sprintf() I originally posted. I deleted it and added your improvements that dropped the program space to 16.7%.QuoteYou hit that one on the head. I have dancing numbers for sure. That is the next task and any hints are more than welcome. I do have timer1 running already to set the blink time for the led. It's triggers an interrupt every ms, counts to 500 and toggles the led. I'm not sure if that is the concept you are talking about when you mentioned "timer". It seems like it should work.Earlier you asked about the voltage divider values. They are in the original code but summarize here:R1 R2Cell 1 = 982 248.2Cell 2 = 663 248.3Cell 3 = 361 248.2Cell 4 = 56.14 247.8All in K ohms! All are fed direct to the 4 ADC inputs after division with no external capacitance or opamp buffering.If you take a balanced 12V 4S battery at 3V per cell this translates to 2.44V (approx) at the ADC.This is how the BMS is designed that I'm working on. I can't change the design. FYI these are expensive $400 - $1000 LFP batteries that I repair/salvage parts from.Again - appreciate the help.Jerry

author: DavidAlfa
author_rank: Super Contributor
timestamp: February 16, 2022, 10:39:08 pm
content: Adding a 100pf capacitor to each ADC pin should provide more stable readings, reducing the high impedance issues.What's the timer input frequency?Ideally it should be free-running with a pre/postscaler so it increases every 1,10,100ms, to whatever resolution you need.Then every function needs a static variable to store last timer value and compare it when entering.If the delay is over, the static var is updated and the function executed.Since the timer will eventually overflow, check that the new value isn't lower than before.So if greater, it's simple as (new-old), but if lower, it's (65536-old) + new.Assuming it's in 16-bit mode.However timer1 capabilities are very limited.Timer0 on ithe other hand, has much better options, 1-32768 prescaler and 1-16 postscaler.If it's for leds and refreshing the lcd (No critical timing) you don't really need very precise timing.Refreshing the lcd every 100 or 102ms won't make any differece, same for the leds.So if you're running at 32MHz, you can use 32768 prescaler and get 1.024ms timing steps, overflowing every 65.5 seconds, which is ok.Don't forget that the 16F18875 ADC has interesting computation options, the low-pass filter would greatly reduce noise.

author: DavidAlfa
author_rank: Super Contributor
timestamp: February 17, 2022, 12:33:40 am
content: This is a more advanced example showing how to program avoiding blocking using timers and state machines:https://pastecode.io/s/o4q28qmnThe ADC never blocks the code execution.When it's time to sample a channel, it will simply check if the time has passed, return or proceed to the next step.When all channels are samples, it waits the specified update time.Same for led and lcd.The static variables might confuse you:They're like local variables, but they hold their values, so when the function is called again, these variables hold their last values.Might be some dumb errors!

author: jerryk
author_rank: Regular Contributor
timestamp: February 17, 2022, 12:52:46 am
content: The clock frequency HFINTOSC set to 16Mhz and timer1 is FOSC/4, prescaler 1:1.I'm looking over the code you posted. It is much easier to follow than the distributed source files in MCC. I'm in no position to be constructively critical of it, I just find it easier to understand as you presented. It will take a moment to firm up my grasp of what you posted.Thanks! - Jerry