Перейти к содержанию

Почему 255 + 1 даёт 0 в микроконтроллере?

Обновлено Редакция GAW

Переменная типа uint8_t или byte хранит 8 бит, то есть числа от 0 до 255. Сумма 255 + 1 = 256 в двоичном виде занимает 9 бит:

1111 11112+0000 00012=1  0000 00002.1111\,1111_2 + 0000\,0001_2 = 1\;0000\,0000_2.

В регистр помещаются только младшие 8 бит — нули. Девятый бит не пропадает бесследно: процессор кладёт его во флаг переноса C. По описанию команды ADD в справочнике AVR флаг C выставляется при переносе из старшего бита, а флаг Z — когда результат равен нулю. У Cortex-M то же самое описывает функция AddWithCarry в архитектурном руководстве Arm, только регистры там 32-битные.

Беззнаковые и знаковые числа

Один и тот же байт можно читать двумя способами, и переполняется он в разных местах:

ТипДиапазонПереход через границуФлаг
uint8_t, byte0…255255 + 1 → 0C (перенос)
int8_t−128…127127 + 1 → −128V (переполнение)
uint16_t0…65 53565 535 + 1 → 0C

Для знаковых чисел байт 1000 0000₂ — это −128 в дополнительном коде, поэтому 127 + 1 превращается в −128. Чем флаг переноса отличается от флага переполнения, разбирают отдельные термины.

В C ноль появляется при записи

В программе на C или Arduino байт в выражении сначала расширяется до int — это правило целочисленного повышения языка. Поэтому при byte a = 255 условие a + 1 == 0 ложно: в выражении a + 1 равно 256. Ноль получается, только когда результат записывают обратно в байт, например a = a + 1 или a++. Так же 127 + 1 для int8_t сначала даёт 128 в int, а −128 появляется при присваивании.

Где это встречается

  • Цикл for (byte i = 0; i < 300; i++) никогда не закончится: после 255 переменная i снова станет 0, и условие i < 300 всегда истинно.
  • Сумма нескольких показаний АЦП не помещается в uint8_t — накопитель берут 16- или 32-битным.
  • Команда INC у AVR флаг C не трогает: так её удобно использовать как счётчик цикла в многобайтных вычислениях, но перенос после 255 + 1 по флагу C не поймать — только по Z.

Сложение, вычитание и флаги для своих чисел можно проверить в калькуляторе двоичной арифметики, а перевести числа между системами счисления — в конвертере систем счисления.

Похожие вопросы

Как сделать, чтобы счётчик не переполнялся?

Взять тип шире — uint16_t считает до 65 535, uint32_t до 4 294 967 295 — или проверять значение перед прибавлением. В ассемблере перенос виден во флаге C: его прибавляют к старшему байту командой ADC, так складывают числа длиннее одного регистра.

Почему 0 − 1 в байте даёт 255?

Это то же переполнение в обратную сторону: вычитание берёт заём из несуществующего девятого бита, и в регистре остаётся 1111 1111₂ = 255. По справочнику AVR команда SUB при этом выставляет флаг C — он означает, что вычитаемое было больше уменьшаемого.

Какие числа помещаются в int16_t и uint16_t?

В 16 бит помещаются 65 536 значений: у uint16_t это 0…65 535, у int16_t в дополнительном коде — от −32 768 до 32 767. Переполнение наступает на тех же границах, что и у байта, только дальше.

Источники

  1. Microchip. AVR Instruction Set Manual, DS40002198B, 2021 — 6.2 ADD: флаг Z — результат равен 0x00, флаг C — перенос из старшего бита результата; V — переполнение в дополнительном коде; 6.61 INC: прибавляет единицу, флаг C не меняет, чтобы INC можно было использовать как счётчик цикла в многобайтной арифметике; V выставляется, если до операции в регистре было 0x7F; для беззнаковых чисел надёжны только переходы BREQ и BRNE; 6.119 SUB: флаг C выставляется, если вычитаемое по модулю больше уменьшаемого (заём)
  2. Arm. Armv7-M Architecture Reference Manual, ARM DDI 0403E.e — A2.2.1 — функция AddWithCarry(): результат — младшие N бит суммы, carry_out = 1, если беззнаковая сумма не помещается в N бит, overflow — если не помещается знаковая; разрядность регистров Cortex-M — 32 бита

Нашли ошибку?