+1 голос
Здравствуйте! У меня уже голова дымит, вопрос такой: Есть плата Arduino к ней подключен переменный резистор на 5 кОм и на выходе повешен светодиод.

Как задать такой порядок ШИМ сигнала, допустим резистор выставлен в 0%, т.е., притянут к земле - ШИМ сигнал отсутствует, если резистор повернуть на 50%, то и светодиод начал бы плавно набирать свою яркость до 50%, причем важно то, что как бы я резко не крутанул резистор на 50% от этого плавность наростания яркости светодиода не зависила.

Если я выкручу переменный резистор на 100%, то и светодиод тоже плавно нарастал бы до 100% не зависимо от того как резко я бы выкрутил его.
(6 баллов) 1 1 2
исправил

3 Ответы

+3 голосов
 
Лучший ответ

Ну что же, я пойду ещё немного дальше: буду использовать ещё один переменный резистор для установки длительности нарастания яркости. Также я буду использовать таблицу со значениями яркостей ввиду того, что кривая чувствительности глаза напоминает логарифмическую зависимость. Ну а т.к. это заготовка под сетевой диммер, то для ШИМа светодиода она требует корректировки. Но здесь я хочу показать принцип, а не изготовить конечное изделие. МК я буду использовать attiny841 (441). У него практически любой вход может быть аналоговым, ШИМ сигнал также можно вывести на практически любую ногу. Также в нем присутствуют два 16-ти битных таймера. Один из них уйдёт под генерацию ШИМ, на другом будем отмерять "системное" время. Тактироваться буду от внутреннего RC осциллятора с включенным fuse CKDIV8, что в итоге даст системную частоту 1 МГц. 

Калибровочная таблица со вспомогательными функциями:

 #include <avr/pgmspace.h>

 const uint16_t light_list[] PROGMEM =					// Массив со значениями яркостей
 {
 11000,								// Нулевое значение далеко в стороне от пересечения нуля
 9782,
 9781,
 9780,
 9778,
 9775,
 9769,
 9761,
 9752,
 9740,
 9726,
 9710,
 9692,
 9672,
 9650,
 9626,
 9600,
 9572,
 9542,
 9510,
 9476,
 9440,
 9402,
 9362,
 9319,
 9275,
 9229,
 9181,
 9131,
 9078,
 9024,
 8968,
 8909,
 8849,
 8786,
 8722,
 8656,
 8587,
 8517,
 8444,
 8370,
 8293,
 8215,
 8134,
 8051,
 7967,
 7880,
 7791,
 7701,
 7608,
 7513,
 7416,
 7318,
 7217,
 7114,
 7009,
 6902,
 6793,
 6682,
 6569,
 6455,
 6338,
 6219,
 6097,
 5974,
 5849,
 5722,
 5593,
 5462,
 5329,
 5194,
 5056,
 4917,
 4776,
 4633,
 4487,
 4340,
 4191,
 4039,
 3886,
 3731,
 3573,
 3414,
 3252,
 3089,
 2923,
 2756,
 2586,
 2415,
 2241,
 2065,
 1888,
 1708,
 1526,
 1343,
 1157,
 969,
 779,
 588,
 394,
 198
 };

 uint16_t Get_Phase (uint8_t light)
 {
	return pgm_read_word(&(light_list[light]));
 }

Этот код помещён в файл light.c и для него создан заголовочный файл light.h:

#ifndef LIGHT_H_
#define LIGHT_H_


uint16_t Get_Phase (uint8_t light);


#endif /* LIGHT_H_ */

Теперь определимся со схемотехникой. У меня пин PA1 - это выход ШИМ, PA4 - это пин управления яркостью (точнее задатчик яркости), PA5 - пин регулировки скорости изменения яркости:


 			     	    	-----------
 			        	VCC	|	    |	GND
 				(PB0)  ->	|	    | <-	(PA0) 
 				(PB1)  ->	|       | ->	(PA1) ШИМ выход (OC1B)
 	RESET		(PB3)  ->	|	    | ->	(PA2) 
 				(PB2)  ->	|   	| <-	(PA3) 
 				(PA7)  <-   |SS	    | <-	(PA4) ADC4D, 00 0100, пин управления яркостью
 				(PA6)  <-	|   	| <-	(PA5) ADC5D, 00 0101, пин управления скоростью изменения яркости
 				        	|_________|

Подключаем необходимые заголовки, дефайним константы, создаём необходимые переменные

 #include <avr/io.h>
 #include <avr/interrupt.h>
 #include <avr/sleep.h>

 #include "Light.h"


 #define	Dimmer_DDR		DDRA
 #define	Dimmer_OUT		PINA1			// Пин c ШИМ сигналом

 #define	Light_pin			0b000100			// Значение регистра мультиплексора входов АЦП
 #define	Speed_pin			0b000101 

 #define	ADC_pause			100			// Необходимая пауза между переключениями входов мультиплексора

 #define	icr1			10000			// Значение TOP для таймера 1

 #define Prescaler_1		(1 << CS10)				// Делитель тактов
 #define Prescaler_8		(1 << CS11)
 #define Prescaler_64		(1 << CS10) | (1 << CS11)
 #define Prescaler_256		(1 << CS12)
 #define Prescaler_1024		(1 << CS10) | (1 << CS12)

 
 #define	Set_Prescaler1		Prescaler_1				// Установка предделителя для таймера 1
 #define	Set_Prescaler2		Prescaler_1024				// Установка предделителя для таймера 2
 #define	ClearPrescaler		GTCCR = (1 << PSR)				// Сброс предделителя


 #define	Timer1_Start()		TCCR1B |= Set_Prescaler1; ClearPrescaler	// Пуск таймера1 с очисткой делителя

 #define	Timer2_Start()		TCCR2B |= Set_Prescaler2			// Пуск таймера2




	// Структуры
 struct Phase_t
 {
	uint8_t		End;				// Индекс целевой (конечной) фазы ШИМ сигнала
	int8_t		Current;				// Индекс текущей фазы ШИМ сигнала
	uint8_t		Save;				// Сохраненное значение индекса
	uint8_t		Change;				// Признак регулирования фазы (индекса)
	uint16_t		Speed;				// Скорость изменния индекса
 };


	// Глобальные переменные
 struct	Phase_t		Phase;

ADC_pause необходима для того, чтобы дать мультиплексору время на переключение входов. Даташит говорит про необходимую паузу в 10мс. Я же задаю около 100мс - мне торопиться некуда.

icr1 - это значение счетчика таймера1, достигнув которого таймер1 сбросится. Таким образом формируется частота ШИМ. В нашем случае она равна 100Гц.

Структура Phase_t содержит все необходимые поля с переменными. Здесь я не использую поле Save - это артефакт)

Теперь запишем функции работы с АЦП.

 inline void ADC_Init(void)
 {
	DIDR0 = (1<<ADC4D) | (1<<ADC5D);					// Отключаем триггер Шмитта от сигнальной ноги
	 
//	ADMUXB = (0<<REFS2) | (0<<REFS1) | (0<<REFS0) | (0<<GSEL1) | (0<<GSEL0);	// Reference = Vcc, усиление = 1

//	ADCSRB = (0<<ADLAR) | (0<<ADTS1) | (0<<ADTS0);				// Запуск вручную
	ADCSRA = (1<<ADEN) |						// Разрешаем работу АЦП
		(1<<ADIE) |						// Разрешаем прерывания
		(0<<ADATE) |						// Триггер выключен, ручной старт
		(1<<ADPS2) | (0<<ADPS1) | (0<<ADPS0);				// Прескалер = 16
 }


 void ADC_Set_channel (uint8_t Analog_input)
 {
	ADMUXA = Analog_input;
 }

 void ADC_Start_Conversion (void)
 {
	ADCSRA =	(1<<ADEN) |
		(1<<ADIE) |
		(0<<ADATE) |
		(1<<ADPS2) | (0<<ADPS1) | (0<<ADPS0) |
		(1<<ADSC);						// Команда на запуск преобразования
 }

