How to check loss of precision using pre-processor?

Go To Last Post
10 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

This should be a common task, but I couldn't find any information in the forum (quick search only).

In my C code I declare a timer overflow frequency using:

#define TMR0_OVF_FREQ (F_CPU/(256.0*256.0))
...
const uint8_t TMR0_TICKS_PER_SECOND = (uint8_t) TMR0_OVF_FREQ;	// Loss of precision here

This constant is an integer, so I want to use a pre-processor directive to check that the loss of precision is acceptable:

const uint8_t TMR0_ERROR = (uint8_t) (100.0 - 100.0*(((uint8_t) TMR0_OVF_FREQ)/TMR0_OVF_FREQ));
#if (TMR0_ERROR > 5)
  # warning "Timer0 error is larger than 5%!"
#endif

But it doesn't seem to work. It compiles, but it will never generate a warning, even if I change the condition to > 0 and set the TMR0_ERROR to 1.

Any ideas?

/Jakob Selbing

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Assuming that F_CPU is an integer constant

#define TMR0_ERROR (100*(F_CPU/65536.0 - F_CPU/65536uL))
#if (TMR0_ERROR > 5)
...

If you had #defined F_CPU as a float constant, you would need to cast:

#define TMR0_ERROR (100*(F_CPU/65536.0 - (unsigned long)F_CPU/65536uL))
#if (TMR0_ERROR > 5 || TMR0_ERROR < -5)
...

Untested.

CPP needs to be able to evaluate any #if expressions.

David.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

david.prentice wrote:
Assuming that F_CPU is an integer constant

#define TMR0_ERROR (100*(F_CPU/65536.0 - F_CPU/65536uL))
#if (TMR0_ERROR > 5)
...

If you had #defined F_CPU as a float constant, you would need to cast:

#define TMR0_ERROR (100*(F_CPU/65536.0 - (unsigned long)F_CPU/65536uL))
#if (TMR0_ERROR > 5 || TMR0_ERROR < -5)
...

Untested.

CPP needs to be able to evaluate any #if expressions.

David.

Thansk for the feedback. Unfortunately gcc fails:

../main.c:45:6: error: floating constant in preprocessor expression
../main.c:45:6: error: division by zero in #if

After looking around a little, I realized that GCC pre-processor does not support floating-point values in #if-expressions at all. I'll have to make some work-around instead.

EDIT: this is a working solution. It requires defining the error for each possible choice of F_CPU. Not great, but it does the job.

// 1 MHz -> 15.258 ticks per second, (1 -  15/15.258) = 0.01696  = 17/1000
// 8 MHz -> 122.07 ticks per second, (1 - 122/122.07) = 0.000576 =  1/1000
#if F_CPU == 1000000
	#define TMR0_ERROR_THOUSANDS 17 
#elif F_CPU == 8000000
	#define TMR0_ERROR_THOUSANDS 1
#else
	#error "Timer0 error not defined for this F_CPU, please update table above."
#endif

#if (TMR0_ERROR_THOUSANDS > 10)
	#error "Timer0 error is too large (> 1%)!"
#endif

/Jakob Selbing

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Maybe check in C itself rather than CPP? See, for example, https://www.avrfreaks.net/index.p...

Eugene

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

ezharkov wrote:
Maybe check in C itself rather than CPP? See, for example, https://www.avrfreaks.net/index.p...

Eugene

That is a good idea, thanks!. I just didn't think of that.

/Jakob Selbing

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

OK, I read up on static assertion a little. This is the code I use now. I use a macro to make code more readable. Note that you need the compiler option "-std=c++0x" to support static assertion.

Also note the built-in (compile-time) round function.

// Static assertion for checking error in float-to-integer conversion 
// dbl    = floating-point value to check
// flimit = max relative error, e.g. 0.01 for 1%
// msg    = error message to print if assertion fails
// NOTE: static_assert requires GCC option "-std=c++0x" 
#define FLOAT2INT_ASSERT(dbl, flimit, msg) static_assert( (dbl - (int)dbl) < flimit*dbl, msg)
...

// Timer0 overflow frequency
#define TMR0_OVF_FREQ (F_CPU/(256.0*256.0))
// NOTE: use built-in round function to get compile-time 
//       rounding (insane amounts of code otherwise).
const uint8_t	TMR0_TICKS_PER_SECOND  = (uint8_t) __builtin_round(TMR0_OVF_FREQ);
FLOAT2INT_ASSERT(TMR0_OVF_FREQ, 0.01, "Timer0 error greater than 1%!");

/Jakob Selbing

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Take a look at the header file util/setbaud.h as this check for a specified tolerance.

Hope that helps.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I'm just a little confused - why do you think you need to use floating point for F_CPU? The F_CPU value is commonly defined as an unsigned long (e.g., 8000000UL). Do all of the math in integer:

#define TMR0_OVF_FREQ (F_CPU/(256UL*256UL))
#if (TMR0_ERROR > 5)
  # warning "Timer0 error is larger than 5%!"
#endif 
...
const uint8_t TMR0_TICKS_PER_SECOND = (uint8_t) TMR0_OVF_FREQ;

Second, with an 8-bit integer, you only ever get 5, 4, 7, 150, ... If you want 5.2, 4.8, and so on, try scaled integers:

#define TMR0_SCALE 10UL
#define TMR0_OVF_FREQ ((TMR0_SCALE*F_CPU)/(256UL*256UL))
#if (TMR0_ERROR > (5*TMR0_SCALE))
  # warning "Timer0 error is larger than 5%!"
#endif 
...
const uint8_t TMR0_TICKS_PER_SECOND = (uint8_t) TMR0_OVF_FREQ;

When you use uint8_t TMR0_TICKS_PER_SECOND you will need to remember it is in units of TMR0_SCALE.

Stu

Engineering seems to boil down to: Cheap. Fast. Good. Choose two. Sometimes choose only one.

Newbie? Be sure to read the thread Newbie? Start here!

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

stu_san wrote:
I'm just a little confused - why do you think you need to use floating point for F_CPU? The F_CPU value is commonly defined as an unsigned long (e.g., 8000000UL). Do all of the math in integer:
#define TMR0_OVF_FREQ (F_CPU/(256UL*256UL))
#if (TMR0_ERROR > 5)
  # warning "Timer0 error is larger than 5%!"
#endif 
...
const uint8_t TMR0_TICKS_PER_SECOND = (uint8_t) TMR0_OVF_FREQ;

Second, with an 8-bit integer, you only ever get 5, 4, 7, 150, ... If you want 5.2, 4.8, and so on, try scaled integers:

#define TMR0_SCALE 10UL
#define TMR0_OVF_FREQ ((TMR0_SCALE*F_CPU)/(256UL*256UL))
#if (TMR0_ERROR > (5*TMR0_SCALE))
  # warning "Timer0 error is larger than 5%!"
#endif 
...
const uint8_t TMR0_TICKS_PER_SECOND = (uint8_t) TMR0_OVF_FREQ;

When you use uint8_t TMR0_TICKS_PER_SECOND you will need to remember it is in units of TMR0_SCALE.

Stu

Could you please show all the code involved? You didn't specify what TMR0_ERROR is.

And who said you need floating-point for F_CPU?

What I want is to check the relative error when a fractional number (typically a timer overflow frequency) is truncated to an integer number.

As I wrote in my first post, this code does not work (i.e. it never produces a warning when I build it):

#define TMR0_OVF_FREQ (F_CPU/(256.0*256.0)) 
const uint8_t TMR0_TICKS_PER_SECOND = (uint8_t) TMR0_OVF_FREQ;   // Loss of precision here
const uint8_t TMR0_ERROR = (uint8_t) (100.0 - 100.0*(((uint8_t) TMR0_OVF_FREQ)/TMR0_OVF_FREQ)); 
#if (TMR0_ERROR > 5) 
  # warning "Timer0 error is larger than 5%!" 
#endif 

However, not even this code produces any warnings:

const uint8_t TMR0_ERROR = 1;
#if (TMR0_ERROR > 0) 
  # warning "Timer0 error is larger than 5%!" 
#endif 

As I understand it, the preprocessor does not know about program variables. Instead it interprets "TMR0_ERROR" as an undefined macro whose value is zero:

const uint8_t TMR0_ERROR = 1;
#if (TMR0_ERROR == 0) 
  # warning "Timer0 error is larger than 5%!" 
#endif
----------------------
../main.h:87:5: warning: #warning "Timer0 error is larger than 5%!"

So that was ONE of the problems with my original code. The static assertion solution is much more fit for this problem because it handles floating-points, but if you still think you have a working preprocessor solution, please share it with us (all of it ;-)).

EDIT: I just realized that the static assertion solution I stated is not entirely correct since it checks truncation but my code uses built-in rounding... :/

/Jakob Selbing

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

howardwalker wrote:
Take a look at the header file util/setbaud.h as this check for a specified tolerance.

Hope that helps.

Thanks, that was very helpful. After figuring out what the setbaud.h code actually does, I changed my error-checking code to this:

#define TMR0_DIVISOR (256UL*256UL)
#define TMR0_OVF_FREQ ((F_CPU + (TMR0_DIVISOR/2))/TMR0_DIVISOR)

#if TMR0_OVF_FREQ*TMR0_DIVISOR*100 < F_CPU*99
# warning "Timer0 error greater than -1%"
#endif
#if TMR0_OVF_FREQ*TMR0_DIVISOR*100 > F_CPU*101
# warning "Timer0 error greater than 1%"
#endif

It's so easy once you see it :oops:

It seems to work correctly, printing warnings for some values of F_CPU (1 MHz => -1.7% relative error) and no warning for other (8 MHz => 0.058% relative error).

/Jakob Selbing