- PVSM.RU - https://www.pvsm.ru -
Привет!
В прошлый раз я написал о том, как можно несложно подключить наш нанокомпьютер к чему-нибудь исполнительному (ёлочной гирлянде, например) и развернуть среду для сборки под него программ на C/C++. Сегодня — вторая часть, о том, как написать программу на C для OpenWRT и, соответственно, Black Swift.
Отмечу два момента. Во-первых, я не буду рассказывать о роли функции main и директивы #include — как я говорил раньше, сейчас я пишу в расчёте на людей, в принципе знакомых с программированием, но не знающих, с какой стороны подступиться к такой штуке, как компьютер для встраиваемого применения. Точнее, сегодняшний текст будет скорее для уже подступившихся — так что я остановлюсь на паре интересных моментов, таких как работа с логическими линиями ввода-вывода (GPIO) и микросекундными временами.

Во-вторых, конечно, писать именно на C не обязательно. Под OpenWRT есть Perl, Python и даже Node.js — с ними вам не нужна, очевидно, никакая среда для сборки софта, только заливай@запускай.
У нас на руках — ёлочная гирлянда, управляемая четырьмя полумостовыми драйверами, работающими попарно: одна пара задаёт полярность одной ветки гирлянды, а полярность определяет цвет свечения. Очевидно, для работы с этим нам нужна широтно-импульсная модуляция, причём достаточно специфическая — несимметричная, то есть в течение каждого периода каждая из полярностей может включаться на разное время (простейший случай — давая только одну полярность, мы получаем только один цвет свечения гирлянды).
Эта схема мешает в лоб применить готовый модуль ШИМ (в 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 в «выход».
Описаний этой процедуры — тысячи, вот, например, одно из них [2].
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]). Также нам интересны адреса на +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, я напомню.
Очевидно, такая скорость управления гирляндой нам даром не нужна — нам прекрасно подходит всё, что выше 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 одного уровня равносильна отключению гирлянды.
Что нам ещё нужно от приложения, управляющего гирляндой?
Во-первых, яркость должна как-то меняться во времени, и лучше, чтобы было несколько программ. Вы можете сделать собственный скриптовый язык, я поступил проще — вбил в код трёхмерный массив, внутри которого хранятся 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 [4], включая Makefile для OpenWRT и для самого проекта (этот ничем вообще не отличается от обычных Makefile'ов для сборки софта под любым линуксом). Видео по заявкам наших читателей будет в третьей части (хотя что вы, гирлянд не видели, что ли?).
Разумеется, всё сказанное применимо к любому роутеру и нанокомпьютеру на OpenWRT, за одной оговоркой: для Black Swift мы собираем ядро с таймером на 1000 Гц против обычных 100 Гц и с включённой вытесняющей многозадачностью. Без этого — на стандартных ядрах — ШИМ может оказаться медленнее или менее стабильным при какой-либо нагрузке на систему.
Впрочем, про сборку своих прошивок для OpenWRT — в другой раз.
Автор: olartamonov
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/diy-ili-sdelaj-sam/79690
Ссылки в тексте:
[1] Гирлянда, подключение Black Swift и среда сборки под OpenWRT на C/C++: http://habrahabr.ru/company/blackswift/blog/247817/
[2] одно из них: http://falsinsoft.blogspot.ru/2012/11/access-gpio-from-linux-user-space.html
[3] страница 65: http://www.black-swift.com/files/AR9331.pdf
[4] https://github.com/olegart/treelights: https://github.com/olegart/treelights
[5] Источник: http://habrahabr.ru/post/247925/
Нажмите здесь для печати.