Выжимаем ATtiny10, практическое применение

Это ещё один маленький домашний DIY (апгрейд гирлянды) на, практически, самом младшем из младших микроконтроллеров из линейки ATtiny — на ATtiny10. Эти МК уже прошли пик своей популярности, лучшие характеристики можно найти за те же деньги в других МК… да что там характеристики, уже ядра в штуках раздают! (ESP32, например).

Но именно этот МК мне нравится сочетанием функциональности (64-битный таймер, АЦП, ШИМ, широкий диапазон питания), удобства разработки и, главное, размера! На хабре есть статьи о проектах на этом МК, последнее, что я видел — это вольтметр, ссылка в конце статьи. В общем для мелкого DIY самое то. Смотрите:

  • Это все находится корпусе SOT-23–6 размером 3×2.7×1 мм. Буквально со спичечную головку. Но при этом паять можно без ухищрений.

  • Производительность — может лупить на 12МГц (если, конечно, придумаете что-то полезное на килобайт прошивки).

  • Для запуска не нужен никакой обвес, кроме напряжения от 1.8В — это любая батарейка/две с напряжением в 3В. Внутренний тактовый генератор присутствует, последовательности сброса при запуске не нужны.

  • Таймер, 2 канала ШИМ, 4 АЦП, аналоговый компаратор, вочдог, разные режимы энергопотребления — всё это позволяет решать широкий спектр задач.

  • Простая разработка без ардуинов: поддержка всех возможностей открытыми решениями — библиотека avr-libc, сборка avr-gcc, программирование avrdude и сторублёвый программатор usbasp, среда разработки VSCode c плагином.

  • Цена — 110р в ЧипиДипе, в других местах можно за 60р при покупке больше десятка.

Мне даже кажется я где-то видел статью о запуске TinyML на этом МК… шутка)

В общем лежали эти «тиньки» у меня в коробке и однажды под новый год в квартире появилась эрзац-ёлка — несколько еловых лап в вазе, обмотанных простой одноцветной гирляндой из 100 белых smd-диодов и отсеком на 3 АА батарейки.

4d752b83745f373011997049a8866c8f.jpg

И не то, чтобы это сразу требовало доработки, смотрелось довольно красиво. Но эти 100 мелких светящихся засранцев наваливались на батарейки так, что потребление было около 1А. Сколько работал первый набор батареек я не засёк, перешёл на аккумуляторы, но и их выносило за 2–3 часа. Они, блин, даже заряжаться не успевали! Это всё безобразие, естественно, пробудило Дух DIY и дальше я уже себе не подчинялся.

Улучшить эту гирлянду решил по примеру эффектов другой, более продвинутой с двумя линиями диодов (включенных последовательно, но встречно и это позволяло меняя полярность сигнала включать их попеременно и делать эффект типа бегущих огней… типовой, очевидно, приём, но когда я это увидел в первый раз мне очень понравилось решение).

Hardware

47d7cc205b043adcc5bcf4e77bdf27db.jpg

Итак, гирлянда состоит из двух проводов в лаке и 100 диодов висящих на них параллельно, ограничения тока нет, видимо расчёт на то, что на 100 штук диодов (плюс их прямое сопротивление в открытом состоянии) батарейка не отдаст больше чем может и будет это как раз 10–20 мА. Для создания разных эффектов «тинька» должна коммутировать ток около ампера и для этого подошёл полевичок FDN337N также в корпусе SOT-23. Тоже очень удобный для всяких мелких батареечных поделок транзистор c достаточным током сток-исток, низким порогом открытия, низким сопротивлением канала и высоким напряжением сток-исток и стоимостью 6р в ЧипиДипе. Смотрите:

FDN337N

FDN337N

FDN337N

FDN337N

Схема этого апгрейда получается простая. Микроконтроллер, транзистор, затворный резистор, конденсатор на питание и ещё один резистор для того, чтобы понизить ток гирлянды. Подбирал его опытным путём так, чтобы не эстетика не пострадала. 10 Ом получилось.
Также добавил гребёнку для программирования и джампер. Дело в том, что выход ШИМ-каналов так или иначе совпадает с пинами для программирования и цепи схемы могут мешать при программировании. Не знаю как там помешала цепь, которая упирается через резистор в базу полевика, ёмкость маленькая, ток никуда не течёт… кто знает почему — расскажите. А я просто поставил джампер который снимается когда подключаем программатор. Хедеров J2 и J3 на самом деле не будет, они нарисованы ради площадок, такое вот костыльное решение придумано в EasyEDA.

