Этюды для начинающих: метроном не загружая таймер

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

Коллеги, продолжим наши игры. Простите. что много букв, я рассчитываю на совсем начинающих и пишу подробно, чтобы было понятно.

Сегодня мы поговорим о том, как организовать в программе «метроном» не занимая дефицитных в AVR таймеров/счётчиков.

Все мы знаем любимый пример «Blink без delay». Основная его слабость в том, что опрос millis() происходит в функции loop(), частота вызова которой, мягко говоря, нестабильна. Как часто будет вызываться loop() зависит от того, как долго она будет работать. Например, если где-то в ней имеется, скажем, «delay(100500)», то будьте спокойны, loop() застынет на сто с половиной секунд, и за это время не будет вызвана ни разу. Что произойдёт с бедным мигающим светодиодом в эти сто с половиной секунд? Правильно, будет оставаться в том состоянии, в котором был в момент вызова delay(). А как же мигание? А никак!

Пример с мигающим диодом не единственен. Например, поймав прерывание от нажатой кнопки, я хочу выждать, скажем, 20мс и глянуть, всё ли она ещё нажата, или то «нажатие» было просто прилетевшей помехой. Или, я хочу выждать, скажем, одну секунду, чтобы дать Wi-Fi модулю установить соединение или там дать пару секунд датчику температуры, чтобы он «согрелся», да мало ли что. Короче, часто бывает нужно отрабатывать регулярные или одиночные паузы и при этом быть уверенным, что пауза не затянется на минуты, если вдруг где-то встретился блокирующий ввод данных от юзера или пресловутый «delay(100500)». Отрабатывать паузы при помощи delay() можно, но часто это создаёт проблемы - эти проблемы общеизвестны и я не буду на них останавливаться.

Правильно было бы отрабатывать такие паузы таймерами. Там и точность повыше, чем в millis() (millis() меняет своё значение один раз в 1024 микросекунды, таймером же можно получить интервал с точность в 1/16 микросекунды), да и понадёжнее всё – никакой delay(100500) не помешает. Но здесь тоже не всё «слава Богу». Как известно, в ATMega328 имеется всего три таймера/счётчика. Нулевой таймер в ардуино занят системными делами – он обслуживает millis() и ей подобные функции, и его лучше не трогать – всё сломается. Два других относительно свободны. Относительно потому, что они тоже заняты, но не столь критично – как только Вы занимаете один из них, тут же теряете ШИМ и tone() на определённых ногах :(.

Как-то у меня в одном проекте были задействованы микросхема TLC5940 (её библиотека требует таймера для организации последовательного обмена) и что-то сидящее на VirtualWire, и это занимало второй таймер. А паузы-то, о которых я говорил выше, нужны! И что делать?

Родилась идея задействовать нулевой (сстемный) таймер без его переконфигурации. Т.е. не трогать его делитель частоты, не лезть в его режимы - никак его не обижать, а просто воспользоваться тем фактом, что в системе он настроен на прерывание по переполнению с делителем 64. Другими словами, в системе уже есть функция, которая вызывается точно через каждые 1024 микросекунды (именно она меняет значение millis()). Так почему бы не вставить в эту функцию вызов некой моей функции и получить таким образом «метроном» - функцию в моём скетче, которая вызывалась бы регулярно и никак не зависела бы ни от delay'ев, ни от блокирующих операций ввода/вывода. С такой функцией можно было бы решить все вопросы по одиночным и регулярным паузам, не трогая конфигурации таймера 0 и вообще не прикасаясь к двум другим таймерам.

Благо для этого требуется всего лишь добавить ровно три строки в два системных файла. Изменения совершенно безопасные и никак не влияющие на работу системы, если «метроном» не пользоваться.

Так, если всё ещё интересно, приступаем к изменению системных файлов (можете сделать страховочные копии от греха подальше).

Менять будем файлы «wiring.c» и «Arduino.h». Живут они в директории:

<где установлена IDE>\hardware\arduino\avr\cores\arduino

Нашли? Поехали …

1
Открываем файл «Arduino.h» в каком-нибудь редакторе и в любое место, где допустимо определение переменной (например, перед самым последний #endif в самом конце файла), добаляем строчку:

extern void (* Timer0_Hook)(const unsigned long);

Закрываем «Arduino.h», он нам больше не нужен.

2
Открываем файл «wiring.c» в каком-нибудь редакторе и ищем в нём ключевое слово TIMER0_OVF_vect. Оно встречается там один раз вот в таком контексте:

#if defined(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
ISR(TIM0_OVF_vect)
#else
ISR(TIMER0_OVF_vect)
#endif
{
	// copy these to local variables so they can be stored in registers
	// (volatile variables must be read from memory on every access)
	unsigned long m = timer0_millis;
	unsigned char f = timer0_fract;

	m += MILLIS_INC;
	f += FRACT_INC;
	if (f >= FRACT_MAX) {
		f -= FRACT_MAX;
		m += 1;
	}

	timer0_fract = f;
	timer0_millis = m;
	timer0_overflow_count++;
}

Перед «#if defined …» вставляем строчку

void (* Timer0_Hook)(const unsigned long) = NULL;

а перед закрывающей фигурной скобкой в самом низу приведённого выше контекста вставляем строчку:

if (Timer0_Hook) Timer0_Hook(timer0_millis);

В результате кусочек, приведённый выше, стал таким:

void (* Timer0_Hook)(const unsigned long) = NULL;

#if defined(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
ISR(TIM0_OVF_vect)
#else
ISR(TIMER0_OVF_vect)
#endif
{
	// copy these to local variables so they can be stored in registers
	// (volatile variables must be read from memory on every access)
	unsigned long m = timer0_millis;
	unsigned char f = timer0_fract;

	m += MILLIS_INC;
	f += FRACT_INC;
	if (f >= FRACT_MAX) {
		f -= FRACT_MAX;
		m += 1;
	}

	timer0_fract = f;
	timer0_millis = m;
	timer0_overflow_count++;
	if (Timer0_Hook) Timer0_Hook(timer0_millis);
}

Сохраняем и этот файл.

Теперь надо попробовать скомпилировать какой-нибудь скетч – хоть пустой. Если Вы нигде не ошиблись, то он должен скомпилироваться нормально. Если не компилируется, ищите, что Вы сделали не так.

Итак, что же мы сделали? Сделали мы следующее:

1. Добавили указатель на функцию типа void <имя функции> (const unsigned long);

2. Имя указателя Timer0_Hook. Изначально он равен нулю;

3. В обработчик прерывания по переполнению таймера 0 мы добавили проверку: если Timer0_Hook равен нулю – ничего не делать, а если он не равен нулю, то вызвать функцию на которую он указывает, передав ей текущее значение millis() в качестве параметра.

Теперь, мы можем в своём скетче определить любую функцию типа void <имя функции> (const unsigned long); и, если мы присвоим её адрес указателю, то она будет «волшебным образом» вызываться каждые 1024 миллисекунды, плюя при этом на delay(100500), на блокирующие операции ввода/вывода и почти на все остальные задержки! Вот, примерно так:

void p(const unsigned long) {
}

void setup() {
	Timer0_Hook = p;	//	Установим адрес нашей hook-функции
}

void loop() {}

и функция p() уже вызывается! Правда, ничего не делает :)

Прежде, чем мы начнём экспериментировать с нашим «системным хаком», давайте проверим. Действительно ли всё работает так, как мы ожидаем. Заодно, кстати, и проверим насколько она «плюёт» на delay().

Для этого напишем скетч в котором наша хук-функция будет всего лишь увеличивать счётчик на 1 при каждом своём вызове. А мы будем печатать (и сбрасывать в 0) значение счётчика каждые 10240 миллисекунд. Если наша функция действительно вызывается каждые 1024 микросекунды, то за 10240 миллисекунд счётчик должен стать 10000.

//
//	Эта строка нужна для потокового вывода в Serial (см. мой предыдущий этюд)
//
template <typename T> inline Print & operator << (Print &s, T n) { s.print(n); return s; }

//
//	Счётчик вызовов функции p()
//
unsigned long counter = 0;

//
// Hook-функция.
//	Вызывается каждые 1024 микросекунды (в чём мы и хотим убедиться)
//	Параметр нам здесь не нужен, поэтому описываем его пустым
//
void p(const unsigned long) {
	counter++;
}

//
//	Инициализация
//
void setup() {
	Timer0_Hook = p;			//	Установим адрес нашей hook-функции
	Serial.begin(115200);
}

void loop() {
	//	Ждём 10240 миллисекунды, а затем печатаем счётчик (должен быть 10000)
	//	и инициализируем подсчёт снова
	delay(10240ul);
	Serial << "Counter=" << counter << ";\n";
	counter = 0;
}

Результат, как и следовало ожидать:

Counter=10000;
Counter=10000;
Counter=10000;
...

Пояснений по скетчу не требуется?

Поехали экспериментировать.

Первый пример – «мигаем без delay()» а на саом деле, так даже «мигаем во время delay» - так будет правильнее.

Здесь мы воспользуемся тем фактом, что параметром нашей хук-функции является ни что иное. как текущее значение mills()! Поэтому мы не будем подсчитывать вызовы, а просто будем пользоваться millis() как обычно.

Итак, мигаем светодиодом с периодом в 100мс в то время, как программа стоит в delay'е.

#define	HALF_PERIOD	50

//
// Hook-функция.
//
void BlinkIt(const unsigned long currentMillis) {
	static unsigned long oldMillis = 0;
	if (currentMillis - oldMillis >= HALF_PERIOD) {
		digitalWrite(LED_BUILTIN, ! digitalRead(LED_BUILTIN));
		oldMillis = currentMillis;
	}
}

void setup() {
	pinMode(LED_BUILTIN, OUTPUT);
	Timer0_Hook = BlinkIt;
}

void loop() {
	delay(100500ul);
}

Следующий пример: мигаем двумя светодиодами. На пине 13 с периодом 100мс и на пине 2 с периодом 666мс.

#define	HALF_PERIOD1	50
#define	HALF_PERIOD2	333

#define LED_2	2

//
// Hook-функция.
//
void BlinkIt(const unsigned long currentMillis) {
	static unsigned long oldMillis1 = 0;
	if (currentMillis - oldMillis1 >= HALF_PERIOD1) {
		digitalWrite(LED_BUILTIN, ! digitalRead(LED_BUILTIN));
		oldMillis1 = currentMillis;
	}

	static unsigned long oldMillis2 = 0;
	if (currentMillis - oldMillis2 >= HALF_PERIOD2) {
		digitalWrite(LED_2, ! digitalRead(LED_2));
		oldMillis2 = currentMillis;
	}
}


void setup() {
	pinMode(LED_BUILTIN, OUTPUT);
	pinMode(LED_2, OUTPUT);
	Timer0_Hook = BlinkIt;
}

void loop() {
	delay(100500ul);
}

На этом примере стоит остановится. Нам пришлось практически дважды дублировать идентичный код в функции BlinkIt. Это не есть хорошо, но деваться некуда. Ведь наша хук-функция всего лишь вызывается с известной нам периодичностью! И всё! Кроме этого, никаких чудес! Всё остальное ручками.

Возникла идея, основываясь на данном трюке, написать "библиотеку" для работы с задержками.

Получилось вот что: по сути в библиотеке есть одна функция:

const bool CallBack::Add(
    const unsigned long ms, 
    const unsigned long (*callback)(const unsigned long, const void *), 
    const void *userData = NULL) 

возвращает она false, если всё в порядке и true, если возникла ошибка.

Функция CallBack::Add говорит системе, что через ms (первый параметр) миллисекунд необходимо вызвать функцию callback (второй параметр) и передать ей текущее значение millis и некое значение userData (третий параметр). Последний параметр опциональный. Если передавать ничего не нужно, его можно опускать.

Когда система вызовет пользовательскую функцию callback та, отработав, может вернуть одно из трёх значений:

1) константу CallBack::STOPIT - значит, она требует больше её не вызывать
2) 0 (число 0) - значит, она требует вызвать её ещё раз через тот же временной интервал, через который её вызывали в этот раз.
3) положительное число - требует вызвать её ещё раз через указанное количество миллисекунд.

