Электронный тортик в кармане: дневник разработки

электронный тортик в осенней гамме

Около года назад, когда я игрался с официальным Arduino Starter Kit, мне пришла в голову мысль сделать в подарок жене «схемотехнический тортик», а именно плату со светодиодами-свечками, которые можно задувать. Эта задача показалась мне хорошим учебным проектом для понимания того, как программировать микроконтроллеры и как воплощать программы в материю (ведь электроникой я стал увлекаться относительно недавно, а программировать начал еще в школе, поэтому ощущал огромный пробел в своем образовании).

Мне хотелось бы поделиться своим опытом разработки такого простого устройства с нуля, и заодно выложить его схему и программу, так что вы сможете воспроизвести его дома.
 
 

История


Первый прототип был сделан на Arduino Uno и прилагаемого к нему пьезо-элемента в качестве детектора задувания свечей:

прототип электронного тортика на Arduino Uno


Считывая напряжение на пьезо-элементе, устройство ожидало малейшего изменения показаний. Но даже на максимальной разрешающей способности АЦП надо было очень хорошо подуть, чтобы свечи начали гаснуть. Зато гасли они хорошо: сначала начинали мерцать с разной частотой и фазой, а потом по очереди гасли в зависимости от заданного случайного времени жизни каждой свечи.

Следующим шагом я решил оформить тортик более нарядно, чем плата с торчащими проводками. А еще хотелось показать жене, что тортик работает от маааленького микроконтроллера. Где-то в интернете я находил статью, что Arduino можно запускать на ATTiny85 контроллере, его я и решил использовать в качестве мозгов. Проблема была в том, что у этого контроллера всего пять ножек общего назначения, которые можно было использовать для свечей и датчика, так что выходило, что я мог использовать только четыре свечи, если посадить каждый светодиод на отдельную ножку. Про чарлиплексирование я тогда не знал, и решил дублировать свечи схемой задержки на инверторе: когда светодиоды мигают, то вряд ли кто-то заметит, что они дублируются с задержкой фазы. Кроме того, добавочные элементы хорошо укладывались в круговой шаблон размещения свечей:

прототип электронного тортика на ATTiny85 со схемой задержки


В качестве питания я использовал четыре ААА батарейки, что нарушало лаконичность внешнего вида. Кроме того, хотелось больше честных свечей и большей чувствительности. Я пошуршал по спецификациям микроконтроллеров из серии ATTiny, и обнаружил прекрасный аналог — ATTiny84 с 11 общими ножками общего назначения. Кроме того, у него имеется вход АЦП с дифференциальным усилителем в 20 раз! Так появился новый прототип:

прототип электронного тортика на ATTiny44 с двумя батарейками АА


Я надеялся засунуть две батарейки ААА между этажами, но просчитался с размером стоечек и пьезо-элемента, который не должен был прилегать к плате вплотную, поэтому пришлось прилепить батарейки под плату. С усилением сигнала тортик стал гораздо чувствительнее, и появились шумы, поэтому понадобилось изменить алгоритм: я измерял среднеквадратичное отклонение сигнала и включал логику задувания, когда это отклонение превышало некоторый эмпирически найденный порог. Программа получилась небольшой, меньше 4 кБ, поэтому я взял ATTiny44, у которого в два раза меньше памяти.

Мне жутко не нравилось питание схемы и пустое пространство между этажами, я пошуршал по спецификациям батареек и обнаружил, что бывают батарейки АААА. Они достаточно маленькие и обеспечивают ток в сотни миллиампер. Их сложно купить, но если взять девятивольтовые батареи типа 6LR61, то они внутри состоят из шести батареек АААА!

Еще я нашел интересную микросхему импульсного источника питания HT7750, повышающую напряжение питания до +5 В за счет увеличения тока батареи, и решил сделать тортик такого размера, чтобы по диагонали вместилась одна АААА батарейка, которая и будет его питать. Получился вот такой прототип:

прототип электронного тортика на ATTiny44 с одной батарейкой АААА


Использование широкоугольных светодиодов не только улучшило эстетику тортика, но и увеличило угол обзора. Платы я заказал в Китае, что улучшило качество готового устройства. А вот чего я не ожидал, так это резкого увеличения шумов при определении задувания. Дело в том, что импульсный источник питания сам по себе шумный, а тут еще и зависимость выходного напряжения от тока, когда горит разное количество светодиодов. Старый алгоритм перестал работать, и мне пришлось подойти к задаче более основательно: считывать показания с пьезо-сенсора на компьютер и анализировать амплитудно-частотную характеристику сигнала. Я записал одну минуту тишины, одну минуту задувания и минуту шорохов трения об корпус. Оказалось, что пьезо-элемент имеет резонанс около 1 кГц, по которому можно достаточно достоверно определить, что свечи сейчас задуваются.

Итак, для определения момента задувания свечей я стал считать быстрое преобразование Фурье с окном Ханнинга для частоты 1 кГц. ATTiny44 не имеет встроенной команды умножения, да и память весьма ограничена (256 байт ОЗУ, 4 кБ на программу), потому все умножения пришлось заменить на сдвиги и сложения, да и вообще существенно оптимизировать программу, чтобы все влезло. В результате изменения программы тортик превзошел по чувствительности все другие прототипы. Далее я стал убирать лишние компоненты на пути сигнала, которые я по наивности считал фильтром высоких частот, и тортик стал реагировать еще лучше. Заменив HT7750 на HT7733, т.е. заменив выход с +5 В на +3.3 В, я получил более экономное использование батареи.

Последним моментом, который меня смущал, было то, что микросхема питания в выключенном режиме потребляла приблизительно 17 µА, что означало опустошение батарейки за год. Но я вспомнил, что у этой микросхемы есть один вариант исполнения в корпусе SOT-25, в котором есть разрешающий вход, и составил небольшую схемку включения, которая при нажатии на кнопку сброса заряжала конденсатор для подачи высокого уровня на разрешающем входе микросхемы с последующей медленной разрядкой в течение трех минут через десятимегаомный резистор. После этого таймаута тортик выключается и начинает потреблять меньше 1 µА, что сопостовимо с током саморазрядки батареи.

электронный тортик на фоне руки для сравнения размеров

 

Принципиальная схема


принципиальная схема тортика

HT7733 — это импульсный источник питания, повышающий напряжение питания на входе до +3.3 В. Пьезо-элемент диаметром 20 мм поключен к дифференциальному АЦП микроконтроллера с усилением в 20 раз. Каждый светодиод подключен к одной ножке микроконтроллера.

При нажатии на RESET открывается транзистор, через который заряжается конденсатор 10 мкФ и подается высокий уровень на разрешающий вход микросхемы питания. Микросхема включается и начинает выдавать на выход +3.3 В в течение трех минут, пока конденсатор 10 мкФ разряжается через десятимегаомный резистор.
 

Программа


Программа на C++ для Arduino IDE
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // BitCake v1.1.1 / November 8, 2014
  // by Maksym Ganenko <buratin.barabanus at Google Mail>
  /////////////////////////////////////////////////////////////////////////////////////////////////
  
  #include <avr/interrupt.h>
  #include <avr/pgmspace.h>
  #include <avr/power.h>
  #include <avr/sleep.h>
  #include <time.h>
  #include <util/delay.h>
  
  /////////////////////////////////////////////////////////////////////////////////////////////////
  
  // set fixed delta loop time in milliseconds
  // 0 to use internal timer
  const uint8_t     DELTA_LOOP_TIME_MS  = 14;
  
  // amplify intermediate values to get better calculation accuracy
  const uint8_t     SAMPLES_GAIN_ORDER  = 5; // x32
  const uint8_t     RESULT_GAIN_ORDER   = 2; // x4
  
  // LEDs encoded by ports ID
  const prog_int8_t LEDS[] PROGMEM      = { 0xA6, 0xA7, 0xB2, 0xB1, 0xB0, 0xA2, 0xA3, 0xA4, 0xA5 };
  const uint8_t     LEDS_NUM            = sizeof(LEDS) / sizeof(LEDS[0]);
  
  // ADMUX register code for ADC
  const uint8_t     PIEZO_ADMUX         = 0b10101001; // Vref = 1.1V, (A1 - A0) x 20
  
  // MCU prescaler
  const uint8_t     MCU_PRESCALER       = 0b000; // 1 => 8 Mhz CPU clock
  
  // ADC prescaler
  const uint8_t     ADC_PRESCALER       = 0b100; // 16 => 512 kHz ADC clock => 38.5k reads per sec
  
  // number of piezo reads to average per sample (for noise reduction)
  const uint8_t     SUBSAMPLE_BUF_ORDER = 4; // => 16
  const uint8_t     SUBSAMPLE_BUF_SIZE  = (1 << SUBSAMPLE_BUF_ORDER);
  
  // FFT samples number - can't be changed without changing FFT calculation code
  const uint8_t     SAMPLE_BUF_ORDER    = 5; // => 32
  const uint8_t     SAMPLE_BUF_SIZE     = (1 << SAMPLE_BUF_ORDER);
  
  // FFT signal threshold to activate blowing logic
  // this value defines the sensitivity of device
  // depends on electronic components noise
  const uint8_t     BLOWING_THRESHOLD   = 3;
  
  // timeouts in milliseconds
  const uint32_t    SETUP_TIME_MS       = 750;    // timeout before activating of cake logic
  const uint32_t    DELAY_BLOWING_MS    = 0;      // delay LEDs flickering when blowing detected
  const uint32_t    PROLONG_BLOWING_MS  = 150;    // prolong blowing logic when no blowing detected
  const uint32_t    NO_ACTIVITY_MS      = 60000;  // turn off cake if no blowing detected
  const uint32_t    TIME_LIMIT_MS       = 150000; // time limit for cake to work
  
  // LEDs blinking periods when blowing logic is activated
  const uint8_t     LEDS_PERIOD_MIN_MS  = 100;
  const uint8_t     LEDS_PERIOD_MAX_MS  = 150;
  
  // LEDs time-to-live timeouts
  const uint16_t    LEDS_TTL_MIN_MS     = 200;
  const uint16_t    LEDS_TTL_MAX_MS     = 1000;
  
  /////////////////////////////////////////////////////////////////////////////////////////////////
  
  volatile uint8_t  samplePos           = SAMPLE_BUF_SIZE;
  
  // FFT10 specific accumulators
  int16_t           sampleAccA          [5];
  int16_t           sampleAccB          [5];
  
  // LEDs state variables
  uint16_t          ledsActivity;
  uint8_t           ledsPeriod          [LEDS_NUM];
  uint8_t           ledsPhase           [LEDS_NUM];
  uint8_t           ledsTTL             [LEDS_NUM];
  
  // blowing logic state
  uint8_t           blowing             = false;
  uint32_t          lastBlowingTime     = 0;
  int16_t           totalBlowingTime    = 0;
  
  uint32_t          globalTime          = 0;
  uint32_t          lastLoopTime;
  uint32_t          setupPhaseTime;
  
  // for compatibility with other Atmel MCUs
  uint8_t           portA, portB, portC, portD;
  
  /////////////////////////////////////////////////////////////////////////////////////////////////
  
  // fast distance approximation
  uint32_t approxDist(int32_t dx, int32_t dy)
  {
     uint32_t min, max;
  
     if (dx < 0)  dx = -dx;
     if (dy < 0)  dy = -dy;
  
     if (dx < dy) { min = dx; max = dy; }
     else         { min = dy; max = dx; }
  
     // coefficients equivalent to (123/128 * max) and (51/128 * min)
     return (((max << 8) + (max << 3) - (max << 4) - (max << 1) +
              (min << 7) - (min << 5) + (min << 3) - (min << 1)) >> 8);
  }
  
  const uint8_t FFT_DIVIDER_ORDER = 8; // => 256
  
  // approximate multiplication
  int32_t mul256(int32_t x) { return x << 8; }
  int32_t mul240(int32_t x) { return (x << 8) - (x << 4); }
  int32_t mul208(int32_t x) { return (x << 7) + (x << 6) + (x << 4); }
  int32_t mul176(int32_t x) { return (x << 7) + (x << 5) + (x << 4); }
  int32_t mul144(int32_t x) { return (x << 7) + (x << 4); }
  int32_t mul96(int32_t x)  { return (x << 6) + (x << 5); }
  int32_t mul48(int32_t x)  { return (x << 5) + (x << 4); }
  
  typedef int32_t (*fmul32)(int32_t);
  const fmul32 fmulVec[4] = { mul96, mul176, mul240, mul256 };
  
  // calculate FFT[10] for 32 samples
  uint8_t fft10() {
    int32_t a = 0;
    for (uint8_t i = 0; i < 4; ++i) {
      a += fmulVec[i](sampleAccA[i + 1]);
    }
    
    int32_t b = 0;
    for (uint8_t i = 0; i < 4; ++i) {
      b += fmulVec[i](sampleAccB[i + 1]);
    }
    
    uint32_t result = approxDist(a << RESULT_GAIN_ORDER, b << RESULT_GAIN_ORDER);
    result >>= FFT_DIVIDER_ORDER;
    if (result > 0xff) return 0xff;
    return result;
  }
  
  /////////////////////////////////////////////////////////////////////////////////////////////////
  
  // fft10 specific coefficients
  const prog_int8_t sampleAccDestA[SAMPLE_BUF_SIZE / 2] PROGMEM = { 
    +4, -1, -2, +3, +0, -3, +2, +1, -4, +1, +2, -3, +0, +3, -2, -1 
  };
  const prog_int8_t sampleAccDestB[SAMPLE_BUF_SIZE / 2] PROGMEM = { 
    +0, +3, -2, -1, +4, -1, -2, +3, +0, -3, +2, +1, -4, +1, +2, -3
  };
  
  const uint8_t HANNING_DIVIDER_ORDER = 6; // => 64
  
  // hanning window coefficients
  int16_t mul0(int16_t x)   { return 0; }
  int16_t mul1(int16_t x)   { return x; }
  int16_t mul3(int16_t x)   { return (x << 2) - x; }
  int16_t mul6(int16_t x)   { return (x << 3) - (x << 1); }
  int16_t mul10(int16_t x)  { return (x << 3) + (x << 1); }
  int16_t mul15(int16_t x)  { return (x << 4) - x; }
  int16_t mul21(int16_t x)  { return (x << 4) + (x << 2) + x; }
  int16_t mul27(int16_t x)  { return (x << 5) - (x << 2) - x; }
  int16_t mul34(int16_t x)  { return (x << 5) + (x << 2); }
  int16_t mul40(int16_t x)  { return (x << 5) + (x << 3); }
  int16_t mul46(int16_t x)  { return (x << 5) + (x << 4) - (x << 2); }
  int16_t mul52(int16_t x)  { return (x << 6) - (x << 3) - (x << 2); }
  int16_t mul56(int16_t x)  { return (x << 6) - (x << 3); }
  int16_t mul60(int16_t x)  { return (x << 6) - (x << 2); }
  int16_t mul63(int16_t x)  { return (x << 6) - x; }
  int16_t mul64(int16_t x)  { return (x << 6); }
  
  // hanning window coefficients
  typedef int16_t (*fmul16)(int16_t);
  const fmul16 hanningVec[] = {
    mul0, mul1, mul3, mul6, mul10, mul15, mul21, mul27, 
    mul34, mul40, mul46, mul52, mul56, mul60, mul63, mul64
  };
  
  // ADC interrup routine
  // we average SUBSAMPLE_BUF_SIZE reads from ADC to reduce noise
  // and apply the calculated value on fft10 specific accumulators
  ISR(ADC_vect)
  {
    static uint8_t subsampleCtr = 0;
    static int16_t subsampleSum = 0;
    
    // read ADC
    uint8_t low = ADCL, high = ADCH;
    int16_t subsample = (high << 8) | low;
  
    if (samplePos < SAMPLE_BUF_SIZE) {
      subsampleSum += subsample;
      ++subsampleCtr;
  
      if (subsampleCtr == SUBSAMPLE_BUF_SIZE) {
        // average of subsamples
        int16_t sample = (subsampleSum >> SUBSAMPLE_BUF_ORDER) << SAMPLES_GAIN_ORDER;
        
        uint8_t halfPos = samplePos & (SAMPLE_BUF_SIZE / 2 - 1);
        uint8_t mulPos = halfPos;
        if (halfPos != samplePos) {
          mulPos = SAMPLE_BUF_SIZE / 2 - mulPos;
        }
        // multiply by hanning window coefficient
        sample = hanningVec[mulPos](sample) >> HANNING_DIVIDER_ORDER;
  
        int8_t destA = pgm_read_byte_near(sampleAccDestA + halfPos);
        int8_t destB = pgm_read_byte_near(sampleAccDestB + halfPos);
  
        if (destA >= 0)  sampleAccA[destA]  += sample;
        else             sampleAccA[-destA] -= sample;
        if (destB >= 0)  sampleAccB[destB]  += sample;
        else             sampleAccB[-destB] -= sample;
        
        ++samplePos;
        subsampleSum = subsampleCtr = 0;
      }
    }  
  }
  
  /////////////////////////////////////////////////////////////////////////////////////////////////
  
  void powerDown() {
    // all pins to low
    portA = portB = portC = portD = 0;
    portsUpdateFinish();
  
    // disable ADC
    ADCSRA &= ~_BV(ADEN);
    
    // power down
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    sleep_mode();
  }
  
  void portsUpdateStart() {
    #if defined(PORTA)
      portA = PORTA;
    #endif
    #if defined(PORTB)
      portB = PORTB;
    #endif
    #if defined(PORTC)
      portC = PORTC;
    #endif
    #if defined(PORTD)
      portD = PORTD;
    #endif
  }
  
  void portsUpdateFinish() {
    #if defined(PORTA)
      if (PORTA != portA) { PORTA = portA; }
    #endif
    #if defined(PORTB)
      if (PORTB != portB) { PORTB = portB; }
    #endif
    #if defined(PORTC)
      if (PORTC != portC) { PORTC = portC; }
    #endif
    #if defined(PORTD)
      if (PORTD != portD) { PORTD = portD; }
    #endif
  }
  
  void writeLed(uint8_t anIndex, uint8_t aValue) {
    uint8_t led = pgm_read_byte_near(LEDS + anIndex);
    uint8_t code = _BV(led & 0x0F);
    if (aValue && bitRead(ledsActivity, anIndex)) {
      switch(led & 0xF0) {
        #if defined(PORTA)
          case 0xA0: portA |= code; break;
        #endif
        #if defined(PORTB)
          case 0xB0: portB |= code; break;
        #endif
        #if defined(PORTC)
          case 0xC0: portC |= code; break;
        #endif
        #if defined(PORTD)
          case 0xD0: portD |= code; break;
        #endif
      }
    } else {
      switch(led & 0xF0) {
        #if defined(PORTA)
          case 0xA0: portA &= ~code; break;
        #endif
        #if defined(PORTB)
          case 0xB0: portB &= ~code; break;
        #endif
        #if defined(PORTC)
          case 0xC0: portC &= ~code; break;
        #endif
        #if defined(PORTD)
          case 0xD0: portD &= ~code; break;
        #endif
      }
    }
  }
  
  /////////////////////////////////////////////////////////////////////////////////////////////////
  
  void setup() {
    portsUpdateStart();
    for (uint8_t i = 0; i < LEDS_NUM; ++i) {
      bitSet(ledsActivity, i);
      uint8_t led = pgm_read_byte_near(LEDS + i);
      uint8_t code = _BV(led & 0x0F);
      switch (led & 0xF0) {
        #if defined(DDRA)
          case 0xA0: DDRA |= code; break;
        #endif
        #if defined(DDRB)
          case 0xB0: DDRB |= code; break;
        #endif
        #if defined(DDRC)
          case 0xC0: DDRC |= code; break;
        #endif
        #if defined(DDRD)
          case 0xD0: DDRD |= code; break;
        #endif
      }
      writeLed(i, HIGH);
    }
    portsUpdateFinish();
    
    // set MCU prescaler
    CLKPR = 0b10000000;
    CLKPR = MCU_PRESCALER;
    
    // set ADC prescaler
    ADCSRA = (ADCSRA & ~0b111) | ADC_PRESCALER;
  
    // activate ADC auto-triggering
    ADCSRA |= _BV(ADATE) | _BV(ADIE);
    ADMUX = PIEZO_ADMUX;
    ADCSRA |= _BV(ADSC);
    
    // disable all digital inputs
    DIDR0 = 0xff;
    
    // disable analog comparator
    ACSR |= _BV(ACD);
    
    // disable timer if delta loop time is defined
    if (DELTA_LOOP_TIME_MS) {
      power_timer0_disable();
      power_timer1_disable();
      set_sleep_mode(SLEEP_MODE_ADC);
    }
  
    _delay_ms(100);
    
    lastLoopTime = DELTA_LOOP_TIME_MS ? 0 : millis();
    setupPhaseTime = lastLoopTime + SETUP_TIME_MS;
  }
  
  void loop() {
    uint32_t time = DELTA_LOOP_TIME_MS ? globalTime : millis();
    uint16_t loopDeltaTime = time - lastLoopTime;
    uint8_t setupPhase = time < setupPhaseTime;
    rand(); // update random seed
      
    // wait for ADC routine to read all samples for FFT
    memset(sampleAccA, 0, sizeof(sampleAccA));
    memset(sampleAccB, 0, sizeof(sampleAccB));
    samplePos = 0;
    while (samplePos != SAMPLE_BUF_SIZE) {
      if (DELTA_LOOP_TIME_MS) { sleep_mode(); }
    }
    
    portsUpdateStart();
    
    // calculate FFT[10]
    uint8_t signal = fft10();
      
    // blowing detection
    if (signal > BLOWING_THRESHOLD) {
      if (!blowing) {
        // generate LEDs flickering values
        for (uint8_t i = 0; i < LEDS_NUM; ++i) {
          ledsPeriod[i] = LEDS_PERIOD_MIN_MS + rand() % (LEDS_PERIOD_MAX_MS - LEDS_PERIOD_MIN_MS);
          ledsTTL[i] = (LEDS_TTL_MIN_MS + rand() % (LEDS_TTL_MAX_MS - LEDS_TTL_MIN_MS)) >> 3;
          ledsPhase[i] = rand() % ledsPeriod[i];
        }
      }
      blowing = !setupPhase;
      lastBlowingTime = time;
    }
  
    if (blowing && time - lastBlowingTime > PROLONG_BLOWING_MS) { 
      blowing = false;
    }
  
    if (blowing) {
      totalBlowingTime += loopDeltaTime;
      if (totalBlowingTime >= DELAY_BLOWING_MS) {
  
        // prolong startup time until noise stabilizes
        if (setupPhase) { setupPhaseTime += SETUP_TIME_MS; }
        
        // update LEDs state
        for (uint8_t i = 0; i < LEDS_NUM; ++i) {
          uint8_t level = ((time + ledsPhase[i]) % ledsPeriod[i] < (ledsPeriod[i] >> 1)) 
                        ? LOW : HIGH;
          if (signal <= BLOWING_THRESHOLD) { level = !level; }
          writeLed(i, level);
          
          if (!setupPhase && totalBlowingTime > (ledsTTL[i] << 3)) { bitClear(ledsActivity, i); }
        }
      }
    } else {
      totalBlowingTime = max(0, totalBlowingTime - loopDeltaTime);
      if (totalBlowingTime < 0) totalBlowingTime = 0;
      for (uint8_t i = 0; i < LEDS_NUM; ++i) { writeLed(i, HIGH); }
    }
  
    if (setupPhase) {
      if (time >= 1500) { // show busy state
          int lowLed = (time >> 6) % LEDS_NUM;
          for (uint8_t i = 0; i < LEDS_NUM; ++i) {
            writeLed(i, (i == lowLed) ? LOW : HIGH);
          }
      }
    } else {
      const bool DEBUG_MODE     = false;    // trace debug value using LEDs
      const bool INVERT_LEVELS  = true;     // LOW level means 1, HIGH level means 0
      const bool MEASURE_TIME   = false;    // measure time in ms (minus offset, see code)
      const bool SHOW_ORDER     = false;    // show value as binary order
  
      if (DEBUG_MODE) {
        int value = signal; // value to show
        if (MEASURE_TIME) {
          static uint32_t totalLoopTime = 0;
          static uint32_t loopCtr = 0;
          totalLoopTime += loopDeltaTime;
          ++loopCtr;
          // set time offset here
          value = totalLoopTime / loopCtr - 10; 
        }
  
        int dbgValue = value;
        if (SHOW_ORDER) {
          dbgValue = 0;
          for (; value > 0; ++dbgValue, value >>= 1);
        }
  
        for (uint8_t i = 0; i < LEDS_NUM; ++i) {
          bitSet(ledsActivity, i);
          writeLed(i, (dbgValue > i)
            ? (INVERT_LEVELS ? LOW  : HIGH)
            : (INVERT_LEVELS ? HIGH : LOW));
        }
  
        // the last LED shows blowing state
        writeLed(LEDS_NUM - 1, blowing ? HIGH : LOW);
     }
    }
    
    portsUpdateFinish();
  
    if (ledsActivity == 0 || time - lastBlowingTime > NO_ACTIVITY_MS || time > TIME_LIMIT_MS) {
      powerDown();
    }
    
    if (DELTA_LOOP_TIME_MS) { 
      globalTime += DELTA_LOOP_TIME_MS; 
    }
    lastLoopTime = time;
  }
  
  /////////////////////////////////////////////////////////////////////////////////////////////////

Для программирования контроллера понадобится ICSP программатор типа USBTiny или плата Arduino, запрограммированная быть программатором (ищите «Arduino как программатор»). Программу можно загрузить прямо из Arduino IDE, но до этого нужно поставить специальные библиотеки для ATTiny и выбрать в качестве контроллера ATTiny44 8MHz.
 

Ссылки


http://bitcake.eu — сайт, посвященный этому проекту (англ.)
Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 45
    +2
    А как же видео??
      +5
      Видео будет! Но у меня проблема с перфекционизмом. Домашние камеры / телефоны не передают всей красоты.
      +10
      Сайт красивый. Кто не сходил, сходите.
        0
        Вот это я понимаю — инженерный подход!
        А где заказывали платы?
          +4
          Очень крутое место, рекомендую: http://dirtypcbs.com
          10 плат 5х5 см за $14 или 30 плат 5x5 см за $28, т.е. около 25 центов за квадратный дюйм, с бесплатной доставкой.
          Но есть несколько нюансов — они никак не помогают с платами, просто давай им gerber файлы и жди, пока придут, практически молча, не считая нескольких статусов (at board house, shipped)
          Они утверждают, что делают платы только для прототипирования, чтобы не было придирок к качеству, но по факту они делают платы хорошо.
          Еще один нюанс — влепляют на шелк свои номера, без возможности проконтроллировать, куда именно, но по опыту они стараются впихнуть его так, чтобы смотрелось хорошо.
            +3
            Думаю, будет полезной статья о специфике подготовки плат для подобных сервисов… С примерами экспорта из популярных программ…
            +1
            Сколько времени обычно проходит от момента отправки им gerber-файлов до момента, когда платы уже у вас?
              +1
              Примерно три недели: одна на производство, две на доставку в Украину. В этот раз на доставку ушло три с половиной недели, поскольку в Гонгконге в последнее время стали тщательнее осматривать выезжающие машины, но это, возможно, временное явление.
              0
              Нюансов там море.
              Могут вместо 10 плат прислать 1 или 2, если речь про protopack (официально на сайте прописано).
              Требования к плате прописаны очень невнятно и с ошибками.
              Сайт — типичная поделка на php (http://dirtypcbs.com/view.php), и заливать свои данные туда… ну не знаю.
              Это то, что с ходу находится.

                0
                Они имели ввиду не одну или две, а плюс минус одну или две. Т.е. для прототипов это от 8 до 12 плат. В первый раз они мне вообще по ошибке прислали 20.
            +2
            Красивый код, демонстрирующий, что и под ардуину можно писать шикарно.
            Особо приятное место
              int32_t mul256(int32_t x) { return x << 8; }
              int32_t mul240(int32_t x) { return (x << 8) - (x << 4); }
              int32_t mul208(int32_t x) { return (x << 7) + (x << 6) + (x << 4); }
              int32_t mul176(int32_t x) { return (x << 7) + (x << 5) + (x << 4); }
              int32_t mul144(int32_t x) { return (x << 7) + (x << 4); }
              int32_t mul96(int32_t x)  { return (x << 6) + (x << 5); }
              int32_t mul48(int32_t x)  { return (x << 5) + (x << 4); }
              
              typedef int32_t (*fmul32)(int32_t);
              const fmul32 fmulVec[4] = { mul96, mul176, mul240, mul256 };
              
              // calculate FFT[10] for 32 samples
              uint8_t fft10() {
                int32_t a = 0;
                for (uint8_t i = 0; i < 4; ++i) {
                  a += fmulVec[i](sampleAccA[i + 1]);
                }
            


            Массив указателей на функции — прекрасный способ избавится от «китайского стиля» и сэкономить место в программной памяти.
              +3
              Все верно, это было сделано именно из необходимости сэкономить место.
              В следующей итерации — избавление от ардуиновского main, что добавляет еще около 200 байт.
              Собственно говоря, от Arduino после этого мало что остается.
                0
                Тут главное, чтобы компилятор не попытался сумничать и развернуть цикл:
                #pragma Loop_Optimize (No_Unroll);
                

                gcc.gnu.org/onlinedocs/gnat_rm/Pragma-Loop_005fOptimize.html
                  +1
                  Спасибо за подсказку!
                  0
                  Библиотеки же ардуиновские не используются?
                  Весь код переписывается на чистом С и все.
                  Можно заодно таблицу векторов урезать (ассемблер чутка нужен), если еще памяти нужно будет.
                +1
                Замечательная история. Это как детектив, фантастика и приключение одновременно для радиолюбителя. Текст, иллюстрации — браво! Спасибо.
                  +2
                  Перейти полностью на SMD, одна плата вместо двух, батарейка CR2032(можно 2016) в держателе, полевичёк вместо DC/DC для отключения от батарейки.
                  … как то так.
                  А вообще здорово что где-то есть люди с таким энтузиазмом вливающиеся в электронику и прграммирование её!
                    0
                    Я вас понял! Но там нужно около 55 мА ток, неужели таблетка выдержит?
                      0
                      55мА это где вы намеряли на батарейке или за DC/DC?
                        0
                        За DC/DC. Девять светодиодов по пять с чем-то миллиампер каждый плюс контроллер на 8 МГц потребляют 55 мА.
                          0
                          Емкость CR2032 >200mAh, правда длительный разрядный ток небольшой, но мА 8-10 даст без проблем. Светодиоды придется подыскать менее прожорливые, 10мА для контроллера тоже сильно много какой нибудь PIC или MSP легко в <1мА впишутся. К тому же это ведь свечи на торте светодиоды мерцают, а значит не горят все вместе.
                          тыц
                    0
                    А почему FFT а не полосный FIR фильтр?
                      0
                      Мне пока не хватает знания теории, за подсказку спасибо.
                      +1
                      Аж прослезился. Прекрасная работа!
                        0
                        Очень интересный тортик получился. Скажите, а чем обусловлен выбор в пользу пьезодатчика? Возможно, стоит обратить внимание на миниатюрные электретные микрофончики, там и усилитеный каскад уже встроен?
                          0
                          Пьезодатчик только из эстетических соображений.
                          0
                          Программа на C++ для Arduino IDE
                          где Вы там C++ увидели?
                            0
                            Как минимум — переменные объявлены где попало. А вообще, в Arduino этот код компилится именно как C++
                              0
                              Не согласен. Общеизвестные компиляторы C (GCC, IAR) по умолчанию разрешают «раскидывание» переменных. Классов, объектов в модуле нет. На язык C похоже, да, но не на C++.

                              "А вообще, в Arduino этот код компилится именно как C++"
                              для меня это новость. Не подскажете, откуда информация?
                                +1
                                Для меня это тоже была новость, пока я не получил Arduino Starter Kit. У них чуть ли не в первых уроках используется Serial.begin(..) и Serial.print(..)

                                Еще, помню, была библиотека от Adafruit по работе с LED матрицей, там у них тоже все операции через объект.
                            +2
                            Может проще было бы использовать какой-нибудь термодатчик, который работает по SPI. При задувании температура должна скачкообразно уменьшаться.
                              +1
                              Тут вообще можно пойти по какому-нибудь нестандартному пути, поставить например дальномеры или еще что то неожиданное придумать. Меня как раз недавняя статья (термометр из дальномера) по этому поводу приятно порадовала.
                              +1
                              Можно со временем добавить на сайт каунтер сколько тортиков уже испечено и отправлено своим заказчикам)))
                                0
                                Хорошая идея!
                                0
                                А можно глупый вопрос. Почему конденсатор 10мкФ подключается к источнику питания через транзистор без резистора?
                                  0
                                  Ток, заряжающий конденсатор, уже ограничен 100к резистором в базе транзистора — это примерно то же самое, как если бы я подключил 100к / 200 = 500 Ом последовательно с конденсатором (где 200 — это грубое округления беты транзистора).
                                    0
                                    Понятно, спасибо.
                                  0
                                  Хорошая статья, только есть вопрос: почему тортик квадратный? А конденсатор бы заменить на танталовый меньшей емкости, можно будет сэкономить место
                                    0
                                    Круглый вариант возможен в будущем. Если заказывать круглые платы, то это уже нужен v-cutter, а это другой ценовой диапазон для малых партий.
                                    +1
                                    Понравилась минимальная концепция сайта.
                                    Интересно — есть ли заказы.
                                      +2
                                      Такая информация появится, когда я добавлю счетчик испекшихся тортиков.
                                      +1
                                      Лучше использовать интегральный синхронный конвертер, например
                                      MAX1724, эффективнее будет.

                                      И еще, не вижу причины использовать пьезодатчик вместо электретного микрофона, уменьшатся габариты и, вероятнее всего, увеличится точность.
                                      Фурье — это, конечно, сильно, имхо хватит простейшего фильтра (КИХ или БИХ).
                                      А что, в самом деле так критично было использовать окно отличное от прямоугольника? Прямо не верится, что в таком применении (да еще и на 10 точек фурье!) действительно ощущается разница между прямоугольным окном и Ханнингом.
                                        0
                                        Спасибо большое за подсказку! Крутая микросхема!
                                        Она в несколько раз дороже, но не требует диода Шоттки и, судя по доке, более эффективная.

                                        Насчет окна, так получилось, что я поехал на отдых к родителям с прототипом и программатором, но без возможности менять что-то в железе. Смеркалось, и я решил попробовать наложить окно Ханнинга — чувствительность возросла в разы! Позже я пробовал другие функции окна, но они давали худший результат. Я пересматриваю сейчас свои сгенеренные картинки (минута шума, задувания и шорохов), но они из каких-то других экспериментов с тортиком, так что не могу показать более наглядно.

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое