DIY-роутер GL-MT300N-V2. Часть 2 - написание и сборка собственной программы под OpenWRT

Дмитрий Филатов
Итак, будем писать свою программу на Си под OpenWRT. Отличий от обычного программирования под UNIX или Linux здесь, в общем-то, нет, за одним исключением: процесс компиляции программы осуществляется не на роутере, а на компьютере, в среде OpenWRT Buildroot. То есть, сначала нужно скомпилировать toolchain и прошивку, а потом уже можно собирать свою программу. Но и здесь есть нюанс: чтобы компилятор программу увидел, её нужно выбрать в меню конфигурирования сборки (том самом, которое появляется после вызова команды make menuconfig). Вся информация о процессе сборки собственной программы хорошо описана здесь, поэтому я представлю только пошаговый алгоритм. Создавать будем простую консольную программу для управления светодиодом, подключенным к GPIO13. Назовём её gpio-ctrl.

Работаем в среде OpenWRT Buildroot в Ubuntu, как это описано в первой части.

1. Подготавливаем структуру проекта. Находясь в каталоге openwrt, переходим в каталог package и создаём там директорию с названием, как у нашей программы. Наполняем эту директорию таким содержимым:

package/gpio-ctrl/
src/
main.c
Makefile
Makefile

Открываем на редактирование файл gpio-ctrl/Makefile и пишем в нём следующее:

#
# This software is licensed under the Public Domain.
#

include $(TOPDIR)/rules.mk

PKG_NAME:=gpio-ctrl # Название нашей программы
PKG_VERSION:=0.1
PKG_RELEASE:=1

# see https://spdx.org/licenses/
PKG_LICENSE:=CC0-1.0

include $(INCLUDE_DIR)/package.mk

define Package/gpio-ctrl
SECTION:=utils # Секция, в которой будет отображаться наша программа в меню конфигурирования
# Should this package be selected by default?
#DEFAULT:=y
CATEGORY:=Utilities
TITLE:=GPIO Control. # Описание программы
MAINTAINER:= Dmitry Filatov <mail@example.com> # Сведения об авторе
URL:=https://rxlab.org # URL программы
endef

# Ещё одно описание программы, здесь можно более подробно
define Package/gpio-ctrl/description
GPIO Control.
endef

define Build/Prepare
mkdir -p $(PKG_BUILD_DIR)
$(CP) ./src/* $(PKG_BUILD_DIR)/
endef

define Package/gpio-ctrl/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/gpio-ctrl $(1)/usr/bin/
endef

$(eval $(call BuildPackage,gpio-ctrl))

В своей версии не забудьте заменить в этом тексте все вхождения подстроки "gpio-ctrl" на название вашей программы.

Теперь редактируем файл gpio-ctrl/src/Makefile.

all: main.c
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -Wall -o gpio-ctrl $^ $(LDLIBS) -lrt

Опять же, замените "gpio-ctrl" на своё. Ну и, собственно, приступаем к написанию программы.

2. Откройте файл gpio-ctrl/src/main.c и введите в него код программы.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#define GPIO_BASE 0x10000600

int main(int argc, char **argv) {
int fd;
void *map;
unsigned long address, offset;

int pagesize = sysconf(_SC_PAGESIZE);

address = GPIO_BASE / pagesize * pagesize; //offset must be a multiple of the page size
offset = GPIO_BASE - address; //offset

fd = open("/dev/mem", O_RDWR | O_SYNC);
if (fd == -1) {
perror("open /dev/mem");
fprintf(stderr, "Is CONFIG_DEVMEM activated?\n");
exit(1);
}

map = mmap(NULL, 256, PROT_READ|PROT_WRITE, MAP_SHARED, fd, address);
if (map == (void *)(-1)) {
fprintf(stderr, "mmap() failed: %s\n", strerror(errno));
exit(1);
}

*(unsigned long *)(map + offset) |= (1 << 13); //set gpio 13 to out

for(int i = 0; i < 7; i++){
*(unsigned long *)(map + offset + 0x20) |= (1 << 13); //set 0x2000
sleep(1);
*(unsigned long *)(map + offset + 0x20) &= ~(1 << 13); //set 0x0
sleep(1);
}

munmap(map, 256);

close(fd);

return 0;
}

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

# echo 13 > /sys/class/gpio/export
# echo out > /sys/class/gpio/gpio13/direction
# echo 1 > /sys/class/gpio/gpio13/value
# echo 0 > /sys/class/gpio/gpio13/value

Но это не интересно и, к тому же, крайне долго. Настоящий электронщик в таком случае будет дёргать регистры процессора напрямую. Чтобы управлять регистрами, нам нужно отобразить их в память программы, для чего и служит функция mmap. Это - стандартная процедура Linux, можете почитать про mmap подробнее в Интернете. Хорошо, но какой адрес у регистра GPIO? Где его взять? Читаем маркировку процессора на плате: MT7628NN. Находим даташит. GPIO-порты расписаны на стр. 168. Нас интересует GPIO13, который входит в следующие регистры:

- Регистр контроля направленности GPIO_CTRL_0 по адресу 0x10000600. Записываем в этот регистр в бит, соответствующий выбранному пину, 0 для включения режима приёма или 1 для включения режима передачи. Нам нужен режим передачи (output mode) для бита №13, то есть в регистр по адресу 0x10000600 мы должны записать число 0b00000000000000000010000000000000 (соответствие номера бита номеру пина см. на стр. 170 даташита). Число 0b00000000000000000010000000000000 в шестнадцатеричном представлении 0x2000, следовательно, мы должны записать:
*0x10000600 = 0x2000;

- Регистр состояния GPIO_DATA_0 по адресу 0x10000620. Здесь всё то же самое. Состояние ножки ввода-вывода записывается в бит, соответствующий нашему 13 пину. Опять-таки, подать напряжение на пин №13: 0b00000000000000000010000000000000, сбросить в ноль: 0b00000000000000000000000000000000. Поэтому, в регистр по адресу 0x10000620 мы должны записать либо 0x2000 (единица у бита 13), либо 0x0 (ноль у всех битов):
*0x10000620 = 0x2000; //Зажечь светодиод
*0x10000620 = 0x0; //Погасить светодиод

Но! Кроме интересующего нас бита, их там ещё много других. И, чтобы не влиять на них, мы должны оперировать только с одним единственным битом. Для этого применимы такие битовые операции:
addr |= 1 << n; //Установить бит номер n
addr &= ~(1 << n); //Очистить бит номер n
Именно поэтому в коде у меня написано:
*(unsigned long *)(map + offset) |= (1 << 13);
Хотя могло бы быть написано и проще:
*(unsigned long *)(map + offset) = 0x2000;
Но при этом наш код влиял бы и на другие выводы GPIO (сбрасывал бы их в ноль).

С этим разобрались. Следующий тонкий момент. Мы не можем мапить адрес 0x10000600 или 0x10000620, потому что функция mmap требует, чтобы адрес смещения был кратен размеру страницы памяти. Она у нас 4096 байт, но лучше не доверять интуиции и получить её размер в байтах при помощи системного вызова sysconf(_SC_PAGESIZE). Когда мы делением-умножением выравниваем адрес относительно размеров страницы, у нас остаётся ещё какой-то остаточек (offset), на который мы позже должны будем сместиться.

Следующий параметр функции mmap, который нужно пояснить, - размер отображаемой памяти. Здесь, обратите внимание, у меня написано 256. Откуда эта цифра? А из даташита. На стр. 100 указан диапазон занимаемой памяти для каждого из регистров.

Ну, а дальше всё просто. Функция mmap возвращает нам адрес памяти, в который была отображена память регистров. Обращаясь к памяти по этому адресу, мы как-будто бы обращаемся к адресам регистров напрямую. Только нам нужно прибавить к ней остаточек от деления, чтобы попасть в нужное место. Ну, и ещё потом прибавить 0x20, чтобы перейти от 0x10000600 к 0x10000620.

В конце не забудьте освободить память, вызвав функцию munmap() и передав ей начальный адрес памяти, в которую были отображены регистры, и размер занятой памяти.

3. Теперь - компиляция. Выполняем:

$ make menuconfig

В появившемся окошке, уже знакомом нам по первой части, переходим в раздел, который мы указали в Makefile нашей программы, находим там нашу программу и включаем её. Только не как модуль, а как встроенную программу (чтобы напротив её названия звёздочка стояла, а не буква M).
Utilities → gpio-ctrl
Сохраняем конфигурационный файл, после чего приступаем к сбоке самой программы:

$ make package/gpio-ctrl/{clean,compile} V=s

Собранный пакет Вы найдёте здесь: bin/packages/mipsel_24kc/base/gpio-ctrl_0.1-1_mipsel_24kc.ipk

4. Установка на роутер. Идеальный вариант - закинуть собранный пакет на какой-нибудь сервер. Ну, пусть будет http://rxlab.org/packages, к примеру. А дальше - подключаемся к роутеру по ssh и выполняем команды:

$ wget http://rxlab.org/packages/gpio-ctrl_0.1-1_mipsel_24kc.ipk
$ opkg install gpio-ctrl_0.1-1_mipsel_24kc.ipk

Запустить программу на выполнение:

$ gpio-ctrl

Удалить программу

$ opkg remove gpio-ctrl

В общем, это основы. Дальше уже только Ваша фантазия. Остановлюсь только ещё на одном моменте. Когда Вы работаете с регистрами процессора напрямую - иногда бывает непонятно, ошибка ли у Вас в программе или Вы выбрали не тот адрес регистра/записали в него не то значение. Для целей отладки отлично подходит пакет io, который можно установить в OpenWRT.

$ cd /tmp
$ wget http://archive.openwrt.org/releases/18.06.1/packages/mipsel_24kc/packages/io_1_mipsel_24kc.ipk
$ opkg install io_1_mipsel_24kc.ipk

Использовать так:

$ io -4 0x10000600 0x2000 //Установить пин #13 на выход (out)
$ io -4 0x10000620 0x2000 //Записать в него 1
$ io -4 0x10000620 0x0 //Записать в него 0

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

1. Working with GPIOs (C/C++)
2. How to turnoff UART to free GPIO (only on ath79 processors)
2019-02-04