Итак, у нас есть микроконтроллер и нам нужно сгенерировать синусоиду. Какие тут могут быть варианты? Ну, самое простое и очевидное решение — использовать встроенный ЦАП. Ага, очень круто, но что если контроллер мы взяли самый дешёвый и ЦАПа в нём просто нет? В этом случае есть два варианта: либо покупать отдельную микросхему ЦАП, либо делать ЦАП самому. Понятно, что купить проще, но по цене такая микросхема вполне может сравниться с контроллером (а может даже окажется дороже). Тогда в чём смысл брать дешёвый контроллер? Нет, так нам денег не сэкономить…
Так, а что там со вторым вариантом? Естественно, в первую очередь на ум приходит бессмертная классика — матрица R-2R. Вроде бы одни резисторы нужны, куда уж проще и дешевле? Ок, давайте рассмотрим этот вариант повнимательнее. Что он нам даёт?
Ну, во-первых, у нас будут использоваться резисторы только двух номиналов. Это отлично. Плюс мы можем получить равномерные (одинаковые) шаги по оси напряжения. Вот тут давайте подумаем, такой ли уж это плюс и нельзя ли тут что-то ещё улучшить.
Как правило, при генерировании синусоиды и для человека, и для контроллера гораздо удобнее шагать равномерно по оси времени, а не по оси напряжения. Ниже для примера показаны две синусоиды, которые я сгенерировал в офисной программе.
У верхней синусоиды по оси T шаги одинаковые, а по оси U — нет. Для построения я просто задал шаг по оси T, равный Пи/6 и вычислил значение синусоиды в промежутке времени от 0 до 2*Пи с этим шагом. У нижней синусоиды наоборот, одинаковые шаги по оси U, но при этом шаги по оси T разные. Здесь мне для построения синусоиды пришлось вычислять арксинусы, но проблем с этим тоже нет (офисная программа рулит). В принципе для человека всё удобство — это дело привычки, реальной разницы нет.
А вот для контроллера разница самая что ни на есть настоящая. Для него ось времени — это не какая-то абстракция, а объективная реальность. В контроллерах есть специальные таймеры, которые отсчитывают заданные промежутки времени, через которые контроллер выполняет различные переключения своих выводов. Причём равные промежутки времени таймеру в контроллере отсчитывать гораздо проще, поскольку в противном случае его приходилось бы каждый раз перенастраивать, что влекло бы дополнительные затраты времени и увеличивало размер кода.
Ладно, а почему мы не можем на той же матрице R-2R генерировать нужные выходные значения, но через равные промежутки времени? Можем. Но, в этом случае нам нужно будет шаг нашей матрицы сделать избыточно маленьким. Если посмотреть на верхнюю синусоиду, то видно, что шаг по оси U между точками в верхней части синусоиды гораздо меньше, чем в нижней. Очевидно, что для получения нормальной синусоиды шаг матрицы придётся взять хотя бы равным этому минимальному значению. Но в этом случае часть резисторов, определяющих низкие выходные значения просто не будет использоваться, так как в нижней части синусоиды при равных временных интервалах нам не нужно будет столько точек.
Для того, чтобы решить сразу все описанные проблемы, можно просто подобрать номиналы резисторов таким образом, чтобы при переключениях они давали нам только точки, лежащие на синусоиде. Да, нам придётся отказаться от матрицы R-2R и номиналы резисторов станут разные, но зато мы сэкономим место на плате и выводы контроллера, увеличим скорость и уменьшим размер программы, а также повысим точность генерации нашей синусоиды. По-моему, вполне приемлемая цена.
Ок, давайте попробуем прикинуть, сколько и каких нам понадобится резисторов. Будем считать, что мы хотим генерировать синусоиду по 12 точкам за период (так же, как и на картинке выше). Возможности IO нашего контролера предположим стандартные: выходы push-pull + z-состояние. Так же предположим, что синусоида у нас должна целиком лежать в области от 0 до U, ну то есть колебаться относительно уровня U/2 (у нашего контроллера однополярное питание и синусоида нам нужна размахом как раз целиком с это питание). Для этого показанную выше синусоиду нам придётся сжать в два раза и сдвинуть вверх на 1/2 (смотрим рисунок ниже). Для удобства на вертикальной оси отложено не напряжение, а относительное напряжение (k = U/Umax), что позволяет не учитывать какая там на самом деле у синусоиды амплитуда. Наша синусоида всегда будет колебаться между нулём и единицей.
Так, ну для того, чтобы получить половину питания никакие ноги контроллера вообще не нужны, нужен только резистивный делитель с двумя одинаковыми резисторами (R), то есть все ноги контроллера должны быть переключены в z-состояние. Тут всё просто.
Следующая точка k=0,75. Понятно, что тут нужно перетянуть уровень выше середины, то есть нужно подключить резистор паралельно верхнему плечу делителя, уменьшив тем самым эквивалентное сопротивление верхнего плеча. Далее, для получения точки k=0,933 подключим в верхнее плечо ещё один резистор параллельно первым двум. И, наконец, для получения точки k=1 нужно подключить в верхнее плечо резистор с нулевым сопротивлением. Можно написать общую формулу для вычисления эквивалентного сопротивления верхнего плеча в каждой точке синусоиды, лежащей выше оси k=0.5:
Rэ = R * (1-k)/k
то есть вот таким оно должно быть, чтобы получились нужные нам точки.
Исходя из этого можно написать общую рекурсивную формулу для определения добавочного сопротивления на основе предыдущего эквивалентного сопротивления плеча (Rэп) и того эквивалентного сопротивления, которое мы хотим получить (Rэн):
Rд = Rэн * Rэп / (Rэп — Rэн)
Если по этим формулам посчитать эквивалентные сопротивления верхнего плеча для точек ниже оси u=0.5, то значения окажутся больше изначального сопротивления верхнего плеча, чего мы, естественно, не можем добиться параллельным добавлением в это плечо дополнительных сопротивлений. Но это и не нужно. Значения нашей синусоиды симметричны относительно оси u=0.5, соответственно, получить их можно просто добавляя точно такие же добавочные сопротивления не в верхнее, а в нижнее плечо делителя. Более того, это будут те же самые сопротивления, а определять в какое из плеч они подключены мы будем переключая соответствующие выходы контроллера между питанием и землёй.
Самое приятное в этой схеме то, что для каждого вывода контроллера все три показанные на схеме ключа уже имеются внутри (помните, мы договорились, что выходы у нас push-pull с возможностью переключения в z-состояние?). То есть для 12 точек синусоиды нам понадобится всего 5 внешних резисторов.
Для получения точек синусоиды показанной на рисунке 3, нужно переключать показанные на рисунке 4 выводы контроллера по следующему алгоритму:
номер точки | Состояние I/O-1 | Состояние I/O-2 | Состояние I/O-3 |
1 | Z | Z | Z |
2 | Vcc | Z | Z |
3 | Vcc | Vcc | Z |
4 | Vcc | Vcc | Vcc |
5 | Vcc | Vcc | Z |
6 | Vcc | Z | Z |
7 | Z | Z | Z |
8 | Vss | Z | Z |
9 | Vss | Vss | Z |
10 | Vss | Vss | Vss |
11 | Vss | Vss | Z |
12 | Vss | Z | Z |
Так, ладно, но ведь сгенерированная в итоге синусоида будет иметь ступеньки! Да, конечно будет, но она бы после любого цифрового генератора была ступеньками и нам бы в любом случае понадобился на выходе генератора сглаживающий фильтр.
В простейшем случае можно взять ФНЧ на базе RC-цепочки с частотой среза чуть выше частоты нашего генератора. Причём для такого фильтра можно даже руками подобрать номиналы элементов. Очень грубо это выглядит так:
- ставим переменный резистор или переменный конденсатор (или и то и другое)
- выкручиваем переменники на минимум, чтобы получить максимальную частоту среза
- начинаем плавно увеличивать значения сопротивления и ёмкости
- как только начнёт уменьшаться размах колебаний, значит частота среза оказалась ниже требуемой, — немного откручиваем назад и так оставляем.
Пример программы для контроллера приводить не буду, поскольку это не статья о программировании, а вот пример сгенерированной таким способом синусоиды пожалуй приведу (только у синусоиды в примере шагов чуть побольше, но смысл тот же).