Одна и та же структура занимает разное пространство в памяти Uno и NodeMCU ESP8266, что приводит к повреждению данных при передаче через nRF24L01+

Пытаюсь передать данные между платой Arduino Uno на плату NodeMCU ESP8266, используя модуль приемопередатчиков nRF24L01+ и библиотеку RF24 с обеих сторон. Данные, которые я передаю, хранятся в структуре, определенной следующим образом:

// Struct объявлена в обоих кодовых
struct sensorData {
    bool doorOpened;
    int light;
    int temperature;
    int humidity;
    float voltage_battery;
};
// Переменная в коде Uno (исходный узел)
sensorData dataToSend;
// Переменная в коде ESP8266 (узел dest)
sensorData dataToReceive;

Но данные неправильно передаются между двумя платами, и я получаю случайные значения или 0 при попытке отобразить их из ESP8266 (значения правильно отображаются из Uno).

Отображение в формате Uno (используя короткий знак в качестве типа данных, см. Следующие параграфы): Data displayed from Arduino UNO

Отображение в ESP8266 (используя короткий знак в качестве типа данных, см. Следующие параграфы): Data displayed from NodeMCU ESP8266

После некоторого тестирования я обнаружил, что распределение памяти на обеих платах различно, так как Uno занимает 2 байта для хранения int, в то время как ESP8266 занимает 4 байта. Более того, sensorData занимает 11 байтов памяти в Uno (1 для bool, 2 для int и 4 для float, что составляет 11 байт), но занимает 20 байтов памяти в ESP8266 (1 для bool, 4 для int и 4 для float, что НЕ составляет 20 !).

Пытаясь решить эту проблему, я попытался изменить int на int со знаком, byte, short, short со знаком, но проблема передачи остается. При использовании короткого или подписанного короткого (который занимает 2 байта на обеих платах), мои данные о структуре по-прежнему занимают 11 байт памяти в UNO, в то время как на ESP8266 требуется 12 байт памяти. По какой-то причине ESP8266 всегда занимает больше места, чем необходимо для хранения данных датчиков, что, вероятно, приводит к неправильному размещению битов при приеме данных.

Я уверен, что обе платы, радиомодуль и протокол передачи работают, так как я успешно протестировал один и тот же код с меньшей структурой (текст был "Привет, мир!", а cmp увеличивался при каждой передаче):

struct Package {
    char text[16];
    long cmp;
};

Кто-нибудь сталкивался с этой проблемой раньше ? Как я мог это решить ?

, 👍9

Обсуждение

Кстати, я привык к кодированию (в основном на VBA, C++ и python), но я все еще новичок в мире Arduino, поэтому, возможно, мне не хватает чего-то очевидного., @Vincent


5 ответов


Лучший ответ:

12

Есть три основные проблемы, с которыми вам, возможно, придется столкнуться при передаче структур между системами, использующими разные процессоры.

Во-первых, типы данных различаются по размеру в разных архитектурах. Вы можете избежать этого для целых чисел, избегая основных типов C и вместо этого используя типы фиксированного размера из stdint.h/cstdint. Для типов с плавающей запятой это менее проблема, так как большинство систем использует типы IEEE с одинарной и двойной точностью для float и double.

Второй - это заполнение, в соответствии со стандартом C заполнение зависит от реализации. На практике большинство компиляторов вставляют минимальное количество отступов, необходимое для выполнения требований к выравниванию. Точные требования к выравниванию также зависят от реализации, но требование к выравниванию типа всегда зависит от его размера.

Таким образом, вы можете избежать заполнения, создаваемого компилятором на большинстве архитектур, разработав свою структуру, чтобы размещать типы на "естественном" выравнивании (то есть их смещение в структуре кратно их размеру) и иметь размер, кратный наибольшему типу в структуре. Таким образом, вы можете спроектировать свою структуру как.

struct sensorData {
    int8_t doorOpened; //size 1 offset 0
    int8_t spare; //size 1 offset 1
    int16_t light; //size 2 offset 2
    int16_t temperature; //size 2 offset 4
    int16_t humidity; // size 2 offset 6
    float voltage_battery; //size 4 offset 8
};

Другой вариант-использовать упакованные структуры, но при их использовании требуется крайняя осторожность, особенно в C++. Легко создавать выровненные указатели или ссылки.

Последняя проблема-порядок байтов (также известный как endian), если ваши системы используют разные порядки байтов для целочисленных значений или значений с плавающей запятой, вам нужно будет вставить преобразования, чтобы справиться с этим.

,

Интересный. У вас есть пример выровненной структуры, пожалуйста? Будет ли это что-то вроде поплавка после int16_t, так что "размер 4 смещен 2"?, @Eric Duminil

