STM32 и FreeRTOS. 3. Очереди

Это черновая версия, финальная опубликована на хабре

“Вас много, а я одна!” – классическая фраза продавщицы, которую затерроризировали покупатели с вопросами “А есть …?”. Вот и в микроконтроллерах случаются полностью аналогичные ситуации, когда несколько потоков требуют внимания от какой-либо медленной штуки, которая просто физически не способна обслужить всех разом.

Возьмем наиболее яркий и богатый проблемами пример, на котором “валятся” большинство неопытных программистов. Есть мощный и достаточно быстрый микроконтроллер. К нему подключен с одной стороны адаптер com-порта, через который пользователь подает команды и получает результаты, а с другой – шаговый двигатель, который согласно этим командам поворачивается на какой-то угол. И конечно же, прикольная кнопочка, которая тоже что-то этакое значит для пользователя. Где можно наловить проблем?

Пойдем со стороны пользователя. Com-порт, или USART (universal asynchronous receiver/transmitter) – штука очень нежная и капризная. Основной каприз заключается в том, что ее нельзя оставлять без внимания. По одной простой причине – для экономии выводов в 99% случаев выводят только сигналы приема и передачи, оставляя сигналы разрешения приема и передачи за бортом. И стоит микропроцессору хоть чуть-чуть замешкаться, как символ будет потерян. Судите сами: магические цифры 9600/8N1 говорят нам, что в секунду по линии летит 9600 бод, а один символ кодируется 9 импульсами. В итоге максимальная скорость передачи составляет 9600/9 = 1066 байт в секунду. Или чуть меньше одной миллисекунды на байт. А если по ТЗ скорость передачи 115200? Там может прилететь 128 байт за миллисекунду. А ведь микроконтроллеру надо еще обработать эти данные. Кажется, что все плохо, но в реальности куча устройств работает и не доставляет проблем. Какие же решения применяются?

Cамым распространенным (и правильным) решением является перекладка всей работы по приему или передаче символов на плечи микроконтроллера. Делать аппаратный usart порт научились сейчас все, поэтому типовое решение в большинстве случаев выглядит вот так:

void URARTInterrupt()
{
a=GetCharFromUSART();
if(a!=0x13)
buffer[count++]=a;
else
startProcessing();
}

Где проблема? Во-первых, проблема в вызове startProcessing. Стоит ей хоть чуть-чуть задержаться с работой, как очередной символ будет потерян. STM32L1 на минимальной частоте успевает за 1мс обработать 84 команды. В итоге при более-менее развесистой логике полученная конструкция будет терять символы.

После дружеского похлопывания по голове у нас в студии программист переписывает код так, что бы вместо вызова startProcessing поднимался семафор, который и запускал бы обработку полученных данных. И тут же получил следующую проблему в полный рост: пока startProcessing что-то там перемалывает из буфера, обработчик прерывания по получению нового символа начинает планомерно затирать еще не обработанный буфер. После очередного разорванного свитера и скуренной книжки по буферам программист реализует кольцевой буфер с указателями, чем закапывает проблему еще глубже.

Обычно где-то на этом месте я замечаю удивленные глаза народа и возмущенные выкрики в духе “ну такие алгоритмы же работают в куче проектов и ничего, никаких проблем”. Да, работают. Но в каких проектах? Только в тех, где можно все общение с контроллером перевести на синхронную логику. Вот пример диалога пользователя (П) и контроллера (К)

П: ATZ (Контроллер, ты живой?)
К: ОК (Ага, я тут)
П: М340,1 (Сделай чего-то)
(тут может быть пауза, иногда очень большая)
К: ОК (Сделал типа)

Где проблемы? Во-первых, нельзя послать несколько команд подряд. Значит либо надо будет менять логику или делать сложные команды с несколькими параметрами. Во-вторых, между запросом и ответом не может быть никаких других запросов и ответов. А вдруг пользователь в процессе движения нажмет кнопку? Что делать? Выход только один – использовать родную природу порта, а именно его асинхронность. В результате диалог между пользователем и контроллером выглядит примерно так

П: ATZ (жив?)
К: ОК (ага)
П: M340,1
К: К1=1 (кнопку нажали)
П: Е333,25,2
(тишина)
К: Е=1 (задачу Е выполнил)
К: М=1 (задачу М выполнил)

Конечно, логика обработки подобного потока немного усложняется, зато благодаря такой асинхронности мы сразу получаем множество преимуществ перед “классической школой”.

Во-первых, резко повышается отзывчивость интерфейса с пользователем. Ну работает там где-то моторчик или считается что-то, но это же не повод “замирать” или “тормозить”. А когда пользователь понимает, что величина тормозов практически не зависит от мощности контроллера, у него возникают резонные вопросы …
Во-вторых, легко реализовать совместное управление. Скажем, у меня на плате 8 выводов. Легко можно сделать так, что бы первые 3 управлялись через USART1, вторые 3 через USB-CDC, а последние два – совместно. И все это на одном контроллере.
И наконец, очень легко отвязать логику от аппаратного интерфейса. Нам все равно, откуда приходят команды – от соседнего процесса, от пользователя или вообще от другого контроллера.

А теперь со всеми этими задумками взглянем на противоположную сторону – на сторону исполнителя. Он достаточно медлителен, что бы попросту не успеть обработать команды по порядку. И время запуска-остановки моторчика очень большое, поэтому хорошо бы сделать примитивную оптимизацию в духе “если поступило две команды на поворот в одну сторону, то поверни за раз”.

Что делает обычный программист? Так как он прочитал предыдущие статьи и кучку книжек, то он рисует логику расставляя семафоры по необходимости, а для блокирования одновременного доступа к моторчику использует мутексы. Все хорошо, но код получается громоздкий и тяжело читаемый. Что делать? У нас в studiovsemoe.com мы используем рецепт Шарикова: “В очередь, сукины дети! В очередь!”

В данном примере можно просто создать три очереди. Первая это команды, полученные от пользователя. В нее засовывается все (ну или после минимальной проверки), что принято со всех входных портов. Вторая это те данные, которые необходимо выдать пользователю. Состояние кнопок, результаты расчетов и так далее. И наконец, третья очередь служит для заданий моторчику/считалке.

Все, всего три очереди, а дикая куча проблем решена. Во-первых, нет даже потенциальной проблемы потери или переписи буфера принятых символов. Правда результатом станет чуть больший расход памяти, но это допустимая цена. Во-вторых, нет проблем с выводом. Все процессы просто пишут в одну очередь, а как выводить, в каком формате и прочее – это уже не их забота. А значит упрощается код и понижается стоимость разработки и поддержки. И наконец благодаря такому подходу задачи очень легко разделить на более мелкие и раскидать их по потокам/ядрам микропроцессора.

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

Где-то в начале кода определим очередь

xQueueHandle q;

Код светодиодов поменяем по принципу “в очереди больше Н элементов? зажигаем, если нет, то нет”

if(uxQueueMessagesWaiting(q)>1)
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_9,GPIO_PIN_SET);
else
НAL_GPIO_WritePin(GPIOE,GPIO_PIN_9,GPIO_PIN_RESET);
osDelay(100);

Перед запуском планировщика проинициализируем очередь так, что бы она могла хранить 8 байт.

q = xQueueCreate( 8, sizeof( unsigned char ) );

Ну и код кнопки для помещения символов в очередь

if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==GPIO_PIN_SET)
{
unsigned char toSend;
xQueueSend( q, ( void * ) &toSend, portMAX_DELAY );
}
osDelay(500);

Чего не хватает? Воркера, который забирает из очереди задания. Пишем.

static void WorkThread(void const * argument)
{
for(;;)
{
unsigned char rec;
xQueueReceive( q, &( rec ), portMAX_DELAY );
osDelay(1000);
}
}

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

