8 канальный ШИМ на attiny13

AVR attiny13 довольно популярный микроконтроллер (далее МК) и довольно дешевый (20 рублей на ebay). Но есть у него небольшой недостаток: 6 пинов I/O. А на деле вообще 5 (если не шаманить с reset). Если надо большое кол-во пинов на вывод чем может позволить микроконтроллер, то с легкостью используется микросхема регистра сдвига 74HC595, которая потребует 3-х пинов. Потратив 3 пина, можно получить 8*N пинов для вывода(где N — кол-во микросхем 74HC595).

Если со статическим выводом всё ясно, то что делать с ШИМ (PWM)?


Если появилась потребность в большом кол-ве ШИМ каналов, то типовыми решениями будут:
1) Приобретение МК с большим количеством аппаратный ШИМ каналов. Но как не крути это не выход. Или дорого или всё равно количества ШИМ каналов слишком маленькое.
2) Приобретение любого МК с нужным количеством свободных пинов и использование программной реализации ШИМ. Пожалуй самый популярный способ.

Но если ни первое, ни второе решение не подходит, то можно попробовать адаптировать программный ШИМ к работе с 74HC595.
Решение получается довольно бюджетное (цена 74HC595 на ebay около 6.50 рублей за штуку).

Не буду вдаваться в подробности работы ШИМ, а начну сразу с кода (писал в Atmel Studio).

Строить будем 8 канальный 100 Гц ШИМ со 100 уровнями на каждом канале.

1) Для начала нам надо определиться с константами и переменными

// Кол-во каналов ШИМ (поддерживается от 1 до 8)
#define PWM_COUNT	8

// задаем порт с которым будем работать
#define PWM_PORT	PORTB

// задаем пины к которым подключена микросхема 74HC595
#define	PWM_CLOCK	(1 << PINB0)
#define	PWM_DATA	(1 << PINB1)
#define	PWM_LATCH	(1 << PINB2)

// Рабочие переменные
uint8_t	PWM_Step = 0;	// Текущий шаг для ШИМ
uint8_t	PWM_Data = 0;	// Данные для ШИМ
uint8_t	PWM_Mask = 0;	// Маска каналов на которых НЕ установлен ноль. 
uint8_t	PWM_ValU[PWM_COUNT] = {0}; // Используемые значения для ШИМ каналов
uint8_t	PWM_ValN[PWM_COUNT] = {0}; // Новые значения для ШИМ каналов


Что касается рабочих переменных то:
1) PWM_Step — с этим всё ясно. Используется как счетчик для определения момент переключения уровня ШИМ каналов с HIGH в LOW
2) PWM_Mask — используется чтобы при начале отсчета подать HIGH на используемые ШИМ каналы (на которых не выставлено значение = 0)
3) PWM_Data — хранит значение для передачи 74HC595.
4) PWM_ValU — значения для всех каналов ШИМ, которые используются в данный момент при вычислении значения PWM_Data
5) PWM_ValN — новые значения для всех каналов ШИМ. Начинают использоваться при начале каждого периода ШИМ. Требуется это для того, чтобы не было глюков(для светодиодов будут мигания) при уменьшении значения ШИМ. т.к. если напрямую менять значения PWM_ValU, то при уменьшении часто будут появляться ситуации когда целый период будет высокий уровень на канале.

Если с переменными разобрались, то теперь необходимо инициализировать всю работу

__inline void PWM_Init()
{
	DDRB	 |= PWM_CLOCK | PWM_DATA | PWM_LATCH;    // настраиваем пины на вывод
	PWM_PORT &= ~(PWM_CLOCK | PWM_DATA | PWM_LATCH); // все пины переводим в LOW 
	
	TCCR0A	|= (1 << WGM01);// Режим таймера по достижению значения OCR0A
	TCCR0B	= (1 << CS01);	// Делитель частоты таймера = 8
	OCR0A	= 119;		// Значение регистра сравнения
}


Тут комментировать особо нечего. На любой другой AVR код будет в принципе подобный. Только стоит обратить внимание на настройку таймера, которая может быть чуть по другому.
Ключевым моментом тут является сам таймер и расчет его частоты. Для того чтобы с частотой 100 Гц выводить 100 уровней ШИМ, нам потребуется таймер на 10 кГц (100*100). При частоте МК 9,6 МГц и делителе 8 потребуется досчитать до 120.
OCR0A = 119 = (((F_CPU / 8) / 100 Гц) / 100 отсчетов) — 1

После инициализации необходимо установить значения ШИМ для каждого канала.

void PWM_SetVal(uint8_t Index, uint8_t Value)
{
	if (Index < PWM_COUNT)
	{
		PWM_ValN[Index] = Value; // Запишем новые значения для указанного канала.
		
		// Вычисление маски каналов
		if (Value)
		{
			PWM_Mask |= (1 << Index); // Установим бит
		}
		else
		{
			PWM_Mask &= ~(1 << Index); // Сбросим бит
		}
	}
}


Просто запоминаем указанное значение для указанного канала. Отдельно тут хочется пояснить момент вычисления маски каналов.
Каждый период ШИМ начинается с высокого уровня, затем по достижении указанного значения меняется на низкий уровень. Но так как не все каналы могут использоваться или некоторые иметь значения равно нулю, то для этих каналов вообще в начале периоде не надо выставлять высокий уровень. Именно для таких ситуаций и используется маска каналов.

Все вычисления и пересылка данных в регистр сдвига осуществляется в обработчике таймера

ISR (TIM0_COMPB_vect)
{
	uint8_t	x;
	
	// Проверяем каждый канал на необходимость переключения в LOW
	for (x = 0; x < PWM_COUNT; x++)
	{
		if (PWM_Step == PWM_ValU[x])
		{
			PWM_Data &= ~(1 << x);
		}
	}
	
	// Если достигли максимального значения
	if (PWM_Step == 100)
	{
		// Запомним новые значения для ШИМ
		for (x = 0; x < PWM_COUNT; x++)
		{
			PWM_ValU[x] = PWM_ValN[x];
		}
		
		PWM_Step = 0;
		PWM_Data = PWM_Mask; // Установим HIGH на существующих каналах со значениями отличными от нуля.
	}
		
	PWM_Step++;
		
	PWM_SetData(); // Передадим информацию на регистер сдвига
}

Как видно, тут мы каждый такт таймера пробегаемся по массиву значений ШИМ и переключаем нужный в низкий уровень
При достижении счетчика PWM_Step значение 100 (конец ШИМ периода), необходимо взять новые значения для каждого канала выставить высокий уровень на нужных каналах на основе маски.
После все вычислений данные посылаются в регистр сдвига функцией PWM_SetData

void PWM_SetData()
{
	int8_t		x;
	
	PWM_PORT &= ~PWM_LATCH; // Ставим низкий уровень на защелке
		
	for (x = 7; x >= 0; x--)
	{
		// Устанавливаем или сбрасываем бит
		if (PWM_Data & (1 << x)) 
		{
			PWM_PORT |= PWM_DATA;
		}
		else
		{
			PWM_PORT &= ~PWM_DATA;
		}

		// посылаем тактовый сигнал, для проталкивания бита
		PWM_PORT |= PWM_CLOCK;
		PWM_PORT &= ~PWM_CLOCK;
	}

	PWM_PORT |= PWM_LATCH;  // Ставим высокий уровень на защелке
}


В общем всё готово, осталось сделать возможность запуска и остановки таймера

// Разрешаем прерывания от таймера
#define PWM_Start()	TIMSK0 |= (1 << OCIE0B)

// Запрещаем прерывания от таймера
#define PWM_Stop()	TIMSK0 &= ~(1 << OCIE0B)


Ну и в добавок демонстраций использования


int main(void)
{
	int8_t	x;	
	int8_t	i;
	
	PWM_Init();
	PWM_Start();
	sei(); // разрешаем прерывания

	for(;;) // бесконечный цикл
	{
		// последовательно увеличиваем значение на каждом канале от 0 до 100
		for (x = 0; x < 8; x++)
		{
			for (i = 0; i <= 100; i++)
			{
				PWM_SetVal(x, i);	
				_delay_ms(5);
			}
		}

		// последовательно уменьшаем значение на каждом канале от 0 до 100		
		for (x = 0; x < 8; x++)
		{
			for (i = 100; i >= 0; i--)
			{
				PWM_SetVal(x, i);
				_delay_ms(5);
			}
			
		}
	}
}


В итоге получаем прошивку:
Program Memory Usage: 442 bytes 43,2 % Full
Data Memory Usage: 19 bytes 29,7 % Full

т.е. еще больше половины памяти есть для полезной части.

По быстрому на макетке собрал схему для теста.

attiny13 была только в SOP-8 корпусе, по этому для удобства она была припаяна на переходник.


На видео не очень четко видны переходы цветов (не хватает возможностей фотоаппарата + сильно засвечивается). На глаз мерцания отсутствуют.

Кому интересно, прикрепил исходник и hex.
  • +6
  • 22 января 2014, 16:18
  • shevmax
  • 1
Файлы в топике: MultiPWM.zip

Комментарии (42)

RSS свернуть / развернуть
Один stm8s003 за 10 рублей решит вашу проблему, но за старания плюсик :)
0
  • avatar
  • ZiB
  • 22 января 2014, 16:29
По большей части являюсь программистом, по этому был большой интерес к реализации именно с программной точки зрения.
0
Полностью Вас поддерживаю, коллега.
Но как зарядка для головы однозначный плюс.
0
Посмотрел в даташит на stm8s003. Не могу там найти 8 PWM каналов. Можете дать подробности по вашему утверждению?
0
А я вроде и не говорил, что там 8 каналов ШИМ, если я не ошибся речь идет о программном ШИМ и 100 Гц на данном МК реализуется без проблем.
0
Просто статья про 8-ми канальный ШИМ и начал радоваться :)
0
Насколько заметен джиттер из-за того, что между прерыванием от таймера и выдачей данных на регистр находится код с переменным временем исполнения?
Я бы, пожалуй, в прерывании сперва выводил предварительно подготовленное значение в регистр, а затем рассчитывал данные для следующего шага (и, возможно, даже загружал их, чтобы на следующем шаге из загрузки оставалось только дернуть LATCH).
0
  • avatar
  • Vga
  • 22 января 2014, 17:21
При таймере на 10кГц и частоте МК 9,6 МГц погрешность почти незаметная получается. Пытался увеличить частоту до 25 кГц (чтобы иметь 256 уровней с частотой 100 Гц) тут уже начиналась погрешности большие.
0
Несколько замечаний по реализации. 120 тиков таймера при делителе 8 = 960 тактов процессора между прерываниями. Я измерил время работы Вашего прерывания таймера и получилось ~200 и ~520 тактов при оптимизации по скорости и размеру кода соответственно. Получается 20 или 50% времени процессор проводит в прерывании. Немало.
uint8_t x;
   for (x = 0; x < PWM_COUNT; x++)
   {
      if (PWM_Step == PWM_ValU[x])
      {
         PWM_Data &= ~(1 << x);
      }
   }

В AVR есть сдвиг только на один бит. Сдвиг на х бит делается циклом. Лучше сделать сделать отдельную переменную для маски и двигать ее на один бит за цикл:
uint8_t x, mask = 1;
   for (x = 0; x < PWM_COUNT; x++)
   {
      if (PWM_Step == PWM_ValU[x])
      {
         PWM_Data &= ~mask;
         mask <<= 1;
      }
   }

И в других местах также.
PWM_PORT |= PWM_CLOCK;
PWM_PORT &= ~PWM_CLOCK;

Клок может не пройти, особенно если провод относительно длинный. Лучше как-то так:
void PWM_SetData()
{
    int8_t          x;
    uint8_t mask = 1;
    PWM_PORT &= ~PWM_LATCH; 
    for (x = 7; x >= 0; x--)
    {
        if (PWM_Data & mask) 
        {
             PWM_PORT |= PWM_DATA;
        }
        else
        {
             PWM_PORT &= ~PWM_DATA;
        }
        PWM_PORT |= PWM_CLOCK;
        mask <<= 1;
        PWM_PORT &= ~PWM_CLOCK;
    }
    PWM_PORT |= PWM_LATCH; 
}

А вообще есть более экономные способы софт ШИМа, с количеством потребных прерываний от таймера не большим числа каналов.
+1
А вообще есть более экономные способы софт ШИМа, с количеством потребных прерываний от таймера не большим числа каналов.