В регистр DIDR0 записываем пины, которые будут аналоговыми входами. Это необходимо для того, чтобы входной триггер Шмитта не "щелкал", когда входной сигнал пересекает точки переключения логических уровней, что несколько снизит помехи. Системная частота у нас 1МГц, но дополнительно я включаю прескалер (делитель) на 16, что в итоге даст частоту тактирования АЦП 62,5 кГц. Мне торопиться некуда)

Раз мы разрешили прерывания у АЦП, то обязательно пишем обработчик, иначе по прерыванию уйдем на ресет:

 ISR (ADC_vect)
 {

 }

Инициализируем таймеры:

	//... Инициализация таймера с ШИМ
 inline void T1_Init(void)
 {	
		
	TCCR1A = (1 << COM1B1) | (1 << COM1B0) | (1 << WGM11) | (0 << WGM10);	// При совпадении значения таймера с блоком сравнения COM1B формируется передний фронт импульса ШИМ
	TCCR1B = (1 << WGM13) | (1 << WGM12);			// При достижении таймером значения ICR1 формируется задний фронт импульса ШИМ
	TOCPMSA0 = (1 << TOCC0S0);				// Привязка блока сравнения COM1B к пину с подключенным симистором
	TOCPMCOE = (1 << TOCC0OE);				// Разрешаем работу пина от блока сравнения 

	ICR1 = icr1;
	
//	TIMSK1 = (1 << OCIE1A) | (1 << OCIE1B);			// Установка прерывания от компаратора 1A и 1B
 }


	//... Инициализация таймера скорости изменения яркости
 inline void T2_Init(void)
 {
 //	TIMSK2 = (1 << OCIE2A);
 }

У attiny841(441) недостаточно разрешить работу компаратора таймера для выдачи ШИМ сигнала наружу. Необходимо также настроить выходной коммутатор. Что где разрешаем должно быть понятно из комментария в коде.

Функция инициализации таймера2 пустая, но оставлена - вдруг что туда ещё записать)

Запускать измерение АЦП я буду при остановленном CPU, поэтому запишем ещё одну функцию:

 void Sleep_Init(uint8_t Sleep_mode)
 {
	set_sleep_mode(Sleep_mode);						// Выбираем режим сна
	sleep_enable();							// Разрешаем сон
 }

Здесь мы определяем режим сна и разрешаем сон (только разрешаем!, а не уходим в него!)

Теперь функция main:

 int main(void)
 {
	uint16_t	Old_time, Current_time, Old_time_ADC;

	uint8_t	light_measur;

	Dimmer_DDR = (1 << Dimmer_OUT);				// Конфигурируем пин ШИМ-а на выход

	PRR = (1 << PRTWI) | (1 << PRUSART1) | (1 << PRUSART0);		// Запрещаем работу ненужных блоков

	T1_Init();						// Инициализация таймеров
	T2_Init();

	Phase.Current = 0;
	Phase.Speed = 0;
	Phase.End = 0;

	Timer1_Start();						// Запускаем таймеры (на самом деле подключаем
	Timer2_Start();						// тактовый вход таймера к одному из выходов предедлителя)

	ADC_Init();						// Инициализация АЦП

	Old_time = TCNT2;						// Фиксируем текущее время
	Old_time_ADC = TCNT2;

	ADC_Set_channel(Light_pin);					// Переключаем мультиплексор на вход задатчика яркости
	light_measur = 1;						// Взводим признак того. что мулитплексор переключен на вход
								// задатчика яркости
	Sleep_Init(SLEEP_MODE_IDLE);

	sei();							// Разрешаем прерывания
    
	while (1) 
	{
		Current_time = TCNT2;				// Фиксируем текущее время

		if ((Current_time - Old_time) > Phase.Speed)		// Если настало время для изменения яркости к целевому значению
			{
				Set_Phase();
				OCR1B = Get_Phase(Phase.Current);	// Пишем в компаратор ШИМ таймера новое значение фазы (скважности)

				Old_time = TCNT2;			// Фиксируем текущее время
			}

		if ((Current_time - Old_time_ADC) > ADC_pause)		// А не пора ли снять показания с АЦП?
			{
				ADC_Start_Conversion();			// Команда на запуск преобразования АЦП

				sleep_cpu();				// Останавливаем ЦПУ

				if (light_measur)			// Если мультиплексор на входе задатчика яркости
					{
						Phase.End = (ADC >> 3);	// Делим значение с АЦП на 4
						if (Phase.End > 100) {Phase.End = 100;}	// Если индекс больше 100, то корректируем
						light_measur = 0;
						ADC_Set_channel(Speed_pin);	// Переключаем мультиплексор на вход управления скоростью изменеия яркостью
					}
					else
						{
							Phase.Speed = ADC;	// Запоминаем значение скорости изменения яркостью
							light_measur = 1;
							ADC_Set_channel(Light_pin);	// Переключаемся на задатчик яркости	
						}

				Old_time_ADC = TCNT2;		// Фиксируем текущее время
			}
	}
 }