@EricDuminil Все, что нарушает естественное выравнивание, uint8/uint16/uint8 нарушает это, потому что uint16 не выровнен по 16-битной границе., @Dave Newton


2

Esp8266 имеет 32-разрядный микроконтроллер, а uno-8-разрядный микроконтроллер, поэтому их переменные имеют разные требования к хранилищу. Вы должны предусмотреть это в своем коде.

,

Как я могу это сделать ? Должен ли я отформатировать исходные данные, чтобы они были правильно поняты при получении ESP8266 ?, @Vincent

Объявление `подписанная короткая дверь открыта вместо открытая дверь открыта " исправило проблему (так как оба имеют размер 2 байта), но если у вас есть время, мне все еще интересно, как правильно обрабатывать `bool` между 2 микроконтроллерами., @Vincent

Используйте типы с явными размерами (uint8_t, uint16_t и т.д.)., @Mat

Особо обратите внимание, что " int " составляет 16 бит на Arduino Uno, но 32 бита на ESP8266., @PMF

@PMF Я заметил это, поэтому я изменил int на короткий (16 бит на обоих микроконтроллерах)., @Vincent


12

int, longи т.д. Имеют разные размеры, зависящие от компилятора и цели.

Используйте явные размеры, чтобы убедиться, что ваши переменные имеют нужный размер. Например. uint8_t, int16_t или int32_t, ... (как прокомментировал @Mat)

Кроме того, я бы не стал использовать bool (при написании на C), поскольку он на самом деле не является стандартным в C, а размер также зависит от реализации: https://stackoverflow.com/questions/1608318/is-bool-a-native-c-type

Кроме того, существуют различные типы явных размеров: https://stackoverflow.com/questions/5254051/the-difference-of-int8-t-int-least8-t-and-int-fast8-t (но не используйте его здесь, иначе у вас снова возникнет та же проблема :))

,

"bool" в целом подходит; мы обсуждаем C++. Проблема с "bool" заключается в том, что его размер зависит от реализации., @Dave Newton

@DaveNewton, я не знал, что мы говорим о " C++". Однако я оставлю это в своем ответе, @Swedgin

Спасибо за пояснения и ссылки, они весьма полезны ! У меня никогда раньше не было этой проблемы, поэтому я не обращал внимания на размеры экспликации, но с этого момента я буду., @Vincent

@Swedgin Вот что использует экосистема Arduino., @Dave Newton

@DaveNewton, Виноват, я так давно не пользовался экосистемой Arduino, что забыл, что это C++., @Swedgin


9

В дополнение к правилу всегда использовать фиксированные и предсказуемые размеры в разных архитектурах, также рекомендуется упаковывать свои структуры. Это не позволяет компилятору заполнять меньшие переменные пробелами, чтобы при необходимости выровнять их по границе слов архитектуры. Например:

struct Package {
    uint8_t doorOpened;
    uint16_t light;
    int16_t temperature;
    uint8_t humidity;
    float voltage_battery;
} __attribute__((packed));

В зависимости от того, какие инструкции "прямого доступа" доступны в целевой 32-разрядной архитектуре, без упаковки вы можете получить (в худшем случае) эквивалент:

struct Package {
    uint8_t doorOpened;
    // 3 bytes padding
    uint16_t light;
    // 2 bytes padding
    int16_t temperature;
    // 2 bytes padding
    uint8_t humidity;
    // 3 bytes padding
    float voltage_battery;
} __attribute__((packed));

Конечно, в архитектуре может быть достаточно детализированный метод доступа, что означает, что упаковка не имеет значения, но лучше иметь ее и игнорировать, чем не иметь ее и нуждаться в ней.

,

2

По всем причинам, упомянутым в других ответах (размеры разных типов, требования к выравниванию и порядку), часто бывает плохой идеей использовать структуры для данных, которые необходимо отправлять/получать (будь то по сети или хранить в файлах), поскольку совместимость часто является проблемой.

Использование простого массива байтов (т. Е. uint8_t[]) часто является гораздо лучшим решением вместе с функциями для чтения/записи данных определенных типов при определенных смещениях.

Во многих языках более высокого уровня (perl, php, python...) это часто делается с помощью пакета/распаковки или эквивалентов, но, насколько я знаю, прямого эквивалента в C.

Затем вы используете временные переменные, htons/ntohs/htonl/ntohl и memcpy для копирования данных между временными переменными и буфером передачи. Float немного сложнее, потому что, хотя их формат четко определен, их конечное значение не совпадает и может отличаться от целых чисел, но htonf/ntohf встречаются редко. В вашем случае я бы рекомендовал преобразовать, например, в целые числа с коэффициентом 10 (если вам нужна одна цифра) или 100 (если вам нужно 2).

Если вы используете C++, вы можете рассмотреть возможность использования буферов протоколов Google

,