Datatype selection

Status
Not open for further replies.

dauntless89

Well-known member
I've been reviewing datatype standards for a couple projects I'm working on, and have a couple points I could use guidance on. The current project is actually based on a Due but this is more for general programming knowledge and will also pertain to the T3.6 I'm using in another project until the T4 comes out. Both are 32-bit architectures so it should apply to both.

In the Arduino library, "int" and "long" are both 32-bit containers on a Due. Most of my variables can be stored in a 16-bit container, but to get a 16-bit container, you must use "short." A lot of example code found online subscribes to a "use the smallest container you can get away with" doctrine, but this page cautions that using a smaller container than the native architecture can cause delays. Is this true all of the time, or just with certain operations? Should I generally use the native container size until I start running out of space?

The other thing I'm curious about is that I have three routines that measure pulse frequencies by measuring the duration between pulses. Precision requires using micros, and if I use a 32-bit container for these variables, I'll get a glitch every 72 minutes as the counter rolls over which will require manual correction of the datalog after the fact. If I use a 64-bit container, it can store something like 600,000 years worth of microseconds, but how bad will it slow the program down on a 32-bit processor?

Thanks,
Tony
 
really? 600,000 years?

just define your datatypes as needed
uint32_t -unsigned int 32 bit
int16_t -signed 16bit int
makes it portable accross other microcontrollers
 
I'd echo tonton's suggestion above - I got into the habit of using explicit datatypes as he's described above some time ago, and long gone are my days of worrying about cross-platform compatibility and/or forgetting what size different data types are on different platforms.

To get some accurate numbers of how much longer it would take, my suggestion would be to just knock up a sketch to test it.
 
A uint64_t would hold 18,446,744,073,709,551,615 micros, or 584,942.4 years. A 36-bit value would be plenty as I don't expect more than 12 hours of uptime at a go, but of course no such thing exists.

The superiority of explicit datatypes seemed like a no-brainer from that small amount of research.

I can test the computational time if I need to, but I'd rather not spend the time on it if it's likely to not make a difference. Here is theory of operation code for one of the sensors. Maybe this will help shed some light:

Code:
int RPMInterruptPin = 53;
volatile unsigned long CurrRPMPulse = 0;
volatile bool RPMCalcReq = false;
unsigned long LastRPMPulse = 0;
unsigned int  CurrentRPM = 0; 
unsigned long PrintTimer = 0;
unsigned long AvgRPM = 0;
byte         RPMArrayPtr = 0;
const byte   RPMArraySize = 8;
unsigned int RPMArray[RPMArraySize];
unsigned long LastRPMUpdate = 0;
unsigned int RPMTimeout = 250; //60 rpm cutout

void setup() 
{                
  Serial.begin(115200);
  pinMode(RPMInterruptPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(RPMInterruptPin), ReadRPMPulse, FALLING);
}

void loop()
{ 
 if (RPMCalcReq == true) {CalcRPM();}
 if ((millis() >= (LastRPMUpdate + RPMTimeout)) && (AvgRPM != 0)) {AvgRPM = 0;}
 if (millis() >= (PrintTimer + 100)) {Serial.print("RPM = "); Serial.println(AvgRPM); PrintTimer = millis();}
}


void ReadRPMPulse()
{
  CurrRPMPulse = micros();
  RPMCalcReq = true;
}

void CalcRPM()
{
  noInterrupts();
  CurrentRPM = (((1000000 / (CurrRPMPulse - LastRPMPulse)) *60) / 4); //simplify this after verification
  LastRPMPulse = CurrRPMPulse;
  RPMCalcReq = false;
  LastRPMUpdate = millis();
  AverageRPM();
  interrupts();
}