Как обычно, использующийся код доступен по адресу http://kaloshin.ru/stm32/freertos/stage3.rar

STM32 и FreeRTOS. 2. Семафорим.

Это черновая версия, финальная опубликована на хабре

В реальной жизни часто случается так, что некоторые события происходят с разной переодичностью. Скажем, заказ сока в “Макдональдсе”, нажатие кнопки пользователем или заказ лыж в прокате. А наш могучий микроконтроллер должен все это обрабатывать. Но как это сделать наиболее удобно?

Рассмотрим процесс заказа сока в макдаке. Покупатель говорит “хочу сок”, один продавец пробивает стоимость в чек, а другой смотрит в экранчик и идет наливать сок. Налил, принес и опять смотрит в экранчик, чего надо принести. И так продолжается до бесконечности или до конца смены. Мы заменяем человеческий труд машинным, поэтому автоматизируем это!

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

bool sok=false;

void thread1(void)
{
for(;;) if(user_want_juice) sok=true; else sok=false;
}

void thread2(void)
{
for(;;;) if(sok) prinesi_sok();
}

Логика думаю понятна: один поток контролирует ситуацию и ставит флаг, когда надо принести сок. А другой контролирует этот флаг и приносит сок, если надо. Где проблемы?

Первая проблема в том, что приносящий сок постоянно спрашивает “сок принести надо?”. Это раздражает продавца и нарушает первое правило программиста встроенных устройств “если функции нечего делать, то отдай управление планировщику”. Казалось бы, ну воткнем osDelay(1), что бы другие задачи отработали или просто понизим приоритет и пускай крутится, ведь железка-то железная выдержит … В том-то и дело, что не выдержит. Перегрев, недостаток ресурсов и так далее и тому подобное …

А вторая проблема зарыта гораздо глубже. Дело в компиляторах,оптимизациях и многопоточности. Вот для примера: у нас подносчик сока такой шустрый, что может обслужить сразу трех продавцов. В итоге легко может получиться классическая “гонка”, когда процессы передерутся между собой.

Продавец1 (далее П1) “Мне нужен сок!” sok=true;
Соконосец (далее С) (просыпаясь) “О! Сок нужен, пошел”
П2 “Мне тоже сок нужен” sok=true;
П3 “Мне тоже!” sok=true;
C (принес сок), П1 – на тебе сок. sok=false;

П2 и П3 в печали.

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

П1 “Соку!” sok++;
C “Счас”
П2 “Соку!” sok++
П3 “Мне тоже два сока!” sok++;sok++;
С -П1 “на сок!” sok- -;
C (sok>0?) “О, еще надо!”
С – П2 “держи” sok- -;
C “и еще надо?”
С- П3 “велкам” sok- -;
C – и еще разок
С -п3 “пжлста” sok- -;
C “О, сока больше никому не надо, пойду спать”.

Код работает красиво, понятно и в принципе ошибок не должно быть. Программист закрывает задачу и приступает к следующей. В конце концов перед финальной сборкой проекта обычно начинается нехватка ресурсов и кто-то хитрый включает оптимизацию или просто меняет контроллер на многоядерный. И все. задача выше перестает работать как положенно. Причем что самое гадкое, иногда она работает как положено, а под отладчиком в 99% случаев она вообще ведет себя идеально. Программист плачет, рыдает и начинает следовать шаманским советам типа “объяви переменную как static или volatile”.

А что происходит в реальности?

Когда соконосец выполняет операцию sok- -; в реальности происходит следующее:

1. Берем во временную переменную значение sok
2. Уменьшаем значение временной переменной на 1
3. Записываем значение временной переменной в sok

А теперь вспоминаем, что у нас многопоточная (и может многоядерная) система. Потоки реально могут выполняться параллельно. И поэтому в реальности, а не под отладчиком, происходит следующее (или продавец и соконосец одновременно обратились к одной переменной)

С. Берем во временную переменную значение sok
П. Берем во временную переменную 2 значение sok;
С. Уменьшаем значение временной переменной на 1
П. Увеличиваем значение временной переменной 2 на 1.
П. Записываем значение временной переменной 2 в sok
С. Записываем значение временной переменной в sok

В результате в sok у нас совершенно не то значение, которое мы ожидали. Обнаружив данный факт, программист рвет на себе свитер с оленями, восклицая что-то типа “я же читал об этом, ну как я мог забыть!” и оборачивает код работы с переменной в обертку, которая запрещает многопоточный доступ к этой переменной. Обычно это связно с запретом переключения задач и прочими штуками. Запускает оптимизации – все работает отлично. Но тут приходит архитектор проекта (или главный технарь в studiovsemoe.com, то есть я) и дает программисту по башке, ибо все запреты и прочее очень сильно просаживают производительность и пускают под откос практически все, что завязано на временные промежутки.

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

В любой приличной ОС есть семафоры. В FreeRTOS есть два типа семафоров – “бинарный” и “счетный”. Все их отличие в том, что бинарный – это частный случай счетного, когда счетчик равен 1. Плюс бинарный очень опасно применять в высокоскоростных системах.

В чем преимущество семафоров против обычной переменной?

Во-первых, можно попросить планировщик задач не выделять ресурсов потоку, пока данный семафор не изменит свое состояние. То есть соконосец не будет больше терроризировать продавца “а нужен ли сок?”, а будет спать где-то в подсобке до тех пор, пока кому-то не понадобится сок.

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

В чем проблема с высокоскоростными системами и бинарными семафорами? В принципе ситуация полностью аналогична первому примеру про соконосцев, только перевернутой с ног на голову.

Представим себе, что контролером в макдак взяли тормознутого паренька (далее К). Его задача простая – подсчитать, сколько сока заказали.

П1-С “Сок!”
К – “О, сок заказали, надо нарисовать единичку!”
П2-С “Сок!”
П3-С “Соку!”
(все это время К, высунув язык, рисует единичку)
К – Так, единичку нарисовали, ждем следующего заказа.

Как понимаете, данные в конце дня совершенно не сойдутся. И именно поэтому у нас в студии запрещено использовать бинарные семафоры как класс – скорости контроллеров всегда разные, а ловить такие ошибки очень сложно.

Как работают счетные семафоры? Возьмем для примера все тот же макдак, отдел приготовления бутербродов (или как там одним словом называются гамбургеры с бигмаками?). С одной стороны есть куча продавцов, которые продают бигмаки. Бигмаки продаются по одному, по два или по десятку разом. В общем, не угадаешь. А с другой стороны есть делатель бигмаков. Он может быть одним, молодым и неопытным, а может быть матерым и быстрым. Или все сразу. Продавцу на это пофиг – ему нужен бигмак и кто его сделает ему все равно. В результате:

П1 “Нужен 1 бигмак” (ставит семафор в 1ку)
Д1 “Ок, я могу делать 1 бимак”. (молодой, оказался ближе, снимает семафор в 0)
П2 “Нужно 3 бигмака” (увеличивает семафор на 3)
Д2 “Ок, я могу сделать 1 бигмака” (следующим в очереди на ожидание. семафор в 2)
(тут приходит Д1)
Д1 “сделал бигмак, еще один могу сделать” (семафор 1)
Д2 “ок, я свой сделал, сделаю счас еще один”. Семафор 0
(приходит назад, он быстрый)
Д2 “Еще бигмаки надо? я подожду 10 тиков, если нет, то уйду”
Д1 “Все, сделал. Разбудите, как еще надо будет” (планировщик тормозит тред)
Д2 “Чего, не надо? ну я ушел. загляну через Нцать тиков”

