Генерируем произвольные последовательности на выводах платы Raspberry Pi

8 июня
Николай Хабаров
Генерируем произвольные последовательности на выводах платы Raspberry Pi
Николай Хабаров — Embedded Expert DataArt и евангелист технологий умного дома — рассказал, как написать обычное user space-приложение на Python для современного ARM-процессора с ОС Linux для генерирования сложных последовательностей импульсов на выводах платы. Суть идеи — использовать DMA-модуль процессора для копирования из предварительно подготовленного буфера в памяти в GPIO с высокой точностью по времени.

Когда речь заходит о необходимости сгенерировать сложную последовательность импульсов, например, для шаговых двигателей, обычно используют старые добрые простенькие микроконтроллеры с установленной специальной операционной системой реального времени или вообще без операционной системы. Реализация при этом, в лучшем случае, написана на C++. Сейчас процессоры шагнули далеко вперед и имеют массу преимуществ: производительность, возможность использования операционной системы Linux со всей инфраструктурой и ПО, а также высокоуровневых языков программирования, таких как Python. И все же современные микроконтроллеры для генерирования сложных последовательностей на выводах GPIO, как правило, не используют.

Я реализовал генерацию импульсов для управления шаговыми двигателями проекта PyCNC — проекта контроллера машин с ЧПУ, станков, 3D-принтеров, полностью написанного на Python и запускаемого на современном ARM-процессоре на плате Raspberry Pi.

Статья может быть полезна желающим реализовать генерацию сложных последовательностей установки уровней на выводах одного или нескольких GPIO на других высокоуровневых языках программирования, используя DMA-модули других процессоров.

GPIO

General Purpose Input Output (GPIO) — модуль процессора, который отвечает за установку логических уровней на физических выводах. Как известно, в цифровом мире на выводе может быть «0», т. е. ножка притянута к «земле», либо «1», т. е. ножка притянута к питанию. Наверняка многие из вас зажигали светодиод, управля одной из ножек микроконтроллера. Например, у микроконтроллеров AVR это делалось установкой соответствующего бита в переменную, или, как ее еще часто называют, регистр PORTx. Что же это за переменная? Если посмотреть заголовочные файлы, там будет нечто наподобие:

#define PORT (*(volatile uint8_t *)(0x12345678))

Иначе говоря, запись состояния вывода — запись этого значения по заранее известному адресу. Обратите внимание, что это — не адрес в оперативной памяти, тем более, не адрес в виртуальной памяти процесса, это адрес в адресном пространстве самого процессора. Физический, где-то внутри чипа, к этому адресу подключен реальный модуль GPIO, к которому мы и обращаемся по этому адресу через ядро процессора, передавая байт информации для установки состояния вывода. И такие модули есть практически у любого процессора. ARM-процессор Raspberry Pi — не исключение. Чтобы узнать, где именно в адресном пространстве расположен каждый модуль, нужно взглянуть на документацию интересующего вас процессора.

Для Raspberry Pi этот документ находится здесь.

В нем по адресу шины 0x7E200000 распологаются регистры GPIO, другими словами, записывая данные в соответствующие адреса, можно управлять состоянием выводов. Сразу отметим, что вся периферия у процессоров Raspberry Pi 1, 2 и 3 одинаковая, отличается только физические адреса, в которые мапятся адреса шины периферии, начиная с адреса 0x7E000000. Для версии 1 Raspberry Pi это будет 0x20000000, для версий 2 и 3 — 0x3F000000. Т. е. для процессора RPi3, чтобы обратиться к адресу шины 0x7E200000, нужно писать по физическому адресу 0x3F200000. В документации по ссылке выше все адреса — адреса шины.

В подавляющем большинстве случаев, на ARM-процессоре будет установлена ОС Linux. Сразу возникает вопрос, как получить доступ к физической памяти. Доступ к ней есть в самом ядре ОС. Мы же хотим сделать обычное приложение, запускаемое в виртуальном адресном пространстве. К счастью, ядро ОС Linux предоставляет доступ к физической памяти через виртуальное устройство '/dev/mem', открыв которое (нам потребуются права суперпользователя), мы можем писать в физическую память. Справедливости ради отметим, что в официальной для Raspberry Pi ОС Raspbian существует еще устройство '/dev/gpiomem,' предоставляемое драйвером 'bcm2835_gpiomem', доступ к которому есть даже без прав суперпользователя — по нулевому отступу  сразу идет доступ к первому регистру GPIO.

