Files
zh_avr_touch_pad/wiki_ru.md
T

353 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Описание прошивки 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`, `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) | Конечный автомат сопряжения |
---
## 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`, `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()`.
### Алгоритм измерения напряжения батареи
- АЦП конфигурируется с внутренним опорным напряжением 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"
```