Ardunio RTOS через хук 0 таймера. Проблемы с отладкой кода.
- Войдите на сайт для отправки комментариев
В общем когда писал, что работает - несколько поторопился. Как раз попытка использовать Serial.print() неожиданно привела к нестабильной работе, и то что оно у меня "работало" оказалось делом случая.
Краткое описание ситуации:
Начало положено идеей ЕвгенийП о том, что можно добавить в обработчик прерывания по переполнению системного таймера 0 - "хук" в виде указателя на функцию, которую обработчик будет вызывать каждую миллисекунду и тем самым исполнять некий код с периодичность 1024 микросекунды.
Заинтриговала простота реализации и я её сделал, даже с небольшим расширением, а именно к хуку добавлена "защелка" - битовый признак "хук запущен", которая позволяет запускать процедуру хука с открытыми прерываниями и в случае выполнения процедуры более 1024 микросекунды - блокирует повторный вызов хука. Что по идее, должно позволять выполнять "длинные" хуки без потери счета системного времени:
// моя С-версия обработчика прерываний с хуком: volatile uint32_t timer0_overflow_count = 0UL; volatile void (* timer0_hook)(void) = 0; uint8_t timer0_hook_run = 0; ISR(ISRtimer(TIME_DEFAULT, TIME_ISR)) { timer0_overflow_count++; if( timer0_hook && !timer0_hook_run ){ timer0_hook_run = 1; sei(); timer0_hook(); cli(); timer0_hook_run = 0; } }
В целом, всё работает как и ожидалось: Если хука нет, то происходит тупой подстчет времени. Блок пересчета в миллисекунды у меня устранен, ибо предпочитаю пользоваться счетчиком переполнений напрямую, и тем самым слегка разгрузить обработчик .. и весь скетч в конечном итоге. Но это - мелочи.
Данная реализация при установленном хуке и его большом периоде исполнения - завешивает надолго основной код (на время хука) и, если в нем был активен Serial.print() в момент прихода прерывания от таймера, то усё.. синхронность интерфейса - нарушена совсем.
Это я как-бы понимал, но сейчас тестируя всё более тщательно столкнулся с другой стороной вопроса:
При отсутствии Serial.print() в основном коде, и налии его в внутри хука .. он ведет себя крайне нестабильно и тоже способен завешать систему "полностью" или уйти в перезагрузку.
Стек - проверял, не плывет .. в ассемблерной реализации таже фигня, если делать полное сохранение регистрового файла в обработчике. Если сохранять только значимые регистры - работает "на порядок устойчивей" .. как-бы наоборот должно быть..
Что может происходить "такого", при вызове Serial.print() из под процедуры хука, при условии блокировки от повторного вызова?
Есть желающие помочь?
Вообще-то исполнять долгие коды в обработчике - есть плохо и лучше уйти от этого. Но если уж совсем никак, то ...попробуйте следующее:
1. Добавьте volatile в описание timer0_hook_run
2. Замените строки: 13 на сохранение регистра SREG, затем sei(); и строку 15 на восстановление регистра SREG
3. Вы пишете, что "что по идее, должно позволять выполнять "длинные" хуки без потери счета системного времени". Это не совсем так, т.к. во время выполнения хука у Вас стоит флаг, что прерывание всё ещё обрабатывается. Флаг будет сброшет только при возврате из прерывания. Перед строкой 14 вставьте сброс флага прерывания вручную. Если склероз не изменяет, для этого нужно записать 1 в бит TOV0 регистра TIFR0, но лучше проверьте по даташиту, а то я по памяти пишу.
Спасибо, обязательно посмотрю как только доберусь до дуньки .. но Вы указали нечто такое, чего я никак не наблюдаю в действительности, а именно:
"Это не совсем так, т.к. во время выполнения хука у Вас стоит флаг, что прерывание всё ещё обрабатывается. Флаг будет сброшет только при возврате из прерывания."
Дело в том, что там есть тест с моргалкой светодиодом. Его последний режим - как раз проверка работы таймера при работающем хуке и он отрабатывается правильно. Там стоит "включить на 2 сек", "выключить на 2 сек" в основном коде через delay() намеренно. А в хуке стоит пауза на 1 сек и после неё стоит моргалка по 200мсек .. то есть, если бы таймер не отрабатывал повторно - ничего бы не "подмаргивало" при исполнении.
А оно "вертится". :) Может, конечно, флаг снимается и по возврату .. надо перечитать даташит .. но, тем не менее таймер отрабатывает - это факт.
Прикол в том, что и сериал "периодически" работает нормально .. и это зависит исключительно от того "сколько команд делается внутри хука" .. что наводит на мыслю про утечки или у меня или у сериала..
2. volatile к защелке добавлял, не помогает.
3. Это тоже делал .. там SREG уже предварительно сохранен в контексте запуска обработчика и восстанавливается по его завершению. Мне просто надо гарантировать, что до конца остатка обработчика никакая зараза ему больше не помешает. И только.
Ну, может я и не прав про TOV0, я же сразу сказал, что "по склерозу", но Вы перечитайте, какая-то тонкость там была.
Посмотрел .. там нифига не очевидно (как и с операциями push/pop изрядно помучался): дословно написано так: "TOV0 is cleared by hardware when executing the corresponding interrupt handling vector", причем оно "так" написано для большинства прерываний .. типа "когда выполняется обработчик", но вот по его старту или финишу .. фиг поймешь.
В детализации к командам ассемблера, тоже ни разу не указано что reti сбрасывает какие-то флаги .. да и КАК она узнает из какого обработчика возвращается, особенно после "оптимизаций" через longjump, а компилятор такие объединения завершений разных процедур делать - очень даже умеет.
Я предполагал, что аппаратный сброс флага - это "особенность" таблицы векторов прерываний: как только производится "прыг" на обработчик, так ЕГО флаг и сбрасывается.. тут вариант реализации в железе - очевиден, чего нельзя сказать про reti.
C push/pop тоже пришлось помучаться .. нигде детально не указано что push делается с ПОСТдекрементом, а pop с ПРЕДинкрементом .. в точности наоборот от (X,Y,Z)++ и --(X,Y,Z) регистров-указателей. :)
Да, ещё что настораживает:
В С-версии обработчика компилятор, видя возможность вызова сторонней процедуры сохраняет .. далеко не все регистры контекста, а только r0,1,r18-r27,r30,r31 .. то есть исключаются регистры r2..r17,X,Y! Как понимаю, компиляция процедуры хука видимо ему гарантирует сохранность остатка самой процедурой .. или он эту часть регистрового файла не пользует вовсе? Вроде написано иначе, к компилятору..
В том, что делал на Асме (есть куски на гитхабе сейчас) .. тоже свои странности: попытка сохранять весь контекст приводит к практически гарантированному зависанию Сериала .. даже если я это делаю в специально созданных для этого процедурах pushAllRegs(), popAllRegs() .. слишком долго? Нипанятна.
.. такое ощущение, что где-то в Serial сидит офигительная дыра ..
попытка сохранять весь контекст
Для запихивания сразу всех регистров на стек в интеле есть замечательная команда pusha, и еще более замечательная, обратная ей команда popa. Вот такая команда у Вас и получается :)))))))))))
Угу. Только с ней - сериал вот 146% не работает. А без неё - иногда пашет. Как так? :)
Всё, без доступа к дуньке, моего логического анализатора выловить косяк - не хватает. Запушил асм. версию обработчика с учетом того что там пытается сохранять компилятор. По идее должно работать.
Всё больше склоняюсь к наличию косяка в Сериале .. причем какого-то мутного-мутного ..
Так, вроде ж Вы свой Serial делали (ну, в смысле класс для USART), нафига Вам чужой поддерживать? Или я что-то перепутал?
А что до косячка - этого то добра везде хватает, но вот как он умудряется подраться с таймерным прерыванием - вот это интересно.
Нет ещё, делал I2C .. его ещё не смотрел как он с хуком пашет. LCD дисплей сейчас в нерабочем состоянии.
Serial только начал смотреть и сразу ляп: _tx_buffer_tail = (_tx_buffer_tail + 1) % SERIAL_TX_BUFFER_SIZE;
Насколько понимаю, у Атмела нет операции деления, равно как и взятия остатка. И это в обработчике прерывания, если не ошибаюсь!
А вот это определение инлайн функций в HardwareSerial.h ваще я не понял "как это"?
inline size_t write(unsigned long n) { return write((uint8_t)n); }
inline size_t write(long n) { return write((uint8_t)n); }
inline size_t write(unsigned int n) { return write((uint8_t)n); }
inline size_t write(int n) { return write((uint8_t)n); }
То есть большое число тупо обрезаем до младшего байта и только его и выводим?!?
А помните ЕвгенийП, тут http://arduino.ru/forum/programmirovanie/etyudy-dlya-nachinayushchikh-metronom-ne-zagruzhaya-taimer было
Сама идея использовать таймер0 для формирования временных интервалов - великолепно!
А вот предоставлять возможность вызова из обработчика прерывания функцию по калбеку - очень стремно. Будет масса вопросов почему мой опрос датчика температуры, запись в файл, вывод на екран и т.д. из калбека не работают и вообще все валится.
.....
Давайте уж четко выпишим требования к функции для калбека. Я так поимаю гдето следующее выходит: выполнятся быстрей 1мс, не содержать ожиданий прерываний, не содержать команд разрешения прерывания... Что ещё? И главное, какие классы соответствуют требованиям? Боюсь окажется возможным только ногодрыг и операции с глобальными переменными.
Повторяю, нельзя "размывать" уровни прерывания и основной программы. Разные требования к коду в этих двух местах. Пока програмист явно пишет обработчик, он это учитывает. А когда появляется возможность вцепить произвольный код, получается то что видм здесь. Многие знают что внутри стандартных либ?! Вот и обращаются к Serial из основной программы и обработчика "ибо не ведают что творят".
Так. Похоже что "не работает" исключительно нардварный сериал .. пробежался по коду, с разбегу так и не понял почто так, надо влезать глыбже.
Мой датчик TCS3200, писан с использованием этого хука и работает как часы, а там обработки - очень даже не мало.
А помните ЕвгенийП, тут http://arduino.ru/forum/programmirovanie/etyudy-dlya-nachinayushchikh-metronom-ne-zagruzhaya-taimer было
Да, ладно Вам.Прерывания - штука не для чайников, а у профессионалов всё нормально работает, может не сразу, но все проблемы решаются.
Если при разработке какой-бы то ни было системы забиваться на чайника, то надо определять какого уровня этот чайник и тогда уже от этого плясать. Сделать для любого - невозможно. Например, сделать какую-то систему/библиотеку для человека, для которого ошибка компиляции является поводом выносить мозг всему форуму глубокомысленными вопросами про "почему функция if не вызывается из класса goto" просто невозможно.
Да, ладно Вам.Прерывания - штука не для чайников, а у профессионалов всё нормально работает, может не сразу, но все проблемы решаются.
Конечно. Только ардуино - любительская штука, не ориентированая на профи.
Вы вот лучше скажите, Вы, вроде, разбирались с IDE, там можно как в Visual Studio добавить свой тип файла и указать чем его компилировать? Или это у них не в конфигурации, а в коде сидит? Если сразу знаете, скажите. рыться специально не надо - не тратьте время, порыться я и сам могу.
Такой возможности не встречал. Думаю что нет такого вобще и сидит в коде.
В самом сериале так пока и не нашел ничего сильно криминального, обработчик кривоваст, но работать должен нормально. Как им удается подраться уже просто интересно. Но, ковыряя "многоэтажный паровоз" из классов Print, Stream, WString, .. возникло стойкое ощущение, что там далеко не всё гладко с переопределением виртуальных функций .. или я С++ ещё не доконца вспомнил. У меня в обработчике используется регистр Z, который в компиляторе пользуется как this .. вот такое ощущение, что они, где-то тут и дерутся. Но, команда eicall есть только для Z регистра, как понимаю..
Тщательное тестирование таймерного хука показало что с I2C драйвером все работает корректно. Ничего никуда не плывет, даже одновременный вывод из главного потока и из под хука отрабатывают ожидаемо. То есть запросто могут нагадить друг другу. Тем не менее, никаких "драк" нет, всё честно.
Похоже, что проблемы с наследованием виртуальных функций у так любимого паровоза классов Stream, Print, Serial ...
Скомпилял и поправил работу с хуком для ATmega328p серии. Тест testTime0.ino с подключением lcd1602 работает успешно и для "мег" и для "прочих". Да, изменено управление хуком (сэкономил 4 байта): теперь сначала проверяется защелка хука (включена как extern в arhat.h - можно пользовать), и если она не нуль, то хук не вызывается. По компиляции устанавливается в 1, то есть "хук" по умолчанию - запрещен.
соответственно, чтобы запустить хук - надо установить процедуру хука (есть функция) И сбросить защелку:
timer0_hook_run=0;
Для запрета теперь достаточно взвести защелку.
Допускается компиляция обработчика таймера и вовсе без хука для экономии памяти. Для этого, в файле arhat.c надо изменить опцию компиляции с 3 (компилировать переключалку хука) на 2 (компилировать экономно, без переключалки).
Также, поправил функции pushAllRegs()/popAllRegs() для обоих вариантов распиновки плат: "меги"/"уны". Первая сохраняет на стеке весь контекст и возвращается БЕЗ какого-либо изменения регистров или состояний .. вот "как не было". А вторая восстанавливает предварительно сохраненный контекст .. вот тут не знаю: надо ли "вот как не было" .. точка вызова - иная.
Вот теперь можно решать задачу "фонового вывода" на тормозные устройства типа LCD1602, Serial и т.д. по прерываниям от таймера, дабы вывод диагностики не тормозил основной скетч. :)