Давайте слегка развеемся практикой, думаю, так будет проще понять все написанное выше. Напишем простое приложение на Python, которое будет зажигать светодиод, подключенный к выводу GPIO21, работающее на Raspberry Pi 2 и 3 (для RPi1 поправьте адрес в листинге). Вместо Python здесь можно использовать любой другой язык программирования, способный вызывать системные функции из libc.so. Вот, собственно, код:

Магические цифры я в коде оставил умышленно — так будет проще было объяснять. Давайте разберем код построчно.

Пять верхних строчек — импорт необходимых стандартных Python-модулей.

В 7-й строке мы непосредственно открываем файл '/dev/mem' чтобы получить доступ к памяти.

В 8-й строке мы вызываем системную функцию memmap, которая спроецирует наш, хоть и виртуальный, файл на виртуальное адресное пространство процесса. Т. е., записывая в виртуальную память процесса, мы  будем реально писать в физическую память. В коде также указан отступ, начиная с какого адреса проецировать память, и длина (mmap.PAGESIZE). Мы указываем адрес первого регистра GPIO и длину размером в одну страницу памяти. С первого взгляда может показаться, что достаточно просто открыть '/dev/mem', отступить до нужного адреса и начать писать файл, но, увы, так сделать не получится.

В 9-й строке мы закрываем файл, т. к. нам не обязательно хранить дескриптор открытым, маппинг уже никуда не пропадет.

В 11–14-й строках мы читаем и пишем в регистр с отступом 0x08. Если взглянуть в документацию, это регистр GPFSEL2 GPIO Function Select 2. С помощью этих регистров мы выбираем, какую функцию будет выполнять ножка чипа. В данному случае мы выставляем (сначала очищаем, затем выставляем при помощи операции ИЛИ) 3 бита начиная с третьего в 001, чтобы вывод работал как выход. Дело в том, что выводов довольно много, чтобы выставить различные режимы, одного регистра недостаточно, поэтому регистры разделены на группы по 10 выводов в каждой — отсюда такая магия с числами.

В 16-й и 22-й строках мы устанавливаем обработчик, ожидающий нажатия Ctrl+C на клавиатуре для выхода из программы.

На 17-й строке мы запускаем бесконечный цикл.

В 18-й строке мы переводим вывод в высокое состояние, записывая битовую маску в регистр GPSET0.

На 19-й и 21-й строчке мы реализуем задержку в половину секунды.

В 20-й строке мы переводим вывод в низкое состояние, записывая битовую маску GPCLR0.

На строках 25 и 26 мы прибираем за собой: деинициализируем пин, переводя в дефолтное состояние — вход — и закрываем маппинг памяти.

Небольшое замечание: у BCM2835 нет регистров, аналогичных PORTA, как у AVR, т. е. мы не можем сразу записать состояние (высокий или низкий уровень) всех пинов разом. Есть только регистр SET, который позволяет устанавливать логические единицы на всех пинах, где в битовой маске указаны единицы, и регистр CLEAR, который позволяет установить логические нули на всех пинах, где в битовой маске указаны единицы.

Запустив этот код с правами суперпользователя на Raspberry Pi 2 или 3, например, через команду 'sudo python gpio.py', и подключив светодиод к выводу 21, мы увидим, что он мигает.

Если вам нужна готовая реализация управления пинами GPIO, можете использовать класс GPIO из этого файла.

Шаговые двигатели

Управление шаговыми двигателями — дело не такое простое, как может показаться на первый взгляд. Давайте совершим краткий экскурс в суть проблемы и разберемся, почему вообще появилась необходимость генерировать такие последовательности. Мы не будем вдаваться в подробности теории устройства шагового двигателя, желающие могут почитать об этом тут.

