93а. nRF24L01: радио 2,4 ГГц на SPI — адреса, каналы, Enhanced ShockBurst, подтверждения и повторы
В разделе 93 радио LoRa: далеко, медленно, с расчётом бюджета линии. Здесь — его противоположность: самый дешёвый модуль на 2,4 ГГц, который бьёт на десятки метров, зато быстро, и сам подтверждает и повторяет пакеты. Микроконтроллеру остаётся SPI, два вывода и прерывание. Разбираем, что летит в эфире, как подключить модуль к F103, какие команды и регистры нужны, как работают подтверждение, повторы и шесть труб приёма и где на этом модуле обычно теряют вечер.
Опоры: RM0008 (DocID13902 Rev 17): таблица режимов выводов, с. 160;
AFIO_EXTICR1, с. 190; EXTI_FTSR, с. 211; делитель BR в SPI_CR1,
с. 743; CPOL и CPHA, с. 744. DocID13587 Rev 17 (STM32F103x8/xB):
PA4–PA7 — SPI1_NSS, SPI1_SCK, SPI1_MISO, SPI1_MOSI, с. 29.
Документа на nRF24L01+ в проекте нет: команды, регистры, значения после
сброса, формат пакета и CRC взяты из общеизвестной спецификации и
заложены в модель. Поэтому всё ниже проверяет согласие драйвера с
моделью, а не с микросхемой. Все числа — прогон драйвера
_verify/nrf93b/nrf93b.c на компьютере (gcc 12.2.0) со своей моделью
модулей и эфира на Python 3.11; прошивка собрана armclang 6.24 под
Cortex-M3.
Как это называется в документации
Оригинал Перевод Enhanced ShockBurst (ESB) протокол пакетов модуля с подтверждением и повтором PTX / PRX передатчик / приёмник data pipe труба приёма: свой адрес, до шести на приёмник auto acknowledgement автоподтверждение auto retransmit delay (ARD) / count (ARC) ожидание подтверждения / число повторов packet control field (PCF) поле управления пакета: длина, PID, NO_ACK packet identity (PID) номер пакета, 2 бита: узнать повтор dynamic payload length (DPL) переменная длина пакета payload полезные данные chip enable (CE) вывод включения приёма и передачи flush очистить FIFO GFSK частотная манипуляция с гауссовым фильтром
93а.1 Что за модуль
nRF24L01+ — приёмопередатчик на 2,4 ГГц с SPI. Модуляция — GFSK,
скорости 250 кбит/с, 1 и 2 Мбит/с, канал задаёт регистр RF_CH:
частота 2400 + RF_CH МГц, каналы 0–125 с шагом 1 МГц. Эти числа — из
общеизвестной спецификации, документа на модуль в проекте нет. Драйвер
этого раздела ставит канал 76: модель пересчитала его в 2476 МГц.
Главное отличие от «просто радио» — Enhanced ShockBurst: пакеты
собирает, подтверждает и повторяет сам модуль. Передатчик кладёт данные в
FIFO и дёргает CE; модуль отправляет пакет, переходит на приём и ждёт
подтверждение. Нет подтверждения — повторяет сам, до ARC раз. Приёмник
тоже без участия программы проверяет CRC, отвечает подтверждением и
откладывает пакет в FIFO на три пакета. Программе приходит один флаг:
передано, не передано или принято.
Отсюда и раскладка раздела: SPI и регистры — 93а.3, пакет — 93а.4, подтверждения и повторы — 93а.5, приём и трубы — 93а.6.
93а.2 Подключение к F103
Подключение то же, что у SX1276 в разделе 93.5, — SPI1 и два вывода:
| Вывод модуля | STM32 | Назначение |
|---|---|---|
| CSN | PA4 | выбор, активный ноль; начало и конец каждой команды |
| SCK | PA5 | такт SPI1 |
| MISO | PA6 | данные от модуля |
| MOSI | PA7 | данные в модуль |
| CE | PB0 | «1» — слушать (приёмник), импульс — передать |
| IRQ | PB1 | ноль, пока поднят флаг; EXTI1 по спаду |
| VCC, GND | — | только 3,3 В |
PA4–PA7 — выводы SPI1 (DocID13587, с. 29). CSN здесь обычный выход: он
должен держать ноль всю команду, от первого байта до последнего. SPI —
режим 0 (CPOL = 0, CPHA = 0, с. 744), старший бит первым. APB2 на
72 МГц, делитель 8 (BR = 010, с. 743) — 9 МГц; предел SPI модуля по
общеизвестной спецификации — 10 МГц. PB1 — вход с подтяжкой (код 1000 в
таблице режимов, с. 160), на EXTI1 его выбирает AFIO_EXTICR1 = 0001
(с. 190), спад включает EXTI_FTSR (с. 211):
static void spi1_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_AFIOEN
| RCC_APB2ENR_SPI1EN;
/* PA7, PA5 — альтернативная функция 50 МГц (1011), PA6 — вход (0100), PA4 — выход 50 МГц (0011) */
GPIOA->CRL = (GPIOA->CRL & 0x0000FFFFu) | 0xB4B30000u;
GPIOA->BSRR = 1u << 4; /* CSN = 1 до первой команды */
/* PB0 — выход (0011), PB1 — вход с подтяжкой (1000) */
GPIOB->CRL = (GPIOB->CRL & 0xFFFFFF00u) | 0x00000083u;
GPIOB->BSRR = 1u << 1; /* подтяжка PB1 вверх; CE = 0 */
GPIOB->BSRR = 1u << 16;
/* PB10 — вход с подтяжкой: перемычка на землю — приёмник */
GPIOB->CRH = (GPIOB->CRH & ~(0xFu << 8)) | 0x8u << 8;
GPIOB->BSRR = 1u << 10;
GPIOC->CRH = (GPIOC->CRH & ~(0xFu << 20)) | 0x2u << 20; /* PC13 — светодиод */
/* APB2 72 МГц / 8 = 9 МГц, режим 0, старший бит первым */
SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_BR_1 | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_SPE;
}
Питание. Модуль питается от 3,3 В. На передаче он берёт ток рывками, а провода и слабый стабилизатор BluePill дают просадку — у самого модуля ставят электролит, обычно 10–100 мкФ (практика, не документ). При просадке модуль перезапускается со значениями после сброса, и связь молча пропадает (93а.7).
93а.3 Команды SPI и регистры
Каждая команда — один обмен при CSN = 0: первый байт — код команды,
дальше данные. В ответ на первый байт модуль всегда отдаёт STATUS —
поэтому флаги можно читать даром, командой NOP (0xFF). В модели после
сброса первый обмен «прочитать CONFIG» — байты 00 FF — вернул
0E 08: STATUS = 0x0E и CONFIG = 0x08.
| Команда | Код | Что делает |
|---|---|---|
R_REGISTER |
000A AAAA | прочитать регистр A |
W_REGISTER |
001A AAAA | записать регистр A |
R_RX_PAYLOAD |
0x61 | забрать пакет из FIFO приёма |
W_TX_PAYLOAD |
0xA0 | положить пакет в FIFO передачи |
FLUSH_TX / FLUSH_RX |
0xE1 / 0xE2 | очистить FIFO |
R_RX_PL_WID |
0x60 | длина первого пакета при переменной длине |
NOP |
0xFF | ничего — только STATUS |
Регистры, которые нужны всегда:
| Регистр | Адрес | После сброса | Что в нём |
|---|---|---|---|
CONFIG |
0x00 | 0x08 | EN_CRC, CRCO (CRC 2 байта), PWR_UP, PRIM_RX |
EN_AA |
0x01 | 0x3F | автоподтверждение по трубам |
EN_RXADDR |
0x02 | 0x03 | включённые трубы |
SETUP_RETR |
0x04 | 0x03 | ARD (7:4) — ждать (ARD + 1) × 250 мкс, ARC (3:0) — повторов |
RF_CH |
0x05 | 0x02 | канал |
RF_SETUP |
0x06 | 0x0E | скорость (RF_DR_LOW, RF_DR_HIGH), мощность |
STATUS |
0x07 | 0x0E | RX_DR, TX_DS, MAX_RT, номер трубы, TX_FULL |
RX_ADDR_P0, TX_ADDR |
0x0A, 0x10 | E7E7E7E7E7 | адрес трубы 0 и адрес передачи |
RX_PW_P0…P5 |
0x11–0x16 | 0 | длина пакета трубы; 0 — труба не работает |
DYNPD, FEATURE |
0x1C, 0x1D | 0 | переменная длина |
Флаги STATUS снимаются записью единицы в свой бит. Адреса пишутся
младшим байтом первым. nrf_init пишет всё нужное подряд; на
передатчике это 20 обменов, на приёмнике вместе с открытием трубы и
nrf_listen — 19. В модели после настройки: CONFIG = 0x0E у
передатчика и 0x0F у приёмника (PRIM_RX), SETUP_RETR = 0x15 — ждать
500 мкс, 5 повторов, RF_CH = 0x4C, RF_SETUP = 0x06 — 1 Мбит/с и
наибольшая мощность.
Первое, что делает nrf_init, — пишет ширину адреса и читает её
обратно. Это проверка, что модуль вообще есть (93а.7):
int nrf_init(nrf_t *d, uint8_t id, const nrf_cfg_t *c, int rx)
{
static const uint8_t rate[] = {0, NRF_RF_DR_HIGH, NRF_RF_DR_LOW};
*d = (nrf_t){0};
d->id = id;
d->len = c->len;
d->dyn = c->dyn;
nrf_port_ce(id, 0);
nrf_write(d, NRF_SETUP_AW, 3u); /* адрес 5 байт */
if (nrf_read(d, NRF_SETUP_AW) != 3u)
return NRF_E_NOCHIP; /* нет питания или провода: читаются нули или единицы */
nrf_write(d, NRF_RF_CH, c->ch & 0x7Fu);
nrf_write(d, NRF_RF_SETUP, (uint8_t)(rate[c->rate] | NRF_RF_PWR_0DBM));
nrf_write(d, NRF_SETUP_RETR, (uint8_t)(c->ard << 4 | (c->arc & 0x0Fu)));
nrf_write(d, NRF_FEATURE, c->dyn ? NRF_EN_DPL : 0u);
nrf_write(d, NRF_DYNPD, 0);
nrf_write(d, NRF_EN_AA, 0);
nrf_write(d, NRF_EN_RXADDR, 0);
nrf_cmd(d, NRF_FLUSH_TX);
nrf_cmd(d, NRF_FLUSH_RX);
nrf_write(d, NRF_STATUS, NRF_RX_DR | NRF_TX_DS | NRF_MAX_RT);
if (!rx) {
nrf_write_addr(d, NRF_TX_ADDR, c->addr);
nrf_open_rx(d, 0, c->addr); /* подтверждение приходит на адрес трубы 0 */
}
d->config = (uint8_t)(NRF_EN_CRC | (c->crc2 ? NRF_CRCO : 0u) | NRF_PWR_UP | (rx ? NRF_PRIM_RX : 0u));
nrf_write(d, NRF_CONFIG, d->config);
nrf_port_delay_us(id, NRF_T_PWR_US);
return NRF_OK;
}
93а.4 Пакет Enhanced ShockBurst
В эфир идёт: преамбула 1 байт, адрес 3–5 байт, поле PCF 9 бит (длина 6 бит, PID 2 бита, NO_ACK 1 бит), данные 0–32 байта и CRC 1 или 2 байта. Преамбула — 10101010 или 01010101, по первому биту адреса. Адрес уходит старшим байтом вперёд — в регистр он пишется наоборот. PCF в 9 бит сдвигает всё дальше с границы байта, поэтому CRC считается по битам:
uint16_t esb_crc(const uint8_t *bits, uint32_t from, uint32_t nbits, int crc2)
{
uint16_t crc = crc2 ? 0xFFFFu : 0xFFu;
uint16_t top = crc2 ? 0x8000u : 0x80u, poly = crc2 ? 0x1021u : 0x07u, mask = crc2 ? 0xFFFFu : 0xFFu;
for (uint32_t i = from; i < from + nbits; i++) {
int fb = ((crc & top) != 0) ^ bit(bits, i);
crc = (uint16_t)((crc << 1) & mask);
if (fb)
crc ^= poly;
}
return crc;
}
CRC 1 байт — многочлен x^8 + x^2 + x + 1 с начальным 0xFF, 2 байта —
x^16 + x^12 + x^5 + 1 с начальным 0xFFFF (общеизвестная спецификация).
Проверка кода — три независимых счёта по строке «123456789»: esb_crc
на стенде, своя функция модели и binascii.crc_hqx из Python дали одно
и то же — 0x29B1; CRC 1 байт — 0xFB. Пакет «STM32» с PID 1 esb_pack
собрал в 113 бит — AAC35AA093011529AA26999944BD00, CRC 0x897A, с CRC
1 байт — 105 бит; сборка модели совпала бит в бит.
Сколько пакет висит в эфире, считается из длины. С адресом 5 байт и CRC 2 байта:
| Пакет | Бит | 2 Мбит/с | 1 Мбит/с | 250 кбит/с |
|---|---|---|---|---|
| 32 байта данных | 329 | 164,5 мкс | 329 мкс | 1316 мкс |
| подтверждение без данных | 73 | 36,5 мкс | 73 мкс | 292 мкс |
Что ловит CRC. Модель портила пакеты из 32 байт — переворачивала k случайных бит — и спрашивала стенд, сойдётся ли CRC. По 4000 пакетов на каждое k:
| Перевёрнуто бит | 1 | 2 | 3 | 4 | 5 | 8 |
|---|---|---|---|---|---|---|
| пропустил CRC 2 байта | 0 | 0 | 0 | 0 | 0 | 1 |
| пропустил CRC 1 байт | 0 | 16 | 0 | 40 | 0 | 38 |
Нечётное число ошибок CRC 1 байт ловит всегда: его многочлен делится на
x + 1. Чётное пропускает около 1 %. CRC 2 байта на 24 000 испорченных
пакетов пропустил один. Её и стоит ставить: CRCO = 1. Во всех 48 000
счётах esb_crc совпал с моделью.
93а.5 Передача: подтверждение и повторы
Передача — пакет в FIFO, импульс CE не короче 10 мкс, ждать IRQ, по
STATUS понять, чем кончилось:
int nrf_send(nrf_t *d, const uint8_t *p, uint8_t n)
{
uint8_t tx[33] = {NRF_W_TX_PAYLOAD}, rx[33];
uint8_t len = d->dyn ? n : d->len; /* постоянная длина: хвост — нули */
for (uint8_t i = 0; i < len; i++)
tx[1 + i] = i < n ? p[i] : 0u;
xfer(d, tx, rx, 1u + len);
nrf_port_ce(d->id, 1); /* импульс CE — один пакет со всеми повторами */
nrf_port_delay_us(d->id, NRF_T_CE_US);
nrf_port_ce(d->id, 0);
if (!nrf_port_wait_irq(d->id, NRF_T_SEND_US)) {
nrf_cmd(d, NRF_FLUSH_TX);
d->timeouts++;
return NRF_E_TIMEOUT; /* IRQ не пришёл: модуль сброшен или не включён */
}
uint8_t st = nrf_cmd(d, NRF_NOP);
d->retries += nrf_read(d, NRF_OBSERVE_TX) & 0x0Fu; /* ARC_CNT: сколько повторов понадобилось */
nrf_write(d, NRF_STATUS, st & (NRF_TX_DS | NRF_MAX_RT));
if (st & NRF_TX_DS) {
d->sent++;
return NRF_OK;
}
nrf_cmd(d, NRF_FLUSH_TX); /* после MAX_RT пакет остаётся в FIFO */
d->failed++;
return NRF_E_MAXRT;
}
В модели на 1 Мбит/с с 32 байтами: загрузка пакета по SPI — 29,3 мкс;
от фронта CE пакет пошёл через 130 мкс и кончился через 459 мкс,
подтверждение кончилось через 662 мкс — тогда и поднялся TX_DS, а
nrf_send вернулась через 666,4 мкс. 130 мкс на перестройку, готовность
через 1,5 мс после PWR_UP — допущения модели по общеизвестной
спецификации. 1000 пакетов подряд с приёмом на другой стороне:
| Скорость | мкс на пакет | данных, кбит/с |
|---|---|---|
| 2 Мбит/с | 529,4 | 483,5 |
| 1 Мбит/с | 730,4 | 350,5 |
| 250 кбит/с | 1936,4 | 132,2 |
Все 1000 приняты без искажений на каждой скорости. Половину времени на 2 Мбит/с съедают две перестройки по 130 мкс, а не эфир.
ARD должен вместить подтверждение. Модуль ждёт его ARD после конца
пакета. Подтверждению нужно 130 мкс и его длина: 166,5 мкс на 2 Мбит/с,
203 мкс на 1 Мбит/с и 422 мкс на 250 кбит/с. Самое короткое ожидание —
250 мкс — на 250 кбит/с мало, и это видно в прогоне: 100 пакетов приняты
все, а передатчик о каждом сообщил MAX_RT, приёмник получил ещё 300
повторов и выбросил их как копии. С ARD = 1 (500 мкс) — все 100
подтверждены.
Потери. Модель переворачивала один бит в пакете и, отдельно, в подтверждении с вероятностью p — CRC приёмника такой пакет отбрасывает. 1000 пакетов на 1 Мбит/с с ожиданием 500 мкс:
| p | ARC | TX_DS |
MAX_RT |
дошло | дошло, но MAX_RT |
копий выброшено |
|---|---|---|---|---|---|---|
| 0,1 | 0 | 809 | 191 | 899 | 90 | 0 |
| 0,1 | 3 | 997 | 3 | 1000 | 3 | 111 |
| 0,1 | 15 | 1000 | 0 | 1000 | 0 | 131 |
| 0,3 | 0 | 485 | 515 | 707 | 222 | 0 |
| 0,3 | 3 | 942 | 58 | 997 | 55 | 335 |
| 0,3 | 15 | 1000 | 0 | 1000 | 0 | 432 |
| 0,5 | 0 | 248 | 752 | 506 | 258 | 0 |
| 0,5 | 3 | 697 | 303 | 940 | 243 | 461 |
| 0,5 | 15 | 993 | 7 | 1000 | 7 | 881 |
Попытка удаётся, когда дошли и пакет, и подтверждение: (1 − p)². Тогда
TX_DS бывает с вероятностью 1 − (1 − (1 − p)²)^(ARC + 1), а пакет доходит
с вероятностью 1 − p^(ARC + 1); прогон сходится с этим в пределах разброса
(проверка считает обе формулы). Два вывода. Повторы вытягивают даже
плохой канал: при p = 0,5 с 15 повторами дошли все 1000. И MAX_RT не
значит «не дошло»: при p = 0,5 и 3 повторах 243 пакета из 303
«потерянных» приёмник получил — пропало подтверждение. Если важно не
задвоить команду, нумеруйте пакеты в данных.
93а.6 Приём по IRQ, трубы 0–5
Приёмник — PRIM_RX = 1 и CE = 1, через 130 мкс он слушает. Пакет с
верной CRC модуль подтверждает, кладёт в FIFO и поднимает RX_DR — IRQ
падает в ноль, EXTI1 вызывает прерывание. Номер трубы — в битах 3:1
STATUS, 7 — FIFO пуст:
int nrf_recv(nrf_t *d, uint8_t *p, uint8_t *pipe)
{
uint8_t tx[33] = {NRF_R_RX_PAYLOAD}, rx[33];
uint8_t st = nrf_cmd(d, NRF_NOP);
if (NRF_RX_P_NO(st) > 5u) {
nrf_write(d, NRF_STATUS, NRF_RX_DR);
return 0; /* FIFO приёма пуст */
}
uint8_t n = d->len;
if (d->dyn) {
uint8_t w[2] = {NRF_R_RX_PL_WID, NRF_NOP};
xfer(d, w, rx, 2);
n = rx[1];
if (n > 32u) {
nrf_cmd(d, NRF_FLUSH_RX);
return 0;
}
}
for (uint8_t i = 1; i <= n; i++)
tx[i] = NRF_NOP;
xfer(d, tx, rx, 1u + n);
for (uint8_t i = 0; i < n; i++)
p[i] = rx[1 + i];
*pipe = (uint8_t)NRF_RX_P_NO(st);
nrf_write(d, NRF_STATUS, NRF_RX_DR); /* снять флаг; в FIFO может быть ещё пакет */
return n;
}
В FIFO может лежать до трёх пакетов, поэтому прерывание забирает всё, а флаг снимает после каждого пакета:
void EXTI1_IRQHandler(void)
{
uint8_t p[32], pipe;
EXTI->PR = 1u << 1;
while (nrf_recv(&radio, p, &pipe) > 0) { /* забрать всё: в FIFO до трёх пакетов */
got++;
GPIOC->ODR ^= 1u << 13;
}
}
Повторы приёмник отсеивает сам. Если подтверждение пропало, передатчик шлёт тот же пакет с тем же PID; приёмник сравнивает PID и CRC с прошлым пакетом трубы, снова подтверждает, но в FIFO не кладёт. В таблице 93а.5 это колонка «копий выброшено» — ни одна копия до программы не дошла.
Трубы. Приёмник слушает до шести адресов сразу. Трубы 0 и 1 имеют
полные адреса по 5 байт, у труб 2–5 свой только младший байт, остальные
четыре — общие с трубой 1. В модели приёмник открыл все шесть
(EN_RXADDR = 0x3F, EN_AA = 0x3F): труба 0 — E011223344, труба 1 —
1093A05AC3, трубы 2–5 — младшие байты 11, 12, 13, 14. Шесть передатчиков
отправили по 10 пакетов, каждый на свой адрес: приёмник получил по 10 в
каждую трубу, все 60 — с верным номером трубы и подтверждены.
Переменная длина. С EN_DPL в FEATURE и битом трубы в DYNPD
длина едет в PCF, а приёмник читает её командой R_RX_PL_WID. Пакеты
длиной от 1 до 32 байт — все 32 подтверждены, длины на приёме совпали.
93а.7 Грабли
Всё по нулям. Модуль без питания или без провода: по MISO приходят
одни нули (или единицы — смотря как висит линия). STATUS = 0x00 или
0xFF, запись «проходит», а чтение возвращает то же. В модели в обоих
случаях nrf_init прочитала не ту ширину адреса и вернула
NRF_E_NOCHIP. Без такой проверки программа «настраивает» пустоту и
ждёт пакеты вечно.
Пакеты доходят, а передатчик пишет MAX_RT. Подтверждение приходит на
адрес трубы 0 передатчика, и RX_ADDR_P0 обязан совпадать с TX_ADDR.
В модели с RX_ADDR_P0, оставшимся E7E7E7E7E7: приёмник получил все
100 пакетов и отправил 400 подтверждений, передатчик не принял ни одного
— 100 MAX_RT и 300 лишних повторов.
Ни одного пакета. У пары должны совпадать канал, скорость, длина CRC, адрес, длина пакета трубы, и приёмник должен держать CE = 1. В модели по 100 пакетов на каждую ошибку:
| Отличие | Принято | MAX_RT |
Отброшено по CRC |
|---|---|---|---|
| нет — всё совпадает | 100 | 0 | 0 |
| канал 77 вместо 76 | 0 | 100 | 0 |
| скорость 2 Мбит/с вместо 1 | 0 | 100 | 0 |
| CRC 1 байт вместо 2 | 0 | 100 | 400 |
RX_PW_P1 = 16, а пакет 32 байта |
0 | 100 | 400 |
RX_PW_P1 = 0 |
0 | 100 | 0 |
| CE приёмника = 0 | 0 | 100 | 0 |
| другой младший байт адреса | 0 | 100 | 0 |
Ошибки CRC и длины видны: приёмник слышит пакет и отбрасывает его. Канал, скорость, адрес и CE не оставляют следов вообще.
Прерывание пришло один раз. EXTI ловит спад, а IRQ держит ноль, пока
поднят хоть один флаг. Обработчик забрал пакет, но не снял RX_DR — IRQ
так и остался в нуле, нового спада нет. В модели из 10 пакетов программа
получила 1. Модуль принял ещё три и заполнил FIFO, а дальше перестал
подтверждать: последние 6 передач — MAX_RT, 24 попытки упали в полный
FIFO. С обработчиком из 93а.6 — 10 из 10.
Связь пропала после рывка тока. Модуль перезапустился от просадки
питания: CONFIG = 0x08, PWR_UP = 0, импульс CE ничего не делает,
IRQ не приходит. В модели передатчик сбросили после 100-го пакета:
дальше все 100 вызовов nrf_send кончились по времени. Если на
NRF_E_TIMEOUT проверять nrf_alive и заново звать nrf_init, дошли
все 200, перенастройка понадобилась одна. Электролит у модуля лечит
причину, проверка — следствие.
Регистры «не пишутся». По общеизвестной спецификации W_REGISTER
работает в режимах выключения и ожидания: настройку меняют при CE = 0.
Модель этого правила не воспроизводит — не проверено.
93а.8 Итоги
- nRF24L01+: 2,4 ГГц, 250 кбит/с, 1 и 2 Мбит/с, частота 2400 +
RF_CHМГц; канал 76 — 2476 МГц. Подтверждения и повторы делает сам модуль. - F103: SPI1 на PA4–PA7, режим 0, 72 / 8 = 9 МГц; CE — PB0, IRQ — PB1 на EXTI1 по спаду; питание 3,3 В и электролит у модуля.
- Первый байт любого ответа —
STATUS;NOPчитает его даром; флаги снимаются единицей. - Пакет: преамбула 1 байт, адрес 5 байт, PCF 9 бит, до 32 байт, CRC — 329 бит, 329 мкс на 1 Мбит/с; CRC 2 байта на 24 000 испорченных пакетов пропустил 1, CRC 1 байт — 94.
- 1000 пакетов: 529,4 мкс на пакет на 2 Мбит/с, 730,4 на 1 Мбит/с, 1936,4 на
250 кбит/с;
ARD250 мкс на 250 кбит/с мал — подтверждению надо 422 мкс. - При p = 0,5 с 15 повторами дошли все 1000;
MAX_RTне значит «не дошло». - Шесть труб — по 10 пакетов из 10;
RX_ADDR_P0передатчика =TX_ADDR; в прерывании сниматьRX_DR; на тайм-аут —nrf_aliveиnrf_init.
Полные исходники
nrf93b.h — команды, регистры и биты nRF24L01+, настройка, порт и функции драйвера.
/* nrf93b.h — раздел 93а: драйвер nRF24L01+ без регистров микроконтроллера.
* Драйвер говорит с модулем только через четыре функции порта (nrf_port_*): в прошивке fw93b.c их
* делают SPI1 и выводы STM32F103, в стенде host93b.c — модель двух и более модулей model93b.py.
* Карта регистров и команды — по общеизвестной спецификации nRF24L01+; документа в проекте нет. */
#ifndef NRF93B_H
#define NRF93B_H
#include <stdint.h>
/* ── команды SPI: первый байт обмена; в ответ на него модуль всегда отдаёт STATUS ── */
#define NRF_R_REGISTER 0x00u /* 000A AAAA: прочитать регистр A */
#define NRF_W_REGISTER 0x20u /* 001A AAAA: записать регистр A */
#define NRF_R_RX_PL_WID 0x60u /* длина первого пакета в FIFO приёма */
#define NRF_R_RX_PAYLOAD 0x61u /* забрать пакет из FIFO приёма */
#define NRF_W_TX_PAYLOAD 0xA0u /* положить пакет в FIFO передачи */
#define NRF_FLUSH_TX 0xE1u
#define NRF_FLUSH_RX 0xE2u
#define NRF_NOP 0xFFu /* ничего не делать — только получить STATUS */
/* ── регистры ─────────────────────────────────────────────────────────── */
#define NRF_CONFIG 0x00u
#define NRF_EN_AA 0x01u /* автоподтверждение по трубам */
#define NRF_EN_RXADDR 0x02u /* включённые трубы приёма */
#define NRF_SETUP_AW 0x03u /* ширина адреса: 1 — 3, 2 — 4, 3 — 5 байт */
#define NRF_SETUP_RETR 0x04u /* ARD (7:4) и ARC (3:0) */
#define NRF_RF_CH 0x05u /* канал: 2400 + RF_CH МГц */
#define NRF_RF_SETUP 0x06u
#define NRF_STATUS 0x07u
#define NRF_OBSERVE_TX 0x08u /* PLOS_CNT (7:4), ARC_CNT (3:0) */
#define NRF_RX_ADDR_P0 0x0Au /* 0x0A…0x0F — адреса труб 0…5 */
#define NRF_TX_ADDR 0x10u
#define NRF_RX_PW_P0 0x11u /* 0x11…0x16 — длина пакета труб 0…5; 0 — труба не работает */
#define NRF_FIFO_STATUS 0x17u
#define NRF_DYNPD 0x1Cu /* переменная длина по трубам */
#define NRF_FEATURE 0x1Du
/* CONFIG */
#define NRF_EN_CRC 0x08u
#define NRF_CRCO 0x04u /* 1 — CRC 2 байта */
#define NRF_PWR_UP 0x02u
#define NRF_PRIM_RX 0x01u /* 1 — приёмник, 0 — передатчик */
/* STATUS: флаги снимаются записью единицы */
#define NRF_RX_DR 0x40u /* пакет принят */
#define NRF_TX_DS 0x20u /* пакет передан и подтверждён */
#define NRF_MAX_RT 0x10u /* повторы кончились, подтверждения нет */
#define NRF_RX_P_NO(s) (((s) >> 1) & 7u) /* труба первого пакета в FIFO; 7 — FIFO пуст */
/* RF_SETUP */
#define NRF_RF_DR_LOW 0x20u
#define NRF_RF_DR_HIGH 0x08u
#define NRF_RF_PWR_0DBM 0x06u
/* FEATURE */
#define NRF_EN_DPL 0x04u
/* ── времена, мкс ─────────────────────────────────────────────────────── */
#define NRF_T_PWR_US 5000u /* после PWR_UP — до готовности, с запасом */
#define NRF_T_CE_US 15u /* импульс CE для передачи: не короче 10 мкс */
#define NRF_T_RX_US 130u /* после CE = 1 приёмник слушает через 130 мкс */
#define NRF_T_SEND_US 100000u /* дольше любой передачи со всеми повторами */
enum { NRF_1MBPS, NRF_2MBPS, NRF_250KBPS };
enum { NRF_OK = 0, NRF_E_NOCHIP = -1, NRF_E_MAXRT = -2, NRF_E_TIMEOUT = -3 };
typedef struct {
uint8_t ch; /* 0…125 */
uint8_t rate; /* NRF_1MBPS, NRF_2MBPS, NRF_250KBPS */
uint8_t ard; /* 0…15: ждать подтверждение (ard + 1) × 250 мкс */
uint8_t arc; /* 0…15 повторов */
uint8_t crc2; /* 1 — CRC 2 байта, 0 — 1 байт */
uint8_t len; /* длина пакета 1…32 при постоянной длине */
uint8_t dyn; /* 1 — переменная длина (FEATURE, DYNPD) */
uint8_t addr[5]; /* адрес, младший байт первым — так он пишется в регистр */
} nrf_cfg_t;
typedef struct {
uint8_t id; /* номер модуля для порта: в прошивке один, в стенде несколько */
uint8_t status; /* STATUS — первый байт ответа последнего обмена */
uint8_t config; /* что драйвер записал в CONFIG — для nrf_alive */
uint8_t len, dyn;
uint32_t sent, failed, timeouts, retries;
} nrf_t;
/* порт: делает прошивка или стенд */
void nrf_port_xfer(uint8_t id, const uint8_t *tx, uint8_t *rx, uint32_t n); /* CSN = 0, n байт, CSN = 1 */
void nrf_port_ce(uint8_t id, int level);
void nrf_port_delay_us(uint8_t id, uint32_t us);
int nrf_port_wait_irq(uint8_t id, uint32_t timeout_us); /* 1 — IRQ = 0, 0 — время вышло */
uint8_t nrf_cmd(nrf_t *d, uint8_t cmd); /* однобайтная команда, вернуть STATUS */
uint8_t nrf_read(nrf_t *d, uint8_t reg);
void nrf_write(nrf_t *d, uint8_t reg, uint8_t v);
void nrf_write_addr(nrf_t *d, uint8_t reg, const uint8_t a[5]);
int nrf_init(nrf_t *d, uint8_t id, const nrf_cfg_t *c, int rx);
void nrf_open_rx(nrf_t *d, uint8_t pipe, const uint8_t a[5]); /* трубы 2…5: только младший байт a[0] */
void nrf_listen(nrf_t *d); /* CE = 1: приёмник слушает */
int nrf_send(nrf_t *d, const uint8_t *p, uint8_t n); /* NRF_OK, NRF_E_MAXRT, NRF_E_TIMEOUT */
int nrf_recv(nrf_t *d, uint8_t *p, uint8_t *pipe); /* длина пакета; 0 — FIFO приёма пуст */
int nrf_alive(nrf_t *d); /* CONFIG тот, что записан: модуль не сбрасывался */
/* ── пакет Enhanced ShockBurst, как его собирает модуль: для проверки на стенде ── */
#define ESB_MAX_BYTES 42u /* 1 + 5 + (9 + 256 + 16 бит) / 8, с округлением */
uint16_t esb_crc(const uint8_t *bits, uint32_t from, uint32_t nbits, int crc2);
uint32_t esb_pack(const uint8_t a[5], uint8_t aw, const uint8_t *p, uint8_t n, uint8_t pid, uint8_t no_ack,
int crc2, uint8_t out[ESB_MAX_BYTES]); /* вернуть число бит */
#endif
nrf93b.c — драйвер и сборка пакета Enhanced ShockBurst без регистров МК: общая часть прошивки и стенда.
/* nrf93b.c — раздел 93а: драйвер nRF24L01+ и сборка пакета Enhanced ShockBurst (без регистров МК). */
#include "nrf93b.h"
static uint8_t xfer(nrf_t *d, const uint8_t *tx, uint8_t *rx, uint32_t n)
{
nrf_port_xfer(d->id, tx, rx, n);
d->status = rx[0]; /* в ответ на первый байт — всегда STATUS */
return rx[0];
}
uint8_t nrf_cmd(nrf_t *d, uint8_t cmd)
{
uint8_t rx[1];
return xfer(d, &cmd, rx, 1);
}
uint8_t nrf_read(nrf_t *d, uint8_t reg)
{
uint8_t tx[2] = {(uint8_t)(NRF_R_REGISTER | reg), NRF_NOP}, rx[2];
xfer(d, tx, rx, 2);
return rx[1];
}
void nrf_write(nrf_t *d, uint8_t reg, uint8_t v)
{
uint8_t tx[2] = {(uint8_t)(NRF_W_REGISTER | reg), v}, rx[2];
xfer(d, tx, rx, 2);
}
void nrf_write_addr(nrf_t *d, uint8_t reg, const uint8_t a[5])
{
uint8_t tx[6] = {(uint8_t)(NRF_W_REGISTER | reg), a[0], a[1], a[2], a[3], a[4]}, rx[6];
xfer(d, tx, rx, 6); /* младший байт адреса — первым */
}
int nrf_init(nrf_t *d, uint8_t id, const nrf_cfg_t *c, int rx)
{
static const uint8_t rate[] = {0, NRF_RF_DR_HIGH, NRF_RF_DR_LOW};
*d = (nrf_t){0};
d->id = id;
d->len = c->len;
d->dyn = c->dyn;
nrf_port_ce(id, 0);
nrf_write(d, NRF_SETUP_AW, 3u); /* адрес 5 байт */
if (nrf_read(d, NRF_SETUP_AW) != 3u)
return NRF_E_NOCHIP; /* нет питания или провода: читаются нули или единицы */
nrf_write(d, NRF_RF_CH, c->ch & 0x7Fu);
nrf_write(d, NRF_RF_SETUP, (uint8_t)(rate[c->rate] | NRF_RF_PWR_0DBM));
nrf_write(d, NRF_SETUP_RETR, (uint8_t)(c->ard << 4 | (c->arc & 0x0Fu)));
nrf_write(d, NRF_FEATURE, c->dyn ? NRF_EN_DPL : 0u);
nrf_write(d, NRF_DYNPD, 0);
nrf_write(d, NRF_EN_AA, 0);
nrf_write(d, NRF_EN_RXADDR, 0);
nrf_cmd(d, NRF_FLUSH_TX);
nrf_cmd(d, NRF_FLUSH_RX);
nrf_write(d, NRF_STATUS, NRF_RX_DR | NRF_TX_DS | NRF_MAX_RT);
if (!rx) {
nrf_write_addr(d, NRF_TX_ADDR, c->addr);
nrf_open_rx(d, 0, c->addr); /* подтверждение приходит на адрес трубы 0 */
}
d->config = (uint8_t)(NRF_EN_CRC | (c->crc2 ? NRF_CRCO : 0u) | NRF_PWR_UP | (rx ? NRF_PRIM_RX : 0u));
nrf_write(d, NRF_CONFIG, d->config);
nrf_port_delay_us(id, NRF_T_PWR_US);
return NRF_OK;
}
void nrf_open_rx(nrf_t *d, uint8_t pipe, const uint8_t a[5])
{
if (pipe < 2u)
nrf_write_addr(d, (uint8_t)(NRF_RX_ADDR_P0 + pipe), a);
else
nrf_write(d, (uint8_t)(NRF_RX_ADDR_P0 + pipe), a[0]); /* старшие 4 байта — общие с трубой 1 */
nrf_write(d, NRF_EN_RXADDR, (uint8_t)(nrf_read(d, NRF_EN_RXADDR) | 1u << pipe));
nrf_write(d, NRF_EN_AA, (uint8_t)(nrf_read(d, NRF_EN_AA) | 1u << pipe));
nrf_write(d, (uint8_t)(NRF_RX_PW_P0 + pipe), d->len);
if (d->dyn)
nrf_write(d, NRF_DYNPD, (uint8_t)(nrf_read(d, NRF_DYNPD) | 1u << pipe));
}
void nrf_listen(nrf_t *d)
{
nrf_port_ce(d->id, 1);
nrf_port_delay_us(d->id, NRF_T_RX_US);
}
int nrf_send(nrf_t *d, const uint8_t *p, uint8_t n)
{
uint8_t tx[33] = {NRF_W_TX_PAYLOAD}, rx[33];
uint8_t len = d->dyn ? n : d->len; /* постоянная длина: хвост — нули */
for (uint8_t i = 0; i < len; i++)
tx[1 + i] = i < n ? p[i] : 0u;
xfer(d, tx, rx, 1u + len);
nrf_port_ce(d->id, 1); /* импульс CE — один пакет со всеми повторами */
nrf_port_delay_us(d->id, NRF_T_CE_US);
nrf_port_ce(d->id, 0);
if (!nrf_port_wait_irq(d->id, NRF_T_SEND_US)) {
nrf_cmd(d, NRF_FLUSH_TX);
d->timeouts++;
return NRF_E_TIMEOUT; /* IRQ не пришёл: модуль сброшен или не включён */
}
uint8_t st = nrf_cmd(d, NRF_NOP);
d->retries += nrf_read(d, NRF_OBSERVE_TX) & 0x0Fu; /* ARC_CNT: сколько повторов понадобилось */
nrf_write(d, NRF_STATUS, st & (NRF_TX_DS | NRF_MAX_RT));
if (st & NRF_TX_DS) {
d->sent++;
return NRF_OK;
}
nrf_cmd(d, NRF_FLUSH_TX); /* после MAX_RT пакет остаётся в FIFO */
d->failed++;
return NRF_E_MAXRT;
}
int nrf_recv(nrf_t *d, uint8_t *p, uint8_t *pipe)
{
uint8_t tx[33] = {NRF_R_RX_PAYLOAD}, rx[33];
uint8_t st = nrf_cmd(d, NRF_NOP);
if (NRF_RX_P_NO(st) > 5u) {
nrf_write(d, NRF_STATUS, NRF_RX_DR);
return 0; /* FIFO приёма пуст */
}
uint8_t n = d->len;
if (d->dyn) {
uint8_t w[2] = {NRF_R_RX_PL_WID, NRF_NOP};
xfer(d, w, rx, 2);
n = rx[1];
if (n > 32u) {
nrf_cmd(d, NRF_FLUSH_RX);
return 0;
}
}
for (uint8_t i = 1; i <= n; i++)
tx[i] = NRF_NOP;
xfer(d, tx, rx, 1u + n);
for (uint8_t i = 0; i < n; i++)
p[i] = rx[1 + i];
*pipe = (uint8_t)NRF_RX_P_NO(st);
nrf_write(d, NRF_STATUS, NRF_RX_DR); /* снять флаг; в FIFO может быть ещё пакет */
return n;
}
int nrf_alive(nrf_t *d)
{
return nrf_read(d, NRF_CONFIG) == d->config; /* после сброса CONFIG = 0x08: PWR_UP = 0 */
}
/* ── пакет Enhanced ShockBurst ────────────────────────────────────────── */
static int bit(const uint8_t *b, uint32_t i)
{
return b[i >> 3] >> (7u - (i & 7u)) & 1u;
}
static void put(uint8_t *b, uint32_t *at, uint32_t v, uint32_t nbits)
{
while (nbits--) {
if (v >> nbits & 1u)
b[*at >> 3] |= (uint8_t)(0x80u >> (*at & 7u));
(*at)++;
}
}
/* CRC по битам, старший первым: 1 байт — x^8 + x^2 + x + 1, начальное 0xFF;
* 2 байта — x^16 + x^12 + x^5 + 1, начальное 0xFFFF. Считается по адресу, PCF и данным. */
uint16_t esb_crc(const uint8_t *bits, uint32_t from, uint32_t nbits, int crc2)
{
uint16_t crc = crc2 ? 0xFFFFu : 0xFFu;
uint16_t top = crc2 ? 0x8000u : 0x80u, poly = crc2 ? 0x1021u : 0x07u, mask = crc2 ? 0xFFFFu : 0xFFu;
for (uint32_t i = from; i < from + nbits; i++) {
int fb = ((crc & top) != 0) ^ bit(bits, i);
crc = (uint16_t)((crc << 1) & mask);
if (fb)
crc ^= poly;
}
return crc;
}
uint32_t esb_pack(const uint8_t a[5], uint8_t aw, const uint8_t *p, uint8_t n, uint8_t pid, uint8_t no_ack,
int crc2, uint8_t out[ESB_MAX_BYTES])
{
uint32_t at = 0;
for (uint32_t i = 0; i < ESB_MAX_BYTES; i++)
out[i] = 0;
put(out, &at, (a[aw - 1u] & 0x80u) ? 0xAAu : 0x55u, 8); /* преамбула: первый бит — как у адреса */
for (uint32_t i = aw; i-- > 0;)
put(out, &at, a[i], 8); /* в эфир адрес идёт старшим байтом вперёд */
put(out, &at, (uint32_t)n << 3 | (uint32_t)(pid & 3u) << 1 | (no_ack & 1u), 9); /* PCF: длина, PID, NO_ACK */
for (uint32_t i = 0; i < n; i++)
put(out, &at, p[i], 8);
uint16_t crc = esb_crc(out, 8, at - 8u, crc2);
put(out, &at, crc, crc2 ? 16u : 8u);
return at;
}
fw93b.c — прошивка STM32F103: порт на SPI1, CE и IRQ, передатчик или приёмник по перемычке.
/* fw93b.c — раздел 93а: nRF24L01+ на STM32F103 (BluePill), 72 МГц.
* SPI1: PA5 — SCK, PA6 — MISO, PA7 — MOSI, PA4 — CSN (обычный выход); PB0 — CE; PB1 — IRQ (EXTI1, спад).
* Перемычка PB10 на землю — приёмник, без неё — передатчик. Передатчик раз в 100 мс шлёт номер пакета;
* приёмник в прерывании EXTI1 забирает из FIFO все пакеты. Светодиод PC13 мигает на каждом успехе.
* Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется): APB2 = 72 МГц, SPI1 / 8 = 9 МГц. */
#include "stm32f10x.h"
#include "nrf93b.h"
#define CPU_HZ 72000000u
static const nrf_cfg_t cfg = {
.ch = 76, .rate = NRF_1MBPS, .ard = 1, .arc = 5, .crc2 = 1, .len = 8, .dyn = 0,
.addr = {0x01, 0x93, 0xA0, 0x5A, 0xC3},
};
static nrf_t radio;
static volatile uint32_t got, lost;
/* ── порт драйвера ────────────────────────────────────────────────────── */
void nrf_port_xfer(uint8_t id, const uint8_t *tx, uint8_t *rx, uint32_t n)
{
(void)id;
GPIOA->BSRR = 1u << (4 + 16); /* CSN = 0: начало команды */
for (uint32_t i = 0; i < n; i++) {
while (!(SPI1->SR & SPI_SR_TXE)) { }
SPI1->DR = tx[i];
while (!(SPI1->SR & SPI_SR_RXNE)) { }
rx[i] = (uint8_t)SPI1->DR; /* первый байт ответа — STATUS */
}
while (SPI1->SR & SPI_SR_BSY) { }
GPIOA->BSRR = 1u << 4; /* CSN = 1: команда закончена */
}
void nrf_port_ce(uint8_t id, int level)
{
(void)id;
GPIOB->BSRR = level ? 1u : 1u << 16; /* PB0 */
}
void nrf_port_delay_us(uint8_t id, uint32_t us)
{
(void)id;
uint32_t t0 = DWT->CYCCNT;
while (DWT->CYCCNT - t0 < us * (CPU_HZ / 1000000u)) { }
}
int nrf_port_wait_irq(uint8_t id, uint32_t timeout_us)
{
(void)id;
uint32_t t0 = DWT->CYCCNT;
while (GPIOB->IDR & (1u << 1)) { /* IRQ активен нулём */
if (DWT->CYCCNT - t0 >= timeout_us * (CPU_HZ / 1000000u))
return 0;
}
return 1;
}
/* ── приём: прерывание по спаду IRQ ───────────────────────────────────── */
void EXTI1_IRQHandler(void)
{
uint8_t p[32], pipe;
EXTI->PR = 1u << 1;
while (nrf_recv(&radio, p, &pipe) > 0) { /* забрать всё: в FIFO до трёх пакетов */
got++;
GPIOC->ODR ^= 1u << 13;
}
}
static void spi1_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_AFIOEN
| RCC_APB2ENR_SPI1EN;
/* PA7, PA5 — альтернативная функция 50 МГц (1011), PA6 — вход (0100), PA4 — выход 50 МГц (0011) */
GPIOA->CRL = (GPIOA->CRL & 0x0000FFFFu) | 0xB4B30000u;
GPIOA->BSRR = 1u << 4; /* CSN = 1 до первой команды */
/* PB0 — выход (0011), PB1 — вход с подтяжкой (1000) */
GPIOB->CRL = (GPIOB->CRL & 0xFFFFFF00u) | 0x00000083u;
GPIOB->BSRR = 1u << 1; /* подтяжка PB1 вверх; CE = 0 */
GPIOB->BSRR = 1u << 16;
/* PB10 — вход с подтяжкой: перемычка на землю — приёмник */
GPIOB->CRH = (GPIOB->CRH & ~(0xFu << 8)) | 0x8u << 8;
GPIOB->BSRR = 1u << 10;
GPIOC->CRH = (GPIOC->CRH & ~(0xFu << 20)) | 0x2u << 20; /* PC13 — светодиод */
/* APB2 72 МГц / 8 = 9 МГц, режим 0, старший бит первым */
SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_BR_1 | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_SPE;
}
static void irq_init(void)
{
AFIO->EXTICR[0] = (AFIO->EXTICR[0] & ~0xF0u) | 0x10u; /* EXTI1 ← PB1 */
EXTI->FTSR |= 1u << 1; /* спад: модуль поднял флаг */
EXTI->IMR |= 1u << 1;
NVIC_EnableIRQ(EXTI1_IRQn);
}
int main(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
spi1_init();
int rx = !(GPIOB->IDR & (1u << 10));
while (nrf_init(&radio, 0, &cfg, rx) != NRF_OK)
nrf_port_delay_us(0, 100000u); /* модуля нет: нули или единицы по MISO */
if (rx) {
nrf_open_rx(&radio, 1, cfg.addr);
irq_init();
nrf_listen(&radio);
for (;;)
__WFI();
}
for (uint32_t n = 0;; n++) {
uint8_t p[8] = {(uint8_t)n, (uint8_t)(n >> 8), (uint8_t)(n >> 16), (uint8_t)(n >> 24)};
int rc = nrf_send(&radio, p, sizeof p);
if (rc == NRF_OK)
GPIOC->ODR ^= 1u << 13;
else
lost++;
if (rc == NRF_E_TIMEOUT && !nrf_alive(&radio))
nrf_init(&radio, 0, &cfg, 0); /* модуль сбросился от просадки питания */
nrf_port_delay_us(0, 100000u);
}
}
Дальше: раздел 94 «LoRaWAN: сеть, ключи, классы».