diff options
-rw-r--r-- | example_avr_double/Makefile | 22 | ||||
-rw-r--r-- | example_avr_double/README.txt | 22 | ||||
-rw-r--r-- | example_avr_double/decode_double.c | 33 | ||||
-rw-r--r-- | example_avr_double/double_conversion.c | 123 | ||||
-rw-r--r-- | example_avr_double/double_conversion.h | 26 | ||||
-rw-r--r-- | example_avr_double/doubleproto.proto | 13 | ||||
-rw-r--r-- | example_avr_double/encode_double.c | 25 | ||||
-rw-r--r-- | example_avr_double/test_conversions.c | 56 |
8 files changed, 320 insertions, 0 deletions
diff --git a/example_avr_double/Makefile b/example_avr_double/Makefile new file mode 100644 index 0000000..74300fc --- /dev/null +++ b/example_avr_double/Makefile @@ -0,0 +1,22 @@ +CFLAGS=-Wall -Werror -I .. -g -O0 +DEPS=double_conversion.c ../pb_decode.c ../pb_decode.h ../pb_encode.c ../pb_encode.h ../pb.h + +all: run_tests + +clean: + rm -f test_conversions encode_double decode_double doubleproto.pb.c doubleproto.pb.h + +test_conversions: test_conversions.c double_conversion.c + $(CC) $(CFLAGS) -o $@ $^ + +%: %.c $(DEPS) doubleproto.pb.h doubleproto.pb.c + $(CC) $(CFLAGS) -o $@ $< double_conversion.c ../pb_decode.c ../pb_encode.c doubleproto.pb.c + +doubleproto.pb.c doubleproto.pb.h: doubleproto.proto ../generator/nanopb_generator.py + protoc -I. -I../generator -I/usr/include -odoubleproto.pb $< + python ../generator/nanopb_generator.py doubleproto.pb + +run_tests: test_conversions encode_double decode_double + ./test_conversions + ./encode_double | ./decode_double + diff --git a/example_avr_double/README.txt b/example_avr_double/README.txt new file mode 100644 index 0000000..0090d72 --- /dev/null +++ b/example_avr_double/README.txt @@ -0,0 +1,22 @@ +Some processors/compilers, such as AVR-GCC, do not support the double +datatype. Instead, they have sizeof(double) == 4. Because protocol +binary format uses the double encoding directly, this causes trouble +if the protocol in .proto requires double fields. + +This directory contains a solution to this problem. It uses uint64_t +to store the raw wire values, because its size is correct on all +platforms. The file double_conversion.c provides functions that +convert these values to/from floats, without relying on compiler +support. + +To use this method, you need to make two modifications to your code: + +1) Change all 'double' fields into 'fixed64' in the .proto. + +2) Whenever writing to a 'double' field, use float_to_double(). + +3) Whenever reading a 'double' field, use double_to_float(). + +The conversion routines should be as accurate as the float datatype can +be. Furthermore, they should handle all special values (NaN, inf, denormalized +numbers) correctly. There are testcases in test_conversions.c. diff --git a/example_avr_double/decode_double.c b/example_avr_double/decode_double.c new file mode 100644 index 0000000..5802eca --- /dev/null +++ b/example_avr_double/decode_double.c @@ -0,0 +1,33 @@ +/* Decodes a double value into a float variable. + * Used to read double values with AVR code, which doesn't support double directly. + */ + +#include <stdio.h> +#include <pb_decode.h> +#include "double_conversion.h" +#include "doubleproto.pb.h" + +int main() +{ + uint8_t buffer[32]; + size_t count = fread(buffer, 1, sizeof(buffer), stdin); + pb_istream_t stream = pb_istream_from_buffer(buffer, count); + + AVRDoubleMessage message; + pb_decode(&stream, AVRDoubleMessage_fields, &message); + + float v1 = double_to_float(message.field1); + float v2 = double_to_float(message.field2); + + printf("Values: %f %f\n", v1, v2); + + if (v1 == 1234.5678f && + v2 == 0.00001f) + { + return 0; + } + else + { + return 1; + } +} diff --git a/example_avr_double/double_conversion.c b/example_avr_double/double_conversion.c new file mode 100644 index 0000000..cf79b9a --- /dev/null +++ b/example_avr_double/double_conversion.c @@ -0,0 +1,123 @@ +/* Conversion routines for platforms that do not support 'double' directly. */ + +#include "double_conversion.h" +#include <math.h> + +typedef union { + float f; + uint32_t i; +} conversion_t; + +/* Note: IEE 754 standard specifies float formats as follows: + * Single precision: sign, 8-bit exp, 23-bit frac. + * Double precision: sign, 11-bit exp, 52-bit frac. + */ + +uint64_t float_to_double(float value) +{ + conversion_t in; + in.f = value; + uint8_t sign; + int16_t exponent; + uint64_t mantissa; + + /* Decompose input value */ + sign = (in.i >> 31) & 1; + exponent = ((in.i >> 23) & 0xFF) - 127; + mantissa = in.i & 0x7FFFFF; + + if (exponent == 128) + { + /* Special value (NaN etc.) */ + exponent = 1024; + } + else if (exponent == -127) + { + if (!mantissa) + { + /* Zero */ + exponent = -1023; + } + else + { + /* Denormalized */ + mantissa <<= 1; + while (!(mantissa & 0x800000)) + { + mantissa <<= 1; + exponent--; + } + mantissa &= 0x7FFFFF; + } + } + + /* Combine fields */ + mantissa <<= 29; + mantissa |= (uint64_t)(exponent + 1023) << 52; + mantissa |= (uint64_t)sign << 63; + + return mantissa; +} + +float double_to_float(uint64_t value) +{ + uint8_t sign; + int16_t exponent; + uint32_t mantissa; + conversion_t out; + + /* Decompose input value */ + sign = (value >> 63) & 1; + exponent = ((value >> 52) & 0x7FF) - 1023; + mantissa = (value >> 28) & 0xFFFFFF; /* Highest 24 bits */ + + /* Figure if value is in range representable by floats. */ + if (exponent == 1024) + { + /* Special value */ + exponent = 128; + } + else if (exponent > 127) + { + /* Too large */ + if (sign) + return -INFINITY; + else + return INFINITY; + } + else if (exponent < -150) + { + /* Too small */ + if (sign) + return -0.0f; + else + return 0.0f; + } + else if (exponent < -126) + { + /* Denormalized */ + mantissa |= 0x1000000; + mantissa >>= (-126 - exponent); + exponent = -127; + } + + /* Round off mantissa */ + mantissa = (mantissa + 1) >> 1; + + /* Check if mantissa went over 2.0 */ + if (mantissa & 0x800000) + { + exponent += 1; + mantissa &= 0x7FFFFF; + mantissa >>= 1; + } + + /* Combine fields */ + out.i = mantissa; + out.i |= (uint32_t)(exponent + 127) << 23; + out.i |= (uint32_t)sign << 31; + + return out.f; +} + + diff --git a/example_avr_double/double_conversion.h b/example_avr_double/double_conversion.h new file mode 100644 index 0000000..62b6a8a --- /dev/null +++ b/example_avr_double/double_conversion.h @@ -0,0 +1,26 @@ +/* AVR-GCC does not have real double datatype. Instead its double + * is equal to float, i.e. 32 bit value. If you need to communicate + * with other systems that use double in their .proto files, you + * need to do some conversion. + * + * These functions use bitwise operations to mangle floats into doubles + * and then store them in uint64_t datatype. + */ + +#ifndef DOUBLE_CONVERSION +#define DOUBLE_CONVERSION + +#include <stdint.h> + +/* Convert native 4-byte float into a 8-byte double. */ +extern uint64_t float_to_double(float value); + +/* Convert 8-byte double into native 4-byte float. + * Values are rounded to nearest, 0.5 away from zero. + * Overflowing values are converted to Inf or -Inf. + */ +extern float double_to_float(uint64_t value); + + +#endif + diff --git a/example_avr_double/doubleproto.proto b/example_avr_double/doubleproto.proto new file mode 100644 index 0000000..d8b7f2d --- /dev/null +++ b/example_avr_double/doubleproto.proto @@ -0,0 +1,13 @@ +// A message containing doubles, as used by other applications. +message DoubleMessage { + required double field1 = 1; + required double field2 = 2; +} + +// A message containing doubles, but redefined using uint64_t. +// For use in AVR code. +message AVRDoubleMessage { + required fixed64 field1 = 1; + required fixed64 field2 = 2; +} + diff --git a/example_avr_double/encode_double.c b/example_avr_double/encode_double.c new file mode 100644 index 0000000..cd532d4 --- /dev/null +++ b/example_avr_double/encode_double.c @@ -0,0 +1,25 @@ +/* Encodes a float value into a double on the wire. + * Used to emit doubles from AVR code, which doesn't support double directly. + */ + +#include <stdio.h> +#include <pb_encode.h> +#include "double_conversion.h" +#include "doubleproto.pb.h" + +int main() +{ + AVRDoubleMessage message = { + float_to_double(1234.5678f), + float_to_double(0.00001f) + }; + + uint8_t buffer[32]; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer)); + + pb_encode(&stream, AVRDoubleMessage_fields, &message); + fwrite(buffer, 1, stream.bytes_written, stdout); + + return 0; +} + diff --git a/example_avr_double/test_conversions.c b/example_avr_double/test_conversions.c new file mode 100644 index 0000000..22620a6 --- /dev/null +++ b/example_avr_double/test_conversions.c @@ -0,0 +1,56 @@ +#include "double_conversion.h" +#include <math.h> +#include <stdio.h> + +static const double testvalues[] = { + 0.0, -0.0, 0.1, -0.1, + M_PI, -M_PI, 123456.789, -123456.789, + INFINITY, -INFINITY, NAN, INFINITY - INFINITY, + 1e38, -1e38, 1e39, -1e39, + 1e-38, -1e-38, 1e-39, -1e-39, + 3.14159e-37,-3.14159e-37, 3.14159e-43, -3.14159e-43, + 1e-60, -1e-60, 1e-45, -1e-45, + 0.99999999999999, -0.99999999999999, 127.999999999999, -127.999999999999 +}; + +#define TESTVALUES_COUNT (sizeof(testvalues)/sizeof(testvalues[0])) + +int main() +{ + int status = 0; + int i; + for (i = 0; i < TESTVALUES_COUNT; i++) + { + double orig = testvalues[i]; + float expected_float = (float)orig; + double expected_double = (double)expected_float; + + float got_float = double_to_float(*(uint64_t*)&orig); + uint64_t got_double = float_to_double(got_float); + + uint32_t e1 = *(uint32_t*)&expected_float; + uint32_t g1 = *(uint32_t*)&got_float; + uint64_t e2 = *(uint64_t*)&expected_double; + uint64_t g2 = got_double; + + if (g1 != e1) + { + printf("%3d double_to_float fail: %08x != %08x\n", i, g1, e1); + status = 1; + } + + if (g2 != e2) + { + printf("%3d float_to_double fail: %016llx != %016llx\n", i, + (unsigned long long)g2, + (unsigned long long)e2); + status = 1; + } + } + + return status; +} + + + + |