# Описание прошивки zh_avr_touch_pad ## Общие сведения **zh_avr_touch_pad** — прошивка для AVR-микроконтроллеров ATMega168/328, реализующая беспроводную сенсорную кнопку на базе трансивера nRF24L01+. Прошивка работает в режиме глубокого сна (Power-down), пробуждается по внешнему прерыванию от сенсорного датчика и передаёт на шлюз пакет с идентификатором устройства и уровнем заряда батареи. При первом запуске выполняется автоматическое сопряжение (pairing) со шлюзом для получения идентификатора устройства, который сохраняется в EEPROM. ### Основные возможности 1. **Сверхнизкое энергопотребление** — около 2 мкА в режиме сна. 2. **Передача уровня заряда батареи** — измерение напряжения через внутренний АЦП с опорным напряжением 1,1 В. 3. **Автоматическое сопряжение со шлюзом** — получение идентификатора устройства по радиоканалу с сохранением в EEPROM. 4. **Работа по протоколу nRF24L01+** — передача через SPI, адресная доставка на шлюз. 5. **Поддержка нескольких МК** — ATMega168/168P/168PB/328/328P/328PB. 6. **Пробуждение по внешнему прерыванию INT1** — rising edge на пине PD3. 7. **Генерация псевдослучайного токена** — на основе шума АЦП и состояния MCUSR для уникализации запроса сопряжения. 8. **Watchdog-таймер в режиме прерывания** — используется для таймаутов при сопряжении без сброса МК. --- ## Внутреннее устройство ### Структура передаваемых данных Прошивка не использует динамическое выделение памяти. Все данные хранятся в статической области и регистрах микроконтроллера. Пакет формируется в глобальной переменной `transmitted_data` типа `zh_rf_sensor_transmitted_data_t` из внешней библиотеки `zh_rf_config` и сериализуется функцией `zh_rf_sensor_build_payload()` перед передачей. | Поле | Тип | Описание | | ------ | ----- | ---------- | | `id` | `int16_t` | Идентификатор устройства, присвоенный шлюзом при сопряжении | | `type` | `int16_t` | Тип устройства (`ZH_RF_SENSOR_TOUCH_SWITCH`) | | `battery` | `int16_t` | Напряжение батареи × 100 (например, 330 = 3.30 В) | ### Конфигурационные макросы | Макрос | Значение | Описание | | -------- | ---------- | ---------- | | `GATEWAY_CHANNEL` | `120` | Радиоканал шлюза | | `GATEWAY_ADDRESS` | `0xDDEEFF` | 3-байтовый адрес шлюза | | `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`. Флаги `send_request`, `nrf_event` объявлены как `static volatile uint8_t` для корректного доступа из ISR и главного цикла. ### Флаги синхронизации | Флаг | Устанавливается | Используется | | ------ | ----------------- | -------------- | | `send_request` | ISR `INT1_vect` (касание) | Главный цикл — запуск передачи | | `nrf_event` | ISR `INT0_vect` (IRQ nRF24) | Конечный автомат сопряжения | --- ## API (интерфейс) Все функции являются `static` и доступны только внутри модуля `main.c`. ### Инициализация | Функция | Описание | | --------- | ---------- | | `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_random_token()` | Генерация 16-битного псевдослучайного токена из шума АЦП и MCUSR для запроса сопряжения | ### Передача данных | Функция | Описание | |---------|----------| | `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()` | Запись буфера данных с указанной 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. Синхронизация между прерываниями и главным циклом реализуется следующим образом: 1. **Флаги `volatile`** — переменные `send_request`, `nrf_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()`. ### Алгоритм измерения напряжения батареи - АЦП конфигурируется с внутренним опорным напряжением 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`. - После измерения АЦП отключается сбросом бита `ADEN`. ### Алгоритм генерации псевдослучайного токена - Инициальное значение — регистр `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 мс (проверка каждые 1 мс). - Сброс CE, чтение статуса, сброс флагов, очистка FIFO, переход в Power Down. ### Алгоритм сна и пробуждения - Главный цикл проверяет флаг `send_request`. - Если флаг не установлен — вызывается `sleep_cpu()` (режим Power-down). - Пробуждение происходит по аппаратному прерыванию INT1 (rising edge на PD3). - ISR устанавливает `send_request = true`, цикл просыпается и выполняет передачу. --- ## Примеры использования ### Типовой цикл работы (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(); set_sleep_mode(SLEEP_MODE_PWR_DOWN); sei(); // Загрузка или получение идентификатора устройства 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 == true) { cli(); send_request = false; sei(); 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 == false) { sleep_enable(); sei(); sleep_cpu(); } sei(); sleep_disable(); } } return 0; } ``` ### Назначение пинов AVR для nRF24L01+ ```c // 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 ``` --- ## Ошибки Прошивка не возвращает коды ошибок в стиле ESP-IDF. Все функции имеют тип `void` или возвращают однобайтовый статус SPI (`uint8_t`) для низкоуровневых операций. | Условие | Поведение | | --------- | ----------- | | Нулевое показание АЦП | `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) в рабочем цикле** — приём используется исключительно на этапе сопряжения, шлюз не может отправить данные устройству в штатном режиме. 2. **Отсутствие шифрования** — CRC включён, но AES-шифрование не используется. Пакеты могут быть перехвачены. 3. **Жёстко зашитый адрес и канал шлюза** — смена `GATEWAY_ADDRESS` или `GATEWAY_CHANNEL` требует перепрошивки. 4. **Таймаут блокирующей передачи 100 мс** — если шлюз недоступен, пакет теряется без повторной попытки на уровне приложения (аппаратные ретрансмиты настроены через `SETUP_RETR = 0xFF`: 15 попыток с максимальным интервалом). 5. **Одно сенсорное событие за цикл** — несколько касаний во время передачи будут потеряны (флаг `send_request` — один бит). 6. **Точность измерения батареи** — зависит от стабильности внутреннего опорного напряжения 1,1 В и может отличаться от реального напряжения на ±3%. 7. **SPI остаётся включённым в режиме сна** — перед окончательным сном SPI отключается в `zh_enter_final_sleep()`, но в штатном режиме ожидания SPI не отключается между передачами. --- ## Требования для сборки - PlatformIO с плагином `platform-atmelavr` - AVR-GCC (входит в состав PlatformIO) - Библиотека `zh_rf_config` (подключается через `lib_deps` в `platformio.ini`) - Программатор USBasp (или совместимый) - Фьюзы: LFUSE=`0xE2`, HFUSE=`0xDF`, EFUSE=`0x07` (8 МГц, без делителя) --- ## Установка ### Сборка и прошивка через PlatformIO ```bash git clone https://github.com/aZholtikov/zh_avr_touch_pad cd zh_avr_touch_pad pio run -e ATmega328P -t upload ``` ### Подключение в проекте Компонент является самодостаточной прошивкой — не подключается как библиотека. Для интеграции в другой проект скопируйте `src/main.c`, добавьте зависимость `zh_rf_config` и адаптируйте макросы конфигурации. ```ini ; platformio.ini 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" ```