PWM своими руками

ites
Offline
Зарегистрирован: 26.12.2013

Изучая второе занятие с Arduino поставил себе задачу сделать PWM без использования analogWrite. Ну, почти получилось, я может быть ещё не все моменты понял, но результат, на мой взгляд, заметно лучше. Во-первых, разрешение в области малых значений лучше, во вторых, можно получить устойчивую светимость светодиода (мой индикатор) при меньшей яркости.

Иллюстрация.

void setup_timer1 () {
	noInterrupts();
	// disable all interrupts
	TCCR1A = 0;
	TCCR1B = 0;
	TCNT1 = 0;

	OCR1A = 1;            // compare match register 16MHz/256/2Hz
	TCCR1B |= (1 << WGM12);   // CTC mode
	TCCR1B |= (1 << CS10)|(1 << CS11);    // 64 prescaler
	TIMSK1 |= (1 << OCIE1A);  // enable timer compare interrupt
	interrupts();
}

Это установка таймера. Я, надо признаться, не совсем понимаю, срабатывает он каждый тик или каждый второй тик. Прошу комментария на эту тему.

int led = 9;

void setup() {
	pinMode(led, OUTPUT);
	setup_timer1();
}

Светиться будет светодиод на 9-м пине. Сделал так, чтобы было проще сравнить с тем, что делает analogWrite.

byte current_state = 0;
/* 1.25 microsecs */
static inline void ledWriteB(byte pin) {
	pin -= 8;
	byte pins = PORTB;
	byte old_pins = pins;
	if (current_state & 1) {
		pins |= (HIGH << pin);
		--current_state;
	} else if (current_state & 2) {
		pins &= ~(HIGH << pin);
		current_state -= 2;
	}
	if (old_pins != pins)
		PORTB = pins;
}

Несколько оптимизированный вариант digitalWrite, по моим замерам, довольно быстрый. Совмещено с логикой распределения импульсов 1 и 0. Если в 0м бите 1, то выдать 1, если в 1м бите 1, то надо выдать 0. Биты устанавливаются специальной логикой так, чтобы уровень 0.5 выдавал всегда 1 и 0 подряд, равномерно, если уровень выше, то 1-цы будут идти соответственно чаще, но равномерно.

typedef struct _int_bresenham {
	int ante, conseq;
	int ante_err, conseq_err;
} int_bresenham;

/* 1.2 microsecs */
static inline byte dither_bresenham(int_bresenham *br) {
	byte ret = 0;
	if (br->ante_err < br->ante && br->conseq_err < br->conseq) {
		br->conseq_err += br->ante;
		br->ante_err += br->conseq;
	}
	if (br->ante <= br->ante_err) {
		++ret;
		br->ante_err -= br->ante;
	}
	if (br->conseq <= br->conseq_err) {
		ret |= 2;
		br->conseq_err -= br->conseq;
	}
	return ret;
}

Это собственно логика, распределяющая переходы равномерно в заданном соотношении. Авторство моё :) Идея позаимствована у Брезенхема из целочисленного алгоритма рисования линий.

void loop() {
	short pot_value = analogRead(A0);
//	analogWrite(led, pot_value / 4);
	current_br.ante = 1023 - pot_value;
	current_br.conseq = pot_value;
}

ISR(TIMER1_COMPA_vect)          // timer compare interrupt service routine
{
	if (current_state == 0) {
		current_state = dither_bresenham(&current_br);
	}
	ledWriteB(led);
}

Тут происходит действие -- считываются показания потенциометра, которые используются для расчёта коэффициентов. В прерывании таймера вычисляется соотношение 1 и 0 на следующий шаг и подсвечивается светодиод нужным уровнем яркости.

Интересует мнение опытного народа о потобных извратах, имеет ли это когда либо смысл или я просто чего-то не знаю? Я всего лишь второй день с новой игрушкой, никогда раньше не имел дел с микроконтроллерами.

MaksMS
Offline
Зарегистрирован: 11.03.2013

Вы изобретаете велосипед : ) http://arduino.ru/forum/programmirovanie/analogwrite-na-lyubom-vyvode

ites
Offline
Зарегистрирован: 26.12.2013

MaksMS пишет:

Вы изобретаете велосипед : ) http://arduino.ru/forum/programmirovanie/analogwrite-na-lyubom-vyvode

Файл библиотеки лежит на таком хостинге, с которого у меня не получается скачать. Нажимаю "скачать", а мне в ответ "http://download73.files.attachmail.ru/DA49F37530AB4AED8D38E2670A7CA509/865449855ff0d45e8b066fdd4a59a1f2/PWM.rar не найден". Поэтому, мне не удалось посмотреть что же там внутри, было интересно сравнить реализации. Собственно, я ж начинающий, это моя обязанность -- изобрести велосипеды самостоятельно. Главное, чтобы колёса квадратными не оказались -- моя тема об этом.

maksim
Offline
Зарегистрирован: 12.02.2012

Там совсем другая история... 

Вы используете аппаратный ШИМ вам сюда.

void setup()
{
  cli();
  DDRB |= 1<<1 | 1<<2;         
  PORTB &= ~(1<<1 | 1<<2);
  TCCR1A = 0b00000010; 
  //TCCR1A = 0b10100010;  
  TCCR1B = 0b00011001;  
  ICR1H = 255;
  ICR1L = 255;
  sei(); 
  Serial.begin(9600);
  Serial.println("PWM resolution: 16-bit");
  Serial.println("PWM frequency: 244 Hz");
  Serial.println("PWM duty OC1A(D9): 0");
  Serial.println("PWM duty OC1B(D10): 0");
  Serial.println();
}

void loop() 
{
  if(Serial.available()) 
  {
    char c = Serial.read();
    uint16_t pwm = Serial.parseInt();
    if(c == 'A')
    { 
      pwm ? TCCR1A|=1<<7 : TCCR1A&=~(1<<7);
      OCR1AH = highByte(pwm);  
      OCR1AL = lowByte(pwm);
      Serial.print("PWM duty OC1A(D9): ");
      Serial.println(pwm);
    }
    if(c == 'B')
    { 
      pwm ? TCCR1A|=1<<5 : TCCR1A&=~(1<<5);
      OCR1BH = highByte(pwm);  
      OCR1BL = lowByte(pwm);
      Serial.print("PWM duty OC1B(D10): ");
      Serial.println(pwm);
    }
    if(c == 'R')
    {
      Serial.println();
      Serial.print("PWM resolution: ");
      Serial.print(pwm);
      Serial.println("-bit");
      pwm = pow(2, pwm);
      ICR1H = highByte(pwm);  
      ICR1L = lowByte(pwm);
      Serial.print("PWM frequency: ");
      Serial.print(16000000/(pwm+1UL));
      Serial.println(" Hz");
      Serial.println();
    }
  }
}

Как реализован программный ШИМ (то что в архиве) написано в сообщении #30.

ites
Offline
Зарегистрирован: 26.12.2013

maksim пишет:

Там совсем другая история... 

Вы используете аппаратный ШИМ вам сюда.

Да неправда, у меня самый настоящий програмный PWM.

maksim пишет:

Как реализован программный ШИМ (то что в архиве) написано в сообщении #30.

Я, к сожалению, плохо (т.е. никак) знаю ассемблер AVR, можешь объяснить что делает CHECK словами? Мой последний чем-то похожий ассемблер, который я знал, был Z80, на котором я писал года так 22 назад.

 

ites
Offline
Зарегистрирован: 26.12.2013

maksim пишет:

Как реализован программный ШИМ (то что в архиве) написано в сообщении #30.

Хочу задать вопрос о выборе прескейл значения. Если я правильно прочитал, то у тебя 256, правильно? Т.е. частота таймера у тебя будет будет 16мгц/256/1 = 62500, правильно? Быстрее просто не успевает обработчик прерывания?

ites
Offline
Зарегистрирован: 26.12.2013

Добавлю иллюстрацию использования метода. Это мой третий урок по Ардуино, подключен RGB LED и  переменный резистор (потенциометер). Три разных цвета RGB зажигаются-гаснут по синусоиде, расхождение угла между ними задаётся положением резистора.

#include <Arduino.h>
#include "HardwareSerial.h"

typedef struct _int_bresenham {
	short ante, conseq;
	short ante_err, conseq_err;
} int_bresenham;

/* 1.2 microsecs */
static inline byte dither_bresenham(int_bresenham *br) {
	byte ret;
	if (br->ante == 0)
		return 1;
	if (br->conseq == 0)
		return 2;
	ret = 0;
	if (br->ante_err < br->ante && br->conseq_err < br->conseq) {
		br->conseq_err += br->ante;
		br->ante_err += br->conseq;
	}
	if (br->ante <= br->ante_err) {
		++ret;
		br->ante_err -= br->ante;
	}
	if (br->conseq <= br->conseq_err) {
		ret |= 2;
		br->conseq_err -= br->conseq;
	}
	return ret;
}

// the setup routine runs once when you press reset:
void setup_timer1() {
	noInterrupts();
	// disable all interrupts
	TCCR1A = 0;
	TCCR1B = 0;
	TCNT1 = 0;

	OCR1A = 1;
	TCCR1B |= (1 << WGM12);   // CTC mode
//	TCCR1B |= (1 << CS10) | (1 << CS11);    // 64 prescaler
	TCCR1B |= (1 << CS12);    // 256 prescaler
	TIMSK1 |= (1 << OCIE1A);  // enable timer compare interrupt
	interrupts();
}

typedef struct _pwm_state {
	int_bresenham br;
	byte state;
} t_pwm_state;

t_pwm_state pwm_state[14];

void setup() {
	// initialize the digital pin as an output.
	for (byte pcnt = 11; pcnt < 14; ++pcnt)
		pinMode(pcnt, OUTPUT);
	setup_timer1();
	for (int i = 0; i < 13; ++i) {
		pwm_state[i].br.ante = pwm_state[i].br.ante_err = pwm_state[i].br.conseq = pwm_state[i].br
				.conseq_err = 0;
		pwm_state[i].state = 0;
	}
//	Serial.begin(115200);
}

int tbl_sinus_scale = 255;
byte tbl_sinus[] = { 0, 4, 9, 13, 18, 22, 27, 31, 35, 40, 44, 49, 53, 57, 62, 66, 70, 75, 79, 83,
		87, 91, 96, 100, 104, 108, 112, 116, 120, 124, 127, 131, 135, 139, 143, 146, 150, 153, 157,
		160, 164, 167, 171, 174, 177, 180, 183, 186, 190, 192, 195, 198, 201, 204, 206, 209, 211,
		214, 216, 219, 221, 223, 225, 227, 229, 231, 233, 235, 236, 238, 240, 241, 243, 244, 245,
		246, 247, 248, 249, 250, 251, 252, 253, 253, 254, 254, 254, 255, 255, 255, 255 };

short irsinus(int grad) {
	short ret;
	int ag;
	grad %= 360;
	if (grad < 0)
		grad += 360;
	ag = grad % 180;
	if (ag > 90)
		ag = 180 - ag;
	ret = tbl_sinus[ag];
	if (grad > 180)
		ret = -ret;
	return ret;
}

/* 1.25 microsecs */
static inline void maskWriteB(byte set, byte mask) {
	byte state = PORTB;
	byte old_state = state;
	state &= ~mask;
	state |= set;
	if (old_state != state)
		PORTB = state;
}

int current_grad = 1;
unsigned long next_turn = 0;

void loop() {
	short pot_value = analogRead(A0);
	unsigned long time = millis();
	if (time > next_turn) {
		++current_grad;
		if (current_grad >= 360)
			current_grad -= 360;
		short cs = irsinus(current_grad);
		if (cs < 0)
			cs = -cs;
		pwm_state[11].br.ante = cs;
		pwm_state[11].br.conseq = tbl_sinus_scale - cs;
		cs = irsinus(current_grad + pot_value / 4);
		if (cs < 0)
			cs = -cs;
		pwm_state[12].br.ante = cs;
		pwm_state[12].br.conseq = tbl_sinus_scale - cs;
		cs = irsinus(current_grad + pot_value / 2);
		if (cs < 0)
			cs = -cs;
		pwm_state[13].br.ante = cs;
		pwm_state[13].br.conseq = tbl_sinus_scale - cs;
		next_turn = time + 10;
	}
}

ISR(TIMER1_COMPA_vect)          // timer compare interrupt service routine
{
	byte set = 0;
	for (byte pcnt = 11; pcnt < 14; ++pcnt) {
#define	bpin  (pcnt-8)
#define cpwm  pwm_state[pcnt]
		if (cpwm.state == 0) {
			cpwm.state = dither_bresenham(&cpwm.br);
		}
		if (cpwm.state & 1) {
			set |= (HIGH << bpin);
			--cpwm.state;
		} else if (cpwm.state & 2) {
			cpwm.state -= 2;
		}
	}
	maskWriteB(set, 0b00111000);
}

int main(void) {
	init();
	setup();
	//endless loop
	for (;;) {
		loop();
	}
	return 0;
}

 

maksim
Offline
Зарегистрирован: 12.02.2012

ites пишет:

Да неправда, у меня самый настоящий програмный PWM.

Да, наверное, не вчитывался... где то промелькнуло про FastPWM вот и подумал...

ites пишет:

что делает CHECK словами?

Да собственно сравнивает pwms[0] с pwm и устанавливает или сбрасывает бит порта.

ites пишет:

Хочу задать вопрос о выборе прескейл значения. Если я правильно прочитал, то у тебя 256, правильно? Т.е. частота таймера у тебя будет будет 16мгц/256/1 = 62500, правильно? Быстрее просто не успевает обработчик прерывания?

256 это все таки разрядность ШИМ, а прескалер по даташиту:

 Будет ли успевать обработчик или нет зависит от количества задействованных выводов, измерений не проводил, так что при разных прескалерах и количестве выводов какая реальная частота ШИМ не могу сказать.

ites
Offline
Зарегистрирован: 26.12.2013

maksim пишет:

Да собственно сравнивает pwms[0] с pwm и устанавливает или сбрасывает бит порта.

Логика действия ускользнула. Можно рассказать на пальцах?

maksim пишет:
256 это все таки разрядность ШИМ, а прескалер по даташиту:

 Будет ли успевать обработчик или нет зависит от количества задействованных выводов, измерений не проводил, так что при разных прескалерах и количестве выводов какая реальная частота ШИМ не могу сказать.

Это какой-то другой даташит, не от Arduino Uno. В том, что я читал, были CS12-CS10.

ites
Offline
Зарегистрирован: 26.12.2013

ites пишет:

maksim пишет:

Да собственно сравнивает pwms[0] с pwm и устанавливает или сбрасывает бит порта.

Логика действия ускользнула. Можно рассказать на пальцах?

Если интересно, я могу рассказать логику действий dither_bresenham, если по коду непонятно. В этом исполнении разрядность PWM динамическая и плавность по большей части зависит только от быстродействия.

ites
Offline
Зарегистрирован: 26.12.2013

ites пишет:

Это какой-то другой даташит, не от Arduino Uno. В том, что я читал, были CS12-CS10.

Сорри, осознал, что мы про разные таймеры говорим. Я про таймер1, а ты про таймер2.

ites
Offline
Зарегистрирован: 26.12.2013

Последняя версия исправленного и оптимизированного dither_bresenham

typedef struct _int_bresenham {
    byte ante, conseq;
    byte err;
} int_bresenham;

static inline byte dither_bresenham(int_bresenham *br) {
	byte ret;
	int_bresenham brl;
	brl.ante = br->ante;
	if (brl.ante == 0)
	return 1;
	brl.conseq = br->conseq;
	if (brl.conseq == 0)
	return 2;
	if (brl.ante == brl.conseq)
	return 3;
	brl.err = br->err;
	if (brl.ante > brl.conseq) {
		brl.err += brl.conseq;
		if (brl.err >= brl.ante) {
			ret = 3;
			brl.err -= brl.ante;
		} else {
			ret = 2;
		}
	} else {
		brl.err += brl.ante;
		if (brl.err >= brl.conseq) {
			brl.err -= brl.conseq;
			ret = 3;
		} else {
			ret = 1;
		}
	}
	br->err = brl.err;
	return ret;
}

 

ites
Offline
Зарегистрирован: 26.12.2013

Наконец-то зачитал популярную статейку о ШИМ, понял как работает код у maksim. Задумался...

yul-i-an
yul-i-an аватар
Offline
Зарегистрирован: 10.12.2012

Моя реализация алгоритма Брезенхема тут.

Видео демо тут.

Собирал термостат с ПИД регулированием.

Tomasina
Tomasina аватар
Offline
Зарегистрирован: 09.03.2013

оно на любой ардуинке будет работать? Или зависит от камушка?

yul-i-an
yul-i-an аватар
Offline
Зарегистрирован: 10.12.2012

Tomasina пишет:

оно на любой ардуинке будет работать? Или зависит от камушка?

Алгоритм Брезенхема на любой! Или Вы не об этом?

Tomasina
Tomasina аватар
Offline
Зарегистрирован: 09.03.2013

я про прямое указание портов в скетче.

ites
Offline
Зарегистрирован: 26.12.2013

У одного класса ардуин как правило да. Вообще, скетч вот, просто попробуй на той, что тебя интересует: https://github.com/jabbervorx/arduino-rgb-lesson/blob/master/src/wave-rgb.cpp

ites
Offline
Зарегистрирован: 26.12.2013

Tomasina пишет:

я про прямое указание портов в скетче.

для Arduino Uno можно вот моё взять: https://github.com/jabbervorx/arduino-any-pc-pwm/blob/master/include/digitalPortsFast.h

если нужно универсально, то есть другие, очень похожие по смыслу варианты. более полные библиотеки попадались, но мне хватало моей.