Скорость работы VCP у STM32

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

Но разрабатывая очередное устройство у меня возникли смутные ощущения, что есть некие тормоза про общении с устройством. Решил проверить.

Для начала сгенерировал в STM32CubeMX пустой проект. В котором есть только USB и он определен как CDC.

Потом прямо в коде приема блока тут же его отправляю его назад. Кусок из usbd_cdc_if.c

static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
  /* USER CODE BEGIN 6 */
  USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
  CDC_Transmit_FS(&Buf[0], *Len);
  USBD_CDC_ReceivePacket(&hUsbDeviceFS);
  return (USBD_OK);
  /* USER CODE END 6 */
}

И написал маленькую программку на питоне, которая тупо спамит в порт увеличивающимися блоками и замеряет скорость. Можно взять тут https://github.com/kiltum/usb-rs485/blob/master/test/test/test.py

import time
import threading
import serial
# f042
#ser = serial.Serial(port='/dev/cu.usbmodem2058335047481')
# f303
ser = serial.Serial(port='/dev/cu.usbmodem2057385756311')

ser.isOpen()
# For windows
#ser.set_buffer_size(rx_size=262144, tx_size=262144)

bytesReceived = 0
minimalSpeed = 10000000
maximumSpeed = 0
counterStep = 0
blockSize = 1
shallExit = 0


def res():
    global bytesReceived
    global minimalSpeed
    global maximumSpeed
    global counterStep
    global blockSize
    global ser
    global shallExit

    if minimalSpeed > bytesReceived:
        if bytesReceived > 0:
            minimalSpeed = bytesReceived
    if maximumSpeed < bytesReceived:
        maximumSpeed = bytesReceived
    bytesReceived = 0

    counterStep = counterStep + 1
    if counterStep > 60:
        print("BlockSize:", blockSize, "Minimal:", minimalSpeed, "Maximum:", maximumSpeed,
              "Average:", round((minimalSpeed+maximumSpeed)/2048), "kb/s")
        with open("result.csv", "a") as myfile:
            myfile.write(str(blockSize) + "," + str(minimalSpeed) + "," + str(maximumSpeed) + "\n")
        ser.read(ser.inWaiting())
        counterStep = 0
        minimalSpeed = 100000000
        maximumSpeed = 0
        blockSize = blockSize * 2

    if shallExit == 0:
        threading.Timer(1, res).start()


with open("result.csv", "w") as myfile:
    myfile.close()

res()


while 1:
    if blockSize > 65536:
        shallExit = 1
        exit(0)
    s = "A" * blockSize
    b = s.encode()
    ser.write(b)
    bytesReceived = bytesReceived + ser.inWaiting()
    ser.read(ser.inWaiting())

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

Под рукой у меня оказалось только два stm32 с usb: F042 и F303. Оранжевая линия это максимальная скорость, синяя – минимальная. Такие прыжки максимальной скорости вызваны буферизацией у всех участников процесса. Ну по крайней мере я сейчас так думаю.

Результаты довольно показательные. Как найду еще процессоров – попробую повторить. Но пока можно сказать, что не стоит использовать блоки больше 128-256 байт и можно надеяться на скорость не менее 200 килобайт в секунду.

UPDATE1: Добавил график от F779. Суть та же. Видимо, где-то прямо в коде CDC у stm большие проблемы

STM32 ADC Multiple Channels HAL

Ну или подобными вопросами заполнены тематические конференции. И большинство из них без ответов.

Суть проблемы проста: если в STM32CubeMX включить в АЦП один канал, то все работает изумительно. Примеры есть повсюду и код работает стабильно. Но если включить несколько, то все ломается. При вызове HAL_ADC_GetValue на выходе получается “мусор”, содержащий случайную выборку с каналов.

Решение: в коде HAL содержится ошибка. В функции HAL_ADC_ConfigChannel индусы заложили такую логику, которая препятствует работе более чем с одним каналом. Согласно ей, перед измерением надо каждый канал конфигурировать с Rank = ADC_RANK_NONE, а затем снова его же конфигурировать, но уже с другим rank.

Если делать по документации, то вместо конфигурирования одного канала происходит конфигурирование всех каналов.

В общем, рассказывать дольше, чем показать код

Решил написать книгу …

… про реальную разработку устройств на stm32. Поэтому думаю обновления если и будут, то нерегулярным и в основном в виде кусочков из книги. Ну прёт меня пока.

Что бы не было скучно, вот вам малюсенький кусочек, можно сказать бета-версия про кварцевые резонаторы и вообще частоты.

STM32 и FreeRTOS. 5. Приносим пользу и добро!

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

Вооружившись осциллографом, я полез внутрь.

Через некоторое время поиски привели к двум проводкам, которые были в жгуте, соединявшем блоки устройства. Осциллограмма показала, что в проводках почти обычный USART. Почти — потому что «туда» данные бежали на скорости 9600, а обратно на 115200.

Продолжение тут

STM32 и FreeRTOS. 4. Шаг в сторону HAL

Одним из основных препятствий для перехода на STM32 является обилие текстов, инструкций и мануалов, описывающих работу с контроллером. Виновником этого обилия стала сама STMicroelectronics, которая поначалу планомерно запутывала своих пользователей, а затем предлагала неверные варианты выхода.

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

Но не так давно ST поняла, в какую яму она угодила и начала усиленно из нее выбираться, привлекая новые силы. И именно благодаря этому сейчас время старта сократилось до несуразно маленьких величин. Как это выглядит на практике?

Статья на хабре

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

Карпутер. 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

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

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

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

Карпутер 4. Кормим микроконтроллер

Сегодня пост будет самым зубодробительным для большинства автомобилистов. Придется вспомнить школьную физику за 5й класс и закон Ома оттуда. Но я покажу, где взять помощи 🙂

Итак, перед нами стоит 3 задачи.

1) Накормить микроконтроллер.
2) Дать данные. То есть позволить микроконтроллеру собирать всякую нужную ему информацию.
3) Позволить ему чем-либо управлять.

