Время летит вперед и до 25 ноября остаётся всего полгода. Это дата следующих соревнований Robotex в этом году (25-26 ноября). Может, надо попробовать успеть, хотя, ничего еще не начато. Но пока это время трачу на отработку различных узлов. Собственно, планирую делать всё-же классический вариант. Моторы micrometal gear 1:30 с моими силиконовыми колёсами и передним опорным шариком и 3 пары оптических дальномерных сенсоров для определения расстояния до боковых стен, фронтальной стены и боковых впереди. В принципе, получится тот-же самый робот, что сейчас для лабиринта из линий, только вместо 8 сенсоров линии будет 6 дальномеров. Микроконтроллер будет силиконлабовский EFM32GG12.
Мне очень нравится этот микроконтроллер. Особенно, после того как до меня дошло, как можно применять его
Linked DMA. Оказалось, это очень мощная штука. Особенно, с учетом того, что можно добавить дескрипторы прямой записи. Например, так как у меня через SPI работает и дисплей, и EEPROM, мне при переходе с одного на другой надо менять роутинг вывода CS SPI. И оказалось, что это можно заставить делать DMA самостоятельно. Просто перед трансфером добавляется прямая запись в регистры USART3->ROUTELOC0:
Код: Выделить всё
EEdescLinktx[1] = (LDMA_Descriptor_t)LDMA_DESCRIPTOR_LINKREL_WRITE(USART_ROUTELOC0_CLKLOC_LOC0 | USART_ROUTELOC0_CSLOC_LOC1 |
USART_ROUTELOC0_RXLOC_LOC0 | USART_ROUTELOC0_TXLOC_LOC0,
&USART3->ROUTELOC0, 1);
EEdescLinktx[2] = (LDMA_Descriptor_t)LDMA_DESCRIPTOR_LINKREL_WRITE(USART_CMD_RXEN | USART_CMD_CLEARRX, &USART3->CMD, 1);
EEdescLinktx[3] = (LDMA_Descriptor_t)LDMA_DESCRIPTOR_LINKREL_M2P_BYTE(cmd_ptr, &USART3->TXDATA, cmd_count, 1);
EEdescLinktx[4] = (LDMA_Descriptor_t)LDMA_DESCRIPTOR_SINGLE_M2P_BYTE(send_data_ptr, &USART3->TXDATA, count);
Вот обращение для передачи данных в EEPROM. Делаем две записи в конфигурацию USART3, передаём 4 байта команды и данные. Тут еще больше! Так как ресурс разделённый, то надо использовать флаги, чтобы пока идёт один трансфер не начался бы другой, поэтому по окончании трансфера необходимо сбросить флаг занятости. Обычно в простых DMA это делается в прерывании по окончанию трансфера, но тут этим можно озаботить сам DMA.
Код: Выделить всё
EEdescLinkrx[0] = (LDMA_Descriptor_t)LDMA_DESCRIPTOR_LINKREL_P2M_BYTE(&USART3->RXDATA, cmd_ptr, cmd_count, 1);
EEdescLinkrx[1] = (LDMA_Descriptor_t)LDMA_DESCRIPTOR_LINKREL_P2M_BYTE(&USART3->RXDATA, send_data_ptr, count, 1);
EEdescLinkrx[2] = (LDMA_Descriptor_t)LDMA_DESCRIPTOR_SINGLE_WRITE(0, &tx_busy);
Как только все данные из EEPROM придут, в переменную tx_busy будет записан ноль.
Так более того. Этим способом, возможно переключать сигнал Data/Command# OLED дисплея. И при этом не сидеть в пустом цикле ожидая окончания передачи первой части данных (команды дисплея). Для этого там есть еще и дескриптор синхронизации. Вот дескрипторы для полного обновления экрана OLED: надо послать команду, переключить сигнал D/C# и послать 1024 байт данных:
Код: Выделить всё
LDMA_Descriptor_t tx_descriptor[] = {
LDMA_DESCRIPTOR_LINKREL_SYNC(0, 0x80, 0x00, 0x00, 1),
LDMA_DESCRIPTOR_LINKREL_WRITE(USART_CMD_RXDIS, &USART3->CMD, 1),
LDMA_DESCRIPTOR_LINKREL_WRITE(USART_ROUTELOC0_CLKLOC_LOC0 | USART_ROUTELOC0_CSLOC_LOC0 |
USART_ROUTELOC0_RXLOC_LOC0 | USART_ROUTELOC0_TXLOC_LOC0,
&USART3->ROUTELOC0, 1),
LDMA_DESCRIPTOR_LINKREL_WRITE(0, &GPIO->P[gpioPortA].DOUT, 1),
LDMA_DESCRIPTOR_LINKREL_M2P_BYTE(start_col, &USART3->TXDATA, sizeof(start_col), 1),
LDMA_DESCRIPTOR_LINKREL_SYNC(0, 0x80, 0x80, 0x80, 1),
LDMA_DESCRIPTOR_LINKREL_WRITE(USART_CMD_RXDIS, &USART3->CMD, 1),
LDMA_DESCRIPTOR_LINKREL_WRITE(USART_ROUTELOC0_CLKLOC_LOC0 | USART_ROUTELOC0_CSLOC_LOC0 |
USART_ROUTELOC0_RXLOC_LOC0 | USART_ROUTELOC0_TXLOC_LOC0,
&USART3->ROUTELOC0, 1),
LDMA_DESCRIPTOR_LINKREL_WRITE(0, &GPIO->P[gpioPortA].DOUT, 1),
LDMA_DESCRIPTOR_LINKREL_M2P_BYTE(buffer, &USART3->TXDATA, sizeof(buffer), 1),
LDMA_DESCRIPTOR_SINGLE_WRITE(0, &tx_busy)
};
Первый SYNC сбрасывает оставшийся от предыдущих трансферов флаг синхронизации. Далее конфигурируется SPI, устанавливается вывод порта A и производится первый трансфер, затем ожидается появление флага синхронизации, делается следующая запись в порт A и передаются данные. Ну и по завершении сбрасывается флаг занятости ресурса tx_busy. Флаг синхронизации устанавливает прерывание SPI "transfer complete":
Код: Выделить всё
void USART3_TX_IRQHandler(void) {
if(USART3->IF & USART_IF_TXC) {
USART3->IFC = USART_IFC_TXC;
BUS_RegMaskedSet(&LDMA->SYNC, 0x80);
}
}
Правда, в референс мануале про эту синхронизацию написано мутно и несколько недостоверно. Вернее, там наверное еще должны быть кой-какие само-собой подразумевающиеся вещи, но с точки зрения формальной логики там дана противоречивая информация.
То что в дескрипторе в порт A пишется в обоих случаях 0 - это просто заготовка. В функции запускающей этот трансфер в те места прописываются нужные значения. Дело в том, что DMA не может изменить один бит порта, так как области памяти BITBAND, SET и CLEAR для DMA недоступны. Но так как у меня на этом порту еще сидят 3 светодиода, сигнал Reset дисплея, о, и еще подключение USART-USB_CDC (PA15), приходится комбинировать эти данные. Все биты, кроме состояния светодиодов, заранее дефинированы и проблем не вызывают. Со светодиодами может случиться так, что если я их во время первого трансфера поменяю, то при начале второго трансфера они вернутся в исходное состояние - но с этим придётся смириться, тем более, что это просто вспомогательная индикация и на основной функционал не влияет.
Ну вот, после того как у меня улучшилось настроение, когда сделал такое улучшение в работе SPI через DMA, занялся контроллером движения. У меня сейчас 3 робота: два на шасси ROMI (классический RSLK с MSP432 и с EFM32GG12) и на PSoC5. Так как первые два в массогабаритном плане одинаковые, то разработку начал на EFM32 используя параметры зависимые от механики из RSLK. А после, уже готовый код переносил на PSoC5, только изменяя те же параметры связанные с механикой. Не обошлось без курьёзов. В какой-то момент времени мне взбрело в голову, что робот идя по линии не должен реверсировать моторы, потому ограничил минимальные обороты двигателя. Результатом стало то, что робот не успевал после поворота выровняться на линии достаточно быстро, как заканчивался "защитный" промежуток и робот вдруг ложно опознавал следующий "узел". Когда я добился того, что робот начал стабильно выполнять заданный маршрут:
Спойлер
Код: Выделить всё
void test_profile(void) {
put_command(command_wait | 2 * FRAMESCANPERSECOND);
put_command(command_entrance | (data.cell_step/2));
put_command(command_forward);
put_command(command_turn | left);
put_command(command_forward | 350);
put_command(command_forward | 300);
put_command(command_wait | 1 * FRAMESCANPERSECOND);
put_command(command_turn | back);
put_command(command_wait | 1 * FRAMESCANPERSECOND);
put_command(command_forward | 300);
put_command(command_forward | 350);
put_command(command_turn | right);
put_command(command_forward);
put_command(command_finish | 100);
}
Въехать в лабиринт, найдя линию следовать на минимальной скорости до узла, где повернуть налево и двигаться прямо два сегмента 350 и 300мм, там остановиться с паузой 1 сек, развернуться, опять пауза и аналогично проследовать обратно. Там где заданы в параметрах команды расстояния, робот расчитывает до какой позиции можно разгоняться и гнать на максимальной скорости и после расчетной позиции начать тормозить. И фишка в том, что видя две последовательных команды forward позиция рассчитывается из суммарного растояния.начал перенос этого кода на PSoC5. И тут возник облом. Я решил задавать скорость аналогично, как на роботах с шасси ROMI в 0,01 rpm... А так как у меня робот может бежать со скоростью выше чем 1200rpm, то я мог бы задавать скорость 120000. Но при вычислении позиции с которой надо тормозить мне нужно считать разницу
квадратов скоростей. А 120000 в квадрате уже вылазит за 32 бита. Поэтому пришлось урезать осетра и скорость задавать в 0,1rpm.
Когда и этот робот начал выполнять вышезаданный маршрут, возник вопрос о передаче данных верхнему уровню. Ну я там сделал набор переменных с флагом и когда данные есть флаг взводится и основная программа заметив взведённый флаг считывала проеханное расстояние и конфигурацию обнаруженного перекрёстка. С расстоянием возникла проблема. Второй прямой сегмент всегда был на "дюйм" длиннее. Собственно, такая же проблема есть и у моего классического RSLK. Это связано с моментом, когда и где ставятся "вешки" по которым измеряется расстояние. Немного унифицировав это дело, получил довольно неплохую точность в пределах 5мм. правда, я не учитываю еще ширину самой линии (15мм).
Теперь, когда есть движение, начал писать решение лабиринта. И снова, взял код с RSLK и повыкидывал все относящееся к движению и заменил на вызовы функции загружающие в очередь команды. После того как получил проход по лабиринту аналогчный RSLK (и даже лучше - по уже известным сегментам робот стал ходить в ускоренном режиме), решил добавить возможность срезать маршрут.

Вот для примера простая петля. робот следуя из узла 1 в 2 видит развилку и выбирает, например, движение прямо. Далее следует по узлам 3-4-5 и снова оказывается у узла 2. По алгоритму люка-тремо, робот должен видя перед собой узел который однажды пересечен развернуться и ехать назад. Это показывает красная линия. Оказавшись снова у узла 2 - он уже не будет "однажды пересеченный", так как мы там уже побывали 2 раза. поэтому надо выбрать или проход, куда мы еще ни разу не ходили, или если такого нет, туда где были один раз. У нас есть проход в сторону узла 6 поэтому нам надо свернуть в ту сторону. Но, мы оказались там, где уже были и весь путь помеченный красным совершенно излишний. Поэтому, я хочу чтобы робот этот момент выполнял оптимальнее. Ну в таком тривиальном случае видно, что надо просто ничего не делать, а двигаться прямо. Но, если, допустим, другой вариант:

В этом случае видно, что вместо того, чтобы разворачиваться и обходить "квадрат" по трём сторонам, оптимальнее повернуть налево и пройти только одну сторону. Вот с этими ситуациями и пытается работать мой алгоритм: вычисляет до куда надо вернуться и вычисляет кратчайший маршрут до той точки и перемещает робота туда. Одна из главных проблем была в том, что маршрут я составить то могу и передать на исполнение, тоже могу. Но как я буду знать, что он выполнен? Выполнен успешно? И вообще, где я нахожусь? Пришлось, конечно, помучаться, пока не удалось эти процессы интегрировать друг в друга. Пока есть уже рабочий код, который с моим домашним лабиринтом (из 9 узлов) вполне справляется заезжая с любого из трёх входов. Вот только хотелось бы еще сделать виртуальный "движок", чтобы посмотреть, как этот алгоритм работает на более сложных лабиринтах.
И хотя, тут почти всё идеально - контроль движения работает исключительно по прерываниям, "верхний" уровень сделан так, что его можно интегрировать в силабовский app.c, т.е. он не обязан захватывать всё процессорное время (собственно, большую часть времени он сидит и ждёт флага, что profiler нашел узел, тогда делает вычисления, кидает одну или две следующих команд и снова ждёт флаг), но есть ложка дёгтя. Как определить цвет поля в тупике? Дело в том, что из profiler я не могу обратиться к сенсору цвета. Сенсор цвета доступен по i2c, а как я могу из прерывания начать i2c транзакцию? Вдруг, какой из верхних уровней сейчас с этим i2c работает? Например, кнопки опрашивает.
И вот это одна из причин, почему я не могу придумать, как сделать перемещение робота на основе сенсоров работающих по i2c типа vl6180, OPT31xx.
С цветом, я конечно, поступаю просто. Получив, информацию, что найден тупик, программа просто стоит и ждёт пока статус profiler-а станет в ожидании следующей команды. Это будет означать, что не только обнаружен тупик, но и робот спозиционирован так, что его ЦТ находится над самым концом линии. А значит, сенсор цвета уже на несколько санттиметров находится над цветным полем.