Автор: Николай Хабаров, 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, становится понятно, что цена процессора не будет сильно влиять на стоимость изделия.

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

Поделиться с друзьями
-->

Комментарии (22)


  1. golf2109
    08.06.2017 20:39

    а зачем использовать Raspberry?
    это как микроскоп вместо молотка
    возьмите например STM32F030 за 1$
    и играйтесь с DMA


    1. DataArt
      08.06.2017 21:58
      +2

      С помощью DMA управлять шаговыми двигателями микроконтролерами типа stm32 не получится (или это будет притянуто за уши) из-за банальной нехватки оперативной памяти для буфера. А вот просто использовать stm32 без операционной системы/с операционной реального времени можно. Существует множество реализаций, автор статьи так же является автором одной из них.
      Для каких-то целей этого вполне достаточно. Но всегда хочется иметь что-то более современное, например, просто подключить полноценный монитор или иметь Wi-Fi на самом блоке управления. Да, безусловно, все это можно реализовать и на маленьком микроконтроллере, но это будет несравнимо сложнее, чем просто вставить usb-донгл в плату (в случае RPi3 он уже сразу есть на плате).
      Кроме того, разработка под микроконтроллер, как правило, ведется на Си/Си++, что заметно сложнее, чем писать на Python. Можно долго филосовствовать о плюсах языков высокого уровня, но в любом случае Python обеспечит более простую портируемость одного и того же кода между процессорами/платами/платформами. В случае того же контроллера CNC достаточно лишь реализовать доступ к DMA, и остальная часть, скорее всего, заработает сразу.
      Так же не забываем о вопросе производительности. Тот же слайсер для 3d-принтера можно запускать сразу на Raspberry Pi, для микроконтроллера он будет тяжеловат по занимаемому в прошивке месту и производительности.
      И опять же производительности микроконтроллера наверняка не хватит, чтобы считать в реальном времени с десяток осей какого-нибудь более сложного устройства. Для Raspberry Pi это не будет большой задачей в принципе, т. к. в случае того же DMA у Raspberry Pi можно управлять выводами платы количеством до 64, т. е. это до 64 шаговых двигателей (за вычетом используемых внутри самой платы и небольшого числа пинов для выбора направления вращения). Есть ARM-процессоры и с большим количеством выводов.
      А по поводу стоимости, в статье уже отмечалось, что Raspberri Pi Zero стоит $5 — и это целая плата. Плата с микроконтроллером будет стоить больше $1, и конечная разница все равно окажется смешной.


      1. Indemsys
        08.06.2017 23:16
        +1

        Наивное заблуждение считать DMA этаким детерминированным автоматом не потребляющим ресурсы.
        DMA делит ту же шину AHB что и процессор и они между прочим могут там конфликтовать.
        DMA замедляет работу процессора. А еще каналы DMA конфликтуют друг с другом.
        Поскольку Raspberry Pi не проектировался для систем реального времени у меня большие сомнения что его DMA работает достаточно детерминированно и без сбоев при большом количестве каналов.
        В любом случае даже DMA требует рано или поздно прерываний и тут детерминизму в Raspberry Pi придет конец.
        Ну и наконец писать такие программы на Python это очень странно. В Pythone нет никаких средств наблюдения за регистрами процессора и периферии в реальном времени. Невозможно увидеть никаких внутренних переменных драйверов, нельзя оперативно менять содержимое регистров и т.д. и т.п. Это язык абсолютно оторванный от реального времени.
        Для управления моторами есть прекрасные микроконтроллеры STM32, Kinetis и т.д.
        Специально заточены, гораздо более гибкий и контролируемый DMA чем у Raspberry Pi, полная доступность всех регистров периферии и процессора в реальном времени, даже трассировка прерываний и исполнения кода. Жесткая детерминированность.


        1. DataArt
          09.06.2017 13:31
          -2

          Ресурсы конечны всегда, и производительность ARM-процессора на порядок выше, чем у какого-нибудь STM32. Именно потому, что DMA-каналы могут влиять друг на друга, в статье и предлагается синхронизироваться еще с PWM-модулем на процессоре, а не копировать в лоб.
          Если не нравится Python, можно реализовать то же самое хоть на Go, хоть на Java, хоть на чем угодно другом. Вся суть алгоритма сводится как раз к тому, чтобы избежать работы в реальном времени на уровне приложения и передать ее другим аппаратным модулям — пускай они с регистрами и работают.
          Программы тоже можно писать на ассемблере, но зачем, когда есть более удобные инструменты.


          1. Indemsys
            09.06.2017 14:55
            +2

            Наверно будет кому-то откровением, но частота APB шины у PI всего 350 МГц.
            Это та шина через которую работает DMA.
            А у STM32H7 есть четыре! шины AHB по 200 МГц. Чувствуете разницу?
            Это значит что STM32 существенно быстрее работает с периферией чем PI.
            Если учесть что STM32 работает не с DDR а с более детерминированной SRAM, то и вовсе по быстродействию при работе с большим количеством двигателей STM32 уделает PI.


            1. DataArt
              09.06.2017 16:12

              Жаль, только в 1 МБ оперативной памяти STM32H7 создать буфер для длительной работы DMA не получится. Да в принципе DMA здесь будет оверкиллом, эффективнее в рилтайме считать и дергать выводы напрямую из регистров.
              Но в любом случае я сомневаюсь, что вы сильно забьете шины этими активностями. Гораздо раньше вы упретесь в производительность вычисления какого-нибудь синуса или косинуса при реализации круговой интерполяции… Даже 4х ядер Raspberry Pi при вычислении тригонометрии заданного движения вряд ли хватит чтобы существенно занять полосу DMA.


              1. Indemsys
                09.06.2017 16:47
                +1

                Заканчиваю уже занудствовать. Только напоследок скажу что оперативная память для DMA тоже за уши притянута.
                При импульсе в 1 мкс можно данные и из SPI Flash читать или NAND. А уж это то подключаются к любым микроконтроллерам и размеры может иметь какие угодно.
                Хотя сама идея заранее формировать импульсы для управления двигателем на интервале минуты без корректировки по обратной связи вызывает нехорошие чувства.


              1. Siemargl
                09.06.2017 19:00
                +1

                Даже 4х ядер Raspberry Pi при вычислении тригонометрии заданного движения вряд ли хватит чтобы существенно...
                Есть элементарные быстрые алгоритмы интерполяции без FPU — точность движков ведь там не супер…

                А на STM32 делают пром.контроллеры с управлением движением по паре осей. Кажется Delta и Овены.

                Но всегда хочется иметь что-то более современное, например, просто подключить полноценный монитор или иметь Wi-Fi на самом блоке управления.
                А по поводу стоимости, в статье уже отмечалось, что Raspberri Pi Zero стоит $5 — и это целая плата.
                Противоречие, на Pi0 нет вифи.

                И с учетом жестких накладок по пинам на Пи- кажется, что они все же для других задач планировались. Хотя сама идея DMA очень привлекательна


                1. Shtucer
                  09.06.2017 20:27

                  На P0W (Raspberry Pi Zero W) — есть.


  1. ser-mk
    08.06.2017 23:55
    +1

    А чем критична постоянная скорость для CNC? В том плане зачем требуется именно постоянная скорость? Ведь мы управляем подачей инструмента а не скоростью вращения. Это же не 3д принтер где простой, с постоянной подачей пластика, может повлиять на геометрию фигуры.

    P.S. что случилось со статьей? Куда делись предыдущие комментарии?


    1. DataArt
      09.06.2017 13:59

      При скачке скорости у CNC, например с фрезой, фрезу может переломить, если она не сумеет пропилить материал. Да и сам пропил будет плохого качества, если она будет все время дергаться.
      Говоря про постоянную скорость, мы скорее имели в виду изменения скорости в более узком временном интервале. А именно, если импульсы на шаговый двигатель буду идти с неправильными (в случае линейного движения с разными) интервалами, мотор будет резко дергаться (ведь фактически мы будем изменять скорость, что моментально нельзя сделать чисто физически), а это будет давать сильные нагрузки на механику устройства.
      P.S. Извините, произошла накладка, открыли повторно, действительно потеряли два комментария. Просим прощения у их атворов!


      1. ser-mk
        09.06.2017 20:18

        если это не было не преднамереное удаление, то снова задам свой первый вопрос:

        каждый импульс будет занимать 128 байт (четыре контрольных блока по 32 байт)

        откуда взялись 4 блока? если для установки и снятия импульса нужно вроде как два блока?


        1. DataArt
          13.06.2017 12:26

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

          P. S. Спасибо за вопрос, удаление, разумеется, не было преднамеренным!


          1. ser-mk
            13.06.2017 17:17

            Спасибо за ответ. Но остается не понятным каким образом задается «задержка».
            Правильно понимаю, что для PWM модуля выделяется еще один канал DMA и там как раз эти блоки и используются?


            1. DataArt
              13.06.2017 17:35

              DMA все тем же одним каналом (внутри контрольных блоков адрес назначения меняется на адрес PWM-модуля) пишет в FIFO буфер модуля PWM. Дело в том, что PWM-модуль сериализует свой FIFO-буфер с жесткой ограниченной частотой, и DMA-модуль при копировании данных вынужден ждать сигнала готовности принятия данных в буфер. Получается, что мы можем реализовать довольно точную задержку за счет того, что запись N байт в PWM займет известное (согласно частоте PWM-модуля) время.


  1. serghs
    09.06.2017 00:07

    Здорово! Особенно заинтересовал сам ключевой момент — использование устройства как микро-компьютера с полным фаршем на борту и при том работа на самом низком низком уровне, необходимом для управления приводами и прочим электрическим добром. Т.е. доступ по локалке для обмена информацией стандартными средствами, без плясок с бубном. Нормальный Линукс, стандартные сервисы + управление сторонним железом и все одной платой.

    Как на ваш взгляд, какое ограничение (программное + аппаратное) имеет ваш подход к реализации, скажем большого количества каналов ШИМов и АЦП, скажем для управления N устройствами с парой ШИМ и тройкой АЦП на устройство?


    1. zedroid
      09.06.2017 08:35

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

      Задумка ужасна до слез. 21 век на дворе, радиоресурсы это ценность почище воздуха, а такие методы его нерационального использования вызывают желание оторвать руки реализаторам.
      Сама статья очень крута, спасибо за нее.
      P.S. Ответил не там.
      Подводные камни- Много каналов==много выделяемой памяти
      Сложные сигналы== много выделяемой памяти
      Небольшой джиттер по фазе


      1. Indemsys
        09.06.2017 09:46

        Большой или не большой зависит от драйверов остального «фарша».
        Самый большой поток DMA идет на дисплей, потом потоки DMA на несколько USB, потом DMA на SD карту, Если Ethernet есть, то DMA же нужен и для него…
        А если еще кто нибудь задумает камеру подключить, то не удивлюсь если DMA на GPIO будет выдавать ошибку.
        Автор скорее всего на свой Raspberry Pi даже не дышит чтобы он протянул минуту работы DMA без искажения сигналов.


    1. DataArt
      09.06.2017 14:23

      Об АЦП
      АЦП на борту у Raspberry Pi нету. Можно по I2C подключить внешний ADC, например ads1115 — потенциально 4 канала ADC и 255 устройств на шине. Можно подумать над подключением других внешних ADC и получить еще больше каналов. Так что ограничения возникнут скорее от того, что вы собираетесь делать с данными от этих устройств, чем от количества самих девайсов.

      О ШИМ
      ШИМ можно реализовать этим методом, ограничением в данном случае будут именно количество ножек процессора и максимальная частота ШИМа. В случае Raspberry Pi, у процессора есть 64 вывода, из которых следует вычесть используемые на самой плате. В реальности на разъем платы выведено 26 выводов, которые доступны для ШИМа таким методом. Хотя потенциально можно было бы использовать все 64 вывода, будь они выведены на разъем и не подключены к чему-нибудь внутри.

      В вашем примере, если предположить, что два вывода мы отдадим на подключение ADC, получим 12 устройств.


      1. serghs
        09.06.2017 15:44

        Большое спасибо за ответ!
        На самом деле в упомянутой задаче про N устройств в расчете на одно устройство одновременно (условно, конечно) нужен 1 ШИМ (управление силовым ключем) и 2 АЦП (ток и напряжение), еще 1 I2C (температура) или один 1-Wire (тут частота опроса не критична). Другое дело, что второй набор пинов на то же самое устройство не должен совпадать с первым, за исключением измерителей температуры.

        На самом деле не хочется огород колхозить из кучи промежуточных плат.


  1. WGH
    09.06.2017 18:48

    А нельзя было запинить память в RAM через mlock(2)?

    > mlock() and mlockall() respectively lock part or all of the calling process's virtual address space into RAM, preventing that memory from being paged to the swap area.


    1. DataArt
      09.06.2017 19:07
      +1

      mlock() блокирует лишь виртуальную память, т. е. он предотвратит попадание памяти в swap. Но реальный адрес в физической памяти все равно может быть изменен, т. к. память может быть перенесена по желанию менеджера памяти (например, чтобы дефрагментировать память).
      В документации ядра сказано: Linux supports migration of mlocked pages and other unevictable pages. This involves simply moving the PG_mlocked and PG_unevictable states from the old page to the new page.