저는 반려견의 사료/물 급식대에 사용할 소형 배터리 구동 동작 표시기로 이것을 만들었습니다. 원래는 나무 플레이트 아래에서 은은하게 빛나는 언더글로를 만들 생각이었지만, 개발 중에 다음과 같이 변경했습니다: 가장자리에서 아크릴을 비추는 4개의 WS2812 LED가 있는 반투명 아크릴 플레이트.
기대했던 것보다 훨씬 잘 작동했습니다. 단순히 아래쪽 영역을 비추는 대신 빛이 아크릴 전체로 퍼져 플레이트 자체가 빛나게 됩니다.
실용적인 목적도 있습니다. Wi-Fi, Home Assistant 또는 외부 로깅 시스템을 사용하지 않고도 낮 동안 반려견이 물그릇이나 사료 그릇을 이용했는지 간단히 확인하고 싶었습니다.
자세한 연결과 핀 할당은 회로도에 나와 있습니다.
PIR 센서가 움직임을 감지하면 핀 변경 인터럽트를 발생시켜 ATtiny를 깨웁니다. LED 전원이 켜지고 LED는 다음을 표시하는 데 사용됩니다: 00부터 99까지의 작동 횟수 카운터.
숫자 0~9는 각각 명확하게 구별되는 고유한 RGB 색상을 가집니다. 카운터의 두 숫자는 각각 5초 동안 순차적으로 표시되며, 그 사이에는 짧은 어두운 시간이 들어갑니다. 따라서 4개의 LED는 별도의 화면을 추가하지 않고도 조명과 매우 간단한 2자리 디스플레이 역할을 모두 수행할 수 있습니다.
카운터를 표시한 후 LED는 낮은 밝기로 서서히 어두워집니다. PIR이 활성 상태인 동안에는 희미하게 켜져 있으며, PIR이 LOW로 돌아가면 완전히 꺼집니다. 그러면 ATtiny는 다시 절전 모드로 들어갑니다.
카운터는 의도적으로 RAM에만 저장되므로 장치의 전원을 껐다 켜면 00으로 재설정됩니다. 정확한 기록이 목적이 아니라, 아침과 나중에 낮 동안 카운터를 확인하여 그릇 주변에서 얼마나 많은 활동이 있었는지 알아보는 것이 목적입니다.
저는 Spece Konde의 ATTinyCore와 함께 Arduino IDE를 사용합니다. Arduino Uno를 ISP로 사용할 때의 핀 연결은 다음과 같습니다:
#include <avr/sleep.h>
#include <avr/interrupt.h>
#include <Adafruit_NeoPixel.h>
// ------------------------------------------------------------
// Hardware pins
// ------------------------------------------------------------
#define NEO PIN_PA2 // ATtiny84 physical pin 11 - WS2812 data
#define WAKE_PIN PIN_PA3 // ATtiny84 physical pin 10 - PIR output
#define LED_POWER PIN_PA1 // ATtiny84 physical pin 12 - BC327 control
// ------------------------------------------------------------
// LED settings
// ------------------------------------------------------------
#define NUMPIXELS 4
#define BRIGHTNESS 125 // Normal LED brightness
#define LED_ON_TIME 5000UL // Display each digit for 5 seconds
#define LED_OFF_TIME 200 // Blank period between digits
#define FADE_MIN 10 // Lowest stable WS2812 brightness
#define FADE_STEP 5
#define FADE_DELAY 40 // ms between fade steps
Adafruit_NeoPixel pixels(NUMPIXELS, NEO, NEO_GRB + NEO_KHZ400);
// ------------------------------------------------------------
// Digit colors
//
// Each color represents one numerical digit 0-9.
// ------------------------------------------------------------
struct Color
{
uint8_t r;
uint8_t g;
uint8_t b;
};
const Color arrayColors[10] =
{
{255, 0, 0}, // 0 - Red
{ 0, 255, 0}, // 1 - Green
{ 0, 0, 255}, // 2 - Blue
{255, 255, 0}, // 3 - Yellow
{ 0, 255, 255}, // 4 - Cyan
{255, 0, 255}, // 5 - Magenta
{255, 255, 255}, // 6 - White
{255, 128, 0}, // 7 - Orange
{128, 0, 255}, // 8 - Violet
{255, 128, 192} // 9 - Pink
};
// Counts PIR activations from 00 to 99.
// This value is stored only in RAM and therefore resets after power loss.
uint8_t activationCounter = 0;
// ------------------------------------------------------------
// Pin-change interrupt
//
// The interrupt does not need to perform any operation.
// Its only purpose is to wake the ATtiny from power-down sleep.
// ------------------------------------------------------------
ISR(PCINT0_vect)
{
}
// ------------------------------------------------------------
// Wake interrupt control
// ------------------------------------------------------------
void enableWakeInterrupt()
{
PCMSK0 |= _BV(PCINT3);
}
void disableWakeInterrupt()
{
PCMSK0 &= ~_BV(PCINT3);
}
void clearWakeInterrupt()
{
// AVR interrupt flags are cleared by writing a logic 1.
GIFR |= _BV(PCIF0);
}
// ------------------------------------------------------------
// LED functions
// ------------------------------------------------------------
void ledsPowerOn()
{
// BC327 high-side transistor:
// LOW = LEDs powered
digitalWrite(LED_POWER, LOW);
// Allow WS2812 supply to stabilize.
delay(10);
}
void setColor(uint8_t colorNumber)
{
pixels.setBrightness(BRIGHTNESS);
Color c = arrayColors[colorNumber];
for (uint8_t i = 0; i < NUMPIXELS; i++)
{
pixels.setPixelColor(i, pixels.Color(c.r, c.g, c.b));
}
pixels.show();
}
void clearLeds()
{
// LEDs remain powered but display black.
pixels.clear();
pixels.show();
}
void fadeOut()
{
// Reduce brightness gradually from normal brightness
// to FADE_MIN.
//
// After this function returns, the LEDs remain ON
// at FADE_MIN until ledsOff() is called.
for (int b = BRIGHTNESS; b >= FADE_MIN; b -= FADE_STEP)
{
pixels.setBrightness(b);
pixels.show();
delay(FADE_DELAY);
}
}
void ledsOff()
{
// First send black while the WS2812s still have power.
pixels.clear();
pixels.show();
// Hold the WS2812 data line LOW before removing power.
// This prevents possible back-powering through DIN.
digitalWrite(NEO, LOW);
delay(1);
// BC327 OFF.
digitalWrite(LED_POWER, HIGH);
// Prepare normal brightness for next activation.
pixels.setBrightness(BRIGHTNESS);
}
// ------------------------------------------------------------
// Sleep
// ------------------------------------------------------------
void system_sleep()
{
// Make sure LEDs are unpowered before sleeping.
digitalWrite(LED_POWER, HIGH);
digitalWrite(NEO, LOW);
// Do not sleep if the PIR is already active.
if (digitalRead(WAKE_PIN) == HIGH)
{
return;
}
// Temporarily disable global interrupts while configuring sleep.
cli();
clearWakeInterrupt();
enableWakeInterrupt();
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_enable();
// Check PIR again immediately before sleeping.
// This prevents missing a transition that occurred while
// sleep was being prepared.
if (digitalRead(WAKE_PIN) == LOW)
{
sei();
sleep_cpu();
}
else
{
sleep_disable();
sei();
return;
}
// Execution continues here after PIR wakes the ATtiny.
sleep_disable();
disableWakeInterrupt();
sei();
}
// ------------------------------------------------------------
// Setup
// ------------------------------------------------------------
void setup()
{
pixels.begin();
pinMode(LED_POWER, OUTPUT);
digitalWrite(LED_POWER, HIGH); // LEDs initially unpowered
pinMode(NEO, OUTPUT);
digitalWrite(NEO, LOW);
pinMode(WAKE_PIN, INPUT);
// Enable pin-change interrupt group for PORT A.
GIMSK |= _BV(PCIE0);
// Individual PIR interrupt is enabled only when entering sleep.
disableWakeInterrupt();
ledsOff();
sei();
}
// ------------------------------------------------------------
// Main loop
// ------------------------------------------------------------
void loop()
{
if (digitalRead(WAKE_PIN) == HIGH)
{
// We do not need PIR interrupts while processing an activation.
disableWakeInterrupt();
// --------------------------------------------------------
// Increment activation counter: 00 -> 01 -> ... -> 99 -> 00
// --------------------------------------------------------
activationCounter++;
if (activationCounter > 99)
{
activationCounter = 0;
}
// Split counter into two decimal digits.
uint8_t tens = activationCounter / 10;
uint8_t ones = activationCounter % 10;
// --------------------------------------------------------
// Display activation count
// --------------------------------------------------------
ledsPowerOn();
// Tens digit
setColor(tens);
delay(LED_ON_TIME);
// Brief blank interval so identical digits can still
// be distinguished, e.g. 11, 22, 33...
clearLeds();
delay(LED_OFF_TIME);
// Ones digit
setColor(ones);
delay(LED_ON_TIME);
// --------------------------------------------------------
// Fade down to low brightness
// --------------------------------------------------------
fadeOut();
// --------------------------------------------------------
// Keep LEDs dim while PIR remains active
// --------------------------------------------------------
while (digitalRead(WAKE_PIN) == HIGH)
{
delay(10);
}
// PIR has returned LOW - turn LEDs completely off.
ledsOff();
// Small settling time before allowing another activation.
delay(100);
// Remove any pending pin-change interrupt from the
// previous PIR transition.
cli();
clearWakeInterrupt();
sei();
}
// Sleep until the next PIR activation.
system_sleep();
}
배터리 수명은 설계에서 중요한 부분이었습니다. 테스트 결과 WS2812의 밝기를 높이면 아크릴을 통해 보이는 밝기가 그만큼 크게 향상되지 않는데도 전류가 크게 증가했습니다. 따라서 전력을 절약하기 위해 일반 LED 밝기를 의도적으로 낮췄습니다.
실제 배터리 수명은 물론 PIR 활성화 횟수와 지속 시간, 그리고 사용 가능한 배터리 용량에 따라 달라집니다.
최종 설계는 의도적으로 매우 단순합니다. 즉, ATtiny84, PIR 센서, 4개의 RGB LED, 트랜지스터 및 충전식 배터리 전원으로 구성됩니다.
아크릴판이 시각적 효과를 제공하고, 소프트웨어는 동일한 4개의 LED를 기본적인 활동 카운터로 활용합니다. Wi-Fi, 디스플레이 또는 외부 시스템 없이도 원하는 정보를 제공하면서 설계를 작고 저전력으로 유지할 수 있었습니다.