Если вы ищете средства для работы с GPIO микрокомпьютера Repka Pi в виде библиотеки на Си, обратите внимание на библиотеку WiringRP. В этой статье мы расскажем, как создавать с ее помощью однопоточные и многопоточные программы, управляющие светодиодами, сервоприводами и реле, а также обрабатывающие прерывание от кнопки, подключенной к контакту GPIO.

Вы можете использовать WiringRP при создании программ для обучения и управления различными устройствами и системами. Это могут быть промышленные и робототехнические установки, устройства IoT, а также устройства умного дома.

Автор выражает благодарность создателю библиотеки WiringRP Сергею Шалапову за помощь в подготовке этой статьи.

Возможности WiringRP

Установка WiringRP

Загрузка библиотеки WiringRP

Мигаем светодиодом

Запускаем multiBlink

Добавляем управление кнопкой

Запускаем потоки при нажатии кнопки

Управляем реле и сервоприводами по кнопке

Полезные ссылки

Итоги

Возможности WiringRP

Библиотека WiringRP может многое. Это управление состоянием и режимов контактов GPIO, а также напряжением на них, подключение резисторов подтяжки «вниз» и «вверх», установленными в микрокомпьютере Repka Pi, обработка прерываний, программная генерация сигнала широтно-импульсной модуляции ШИМ (Pulse Width Modulation, PWM).

С помощью WiringRP можно управлять аппаратным контроллером ШИМ, встроенным в SoC Allwinner H5, а также управлять устройствами, подключенными к интерфейсам UART, I2C и SPI, выведенным на разъем GPIO.

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

Вместе с библиотекой WiringRP поставляется много примеров программ, которые вы можете использовать при создании ПО для ваших устройств. Также доступны исходные коды и самой этой библиотеки.

Установка WiringRP

Мы будем устанавливать WiringRP в операционную систему (ОС) Repka OS RT (Real Time). Для того чтобы установить WiringRP на Repka Pi, нужно загрузить прошивку ОС Repka OS RT, а затем обновить ядро ОС.

Подготовка прошивки Repka OS RT

Скачайте образ этой ОС на сайте проекта в разделе Дистрибутив Repka OS RT (Real Time) - ОС для работы в системах реального времени. Выберите здесь версию прошивки для вашей версии платы по ссылкам: 1.3 и младше или 1.4 и выше.

Запишите полученный архив на карту памяти, например, с помощью программы balenaEtcher, и вставьте карту в Repka Pi.

Далее загрузите Repka OS RT и выполните обновление:

# apt update
# apt upgrade

Установите необходимые пакеты:

# apt-get install git gdb cmake

Обновление ядра ОС

На следующем этапе необходимо обновить ядро ОС.

Прежде всего, обновите версию утилиты repka-config. Для этого загрузите пакетный файл обновления repka-config-update.sh:

# wget https://download.repka-pi.ru/repka-tools/repka-config/repka-config-update.sh

Полученный файл нужно скопировать в каталог /boot под именем repka-config.sh, а затем разрешить выполнение файла repka-config.sh:

# cp repka-config-update.sh /boot/repka-config.sh
# chmod +x /boot/repka-config.sh

Затем запустите файл:

# cd /boot 
# ./repka-config.sh

Появится окно, в котором показана текущая максимальная частота и объем оперативной памяти (рис. 1).

Рис. 1. Текущая максимальная частота и объем оперативной памяти.
Рис. 1. Текущая максимальная частота и объем оперативной памяти.

Нажмите клавишу Enter и в новом окне выберите строку System Options для настройки системы (рис. 2).

Рис. 2. Выбор System Options для настройки системы.
Рис. 2. Выбор System Options для настройки системы.

Далее используйте строку S1 Select-kernel для выбора ядра Linux (рис. 3).

Рис. 3. Строка S1 Select-kernel для выбора ядра Linux.
Рис. 3. Строка S1 Select-kernel для выбора ядра Linux.

Вам тут нужно выбрать ядро Kernel-3 6.1.11-rt7-sunxi (с патчем PREEMPT_RT), как это показано на рис. 4.

Рис. 4. Выбор ядра Kernel-3 6.1.11-rt7-sunxi (с патчем PREEMPT_RT).
Рис. 4. Выбор ядра Kernel-3 6.1.11-rt7-sunxi (с патчем PREEMPT_RT).

Дождитесь завершения установки пакетов, а потом перезагрузите ОС.

После перезагрузки проверьте версию ядра:

# uname -a
Linux Repka-Pi 6.1.11-rt7 #2 SMP PREEMPT_RT Wed Jan 17 17:29:18 +04 2024 aarch64 aarch64 aarch64 GNU/Linux  
# uname -r
6.1.11-rt7

Если вы все сделали правильно, то у вас будет ядро 6.1.11-rt7.

Загрузка библиотеки WiringRP

Для загрузки библиотеки WiringRP создайте каталог проектов и выполните клонирование:

# mkdir myprojects
# cd myprojects
# git clone https://gitflic.ru/project/repka_pi/wiring-repka.git

Далее перейдите в каталог wiring-repka:

# cd wiring-repka

Сделайте текущим каталог utility и перейдите в каталог нужного ядра ОС:

# cd utility 
# cd repka-pi_1.2-1.6_kernel_6.1.11-rt

Сделайте исполнимым файл init_dev.sh из этого каталога и запустите его:

# chmod +x init_dev.sh
# ./init_dev.sh

На консоли появятся такие сообщения:

Загрузка драйвера repka_gpio.ko
Настройка доступа к устройствам...
chmod: невозможно получить доступ к '/dev/i2c-2': Нет такого файла или каталога
chmod: невозможно получить доступ к '/dev/spidev0.0': Нет такого файла или каталога
chmod: невозможно получить доступ к '/dev/spidev1.0': Нет такого файла или каталога
Настройка выполнена.