В чем основная проблема? Проблема как ни странно, в бортовой сети автомобиля. Напряжение в ней гуляет от 9 вольт до 14,5 вольт с возможными выбросами до 60 вольт. Попутно в этом самом “напряжении” есть куча шумов, всплесков и прочей гадости.

А микроконтроллеры очень нежные. Питаются только 5ю вольтами или 3,3мя. И желательно, что бы эти вольты не гуляли туда сюда больше 5%. То же самое и с входами-выходами. Те же самые условия, только еще желательно, что бы ток, проходящий по ним, не превышал единицы миллиампер. В случае необходимости ток может быть поднят до десятка-другого миллиампер, но это уже черевато.

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

Для питания “на быструю руку” давно придуманы готовые схемы. В любом приличном магазине электроники можете произнести слово “L7805” и вам за 10-15 рублей вынесут маленькую черную штучку, которая получает на входе от 8 до 30 вольт, а на выходе выдает стабильные 5 вольт. Вот тут (multik.org/carsim) вам собранная в proteuse схема на поиграться (carsim-power).

car-power1

Сколько бы вы не давали на “вход” напряжения, на выходе всегда будет около 5В. В чем подвох? Подвоха два. Во-первых, на холостом ходу (когда к ней ничего не подключено) эта штука потребляет единицы миллиампер. Для обычных условий эксплуатации это нормально, а вот если делать что-то типа сигналки, когда схема постоянно подключена к аккумулятору, это .. некрасиво. Во-вторых, если вы задумаете “снимать” с стабилизатора 1-1,5 ампера (хинт – это почти готовая зарядка для телефона), то вы внезапно обнаружите, что стабилизатор греется. Почему? Потому что он вынужден куда-то тратить те вольты, которые оказались ненужными. Считайте сами: пусть будет в бортовой сети 13,4 вольта. Телефон хочет 5В и 1А для своего заряда. Значит стабилизатору надо рассеять где-то (13,4-5)*1=8,4 ватта. Это вполне себе нехилая грелка. И именно поэтому у этого стабилизатора есть металлическое ушко, за которое его прикрепляют к радиатору (Кстати, радиатором может быть любая массивная железная часть автомобиля). Но повторюсь, для обычных применений это вполне себе штуковина.

Но если мы заранее задумаемся о снижении напряжения и тока питания, то есть варианты. К примеру LP2950. Он включается точно так же, как и 7805, но во-первых, на холостом ходу ест десятки микроампер, а во-вторых, больше 100 миллиампер с него не снять. Для многих схем этого может хватить.

Но что делать, если по каким-то причинам надо больше 1,5 ампер и радиолюбителей (что бы собрать блок питания) нет под боком? Тогда на выручку придут готовые DC-DC преобразователи. По сути это собранные и оттестированные блоки питания в миниатюрных корпусах. Например NSD15-12S5 (вход 9.4-36В, выход 5В и 3А) или наш МПА15А (вход 9-18В, выход 5В и 3A). На самом деле их куча, стоит только поискать. В чем их плюсы? Ну, они не греются, имеют встроенные фильтры, управление, защиту от КЗ и прочее, что полагается иметь блокам питания. Основной их минус в цене: от тысячи рублей и выше.

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

Переходим к следующей задаче – дать микроконтроллеру данные. Проблемы те же самые: не дать микроконтроллеру много вольт и миллиампер. Все используемые варианты давно уже описаны и можно сказать, стали классическими.

Первый: делитель на резисторах. Просто ставим в цепь два резистора, в точке их соединения забираем данные. Уровень напряжения в точке будет равен входному напряжению * соотношение номиналов резисторов. А ток можно подсчитать по закону Ома – i=u/r Опять же, я понимаю, что это тяжко, поэтому вот очередная схема в протеусе, которая позволит “поиграться” (там же файл carsim-power-res). Ну или набрать в поисковике “калькулятор делителя напряжения”.

carsim-power-res copy

Справа можно увидеть амперметр (показывает миллиамперы) и вольтметр. Наша задача – добиться таких сочетаний резисторов R1 и R2, что бы на микроконтроллер не приходило больше 3В (если ардуино, то 5) и потребление тока было минимальным (а то так повесим с десяток датчиков, сжигающих на резисторах по 10 миллиампер и получим 0,1 ампера, способных посадить аккумулятор в “ноль”). Казалось бы, простая задача? Но давайте снова посмотрим на то, что может происходить в бортовой сети. От 9 до 60 вольт … Если сделать совсем классический вариант, то 99% времени напряжение на ножке контроллера будет составлять доли вольта. Как-то некрасиво.

Поэтому обычно рассчитывают делитель на входное напряжение 0-15В, а всплески (которых может и не быть) убирают отдельным элементом. Есть много схем, но я предпочитаю самую простую в изготовлении: на вход, перед делителем, ставлю защитный диод (он же супрессор, он же TVS). Скажем, китайский SA15 пропускает через себя все, что попадает в диапазон 0-15 вольт. Остальное он убирает и убирает довольно качественно (в смысле после него конденсатор если и нужен, то только для сглаживания). Минус у него только один – если что, то он (диод) превращается в простое короткое замыкание, заставляя сгорать всё что можно перед собой. Для 99% случаев этого достаточно, а для оставшегося 1% существуют предохранители. Схемы я нарисую в следующем посте.

ВАЖНО: нельзя использовать “делители напряжения” для схем, где есть напряжения больше нескольких десятков вольт. Просто небезопасно для здоровья. Для этого есть второй вариант.

Вторым вариантом ввода сигнала в микроконтроллер является оптопара. Внутри оптопары стоит светодиодик, который своим светом управляет транзистором, “включая” или “выключая” выходы. У оптопары главный плюс в следующем: легко можно измерять наличие сотен и тысяч вольт. Самая обычная оптопара легко “развязывает” до 1500 вольт. Минус же тоже существенный: с её помощью нельзя (практически, да) измерять уровень сигнала. Только “есть” или “нет”.

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

С вводом разобрались. Теперь с выводом. Сначала я начал писать свой текст, но потом вспомнил, что есть отличный пост на эту тему. Не мой, но это же не повод не считать его отличным?

062-Как подключить к микроконтроллеру нагрузку?

Моими любимыми способами являются использование “составного транзистора Дарлингтона” и “полевой транзистор”. Тут их переписывать не буду, ибо все прекрасно описано по ссылке выше. Мой выбор между способами простой: если надо коммутировать что-то до 5-10 ампер или быстро включать-выключать, выбираем транзистор. Выше – реле через составной транзистор. Быстро включать-выключать большие нагрузки мне не приходилось, но думаю, схем на всяких тиристорах и симисторах навалом.

Что бы проиллюстрировать вообще все вышеприведенное, вот вам очередная схема (она же в файле carsim-final)

carsim-final

Думаю, вдумчиво читающим уже почти все понятно, но на всякий случай прокомментирую. Слева вверху я сымитировал бортовую сеть автомобиля. Можно переключиться на 12В, 13,4, 16В и на регулируемое. Правее – стабилизатор напряжения на 5В. Слева внизу – делитель напряжения на резисторах R1 и R2 и развязка через оптопару U2. Правее стабилизатора две схемы включения нагрузки – через N-канальный полевой транзистор IRF610 и через сборку Дарлингтона ULN2003A.

К моему сожалению, proteus довольно небрежно относится (или у меня такая версия) к мощностям элементов, поэтому симуляция иногда прерывается (как и игнорируется D2). Если такое случилось, просто запустите заново.

В общем, схема выглядит достаточно страшной, что бы напугаться окончательно, особенно с непривычки. Но открою страшный секрет: для отладки нашей первоначальной задачи достаточно к плате с микроконтроллером дополнительно прикупить обычный переменный или подстроечный резистор на 500 Ом и выше. Индикатором срабатывания будет “пользовательский” светодиод, который есть на большинстве плат. Но как обычно, об этом в следующем посту.

Карпутер. 3. Выбираем микроконтроллер.

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

Как выбирают контроллер?

Во-первых, по личным предпочтениям. Кому-то нравится одна архитектура, кому-то другая. У кого-то уже есть опыт с одними микроконтроллерами и ему лень изучать другие … Но я считаю, что раз опыта и предпочтений нет, поэтому – stm32. Вам на данном этапе все равно, а мне потом спасибо скажете.

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

И наконец, по числу портов и их возможностями. Все порты делятся на два типа: цифровые и аналоговые. Цифровые оперируют уровнями типа “есть сигнал” и “нету сигнала”, а аналоговые – “какой уровень у сигнала?”. Говоря другими словами – кнопки, выключатели и прочие переключатели – это цифровые, а всякие регуляторы, измерители и прочее – аналоговые. На каждую кнопку, релюшку или измеритель надо по одному порту (конечно, есть куча возможностей, как от этого уйти, но пока нам этого не надо). И крайне рекомендую при выборе зарезервировать пару портов под всякие доделки и внезапно всплывшие идеи.

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

Что будем делать-то? В смысле для чего мы все это затеяли? Давайте начнем с простого. Пусть будет система автоматического включения света в машине. Как пример использования: садимся в машину, включаем зажигание, заводим машину и через некоторое время наш контроллер включает фары. Выключили зажигание – все выключилось. Сплошные бонусы: стартеру легче крутить двигатель – фары не отбирают лишних ампер и вам не надо будет помнить о включении фар.

Какой будет алгоритм работы?

1. Измеряем напряжение бортовой сети
2. Напряжение меньше 13В? если да, иди на п.1
3. Подождать 5 секунд.
4. Включить фары.

Все, никаких заморочек. Просто и совсем не страшно. Как видно, нам потребуется всего 2 порта: один аналоговый для измерения напряжения в сети и один цифровой, для управления релюшкой фар.

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

Вернемся к нашим баранам. Нам нужен микроконтроллер, у которого есть как минимум 1 аналоговый порт на вход и 1 цифровой порт на выход. Сейчас смешно, да. Но потом всего будет не хватать и начнутся мучения.

Теперь готовимся скачать много из сети. Для начала нам нужна программа STM32CubeMx. (Все поисковики ее легко находят, но вот прямая ссылка http://www.st.com/web/catalog/tools/FM147/CL1794/SC961/SS1533/PF259242?sc=stm32cube там мотайте страницу в самый низ и справа будет маленькая красная кнопочка Download).

Зачем нужна эта программа? Как я писал в первом посте, STM32 имеют один, но очень большой и неприятный минус – очень сложно начать с ними работать. В этих микроконтроллерах очень много возможностей и вариаций, поэтому даже просто запустить его составляет очень большую проблему. Команды инициализации, предназначенные для одной серии, не подходят для другой. Один и тот же порт может выполнять разные функции на разной частоте, причем все это настраивается в четырех или пяти местах. В общем, реальный кошмар после атмеловских контроллеров и 99% причин неработащих программ.

Вот и придумали этакий “генератор кода инициализации”, когда можно мышкой не торопясь повыбирать порты и их функции. При этом идет одновременный контроль правильности использования порта и непересечение его с другими функциями. В общем, скачивайте, распаковывайте и запускайте программу (может потребовать java, так что тоже ставьте). Как ставить программы, нажимая next, я рассказывать не буду 🙂

stmcube1

Перед вами откроется очень информативное окно. New Project – это создать новый проект, Load Project – загрузить старый, который редактировали раньше. Нам естесственно надо выбрать создать новый. И тут …

stmcube2

И тут перед вами откроется окно, в котором собрана вся (почти) линейка микроконтроллеров. Первая вкладка – MCU Selector позволяет выбрать подходящие контроллеры в их голом виде. Слева в табличке функционал, справа – подходящие контроллеры. Скажем, нужно нам в нашем проекте использовать одновременно ethernet и часы реального времени, так значит ставим галочки и получаем, что нам подходят 88 микроконтроллеров из 590 (на момент написания). Но эта вкладка для продвинутых пацанов.

Нам нужна следующая вкладка, которая называется Board Selector. Тут уже можно выбрать готовые платы, со всем распаянным. Сразу рекомендую нажать кнопку “>>”, которая будет показывать изображение платы.

stmcube3

Механизм тот же самый – слева выбираем что хочется, а справа получаем список того, где это есть. Потом открываем веб-сайт ближайшего магазина электроники и смотрим на наличие и цену. Лично у меня есть платы STM32L100 и STM32F3 (именно она изображения на скриншоте). Так как F3 мне нравится больше, то и в дальнейшем я буду использовать именно эту плату. Но повторюсь, вы можете использовать любую плату или процессор – главное, что бы он вам подошел по характеристикам.

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

stmcube4

Слева будут всякие возможности, которые умеет процессор, а справа – как и на что распределены ножки и процессора. Если вы вышли сюда из выбора плат, то программа сама показала, что на плате куда подключено. Как видите, все ножки помечены разными цветами.

Желтенькие и светлозеленые – ножки, назначение которых изменить нельзя. Питание, земля и прочие подобные ножки.
Оранживенькие – ножки, на которые повешено то, что есть на плате и что можно либо отключить, либо заиспользовать. У меня это кварцевые резонаторы, гироскоп с компасом, USB порт и так далее.
Зелененькие – это ножки, на которые тоже повешено то, что есть на плате, но это ТО – кнопки, светодиодики и прочее. Грубо говоря, отличие только в сложности с точки зрения контроллера. Таким же цветом будут обозначаться и ножки, которые вы выделили для вашего проекта.
Серенькие – свободные ножки, которые можно использовать.

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

Как менять назначение ножек? Есть два способа. Первый – это просто мышкой ткните на ножке. Вот для примера я ткнул на ножке, которая у меня подключена к голубой кнопке.

stmcube5

Как видите, эта ножка может выполнять аж 17 функций, но сейчас она работает как GPIO_Input (я ниже объясню, что это значит).

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

stmcube6

Как видим, у функции вообще горит желтый предупреждающий знак, который показывает, что что-то с ней не то. Открыв ее, можно увидеть подсвеченным красным подфункцию. В данном случае это IN1. Подведя мышку к красному, можно узнать, что с чем конфликтует. В данном конкретном случае можно увидеть, что 1й канал 1го аналого-цифрового преобразователя конфликтует на ножке процессора PA0, которая уже стоит в режиме GPIO_Input. Белиберда, да? Но ничего. Для примера можно обидеться и раз нам не дают использовать IN1, выбрать IN3, что бы это не значило. И обратите внимание, на рисунке процессора справа одновременно начнет показываться как “занятая” соответствующая ножка процессора. В нашем случае это PA3, в левом нижнем углу.

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

Итак, как же понять, какие функции можно повесить на ножку? Что бы не забивать голову, я опишу только наиболее нужные и часто используемые функции. Назначение других можете узнать сами, когда прижмет (но 90% это никогда не понадобится).

Итак, что можно выбрать?

ADC – Или АЦП, аналогово-цифровой преобразователь. Показывает значение напряжения. У большинства АЦП есть каналы, к которым он может подключаться. А каналы напрямую подключены к ножкам. То есть когда вам надо измерить напряжение на 1,2 и 3й ножке, то микроконтроллер на самом деле будет выполнять примерно следующее “подключить ацп к ножке 1, измерить, подключить ацп к ножке 2, измерить, подключить ацп к ножке 3, измерить”. В принципе, для большинства задач этого достаточно, ведь измерение одного канала занимает от 1 до 10 мс. Но есть задачи, когда необходимо реально одновременно измерить напряжение на несколких ножках. В таком случае используют два или больше АЦП. Например, в F3 серии аж 4 АЦП, поэтому мы можем измерять 4 уровня одновременно. Если мы заиспользуем все доступные ресурсы, то сможем за 0,1с измерить 59 аналоговых выводов (ардуинщики, вы рыдаете? :).

DAC – или ЦАП. Цифро-аналоговый преобразователь. Преобразует некоторое значение в уровень сигнала на выходе. Обычно один DAC имеет от 1 до 10 выходов, каждый из которых можно регулировать отдельно.

TIM – таймеры. Срабатывающие “раз в нное время” сигналы. На таймерах в stm делается очень многое – от PWM (управление сервомоторами и яркостью) до подсчета частоты смены сигнала на входе. Немножко к ним имеют отношение RTC – часы реального времени (которые считают минуты и секунды, а не тики и такты) и WDG – системы, которые автоматически перезагружают контроллер, если он завис, но я их касаться не буду

USART/UART – контроллеры для связи с “внешним миром”: с компьютерами, с другими контроллерами и так далее.

И наконец GPIO. Это порты общего назначения. То есть на них можно вешать все, что душе угодно. Они могут быть GPIO_Input – порт, работающий на вход (который принимает сигнал “есть” и “нет”) и GPIO_Output – порт, работающий на выход (который выдает сигнал “есть” и “нет”). Вы можете увидеть GPIO_Reset – это означает, что порт находится в хз каком стоянии и GPIO_EXTI – это выход прерывания. В общем лишнее на данном этапе.

Все ножки маркируются следующим способом: [подсистема]_[функция]. Пример:

ADC1_IN6 – 6й вход 1го АЦП контроллера
DAC1_OUT1 – 1й выход 1го ЦАП контроллера
USART1_TX – порт передачи 1 контроллера связи.

Но вернемся назад. Из всего выше перечисленного нам нужен один ADC ввод и один GPIO_Output вывод. Для ввода я заиспользую ADC1_IN2 (ножка PA1), а для вывода GPIO_Output – PC5. Они расположены на одной стороне реальной платы, поэтому мне будет удобно с ними работать. И что самое главное, они не конфликтуют ни с чем, что уже есть на плате.

stmcube7

Щелкаем и меняем назначение нужных нам ножек. Обратите внимание на то, что у PA1 нет булавки, а у PC5 – есть. Это та самая функция переназначения портов, когда вдруг функционал будет конфликтовать, а нам нет разницы, откуда его брать. Что бы “прикрепить” функционал к ножке, надо просто правой кнопкой мышки по ней щелкнуть и выбрать Signal Pinning. Теперь ни одна сволочь не отберет у нас ее :). Кстати, там же можно и дать название ножке, что бы не запутаться.

stmcube8

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

Можно сохранить проект на всякий случай, ибо это только начало.

Теперь щелкаем следующую вкладку – Clock Configuration. И у некоторых сейчас порвет мониторы 🙂

stmcube9

На этой вкладке вы можете увидеть, с какой скоростью работают внутренности микроконтроллера. Эта вкладка очень полезна тогда, когда мы озабочены энергопотреблением микроконтроллера. Играясь тут, можно легко на порядок понизить энергопотребление микроконтроллера. Но нам это не надо, поэтому переходим на следующую – Configuration

stmcube10

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

На картинке мы видим включенный контроллер ADC1 (для него нужны контроллер DMA, NVIC и RCC) и контроллер GPIO (Для него нужен RCC). В общем, давайте поверим, что нам это надо.

На этом экране у нас есть возможность тонкой настройки. Жмем на ADC1

stmcube11

Тут собраны все тонкие настройки для данного контроллера. Особо менять нечего, за исключением подсвеченного – Continuous Conversion Mode. Поставьте его в Enabled. Данная галочка говорит, что мы желаем запрограммировать контроллер так, что бы он постоянно мерял свои входы. И более того, измерянные значения сразу присваивал переменным. Ну ленивый я, пусть железка сама все делает 🙂

Аналогичную картинку можно получить и по GPIO портам.

stmcube12

Тут можно тоже поизменять разные параметры, но нам тут менять тоже ничего не надо.

В общем, полезная вкладка, особенно когда начинаешь использовать более сложные вещи, как USART или USB – здесь можно настроить все.

И наконец, последняя вкладка – Power Consumption Calculator. Тут можно прикинуть, сколько электричества будет потреблять микроконтроллер.

stmcube13

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

Итак, остался один последний шаг. Сгенерировать исходный код, котрый будет компилироваться и прошиваться в контроллер. На панели сверху есть кнопка “шестеренка с палкой”. Нажимаем ее и …

stmcube14

Заполняем, где будут располагаться файлики и как мы обзовем проект. Так же выберем, для какой среды разработки будет генерироваться исходный текст. Выбор небольшой и все предлагаемые системы – полный шлак (тут конечно у каждого свои фломастеры, но тот же бесплатный CoCox уделывает Keil как бог черепаху), поэтому выберите MDK-ARM, как наиболее описанную в русском интернете.

Если вы запускаете генерацию в первый раз, система предложит скачать Firmware Package именно для вашего процессора. Ждем пока скачает и нагенерирует.

stmcube15

Увидели это окошко? Поздравляю! Вы прошли большую часть пути. Остался еще один шаг. Всего один …

Открываем браузер на https://www.keil.com/download/product/ и выбираем MDK-ARM v5. Вам дадут анкету, в которой реально проверяется только email. Проверяется – в смысле их сервер подключается к вашему серверу и проверяет валидность ящика, поэтому емайлы типа 2@1.co не проходят. Остальное нужно только для того, что бы выбрать, на каком языке потом вам напишет продавец со словами “купите у нас”. Как обычно, у данной версии есть ограничения и самым главным из которых является ограничение в объеме кода в 32 килобайта. Поверьте, это довольно приличный объем для микроконтроллеров и вам его хватит надолго. Но если вас это напрягает, то сами знаете где можно найти вылеченную версию совершенно бесплатно.

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

Как обычно, рассказывать как ставить программы с помощью нажатия кнопки next, я не буду. Единственное, что при первом запуске вылезет Packs Installer – дождитесь, пока он отработает и закрывайте его. Так что ставьте Keil и в той папочке, куда сохранили проект, найдите каталог Projects, в нем MDK-ARM и там ваш файл с типом mVision4 Project. Нажимаем на него …

stmcube16

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

stmcube17

И только после того, как вы увидели в окошке снизу строчку 0 Errors(s), 0 Warning(s) вы можете поздравить себя – у вас есть полностью готовая прошивка для микроконтроллера. Ну и что, что она пока ничего не делает, зато 90% нашей первоначальной задачи уже выполнено. Теперь вы можете идти в магазин и покупать реальный микроконтроллер за реальные деньги. И у вас есть уверенность, что он заработает.

А вот как и что подключать к контроллеру – это уже в следующем посте.

Карпутер. 2. Все не так страшно …

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

Раз вы читаете это, значит у вас есть компьютер и браузер. Открываете браузер и идете на http://www.labcenter.com/. Там все по английски, но вам нужен раздел Downloads, а там скачать prodemo.exe. В этом здоровом файле находится редактор схем и печатных плат. А так же симулятор этих самых электрических схем. То есть вы можете как угодно “коротить”, “вешать сопли” и развлекаться другими способами: никакого урона электронике вы не нанесете.

proteus_screen

Со скачанным с сайта есть одна засада: он не сохраняет и не печатает нарисованного. Но в интернете можно найти уже вылеченное и даже руссифицированное. Если сами не можете, спросите и вам ответят. В интернете же легко можно найти и инструкции, как и что делать с ним, поэтому я буду очень короток. Нет, я пойду другим путем.

Вот тут http://multik.org/carsim/ лежит готовый проект для proteus (просто скачайте файл carsim.dsn и кликните на нем – он откроется в proteus). Он довольно приблизительно имитирует электропроводку обычной машины. Знающие электрику начнут кривить носом и искать ошибки (а они там есть), а незнающие могут нажать слева внизу кнопочку “play” (треугольник вправо, если кто не понял) и поиграться. Поиграться в буквальном смысле: выключатели выключают и включают лампочки, а пипикалка пипикает.

При этом схема полностью отражает реалии: скажем, можно заменить предохранитель на имеющий более низкий номинал (Выделите мышкой предохранитель и нажмите Ctrl-E, там увидите что-то типа 2А) и увидеть, как он сгорает при попытке включить нагрузку.

Итак, что бы было понятно что к чему, пройдусь по схеме слева-направо.

proteus1

Здесь имитация генератора и ключа зажигания (SW1), аккумулятора и отключателя массы (SW2) вместе с главным предохранителем. Вольтметр показывает напряжение в “бортовой сети”.

proteus2

Тут имитация работы ближнего и дальнего света. Переключатель SW3 имеет три положения – выключено, ближний свет и дальний свет. Лампы L1 и L2 отвечают за “ближний”, а L3 и L4 – за “дальний”.

proteus3

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

proteus4

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

С последней частью предлагаю разобраться самим, главное громкость поменьше сделайте 🙂

Итак, я буду считать, что наиболее нетерпеливые наигрались, а некоторые даже пошли читать инструкции или даже смотреть (в гугле “proteus начинающим” выдаст все необходимое).

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

Кнопки. Бывают нормально замкнутые (концевики в дверях) и нормально разомкнутые (кнопка сигнала). Принцип простой: нажал, она сменила состояние (замыкала цель – теперь размыкает) и будет в таком состоянии, пока ее не отпустить.

Выключатель. Такой же, как кнопка, только состояние фиксируется (держать не надо)

Переключатель. Полностью аналогичен выключателю, только коммутирует несколько выходов. Пример – SW3 выше. Или замок зажигания.

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

Переменный резистор (он же переменное сопротивление). Штука, меняющая свое сопротивление в зависимости от каких-то условий. Наиболее распространенный пример – датчик уровня топлива. Много топлива – сопротивление маленькое, мало – сопротивление большое.

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

Да, особняком стоят всякие OBD, CAN и прочие, но там все в цифре и я этого коснусь потом.

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

С обратным управлением все гораздо проще: микроконтроллеры “дохлые” и поэтому без транзисторов/релюшек не обойтись вообще никак. Типовые схемы я тоже опишу.

Согласитесь, совсем не страшно же? Тогда в следующем посту будем выбирать конкретный микроконтроллер под ваши нужды.

Карпутер. 1. Предисловие и выбор микроконтроллера

Хотел начать с обычного отсыла к классике (“этим постом я начинаю …”), но потом передумал. Начну обыкновенно.

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

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

Для начала я решил составить список того, что у меня недоделано (просто скопирую из форумных постов)

1) Контроллер второго аккумулятора. Штука с вольтметрами, контролирующая подключение второго аккумулятора. Есть подобная у t-max, но она вся в светодиодиках и тупая. Алгоритм грубо говоря такой: смотрим на напряжение на первом аккумуляторе, как превысило 13В (значит завелись), подключаем второй (для зарядки и теде). Если на первом долгое время 12В, значит можно отключить второй — мы на стоянке. Если ниже, то не отключать — мы лебедимся и второй аккумулятор нужен. Ну и кнопочка принудительного подключения второго, если первый на стоянке разрядился по каким-то причинам. У водителя индикатор с вольтами и кнопочка.

2) “Сумматор баков”. У многих машин есть два источника горючки (скажем, бензин и газ), а у некоторых их три (два бака с бензином и газ). Суть — выводить на штатный указатель уровня сумму уровней баков. Например, от нуля до четверти — первый бак, от четверти до половины — второй и от половины до полного — газ. Или еще как. Дополнительно выравнивание нелинейности датчиков уровня, когда то стрелка не двигается, то начинает падать. И специальная фича для патриотоводов — обработка уровня “приборка пикнула” так, что бы она не пищала при каждом торможении-разгоне.

3) “Корректор спидометра”. Меня напрягает 10-15% погрешность спидометра даже на штатных колесах. Меня напрягает нелинейность этого вранья. И стоит сменить размерность колес, как все привычки типа “раз стрелка на 90, значит я еду 85” приходится “перепривычивать”. Дополнительно была мысль сделать вывод “поехали — включи ближний свет/ДХО”, что бы не дергать переключатели руками и не зажигать фары перед заводом зимой.

4) “удлинитель выключателей”. Грубо говоря — управляемые удаленное релюшки. Например поставил свет вокруг, и к нему вместо кучи силовых проводов тащишь один силовой потолще и один управляющий. Или для компрессора сзади или для усилителя … в общем, везде, где надо управлять чем-то мощным и это мощное далеко от водителя.

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

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

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

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

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

AtMega или Adrduino. Те же самые плюсы, что и у “тинек”, но добавляется то, что все решения уже идут готовыми. Подключил плату к компьютеру, настучал в окошке код и он уже выполняется. Главный минус же состоит в том, что все эти решения рассчитаны на “сделал максимально быстро, не считаясь с затратами”. Отсюда совершенно дикие цены как на сами платы, так и на модули расширения (shield, шилд). Скажем, самая дешевая плата в классическом (для ардуинки) формате стоит от тысячи рублей, при этом функционала на ней – кот наплакал. А цены на самые дорогие, с чуть-чуть большим набором портов легко упрыгивают за сотню баксов.

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

Следующими идут миниатюрные компьютеры. Paspberry Pi, OLinuXino и так далее. Основной плюс – внутри них работает привычный многим Linux, код можно писать практически на чем угодно и они подключаются к монитору или обычному телевизору. Так как основное предназначение у них тоже самое, что и у ардуинок, то и минусы те же самые – дикие цены как на сами компьютеры, так и на платы расширения к ним. Плюс все эти компьютеры совершенно (ну по сравнению с вышеперечисленным) не имеют портов ввода вывода и обладают диким энергопотреблением.

К этому описанию можно смело добавить разные платы с Android внутри. Плюсы и минусы абсолютно те же самые. Для планшетов еще пойдет, а для встраиваемой электроники – никак.

И наконец, моя нынешняя вершина – микроконтроллеры stm32. У есть один, но очень большой и жирный минус: в них совершенно невозможно разобраться “с наскоку”. Даже простая задача “помигать светодиодиком” для начинающего легко может вылиться в многочасовое копание в документации, которая к тому же вся на английском. Все остальное – сплошные плюсы: от широчайшего арсенала портов и интерфейсов до дикой производительности. Как дополнение – архитектуру stm32 для своих микроконтроллеров используют несколько производителей, поэтому средств разработки – дикие горы, на любой вкус, любую задачу и на любой кошелек.

Скажем, возьму самую дешевую плату для разработки на stm32 – STM32L100C-DISCO. 256 килобайт памяти для программ (ардуинки и прочие уже рыдают), 16 килобайт ОЗУ, работает на 32Мгц, кроме АЦП (16 каналов!) и ЦАП имеет кучу аппаратных(!) интерфейсов типа I2C и 40 с лишним портов ввода-вывода. По меркам stm – это микроконтроллер начального (нет, начальнейшего) уровня. И вот эта плата, с уже готовым программатором и парочкой светодиодиков стоит в два раза дешевле самой дешевой ардуинки.

А стоит сравнять стоимость, как уже можно получить STM32F3DISCOVERY, где добавят гироскоп с компасом, кучку светодиодиков, поднимут объем ОЗУ до 48килобайт и частоту процессора до 72Мгц. После этого увеличат число портов ввода-ввывода практически в два раза и добавят горку аппаратных интерфейсов.

А за цену какой-нибудь arduino mega можно взять офигенный набор разработчика с LCD экраном и дичайшими возможностями.

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

Думаю, после этого вам станет понятным, какие микроконтроллеры я буду использовать в дальнейших разработках 🙂

Если вам стало мало avr …

… или добро пожаловать в ад, ардуинщик!

Любой человек, который использует в своих проекта arduino, рано или поздно сталкивается с тем, что ему перестает хватать возможностей имеющегося микроконтроллера. Те, кому просто не хватает ножек, пытаются мигрировать на mega, остальные ищут шилды с необходимым функционалом или начинают изобретать свои. Но любому дрыгоножеству рано или поздно наступает предел: не хватает! Наиболее малодушные спрыгивают на raspberry или другие подобные платы, а считающие что они сильны духом начинают смотреть на другие платформы.

У меня есть и ардуинки и “малинки” и кучка других плат, поэтому я вполне резонно считал, что в микроконтроллерах я далеко не чайник и знаю, с какой стороны пины дергать … В общем, через друзей ко мне поступило предложение сделать довольно простенькую (на фоне уже сделанных проектов) систему. Но на stm32. В памяти сразу всплыла отладочная плата stm32f3discovery, купленная “на сдачу” при очередной закупке и пылящаяся в закромах. Даже не обсудив условия, соглашаюсь: форсированное обучение новой платформе, да еще и на живом проекте, всегда лучше “ну вот посижу вечерком, побалуюсь проектиками …”. Дополнительным бонусом стало изучение микроконтроллеров, на которых построено управление раздаточной коробкой и климатом в моем УАЗе.

Лирическая завлекалка: (из описания) … плата на базе микроконтроллера STM32F303VCT6 включает в себя встроенный отладчик ST-LINK/V2, акселерометр, гироскоп, электронный компас STMEMS, разъем MiniUSB, светодиоды и пользовательские кнопки. Процессор работает на частоте до 72МГц и предоставляет пользователю 90 каналов ввода-вывода. И все это продается за 600 рублей. В одном из самых дорогих магазинов. Аналогичный функционал (ну почти аналогичный: таких скоростей и объемов памяти нет) на ардуине будет стоить около 7-8 тысяч рублей. А отладчиков с аналогичными возможностями на ардуинках вообще нет как класса.

И тут я совершил первую ошибку, впрочем вполне понятную в моем положении: не посмотрел на поддержку микроконтроллера производителем. Ну в самом деле, ведь они делают микропроцессор и плату на нем, а значит должна быть и документация и примеры … Как же я ошибался … В общем, выбрали микроконтроллер и плату просто по необходимому функционалу. Ей оказалась плата STM32L100C-DISCO.

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

Судите сами. Вот кусочек кода:

RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
GPIOC->CRH |=0x33;
GPIOC->CRH &= ~0xCC;
GPIOC->ODR |= (GPIO_ODR_ODR9 | GPIO_ODR_ODR8);
GPIOC->BSRR=(GPIO_BSRR_BS8|GPIO_BSRR_BS9);

Это всего-лишь простейшее зажигание двух светодиодиков.

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

Второе отступление: в отличии от ардуинки, для stm32 есть много вариантов сред для разработки. В результате любая тема “а что выбрать?” довольно быстро превращается в обычный срач. Лично я выбрал бесплатную CooCox. Не без глюков, но все что надо есть.

Вторая ошибка: я считал, что раз микроконтроллеры принадлежат к одной архитектуре, то и программироваться они должны одинаково. Ну грубо говоря если на SMT32F0 какой-то код включает ножку А0 на чтение, то тот же код должен включать ту же ножку и на STM32L1. Хренушки. Все разное даже для микроконтроллеров одной серии, но различающихся разными буковками на конце. Особенно для дополнительного функционала типа таймеров. В результате примеры из интернета в лучшем случае просто не работали (вы когда-нибудь видели неработающий blink на ардуинке? А тут легко!), а в худшем просто не компилировались.

Я было впал в печаль, но решил что я не первый в таком положении, а значит должен быть какой-то выход. И выход был найден практически сразу: SPL. Фирменная библиотека от STmicrosystems.

Еще одно отступление: для других процессоров у ST есть более навороченные и новые библиотеки. Но вот для L – нету. Смотреть на мою первую ошибку.

Радости моей не было предела. Код сразу преобразился в почти читаемый:

GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC , ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init( GPIOC , &GPIO_InitStructure);
GPIO_SetBits(GPIOC, GPIO_Pin_8|GPIO_Pin_9);

Это уже было прорывом. Хоть и были проблемы со всякими константами, но они были вполне понятными: не стоит ожидать скорости работы ножки в 50МГц от контроллера работающего на 32х.

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

На одном из шагов потребовалось прицепиться к контроллеру по обычному ком-порту (USART по импортному). Уже вполне привычные заклинания, аналогичные вышеприведенным и честно спертым из интернета и … и ничего. В смысле вообще ничего.

Попробовал один пример: пусто. Второй: пусто. Начитался про DMA (в stm32 можно сделать так, что бы внутренности микроконтроллера сами отсылали данные, не требуя постоянного присмотра из программы), написал: пусто. Я начал метаться в разные стороны: то пример попробую, то документацию почитаю. Ничего. Не работает.

Тогда я вспомнил, что я вообще-то работаю с железной штукой, а не с обычной программой. Сажусь логическим пробником на порты микроконтроллера и вижу, что что-то он там все-таки передает. Обрадованно кричу “это проблемы на другой стороне, железка по rs232 не хочет работать!”. Тут же сам себя урезониваю: с другими штуками эта железка вполне себе работает. А с моей нет. Значит проблема все-таки у меня …

Достаю осциллограф и смотрю на ножки. Сигналы есть, всякие нолики-единички я вижу очень отчетливо.

Нахожу переходник для компьютера и подключаю. Запускаю терминалку. С контроллера передаю 00, в компьютер приходит 00. Передаю с контроллера 55, в компьютер приходит два байта. Передаю АА, компьютер получает аж три байта! Офигеваю. Даже мрачно.

Еще одно отступление: в принципе всем необходимым арсеналом настоящий ардуинщик обзаводится еще в процессе, но многие обходятся дешевым китайским мультиметром (а то и вовсе живут без него). Тут же очень желательно заиметь осциллограф, даже самый простенький. Более-менее приличные, подключаемые к компу стоят 7-8 тысяч и очень сильно облегчат отладку устройств. Даже хвалимый многими Keil (он умеет рисовать рисуночки происходящего на портах) в некоторых случаях очень сильно лажает.

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

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

Через некоторое время я обнаружил себя тупо играющимся частотой развертки осциллографа. Рисуночек передачи раздвигается-сжимается … а внизу бегают циферки разрешения. Постепенно в мой мозг прямо-таки просачивается мысль “какого хрена этот пик шириной 20мс? он же должен быть 10!”. Лезу в программу, ставлю вместо положенных 115200 скорость 230400, заливаю и вуаля! Все работает! Попрыгав горным козлом по комнате, я стал разбираться, в чем же причина такого поведения.

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

Кратко говоря, все дело было в неправильном указании частоты процессора. Библиотекописатели были уверены, что он работает на 32МГц, а в реальности он работает на 16МГц. А так как в STM32 все (в смысле вообще все) рассчитывается исходя из этого значения, то и usart в реальности работал на скорости 57600 вместо требуемых мне 115200.

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

Прослезившись, я пошел реализовывать следующий по плану функционал: простой АЦП. Программе надо замерить уровень сигнала на одной из ножек и потом согласно этому уровню начать делать свое черное дело. И тут я опять встрял: программа либо висла, либо в ответ возвращала 0. Уже наученный горьким опытом, я посмотрел реальные данные на ножке. Как легко догадаться, они отличались от 0 …

В общем, я мучался. Я страдал и рыдал. Я даже помощи на форумах попросил. Но судя по обилию аналогичных вопросов, я был не одинок. Я даже чуть не смалодушничал и не решил попробовать бэйсик для stm (есть закрытый проект, который позволяет программировать на бейсике, С или паскале. Со своими библиотеками и ни с чем не совместимый).

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

И тут я натыкаюсь на библиотеку opencm3. С открытыми исходниками, поддерживает мой процессор, как и кучку других и даже примеры есть. Уже наученный горьким опытом, полез в исходники. Библиотека сделана по принципу SPL, но как-то более изящней что ли. То есть библиотека по сути является набором функций-оберток вокруг трахтибидохов с регистрами. Но с документацией. Пусть скудной, пусть не всегда полной, но весь есть что почитать! На фоне остального бардака это выглядело просто изумительно.

В итоге код практически приблизился по внятности к ардуинному:

rcc_periph_clock_enable(RCC_GPIOС);
gpio_mode_setup(GPIOС, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO8 | GPIO9);
gpio_set(GPIOС, GPIO8|GPIO9);

Попробовал на этой библиотеке сделать АЦП – работает. ЦАП – работает. USART – работает. Даже таймеры удалось сконфигурировать без шаманства!

В общем, пока я категорически рекомендую эту библиотеку. Стало сильно проще работать.

Прочитавший это “ардуинщик” легко может ужаснуться и подумать “нафиг-нафиг. лучше я уж потихоньку, полегоньку на старом добром avr переживу”. Зря. Да, ST еще те редиски и специально делают все так, что бы их продукция была доступна только профессионалам, имеющим возможность потратить кучу времени и денег на изучение платформы. Зато в ответ вы получаете доступ к дешевым и мощным микроконтроллерам, которые могут делать столько интересных вещей, которых на avr попросту нет. И поверьте, даже прочитав описание какого-нибудь контроллера типа “имеет 5 таймеров”, вы никогда не узнаете, что с этими таймерами можно делать, пока не попробуете и не залезете в документацию. Ведь архитектура stm позволяет скрещивать функционал внутри самым причудливым образом. И к примеру, можно сделать так, что бы один и тот же таймер в одной прошивке мигал светодиодиком, а в другой читал показания енкодера моторчика …

Как говорится, продолжение следует.