ШИМ произвольного разрешения на UNO/Nano (328)

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Столкнулся с проблемой - не хватало разрешения стандартного 8-бит ШИМа. Изменение скважности на 1 давало слишком большой эффект. Потребовался ШИМ более высокого разрешения (второй параметр analogWrite нужен не 0-255, а 0-8192 в моём случае).

Написал три функции, которые позволяют сделать на пинах 9 и 10 ШИМ с разрешением от 2 бит (второй параметр analogWrite в пределах 0-3) до 16 бит  (второй параметр analogWrite - 0-65 535).

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

Вот код.

inline void pwmInit(const int8_t bitRate = 16) {
	noInterrupts();
	TCCR1A = bit(COM1A1) |  bit(COM1B1) | bit(WGM11);
	TCCR1B = bit(WGM13) | bit(WGM12) | bit(CS10);
	TCCR1C = TCNT1 = OCR1A = OCR1B = TIMSK1 = TIFR1 = 0;
	ICR1 = (1u << bitRate) - 1u;
	interrupts();
}
inline void pwmWrite9(const uint16_t v) { DDRB |= bit(PB1); OCR1A = v; }
inline void pwmWrite10(const uint16_t v) { DDRB |= bit(PB2); OCR1B = v; }

Функцию pwmInit нужно вызвать один раз (например, в setup). Её параметр - разрядность ШИМ. Может быть от 2 до 16. Функция не проверяет корректность параметра - на Вашей совести.

После этого можно вызывать функции pwmWrite9 (для 9-го пина) и/или pwmWrite10 (для 10-го) пина и устанавливать скважность точно также, как мы вызываем analogWrite. Значение 0 - сигнала нет, максимальное значение - сигнал сплошной.

Допустимые максимальные значения (как и частота ШИМ) зависят от разрядности (параметра, который передавали функции pwmInit).

В таблице первый столбец - разрядность (параметр, который передаём pwmInit), второй столбец - максимальное значение, которое можно передавать pwmWrite9 и pwmWrite10 (то, при котором сигнал будет сплошным) в столбцах - частоты ШИМ при тех или иных тактовых частотах микроконтроллера. Для стандартной ардуины следует смотреть столбец 16МГц (второй справа).

Бит Макс Частота контроллера (МГц)
1 2 4 8 16 20
2 3 250 000,0 500 000,0 1 000 000,0 2 000 000,0 4 000 000,0 5 000 000,0
3 7 125 000,0 250 000,0 500 000,0 1 000 000,0 2 000 000,0 2 500 000,0
4 15 62 500,0 125 000,0 250 000,0 500 000,0 1 000 000,0 1 250 000,0
5 31 31 250,0 62 500,0 125 000,0 250 000,0 500 000,0 625 000,0
6 63 15 625,0 31 250,0 62 500,0 125 000,0 250 000,0 312 500,0
7 127 7 812,5 15 625,0 31 250,0 62 500,0 125 000,0 156 250,0
8 255 3 906,3 7 812,5 15 625,0 31 250,0 62 500,0 78 125,0
9 511 1 953,1 3 906,3 7 812,5 15 625,0 31 250,0 39 062,5
10 1 023 976,6 1 953,1 3 906,3 7 812,5 15 625,0 19 531,3
11 2 047 488,3 976,6 1 953,1 3 906,3 7 812,5 9 765,6
12 4 095 244,1 488,3 976,6 1 953,1 3 906,3 4 882,8
13 8 191 122,1 244,1 488,3 976,6 1 953,1 2 441,4
14 16 383 61,0 122,1 244,1 488,3 976,6 1 220,7
15 32 767 30,5 61,0 122,1 244,1 488,3 610,4
16 65 535 15,3 30,5 61,0 122,1 244,1 305,2

Используется режим Fast-PWM, если это кому-то важно. Кто не знает, что это такое - либо забейте, либо посмотрите в даташите.

Кому надо, пользуйтесь.

Финальное замечание: В принципе, функции pwmWrite9 и pwmWrite10 нафиг не нужны. Если вместо их использовать стандартные analogWrite(9, …) и analogWrite(10, …), всё будет работать ничуть не хуже. Можете попробовать. А выделил я их вот для чего. Дело в том, что после вызова pwmInit у Вас изменились разрешение и частота и разрешение ШИМ только на пинах 9 и 10. На остальных всё осталось как было раньше. Поэтому, если Вы, например, вызвали pwmInit(16); то с этого момента второй параметр analogWrite для пинов 9 и 10 имеет право быть от 0 до 65 535 в то время как для остальных пинов, он по прежнему должен быть в границах 0-255. Так вот, чтобы было поменьше путаницы, я и ввёл специальные функции для 9-го и 10-го пинов. Хотя, стандартные тоже нормально работают.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Блин, забыл написать - частота ШИМ в таблице дана в герцах.

Ворота
Ворота аватар
Offline
Зарегистрирован: 10.01.2016

Работающий пример можно?

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Ну, не знаю, чего там примерять-то? Ну, ... вот ...

inline void pwmInit(const int8_t bitRate = 16) {
	noInterrupts();
	TCCR1A = bit(COM1A1) |  bit(COM1B1) | bit(WGM11);
	TCCR1B = bit(WGM13) | bit(WGM12) | bit(CS10);
	TCCR1C = TCNT1 = OCR1A = OCR1B = TIMSK1 = TIFR1 = 0;
	ICR1 = (1u << bitRate) - 1u;
	interrupts();
}
inline void pwmWrite9(const uint16_t v) { DDRB |= bit(PB1); OCR1A = v; }
inline void pwmWrite10(const uint16_t v) { DDRB |= bit(PB2); OCR1B = v; }

void setup(void) {
	pwmInit(13);
}

void loop(void) {
	int i, step = 1024;
	for (i = step; i < 8192; i += step) {
		pwmWrite9(i);
		pwmWrite10(8192 - i);
		delay(1000);
	}
	for (i -= step; i > 0; i -= step) {
		pwmWrite9(i);
		pwmWrite10(8192 - i);
		delay(1000);
	}
}

 

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

:) Интересный подход .. а каким макаром у Вас, Евгений,  16-и разрядное "v" передается в 8-и разрядный OCR1A? Там, вроде как по даташиту, требуется делать 2 пересылки в строго заданной последовательности И с запрещеннымии прерываниями .. макрос из io.h ( OCR1A=..) разве на такое способен?

:)

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Вы что-то перепутали, OCR1A - 16-ти  разрядный. См. п. 16.11.5 даташита (ссылка на даташит есть в исходном посте темы).

Там, похоже, ошибка другого рода имеется, как выловлю, выложу правку. При записи 0 (причём, когда там и так ноль был) иногда откуда-то высокий уровень проскакивает. Не часто и всплеск короткий - но непорядок. Ноль он должен и в Африке быть нулём. Сейчас пытаюсь отловить проблему и найти решение. 

dimax
dimax аватар
Онлайн
Зарегистрирован: 25.12.2013

ЕвгенийП, ну проблемой то это трудно назвать, так и должно быть. Ноль в регистре сравнения -это не ничего, это когда TCNT1==0  :) В ардуиновской функции этот ноль специально отлавливается, и таймер просто останавливается.

 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

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

Другое дело, почему он не вылетает, когда я ничего туда не пишу? Вот сидит там себе 0 и никого не трогает и ничего не вылетает. ... Хотя, я уже начал слмневаться, не помню поставил ли я режим "пик детект" на осциллографе. А без него такой импульс нетрудно и пропустить.

В любом случае спасибо, в выходной займусь и как победю - выложу правку.

dimax
dimax аватар
Онлайн
Зарегистрирован: 25.12.2013

ЕвгенийП, пик должен быть периодическим, каждый раз при переходе через ноль. Можно позаимствовать метод "борьбы" из wiring_analog.c

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Да, я уже проверил, спасибо. Действительно я просто не включал пик-детект и потому мне казалось, что эффект "плавающий". И когда собственно понял причину всё думал, но блин он должен быть всегда, почему он только иногда проявляется :))) Спасибо. Насчёт метода посмотрю - подумаю. В выходные выложу обновление. Там, на самом деле, если пожертвовать 16-битным режимом, можно просто вместо 0 зафигачивать туда "нечто большее, чем ICR1  - проблема уйдёт. Но 16-битный режим жалко, хотя прямо сейчас он мне и не нужен. 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Добрый день, коллеги.

Как и обещал, в выходной вернулся к задаче и подробно исследовал проблему.

Значит, таки да, если параметр, отвечающий за скважность (впредь я и буду просто говорить «скважность») установить в 0 (или в максимальное значение), то это таки не LOW (HIHG) а просто минимальная (максимальная) длительность высокого уровня.

Ниже три осциллограммы, которые это показывают. При нуле (верхняя картинка) длительность импульса 62 наносекунды, при единице (средняя картинка) – 124 нс, а при двойке (нижняя картинка) – 186 нс.


pwmWrite9(0);


pwmWrite9(1);


pwmWrite9(2);

Таким образом, получается, что если делать так, как сделано в wrining_analog.c (т.е. при нуле и при максимальном значении просто нагло вставлять туда digitalWrite) то, мы получим скачок скважности между 0 и 1 вдвое больший, чем, между, скажем 1 и 2 или любыми другими соседними числами.

К сожалению, для моей задачи это неприемлемо и я от этого отказался.

Решил в целом оставить как есть, а если нужны чистые LOW и HIGH, то пользоваться аналогом digitalWrite напрямую.Написал для этих двух пинов digitalWrite9 и digitalWrite10. 

Получилось несколько менее удобно, чем в среде Ардуино, т.к. нужно самому заботиться о том, чтобы явно вызвать digitalWrite9/10 для постоянного низкого или высокого уровней. Зато шаг между низким уровнем и 0 точно такой же, как между 0 и 1 и т.д., т.е. регулировка равномерная на всём диапазоне, а для меня это сейчас важно и стоит того удобства.

Оформил include’файлом (кому нравится, могут называть это библиотекой).

#ifndef	FlexPWM_h
#define	FlexPWM_h

#define __bit(b) (1u << (b))

inline void pwmInit(const int8_t bitRate = 16) {
	noInterrupts();
	TCCR1A = __bit(WGM11);
	TCCR1B = __bit(WGM13) | __bit(WGM12) | __bit(CS10);
	TCCR1C = TCNT1 = OCR1A = OCR1B = TIMSK1 = TIFR1 = 0;
	ICR1 = (1u << bitRate) - 1u;
	interrupts();
}

inline void pwmWrite9(const uint16_t v) { 
	DDRB |= __bit(PB1);
	TCCR1A |= __bit(COM1A1);
	OCR1A = v;
}

inline void digitalWrite9(const uint8_t v) {
	DDRB |= __bit(PB1);
	TCCR1A &= ~__bit(COM1A1); 
	if (v) PORTB |= __bit(PB1); else PORTB &= ~__bit(PB1);
}

inline void pwmWrite10(const uint16_t v) {
	DDRB |= __bit(PB2);
	TCCR1A |= __bit(COM1B1);
	OCR1B = v;
}

inline void digitalWrite10(const uint8_t v) {
	DDRB |= __bit(PB2);
	TCCR1A &= ~__bit(COM1B1);
	if (v) PORTB |= __bit(PB2); else PORTB &= ~__bit(PB2);
}

#endif	// FlexPWM_h

Вот пример. На экране осциллографа сигнал растягивается/стягивается как гармошка, а на чистых HIGH и LOW замирает на 5 секунд.

#include "FlexPWM.h"

void setup(void) {
	pwmInit(13);
}

void loop(void) {
	digitalWrite10(LOW);
	delay(5000);
	int duty = 0;
	for (; duty < 8192; duty++) {
		pwmWrite10(duty);
		delay(1);
	}
	digitalWrite10(HIGH);
	delay(5000);
	for (duty--; duty >=0; duty--) {
		pwmWrite10(duty);
		delay(1);
	}
}

 

Gregor-R
Offline
Зарегистрирован: 10.07.2017

ЕвгенийП пишет:
Написал три функции, которые позволяют сделать на пинах 9 и 10 ШИМ с разрешением от 2 бит (второй параметр analogWrite в пределах 0-3) до 16 бит  (второй параметр analogWrite - 0-65 535).

Вот код.

inline void pwmInit(const int8_t bitRate = 16) {
	noInterrupts();
	TCCR1A = bit(COM1A1) |  bit(COM1B1) | bit(WGM11);
	TCCR1B = bit(WGM13) | bit(WGM12) | bit(CS10);
	TCCR1C = TCNT1 = OCR1A = OCR1B = TIMSK1 = TIFR1 = 0;
	ICR1 = (1u << bitRate) - 1u;
	interrupts();
}
inline void pwmWrite9(const uint16_t v) { DDRB |= bit(PB1); OCR1A = v; }
inline void pwmWrite10(const uint16_t v) { DDRB |= bit(PB2); OCR1B = v; }

Функцию pwmInit нужно вызвать один раз (например, в setup). Её параметр - разрядность ШИМ. Может быть от 2 до 16. Функция не проверяет корректность параметра - на Вашей совести.

После этого можно вызывать функции pwmWrite9 (для 9-го пина) и/или pwmWrite10 (для 10-го) пина и устанавливать скважность точно также, как мы вызываем analogWrite. Значение 0 - сигнала нет, максимальное значение - сигнал сплошной.

Допустимые максимальные значения (как и частота ШИМ) зависят от разрядности (параметра, который передавали функции pwmInit).

Вопросы по коду программы:

- Входной параметр функции определен как константа, для чего это предусмотрено?

const int8_t bitRate = 16

- Можно менять частоту ШИМ на меньшую, чем указано в таблице и как ?

- Какая расчетная формула  для частот ШИМ  в таблице?

- Что означает "1u"  в строке 6 ?

 

 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Gregor-R пишет:

- Входной параметр функции определен как константа, для чего это предусмотрено?

const int8_t bitRate = 16

Указание компилятроу на то, что я не собираюсь изменять значение bitRate внутри функции. В большинстве случаев не влияет ни на что, но в редких случаях помогает ему (компилятору) в оптимизации кода. Заведите себе хорошую привычку: если объект реально константа - не скрывать это от компилятора. Нет-нет, да принесёт пользу.

Gregor-R пишет:

- Можно менять частоту ШИМ на меньшую, чем указано в таблице и как ?

Можно. При помощи делителя частоты таймера. Но тогда, само собой,  поползёт таблица - её нужно будет пересчитывать.

Делитель частоты задаётся битами CS1x регистра TCCR1B. У меня в строке 4 задано CS10 == 1, а остальные (CS11 и CS12) оставлены нулями. Это соответсвует делителю, равному 1. Если нужно делить частоту (до 1024), то надо задать правильную комбинацию битов (см. таблицу 16-5 в даташите).

Gregor-R пишет:

- Какая расчетная формула  для частот ШИМ  в таблице?

Формула на стр. 125 даташита вверху при N=1 (это делитель частоты). В принципе, вот здесь можете взять готовый Excel, там есть эта таблица. Там же можно ввести другой делитель частоты и всё пересчитается.

Gregor-R пишет:

- Что означает "1u"  в строке 6 ?

u или U после числа (без пробела) означает, что число беззнаковое (unsigned).

 

GAMON
Offline
Зарегистрирован: 03.07.2017


 Пытался переделать исходный текст программы под ATmega2560. Но не смог. "Ошибка компиляции для платы Arduino/Genuino Mega or Mega 2560."

Укажите, пожалуйста, на ошибки

#ifndef  FlexPWM_h
#define FlexPWM_h

#define __bit(b) (1u << (b))

inline void pwmInit(const int8_t bitRate = 16) {
  noInterrupts();
  TCCR1A = __bit(WGM11);
  TCCR1B = __bit(WGM13) | __bit(WGM12) | __bit(CS10);
  TCCR1C = TCNT1 = OCR1A = OCR1B = TIMSK1 = TIFR1 = 0;
  ICR1 = (1u << bitRate) - 1u;
  interrupts();
}

inline void pwmWrite11(const uint16_t v) { 
  DDRB |= __bit(PB5);
  TCCR1A |= __bit(COM1A1);
  OCR1A = v;
}

inline void digitalWrite11(const uint8_t v) {
  DDRB |= __bit(PB5);
  TCCR1A &= ~__bit(COM1A1); 
  if (v) PORTB |= __bit(PB5); else PORTB &= ~__bit(PB5);
}

inline void pwmWrite12(const uint16_t v) {
  DDRB |= __bit(PB6);
  TCCR1A |= __bit(COM1B1);
  OCR1B = v;
}

inline void digitalWrite12(const uint8_t v) {
  DDRB |= __bit(PB6);
  TCCR1A &= ~__bit(COM1B1);
  if (v) PORTB |= __bit(PB6); else PORTB &= ~__bit(PB6);
}

#endif  // FlexPWM_h

 

Jeka_M
Jeka_M аватар
Offline
Зарегистрирован: 06.07.2014

GAMON пишет:

Укажите, пожалуйста, на ошибки

Тебе компилятор указал на конкретную ошибку в конкретной строке, читай лог полностью.

GAMON
Offline
Зарегистрирован: 03.07.2017

Jeka_M пишет:

GAMON пишет:

Укажите, пожалуйста, на ошибки

Тебе компилятор указал на конкретную ошибку в конкретной строке, читай лог полностью.

collect2.exe: error: ld returned 1 exit status

 

 Если это - что сие означает?

Jeka_M
Jeka_M аватар
Offline
Зарегистрирован: 06.07.2014
GAMON
Offline
Зарегистрирован: 03.07.2017

 Может быть связь с тем, что на входе стоит CH340?

Другие программы норм работают; без проблем

Jeka_M
Jeka_M аватар
Offline
Зарегистрирован: 06.07.2014

Нет, это вообще не при чём. Почитайте внимательно по тем ссылкам, что я дал.

GAMON
Offline
Зарегистрирован: 03.07.2017

Jeka_M пишет:

Нет, это вообще не при чём. Почитайте внимательно по тем ссылкам, что я дал.

 Я прочитал. Я hardware; начну лазить и удалять всякие .lto - вообще завалю все)

Спасибо за оперативный консалт

Jeka_M
Jeka_M аватар
Offline
Зарегистрирован: 06.07.2014

Для начала попробуйте заменить ld.exe из другой версии Arduino IDE (более старой или более новой)

CatSoup
Offline
Зарегистрирован: 01.05.2017

Интересные осциллограммы, однако я снимал прямую линию с этим кодом при 100% коэфф. заполнения (обозвал его яркостью по аналогии с либой pwm):

void setPWM (unsigned int frequency, unsigned int brightness){
  unsigned long halfavrfreq = 8000000;
  unsigned long top = 0;
  unsigned long regA = 0;
  unsigned long regB = 0;
  unsigned long tick = 0;
  top = halfavrfreq/frequency;
  ICR1=top;
  tick = top*brightness/100;
  regA = tick-1;
  regB = top-tick;
  OCR1A=regA; 
  OCR1B=regB;
}

 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Ну, так не должно быть. Это противоречит и даташиту и здравому смыслу. Я тоже видел прямые линии пока не поставил редим peak detect на осциллографе.

Код Ваш не смотрел, надеюсь там всё нормально, но если что, могу запустить у себя.

CatSoup
Offline
Зарегистрирован: 01.05.2017

Да там все нормально, кусок просто выдрал из скетча, инициализация в другом месте осталась)). Я делал 2х каналку - один счетчик включался по счету вверх выше значения регистра сравнения и отключался при счете вниз, другой - наоборот. Еще раз пролистал сабж и даташит - в сухом остатке мы имеем то, что при здании регистру сравнения топового значения, остаются щелчки на частоте, зависящей от опорной и количества попугаев в регистре TOP. Я их не видел, но раз Вы говорите что они есть и без peak-detect их не видно - то я их и не увижу, мой осцил старше меня. А если регистру сравнения здать нулевое значение, то при включении на счете вверх же ничего не должно щелкать, должен быть полный "0"? Почему задал вопрос - при изменении чстоты/скважности "на ходу", даже в последовательности обнулить регистры - подождать микросекунду - задть новые значения иногда бывают непредсказуемые "броски" с большой скважностью. Для большинства применений на это, конечно, можно было бы забить, но хорошие IGBT дороговты)

nickavia22
Offline
Зарегистрирован: 11.07.2018

Добрый вечер. Читаю все Ваши коменты и решил задать вопрос о замере сигнала датчика давления 50гц и преобразовании в 25гц с темже процентом шима.

Заранее благодарю   Николай

mail: nickavia@ukr.net    Очень нужно.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

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

nickavia22
Offline
Зарегистрирован: 11.07.2018

 

Добрый вечер. Датчик давления кондиционера видаёт сигнал 12вольт 50 герц с шимом 5-95% в зависимости от давления. Нужно преобразоать сигнал в 25 герц с шимом 5-95%

Тоесть шим в % должен быть такой  как в датчике давлания. С уважением Николай.