Можно по подробнее?
0
Находим канал(ы), который долженн изменить свое сотояние первым и вычисляем через какое время это должно произойти. Заводим таймер на это время, в его прерывании переключаем канал и повторяем всё сначала.
Например как тут с сервомашинками: easyelectronics.ru/upravlenie-mnozhestvom-servomashinok.html
0
Спасибо, как-то не догадался так сделать. Надо будет попробовать по такому способу написать.
0
Я тоже делал точно по такому же принципу только на Меге16 по I2C. Все бы ничего только вот у меня получилось что при достижении 40% шим на светодиодах не особо заметно было дальнейшее увеличение (яркость глаза режет). Самый заметный период от 0 до 40%.
0
Это называется «логарифмическое восприятие» яркостей зрением. Недаром в мониторах присутствует такая фишка, как «Гамма 2.2» «Гамма 1.8» и т.д. На самом деле для хорошего восприятия яркостей в на свету и в тенях — 256 одинаково равных градаций недостаточно. Нужно как минимум 10-12-16 бит.
0
Для управления светодиодами я остановился на BAM (yandex). Требует гораздо меньше ресурсов и процессорного времени. Видео не очень (ночью снимал, когда мои спали). www.youtube.com/watch?v=cpaL6t4AFjQ
0
Большое спасибо. Буду знать. И правда способ очень простой и легко реализуемый.
0
Былобы не плохо если бы яндекс по запросу ВАМ давал ссылку на этот ресурс а не на БАМ и всяку херню. Кто нбудь здесь написал бы статю чтоли с подробным кодом — вот реальная тема.
0
Например тут: bsvi.ru/bam-alternativa-shimu/
Есть код на асме. Но не комментил, щас глянул, блин разбираться, вспоминать нужно.
0
Спасибо это уже читал. Я как то попытался педставит код на асме и меня это прям даже немного напугало если делать на 8 каналов. Там походу портянка еще та получается (на КАвтомате)- или я просто как то не так понимаю суть подхода к алгоритму.
0
У меня 16 каналов (ссылка на видео чуть выше).
Да ну, портянка. Благо, и учителя неплохие были… Мда-с… Да и сам старался писать более-менее нормально.
КА. Никак иначе.
0
Былобы неплохо понимать особенности поисковиков и искать, например, «BAM PWM» или сходу предлагаемое гуглём «BAM ШИМ»
0
На контроллере за 0,7$ (psoc1) я реализую 4 аппаратных ШИМ на макс. частоте 187кГц, а на контроллере за 2,5$(psoc3) до 44 ШИМ на ещё больших частотах… И остается еще тьма ресурсов, без загрузки процессора вообще…
0
Ааааа верните моё потраченое время на avr. Ведь есть psoc.
Мне интресно сколько этих мк понадобится чтобы собрать Пенек4 и сколько это будет стоить, наверно дороже чем доже новый Пенёк. Я это к тому что здесь стоит тег avr и нет никакой необходимости обсуждать варианты дешевле на других мк. Человек пишет про решение на аврах если Вы можете предложить еще проще и дешевле на аврах то здесь это приемлемо для обсуждения, а так боюсь это здесь никого не всколыхнет.
Вобщем зря Вы это написали и особенно троеточия (типа задумайтесь) А для psoc наверно есть своя ветка.
+1
Если чесно, я вообще не заметил этого тага avr, и сейчас не вижу-а зашел именно в статью по программным ШИМ. Atmel ведь не спонсор рубрики правильно?… Тогда лучше упомянуть и другие варианты реализации для полноты картины, их плюсы и минусы…
0
Не волнуйтесь, можно еще на FPGA сделать пол-сотни канаклов с базовой частотой, скажем 400Мгц… Ну и абсолютно синхронных, естественно :-P
0
Можно, но сложно и дорого)…
0
Можно в $5 уложиться, если программить FPGA c МК. ($3.5 EP1C3T100 + $1.5 STM32F051C8) Сразу предупреждаю — цены в Китае, у нас в 2-3 раза больше сдерут.
0
в Китае всё можно, там детали практически не стоят денег… белые боковые светодиоды на 2500мкд Нича я брал по 90 центов, а потом нашел китайские не хуже по 5!.. Конденсаторы ТДК тоже самое, там государство доплачивает экспортерам…
0
Я с ними общался на эту тему. Насчет доплат они ничего не говорили, но:
1. У них нет таможенных сборов на компоненты, которые не производятся в Китае. Те FPGA китайских нет — пошлина=0.
2. Крупные производители деталек предлагают в Китае цену, как для крупного опта, даже для опытных партий. Грубо говоря, ты делаешь 10 «пробничков», а комплектуху сразу получаешь по цене, как для 5000-10000 шт. Красота!
0
У них доплату получают через курс валюты, то есть долары государство забирает себе чтобы скупать за них активы других стран-а экспортерам дают внутренние деньги по очень высокому курсу… как-то так…
0
Что за psoc? Ссылку можно?
0
ищите по словам Cypress PSOC — www.cypress.com/psoc/
0
Что-то мне подсказывает, что 44 аппаратных ШИМ не получится. Сколько там логических вентилеей (LE) у PSOC за $2.5? Вы проектик набросайте, и посмотрите на сколько ШИМ ресурсов хватит?
0
Там 20UDB, на каждом 2 ШИМ, и того 40 + там 4 таймера, каждый таймер можно включить в режиме ШИМ
0
Ну, может… Никогда с этой штукой не имел дела. Так сделайте проектик и статейку, думаю многим интересно будет.
0
Сделал уже не один, но мне за них деньги платят на работе и не будут рады если выложу здесь )
0
Таки учебный/обучающий простой проект != рабочему с производства =)
Задача обучающего проекта — показать принцип и идею, может, подход, а не дать готовый код. Естественно, написание производится отдельно в иной манере.
0
Я так понимаю это цена для крупного опта.
0
Тысяча чипов это крупный опт?.. На розницу цена пусть вдвое выше..1,5дол это дорого?
0
быстрый поиск дал мне 5 долларов для psoc1, видно не там искал.
0
cy8c21123
0
да, так намного лучше, спасибо.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.