В итоге бигмаки может делать один человек, а могут 10 – разница будет только в числе произведенных бигмаков в единицу времени. Ладно, хватит про бигмаки и макдональдсы, надо реализовывать все это в коде. Опять же берем плату и код из прошлого примера. У нас 8 светодиодов, которые мигают по разному, с разной скоростью. Вот пусть будет один сделанный “мырг” равен одному “бутерброду”. На плате есть пользовательская кнопка, поэтому сделаем так, что бы одно нажатие требовало 1 “бутерброд”. А если кнопку держим, что пусть требует по 5 “бутербродов в секунду”.

Где-нибудь в “глобальном” коде создаем семафор.

xSemaphoreHandle BigMac;

В коде треда StartThread инициализируем семафор

BigMac = xSemaphoreCreateCounting( 100, 0 );

То есть максимум мы можем заказать 100 бигмаков, а сейчас их надо 0

И изменим код бесконечного цикла на следующий

if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==GPIO_PIN_SET)
{
xSemaphoreGive(BigMac);
}
osDelay(200);

То есть если кнопка (а она на плате прицелена к PА0) нажата, то каждые 200мс мы выдаем один семафор/требуем бигмак.

И к каждому коду мигания светодиодиком добавим

xSemaphoreTake( BigMac, portMAX_DELAY);
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_15,GPIO_PIN_RESET);
...

То есть ждем семафора BigMac до опупения (portMAX_DELAY). Можно поставить любое число тиков или через макрос portTICK_PERIOD_MS задать число миллисекунд для ожидания.

Внимание! В коде я не стал вводить никаких проверок для повышения его читабельности.

Компилируем, запускаем. Чем дольше держим кнопку, тем больше светодиодиков мигает. Отпускаем – перестают. Но один, самый быстрый (и дальний в очереди) у меня не замигал – ему просто не хатает “заказов”. Ок, увеличиваю скорость до 50мс на каждый бутерброд. Теперь заказов хватает всем и все мигают. Отпускаешь кнопку – они некоторое время продолжают мигать, делая собранные заказы. Что бы было совсем хорошо, я разрешил заказывать бигмаков аж 60 тыщ (можно до unsigned long) и период заказа поставил 10мс.

Теперь все стало совсем красиво – нажал, светодиодики замигали. Чем дольше держишь кнопку, тем дольше мигают светодиодики после отпускания. Полная аналогия реальной жизни.

Что бы продолжить аналогию с реальной жизнью, вспомним, что это в макдаке всегда есть какие-то сборщики бутербродов. То есть продавец может не оборачиваясь, махнуть “надо бутерброд” и кто-нибудь его сделает. А если это обычная столовая в необеденное время? Там кассирша может хоть обмахаться – никто просто не увидит, ибо все кроме нее смотрят очередной сериал. Кассирше надо понять, чего хочет забредший в неурочное время посетитель и крикнуть что-то типа “Татьяна Васильевна, выйдите пожалуйста, тут суп налить надо”.

Для таких адресных случаев семафоры использовать нет смысла. В старый версиях FreeRTOS можно было просто через API разбудить задачу (“там суп надо”), а в новых появился вызов vTaskNotify (отличие только в передаваемом параметре “там борщ надо”), использование которого полностью аналогично семафорам, но адресно. По сравнению с обычными обещают дикое повышение производительности, но на данный момент мы масштабных тестов не проводили.

Есть еще один подвид семафоров – мутексы (mutex), но это те же самые бинарные семафоры. А рекурсивные мутексы – это счетные семафоры. Сделаны абсолютно так же, работают абсолютно так же, только “можно делать” состояние у них не “больше нуля”, как у обычных, а “только ноль”. Используются для разделения к ресурсам и переменным. Предлагаю придумать примеры применения самим.

Результат работы кода

Как обычно, полный код с обновлениями из поста можно найти тут http://kaloshin.ru/stm32/freertos/stage2.rar

STM32 и FreeRTOS. 1. Развлекаемся с потоками

Это черновая версия, финальная опубликована на хабре

IMG_0278

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

Вот задача для примера: у нас есть 4 выхода, на которых необходимо выводить импульсы разной длительности с разными паузами. Все, что у нас есть – это системный таймер, который считает в миллисекундах.

Усложняем задачу в духе “сам себя замучаю на ардуино”. Таймеры заняты другим, PWM не подходит, ибо не на всех ножках он работает, да и не загонишь его на нужные режимы обычно. Немного подумав, садимся и пишем примерно такой код


// инициализация
int time1on=500; // Время, пока выход 1 должен быть включен
int time1off=250; // Время, пока выход 1 должен быть выключен
unsigned int now=millis();
....
// где-то в цикле
if(millis()<now+time1on)
{
port1=ON;
}
else
{
port1=OFF;
if(millis()>now+time1on+time1off)
{
now=millis();
}
}

И так или примерно так для всех 4 портов. Получается приличная портянка на несколько экранов, но эта портянка работает и работает довольно быстро, что для микроконтроллера важно.

Потом внезапно программист замечает, что при каждом цикле дергается порт, даже если его состояние не меняется. Правит всю портянку. Потом число портов с такими же потребностями увеличивается в два раза. Программист плюет и переписывает все в одну функцию типа PortBlink(int port num).

Почти наступило счастье, но внезапно потребовалось что бы на каком-то порту вместе с управлением “на выход” что-то предварительно считывалось и уже на основе этого считанного управлялся порт. Программист снова матерится и делает еще одну функцию, специально под порт.

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

(типа реклама и восхваление) А все почему? Потому что изначально было принято неправильное решение о платформе. Если есть возможность, мы предлагаем навороченную платформу даже для примитивных задач. По опыту стоимость разработки и поддержки потом оказываются гораздо ниже. Вот и сейчас для управления 8мю выходами я возьму STM32F3, который может работать на 72МГц. (шепотом) На самом деле просто у меня под рукой демоплата с ним (смаил). Была еще с L1, но мы ее нечаянно использовали в одном из проектов.

Открываем STM32Cube, выбираем плату, включаем галочку около FreeRTOS и собираем проект как обычно. Нам ничего этакого не надо, поэтому оставляем все по умолчанию.

Что такое FreeRTOS? Это операционная система почти реального времени для микроконтроллеров. То есть все, что вы слышали про операционные системы типа многозадачности, семафоров и прочих мутексов. Почему FreeRTOS? Просто ее поддерживает STM32Cube ;-). Есть куча других подобных систем – та же ChibiOS. По своей сути они все одинаковые, только различаются командами и их форматом. Тут я не собираюсь переписывать гору книг и инструкций по работе с операционными системами, просто пробегусь широкими мазками по наиболее интересным вещам, которые очень сильно пмогают программистам в их нелегкой работе.

Ладно, буду считать что прочитали в интернете и прониклись. Смотрим, что поменялось

Где-то в начале

static void StartThread(void const * argument);

и после всех инициализаций

/* Create Start thread */
osThreadDef(USER_Thread, StartThread, osPriorityNormal, 0, configMINIMAL_STACK_SIZE);
osThreadCreate (osThread(USER_Thread), NULL);

/* Start scheduler */
osKernelStart(NULL, NULL);

И пустая StartThread с одним бесконечным циклом и osDelay(1);

Удивлены? А между тем перед вами практически 90% функционала, которые вы будете использовать. Первые две строки создают поток с нормальными приоритетом, а последняя строка запускает в работу планировщик задач. И все это великолепие укладывается в 6 килобайт флеша.

Но нам надо проверить работу. Меняем osDelay на следующий код

HAL_GPIO_WritePin(GPIOE,GPIO_PIN_8,GPIO_PIN_RESET);
osDelay(500);
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_8,GPIO_PIN_SET);
osDelay(500);

Компилируем и заливаем. Если все сделано правильно, то у нас должен замигать синий светодиодик (на STM32F3Discovery на PE8-PE15 распаяна кучка светодиодов, поэтому если у вас другая плата, то смените код)

А теперь возьмем и растиражируем полученную функцию для каждого светодиода.

