Приветствую Хабр, и всех его многочисленных обитателей!
Сразу оговорюсь, что то о чём здесь пойдет речь не очень рассчитано на новичков, тем не менее если есть интерес и тяга к изучению, напротив — прошу познать.
А в этот раз, речь пойдет о реализации гибридного ШИМ, которых наплодилось в сети уже множество. Так что думаю еще один, или два, или три (бонусом), будут не лишними или не будут лишними.
Итак, ну во-первых почему гибридный: прежде всего, потому что не аппаратный на 100%, но и не софтварный тоже на 100%.
Что это бы значило: для реализации используется аппаратный счетчик, но вывод формируется не в один канал порта ввода/вывода, а сразу в три, конфигурационной маской, которая маскирует каналы порта в прерывании по таймеру.
В чем отличие от аппаратного? Да все просто — в числе каналов для микроконтроллера Attiny13a.
Теперь подробней.
Кому и зачем это нужно?! Ну основная область применения таких задач, это управление двигателями — асинхронными, биполярными и униполярными — шаговыми, контроллеры световых и звуковых эффектов, ультразвуковые устройства (ультразвуковые ванны, гравёры, смесители и т.п.), и т.д. Конечно для таких целей созданы специализированные контроллеры, но зачем искать, тратиться, и копать ДШ, если есть под рукой маленький питомец.
Я остановился на первом вопросе, когда интерес к нему появился внезапно, при прочтении одного из форумов, где один разработчик (назовем его «Опечаленный»), был опечален тем фактом, что микроконтроллер Attiny13a, не имея достаточного количества аппаратных каналов ШИМ, не может похвастать необходимой производительностью при программной реализации задуманного.
«Опечаленный» испробовал множество вариантов, менял константы, оптимизировал код на Си, даже задействовал прерывания, но всё тщетно. А поднятая им тема на форуме так и протухла, в каком-то лохматом году, оставшись неразрешенной и безответной.
Меня заинтересовало, насколько же медленный программный режим ШИМ у упомянутого микроконтроллера, который работает на 9.6 МГц. И я, набросав маленький исходник, используя по-возможности все аспекты расписанные «Опечаленным» и воплотив их, неспешно получил на трёх лапах Тиника 13-го программный ШИМ, на частоте… около 4 кГц, при программной эмуляции в Proteus.
Мало… Слишком мало! И как то это все странно… но безумно интересно, потому что если не победить такую простую задачу, то это действительно — одно из первых, маленьких разочарований, и прямая дорога в сторону более производительных и навороченных чипов, которые и стоят соответственно. С другой стороны это скорее плюс, но все же будем разбираться.
И тут мне подумалось: «А почему, собственно, синхронизация строится на переполнениях таймера, когда задача реализуется через сравнения?!»
И правда, ведь в режиме переполнения таймер просто обязан вызывать прерывание лишь однажды, при переполнении, пройдя полный цикл. Слегка модифицировав исходник получил около 40 кГц. «Уже лучше, но все равно — тут что-то не так!»
Вобщем не буду больше утомлять: все переменные были объявлены в регистрах, код был оптимизирован по производительности,
и размеру (практически весь исчез), а при дальнейших играх были полученны еще два режима с относительными смещениями импульсов.
А дальше последовали новые оптимизации в результате которых, самый медленный из режимов (6 — фаз) был переписан на инлайновый ассемблер. И сделано это было случайно, когда я обратил внимание на полученный после компиляции код, который мягко просто сказать — сильно меня удивил! Даже при объявлении переменных в регистрах, все операции компилятор решил делать через временные пары, соответственно код раздулся почти вдвое, и это при включенной оптимизации по размеру! Несколько лишних движений и выигрыш в 40 байт, от первого варианта, и почти 10 кГц прироста в частоте.
В результате получилось три режима:
1. Импульсы со смещением в фазу, максимальная доступная частота 75.500 кГц при CLK=9.6 МГц (При CLK=20 МГц, частота приближена к 160 кГц ), скважность ШИМ=33.3% период — три фазы:
2. Импульсы смещены на треть, максимальная частота 37.400 кГц (75 кГц при CLK=20 МГц), скважность=50%, период — шесть фаз (без использования ассемблера не удавалось преодолеть предел в 28 кГц, подробней — см. исходный код):
3. Импульсы смещены на половину, максимальная частота как в первом режиме, скважность=66.6%, период — три фазы:
Под частотой здесь подразумевается полное следование импульсов по трем каналам до повторения в периоде (либо период одного импульса). Да, должен отметить, что изменение скважности в рамки данной задачи не входило, хотя и это очень просто сделать, используя второй регистр сравнения таймера (правда это справедливо только для первого режима). При этом изменение частоты реализовано, и выполнено через изменение ширины фазы (подробнее в тексте исходного кода).
Зачем столько режимов? Это еще не все, и все их множество обусловлено особенностями тех или иных устройств.
Так, например асинхронные двигатели управляются первым режимом, а униполярные-шаговики — вторым (впрочем и каналов им необходимо или меньше, или больше, но при наличии примера, расширить или уменьшить число каналов — пустяк).
Для получения меньшей частоты программно, необходимо изменять регистр делителя таймера.
Если и этого слишком много (например для шаговых двигателей), необходимо опустить частоту микроконтроллера.
Так при частоте чипа 128 кГц, с выставленным делителем CLK, без делителя таймера и минимальной ширине импульса (максимальной частоте), при использовании второго режима, частота импульсов 62 Гц.
Если же говорить об управлении асинхронным двигателем, то при использовании двигателя с 12-ю полями, получаем максимальную частоту следования импульсов при использовании первого режима около 20 кГц (~78/4), что несомненно так же много, ввиду отсутствия двигателей способных вращаться на таких оборотах (в секунду). :o)
Конечно в расчетах выше не отражены накладные расходы, которых требует полное управление, такие как чтение с датчиков Холла или обратной EMF, корректировки длительности фаз и тп. Но с таким запасом в частотах, эти задачи решаются легко, прямо в основном цикле программы.
Отдельно прошу прощения у гуру, которых нервируют условия в одну строку, но здесь это сделано сознательно для удобства быстрого комментирования. Вторая, и более прозаичная причина: я терпеть ненавижу — /*комментарии со звездочками*/, хотя при переходе к инлайну — иного не дано.
Если Вы намереваетесь использовать мой код в своем проекте, когда он станет раздуваться и обрастать Вашими обработчиками, необходимо вооружить все объявления использованных переменных в регистрах ключевым словом «volatile», в результате чего компилятор огрызнётся, что он плевать хотел на Ваши запросы, и что в случае, если ему вздумается, то он задействует эти регистры на свое усмотрение (в случае нехватки таковых).
Более подробную информацию я постарался как можно полнее отразить в комментариях, стараясь при этом быть максимально кратким при кодировании:
С удовольствием отвечу на Ваши вопросы, и помогу с изменением конфигураций сигналов, если конечно это потребуется.
При всем выше-сказанном, хочу обозначить: я не позиционирую себя как супер-оптимизатора, супер-программера и супер-электронщика.
Все сказанное я написал для того, чтобы новички верили, что непобедимых задач не бывает, а профессионалы обратили внимание на чипы, которые рано списывать со счетов, и во множестве классов задач им можно найти достойное применение.
PS: Для новичков: ИМХО, проект не достаточно интересен для начинающих, ввиду использования высоких частот.
Однако контроллер прошитый прошивкой с раскоментированной строкой в основном цикле для изменения ширины фаз,
на частоте 4.8 МГц с включенным делителем частоты (4.8/8) (вот только не помню до оптимизации кода или после),
и с подключенными светодиодами на линиях PB0, PB1, PB2, забавно выглядит в роли настольной мигалки.
Хотя некоторые, особо нервные индивиды утверждают, что «раздражает эта фигня — до
PPS: Для НЕ новичков: в дополнение хочется сказать об оптимизации, и использовании (обойдемся без холивара друзья!) подобного кода:
byte A=0, B=1, C=2;
if (A==0 || B==1 || C==2) doSomething(); / if (A==0 && B==1 && C==2) doSomething();
И «неправильное» использование вида:
if (A==0 | B==1 | C==2) doSomething(); / if (A==0 & B==1 & C==2) doSomething();
Результат будет один и тот же, а вот выполняться второй пример будет ЗНАЧИТЕЛЬНО дольше по времени, и ЗНАЧИТЕЛЬНО раздует код.
А почему?! А я не скажу, Вы сами угадайте! :)
На этой ноте предлагаю остановиться, и ознакомиться с прилагаемыми материалами:
проект для Proteus 8.1 и исходный код созданный в Atmel Studio 6.2
Спасибо за внимание, до новых встреч!
Использование в коммерческих проектах, перепродажа исходного кода и использование с целью наживы, запрещено.
Исходные тексты распространяются бесплатно, в случае использования на других сайтах,
либо в других источниках, указание автора и уведомление о размещении — обязательно.
Автор: DolphinSoft