Сообщения о невозможности получения доступа к устройствам появляются в том случае, если в текущей версии распиновки эти устройства не поддерживаются. В этом случае данные устройства отсутствуют в дереве устройств ОС, и для них не создаются символьные устройства в каталоге /dev.

Если установить, например, четвертый или пятый вариант распиновки, подобные сообщения появляться не будут:

root@Repka-Pi:~/wiring-repka/utility/repka-pi_1.2-1.6_kernel_6.1.11-rt# ./init_dev.sh
Загрузка драйвера repka_gpio.ko
Настройка доступа к устройствам...
Настройка выполнена.

Для смены распиновки запустите программу repka-config. Выберите там строку Interface Options,  а затем Frequency/Pinout Options. Далее нужно выбрать нужную частоту и версию вашей платы, а потом — нужный вариант распиновки (рис. 5).

Рис. 5. Выбор нужного варианта распиновки.
Рис. 5. Выбор нужного варианта распиновки.

Обратите внимание, что для управления аппаратным контроллером ШИМ, встроенным в SoC Allwinner H5, нужен пятый вариант распиновки.

Теперь перейдите в каталог /root/myprojects/wiring-repka, создайте в нем каталог для сборки проекта и сделайте его текущим:

# mkdir build && cd build

Если вы собираете драйвер из исходных текстов, убедитесь, что в файле CmakeLists.txt снят символ комментария # со строки `#add_subdirectory(driver). В большинстве случаев, однако, можно воспользоваться уже собранным драйвером из каталога wiring-repka/utility.

Выполните сборку и компиляцию проекта:

# cmake .. && make

Запустите из каталога /root/myprojects/wiring-repka/build/bin программу blink:

# ./blink

Если все было сделано правильно, светодиод начнет мигать, а на консоли появятся такие сообщения:

Инициализация WiringRP...
Модель устройства: Repka-Pi3-H5
Частота ЦП: 1.368 GHz
SN: 82c00001e6cad661
Вариант распиновки: 1
Инициализирован режим нумерации контактов: SUNXI
Инициализация драйвера repka_mmap...
Контроллеры GPIO инициализированы.

Программа blink выведет на консоль модель устройства, частоту процессора, серийный номер процессора, уникальный для каждого устройства, текущий вариант распиновки, а также режим нумерации контактов.

Кроме режима SUNXI доступны варианты PHYSIC (физические номера контактов 1–40) и WIRING (соответствуют надписям к контактам в Raspberry Pi и шилдах).

К сожалению, пока новый драйвер не встроен в ядро Repka OS, и после перезагрузки нужно будет вновь запускать скрипт init_dev.sh.

Если этого не сделать, то при запуске, например, программы blink доступ к GPIO будет только через драйвер символьного устройства /dev/mem:

# ./blink
Инициализация WiringRP...
Модель устройства: Repka-Pi3-H5
Частота ЦП: 1.368 GHz
SN: 82c00001e6cad661
Вариант распиновки: 1
Инициализирован режим нумерации контактов: SUNXI
Инициализация драйвера repka_mmap...
Ошибка чтения файла /dev/repka_gpio0
Задействован резервный драйвер /dev/mem
Ошибка чтения файла /dev/repka_gpio1
Задействован резервный драйвер /dev/mem
Контроллеры GPIO инициализированы.

Со временем, когда драйвер будет включен в состав ядра, процедура инициализации скриптом init_dev.sh не потребуется, хотя такую возможность планируется оставить.

Для загрузки драйвера после перезагрузки ОС перейдите в каталог /root/myprojects/wiring-repka/utility/repka-pi_1.2-1.6_kernel_6.1.11-rt и запустите там скрипт:

# ./init_dev.sh

Теперь снова запустите программу blink:

# /root/myprojects/wiring-repka/build/bin/blink
Инициализация WiringRP...
Модель устройства: Repka-Pi3-H5
Частота ЦП: 1.368 GHz
SN: 82c00001e6cad661
Вариант распиновки: 1
Инициализирован режим нумерации контактов: SUNXI
Инициализация драйвера repka_mmap...
Контроллеры GPIO инициализированы.

Как видите, все работает правильно.

Оригинальная инструкция по установке WiringRP от автора этого проекта доступна на форуме Repka-Pi.

Мигаем светодиодом

Начнем с самой простой программы blink.c, мигающей светодиодом (листинг 1). Эта программа входит в состав проекта библиотеки WiringRP и находится в каталоге примеров examples.

Листинг 1. https://gitflic.ru/project/repka_pi/wiring-repka/blob/raw?file=examples/blink.c&commit=8ae264b69c0eb8185a2c8431b4b5b0c3b374fc8a

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include "wiringRP.h"

// Глобальные переменные и константы
const int LED_1 = 6;

void setup() {
    // Инициализация библиотек wiringRP
    if(setupWiringRP(WRP_MODE_SUNXI) < 0)
        exit(EXIT_FAILURE);

    // Инициализация пользовательских объектов
    pinMode(LED_1, OUTPUT);
}

void loop() {
    // Основной цикл программы
    digitalWrite(LED_1, HIGH);
    delay(1000);
    digitalWrite(LED_1, LOW);
    delay(500);
}

ONDESTROY(){
    // Освобождение занятых ресурсов, выключение напряжения на пинах
    digitalWrite(LED_1, LOW);
    pinMode(LED_1, DISABLE);

    // Завершение работы библиотек
    releaseWiringRP();

    exit(0);    // выход из программы
}

MAIN_WIRINGRP();

Исходный текст программы начинается с включения стандартных заголовочных файлов stdio.h, stdlib.h и stdbool.h. Первый из них включает определение функций для ввода и вывода, второй — для управления памятью и для других стандартных функций, а третий — для представления логических значений true и false.

Далее добавлен заголовочный файл wiringRP.h, необходимый для работы с библиотекой WiringRP.

В программе определена глобальная константа LED_1, которая задает номер контакта GPIO Repka Pi для подключения светодиода в режиме SUNXI:

const int LED_1 = 6;

Этот контакт выделен красной рамкой на схеме распиновок (рис. 6).

Рис. 6. Контакт с номером 6 в режиме SUNXI.
Рис. 6. Контакт с номером 6 в режиме SUNXI.

Положительный вывод светодиода подключен через резистор 240 Ом к контакту с физическим номером 12, а отрицательный — к выводу GND (рис. 7). Для подключения к GND (то есть к земле) можно использовать один из контактов с физическими номерами 6, 9, 14, 20 25, 30, 34 и 39.

Рис. 7. Подключение светодиода.
Рис. 7. Подключение светодиода.

Сразу после запуска программы управление получает код, определенный в макросе MAIN_WIRINGRP:

#define MAIN_WIRINGRP() \
    int main(void) { \
    signal(SIGINT, onClose); \
    setup(); \
    for (;;) loop();}

Здесь функция main устанавливает функцию onClose как обработчик сигнала SIGINT. Эта функция освобождает ресурсы при завершении работы программы, например, при нажатии комбинации клавиш Ctrl+C.

Далее вызывается функция setup, выполняющая необходимую инициализацию, а затем программа вызывает в бесконечном цикле функцию loop.

Функция setup инициализирует библиотеку wiringRP с помощью функции setupWiringRP, а также переводит контакт (пин), к которому подключен светодиод, в режим вывода:

void setup() {
    if(setupWiringRP(WRP_MODE_SUNXI) < 0)
        exit(EXIT_FAILURE);
    pinMode(LED_1, OUTPUT);
}

В качестве параметра вы можете передать функции setupWiringRP один из следующих режимов нумерации контактов:

  • WRP_MODE_SUNXI — по номерам SUNXI (PA-PL);

  • WRP_MODE_PHYS — по физическому расположению контактов (1–40);

  • WRP_MODE_WIRING — по номерам wiring (для совместимости с Raspberry Pi 3)

Если инициализация выполнилась успешно, функция setup переводит пин светодиода LED_1 в режим вывода с помощью функции pinMode.

Также возможны и другие режимы. Полный список констант для установки режимов приведен ниже:

  • INPUT — ввод, интегрированный подтягивающий резистор выключен;

  • INPUT_PULLUP — ввод, интегрированный подтягивающий "вверх" резистор включен;

  • INPUT_PULLDOWN — ввод, интегрированный подтягивающий "вниз" резистор включен;

  • OUTPUT — вывод;

  • DISABLE — контакт деактивирован

Мигание светодиодом программа blink.c выполняет в цикле, который бесконечно вызывает функцию loop:

void loop() {
    digitalWrite(LED_1, HIGH);
    delay(1000);
    digitalWrite(LED_1, LOW);
    delay(500);
}

Напомним, что этот цикл определен в макросе MAIN_WIRINGRP.

Для установки высокого и низкого уровня вызывается функция digitalWrite. Чтобы включить светодиод, ей передается логическая константа HIGH, а чтобы выключить — LOW. После включения выполняется задержка 1000 мс, а после выключения — 500 мс.

При завершении работы программы вызывается функция onClose, определенная макросом ONDESTROY:

#define ONDESTROY() void onClose(int sig_num)

Эта функция освобождает занятые ресурсы, отключает напряжение на пине светодиода и переводит этот пин в неактивное состояние:

ONDESTROY(){
    // Освобождение занятых ресурсов, выключение напряжения на пинах
    digitalWrite(LED_1, LOW);
    pinMode(LED_1, DISABLE);
    releaseWiringRP();
    exit(0); // выход из программы
}

Далее вызывается функция releaseWiringRP, освобождающая ресурсы, занятые библиотекой WiringRP. После этого работа программы завершается вызовом функции exit.

Запускаем multiBlink

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

Создаем файлы программы

Создадим для программы каталог:

/root/rover_connect2/wiring-repka-pi/mblink/

Разместите в этом каталоге программу mblink.c (листинг 2), а также файл CMakeLists.txt (листинг 3).

Листинг 2. https://raw.githubusercontent.com/AlexandreFrolov/rover_connect2/main/wiring-repka-pi/mblink/mblink.c

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <pthread.h>
#include "wiringRP.h"

const int LED_1 = 6;
const int LED_2 = 7;

void* led1_blink(void* arg) {
    while (true) {
        digitalWrite(LED_1, HIGH);
        delay(1000);
        digitalWrite(LED_1, LOW);
        delay(500);
    }
    return NULL;
}

void* led2_blink(void* arg) {
    while (true) {
        digitalWrite(LED_2, HIGH);
        delay(300);
        digitalWrite(LED_2, LOW);
        delay(150);
    }
    return NULL;
}

void setup() {
    if (setupWiringRP(WRP_MODE_SUNXI) < 0)
        exit(EXIT_FAILURE);
   
    pinMode(LED_1, OUTPUT);
    pinMode(LED_2, OUTPUT);
    
    pthread_t thread_led1, thread_led2;
    pthread_create(&thread_led1, NULL, led1_blink, NULL);
    pthread_create(&thread_led2, NULL, led2_blink, NULL);
}

void loop() {
    delay(300);
}