static void PE8Thread(void const * argument);
static void PE9Thread(void const * argument);
static void PE10Thread(void const * argument);
static void PE11Thread(void const * argument);
static void PE12Thread(void const * argument);
static void PE13Thread(void const * argument);
static void PE14Thread(void const * argument);
static void PE15Thread(void const * argument);

Добавим поток для каждого светодиода
osThreadDef(PE8_Thread, PE8Thread, osPriorityNormal, 0, configMINIMAL_STACK_SIZE);
osThreadCreate (osThread(PE8_Thread), NULL);

И перенесем туда код для зажигания светодиода
static void PE8Thread(void const * argument)
{
for(;;)
{
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_8,GPIO_PIN_RESET);
osDelay(500);
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_8,GPIO_PIN_SET);
osDelay(500);
}
}

В общем все однотипно.

Компилируем, заливаем … и получаем фигу. Полную. Ни один светодиод не мигает.

Путем страшной отладки методом комментирования выясняем, что 3 потока работают, а 4 – уже нет. В чем проблема? Проблема в выделенной памяти для шедулера и стека.

Смотрим в FreeRTOSConfig.h

#define configMINIMAL_STACK_SIZE ((unsigned short)128)
#define configTOTAL_HEAP_SIZE ((size_t)3000)

3000 байт на все и каждой задаче 128 байт. Плюс еще где-то надо хранить информацию о задаче и прочем полезном. Вот поэтому, если ничего не делать, планировщик при нехватке памяти даже не стартует.

Судя по факам, если включить полную оптимизацию, то сам FreeRTOS возьмет 250 байт. Плюс на каждую задачу по 128 байт для стека, 64 для внутреннего списка и 16 для имени задачи. Считаем: 250+3*(128+64+16)=874. Даже до килобайта не дотягивает. А у нас 3 …

В чем проблема? Поставляемая с STM32Cube версия FreeRTOS слишком старая (7.6.0), что бы заиметь vTaskInfo, поэтому я зашел сбоку:

Перед и после создания потока я поставил следующее (fre – это обычный size_t)

fre=xPortGetFreeHeapSize();

Воткнул брекпоинты и получил следующие цифры: перед созданием задачи было 2376 свободных байт, а после 1768. То есть на одну задачу уходит 608 байт. Повтыкал еще. Получил цифры 2992-2376-1768-1160. Цифра совпадает. Путем простых логических умозаключений понимаем, что те цифры из фака взяты для какого-нибудь дохлого процессора, со включенными оптимизациями и выключенными всякими модулями. Смотрим дальше и понимаем, что старт шедулера отьедает еще примерно 580 байт.

В общем, принимаем для расчетов 610 байт на задачу с минимальным стеком и еще 580 байт для самой ОС. Итого в TOTAL_HEAP_SIZE надо записать 610*9+580=6070. Округлим и отдадим 6100 байт – пусть жирует.

Компилируем, заливаем и наблюдаем, как мигают все светодиоды разом. Пробуем уменьшить стек до 6050 – опять ничего не работает. Значит, мы подсчитали правильно 🙂

Теперь можно побаловаться и позадавать для каждого светодиодика свои промежутки “импульса” и “паузы”. В принципе, если обновить FreeRTOS или поколдовать в коде, то легко дать точность на уровне 0,01мс (по умолчанию 1 тик – 1мс).

Согласитесь, работать с 8ю задачами поодиночке гораздо приятней, чем в одной с 8ю одновременно? В реальности у нас в проектах обычно крутится по 30-40 потоков. Сколько было бы смертей программистов, если всю их обработку запихать в одну функцию я даже подсчитать боюсь 🙂

Следующим шагом нам необходимо разобраться с приоритетами. Как и в реальной жизни, некоторые задачи “равнее” остальных и им необходимо больше ресурсов. Для начала заменим одну мигалку мигалкой же, но сделанной неправильно, когда пауза делается не средствами ОС, а простым циклом.

То есть вместо osDelay() вставляется вот такой вот ужас.

for(int i=0;i<1000000;i++) { c++; }

Число циклов обычно подбирается экспериментально (ибо если таких задержек несколько, то куча головной боли в расчетах обеспечено). Эстеты могут подчитать время выполнения команд.

Заменяем, компилируем, запускаем. Светодиодики мигают по прежнему, но как-то вяло. Просмотр осциллографом дает понять, что вместо ровных границ (типа 50мс горим и 50мс не горим), границы стали плавать на 1-2мс (глаз, как ни странно, это замечает). Почему? Потому что FreeRTOS не система реального времени и может позволить себе такие вольности.

А теперь давайте поднимем приоритет этой задаче на один шажок, до osPriorityAboveNormal. Запустим и увидим одиноко мигающий светодиод. Почему?

Потому что планировщик распределяет задачи по приоритетам. Что он видит? Что задача с высоким приоритетом постоянно требует процессор. Что в результате? Остальным задачам времени на работу не остается.

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

Отсюда можно легко вывести первое правило программиста: если функции нечего делать, то отдай управление планировщику.

В FreeRTOS есть два варианта "подожди"

Первый вариант "просто подожди N тиков". Обычная пауза, без каких либо изысков: сколько сказали подождать, столько и ждем. Это vTaskDelay (osDelay просто синоним). Если посмотреть на время во время выполнения, то будет примерно следующее (примем что полезная задача выполняется 24мс):

... [0ms] - передача управления - работа [24ms] пауза в 100мс [124ms] - передача управления - работа [148ms] пауза в 100мс [248ms] ...

Легко увидеть, что из-за времени, требуемой на работу, передача управления происходит не каждые 100мс, как изначально можно было бы предположить. Для таких случаев есть vTaskDelayUntil. С ней временная линия будет выглядеть вот так

... [0ms] - передача управления - работа [24ms] пауза в 76мс [100ms] - передача управления - работа [124ms] пауза в 76мс [200ms] ...

Как видно, задача получает управление в четко обозначенные временные промежутки, что нам и требовалось. Для проверки точности планировщика в одном из потоков я попросил делать паузы по 1мс. На картинке можете оценить точность работы с 9ю потоками (про StartThread не забываем)

1ms

Думаю, для первой части достаточно 🙂

Как обычно, полный пакет проекта можно взять тут: http://kaloshin.ru/stm32/freertos/stage1.rar

Загадочный AES

В процессе обкатки модулей для супер-мега-экстра часов добрался до СС модулей от Texas Instruments. Документации куда больше, чем на nRF серию от Nordic, демоплатки тоже завелись без проблем.

Читаю документацию к SimplyciTI (это такой фирменный сетевой стек для этих чипов, сразу позволяет делать точки доступа, репитеры и прочее). Добираюсь до секурности – ведь никто не должен знать температуру за моим окном или включать-выключать свет в туалете (это одна из моих претензий к noolite)

There are 3 components of the encryption scheme that can be used to maintain security: the 128 bit key, a 32-bit Initialization Vector (IV) and a 32-bit counter. The key and the IV are fixed at build time.

Офигеть. Даже мне, далекому от криптографии, становится смешно. 32 бита “энтропии”, которые меняются по жесткому алгоритму … Ну ладно, что там еще

The scheme encrypts 64-bit blocks using an encryption scheme available in the public domain (XTEA: EXtended Tiny Encryption Algorithm). The 64-bit block is a concatenation of the 32-bit IV and a 32-bit counter. Changing the counter for each encryption guarantees a unique block for each encryption action. The key length used in the encryption is 128 bits.

Читаю дальше …

Because SimpliciTI does not support guaranteed delivery there must be a mechanism to synchronize the counters. Otherwise correct decryption could not occur. The synchronization is done by supplying a counter hint in the frame containing the encrypted payload. The hint is a single byte representing the least significant byte (lsb) of the 4 byte counter value used to encrypt the accompanying payload. It is sent in the clear over the air.

