Как написать библиотеку stm32

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

Я долго не мог придумать, что же такое взять в качестве примера из того, что у меня самого не реализовано. И с удивлением обнаружил, что у меня нет библиотеки для классического текстового LCD на Hitachi HD44780. Это 1-но, 2-х или 4-х строчные дисплеи до 20 символов на строку. Те самые, которые все так любят втыкать во все свои DIY.

Полазил по просторам и с ещё большим удивлением обнаружил, что все реализации для шины I2C основаны на дурацкой классической библиотеке Arduino LiquidCrystal_I2C. Ну, думаю, сам Бог велел!

Документация

Начнём с главного: чтения документации.

Как работает дисплей

Дисплей основан на старинном чипе от HITACHI HD44780. У него нет последовательного интерфейса, как, например, у ST7920. Тем не менее, он прост до безобразия.

Открываем даташит раздел “Interfacing to the MPU” и видим примерную диаграмму, как устроен обмен данными. Смотрим, смотрим и видим фигу. Но всё-таки что-то почерпнуть можно.
Скрин

Базовый его режим 8-ми битный. Т.е. мы можем передавать ему 1 байт за раз. Для чего у него есть восемь ног DB0-DB7. Ещё у него используются 3 ноги:

  • E: выдаём строб (импульс), который сообщает дисплею, что на ногах DB0-DB7 выставлены нужные данные, мол, давай, считывай
  • RS: Сообщаем дисплею, что мы хотим передать или считать, команду или конфигурацию
  • R/W: Сообщаем дисплею, пишем мы данные или считываем

На схеме показывается 4-битный режим. Это когда мы используем 4 ноги DB4-DB7 вместо восьми и передаём два раза по 4 бита. Режим полезен там, где жалко отдавать лишние ноги у МК. Или для нашего расширителя портов на PCF8574.

Пытливый ум заметит, что сначала мы передаём старшие 4 бита, потом младшие. Также обратит внимание, что для передачи данных на ноге R/W должен быть 0, а для чтения 1.

Итак, как же выглядит передача данных в 8-битном режиме:

  • Для передачи команды дисплею, на ноге RS мы выставляем 0. Если надо передать символ, выставляем 1;
  • Если мы передаем команду или данные, то выставляем 0 на ноге R/W;
  • На ногах DB0-DB7, мы выставляем значения побитово того, что хотим передать;
  • Выдаём строб (импульс) на ноге E;
  • Документация рекомендует после строба считывать готовность дисплея к приёму следующей команды.

Как же выглядит передача данных в 4-битном режиме:

  • Для передачи команды дисплею, на ноге RS мы выставляем 0. Если надо передать символ, выставляем 1;
  • Если мы передаем команду или данные, то выставляем 0 на ноге R/W;
  • На ногах D4-D7 дисплея, мы выставляем значения старших 4-х бит, что хотим передать;
  • Выдаём строб (импульс) на ноге E;
  • На ногах D4-D7 дисплея, мы выставляем значения младших 4-х бит, что хотим передать;
  • Выдаём строб (импульс) на ноге E;
  • Документация рекомендует после двух стробов считывать готовность дисплея к приёму следующей команды.

Я тут накидал диаграмку, как передаются данные в 4-х битном режиме. Передаём два байта 0xA6 и 0xE9.

Скрин

Обратите внимание, нельзя вот просто так взять и щёлкнуть стробом. Нужно помнить, что ширина строба и пауза между ними должны соответствовать паспортным данным. Идём в даташит и ищем что-то похожее на delay, timeout, execution time и т.д. Обязательно даются такие данные. Находим табличку “Table 6: Instructions” и видим, что на исполнение команды требуется от 37мкс до 41мкс. На возврат курсора в начало экрана требуется 1.52мс. Также при хаотичном листании документа в поисках информации, какая же должна быть пауза, находим в диаграмме “Figure 24: 4-Bit Interface” это:

When BF is not checked, the waiting time between
instructions is longer than the execution instuction
time. (See Table 6.)

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

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

Как с ними работать, будем смотреть позже. Сейчас нас волнует подключение и протокол.

Подключение дисплея к шине I2C

Но нам вот жалко отдавать 7 ног МК (в 4-битном режиме) на дисплей. И кто-то взял и придумал копеешный модуль, который цепляет дисплей к I2C и сохраняет старый протокол.

Основан он на расширителе портов PCF8574. Вещь простая до безобразия. У него есть 8 ног, на которых мы можем выставлять 0 или 1. По I2C мы тупо шлём один байт на него, где каждый бит соответствует ноге. Либо тупо считываем такой же байт с текущим состоянием этих самых ножек.

Так вот модуль подключен по аналогичной схеме (я реализовывал это у себя на плате года два назад):
Скрин

Пытливый ум, глядя на эту схему, задастся вопросом: А как же строб выдавать? Да ещё тайминги соблюдать. Да и вообще, как дрыгать ножками RS да R/W, чтоб не мешать данным и не сводить с ума дисплей? А вот тут и начинается самое интересное.

Ход мыслей такой. Давайте сначала заглянем в документацию PCF8574 и поищем там диаграмму по обмену данными. Находим прекрасную картинку:
Скрин

Внимательно смотрим и видим, что состояние на ногах меняется сразу по окончании приёма байта от МК. Т.е. нам нужно передать данные и выставить ногу P2 в высокий уровень чтобы включить строб. Потом передать данные и выставить P2 уже в ноль, т.е. строб мы выключаем. А для этого нам надо разобраться, что такое шина I2C и с чем её едят.

Шина I2C

Откровенно говоря, не люблю я её. Использую только там, где нет альтернативы. Скорость небольшая, ёмкость линии ограничена 400пФ, в результате длина линии очень маленькая. К тому же сама суть протокола имеет существенный недостаток, об этом позже. Для каждого готового устройства приходится вручную подбирать номиналы подтягивающих резисторов. В этом плане SPI гораздо удобнее и круче, хоть и требует минимум 3-х ног. Ладно, к сути.

I2C — это асимметричная последовательная шина, использует две двунаправленные линии. Т.е. данные передаются последовательно в одном направлении. Обе линии подтянуты к питанию. Шина типа “Ведущий-Ведомый”. Теоретически возможно ситуация, когда хоть все устройства могут быть как ведущим, так и ведомым. На практике реализовать это почти невозможно.

Для понимания работы, надо сначала запомнить правила:

  • Данные на линии SDA могут меняться только при низком уровне на линии SCL
  • Пока на линии SCL высокий уровень, на линии SDA данные не меняются
  • Утрируя, есть три состояния: СТАРТ, СТОП и передача данных.
  • Формировать сигналы СТАРТ и СТОП может только ведущий, даже в случае приёма им данных от ведомого
  • Адрес ведомого устройства состоит из 7-ми бит.

Сигнал СТАРТ — это перевод линии SDA в низкий уровень при высоком уровне линии SCL.

Сигнал СТОП — перевод линии SDA в высокий уровень также при высоком уровне SCL.

Т.о. для начала передачи данных ведомогу, ведущий формирует сигнал СТАРТ. Все ведомые устройства на линии начинают слушать. Затем ведущий выстреливает адрес ведомого, с которым он хочет поговорить и сажает SDA на ноль. Адрес этот, как видно по картинке выше, занимает старшие 7 бит, а последний бит задаёт читаем мы данные или пересылаем. Если устройство на линии есть, оно удержит линию SDA в низком уровне, это значит, что оно готово общаться. Тоже самое и по окончании приёма данных. По окончании передачи ведущий формирует сигнал СТОП.

Вот тут и кроется главная проблема шины I2C. После передачи данных, если ведомый занят, он может продолжать удерживать линию SDA. Ведомый также может удерживать и SCL, если он не успевает обрабатывать данные, т.е. ведомый может снижать скорость передачи данных. По стандарту, устройства должны управлять линиями по схеме Open Drain. И даже если какое-то устройство монопольно займёт линию, другое сможет её сбросить. Теоретически. На практике же, если, например, ведомый подвис и держит линию, а мы поднимаем её на ведущем, оживить ведомого порой можно только reset’ом. Там вообще такие бывают дичайшие комбинации, что однажды даже пришлось прокидывать отдельную линию RESET для ведомых устройств и периодически их дергать.

Итак. Более менее и в общих чертах мы разобрались с I2C. На wiki есть неплохая статья, да и вообще погуглите, шина непростая, я дал лишь общую информацию для понимания вопроса.

Приступаем к написанию библиотеки

Вот и настал момент, когда мы почти готовы написать первые строки кода. Давайте сначала посмотрим, как устроены другие библиотеки.
Мы же ленивые и надеемся обойтись малой кровью.

Откроем классическую Arduino LiquidCrystal_I2C. Просто бегло пройдём по ней глазками. Не знаю, как у вас, у меня сразу глаз цепляется за несколько вещей:

  • Используются аппаратные задержки
  • Куча однотипных функций
  • Нет никаких оптимизаций по экономии потребления памяти
  • Нет контроля ошибок
  • Нет вменяемых комментариев

Если мы просто пороемся на GitHub в поисках библиотек для STM32, почти все они будут на основе этой же LiquidCrystal_I2C. С теми же недостатками. Я не буду глубоко туда влезать, я просто сделаю всё по-своему.

Итак, составим требования к нашей библиотеке:

  • Никаких аппаратных задержек
  • Использовать DMA для передачи данных
  • Минимум функций, максимально выносить всё в #define
  • Максимально экономим память
  • Каждое обращение к дисплею должно контролироваться

Создаём проект

Для начала надо создать проект. Я уже написал инструкцию, как правильно настроить STM32CubeMX у себя в блоге, не буду повторяться тут. Полностью проект с уроком доступен в моем репо на GitHUB.

Отмечу только, что урок написан для отладочной платы на STM32F303VC. У меня сейчас нет под рукой STM32F103C8, так что всё проверял на STM32F3DISCOVERY. Но адаптировать под любую другую плату можно без особых проблем.

Дальше, конечно, мы можете взять готовую библиотеку, я её выложил на GitHub. Я вкратце напишу, что я делал.

Создадим два файла:

Inc/lcd_hd44780_i2c.h
Src/lcd_hd44780_i2c.c

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

Скрин

Отлично! Всё написано по шагам, с таймингами и даже биты указаны! Но мы любопытные и хотим сразу знать, что же битики значат, чтобы сразу заполнить заголовочный файл #define’ами. Вспоминаем про “Table 6: Instructions”. Там прям идеально, с комментариями, расписаны все биты команд.

Открываем наш заголовочный файл и предварительно накидываем:

#define LCD_BIT_RS                 ((uint8_t)0x01U)
#define LCD_BIT_RW                 ((uint8_t)0x02U)
#define LCD_BIT_E                  ((uint8_t)0x04U)
#define LCD_BIT_BACKIGHT_ON        ((uint8_t)0x08U)
#define LCD_BIT_BACKIGHT_OFF       ((uint8_t)0x00U)

#define LCD_MODE_4BITS             ((uint8_t)0x02U)
#define LCD_BIT_1LINE              ((uint8_t)0x00U)
#define LCD_BIT_2LINE              ((uint8_t)0x08U)
#define LCD_BIT_4LINE              LCD_BIT_2LINE
#define LCD_BIT_5x8DOTS            ((uint8_t)0x00U)
#define LCD_BIT_5x10DOTS           ((uint8_t)0x04U)
#define LCD_BIT_SETCGRAMADDR       ((uint8_t)0x40U)
#define LCD_BIT_SETDDRAMADDR       ((uint8_t)0x80U)

#define LCD_BIT_DISPLAY_CONTROL    ((uint8_t)0x08U)
#define LCD_BIT_DISPLAY_ON         ((uint8_t)0x04U)
#define LCD_BIT_CURSOR_ON          ((uint8_t)0x02U)
#define LCD_BIT_CURSOR_OFF         ((uint8_t)0x00U)
#define LCD_BIT_BLINK_ON           ((uint8_t)0x01U)
#define LCD_BIT_BLINK_OFF          ((uint8_t)0x00U)

#define LCD_BIT_DISP_CLEAR         ((uint8_t)0x01U)
#define LCD_BIT_CURSOR_HOME        ((uint8_t)0x02U)

#define LCD_BIT_ENTRY_MODE         ((uint8_t)0x04U)
#define LCD_BIT_CURSOR_DIR_RIGHT   ((uint8_t)0x02U)
#define LCD_BIT_CURSOR_DIR_LEFT    ((uint8_t)0x00U)
#define LCD_BIT_DISPLAY_SHIFT      ((uint8_t)0x01U)

Это та самая нудная часть работы, о которой я говорил. Внимательно смотрим в табличку, двоичный код переводим в HEX. Поясню на примере:

Инструкция Display on/off control требует всегда выставленного бита DB3. Открываем калькулятор, вводим двоичное 1000 и получаем 0x08 HEX.

В самой инструкции есть три команды:

  • Display on/off
  • Cursor on/off
  • Blinking of cursor position character

Калькулятором высчитываем их HEX и будем их потом суммировать с LCD_BIT_DISPLAY_CONTROL.

Биты RS, RW, E и Backlight относятся к PCF8574, так что не забываем прописать и их.

Позже аналогичным способом напишем и остальные #define.

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

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

typedef struct {
    I2C_HandleTypeDef * hi2c;  // I2C Struct
    uint8_t lines;             // Lines of the display
    uint8_t columns;           // Columns
    uint8_t address;           // I2C address shifted left by 1
    uint8_t backlight;         // Backlight
    uint8_t modeBits;          // Display on/off control bits
    uint8_t entryBits;         // Entry mode set bits
} LCDParams;

Обратите внимание. В этом struct мы храним не саму структуру для I2C, а лишь указатель. Т.о. мы не дублируем данные и всегда под рукой их состояние.

Судя по алгоритму инициализации, первые этапы уникальны и можно реализовать их тупо отправляя данные через базовые функции HAL. Их мы реализуем в функции lcdInit().

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

А вот и реализация в готовой библиотеке.

/**
 * @brief  Local function to send data to display
 * @param  rsRwBits State of RS and R/W bits
 * @param  data     Pointer to byte to send
 * @return          true if success
 */
static bool lcdWriteByte(uint8_t rsRwBits, uint8_t * data) {

    /* Higher 4 bits*/
    lcdCommandBuffer[0] = rsRwBits | LCD_BIT_E | lcdParams.backlight | (*data & 0xF0);  // Send data and set strobe
    lcdCommandBuffer[1] = lcdCommandBuffer[0];                                          // Strobe turned on
    lcdCommandBuffer[2] = rsRwBits | lcdParams.backlight | (*data & 0xF0);              // Turning strobe off

    /* Lower 4 bits*/
    lcdCommandBuffer[3] = rsRwBits | LCD_BIT_E | lcdParams.backlight | ((*data << 4) & 0xF0);  // Send data and set strobe
    lcdCommandBuffer[4] = lcdCommandBuffer[3];                                                 // Strobe turned on
    lcdCommandBuffer[5] = rsRwBits | lcdParams.backlight | ((*data << 4) & 0xF0);              // Turning strobe off


    if (HAL_I2C_Master_Transmit_DMA(lcdParams.hi2c, lcdParams.address, (uint8_t*)lcdCommandBuffer, 6) != HAL_OK) {
        return false;
    }

    while (HAL_I2C_GetState(lcdParams.hi2c) != HAL_I2C_STATE_READY) {
        vTaskDelay(1);
    }

    return true;
}

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

И вот, что получается на деле:
Скрин

