Управление сервомашинкой из Cubieboard2

Продолжая тему разработки модулей ядра сегодня посмотрим на использование таймеров и прерываний. Это пожалуй 2 наиболее специфичные функции, которые умеет микроконтроллер и обычно недоступны при разработке классического приложения для ОС Linux(да и Windows тоже), а при работе с внешними устройствами эти функции очень нужны. В качестве примера — управление сервомашинкой и кино в жанре «прибытие поезда».

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

В нашем процессоре есть 2 типа таймеров: обычные и высокоточные(тактируемые от AHB_CLK, который составляет сотни мегагерц(зависит от конкретных настроек)). Сегодня рассмотрим обычные аппаратные таймеры. (Есть еще и программные таймеры — таймерные функции, они так же выполняются в контексте прерывания но уже благодаря системному таймеру)

Таймеры общего назначения

Страница 108 в мануале на процессор.
Всего в системе 6 таймеров:
  • Timer0 и Timer1 — используются системой для отсчета времени. Так рекомендует даташит и это, вродебы, было так в Cubieez. В Арче же Таймер 0 похоже отключен, но обработчик прерывания зарегистрирован. Таймер 1 судя по всему используется при загрузке, на нем остается установленный обработчик, но в процессе работы число срабатываний не увеличивается и постоянно равно 11.
  • Timer2 — используется RTC и для генерации сигналов переключения задач ОС. Тут у нас так же висит обработчик прерываний, но счетчик его срабатываний = 0.
  • Timer 3, 4, 5 — свободные таймеры которые можно смело использовать для своих целей
Еще есть WatchDog и «Alarm» — напоминания в определенное время. Но эти функции пока оставим без внимания.
Т.к. первые три таймера в общем случае заняты — обратим внимание на Таймеры 3-5.
Каждый* таймер имеет три регистра:
  • TMRn_CTRL_REG — управляющий регистр, тут выбирается входной тактовый сигнал, делитель и режим работы таймера.
  • TMRn_INTV_VALUE_REG — начальное значение таймера при сбросе
  • TMRn_CUR_VALUE_REG — текущее значение таймера(*таймер3 этого регистра не имеет)

Так же есть регистры настройки прерываний от таймеров:
  • TMR_IRQ_EN_REG — разрешение, смещение 0x00
  • TMR_IRQ_STA_REG — флаги прерываний, смещение 0x04
Причем у этого регистра(TMR_IRQ_STA_REG) особенность: запись нуля не делает ничего, запись 1 в бит сбрасывает его! С чтением все как обычно. Бит устанавливается аппаратно при срабатывании прерывания. Писать в эти регистры надо как и в GPIO. Сначала получаем указатель на область памяти, а потом пишем.
Адрес страницы памяти таймеров: 0x01C20C00
Более наглядно содержимое регистров здесь

Все наши таймеры работают по одному принципу: при старте, если установлен бит TMR_RELOAD в регистр TMRn_CUR_VALUE_REG загружается значение TMRn_INTV_VALUE_REG(если не установлен — счет продолжается от имеющегося значения), затем TMRn_CUR_VALUE_REG декрементируется до нуля. Далее останавливается или перезагружается TMRn_INTV_VALUE_REG, если TMR_MODE=0.
Из настроек(Для таймеров 4,5) доступно:
бит 7: TMR_MODE
0 — режим автоматической перезагрузки
1 — однократное срабатывание

биты 6-4: TMR_CLK_PRES входной делитель от 1 до 128 (2^n), n — число в этих трех битах.
биты 3-2: TMR_CLK_SRC выбор тактового сигнала:
00 — LOSC = 32.768 КГц
01 — 24MHz
10 — Внешний вход тактового сигнала CLKIN0 для Таймера 4, CLKIN1 для Таймера 5
11 — не используется

бит 1: TMR_RELOAD флаг, указывающий перезагружать ли (если 1) значение таймера при старте или возобновлении отсчета(записи 1 в бит 0)
бит 0: TMR_EN запись 1 — запуск таймера, 0 — остановка/пауза.

Для таймера 3:
бит 4: TMR_MODE однократное и периодическое срабатывание (как бит 7 у Таймера 4)
бит 3-2: TMR_CLK_PRES делитель входного сигнала LOSC=32768 Гц
00 — /16
01 — /32
10 — /64
11 — не используется

бит 1: не используется
бит 0: TMR_EN разрешение работы таймера.
Тоесть таймер3 хорошо использовать для нечастого срабатывания, а замерять что-либо им вообще нельзя. А таймеры 4 и 5 — нормальные полноценные таймеры.

