1

I am writing a class to setup a serial port on an AVR microcontroller. I have a template function that takes as parameters a cpu clock value and a desired baud rate, does a quick calculation, verifies if the actual value is within 1.5% margin from the desired value with static asserts, then returns the actual value to set inside an 8 bit register. I need to use std::round and its return value needs to be constexpr to have everything evaluated at compile time. This is the problematic bit:

#include <cmath>

template<int c, int b>
constexpr int UBRRValue() {
   // return the value for UBRR register to get close to the desired baud rate
   // avoid integer division
    return std::round( static_cast<float>(c) / ( 16 * b ) - 1 );
}

int main() {
    constexpr auto val = UBRRValue<2000,25>();
    return val;
}

This works fine for x86 on compiler explorer, it returns 4. On AVR there is no cmath, float round(float) is defined in math.h and implemented in assembly, so probably not constexpr. After a quick search I found this: https://stackoverflow.com/a/24348037/11221049 I made a few adjustments, to then have gcc point out that this function is non-constexpr. I made it constexpr, but then its result will never be constexpr because it requires access to a union member that has not been initialized. Union tricks are not constexpr. So... is it possible to make a constexpr round function (knowing that anything from math.h is written directly in assembly)? How is it done in gnu libc++?

fmiz
  • 43
  • 1
  • 5

2 Answers2

2

What you are trying to compute is the correctly rounded result of (c / (16 * b)) - 1. You are casting to float to avoid integer division, but this is almost pointless if you're going to round afterwards anyway.

Notice that we can move the -1 outside the rounding safely (will only change the result if you were discarding the -1 due to lack of float precision, which you don't seem to intend). So all we need is the correctly rounded result of c / (16*b). If we do this as integer division we get the rounded-down result. We can get a midway-rounded result by just adding half the divisor to the dividend (assuming that both are positive):

template<int c, int b>
constexpr int UBRRValue() {
   // return the value for UBRR register to get close to the desired baud rate
    return (c + 8*b) / (16 * b) - 1;
}

Here are some test cases it passes: https://godbolt.org/z/Va6qDT

Max Langhof
  • 23,383
  • 5
  • 39
  • 72
  • 2
    I'd get rid of the `- 1` and change the plus to a minus. Also, using regular arguments instead of template parameters seems to work just fine since this thing can be evaluated at compile time anyway. https://godbolt.org/z/-8Ld9H – David Grayson Mar 27 '19 at 17:41
  • Unfortunately I can't use regular arguments, I've made a small pile of template functions that use the intermediate results as parameters, this is why I need it it be constexpr. I would need to discard constexpr and templates in all the functions I have. This is the whole thing, it is ugly: https://godbolt.org/z/oJJQr5 – fmiz Mar 27 '19 at 18:57
1

Rounding floating point values can always be done by simply adding or subtracting 0.5 and then casting back to integer. There is no need to invoke anything from std:: namespace.

constexpr int round(double x) {
  return (x >= 0.0) ? int(x + 0.5) : int(x - 0.5);
}

constexpr int UBRRValue(int c, int b) {
  return round(static_cast<double>(c) / (16 * b) - 1);
}

int main() {
  constexpr auto val = UBRRValue(2000, 25);
  return val;
}

In case you're sure that the functions are always const-evaluated you can safely use doubles instead of floats since they won't end up in flash memory anyway.

/edit As mentioned in the comments the claim that this "always" works isn't true. It's sufficient for this situation however as the baudrate register will be neither negative nor 0.

Vinci
  • 1,382
  • 10
  • 12
  • "Rounding floating point values can always be done by simply adding or subtracting 0.5 and then casting back to integer" - No! That's *not* correct. There are multiple situations where that will produce the wrong answer. For example; negative numbers, numbers *just* below `0.5` and more. `std::round` (and friends) does it right, adding `0.5` and then truncating to `int` does *not*. – Jesper Juhl Mar 27 '19 at 17:23
  • Thats true yes. Feel free to add further checks to prevent that. In this case you're safe though because the baudrate register won't be 0. – Vinci Mar 27 '19 at 17:26
  • @JesperJuhl Technically correct, irrelevant in this concrete scenario. I guess the answer should discuss that/not make such broad claims. – Max Langhof Mar 27 '19 at 17:26