При таком раскладе и скорости шины I2C в 100кбит, ширина строба ~180мкс, пауза между стробами ~90мкс, и пауза между парными стробами ~530мкс. По идее, и я так думал предварительно, можно не удлинять строб на два байта, обойтись одним. Но на деле оказалось, что 90мкс мало для ширины строба, но достаточно для паузы между стробами. Похоже, что кварц в дисплее работает на более низкой частоте, чем положено по даташиту. Как говорил — Китай такой Китай =( А может мой дисплей дурит.

Также можно сократить длинную паузу раза в два, для этого есть два способа:

  • Использовать прерывания и циклом фигачить побайтово прямо в регистр. Но это постоянные прерывания с обработчиками, на больших данных будут блокировки. А я этого ой как не люблю. Я предпочитаю отправить данные через DMA и забыть о них. И начать заниматься другими делами, пусть МК сам разруливает.
  • Либо создать большущий буфер на отправку, для 20 символьного дисплея это будет порядка 120 байт. Надо будет просто подготовить данные в буфере и отправить одним выстрелом в DMA. Но я решил экономить память.

Но нас интересует вопрос, я так ругал Ардуиновскую библиотеку, а есть ли выигрыш? А вот смотрите, что показывает LiquidCrystal_I2C:
Скрин

Комментарии излишни. Но ведь я учу вас критическому мышлению, не так ли? Что если оптимизировать код библиотеки Ардуино? Да! И я уверен, получится значительно улучшить параметры передачи. Делайте, думаю стотыщпятьсот людей вам скажут спасибо. Ведь я к чему это всё говорю. К тому, что в этом и есть беда Ардуино — такой вот код, где никто не думает об оптимизациях. Причём ведь делает вид, что всё согласно даташиту. Например, зачем нужны задержки между передачами в 50мкс, если в реальности между стробами 500мкс??

У пытливого ума опять же возникает вопрос, а есть выигрыш на большой передаче? А вот смотрите, сверху STM32, а снизу LiquidCrystal_I2C, данные одинаковые, процедура инициализации тоже:
Скрин
Скрин

Итог: STM32 83мс, LiquidCrystal_I2C 122мс.
Повторю, если использовать прерывания вместо чистого DMA, можно получить ещё больший выигрыш, думаю вполне реально сократить это время до 60мс. Но надо ли? С таким дисплеем и его откликом это уже за гранью добра и зла =)

Что ещё интересного в библиотеке

Я написал одну единственную функцию, которая занимается командами. Это функция lcdCommand().

Она занимается как установкой параметров, так и снятием. В качестве входных параметров, у неё команда и флаг — снять или выставить команду. Оба параметра — это нумерованные списки LCDCommands и LCDParamsActions.

Обратите внимание, никаких if/else. Всё сделано на Switch/Case. Несмотря на то, что их аж три штуки и приходится как минимум дважды проверять команду, работает это невероятно быстро. И причина тому — Бинарное дерево, в которое компилятор транслирует наш код. По сути, там всего два узла, так что поиск происходит за несколько тактов.

Конечно, вы можете использовать и запись типа if (command == LCD_DISPLAY), это также будет откомпилировано в бинарное дерево, но такой код читается хуже.

В результате мы получили возможность через #define определить прототипы функций, с коротким написанием и удобным чтением:

/* Function defines */
#define lcdBacklightOn()           lcdBacklight(LCD_BIT_BACKIGHT_ON)
#define lcdBacklightOff()          lcdBacklight(LCD_BIT_BACKIGHT_OFF)
#define lcdAutoscrollOn()          lcdCommand(LCD_DISPLAY_SHIFT, LCD_PARAM_SET)
#define lcdAutoscrollOff()         lcdCommand(LCD_DISPLAY_SHIFT, LCD_PARAM_UNSET)
#define lcdDisplayClear()          lcdCommand(LCD_CLEAR, LCD_PARAM_SET)
#define lcdDisplayOn()             lcdCommand(LCD_DISPLAY, LCD_PARAM_SET)
#define lcdDisplayOff()            lcdCommand(LCD_DISPLAY, LCD_PARAM_UNSET)
#define lcdCursorOn()              lcdCommand(LCD_CURSOR, LCD_PARAM_SET)
#define lcdCursorOff()             lcdCommand(LCD_CURSOR, LCD_PARAM_UNSET)
#define lcdBlinkOn()               lcdCommand(LCD_CURSOR_BLINK, LCD_PARAM_SET)
#define lcdBlinkOff()              lcdCommand(LCD_CURSOR_BLINK, LCD_PARAM_UNSET)
#define lcdCursorDirToRight()      lcdCommand(LCD_CURSOR_DIR_RIGHT, LCD_PARAM_SET)
#define lcdCursorDirToLeft()       lcdCommand(LCD_CURSOR_DIR_LEFT, LCD_PARAM_SET)
#define lcdCursorHome()            lcdCommand(LCD_CURSOR_HOME, LCD_PARAM_SET)

А вообще, совет. Там, где у вас чётко обозначенные варианты, использовать switch/case, там, где необходимо сравнивать величины, использовать if/else. И не стесняйтесь нумерованных списков enum —
они занимают очень мало памяти. Компилятор сам подбирает тип, но всё также, как с обычными целочисленными переменными, чем больше список, тем больше разрядность.

Почему не проверяем готовность дисплея, как в даташите

А потому, мои дорогие, что в случае с I2C это лишено смысла. Посмотрите на реальную передачу. На один только запрос уходит минимум 1 байт плюс ещё байт на адрес. Итого 180мкс. Для проверки готовности мы сначала должны выставить R/W в 1, потом еще щелкать стробами и внутри 1-го строба проверять бит BF на ноге DB7. Посчитали? Это при том, что по документации занят дисплей от 37мкс до 1,52мс. Проще просто использовать трюк с I2C.

Что можно придумать с русскими символами

У нас есть только возможность загрузить своих 8 символов. Я с этим сталкивался и, скажу, это нелегкий выбор =) Для этого в дисплее есть доступный EPROM на 8 ячеек. Каждая в каждую ячейку можно записать символ из 8 строк по 5 точек в каждой. Соответственно, это массив из 8 байт, где младшие 5 бит и есть наши точки. На самом деле, последняя строка — это курсор, так что, если уж соответсвовать стандартам, на символ можно использовать 5х7 точек. Вот схема из даташита (Example of Correspondence between EPROM Address Data and Character Pattern (5 × 8 Dots)):

Скрин

Например, символ Д в HEX будет такой:

uint8_t symD[8]   = { 0x07, 0x09, 0x09, 0x09, 0x09, 0x1F, 0x11 }; // Д

Соответственно загружаем его в CGRAM функцией:

lcdLoadCustomChar(0, &symD);

и выводим функцией:

Ну а как просто вывести текст?

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

Код символа в C/С++ определяется, если взять его в одиночные кавычки, например, ‘B’. Либо просто перебором берём из строки &data[i].

/**
 * @brief  Print string from cursor position
 * @param  data   Pointer to string
 * @param  length Number of symbols to print
 * @return        true if success
 */
bool lcdPrintStr(uint8_t * data, uint8_t length) {
    for (uint8_t i = 0; i < length; ++i) {
        if (lcdWriteByte(LCD_BIT_RS, &data[i]) == false) {
            return false;
        }
    }

    return true;
}

В готовом виде это:

lcdSetCursorPosition(3, 0);
lcdPrintStr((uint8_t*)"Hello, World!", 13);

lcdSetCursorPosition(3, 1);
lcdPrintStr((uint8_t*)"Hello, GitHub!", 14);

lcdSetCursorPosition(1, 2);
lcdPrintStr((uint8_t*)"LCD Display example", 19);

lcdSetCursorPosition(2, 3);
lcdPrintStr((uint8_t*)"by Comrade Bulkin", 17);

Обратите внимание. Мы отправляем в функцию не строку, а указатель на массив uint8_t. Т.е. мы, во-первых, создаём строку в памяти, во-вторых, преобразуем строку в unsigned int, в-третьих, отправляем указатель на неё. Это, конечно, вариант для примера. В боевых устройствах использовать такую запись плохой тон. Т.к. во-первых, мы используем динамическое выделение памяти, что само по себе в условиях крайне её ограниченности не айс. Лучше стараться заранее выделить память под некоторые переменные. А во-вторых, приходится вручную пересчитывать размер строки. Так что хорошим тоном будет примерно так:

lcdInit(&hi2c2, (uint8_t)0x27, (uint8_t)4, (uint8_t)20);

/* Выведем предопределённую строку */
static const char helloWorld[] = "Hello, world!";
lcdSetCursorPosition(3, 0);
lcdPrintStr((uint8_t*)helloWorld, strlen(helloWorld));

/* Выведем строку по формату */

// Буффер, куда будем записывать генерируемый текст
// Размер буффера - макс. количество символов в строке + 1 на терминальный символ 
char buffer[21];
    
/**
* В отличие от sprintf, snprintf() в конце строки добавляет символ конца строки 
* и можно использовать функцию strlen() для подсчета длины строки.
*/
snprintf(buffer, (size_t)21, "Hello, %s", "GitHub!");  // Будет "Hello, GitHub!"

lcdSetCursorPosition(3, 1);
lcdPrintStr((uint8_t*)buffer, strlen(buffer));

/* Ну и выведем просто две строки текста "в лоб" */
lcdSetCursorPosition(1, 2);
lcdPrintStr((uint8_t*)"LCD Display example", 19);

lcdSetCursorPosition(2, 3);
lcdPrintStr((uint8_t*)"by Comrade Bulkin", 17);

Немного о комментариях в коде

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

Конечно, глупо комментировать каждую строчку. Но хорошим тоном, по моему опыту, являются:

  • Перед каждой функцией писать стандартный заголовок с тегами @brief и @note. В них стоит описать что это за функция и как она работает. Там же дать описания переменных и что она возвращает. Во многих современных редакторах есть плагин типа Docblockr. Просто перед функцией пишете /** и плагин сам создаёт отформатированный заголовок, вам нужно только дописать ручками несколько строк.
  • Давать отсылки на переменные из других файлов и документацию
  • Если алгоритмов для реализации несколько, напишите, почему выбрали конкретный. Сильно упрости общение с другими в будущем.
  • Добавляйте комменты для выделения этапов и всяких неочевидных вещей

Я сейчас дописываю документацию к библиотеке, читать её можно будет тут.

Напоследок

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

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

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

Оригинал статьи опубликован на Pikabu

Уроки STM32

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

Предыдущий урок     Список уроков     Следующий урок

В предыдущем уроке мы разработали класс Debounce для обработки дискретных сигналов. С помощью него можно устранять дребезг контактов, фильтровать сигнал, ослаблять влияние электромагнитных помех и т.п.

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

Выход очевиден – оформить класс библиотекой. Давайте этим и займемся.

Последовательность действий для создания библиотеки STM32.

Библиотека, по крайней мере, на нашем уровне программирования, это тот же класс, только размещенный в других файлах. В текст программы файлы библиотеки включаются директивой препроцессора #include. Таким образом, текст библиотеки в нашей программе мы не видим. А на этапе компиляции он “подсовывается” компилятору директивой #include . И благополучно транслируется вместе с остальным исходным кодом.

Я скопировал проект предыдущего урока Lesson13_1 в новую папку Lesson14.

Переименовал его в Lesson14_1.

Открыл в Atollic TrueStudio и переименовал его в IDE еще раз. Все это мы уже делали.

Atollic TrueStudio

Получился проект с классом Debounce из предыдущего урока. Этот класс мы будем оформлять библиотекой.

Библиотека состоит как минимум из двух файлов:

  • заголовочного файла, с расширением .h ;
  • файла исходного текста, расширение .cpp.

Заголовочный файл должен содержать описание класса, объявление переменных, константы. Программный код в нем не размещают.

Второй файл (.cpp) содержит код методов класса.

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

Правой кнопкой мыши нажимаем на имя проекта, New -> Folder.

Создание новой папки

Задаем имя папки Libraries.

Теперь нажимаем на Libraries, New -> Folder.

Создаем папку Debounce.

Теперь выбираем папку Debounce, New -> File.

Создаем в ней файлы Debounce.h  и Debounce.cpp.

IDE

Заголовочный файл Debounce.h.

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

/*
* Debounce.h — библиотека обработки дискретных сигналов STM32.
*
* Может быть использована для устранения дребезга контактов, цифровой фильтрации сигналов от помех.
*
* Работает в фоновом режиме.
*
* В параллельном процессе регулярно должен вызываться один из методов обработки:
*
* void scanStability(void); // метод ожидания стабильного состояния сигнала
* void scanAverage(void); // метод фильтрации сигнала по среднему значению
*
* В результате формируются признаки состояния сигнала:
*
* uint8_t flagLow; // признак СИГНАЛ В НИЗКОМ УРОВНЕ
* uint8_t flagRising; // признак БЫЛ ПОЛОЖИТЕЛЬНЫЙ ФРОНТ
* uint8_t flagFalling; // признак БЫЛ ОТРИЦАТЕЛЬНЫЙ ФРОНТ
*
* Признаки могут быть прочитаны функциями:
*
* uint8_t readFlagLow(void); // чтение признака СИГНАЛ В НИЗКОМ УРОВНЕ
* uint8_t readFlagRising(void); // чтение признака БЫЛ ПОЛОЖИТЕЛЬНЫЙ ФРОНТ
* uint8_t readFlagFalling(void); // чтение признака БЫЛ ОТРИЦАТЕЛЬНЫЙ ФРОНТ
*
* Пример создания объекта:
* Debounce button(GPIOC, 1 << 11, 10); // экземпляр класса Debounce
*
* Подробно описана http://mypractic.ru/uroki-stm32 в уроках 12, 13, 14
*
* Разработана Калининым Эдуардом.
*
* http://mypractic.ru
*/

Дальше следует объявление класса, которое надо заключить в конструкцию.

/* Проверка, что библиотека не подключена */
#ifndef DEBOUNCE_H
#define DEBOUNCE_H

. . . . . . . . . . . . . . . . . . . 

#endif/* DEBOUNCE_H */

Это предотвратит повторное подключение библиотеки.

И еще необходимо подключить файл stm32f103xb.h. В нем содержатся CMSIS-имена регистров.

/* Проверка, что библиотека не подключена */
#ifndef DEBOUNCE_H
#define DEBOUNCE_H

#include «stm32f103xb.h»

class Debounce {

    . . . . . . . . .

};

#endif /* DEBOUNCE_H */

В файл Debounce.cpp мы копируем код методов. И в самом начале подключаем заголовочный файл. В нем содержится определение класса.

#include «Debounce.h»

//—————— методы класса Debounce

. . . . . . . . . . . . . . .

Все библиотека создана. Теперь необходимо подключить ее.

Прежде всего, удаляем из main.cpp все то, что мы переместили в библиотечные файлы. А именно: объявление класса и код методов.

Подключаем нашу библиотеку.

/* Private includes ———————————————————-*/
/* USER CODE BEGIN Includes */

#include «../Libraries/Debounce/Debounce.h»

Мы указали полный путь к папке библиотеки.

Можно написать короче:

#include «Debounce.h»

Но тогда путь к папке необходимо сообщить компилятору:

Project -> Build Settings -> Tool Settings -> C++ Compiler -> Directories -> Add (зеленый плюсик сверху).

Atollic TrueStudio

В списке появится путь к нашей библиотеке

../Inc
../Libraries/Debounce
../Drivers/STM32F1xx_HAL_Driver/Inc
../Drivers/STM32F1xx_HAL_Driver/Inc/Legacy
../Drivers/CMSIS/Device/ST/STM32F1xx/Include
../Drivers/CMSIS/Include

Еще вариант той же операции:

Правой кнопкой мыши нажимаем на папку Debounce в проекте, Add/Remove Include Path -> OK

