nRF SDK: сериализация BLE Печать
Добавил(а) microsin   

В некоторых примерах из SDK можно увидеть разные версии одного и того же приложения. Например, ble_app_hts представлен в версиях _hci, _spi и _uart. В документации примеров ничего про это не сказано. В чем конкретная разница между ними?

В сущности между этими примерами в контексте радиопротокола разницы нет, кроме так называемой сериализации Bluetooth Low Energy (BLE Serialization) [1]. Это способ разделения функционала, когда приложение Bluetooth работает в одном чипе (Application Chip), и подключается к при этом к другому чипу, который обеспечивает соединение по радио (Connectivity Chip, работающий под управлением BLE SoftDevice).

Подробнее про запуск сериализации приложения см. [2], где объясняется соединение Bluetooth Application Board и BLE Connectivity Board.

Connectivity Chip. Это микросхема серии nRF5, в которую прошито firmware, декодирующие serialized-команды SoftDevice. Connectivity Chip по этим командам управляет соответствующим образом нижележащим BLE SoftDevice firmware, используя аппаратуру чипа nRF5.

Исходный код и файл проекта Connectivity Chip можно найти в каталоге установки SDK examples\ble_central_and_peripheral\ble_connectivity. Пример разделен по отдельным каталогам с суффиксами _hci, _spi, _spi_5W, _uart, соответствующим различным способам последовательного подключения (тип слоя PHY).

Bluetooth Application Chip. Это также микросхема nRF5, которая используется здесь просто как демонстрационное устройство, которое запускает сериализованное приложение, где BLE SoftDevice заменен на кодер команд (Commands Encoder) и декодер событий (Events Decoder). Трафик кодера команд и декодера событий проходит по каналу последовательного обмена данными, с помощью кодеков сериализации (Serialization Codecs). Микросхема nRF5, работающая как Application Chip, не использует BLE. После портирования аппаратного драйвера на выбранный слой физики PHY (Serialization PHY), микросхема nRF5 на стороне приложения предполагается заменить устройством пользователя.

SDK Serialization overview fig01

Рис. 1. Архитектура сериализованного приложения Bluetooth.

Пояснения к рис. 1: SD означает SoftDevice. SD call, SD resp и SD event обозначают вызов, ответ и событие SoftDevice.

В Bluetooth Application Chip двоичный код BLE SoftDevice заменяется кодеком, который реализует SoftDevice API. Все вызовы функций для кодека сериализуются и передаются в BLE Connectivity Chip с помощью драйверов транспортного уровня. Такая конструкция позволяет заменить существующий уровень PHY, такой как UART или SPI, не влияя на кодеки.

BLE Connectivity Chip декодирует сериализованные команды, поступающие с платы приложения (Bluetooth Application Board) по транспортному слою, и выполняет соответствующие им функции в SoftDevice. Любое событие из SoftDevice кодируется кодеком и передается в Bluetooth Application Chip по тому же транспортному слою. Bluetooth Application Chip эти события декодируются и передаются в приложение.

BLE Serialization упрощает сериализацию существующего приложения Bluetooth, потому что в самом приложении делаются лишь незначительные изменения. Подробнее о том, как портировать код приложения на разные MCU, можно найти в статье [3].

В serialized-приложении кодеки сериализации обеспечивают механизм для кодирования и декодирования команд и событий между Bluetooth Application Chip и BLE Connectivity Chip.

SDK Serialization Frame Format fig02

Риc. 2. Общий формат кодирования пакета сериализации.

Команды и ответы. На следующем рисунке представлен поток команд и ответов, которые передаются в serialized-приложении.

SDK Serialization Command and Response fig03

Рис. 3. Курсирование пакетов команд и ответов.

Тип пакета Описание
0x00 Command Пакет, отправленный из Application Chip в BLE Connectivity Chip, где он декодируется, и выполняется соответствующая функция SoftDevice.
0x01 Command Response После того, как в принята функция в SoftDevice, в BLE Connectivity Chip кодируется ответ, и в Application Chip отправляется пакет ответа (response packet).
0x03 DTM Command Пакет, отправленный из Application Chip в BLE Connectivity Chip, где он декодируется, и чип входит в режим DTM.
0x04 DTM Command Response Перед тем, как BLE Connectivity Chip войдет в режим DTM, он ответит response-пакетом, передавая этот пакет в Application Chip.

Примечание: если поле длины недопустимое для определенной команды, то вызов serialized API вернет код ошибки NRF_ERROR_INVALID_LENGTH из кодека в Application Chip.

События. На следующем рисунке показан поток пакетов событий в serialized-приложении.

SDK Serialization Event packet fig04

Рис. 4. Перемещение пакетов событий.

Тип пакета Описание
0x02 Event Если в SoftDevice произошло событие, то пакет события (event packet) отправится из BLE Connectivity Chip в Application Chip.