Вместо это сразу перейдем к реальности, где у шагового двигателя имеется 4 или 5 выводов от четырех обмоток. Токи, необходимые для вращения двигателя, довольно велики — ни один микроконтроллер или процессор обеспечить такие не сможет. Также для создания таких токов необходимо напряжение, как правило, превышающее напряжение логики процессора. Поэтому с шаговыми двигателями применяют специализированные микросхемы — драйверы, иногда даже интегрированные в корпус двигателя. Простейшие драйверы (например, ULN2003) просто предоставляют низкоточные входы управления обмотками, совместимые со стандартным напряжением логики микроконтроллеров. С таким драйвером придется генерировать последовательность импульсов для каждой обмотки. Большинство же драйверов, включая одни из самых популярных A4988 и DRV8825, берут всю головную боль по работе с обмотками двигателя на себя. Они предоставляют простой интерфейс из двух низкоточных выводов STEP и DIR. Как можно догадаться из их названий, при подаче импульса на STEP драйвер двигает двигатель на один шаг (также драйвер можно сконфигурировать на так называемые микрошаги, т. е. движения на 1/2, 1/4 и т. д. шага — это достигается приемами работы с обмотками). Вывод DIR используется для выбора направления вращения двигателя в зависимости от того, приложен к нему низкий или высокий уровень.

Типовая схема включения A4988 выглядит так (картинку я заимствовал на просторах интернета):

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

Этот подход к управлению двигателями с выводами STEP и DIR наиболее универсален, т. к. такое подключение позволяет реализовать на самом микроконтроллере практически любые изыски. Например, у читателя может возникнуть вопрос — есть ли драйверы с I2C или UART-интерфейсом? Теоретически, такой драйвер можно сделать, но для управления тем же 3D-принтером нужно синхронно управлять несколькими шаговыми двигателями. Причем и у каждого из них могут быть разные параметры ускорения, количество шагов на миллиметр, максимальные скорости. В итоге подобный драйвер по своей сложности был бы аналогом контроллера CNC(ЧПУ).

Как вы могли догадаться, описанным выше методом управления GPIO нельзя добиться правильного формирования импульсов на выводах. Дело не только в том, что задержки при выполнении Python-кода непредсказуемы. Даже если реализовать все на C и в виде ядерного модуля OC Linux, мы не добьемся хорошего результата, т. к. Linux — не операционная система реального времени. В любой момент ядра процессора могут переключиться на выполнение какой-либо другой задачи, и мы не дадим импульс вовремя. Но разве такие мелочи могут остановить нас на пути к задуманному!

DMA

Direct Memory Access — специализированный аппаратный модуль процессора, позволяющий копировать память из одного места в другое, не прибегая к услугам самих ядер центрального процессора. Такие модули существует даже у простых микроконтроллеров, однако реализация сильно различается от одной модели процессора к другой. У Raspberry Pi реализация довольно средненькая — есть все, что нужно, но без излишеств. А, например, у Rockchip RK3399 DMA-модуль больше напоминает мини-ядро процессора с собственным, хоть и небольшим, набором инструкций.

Я не ставлю перед собой задачу написать полный перевод оригинальной документации. И расскажу всего лишь об основных регистрах, которые позволят нам запустить DMA-модуль в режиме копирования данных в GPIO модуль.

Как вы могли предположить, мы будем генерировать в памяти буфер и копировать его по адресу расположения GPIO-модуля. Но для работы DMA-модуля необходимо, чтобы этот буфер располагался где-то в физической памяти. Естественно, любая виртуальная память, если она не попала в swap, будет также находится в физической памяти. При помощи '/proc/self/pagemap' можно получить таблицу маппинга памяти собственного процесса в физическую память, т. е. выделить буфер и затем найти его физический адрес.

Но с DMA так делать не стоит — все из-за той же возможности попадания памяти в swap и того, что менеджер памяти операционной системы может перенести вашу выделенную память в другое место. Можно написать простенький ядерный модуль, который будет при загрузке вызывать метод из ядра Linux kmalloc() — этот метод выделяет и также блокирует память от возможных переносов. Затем вы отдаете приложению адрес выделенной памяти, например, через виртуальное устройство. Имея этот адрес, приложение может получить доступ к памяти абсолютно тем же методом, который был описан в разделе GPIO. Адрес или буфер, который мы выделили, можно использовать с DMA-модулем.

Но писать ядреный модуль, пусть и предельно простой, вам, вероятно не захочется.

