Windows 10 IoT Core: GPIO, Lightning і RemoteClient

enter image description here
Існує величезна кількість прикладів і статей про Windows 10 IoT Core, що розповідають про те, як легко та зручно робити з його допомогою різноманітні пристрої. Проте в реальності робота з будь-яким "залізом" завжди пов'язана з безліччю не найбільш очевидних нюансів, знання яких приходить тільки з практикою. Я розповім про деякі особливості роботи з GPIO на Raspberry Pi2 і Windows 10 IoT Core і заодно про нову функції Remote Client доступної версії Insider Preview.
Почалося все з того, що мені потрібно було отримати номер картки зі зчитувача системи СКУД (контролю доступу). Майже всі зчитувачі вміють передавати ці дані по інтерфейсу Wiegand. Він являє собою 3 дроти: сигнальний для передачі одиниць, сигнальний для передачі нулів і земля. У режимі очікування на кожному сигнальному проводі встановлюється 5В. Дані передаються "зворотними" імпульсами. Ширина імпульсів від 50 до 200 мкс, період від 300 до 3000 мкс:
enter image description here
Дані йдуть завжди в одну сторону від зчитувача до контролера. Кількість біт може змінюватись і їх інтерпретація теж може бути різною. Закінчення посилки визначається тайм-аутом від 50 до 250 мс.
Такий розкид параметрів стався тому, що це "історично сформований" інтерфейс, який не має чіткого стандарту.
Мені дісталося пристрій з протоколом wiegand26 — посилка в ньому містить 26 біт, в яких 2 біт контролю парності.
Завдання було дано в рамках створення демонстраційного стенду, так що можна було поекспериментувати з платформою. Тому дуже до речі прийшлася Raspbery Pi2 з Windows 10 IoT Core на борту.
Проблеми з GPIO
Вирішити це завдання можна двома способами:
  • використовувати переривання при зміні напруги на пинах
  • використовувати опитування пінів
найпростішим мені здався перший варіант. Правильно инициализировав піни, можна не навантажувати процесор постійним опитуванням, а відпрацьовувати тільки асинхронні виклики. Код тут дуже простий:
var gpio = GpioController.GetDefault();

var data0 = gpio.OpenPin(data0Pin);
var data1 = gpio.OpenPin(data1Pin);

// Check if input pull-down resistors are supported
if (data0.IsDriveModeSupported(GpioPinDriveMode.InputPullUp))
data0.SetDriveMode(GpioPinDriveMode.InputPullUp);
else
data0.SetDriveMode(GpioPinDriveMode.Input);

if (data1.IsDriveModeSupported(GpioPinDriveMode.InputPullUp))
data1.SetDriveMode(GpioPinDriveMode.InputPullUp);
else
data1.SetDriveMode(GpioPinDriveMode.Input);

var ticksPerMillisecond = TimeSpan.TicksPerMillisecond;
var tenMs = ticksPerMillisecond / 1000; //1 мікросекунда

data0.DebounceTimeout = TimeSpan.FromTicks(tenMs);
data0.ValueChanged += data0_ValueChanged;
data1.DebounceTimeout = TimeSpan.FromTicks(tenMs);
data1.ValueChanged += data1_ValueChanged;

Відразу потрібно сказати про DebounceTimeout. Взагалі він тут не потрібен, бо, як правило, при нормальній роботі інтерфейсу без істотних наведень "брязкоту" в ньому немає. Але з-за того, що в загальному випадку ми не знаємо, з яким зчитувачем і в яких умовах буде все це працювати, то я вирішив поекспериментувати з цим таймаутом.
Але нічого доброго з цього не вийшло. У прикладах DebounceTimeout скрізь задається в мілісекундах, але для мого випадку потрібні мікросекунди. Яке б значення я не встановлював, події ValueChanged не з'являлися. Тому таймаут довелося просто відключити, і все стало працювати нормально:
data0.DebounceTimeout = TimeSpan.FromTicks(0);
data0.ValueChanged += data0_ValueChanged;
data1.DebounceTimeout = TimeSpan.FromTicks(0);
data1.ValueChanged += data1_ValueChanged;

Дані зчитувалися так:
private void data0_ValueChanged(GpioPin sender, pioPinValueChangedEventArgs e)
{
if (e.Edge == GpioPinEdge.FallingEdge)
{
UpdateValue(0);
}
}
private void data1_ValueChanged(GpioPin sender, pioPinValueChangedEventArgs e)
{
if (e.Edge == GpioPinEdge.FallingEdge)
{
UpdateValue(1);
}
}