Запускаю АЦП на измерение и сразу увожу МК в режим сна IDLE. Почему не SLEEP_MODE_ADC? Всё просто - в этом режиме останавливается тактирование таймеров, что негативно отразится на ШИМ сигнале.

Последняя функция: установка текущей фазы (скважности) ШИМ сигнала. Я через определённые промежутки времени (задаваемые вторым потенциометром) инкрементирую или декрементирую (в зависимости от того, с какой "стороны" нахожусь от целевого значения) текущий индекс, который определяет уровень яркости (см. выше light.c):

	//... Установка скважности ШИМ сигнала
 void Set_Phase(void)
 {
	if (Phase.Change)
	{
		if (Phase.End > Phase.Current)
		{
			Phase.Current++;

		}
		else
			{
				Phase.Current--;
			}
	}

	if (Phase.End == Phase.Current)
	{
		Phase.Change = 0;
	}
	else
		{
			Phase.Change = 1;
		}
 }

Скомпилированный с оптимизацией -O3 код занимает:

Program Memory Usage     :    688 bytes   8,4 % Full
Data Memory Usage         :    6 bytes   1,2 % Full

(2.7 тыс. баллов) 9 29 55
выбран
+1 голос
Я таким алгоритмом сейчас занят. Идея у меня такая - по тику таймера (через определенные промежутки времени) инкрементируем/декрементируем переменную, которая задаёт скважность ШИМ сигнала. Как только достигли целевой скважности (которую задаёт положение потенциометра), останавливаемся.
(2.7 тыс. баллов) 9 29 55
исправил
+1 голос
Если конечно это не принципиально, то зачем тут алгоритм, когда можно это решить двумя деталями?

На аналоговый вход ,который зашунтирован ёмкостью 10-220мкФ(емкость подобрать) ,подавать через резистор 100 КОм с потенциометра.

Остальное как обычно масштабируем аналоговый вход в ШИМ.
(589 баллов) 4 8 19
Как раз таки это принципиально, что нужна стабильность, я нашел нечто похожее готовое, но там есть в начале длительная задержка в 4 секунды, что меня не устраивает

Вот ссылка на тот проект который я нашел, тут для велосипедов а мне надо для электрогольфкара https://vk.com/topic-148896175_39735997
А что Вам мешает контролировать ток двигателя и уменьшать его автоматически до нужного предела .
Там схема чисто интуитивного регулирования тока .Интуитивно регулировать ток это не правильно ,т.к дорога бывает в гору и прямая дорога ,есть еще торможение которое быстро остановит байк и резко начнется разгон всё равно.
В старинных электрокарах плавный " газ " сделан с помощью механического устройства , которое при резком нажатии на педаль медленно поворачивало коммутатор тем самым плавно давая ток. А при отпускании педали происходил сброс с помощью храпового механизма , как то так.
Добро пожаловать на Бредборд! Сайт вопросов и ответов на тему Arduino, Raspberry Pi и хоббийной электроники в целом. Цель Бредборда — быть максимально полезным. Поэтому мы строго следим за соблюдением правил, боремся с холиворами и оффтопиком.
  1. Un_ka

    8 балл(ов)

  2. Eriak

    4 балл(ов)

  3. AlexGorr

    2 балл(ов)

...