Skip to content

Instantly share code, notes, and snippets.

@stecman
Last active April 21, 2024 10:03
Show Gist options
  • Save stecman/9c2d2dcbf26e83ab438adb4c894a4557 to your computer and use it in GitHub Desktop.
Save stecman/9c2d2dcbf26e83ab438adb4c894a4557 to your computer and use it in GitHub Desktop.
Detect audio signal with an AVR attiny13a and emulate a MS-6147-RC 443 MHz remote to turn speaker power on
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Automatic speaker power control from multiple audio sources

Switches mains power on for speakers while an audio signal is detected, and times out after a period of silence. Uses the AVR ATTiny13a and a couple of op-amps.

Mains switching is achieved by emulating a MS-6147-RC 433.92 MHz remote control with firmware and a simple simple on-off-keying (OOK) transmit module.

This solves two problems for me:

  • Mix audio from two sources that previously had to be plugged/unplugged physically to switch. This was annoying and frequently confusing for my partner.
  • Save power by only having my powered speakers turned on when necessary. The switches are in really awkward locations.

Usage

# Install build tooling
sudo apt-get install make gcc-avr avr-libc avrdude

# Get the code
git clone https://gist.github.com/9c2d2dcbf26e83ab438adb4c894a4557.git avr-audio-detect-switch

# Build
make

# Flash (you'll need to update the Makefile if you're not using an AVR Dragon programmer)
make flash

There's no programming header in the schematic as I used a SOIC-8 clip connector to attach directly to the microcontroller IC.

Further reading

JayCar has a detailed article on emulating these 443 MHz remotes if you want more details or Arduino code.

/*
* Automatic, audio-triggered power switch
*
* This program controls the mains power to a set of speakers based on the
* presence of an audio signal. To switch the mains power, it sends a 433.92MHz
* radio signal to a MS-6148 ("Mains Outlet with Remote" from JayCar), emulating
* the MS-6147-RC remote control.
*
* Created: 15/05/2022
* Author : Stephen Holdaway
*/
#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <stdbool.h>
#include <stdlib.h>
#include <util/delay.h>
#define ADC_IN_PIN PB4
#define RADIO_OUT_PIN PB3
#define WDT_PERIOD_SECS 0.032
// Delays for RF modulation
#define SHORTPULSE_US 316
#define LONGPULSE_US 818
enum RemoteCommand {
kRF_One_On = 0xF,
kRF_One_Off = 0xE,
kRF_TWo_On = 0xD,
kRF_Two_Off = 0xC,
kRF_Three_On = 0xB,
kRF_Three_Off = 0xA,
kRF_Four_On = 0x7,
kRF_Four_Off = 0x6,
kRF_All_On = 0x4,
kRF_All_Off = 0x8,
};
// 20-bit address of the remote
//
// Your remote's address can be found by capturing a signal from any button
// press with a either a software-defined radio or wiring directly to a test
// point on the remote's PCB:
//
// > ...solder jumper wires to...the large solder pad next to C11 (if you follow
// > this trace, it leads back to pin 2 on the IC). This is the raw signal out of
// > the IC before it gets to the 433MHz transmitter.
//
// The remote's address is the first 20 bits (short on, long off = 0; long on, short off = 1).
static const uint32_t kAddress = 0xFFFFF; // Replace with your remote's address
/**
* Reverse order of bits in byte
*/
uint8_t reverse_bits(uint8_t val)
{
return ((val & 0x80) >> 7) |
((val & 0x40) >> 5) |
((val & 0x20) >> 3) |
((val & 0x10) >> 1) |
((val & 0x08) << 1) |
((val & 0x04) << 3) |
((val & 0x02) << 5) |
((val & 0x01) << 7);
}
/**
* Calculate CRC on lower 24 bits of input for MS-6148
*/
uint8_t rf_crc(uint32_t data)
{
uint8_t a = reverse_bits(data >> 16);
uint8_t b = reverse_bits(data >> 8);
uint8_t c = reverse_bits(data);
return reverse_bits(a + b + c);
}
/**
* Create a packet with the passed 20-bit address, command byte and a checksum
*/
uint32_t packet(uint32_t addr, uint8_t cmd)
{
uint32_t data = ((addr & 0xFFFFF) << 4) | (cmd & 0xF);
return (data << 8) | rf_crc(data);
}
/**
* Transmit sequence representing a 1 bit
*/
void rf_send_high()
{
PORTB |= _BV(RADIO_OUT_PIN);
_delay_us(LONGPULSE_US);
PORTB &= ~_BV(RADIO_OUT_PIN);
_delay_us(SHORTPULSE_US);
}
/**
* Transmit sequence representing a 0 bit
*/
void rf_send_low()
{
PORTB |= _BV(RADIO_OUT_PIN);
_delay_us(SHORTPULSE_US);
PORTB &= ~_BV(RADIO_OUT_PIN);
_delay_us(LONGPULSE_US);
}
void sendrf(uint32_t packet)
{
// Repeat packet multiple times to ensure delivery
for(int i = 0; i < 8; i++) {
// Send all bits (MSB-first)
for(uint32_t bit = 0x80000000UL; bit > 0; bit >>= 1) {
if(bit & packet){
rf_send_high();
} else {
rf_send_low();
}
}
// 2 more low bits
rf_send_low();
rf_send_low();
// Brief delay between repeats
_delay_ms(10 * 2.06);
}
}
inline void setup()
{
uint8_t ddr = 0;
uint8_t port = 0;
// Radio modulation output, initially low
ddr |= _BV(RADIO_OUT_PIN);
port &= ~_BV(RADIO_OUT_PIN);
// Audio signal input, no pull-up
ddr &= ~_BV(ADC_IN_PIN);
port &= ~_BV(ADC_IN_PIN);
DDRB = ddr;
PORTB = port;
}
inline void adc_init()
{
ADMUX = 2; // Select ADC2 (PB4)
DIDR0 = _BV(ADC2D); // Disable digital input on ADC2
// Enable ADC
ADCSRA = _BV(ADEN);
}
inline void watchdog_init()
{
// Set up watchdog timer for waking from sleep
WDTCR |= _BV(WDCE); // Allow watchdog changes in the next 3 cycles
WDTCR = _BV(WDP0) | _BV(WDTIE); // Set watchdog timeout to 32 ms, interrupt-only
}
int main(void)
{
setup();
adc_init();
watchdog_init();
sei();
// State for managing the wireless power switch
// Active ticks initially acts as a wait for readings to stablise (while higher than maxTickAccumulation)
bool is_powered = false;
uint16_t activeTicks = 0xFFFF - ((1 /*xecconds*/ / WDT_PERIOD_SECS) * 5);
// Fixed-point accumulation filter for measuring audio energy over time
int32_t sum_filter = 0;
// Settings for power on and off
// Each tick is a watchdog timer overflow period
const uint16_t powerOnInertia = (2 /*xecconds*/ / WDT_PERIOD_SECS);
const uint16_t maxTickAccumulation = ((20 /*minutes */ * 60) / WDT_PERIOD_SECS);
uint16_t delta_filter = 0;
uint16_t previous_sum = 0;
while (1) {
uint16_t sum = 0;
// Take multiple samples over a short period of time
const uint8_t samples = 4;
for (uint8_t i = samples; i != 0; --i) {
// Start an ADC conversion and wait for it to finish
ADCSRA |= _BV(ADSC);
while (ADCSRA & _BV(ADSC));
// Grab reading, centred to half VCC
uint16_t reading = ADCL;
reading |= (ADCH<<8);
reading -= 512;
sum += abs(reading);
// Sample over a period of time
_delay_us(250);
}
// Pass summed energy through a first-order infinite impulse response (IIR) filter
// This accumulates energy over time, so we get an idea of how much audio signal is present
{
int32_t local = sum;
local <<= 16;
sum_filter += (local - sum_filter) >> 5;
sum = (sum_filter + (1<<15)) >> 16;
}
// Monitor changes over time in the measured energy to detect audio signals
{
uint16_t delta = abs(sum - previous_sum);
delta_filter += (delta << 5);
if (delta_filter > 0) {
delta_filter -= delta_filter >> 4;
}
previous_sum = sum;
}
const uint16_t activeness = delta_filter >> 4;
if (activeTicks > maxTickAccumulation) {
// Still in the stablising period after reset: don't use the signal yet
activeTicks++;
} else {
// Handle turning on and off
if (activeness >= 10) {
// Turn on when there appears to be an audio signal
if (!is_powered && activeTicks > powerOnInertia) {
sendrf(packet(kAddress, kRF_One_On));
is_powered = true;
// Add extra time to avoid turning on and off repeatedly
const uint16_t histeresis = (60/WDT_PERIOD_SECS);
activeTicks = histeresis;
}
if (activeTicks < maxTickAccumulation) {
activeTicks++;
}
} else {
if (activeTicks != 0) {
activeTicks--;
}
if (activeTicks == 0 && is_powered) {
sendrf(packet(kAddress, kRF_One_Off));
is_powered = false;
}
}
}
// Go to sleep (idle) until the watchdog timer wakes us up again
sleep_enable();
sleep_cpu();
sleep_disable();
}
return 0;
}
ISR(WDT_vect)
{
// An actual interrupt is required to wake from sleep using the watchdog timer.
// This empty implementation is here to override the default vector of a soft reset
return;
}
# Name: Makefile
# Author: <insert your name here>
# Copyright: <insert your copyright message here>
# License: <insert your license reference here>
# This is a prototype Makefile. Modify it according to your needs.
# You should at least check the settings for
# DEVICE ....... The AVR device you compile for
# CLOCK ........ Target AVR clock rate in Hertz
# OBJECTS ...... The object files created from your source files. This list is
# usually the same as the list of source files with suffix ".o".
# PROGRAMMER ... Options to avrdude which define the hardware you use for
# uploading to the AVR and the interface where this hardware
# is connected. We recommend that you leave it undefined and
# add settings like this to your ~/.avrduderc file:
# default_programmer = "stk500v2"
# default_serial = "avrdoper"
DEVICE = attiny13a
CLOCK = 1200000 # DIV8
PROGRAMMER = -c dragon_isp -B 0.1MHz
SOURCES = main.c softuart.c
OBJECTS = $(SOURCES:.c=.o)
AVRDUDE = avrdude $(PROGRAMMER) -p t13
COMPILE = avr-gcc -Wall -Os -DF_CPU=$(CLOCK) -mmcu=$(DEVICE)
COMPILE += -I -I. -I./lib/
COMPILE += -funsigned-char -funsigned-bitfields -fpack-struct -fshort-enums
COMPILE += -ffunction-sections -fdata-sections -Wl,--gc-sections
COMPILE += -Wl,--relax -mcall-prologues
COMPILE += -std=gnu11
# symbolic targets:
all: $(SOURCES) main.hex
.c.o:
$(COMPILE) -c $< -o $@
.S.o:
$(COMPILE) -x assembler-with-cpp -c $< -o $@
# "-x assembler-with-cpp" should not be necessary since this is the default
# file type for the .S (with capital S) extension. However, upper case
# characters are not always preserved on Windows. To ensure WinAVR
# compatibility define the file type manually.
.c.s:
$(COMPILE) -S $< -o $@
flash: all
$(AVRDUDE) -U flash:w:main.hex:i
fuse:
@echo "For computing fuse byte values see the fuse bit calculator at http://www.engbedded.com/fusecalc/"
@echo "Suggested fusing is: $(AVRDUDE) -U lfuse:w:0x29:m -U hfuse:w:0xfb:m"
# Xcode uses the Makefile targets "", "clean" and "install"
install: flash fuse
clean:
find -name '*.d' -exec rm {} +
find -name '*.o' -exec rm {} +
rm -f main.hex main.elf
# file targets:
main.elf: $(OBJECTS) Makefile
$(COMPILE) -o main.elf $(OBJECTS)
main.hex: main.elf
rm -f main.hex
avr-objcopy -j .text -j .data -O ihex main.elf main.hex
avr-size --format=avr --mcu=$(DEVICE) main.elf
# If you have an EEPROM section, you must also create a hex file for the
# EEPROM and add it to the "flash" target.
# Targets for code debugging and analysis:
disasm: main.elf
avr-objdump -d main.elf
/**
* This is loosely based on MarcelMG/AVR8_BitBang_UART_TX, but I've mostly rewritten it.
* The original used an interrupt unecessarily. This replaces that with a blocking loop.
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#define TX_PORT PORTB
#define TX_PIN PB3
#define TX_DDR DDRB
#define TX_DDR_PIN DDB0
#define BAUD_RATE 9600
static void bit_delay()
{
// Wait for timer to hit comparison value again
while ((TIFR0 & _BV(OCF0A)) == 0);
// Clear comparison flag for the next bit_delay call
TIFR0 |= _BV(OCF0A);
}
void uart_send_byte(char byte)
{
// Reset timer and comparison flag
TCNT0 = 0;
TIFR0 |= _BV(OCF0A);
// Start timer0 with a prescaler of 8
TCCR0B = (1<<CS01);
// Send start bit
TX_PORT &= ~(1<<TX_PIN);
bit_delay();
for (uint8_t i = 8; i != 0; --i) {
if(byte & 0x01) {
TX_PORT |= (1<<TX_PIN);
} else {
TX_PORT &= ~(1<<TX_PIN);
}
byte >>= 1;
bit_delay();
}
// Send stop bit
TX_PORT |= (1<<TX_PIN);
bit_delay();
// Stop timer0
TCCR0B = 0;
}
void uart_send(char* string)
{
while (*string) {
uart_send_byte(*string++);
}
}
void uart_init()
{
// Set TX pin as output, idling high
TX_DDR |= (1<<TX_DDR_PIN);
TX_PORT |= (1<<TX_PIN);
// Set timer0 to CTC mode
TCCR0A = (1<<WGM01);
// Set bit length timer
OCR0A = ((F_CPU/8) / BAUD_RATE);
}
#pragma once
void uart_send_byte(char byte);
void uart_send(char* string);
void uart_init();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment