Елочка, зажгись! Часть 2: софт на C, работа с GPIO и программная ШИМ

в 14:20, , рубрики: Black Swift, diy или сделай сам, OpenWrt, Блог компании Black Swift, микрокомпьютер, Электроника для начинающих

Привет!

В прошлый раз я написал о том, как можно несложно подключить наш нанокомпьютер к чему-нибудь исполнительному (ёлочной гирлянде, например) и развернуть среду для сборки под него программ на C/C++. Сегодня — вторая часть, о том, как написать программу на C для OpenWRT и, соответственно, Black Swift.

  1. Гирлянда, подключение Black Swift и среда сборки под OpenWRT на C/C++
  2. Управляющая программа на C и прямая и быстрая работа с GPIO
  3. Веб-интерфейс и приложение для Android

Отмечу два момента. Во-первых, я не буду рассказывать о роли функции main и директивы #include — как я говорил раньше, сейчас я пишу в расчёте на людей, в принципе знакомых с программированием, но не знающих, с какой стороны подступиться к такой штуке, как компьютер для встраиваемого применения. Точнее, сегодняшний текст будет скорее для уже подступившихся — так что я остановлюсь на паре интересных моментов, таких как работа с логическими линиями ввода-вывода (GPIO) и микросекундными временами.

Елочка, зажгись! Часть 2: софт на C, работа с GPIO и программная ШИМ - 1

Во-вторых, конечно, писать именно на C не обязательно. Под OpenWRT есть Perl, Python и даже Node.js — с ними вам не нужна, очевидно, никакая среда для сборки софта, только заливай@запускай.

Быстрая и простая работа с GPIO

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

Эта схема мешает в лоб применить готовый модуль ШИМ (в OpenWRT есть такой) — он выдаёт симметричный сигнал, то есть максимум, чего с ним можно добиться — регулировки яркости всей гирлянды по всем цветам одновременно, без возможности управлять цветами независимо. Поэтому ШИМ надо сделать самому.

ШИМ — это, на базовом уровне, всего лишь быстрое дергание ножками процессора, в простейшем случае вот в таком цикле:

while (true)
{
  if (value > 0)
    gpioSet(GPIO_NUMBER, 1);

  for (int i=1; i<=100; i++)
    if (value == i)
      gpioSet(GPIO_NUMBER, 0);
}

Смысл, я думаю, очевиден: значением «value» задаём нужную нам скважность, выражающуюся в нашем случае в яркости свечения светодиодов гирлянды (смену полярности и управление цветом пока не трогаем).

Стандартное содержимое функции gpioSet(int gpio, int level) в линуксе — это дёргание ножками GPIO через стандартный же интерфейс sysfs: чтобы установить выход N в единицу, надо записать единицу в псевдофайл /sys/class/gpio/gpioN/value. До этого надо записать номер N в /sys/class/gpio/export, чтобы gpioN стал доступен пользователю, а также слово «out» в /sys/class/gpioN/direction, чтобы установить режим данного GPIO в «выход».

Описаний этой процедуры — тысячи, вот, например, одно из них.

void gpioSet(int gpio, int value)
{
  sprintf(buf, "/sys/class/gpio/gpio%d/value", gpio);
  fd = open(buf, O_WRONLY);
  sprintf(buf, "%d", value);
  write(fd, buf, 1); 
}

Способ простой, универсальный (трудно найти язык, из которого не удастся писать в файл), но — очень медленный. Если с его помощью сделать управление нашей гирляндой, то в итоге на дёргание четырёх ножек (две пары полумостов) будет уходить столько времени, что частота ШИМ на Black Swift с его 400-МГц процессором будет около 100 Гц, а на маленькой яркости светодиоды будут отчётливо и неприятно мерцать — там вообще начнётся пропуск тактов.

К счастью, есть способ лучше. К ещё большему счастью, он не то что не сложнее — он проще. Прямолинейнее.

void gpioSet(int gpio, int value)
{
  if (value == 0)
    *(GPIO_ADDR + 4) = (1 << gpio);
  else
    *(GPIO_ADDR + 3) = (1 << gpio);
}

Фокус в том, что GPIO в процессоре управляются через регистры — все операции с ними делаются через них. А регистры отображаются в обычную память. А обычная память доступна нам напрямую через обычное устройство /dev/mem. Соответственно, всё, что нам надо — это отобразить нужный маленький кусочек /dev/mem в память (память в память… ну да ладно) с помощью mmap, а потом дёргать в этом кусочке нужные биты.

if ((m_mfd = open("/dev/mem", O_RDWR) ) < 0)
	return -1;

m_base_addr = (unsigned long*)mmap(NULL, GPIO_BLOCK, PROT_READ|PROT_WRITE, MAP_SHARED, m_mfd, GPIO_ADDR);
close(m_mfd);

Отображение регистров в память описано в даташите на процессор, в случае с AR9331 адреса начинаются с GPIO_ADDR = 0x18040000 (страница 65). Также нам интересны адреса на +3 и +4 к базовому — запись единицы в бит, соответствующий номеру GPIO, по первому адресу устанавливает GPIO в 1, а по второму — сбрасывает в 0 (если что, есть ещё регистр по адресу +2, который может и сбрасывать, и устанавливать GPIO — в него надо писать или 0, или 1 в нужный бит). Направление GPIO устанавливается битами по базовому адресу — 1 для выхода, 0 для входа.

Nota bene: некоторые GPIO многофункциональны, это задаётся отдельным регистром — и пока, например, вы не выключите UART на GPIO 9, он не будет работать как обычный вход/выход. Кроме того, GPIO с 13 по 17 нельзя использовать как входы.

Скорость? Дёргание четырьмя GPIO в двойной несимметричной ШИМ — примерно 4,5 кГц. Против примерно 100 Гц при работе с sysfs, я напомню.

Регулировка периода ШИМ с помощью nanosleep

Очевидно, такая скорость управления гирляндой нам даром не нужна — нам прекрасно подходит всё, что выше 100 Гц, особенно если на маленьких яркостях оно не будет «терять» такты (а с прямой работой с GPIO оно не будет). Надо вводить задержку. Стандартно короткие задержки вводятся с помощью функции nanosleep(&tw, NULL):

struct timespec tw;
tw.tv_sec = 0; // секунды
tw.tv_nsec = 10000; // наносекунды

while (true)
{
  if (value > 0)
    gpioSet(1);

  for (int i=0; i<100; ++i)
  {
    if (value == i)
      gpioSet(0);
    nanosleep(&tw, NULL);
  }
}

Теоретически здесь мы должны получить задержку 10 мкс на каждый такт ШИМ, всего 100 тактов — итого 1 мс, или частота ШИМ 1 кГц (не учитывая накладных расходов на дёргание ножкой). Компилируем, запускаем… и получаем около 140-150 Гц.

Проблема заключается в том, что минимальный штатно обслуживаемый период nanosleep в OpenWRT и на таком процессоре — порядка 60 мкс. То есть, даже если вы передадите в функцию tw.tv_nsec = 0, она всё равно затормозит тред на 60 мкс.

К счастью, есть примитивный, не очень точный, но работающий способ борьбы с этим: вызов nanosleep(NULL, NULL) занимает примерно 3 мкс.

void nsleep(unsigned long nsecs)
{
	if(nsecs == 0)
	{
		nanosleep(NULL,NULL);
		return;
	}

	struct timespec ts;
	ts.tv_sec=nsecs / 1000000000L;
	ts.tv_nsec=nsecs % 1000000000L;
	nanosleep(&ts,NULL);
}

const int nsleep0_factor=3000;
const int nsleep1_factor=70;

void _usleep(unsigned long usecs)
{
	if (usecs == 0)
		return;

	unsigned long value = (usecs*1000) / nsleep0_factor;

	if (value > nsleep1_factor)
	{
		nsleep((value-nsleep1_factor) * nsleep0_factor);
		value = value % nsleep1_factor;
	}

	for (unsigned long i=0; i < value; ++i)
		nsleep(0);
}

В результате, позвав _usleep менее чем на 70 мкс, мы вызовем nanosleep не штатным образом, а просто много-много раз прокрутим nanosleep(NULL, NULL), каждый вызов которого займёт 3 мкс. Грубо, но нам точнее для наших целей и не надо (если вам нужна качественная ШИМ, её надо делать всё же аппаратно или программно на системе, где вы гарантируете режим реального времени — например, подцепить к Black Swift обычный ATTiny через UART).

Ну, в общем, базовые кирпичики готовы — мы можем сделать вполне стабильно работающий программный ШИМ с частотой в сотню-две герц.

Можно ещё вспомнить, что данной ШИМ мы контролируем две пары полумостов, но это уже банально:

for (int i = 0; i < 100; i++)
{   
	for (int j = 0; j < 2; j++)
	{
		if (i == floor((float)gpioValuePos/brightness))
		{
			gpioSet(gpio[2*j], GPIO_OFF);
			gpioSet(gpio[2*j+1], GPIO_OFF);
		}
		if (i == (100 - floor(((float)gpioValueNeg/brightness))))
		{
			gpioSet(gpio[2*j], GPIO_OFF);
			gpioSet(gpio[2*j+1], GPIO_ON);
		}
	}
	_usleep(PWM_PERIOD);
}

Где gpioValuePos и gpioValueNeg — значения двух полярностей (с условием, что их сумма не должна превышать 100, конечно), brightness — это мы заранее заложились на возможность регулировки яркости всей гирлянды сразу. Установка на два GPIO одного уровня равносильна отключению гирлянды.

Bells and whistles

Что нам ещё нужно от приложения, управляющего гирляндой?

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

int prg[PROGRAMS][4][4] = {
	{ {0, 1, 0, 0}, {99, -1, 0, 0}, {0, 0, 0, 1}, {0, 0, 99, -1} },
	{ {0, 1, 0, 0}, {0, 0, 0, 1}, {0, 1, 0, 0}, {0, 0, 0, 1} },
	{ {99, -1, 0, 1}, {0, 1, 99, -1}, {99, -1, 0, 1}, {0, 1, 99, -1} },
	{ {99, 0, 0, 0}, {99, 0, 0, 0}, {99, 0, 0, 0}, {99, 0, 0, 0} },
	{ {0, 0, 99, 0}, {0, 0, 99, 0}, {0, 0, 99, 0}, {0, 0, 99, 0} },
	{ {49, 0, 50, 0}, {49, 0, 50, 0}, {49, 0, 50, 0}, {49, 0, 50, 0} }
};

То есть, например, первая программа — плавно нарастает яркость канала "+", потом она плавно спадает, потом так же нарастает и спадает яркость канала "-". Говоря проще — плавно зажигается и гаснет один цвет, потом так же плавно зажигается и гаснет второй. А в последней постоянно светятся все светодиоды всех цветов.

Во-вторых, раз уж у нас тут целый Black Swift, давайте сделаем управление через Wi-Fi? А потом и смартфонное приложение, в конце концов, что ты за гик, если у тебя даже ёлочная гирлянда без своего IP-адреса? В общем, надо делать интерфейс к обычному вебу.

Технически проще всего сделать управление через UNIX-socket — псевдофайл, команды в который можно пихать хоть из PHP, хоть из командной строки (с утилитой socat).

mode_t mask = umask(S_IXUSR | S_IXGRP | S_IXOTH);
int s, s2, len;
unsigned int t;
struct sockaddr_un local, remote;
if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
{
    printf("Socket errorn");
    return -1;
}

int flags = fcntl(s, F_GETFL, 0);
fcntl(s, F_SETFL, flags | O_NONBLOCK); 
local.sun_family = AF_UNIX;
strcpy(local.sun_path, "/tmp/treelights.sock");
unlink(local.sun_path);
len = strlen(local.sun_path) + sizeof(local.sun_family);
if (bind(s, (struct sockaddr *)&local, len) == -1)
{
    printf("Socket bind failedn");
    return -1;
}
if (listen(s, 5) == -1)
{
    printf("Socket listen failedn");
    return -1;
}
umask(mask);

Здесь всё фактически шаблонное, поясню только, что операции с umask в начале и конце нужны на системах, где веб-сервер и наше приложение работают под разными пользователями. В OpenWRT по умолчанию это не так, там все под root. Принципиальна также неблокирующая работа сокета — иначе первое же обращение к нему приведёт к тому, что все встанут и будут ждать, пока в сокет что-нибудь свалится.

Внутрь нашей ШИМ вставляем код обработки сваливающегося в сокет:

s2 = accept(s, (struct sockaddr *)&remote, &t);	
if (s2 > 0)
{
    int i = recv(s2, sockinput, 25, 0);
    sockinput[i] = 0; // null-terminated string
    close(s2);

    cmd[0] = strtok(sockinput, "- n");
    cmd[1] = strtok(NULL, "- n");
				
    if (strcmp (cmd[0], "brightness") == 0) // set brightness
    {
        brightness = atoi(cmd[1]); // 1 is maximum, 2 is half-brightness, etc.
    }
}

Идея, я думаю, понятна. Кидаем в сокет «brightness 2» — получаем уполовинивание яркости гирлянды. Обработка любых других команд дописывается аналогично.

Что ещё добавим? Для удобства ручного запуска при отладке — пусть программа корректно реагирует на Ctrl-C и другие просьбы подвинуться:

do_exit = 0;
static struct sigaction act; 
sigemptyset (&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = SIG_IGN;
sigaction (SIGHUP, &act, NULL);
act.sa_handler = cleanup;
sigaction (SIGINT, &act, 0);
act.sa_handler =  cleanup;
sigaction (SIGTERM, &act, 0);
act.sa_handler =  cleanup;
sigaction (SIGKILL, &act, 0);

while (!do_exit)
{
// тут по-прежнему вся наша ШИМ
}

И добавляем функцию:

void cleanup(int sig)
{
    do_exit = 1;
}

Теперь прилетевший от ОС сигнал с просьбой прерваться переключит переменную do_exit в 1 — и цикл ШИМ закончится (do_exit, конечно, неплохо бы объявить как глобальную переменную).

В общем, на этом всё. У нас есть готовые кирпичики для построения несимметричной ШИМ с регулировкой яркости, несколькими программами работы и управлением через веб.

Результат лежит тут: https://github.com/olegart/treelights, включая Makefile для OpenWRT и для самого проекта (этот ничем вообще не отличается от обычных Makefile'ов для сборки софта под любым линуксом). Видео по заявкам наших читателей будет в третьей части (хотя что вы, гирлянд не видели, что ли?).

Разумеется, всё сказанное применимо к любому роутеру и нанокомпьютеру на OpenWRT, за одной оговоркой: для Black Swift мы собираем ядро с таймером на 1000 Гц против обычных 100 Гц и с включённой вытесняющей многозадачностью. Без этого — на стандартных ядрах — ШИМ может оказаться медленнее или менее стабильным при какой-либо нагрузке на систему.

Впрочем, про сборку своих прошивок для OpenWRT — в другой раз.

Автор: olartamonov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js