Хорошо, есть и готовое решение — виртуальное устройство '/dev/vcio'. Оно создается драйвером видеокарты Raspberry Pi. Для общения VideoCore и ЦП используются так называемые mailboxes, которые, по сути, кусочки, выделенные в памяти, отведенной для видеокарты. По умолчанию, видеокарте выделяется 64 МБ, а при желании это значение можно изменить. То, что мы выделим немного ресурсов из видеопамяти для наших нужд, на работу видеокарты не повлияет, главное, не отобрать слишком много. На практике для нормального функционирования рабочего стола ОС Raspbian достаточно 30-35 МБ памяти, т. е. примерно половины. Т. ч. вторая половина полностью доступна нам, если мы не планируем запускать приложения, использующие OpenGL. Тем более, выделение части памяти для передачи в сам GPU — штатная процедура. И, если мы будем выделять память, но не отдавать ее видеопроцессору, никаких проблем не возникнет. Вот официальный пример, который использует такое выделения памяти.

Этот процесс довольно тривиален, хотя и скрыт за магическими цифрами в коде. Открываем /dev/vcio, затем, используя метод ioctl() передаем структуру с запросом и получаем ответ. Нам нужно выделить память и заблокировать ее, чтобы менеджер памяти никуда не утащил выделенный нами кусочек. Можете посмотреть реализацию по ссылке выше, нас интересуют методы mem_alloc(), mem_lock() и, конечно, методы, позволяющие прибрать за собой mem_unlock(), mem_free(). Если вас смущает реализация на C, можно переписать эти методы на любой другой язык.

Ну хорошо. Место, где выделять буфер, мы нашли. Теперь необходимо как-то запрограммировать DMA, чтобы он выполнил то, что мы хотим. Еще раз напомним, что DMA-модуль — всего лишь несколько регистров в физической памяти. Управление им происходит все так же — записью структур по нужным адресам. Всего у Raspberry Pi 16 DMA-каналов. Первые восемь полнофункциональные, остальные восемь по функционалу немного ограничены. У каждого канала — свой набор регистров. Базовый адрес шины DMA модуля — 0x7E007000, т. е. для Raspberry Pi 2 или 3 нужно писать по адресу 0x3F007000. Здесь располагается первый регистр первого канала DMA. Каждый последующий находится со сдвигом 0x100 (за исключением 15, он расположен на 0x7EE05000).

Каким же каналом воспользоваться? Выбор довольно сложен, т. к. каналы могут быть использованы ядром. Можно узнать, какие именно каналы использованы, у самого ядра через sysfs командой 'cat /sys/class/dma/dma0chan*/in_use'. Как правило, стоит избегать использования 0, 1, 2, 3, 6, 7 каналов, т. к. их используют ридер microSD-карточки и драйвер видеоплаты.

Основные регистры для управления DMA-модулем — CS и CONBLK_AD. Именно заполняя их, мы запускаем DMA. Первый регистр CS, в нем нас интересуют поля:

Бит номер Поле Описание Доступ
31 RESET Запись единицы сбрасывает DMA-модуль. W
1 END При записи единицы сбрасывается флаг окончания передачи. W
0 ACTIVE При записи единицы DMA начнет работать. С помощью этого поля можно ставить DMA на паузу. После окончания передачи поле автоматически примет значение 0. RW

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

В регистр CONBLK_AD записывается адрес, в котором лежит так называемый контрольный блок, описывающий, что делать DMA-модулю. При этом контрольные блоки представляют собой связанный список, т. е. с помощью несколько контрольных блоков можно выстроить цепочку из различных задач. DMA-модуль, выполнив задачи, автоматически пройдется по всем контрольным блокам. Именно с адреса в CONBLK_AD DMA-модуль начнет выполнение копирования при установке бита ACTIVE в регистре CS.

Контрольный блок должен обязательно храниться в памяти с выравниванием в 32 байта (5 младших бит адреса должны быть нулевыми). А теперь давайте посмотрим, как устроен контрольный блок:

Отступ,
байт
Поле Описание
0 TI Набор различных флагов, задающих параметры копирования данных.
4 SOURCE_AD Адрес источника, из которого начинать копирование.
8 DEST_AD Адрес назначения, в который производить копирование.
12 TXFR_LEN Количество байт, которое необходимо скопировать. В специальном 2D-режиме старшие 16 бит задают количество циклов копирования, младшие 16 бит — количество байт, которое необходимо скопировать за цикл.
16 STRIDE Используется только в 2D-режиме. Младшие 16 байт хранят знаковое число, задающее, насколько нужно сдвинуть адрес источника перед началом следующего цикла. Старшие 16 байт хранят знаковое число, на которое нужно сдвинуть адрес назначения перед началом следующего цикла.
20 NEXTCONBK Указатель на следующий контрольный блок.

Каналы с 0 по 7 поддерживают 2D-режим (задается флагом TDMODE(бит 1) в поле TI контрольного блока), который позволяет организовать X копирований по Y байт. При этом адреса назначения и/или источника перед каждым копированием могут быть увеличены/уменьшены на некоторую величину, согласно полю STRIDE в контрольном блоке.

У каждого DMA-канала существует еще набор регистров TI, SOURCE_AD, DEST_AD, TXFR_LEN, STRIDE, NEXTCONBK, которые можно только читать. Они загружаются из текущего контрольного блока.

Что ж, попробуем сделать что-нибудь простое с DMA и GPIO-модулями на Python.

Чтобы листинг программы не был утомительно длинен и не содержал лишних магических цифр, давайте возьмем немного готового кода из этого файла. Из него мы будем использовать константы и классы 'PhysicalMemory' (доступ к физической памяти, как мы делали это в примере с GPIO) и 'CMAPhysicalMemory' (это реализация выделения физической памяти с использованием видеодрайвера /dev/vcio, о котором мы писали выше).

Когда вы запустите эту программу(не забудьте запустить с правами суперпользователя), светодиод, подключенный к 21-му выводу, на полсекунды загорится на полную яркость (чтобы была заметна разница), а затем будет светиться на половину яркости, пока вы не нажмете клавишу enter. Давайте разберем построчно.

2–3-я строки — импорт модулей.

5-я и 6-я — переменные указывающие какой DMA канал и вывод GPIO мы используем.

8–15-я — инициализируем указанный GPIO-вывод как выход и зажигаем на полсекунды (чтобы глазом была хорошо заметна разница, когда включится DMA). По сути, это то же самое, что мы делали в первой программе, но написанное на чуть более высоком уровне.

17-я строка — просим видеодрайвер выделить нам 64 байта в памяти. В классе CMAPhysicalMemory память выделяется с выравниванием равным размеру страницы, т. е. 4096 байт, поэтому при использовании этой памяти, начало выделенной памяти всегда также будет выровнено по 32 байтам, которые требуются DMA-модулю.

18-я строка — заполняем первую структуру контрольного блока. На этом остановимся чуть подробнее.

19-я строка — первые 4 байта — флаги TI (Transfer Information). Мы используем DMA_TI_NO_WIDE_BURSTS — отключаем burst-режим — оптимизацию для копирования памяти — нам же надо, чтобы память копировалась строго, как мы хотим.

DMA_TI_WAIT_RESP — DMA-модуль при каждой записи будет ожидать подтверждения записи со стороны принимающего модуля.

Еще могут быть полезны флаги DMA_TI_SRC_INC и DMA_TI_DST_INC — по умолчанию, DMA-модуль будет читать и писать всегда по одному адресу по 4 байта. Установив эти флаги, мы скажем DMA-модулю, что после каждой записи нужно увеличить адрес источника и/или назначения. Полный список флагов есть в документации.

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

21-я строка — указываем записывать данные по адресу GPIO-модуля в регистр SET, т. е. запись единиц будет переводить пины в высокий уровень, нули никак не будут влиять на выводы.

22-я — указываем длину передачи — 4 байта.

23-я строка — stride, мы его не используем, поэтому пишем 0.

24-я строка — поле next control block, ссылаемся на следующие 32 байта.

25-я строка — выравнивание, но как мы помним, в 21-й строке мы указали именно эти 4 байта как источник, поэтому кладем туда полезную информацию, а именно — тот бит, который мы должны передать в GPIO.

26 строка — выравнивание.

28–37-я строки — заполнение второй, такой же структуры. Но теперь мы пишем в регистр CLEAR модуля GPIO, чтобы переводить пин в низкий уровень. И в качестве следующего блока используем наш первый блок, чтобы закольцевать передачу.

38–39-я строки — записываем наши контрольные блоки в физическую память.

41-я строка — получаем доступ к памяти с DMA-модулем с указанным каналом.

42-43-я строки — сбрасываем DMA-модуль.

44-я строка — заполняем адрес первого контрольного блока.

45-я строка — запускаем DMA-модуль.

47-я строка — ждем нажатия клавиши Enter.

49–52-я строки — прибираем за собой. Останавливаем DMA-модуль и переводим наш вывод в дефолтное состояние (вход).

Если вместо светодиода подключить осциллограф, мы увидим такую картинку:

Картинка получена с выводов платы Raspberry Pi 2. Как видите, частота около 1.5 МГц — почти предел (можно еще под оптимизировать и получить ~2 МГц) для такого метода, да и по осциллограмме видно, что прямоугольники становятся уже не совсем прямоугольными.

Сюрпризы DMA

Казалось бы, вот оно, счастье — мы можем задавать произвольное количество DMA-блоков с самыми хитрыми записями в GPIO-модуль. Сам DMA — аппаратный модуль, который тактируется от физического генератора на чипе, все должно быть хорошо… Но нет, есть еще пара сюрпризов.

С помощью DMA такими записями можно добиться разрешения порядка 1 микросекунды. Этого достаточно, чтобы сгенерировать импульс на вывод STEP, который, как правило, должен быть около 2 микросекунд. Из этого следует, что разрешающая способность для генерирования импульсов для шаговых двигателей должна быть 2 микросекунды, т. е. 500 000 записей на секунду. На каждую запись желательно отводить 4 байта, чтобы контролировать все ножки. Из этого следует: чтобы запустить двигатели на 1 минуту (например простое перемещение на 200 мм со скоростью 200 мм/мин — довольно реалистичное действие), нам понадобиться 114 МБ. А скорость может быть в два раза ниже — тогда потребуется уже 228 МБ. Немного расточительно, не так ли? Но, кроме этого, возникнет еще и необходимость быстро сгенерировать такой объем информации. Даже простое обнуление 114 МБ займет много времени.

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

В таком виде управление GPIO через DMA вполне имеет право на жизнь, если речь идет об управлении чем-либо инерционным. Например, так можно реализовать ШИМ для нагревателя экструдера или стола 3D-принтера. Но шаговыми двигателями так управлять нельзя. Что же делать и как быть?

Давайте оптимизировать объемы и искать пути синхронизации.

В результате долгих дум и экспериментов была выбрана следующая организация контрольных блоков:

Осциллограмма вверху показывает, как бы менялся уровень на выводе GPIO, которым мы управляем на временной дорожке с контрольными блоками DMA (масштаб не соблюден). «Пауза 1» задает длительность самого импульса, «Пауза 2» — паузу между импульсами. При такой организации блоков объем занимаемой памяти будет гораздо ниже. Возьмем упоминавшийся ранее пример с перемещением двигателем на 200 мм со скоростью 200 мм/мин. Тут уже для задания скорости нужно будет менять значение «Пауза 2», не трогая при этом общее количество блоков. Количество блоков будет зависеть от количества импульсов, которые мы хотим создать, т. е. от длины перемещения, а не от его скорости. А физические размеры любой CNC-машины конечны.

В том же примере (200 мм при 400 импульсах на мм — 80 000 импульсов) каждый импульс будет занимать 128 байт (четыре контрольных блока по 32 байт), а суммарный объем буфера DMA будет ~9.8 МБ. Экономия налицо. Конечно, мы бы хотели управлять большим количество двигателей одновременно, в этом случае количество контрольных блоков возрастет. Но в классическом 3D-принтере четыре мотора, т. е. нам потребуется не более 39.2 МБ, а учитывая, что многие импульсы могут совершаться в одно и тоже время (в одном контрольном блоке мы по-прежнему можем выставлять несколько выводов), и того меньше.

С объемом и нагрузкой на память разобрались, как теперь быть с точностью? Здесь задача сводится к тому, чтобы в принципе сделать паузы, да еще и сделать их максимально точными, т. к. время записи в модуль GPIO довольно стабильно. Для этого используем специальную возможность DMA-модуля — ожидание сигнала готовности стороннего модуля. Удобнее всего использовать модуль PWM (да, тот, который может генерировать импульсы на ножках, но мы используем его без выведения сигналов на ножки). Если модуль PWM запустить в режиме сериализации данных, он также будет генерировать сигналы готовности, понятные DMA при заполнении FIFO-буфера модуля PMW. При этом сам PWM-модуль не зависит от памяти и всегда работает со стабильной частотой. При таком решении, паузы будут зависеть от частоты PWM модуля и количества байт, которые мы будем класть в FIFO.

Чтобы заставить DMA-модуль ожидать сигналов готовности PWM-модуля, нужно установить флаг PERMAP в регистре TI контрольных блоков, выполняющих задержки, в значение 5, и запустить PWM-модуль в режиме сериализации с необходимой нам частотой.

Поскольку здесь поставлена более специфическая задача и ее реализация выходит за рамки короткого и простого листинга программы, предлагаю ознакомиться с этой реализацией самостоятельно по ссылке, класс DMAGPIO. Можете посмотреть на пример использования классов из модуля rpgpio, это тот самый код, который генерировал импульсы для картинки в самом начале статьи (последовательность из 1, 2, 3, 4 и 5 импульсов). Осциллограмма более крупно:

Код довольно простой, поэтому комментарии, думаю, не нужны.

import rpgpio   PIN=21 PINMASK = 1 << PIN PULSE_LENGTH_US = 1000 PULSE_DELAY_US = 1000 DELAY_US = 2000   g = rpgpio.GPIO() g.init(PIN, rpgpio.GPIO.MODE_OUTPUT)   dma = rpgpio.DMAGPIO() for i in range(1, 6):     for i in range(0, i):         dma.add_pulse(PINMASK, PULSE_LENGTH_US)         dma.add_delay(PULSE_DELAY_US)     dma.add_delay(DELAY_US) dma.run(True)   raw_input("Press Enter to stop") dma.stop() g.init(PIN, rpgpio.GPIO.MODE_INPUT_NOPULL)

И еще немного об элегантных технических решениях на DMA-модулях

Говоря о DMA-модуле, нельзя не упомянуть очень красивый проект, реализующий FM-трансмиттер на плате Raspberry Pi.

Проект также использует DMA-модуль, авторы говорят о возможности генерировать несущую до 250 МГц. Очевидно, что DMA-модуль простым копированием буфера не сможет передать сигнал с частотной модуляцией на такой частоте. Реализация хитра и красива. Несущая обеспечивается модулем PWM (в данном случае сигнал PWM выводиться на ножку, которая жестко выбирается из доступных для PWM-модуля и является антенной), которому вполне под силу генерировать прямоугольники с такой частотой. А, как известно, первая гармоника спектра периодического прямоугольного сигнала как раз и будет нужным нам синусом для несущей частоты. Затем, используя DMA-модуль, аудиоданные копируют во все тот же PWM-модуль, сдвигая его частоту, и тем самым добиваются частотной модуляции.

PRU

На платах BeagleBone тоже имеется так называемый PRU (programmable real-time unit), который представляет собой 200 МГц 32-битный сопроцессор с доступом к выводам, памяти и другой периферии процессора AM3358. Это некое сочетание современного процессора и классического микроконтроллера на одной плате. С помощью этого решения также можно генерировать цифровые сигналы сложной формы и управлять шаговыми двигателями. В этой статье рассматривать такое решение мы подробно не будем — всего лишь обозначим факт его существования.

Заключение

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

Как видите, современные процессоры вполне способны выполнять задачи, которые обычно перекладывают на классические микроконтроллеры. При этом мы можем использовать весь арсенал окружения современного процессора и реализовывать куда более современные вещи, обращаясь для достижения цели к более продвинутым средствам разработки. Если взглянуть на стоимость Raspberry Pi Zero, становится понятно, что цена процессора не будет сильно влиять на стоимость изделия.

В завершение — небольшое видео, демонстрирующее, как работает этот подход на реальном железе: