Установить и извлечь отдельные байты числа (lowByte() и highByte())

Вы можете извлечь

младший (самый правый) байт переменной

или

старший (самый левый) байт слова

с функциями lowByte() и highByte() соответственно (кавычки взяты из справочника Arduino).

Предоставляет ли Arduino способ извлечения любого байта из числа с помощью аналогичной функции?

И предоставляет ли он способ установить какие-либо отдельные байты числа (которые будут аналогами этих функций, если они существуют)?

, 👍1


3 ответа


5

С помощью бинарных операторов можно выполнить любой более сложный обмен байтами.

Извлечение информации

Вам понадобятся некоторые константы, например

MASK         SHIFT                                    
0xFF000000   24       (first byte, most significant)
0x00FF0000   16       (second byte)
0x0000FF00    8       (third byte)
0x000000FF    0       (last byte, least significant)

Вы бы использовали двоичное И исходное значение с помощью MASK

     0x12345678
AND  0x00FF0000
 =   0x00340000

а затем сдвиньте его влево, например

   0x00340000
>> 16
 = 0x00000034

Итак, из исходного значения 0x12345678 вы получили 0x34.

Информация о настройках

Обратное направление также возможно с теми же константами, но с противоположными операторами и в противоположном порядке:

   0x00000034
<< 16
 = 0x00340000

а затем

   0x12005678
OR 0x00340000
 = 0x12345678

Обратите внимание, что операция ИЛИ надежно работает только в том случае, если соответствующие позиции равны 0x00. Это нормально при начальном значении 0x00000000.

Если вы этого не знаете или хотите обработать произвольное число, вы можете ввести промежуточный шаг

    0x12??5678   (?? could be anything)
AND 0xFF00FFFF   (which is the inverse of 0x00FF0000 and can be expressed as ~0x00FF0000)
 =  0x12005678   (whatever ?? was, it'll be cleared out)

Действительный код

Протестировано на Arduino Uno. Тип данных long составляет 32 бита или 4 байта.

Приведенный здесь код может быть не самым эффективным. Опытные разработчики вряд ли будут использовать MASK и SHIFT в качестве массивов, подобных этому. В этом ответе больше внимания уделяется образовательным вопросам.

long MASK[]  = {0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF};
byte SHIFT[] = {24        , 16        , 8         , 0};

// Извлекает байт из большего типа данных
// значение: больший тип данных для извлечения байта
// позиция: 0 на основе числа, 0 = MSB, 3 = LSB
byte getByteAt(long value, byte position)
{
  long result = value & MASK[position];  // бинарное И
  result = result >> SHIFT[position];    // Сдвиг вправо, перемещение всех битов
  byte resultAsByte = (byte) result;     // Преобразование в фактический байт
  return resultAsByte;  
}

// Устанавливает байт в больший тип данных
// значение: больший тип данных, где установить байт
// позиция: 0 на основе числа, 0 = MSB, 3 = LSB
// newPartialValue: вставляемый байт
long setByteAt(long value, byte position, byte newPartialValue)
{
  long result = value & ~MASK[position];                       // очищаем затронутый байт, чтобы он был 0x00
  long valueToSet = (long) newPartialValue << SHIFT[position]; // Сдвиг влево, перемещение всех битов
  result = result | valueToSet;                                // бинарное ИЛИ
  return result;  
}

void setup() {
  Serial.begin(9600);
  Serial.println(getByteAt(0x12345678, 1));       // Печатает 52, что равно 0x34
  Serial.println(setByteAt(0x87654321, 1, 0xBB)); // Печатает -2017770719, что равно 0x87BB4321
}

void loop() {

}
,

Стоит отметить, что значение, с которым вы выполняете операцию «И» для очистки битов перед операцией «ИЛИ», является *обратной маской*. Вы можете использовать ~MASK для его создания., @Majenko

@Majenko: используется и добавлено в примере, @Thomas Weller

Спасибо за подробное объяснение, но я уже знал об этой возможности. Значит, нет встроенного способа сделать это со встроенными функциями?, @LukasFun

Вам не нужно загружать MASK и SHIFT из массива! shift = position * 8 = position << 3 (преобразовать смещение в битах в смещение в байтах. Кроме того, замаскируйте *после* сдвига, чтобы он просто возвращал (uint8_t)(value >> shift);`. например, на Godbolt для Функции получения и установки байтов AVR и ARM cortex-M0: https://godbolt.org/z/2uFXN5 Также включены версии, которые сохраняют/перезагружают и используют индекс байтов, избегая циклов сдвига на AVR, которые могут сдвигать только 1 байт за раз. время. (Публикую ответ, если вернусь к этому.), @Peter Cordes

@PeterCordes: моя главная задача состояла в том, чтобы объяснить это подробно и понятно, наглядно. Я также участвую в кодовом гольфе, так что да, это определенно не самая короткая форма :-) В общем, я не оптимизирую скорость, пока нет требований к скорости., @Thomas Weller

Да, это хорошее объяснение, но написание сверхсложного кода усложняет задачу для будущих читателей *кода*. Кроме того, я только что заметил недостаток: ваши массивы задом наперед! Большинство людей считают «байт 0» целого числа наименее значащим байтом. Ваш код дает наиболее значимое значение, противоположное value >> bitpos. (И противоположно порядку памяти: AVR и ARM имеют обратный порядок байтов). Я думаю, что код в моей ссылке Godbolt очень удобочитаем, например, bitpos = position * 8., @Peter Cordes

Что касается эффективности, небольшие различия, как правило, не имеют значения, но для постоянной времени компиляции ваша версия GetByteAt по-прежнему компилируется примерно в 50 инструкций AVR вместо 1, потому что вы забыли сделать свои массивы const и/или статический. Таким образом, компилятор не может выполнять распространение констант через индексирование массива. (см. вызывающих test_...: https://godbolt.org/z/chtKzJ) Это довольно значительная разница в объеме статического кода для чего-то, что вы ожидаете быть дешевым., @Peter Cordes


1

Заголовок Arduino.h определяет макрос word(high_byte, low_byte). результирующее значение — uint16_t.

,

Спасибо, это отвечает на вопрос о аналогах lowByte() и highByte(). Есть ли какие-либо функции для остальной части моего вопроса?, @LukasFun

@LukasFun: я ничего не нашел. Но обратите внимание, что в Arduino на основе AVR int составляет всего два байта., @Edgar Bonet

Вы должны просто относиться к этим определениям как к примерам, достаточно изучить C/C++, чтобы понять их, и расширить их до того, что вам нужно. Если uint16_t не то, что вам нужно, почему именно uint32_t должно быть вашим решением?, @DataFiddler


1

Есть два альтернативных метода, и оба они могут быть легко использованы "в обратном порядке":

Союзы

union byte_extract {
    uint32_t ival;
    struct {
        uint8_t byte_0; // младший значащий байт
        uint8_t byte_1;
        uint8_t byte_2;
        uint8_t byte_3; // старший байт
    } bval;
};

... later ...

union byte_extract x;

x.ival = 65536 * 99; // меняем на что хочешь
Serial.println(x.bval.byte_2); //3-й байт

Союзы — это способ определить несколько возможных способов интерпретации фрагмента памяти, обычно для экономии памяти, когда вы знаете, что что-то не может быть двумя вещами одновременно. Строго говоря, использовать их таким образом не рекомендуется, но это работает.

Арифметика указателя

uint32_t *xaddr;
uint8_t *xbyteaddr;

uint32_t x = 65536 * 99; // меняем на что хочешь
xaddr = &x;
xbyteaddr = (uint8_t *) xaddr;

Serial.println(xbyteaddr[2]); //3-й байт

Адреса памяти (как на Arduino, так и на ПК) относятся к первому байту переменной. Если у вас есть (uint32_t *), вы будете читать 4 байта, начиная с этого адреса. Но если у вас есть (uint8_t *) или (char *), вы будете читать только 1 байт. Вы можете сдвинуть этот указатель на uint8_t вперед на любое количество байтов.

Извините, если это не совсем работает на Arduino, я тестировал это только на ПК.

,

Такое использование союзов четко определено C99. Тем не менее, *массив* из uint8_t bytes[4] будет более удобочитаемым и позволит индексировать во время выполнения. Ваша версия с арифметикой указателей безопасна только потому, что uint8_t будет unsigned char, а unsigned char* разрешено использовать псевдонимы для чего угодно, не нарушая строгого алиасинга. Обратите внимание, что оба они зависят от порядка байтов., @Peter Cordes