Потім прийшла пора вирішувати питання з моментом закінчення посилки. Як я писав вище, закінченням посилки вважається відсутність імпульсів на обох сигнальних контактах протягом мінімум 50 мс. Тут відмінно підійшов би таймер, який би регулярно перевіряв, чи був обмін, і якщо немає, то відправляв би отриманий результат далі.
Такий підхід працював погано. Я використовував ThreadPoolTimer, так як код повинен був працювати і без UI. Особливістю цього таймера є те, що кожен раз його обробник викликається в новому потоці. Якщо обробник виконується довше періоду таймера, то паралельно буде запущений ще один, а потім ще один, і так далі. При цьому в нашому випадку, в оброблювачі таймера повинні бути скинуті дані, щоб почати новий прийом. Тому він є критичною секцією і не повинен виконуватися одночасно більше ніж в одному примірнику. Відповідно, короткі інтервали таймера використовувати не можна було.
Далі виявилося, що і довгі інтервали теж можна використовувати, як і синхронізацію обробника через lock, так як поки виконується обробник таймера, перестають приходити події ValueChanged від портів. Схоже, що ці події менш пріоритетними, ніж обробники таймерів. Причому експерименти показали, що події не висять в черзі, а просто мовчки відкидаються.
Тому, як не крути, використання таймера призводило до втрати подій від GPIO і, як наслідок, втрати даних. Їх можна було зменшити шляхом підбору періоду таймера і перерозподілу обчислень на ValueChanged. Але абсолютної надійності досягти з таким підходом не можна навіть теоретично. Крім того, з'ясувалося, що до пропуску подій і втрати даних веде і занадто довгий виконання ValueChanged.
Я вирішив використовувати замість таймера потік. Для синхронізації застосовувалася queue, в яку складалися прийшли в ValueChanged біти. Потік повинен деякий час спати, потім перевіряти, чи був обмін за цей час, і якщо не було, то забирати біти з черги, збирати з них результат і відправляти далі.
Але й тут нічого не вийшло. Можна було припустити, що ValueChanged не буде викликатися при роботі потоку, але ось те, що він не викликається, коли потік спить, для мене виявилося сюрпризом. В якості аналога Thread.Sleep я використовував Task.Delay. Не знаю, чи відбувається при цьому виконання інших Task'ів (судячи з документації, Delay запускає ще один Task з таймером), але події від GPIO це все наглухо блокує.
загалом, драйвер GPIO для Windows 10 IoT написаний так, що його майже неможливо використовувати асинхронно, так як його події мають крані низький пріоритет в системі.
Lightning
У процесі вивчення роботи з GPIO я дізнався, що існує ще один, більш швидкий драйвер, який можна використовувати на Windows 10 IoT Core. Називається він Lightning і включає в себе не тільки GPIO, але і роботу з ADC, I2C, PWM, SPI. Швидкість роботи досягається за рахунок "прямого доступу до пам'яті"(direct memory access).
Драйвер знаходиться на стадії preview, але вже включений у версії Windows 10 IoT Core Insider Preview. Використовувати його варто тим, кому не вистачає швидкості роботи стандартного драйвера. Я ж сподівався тут отримати більш пріоритетний ValueChanged.
За посиланням вище є інструкція, як його використовувати, АЛЕ, просто так він не запрацює. Проблема полягає в NuGet пакеті. Все встановлюється, але до namespace Microsoft.IoT.Lightning.Providers достукатися неможливо. Причому єдина згадка про те, що проблему не можна побороти, я знайшов у цієї статті. Автор розповідає як керувати світлодіодами. Зокрема, він зіткнувся з повільною роботою PWM і виправив ситуацію з допомогою використання Lightning.
Виглядає воно так:
Third, you'll need to reference the Lightning SDK. According to the documentation, you just reference via NuGet. Unfortunately, this doesn't work as of v1.0.3-alpha. I had to download the Microsoft.IoT.Lightning.Providers C++ source, add the Microsoft.Iot.Lightning.Providers.vcxproj project to my solution, and then make a project reference. 

Incidentally, I contacted some folks at Microsoft, and they said a new nuget will be published shortly with binaries that will fix this issue.

Мені допоміг спосіб, описаний у статті. Я теж скачав исходники, додав в Solution файл проекту і зробив на нього Reference. І якого ж було моє здивування, коли виявилося, що події ValueChanged в Lightning не реалізовані...
Після цього я кинув ідею використовувати асинхронний підхід і вирішив зробити опитування пінів в циклі.
UPDATE: В процесі вирішення завдання я поставив розробникам запитання з приводу ValueChanged. Вони відповіли, що скоро все зроблять. І свою обіцянку вони выполнили.
Крім того, в NuGet з'явилася Microsoft.IoT.Lightning.Providers 1.0.0, яка робить видимим namespace Microsoft.IoT.Lightning.Providers.
Рішення задачі
У підсумку завдання вдалося вирішити шляхом створення циклу з періодом опитування пінів в 10 мкс. Спочатку були побоювання, що може не вистачити швидкості, але виявилося, що все працює досить швидко навіть на стандартному драйвері.
Код виглядає приблизно так:
_data0 = gpio.OpenPin(data0Pin, GpioSharingMode.Exclusive);
_data1 = gpio.OpenPin(data1Pin, GpioSharingMode.Exclusive);

if (_data0.IsDriveModeSupported(GpioPinDriveMode.InputPullUp))
_data0.SetDriveMode(GpioPinDriveMode.InputPullUp);
else
_data0.SetDriveMode(GpioPinDriveMode.Input);

if (_data1.IsDriveModeSupported(GpioPinDriveMode.InputPullUp))
_data1.SetDriveMode(GpioPinDriveMode.InputPullUp);
else
_data1.SetDriveMode(GpioPinDriveMode.Input);

_task = Task.Run(() => TaskHandler());

Щоб не блокувати виконання інших потоків, цикл зроблений всередині Task:
private void TaskHandler()
{
var ticksPerMillisecond = TimeSpan.TicksPerMillisecond;
var mks = ticksPerMillisecond / 1000; //1 мікросекунда
while (!_stopTask)
{
Task.Delay(TimeSpan.FromTicks(mks*10)).Wait();
var dt0 = _data0.Read();
var dt1 = _data1.Read();
.....
}
}

Працює стабільно, але за умови, що картки до рідера підносяться не частіше ніж 1 раз в секунду.
enter image description here
Даний проект з усіма інструкціями про підключення рідера до Raspberry Pi2 я планую через пару тижнів викласти на GitHub.
Remote client
Багато розробники, які користуються Windows 10 IoT Core, відзначають, що їм дуже не вистачає віддаленого робочого столу. Монітор або телевізор не завжди є під рукою, та й користуватися ними не завжди зручно. Нарешті цей пробіл був закритий, починаючи з версії 10.0.14295.1000, з'явився віддалений клієнт. Зараз ця і більш нова версія доступні як Insider Preview.
Працює все дуже просто. На стороні Windows 10 IoT Core потрібно дозволити підключення віддаленого клієнта в веб інтерфейсі:
enter image description here
Встановити сам клієнт
enter image description here
Запустити його і підключитися з Windows IoT Core
enter image description here
В інструкції зазначено, що всі добре працює на Raspberry Pi 2 і 3, Minnowboard Max і Dragonboard. При цьому на Pi2 продуктивність трохи нижче, так як там відсутня підтримка GPU.
У мене якраз була Rapberry Pi2.
enter image description here
є Невеликі затримки, але, на мій погляд, вони не критичні. При цьому все працює стабільно і без проблем.
Висновки
Підводячи підсумок всьому вищесказаному, зазначу, що Windows 10 IoT Core працює не завжди так, як може чекати від неї чоловік, звиклий до мікроконтролерів. Все-таки це повноцінна операційна система, яка досить сильно абстрагується від "заліза":
  1. Пріоритет подій від GPIO вкрай низький. Та й механізм подій не зовсім зрозумілий.
  2. Складнощі з асинхронним програмуванням, безпосередньо не пов'язані з роботою переривань.
  3. Існуючого функціоналу не завжди вистачає, а той, що представлений у вигляді preview, не завжди коректно працює. Хоча розвивається все це дуже швидко.
  4. Продуктивності Windows 10 IoT Core цілком вистачає для завдань, чутливих до часу виконання. При цьому є ще запас у вигляді Lightning.
  5. З'явився Remote Client, що істотно підвищило зручність роботи.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.