Compare commits

...
6 Commits
Author SHA1 Message Date
aZholtikov 5a8094e9ff style: big renaming 2026-10-31 07:37:20 +03:00
aZholtikov e334aec37f style: big renaming 2026-10-25 11:48:46 +03:00
aZholtikov cb5c29518b doc: updated wiki 2026-10-25 08:48:53 +03:00
aZholtikov 6261106b30 style: minor changes 2026-10-25 08:31:08 +03:00
aZholtikov 035e5edfd4 perf: removed unused variable 2026-10-24 11:09:19 +03:00
aZholtikov 4514ce4a10 feat: added sensor pairing to gateway 2026-10-24 10:03:54 +03:00
5 changed files with 959 additions and 441 deletions

No files matched your search

+3 -2
View File
@@ -7,8 +7,8 @@
3. Transmits device identifier and battery voltage level on each touch event.
4. Battery measurement through internal ADC with 1.1 V bandgap reference.
5. Wake-up from power-down mode by external interrupt on rising edge.
6. Supports ATMega168, ATMega168P, ATMega168PB, ATMega328, ATMega328P, ATMega328PB.
7. Easy installation into any standard touch pad enclosure.
6. Automatic pairing with the gateway to obtain a device identifier stored in EEPROM.
7. Supports ATMega168, ATMega168P, ATMega168PB, ATMega328, ATMega328P, ATMega328PB.
## Attention
@@ -16,6 +16,7 @@
2. After power-on reset the first touch event may not trigger a transmission.
3. Multi-touch is not supported, only a single touch event per transmission cycle.
4. Changing gateway address or channel requires reflashing the microcontroller.
5. If the gateway does not respond during pairing, the device enters permanent sleep until reset.
## Using
+6 -6
View File
@@ -12,7 +12,7 @@ upload_flags =
-Pusb
-e
lib_deps =
https://github.com/aZholtikov/zh_rf_config
https://github.com/aZholtikov/zh_nrf24_config#dev
[env:ATmega168P]
platform = atmelavr
@@ -28,7 +28,7 @@ upload_flags =
-Pusb
-e
lib_deps =
https://github.com/aZholtikov/zh_rf_config
https://github.com/aZholtikov/zh_nrf24_config#dev
[env:ATmega168PB]
platform = atmelavr
@@ -44,7 +44,7 @@ upload_flags =
-Pusb
-e
lib_deps =
https://github.com/aZholtikov/zh_rf_config
https://github.com/aZholtikov/zh_nrf24_config#dev
[env:ATmega328]
platform = atmelavr
@@ -60,7 +60,7 @@ upload_flags =
-Pusb
-e
lib_deps =
https://github.com/aZholtikov/zh_rf_config
https://github.com/aZholtikov/zh_nrf24_config#dev
[env:ATmega328P]
platform = atmelavr
@@ -76,7 +76,7 @@ upload_flags =
-Pusb
-e
lib_deps =
https://github.com/aZholtikov/zh_rf_config
https://github.com/aZholtikov/zh_nrf24_config#dev
[env:ATmega328PB]
platform = atmelavr
@@ -92,4 +92,4 @@ upload_flags =
-Pusb
-e
lib_deps =
https://github.com/aZholtikov/zh_rf_config
https://github.com/aZholtikov/zh_nrf24_config#dev
-334
View File
@@ -1,334 +0,0 @@
#include "avr/io.h"
#include "avr/sleep.h"
#include "avr/fuse.h"
#include "avr/interrupt.h"
#include "util/delay.h"
#include "math.h"
#include "zh_rf_config.h"
#if defined(__AVR_ATmega328PB__)
#ifndef SPCR
#define SPCR SPCR0
#endif
#ifndef SPDR
#define SPDR SPDR0
#endif
#ifndef SPSR
#define SPSR SPSR0
#endif
#ifndef SPE
#define SPE SPE0
#endif
#ifndef MSTR
#define MSTR MSTR0
#endif
#ifndef SPR0
#define SPR0 SPR00
#endif
#ifndef SPIF
#define SPIF SPIF0
#endif
#endif
#define GATEWAY_CHANNEL 120
#define GATEWAY_ADDRESS 0xDDEEFF
#define DEVICE_ID 6
#define NRF_CMD_W_REGISTER 0x20
#define NRF_CMD_W_TX_PAYLOAD 0xA0
#define NRF_CMD_FLUSH_TX 0xE1
#define NRF_CMD_NOP 0xFF
#define NRF_REG_CONFIG 0x00
#define NRF_REG_EN_AA 0x01
#define NRF_REG_EN_RXADDR 0x02
#define NRF_REG_SETUP_AW 0x03
#define NRF_REG_SETUP_RETR 0x04
#define NRF_REG_RF_CH 0x05
#define NRF_REG_RF_SETUP 0x06
#define NRF_REG_STATUS 0x07
#define NRF_REG_RX_ADDR_P0 0x0A
#define NRF_REG_TX_ADDR 0x10
#define NRF_CONFIG_MASK_RX_DR 0x40
#define NRF_CONFIG_EN_CRC 0x08
#define NRF_CONFIG_PWR_UP 0x02
#define NRF_STATUS_RX_DR 0x40
#define NRF_STATUS_TX_DS 0x20
#define NRF_STATUS_MAX_RT 0x10
/**
* @brief Initialize INT1 external interrupt for the touch-pad wake-up signal.
*
* Configures PD3 as a rising-edge triggered external interrupt source and
* enables the INT1 interrupt mask bit. The ISR sets the send_request flag
* to trigger a radio transmission from the main loop.
*/
static void zh_pad_interrupt_init(void);
/**
* @brief Initialize the hardware SPI peripheral in master mode.
*
* Configures MOSI, SCK and SS pins as outputs, drives SS high, and enables
* SPI with Fosc/16 clock rate.
*/
static void zh_spi_init(void);
/**
* @brief Perform a single full-duplex SPI byte transfer.
*
* @param data Byte to transmit
*
* @return Byte received from the slave during the transfer
*/
static uint8_t zh_spi_transfer(uint8_t data);
/**
* @brief Initialize the nRF24L01+ transceiver for TX-only operation.
*
* Drives CE low, configures auto-acknowledgment, address width, retransmit
* settings, channel, RF power and data rate, and loads the gateway address
* into both TX and RX pipe 0 registers.
*/
static void zh_nrf24_init(void);
/**
* @brief Transmit a payload buffer through the nRF24L01+ module.
*
* Powers up the radio, flushes the TX FIFO, loads the payload, pulses CE to
* start transmission, waits for the TX_DS or MAX_RT flag, then clears status
* flags and returns the radio to power-down mode.
*
* @param buf Pointer to the payload data to transmit (must not be NULL)
* @param size Number of bytes in the payload buffer (must be > 0)
*/
static void zh_nrf24_send(uint8_t *buf, uint8_t size);
/**
* @brief Write a data buffer to the nRF24L01+ via SPI with a given command.
*
* Asserts CSN low, sends the command byte followed by the buffer contents,
* then deasserts CSN.
*
* @param cmd SPI command byte to send before the data
* @param buf Pointer to the data buffer to write (must not be NULL)
* @param count Number of bytes to transfer from the buffer
*
* @return Status register value returned by the nRF24L01+
*/
static uint8_t zh_nrf24_write_buffer(uint8_t cmd, uint8_t *buf, uint8_t count);
/**
* @brief Write a single byte value to an nRF24L01+ register.
*
* @param reg Register address to write to
* @param val Byte value to store in the register
*
* @return Status register value returned by the nRF24L01+
*/
static uint8_t zh_nrf24_write_register(uint8_t reg, uint8_t val);
/**
* @brief Write a multi-byte buffer into an nRF24L01+ register.
*
* Convenience wrapper around zh_nrf24_write_buffer that prepends the
* W_REGISTER command to the register address.
*
* @param reg Register address to write to
* @param buf Pointer to the data buffer to write (must not be NULL)
* @param count Number of bytes to transfer from the buffer
*
* @return Status register value returned by the nRF24L01+
*/
static uint8_t zh_nrf24_write_buff_register(uint8_t reg, uint8_t *buf, uint8_t count);
/**
* @brief Send a single-byte command to the nRF24L01+ and read the status.
*
* @param cmd Command byte to send
*
* @return Status register value returned by the nRF24L01+
*/
static uint8_t zh_nrf24_send_command(uint8_t cmd);
/**
* @brief Measure the battery voltage using the internal AVcc reference.
*
* Enables the ADC with the 1.1 V internal reference on the 1.1 V supply
* channel, performs two conversions and returns the calculated battery
* voltage based on the AVcc-to-internal-reference ratio.
*
* @return Battery voltage in volts, or 0.0 if the ADC reading is zero
*/
static float zh_get_battery_voltage(void);
static volatile uint8_t send_request = 0;
zh_rf_sensor_transmitted_data_t transmitted_data = {DEVICE_ID, ZH_RF_SENSOR_TOUCH_SWITCH, 0, 0, 0, 0, 0};
int main(void)
{
cli();
zh_pad_interrupt_init();
zh_spi_init();
zh_nrf24_init();
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sei();
for (;;)
{
if (send_request == 1)
{
cli();
send_request = 0;
sei();
transmitted_data.battery_voltage = (int16_t)lroundf(zh_get_battery_voltage() * 100.0);
zh_nrf24_send((uint8_t *)&transmitted_data, sizeof(transmitted_data));
}
else
{
cli();
if (send_request == 0)
{
sleep_enable();
sei();
sleep_cpu();
}
sei();
sleep_disable();
}
}
return 0;
}
ISR(INT1_vect)
{
send_request = 1;
}
static void zh_pad_interrupt_init(void)
{
DDRD &= ~(1 << PD3);
PORTD &= ~(1 << PD3);
EICRA &= ~((1 << ISC11) | (1 << ISC10));
EICRA |= (1 << ISC11) | (1 << ISC10);
EIFR |= (1 << INTF1);
EIMSK |= (1 << INT1);
}
static void zh_spi_init(void)
{
DDRB |= (1 << PB2) | (1 << PB3) | (1 << PB5);
PORTB |= (1 << PB2);
SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0);
}
static uint8_t zh_spi_transfer(uint8_t data)
{
SPDR = data;
while ((SPSR & (1 << SPIF)) == 0)
{
}
return SPDR;
}
static void zh_nrf24_init(void)
{
_delay_ms(100);
DDRB |= (1 << PB1);
PORTB &= ~(1 << PB1);
DDRD &= ~(1 << PD2);
PORTD |= (1 << PD2);
uint64_t gateway_address = GATEWAY_ADDRESS;
zh_nrf24_write_register(NRF_REG_CONFIG, 0x00);
zh_nrf24_write_register(NRF_REG_EN_AA, 0x01);
zh_nrf24_write_register(NRF_REG_EN_RXADDR, 0x01);
zh_nrf24_write_register(NRF_REG_SETUP_AW, 0x01);
zh_nrf24_write_register(NRF_REG_SETUP_RETR, 0xFF);
zh_nrf24_write_register(NRF_REG_RF_CH, GATEWAY_CHANNEL);
zh_nrf24_write_register(NRF_REG_RF_SETUP, 0x26);
zh_nrf24_write_buff_register(NRF_REG_TX_ADDR, (uint8_t *)&gateway_address, 3);
zh_nrf24_write_buff_register(NRF_REG_RX_ADDR_P0, (uint8_t *)&gateway_address, 3);
zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_RX_DR | NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT);
zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_MASK_RX_DR | NRF_CONFIG_EN_CRC);
}
static void zh_nrf24_send(uint8_t *buf, uint8_t size)
{
zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_MASK_RX_DR | NRF_CONFIG_EN_CRC | NRF_CONFIG_PWR_UP);
_delay_ms(5);
zh_nrf24_send_command(NRF_CMD_FLUSH_TX);
zh_nrf24_write_buffer(NRF_CMD_W_TX_PAYLOAD, buf, size);
PORTB |= (1 << PB1);
uint8_t count = 0;
while ((PIND & (1 << PD2)) != 0)
{
if (++count > 100)
{
break;
}
_delay_ms(1);
}
PORTB &= ~(1 << PB1);
uint8_t status = zh_nrf24_send_command(NRF_CMD_NOP);
zh_nrf24_write_register(NRF_REG_STATUS, status);
zh_nrf24_send_command(NRF_CMD_FLUSH_TX);
zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_MASK_RX_DR | NRF_CONFIG_EN_CRC);
}
static uint8_t zh_nrf24_write_buffer(uint8_t cmd, uint8_t *buf, uint8_t count)
{
PORTB &= ~(1 << PB2);
uint8_t status = zh_spi_transfer(cmd);
while (count-- != 0)
{
zh_spi_transfer(*(buf++));
}
PORTB |= (1 << PB2);
return status;
}
static uint8_t zh_nrf24_write_register(uint8_t reg, uint8_t val)
{
PORTB &= ~(1 << PB2);
uint8_t status = zh_spi_transfer(reg | NRF_CMD_W_REGISTER);
zh_spi_transfer(val);
PORTB |= (1 << PB2);
return status;
}
static uint8_t zh_nrf24_write_buff_register(uint8_t reg, uint8_t *buf, uint8_t count)
{
return zh_nrf24_write_buffer((reg | NRF_CMD_W_REGISTER), buf, count);
}
static uint8_t zh_nrf24_send_command(uint8_t cmd)
{
PORTB &= ~(1 << PB2);
uint8_t status = zh_spi_transfer(cmd);
PORTB |= (1 << PB2);
return status;
}
static float zh_get_battery_voltage(void)
{
ADMUX = (1 << REFS0) | (1 << MUX3) | (1 << MUX2) | (1 << MUX1);
ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
_delay_ms(10);
ADCSRA |= (1 << ADSC);
while (ADCSRA & (1 << ADSC))
{
}
ADCSRA |= (1 << ADSC);
while (ADCSRA & (1 << ADSC))
{
}
uint16_t adc = ADCL;
adc |= (uint16_t)ADCH << 8;
ADCSRA &= ~(1 << ADEN);
if (adc == 0)
{
return 0.0;
}
return (1024.0 * 1.1) / (float)adc;
}
+771
View File
@@ -0,0 +1,771 @@
#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_nrf24_config.h"
#if defined(__AVR_ATmega328PB__)
#ifndef SPCR
#define SPCR SPCR0
#endif
#ifndef SPDR
#define SPDR SPDR0
#endif
#ifndef SPSR
#define SPSR SPSR0
#endif
#ifndef SPE
#define SPE SPE0
#endif
#ifndef MSTR
#define MSTR MSTR0
#endif
#ifndef SPR0
#define SPR0 SPR00
#endif
#ifndef SPIF
#define SPIF SPIF0
#endif
#ifndef PRR
#define PRR PRR0
#endif
#ifndef PRTWI
#define PRTWI PRTWI0
#endif
#ifndef PRSPI
#define PRSPI PRSPI0
#endif
#endif
#define GATEWAY_CHANNEL 120
#define GATEWAY_ADDRESS 0xDDEEFF
#define EEPROM_DEVICE_ID_ADDR ((uint8_t *)0x00)
#define DEVICE_ID_UNASSIGNED 0xFF
#define PAIRING_TIMEOUT_ATTEMPTS 7
#define WDT_T_250MS 0x04
#define WDT_T_4S 0x08
#define NRF_CMD_W_REGISTER 0x20
#define NRF_CMD_W_TX_PAYLOAD 0xA0
#define NRF_CMD_R_RX_PAYLOAD 0x61
#define NRF_CMD_FLUSH_TX 0xE1
#define NRF_CMD_FLUSH_RX 0xE2
#define NRF_CMD_NOP 0xFF
#define NRF_CMD_ACTIVATE 0x50
#define NRF_CMD_R_RX_PL_WID 0x60
#define NRF_REG_CONFIG 0x00
#define NRF_REG_EN_AA 0x01
#define NRF_REG_EN_RXADDR 0x02
#define NRF_REG_SETUP_AW 0x03
#define NRF_REG_SETUP_RETR 0x04
#define NRF_REG_RF_CH 0x05
#define NRF_REG_RF_SETUP 0x06
#define NRF_REG_STATUS 0x07
#define NRF_REG_RX_ADDR_P0 0x0A
#define NRF_REG_TX_ADDR 0x10
#define NRF_REG_RX_PW_P0 0x11
#define NRF_REG_DYNPD 0x1C
#define NRF_REG_FEATURE 0x1D
#define NRF_CONFIG_MASK_RX_DR 0x40
#define NRF_CONFIG_MASK_TX_DS 0x20
#define NRF_CONFIG_MASK_MAX_RT 0x10
#define NRF_CONFIG_EN_CRC 0x08
#define NRF_CONFIG_PWR_UP 0x02
#define NRF_CONFIG_PRIM_RX 0x01
#define NRF_STATUS_RX_DR 0x40
#define NRF_STATUS_TX_DS 0x20
#define NRF_STATUS_MAX_RT 0x10
#define NRF_FEATURE_EN_DPL 0x04
#define NRF_DYNPD_DPL_P0 0x01
/**
* @brief Load the device identifier from EEPROM.
*
* @return Stored device id, or DEVICE_ID_UNASSIGNED if none is stored
*/
static uint8_t _zh_load_device_id(void);
/**
* @brief Save the device identifier to EEPROM.
*
* @param id Device identifier to store
*/
static void _zh_save_device_id(uint8_t id);
/**
* @brief Initialize INT1 external interrupt for the touch-pad wake-up signal.
*
* Configures PD3 as a rising-edge triggered external interrupt source and
* enables the INT1 interrupt mask bit. The ISR sets the _send_request flag
* to trigger a radio transmission from the main loop.
*/
static void _zh_pad_interrupt_init(void);
/**
* @brief Initialize the hardware SPI peripheral in master mode.
*
* Configures MOSI, SCK and SS pins as outputs, drives SS high, and enables
* SPI with Fosc/16 clock rate.
*/
static void _zh_spi_init(void);
/**
* @brief Perform a single full-duplex SPI byte transfer.
*
* @param data Byte to transmit
*
* @return Byte received from the slave during the transfer
*/
static uint8_t _zh_spi_transfer(uint8_t data);
/**
* @brief Initialize the nRF24L01+ transceiver for TX-only operation.
*
* Drives CE low, configures auto-acknowledgment, address width, retransmit
* settings, channel, RF power and data rate, and loads the gateway address
* into both TX and RX pipe 0 registers.
*/
static void _zh_nrf24_init(void);
/**
* @brief Initialize INT0 external interrupt for the nRF24 IRQ signal.
*
* Configures PD2 as a falling-edge triggered external interrupt source and
* enables the INT0 interrupt mask bit. The ISR sets the _nrf_event flag used
* by the pairing state machine.
*/
static void _zh_nrf24_interrupt_init(void);
/**
* @brief Transmit a payload buffer through the nRF24L01+ module.
*
* Powers up the radio, flushes the TX FIFO, loads the payload, pulses CE to
* start transmission, waits for the TX_DS or MAX_RT flag, then clears status
* flags and returns the radio to power-down mode.
*
* @param buf Pointer to the payload data to transmit (must not be NULL)
* @param size Number of bytes in the payload buffer (must be > 0)
*/
static void _zh_nrf24_send(uint8_t *buf, uint8_t size);
/**
* @brief Start a transmission without blocking on completion.
*
* Loads the payload, pulses CE for 15 us and returns immediately. The
* transmission completion is signalled by the nRF24 IRQ pin.
*
* @param buf Pointer to the payload data to transmit (must not be NULL)
* @param size Number of bytes in the payload buffer (must be > 0)
*/
static void _zh_nrf24_send_no_wait(uint8_t *buf, uint8_t size);
/**
* @brief Send a single-byte command to the nRF24L01+ and read the status.
*
* @param cmd Command byte to send
*
* @return Status register value returned by the nRF24L01+
*/
static uint8_t _zh_nrf24_send_command(uint8_t cmd);
/**
* @brief Write a data buffer to the nRF24L01+ via SPI with a given command.
*
* Asserts CSN low, sends the command byte followed by the buffer contents,
* then deasserts CSN.
*
* @param cmd SPI command byte to send before the data
* @param buf Pointer to the data buffer to write (must not be NULL)
* @param count Number of bytes to transfer from the buffer
*
* @return Status register value returned by the nRF24L01+
*/
static uint8_t _zh_nrf24_write_buffer(uint8_t cmd, uint8_t *buf, uint8_t count);
/**
* @brief Write a single byte value to an nRF24L01+ register.
*
* @param reg Register address to write to
* @param val Byte value to store in the register
*
* @return Status register value returned by the nRF24L01+
*/
static uint8_t _zh_nrf24_write_register(uint8_t reg, uint8_t val);
/**
* @brief Write a multi-byte buffer into an nRF24L01+ register.
*
* Convenience wrapper around _zh_nrf24_write_buffer that prepends the
* W_REGISTER command to the register address.
*
* @param reg Register address to write to
* @param buf Pointer to the data buffer to write (must not be NULL)
* @param count Number of bytes to transfer from the buffer
*
* @return Status register value returned by the nRF24L01+
*/
static uint8_t _zh_nrf24_write_buff_register(uint8_t reg, uint8_t *buf, uint8_t count);
/**
* @brief Read the received payload from the nRF24L01+ into a buffer.
*
* @param buf Pointer to the destination buffer (must not be NULL)
* @param count Number of bytes to read from the RX FIFO
*/
static void _zh_nrf24_read_payload(uint8_t *buf, uint8_t count);
/**
* @brief Read the width of the payload in the nRF24L01+ RX FIFO.
*
* @return Payload width in bytes
*/
static uint8_t _zh_nrf24_read_payload_width(void);
/**
* @brief Switch the nRF24L01+ to RX mode on pipe 0.
*/
static void _zh_nrf24_enter_rx(void);
/**
* @brief Put the nRF24L01+ into power-down mode.
*/
static void _zh_nrf24_enter_power_down(void);
/**
* @brief Start the watchdog timer in interrupt mode with the given timeout.
*
* @param timeout_bits Watchdog timeout prescaler bits (WDP3:0)
*/
static void _zh_wdt_start(uint8_t timeout_bits);
/**
* @brief Stop the watchdog timer.
*/
static void _zh_wdt_stop(void);
/**
* @brief Generate a pseudo-random token from MCU state and ADC noise.
*
* @return 16-bit random token (never zero)
*/
static uint16_t _zh_get_random_token(void);
/**
* @brief Run the non-blocking pairing procedure to obtain a device id.
*
* Sends pairing requests, waits for the gateway acknowledgment in RX mode
* and retries with watchdog-timed backoff. Sleeps forever if the gateway
* does not answer within the allowed number of attempts.
*
* @return Device identifier assigned by the gateway
*/
static uint8_t _zh_do_pairing(void);
/**
* @brief Enter permanent power-down sleep until the next reset.
*
* Disables all interrupts, the watchdog, the radio, the ADC, SPI and all
* unused peripheral clocks, then loops in power-down sleep mode.
*/
static void _zh_enter_final_sleep(void);
/**
* @brief Measure the battery voltage using the internal AVcc reference.
*
* Enables the ADC with the 1.1 V internal reference on the 1.1 V supply
* channel, performs two conversions and returns the calculated battery
* voltage based on the AVcc-to-internal-reference ratio.
*
* @return Battery voltage in volts, or 0.0 if the ADC reading is zero
*/
static float _zh_get_battery_voltage(void);
static volatile bool _send_request = false;
static volatile bool _nrf_event = false;
zh_nrf24_sensor_transmitted_data_t _transmitted_data = {0};
int main(void)
{
_transmitted_data.type = ZH_NRF24_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;
{
uint8_t probe[ZH_NRF24_MAX_PAYLOAD_SIZE];
if (zh_nrf24_sensor_build_payload(&_transmitted_data, probe, sizeof(probe)) == 0)
{
_zh_enter_final_sleep();
}
}
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_NRF24_MAX_PAYLOAD_SIZE];
uint8_t size = zh_nrf24_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;
}
ISR(INT1_vect)
{
_send_request = true;
}
ISR(INT0_vect)
{
_nrf_event = true;
}
ISR(WDT_vect)
{
}
static uint8_t _zh_load_device_id(void)
{
uint8_t id = eeprom_read_byte(EEPROM_DEVICE_ID_ADDR);
if (id == 0xFF || id == 0x00)
{
return DEVICE_ID_UNASSIGNED;
}
return id;
}
static void _zh_save_device_id(uint8_t id)
{
eeprom_update_byte(EEPROM_DEVICE_ID_ADDR, id);
}
static void _zh_pad_interrupt_init(void)
{
DDRD &= ~(1 << PD3);
PORTD &= ~(1 << PD3);
EICRA &= ~((1 << ISC11) | (1 << ISC10));
EICRA |= (1 << ISC11) | (1 << ISC10);
EIFR |= (1 << INTF1);
EIMSK |= (1 << INT1);
}
static void _zh_spi_init(void)
{
DDRB |= (1 << PB2) | (1 << PB3) | (1 << PB5);
PORTB |= (1 << PB2);
SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0);
}
static uint8_t _zh_spi_transfer(uint8_t data)
{
SPDR = data;
while ((SPSR & (1 << SPIF)) == 0)
{
}
return SPDR;
}
static void _zh_nrf24_init(void)
{
_delay_ms(100);
DDRB |= (1 << PB1);
PORTB &= ~(1 << PB1);
DDRD &= ~(1 << PD2);
PORTD |= (1 << PD2);
uint64_t gateway_address = GATEWAY_ADDRESS;
uint8_t activation_key = 0x73;
_zh_nrf24_write_register(NRF_REG_CONFIG, 0x00);
_zh_nrf24_write_register(NRF_REG_EN_AA, 0x01);
_zh_nrf24_write_register(NRF_REG_EN_RXADDR, 0x01);
_zh_nrf24_write_register(NRF_REG_SETUP_AW, 0x01);
_zh_nrf24_write_register(NRF_REG_SETUP_RETR, 0xFF);
_zh_nrf24_write_register(NRF_REG_RF_CH, GATEWAY_CHANNEL);
_zh_nrf24_write_register(NRF_REG_RF_SETUP, 0x26);
_zh_nrf24_write_buff_register(NRF_REG_TX_ADDR, (uint8_t *)&gateway_address, 3);
_zh_nrf24_write_buff_register(NRF_REG_RX_ADDR_P0, (uint8_t *)&gateway_address, 3);
_zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_RX_DR | NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT);
_zh_nrf24_write_buffer(NRF_CMD_ACTIVATE, &activation_key, sizeof(activation_key));
_zh_nrf24_write_register(NRF_REG_FEATURE, NRF_FEATURE_EN_DPL);
_zh_nrf24_write_register(NRF_REG_DYNPD, NRF_DYNPD_DPL_P0);
_zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_MASK_RX_DR | NRF_CONFIG_EN_CRC);
}
static void _zh_nrf24_interrupt_init(void)
{
DDRD &= ~(1 << PD2);
PORTD |= (1 << PD2);
EICRA &= ~((1 << ISC01) | (1 << ISC00));
EICRA |= (1 << ISC01);
EIFR |= (1 << INTF0);
EIMSK |= (1 << INT0);
}
static void _zh_nrf24_send(uint8_t *buf, uint8_t size)
{
_zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_MASK_RX_DR | NRF_CONFIG_EN_CRC | NRF_CONFIG_PWR_UP);
_delay_ms(5);
_zh_nrf24_send_command(NRF_CMD_FLUSH_TX);
_zh_nrf24_write_buffer(NRF_CMD_W_TX_PAYLOAD, buf, size);
PORTB |= (1 << PB1);
uint8_t count = 0;
while ((PIND & (1 << PD2)) != 0)
{
if (++count > 100)
{
break;
}
_delay_ms(1);
}
PORTB &= ~(1 << PB1);
uint8_t status = _zh_nrf24_send_command(NRF_CMD_NOP);
_zh_nrf24_write_register(NRF_REG_STATUS, status);
_zh_nrf24_send_command(NRF_CMD_FLUSH_TX);
_zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_MASK_RX_DR | NRF_CONFIG_EN_CRC);
}
static void _zh_nrf24_send_no_wait(uint8_t *buf, uint8_t size)
{
PORTB &= ~(1 << PB1);
_zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_MASK_RX_DR | NRF_CONFIG_EN_CRC | NRF_CONFIG_PWR_UP);
_delay_ms(2);
_zh_nrf24_send_command(NRF_CMD_FLUSH_TX);
_zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT | NRF_STATUS_RX_DR);
_zh_nrf24_write_buffer(NRF_CMD_W_TX_PAYLOAD, buf, size);
PORTB |= (1 << PB1);
_delay_us(15);
PORTB &= ~(1 << PB1);
}
static uint8_t _zh_nrf24_send_command(uint8_t cmd)
{
PORTB &= ~(1 << PB2);
uint8_t status = _zh_spi_transfer(cmd);
PORTB |= (1 << PB2);
return status;
}
static uint8_t _zh_nrf24_write_buffer(uint8_t cmd, uint8_t *buf, uint8_t count)
{
PORTB &= ~(1 << PB2);
uint8_t status = _zh_spi_transfer(cmd);
while (count-- != 0)
{
_zh_spi_transfer(*(buf++));
}
PORTB |= (1 << PB2);
return status;
}
static uint8_t _zh_nrf24_write_register(uint8_t reg, uint8_t val)
{
PORTB &= ~(1 << PB2);
uint8_t status = _zh_spi_transfer(reg | NRF_CMD_W_REGISTER);
_zh_spi_transfer(val);
PORTB |= (1 << PB2);
return status;
}
static uint8_t _zh_nrf24_write_buff_register(uint8_t reg, uint8_t *buf, uint8_t count)
{
return _zh_nrf24_write_buffer((reg | NRF_CMD_W_REGISTER), buf, count);
}
static void _zh_nrf24_read_payload(uint8_t *buf, uint8_t count)
{
PORTB &= ~(1 << PB2);
_zh_spi_transfer(NRF_CMD_R_RX_PAYLOAD);
while (count-- != 0)
{
*buf++ = _zh_spi_transfer(0xFF);
}
PORTB |= (1 << PB2);
}
static uint8_t _zh_nrf24_read_payload_width(void)
{
PORTB &= ~(1 << PB2);
_zh_spi_transfer(NRF_CMD_R_RX_PL_WID);
uint8_t width = _zh_spi_transfer(0xFF);
PORTB |= (1 << PB2);
return width;
}
static void _zh_nrf24_enter_rx(void)
{
PORTB &= ~(1 << PB1);
_zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT | NRF_STATUS_RX_DR);
_zh_nrf24_send_command(NRF_CMD_FLUSH_RX);
_zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_EN_CRC | NRF_CONFIG_PWR_UP | NRF_CONFIG_PRIM_RX);
_delay_ms(2);
PORTB |= (1 << PB1);
_delay_us(130);
}
static void _zh_nrf24_enter_power_down(void)
{
PORTB &= ~(1 << PB1);
_zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_EN_CRC);
}
static void _zh_wdt_start(uint8_t timeout_bits)
{
uint8_t wdp = (timeout_bits & 0x07) | ((timeout_bits & 0x08) ? (1 << WDP3) : 0);
cli();
wdt_reset();
WDTCSR = (1 << WDCE) | (1 << WDE);
WDTCSR = (1 << WDIE) | wdp;
sei();
}
static void _zh_wdt_stop(void)
{
cli();
wdt_reset();
WDTCSR = (1 << WDCE) | (1 << WDE);
WDTCSR = 0;
sei();
}
static uint16_t _zh_get_random_token(void)
{
uint16_t token = MCUSR;
MCUSR = 0;
ADMUX = (1 << REFS0);
ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
_delay_ms(5);
for (uint8_t i = 0; i < 16; ++i)
{
ADCSRA |= (1 << ADSC);
while ((ADCSRA & (1 << ADSC)) != 0)
{
}
token = (token << 1) ^ (token >> 15) ^ ADC;
}
ADCSRA &= ~(1 << ADEN);
if (token == 0)
{
token = 0xACE1;
}
return token;
}
static uint8_t _zh_do_pairing(void)
{
uint16_t token = _zh_get_random_token();
zh_nrf24_sensor_pairing_data_t pairing_request;
pairing_request.type = (uint8_t)_transmitted_data.type;
pairing_request.magic = ZH_NRF24_PAIRING_REQ_MAGIC;
pairing_request.token_hi = (uint8_t)(token >> 8);
pairing_request.token_lo = (uint8_t)(token & 0xFF);
pairing_request.id = 0;
_zh_nrf24_interrupt_init();
zh_nrf24_sensor_pairing_state_t state = ZH_NRF24_PAIR_SEND;
uint8_t attempts = 0;
for (;;)
{
switch (state)
{
case ZH_NRF24_PAIR_SEND:
_nrf_event = false;
_zh_nrf24_send_no_wait((uint8_t *)&pairing_request, ZH_NRF24_PAIRING_PACKET_SIZE);
state = ZH_NRF24_PAIR_TX_WAIT;
break;
case ZH_NRF24_PAIR_TX_WAIT:
if ((PIND & (1 << PD2)) == 0)
{
_nrf_event = true;
}
else
{
_nrf_event = false;
_zh_wdt_start(WDT_T_250MS);
sleep_enable();
sei();
sleep_cpu();
sleep_disable();
_zh_wdt_stop();
sei();
}
if (_nrf_event == true)
{
_nrf_event = false;
uint8_t status = _zh_nrf24_send_command(NRF_CMD_NOP);
_zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT | NRF_STATUS_RX_DR);
if ((status & NRF_STATUS_TX_DS) != 0)
{
_zh_nrf24_enter_rx();
state = ZH_NRF24_PAIR_RX_WAIT;
}
else
{
state = ZH_NRF24_PAIR_BACKOFF;
}
}
else
{
state = ZH_NRF24_PAIR_BACKOFF;
}
break;
case ZH_NRF24_PAIR_RX_WAIT:
if ((PIND & (1 << PD2)) == 0)
{
_nrf_event = true;
}
else
{
_nrf_event = false;
_zh_wdt_start(WDT_T_250MS);
sleep_enable();
sei();
sleep_cpu();
sleep_disable();
_zh_wdt_stop();
sei();
}
if (_nrf_event == true)
{
_nrf_event = false;
uint8_t status = _zh_nrf24_send_command(NRF_CMD_NOP);
if ((status & NRF_STATUS_RX_DR) != 0)
{
uint8_t width = _zh_nrf24_read_payload_width();
if (width > ZH_NRF24_MAX_PAYLOAD_SIZE)
{
_zh_nrf24_send_command(NRF_CMD_FLUSH_RX);
_zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_RX_DR);
state = ZH_NRF24_PAIR_BACKOFF;
break;
}
if (width != ZH_NRF24_PAIRING_PACKET_SIZE)
{
uint8_t dummy[ZH_NRF24_MAX_PAYLOAD_SIZE];
_zh_nrf24_read_payload(dummy, width);
_zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_RX_DR);
_zh_nrf24_send_command(NRF_CMD_FLUSH_RX);
state = ZH_NRF24_PAIR_BACKOFF;
break;
}
zh_nrf24_sensor_pairing_data_t pairing_response;
_zh_nrf24_read_payload((uint8_t *)&pairing_response, width);
_zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_RX_DR | NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT);
_zh_nrf24_send_command(NRF_CMD_FLUSH_RX);
if (pairing_response.magic == ZH_NRF24_PAIRING_ACK_MAGIC && pairing_response.type == pairing_request.type &&
pairing_response.token_hi == pairing_request.token_hi && pairing_response.token_lo == pairing_request.token_lo &&
pairing_response.id != 0 && pairing_response.id != DEVICE_ID_UNASSIGNED)
{
_zh_nrf24_enter_power_down();
EIMSK &= ~(1 << INT0);
_zh_save_device_id(pairing_response.id);
return pairing_response.id;
}
}
state = ZH_NRF24_PAIR_BACKOFF;
}
else
{
state = ZH_NRF24_PAIR_BACKOFF;
}
break;
case ZH_NRF24_PAIR_BACKOFF:
_zh_nrf24_enter_power_down();
_nrf_event = false;
_zh_wdt_start(WDT_T_4S);
sleep_enable();
sei();
sleep_cpu();
sleep_disable();
_zh_wdt_stop();
sei();
if (++attempts >= PAIRING_TIMEOUT_ATTEMPTS)
{
_zh_enter_final_sleep();
}
state = ZH_NRF24_PAIR_SEND;
break;
}
}
}
static void _zh_enter_final_sleep(void)
{
cli();
EIMSK = 0;
EIFR = 0xFF;
_zh_wdt_stop();
PORTB &= ~(1 << PB1);
_zh_nrf24_write_register(NRF_REG_CONFIG, 0x00);
ADCSRA = 0;
SPCR = 0;
PRR = (1 << PRTWI) | (1 << PRTIM2) | (1 << PRTIM1) | (1 << PRTIM0) | (1 << PRSPI) | (1 << PRUSART0);
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_enable();
for (;;)
{
sleep_cpu();
}
}
static float _zh_get_battery_voltage(void)
{
ADMUX = (1 << REFS0) | (1 << MUX3) | (1 << MUX2) | (1 << MUX1);
ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
_delay_ms(10);
ADCSRA |= (1 << ADSC);
while ((ADCSRA & (1 << ADSC)) != 0)
{
}
ADCSRA |= (1 << ADSC);
while ((ADCSRA & (1 << ADSC)) != 0)
{
}
uint16_t adc = ADCL;
adc |= (uint16_t)ADCH << 8;
ADCSRA &= ~(1 << ADEN);
if (adc == 0)
{
return 0.0;
}
return (1024.0 * 1.1) / (float)adc;
}
+179 -99
View File
@@ -1,39 +1,37 @@
# Описание компонента 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_nrf24_sensor_transmitted_data_t` из внешней библиотеки `zh_nrf24_config` и сериализуется функцией `zh_nrf24_sensor_build_payload()` перед передачей.
| Поле | Тип | Описание |
| ------ | ----- | ---------- |
| `id` | `int16_t` | Идентификатор устройства (`DEVICE_ID = 6`) |
| `type` | `int16_t` | Тип устройства (`ZH_RF_SENSOR_TOUCH_SWITCH`) |
| `battery_voltage` | `int16_t` | Напряжение батареи × 100 (например, 330 = 3.30 В) |
| `value_1`–`value_4` | `int16_t` | Зарезервированы (равны 0) |
| `id` | `int16_t` | Идентификатор устройства, присвоенный шлюзом при сопряжении |
| `type` | `int16_t` | Тип устройства (`ZH_NRF24_SENSOR_TOUCH_SWITCH`) |
| `battery` | `int16_t` | Напряжение батареи × 100 (например, 330 = 3.30 В) |
### Конфигурационные макросы
@@ -41,89 +39,148 @@
| -------- | ---------- | ---------- |
| `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` объявлены как `static volatile bool` для корректного доступа из ISR и главного цикла.
### Флаги синхронизации
| Флаг | Устанавливается | Используется |
| ------ | ----------------- | -------------- |
| `_send_request` | ISR `INT1_vect` (касание) | Главный цикл — запуск передачи |
| `_nrf_event` | ISR `INT0_vect` (IRQ nRF24) | Конечный автомат сопряжения |
---
## API (интерфейс)
Все функции являются `static` и доступны только внутри модуля `main.c`.
Все функции являются `static` и доступны только внутри модуля `src/zh_avr_touch_pad.c`. Имена функций имеют префикс `_zh_`.
### Инициализация
| Функция | Описание |
| --------- | ---------- |
| `zh_pad_interrupt_init()` | Настройка внешнего прерывания INT1 (PD3, rising edge) для пробуждения по касанию |
| `zh_spi_init()` | Инициализация аппаратного SPI в режиме мастера (Fosc/16) |
| `zh_nrf24_init()` | Инициализация трансивера nRF24L01+ в режим передачи |
| `_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_write_register()` | Запись одного байта в регистр nRF24L01+ |
| `zh_nrf24_write_buff_register()` | Запись многобайтового буфера в регистр nRF24L01+ |
| `zh_nrf24_write_buffer()` | Запись буфера данных с указанной командой |
| `zh_nrf24_send_command()` | Отправка однобайтовой команды и чтение статуса |
| `_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. Синхронизация между прерыванием и главным циклом реализуется следующим образом:
Прошивка работает на bare-metal AVR без RTOS. Синхронизация между прерываниями и главным циклом реализуется следующим образом:
1. **`volatile` флаг** — переменная `send_request` объявлена как `volatile`, что гарантирует чтение актуального значения из памяти при каждой проверке в главном цикле.
2. **Атомарная запись из ISR** — обработка INT1 устанавливает флаг в единицу. Запись одного байта на AVR является атомарной операцией.
3. **Отключение прерываний при инициализации** — `cli()` перед настройкой периферии исключает гонки при старте.
1. **Флаги `volatile`** — переменные `_send_request`, `_nrf_event` объявлены как `static volatile bool`, что гарантирует чтение актуального значения из памяти при каждой проверке в главном цикле.
2. **Атомарная запись из ISR** — обработка прерывания устанавливает флаг в `true`. Запись одного байта на AVR является атомарной операцией.
3. **Отключение прерываний при проверке и сбросе флага** — `cli()` перед проверкой и сбросом `_send_request` в главном цикле исключает гонку между ISR и главным циклом.
4. **Отключение прерываний при инициализации** — `cli()` перед настройкой периферии исключает гонки при старте.
---
## Алгоритмы
### Алгоритм сопряжения (pairing)
Конечный автомат `_zh_do_pairing()` работает в четырёх состояниях:
1. **`ZH_NRF24_PAIR_SEND`** — генерируется псевдослучайный токен, формируется пакет запроса (`zh_nrf24_sensor_pairing_data_t` с `magic = ZH_NRF24_PAIRING_REQ_MAGIC`), выполняется неблокирующая передача через `_zh_nrf24_send_no_wait()`.
2. **`ZH_NRF24_PAIR_TX_WAIT`** — ожидание прерывания IRQ (завершение передачи). Если пин IRQ уже низкий — событие считается наступившим. Иначе — переход в сон с watchdog-таймером 250 мс. При флаге `TX_DS` — переход в приём, при `MAX_RT` — в backoff.
3. **`ZH_NRF24_PAIR_RX_WAIT`** — ожидание ответа шлюза. При получении пакета проверяется: длина равна `ZH_NRF24_PAIRING_PACKET_SIZE`, `magic == ZH_NRF24_PAIRING_ACK_MAGIC`, тип совпадает, токен совпадает, `id != 0` и `id != DEVICE_ID_UNASSIGNED`. При успехе — сохранение `id` в EEPROM и возврат.
4. **`ZH_NRF24_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`.
- Главный цикл проверяет флаг `_send_request`.
- Если флаг не установлен — вызывается `sleep_cpu()` (режим Power-down).
- Пробуждение происходит по аппаратному прерыванию INT1 (rising edge на PD3).
- ISR устанавливает `send_request = 1`, цикл просыпается и выполняет передачу.
- ISR устанавливает `_send_request = true`, цикл просыпается и выполняет передачу.
---
@@ -132,40 +189,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_nrf24_config.h"
static volatile bool _send_request = false;
zh_nrf24_sensor_transmitted_data_t _transmitted_data = {0};
ISR(INT1_vect)
{
_send_request = true;
}
int main(void)
{
// Отключаем прерывания на время инициализации
_transmitted_data.type = ZH_NRF24_SENSOR_TOUCH_SWITCH;
MCUSR &= ~(1 << WDRF);
wdt_disable();
cli();
// Настраиваем периферию
zh_pad_interrupt_init();
zh_spi_init();
zh_nrf24_init();
// Конфигурируем режим сна Power-down
_zh_pad_interrupt_init();
_zh_spi_init();
_zh_nrf24_init();
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_NRF24_MAX_PAYLOAD_SIZE];
uint8_t size = zh_nrf24_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 +251,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_NRF24_SENSOR_TOUCH_SWITCH из zh_nrf24_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_NRF24_PAIR_BACKOFF` |
| Превышение числа попыток сопряжения (7) | `_zh_enter_final_sleep()` — сон до сброса |
| `zh_nrf24_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 не отключается между передачами.
---
@@ -236,7 +308,7 @@ int main(void)
- PlatformIO с плагином `platform-atmelavr`
- AVR-GCC (входит в состав PlatformIO)
- Библиотека `zh_rf_config` (подключается через `lib_deps` в `platformio.ini`)
- Библиотека `zh_nrf24_config` (подключается через `lib_deps` в `platformio.ini`)
- Программатор USBasp (или совместимый)
- Фьюзы: LFUSE=`0xE2`, HFUSE=`0xDF`, EFUSE=`0x07` (8 МГц, без делителя)
@@ -247,25 +319,33 @@ 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
```
### Подключение в проекте
Компонент является самодостаточной прошивкой — не подключается как библиотека. Для интеграции в другой проект скопируйте `src/main.c`, добавьте зависимость `zh_rf_config` и адаптируйте макросы конфигурации.
Компонент является самодостаточной прошивкой — не подключается как библиотека. Для интеграции в другой проект скопируйте `src/zh_avr_touch_pad.c`, добавьте зависимость `zh_nrf24_config` и адаптируйте макросы конфигурации.
```ini
; platformio.ini
lib_deps =
https://github.com/aZholtikov/zh_rf_config
https://github.com/aZholtikov/zh_nrf24_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 "zh_rf_config.h"
#include "math.h"
#include "stdbool.h"
#include "zh_nrf24_config.h"
```