Вот так вот просто, берут и выдают последний байт счетчика в открытом виде в воздух. Круто, разом на треть уменьшают и без того маленький “ключ”

И как-то все. В задумчивости сижу, думаю, что не так.

Потом вспоминаю рекламные зазывалки.

The CC1110’s integrated AES-128 co-processor makes it ideal for wireless payment systems, since strong encryption can be done on the chip itself

Ну AES-128 это круто. Странно, что не написали в аппноуте, как до него добраться. Но счас почитаю оставшуюся документацию.

(пропущено несколько часов ползанья по докам и исходникам)

А нигде нету ни слова про AES вообще. Печаль. Может это из-за того, что я скачивал документацию с фирменного сайта TI, а там меня спрашивали, откуда я. Вот может под какие санкции попал. Пошел спрашивать интернет.

И тут открылось обычное. На более-менее приличных сайтах, которые не занимаются тупой перепечаткой, нет ни слова про AES. Вообще.

Вот такой вот AES. То он есть, то его сразу нет …

ЗЫ И именно поэтому я не люблю маркетологов, которые уцепившись за циферку 128, сразу прилепили к ней слева магические (в 2009 году) буквы AES.

Мега-часы. 2. Новые горизонты

Опытно-промышленное тестирование бутерброда из Arduino Leonardo и Arduino Wifi Shield показало, что wifi shield … В общем, очень плохо работает. Может тупо не запуститься или взять и повиснуть прямо на середине работы. А может завестись без каких-либо проблем и проработать сутки под нагрузкой.

В итоге wifi (да и вообще вся сетевая) часть переезжает на raspberry. Там с этим гораздо лучше просто по определению.

Следующее тестирование (в предидущих постах можно прочитать подробнее) показало, что первоначально выбранный контроллер nRF24 оказался тоже не очень хорошим. То ли китайцы чего-то не так распаяли, что ли у меня руки кривые, но дальность действия радиочасти оказалась ниже плинтуса. А я уже раскатал губу про выносные датчики и прочие штуки …

Но не одним nordic’ом полон мир. Тут же на замену был приобретен комплект разработчика на чипе MSP430+СС1100 от Texas Instruments производства нашей ТерраЭлектроники . Что бы дополнительно “замативировать” себя, взял еще СС430-Chronos. Настоящие умные часы, которые можно запрограммировать так, как тебе надо, а не просто менять циферблатики.

IMG_0269

Период прочтения документации принес очень много удовольствия: её просто на порядки больше, чем про nRF24. На первый взгляд кажется, что описано все. Вторым взглядом не смотрел еще.

Так как я с MSP430 не работал до этого момента, то из launchpad был вынут “крутой” проц и засунут простой.

IMG_0272

Первый шаг сделан – он замигал светодиодиком. Теперь осталось проверить мысль, хватит ли этому ресурсов заменить ардуинку в деле мигания дисплейчиком и считывания кнопок …

NRF24LE1 и NRF24LU1. Часть 2

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

Но все оказалось проще.

Во-первых, бравые норвежцы слишком перемудрились и сделали так, что все операции ввода-вывода через USB делаются специальным чипом. В принципе, в этом нет ничего плохого, за исключением того, что они позабыли дать доступ к идентификаторам USB напрямую. В итоге фиг вы чего сделаете без драйверов, и заставить LU1 представляться COM портом не получится.

А во-вторых, “the USB chip that these kits use is obsoleted from the supplier, and doesn’t have a driver for 64-bit Windows 7.”, а так же для всяких OS X и прочего.

И наконец, я решил посмотреть в последний SDK (20 апреля 2012 года) и найти там примеры работы с беспроводной частью. Нашел, скомпилировал (если чего, искать в gazel_examples). Там все просто – на “девайсе” что-то суют в P0, а на “хосте” это что-то вылазит оттуда. Очень удобно – на дивайс посадил щуп от генератора, а на хосте смотришь, когда сигналы перестанут приходить.

Условия у меня очень тяжелые.

Screenshot_2015-01-14-12-54-39

И это еще не все сетки, ибо пока я обжимаю телефон для снятия скриншота, наиболее дохлые исчезают. WiFi адаптер tp-link, который воткнут в “малинку”, имеет миниатюрную антенну, но тем не менее честно сообщает о наличии 38 сетей.

Итак, результаты. Результаты хреновые.
1) В пределах комнаты – на троечку. То есть если “дивайс” находится в центре комнаты, то еще нормально. Но если “дивайс” в одном углу комнаты, а “хост” – в другом, то уже идут потери “пакетов”.
2) В пределах квартиры – уверенная двойка. То есть обычная, не капитальная стенка – уже плохо. Две – не видят друг друга.
3) На открытом пространстве (у меня за окнами лес) – метров 30 берет.

Все вышеперечисленное ставит жирный крест на использовании данных контроллеров для меня. Буду использовать другие 🙂

NRF24LE1 и Raspberry Pi. Часть 1

В принципе, этот пост просто перепись моих исследований, ибо в русскоязычном сегменте сети есть только одна, много раз скопированная статья MaxMS (http://habrahabr.ru/post/210974/). В принципе, я довольно много подчерпнул оттуда.

Итак, у меня есть контроллеры на данном чипсете и есть желание сделать с ними что-либо полезное. Но прежде “деланья” чего-либо полезного, надо этот контроллер запрограммировать. Вместе с контроллером ко мне попал “официальный” программатор, но чего с ним можно делать, я даже не разбирался, потому что неспортивно.

Для начала я определил, какая именно версия nrf у меня.

Screen Shot 2015-01-13 at 12.26.57

Судя по плате (плюс я глазами пересчитал выводы), у меня 32х пиновая версия.

А значит, из этой таблички берем среднее

aadd6daf8ee7e68f19eac9fbc8471494

Теперь как программировать. Из той же статьи наиболее приемлемым для меня оказался вариант с Raspberry Pi. Во-первых, у него везде 3,3 вольта, а во вторых, из него легко получается мобильный комплекс – воткнул wifi и питание и все, комп для прошивки готов.

Результатом стал следующий ужас

IMG_0261

Для памяти
RESET и VDD – на 3,3
GND – на землю
FCSN – 24я ножка CE0
PROG – 18я ножка GPIO24
Остальное согласно SPI на малинке. MISO-MISO, MOSI-MOSI и так далее

Затем выкачиваю с гитхаба https://github.com/derekstavis/nrf24le1-libbcm2835 программатор и собираю его

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

# ./nrf24le1 read firmware
[nrf24le1] nrf24le1_init: Initializing nRF24LE1

Initiate programming
Number of bytes to read: 16384, max flash size 16384
[nrf24le1] uhet_read: read addr: 0x0xbeae07dd, pack header: 0x3 0x0 0x0, bytes read: 16384
:2000000002024D41808B004180890142808D0000458084345678901200E4FDFCC3ED9FECA4
:200020009E501D908055E4F0A3746CF090805574FFF5F011F645F070F30DBD00010C80DC7A
:2000400022E490808AF0A3F022FFFFA10BC291914FE4FF914FD2912280E7FF0208EA80E17B
:20006000FFFFFF020CEABB010689828A83E0225002E722BBFE02E32289828A83E49322BB28
:20008000010CE58229F582E5833AF583E0225006E92582F8E622BBFE06E92582F8E222E51A
:2000A0008229F582E5833AF583E49322BB010689828A83F0225002F722BBFE01F322F8BB92
:2000C000010DE58229F582E5833AF583E8F0225006E92582C8F622BBFE05E92582C8F22207
:2000E000C5F0F8A3E028F0C5F0F8E582158270021583E038F022A3F8E0C5F025F0F0E5823D
.....

Внутри что-то записано и это что-то не очень похоже на мусор. Но для меня это мусор, ибо я не в курсе, что там записано и для чего.

Теперь надо проверить, что прошивка работает. Читаем в test текущую фирмварь

./nrf24le1 read firmware test

Любым редактором ее портим, но по-своему (я просто записал посрединке немного байтиков со значениями 0х01-0х20) и заливаем назад и пофиг, что работать не будет.

./nrf24le1 write firmware test

И тут же читаем назад, но в другой фаил.

./nrf24le1 read firmware test1

А теперь ищем в test1 наши правки. Если они есть – значит “программатор” работает. У меня все заработало. Теперь можно и свою программу написать 🙂

Посмотрел на официальный SDK, покривился, забрал “правленный”, взял пример с дерганьем порта p0.0

#include <stdint.h>
#include <stdio.h>

//подключение необходимых функций:

#include "src/gpio/src/gpio_pin_configure.c"
#include "src/gpio/src/gpio_pin_val_clear.c"
#include "src/gpio/src/gpio_pin_val_set.c"

#include "delay.h"
#include "src/delay/src/delay_us.c"
#include "src/delay/src/delay_s.c"
#include "src/delay/src/delay_ms.c"

void main()
{
// мигаем портом P0_0
gpio_pin_configure(GPIO_PIN_ID_P0_0, // укажем необходимые параметры
GPIO_PIN_CONFIG_OPTION_DIR_OUTPUT |
GPIO_PIN_CONFIG_OPTION_OUTPUT_VAL_CLEAR |
GPIO_PIN_CONFIG_OPTION_PIN_MODE_OUTPUT_BUFFER_NORMAL_DRIVE_STRENGTH);

while(1)
{
gpio_pin_val_set(GPIO_PIN_ID_P0_0); //установка 1
delay_ms(500);
gpio_pin_val_clear(GPIO_PIN_ID_P0_0); //установка 0
delay_ms(500);

}

}

Пытаемся скомпилировать

# sdcc -Iinclude led_main.c
# ls -l
total 312
drwxr-xr-x 2 root root 4096 Jan 30 2014 include
-rw-r--r-- 1 root root 29083 Jan 13 13:14 led_main.asm
-rw-r--r-- 1 root root 850 Jan 13 13:13 led_main.c
-rw-r--r-- 1 root root 2382 Jan 13 13:14 led_main.ihx
-rw-r--r-- 1 root root 248 Jan 13 13:14 led_main.lk
-rw-r--r-- 1 root root 80283 Jan 13 13:14 led_main.lst
-rw-r--r-- 1 root root 26676 Jan 13 13:14 led_main.map
-rw-r--r-- 1 root root 1129 Jan 13 13:14 led_main.mem
-rw-r--r-- 1 root root 14900 Jan 13 13:14 led_main.rel
-rw-r--r-- 1 root root 80283 Jan 13 13:14 led_main.rst
-rw-r--r-- 1 root root 48610 Jan 13 13:14 led_main.sym
-rw-rw-rw- 1 root root 3087 Jan 31 2014 libs.h
drwxr-xr-x 22 root root 4096 Jan 30 2014 src

Судя по содержимому, мне нужно залить lem_main.ihx. Только формат ему поменять. Заодно и выкачиваю hex2bin (http://hex2bin.sourceforge.net)

# ./hex2bin -p 00 led_main.ihx
hex2bin v1.0.12, Copyright (C) 2012 Jacques Pelletier & contributors

Lowest address = 00000000
Highest address = 000003C2
Pad Byte = 0
8-bit Checksum = FB

Ну теперь полученный led_main.bin можно залить в контроллер.

# ./nrf24le1 write firmware led_main.bin

А теперь посмотрим осциллографом на вывод P0.0

500_500

Как видим, все согласно программе: 500мс включено и 500мс выключено.

Как говорится, ура. Обязательная программа для любого микроконтроллера работает как надо.

Но вообще-то у меня немного другая задача: связать два микроконтроллера между собой по радиоканалу и оценить, насколько он работоспособен в условиях обычной многоэтажки, где диапазон 2,4ГГц забит напрочь и намертво WiFi.

Но это тема следующего поста. Или даже через одного 🙂

Развлекалочка. Или контроллер сварочного аппарата

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

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

Для проверки “а пойдет” рисую схему

schema

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

pcb_example

Потом посмотрел, как оно будет выглядеть “в реале”

3dmodel

Вроде ничего, гламурно даже. Правда, в процессе пайки мне стало лень паять столько резисторов и я взял резисторную сборку. Ну и немного поменял расположение компонентов, ибо макетная плата у меня “не ровная”. В общем, получилось вот так

overview

Теперь добавляем щепотку “магии”

#include <EEPROM.h>
int pinAch = 2; //Энкодер вывод А включен на interrupt 0 (D2)
int pinBch = 3; // Энкодер вывод Б - D3

int pinled = 5; // Куда включен первый сетодиодик
int ledcount = 8; // Сколько светодиодиков

int pinButton = A5; // куда включена кнопка "пуск"
int pinRelay = A0; // Куда подключено реле

int impulsTime = 50; // Время включения реле по умолчанию (50 мс)
int relayShift = 2; // Время реакции реле
unsigned long bl; // переменная для хранения состояния "мигающего" светодиода
unsigned long dr; // переменная для антидребезга энкодера
int out=0; // мы бабахнули?

void setup() {                
  Serial.begin(9600);

  pinMode(pinAch, INPUT_PULLUP);  
  pinMode(pinBch, INPUT_PULLUP);

  pinMode(pinled, OUTPUT);
  pinMode(pinled+1, OUTPUT);
  pinMode(pinled+2, OUTPUT);
  pinMode(pinled+3, OUTPUT);
  pinMode(pinled+4, OUTPUT);
  pinMode(pinled+5, OUTPUT);
  pinMode(pinled+6, OUTPUT);
  pinMode(pinled+7, OUTPUT);

  pinMode(13, OUTPUT); // светодиодик, впаянный в ардуинку

  pinMode(pinButton, INPUT_PULLUP);
  pinMode(pinRelay, OUTPUT);

  digitalWrite(pinled, HIGH); // все светодидиоды включить для проверки
  digitalWrite(pinled+1, HIGH);
  digitalWrite(pinled+2, HIGH);
  digitalWrite(pinled+3, HIGH);
  digitalWrite(pinled+4, HIGH);
  digitalWrite(pinled+5, HIGH);
  digitalWrite(pinled+6, HIGH);
  digitalWrite(pinled+7, HIGH);

  digitalWrite(pinRelay,LOW); // для безопасности

  dr=millis();
  bl=millis();
  if(EEPROM.read(0)==1)
    {
      impulsTime=EEPROM.read(1);
    }
  attachInterrupt(0, encoderClick, RISING);
}


void loop() {
  // Зажигаем светодиодики в зависимости от уровня impulsTime
  for(int i=pinled;i<(pinled+ledcount);i++)
  {
    int s=(i-pinled+1)*10; // время импульса для зажигания светодиода
    // то есть для 1 светодиода 10, для 2 - 20 и так далее

    if((impulsTime-s)<0 && (impulsTime-s)>-10) // этот светодиодик промежуточный, поэтому пусть мигает.
    {
      if((millis()-bl)<500)
      {
        digitalWrite(i,HIGH);
      }
      else
      {
        digitalWrite(i,LOW);
        if((millis()-bl)>1000)
        {
          bl=millis();
        }
      }
    }
    else
    {
      if(s<=impulsTime)
      {
        digitalWrite(i,HIGH);
      }
      else
      {
        digitalWrite(i,LOW);
      }
    } 
  }
  
  if(digitalRead(pinButton)==LOW && out==0) // оппа, кнопку нажали и еще не бабахали
    {
      delay(100); // ждем 100мс - вдруг рука дрогнула
      if(digitalRead(pinButton)==LOW) // точно нажата?
        {
          digitalWrite(13,HIGH);
          digitalWrite(pinRelay,HIGH);
          out=1;
          delay(impulsTime+relayShift);
          digitalWrite(13,LOW);
          digitalWrite(pinRelay,LOW);
          out=1;
          EEPROM.write(0, 1); // ставим флаг
          EEPROM.write(1, impulsTime);
        }
    }
    
  if(digitalRead(pinButton)==HIGH && out==1) // Кнопку отпустили после бабханья
    {
      delay(100); // ждем 100мс - вдруг рука дрогнула
      if(digitalRead(pinButton)==HIGH) // до сих пор отпущена?
        {
          out=0; // Хорошо, можно еще раз бабахнуть
        }
    }


//  Serial.println(impulsTime);
//  delay(300);
}


// обработка энкодера
void encoderClick(){
  if((millis()-dr)>10) // От предидущего прерывания прошло больше 10мс? если нет, то это дребезг
  {
    dr=millis();
    int valA = digitalRead(pinAch);
    int valB = digitalRead(pinBch);

    if (valA != valB){
      impulsTime--;
    }
    else{
      impulsTime++;
    }

    if(impulsTime < 5) {
      impulsTime=5;
    }

    if(impulsTime > 100) {
      impulsTime=100;
    }
  Serial.println(impulsTime); 
  }
}

И проверяем, насколько полученное устройство соответствует заявленным мной же спецификациям

Импульс в 20 миллисекунд

20ms

В 50

50ms

и 75, что бы не было ровных чисел.

75ms

Теперь осталось только устроить ресурсные испытания релюшке и узнать, сколько времени она выдержит в таком режиме. Но это уже не так интересно, ибо стоимость релюшки около 100 рублей и перепаять ее – дело буквально пяти минут.

Задача выполнена быстро и с перерасходом ресурсов, как и положено для такого класса задач 🙂

Мега-часы. 1. Просто часы, но с WiFi

Занимаясь на выходных разборкой и сортировкой скопившегося имущества, практически внезапно обнаружил давно забытые детали. Ну как забытые … я конечно помнил, что они где-то есть, но уж очень давно не держал их в руках. Ну и плюс ко мне в руки попали платы на 8051 микроконтроллере, со встроенным приемопередатчиком. Просто так баловаться не интересно, поэтому необходимо совместить приятное с полезным.

Скажу сразу, просто еще одни часы делать мне не интересно – один раз я уже делал аж 2 года назад, до сих пор работают у тещи. Внутри ардуинка и sure 3208

20131113_165232

Что у нас из приятного образуется? Во-первых, более большой и цветной индикатор.

В старых часах был одноцветный (только красный) индикатор, в котором была кучка светодиодов, расположенных 32 рядами по 8 штук. В новых будет аналогичная матрица, только умеющая уже 3 цвета и с матрицей светодиодов 64 на 16. Из-за размера светодиодов (в старой 5мм, в новой 3мм) разница в размерах не очень большая.

Во-вторых, использованная в первых часах микросхема DS1307 обладает небольшим уводом времени. Примерно минуту в неделю. Нет, я там приделал кнопки для коррекции времени, но это же несерьезно в 21м-то веке. Ловить всякие радиосигналы точного времени для меня безсмысленно, ибо лень. А вот подцепиться к WiFi и сбегать за точным временем в интернет к специализированному серверу – вот это да, уже стоит подумать.

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

Итак, пока часы. Схемы рисовать не буду, потому что 99% всего будет состоять из соединения проводками.

Для начала берем 2 индикатора sure 3216 и соединяем их приложенными к ним же проводами. Потом берем arduino leonardo и wifi shield. Все родное, итальянское, поэтому соединяем все в кучу. Достаем из пакетика мастеркитовский MP1095 и опять же соединяем проводками с ардуинкой. Все подписанное к подписанному (SDA к SDA, SCL к SCL и так далее).

И наконец соединяем полученный бутерброд из 3216 к ардуинке. На пины А0-А3. В нижеприведенном куске кода любой ардуинщик разберется, что куда подключается.

//Analog In 0 as PF7
//Analog In 1 as PF6
//Analog In 2 as PF5
//Analog In 3 as PF4
ht1632c ledMatrix = ht1632c(&PORTF, 7, 6, 5, 4, GEOM_32x16, 2);
ht1632c::ht1632c(const uint8_t data, const uint8_t wr, const uint8_t clk, const uint8_t cs, const uint8_t geometry, const uint8_t number)

И теперь добавляем магию, сиречь программирование. Я не стал сильно раздумывать, поэтому тупо собрал в одну кучу примеры общения с RTC, с NTP, c WiFi и с индикатором.

#include <ht1632c.h>
#include <Wire.h>
#include <RealTimeClockDS1307.h>
#include <SPI.h>
#include <WiFi.h>
#include <WiFiUdp.h>

ht1632c dm = ht1632c(&PORTF, 7, 6, 5, 4, GEOM_32x16, 2);

int status = WL_IDLE_STATUS;

char ssid[20] = "multik";  //  your network SSID (name)
char pass[20] = "MyVerySecretPassword";       // your network password
int timeshift=3; // GMT offset

unsigned int localPort = 2390;      // local port to listen for UDP packets

IPAddress timeServer(129, 6, 15, 28); // time.nist.gov NTP server

const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message

byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
WiFiUDP Udp;

char clock[4];
int clockset=0; // does need to set clock from NTP?
int clockprocess=0; // we start asking clock server?

void setup() {  
  Serial.begin(9600);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for Leonardo only
  }
  // check for the presence of the shield:

  while (WiFi.status() == WL_NO_SHIELD)
    Serial.println("NOWIFI");

  String fv = WiFi.firmwareVersion();
  if ( fv != "1.1.0" )
    Serial.println("FIRM");

  status = WiFi.begin(ssid, pass); // start connecting

  dm.clear();
  dm.setfont(FONT_8x16B);

  RTC.switchTo24h();
}

void loop()
{
  if(clockset==0)
  {
    //Serial.println("Setting clock");
    if(status==WL_CONNECTED) // ok, we online
    {
      //Serial.println("WiFi con");
      if(clockprocess==0)
      {
        Udp.begin(localPort);
        sendNTPpacket(timeServer); // send an NTP packet to a time server
        clockprocess=1;
      }
      else
      {
        if ( Udp.parsePacket() ) 
        {
          //Serial.println("packet received");
          Udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer

          //the timestamp starts at byte 40 of the received packet and is four bytes,
          // or two words, long. First, esxtract the two words:

          unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
          unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
          // combine the four bytes (two words) into a long integer
          // this is NTP time (seconds since Jan 1 1900):
          unsigned long secsSince1900 = highWord << 16 | lowWord;
          //Serial.print("Seconds since Jan 1 1900 = " );
          //Serial.println(secsSince1900);

          // now convert NTP time into everyday time:
          // Serial.print("Unix time = ");
          // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
          const unsigned long seventyYears = 2208988800UL;
          // subtract seventy years:
          unsigned long epoch = secsSince1900 - seventyYears;
          // print Unix time:
          //Serial.println(epoch);


          // print the hour, minute and second:
          //Serial.print("The UTC time is ");       // UTC is the time at Greenwich Meridian (GMT)
          //Serial.print((epoch  % 86400L) / 3600); // print the hour (86400 equals secs per day)
          int h=(epoch  % 86400L) / 3600;

          RTC.setHours(h+timeshift);
          //Serial.print(h);Serial.print(':');
          //if ( ((epoch % 3600) / 60) < 10 ) {
          // In the first 10 minutes of each hour, we'll want a leading '0'
          //Serial.print('0');
          //}
          int m=(epoch  % 3600) / 60;

          //Serial.print((epoch  % 3600) / 60); // print the minute (3600 equals secs per minute)
          RTC.setMinutes(m);
          //Serial.print(m);Serial.print(':');
          //if ( (epoch % 60) < 10 ) {
          // In the first 10 seconds of each minute, we'll want a leading '0'
          //Serial.print('0');
          //}
          int s=epoch % 60;
          //Serial.println(s);
          //Serial.println(epoch % 60); // print the second
          RTC.setSeconds(s);
          RTC.setClock();
          clockset=1;
        }
      }
    }
    //    else
    //    {
    //      Serial.println("WiFi NOT con");
    //    }
  }
  else
  {
    // Serial.println("Clock already setted");
  }
  int x=0;
  int clr=GREEN;
  delay(1000);
  RTC.readClock();
  sprintf(clock, "%02d", RTC.getHours()); // RTC.getMinutes(),RTC.getSeconds());

  dm.setfont(FONT_8x16B);
  dm.putchar(x,0,clock[0],GREEN,0,BLACK);
  dm.putchar(x+9,0,clock[1],GREEN,0,BLACK);  

  // first :
  dm.plot(x+18,4,clr);
  dm.plot(x+18,4+1,clr);
  dm.plot(x+18+1,4,clr);
  dm.plot(x+18+1,4+1,clr);
  dm.plot(x+18,4+4,clr);
  dm.plot(x+18,4+1+4,clr);
  dm.plot(x+18+1,4+4,clr);
  dm.plot(x+18+1,4+1+4,clr);

  sprintf(clock, "%02d", RTC.getMinutes()); //,RTC.getSeconds());
  dm.putchar(x+21,0,clock[0],GREEN,0,BLACK);
  dm.putchar(x+30,0,clock[1],GREEN,0,BLACK);
  // second :
  dm.plot(x+39,4,clr);
  dm.plot(x+39,4+1,clr);
  dm.plot(x+39+1,4,clr);
  dm.plot(x+39+1,4+1,clr);
  dm.plot(x+39,4+4,clr);
  dm.plot(x+39,4+1+4,clr);
  dm.plot(x+39+1,4+4,clr);
  dm.plot(x+39+1,4+1+4,clr);

  sprintf(clock, "%02d", RTC.getSeconds());
  dm.putchar(x+42,0,clock[0],GREEN,0,BLACK);
  dm.putchar(x+51,0,clock[1],GREEN,0,BLACK);

  dm.sendframe();
  //Serial.println(clock);

}

// send an NTP request to the time server at the given address
unsigned long sendNTPpacket(IPAddress& address)
{
  //Serial.println("1");
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  //Serial.println("2");
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;

  //Serial.println("3");

  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  //Serial.println("4");
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  //Serial.println("5");
  Udp.endPacket();
  //Serial.println("6");
}

В итоге получилось вот это

015dbb46cc547788abd94e57ccc08f1ec8fa680f70

И видео

Что получилось в итоге? Часы запускаются и пытаются подключиться к WiFi с заранее заданным SSID и паролем. Как только им это удается, то они запрашивают NTP сервер о точном времени и как только получают ответ, обновляют соответствующие значения в RTC. А пока все это крутится, то часы просто берут уже сохраненное ранее время и показывают его.

Просто и дешево. Но для дальнейшей работы необходимо немного расширить функционал и научиться обрабатывать ошибки. К примеру, если WiFi есть, а интернета в нем нет и так далее. Ну и предусмотреть … скажем так, подключение внешних устройств, в качестве которых будет как минимум одна Raspberry Pi плата. Опять же, фотодиод для замера освещенности надо поставить …

В общем, есть что писать в следующем посте 🙂

Карпутер. 5. Пора программировать!

Что-то давно ничего не писал. Наверное, новогодние праздники помешали. Но алкомарафон закончился, поэтому неплохо бы вернуться к нашему контроллеру.

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

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

Возьмем наш первоначальный алгоритм. Если напряжение поднялось больше 13В, то замкнуть реле ближнего света. Где мы тут можем ошибиться? Да везде – от подбора резисторов для делителя напряжения до распайки реле и неправильного подсоединения. Поэтому сокращаем возможность ошибки до минимума: вместо делителя напряжения бортовой сети воспользуемся обычным переменным резистором, а вместо реле – обычным светодиодиком. Как подключать – я уже писал в предидущем посте. Но если у вас, как и у меня, плата discovery, то там уже есть светодиодики (а значит, вероятность ошибки снижается еще сильнее).

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

01a9541850ca680e486975cc6dcf78e69f85d7241d

Как видно из фотографии, я подключил переменный резистор к GND и 3V, а вывод движка резистора – на PA1. В итоге даже в самом плохом случае, когда я выверну резистор “до упора”, на ножку контроллера попадут безопасные 3 вольта.

Теперь пора описать, где и как писать код. Открываем сгенерированный пару постов назад код и находим в левой панели фаил main.c. В нем ищем кусок кода while(1), как показано на картинке

p1

Вот все, что будет расположено между фигурными скобочками и будет выполняться бесконечно (для тех. кто знаком с ардуиной, это void loop()). Писать надо на языке С, обучать которому я тоже тут не буду.

Еще одно отступление – как управлять выводами. У ардуины есть кошмар под названием port mapping (это когда при смене контроллера все назания ножек меняются и программы перестают работать), на stm тоже до недавнего времени такой кошмар был. Именно был, сейчас все проще.

Итак, если вы посмотрите на микроконтроллер, что обнаружите, что ножки нумеруются от PA0 до PF15. Все функции используют следущую нумерацию ножек – Блок от А до F и номер ножки в блоке.

Переведение ножки PC12 в “высокое” состояние

HAL_GPIO_WritePin(GPIOC,GPIO_PIN_12,GPIO_PIN_SET);

А ножку PE15 – в “низкое”

HAL_GPIO_WritePin(GPIOE,GPIO_PIN_15,GPIO_PIN_RESET);

Выдаем на 1й канал ЦАП значение переменной s

HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, s);

И мной написанная (или подправленная) функция для чтения значения АЦП

static int GetADCValue(uint32_t Channel,uint32_t Count)
{
int val = 0;
ADC_ChannelConfTypeDef sConfig;
sConfig.Channel=Channel;
sConfig.Rank=1;
sConfig.SamplingTime=10;
HAL_ADC_ConfigChannel(&hadc,&sConfig);

for(int i = 0; i < Count; i++) { HAL_ADC_Start(&hadc); HAL_ADC_PollForConversion(&hadc,1); val += HAL_ADC_GetValue(&hadc); } return val / Count; }

Как видно из кода, работает только с ADC1 (ссылки hadc).

В посте описать все невозможно, поэтому рекомендую заглянуть в "Firmware", которую скачал STMCube, и посмотреть там примеры.

Итак, вот мой код

if(GetADCValue(2,3)>2048)
{
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_15,GPIO_PIN_SET);
}
else
{
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_15,GPIO_PIN_RESET);
}

В чем его суть? Если значение, которое прочитали из АЦП, больше 2048, то перевести ножку PE15 на высокий уровень. Если меньше, что наоборот, на низкий.

В чем магия? Магия в двух вещах. Первое, это значение АЦП. Оно может быть от 0 до 4095 (3В на ножке). Как им распорядиться, решать вам. А второе - ножка PE15 на плате подключена с зеленому светодиоду.

Компилируем, заливаем (иконка с буковками LOAD во втором ряду) и наслаждаемся своей первой программой.

Сам код (полностью готовый и скомпилированный) можно взять отсюда http://multik.org/carsim/carputer.rar

Что дальше? А дальше пишем, отлаживаем и снова пишем. Пока не будет готово то, что требовалось изначально.

Желаю удачи в написании своих проектов!

А я чуть попозже напишу о том, как перенести написанное в "железо". Печатные платы и прочие радости радиогубителей 🙂