feat: added sensor pairing to gateway

This commit is contained in:
aZholtikov committed 2026-10-24 10:03:54 +03:00
1 parent cd81e27665
commit 4514ce4a10
4 files changed
+638 -112

No files matched your search

+165 -84
View File
@@ -1,53 +1,61 @@
# Описание компонента zh_avr_touch_pad
# Описание прошивки zh_avr_touch_pad
## Общие сведения
**zh_avr_touch_pad** — прошивка для AVR-микроконтроллеров ATMega168/328, реализующая беспроводную сенсорную кнопку на базе трансивера nRF24L01+.
Компонент работает в режиме глубокого сна (Power-down), пробуждается по внешнему прерыванию от сенсорного датчика и передаёт на шлюз пакет с идентификатором устройства и уровнем заряда батареи.
Прошивка работает в режиме глубокого сна (Power-down), пробуждается по внешнему прерыванию от сенсорного датчика и передаёт на шлюз пакет с идентификатором устройства и уровнем заряда батареи. При первом запуске выполняется автоматическое сопряжение (pairing) со шлюзом для получения идентификатора устройства, который сохраняется в EEPROM.
### Основные возможности
1. **Сверхнизкое энергопотребление** — около 2 мкА в режиме сна.
2. **Передача уровня заряда батареи** — измерение напряжения через внутренний АЦП с опорным напряжением 1,1 В.
3. **Работа по протоколу nRF24L01+** — передача через SPI, адресная доставка на шлюз.
4. **Поддержка нескольких МК** — ATMega168/168P/168PB/328/328P/328PB.
5. **Минимальная обвязка** — установка в стандартный корпус сенсорного выключателя.
3. **Автоматическое сопряжение со шлюзом** — получение идентификатора устройства по радиоканалу с сохранением в EEPROM.
4. **Работа по протоколу nRF24L01+** — передача через SPI, адресная доставка на шлюз.
5. **Поддержка нескольких МК** — ATMega168/168P/168PB/328/328P/328PB.
6. **Пробуждение по внешнему прерыванию INT1** — rising edge на пине PD3.
7. **Автономная работа** — не требует внешних подтягивающих резисторов.
7. **Генерация псевдослучайного токена** — на основе шума АЦП и состояния MCUSR для уникализации запроса сопряжения.
8. **Watchdog-таймер в режиме прерывания** — используется для таймаутов при сопряжении без сброса МК.
---
## Внутреннее устройство
### Структура данных
### Структура передаваемых данных
Компонент не использует динамическое выделение памяти и сложные структуры данных. Все данные передаются как массив `int16_t` фиксированной длины.
Прошивка не использует динамическое выделение памяти. Все данные хранятся в статической области и регистрах микроконтроллера.
#### Формат передаваемого пакета
Пакет формируется в глобальной переменной `transmitted_data` типа `zh_rf_sensor_transmitted_data_t` из внешней библиотеки `zh_rf_config` и передаётся побайтово как массив байтов (`sizeof` структуры — 14 байт).
Пакет формируется в глобальной переменной `transmitted_data` типа `zh_rf_sensor_transmitted_data_t` из внешней библиотеки `zh_rf_config` и сериализуется функцией `zh_rf_sensor_build_payload()` перед передачей.
| Поле | Тип | Описание |
| ------ | ----- | ---------- |
| `id` | `int16_t` | Идентификатор устройства (`DEVICE_ID = 6`) |
| `id` | `int16_t` | Идентификатор устройства, присвоенный шлюзом при сопряжении |
| `type` | `int16_t` | Тип устройства (`ZH_RF_SENSOR_TOUCH_SWITCH`) |
| `battery_voltage` | `int16_t` | Напряжение батареи × 100 (например, 330 = 3.30 В) |
| `value_1`–`value_4` | `int16_t` | Зарезервированы (равны 0) |
| `battery` | `int16_t` | Напряжение батареи × 100 (например, 330 = 3.30 В) |
### Конфигурационные макросы
| Макрос | Значение | Описание |
|--------|----------|----------|
| -------- | ---------- | ---------- |
| `GATEWAY_CHANNEL` | `120` | Радиоканал шлюза |
| `GATEWAY_ADDRESS` | `0xDDEEFF` | 3-байтовый адрес шлюза |
| `DEVICE_ID` | `6` | Уникальный идентификатор устройства |
Тип устройства задаётся не макросом, а значением перечисления `ZH_RF_SENSOR_TOUCH_SWITCH` из библиотеки `zh_rf_config`.
| `EEPROM_DEVICE_ID_ADDR` | `((uint8_t *)0x00)` | Адрес в EEPROM для хранения идентификатора устройства |
| `DEVICE_ID_UNASSIGNED` | `0xFF` | Значение, обозначающее отсутствие сохранённого идентификатора |
| `PAIRING_TIMEOUT_ATTEMPTS` | `7` | Максимальное число попыток сопряжения перед уходом в окончательный сон |
| `WDT_T_250MS` | `0x04` | Таймаут watchdog-таймера 250 мс |
| `WDT_T_4S` | `0x08` | Таймаут watchdog-таймера 4 с |
### Управление памятью
Компонент не использует динамическую память. Все данные хранятся в статической области и регистрах микроконтроллера. Глобальная переменная `transmitted_data` заполняется один раз при старте (идентификатор и тип), в рабочем цикле обновляется только поле `battery_voltage`. Флаг `send_request` объявлен как `static volatile uint8_t` для корректного доступа из ISR и главного цикла.
Прошивка не использует динамическую память. Глобальная переменная `transmitted_data` заполняется при старте (идентификатор и тип), в рабочем цикле обновляется только поле `battery`. Флаги `send_request`, `nrf_event`, `wdt_event` объявлены как `static volatile uint8_t` для корректного доступа из ISR и главного цикла.
### Флаги синхронизации
| Флаг | Устанавливается | Используется |
| ------ | ----------------- | -------------- |
| `send_request` | ISR `INT1_vect` (касание) | Главный цикл — запуск передачи |
| `nrf_event` | ISR `INT0_vect` (IRQ nRF24) | Конечный автомат сопряжения |
| `wdt_event` | ISR `WDT_vect` (watchdog) | Конечный автомат сопряжения |
---
@@ -62,68 +70,118 @@
| `zh_pad_interrupt_init()` | Настройка внешнего прерывания INT1 (PD3, rising edge) для пробуждения по касанию |
| `zh_spi_init()` | Инициализация аппаратного SPI в режиме мастера (Fosc/16) |
| `zh_nrf24_init()` | Инициализация трансивера nRF24L01+ в режим передачи |
| `zh_nrf24_interrupt_init()` | Настройка внешнего прерывания INT0 (PD2, falling edge) для сигнала IRQ nRF24 |
### Работа с EEPROM
| Функция | Описание |
|---------|----------|
| `zh_load_device_id()` | Чтение идентификатора устройства из EEPROM, возврат `DEVICE_ID_UNASSIGNED` если не сохранён |
| `zh_save_device_id()` | Сохранение идентификатора устройства в EEPROM |
### Измерение
| Функция | Описание |
|---------|----------|
| `zh_get_battery_voltage()` | Измерение напряжения батареи через внутренний АЦП с опорой 1,1 В |
| `zh_get_battery_voltage()` | Измерение напряжения батареи через внутренний АЦП с опорой 1,1 В, возврат значения в вольтах |
| `zh_get_random_token()` | Генерация 16-битного псевдослучайного токена из шума АЦП и MCUSR для запроса сопряжения |
### Передача данных
| Функция | Описание |
|---------|----------|
| `zh_nrf24_send()` | Передача буфера через nRF24L01+ с полным циклом TX: Power Up, `FLUSH_TX`, загрузка `W_TX_PAYLOAD`, импульс CE, ожидание флага TX_DS/MAX_RT по пину IRQ, сброс статуса и переход в Power Down |
| `zh_nrf24_send()` | Блокирующая передача буфера через nRF24L01+ с ожиданием завершения по пину IRQ (таймаут 100 мс) |
| `zh_nrf24_send_no_wait()` | Неблокирующая передача — загружает буфер, импульс CE 15 мкс, завершение по прерыванию IRQ |
### Управление режимами nRF24L01+
| Функция | Описание |
|---------|----------|
| `zh_nrf24_enter_rx()` | Переключение nRF24L01+ в режим приёма на трубе 0 |
| `zh_nrf24_enter_power_down()` | Перевод nRF24L01+ в режим Power Down |
### Низкоуровневые SPI-операции
| Функция | Описание |
| --------- | ---------- |
| `zh_spi_transfer()` | Обмен одного байта по SPI (полный дуплекс) |
| `zh_nrf24_send_command()` | Отправка однобайтовой команды и чтение регистра статуса |
| `zh_nrf24_write_register()` | Запись одного байта в регистр nRF24L01+ |
| `zh_nrf24_write_buff_register()` | Запись многобайтового буфера в регистр nRF24L01+ |
| `zh_nrf24_write_buffer()` | Запись буфера данных с указанной командой |
| `zh_nrf24_send_command()` | Отправка однобайтовой команды и чтение статуса |
| `zh_nrf24_write_buffer()` | Запись буфера данных с указанной SPI-командой |
| `zh_nrf24_read_payload()` | Чтение принятого пакета из RX FIFO nRF24 в буфер |
| `zh_nrf24_read_payload_width()` | Чтение длины пакета в RX FIFO nRF24 |
### Watchdog-таймер
| Функция | Описание |
|---------|----------|
| `zh_wdt_start()` | Запуск watchdog-таймера в режиме прерывания с указанным таймаутом |
| `zh_wdt_stop()` | Остановка watchdog-таймера |
### Сон и сопряжение
| Функция | Описание |
|---------|----------|
| `zh_do_pairing()` | Запуск конечного автомата сопряжения со шлюзом для получения идентификатора |
| `zh_enter_final_sleep()` | Переход в окончательный Power-down сон до сброса: отключение всех прерываний, watchdog, радиомодуля, АЦП, SPI и неиспользуемых периферийных блоков |
---
## Потокобезопасность
Компонент работает на bare-metal AVR без RTOS. Синхронизация между прерыванием и главным циклом реализуется следующим образом:
Прошивка работает на bare-metal AVR без RTOS. Синхронизация между прерываниями и главным циклом реализуется следующим образом:
1. **`volatile` флаг** — переменная `send_request` объявлена как `volatile`, что гарантирует чтение актуального значения из памяти при каждой проверке в главном цикле.
2. **Атомарная запись из ISR** — обработка INT1 устанавливает флаг в единицу. Запись одного байта на AVR является атомарной операцией.
3. **Отключение прерываний при инициализации** — `cli()` перед настройкой периферии исключает гонки при старте.
1. **Флаги `volatile`** — переменные `send_request`, `nrf_event`, `wdt_event` объявлены как `static volatile uint8_t`, что гарантирует чтение актуального значения из памяти при каждой проверке в главном цикле.
2. **Атомарная запись из ISR** — обработка прерывания устанавливает флаг в `true`. Запись одного байта на AVR является атомарной операцией.
3. **Отключение прерываний при проверке и сбросе флага** — `cli()` перед проверкой и сбросом `send_request` в главном цикле исключает гонку между ISR и главным циклом.
4. **Отключение прерываний при инициализации** — `cli()` перед настройкой периферии исключает гонки при старте.
---
## Алгоритмы
### Алгоритм сопряжения (pairing)
Конечный автомат `zh_do_pairing()` работает в четырёх состояниях:
1. **`ZH_RF_PAIR_SEND`** — генерируется псевдослучайный токен, формируется пакет запроса (`zh_rf_sensor_pairing_data_t` с `magic = ZH_RF_PAIRING_REQ_MAGIC`), выполняется неблокирующая передача через `zh_nrf24_send_no_wait()`.
2. **`ZH_RF_PAIR_TX_WAIT`** — ожидание прерывания IRQ (завершение передачи). Если пин IRQ уже низкий — событие считается наступившим. Иначе — переход в сон с watchdog-таймером 250 мс. При флаге `TX_DS` — переход в приём, при `MAX_RT` — в backoff.
3. **`ZH_RF_PAIR_RX_WAIT`** — ожидание ответа шлюза. При получении пакета проверяется: длина равна `ZH_RF_PAIRING_PACKET_SIZE`, `magic == ZH_RF_PAIRING_ACK_MAGIC`, тип совпадает, токен совпадает, `id != 0` и `id != DEVICE_ID_UNASSIGNED`. При успехе — сохранение `id` в EEPROM и возврат.
4. **`ZH_RF_PAIR_BACKOFF`** — переход в Power Down, ожидание 4 с через watchdog. После `PAIRING_TIMEOUT_ATTEMPTS` (7) неудачных попыток — переход в `zh_enter_final_sleep()`.
### Алгоритм измерения напряжения батареи
- ADC конфигурируется с внутренним опорным напряжением 1,1 В на канале внутреннего источника 1,1 В (`REFS0=1`, `MUX[3:1]=111`), вход AVcc сравнивается с опорой.
- Предделитель частоты: 64 (ADPS[2:0] = 111), что даёт ~125 кГц при 8 МГц.
- АЦП конфигурируется с внутренним опорным напряжением 1,1 В на канале внутреннего источника 1,1 В (`REFS0=1`, `MUX[3:1]=111`), измеряется отношение AVcc к опоре 1,1 В.
- Предделитель частоты: 64 (`ADPS[2:0] = 111`), что даёт ~125 кГц при 8 МГц.
- Выполняются **две последовательные конверсии**: первая стабилизирует выборку, вторая даёт результат.
- Формула расчёта: `Vbat = (1024.0 × 1.1) / adc_value`.
- При нулевой выборке (`adc == 0`) возвращается 0.0.
- При нулевой выборке (`adc == 0`) возвращается `0.0`.
- После измерения АЦП отключается сбросом бита `ADEN`.
### Алгоритм передачи через nRF24L01+
### Алгоритм генерации псевдослучайного токена
- Инициальное значение — регистр `MCUSR` (состояние сброса).
- АЦП запускается на 16 конверсий в канале с внутренним опорным напряжением.
- На каждой итерации: `token = (token << 1) ^ (token >> 15) ^ ADC` — сдвиг с переносом и XOR с шумом АЦП.
- Если результат равен 0, возвращается константа `0xACE1`.
### Алгоритм блокирующей передачи через nRF24L01+
- Переход в Power Up (бит `PWR_UP` в CONFIG).
- Ожидание 5 мс (время стартования осциллятора).
- Очистка TX FIFO командой `FLUSH_TX`.
- Загрузка полезной нагрузки командой `W_TX_PAYLOAD`.
- Импульс CE (установка в 1) для запуска передачи.
- Ожидание завершения по пину IRQ (PD2) с таймаутом 100 мс.
- Сброс флагов статуса, очистка FIFO, переход в Power Down.
- Установка CE в 1 для запуска передачи.
- Ожидание спада пина IRQ (PD2) с таймаутом 100 мс (проверка каждые 1 мс).
- Сброс CE, чтение статуса, сброс флагов, очистка FIFO, переход в Power Down.
### Алгоритм сна и пробуждения
- Главный цикл проверяет флаг `send_request`.
- Если флаг не установлен — вызывается `sleep_cpu()` (режим Power-down).
- Пробуждение происходит по аппаратному прерыванию INT1 (rising edge на PD3).
- ISR устанавливает `send_request = 1`, цикл просыпается и выполняет передачу.
- ISR устанавливает `send_request = true`, цикл просыпается и выполняет передачу.
---
@@ -132,40 +190,59 @@
### Типовой цикл работы (main)
```c
#include "avr/io.h"
#include "avr/sleep.h"
#include "avr/interrupt.h"
#include "avr/wdt.h"
#include "math.h"
#include "stdbool.h"
#include "zh_rf_config.h"
static volatile uint8_t send_request = false;
zh_rf_sensor_transmitted_data_t transmitted_data = {0};
ISR(INT1_vect)
{
send_request = true;
}
int main(void)
{
// Отключаем прерывания на время инициализации
transmitted_data.type = ZH_RF_SENSOR_TOUCH_SWITCH;
MCUSR &= ~(1 << WDRF);
wdt_disable();
cli();
// Настраиваем периферию
zh_pad_interrupt_init();
zh_spi_init();
zh_nrf24_init();
// Конфигурируем режим сна Power-down
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sei();
// Глобальная структура из zh_rf_config
zh_rf_sensor_transmitted_data_t transmitted_data =
{DEVICE_ID, ZH_RF_SENSOR_TOUCH_SWITCH, 0, 0, 0, 0, 0};
// Загрузка или получение идентификатора устройства
uint8_t device_id = zh_load_device_id();
if (device_id == DEVICE_ID_UNASSIGNED)
{
device_id = zh_do_pairing();
}
transmitted_data.id = (int16_t)device_id;
for (;;)
{
if (send_request == 1)
if (send_request == true)
{
// Собираем и отправляем пакет
cli();
send_request = 0;
send_request = false;
sei();
transmitted_data.battery_voltage =
(int16_t)(zh_get_battery_voltage() * 100.0);
zh_nrf24_send((uint8_t *)&transmitted_data,
sizeof(transmitted_data));
transmitted_data.battery = (int16_t)lroundf(zh_get_battery_voltage() * 100.0);
uint8_t payload[ZH_RF_MAX_PAYLOAD_SIZE];
uint8_t size = zh_rf_sensor_build_payload(&transmitted_data, payload, sizeof(payload));
if (size > 0)
{
zh_nrf24_send(payload, size);
}
}
else
{
// Спим до следующего прерывания
cli();
if (send_request == 0)
if (send_request == false)
{
sleep_enable();
sei();
@@ -175,60 +252,56 @@ int main(void)
sleep_disable();
}
}
return 0;
}
```
### Изменение конфигурации под другое устройство
### Назначение пинов AVR для nRF24L01+
```c
// Измените идентификатор для нового устройства
#define DEVICE_ID 7 // Уникальный ID в сети
// Тип устройства задаётся перечислением ZH_RF_SENSOR_TOUCH_SWITCH
// PB2 (SS) -> CSN (выбор кристалла SPI, активный низкий)
// PB3 (MOSI) -> MOSI
// PB4 (MISO) -> MISO
// PB5 (SCK) -> SCK
// PB1 -> CE (запуск передачи/приёма)
// PD2 (INT0) -> IRQ (вход, активный низкий, с подтяжкой к VCC)
// PD3 (INT1) -> Выход сенсорного датчика (rising edge, без подтяжки)
```
### Изменение конфигурации шлюза
```c
// Измените адрес и канал под ваш шлюз
#define GATEWAY_CHANNEL 120
#define GATEWAY_ADDRESS 0xDDEEFF
// Тип устройства задаётся перечислением ZH_RF_SENSOR_TOUCH_SWITCH из zh_rf_config
```
### Подключение по SPI (схема выводов)
```c
// Назначение пинов AVR для nRF24L01+:
// PB2 (SS) -> CSN (управление SPI-селектом)
// PB3 (MOSI) -> MOSI
// PB5 (SCK) -> SCK
// PB1 -> CE (управление передачей)
// PD2 -> IRQ (вход, активный низкий, с подтяжкой к VCC)
// PD3 -> INT1 (вход сенсорного датчика, rising edge)
```
---
## Логирование
Компонент не использует ESP-IDF логирование (прошивка для AVR, а не ESP32). Логирование отсутствует.
---
## Ошибки
Компонент не возвращает коды ошибок. Все функции имеют тип `void` или возвращают однобайтовый статус SPI (`uint8_t`) для низкоуровневых операций.
Прошивка не возвращает коды ошибок в стиле ESP-IDF. Все функции имеют тип `void` или возвращают однобайтовый статус SPI (`uint8_t`) для низкоуровневых операций.
| Условие | Поведение |
| --------- | ---------- |
| Нулевое показание АЦП | `zh_get_battery_voltage()` возвращает 0.0 |
| Превышение таймаута передачи | Передача прерывается, пакет теряется |
| Неверный аргумент | Не применимо — все функции `static`, аргументы не валидируются |
| --------- | ----------- |
| Нулевое показание АЦП | `zh_get_battery_voltage()` возвращает `0.0` |
| Превышение таймаута передачи (100 мс) | Передача прерывается, пакет теряется |
| Неверная длина принятого пакета при сопряжении | Пакет отбрасывается, переход в `ZH_RF_PAIR_BACKOFF` |
| Превышение числа попыток сопряжения (7) | `zh_enter_final_sleep()` — сон до сброса |
| `zh_rf_sensor_build_payload()` возвращает 0 | Передача пропускается, переход в `zh_enter_final_sleep()` |
---
## Ограничения
1. **Только передача (TX-only)** — компонент не принимает данные, шлюз не может отправить подтверждение на уровне приложения.
1. **Только передача (TX-only) в рабочем цикле** — приём используется исключительно на этапе сопряжения, шлюз не может отправить данные устройству в штатном режиме.
2. **Отсутствие шифрования** — CRC включён, но AES-шифрование не используется. Пакеты могут быть перехвачены.
3. **Жёстко зашитый адрес шлюза** — смена адреса требует перепрошивки.
4. **Таймаут передачи 100 мс** — если шлюз недоступен, пакет теряется без повторной попытки на уровне приложения (аппаратные ретрансмиты настроены через `SETUP_RETR = 0xFF`: 15 попыток с максимальным интервалом).
5. **Одно сенсорное событие за цикл** — несколько касаний во время передачи будут потеряны (флаг — один бит).
3. **Жёстко зашитый адрес и канал шлюза** — смена `GATEWAY_ADDRESS` или `GATEWAY_CHANNEL` требует перепрошивки.
4. **Таймаут блокирующей передачи 100 мс** — если шлюз недоступен, пакет теряется без повторной попытки на уровне приложения (аппаратные ретрансмиты настроены через `SETUP_RETR = 0xFF`: 15 попыток с максимальным интервалом).
5. **Одно сенсорное событие за цикл** — несколько касаний во время передачи будут потеряны (флаг `send_request` — один бит).
6. **Точность измерения батареи** — зависит от стабильности внутреннего опорного напряжения 1,1 В и может отличаться от реального напряжения на ±3%.
7. **Нет механизма глубокого сна для периферии** — АЦП полностью отключается после измерения (`ADEN = 0`), но SPI остаётся включённым в режиме сна.
7. **SPI остаётся включённым в режиме сна** — перед окончательным сном SPI отключается в `zh_enter_final_sleep()`, но в штатном режиме ожидания SPI не отключается между передачами.
---
@@ -247,6 +320,8 @@ int main(void)
### Сборка и прошивка через PlatformIO
```bash
git clone https://github.com/aZholtikov/zh_avr_touch_pad
cd zh_avr_touch_pad
pio run -e ATmega328P -t upload
```
@@ -260,12 +335,18 @@ lib_deps =
https://github.com/aZholtikov/zh_rf_config
```
### Подключение заголовков в коде
```c
// Зависимости: стандартные заголовки AVR-libc и конфигурация протокола
#include "avr/io.h"
#include "avr/sleep.h"
#include "avr/fuse.h"
#include "avr/interrupt.h"
#include "avr/eeprom.h"
#include "avr/wdt.h"
#include "util/delay.h"
#include "math.h"
#include "stdbool.h"
#include "zh_rf_config.h"
```