Тестовой платформой стала давно лежащая без дела STM Nucleo F030, поддерживаемая этой платформой. О том, как зарегистрироваться и начать первый проект, есть много хороших туториалов, об этом не будем. Перейдем сразу к интересному.
Данная плата не содержит слишком много периферии «на борту». Светодиод и кнопка – вот и все богатство. Что ж, первый проект – классический «Hello world» из мира микроконтроллеров – помигать светодиодом. Вот код:
#include "mbed.h"
DigitalOut myled(LED1);
int main() {
while(1)
{
wait_ms(500);
myled = myled ^ 1;
}
}
Красиво ведь, нет? В даташит контроллера действительно даже заглядывать не нужно!
Кстати, классический «Hello world» тоже доступен сразу же из коробки:
#include "mbed.h"
Serial pc(USBTX, USBRX);
int main() {
pc.printf("Hello world\n\r");
while(1)
{
}
}
Ведь правда здорово? «Из коробки» у нас консоль, в которой работает стандартный сишный принтф? Порт, кстати, тоже появляется сразу при подключении платы к компу, вместе с виртуальным диском, на который нужно просто скопировать собранный бинарник – и плата сама перезагрузится.
Но вернемся к мигающему светодиоду. Компилим, загружаем на плату – мигает! Но как-то слишком быстро, явно не раз в секунду, а минимум раз 5… Упс…
Немного отвлекусь на упомянутые в заголовке «дырявые абстракции». Про этот термин я прочитал у Джоэля Спольски. Вот его цитата: «Если я обучаю программистов C++, было бы здорово, если бы мне не нужно было рассказывать им про char* и арифметику указателей, а можно было сразу перейти к строкам из стандартной библиотеки темплейтов. Но в один прекрасный день они напишут «foo»+«bar», и возникнут странные проблемы, а мне придётся всё равно объяснить им, что такое char*. Или они попытаются вызвать функцию Windows с параметром типа LPTSTR и не смогут, пока не выучат char* и указатели и Юникод и wchar_t и хедерные файлы TCHAR — все то, что просвечивает через дырки в абстракциях.»
Итак, как бы парадоксально это ни было, закон дырявых абстракций не позволяет вот так вот взять и начать программировать микроконтроллер (даже если это самый простой «hello world» из трех строчек), не заглядывая в его даташит и не имея представления о том, что же у микроконтроллера внутри, а также о том, что стоит за всеми этим классами в абстракции. Вопреки обещаниям маркетологов…
Итак, тяжело вздохнув, начинаем искать. Будь у меня перед глазами не абстракция, а полноценная среда разработки для контроллера, было бы понятно, куда копать. Но вот куда копать в случае приведенного выше кода? Если даже содержимое файла mbed.h абстракция не позволяет увидеть?
К счастью, сама библиотека mbed – c открытым кодом, и ее можно скачать и посмотреть, что же там внутри. Оказалось, опять же к счастью, что сквозь абстракцию для программиста вполне доступны все регистры микроконтроллера, хоть они явно и не объявлены. Уже лучше. Например, можно проверить, что у нас с тактовой частотой, запустив вот это (для чего пришлось и в даташиты позаглядывать, и сам mbed изрядно покопать):
RCC_OscInitTypeDef str;
RCC_ClkInitTypeDef clk;
pc.printf("SystemCoreClock = %d Hz\n\r", HAL_RCC_GetSysClockFreq());
HAL_RCC_GetOscConfig(&str);
pc.printf("->HSIState %d\n\r", str.HSIState);
pc.printf("->PLL.PLLState %d\n\r", str.PLL.PLLState);
pc.printf("->PLL.PLLSource %d\n\r", str.PLL.PLLSource);
pc.printf("->PLL.PLLMUL %d\n\r", str.PLL.PLLMUL);
pc.printf("->PLL.PREDIV %d\n\r", str.PLL.PREDIV);
pc.printf("\n\r");
HAL_RCC_GetClockConfig(&clk, &flat);
pc.printf("ClockType %d\n\r", clk.ClockType);
pc.printf("SYSCLKSource %d\n\r", clk.SYSCLKSource );
pc.printf("AHBCLKDivider %d\n\r", clk.AHBCLKDivider );
pc.printf("APB1CLKDivider %d\n\r", clk.APB1CLKDivider );
С частотой оказалось все хорошо, она была 48 МГц, как и было задумано.
Что ж, копаем дальше. Пробуем вместо wait_ms() подключить таймер:
Timer timer;
timer.start();
while(1) {
myled ^= 1;
t1 = timer.read_ms();
t2 = timer.read_ms();
while (t2 - t1 < 500)
{
t2 = timer.read_ms();
}
}
Ведь красиво же, нет? Вот только светодиод по-прежнему мигает раз в 5 быстрее желаемого…
Ладно, копаем еще глубже и смотрим, что же у нас скрывается за волшебным таймером, который можно, как обещает документация, создавать в любых количествах, независимо от того, сколько аппаратных таймеров у микроконтроллера. Круто, ага…
А скрывается за ним в случае STM F030 аппаратный таймер контроллера TIM1, запрограммированный так, чтобы отсчитывать микросекундные тики. А на этой основе уже построено все остальное.
Так, уже горячо. Помните, я говорил, что все регистры доступны? Смотрим следующее:
pc.printf("PSC: %d\n\r", TIM1->PSC);
Вуаля! Это ставит точку в расследовании того, кто виноват: на консоль отправляется число 7. В регистре PSC (писец? неужели просто совпадение?) находится значение, при достижении которого таймер сгенерирует прерывание и начнет считать заново. А на это прерывание и повешен микросекундный счетчик. И чтобы при частоте в 48 МГц прерывание происходило раз в микросекунду, там должно быть совсем не 7, а 47. А 7 туда попало, скорее всего, на самом первом этапе загрузки микроконтроллера, т.к. он запускается на частоте 8 МГц, а потом перенастраивает PLL так, чтобы частота умножилась на 6, дав желаемые 48 МГц. И похоже, что таймер инициализируется слишком рано…
Кто виноват, понятно. Но что же делать? Раскопки фреймворка привели к следующим цепочкам вызова:
Первая: SetSysClock_PLL_HSI() -> HAL_RCC_OscConfig(), HAL_RCC_ClockConfig() -> HAL_InitTick() – при изменении частоты вызвать функцию, настраивающую микросекундный тик.
Вторая: HAL_Init() -> HAL_InitTick() -> HAL_TIM_Base_Init() -> TIM_Base_SetConfig() -> TIMx->PSC – вызываемая из одной из самых-самых глобальных функций HAL_InitTick записывает значение в регистр PSC необходимое значение, в зависимости от текущей тактовой частоты…
Что осталось для меня загадкой – так это то, что именно для семейства STM F0 вторая цепочка не вызывается. Я не нашел ни одного вызова функции HAL_Init()!
Более того, если заглянуть в реализацию HAL_InitTick(), то в самом ее начале будут следующие строчки:
static uint32_t ticker_inited=0;
if(ticker_inited)return HAL_OK;
ticker_inited=1;
Вызвать эту функцию получится ровно один раз. То есть вызвать-то ее можно сколько угодно раз, но все последующие вызовы она не будет делать ничего. И то, что фреймворк вызывает ее в случае изменения тактовой частоты, оказывается бесполезным…
Пересобирать библиотеку мне было лень, поэтому исправление обрело вот такой вид: первой строчкой в функции main стало вот это:
TIM1->PSC = (SystemCoreClock / 1000000) - 1;
После этого светодиод наконец-то стал мигать с правильной частотой 1 Гц…
Интересно, на каком этапе бы остановился гипотетический кто-то, кого маркетологи mbed убедили попробовать ставшее столь легким программирование микроконтроллера с нуля? Если с микроконтроллерами он до этого не сталкивался?
Ладно, если я еще не сильно утомил, движемся дальше. Мигать светодиодом – это хорошо, но неинтересно. Хочется чего-то большего. Из бОльшего нашелся датчик температуры DS1820, да и готовая библиотека для него нагуглилась. Простая в использовании, скрывающая всю сложность работы с этим датчиком внутри. Все, что надо – это написать что-то вроде
#include "DS1820.h"
DS1820 probe[1] = {D4}; // D4 – имя пина, к которому подключен датчик
probe[0].convert_temperature(DS1820::all_devices);
float temperature = probe[0].temperature('c');
Как вы думаете, что произошло после компиляции и запуска? Правильно. Оно не заработало :)
Дырявые абстракции, да. Что ж, нас ждет еще одно увлекательное исследование внутренностей mbed, похоже. И подробностей работы самого датчика.
Датчик весьма интересный. Со своим цифровым интерфейсом. По одной линии, т.е. работает в режиме полудуплекса. Контроллер инициирует обмен данных, датчик посылает последовательность бит в ответ. Специально для таких случаев фреймворк mbed имеет класс DigitalInOut, при помощи которого можно у GPIO пина на лету менять направление его работы. Как-то так:
bool DS1820::onewire_bit_in(DigitalInOut *pin) {
bool answer;
pin->output();
pin->write(0);
wait_us(3);
pin->input();
wait_us(10);
answer = pin->read();
wait_us(45);
return answer;
}
Контроллер посылает импульс «1 -> 0 -> 1» датчику, что является для него сигналом послать в ответ один бит. Что же тут может не работать? Вот кусочек даташита датчика:
Как можно заметить, после того, как мы послали импульс датчику, для того, чтобы прочитать бит, у нас есть целых 15 микросекунд.
Подключаем осциллограф и видим, что датчик вполне себе посылает биты, как указано в даташите. Но вот контроллер читает мусор. В чем может быть причина?
Не буду много писать о том, как я пришел к тому, чтобы выполнить вот этот код:
pin->output();
t1 = timer.read_us();
pin->input();
t4 = timer.read_us();
pc.printf("Time: %d us\n\r", t4-t1);
Ну что, кто угадает, не читая дальше, сколько времени на микроконтроллере с тактовой частотой 48 МГц занимает изменение направления работы одного пина GPIO (вообще-то это ОДНА запись в регистр, если что), если его делать при помощи фреймворка mbed, написанного на С++ с использованием слоя, независимого от железа?
13 микросекунд.
А чтобы прочитать ответ датчика, у нас есть целых 15. И даже если совсем убрать wait_us(10) из кода выше, команда answer = pin->read(); тоже занимает некоторое количество времени, бОльшее, чем 2 микросекунды. Чего достаточно, чтобы не успеть прочитать ответ.
К счастью, послать импульс на STM оказалось возможным и без изменения направления работы GPIO. В режиме Input при подключении встроенного PullDown резистора эффект тот же. И еще раз к счастью, вызов pin->mode(PullUp) вместо pin->input() требует всего 6 микросекунд.
Чего оказалось достаточно, чтобы успеть прочитать ответ.
И напоследок еще одна дырка в абстракции. После того, как DS1820 заработал, следующим попавшим под руку датчиком стал DHT21, датчик, меряющий и температуру, и влажность. Тоже с 1-wire интерфейсом, но на этот раз кодирующий ответ длительностью импульсов. Казалось бы, очевидным решением будет повесить эту линию на GPIO input interrupt, что позволит легко измерять продолжительность импульсов. И даже класс в mbed для этого есть. И он даже работает так, как задокументировано.
Но проблема в том, что датчик также работает в полудуплексе. И чтобы он начал передавать данные, ему нужно послать импульс «1 -> 0 -> 1» как в примере выше.
И вот тут mbed этого сделать не позволяет. Либо ты объявляешь порт как DigitalInOut, но тогда не можешь использовать прерывания. Либо объявляешь порт как InterruptIn, но тогда не можешь ничего на него посылать. Трюк с PullDown, к сожалению, здесь не прокатил, т.к. датчик имеет встроенный PullUp, и встроенного PullDown микроконтроллера недостаточно, чтобы притянуть пин к 0.
Пришлось в результате делать гораздо менее красивый цикл опроса линии. Все, конечно, заработало и так, но красиво сделать не получилось. Что не представляло бы собой проблемы при непосредственном доступе к регистрам…
Вот так вот. Попытка сделать абстракцию, чтобы кто угодно мог сразу начать программировать микроконтроллеры, достойная. Многие вещи действительно сильно упростились, и даже работают. Но, как и любая высокоуровневая абстракция, эта абстракция тоже дырявая. И при столкновении с любой из многочисленных дыр придется спуститься с небес на землю.
Комментарии (12)
tronix286
02.12.2015 10:06Да ладно, mbed — ну оберточка просто, коих множество… Они же туда (в контроллеры) всякие явы на пару с .NET подтягивают.
Mirn
02.12.2015 11:33я за .NET и прочие фреймворки типа джавы.
Потому что:
всё равно будет необходимость быстрого прототипирования и будет потребность переноса уже существующего кода для ПК и встраиваемых компов в МК. Особенно если задача рулить чем то простым и очень медленным, например климатом.
Я сам так делаю: пишу код на Си и отлаживаю и провожу юнит тесты для сигнальной обработки под MinGW 32 бита. А потом импортирую в 32 битный микроконтроллер. И когда сам взял Eclipse и допилил его до возможности компилировать под MinGW и STM32, то стало на порядок удобнее и быстрее вести разработку. И там и там GCC, оба 32 бита, и там и там исходники нормально работают и всё удобно и переносимо. В добавок у меня есть своя прослойка наподобии ардуино которая обеспечивает переносимость кода между всеми семействами STM32 и NXP которую я разработал лет 6 назад. Да у неё те же косяки с быстродействием как в данной статье, но в замен я просто забываю про конкретный камень и быстро делаю что мне надо и это работает как в производстве так и у клиентов замечательно.
А если нужно реально быстро ножками дёргать, например для управления двигателем или светодиодами RGB, то пожалуйста: есть FPGA, CPLD и замечательный язык AHDL где просто таблицей можно описать что тебе надо получить на выходе, в зависимости от входа и внутреннего состояния вообще не парясь как это будет внутри сделано. Плис реально ускоряют разработку — ты за час делаешь в лоб то, что настроить на таймерах STM32 займёт неделю. И часто плис нужна там где можно поднять себестоимость на 1000р смело, и даже на 10тр будет не так критично.
HomoLuden
02.12.2015 11:09Я может ерунду сморожу… Но нельзя ли завести две ножки In, Out на стороне МК и подключить их параллельно к ножке датчика?
olekl
02.12.2015 11:15+1В общем-то можно. Если обеспечить, чтобы Out оставался «в воздухе» когда датчик ведет передачу. Но зачем тогда в библиотеке класс для InOut пина?
HomoLuden
03.12.2015 12:47Видимо для небыстрых переключений. Если четко не задокументировано, то явная утечка в абстракции.
Вообще софтовая реализация кастомного протокола всегда небыстрая.
Я, кстати, потому долго возился с подбором частоты и битности SPI для протокола WS2811, что иначе Bit Banging производительность убьюет.
GarryC
Ну а теперь по существу — а зачем менять направление пина? Настроили на открытый коллектор, включили подпитку (или поставили внешнюю — я стараюсь так делать, уж больно нестабильные внутренние подпитки) подали нолик, подали единицу, сделали задержку и считывайте. Не может быть, чтобы библиотека такого не позволяла, если не позволяет, тогда да, в топку ее.
olekl
Возможно что и с открытым коллектором бы получилось, хотя тоже уже надо посмотреть глубже, чем предполагает фреймворк… Но вот для InterruptIn пина подать на него ничего нельзя штатными средствами…
GarryC
Хм, а почему нельзя? Функция бросает отказ? Или просто не проходит информация? Надо будет посмотреть.
А так Вы верно вопрос подняли, но, похоже, что-то одно, или универсальность, или эффективность.
olekl
Почему нельзя? В классе просто нет соответствующей функции. Можно конечно свой класс написать, доступ к регистрам же есть :) Но это же опять сильно глубже чем просто взять и попользоваться фреймворком.