ONDESTROY() {
    digitalWrite(LED_1, LOW);
    digitalWrite(LED_2, LOW);
    pinMode(LED_1, DISABLE);
    pinMode(LED_2, DISABLE);
    releaseWiringRP();
    exit(0);
}

MAIN_WIRINGRP();

Листинг 3. https://raw.githubusercontent.com/AlexandreFrolov/rover_connect2/main/wiring-repka-pi/mblink/CMakeLists.txt

cmake_minimum_required(VERSION 3.16.0 FATAL_ERROR)
project("multiBlink")
add_subdirectory(../wiring-repka/wiringRP ${CMAKE_BINARY_DIR}/wrp)
add_executable(multiBlink mblink.c)
target_link_libraries(multiBlink PRIVATE wiringrp)

Собираем проект

Для сборки проекта скопируйте каталог wiring-repka/wiringRP в каталог /root/rover_connect2/wiring-repka-pi.

Далее сделайте текущим каталог /root/rover_connect2/wiring-repka-pi/mblink, создайте там каталог build и соберите проект:

mkdir build
cd build
cmake ..
cmake --build .

Подключаем второй светодиод

Подключите положительный вывод второго светодиода через резистор 240 Ом к пину с физическим номером 7, а отрицательный — к земле, как мы это делали при запуске программы blink.c (рис.  8).

Рис. 8. Подключение второго светодиода.
Рис. 8. Подключение второго светодиода.

Запускаем многопоточную программу

После подключения второго светодиода и сборки программы multiBlink запустите эту программу из каталога build:

./multiBlink

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

Описание программы mblink.c

Для реализации многопоточности в исходный текст программы мы добавили стандартный заголовочный файл pthread.h.

Номера пинов светодиодов определены при помощи констант LED_1 и LED_2:

const int LED_1 = 6;
const int LED_2 = 7;

Для управления двумя светодиодами в разных потоках мы определили функции led1_blink и led2_blink:

void* led1_blink(void* arg) {
    while (true) {
        digitalWrite(LED_1, HIGH);
        delay(1000);
        digitalWrite(LED_1, LOW);
        delay(500);
    }
    return NULL;
}

void* led2_blink(void* arg) {
    while (true) {
        digitalWrite(LED_2, HIGH);
        delay(300);
        digitalWrite(LED_2, LOW);
        delay(150);
    }
    return NULL;
}

Каждая из них мигает в цикле своим светодиодом аналогично тому, как это делает рассмотренная ранее программа blink.c.

Обратите внимание,  что здесь для разных светодиодов мы определили разные времена задержки.

Инициализация библиотеки WiringRP и пинов выполняется, как и раньше, функциями setupWiringRP и pinMode:

void setup() {
    if (setupWiringRP(WRP_MODE_SUNXI) < 0)
        exit(EXIT_FAILURE);
   
    pinMode(LED_1, OUTPUT);
    pinMode(LED_2, OUTPUT);
    
    pthread_t thread_led1, thread_led2;
    pthread_create(&thread_led1, NULL, led1_blink, NULL);
    pthread_create(&thread_led2, NULL, led2_blink, NULL);
}

Здесь в переменных thread_led1 и thread_led2 определяются переменные для хранения идентификаторов потоков.

Потоки создаются функцией pthread_create. Ей нужно передать четыре аргумента:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    void *(*start_routine)(void*), void *arg);
  1. *thread — указатель на переменную типа pthread_t, в которую будет сохранен идентификатор нового потока;

  2. *attr — атрибуты потока. Если аргумент указан как NULL, используются атрибуты по умолчанию;

  3. *(*start_routine)(void*) — указатель на функцию, которая должна работать в рамках создаваемого потока;

  4. *arg — через этот параметр в запускаемую функцию start_routine можно передать аргументы функции start_routine. В нашем случае аргументы не передаются, поэтому значение четвертого аргумента указано как NULL

Так как при использовании макроса MAIN_WIRINGRP программа запускает бесконечный цикл в функции loop, нужно позаботиться, чтобы этот, не нужный в нашем случае цикл не перегружал процессор микрокомпьютера. С этой целью мы вставим в цикл небольшую задержку, на выполнение которой ресурсы процессора не расходуются:

void loop() {
    delay(300);
}

При завершении программы мы устанавливаем нулевое напряжения на выводах, к которым подключены два наших светодиода, и переводим эти выводы в неактивный режим:

ONDESTROY() {
    digitalWrite(LED_1, LOW);
    digitalWrite(LED_2, LOW);
    pinMode(LED_1, DISABLE);
    pinMode(LED_2, DISABLE);
    releaseWiringRP();
    exit(0);
}

Добавляем управление кнопкой

Напишем небольшую программу button.c, управляющую двумя светодиодами с помощью кнопки (листинг 4).

Листинг 4. https://raw.githubusercontent.com/AlexandreFrolov/rover_connect2/main/wiring-repka-pi/mblink/button.c

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include "wiringRP.h"

const int LED_1 = 6;
const int LED_2 = 7;
const int BUTTON_1 = 10;

void setup() {
    if(setupWiringRP(WRP_MODE_SUNXI) < 0)
        exit(EXIT_FAILURE);
     pinMode(LED_1, OUTPUT);
     pinMode(LED_2, OUTPUT);
     pinMode(BUTTON_1, INPUT_PULLUP);
}

void loop() {
	if (digitalRead(BUTTON_1)) {
        delay(100);
        if (digitalRead(BUTTON_1)) {
            digitalWrite(LED_1, HIGH);
			digitalWrite(LED_2, LOW);
        }
    } else {
        digitalWrite(LED_1, LOW);
        digitalWrite(LED_2, HIGH);
    }	
}

ONDESTROY(){
    digitalWrite(LED_1, LOW);
    pinMode(LED_1, DISABLE);
    pinMode(LED_2, DISABLE);
    pinMode(BUTTON_1, DISABLE);
    releaseWiringRP();
    exit(0);
}

MAIN_WIRINGRP();

Для сборки программ воспользуйтесь файлом CMakeLists.txt (листинг 5).

Листинг 5. https://raw.githubusercontent.com/AlexandreFrolov/rover_connect2/main/wiring-repka-pi/mblink/CMakeLists.txt

cmake_minimum_required(VERSION 3.16.0 FATAL_ERROR)
project("multiBlink")
add_subdirectory(../wiring-repka/wiringRP ${CMAKE_BINARY_DIR}/wrp)
add_executable(multiBlink mblink.c)
target_link_libraries(multiBlink PRIVATE wiringrp)
add_executable(button button.c)
target_link_libraries(button PRIVATE wiringrp)

В нашей программе определены константы для двух светодиодов и для кнопки:

const int LED_1 = 6;
const int LED_2 = 7;
const int BUTTON_1 = 10;

На этапе инициализации программа задает режим пинов, к которым подключены светодиоды, для вывода, а пина,  к которому подключена кнопка — для ввода в режиме подтяжки «верх»:

pinMode(LED_1, OUTPUT);
pinMode(LED_2, OUTPUT);
pinMode(BUTTON_1, INPUT_PULLUP);

Чтобы не было проблем при инициализации микроконтроллера, мы соединили кнопку с землей через резистор на 240 Ом, а не напрямую.

Второй вывод кнопки подключили к физическому пину 15. Также мы предусмотрели подтягивающий «наверх» резистор на 10КОм, подключенный к +3.3 В. Это напряжение есть на пине 1 (рис. 9).

Рис. 9. Подключение кнопки и подтягивающего резистора.
Рис. 9. Подключение кнопки и подтягивающего резистора.

Функция loop вызывается в бесконечном цикле. Она опрашивает состояние кнопки с помощью функции digitalRead, и в зависимости от того, нажата кнопка или отпущена, включает один из светодиодов:

void loop() {
	if (digitalRead(BUTTON_1)) {
        delay(100);
        if (digitalRead(BUTTON_1)) {
            digitalWrite(LED_1, HIGH);
			digitalWrite(LED_2, LOW);
        }
    } else {
        digitalWrite(LED_1, LOW);
        digitalWrite(LED_2, HIGH);
    }	
}

При завершении работы программы освобождаются ресурсы, связанные с пинами светодиодов и кнопки, а также библиотеки WiringRP:

ONDESTROY(){
    digitalWrite(LED_1, LOW);
    pinMode(LED_1, DISABLE);
    pinMode(LED_2, DISABLE);
    pinMode(BUTTON_1, DISABLE);
    releaseWiringRP();
    exit(0);
}

Запускаем потоки при нажатии кнопки

Библиотека WiringRP позволяет обрабатывать прерывания, возникающие при изменении уровня напряжения на входных контактах.

В листинге 6 мы привели пример программы mbutton.c, в которой прерывание от кнопки, подключенной к контакту 10, используется для запуска двух потоков управления светодиодами.

Листинг 6. https://raw.githubusercontent.com/AlexandreFrolov/rover_connect2/main/wiring-repka-pi/mblink/mbutton.c

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <pthread.h>
#include "wiringRP.h"
#include "event_gpio.h"

const int LED_1 = 6;
const int LED_2 = 7;
const int BUTTON_1 = 10;
bool LED_1_ON = false;
bool LED_2_ON = false;
pthread_t thread_led1, thread_led2;

void pushButton1(__u32 event, __u64 time);

void* led1_blink(void* arg) {
    for (int i = 0; i < 3; ++i) {
        digitalWrite(LED_1, HIGH);
        delay(1000);
        digitalWrite(LED_1, LOW);
        delay(500);
    }
    pthread_exit(NULL);
}

void* led2_blink(void* arg) {
    for (int i = 0; i < 5; ++i) {
        digitalWrite(LED_2, HIGH);
        delay(300);
        digitalWrite(LED_2, LOW);
        delay(150);
    }
    pthread_exit(NULL);
}

void pushButton1(__u32 event, __u64 time) {
    printf("Button 1 - is %s, timestamp: %lld\n", event == 1 ? "RISING" : "FALLING", time);
    delay(100);
	
    if(event && digitalRead(BUTTON_1)) {		
      pthread_create(&thread_led1, NULL, led1_blink, NULL);
	  pthread_create(&thread_led2, NULL, led2_blink, NULL);
	}
}

void setup() {
    if (setupWiringRP(WRP_MODE_SUNXI) < 0)
        exit(EXIT_FAILURE);
    pinMode(LED_1, OUTPUT);
    pinMode(LED_2, OUTPUT);
    pinMode(BUTTON_1, INPUT_PULLUP);
    interruptCreate(BUTTON_1, pushButton1);
}

void loop() {
	delay(50);
}

ONDESTROY() {
    digitalWrite(LED_1, LOW);
    digitalWrite(LED_2, LOW);
    pinMode(LED_1, DISABLE);
    pinMode(LED_2, DISABLE);
    pinMode(BUTTON_1, DISABLE);
    interruptStopAll();
    releaseWiringRP();
    exit(0);
}

MAIN_WIRINGRP();

Сразу после запуска функция setup инициализирует библиотеку WiringRP, переключает контакты светодиодов в режим OUTPUT, а контакт кнопки — в режим INPUT_PULLUP:

if (setupWiringRP(WRP_MODE_SUNXI) < 0)
    exit(EXIT_FAILURE);
pinMode(LED_1, OUTPUT);
pinMode(LED_2, OUTPUT);
pinMode(BUTTON_1, INPUT_PULLUP);

Далее при помощи функции interruptCreate для событий от кнопки BUTTON_1 назначается функция обработки pushButton1:

interruptCreate(BUTTON_1, pushButton1);

Эта функция выводит на консоль событие и штамп времени, после чего выполняет задержку на 100 мс и запускает два процесса, мигающих светодиодами:

void pushButton1(__u32 event, __u64 time) {
    printf("Button 1 - is %s, timestamp: %lld\n", event == 1 ? "RISING" : "FALLING", time);
    delay(100);
    if(event && digitalRead(BUTTON_1)) {		
       pthread_create(&thread_led1, NULL, led1_blink, NULL);
       pthread_create(&thread_led2, NULL, led2_blink, NULL);
    }
}

Процесс led1_blink включает светодиод LED_1 три раза, после чего завершает свою работу функций pthread_exit:

void* led1_blink(void* arg) {
    for (int i = 0; i < 3; ++i) {
        digitalWrite(LED_1, HIGH);
        delay(1000);
        digitalWrite(LED_1, LOW);
        delay(500);
    }
    pthread_exit(NULL);
}

Функция led2_blink второго процесса устроена аналогично, но работает со светодиодом LED_2, мигая пять раз до завершения своей работы.

При завершении работы вызывается функция interruptStopAll, завершая обработку прерываний. Кроме того, освобождаются ресурсы, как и в предыдущих программах.

Для сборки программ используйте файл CMakeLists.txt, представленный в листинге 7.

Листинг 7. https://raw.githubusercontent.com/AlexandreFrolov/rover_connect2/main/wiring-repka-pi/mblink/CMakeLists.txt

cmake_minimum_required(VERSION 3.16.0 FATAL_ERROR)
project("multiBlink")

add_subdirectory(../wiring-repka/wiringRP ${CMAKE_BINARY_DIR}/wrp)

add_executable(multiBlink mblink.c)
target_link_libraries(multiBlink PRIVATE wiringrp)

add_executable(button button.c)
target_link_libraries(button PRIVATE wiringrp)

add_executable(mbutton mbutton.c)
target_link_libraries(mbutton PRIVATE wiringrp)

Перед тем как запускать программу, подключите контакт кнопки 10 двумя резисторами к земле (резистор 240 Ом) и к линии питания +3.3 В (резистор 1 КОм), как это было показано на рис. 9.

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

Управляем реле и сервоприводами по кнопке

В этом разделе статьи мы займемся подключением к Repka Pi четырехканального модуля реле, а также сервоприводов через контроллер Robointellect Controller 001.

Для управления реле и сервоприводов мы будем использовать библиотеку WiringRP, запуская все задачи в отдельны процессах по нажатию кнопки.

Подключение реле

Мы будем использовать четырехканальное реле, показанное на рис. 10 (можно купить, например, на Яндекс.Маркете), для управления светодиодом, подключенным к батарейке на 9 В через резистор 10 Ком.

Рис. 10. Модуль реле на 4 канала.
Рис. 10. Модуль реле на 4 канала.

Схема подключения реле показана на рис. 11.

Рис. 11. Подключение блока реле.
Рис. 11. Подключение блока реле.

Выводы реле DC+ и DC- подключены, соответственно, к +5 В (физический контакт 2 микрокомпьютера Repka Pi) и к земле.

Сигнал управления подается на контакт IN4 реле (здесь мы используем четвертое реле из блока) с физического контакта 13 микрокомпьютера.

Нормально разомкнутые контакты ТС4 и COM4 четвёртого реле подключены к светодиоду и батарейке на 9 В через резистор 10 КОм.

Вы можете ознакомиться с даташитом на реле, установленные в этом блоке.

Подключение контроллера Robointellect Controller 001

С помощью одного контроллера Robointellect Controller 001, созданного на базе чипа pca9685, можно управлять до 16 устройствами ШИМ, такими как сервопривод.

Подключение контроллера к Repka Pi через интерфейс I2C показано на рис. 12.

Рис. 12. Подключение контроллера Robointellect Controller 001 к интерфейсу I2C микрокомпьютера Repka Pi.
Рис. 12. Подключение контроллера Robointellect Controller 001 к интерфейсу I2C микрокомпьютера Repka Pi.

Выводы GND и VCC 5V контроллера Robointellect Controller 001 нужно подключить, соответственно, к земле и контакту +5 В микрокомпьютера Repka Pi.

Выводы SDA и SCL контроллера Robointellect Controller 001 нужно подключить, соответственно, к физическим контактам 3 и 5 микрокомпьютера Repka Pi.

Также в разъем DC IN подключите блок питания, который поставляется вместе с Robointellect Controller 001.

Полностью собранная схема показана на рис. 13.

Рис. 13. Полностью собранная схема.
Рис. 13. Полностью собранная схема.

Программа mtool.c

Для управления светодиодами, реле и сервоприводами мы подготовили многопоточную программу mtool.c (листинг 8). Программа слишком большая, чтобы публиковать ее в статье, но вы можете посмотреть ее полный исходный код по ссылке.

Листинг 8. https://raw.githubusercontent.com/AlexandreFrolov/rover_connect2/main/wiring-repka-pi/mblink/mtool.c

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

Листинг 9. https://raw.githubusercontent.com/AlexandreFrolov/rover_connect2/main/wiring-repka-pi/mblink/CMakeLists.txt

cmake_minimum_required(VERSION 3.16.0 FATAL_ERROR)
project("multiBlink")
add_subdirectory(../wiring-repka/wiringRP ${CMAKE_BINARY_DIR}/wrp)
add_executable(multiBlink mblink.c)
target_link_libraries(multiBlink PRIVATE wiringrp)
add_executable(button button.c)
target_link_libraries(button PRIVATE wiringrp)
add_executable(mbutton mbutton.c)
target_link_libraries(mbutton PRIVATE wiringrp)
add_executable(mtool mtool.c)
target_link_libraries(mtool PRIVATE wiringrp)

Теперь расскажем о том, как работает программа mtool.c.

Инициализация

На этапе инициализации работает функция setup:

void setup() {
    if (setupWiringRP(WRP_MODE_SUNXI) < 0)
        exit(EXIT_FAILURE);
    pinMode(LED_1, OUTPUT);
    pinMode(LED_2, OUTPUT);
    pinMode(BUTTON_1, INPUT_PULLUP);
    interruptCreate(BUTTON_1, pushButton1);
	
    pca9685_fd = pca9685Setup(I2C1_BUS, pca9685Address, HERTZ);
    if (pca9685_fd < 0)
        exit(EXIT_FAILURE);
    pca9685PwmReset(pca9685_fd);
    setNeutralPos();
    delay(1000);	
}

Вначале выполняется инициализация библиотеки WiringRP, установка режимов контактов для светодиодов и кнопки, а также назначается функция pushButton1 для обработки прерываний от кнопки BUTTON_1.

Далее с помощью функции pca9685Setup выполняется инициализация контроллера Robointellect Controller 001. В качестве первого параметра I2C1_BUS этой функции передается шина I2C, адрес контроллера pca9685Address на этой шине, а также частота генерируемых импульсов HERTZ, равная в нашем случае 50 Гц.

Если инициализация контроллера прошла успешно, функция setup сбрасывает контроллер, вызывая для этого функцию pca9685PwmReset и устанавливает сервоприводы в начальную позицию функцией setNeutralPos:

void setNeutralPos() {
    float millis = 1.5f;
    int tick = calcTicks(millis, HERTZ);
    pca9685PwmWrite(pca9685_fd, pin_1, 0, tick);
    pca9685PwmWrite(pca9685_fd, pin_2, 0, tick);
    tick = calcTicks(1.56f, HERTZ);
    pca9685PwmWrite(pca9685_fd, pin_3, 0, tick);
}

Чтобы дождаться завершения установки, после вызова этой функции в функции setup предусмотрена пауза на 1 сек.

Для инициализации сервоприводов с удержанием угла контроллер будет подавать на него импульсы длительностью 1.5 мс.

Вначале функция setNeutralPos с помощью функции calcTicks рассчитывает количество тактов (ticks) должно быть для импульса определенной длительности millis при заданной частоте HERTZ.

Далее вычисленное количество тактов передается функции pca9685PwmWrite через последний параметр.

В качестве первого параметра функции pca9685PwmWrite нужно передать идентификатор контроллера, полученный от функции pca9685Setup. Второй параметр задает номер канала контроллера (от 0 до 15), а третий и четвертый начало и конец импульса в 4096 частях (то есть от начала до количества тактов для импульсов заданной длительности).

Для сервоприводов SG90 и MG90S устанавливается длина импульса 1.5 мс. Что же касается сервопривода непрерывного вращения DS04-NFC, то для перевода его вала в состояние покоя нужно передать функции calcTicks значение 1.6.

К сожалению, при использовании недорого контроллера pca9685 приходится выполнять калибровку, изменяя длительность импульса, передаваемого  функции calcTicks, до достижения нужного результата. Также в сервоприводе DS04-NFC имеется регулятор для калибровки.

Код функции calcTicks представлен ниже:

int calcTicks(float impulseMs, float hertz) {
    float cycleMs = 1000.0f / hertz;
    return (int) ((float)MAX_PWM * impulseMs / cycleMs + 0.5f);
}

Вначале вычисляется cycleMs — длительность одного цикла (в мс) для заданной частоты. Для частоты 50 Гц длительность цикла составит 20 мс.

Далее длительность импульса impulseMs, для которого нужно рассчитать количество тактов, умножается на максимальное количество тактов MAX_PWM для контроллера и делится на длительность цикла. В нашем случае максимальное количество тактов составляет 4096, а длительность цикла равна 20 мс.

К полученному значению прибавляется 0.5 для более точного округления. Результат затем округляется до целого числа, чтобы получить количество тактов, и возвращается как целое значение.

Основной цикл программы

После инициализации функцией setup макрос MAIN_WIRINGRP запускает основной цикл программы loop:

void loop() {
  delay(100);
}

В нашем случае внутри этого цикла никакие действия, кроме вызова функции задержки, не выполняются. Задержка с помощью delay не приводит к дополнительной нагрузке на процессор микрокомпьютера.

Обработка прерывания от кнопки

Если нажать на кнопку BUTTON_1, а потом отпустить ее, функция pushButton1 запустит пять процессов при помощи pthread_create:

void pushButton1(__u32 event, __u64 time) {
    printf("Button 1 - is %s, timestamp: %lld\n", event == 1 ? "RISING" : "FALLING", time);
    delay(100);
	
    if(event && digitalRead(BUTTON_1)) {		
       pthread_create(&thread_led1, NULL, led1_blink, NULL);
       pthread_create(&thread_led2, NULL, led2_blink, NULL);
       pthread_create(&thread_servo1, NULL, thread_servo1_rotate, NULL);
       pthread_create(&thread_servo2, NULL, thread_servo2_rotate, NULL);
       pthread_create(&thread_servo3, NULL, thread_servo3_rotate, NULL);
	}
}

Процессы thread_led1 и thread_led2 мы уже рассматривали ранее — они вызывают мигание светодиодов, при этом каждый процесс управляет своим светодиодом. Но в процесс thread_led1 мы добавили включение и отключение реле RELAY_1:

void* led1_blink(void* arg) {
    pinMode(RELAY_1, OUTPUT);
    delay(100);
    digitalWrite(RELAY_1, HIGH);
	
    for (int i = 0; i < 3; ++i) {
        digitalWrite(LED_1, HIGH);
        delay(1000);
        digitalWrite(LED_1, LOW);
        delay(500);
    }
    digitalWrite(RELAY_1, LOW);
    pthread_exit(NULL);
}

Реле будет включено в начале работы процесса, и отключено перед его завершением.

Процессы thread_servo1_rotate, thread_servo2_rotate и thread_servo3_rotate запускают вращение сервоприводов.

Что касается процесса thread_servo1_rotate, то он поворачивает вал сервопривода из одного крайнего положения в другое, от 0⁰ до 180⁰:

void* thread_servo1_rotate(void* arg) {
	while (degree_1 < 180) {
		float millis = mapf(degree_1, 0.0f, 180.0f, 0.5f, 2.5f);
		printf("degree_1: %.2f, millis: %.2f\n", degree_1, millis);
		int tick = calcTicks(millis, HERTZ);
		printf("tick: %d\n", tick);
		pca9685PwmWrite(pca9685_fd, pin_1, 0, tick);
		delayMicroseconds(50);
		degree_1 += dir_1 == 1 ? 0.1f : -0.1f;
		if (degree_1 > 180) {
			degree_1 = 180;
			dir_1 = 0;
		} else if (degree_1 < 0) {
			degree_1 = 0;
			dir_1 = 1;
		}
	}
	degree_1 = 0;
  	pthread_exit(NULL);
}

Диапазон углов от 0⁰ до 180⁰ преобразуется в диапазон длительностей импульсов от 0.5 до 2.5 мс функцией mapf. Далее процесс вычисляет количество тактов для импульсов определенной длительности и передает его функции pca9685PwmWrite для поворота вала сервопривода.

Вслед за этим процесс выполняет задержку на 50 мкс функцией delayMicroseconds, и увеличивает значение текущего угла поворота, хранящееся в переменной degree_1.

Если цикл while завершил свою работу при достижении угла 180⁰, текущий угол устанавливается равным нулю, после чего процесс завершает свою работу.

В переменной dir_1 хранится текущее направление вращения вала.

Процесс thread_servo2_rotate работает аналогично, но выдает сигналы на сервопривод, подключенный к порту pin_2 контроллера.

Для управления сервоприводом непрерывного вращения используется процесс thread_servo3_rotate:

void* thread_servo3_rotate(void* arg) {
	int tick;
	delay(1000);
	for (int i = 0; i < 3; ++i) {	
		tick = calcTicks(1.0f, HERTZ);
		pca9685PwmWrite(pca9685_fd, pin_3, 0, tick);
		delay(2000);
		tick = calcTicks(2.0f, HERTZ);
		pca9685PwmWrite(pca9685_fd, pin_3, 0, tick);
		delay(2000);
		tick = calcTicks(1.6f, HERTZ);
		pca9685PwmWrite(pca9685_fd, pin_3, 0, tick);
	}
      pthread_exit(NULL);
}

После задержки 1 сек он запускает вращение вала вначале в одну сторону, затем в другую, после этого вал сервопривода останавливается, а процесс завершает свою работу.

Вы можете посмотреть эту программу в работе на YouTube.

Полезные ссылки

Библиотека WiringRP — документация и API

Библиотека WiringRP — проект на GitFlic

Библиотека WiringRP — каталог примеров на GitFlic

Оригинальная инструкция по установке от автора WiringRP

Распиновки 40 pin разъёма Repka Pi

Pthreads: Потоки в русле POSIX

Изучаем GPIO в Raspberry Pi, эксперимент со светодиодом и кнопкой

Repka Pi и управление сервоприводами, ЧАСТЬ 1. Сервоприводы - устройство и способы управления

Repka Pi и управление сервоприводами, ЧАСТЬ 2. Управляем сервоприводами с помощью Robointellect Controller 001 и RISDK

Repka Pi и управление сервоприводами, ЧАСТЬ 4. Прямое управление по I2C ШИМ/PWM сервоприводами и светодиодами с Repka Pi через Robointellect Controller 001

Итоги

Прочитав нашу статью, вы научились работать с портом GPIO микрокомпьютера Repka Pi из программы, составленной на языке Си с применением библиотеки WiringRP.

Вы скачали образ дистрибутив Repka OS RT Real Time, записали ее на карту памяти, а затем собрали библиотеку WiringRP и обновили ядро ОС.

После этого вы запустили программу blink.c из каталога примеров WiringRP, мигающую светодиодом, подключенным к одному из контактов GPIO.

Далее вы создали простейшую многопоточную программу mblink.c, каждый из потоков которой мигает своим светодиодом. В дальнейшем в программе button.c вы научились обрабатывать прерывания от кнопки, подключенной к GPIO. Теперь процессы мигания кнопками запускаются при отпускании ранее нажатой кнопки.

И завершая работу над статьей, вы добавили к макету с двумя светодиодами контроллер Robointellect Controller 001, созданный на базе чипа pca9685. К контроллеру вы подключили три сервопривода, а к реле — батарейку на 9 В и светодиод через резистор.

В итоге после отпускания нажатой кнопки вызывалась функция обработки прерывания, которая запускала пять процессов, работающих параллельно.

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

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

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

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