Регистры настройки прерываний выглядят просто: номер бита соответствует номеру таймера. Т.е. записывая 0x10 в TMR_IRQ_EN_REG мы разрешим срабатывание прерываний от таймера 4, а записывая те же 0х10 в TMR_IRQ_STA_REG сбросим флаг его прерывания.

Generic Interrupt Controller

Касаемо источников прерываний — все отлично, их у нас 118, любой x86 проц обзавидуется. На каждый модуль оборудования есть свое прерывание: на каждый таймер, на каждый UART и т.д. Полная таблица — в даташите на странице 147.
По таблице находим номер прерывания которые собрались использовать: Timer 4 — IRQ = 99

Установка обработчика прерывания

Более подробно можно почитать:
перевод LDD3 — видимо несколько устарел, т.к. там описаны флаги начинающиеся с SA_*, а актуальны IRQF_*
Цилюрик О.И. Модули ядра Linux
Система прерываний в Linux тоже хорошо развита и оптимизирована на максимальную скорость реакции. Возможность разделения прерывания на 2 части, микрозадачи и очереди задач мы пока оставим в стороне(и применим в примере с моножеством сервомашинок, вот там им будет место), т.к. они применяются для длительных действий, которые можно несколько отложить во времени. Мы же в прерывании будем лишь переключать бит в выходном регистре. Нам понадобятся 2 функции:
int request_irq(unsigned int irq, //номер прерывания
                irqreturn_t (*handler)(int, void *, struct pt_regs *),  //функция - обработчик прерывания
                unsigned long flags,  //флаги указывающие особенности работы прерывания
                const char *dev_name,  //имя нашего обработчика или устройства
                void *dev_id);  //обычно используется для передачи указателя на структуру данных устройства в dev_id
 
void free_irq(unsigned int irq, void *dev_id); //тут аргументы аналогичны

После каждого удачного получения прерывания оно должно быть освобождено, иначе больше никто не сможет получить его.
Об особенностях обработчика прерываний хорошо сказано здесь:
Единственной особенностью является то, что обработчик работает во время прерывания и, следовательно, испытывает некоторые ограничения в том, что может делать. Эти ограничения являются такими же, какие мы видели для таймеров ядра. Обработчик не может передавать данные в или из пространства пользователя, так как не выполняется в контексте процесса. Обработчики также не могут делать ничего, что могло бы заснуть, например, вызвать wait_event, выделять память ни с чем другим, кроме GFP_ATOMIC, или блокировать семафор. Наконец, обработчик не может вызвать schedule.
Так как у нас линий прерываний много и они преимущественно выделенные(1 линия — 1 устройство), то проверка источника для нас не актуальна. + Нам необходимо вручную сбросить флаг прерывания. Поэтому для таймера номер n обработчик будет выглядеть как-то так:
static irqreturn_t intr_handler ( int irq, void *dev ) {
    iowrite32(1<<n,TMR_IRQ_STA_REG); //сброс аппаратного флага. 
    //если не сбросить вручную - система вешается, что вполне логично
    /* полезный код */
    return IRQ_HANDLED;
}      

Все использующиеся и использовавшиеся прерывания системы вместе с именами модулей можно посмотреть командой
cat /proc/interrupts
           CPU0       CPU1
 29:      32007      10210       GIC  arch_timer
 30:          0          0       GIC  arch_timer
 32:          0          0       GIC  axp_mfd
 33:        283          0       GIC  serial
 37:          1          0       GIC  RemoteIR
 39:      44276          0       GIC  sunxi-i2c.0
 40:          0          0       GIC  sunxi-i2c.1
 41:          0          0       GIC  sunxi-i2c.2
 54:          0          0       GIC  timer0
 55:         11          0       GIC  aw_clock_event
 56:          0          0       GIC  sunxi-rtc alarm
 59:        518          0       GIC  dma_irq
 60:          0          0       GIC  sunxi-gpio
 64:      77051          0       GIC  sunxi-mmc
 69:          0          0       GIC  nand
 70:          0          0       GIC  sw_usb_udc
 71:          0          0       GIC  ehci_hcd:usb1
 72:          0          0       GIC  ehci_hcd:usb3
 78:          0          0       GIC  g2d
 87:      47472          0       GIC  eth0
 88:          0          0       GIC  sw_ahci
 92:          0          0       GIC  ace_dev
 96:          0          0       GIC  ohci_hcd:usb2
 97:          0          0       GIC  ohci_hcd:usb4
 99:     569597          0       GIC  Timer4 interrupt