Ниже описываются определенные правила при кодировании каждого типа данных в serialized-приложении.

Кодирование примитивных аргументов. Аргументы функции кодируются в том же порядке, как они следуют в декларации функции. Когда аргументы передаются по значению (by value), они кодируются как есть (независимо от их размера и порядка следования). Подразумевается, что получатель знает о порядке следования и типе элементов. Возвращаемое значение из функции также кодируется в пакет и посылается в ответе.

Пример:

uint32_t foo(uint32_t arg1, uint8_t arg2);

Пакет запроса:

Тип пакета:                            1 байт
Идентификатор функции (Function ID):   1 байт
arg1:                                  4 байта
arg2:                                  1 байт

Пакет ответа:

Тип пакета:                            1 байт
Идентификатор функции (Function ID):   1 байт
Возвращаемое значение:                 4 байта

Кодирование аргументов, переданное по указателю. Если аргументом функции является указатель, то он кодируется в зависимости от характера аргумента.

Если данные, переданные по указателю, это входной аргумент, то он кодируется только в пакете запроса, и не присутствует в пакете ответа. Аргумент, представленный указателем, предшествует в пакете 1-байтным флагом, который указывает, равен ли указатель NULL или нет. Данные присутствуют только если указатель не равен NULL.

Пример:

uint32_t foo(uint32_t * arg1);

Пакет запроса:

Тип пакета:                            1 байт
Идентификатор функции (Function ID):   1 байт
Arg1 представляет флаг:                1 байт
arg1:                                  4 байта (опционально)

Пакет ответа:

Тип пакета:                            1 байт
Идентификатор функции (Function ID):   1 байт
Возвращаемое значение:                 4 байта

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

Пример:

uint32_t foo(uint32_t * arg1);

Пакет запроса:

Тип пакета:                            1 байт
Идентификатор функции (Function ID):   1 байт
Arg1 представляет флаг:                1 байт

Пакет ответа:

Тип пакета:                            1 байт
Идентификатор функции (Function ID):   1 байт
Возвращаемое значение:                 4 байта
Arg1 представляет флаг:                1 байт
arg1:                                  4 байта (опционально)

Если аргумент описан как для ввода/вывода, то его содержимое присутствует как в пакете запроса, так и в пакете ответа.

Кодирование структур. Структуры кодируются таким же способом, как и аргументы функции. Элементы сохраняют определенный порядок следования кроме случаев, когда этот порядок необходимо поменять. Такие изменения необходимы, когда структура содержит поле указателя на данные и поле длины, и поле длины определено после поля указателя на данные. В этом случае важно отправить сначала длину поля перед реальными данными. Если структура содержит в себе вложенные структуры, то они разворачиваются и кодируется в порядке определения.

Пример:

typedef struct
{
   uint8_t          elementA;
} nested_element_t;
 
typedef struct
{
   uint8_t          elementA;
   nested_element_t elementB;
   uint8_t          elementC;
} foo_t;
 
uint32_t foo(foo_t * p_data);

Пакет запроса:

Тип пакета:                            1 байт
Идентификатор функции (Function ID):   1 байт
p_data, флаг присутствия:              1 байт
p_data->elementA:                      1 байт
p_data->elementB->elementA:            1 байт
p_data->elementC:                      1 байт

Если структура содержит указатели, то применяется то же самое правило разворачивания, например:

typedef struct
{
   uint8_t          elementA;
} nested_element_t;
 
typedef struct
{
   uint8_t           elementA;
   nested_element_t* p_elementB;
   uint8_t           elementC;
} foo_t;
 
uint32_t foo(foo_t * p_data);

Пакет запроса:

Тип пакета:                            1 байт
Идентификатор функции (Function ID):   1 байт
p_data, флаг присутствия:              1 байт
p_data->elementA:                      1 байт
p_data->p_elementB, флаг присутствия:  1 байт
p_data->p_elementB->elementA:          1 байт (опционально)
p_data->elementC:                      1 байт

Кодирование данных переменной длины. Кодирование длины данных переменной состоит из кодирования длины и буфера. Из-за того, что буфер это указатель, флаг присутствия помещается перед буфером, и он кодируется только если этот указатель не равен NULL. Поле длины кодируется перед данными, независимо от порядка следования аргументов функции или порядка следования полей в структуре.

Пример:

uint32_t foo(foo_t * p_data, uint16_t len);

Пакет запроса:

Тип пакета:                            1 байт
Идентификатор функции (Function ID):   1 байт
Длина:                                 2 байта
p_data, флаг присутствия:              1 байт
p_data, содержимое:                    n байт

В определенных случаях в SoftDevice API должны быть данные переменной длины, где поле данных не предоставляется. В этом случает применяются другие методы для определения размера буфера переменной длины (например флаг пользовательского типа). В подобных случаях сериализация выполняет парсинг таких данных для определения их длины.

Кодирование битовых полей. Порядок кодирования битовых полей управляется сериализацией, поэтому этот порядок не зависит ни от компилятора, ни от архитектуры.

Здесь можно найти декларации функций и определений структур данных и идентификаторов (typedef enum), используемых в качестве API слоя serialization PHYr.

Каждый определенный слой PHY (SPI, I2C, UART, low power UART, и т. д.) должен предоставлять одинаковый API. Это позволит следующему, более верхнему слою (HAL Transport layer, который отвечает за управление слоем PHY, управлением памяти, проверками CRC, повторными передачами, и т. п.) отвязаться от зависимостей, связанной со спецификой аппаратуры.

Управление и взаимосвязь между слоями. Слой PHY управляется слоем HAL Transport путем вызова функций, декларированных в этом файле. Слой PHY передает события слою HAL Transport путем вызова callback-функции. Обработчик для этой функции передается в функции ser_phy_open. Эта callback-функция должна быть вызвана с параметром типа ser_phy_evt_t, заполненным в соответствии с передаваемым событием. Поддерживаемые типы событий определены в ser_phy_evt_type_t. Например, для передачи события, показывающего успешный прием пакета (RX packet received), сначала должна быть заполнена структура типа ser_phy_evt_t:

ser_phy_evt_t phy_evt;
phy_evt.evt_type = SER_PHY_EVT_RX_PKT_RECEIVED;
phy_evt.evt_params.rx_pkt_received.p_buffer = (указатель на буфер RX);
phy_evt.evt_params.rx_pkt_received.num_of_bytes = (количество принятых данных);

После этого должна быть вызвана callback-функция:

events_handler(phy_evt);

Все функции, декларированные в этом файле, обязательны к реализации. Некоторые события, указанные в ser_phy_evt_type_t, опциональны для реализации.

Передача пакета. Каждый слой PHY отвечает за добавление заголовка (PHY header) к отправляемому пакету. Этот заголовок содержит 16-разрядное поле, которое несет длину пакета (для обеспечения независимости от endianness должна использоваться функция uint16_encode, которая определена в app_util.h). Указатель на пакет для отправки и длина пакета помещаются в параметры функции ser_phy_tx_pkt_send. Когда пакет передан, должно генерироваться событие типа SER_PHY_EVT_TX_PKT_SENT.

SDK Serialization TX interlayer communication fig05

Рис. 5. TX - коммуникация на внутреннем слое.

Прием пакета. Слой PHY должен иметь возможность сохранить только PHY header (16-разрядное поле, несущее информацию о длине пакета). После получения PHY header передача останавливается, и слой PHY должен отправить запрос в слой HAL Transport для памяти, куда должен быть сохранен пакет - генерируется событие типа SER_PHY_EVT_RX_BUF_REQUEST с параметрами события, определенными в ser_phy_evt_rx_buf_request_params_t (для обеспечения независимости от endianness должна использоваться функция uint16_encode, которая определена в app_util.h). Передача должна быть возобновлена после вызова функции ser_phy_rx_buf_set.

Когда параметр функции ser_phy_rx_buf_set равен NULL, это означает, что не хватает памяти для сохранения пакета, однако пакет принят в несуществующее место (dummy location), чтобы гарантировать непрерывность обмена. После завершения приема генерируется событие типа SER_PHY_EVT_RX_PKT_DROPPED.

SDK Serialization RX dropping interlayer communication fig06

Рис. 6. Отбрасывание данных на приеме (RX dropping) - коммуникация на внутреннем слое.

Когда параметр функции ser_phy_rx_buf_set отличается от NULL, пакет принимается в указанный буфер. После завершения приема генерируется событие SER_PHY_EVT_RX_PKT_RECEIVED с параметрами события, определенными в ser_phy_evt_rx_pkt_received_params_t.

SDK Serialization RX interlayer communication fig07

Рис. 7. RX - коммуникация на внутреннем слое.

Ошибки слоя PHY. Существует возможная сигнализация об ошибках слоя PHY с помощью события типа SER_PHY_EVT_RX_OVERFLOW_ERROR, SER_PHY_EVT_TX_OVERREAD_ERROR, или SER_PHY_EVT_HW_ERROR с параметрами события, определенными в ser_phy_evt_hw_error_params_t.

Здесь можно найти декларации функций и определения типа, используемые в качестве API для сериализации слоя HAL Transport. Этот слой полностью не зависит от аппаратуры. В настоящий момент слой HAL Transport отвечает за управление слоем PHY и за управление памятью. В будущем возможно добавление других функций, таких как проверка CRC или повторная передача.

Рис. 8. Машина состояний RX.

Рис. 9. Машина состояний TX.

[Ссылки]

1. nRF SDK BLE serialization site:nordicsemi.com.
2. nRF SDK Running a serialized application site:nordicsemi.com.
3. nRF SDK Porting serialization libraries site:nordicsemi.com.