Рисовал в EasyEda

Рисовал в EasyEda

Однослойную лутовую плату размером 12×14 мм сделал под размеры свободного пространства в батарейной коробке.

Рисовал в EasyEDA. Прошу прощения за прямой угол дорожки к GND U1. Электроны в этом месте по инерции выскакивают наружу и с этим приходится мириться.

Рисовал в EasyEDA. Прошу прощения за прямой угол дорожки к GND U1. Электроны в этом месте по инерции выскакивают наружу и с этим приходится мириться.

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

Вот такой результат получился (немного отличается, как я выше написал). Ещё один момент — для принтера не стояли родные драйверы и качество выше 300 dpi не поднималось. Из-за этого у некоторых дорожек видна пила по краю.

d1024f98bbdf4baaf234f51969090d51.jpgad959a8342d6cc894604d11bbeb8ddbe.jpg

Software

Теперь возвращаемся на родную территорию — программирование)

Прогаем в VSCode с плагином AVR Helper. Плагин добавляет несколько удобных действий типа build и flash с правильными вызовами avr-gcc и avrdude для данного МК. Установку/настройку не описываю, потому что если просто поставить VSCode, плагин и выполнить действия из его инструкции, то сразу всё получится.

Наш проект состоит из одного файла — main.cpp. Ссылка на код на гитхабе ниже, кому понадобится — используйте.

Далее быстренько поясню код. Тут код немного перетасован для удобства, оригинал смотрите в репозитории:

Зависимости только от avr-libc. Все функции/константы/макросы не объявленные явно в коде, объявлены в этих хэдерах:

#include 
#include 
#include 
#include 

Эффекты описаны декларативно, интерпретатор будет дальше. Одна строка — один эффект.

  • Начальное состояние первого эффекта — выключено.

  • Ноль — конец последовательности.

  • Если число положительное, то это переход во включенное состояние, иначе в выключенное.

  • Если число меньше 128, то это мгновенный переход в состояние, иначе плавный через ШИМ в течение n миллисекунд.

  • Если знак очередного числа равен предыдущему, то это просто задержка.

Пример: 2000, 1000, -1, -500, 1, 500, -1, -500, 1, 500, -1, -500, 1, 1000, -2000

плавно в течение 2 секунд включили, подождали секунду, мигнули 3 раза (по 500 мс), подождали секунду, плавно в течение 2 секунд выключили.

Нужно помнить каким состоянием закончился предыдущий эффект, чтобы следующий начался бесшовно. Например, я каждый закончил выключением.

const int * const patterns[] = {
    (const int16_t[]){500, -500, 500, -500, 1, 50, -1, -50, 1, 50, -1, -50, 1, 50, -3000, -2000, 0},
    (const int16_t[]){1, 500, -1, -500, 0},
    (const int16_t[]){4000, -1, -2000, 0},
    (const int16_t[]){1, 250, -1, -250, 0},
    (const int16_t[]){500, -500, -3000, 0},
    (const int16_t[]){1, 50, -1, -50, 0},
    (const int16_t[]){1, -4000, -2000, 0},
    (const int16_t[]){2000, -1, -100, 1, 100, -1, -100, 1, 100, -1, -100, 1, 100, -1, -100, 1, -2000, -3000, 0},
    (const int16_t[]){1000, -1000, 0}
};

Код инициализации таймера и ШИМ. Привожу его сразу полностью, потому что остальной код зависит от него: тактирование выставляем от внутреннего генератора 128КГц, ограничиваем счётчик таймера значением 128, разрешаем прерывания по таймеру и получаем вызов нашего обработчика один раз в миллисекунду. Теперь удобно отсчитывать задержки.

static void setup()
{
    // Уменьшение энергопотребления для режима Idle
    // Отключение ADC
    power_adc_disable();

    // Отключение analog comparator
    ACSR ^= ~_BV(ACIE);
    ACSR |= ACD;

    // Установка тактирования на Internal 128 kHz Oscillator
    CCP = 0xD8;
    CLKMSR = 0b01;
    CCP = 0xD8;
    CLKPSR = 0;

    // Настройка ШИМ на порту PB0
    DDRB |= _BV(DDB0);

    cli();

    TCCR0A = 0;
    TCCR0B = 0;

    // Режим - FastPWM 8 bit, вывод на OC0A, частота - system clock, TOP=128
    TCCR0A = _BV(COM0A1) | _BV(WGM01);
    TCCR0B = _BV(CS00) | _BV(WGM02) | _BV(WGM03);
    ICR0 = 128;
    OCR0A = 0;

    sei();

    // Переполнение таймера также отсчитывает системные тики (1мс)
    TIMSK0 |= _BV(TOIE0);
}

Код для отсчёта времени и организации задержек не потребляющих ресурсы: функция, реализующая задержку засыпает, просыпается каждую миллисекунду (потому что прерывание выводит проц из сна), проверяет нужно ли ещё спать и всё повторяет. Мне было важно это реализовать, потому что весь сыр-бор начался как раз из-за очень короткого времени работы гирлянды от одного заряда аккумуляторов/комплекта батареек. Не было бы смысл всё это начинать если не приложить максимальные усилия.

Кстати, можете запомнить приём и утащить к себе. Должен работать в любом однопоточном коде с любыми другими прерываниями. Главное адаптировать под частоту вашего генератора.

// Счётчик системных тиков
volatile uint16_t ticks = 0;

// Обработчик прерывания
ISR(TIM0_OVF_vect)
{
    ++ticks;
}

// Задержка с использованием sleep mode. Каждое переполнение таймера будит цикл.
void sleep(const uint16_t & delay)
{
    uint16_t end = ticks + delay;

    do
    {
        set_sleep_mode(SLEEP_MODE_IDLE);  
        sleep_enable();
        sleep_cpu(); 
        sleep_disable();
    } 
    while (end > ticks);
}

Основной цикл. Места во флэше вроде хватало, поэтому решил реализовать линейную яркость. То есть компенсировать нелинейное восприятие зрением различных значений яркости. Без этого казалось, что при плавном включении светодиоды слишком быстро проходят низкие значения яркости и долго зависают в ярком состоянии, где изменение между соседними уровнями практически не видно на глаз. Для этого просто чуть быстрее пробегаем промежуток где диод уже слишком ярок.

Итак, перебираем все эффекты (в коде — «паттерны») в бесконечном цикле, каждый эффект крутим 30 секунд. Реализация эффекта резкого включения/выключения простая — сразу втыкаем минимальный или максимальный период ШИМ. На минимальном уровне светодиоды еле светятся и не гаснут полностью. В темноте это даже прикольно.

// Каждый паттерн проигрывается 30 сек
const uint16_t PATTERN_DURATION = 30000;

// В массиве находятся точки ускорения/замедления, после которых шаг приращения периода увеличивается/уменьшается на 1.
// Первый и последний элементы - ограничительные.
const uint8_t accelerationPoints[] = {0, 30, 60, 80, 100, 255};

int main()
{
    setup();

    // Текущее состояние, true = включено, false = выключено
    bool state = false;

    while (true)
    {   
        // Перебираем все паттерны по очереди
        for (uint8_t i = 0; i < sizeof(patterns)/sizeof(patterns[0]); i++)
        {
            // Сбрасываем счётчик системных тиков, которого хватает на 65 сек (1кГц*16бит) чтобы не попасть на переполнение
            ticks = 0; 

            // Каждый паттерн крутим в течение PATTERN_DURATION
            do 
            {
                // Перебираем все ноты
                for (uint8_t j = 0; patterns[i][j] != 0; j++)
                {
                    int16_t delay = patterns[i][j];
                    bool sign = true;

                    // Сохраняем знак и оставляем модуль задержки
                    if (delay < 0)
                    {
                        delay = -delay;
                        sign = false;
                    } 

                    // Если текущее состояние не изменяется, то просто спим
                    if (state == sign)
                    {   
                        sleep(delay);
                    }
                    // Иначе переключаем состояние
                    else
                    {
                        // Быстро
                        if (delay < 128)
                        {
                            setPwmDutyCycle(sign ? 127 : 0);
                        }

Для плавного включения/выключения нам нужно перебрать все (на самом деле не все) значения периода ШИМ от минимального до максимального (или наоборот) и немного поспать на каждом значении. То есть от 0 до 128. Это в теории.

А на практике используется всего 64 уровня яркости. И вот почему: если делать нелинейную яркость, то надо как-то укоротить более яркие состояния… или сделать таких состояний меньше. Использую второй способ. Задержки на каждом шаге всегда одинаковые, но когда мы двигаемся по «более ярким» значениям периода ШИМ, то начинаем пропускать некоторые уровни.

В массиве accelerationPoints как раз обозначены значения после которых шаг начинает ускоряться. Именно поэтому несмотря на то, что цикл идёт по 128 периодам ШИМ, шагов на самом деле меньше (30 шагов от нуля до 30, 15 шагов от 30 до 60, 7 шагов от 60 до 80, 6 шагов от 80 до 105, 3 шага от 105 до 128… примерно:)) В коде выбрано число 64 для количества шагов, это оптимизации кода, чтобы деление выполнялось сдвигом (оптимизация по размеру — отдельная песня, во время которой я порвал баян).

                        // Или плавно
                        else
                        {
                            // Используется 64 шага для плавного включения/выключения с помощью ШИМ. 
                            // Так как задержки не делятся нацело на 64, сохраняем остаток от деления для поправки задержек
                            uint8_t remainder = delay % 64;

                            // Шаг приращения периода ШИМ меняется из-за нелинейной зависимости яркости от заполения
                            uint8_t step;

                            if (sign)
                            {
                                // Если включаемся, то начинаем делать это медленно
                                step = 1;
                            }
                            else
                            {
                                // Если выключаемся, то начинаем делать это быстро. 
                                // Максимальный шаг равен кол-ву точек минут ограничительные элементы 
                                step = sizeof(accelerationPoints) / sizeof(accelerationPoints[0]) - 2;
                            }

                            // Проходим 128 ступеней ШИМ с задержкой delay/64 на каждой итерации и прыгая через step ступеней
                            for (uint8_t k = 1; k < 128; k += step)
                            {
                                uint8_t value;

                                if (sign)
                                {
                                    value = k - 1;

                                    // Реагируем на точки ускорения
                                    if (value >= accelerationPoints[step])
                                    {
                                        ++step;
                                    }
                                }
                                else
                                {
                                    value = 128 - k;

                                    // Реагируем на точки замедления
                                    if (value <= accelerationPoints[step - 1])
                                    {
                                        --step;
                                    }
                                }

                                // Устанавливаем период ШИМ для данной итерации
                                setPwmDutyCycle(value);

При расчёте задержек есть фишка: в этом МК нам доступны только целочисленные арифметические операции и поэтому легко набегают ошибки. Для задержек меньше 64 мс простым делением на 64 мы получим 0 и всё сломается. И даже если задержка больше, чем 64, то всё равно можно получить значительное ускорение если отбрасывать остаток от деления. Например, при задержке в 125 мс, при потере остатка от деления (125/64=1,953125) мы потеряем 61 миллисекунду. Короче говоря, задержки станут кратны 64. Поэтому сохраняем остаток от деления (он получается в миллисекундах) и потихоньку размазываем его по всем шагам.

Мне кажется, красивое решение.

                                // Учитываем "потерянное" на целочисленном делении время сна
                                if (remainder)
                                {
                                    --remainder;
                                    sleep((delay / 64) + 1);                                
                                }
                                else
                                {
                                    sleep(delay / 64);                                
                                }
                            }
                        }

                        state = sign;
                    }
                }
            } while (ticks < PATTERN_DURATION);
        }
    }
}

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

Но теперь, всё оптимизировано и отлажено, собираем кнопкой Build в VSCode:


 *  Executing task: AVR Helper: 
    
            

© Habrahabr.ru