120:          0          0       GIC  sunxi-i2c.3
121:          0          0       GIC  sunxi-i2c.4
IPI0:          0          0  Timer broadcast interrupts
IPI1:       1950      20450  Rescheduling interrupts
IPI2:          0          0  Function call interrupts
IPI3:         11          4  Single function call interrupts
IPI4:          0          0  CPU stop interrupts
IPI5:          0          0  CPU backtrace

Отсюда, кстати, видно что Cubieboard обрабатывает прерывания на одном ядре.

Заголовочные файлы

Как и ожидалось, в исходниках ядра уже определены константы адресов страниц настроек различных модулей. Находятся они в файле platform.h, который включается в interrupt.h по вот такой цепочке:
from arch/arm/mach-sun7i/include/mach/platform.h:4:0,
from arch/arm/plat-sunxi/include/plat/irqs.h:31,
from arch/arm/mach-sun7i/include/mach/irqs.h:25,
from /usr/src/linux-3.4.90-4-ARCH/arch/arm/include/asm/irq.h:7,
from /usr/src/linux-3.4.90-4-ARCH/arch/arm/include/asm/hardirq.h:6,
from include/linux/hardirq.h:7,
from include/linux/interrupt.h:12,
Просмотреть его можно командой:
nano /usr/src/linux-3.4.90-4-ARCH/arch/arm/plat-sunxi/include/plat/platform.h

И оттуда берем и просто используем имена констант, благо они говорящие.

Собираем все вместе и пишем модуль

Для этого нам надо решить задачи:
  • Реализовать обработчик прерывания
  • Реализовать интерфейс управления
  • Зарегистрировать прерывание
  • Настроить порт GPIO
  • Настроить таймер
  • Инициализировать состояние
  • Запустить таймер
Будем использовать таймер4, тактируя его от 24МГц. Так как таймер будет перезапускаться после каждого прерывания то лучше подойдет одиночный режим срабатывания, чтобы не останавливать его вручную. Самый длительный импульс, который необходим серве — пауза 10 мс. При 24МГц за 10мс таймер досчитает до 240 000, что вполне вписывается в 32 разряда, значит делитель нам не нужен. Тогда минимальное значение: 0.5 мс => 12000, а максимальное 2.5 мс => 60000. Т.е. имеем 48000 дискрет регулирования. Жаль, что никакая серва там такой точности не обеспечит, поэтому пусть управляющий сигнал будет от 0 до 1000. Тогда начальное значение таймера = 12000+48*VALUE. При этом стоит следить, чтобы не выйти за границы. В принципе ничего страшного, но если вдруг получим значение периода -1 то период таймера станет почти 180 секунд…
В прерывании будем устанавливать начальное значение таймера: из переменной паузы либо из переменной импульса и соответственно устанавливать 0 или 1 на выход PD0. После запускаем таймер дальше. Счет в обратном порядке нам тут не очень удобен, но имеем то что имеем.
Сегодня интерфейс sysfs подходит нам наилучшим образом: мы передаем настройки в модуль ядра. Подробности о его реализации в прошлом посте.
В функции записи будем обновлять переменные, которые загружаются в таймер для генерации паузы и импульса. Обновлять их будем не потокобезопастно, т.к. если вдруг 1 такт будет неверная пауза — это не смертельно и не критично. Т.к. входное значение — строка, то ее сначала необходимо преобразовать в int. Но сначала необходимо проверить ее длину(мало-ли что там нам написали) и если все хорошо, то скопировать в модуль из пользовательского пространства. Для преобразование str->int в ядре есть специальная функция kstrtoint. Важно, что строка должна быть нуль-терминированная, а что там нам записали — неизвестно, поэтому добавим в конец перевод строки.
В качестве основы опять же воспользуемся примером из Kexamples.BOOK/IRQ/lab1_interrupt.c добавив новый код:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>  //заголовочный файл работы с прерываниями
#include <asm/io.h>
#include <linux/delay.h> //функции задержек
#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/pci.h>

#define IRQ 99 //tmr4 interrupt
static int irq = IRQ, my_dev_id;

static void* __gpio_map;
static void* __tmr_base;
static void* __PD;

static bool __pulse;  //флаг импульс или пауза

static int __servo_pause=216000; //переменная начального значения таймера для паузы
static int __servo_pulse=24000;  // тоже для фронта

static char __buf_msg[10] = "250\n";

//этой функцией можно будет прочитать текущее значение
static ssize_t servo_show( struct class *class, struct class_attribute *attr, char *buf ) {
    strcpy( buf, __buf_msg );
    return strlen( buf );
}

static ssize_t servo_store( struct class *class, struct class_attribute *attr, const char *buf, size_t count ) {
    int input;
    if (count<=8){
      strncpy( __buf_msg, buf, count ); //получили строку из пространства пользователя в модуль
      __buf_msg[count] = '\0'; //добавим символ перевода строки. необходим для kstrtoint
      if (!kstrtoint(__buf_msg,10,&input)){ //преобразуем в число. Если 0 - преобразовалось
        input=input*48; //коэффициент
        if ((input>=0)&&(input<=48000)){ //проверяем диапазон
           __servo_pulse=input+12000; //обновляем значение импульса
           __servo_pause=240000-__servo_pulse; //для сохранения частоты обновим и паузу
        }
      } else {
        printk( "Servo value not updated: not number\n");
      }
   } else {
        printk( "Servo value not updated: too big\n");
    }
   return count;
}

//указатель на структуру в /sys/class
static struct class *servo_class;
CLASS_ATTR( servo, 0666, &servo_show, &servo_store);

// обработчик прерывания
static irqreturn_t my_interrupt( int irq, void *dev_id ) {
    iowrite32(0x1<<4,__tmr_base+4);  //сброс флага прерывания
    if (__pulse){
        iowrite32(__servo_pulse,__tmr_base+0x54);  //начинаем импульс
        iowrite32(ioread32(__PD)|0x1,__PD);
    } else {
        iowrite32(__servo_pause,__tmr_base+0x54);  //начинаем паузу
        iowrite32(ioread32(__PD)&(~0x1),__PD);     //завершаем импульс
    }
    __pulse=!__pulse;  //переключаем флаг
    iowrite32(0x87,__tmr_base+0x50); //24Mhz, одиночное срабатывание, перезагрузить значение, старт
    return IRQ_HANDLED; //отмечаем прерывание как обработанное
}

static int __init my_init( void ) {
    //регистрируем обработчик
    if( request_irq( irq, my_interrupt, IRQF_TIMER, "Timer4 interrupt", &my_dev_id ) )
        return -1;
    printk( "Successfully loading ISR handler on IRQ %d\n", irq );

    servo_class = class_create( THIS_MODULE, "servos" );  //создаем интерфейс как и в прошлом примере
    if( IS_ERR( servo_class ) ) printk( "bad class create\n" );
    int res = class_create_file( servo_class, &class_attr_servo );

    __tmr_base = ioremap(SW_PA_TIMERC_IO_BASE, 4096);   //
    __gpio_map=ioremap(SW_PA_PORTC_IO_BASE,4096);
    __PD = ioremap(SW_PA_PORTC_IO_BASE+0x7C, 4);

    iowrite32(0x1<<4,__tmr_base+4); //на всякий случай сбрасываем флаг прерывания таймера 4

    int irq_en = ioread32(__tmr_base+0);
    irq_en=irq_en|(1<<4); //разрешаем прерывание от таймера 4
    iowrite32(irq_en,__tmr_base+0);

    iowrite32(0x016E3600,__tmr_base+0x54); //устанавливаем начальное значение 24000000 (1 секунда задержки до перового срабатывания)
    iowrite32(0,__tmr_base+0x58);  //очищаем значение таймера
    iowrite32(0x87,__tmr_base+0x50); //запускаем таймер: 24МГц, без делителя, одиночное срабатывание, загрузка начального значения

    __pulse=false; //начнем с паузы

    int DD=ioread32(__gpio_map+0x6C); 
    DD=DD|0x1; //настраиваем PD0
    iowrite32(DD,__gpio_map+0x6c);

    printk("Servo module loaded\n");
    return 0;
}

static void __exit my_exit( void ) {
    synchronize_irq( irq );  //эта функция ожидает завершения обработчика, если он выполнялся.

    int irq_en = ioread32(__tmr_base+0);
    irq_en=irq_en&(~(1<<4)); //tmr4 запрещаем прерывания от таймера
    iowrite32(irq_en,__tmr_base+0);

    iowrite32(0x1<<4,__tmr_base+4);  //сбрасываем флаг
    free_irq( irq, &my_dev_id );     //освобождаем линию
    printk( "Servo module unloaded\n");
    iowrite32(ioread32(__PD)&(~0x1),__PD); //записываем нолик на выход, вдруг модуль выгружается когда __pulse==true
    iounmap(__gpio_map);
    iounmap(__PD);
    iounmap(__tmr_base);
    class_remove_file( servo_class, &class_attr_servo );
    class_destroy( servo_class );
}

module_init( my_init );
module_exit( my_exit );

MODULE_LICENSE( "GPL v2" );


компилируем и загружаем модуль. Смотрим осцилом:

echo 1000 > /sys/class/servos/servo


echo 0 > /sys/class/servos/servo


Похоже на правду. Просмотр вывода команды top говорит что все хорошо и заметной нагрузки на систему наш модуль не оказывает.
Напишем маленький bash-скрипт, которы будет рулить нашей сервой, водя ее из одного крайнего положения в другое.

#/usr/bin/bash

echo "Start servo test"
#инициализируем положение сервы
echo 1000 >  /sys/class/servos/servo
sleep 1
#и крутим туда-сюда пока не надоест
while [ 1 ]; do
    i=1000
    while [ $i -gt 0 ]; do
        echo $i >  /sys/class/servos/servo
    let i=i-5
    sleep 0.01
    done

    while [ $i -lt 1000 ]; do
        echo $i >  /sys/class/servos/servo
    let i=i+5
    sleep 0.01
    done
done

В итоге можем рулить сервой через удобный интерфейс из чего угодно: от с++ или питона и до bash-скриптов.
А вот и кино:

Серва питается от +5В из Cubieboard
  • +12
  • 08 августа 2014, 23:42
  • kest

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

RSS свернуть / развернуть
В закладки!
А не замеряли ли вы максимальную частоту, с которой кубик может обрабатывать прерывания?
0
частоту не мерял, но для GPIO предел, который я смог достичь — скорость записи(не RMW, а чисто запись ассемблером) порядка 30 МГц
0
Сейчас написал модудь, который по прерыванию от GPIO дергал выводом. Измерил время реакции, оказалось 9-30 мкС.
А про DMA статья будет? )
0
Компилятор при использовании iowrite вставляет барьеры синхронизации, которые заставляют нас ждать пока все операции завершатся и данные будут сохранены. если их убрать — скорость работы с gpio возрастает(у меня получилось ~в 2 раза). При этом эта пауза сильно от нагрузки зависит.

До DMA пока далеко, я про это не смотрел даже еще ничего + на мк только один раз использовал. А для чего вам DMA надо? :)
0
DMA нужен для быстрого приема данных с SPI. Интересно, можно ли заблокировать все прерывания в 1 ядре и с помощью GPIO длительное время принимать данные?
0
Думаю лучше попробовать повесить свое прерывание на второе ядро (я кажется видел функции для этого, но прям так уже не вспомню).
dmilvdv.narod.ru/Translate/LDD3/ldd_implementing_handler.html
0
не дописал… еще тут гляньте:
rus-linux.net/MyLDP/BOOKS/Moduli-yadra-Linux/06/kern-mod-06-26.html
0
А вот вопрос не совсем по данной теме… Но как к человеку влезшему в Линукс по самые уши… :-)
В каком состоянии будет находится вывод GPIO после исполнения команды shutdoewn -P (ну или halt)
0
  • avatar
  • kos
  • 12 августа 2014, 12:29
В имеющемся арче halt и shutdown не одно и тоже:)
shutdown -P завершает работу и выключает питание, все светодиоды на плате тухнут и все отключается, т.е. никакого там уровня нет, и даже говорить что там HI-Z наверное не совсем корректно.
halt завершает работу системы, но не выключает ее питание (у меня 3 раза подряд получилось, что вывод в высоком состоянии остался ). Это видно и потому что красный светодиод продолжает гореть.
0
Спасибо! У меня правда несколько иной Линукс. На другой плате. И я понимаю что вопрос был не совсем корректен но все ж надеюсь что работает схоже. :-)
0
А еще вопрос… Откуда у Вас взято это самое волшебное 4096 в функции ioremap? :-)
0
  • avatar
  • kos
  • 13 августа 2014, 10:23
это размер страницы памяти. указан в дш на проц в главе где перечислены страницы с адресами.
0
Пробовал ставить меньше, работает точно также. Правда я не выходил за границу.
0
ЕМНИП, правильным подходом считается использовать функцию request_mem_region перед тем как мапаить память, если мы хотим чтобы другие драйвера туда не лезли. Эта функция проверяет что данный блок памяти «свободен» и, если он свободен, закрепляет его за нашим модулем. Иначе другой модуль может тоже замапить себе «наши» регистры и пытаться использовать «наш» таймер.
0
  • avatar
  • e_mc2
  • 13 августа 2014, 19:05
есть просьба по статейкам дальше — установка графики на кубик. для Mali
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.