(осторожно, шестнадцатеричные представления чисел!)Знаковые (signed) числа хранятся в компьютере в т.н. "дополнительном коде".
Я хранил массивы данных разных типов. Но для передачи их из микроконтроллера мне показалось логичным их все представлять как один тип. uint32_t (беззнаковый, 32-битный). Естественно, я просто записывал знаковые числа по нужному адресу, на который указывал беззнаковый указатель. В компьютере я всё проделывал наоборот и получал своё изначальное число.
Всё было хорошо, пока я не стал вводить усреднение: я получаю с датчика несколько чисел, складываю их, а потом делю на количество. Получаю вместо 10 чисел -- одно. И отправляю в компьютер только его.
Так вот, когда среднее должно было равняться примерно нулю, я получал в массиве данных непонятный выброс -- всё время разной величины. В остальных случаях усреднение работало правильно.
Датчики возвращали только 18 бит в дополнительном коде (потому что датчики 18-разрядные). Старшие биты 32-разрядных чисел были забиты нулями.
Почему дополнительный код для отрицательных чисел так часто используется? У него есть фишка: сложение и вычитание знаковых и беззнаковых чисел происходит одинаковым образом. Поэтому храня и даже складывая знаковые числа я ошибки не совершал. А ошибка была при делении. Делить знаковые числа как беззнаковые нельзя.
Что же происходило?
Пусть есть два числа и мы хотим посчитать среднее. Рассмотрим три случая.
1. Оба числа положительные
Допустим, это числа 16 (0x00000010) и 18 (0x00000012). При сложении (в 32-разрядной сетке) мы получим число 0x00000022. И при делении 0x00000011. Это 17. Всё правильно.
2. Оба числа отрицательные
Пусть теперь оба числа будут отрицательными, 18-разрядными в дополнительном коде, расширенные слева нулями до 32-бит. -16 и -18.
В шестндацатеричном виде это будет 0x0003FFEA и 0x0003FFE8. При сложении это даёт 0x0007FFD2. При делении пополам как беззнакового это даёт 0x0003FFE9. Если взять младшие 18 разрядов и рассмотреть их как знаковое число в дополнительном коде, мы получим как раз -17 (подробный разбор преобразований я не привожу). Пока всё правильно.
3. Одно положительное, другое отрицательное
Когда же результат должен быть около нуля, то часть чисел при усреднении могут быть положительными, а часть отрицательными. И именно этот-то случай и вызывал ошибку. В качестве супер-примера теперь будут числа 1 и -1. Очевидно, в среднем они должны давать ноль. В шестнадцатеричном виде 1 это будет 0x00000001. А -1 (по тем же правилам: дополнительный код 18 бит, расширенный нулями до 32) это будет 0x0003FFFF. Если сложить эти два числа, получается 0x00040000. Деление пополам как беззнакового даёт 0x00020000.
Вот тут я обратное преобразование из дополнительного кода распишу подробно:
В двоичном виде число выглядит так:
0b00000000 00000010 00000000 00000000
Единичный бит означает, что число отрицательное (число 18-разрядное, поэтому старшим следует рассматривать 18-й бит, если младший считать первым). Остальные биты равны нулю. И эти нули надо преобразовать в модуль отрицательного числа. Для этого надо все (младшие) биты инвертировать и прибавить единицу (это правила работы с дополнительным кодом). Результат надо трактовать как беззнаковое число. Получается:
0b00000000 00000001 11111111 11111111
+
0b00000000 00000000 00000000 00000001
=
0b00000000 00000010 00000000 00000000
(кстати, получилось побитово то же, что и было)
В итоге модуль этого числа будет 131072, а знак отрицательный. Т.е. это -131072. А должно было быть 0, вообще-то.
В итоге я стал суммировать и делить в знаковой переменной, а только потом записывать в массив как беззнаковое. И тогда всё стало работать.