Как работает bdb_RepReporting на сс2500 с z-lib 3.0.2.
На редкость забагованая библиотека z-lib 3.0.2. А ведь фирма TI серьезная, продает свои отладочные платы за дорого...
Пришлось поразбираться как работает bdb_RepReporting от TI ввиду того, что, все сделал круто, а не работает.
Все в z-lib работает на событиях и эти события часто таймеры. За это отвечает библиотека
OSAL. Есть также CallBack функция, которую часто называют
zclSomething_event_loop( uint8 task_id, uint16 events ), которая регулярно вызывается. Там проверяем случившееся событие и на него реагируем, возможно вызывая новые события.
bdb_RepReporting - не исключение. Тут обрабатываются события реконфигурации и основное событие по отправке регулярного репорта.
Там заведен один таймер на все, который генерирует событие
BDB_REPORT_TIMEOUT. Таймер этот не регулярный.
В OSAL_Timers два вида таймера:
osal_start_timerEx - создает таймер, который сработает один раз через заданное время.
osal_start_reload_timer - создает таймер который будет регулярно срабатывать через заданный промежуток времени.
.
BDB_REPORT_TIMEOUT срабатывает всегда один раз, а потом запускается снова, через новый промежуток времени. Если необходимо, чтобы он сработал вот прямо сейчас, то его отменяют, создают новый с временем срабатывания 0, что его вызывает сейчас.
Например,
bdb_RepStartReporting - только если таймер остановлен, запускает его с задержкой 0. Как правило перед этим вызывается
bdb_RepStopEventTimer.
Зачем? Если мы все будем делать подряд, т.е. в одном потоке, то мы будем выполнять только одну задачу, вот прямо сейчас срочно и только ее. Сложное устройство, работающее в некой сети, должно одновременно обрабатывать кучу заданий, которые не терпят откладывания. Вот b]bdb_RepStartReporting[/b] позволяет отложить нашу задачу, но выполнить ее сразу, как другие будут обработаны. Таким образом наше устройство "не зависнет" и будет иметь хороший отклик.
По событию
BDB_REPORT_TIMEOUT вызывается
bdb_RepProcessEvent, которая проверяет проверяет план отправки обязательных репортов.
Что касается плана, то при конфигурации репортов, собирается весь список комбинаций КонечнаяТочка-Кластер и список Параметров, что подлежат Отчетам. Каждая комбинация отмечается, есть ли Отчет и его параметры (минВремя, МаксВремя, МинКритерий). Если отчетных нет, то и вообще не будет создаваться событие. Также там ведется учет, сколько времени прошло с последнего Отчета (Репорта).
При обработке Эвента сперва ко всем репортам (комбинациям КонечнаяТочка-Кластер) прибавляется время, прошедшее с последнего срабатывания, ко времени, когда последний раз сработало. Выполняется расчет, когда надо отправить репорт, берется минимальное время - это и будет время следующего события для репорта.
Если следующий репорт через какое-то время, то на это время запускается новый таймер. Если Время = 0, т.е. сейчас, то отправляется репорт.
Выбор репорта для отправки выбирается также. За раз выбирается один репорт, вернее один кластер (КонечнаяТочка-Кластер). Берется первый, который должен отправится сейчас. Затем цикл события повторяется с запуском СЕЙЧАС, снова находится, что есть репорты на сейчас, снова находится первый и т.д. пока не отправятся все, что должны отправится СЕЙЧАС. И только потом найдется минимальное время НЕ СЕЙЧАС и Будет запуск нового Таймера, но уже отсрочкой до следующего репорта.
И вот мы подошли к
bdb_RepChangedAttrValue.
Функция сперва проверяет, что за репорт проверяется, а репорт ли он и т.п.. Проверяется минимальное время репорта, отправлять чаще которого нельзя, и, если мы отправили репорт недавно, то пропускаем. Заметим, что не откладываем до минимального времени, а именно пропускаем. Далее проверяется, было ли изменение параметра достаточным для репорта, согласно критерию (из настроек репорта Минимальное изменение) и тоже пропускается, если изменилось меньше чем задано. Если принято решение ОТПРАВЛЯТЬ, то отключается
BDB_REPORT_TIMEOUT, оправляется репорт и запускается
BDB_REPORT_TIMEOUT через
bdb_RepStartReporting (запускает с задержкой 0, далее будет пресчет нового времени старта и новый старт через новое время).
Вот так долго я шел к поиску решения проблемы, которая спряталась в этом алгоритме. Вернее алгоритм рабочий, но в его реализацию закралась бага.
БАГА
Я выше описал, что сделал для типа UINT48 расчет его дельты для учета минимального критерия срабатывания (см алгоритм
bdb_RepChangedAttrValue). Теперь максимальное время отправки репорта нет смысла ставить на 30 или 60 секунд, чтобы именно с этой задержкой отправлялся репорт и чтобы заработал метод
bdb_RepChangedAttrValue (А зачем он нужен, если не работает, так вот теперь-то работает). Я установил максимальное время 3600 секунд. И репорты ПЕРЕСТАЛИ отправляться. Ну как перестали, через час бы репорт отправился, наверное, но ждать час - это такое.
bdb_RepChangedAttrValue - вызывается каждые 30 секунд, но ничего не происходит. Путем перебора разных значений, я выяснил, что на 65 секунд (в максимальном значении репорта) репорт по
bdb_RepChangedAttrValue отправляется каждые 30 секунд (время вызова
bdb_RepChangedAttrValue), а при установке 66 секунд и более, уже нет.
Вот я и начал полностью разбирать как работает алгоритм bdb_RepReporting, чтобы понять от куда такое странное ограничение.
Собственно, алгоритм выглядел на 100% рабочим и проблем я не нашел.
Пошел по пути понимания, а что такого в числе
65 секунд?
В настройках используются секунды, а таймер работает в миллисекундах, т.е. чтобы из секунд сделать миллисекунды, надо их умножить на 1000.
Если 65 умножить на 1000, то получим число укладывающееся в uint16, а вот число 66 * 1000 уже в uint16 не помещается и уже нужно uint32. Собственно, таймер запускается в миллисекундах и использует для этого uint32. А в настройках просто секунды хранятся в uint16. Вот где-то тут видимо и наша проблема.
Я предполагал также, что кто-то просто отключает таймер, т.к. при запуске таймера в ручную по нажатию кнопки (правда я время указывал в секундах, вот тут мой косяк), один репорт отправлялся и далее снова тишина.
Я наставил кучу принтов, почти везде, где запускаются и останавливаются таймеры, но там все было норм, кроме того, что библиотека
debug.h использует для форматирования vsprintf, который у IAR 8051 похоже тоже проблемный. Во-первых, он принтовал числа не более чем 16-ти разрядные, т.е. от 32-х разрядного числа он печатал только LOW часть, а во-вторых, он не поддерживает unsigned числа, у него формат "%u" менялся просто на "u". Я просто не понимал, что за шлак он мне печатает. Когда понял, то стал печать в HEX и переводить uint32 в uint8[4] и просто печатать их в HEX побайтно. И вот тут я наконец получил нормальные значения и нашел место где все ломалось.
РЕШЕНИЕ
Проблема нашлась все в той же
bdb_RepChangedAttrValue, а вернее в расчете времени до следующего отчета, не осталось ли до следующего отчета времени менее, чем минимальное время отчета.
Знакомьтесь:
Код: Выделить всё
/*********************************************************************
* @fn bdb_RepCalculateEventElapsedTime
*
* @brief Calculate the elapsed time of the currently running timer,
* the remaining time is roundup.
*
* @param remainingTimeoutTimer - timeout value from the osal_get_timeoutEx method,
* its in milliseconds units
* @param nextEventTimeout - the timeout given to the timer when it started
*
* @return the elapsed time in seconds
*/
static uint16 bdb_RepCalculateEventElapsedTime( uint32 remainingTimeoutTimer, uint16 nextEventTimeout )
{
uint32 passTimeOfEvent = 0;
passTimeOfEvent = nextEventTimeout*1000 >= remainingTimeoutTimer? nextEventTimeout*1000 - remainingTimeoutTimer: 0;
uint16 elapsedTime = passTimeOfEvent / 1000;
elapsedTime = elapsedTime + ((passTimeOfEvent % 1000) >0 ? 1:0); //roundup
return elapsedTime;
}
На первый взгляд, тут все хорошо. Я более того скажу, что на 32-х разрядном компиляторе, а тем более на 64-х разрядном, все будет работать. А вот на 8-ми разрядном есть проблемка. Увидели?
Я повторил туже проблему и на AVR.
просто выполнил вот такой код:
Код: Выделить всё
uint16 = 3600;
uint32 val= 3500000;
uint32 res = (num * 1000>= val? 1 : 0);
Чему будет равен
res? На 8-ми битном ЦПУ будет 0, хотя мы ожидаем 1.
Код: Выделить всё
uint16 = 3600;
uint32 val= 3500000;
uint32 res = (1000 * num>= val? 1 : 0);
А тут? Тоже самое, 0!
Код: Выделить всё
uint16 = 3600;
uint32 val= 3500000;
uint32 res = (1000L * num>= val? 1 : 0);
А если так?
Пояснительная бригада!
Компилятор старается сделать все как можно проще и компактнее. У 32-х разрядного, каждый регистр 32 разряда, и он каждое число по умолчанию будет вычислять как 32-х разрядное, а потом уже попытается его запихнуть в то что есть.
8-ми битному сперва надо увидеть, что 1000 в 8 бит не влезает, а в 16 влезает, и использует 16-ти разрядную арифметику, тем более, что умножается число на тоже 16-ти разрядное.
1000L - 32-х разрядное, типа long. Компилятор видит, что тут явно тип 32 бита и уже отталкивается от первого числа использует уже 32-х разрядную арифметику. Конечно это эмуляция 32-х разрядной арифметики, но все же.
Обычно при автоматическом преобразовании типов, если это возможно, за основу берется именно первый операнд, т.е. используется его тип и второй операнд приводится к его типу. В более жесткой семантике компилятор бы потребовал от разработчика сперва всё привести к одному типу, а потом уже делать вычисления (тут обобщения и для др. языков программирования, но везде есть свои нюансы).
Интересно, что в файле bdb_RepReporting.c есть вот такая функция:
Код: Выделить всё
static void bdb_RepRestartNextEventTimer( void )
{
uint32 timeMs;
// convert from seconds to milliseconds
timeMs = 1000L * (bdb_reportingNextEventTimeout);
osal_start_timerEx( bdb_TaskID, BDB_REPORT_TIMEOUT, timeMs );
}
Они знали! ЗНАЛИ!!! Переменная bdb_reportingNextEventTimeout тоже uint16.
Это еще раз подтверждает мою теорию, что писали разные куски разные люди с разной квалификацией. Нет, я за молодых, но они должны учится и получать опыт. Тут опыт получен не был, т.к. не было произведено полноценное тестирование. Не было получено знание, что там есть ошибки. Ошибки не были исправлены. Это последняя версия аж 16-го года. Ей уже 10 лет. Все-таки от коммерческого продукта ожидаешь как-то больше (да библиотека бесплатная, но прилагается к вполне платному железу, как бы в комплекте, т.е. это не OpenSource).