В списке путей для include появится

«${workspace_loc:/${ProjName}/Libraries/Debounce}»

Транслируем проект. Получаем сообщения об ошибках компиляции.

Сообщение об ошибках

Компилятор не нашел коды для методов класса. Мы не сообщили ему, где находится файл Debounce.cpp.

Можно поместить этот  файл в папку Src. Компилятор привык там искать исходные тексты. Но мы не будем ломать стройности проекта.

Сделаем так: Project -> Build Settings -> C/C++ General -> Paths and Symbols -> Source Location -> Add Folder -> Libraries -> OK

Atollic TrueStudio

Другой способ, немного проще:

Правой кнопкой по Libraries, Properties -> C/C++ General -> Paths and Symbols -> Source Location -> Add Folder -> Apply

Свершилось. Теперь компилируется без ошибок.

Вот ссылка на полный проект:

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!

 

Применение библиотеки.

Давайте в этом разделе научимся использовать готовые библиотеки. Повторим часть действий из первой половины урока.

Нам надо реализовать задачу – на каждое нажатие кнопки светодиод меняет свое состояние.

Мы собираемся воспользоваться готовой библиотекой Debounce.

Загружаем ее архив.

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!

 

В нем 2 файла в папке Debounce.

С помощью STM32CubeMX создаем проект Lesson14_2.

  • Устанавливаем конфигурацию системы тактирования.
  • Вывод PB13 конфигурируем на активный выход.
  • Вывод PB12 (кнопка) не трогаем.

Конвертируем проект в C++.

Создаем папку Libraries и копируем в нее папку Debounce с файлами библиотеки.

Задаем в IDE путь к библиотеке. Правой кнопкой по Libraries, Properties -> C/C++ General -> Paths and Symbols -> Source Location -> Add Folder -> Apply

Открываем main.cpp.

Подключаем библиотеку Debounce.

/* Private includes ———————————————————-*/
/* USER CODE BEGIN Includes */

#include «../Libraries/Debounce/Debounce.h»

Создаем объект button.

/* Private variables ———————————————————*/
/* USER CODE BEGIN PV */

Debounce button(GPIOB, 1 << 12, 10); // экземпляр класса Debounce

Лучше запустить компиляцию, убедиться, что ошибок нет.

Теперь мы можем использовать объект button.

/* Infinite loop */
/* USER CODE BEGIN WHILE */

while (1)
{
    if(button.readFlagFalling() != 0)
        HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_13);

    button.scanAverage();
    HAL_Delay(1);
}

Все. Компилируем, загружаем в плату, проверяем.

Вот ссылка на проект:

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!

 

Папку Libraries со своим библиотеками удобно копировать в проект целиком, а не выбирать нужные библиотеки. Не подключенные директивой #include файлы компилироваться не будут.

В следующем уроке будем работать с прерываниями по таймеру. Поговорим о параллельных процессах.

Предыдущий урок     Список уроков     Следующий урок

Автор публикации


263

Комментарии: 1889Публикации: 183Регистрация: 13-12-2015

Добрый день. Решил написать ещё одну статейку по STM32. На этот раз мы поговорим о SPL -Standard Peripherals Library. Вообще на данный момент у нас есть 3 варианта написания наших программ для STM32

1)CMSIS — это стандартная библиотека для кортексов. То есть это стандарт. С помощью этой библиотеки мы можем писать наши программы — но только с прямой записью в регистры. Это самый профессиональный способ написания программ, и кстати самый правильный, но сложный, так как придётся от а до я учить даташиты, постоянно сидеть с открытыми доками на процессор чтобы найти куда записать тот или иной бит в регистры.

2)SPL -Standard Peripherals Library — это попытка ST Electronics выпустить одну общую библиотеку для объединения всех своих процессоров. ЧТобы было проще переносить код и т.д и т.п. Работать проще как для начинающего, но всё равно всё вбиваем ручками — Никакой Автоматики!

3)HAL — Hardware Acess Level — это вторая попытка ST Electronics выпустить единую библиотеку для разработки. Заодно с ней вышла и программа CubeMX для настройки всего этого хозяйства.Всё гладко и хорошо по началау, но только по началу — дальше всё, приехали — ни примеров, ни обзоров. Поэтому пока не исследуют эту библиотеку вдоль и поперёк- делов не будет)))

То есть смысл в том что SPL уже вдоль и поперёк всю распилили и изучили. Поэтому, хочешь не хочешь а учить нужно. И вообще нужно стремиться к CMSIS. Но это уже кому захочется))

Итак, давайте попробуем написать простейшую программу для мигания светодиодом с помощью SPL. Первое что нам понадобиться — Reference Manual и Datasheet на наш контроллер. Я этот пример буду писать для контроллера установленного на отладочной плате STM32F4 Discovery — STM32F407VGT6

Идём на сайт ST.com и вбиваем в поиск наш процессор. Качаем в найденном Datasheet и далее переходим на вкладку Design Resources и ниже ищем Reference Manual.

Далее, думаем, что нам нужно. Нужно нам помигать светодиодом. Раз помигать светодиодом, значит мы должны настроить нашу ножку к которой этот светодиод подключён. Вспоминаем (ну или если не знали то читаем) — что изначально все порты микроконтроллера отключены от тактирования, для экономии энергии. Нам же нужно включить тактирование на нужный нам порт. Светодиод у нас подключен к пину 12 порта D.

Открываем Reference Manual и пробцем искать интересные нам слова как GPIO, Bus, AHB…
Я в самом начале документа нашёл вот такую интересную информацию

Далее. Переходим уже к практике, чтобы сразу и писать программу и читать. Создаём проект в CooCox — Create New Project. Даём название проекту

Далее выбираем для чего мы пишем — Процессор или демо плата. В нашем случае Chip

Выбираем нужный нам контроллер и нажимаем FINISH

Открывается окно репозитория, где нам предлагают выбрать нужные нам библиотеки. Так как нам нужно подать тактирование на порт подключаем библиотеку RCC, Далее — мы же будем работать с портами ввода-вывода, значит подключаем библиотеку GPIO. Также включаем CMSIS BOOT.

Первым делом нам нужно подключить заголовочные файлы
#include «stm32f4xx.h»
#include «stm32f4xx_gpio.h»
#include «stm32f4xx_rcc.h»

Далее, нам нужно проинициализировать нужную переферию. Можно для этого написать отдельную функцию типа init_gpio(), в ней описать всю инициализацию, и потом вызвать её в mainn(), а можно чтобы по простому пока — прямо в main() и написать всю инициализацию. Так и сделаем.

Первое что нам нужно, это подать тактирование на наш portD. Как мы помним — порты ввода-вывода у нас сидят на шине AHB1. Для простоты в CooCox есть примеры инициализаций прямо в репозитории. Вверху на вкладке открываем опять страницу репозитория, где мы библиотеки подключали. Нажимаем на библиотеку RCC, и справа мы видим большой перечень функций. Ищем вот такую RCC_AHB1PeriphClockCmd, нажимаем на неёи видим её полное описание. Выделяем и копируем её в наш проект
void RCC_AHB1PeriphClockCmd ( uint32_t RCC_AHB1Periph, FunctionalState NewState);

Убираем слово void из начала нашей строчки, и далее вписываем два аргумента. Первый Что мы включаем, второй Enable — типо включаем. Кстати начиная писать начало аргумента, нижимете Ctrl+Пробел и у вас вылазит подсказка для упрощения работы.

Итак, тактирование на наш порт D подали. Теперь нам нужно настроить ножку нашего порта. Параметры нашей ножки такие — Выход, Push-Pull, Низкая скорость до 2MHz

Первое, что нам нужно, это обозначить структуру, которая будет хранить настройки нашего порта.Давайте её назовём PIN_INIT. Что такое структура в языке C — читайте в Google!

GPIO_InitTypeDef PIN_INIT;

Теперь нам нужно инициализировать структуру, которую мы только что создали.
GPIO_StructInit (&PIN_INIT);

& — это значок показывает нам что это указатель

Теперь нам нужно нужно заполнить эту структуру.
PIN_INIT.GPIO_Mode = GPIO_Mode_OUT; //режим — выход
PIN_INIT.GPIO_OType = GPIO_OType_PP ; //тип — Push-Pull
PIN_INIT.GPIO_Pin = GPIO_Pin_12; // — пин 12
PIN_INIT.GPIO_Speed = GPIO_Speed_2MHz; //Низкая скорость работы

Ну и осталось записать данные в структуру командой
GPIO_Init(GPIOD, &PIN_INIT); //инициализируем

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

Вот наша примитивная задержка

void Delay_ms(uint32_t ms)
{
int i;
for(i=0;i<ms;i++){}
}

Ну и теперь осталось только устанавливать нашу ножку в единицу, и сбрасывать в ноль.
Устанавливаем ножку в единицу такой командой — GPIO_SetBits(GPIOD, GPIO_Pin_12); //установка ножки 12 в единицу

Сбрасываем ножку в ноль такой командой — GPIO_ResetBits(GPIOD, GPIO_Pin_12); //сброс ножки 12 в ноль

Вот код программы в цикле while(1)

while(1)
{
GPIO_SetBits(GPIOD, GPIO_Pin_12);
Delay_ms(65535);
GPIO_ResetBits(GPIOD, GPIO_Pin_12);
Delay_ms(65535);
}

Ну вот собственно и всё на сегодня. Надеюсь будет полезно и время потрачено не зря. И главное что старайтесь прививать себе — это чтение документации. Даташиты и reference manual должны быть вашими настольными книгами!
Готовый проект в Coocox по ссылке — yadi.sk/d/jlvSrFiCiaavv

Приветствую всех читателей моей первой статьи. Меня зовут Назаров Александр, я программист и резидент Ресурсного центра робототехники — структурного подразделения Донского государственного технического университета. Наши проекты направлены, в основном на мобильную робототехнику и его составляющие: изготовление механических узлов и их сборка, проектирование электрических схем и программирование микроконтроллеров.

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

Рендер платформы EduBot.

Рендер платформы EduBot.

Первый шаг — стандартизировать обучающие материалы — так появился «EduBots» — двухколесный робот и периферийная библиотека для обучения резидентов центра. Робот представляет собой двухколесную платформу 150х150х70 мм массой 550 грамм с двумя опорными роликами, реализован по дифференциальной схеме. В движение робот приводится двумя электромоторами с датчиками углового перемещения (энкодерами). Напряжение питания системы составляет 12,6 В и обеспечивается встроенными аккумуляторами.

Для управления платформой используется микроконтроллер STM32F401 на базе ядра Cortex-M4. Рабочая частота микроконтроллера – 84 МГц. Периферия позволяет подключать различные внешние устройства, работающие по интерфейсам USART, I2C, SPI. На платформе установлено 5 аналоговых датчиков линии, помимо этого у пользователей есть возможность подключения до 8 дополнительных аналоговых сенсоров и прочего оборудования (одноплатных компьютеров, лидаров и камер). Разработка проходила в несколько этапов, а испытания в рамках обучения новых резидентов позволяло оперативно получать информацию о недоработках и слабых местах платформы.

Важным компонентом проекта «EduBot» стала разработанная периферийная библиотека FIL (Fast Initializarion Library) для микроконтроллеров STM32. Основная задача библиотеки — облегчить процесс написания кода инициализации и отладки роботов за счет повышения уровня абстракции программного кода минимально влияющего на время обработки инструкций. Отличие библиотеки FIL от других наиболее популярных библиотек(HAL, SPL) — наличие функций и макросов, оптимизированных для выполнения различных задач робототехники: управление двигателем, обработка цифровых и аналоговых датчиков, подключение устройств по шине I2C и много других. Другой причиной создания — предотвратить «стихийную разработку» резидентов. Обучающиеся находясь на начальном этапе, изучая принципы функционирования и работы периферии микроконтроллера с базисных основополагающих шагов (работа с регистрами микроконтроллера), зачастую полагаются лишь на библиотеку CMSIS, и, как следствие, тратят много временных ресурсов. Таким образом, библиотека FIL преподносится как удобный и оптимизированный инструмент для разработчика. Но обо всем по порядку.

Архитектура проекта с библиотекой FIL можно представлена на диаграмме ниже:

Архитектура проекта с библиотекой FIL.

Архитектура проекта с библиотекой FIL.

Структура проекта содержит несколько функциональных блоков:

  • Блок проекта — к нему относятся всеми известные файлы main.c, main.h и другие файлы проекта. Непосредственно в них ведется написание логики работы робота и сценарии его поведения;

  • Блок CMSIS определений — необходим для облегчения доступа к периферии через стандартизированные идентификаторы. Данные файлы и их содержимое задействуются при работе библиотеки FIL;

  • Блок ядра библиотеки FIL — включает в себя файл-линкер для подключения файлов с API библиотеки согласно требованиям конфигурации;

  • Блок конфигурации робота — содержит, как правило, от одного до двух файлов. Первый — карта портов, она содержит определения необходимых портов микроконтроллера с указанием пользовательского идентификатора (Label). Второй файл конфигурирования необходим для указания требований библиотеке, проще говоря, какие API необходимы в данном проекте.

Перед непосредственным знакомством и рассмотрением примеров работы API, выполним процедуру установки и подключения библиотеки FIL. Программировать будем ранее упомянутую платформу, на которой установлен микроконтроллер STM32F401CC.

Создание проекта и добавление библиотеки

В настоящее время библиотека поддерживается средой программирования EmBitz версии от 2.30. Подробная инструкция приведена в официальном репозитории библиотеки FIL. Для этого потребуется обзавестись на вашем персональном компьютере следующим программным обеспечением:

  • Система контроля версий Git — потребуется для скачивания библиотеки с репозиториев (ссылка на установщик);

  • Среда разработки EmBitz 2.30 — наша основная программа, где будет демонстрироваться работоспособность библиотеки. К сожалению, доступ к основному сайту разработка был закрыт для жителей КНР и Российской Федерации, поэтому я прикрепил ссылку на свою копию установщика. Также, вместе со средой будет автоматически установлен драйвер ST-Link.

  • USB драйвер для программатора ST-Link — необходим только в том случае, если у вас выскакивает ошибка при прошивке контроллера, установка этого драйвера обычно помогает. Способов его установки существует множество, я, в рамках обзора, воспользуюсь удобной для меня программой Zadig (ссылка на установщик).

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

Переход к установке для среды EmBitz.

Переход к установке для среды EmBitz.

В начале предлагают создать новый проект. Я буду следовать всем шагам установки, в том числе с отключением других периферийных библиотек, оставив только CMSIS. Для этого в меню выбора модели контроллера измените опцию Peripherals Library на not used.

Убираем добавление в проект периферийных библиотек.

Убираем добавление в проект периферийных библиотек.

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

git clone https://github.com/Casonka/FIL-EmbitzDeploy.git & cd FIL-EmbitzDeploy & rmdir /q /s images & del /q README.md

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

Недостаточно просто загрузить файлы, их нужно добавить в текущий проект. Для этого нажмём ПКМ по проекту и выберем опцию Add files recursively. Через всплывающее окно проводника выбрал папку FIL-EmBitzDeploy (была загружена через Git).

Открыв проект в нашей IDE, остается добавить подключение библиотеки в заголовочном файле main.h.

Добавление директивы включения главного файла ядра библиотеки.

Добавление директивы включения главного файла ядра библиотеки.

В файле main.c добавим стандартное макро определение Board_Config, про которое было упомянуто в конце инструкции. Теперь, при нажатии кнопки build или сочетания клавиш ctrl+b будет собран проект. Результат порадовал, проект собрался без ошибок.