Можно вызывать CallBack::Add несколько раз с разными значениями. Все запросы на вызов функций будут накапливаться, и соответствующие функции будут вовремя вызываться. 

Всего допускается до 16 одновременно ожидающих запросов (константу 16 можно изменить в библиотеке). Если максимальное количество запросов превышено, функция CallBack::Add вернёт true, что является сигналом об ошибке.

Если некоторая callback-функция вернёт CallBack::STOPIT (попросит больше её не вызывать), она будет удалена из списка запросов и в списке освободится место для другого запроса.

Текст библиотеки помещён ниже. Для того, чтобы установить библиотеку надо:

1. через меню «Файл|Настройки» в IDE узнать расположение папки скетчей (там оно сверху написано)
2. В папке скетчей найти (если нет, то создать) подпакпу libraries
3. в подпапке libraries создать подпапку Callback
4. сохранить в подпапке Callback два файла Callback.h и Callbac.cpp (приведены ниже)
5. перезапустить IDE

Всё. библиотекой можно пользоваться.

Текст файла Callback.h

#ifndef	_CALLBACK_H
#define	_CALLBACK_H

/*
Для того, чтобы библиотека работала, необходимо внести изменения
в файлы arduino.h и wiring.c
находятся они в папке <корень IDE>\hardware\arduino\avr\cores\arduino

В файл arduino.h необходимо добавить объявление:

	extern void (* Timer0_Hook)(const unsigned long);

В файл wiring.c ВЫШЕ функции ISR(TIMER0_OVF_vect) необходимо вставить объвление

	void (* Timer0_Hook)(const unsigned long) = NULL;

	а последней строкой функции ISR(TIMER0_OVF_vect) прямо перед закрывающей 
	фигурной скобкой необходимо вставить строку

	if (Timer0_Hook) Timer0_Hook(timer0_millis);
*/

#ifndef	TOTAL_SIMULTANEOUS_CALLBACKS
#define TOTAL_SIMULTANEOUS_CALLBACKS	16
#endif	//	TOTAL_SIMULTANEOUS_CALLBACKS

class CallBack {
public:
	enum { STOPIT = 0xffffffff };
	static const bool Add(const unsigned long ms, const unsigned long (*callback)(const unsigned long, const void *), const void *userData = 0);

private:
	CallBack(const unsigned long ms, const unsigned long (*callback)(const unsigned long, const void *), const void *userData);
	static void AddRequest(CallBack *req);
	static void InterruptHook(const unsigned long currentMillis);

	static CallBack * array [TOTAL_SIMULTANEOUS_CALLBACKS];
	static short m_ptr;
	static bool Error;

	unsigned long m_period, m_startmillis;
	const unsigned long (*m_callBack)(const unsigned long, const void *);
	void * m_userData;
};

#endif	//		_CALLBACK_H

Текст файла Callback.cpp

#include "CallBack.h"
#include <arduino.h>

const bool CallBack::Add(const unsigned long ms, const unsigned long (*callback)(const unsigned long, const void *), const void *userData) {
	AddRequest(new CallBack(ms, callback, userData));
	return Error;
}

CallBack::CallBack(const unsigned long ms, const unsigned long (*callback)(const unsigned long, const void *), const void *userData) {
	m_period = ms;
	m_startmillis = millis();
	m_callBack = callback;
	m_userData = (void *) userData;
}

void CallBack::AddRequest(CallBack *req) {
	if (!Timer0_Hook) Timer0_Hook = InterruptHook;
	if (! (Error = (m_ptr >= TOTAL_SIMULTANEOUS_CALLBACKS))) {
		cli();
		array[m_ptr++] = req;
		sei();
	}
}

void CallBack::InterruptHook(const unsigned long currentMillis) {
	for (short i=0; i < m_ptr; i++) {
		CallBack * cb = array[i];
		if (currentMillis - cb->m_startmillis >= cb->m_period) {
			const unsigned long ulRet = cb->m_callBack(currentMillis, cb->m_userData);
			if (ulRet == STOPIT) {
				cli();
				delete cb;
				const short nRightElements = m_ptr-- - i - 1;
				if (nRightElements) memcpy(array+i, array+i+1, sizeof(CallBack *) * nRightElements);
				i--;
				sei();
			} else {
				cb->m_startmillis = currentMillis;
				if (ulRet) cb->m_period = ulRet;
			}
		}
	}
}

CallBack * CallBack::array [TOTAL_SIMULTANEOUS_CALLBACKS];
short CallBack::m_ptr = 0;
bool CallBack::Error = false;

Теперь несколько примеров использования библиотеки:

На втором пине мигаем с периодом 100ms, на 13-ом пине - с периодом 1с. Обратите внимание - используется ОДНА хук-функция. Ей просто передаётся пользовательский параметр (userData) который собственно и есть номер пина, на котором надо переключить светодиод. А возвращает она всего 0 - т.е. предлагает вызвать её ещё раз через тот же интервал. Таким образом одна и та же функция используется для двух светодиодов. мигающих с разными частотами!

#include <CallBack.h>

const unsigned long BlinkIt(const unsigned long currentMillis, const void * vLed) {
	digitalWrite((int)vLed, ! digitalRead((int)vLed));
	return 0;
}

#define	LED_2		2
#define	BLINK_DELAY1	500ul
#define	BLINK_DELAY2	50ul

void setup() {
	pinMode(LED_BUILTIN, OUTPUT);
	pinMode(LED_2, OUTPUT);
	CallBack::Add(BLINK_DELAY1, BlinkIt, (const void *)LED_BUILTIN);
	CallBack::Add(BLINK_DELAY2, BlinkIt, (const void *)LED_2);
}

void loop() {
	delay(100500);
}

Мигаем одним светодиодом с периодом 1000мс и скважностью 20. Здесь используется возврат положительного числа из хук-функции, чтобы добиться вызовов через различные промежутки времени.

#include <CallBack.h>

#define	OFF_TIME	950ul
#define	PERIOD	1000ul

const unsigned long BlinkIt(const unsigned long currentMillis, const void *) {
	static unsigned long lastDelay = OFF_TIME;
	digitalWrite(LED_BUILTIN, ! digitalRead(LED_BUILTIN));
	return lastDelay = PERIOD - lastDelay;
}

void setup() {
	pinMode(LED_BUILTIN, OUTPUT);
	CallBack::Add(OFF_TIME, BlinkIt);
}

void loop() {
	delay(100500ul);
}

Включаем светодиод на 10 секунд, а затем гасим его. Здесь используется CallBack::STOPIT, чтобы хук-функция больше не вызывалась.

#include <CallBack.h>

const unsigned long SwitchOff(const unsigned long currentMillis, const void *) {
	digitalWrite(LED_BUILTIN, LOW);
	return CallBack::STOPIT;
}

void setup() {
	pinMode(LED_BUILTIN, OUTPUT);
	digitalWrite(LED_BUILTIN, HIGH);
	CallBack::Add(10000ul, SwitchOff);
}

void loop() {
	delay(100500ul);
}

При запуске мигаем светодиодом 5 раз, а затем успокаиваемся

#include <CallBack.h>

//
//	Мигнуть 5 раз и на этом успокоиться
//

const unsigned long BlinkIt(const unsigned long currentMillis, const void *) {
	static int counter = 10;	//	10 потому что одно минагие - это два действия: включить и выключить
	digitalWrite(LED_BUILTIN, ! digitalRead(LED_BUILTIN));
	return (--counter) ? 0 : CallBack::STOPIT;
}

void setup() {
	pinMode(LED_BUILTIN, OUTPUT);
	CallBack::Add(1000, BlinkIt);
}

void loop() {
	delay(100500ul);
}

----------------------

Два заключительных замечания.

Первое: в общем случае менять системные файлы не самая лучшая идея. Здесь я это сделал и получил большую пользу. Однако, я никого не призываю так поступать. Те, кто это делают, делают это на свой страх и риск. Кто боится - восстановите в оригинальное состояние файлы и забудьте об этом посте. Сам я этим пользуюсь, но это мой выбор. Ваш выбор за Вами.

Второе: все упомянутые здесь хук-функции вызываются в контексте обработки прерывания, потому должны выполняться очень быстро. Боже упаси использовать в них delay или ещё что-нибудь подобное.

 

 

 

 

 

 

kisoft
kisoft аватар
Offline
Зарегистрирован: 13.11.2012

IMHO, не более того:

1. CallBack::InterruptHook выполняется в контексте прерывания, использование здесь cli & sei - под большим вопросом.

2. callback - напрашивается сделать typedef, а то в двух местах (параметр конструктора и член класса) одно и тоже. Лучше по феншую.

3. Это всё хорошо для ненагруженных систем, плюс обработчик, как было справедливо замечено не должен делать что то сложного и тяжелого (кроме delay, также, не выводить в Serial или на дисплей, не должен ждать флагов, никаких затяжных циклов).

4. Уже совсем мои тараканы (не настаиваю!):

4.1. Использование new & delete. Я бы предпочел статические структуры (которые содержат динамическу составляющую статического класса CallBack). Плюс в структуре держать флаг активности, т.е. не удалять ничего, от слова совсем. Это надежней.

4.2. memcpy - геморрой, можно наступить на грабли. На мой взгляд см. п.4.1. флаг - надежней.

PS Кстати, есть клевая функция memmove, правда она чутка тяжелее, зато она сама решает копировать область с начала или с конца (кто знает, тот поймет). Часто пользуюсь, когда нужно сдвинуть или раздвинуть часть массива.

Если не согласен, могу удалить своё сообщение ;)

 

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

Ну, с чем-то я согласен, с чем-то не очень, мы могли бы подискутировать, но ... мы ведь оба понимаем о чём речь и в общем-то смысла в дискуссии особого нет. Плюсы и минусы как того. что написано у меня, так и того, что Вы прделожили известны и Вам и мне - ничего нового из дискуссии ни я, ни Вы не узнаем. Так ведь?

Удалять - ни в коем случае. Пусть люди читают и делают свои выводы. Кто-то так воспримет, кто-то эдак - и то и другое лучше, чем никак.

А вот как можно удалить своё сообщение, Вы меня научите, я что-то такого не вижу на сайте. Плохо смотрю или Вы просто модератор?

Logik
Offline
Зарегистрирован: 05.08.2014

Сама идея использовать таймер0 для формирования временных интервалов - великолепно!

А вот предоставлять возможность вызова из обработчика прерывания функцию по калбеку - очень стремно. Будет масса вопросов почему мой опрос датчика температуры, запись в файл, вывод на екран и т.д. из калбека не работают и вообще все валится. И хоть трижды пиши что так делать низзя - будут делать. Ведь моргание светодиодом как правило не самоцель, а средство изучения способов формирования временных интервалов для каких либо более сложных операций. А тут выходит на светодиоде натренировались, а опросить, к примеру ds18b20 как любят - 1 раз в секунду, не получается. 

Давайте уж четко выпишим требования к функции для калбека. Я так поимаю гдето следующее выходит: выполнятся быстрей 1мс, не содержать ожиданий прерываний, не содержать команд разрешения прерывания... Что ещё? И главное, какие классы соответствуют требованиям? Боюсь окажется возможным только ногодрыг и операции с глобальными переменными.

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

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

ПС. Этюды у Вас хороши, но они явно не для новичка.

 

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

Logik, ну, низзя так забижать delay! :)))

Есть два соображения по которым он в моих loop'ах уместен и даже желателен.

1. В начале поста говорилось, что любимый "Блинк без делей" сломается, если в другом месте программы delay таки встретится, а здесь мол, при таком подходе, delay пофиг. Так что в примерах просто подтверждается эта мысль и показывается, что всё действительно работает, несмотря на гигантский delay.

2. delay(100500) создаёт точку комфорта для читающего newbie. Вот читает человек про какие-то перывания, хуки и прочие "контексты" - ничего не понятно и уже хочется выкрикнуть бессмертное "неприличными словами не выражаться!", а тут вдруг delay(100500) - ура! Хоть что-то знакомое! Это как у Высоцкого: "Он сделал ход e2 на e4! Чтой-то мне знакомое, так-так!" :)))

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

Logik
Offline
Зарегистрирован: 05.08.2014

ЕвгенийП пишет:

Logik, ну, низзя так забижать delay! :)))

Конешно низзя забижать - тока гнобить :)))

ЕвгенийП пишет:

Считаю, что любой инструмент полезен и уместен когда используются правильно и для тех работ, для которых он (инструмент) предназначен. Это касается абсолютно любого инструмента - молотка, адронного коллайдера или там того же делэя. Если интрумент используется по назначению и с головой, то он полезен и уместен. 

Да. Только инструмент надо использовать не с головой а по назначению. Открыв блинк человек решает что делей - для формирования временных интервалов в скетче. Пишет мегаскетч по автоматизации и рожает тему "многопоточность" или "как склеить ужа и ежа в один скетч". Его пихают в блинк без делея, на что он справедливо возмущается какого сразу учили не так. Но этому холивару много лет и он тут не к месту. ИМХО - например в лупе вечный цикл с serial.println( чегототам намного интересней чем делей100500.

Намного интересней первый вопрос - что разрешено в функции вызываемой из калбека. Особенно в контексте базовых либ. Возвращаясь к аналогии с инструментами - дали новый инструмент, так скажите что им можно делать, чего нельзя. Ну или коллективным разумом определимся. Чтоб не методом тыка каждый для себя определял. А то ведь от такой "болгарки" и пальцев не останется :)))

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

Logik пишет:

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

Не знаю Вашего возраста, потому не могу судить, будет ли то, что я расскажу для Вас новостью, или просто воспоминанием, но таки расскажу – может для кого-то будет новостью.

В древние, легендарные времена, о которых сейчас даже нет единого мнения, а были ли они на самом деле или это просто легенды – т.е. в то время, когда Интернет уже был, а протокола HTTP ещё не было, для общения крайне немногочисленного сетевого сообщества использовались в основном два протокола – FIDO и NNTP. Приверженцы второго называли адептов первого «FIDOrasy», а фанаты FIDO расшифровывали аббревиатуру NNTP как «Ne, Nu Tochno Pidory».

В каждой из сред сложилась своя субкультура и свой сленг и потому FIDO'шника от NNTP'шника можно было отличить в реале буквально с первых же фраз по специфическим словечкам и выражениям.

Но было одно выражение (вернее одна аббревиатура) которое эти движения объединяла. Новичкам, которые лезли с бесконечными вопросами как и что, в обеих сетях всегда отвечали кратким «RFM» (вариант – «RTFM»), что было просто аббревиатурой от аглицой фразы «Read the Fucking Manual!».

Так вот, пока что этот пост комментируют только люди вроде Вас и kisoft, т.е. люди, которым не нужно объяснять что можно, а что нельзя делать в обработчике прерывания. Если же вдруг сюда забредёт тот, кому это объяснять нужно, я предлагаю не рожать всем всем миром, то, что уже давно родили до нас, а как в старые, добрые, теперь уже легендарные времена, сказать:

RTFM!

Logik
Offline
Зарегистрирован: 05.08.2014

Да Вы не нервничайте так. Даже 72 шрифт линка не поможет скрыть тот факт, что предложенное Вами решение не позволяет использовать стандартные библиотеки.  И этот факт сильно снижает его полезность.

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

Да, решение лежащее на поверхности. Где-то ещё в июле, как только выпустил свою первую версию именно так и наклепал в обработчик переполнения таймера для вставки timeout callback-ов  (на cyber-place - начало обсуждения подсчета длительности импульсов через PCINT2 на меге2560) .. но выкладывать в "общий доступ" так и не стал именно из-за таких вот соображений, что если есть возможность завешать систему, да ещё и при закрытых прерываниях - будьте уверены, именно это и сделают. Что сам же и сотворил при первых использованиях и попытках замерять дальность через прерывания. Отказался. Если мне, разрабу, это удалось, то другим и подавно, что и было обнаружено в переписке с пользователями при консультациях... там же на кибер-плэйсе, даже какой-то ликбез по прерываниям пытался начать..

Сейчас реализованы таймауты через .. угу, проверку их корректности в той же самой loop() вполне хватает и доходчивао понятно большинству ..

имхо: нефиг лезть в обработчики прерываний за ненадобностью.

kisoft
kisoft аватар
Offline
Зарегистрирован: 13.11.2012

А ещё есть yield и тогда delay не проблема. Хотя это уже не для новичков.

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

Я не стал реализовывать yield() в своей версии Wiring. Есть 3 способа реализации автоматного стиля программирования и построения конечных автоматов практически произвольной сложности .. смысл в yield()? если не пользуем delay()? :)

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

Logik пишет:

Да Вы не нервничайте так. Даже 72 шрифт линка не поможет скрыть тот факт, что предложенное Вами решение не позволяет использовать стандартные библиотеки.  И этот факт сильно снижает его полезность.

С какого это перепугу и какие именно библиотеки оно не позволяет использовать?

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

kisoft пишет:
А ещё есть yield и тогда delay не проблема. Хотя это уже не для новичков.

Он используется в штатном delay.

kisoft
kisoft аватар
Offline
Зарегистрирован: 13.11.2012

ЕвгенийП пишет:

kisoft пишет:
А ещё есть yield и тогда delay не проблема. Хотя это уже не для новичков.

Он используется в штатном delay.


Так я про это и говорю

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

Предлагаю ещё дополнить обработчик таймера "защелкой", не позволяющей запускать хук-функцию, если она уже была вызвана и открывать прерывания перед её запуском:

// Защелка там же, где объявлена переменная хука перед обработчиком:
uint8_t    Timer0_isrunning = 0;

// в обработчике расширить так:

if( Timer0_Hook && !Timer0_isrunning ) {
    Timer0_isrunning = 1;
    sei();          // тут конечно лучше восстанавливать пред. состояние
    Timer0_Hook(timer0_millis);
    Timer0_isrunning = 0;
    cli();
}

Это позволит запускать ваши функции, и не останавливать подсчет времени, ценой двойного сохранения регистров обработчика на стеке. В случае, если хук отрабатывает "долго" - таймер остается рабочим, но повторного вызова не происходит. Пред. описание защелки можно не втыкать в Arduino.h, в этом случае она будет недоступна из скетча, тем кто в исходники не лазит и "не читатель". :)

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

Arhat109-2,

это чревато. Хандлер прерываний должен работать быстро - этого и надо требовать, а не ставить костыли для исключений.

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

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

Так это в любом случае "чревато", что в вашем, что в другом. Объяснять придется или одно или другое.

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

Так что "объяснять" остается только один момент: что хук вызывается, но ровно один раз пока не завершится его предыдущая отработка.

В вашем релизе потерять прерывание куда как проще. И требование "очень быстро" - ни о чем. Формально должно быть "быстрее чем 1024 - 3.5" микросекунды (время отработки самого обработчика + вывоз хука) .. что в общем случае потребует от прогера вычислений "а сколько же оно отрабатывается на самом деле" .. что для программ на С/С++ весьма "проблематично". А если учесть, что работа с хуком выделена в С++ класс и хочется вызывать список функций .. то получить пропуск прерывания по переполнению - оч. легко.

Да, и ещё "момент" функции времении delayMicroseconds(), millis() анализируют флаг переполнения для того, чтобы корректно ловить ситуацию "переполнение произошло, но обработчик ещё не запущен" .. так вот в случае потери прерывания, думаю что это место в них приведет к весьма "странным" результатам в скетчах: внезапным скачкам времени "туда-сюда-обратно". Наблюдал такое, когда писал свою часть.

В целом, спасибо за напоминание этого изменения обработчика. Пожалуй верну его в свою либу в таком виде (с блокировкой хука). :)

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

Добавил в свою либу "хук с защелкой" http://arduino.ru/forum/programmirovanie/arduino-kak-konechnyi-avtomat-z...

Увы .. теперь Блинк на Мега2560 компиляется в 568 байт вместо 502-х .. Шишдисят шешть байт, аднака! :( .. может дополнить режимом компиляции, типа "с хуком и без"?

Alex013
Offline
Зарегистрирован: 16.12.2015

Уважаемые, RTFM воспринял, в меру сил :) ...

А зашёл я сюда в попытке найти решение, которое не остановит http://arduino.ru/tutorials/BlinkWithoutDelay через 50 дней, когда millis обнулиться.

Это реально моё первое сообщение здесь, и я старался быть внимательным в поиске... Но функции "поиск по форуму" почему-то не обнаружил :( ...

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

Alex013 пишет:

А зашёл я сюда в попытке найти решение, которое не остановит http://arduino.ru/tutorials/BlinkWithoutDelay через 50 дней, когда millis обнулиться.

А с какого перепугу ему останавливаться?

Alex013
Offline
Зарегистрирован: 16.12.2015

Так... RTFM таки довёл меня до понимания, что всё-таки не остановится - благодаря unsigned long currentMillis 

Но вопрос с поиском остался открытым :(

Alex013
Offline
Зарегистрирован: 16.12.2015

Ну, потому что я предположил, что при обнулении может не выполниться условие

 if(currentMillis - previousMillis > interval)

если previousMillis будет получено до переполнения а currentMillis после

...и что-то мне опять кажется, что unsigned в данном случае не спасёт...

 

inspiritus
Offline
Зарегистрирован: 17.12.2012

Евгений, Вы просто боольшущий молодец. Спасибо Вам. 

О себе. В своих программах активно и в общем успешно использую TimerOne, потому тема заинтересовала исключительно.

По материалу. На мой неискушенный взгляд предложенное - классная штука для формирования некоего аналога механизма исключений в больших системах: контролирующего процесса с навысшим приоритетом, по крайней мере я буду пробовать ее именно в этом амплуа.

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

Был бы рад видеть Ваши материалы на Вашем персональном ресурсе , во избежание риска потери.

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

Alex013,

почитайте внимательнее правила арифметических операций с беззнаковыми числами.

Впрочем, это ведь нетрудно и тупо промоделировать (вот чему я пытаюсь всех новичков учить!). Просто берём unsigned long переменную, присваиваем ей значение очень близкое к переполнению и запускаем точно такой же цикл, как в том примере, используя нашу переменную как заменитель миллиса. (разумеется в цикле мы её постоянно на 1 увеличиваем). Запускаем и смотрим как она будет переходить через переполнение. Делов-то. Щас напишу, миутку...

Ну, вот держите. Запустите и посмотрите как она переполнения проходит.

template <typename T> inline Print & operator << (Print &s, T n) { s.print(n); return s; }

void setup() {
	Serial.begin(115200);
	unsigned long millisValue = 0xfffffffful - 8;
	unsigned long previousMillis = millisValue;
	long interval = 5;

	for (int i=0; i<30; i++) {
		millisValue++;
		unsigned long currentMillis = millisValue;
		Serial << "currentMillis=" << currentMillis << "; previousMillis=" << previousMillis << "; diff=" << (currentMillis - previousMillis) << "\n";
		if (currentMillis - previousMillis > interval) {
	 	   // сохраняем время последнего переключения
	    	previousMillis = currentMillis; 
	    	Serial << "SWITCHED\n";
		}
  }
}

void loop() {
}

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

bwn
Онлайн
Зарегистрирован: 25.08.2014

Alex013 пишет:

 

Но вопрос с поиском остался открытым :(

Правый верхний угол.

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Я не понимаю, а чего все упердись в это unsigned. Что, с обычным [signed] long будет как-то по-другому?

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

Да, нет, не было бы по-другому. Просто millis возвращает unsigned, ну и зачем мешать знаковые и беззнаковые в одном флаконе?

Alex013
Offline
Зарегистрирован: 16.12.2015

Прежде всего - спасибо за разъяснения!

Извините, что продолжаю офтопить, но мысль о том, что будет проблема возникла из примера 

http://www.hobbytronics.co.uk/arduino-tutorial3-timing

И пока мне кажется, что там он всё-таки может встать... А так как в местной статье и название и методы схожие - возникло непонимание.

А поиск я не видел из-за того, что его блокировал Ghostery. Так что я, слава богу, не совсем слепой :)

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

Alex013 пишет:

И пока мне кажется, что там он всё-таки может встать... А так как в местной статье и название и методы схожие - возникло непонимание.

Что Вам кажется? Кто может сломаться? Пример с этого сайта или пример из той статьи?

Если пример с этого сайта - так Вам же дали скетч, который полностью его эмулирует (только вместо millis переменную испольует). Если запустить пример, то видно, что он отлично проходит переполнение и чему тут казаться-то, когда всё опробовано?

Что касается примера из той статьи, так он действительно сломается на переполнении - он же совсем по-другому написан. Можете сделать проверочный скетч по образу и подобию моего и убедиться. А лучше, изучите правила двоичной арифметики, чтобы понимать почему один ломается. а другой нет.

А примерчик я созраню для очередного этюда "как не надо делать". Помнится на этом самом форуме я кому-то объяснял. что написать так, чтобы сломалось в общем-то непросто, но специалисты имеются - и Вот Вы мне статью такого специалиста подкинули, спасибо.

Alex013
Offline
Зарегистрирован: 16.12.2015

Да, конечно я про "сломается" - "из того примера". Просто там я детально разобрался почему ломается, и подсознательно ожидал эту же болезнь "здесь". Я иначально пытался описать именно это, но короче - видимо не получилось, сори.

А с "того примера" уже успешно растираживали:

http://cxem.net/arduino/arduino5.php

http://full-chip.net/arduino-proekty/82-arduino-urok-1-migaem-svetodiodom.html

http://radioman-portal.ru/pages/1736/index.shtml

...и даже "здесь" заразили:

http://arduino.ru/forum/programmirovanie/upravlenie-servo-privodom-i-regulyatorom-skorosti-esc#comment-97459

Рад, что смог хоть чем-то быть полезным :)

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

Ну, я ж говорю: "специалисты есть" :)

faeton
faeton аватар
Offline
Зарегистрирован: 21.03.2016

Зачем трогать системные файлы, если можно в процедуре инициализации перехватить вектор прерывания таймера, как это всегда делается с незапамятных времён. Оно для того там и таблица, так делали резидентные программы в DOS, например. 

Создаём свой любой обработчик, определяем переменную OldInt0, записываем в неё адрес обработчика, который Андруиновая библиотека поставила для своего обработчика, и перезаписываем в таблицу прерываний адрес своей процедуры. Надо учесть, что, насколько я понял, у меги таблица прерываний выглядит как команды перехода "jmp адрес", а отличии от х86, где просто адреса хранятся. В своём обратчике не забываем выполнить вызов обработчика через OldInt0 в конце. В начале или середине своего кода не рекомендую, ибо андруиновый обработкик уже разрешит прерывания. Сделать обычные jmp (как это всегда делают на ассемблере) на адрес прежнего обработчика в Андруине нельзя, т.к. неизвестно что оно там в стек кладёт при объявлении процедуры как обработчик прерывания.

Кстати, зактоки меги, подскажите, пожалуйста, как мега вызывает прерывание? В х86 оно выглядит так:

push cs                    // записать в стек сегмент указателя команд
push ip                    // записать в стек указатель команд
pushf                       // записать в стек регистр флагов (в нём, кстати и флаг разрешения прерываний хранится
cli                             // запрет прерываний
call [0:IntNum*2]     // вызов прерывания
 
В конце прерывания пишется iret, который выполняет
 
popf
pop ip
pop cs
 
ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

faeton пишет:

в процедуре инициализации перехватить вектор прерывания таймера, как это всегда делается с незапамятных времён. 

Зачем? Чтобы лишиться уже посчитанного миллиса и считать самому? У меня ж суть в том, что сначала обраюотчик (ардуиновский) посчитает свои дела, а потом я спокойно передаю готовое значение текущего миллиса своему обработчику. А если я буду работать раньше, то всё самому считать придётся.

faeton
faeton аватар
Offline
Зарегистрирован: 21.03.2016

ЕвгенийП пишет:

faeton пишет:

в процедуре инициализации перехватить вектор прерывания таймера, как это всегда делается с незапамятных времён. 

Зачем? Чтобы лишиться уже посчитанного миллиса и считать самому? У меня ж суть в том, что сначала обраюотчик (ардуиновский) посчитает свои дела, а потом я спокойно передаю готовое значение текущего миллиса своему обработчику. А если я буду работать раньше, то всё самому считать придётся.

Почитайте внимательно. :) Перехватить прерывание - это не захватить, не отобрать, а встроется в цепочку обработчиков прерываний.  Так часто делают: настраивают таймер на максимально необходимое число прерываний в секунду, а уже в цепочке прерываний по внутренним переменным счётчикам-делителям зовут прежние прерывания, которые могут звать ещё более прежние. Это когда другие обработчики чужие или вообще код их недоступен. На то она и таблица прерываний - это же по сути виртуальный метод в ООП. :)

Например, в DOS х86 самое интересное прерывание, после прерывания от клавы, есть прерывание таймера и оно происходит 18.2 раза в секунду по умолчанию. С какого перепуга такую хрень сделали я не знаю, но для своих нужд настраивал таймер на 1000 тиков в секунду, по переменной счёчку-делителю вызывал штатное прерывание каждый 55 раз. Получалось, что в своём прерывании я получал милисекунда, а штатное работало на той же самой частоте. И плюс ещё корректировал "убегание" не помню через сколько раз, кажется, пропускал каждое 1001-е прерывание.

Лезть и поправлять чужой код в данном случае нехорошо и есть на то даже веская причина: библиотеки могут запросто обновиться и обноления затрут Ваши изменения.

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

"Перехватить" вектор прерывания у AVR не так-то просто, как вам кажется. Дело в том, что таблица векторов лежит во FLASH и как-бы является частью программного кода. При инициализации - она ни в какое ОЗУ не переписывается. К сожалению, при попытке указать компилятору свой вектор переполнения 0-го таймера или какой ещё переопределенный вектор из инклуденных библиотек .. с большой долей вероятности получите сообщение о повтороном переопределении вектора и "фигвам", как "жилище индейца".

По странному стечению обстоятельств, версии ИДЕ 1.6.4 и 1.6.5 (старше не пробовал) для Линукс и если их не ставить "поверх" инсталяций из репозиториев, а тупо распаковывать в папку .. позволяют переопределять вектора, если обработчик находится в самом скетче. Не выяснял "почто так", но так оно работает у меня (одна из причин почему и не пробовал старшие версии: есть эти и работают стабильно).

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

встроиться в цепочку можно только в том случае, если она есть, то есть реализована типовым обработчиком. У wiring есть методы для такой работы, у родного кода (ассемблер) - нет, что и попытался изложить выше.

Пока писал свой пост, появился ваш "чуть раньше".

faeton
faeton аватар
Offline
Зарегистрирован: 21.03.2016

Arhat109-2 пишет:

встроиться в цепочку можно только в том случае, если она есть, то есть реализована типовым обработчиком. У wiring есть методы для такой работы, у родного кода (ассемблер) - нет, что и попытался изложить выше.

Пока писал свой пост, появился ваш "чуть раньше".

На уровне скетчей к векторам лучше вообще не лезть. :)))

Чтобы прочитать из флэш напрямую два байта и записать туда другие 2 байта надо немного подумать самому, а не искать готовый скеч. На пользу пойдёт - обещаю! Уж гарантировано полезнее выяснения почему новая версия дуни такая корявая получилась. :)

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

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

Типа есть интсрукции spm,lpm .. но их применение идля одной и той же части флеша - не рекомендовано, насколько помню даташит.

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

faeton пишет:

Почитайте внимательно. :) Перехватить прерывание - это не захватить, не отобрать, а встроется в цепочку обработчиков прерываний.  Так часто делают: настраивают таймер на максимально необходимое число прерываний в секунду, а уже в цепочке прерываний по внутренним переменным счётчикам-делителям зовут прежние прерывания, которые могут звать ещё более прежние. Это когда другие обработчики чужие или вообще код их недоступен. На то она и таблица прерываний - это же по сути виртуальный метод в ООП. :)

Вы говорите. что несколько дней всего как увидели AVR. Через пару месяцев перечитайте этот пост и посмейтесь.

Вы лезете в чужую ахитектуру со своим уставом. Получается смешно :)

 

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

Ну .. положим каждый спец. приходит в чужую архитектуру со своим уставом .. помнится и вы пришли со своим "супер ООП" примерами. Тоже было смешно.. :)

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

Arhat109-2 пишет:

Ну .. положим каждый спец. приходит в чужую архитектуру со своим уставом .. помнится и вы пришли со своим "супер ООП" примерами. Тоже было смешно.. :)

Архат, я и сейчас с ООП примерами. И Вы, кстати, тоже (Вы тут восхищались некотороми средствами С++). А по-поводу смешно - ради Бога, смейтесь, это жизнь продлевает.

Pyotr
Онлайн
Зарегистрирован: 12.03.2014

Нужно было ШИМить 2 канала и измерять ток открытых ключей. Не средний, а именно импульсов. Поставил по шунту 1 Ом на каждый канал и читаю значение АЦП в момент включения нагрузки. По переполнению ТС0 запускается преобразование поочередно на А0 и А1.
Скетч работает, только при значении ШИМ 4 и более. (импульсы 16 мкс и более). При ШИМ 3 и менее значение АЦП=0.

Как можно считать АЦП более коротких импульсов?

Строку

  if (Timer0_Hook) Timer0_Hook(timer0_millis);

поместил в начало ISR(TIMER0_OVF_vect) и передаваемый параметр в функцию заменил на unsigned int.

Скетч

template <typename T> inline Print & operator << (Print &s, T n) { s.print(n); return s; }

#define SENS1_PIN  0 //A0 к шунту ШИМ на D5
#define SENS2_PIN  1 //A1 к шунту ШИМ на D6
#define TOT_SENSOR 2
const byte pinSens[TOT_SENSOR] = {
  SENS1_PIN,
  SENS2_PIN 
  };
word carrMillis, prevMillis, intervalMs = 1000;
volatile word valAdc[TOT_SENSOR];
const byte adcStart = 0xC7;//ADCSRA | (1<<ADSC);

void setup() {
  Timer0_Hook = p;      //  Установим адрес нашей hook-функции
  Serial.begin(9600);
  ADCSRA = (1<<ADEN);//включаем АЦП 
  ADCSRA |= (1<<ADPS2) | (1<<ADPS1); //CLK/64 (50 мксек)
  ADCSRA |= (1<<ADPS0); //CLK/128 (100-110 мксек)
//  ADMUX = (1<<REFS0);//опорное Vcc
//  ADMUX |= (1<<REFS1);//внутренний ИОН 1.1 В
  ADMUX = 0xC0 | pinSens[0];//вместо верхних двух строк 
  ADCSRB = 0; 
 
  analogWrite(5, 4);
  analogWrite(6, 4);
}

void loop() {
 carrMillis = millis();
  if(carrMillis - prevMillis >= intervalMs){
    prevMillis = carrMillis;
    //Когда требуются значения АЦП с датчиков
    word val[TOT_SENSOR] = {0};
    byte oldSREG = SREG;
    cli();
    for(byte i=0; i<TOT_SENSOR; i++){
     val[i] = valAdc[i];     
    }
    SREG = oldSREG;
    Serial << val[0] << "\n" << val[1] << "\n" << "\n";
  }
}
//=========================
void p(const unsigned int) {
  static byte count = 0;
  valAdc[count & 1] = ADC;//считываем значение предыдущего АЦП
  count++;  
  //ADMUX &= 0xF0;//сбрасываем 4 младших бита (вообще достаточно трех)
  //ADMUX |= pinSens[count & 1]; //выбираем следующий пин для чтения
  ADMUX = 0xC0 | pinSens[count & 1]; 
  //ADCSRA |= (1<<ADSC); //чтение-модификация-запись
  ADCSRA = adcStart;//начать преобразование
}

 

nik182
Offline
Зарегистрирован: 04.05.2015

Конечно можно. Время выборки АЦП около 1.5 мкс. Так что любой больше это времени импульс можно оцифровать. Проблема в том, как синхронизовать начало выборки и начало импульса. Например запускайте АЦП от аналогового компаратора соединённого с выходом измерительной цепи и когда импульс превысит порог компаратора получите значение. Только следует помнить, что преключение входов тоже требует времени.

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

Pyotr пишет:

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

Длительность импульса (если Вы используете стандартный ШИМ ардуино) около 7.8 микросекунд. Период же с которым Вас дергает переполнение таймера - 4 микросекунды. Вот и смотрите насколько точно Вы там намеряете. Особенно, если учесть, что вызов функции p никак несвязан с гачалом импульса. Повезёт - в начале вызовется, не повезёт - в конце.

Тепрь о том как это делать. Вы хотите каждый импуль в отдельности измерять?

Тогда смотрите. Если Вы откроете раздел 24.4 даташита (надеюсь, у Вас ATmega328 ?), то прочитаете, что

1. частота часов АЦП должна быть между 50 и 200 килогерц. Устанавливается она из часов контроллера при помощи делителя 2, 4, 8, 16, 32, 64 и 128.

2. Если в Вашей ардуине частота 16МГц, то единственный подходящий делитель - 128.

3. Время преобразования составляет 13 тактов часов АЦП, т.е. в нашем случае = 13*128/16 = 104 микросекунды.

4. Но Вас больше интересует не время преобразования, а время захвата, которое составлет 1,5 такта часов ADC (2 такта в случае автоматического преобразования). Время захвататаким образом = 1,5*128/16 = 12мкс.

Это собственно ответ на Ваш вопрос. Если время преобразования захвата значения ЦАП - 12 мкс, то замерить сигнал длительность в 7.8 мкс. достаточно трудно

Что можно сделать?

Если Вас устроит меньшее разрешение АЦП (не 10 бит, а меньше), то можно попробовать делитель 64 вместо 128. Тогда время захвата составит 6 мкс (1,5*64/16). 

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

Устроит меньшее разрешение? Измерение с пониженной точностью? Если устроит, могу рассказать как начинать измерение точно по началу импульса.

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

ЕвгенийП пишет:

Устроит меньшее разрешение? Измерение с пониженной точностью? Если устроит, могу рассказать как начинать измерение точно по началу импульса.


Я бы послушал, если даже не устроит!!!

Pyotr
Онлайн
Зарегистрирован: 12.03.2014

ЕвгенийП пишет:

Длительность импульса (если Вы используете стандартный ШИМ ардуино) около 7.8 микросекунд. Период же с которым Вас дергает переполнение таймера - 4 микросекунды. Вот и смотрите насколько точно Вы там намеряете. Особенно, если учесть, что вызов функции p никак несвязан с гачалом импульса. Повезёт - в начале вызовется, не повезёт - в конце.

Тепрь о том как это делать. Вы хотите каждый импуль в отдельности измерять?

Тогда смотрите. Если Вы откроете раздел 24.4 даташита (надеюсь, у Вас ATmega328 ?), то прочитаете, что

1. частота часов АЦП должна быть между 50 и 200 килогерц. Устанавливается она из часов контроллера при помощи делителя 2, 4, 8, 16, 32, 64 и 128.

2. Если в Вашей ардуине частота 16МГц, то единственный подходящий делитель - 128.

3. Время преобразования составляет 13 тактов часов АЦП, т.е. в нашем случае = 13*128/16 = 104 микросекунды.

4. Но Вас больше интересует не время преобразования, а время захвата, которое составлет 1,5 такта часов ADC (2 такта в случае автоматического преобразования). Время захвататаким образом = 1,5*128/16 = 12мкс.

Это собственно ответ на Ваш вопрос. Если время преобразования захвата значения ЦАП - 12 мкс, то замерить сигнал длительность в 7.8 мкс. достаточно трудно

Что можно сделать?

Если Вас устроит меньшее разрешение АЦП (не 10 бит, а меньше), то можно попробовать делитель 64 вместо 128. Тогда время захвата составит 6 мкс (1,5*64/16). 

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

Устроит меньшее разрешение? Измерение с пониженной точностью? Если устроит, могу рассказать как начинать измерение точно по началу импульса.

Петрович, виноват - не уточнил. Ардуино УНО, 16 мГц, стандартный ШИМ 8 бит, 976 Гц.

Разрешение желательно 10 бит, так как диапазон значений АЦП от 10 до 300, да еще планирую шунт уменьшить до 0.5 Ом. При делителе 64 я как то пробовал результат как и при делителе 128. Так что устроит делитель 64.

Импульсы отдельно измерять, но можно не каждый раз. Да хоть раз через три.

Тут еще такой момент. Планирую на тиньку (у меня есть 13 и 85) перенести, поэтому хотелось бы без внешних прерываний, компараторов и других таймеров. Это уже наглость с моей стороны))

Хотя бы поймать импульс при ШИМ = 2. Понимаю что нужно раньше запускать преобразование, но как?

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

Поправка.

Меня там бес попутал и я написал

ЕвгенийП пишет:

Длительность импульса (если Вы используете стандартный ШИМ ардуино) около 7.8 микросекунд. 

На самом деле - точно 8 мкс.

Если Вас устраивает некоторая потеря точности взамен на то, что успеете и Вам нужна моя помощь в синхронизации, то напишите точно какой у Вас контроллер.

Кстати, о потере точности. Какая она? Как уже говорилось, рекомендуемая частота часов АЦП - 50-200кГц. При делителе 128 частота получается 16000000/128 = 125кГц, т.е. всё ок. А вот при делителе 64, частота получается 250кГц - многовато. Какая точность при этой частоте - не знаю, но в даташите в разделе 29.8 сказано, что при 50-200кГц абсолютная погрешность 2LBS, а при 1МГц - 4,5 LBS. Сколько будет при 250кГц? Я бы на Вашем месте, прежде, чем принимать рещение, поэкспериментировал бы.

Pyotr
Онлайн
Зарегистрирован: 12.03.2014

 

ЕвгенийП пишет:

Если Вас устроит меньшее разрешение АЦП (не 10 бит, а меньше), то можно попробовать делитель 64 вместо 128. Тогда время захвата составит 6 мкс (1,5*64/16). 

Попробовал делитель 64, при ШИМ=2 работает через раз. Примерно в 5-10 % значений прилетают нули.

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

Так, сами подумайте. Вы ведь начинаете измерять не с момента, когда импульс начался, а когда попало с интервалом 4 мкс - так нельзя, надо синхронизировть. Я же Вам писал про это! Вы же можете начать мерять с середины, а то и сконца импульса - как повезёт. так не делается.

А когда я говорил "попробуйте", я не это имел в виду. Я имел в виду, что просто попробуйте измерять напряжение безо всякого ШИМа и безо всякой экстремальности - просто попробуйте подать сигнал (надолго) и измерить с делителем 64 - чтобы понять устраивает ли Вас точность! Понимаете, о чём я?

Да, кстати, можно ведь и другим путём пойти - не уменьшать делитель, а уменьшить частоту ШИМ (например вдвое) - тогда импульсы станут длинее и всё и с делителем 128 будет работать (только синхронизировать надо!). 

Думайте, что Вам лучше, снижать точность измерения или уменьшить частоту ШИМ.

Pyotr
Онлайн
Зарегистрирован: 12.03.2014

ЕвгенийП пишет:

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

2. А когда я говорил "попробуйте", я не это имел в виду. Я имел в виду, что просто попробуйте измерять напряжение безо всякого ШИМа и безо всякой экстремальности - просто попробуйте подать сигнал (надолго) и измерить с делителем 64 - чтобы понять устраивает ли Вас точность! Понимаете, о чём я?

3. Да, кстати, можно ведь и другим путём пойти - не уменьшать делитель, а уменьшить частоту ШИМ (например вдвое) - тогда импульсы станут длинее и всё и с делителем 128 будет работать (только синхронизировать надо!). 

Думайте, что Вам лучше, снижать точность измерения или уменьшить частоту ШИМ.

1. Этот момент не понимаю.
Представляю процесс так. Таймер0 тикает каждые 1/16мГц/64=4 мкс. Предделитель у него 64. Переполнение каждые 4*256=1024 мкс в момент перехода TCNT0 через ноль. В этот же самый момент выводы ОС0А (D6) и ОС0В (D5) переключаются в "1" при данных настройках, и вызывается ISR(TIMER0_OVF_vect). В этот же момент запускается АЦП (каждые 1024 мс). Но для заряда конденсатора выборки-хранения (14пФ) требуется 12 мкс при CLK/128 и 6 мкс при CLK/64. Точнее заряд происходит столько времени и по его истечении начинается процесс АЦП. А импульс длится например 8 мкс  и он успевает и зарядить конденсатор, и тут же разрядить его до нуля. Поэтому АЦП возвращает ноль при коротких импульсах.

Получается, нужно запускать АЦП при TCNT0 = 254 или 255, а не в момент переполнения.

2. Об этом я и говорил. Как-то пробовал измерять одно напряжение с делителем 128 и 64, и результат один в один.

3. Частоту ШИМ можно уменьшить хоть в 4 раза и предделитель 64, но это надо на таймере0.
Сейчас попробую.

Pyotr
Онлайн
Зарегистрирован: 12.03.2014

Изменил предделитель для таймера0 на 256 вместо 64. Миллис стал тикать в 4 раза медленнее. Частота ШИМ на D5 и D6 стала 240 Гц. Для моего случая норм. Теперь АЦП видит самые короткие импульсы в 16 мкс при ОСR0x=0 даже с делителем 128.
Это костыли,  но рабочие... Больше пока не "думается"...


template <typename T> inline Print & operator << (Print &s, T n) { s.print(n); return s; }

#define SENS1_PIN  0 //A0 к шунту ШИМ на D5
#define SENS2_PIN  1 //A1 к шунту ШИМ на D6
#define TOT_SENSOR 2
const byte pinSens[TOT_SENSOR] = {
  SENS1_PIN,
  SENS2_PIN 
  };
word carrMillis, prevMillis;
word intervalMs = 250;//1 сек с предделителем ТС0 256
volatile word valAdc[TOT_SENSOR];
const byte adcStart = 0xC6;//7;//ADCSRA | (1<<ADSC);

void setup() {
  Timer0_Hook = p;      //  Установим адрес нашей hook-функции
  Serial.begin(9600);
  ADCSRA = (1<<ADEN);//включаем АЦП 
  ADCSRA |= (1<<ADPS2);// | (1<<ADPS1); //CLK/64 (50 мксек)
  ADCSRA |= (1<<ADPS0); //CLK/128 (100-110 мксек)
//  ADMUX = (1<<REFS0);//опорное Vcc
//  ADMUX |= (1<<REFS1);//внутренний ИОН 1.1 В
  ADMUX = 0xC0 | pinSens[0];//вместо верхних двух строк 
  ADCSRB = 0; 
  TCCR0B = 1 << CS02;//предделитель для таймера0 256
  analogWrite(5, 1);
  analogWrite(6, 1);
  OCR0A = 0;//импульс 16 мкс 240 Гц
  OCR0B = 0;  
}

void loop() {
 carrMillis = millis();
  if(carrMillis - prevMillis >= intervalMs){
    prevMillis = carrMillis;
    //Когда требуются значения АЦП с датчиков
    word val[TOT_SENSOR] = {0};
    byte oldSREG = SREG;
    cli();
     val[0] = valAdc[0]; 
     val[1] = valAdc[1];     
    SREG = oldSREG;
    Serial << val[0] << "\n" << val[1] << "\n" << "\n";
  }
}
//=========================
void p(const unsigned int) {  
  static byte count = 0;
  valAdc[count & 1] = ADC;//считываем значение предыдущего АЦП
  count++;  
  ADMUX = 0xC0 | pinSens[count & 1]; 
  ADCSRA = adcStart;//начать преобразование
}