Этот пост заканчивается на середине решения задачи, публиковать его рано. Но он как раз подводит к теме осциллографа, которая почему-то начала обсуждаться в предпоследнем посте. Поэтому публикую то, что есть на данный момент.
Первоначальный тест датчика от оптической мыши PAN3101 был выполнен с помощью какой-то платки на ATmega64 с графическим дисплеем, где все делалось чисто программно. Все нормально работало, но теперь надо реализовать обмен «начисто», думая об используемых ресурсах. В качестве процессора будет применен какой-то из STM32 (пока точный тип не выбрал), на первом этапе работаю с STM32F100RBT6B, который установлен на «Discovery».
Сенсор имеет двухпроводный последовательный интерфейс, состоящий из линии тактирования SCLK и двунаправленной линии данных SDIO. Частота клока может быть довольно большой (до 10 МГц), но между передачей команды и приемом ответа нужно делать паузу, чтобы дать возможность процессору датчика как следует подумать и не сказать в ответ какую-то глупость. Для PAN3101 это время минимум 3 мкс, а для других типов датчиков может быть намного больше. Например, для ADNS-2051 это время составляет 100 мкс.
Особенно спешить считывать датчик мне не надо, внутренние регистры хранят перемещение до 127 шагов. При разрешении 800 cpi это целых 4 мм пути. Максимальная скорость перемещения, которую поддерживает датчик, это 0.5 м/с. Чтобы не потерять координату, частота считывания должна быть не менее 133 Гц, т.е. примерно раз в 7.5 мс. Для моего применения такая скорость вряд ли нужна, считывать можно еще реже.
Всегда желательно иметь в системе как можно меньше прерываний. Они сотрясают размеренно выполняющуюся программу и приумножают хаос. Использовать прерывания можно лишь по большой нужде, по возможности нужно обходиться без них. В данном случае вполне можно обойтись без прерываний, и даже без программного опроса флагов.
В системе всегда есть таймер, у меня он тикает с периодом 1 мс. Любая задача может привязать выполнение какого-то фрагмента к системному тику. Так решил сделать и здесь. По системному тику запускаю передачу команды в датчик. По следующему тику запускаю чтение результата. По следующему - достаю из порта это значение и запускаю передачу следующей команды. Затем опять запускаю чтение результата, а через тик читаю результат. После цикла опроса можно сделать паузу длительностью несколько системных тиков. А можно и не делать. Скорость обмена должна быть выбрана так, чтобы к моменту следующего тика транзакция гарантированно завершилась. Тогда не анализируя никаких флагов можно начинать новую транзакцию.
Интерфейс датчика больше всего похож на SPI, только имеет совмещенный сигнал MISO/MOSI. Придется с этим работать, используя STM32. А процессоры STM32 славятся своей периферией. Она у них страшно навороченная, что раздуло документацию до немыслимых объемов. Каждый раз, подходя к проекту на STM32, начинается видение, что на тебя обрушивается огромная стопка бумаг, норовящая придавить к полу и раздавить. Каждый раз, садясь за компилятор для STM32, нужно совершать нечеловеческое волевое усилие. На этом фоне проекты на основе AVR кажутся приятным развлечением.
Что нам может предложить порт SPI STM32? Да все, что нужно. Есть и требуемый полудуплексный режим, который поддерживает двунаправленную линию данных. После некоторого погружения в документацию инициализация порта SPI готова.
RCC->APB1ENR |= RCC_APB1ENR_SPI2EN; //включение тактирования SPI2
SPI2->CR1 = //настройка SPI:
SPI_CR1_CPHA * 1 | //CPHA = 1
SPI_CR1_CPOL * 1 | //CPOL = 1
SPI_CR1_MSTR * 1 | //мастер
SPI_CR1_BR_0 * 3 | //тактовая частота APB2 / 16
SPI_CR1_LSBFIRST * 0 | //MSB first
SPI_CR1_SSI * 1 | //мастер
SPI_CR1_SSM * 1 | //программное управление NSS
SPI_CR1_RXONLY * 0 | //full duplex
SPI_CR1_DFF * 0 | //8 bit data frame
SPI_CR1_CRCNEXT * 0 | //CRC не используется
SPI_CR1_CRCEN * 0 | //CRC не используется
SPI_CR1_BIDIOE * 1 | //output enable
SPI_CR1_BIDIMODE * 1 | //1-line mode
SPI_CR1_SPE * 1; //разрешение SPI
Протестировал передачу - все работает отлично. Ставлю SPI_CR1_BIDIOE = 0 и тестирую прием - что за ерунда? Я по простоте душевной считал, что прием байта инициируется записью в DR любого кода, а после приема байта процесс останавливается. Так было бы логично. Но на практике при включении приема сразу начинает генерироваться SCLK, и он генерируется постоянно, пока прием не будет выключен. Почитал документацию в этом месте (а всю ее заранее прочесть невозможно - времени ни на что другое не хватит) - так и есть, SCLK постоянно пилит. Обратился к сетевым духам за помощью. Ничего интересного по вопросу не нашел, лишь подтверждение, что есть такое дело. Я не понимаю, как такой режим можно вообще использовать. Любой лишний клок, просочившийся на слейв, приведет к нарушению синхронизации, все сломается напрочь. Вот
тут дают рекомендации отслеживать флаги и быстренько выключать прием, при этом проверяя правильность работы программы осциллографом или логическим анализатором. Но это совсем не то, за что боролись. Занимать процессор тупым отслеживанием флагов - никогда. Да еще и результат не гарантируется. А вдруг какое-то левое прерывание вклинится, и всё - лишний клок и нарушение синхронизации. Получается, опять придется снаружи соединять два вывода процессора и программно переводить один из них в третье состояние при приеме. Пресловутая периферия STM32, которая может все, опять оказалась не такой универсальной.
Прежде чем закорачивать снаружи выводы, решил испробовать еще одну возможность - использование USART в синхронном режиме. К тому же подумалось, что столь ценный ресурс, как аппаратный SPI, негоже тратить на этот датчик. Порт может пригодиться для более важных дел.
USART в синхронном режиме заработал, но на линии данных заметил нечто неладное. Пересчитал на осциллографе клоки и не досчитался одного - не был установлен бит USART_CR2_LBCL. Это написано в документации, но, опять же, ее объем ставит перед выбором: или читать, или делать. Когда сенсор недосчитывается клока, то запускается процесс ресинхронизации интерфейса: через 1.7 мс логика интерфейса сбрасывается, а вывод SDIO переходит в состояние Hi-Z. Это я и пронаблюдал на экране осциллографа. Еще нюанс: в синхронном режиме USART все равно шлет мусор в виде стартового и стопового бита, но, к счастью, клоками их не сопровождает.
USART тоже имеет полудуплексный режим работы. Для этого надо установить бит USART_CR3_HDSEL. Тогда вывод RX не используется, а TX становится двунаправленным. Такой режим я когда-то успешно проверял при работе через USART с датчиками DS18B20. Но, оказывается, синхронный режим USART и полудуплексный режим вместе не живут. Так написано в документации. Для надежности я проверил в железе - точно не работает. И тут опять приходим к необходимости соединить два вывода процессора снаружи.
Но это еще не все. USART всегда передает первым младший бит, и это не программируется. А для датчика PAN3101 нужно первым передавать старший бит. Придется вращать байт. Сначала сделал это универсальным способом на Си:
v = ((v >> 1) & 0x55) | ((v << 1) & ~0x55); // swap odd and even bits
v = ((v >> 2) & 0x33) | ((v << 2) & ~0x33); // swap consecutive pairs
v = ((v >> 4) & 0x0F) | ((v << 4) & ~0x0F); // swap nibbles
Затем вспомнил, что под капотом целый Cortex M3 с его богатой системой команд. Хорошо, что CMSIS и компилятор через intrinsics-функции предоставляет доступ к многим ассемблерным командам. Для реверса битов есть функция __RBIT. Но она крутит биты в 32-разрядном слове, поэтому нужен еще сдвиг, который оптимизируется компилятором просто до взятия нужного байта.
b = __RBIT(b) >> 24;
Вообще, STM32 - это первый процессор в моей практике, для которого я не знаю ассемблер. Вообще. Бывает, конечно, что смотрю в листинги, но только с целью увидеть, «длинно» там накомпилилось, или «коротко». Хотя в свое время плотно сидел на ассемблере. Для MCS51 писал только на ассемблере. Для AVR тоже начинал с ассемблера, много лет на нем сидел, перейти на Си стоило огромных усилий. Как обычно поучают на форумах, без знания ассемблера хорошим программистом не стать. Я тоже так считал и гордился знанием ассемблера. Работая на Си, ассемблер фактически был не нужен, и когда со временем появлялись задачи что-то подправить в старых проектах, я с ужасом замечал, что совершенно ассемблер забыл. Ну а для STM32 и не знал вовсе.
Про периферию - касательно другой части проекта. Есть дисплей, загружается он через SPI. Там все хорошо, SPI используется только на передачу, полудуплексный режим успешно работает, для данных задействована только одна ножка MOSI. Но ведь периферия может больше! Пытаюсь прикрутить DMA, чтобы программно надо было только запустить процесс загрузки дисплея, а дальше он прошел автоматически. Черта с два! Тривиальнейшая вещь - управление сигналом Chip Select, что требуется почти для любого SPI-устройства - не автоматизировано (в более продвинутых семействах контроллеров что-то такое начало появляться). Поэтому данные-то DMA передаст, но ничего не заработает, если в конце пересылки вручную не дернуть сигнал CS. Это напоминает автомобильный завод, где все операции делают роботы, но в конце все же надо один раз ударить кувалдой, чтобы завершить сборку. Можно, конечно, навернуть какую-то дикую конструкцию с таймером и дополнительным каналом DMA, который через bit band управлял бы CS. Но это больше похоже на крик отчаяния, чем на программирование. Вообще, сложная и обширная периферия STM32 с каждым разом кажется все более потемкинской.
Запас неприятностей при подключении оптического сенсора этим не иссяк. Его интерфейс имеет лишь два сигнала, SCLK и SDIO, а сигнала CS нет. В этой связи наблюдается специфическое поведение при переключении сенсора с передачи на прием. Для чтения сенсора сначала мастер должен передать команду с адресом регистра, который собираемся читать. Младший бит адреса должен быть равен 0, это как раз и означает, что мы собрались читать. Биты данных стробируются фронтом SCLK, это вполне обычно. После окончания передачи нужно выждать некоторую паузу (> 3 мкс), после чего сформировать еще 8 периодов на SCLK для чтения байта из сенсора. Как только на SCLK появляется низкий уровень, вывод сенсора SDIO переключается на передачу и начинает выдавать бит за битом по каждому спаду SCLK. Когда заканчивается 16-й период на SCLK, там устанавливается высокий уровень (на верхней диаграмме на рисунке я подрисовал красным). При этом вывод сенсора SDIO остается включенным на передачу и на нем остается висеть значение последнего (младшего) считанного бита.
На самой нижней диаграмме это место показано более детально. Спад на SCLK, который идет после 16-го периода, является началом передачи мастером следующей команды. По этому спаду выход SDIO сенсора переходит в состояние Hi-Z. Но ведь по этому же спаду выход мастера включается на передачу (я показал синей линией). На линии SDIO возникает коллизия. Что они нарисовали в datasheet - не понимаю. Судя по их рисунку, мастер должен быть более тормознутым, чем сенсор, и включать свой выход с заметной задержкой. На практике такого нет, данные на выходе мастера появляются с задержкой всего пару нан после спада SCLK. В случае использования USART вместо SPI ситуация еще хуже - перед первым периодом клока передатчик включает свой выход и втихаря выдает старт-бит, не сопровождая его клоком. В результате вместо короткой наносекундной коллизии получаем коллизию длиной в целый бит. Похожие картинки рисуют и для других сенсоров, но как сделать так, чтобы момент «Driven by micro» наступал с задержкой, как на картинке, неизвестно.
Для ловли коллизий сделал каллизиоскоп (см. заглавное фото поста) - в линию данных включил два резистора, к средней точке которых подключил осциллограф. Коллизия хорошо видна. Можно даже посмотреть, кто кого и куда тянет. Пытался менять полярность и фазу SCLK. Если коллизия уходит в одном месте, то приходит в другом. Полная безвыходность. Думаю, ладно, чисто из спортивного интереса придумаю, как избежать коллизии. Не ограничивая себя ни в чем - можно применять какую угодно внешнюю логику, с тремя состояниями и прочее. Но ничего не получилось.
Из рекомендаций на эту тему удалось найти только одну, но совершенно гениальную. В документе «STM32L4 - SPI» рекомендуют в полудуплексном режиме всегда включать последовательный резистор в линию данных для избежания коллизий. Правильнее было бы сказать, для смягчения последствий коллизий. Что же, так придется и сделать, раз нормального выхода нет.
Неприятности с оптическим сенсором на этом не закончились. Его показания нормально считывались, дисплей исправно показывал значения координат. Но мне надо было выключить режим автоматического засыпания, который включен по умолчанию. Перемещение у меня будет медленное, сенсор будет постоянно засыпать, а при просыпании возможны припадки и пропуски координат. К тому же, светодиод у меня все равно запитан отдельно и постоянно (если управлять им от сенсора, то там всегда ШИМ со своими помехами), а экономить электричество здесь задача не стоит.
Когда при инициализации я добавил передачу нужной команды в сенсор, она не заработала. Сенсор на начальном этапе вел себя лишь бы как, и только потом приходил в чувства с началом циклического опроса. Было похоже, что теряется синхронизация его порта. Действительно, а что происходит на выводе SCLK контроллера после включения питания? Сначала у меня при настройке тактирования все порты контроллера включаются на ввод с подтяжкой вниз. Это на тот случай, если в программе какой-то порт не будет использоваться и настраиваться, чтобы он не остался в Hi-Z. Затем нужные пины настраиваются в режим альтернативных функций с нужными параметрами. Затем включается и инициализируется периферийный блок (SPI или USART). Возможны ли переключения уровней на выводе SCLK в процессе всех этих настроек? Да запросто. Ладно, забудем все, что было, искусственно создадим условия для ресинхронизации порта. Делаю, проверяю - не работает. Наверное, дело в том, что приходится временно отключать порт от периферии и управлять им вручную, а потом снова включать альтернативную функцию. Могут быть при этом лишние переключения уровней? Кто его знает. Вот тут я в первый раз столкнулся с ситуацией, когда не хватило моего осциллографа. Процесс инициализации проходит однократно, что там проскакивает на SCLK - не видно. Хотел даже поднять из руин обломки цифрового осциллографа, который когда-то пытался сделать. Плата front-end почти готовая, до сотни-другой мегагерц потянет, все остальное тоже есть, и быстрый АЦП, и жирная ПЛИС. Но надо травить плату, затем софт, хоть он есть какой-то, но без переделок не обойтись.
Еще вспомнил, что у меня есть китайский логический анализатор. Но я его ни разу не включал и поэтому не знаю, способен ли он вообще помочь в подобной ситуации. В результате пошел по самому легкому пути - пути параметрического программирования. Перебираем все возможные способы написания интересующего фрагмента программы и просто смотрим на результат. Устраивает - останавливаемся. Не устраивает - перебираем дальше. Хоть возможных комбинаций чуть меньше, чем бесконечность, работающий вариант все же быстро находится. Здесь он выглядит так: сначала надо настроить порт SCLK на вывод. Подать на него ноль. Подождать 2 мс. Настроить порт на альтернативную функцию. Включить и настроить USART. Опять переключить порт на вывод. Подать последовательность ресинхронизации (единица, задержка 2 мс, ноль, задержка 2 мс, единица, задержка 2 мс). Затем снова настроить порт на альтернативную функцию. Теперь можно подавать команду отключения SLEEP - все работает. И дальше датчик работает исправно. Если выбросить любой шаг цепочки или перенести в другое место - не работает. Достал меня сильно этот датчик, добавил в исходник коммент «Magic pass», забросил макет в дальнюю коробку и больше пока к нему не подходил.