Окно успешного завершения сборки проекта.

Окно успешного завершения сборки проекта.

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

Демонстрация работы библиотеки на примерах

Далее мы рассмотрим демонстрационные примеры. Это необходимо для общего представления некоторых возможностей библиотеки и будет служить подспорьем для начинающих разработчиков.

Для демонстрации работы некоторых примеров было использовано окно для отслеживания переменных, которое можно вывести себе через контекстное меню выше — Debug > Debugging Window > Watches.

Зная структуру библиотеки, о которой было оговорено ранее, заглянем в содержимое нашей конфигурации. Согласно инструкции, по умолчанию, работает демо конфигурация, поэтому заглянем в файлы по пути FIL-EmBitzDeploy/conf/demo/.

Здесь мы видим два файла: файл карты портов и файл настроек демо-конфигурации. В карте портов содержится макрос порта светодиода — LED_PIN(расположен на верхней плате робота). Запомним название, оно нам пригодятся далее. Структура файла приведена ниже, содержит всего одну строку.

#define LED_PIN                 GPIOPinID(PORTC, 13)

В файле настроек видим общий список параметров.

/*! General settings */ 
#define __configUSE_RCC                   1 
#define __configUSE_GPIO                  1 
#define __configUSE_TIM                   1 
#define __configUSE_USART                 0 
#define __configUSE_DMA                   0 
#define __configUSE_I2C                   0 
#define __configUSE_SPI                   0 
#define __configUSE_ADC                   0 
#define __configUSE_EXTI                  0 
#define __configUSE_RTC                   0

/*! Optional settings */ 
#define __configCALC_RCC                  1 
#define __configCALC_TIM                  1 
#define __configCALC_USART                0 
#define __configCALC_I2C                  0

Каждый из параметров отвечает за получения доступа в пользовательское пространство какого-либо участка периферии. Ключевая приставка USE относит группу параметров к главным, поскольку их изменение влияет на включение/исключение периферийных файлов. Например, параметр __configUSE_GPIO выставленный в значение 1 приведет к подключению файла GPIO.h библиотеки и откроет доступ к командам для работы непосредственно с портами ввода/вывода.

Для демонстрационных примеров используем такой вариант, если потребуются изменения, добавим. Группа параметров с приставкой CALC исходя из названия носит некий характер расчетов, но их назначение гораздо шире — помощь в отладке, упрощение работы некоторых функций, применение автоматических математических расчетов для упрощения инициализации, все это предлагается опциональными параметрами, с ними мы ещё познакомимся. Список параметров и функций был взят из документации на библиотеку (ссылка).

Ниже, в том же файле, находим макрос Board_Config — основная команда применения настроек.

#define Board_Config { 
    SetGPIOC;
    InitPeriph;
    SysTick_Config(84000);
    }

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

  • SetGPIOC — подает тактирование на группу портов С. Акцентирую ваше внимание на то, что данная команда не будет работать, если параметр сектора тактирования __configUSE_RCC (см. выше) не включен (значение не 1);

  • SysTick_Config(84000) — команда инициализации системного таймера. Числом задается делитель тактовой частоты контроллера. В данном случае системный таймер будет настроен на частоту 1кГц. Не относится к API библиотеки, её определение можно найти в файлах CMSIS библиотеки. Вызов данной команды необходим для работы команд, отсчитывающих интервалы времени при выполнении, например, функция задержки delay_ms();

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

#define InitPeriph {
  GPIOConfPin(LED_PIN, GENERAL, PUSH_PULL, FAST_S, NO_PULL_UP);
  }

Здесь видим команду GPIOConfPin, которая выполняет конфигурирацию указанного порта микроконтроллера в соответствии с указанными режимами работы:

  • LED_PIN — ключевое название, которое мы ранее уже видели в карте портов. При выполнении команд идентификатор замещаемый словом будет использован для перехода к номеру пина, находящегося в группе, указанной программистом;

  • GENERAL — работа порта в режиме выхода. Согласно документации кроме данного режима можно задать ещё три: INPUT — режим входа, ALTERNATE — альтернативный режим, ANALOG — аналоговый режим;

  • PUSH_PULL — стандартная схемотехника подключение порта. Если необходимо сменить, есть второй режим — OPEN_DRAIN;

  • FAST_S — скорость работы порта 50 МГц. Помимо этой опции возможно задать ещё несколько: LOW_S (2 МГц), MEDIUM_S (25 МГц), HIGH_S (100 МГц);

  • NO_PULL_UP — не использовать никаких подтягивающих резисторов. В ином случае используются опции PULL_UP (подтяжка к питанию) и PULL_DOWN (подтяжка к земле).

Мигание светодиодом

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

int main(void)
{
Board_Config;
  while(1) 
  {
      TooglePin(LED_PIN);
      delay_ms(250);
  }
}

Для применения настроек всегда в первую очередь выполняется макрос Board_Config. Приведенный пример меняет состояние пина каждые 250 миллисекунд. Команда TooglePin() внутри себя сравнивает текущее состояние выхода порта и меняет его логический уровень на противоположный, за счет этого и достигается изменение. Задержка лишь для видимости изменений. Ниже можете наблюдать, что пример успешно загрузился и светодиод мигает на нашем роботе.

Демонстрационный пример работает.

Демонстрационный пример работает.

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

  • SetPin(pin) — устанавливает на указанном идентификаторе пина высокий потенциал (зажечь светодиод или просто высокий потенциал для проверки);

  • ResetPin(pin) — устанавливает на указанном идентификаторе пина низкий потенциал (потушить светодиод или установить низкий потенциал для отладки и других целей).

Использование таймеров

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

В этом разделе будут продемонстрированы примеры работы таймеров с помощью библиотеки FIL. Перед тем как производить манипуляции и использовать команды с таймерами необходимо убедиться, что значения параметров __configUSE_TIM и __configCALC_TIM равны 1.

Широтно-импульсная модуляция (ШИМ) с использованием FIL

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

#include "main.h"

int main(void)
{
    Board_Config;
    while(1)
    {
        SetPWM(TIM1, 1, 0.5);
    }
}

Встречаем новую командуSetPWM(). Исходя из названия, можно догадаться что её задача — генерация импульса ШИМ. На вход она требует 3 аргумента: указатель таймера, на выходе которого нужно установить сигнал (в примере указан первый таймер), номер канала таймера (в примере первый канал), скважность импульса. Принцип записи значения скважности такой — диапазон значений от -1.0 до 1.0. По факту это значение заполнения в процентах поделенной на 100. Минусовой диапазон включен для того, чтобы можно было генерировать шим обратной полярности (хотя для этого можно было использовать регистр полярности, но что имеем то имеем).

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

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

В файле с конфигурацией нужно прежде всего добавить в Board_Config некоторые команды:

  • SetGPIOA — подаем тактирование на группу портов А.

  • SetTIM1 — подаем тактирование на таймер №1. Просто и быстро;

  • TimPWMConfigure(TIM1, 8400, 10000, 1, 0, 0, 0) — однострочная команда для быстрой инициализации таймера в режиме генерации ШИМ. На вход подается уже ранее упомянутый TIM1 (идентификатор таймера №1), далее идут аргументы предделителя и периода. В документации на эту модель контроллера можно найти частоту тактирования первого таймера (84 МГц) поскольку он находится на высокоскоростной шине APB1. Результирующая частота, при указанных аргументах, получается равной 1 Гц. Формула для расчета частоты таймера будет приведена ниже, однако для новичков рекомендуем обратить своё внимание на команду TimPWMConfigureAutomatic(TIM1, 100, 1, 0, 0, 0) — облегченная команда, призванная взять на себя ответственность за расчет предделителя и периода. От вас потребуется только указать требуемую частоту в герцах (я указал 100 Гц). Последние 4 аргумента функций — включение каналов (их может быть до 4). Нам для примера достаточно задействовать только первый, поэтому его выставил в 1.

Согласно формуле, частота находится путем деления частоты шины (bus), на которой находится нужный нам таймер, на произведение предделителя (PSC) + 1 и периода (ARR).

Формула нахождения частоты таймера.

Формула нахождения частоты таймера.

Также, не забываем про привязку таймера к пину через альтернативную функцию. Для этого в первую очередь, зайдем в файл PinMap.h и добавим новый идентификатор для выхода таймера (пин PA15).

#define LED_PIN                 GPIOPinID(PORTC, 13)

// Добавим ниже новое определение, привяжем к пину PA15

#define PWM_PIN                 GPIOPinID(PORTA, 15)

Теперь, добавим в макрос InitPeriph несколько команд:

  • GPIOConfPin(PWM_PIN, ALTERNATE, PUSH_PULL, FAST_S, NO_PULL_UP) — Инициализация пина для первого канала таймера №1, настройки почти совпадают с примером для светодиода, за исключением другого названия (мы используем другой пин), режим ALTERNATE для подготовки к подключению альтернативной функции, все остальное предпочел оставить как есть;

  • GPIOConfAF(PWM_PIN, AF1) — Подключение первого канала альтернативной функции. В нашем случае произойдет привязка пина PWM_PIN к первому каналу таймера №1 (смотри рисунок ниже). Всю информацию можно найти в документации контроллера (ссылка), в разделе pinout and pin description.

Поиск нужной альтернативной функции.

Поиск нужной альтернативной функции.

Добавив все необходимое, получаем новую версию конфигурации:

#define Board_Config {
                    SetGPIOA;
                    SetGPIOC;
                    SetTIM1;
                    InitPeriph;
                    SysTick_Config(84000);
                    TimPWMConfigure(TIM1,8400, 10000, 1, 0, 0, 0);
                    }

#define InitPeriph {
        GPIOConfPin(LED_PIN, GENERAL, PUSH_PULL, FAST_S, NO_PULL_UP);
        GPIOConfPin(PWM_PIN, ALTERNATE, PUSH_PULL, FAST_S, NO_PULL_UP);
        }

Все изменения в настройке проделаны, пример запускается без проблем.

Подключение инкрементального энкодера

Таймер можно использовать для получения данных о вращении приводных колес робота. В платформе «EduBot» установлены два энкодера на входной вал двигателя, то есть без учета коэффициента передачи редуктора. Следующий пример демонстрирует работу команды инициализации таймера в режиме энкодера, входы которого можно использовать для регистрации вращения. Эти команды необходимо добавлять в Board_Config и InitPeriph, запишем как оно должно выглядеть, в случае использования обучающей платформы.

 /* [PinMap.h] */
// Добавляем идентификаторы для пинов энкодера
#define ENCODER1A_PIN           GPIOPinID(PORTA,0)
#define ENCODER1B_PIN           GPIOPinID(PORTA,1)

 /* [Configuration.h] */

/* Подаем тактирование на таймера №4 и 
   какая-то команда*/

    SetTIM4;
    TimEncoderConfigure(TIM4);

/* Используем уже известные нам команды, 
  скорость тактирования пинов намеренно делаем LOW_S,
  режим работы - вход, согласно таблице это вторая альтернативная функция */

    GPIOConfPin(ENCODER1A_PIN, ALTERNATE, PUSH_PULL, LOW_S, PULL_UP);      
    GPIOConfAF(ENCODER1A_PIN, AF2);                                        
    GPIOConfPin(ENCODER1B_PIN, ALTERNATE, PUSH_PULL, LOW_S, PULL_UP);      
    GPIOConfAF(ENCODER1B_PIN, AF2);                                        

 /* [main.c] */

uint16_t EncoderData;
int main(void)
{
    Board_Config;
    while(1)
    {
        EncoderData = GetTimCNT(TIM4);
    }
}

В примере встречаются почти все знакомые команды и функции, за исключением двух:

  • TimEncoderConfigure(TIM4) — инициализация таймера №4 для работы в режиме энкодера. Просто записав это макроопределение и выполнив его, система будет готова к принятию входных данных с энкодера;

  • GetTimCNT(TIM4) — макрос, который позволяет получить данные из регистра счетчика. Содержит внутри обращение к CNT с приведением к типу.

Демонстрационного примера достаточно, чтобы производить замеры общего пройденного расстояния. Для проверки, задал вращение одного приводного колеса робота (задействован предыдущий пример) и вывел переменную EncoderData в окно Watches.

Результат работы счетчика энкодера спустя некоторое время.

Результат работы счетчика энкодера спустя некоторое время.

Генерация прерываний с помощью таймеров

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

Рассмотрим новый демонстрационный пример, взяв предыдущий за основу. Единственное отличие будет заключаться в том, что считывание данных будет в обработчике прерывания. Рассмотрим пример подробнее. Команда TimPIDConfigure(TIM3, 4200, 1000) позволяет настроить таймер на генерацию прерываний в зависимости от настроенной частоты. Для этой функции есть упрощенная альтернатива TimPIDConfigureAutomatic(TIM3, 10), на вход которой вторым аргументов записывается частота в герцах. Теперь, перейдем к коду примера.

/* [Configuration.h] */

/* Инициализация таймера №3 для генерации
   прерывания */

  TimPIDConfigure(TIM3, 4200, 1000); 
  NVIC_EnableIRQ(TIM3_IRQn); 

 /* [main.c] */

uint16_t EncoderData;
void TIM3_IRQHandler(void)
{
  EncoderData = GetTimCNT(TIM4);

  ResetTIMSR(TIM3);
}

int main(void)
{
    Board_Config;
    while(1)
    {
      
    }
}

Обработчик прерывания TIM3_IRQHandler() необходимо объявить таким же по названию, каким он указан в стартовом файле startup_stm32f401xc.s, генерируемом при создании проекта. В примере демонстрируется инициализация прерывания по переполнению счетчика таймера, в его работоспособности вы можете убедиться при запуске отладки.

Взаимодействие с АЦП

Аналого-цифровой преобразователь (АЦП) используется для получения данных с аналоговых сенсоров. С учетом того, что комплект обучающей платформы включает аналоговые сенсоры Sharp 2Y0A21, то следующий пример будет актуален для начинающих. Конструкция и оснащение платформы позволяет оснастить такими датчиками в количестве до 8 единиц.

Добавим новые фрагменты кода в конфигурацию и рабочее пространство.

 /* [PinMap.h] */
// Добавляем идентификатор для аналогового входа A4

#define ADC_PIN                 GPIOPinID(PORTA,4)

/* [Configuration.h] */

/* Подаем тактирование на единственный и 
   первый АЦП */

    SetADC1; 

/* Добавляем в InitPeriph 
   инициализацию пина  как аналогового входа */
    GPIOConfPin(ADC_PIN, ANALOG, PUSH_PULL, FAST_S, NO_PULL_UP);

 /* [main.c] */
uint16_t ADC_Data;
int main(void)
{
    Board_Config;
    ADCAddChannel(4, REGULAR, ADC_480_CYCLES);
    ADC_Init();
    while(1)
    {
      ADC_Data = GetADCData;
    }
}

В примере все выглядит понятным до главной точки входа программмы int main(void). Инициализация АЦП требует дополнительных действий от разработчика:

  • ADCAddChannel(4, REGULAR, ADC_480_CYCLES) — указанной командой мы добавляем 4 вход АЦП (пин A4) к очереди на обработку. Тип канала в данном случае — регулярный, для инжектированных каналов, согласно документации необходимо изменить второй параметр на INJECTED. третий параметр регулирует количество циклов преобразования перед выдачей окончательного результата, в примере указано 480 циклов, однако параметр можно уменьшать до 3 циклов, но результат будет соответствовать затраченному времени, нужно смотреть по ситуации;

  • ADC_Init() — общая функция инициализации АЦП и его параметров. Необходимо её выполнить после добавления каналов, во избежания того, что каналы не будут обрабатываться АЦП после старта;

  • GetADCData — получение данных с буферного регистра. Ничем не примечательна, считывает данные из регистра DR с приведением к типу uint16_t.

Данные с АЦП приходят.

Данные с АЦП приходят.

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

/* Добавляем новый параметр конфигурации */
#define __configADC_DMARequest          (1)

Данный параметр при общей инициализации АЦП через ADC_Init() подключает АЦП к контроллеру памяти, в результате мы будет спокойно получать данные с нескольких каналов в массив, который предварительно создадим.

ConnectADCTODMA(HIGH_P, ADC1_Data, 0);

С помощью этого макроса инициализируется один из каналов DMA и подключается к буферу АЦП. По своему определению, контроллер просто перебрасывает информацию из одного места в другое. Параметр HIGH_P означает высокий приоритет выполнения операций на данном канале, помимо этого существуют ещё варианты указать низкий (LOW_P), средний (MEDIUM_P) и очень высокий (VeryHigh_P) приоритеты. Второй параметр содержит ссылку на буфер куда нам будут присылаться значения с АЦП. Определения всех возможных буферов приведены в файле DMA_FIFOBuffers.h. Взглянем на фрагмент этого файла для общего понимания:

#if(FIL_DMA == 1)
/*!
*   @note [FIL:DMA] This place configuration sizes of buffers
*
*/
#define ADC1_NUMB   2

#ifdef STM32F40_41xxx
#define ADC2_NUMB   2
#define ADC3_NUMB   3
#endif /*STM32F40_41xxx*/

#define USART1RX_NUMB  8
#define USART1TX_NUMB  8
#define USART2RX_NUMB  8
#define USART2TX_NUMB  8

/*!
*   @brief  ADC1_Data[ADC_NUMB] - buffer ADC1 Conversions
*   @list  ADC1_Data
*/
extern uint16_t ADC1_Data[ADC1_NUMB];
...

Найдем определение для буфера первого АЦП. Он называется ADC1_Data (это было нетрудно). Его необходимо скопировать и добавить в качестве второго аргумента ранее указанного макроса, если вы этого ещё не сделали. Так, в остальном тут зарезервированы буферы для некоторых других интерфейсов, таких как UART и так далее.

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

  • 0 — прерывания по завершению переброса не будут действовать;

  • 0.5 — включение прерывания по переброске половины всех данных;

  • 1 — включение прерывания по завершению передачи данных;

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

/*!
*   @note [FIL:DMA] This place configuration sizes of buffers
*
*/
#define ADC1_NUMB   2

Макрос, ответственный за количество пересылаемых данных ADC1_NUMB во время инициализации контроллера заменит себя на число 2, которое будет успешно записано в регистр NDTR. Для демонстрации работы примера двух каналов будет более чем достаточно.

Действуем по той же схеме: добавляем второго идентификатор аналогового входа, подаем тактирование на второй DMA, добавляем инициализацию в файл main.c.

/* [PinMap.h] */
/* Добавляем новый идентификатор */
#define ADCnew_PIN              GPIOPinID(PORTA, 4)

/* [Configuration.h] */
/* Подача тактирования на DMA2 */
  SetDMA2;

/* [main.c] */

uint16_t ADC1_Data[2];
int main(void)
{
  Board_Config;
  // Добавляем каналы АЦП
  ADCAddChannel(3, REGULAR, ADC_480_CYCLES);
  ADCAddChannel(8, REGULAR, ADC_480_CYCLES);
  // Не забываем про инициализацию DMA
  ConnectADCTODMA(HIGH_P, ADC1_Data, 0);
  // Общая инициализация АЦП
  ADC_Init();
  while(1)
  {
    /* Оставляем пустым */
  }
}

При выполнении этого демонстрационного примера массив ADC1_Data будет заполняться данными от каждого из каналов раздельно.

Готово, данные с нескольких каналов записываются раздельно.

Готово, данные с нескольких каналов записываются раздельно.

Итоги

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

В статье не стал рассматривать такую фичу библиотеки как External Manager — менеджер подключения сторонних библиотек. Материал получился и без того объемным, при необходимости появится вторая статья, в которой подробно на этом остановлюсь. Перейдем к определению преимуществ и недостатков.

Плюсы

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

  2. API для новичков и экономия времени — самым важной целью при создании каждой функции и макроса это сделать процесс инициализации неотъемлемой частью проекта, но при этом не затрачивать на это колоссальные отрезки времени. Ведение занятий в комбинации с библиотекой FIL экономит время, остаток которого можно направить для рассмотрения большего количества тем, считаю это хорошим достижением и в то же время стимулом для продолжения модернизации. Вариативность функций позволяет инициализировать периферийные участки с максимальным контролем и доступностью или заменить некоторыми автоматизированными методами, в рамках обучения которых будет более чем достаточно.

  3. Удобство при подключении внешних библиотек — вскользь, я упоминал про менеджер внешних библиотек. Его функционал не был освещен, однако я должен выделить его в плюс того, что уже есть. Часто, возникает необходимость в написании драйвера для какого-либо устройства и последующая его адаптация к проекту. Поэтому, с помощью менеджера можно только лишь объявив параметр подключить всю библиотеку целиком. В этом репозитории, содержатся все доступные и оптимизированные библиотеки, которые можно подключать и использовать в своих роботах;

Минусы

  1. Скромный ассортимент внешних библиотек — плюс наличия менеджера внешних библиотек, так же порождает и недостаток в его скудности. Дело в том, что привязывать к проекту что-либо стороннее невозможно без некоторых манипуляций. Библиотеки, необходимые для работы, зачастую, разработаны с использованием других периферийных библиотек, поэтому по необходимости они полностью заменяются на аналоги FIL, что не очень удобно и вообще не экономит время. Для разработчика и последователей этой стези нет обходного пути, если библиотека взаимодействует с периферией контроллера, этот путь тернист и требует выдержки, чтобы пополнять эту библиотеку. Возможно, спустя время не потребуется добавлять новое, но пока что ситуация не впечатляющая;

  2. Не вся периферия готова к использованию — рассмотрение демонстрационных примеров это по сравнению со всеми возможностями библиотеки, как капля в море, но даже с учетом этого некоторые участки периферии не были добавлены в библиотеку. К самым известным из них можно отнести SPI, DAC, CAN и другие интерфейсы. В последующих версиях список будет уменьшаться, следите за обновлениями;

  3. Что там по RTOS — несомненно, для реализации сложных и серьезных проектов в области робототехники потребуется подвязать операционную систему реального времени. Проводилось немало тестов при добавлении FreeRTOS, её можно будет скоро добавить через External Manager. Но пока её нет, проект библиотеки считаю не совсем полноценным и жизнеспособным;

  4. Некоторые жалобы на ошибки при выполнении — замечено (и не раз), как при выполнении абсолютно правильного кода происходит уход в ошибки HardFault и Default. Детальный анализ структуры кода навел на мысль, что при выполнении некоторых макросов, либо их множества приводит к ошибкам из-за слишком быстрой скорости выполнения (или может быть я ошибаюсь). Причиной этого стало уделение слишком малого количества времени на добавление всевозможных синхронизирующих барьеров и проверок.

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

Планы по дальнейшему развитию

Выложив сюда эту статью, я закончил вводный этап развития библиотеки. Впереди запланированы добавления и изменения. Следующим шагом станет поиск способов добавления библиотеки micro-ROS (ROS для микроконтроллеров), которая необходима резидентам и разработчикам для создания стандартизированных сообщений ROS msgs, их отправка и получение со стороны ЭВМ верхнего уровня. К сожалению, среда EmBitz не адаптирована к подключению такой библиотеки, поэтому первый подэтап — оптимизация кода библиотеки под работы в среде Eclipse. Задач и планов множество, буду улучшать и модифицировать.

Спасибо, что дочитал статью до конца! Мне очень приятно осознавать, что мой проект добрался до пространства Хабр и с ним ознакомилось намного больше людей чем год назад, когда проект представлял собой лишь нарезки кода и идею. Нашему центру будет очень интересно узнать ваше мнение о разработке, для себя я услышу нужную мне сейчас, как никогда, критику и/или советы по наполнению. Отдельно, буду признателен за проставленный star проекта на гитхаб.

Прекомпилированные библиотеки, которые созданы из модулей кода, полезно использовать в больших проектах для ускорения процесса компиляции. В библиотеки целесообразно поместить код, который не меняется в настоящий момент (когда идет работа над чем-то другим). В составе IAR есть обстоятельная документация «IAR Runtime Environment and Library User Guide» [1], однако это руководство довольно большое и сложное для изучения. Здесь представлены простые инструкции — как в проекте использовать заранее скомпилированный код в виде библиотек (prebuild library), чтобы помногу раз не перекомпилировать одни и те же файлы и экономить время разработки.

[Как создать библиотеку в IAR]

1. Создайте проект, куда добавьте модули исходного кода, которые должны быть помещены в библиотеку (меню Project -> Create New Project… -> C -> main -> OK -> укажите имя файла проекта и выберите папку, куда он будет сохранен). Удалите из проекта модуль main.c, который был создан автоматически, и добавьте в проект все модули кода, которые должны быть помещены библиотеку. Настройки нового проекта для библиотеки будут во многом копировать настройки других Ваших проектов — нужно выбрать тип процессора, уровень оптимизации, прописать пути поиска заголовков и проч.

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

Процесс по шагам, На примере кода STM32_USB_Host_Library из STM32Cube_FW_F4_V1.24.0:

1. Создайте пустой проект через меню Project -> Create New Project… -> Empty Project -> OK.

IAR create lib01

IAR запросит ввести имя файла для проекта (файл с расширением *.ewp). Укажите короткое, но понятное имя, отрающее назначение будущей библиотеки. Например, для библиотеки хоста USB STM32 (STM32_USB_Host_Library) я указал имя USBhost, и сохранил этот файл в корневом каталоге библиотеки.

IAR create lib02

2. Откройте опции проекта.

IAR create lib03

В разделе General Options -> Output выберите радиокнопку Library. Как вариант, можно указать папку, куда будет сохранен выходной файл компилируемой библиотеки. Я обычно указываю этот путь относительно каталога проекта $PROJ_DIR$, см. скриншот:

IAR create lib04

3. Выберите тип процессора, для которого компилируете библиотеку, для чего перейдите в раздел General Options -> Target. Переключите радиокнопку «Processor variant» на Device, и с помощью кнопки выберите тип микроконтроллера (ST -> STM32F4 -> STM32F429 ->STM32F429ZI).

IAR create lib05

Закройте окно опций проекта кнопкой OK.

4. Добавите в проект все необходимые файлы библиотеки.

IAR create lib06

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

Changed settings forces a full rebuild...
Building configuration: USBhost - Debug
Updating build tree...
0  file(s) deleted.
Updating build tree...
usbh_ioreq.c Fatal Error[Pe1696]: cannot open source file "usbh_ioreq.h" D:asmradiopagerMiddlewaresSTSTM32_USB_Host_LibraryCoreSrcusbh_ioreq.c 22 searched: "D:asmradiopagerMiddlewaresSTSTM32_USB_Host_LibraryCoreSrc" searched: "C:Program Files (x86)IAR SystemsEmbedded Workbench 8.1arminc" searched: "C:Program Files (x86)IAR SystemsEmbedded Workbench 8.1armincc" current directory: "C:UsersAndreyDesktop" Error while running C/C++ Compiler usbh_core.c Fatal Error[Pe1696]: cannot open source file "usbh_core.h" D:asmradiopagerMiddlewaresSTSTM32_USB_Host_LibraryCoreSrcusbh_core.c 23 searched: "D:asmradiopagerMiddlewaresSTSTM32_USB_Host_LibraryCoreSrc" searched: "C:Program Files (x86)IAR SystemsEmbedded Workbench 8.1arminc" searched: "C:Program Files (x86)IAR SystemsEmbedded Workbench 8.1armincc" current directory: "C:UsersAndreyDesktop" Error while running C/C++ Compiler usbh_msc_scsi.c Fatal Error[Pe1696]: cannot open source file "usbh_msc.h" D:asmradiopagerMiddlewaresSTSTM32_USB_Host_LibraryClassMSCSrcusbh_msc_scsi.c 28 searched: "D:asmradiopagerMiddlewaresSTSTM32_USB_Host_LibraryClassMSCSrc" searched: "C:Program Files (x86)IAR SystemsEmbedded Workbench 8.1arminc" searched: "C:Program Files (x86)IAR SystemsEmbedded Workbench 8.1armincc" current directory: "C:UsersAndreyDesktop" Error while running C/C++ Compiler usbh_msc_bot.c Fatal Error[Pe1696]: cannot open source file "usbh_msc_bot.h" D:asmradiopagerMiddlewaresSTSTM32_USB_Host_LibraryClassMSCSrcusbh_msc_bot.c 28 searched: "D:asmradiopagerMiddlewaresSTSTM32_USB_Host_LibraryClassMSCSrc" searched: "C:Program Files (x86)IAR SystemsEmbedded Workbench 8.1arminc" searched: "C:Program Files (x86)IAR SystemsEmbedded Workbench 8.1armincc" current directory: "C:UsersAndreyDesktop" Error while running C/C++ Compiler usbh_ctlreq.c Fatal Error[Pe1696]: cannot open source file "usbh_ctlreq.h" D:asmradiopagerMiddlewaresSTSTM32_USB_Host_LibraryCoreSrcusbh_ctlreq.c 22 searched: "D:asmradiopagerMiddlewaresSTSTM32_USB_Host_LibraryCoreSrc" searched: "C:Program Files (x86)IAR SystemsEmbedded Workbench 8.1arminc" searched: "C:Program Files (x86)IAR SystemsEmbedded Workbench 8.1armincc" current directory: "C:UsersAndreyDesktop" Error while running C/C++ Compiler usbh_msc.c Fatal Error[Pe1696]: cannot open source file "usbh_msc.h" D:asmradiopagerMiddlewaresSTSTM32_USB_Host_LibraryClassMSCSrcusbh_msc.c 41 searched: "D:asmradiopagerMiddlewaresSTSTM32_USB_Host_LibraryClassMSCSrc" searched: "C:Program Files (x86)IAR SystemsEmbedded Workbench 8.1arminc" searched: "C:Program Files (x86)IAR SystemsEmbedded Workbench 8.1armincc" current directory: "C:UsersAndreyDesktop" Error while running C/C++ Compiler usbh_pipes.c Fatal Error[Pe1696]: cannot open source file "usbh_pipes.h" D:asmradiopagerMiddlewaresSTSTM32_USB_Host_LibraryCoreSrcusbh_pipes.c 21 searched: "D:asmradiopagerMiddlewaresSTSTM32_USB_Host_LibraryCoreSrc" searched: "C:Program Files (x86)IAR SystemsEmbedded Workbench 8.1arminc" searched: "C:Program Files (x86)IAR SystemsEmbedded Workbench 8.1armincc" current directory: "C:UsersAndreyDesktop" Error while running C/C++ Compiler
Total number of errors: 7 Total number of warnings: 0

Снова откройте опции проекта, и в разделе C/C++ Compiler -> Preprocessor, в область ввода «Additional include directories: (one per line)» добавьте строки путей поиска заголовочных файлов, например:

$PROJ_DIR$......MiddlewaresSTSTM32_USB_Host_LibraryCoreInc
$PROJ_DIR$......MiddlewaresSTSTM32_USB_Host_LibraryClassMSCInc
$PROJ_DIR$......Inc
$PROJ_DIR$......DriversCMSISDeviceSTSTM32F4xxInclude
$PROJ_DIR$......DriversCMSISInclude
$PROJ_DIR$......DriversSTM32F4xx_HAL_DriverInc
$PROJ_DIR$......DriversBSP
$PROJ_DIR$......MiddlewaresThird_PartyFreeRTOSSourceCMSIS_RTOS
$PROJ_DIR$......MiddlewaresThird_PartyFreeRTOSSourceinclude
$PROJ_DIR$......MiddlewaresThird_PartyFreeRTOSSourceportableIARARM_CM4F

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

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

usbh_msc_scsi.c
Fatal Error[Pe035]: #error directive: "Please select first the target STM32F4xx device
used in your application (in stm32f4xx.h file)"
D:asmradiopagerDriversCMSISDeviceSTSTM32F4xxIncludestm32f4xx.h 191
Error while running C/C++ Compiler

Снова откройте опции проекта, и снова перейдите в раздел C/C++ Compiler -> Preprocessor. В область ввода «Define symbols: (one per line)» добавьте недостающие символы.

IAR create lib07

Снова скомпилируйте проект. В случае появления ошибок поиска заголовочных файлов повторите шаг 5.

7. В результате проект скомпилируется без ошибок, например:

Building configuration: USBhost - Debug
Updating build tree...
8 file(s) deleted. Updating build tree... usbh_msc_scsi.c usbh_ctlreq.c usbh_msc_bot.c usbh_core.c usbh_msc.c usbh_ioreq.c usbh_pipes.c Building library
Total number of errors: 0

В выходном каталоге появится библиотечный файл имяпроекта.a (в нашем примере USBhost.a). Теперь этот двоичный файл библиотеки можно использовать в другом проекте вместо файлов исходного кода — удалите из проекта модули исходного кода, и добавьте в проект файл скомпилированной библиотеки.

[Дополнительные настройки]

8. Можно настроить для библиотеки дополнительный уровень оптимизации кода, для чего перейдите в раздел опций C/C++ Compiler -> Optimizations. Радиокнопка Level вместе с выпадающим списком Balanced/Size/Speed/ и галочками тонкой настройки позволяют указать необходимую опимизацию, выполняемую компилятором.

IAR create lib08

Как обычно, когда разрабоатываемое приложение еще отлаживается, полезно выбрать уровень оптимизации None, тогда будут полностью доступны все символы библиотеки и отладка по её исходному коду. Если же разработка близка к завершению, обычно выбирают уровень оптимизации High -> Speed (оптимизация по скорости) или High -> Size (оптимизация по размеру).

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

9. Можно настроить две конфигурации проекта библиотеки — одну для отладки (Debug), другую для релиза (Release). По умолчанию при создании пустого проекта автоматически создаются эти две конфигурации Debug и Release (диалог конфигураций открывается через Project -> Edit Configurations…):

IAR create lib09

Одна из конфигураций Debug у нас уже настроена. Теперь для настройки конфигураци и Release нужно её выбрать, и настроить в ней все необходимые опции, повторив шаги 2, 3, 5, 6. Это довольно утомительный процесс, даже с учетом того, что можно копировать опции через буфер обмена.

Но есть способ поступить проще — удалить конфигурацию Release и создать её заново на основе конфигурации Debug, при этом все опции скопируются автоматически. Останется выбрать только уровень оптимизации. Для этого откройте диалог Project -> Edit Configurations…, удалите конфигурацию Release (кнопкой Remove, при этом текущей должна быть конфигурация Debug). После этого нажмите кнопку New… введите имя конфигурации (например Release) и создайте её на основе конфигурации Debug, нажав кнопку OK:

IAR create lib10

В созданной конфигурации придется заново повторить ввод выходной директории (раздел General Options -> Output, поле ввода Executables/libraries), и настроить оптимизацию (C/C++ Compiler -> Optimizations) и отключить вывод отладочной информации (C/C++ Compiler -> Output, снять галочку «Generate debug information»).

2. Откройте свойства проекта, перейдите в раздел General Options, выберите закладку Output. Переставьте радиокнопку Output file в положение Library.

3. Скомпилируйте проект. По умолчанию выходной файл библиотеки создастся в папке DebugExe, и файл получит имя проекта и расширение *.a. Это и есть прекомпилированный файл библиотеки. Теперь его нужно добавить в тот проект.

[Как добавить библиотеку в проект вместо модулей исходного кода]

1. Удалите из проекта все файлы модулей, которые были перенесены библиотеку. Проще всего, когда эти модули в проекте содержатся в отдельной папке. Тогда достаточно просто отключить от процесса компиляции в проекте папку с этими модулями, не удаляя их из проекта (впоследствии всегда можно их подключить обратно). Щелкните в дереве проекта правой кнопкой на папке, которую хотите отключить от компиляции, выберите Options… и поставьте галочку Exclude from build. Теперь эти модули компилироваться не будут. Код из этих модулей будет браться из библиотеки.

2. В проекте, куда хотите добавить библиотеку, зайдите в свойства проекта через меню Project -> Options, перейдите в раздел Linker -> закладка Library, и в поле Additional libraries добавьте полный путь до файла библиотеки. Не стесняйтесь использовать макросы IAR типа $PROJ_DIR$, чтобы удалить привязку к абсолютным путям. Например, путь до файла библиотеки может быть наподобие следующего:

$PROJ_DIR$....precompiled-IAR-libsefsl-libDebugExeefsllibprj.a

3. Перекомпилируйте проект с добавленной библиотекой. Он должен скомпилироваться без ошибок.

[Ссылки]

1. IAR Runtime Environment and Library User Guide site:supp.iar.com.

Компания STMicroelectronics прекратила поддержку библиотеки SPL, которая использовалась в этом курсе. Поэтому я создал новую рубрику, посвященную работе уже с новыми инструментами, так что буду рад видеть вас там — STM32CubeMx. Также вот глобальная рубрика по STM32 — ссылка.

Итак, компилятор установлен, пришло время создания первого проекта. Сразу думаю надо обсудить библиотеки, которые мы будем использовать в нашей работе с STM32.

Во-первых, CMSIS. Это замечательная библиотека, единый стандарт для всех Cortex. CMSIS  позволяет легко переносить код с одного контроллера с ядром ARM Cortex на любой другой. Короче, в CMSIS стандартизирован доступ к разной периферии разных микроконтроллеров STM.  Библиотека состоит из нескольких файлов, которые нам надо будет добавить в проект, но об этом немного позднее.

Во-вторых, библиотека Standard Peripheral Library. В SPL очень удобно реализована настройка всевозможных периферийных модулей, а также есть множество функций для работы с ними же. Эту библиотеку достаточно подробно будем изучать при обсуждении какой-либо конкретной периферии в будущих статьях. А сейчас кратенько рассмотрим ее структуру в общих чертах. Для любого модуля микроконтроллера есть 2 файла: заголовочный файл и, собственно, файл с исходным кодом. Так что, создавая проект для работы, например, с АЦП, мы будем включать в проект два файла из Standard Peripheral Library.

Возможно, кто-то сомневается, стоит ли использовать эту библиотеку. Речь, конечно о SPL (по поводу CMSIS думаю ни у кого нет особых сомнений). Так вот, я для себя однозначно решил, что стоит. Потому что ни к чему изобретать велосипед, библиотека написана и очень удобна, гораздо удобнее, чем напрямую ковыряться в многочисленных регистрах. НО! При всем этом, надо все-таки иметь представление какой регистр за что отвечает, и как с ними работать. Для этого не лишним будет просматривать раздел даташита на конкретную периферию при работе с ней.

Итак, скачиваем CMSIS и Standard Peripheral Library (SPL) и шагаем дальше:

  • скачать CMSIS — STM32_CMSIS
  • скачать Standard Peripheral Library — STM32_SPL

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

Keil. Создание нового проекта.

Идем в Project -> New uVision Project. Появляется окошко для выбора папки, в которой будет создан проект. Также надо дать нашему проекту имя. Пусть будет, например, test, это будет просто тестовый проект. Скриншот приводить не буду, тут все и так понятно. Теперь нам надо выбрать микроконтроллер. Я остановил свой выбор на STM32F103CB:

Keil. Создание нового проекта, выбор контроллера.

Жмем OK,  появляется диалоговое окно, жмем «Да», и вот первый шаг позади.

В левой части видим окошко Project. Там будут отображаться все файлы, добавленные в наш проект. Я обычно переименовываю группы, которые по умолчанию называются Target 1 и Source Group 1 в STM32 и StartUp соответственно. Сразу же добавим еще пару групп – для библиотек, а также для наших файлов, в которых будет собственно сам код. У меня после модификации все выглядит вот так:

STM32, учебный курс, дерево нового проекта.

Конечно, такой вариант не является единственно верным, просто так удобнее, а пока проект содержит немного файлов, пары папок вполне хватит. Идем дальше. Создадим пустой файл .c: File — New .

Появляется пустой файл, жмем File — Save As и называем наш первый файл test.c. Готово! Теперь нужно добавить все необходимые файлы в проект. Дважды тыкаем на папку CMSIS  в дереве проекта и добавляем следующие файлы:

  • core_cm3.c
  • system_stm32f10x.c

И вот они, первые грабли, с которыми я столкнулся. Проект отказался собираться без файла stm32f10x_conf.h. А в моей скачанной CMSIS  он напрочь отсутствовал. Поэтому качаем его отдельно и тоже добавляем в проект (если качали библиотеки по ссылкам выше, то там уже есть этот файл). Далее открываем этот файл, находим строчку #include «stm32f10x_type.h» и комментируем ее. Этот файл мы использовать не будем. Такие действия лично мне пришлось осуществить, чтобы успокоить компилятор,  возможно, у меня просто оказалась битая библиотека. Мне было проще поправить имеющуюся под себя, чем искать какие то другие варианты 🙂

Теперь аналогично добавим файлы библиотеки SPL. Пока они нам не понадобятся, но пусть будут. Добавим, например stm32f10x_rcc.c и подхватим includ’ом stm32f10x_rcc.h (файлы, содержащие все связанное с тактированием различной периферии). И наконец добавим наш созданный файл test.c в папку Source Files в дереве проекта.

Далее идем в Project — Options for target… Тут несколько вкладок для различных настроек. Идем во вкладку Output и ставим галку Create HEX File. Теперь наша цель – вкладка C/C++. В поле define пишем следующее: USE_STDPERIPH_DRIVER. Без этого проект не соберется. Осталось в Include Paths добавить пути ко всем(!) файлам, включенным в проект. Получим примерно следующее:

Создание нового проекта, пути к файлам.

И вот тут еще одни грабли. Все папки, которые добавляем в Include Paths не должны содержать пробелов. Так что мои Header Files, Source Files и Project Files плавно превращаются в Header_Files, Source_Files и Project_Files.

Еще один шаг к созданию проекта сделан, в принципе, осталось совсем чуть-чуть. В файл test.c закинем следующий тестовый код, который абсолютно ничего не делает:

#include "stm32f10x.h"
#include "stm32f10x_rcc.h"

int main()
{
	while(1);
}

Осталось лишь нажать F7 (build) и проект соберется. Все отлично, но мы наблюдаем непредвиденные warning’и. Хорошо написанная программа не должна их содержать, так что будем править. К счастью, это потребует от нас минимум усилий. Итак:

incompatible redefinition of macro "HSE_Value"

Чтобы это не вылетало, открываем stm32f10x_conf.h, ищем:

#define HSE_Value    ((u32)8000000) /* Value of the External oscillator in Hz*/

И беспощадно комментируем эту строку. Вот и все… Если у кого-то вылезло:

warning:  #1-D: last line of file ends without a newline

то надо сделать так, чтобы файл test.c не заканчивался строкой кода. Проще говоря, ставим курсор после последней скобки в этом файле и жмем Enter. Warning сразу же пропадает.

Ну вот, первые шаги сделаны. Мы создали пустой проект, который абсолютно ничего не делает полезного (бесполезного впрочем тоже 🙂 ). Но для начала уже неплохо, в следующих статьях будем заполнять проект кодом. Кстати, если у кого-нибудь возникли какие-либо трудности при создании проекта, пишите в комментарии, не стесняйтесь! И не пропустите статью нашего курса по STM32CubeMx, описывающую аналогичный процесс создания базового проекта.

Hello World по микроконтроллеровски

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

Как уже говорилось в первой части, в качестве инструмента для написания программ на языке Си мы будем использовать среду разработки «IAR Embedded Workbench for ARM» (далее просто IAR), надеюсь, Вы уже скачали и установили её на свой компьютер. Также нам потребуется библиотека для работы с периферией микроконтроллеров серии STM32 «STM32F10x standard peripheral library» и драйвер внутрисхемного JTAG/SWD отладчика «ST-LINK USB». Если у Вас еще не установлен драйвер для отладчика ST-LINK, то в таком случае необходимо произвести его установку.

Структура файлов библиотеки

При создании проекта, нам понадобится библиотека от ST для работы с периферией и ядром микроконтроллера. Структура содержащихся папок в скачанном архиве с библиотекой «STM32F10x standard peripheral library» представлена на рисунке 1.

Структура файлов библиотеки STM32

Рис. 1. Структура папок архива STM32F10X STDPERIPH LIB

Рассмотрим подробнее, что содержится в данных папках.

В корневой папке архива находится файл «stm32f10x_stdperiph_lib_um.chm», представляющий из себя руководство пользователя библиотекой «STM32F10x Standard Peripherals Firmware Library». В этом файле описана структура всех содержащихся файлов в архиве, а также в нем можно прочесть описание всех используемых в библиотеке функций и макроопределений.

В папке «LibrariesSTM32F10x_StdPeriph_Driverinc» содержатся заголовочные файлы библиотек для работы с периферийными модулями микроконтроллера. Рассмотрим подробнее назначение каждого содержащегося файла.

Таблица 1. Список и краткое описание заголовочных файлов библиотек периферийных модулей

Имя файла

Описание

misc.h

Данный файл содержит заголовки функций и описание структур для настройки контроллера вложенных векторизованных прерываний и конфигурации источника тактирования системного таймера

stm32f10x_adc.h

Данный файл содержит заголовки функций и описание структур для настройки аналогово-цифрового преобразователя

stm32f10x_bkp.h

Данный файл содержит заголовки функций и описание структур для настройки области резервных данных (Backup-домен) — это область, которая может при пропадании основного питания питаться от резервной батареи.

stm32f10x_can.h

CAN шина передачи данных

stm32f10x_cec.h

Интерфейс для  реализации протокола управления бытовой мультимедийной аппаратурой по интерфейсу HDMI

stm32f10x_crc.h

Модуль подсчета контрольной циклической суммы

stm32f10x_dac.h

Цифро-аналоговый преобразователь

stm32f10x_dbgmcu.h

Настройка поведения периферии при работе с отладчиком (jtag и swd)

stm32f10x_dma.h

Контроллер модуля прямого доступа к памяти

stm32f10x_exti.h

Настройка внешних прерываний

stm32f10x_flash.h

Запись, чтение, стирание и прочие действия с флеш памятью

stm32f10x_fsmc.h

Шина для подключения внешних RAM, ROM, LCD и т.п. устройств с параллельной шиной передачи данных

stm32f10x_gpio.h

Настройка портов ввода/вывода

stm32f10x_i2c.h

Шина I2C

stm32f10x_iwdg.h

Независимый сторожевой таймер

stm32f10x_pwr.h

Управление питанием микроконтроллера

stm32f10x_rcc.h

Управление тактированием

stm32f10x_rtc.h

Часы реального времени

stm32f10x_sdio.h

Интерфейс для высокоскоростного подключения карт памяти SD/SDHC

stm32f10x_spi.h

Последовательный интерфейс передачи данных SPI

stm32f10x_tim.h

Настройка таймеров

stm32f10x_usart.h

Последовательный интерфейс передачи данных USART

stm32f10x_wwdg.h

Оконный сторожевой таймер

В папке «LibrariesSTM32F10x_StdPeriph_Driversrc» содержатся файлы с исходными кодами настройки и работы с соответствующими периферийными модулями.

В папке «LibrariesCMSISCM3CoreSupport» содержится файл исходного кода и соответствующий ему заголовочный файл «core_cm3», данные файлы представляют собой файлы поддержки ядром периферийных модулей.

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

Файл «stm32f10x.h» содержит:

  • настройки выбора серии микроконтроллера с описанием обозначения серии. Наш микроконтроллер относится к 100 серии и имеет 128кБ флеш памяти, значит, в соответствии с этой классификацией он будет иметь обозначение – STM32F10X_MD_VL (Medium-density value line devices are STM32F100xx microcontrollers where the Flash memory density ranges between 64 and 128 Kbytes);
  • в макросе HSE_VALUE указывается частота работы кварцевого резонатора, для правильного расчета системы тактирования;
  • выбор источника тактирования – внешнего кварцевого резонатора или встроенного RC генератора;
  • переопределение объявления переменных на более короткую форму записи;
  • определение регистров и битовых флагов регистров периферии с кратким описанием их назначения.

В папке «LibrariesCMSISCM3DeviceSupportSTSTM32F10xstartup» содержится ассемблерный код начальной инициализации микроконтроллера для различных компиляторов. В папке с именем нашего компилятора имеется несколько файлов для разных серий микроконтроллеров, для нашего микроконтроллера подходит файл «startup_stm32f10x_md_vl.s».

Помимо файлов с библиотеками, в скачанном архиве также имеются папки «Utilities» с примерами исходных кодов для платы STM32_EVAL и папка «Project». В папке «Project» находятся папки «STM32F10x_StdPeriph_Examples» с примерами использования библиотеки и «STM32F10x_StdPeriph_Template» с шаблоном проекта для различных компиляторов.

Создание проекта

Итак, приступим к созданию своего первого проекта в среде программирования IAR на микроконтроллере STM32. Процесс создания и настройки шаблона проекта показан на видео.

После того как мы создали свой проект, необходимо произвести настройку файлов библиотеки. Рассмотрим еще раз, более детальнее, содержимое файлов библиотеки и их назначение. Для этого открываем для редактирования файл «stm32f10x.h», предварительно сняв атрибут «только чтение». Находим в файле место, где определяется линейка используемого нами микроконтроллера и раскомментируем строку с используемой нами линейкой микроконтроллеров. Подсказка, как определить какая линейка соответствует какому микроконтроллеру, расположена сразу после выбора линейки микроконтроллера. Поскольку мы иcпользуем микроконтроллер серии STM32F100 с 128кБайтами флеш памяти, то нам подходит линейка «Medium-density value line», таким образом необходимо расскоментировать макроопределение STM32F10X_MD_VL.

/* Uncomment the line below according to the target STM32 device used in your
  application
 */

#if !defined (STM32F10X_LD) && !defined (STM32F10X_LD_VL) && !defined (STM32F10X_MD) && !defined (STM32F10X_MD_VL) && !defined (STM32F10X_HD) && !defined (STM32F10X_HD_VL) && !defined (STM32F10X_XL) && !defined (STM32F10X_CL)

 /* #define STM32F10X_LD */   /*!< STM32F10X_LD: STM32 Low density devices */
 /* #define STM32F10X_LD_VL */ /*!< STM32F10X_LD_VL: STM32 Low density Value Line devices */ 
 /* #define STM32F10X_MD */   /*!< STM32F10X_MD: STM32 Medium density devices */

#define STM32F10X_MD_VL /*!< STM32F10X_MD_VL: STM32 Medium density Value Line devices */ 

 /* #define STM32F10X_HD */   /*!< STM32F10X_HD: STM32 High density devices */
 /* #define STM32F10X_HD_VL */ /*!< STM32F10X_HD_VL: STM32 High density value line devices */
 /* #define STM32F10X_XL */   /*!< STM32F10X_XL: STM32 XL-density devices */
 /* #define STM32F10X_CL */   /*!< STM32F10X_CL: STM32 Connectivity line devices */

#endif

/* Tip: To avoid modifying this file each time you need to switch between these
   devices, you can define the device in your toolchain compiler preprocessor.
 — Low-density devices are STM32F101xx, STM32F102xx and STM32F103xx microcontrollers
  where the Flash memory density ranges between 16 and 32 Kbytes.
 — Low-density value line devices are STM32F100xx microcontrollers where the Flash
  memory density ranges between 16 and 32 Kbytes.
 — Medium-density devices are STM32F101xx, STM32F102xx and STM32F103xx microcontrollers
  where the Flash memory density ranges between 64 and 128 Kbytes.
 — Medium-density value line devices are STM32F100xx microcontrollers where the
  Flash memory density ranges between 64 and 128 Kbytes.  
 — High-density devices are STM32F101xx and STM32F103xx microcontrollers where
  the Flash memory density ranges between 256 and 512 Kbytes.
 — High-density value line devices are STM32F100xx microcontrollers where the
  Flash memory density ranges between 256 and 512 Kbytes.  
 — XL-density devices are STM32F101xx and STM32F103xx microcontrollers where
  the Flash memory density ranges between 512 and 1024 Kbytes.
 — Connectivity line devices are STM32F105xx and STM32F107xx microcontrollers.
 */

После выбора линейки используемого микроконтроллера, идет указание частоты источника тактирования. По умолчанию для всех микроконтроллеров, за исключением линейки STM32F10X_CL, установлена частота работы кварцевого резонатора 8МГц, поскольку именно на такую частоту у нас и установлен кварц, то нам не требуется изменять это значение. Помимо частоты работы установленного кварцевого резонатора, возможно также указание частоты работы встроенного RC генератора, это делается изменением макроса HSI_VALUE. Более никаких изменений вносить в данный файл не требуется.

/**
 * @brief In the following line adjust the value of External High Speed oscillator (HSE)
  used in your application
  Tip: To avoid modifying this file each time you need to use different HSE, you
   can define the HSE value in your toolchain compiler preprocessor.
 */

#if !defined HSE_VALUE

 #ifdef STM32F10X_CL

 #define HSE_VALUE   ((uint32_t)25000000) /*!< Value of the External oscillator in Hz */

 #else

 #define HSE_VALUE   ((uint32_t)8000000) /*!< Value of the External oscillator in Hz */

 #endif /* STM32F10X_CL */

#endif /* HSE_VALUE */

/**
 * @brief In the following line adjust the External High Speed oscillator (HSE) Startup
  Timeout value
  */

#define HSE_STARTUP_TIMEOUT  ((uint16_t)0x0500) /*!< Time out for HSE start up */
#define HSI_VALUE   ((uint32_t)8000000) /*!< Value of the Internal oscillator in Hz*/

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

В файле «system_stm32f10x.c» также содержатся некоторые настройки, необходимость изменения которых также может возникнуть. В частности там задается рабочая тактовая частота системной шины:

/*!< Uncomment the line corresponding to the desired System clock (SYSCLK)
  frequency (after reset the HSI is used as SYSCLK source)
  IMPORTANT NOTE:
  ==============
  1. After each device reset the HSI is used as System clock source.
  2. Please make sure that the selected System clock doesn’t exceed your device’s maximum frequency.
  3. If none of the define below is enabled, the HSI is used as System clock source.
  4. The System clock configuration functions provided within this file assume that:
   — For Low, Medium and High density Value line devices an external 8MHz
   crystal is used to drive the System clock.
   — For Low, Medium and High density devices an external 8MHz crystal is
   used to drive the System clock.
   — For Connectivity line devices an external 25MHz crystal is used to drive the System clock.
   If you are using different crystal you have to adapt those functions accordingly.
   */

#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)

/* #define SYSCLK_FREQ_HSE   HSE_VALUE */

 #define SYSCLK_FREQ_24MHz 24000000

#else

/* #define SYSCLK_FREQ_HSE   HSE_VALUE */
/* #define SYSCLK_FREQ_24MHz 24000000 */
/* #define SYSCLK_FREQ_36MHz 36000000 */
/* #define SYSCLK_FREQ_48MHz 48000000 */
/* #define SYSCLK_FREQ_56MHz 56000000 */

#define SYSCLK_FREQ_72MHz 72000000

#endif

При включении, микроконтроллер начинает свою работу с тактированием от встроенного RC генератора, во время выполнения кода инициализации, программа производит переключение системы тактирования, на тактирование от внешнего кварцевого резонатора, если кварцевый резонатор окажется неисправным или не подключенным, то микроконтроллер аппаратно изменит выбор источника тактирования на встроенный RC генератор. Если кварцевый резонатор является исправным, то после его запуска производится умножение его частоты модулем «PLL», полученная частота является частотой системного тактирования (тактирования ядра микроконтроллера), и она не должна превышать максимальной частоты для данного микроконтроллера, в нашем случае 24МГц. Для тактирования периферийных модулей, эта частота может быть поделена модулем «AHB Prescaler».

Для линеек микроконтроллеров STM32F10X_LD_VL, STM32F10X_MD_VL и STM32F10X_HD_VL доступны только три возможных варианта тактирования, это работа на тактовой частоте 24МГц (при установленном кварцевом резонаторе на 8МГц), работа на частоте кварцевого резонатора (для этого необходимо закомментировать SYSCLK_FREQ_24MHz и раскомментировать SYSCLK_FREQ_HSE) либо от встроенного RC генератора (для этого необходимо закомментировать все строки).

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

Написание программы

Закончив с созданием и настройкой проекта, можно приступить к написанию реальной программы. Как повелось у всех программистов, первой программой написанной для работы на компьютере, является программа, выводящая на экран надпись «HelloWorld», так и у всех микроконтроллерщиков первая программа для микроконтроллера производит мигание светодиода. Мы не будем исключением из этой традиции и напишем программу, которая будет управлять светодиодом LD3 на плате STM32VL Discovery.

После создания пустого проекта в IAR, он создает минимальный код программы:

Поскольку наша функция main не должна завершаться, и возвращать каких либо значений (она будет работать в бесконечном цикле), то изменим данный код, убрав тип возвращаемой функцией переменной, оператор возврата из функции return и добавим цикл while, сделав его бесконечным.

void main()
{
 while(1)
 {

 }
}

Теперь наша программа будет всегда «крутиться» в цикле while.

Для того, чтобы мы могли управлять светодиодом, нам необходимо разрешить тактирование порта к которому он подключен и настроить соответствующий вывод порта микроконтроллера на выход. Как мы уже рассматривали ранее в первой части, за разрешение тактирования порта С отвечает битIOPCEN регистра RCC_APB2ENR. Согласно документу «RM0041 Reference manual.pdf» для разрешения тактирования шины порта С необходимо в регистре RCC_APB2ENR установить бит IOPCEN в единицу. Чтобы при установке данного бита, мы не сбросили другие, установленные в данном регистре, нам необходимо к текущему состоянию регистра применить операцию логического сложения (логического «ИЛИ») и после этого записать полученное значение в содержимое регистра. В соответствии со структурой библиотеки ST, обращение к значению регистра для его чтения и записи производится через указатель на структуру RCC->APB2ENR. Таким образом, вспоминая материал со второй части, можно записать следующий код, выполняющий установку бита IOPCEN в регистре RCC_APB2ENR:

RCC->APB2ENR = RCC->APB2ENR | RCC_APB2ENR_IOPCEN;

//Или этот же код в более короткой форме записи

RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

Как можно убедиться, из файла «stm32f10x.h», значение бита IOPCEN определено как 0x00000010, что соответствует четвертому биту (IOPCEN) регистра APB2ENR и совпадает со значением указанным в даташите.

Теперь аналогичным образом настроим вывод 9 порта С. Для этого нам необходимо настроить данный вывод порта на выход в режиме push-pull. За настройку режима порта на вход/выход отвечает регистр GPIOC_CRH, мы его уже рассматривали в первой статье, его описание также находится в разделе «7.2.2 Port configuration register high» даташита. Для настройки вывода в режим выхода с максимальным быстродействием 2МГц, необходимо в регистре GPIOC_CRH установить MODE9[1] в единицу и сбросить бит MODE9[0] в нуль. За настройку режима работы вывода в качестве основной функции с выходом push-pull отвечают биты CNF9[1] и CNF9[0], для настройки требуемого нам режима работы, оба эти бита должны быть сброшены в ноль.

//очистить разряды MODE9 (сбросить биты MODE9_1 и MODE9_0 в нуль)

GPIOC->CRH &= ~GPIO_CRH_MODE9;

//Выставим бит MODE9_1, для настройки вывода на выход с быстродействием 2MHz

GPIOC->CRH |= GPIO_CRH_MODE9_1;

//очистить разряды CNF и настроить как выход общего назначения, симметричный (push-pull)

GPIOC->CRH &= ~GPIO_CRH_CNF9;

Теперь вывод порта, к которому подключен светодиод, настроен на выход, для управления светодиодом нам необходимо изменить состояние вывода порта, установив на выходе логическую единицу. Для изменения состояния вывода порта существует два способа, первый это запись непосредственно в регистр состояния порта измененного содержимого регистра порта, так же как мы производили настройку порта. Данный способ не рекомендуется использовать в виду возможности возникновения ситуации, при которой в регистр порта может записаться не верное значение. Данная ситуация может возникнуть если во время изменения состояния регистра, с момента времени когда уже было произведено чтение состояния регистра и до момента когда произведется запись измененного состояния в регистр, какое либо периферийное устройство или прерывание произведет изменение состояния данного порта. По завершению операции по изменению состояния регистра произойдет запись значения в регистр без учета произошедших изменений. Хотя вероятность возникновения данной ситуации является очень низкой, все же стоит пользоваться другим способом, при котором описанная ситуация исключена. Для этого в микроконтроллере существует два регистра GPIOx_BSRR и GPIOx_BRR. При записи логической единицы в требуемый бит регистра GPIOx_BRR произойдет сброс соответствующего вывода порта в логический ноль. Регистр GPIOx_BSRR может производить как установку, так и сброс состояния выводов порта, для установки вывода порта в логическую единицу необходимо произвести установку битов BSn, соответствующих номеру необходимого бита, данные биты располагаются в младших регистрах байта. Для сброса состояния вывода порта в логический нуль, необходимо произвести запись битов BRn соответствующих выводов, эти биты располагаются в старших разрядах регистра порта.

Светодиод LD3 подключен к выводу 9 порта С. Для включения данного светодиода, нам необходимо подать на соответствующем выводе порта логическую единицу, чтобы «зажечь» светодиод.

//Установка вывода 9 порта С в логическую единицу

GPIOC->BSRR = GPIO_BSRR_BS9;

//Сброс состояния вывода 9 порта С в логический ноль

GPIOC->BSRR = GPIO_BSRR_BR9;

//Еще один вариант сброса состояния вывода 9 порта С в логический ноль

GPIOC->BRR  = GPIO_BRR_BR9;

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

//Не забываем подключить заголовочный файл с описанием регистров микроконтроллера

#include «stm32f10x.h»

//объявляем функцию программной задержки

void Delay (void);

//сама функция программной задержки

void Delay (void)
{
 unsigned long i;
 for (i=0; i<2000000; i++);
}

//Наша главная функция