void AverageRPM()
{
  int RPMAvgVar = 0;
  RPMArray[RPMArrayPtr] = CurrentRPM;
  RPMArrayPtr = RPMArrayPtr + 1;
  if (RPMArrayPtr >= RPMArraySize) {RPMArrayPtr = 0;}
  for (int i=0; i<RPMArraySize; i++)
    {RPMAvgVar = RPMAvgVar + RPMArray[i];}
  AvgRPM = round(RPMAvgVar / RPMArraySize);
}

As I mentioned before, there will be three of these in the final program. They will reach maximums of 240Hz, 65Hz, and 1.5-3.5kHz respectively. Each routine is similar, the only differences are in the array sizes which will be tuned as needed to give consistent, responsive outputs. There's other stuff going on in the background, namely reading 5 analog inputs (with the same array averaging) at a planned 25Hz sample rate and drawing to a character LCD. The occasional datalogging will be done using the serial monitor.

With a program along these lines, would it be worth my time to test the computational speeds? Assuming the final program doesn't have timing issues.

Thanks,
Tony
 
Why not use the input capture feature of one of the FTMs? These have only a 16bit counter, but there is an overflow interrupt which allows to increment a 32bit value, so that you had a total of 48bit resolution. And these work mostly as autonomous sub system without eating CPU cycles except for the capture and the overflow interrupts, so that the Teensy could do more meaningful things while the pulse capture is done in the background. Example code can be found in the FreqMeasureMulti And in the PulsePosition libraries.
 
This specific project is using a Due, but that looks like a good idea for the T3.6/T4 projects, which will need to read pulse signals as well.
 
nothing wrong with using millis(), rollovers wont affect it if you do it right, however, not so sure about unsigned int's, better switch them to longs, or unsigned x ---> uint32_t, which should be used for millis() ;)
 
Changing to explicits is the plan, though as I mentioned "int" and "long" are both 32-bit types on this microcontroller. Regardless, none of the ints in that example will be more than a 4-digit number. Which brings me back to the first point regarding whether or not a 16-bit type is actually saving resources with a 32-bit processor.

Though, as I'm continuing to think about it, using a 64-bit type for timestamping the pulses is probably dumb when I could just have CalcRPM() check for CurrRPMPulse being > LastRPMPulse and if not, simply pass CurrRPMPulse into LastRPMPulse and then discard it without a calculation.

Anyway, I'm guessing null loops are what I would want to use to benchmark operations with different-sized datatypes? Have it add, subtract, multiply, divide each size of type, say 10,000 times and dump the timing results to the serial monitor?
 
yes int is 32bits, but signed, millis uses unsigned, therefore the limits are different and wont protect you from rollover
 
I don't understand. Everything millis() is used with is an unsigned long. The only standard "int" in there is the pin declaration and there are no signed "long" variables at all.
 
rpmtimeout,rpmarray,currentrpm, int,int,int —> int == int32_t != uint32_t
just sayin, cuz rpmtimeout is being used for millis() time check
:)
 
sorry your right, skimming from phone too long :) but yeah depending on other mcus that may end up being 16bit as well
 
Should I generally use the native container size until I start running out of space?
You should be able to estimate your data usage, and as long as it is expected to be well below the memory limit, you should just use the native container sizes as-is.

If you have lots of data than you can start to store booleans as bit fields, etc.
 
Interesting. I will be well below whatever the Due's memory limit is (don't recall and I'm not going to look it up right now. 512kb?).

My curiosity has been piqued enough to benchmark the hardware as mentioned previously. I found this other thread that talks about similar testing. I figured I could start with the sketch attached to that first post and modify/expand it to cover different operations across all the explicit container sizes. This isn't a high priority at the moment, the machine my Due is interfaced with needs to be online weeks ago, but this research will be beneficial to all my currently-planned projects.

If anyone cares to take a look at that sketch, what recommendations would you have for when I'm able to start messing with it?
 
8 and 16 bit variables are often slightly slower than full 32 bit variables on these chips. The resisters are always 32 bits. Sometimes the compiler is forced to add instructions to truncate results to less than 32 bits.
 
Status
Not open for further replies.
Back
Top