void main(void)
{

 //Разрешаем тактирование шины порта С
 RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

 //очистим разряды MODE9 (сбросить биты MODE9_1 и MODE9_0 в нуль)
 GPIOC->CRH &= ~GPIO_CRH_MODE9;

 //Выставим бит MODE9_1, для настройки вывода на выход с быстродействием 2MHz
 GPIOC->CRH |= GPIO_CRH_MODE9_1;

 //очистим разряды CNF (настроить как выход общего назначения, симметричный (push-pull))
 GPIOC->CRH &= ~GPIO_CRH_CNF9;

 //Наш основной бесконечный цикл

  while(1)
  {

  //Установка вывода 9 порта С в логическую единицу («зажгли» светодиод)
  GPIOC->BSRR = GPIO_BSRR_BS9;

  //Добавляем программную задержку, чтобы светодиод светился некоторое время
  Delay();

  //Сброс состояния вывода 9 порта С в логический ноль
  GPIOC->BSRR = GPIO_BSRR_BR9;

  //Добавляем снова программную задержку
  Delay();

  }
}

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

Наша первая работоспособная программа написана, при её написании, для работы и настройки периферии, мы пользовались данными из официального даташита «RM0041 Reference manual.pdf», данный источник информации о регистрах микроконтроллера является самым точным, но для того чтобы им пользоваться приходится перечитывать много информации, что усложняет написание программ. Для облегчения процесса настройки периферии микроконтроллера, существуют различные генераторы кода, официальной утилитой от компании ST представлена программа Microxplorer, но она пока еще малофункциональна и по этой причине сторонними разработчиками была создана альтернативная программа «STM32 Генератор программного кода». Данная программа позволяет легко получить код настройки периферии, используя удобный, наглядный графический интерфейс (см. рис. 2).

Скриншот программы STM32 генератор кода
Рис. 2 Скриншот программы STM32 генератор кода

Как видно из рисунка 2, сгенерированный программой код настройки вывода светодиода совпадает с кодом написанным нами ранее.

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

Видео режима отладки программы мигания светодиодом

Видео работы программы мигания светодиодом на плате STM32VL Discovery

Библиотечные функции работы с периферией

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

Теперь напишем нашу программу с использованием библиотеки ST. В программе требуется произвести настройку портов ввода/вывода, для использования библиотечных функций настройки портов, необходимо произвести подключение заголовочного файла «stm32f10x_gpio.h» (см. табл. 1). Подключение данного файла можно произвести расскоментированием соответствующей строки в подключенном заголовочном конфигурационном файле «stm32f10x_conf.h». В конце файла «stm32f10x_gpio.h» имеется список объявлений функций для работы с портами. Подробное описание всех имеющихся функций можно прочитать в файле «stm32f10x_stdperiph_lib_um.chm», краткое описание наиболее часто применяемых приведено в таблице 2.

Таблица 2.Описание основных функций настройки портов

Функция

Описание функции, передаваемых и возвращаемых параметров

GPIO_DeInit (
GPIO_TypeDef* GPIOx)

Производит установку значений регистров настройки порта GPIOx на значения по умолчанию

GPIO_Init (
GPIO_TypeDef* GPIOx,
GPIO_InitTypeDef* GPIO_InitStruct)

Производит установку регистров настройки порта GPIOx в соответствии с указанными параметрами в структуре GPIO_InitStruct

GPIO_StructInit (
GPIO_InitTypeDef* GPIO_InitStruct)

Заполняет все поля структуры GPIO_InitStruct, значениями по умолчания

uint8_t GPIO_ReadInputDataBit(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin);

Чтение входного значения вывода GPIO_Pin порта GPIOx

uint16_t GPIO_ReadInputData (
GPIO_TypeDef* GPIOx)

Чтение входных значений всех выводов порта GPIOx

GPIO_SetBits(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin)

Установка выходного значения вывода GPIO_Pin порта GPIOx в логическую единицу

GPIO_ResetBits(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin)

Сброс выходного значения вывода GPIO_Pin порта GPIOx в логический ноль

GPIO_WriteBit(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin,
BitAction BitVal)

Запись значения BitVal в вывод GPIO_Pin порта GPIOx

GPIO_Write(
GPIO_TypeDef* GPIOx,
uint16_t PortVal)

Запись значения PortVal в порт GPIOx

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

Список переменных, включенных в структуры для функций работы с портами, описаны в том же файле несколько выше описания функций. Так, например, структура «GPIO_InitTypeDef» имеет следующую структуру:

typedef struct
{

 uint16_t GPIO_Pin;   /*!< Specifies the GPIO pins to be configured.
   This parameter can be any value of @ref GPIO_pins_define */

 GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins.
   This parameter can be a value of @ref GPIOSpeed_TypeDef */

 GPIOMode_TypeDef GPIO_Mode;   /*!< Specifies the operating mode for the selected pins.
   This parameter can be a value of @ref GPIOMode_TypeDef */

}GPIO_InitTypeDef;

Первое поле данной структуры содержит переменную «GPIO_Pin» типа unsigned short, в данную переменную необходимо записывать флаги номеров соответствующих выводов, для которых предполагается произвести необходимую настройку. Можно произвести настройку сразу несколько выводов, задав в качестве параметра несколько констант через оператор побитовое ИЛИ (см. часть 2). Побитовое ИЛИ «соберёт» все единички из перечисленных констант, а сами константы являются маской, как раз предназначенной для такого использования. Макроопределения констант указаны в этом же файле ниже.

Второе поле структуры «GPIO_InitTypeDef» задает максимально возможную скорость работы выхода порта. Список возможных значений данного поля перечислен выше:

typedef enum
{
 GPIO_Speed_10MHz = 1,
 GPIO_Speed_2MHz,
 GPIO_Speed_50MHz
}GPIOSpeed_TypeDef;

Последнее, третье поле, «GPIOMode_TypeDef» отвечает за режим работы выводов порта. Данное поле представляет из себя перечисляемый тип enum и выглядит следующим образом:

typedef enum
{ GPIO_Mode_AIN = 0x0,
 GPIO_Mode_IN_FLOATING = 0x04,
 GPIO_Mode_IPD = 0x28,
 GPIO_Mode_IPU = 0x48,
 GPIO_Mode_Out_OD = 0x14,
 GPIO_Mode_Out_PP = 0x10,
 GPIO_Mode_AF_OD = 0x1C,
 GPIO_Mode_AF_PP = 0x18
}GPIOMode_TypeDef;

Описание возможных значений:

  • GPIO_Mode_AIN — аналоговый вход (англ. Analog INput);
  • GPIO_Mode_IN_FLOATING — вход без подтяжки, болтающийся (англ. Input float) в воздухе
  • GPIO_Mode_IPD — вход с подтяжкой к земле (англ. Input Pull-down)
  • GPIO_Mode_IPU — вход с подтяжкой к питанию (англ. Input Pull-up)
  • GPIO_Mode_Out_OD — выход с открытым стоком (англ. Output Open Drain)
  • GPIO_Mode_Out_PP — выход двумя состояниями (англ. Output Push-Pull — туда-сюда)
  • GPIO_Mode_AF_OD — выход с открытым стоком для альтернативных функций (англ. Alternate Function). Используется в случаях, когда выводом должна управлять периферия, прикрепленная к данному выводу порта (например, вывод Tx USART1 и т.п.)
  • GPIO_Mode_AF_PP — то же самое, но с двумя состояниями

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

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

GPIO_InitTypeDef GPIO_Init_struct;  //Объявляем структуру

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

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

GPIO_StructInit(&GPIO_Init_struct);

/* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем настраивать далее */

GPIO_Init_struct.GPIO_Pin=GPIO_Pin_9;

/* Подобным образом заполним поле GPIO_Speed */

GPIO_Init_struct.GPIO_Speed= GPIO_Speed_2MHz;
GPIO_Init_struct.GPIO_Mode = GPIO_Mode_Out_PP;

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

GPIO_Init(GPIOC, &GPIO_Init_struct);

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

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

//Не забываем подключить заголовочный файл с описание регистров микроконтроллера

#include «stm32f10x.h»
#include «stm32f10x_conf.h»

//объявляем функцию программной задержки

void Delay (void);

//сама функция программной задержки

void Delay (void)
{
 unsigned long i;
 for (i=0; i<2000000; i++);
}

//Наша главная функция

void main(void)
{

 //Разрешаем тактирование шины порта С
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

 //Объявляем структуру для настройки порта
 GPIO_InitTypeDef GPIO_Init_struct;

 //Заполняем структуру начальными значениями
 GPIO_StructInit(&GPIO_Init_struct);

 /* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем   настраивать далее */
 GPIO_Init_struct.GPIO_Pin = GPIO_Pin_9;

 // Подобным образом заполним поля GPIO_Speed и GPIO_Mode
 GPIO_Init_struct.GPIO_Speed= GPIO_Speed_2MHz;
 GPIO_Init_struct.GPIO_Mode = GPIO_Mode_Out_PP;

 //Передаем заполненную структуру, для выполнения действий по настройке регистров
 GPIO_Init(GPIOC, &GPIO_Init_struct);

 //Наш основной бесконечный цикл
 while(1)
 {
  //Установка вывода 9 порта С в логическую единицу («зажгли» светодиод)
  GPIO_SetBits(GPIOC, GPIO_Pin_9);

  //Добавляем программную задержку, чтобы светодиод светился некоторое время
  Delay();

  //Сброс состояния вывода 9 порта С в логический ноль
  GPIO_ResetBits(GPIOC, GPIO_Pin_9);

  //Добавляем снова программную задержку
  Delay();
 }
}

Пример рабочего проекта программы мигания светодиода с использованием прерывания можно скачать по ссылке.

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

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

Обработчик прерывания

Микроконтроллеры имеют одну замечательную способность – останавливать выполнение основной программы по какому-то определенному событию, и переходить к выполнению специальной подпрограммы – обработчику прерывания. В качестве источников прерывания могут выступать как внешние события – прерывания по приему/передаче данных через какой либо интерфейс передачи данных, или изменение состояния вывода, так и внутренние – переполнение таймера и т.п.. Список возможных источников прерывания для микроконтроллеров серии STM32 приведен в даташите «RM0041 Reference manual» в разделе «8 Interrupts and events».

Поскольку обработчик прерывания также является функцией, то и записываться она будет как обычная функция, но чтобы компилятор знал, что данная функция является обработчиком определенного прерывания, в качестве имени функции следует выбрать заранее определенные имена, на которые указаны перенаправления векторов прерывания. Список имен этих функций с кратким описанием находится в ассемблерном файле «startup_stm32f10x_md_vl.s». На один обработчик прерывания может приходиться несколько источников вызывающих прерывания, например функция обработчик прерывания «USART1_IRQHandler» может быть вызвана в случае окончания приема и окончания передачи байта и т.д..

Для начала работы с прерываниями следует настроить и проинициализировать контроллер прерываний NVIC. В архитектуре Cortex M3 каждому прерыванию можно выставить свою группу приоритета для случаев, когда возникает несколько прерываний одновременно. Затем следует произвести настройку источника прерывания.

//Для начала выбираем приоритетную группу для прерываний

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);

/* Далее, для каждого прерывания, нам надо произвести настройку и инициализацию с помощью структуры — точно так же, как мы поступали ранее при настройке портов*/

//Объявляем структуру

NVIC_InitTypeDef NVIC_InitStruct;

//Производим заполнение полей структуры

NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;

// Структура настроена, передаем её в функцию, тем самым инициализируем прерывания от USART:

NVIC_Init(&NVIC_InitStruct);

В поле NVIC_IRQChannel указывается, какое именно прерывание мы хотим настроить. Константа USART1_IRQn обозначает канал, отвечающий за прерывания, связанные с USART1. Она определена в файле «stm32f10x.h», там же определены другие подобные константы.

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

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

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

//Разрешаем прерывания по окончанию передачи байта по USART1
USART_ITConfig(USART1, USART_IT_TXE, ENABLE);

//Разрешаем прерывания по окончанию приема байта по USART1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

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

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

void USART1_IRQHandler(void)
{
//Обработка события приема байта
if (USART_GetITStatus(USART1, USART_IT_RXNE) )
   {
   USART_ClearITPendingBit(USART1, USART_IT_RXNE); //Сбрасываем флаг
    // … Код обработчика прерывания…
   };

// Обработка события завершения передачи байта
if ( USART_GetITStatus(USART1, USART_IT_TXE) )
   {
   USART_ClearITPendingBit(USART1, USART_IT_TXE);
    // … Код обработчика прерывания …
   };
}

Для выполнения различных, небольших, повторяющихся с точным периодом действий, в микроконтроллерах с ядром Cortex-M3 имеется специально предназначенный для этого системный таймер. В функции данного таймера входит только вызов прерывания через строго заданные интервалы времени. Как правило, в вызываемом этим таймером прерывании, размещают код для измерения продолжительности различных процессов. Объявление функции настройки таймера размещено в файле «core_cm3.h». В качестве передаваемого функции аргумента указывается число тактов системной шины между интервалами вызова обработчика прерывания системного таймера.

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

Пример файла «main.с» программы мигания светодиода с использованием прерывания:

//Подключаем заголовочный файл с описанием регистров микроконтроллера

#include «stm32f10x.h»
#include «stm32f10x_conf.h»
#include «main.h»

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

unsigned int LED_timer;

//Функция, вызываемая из функции-обработчика прерываний системного таймера

void SysTick_Timer_main(void)
{
//Если переменная LED_timer еще не дошла до 0,
   if (LED_timer)
   {
   //Проверяем ее значение, если оно больше 1500 включим светодиод
   if (LED_timer>1500)  GPIOC->BSRR= GPIO_BSRR_BS9;

   //иначе если меньше или равно 1500 то выключим
   else GPIOC->BSRR= GPIO_BSRR_BR9;

   //Произведем декремент переменной LED_timer
   LED_timer—;
   }

   //Ели же значение переменной дошло до нуля, зададим новое значение 2000
   else LED_timer=2000;
}

//Наша главная функция

void main(void)
{

   //Разрешаем тактирование шины порта С
   
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

   //Объявляем структуру для настройки порта
   
GPIO_InitTypeDef GPIO_Init_struct;

   //Заполняем структуру начальными значениями
   
GPIO_StructInit(&GPIO_Init_struct);

   /* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем    настраивать далее */
   
GPIO_Init_struct.GPIO_Pin = GPIO_Pin_9;

   // Подобным образом заполним поля GPIO_Speed и GPIO_Mode
   
GPIO_Init_struct.GPIO_Speed= GPIO_Speed_2MHz;
   GPIO_Init_struct.GPIO_Mode = GPIO_Mode_Out_PP;

   //Передаем заполненную структуру, для выполнения действий по настройке регистров
   
GPIO_Init(GPIOC, &GPIO_Init_struct);

   //выбираем приоритетную группу для прерываний
   
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);

   //Настраиваем работу системного таймера с интервалом 1мс
   
SysTick_Config(24000000/1000);

   //Наш основной бесконечный цикл
   
while(1)
   {
   //В этот раз тут пусто, все управление светодиодом происходит в прерываниях
  
  }
}

Часть исходного кода в файле «stm32f10x_it.c»:


#include «main.h»

/**
 * @brief This function handles SysTick Handler.
 * @param None
 * @retval None
 */

void SysTick_Handler(void)
{
    SysTick_Timer_main();
}

Пример рабочего проекта программы мигания светодиода с использованием прерывания можно скачать по ссылке.

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

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

Часть 1. ARM – это просто. Введение.
Часть 2. ARM – это просто. Учимся говорить с электроникой на одном языке.

Теги:

Понравилась статья? Поделить с друзьями:
  • Как написать библиографию
  • Как написать библиографическое описание статьи
  • Как написать библиографическое описание книги
  • Как написать библиографический список интернет ресурсов
  • Как написать бешу или бесю