§> Общие вопросы. Переменные объявляемые пользователем.

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


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

Зачем нужно объявлять переменные?

Хоть язык Си и абстрактный, используемый разработчиком микроконтроллер, как правило, вполне конкретный и имеет своё адресное пространство памяти с заданными свойствам, где и будет храниться объявляемая переменная. Объявление, помимо присвоения переменной имени, заставляет компилятор разместить её по конкретному адресу в памяти микроконтроллера (по какому именно нас в большинстве случаев совершенно не интересует).

Как нужно объявлять переменные?

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

Type name; // Переменная с именем "name" и типом "type".

Здесь: type - так называемый идентификатор типа переменной из определённого набора стандартных типов;
name - произвольное имя переменной, лишь бы оно не начиналось с цифры, состояло только из латинских символов, и не совпадало со служебными словами языка Си (список которых не так велик, чтобы столкнуться с такой ситуацией нужно на самом деле очень постараться).

Что такое идентификатор типа и зачем его упоминать?

Для хранения переменной микроконтроллер использует ячейки памяти, размер которых определяется его разрядностью. Так например, микроконтроллеры семейства AVR - 8-разрядные, а значит для хранения данных используют ячейки памяти размером в один байт, которые способны сохранять 256 различных числовых значений. Если ожидаемые значения переменной могут превысить это количество, то для её хранения понадобится две или более ячеек памяти. Поскольку Си, строго говоря, не представляет какие значения мы планируем присваивать переменной, то просит нас указать её тип, который как раз и определяет допустимый диапазон значений. Это необходимо чтобы не зарезервировать за ней избыточный или недопустимо малый объём памяти, а так же предупреждать нас при попытке присвоить слишком большое значение переменной, не способной его сохранить. Для 8-разрядных микроконтроллеров наиболее часто употребимые целочисленные типы данных следующие:

Способные хранить только положительные значения (беззнаковые):
unsigned char - занимает один байт памяти, значения 0...255
unsigned int - два байта, значения 0...65535
unsigned long - четыре байта, от 0 до (2^32)-1
способные хранить значения со знаком (знаковые):
signed char - занимает один байт памяти, от -128...127
signed int - два байта, значения -32768...32767
signed long - требует четыре байта, значения от -(2^31) до (2^31)

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

Float – 32 бита, значения от ±1.18E-38 до ±3.39E+38
double – 32 (±1.18E-38…±3.39E+38) или 64 бита (±2.23E-308…±1.79E+308) в зависимости от настроек компилятора.

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

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

Например:

Int A=100; // Переменная с именем "А" типом int и начальным значением равным 100.

Практический пример: пусть планируется написать программу, мигающую светодиодом 5 раз. Для подсчёта числа миганий потребуется переменная, значение которой, очевидно никогда не будет отрицательным и не выйдет за пределы диапазона от 0 до 255, а значит в данном случае будет вполне достаточно использовать однобайтовый тип char:

§> Область видимости переменной.

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

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

§> Область размещения переменной.

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


Для хранения пользовательских переменных может быть использована ОЗУ, энергонезависимая память EEPROM, а для хранения констант, значение которых не может быть изменено в процессе работы программы также и FLASH- память микроконтроллера.

Для начала полезно знать, что переменные, объявленные пользователем без использования специальных ключевых слов типа _eeprom или _flash, размещаются в ОЗУ микроконтроллера, в виде одной или нескольких ячеек статической памяти SRAM. В процессе работы они периодически копируются в быструю регистровую память РОН, которая непосредственно взаимодействует с арифметически-логическим блоком АЛУ микроконтроллера.
Вопросы размещения переменных внутри ОЗУ, как правило, представляют интерес только в контексте быстродействия программы.

§> Регистры специального назначения микроконтроллера SFR.

Итак, мы кратко рассмотрели объявление переменных предназначенных для организации вычислительного процесса, которые мало связаны со спецификой аппаратной части МК.

Управление и контроль работы микроконтроллера и его отдельных внутренних модулей осуществляется путём записи и чтения специальных ячеек-регистров в служебной области памяти ОЗУ - регистров специального назначения (Special Function Register, далее просто SFR).

Основная идея, позволяющая использовать Си для программирования микроконтроллеров, такова: регистры специального значения являются такими же переменными языка Си, как и объявленные пользователем. Этим переменным можно присваивать значения, управляя работой микроконтроллера, или считывать их, получая таким образом информацию о его текущем состоянии. Объявлять регистры микроконтроллера подобно пользовательским переменным не нужно по нескольким причинам. Во-первых, их размер заранее известен: в Си для AVR это беззнаковые 8-разрядные переменные. Во-вторых, SFR имеют строго определённые имена и адреса в памяти, являясь так называемыми регистрами ввода-вывода.

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

В начале любой программы на Си мы можем видеть строки типа:

#include "file1.h" // Включить в код содержимое файла "file1.h".

#include - это директива (указание), заставляющая среду разработки поместить в данное место программы содержимое файла с именем file1.h. Файлы с расширением.h называются заголовочными или h-файлами. Разработчик может создавать собственные h-файлы и помещать их, учитывая содержимое, в любое место программы. Однако, чтобы познакомить программу с SFR для данного типа микроконтроллера, необходимо подключать вполне конкретные заголовочные файлы. Их имена и количество зависит от конкретной среды разработки и типа используемого микроконтроллера, так, например, в IAR для Atmega64 достаточно прописать строки:

#include "iom64"
#include "inavr.h"

После включения в текст необходимых h-файлов программа будет узнавать упоминаемые в ней имена SFR, например, регистр статуса микроконтроллера AVR с именем SREG, буфер приёма/передачи модуля UART - UDR и так далее.

Заготовка программы для IAR, которая ничего не делает, но уже не "ругается" на имена регистров специального назначения микроконтроллера Atmega16, должна выглядеть так:

#include "iom16.h"
#include "inavr.h"
unsigned char ChisloMiganiy=0;
void main (void)
{
// Здесь мы разместим программу, использующую переменную ChisloMiganiy
// и любые регистры Atmega16, имена которых прописаны в файле iom16.h.
}

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

§> Обзор стандартных операций с регистрами.

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

1. Запись в регистр необходимого значения.
2. Чтение значения регистра.
3. Установка в единицу нужных разрядов регистра.
4. Сброс разрядов регистра в ноль.
5. Проверка разряда на логическую единицу или логический ноль.
6. Изменение логического состояния разряда регистра на противоположное.

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

A = 16; // Присвоить переменной A значение 16;
A = B; // Считать значение переменной B и присвоить это значение переменной A;
A = B+10; // Считать значение переменной B, прибавить к считанному значению 10, результат присвоить переменной A (значение переменной B при этом не изменяется).

§> Запись и чтение регистров.

Из рассмотренных примеров видно, что оператор присваивания сам по себе решает две первые задачи — запись и чтение значений регистров. Например для отправки микроконтроллером AVR байта по шине UART достаточно записать его в передающий регистр с именем UDR:

UDR = 8; // Отправить по UART число 8;

Чтобы получить принятый по UART байт достаточно считать его из регистра UDR:

§> Установка битов регистров.

Язык Си не имеет в своём составе команд непосредственного сброса или установки разрядов переменной, однако присутствуют побитовые логические операции "И" и "ИЛИ", которые успешно используются для этих целей.
Оператор побитовой логической операции "ИЛИ" записывается в виде вертикальной черты - "|" и может выполнятся между двумя переменными, а так же между переменной и константой. Напомню, что операция "ИЛИ" над двумя битами даёт в результате единичный бит, если хотя бы один из исходных битов находится с состоянии единицы. Таким образом для любого бита логическое "ИЛИ" с "1" даст в результате "1", независимо от состояния этого бита, а "ИЛИ" с логическим "0" оставит в результате состояние исходного бита без изменения. Это свойство позволяет использовать операцию "ИЛИ" для установки N-ого разряда в регистре. Для этого необходимо вычислить константу с единичным N-ным битом по формуле 2^N, которая называется битовой маской и выполнить логическое "ИЛИ" между ней и регистром, например для установки бита №7 в регистре SREG:

(SREG | 128) — это выражение считывает регистр SREG и устанавливает в считанном значении седьмой бит, далее достаточно изменённое значение снова поместить в регистр SREG:

SREG = SREG | 128; // Установить бит №7 регистра SREG.

Такую работу с регистром принято называть "чтение - модификация - запись", в отличие от простого присваивания она сохраняет состояние остальных битов без именения.
Приведённый программный код, устанавливая седьмой бит в регистре SREG, выполняет вполне осмысленную работу - разрешает микроконтроллеру обработку программных прерываний. Единственный недостаток такой записи — в константе 128 не легко угадать установленный седьмой бит, поэтому чаще маску для N-ного бита записывают в следующем виде:

(1<

SREG = SREG | (1<<7);

Или ещё проще с использование краткой формы записи языка Си:

SREG |= (1<<7);

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

§> Сброс битов в регистрах.

Ещё одна логическая операция языка Си – побитовое "И", записывается в виде символа "&". Как известно, операция логического "И", применительно к двум битам даёт единицу тогда и только тогда, когда оба исходных бита имеют единичное значение, это позволяет применять её для сброса разрядов в регистрах. При этом используется битовая маска, в которой все разряды единичные, кроме нулевого на позиции сбрасываемого. Её легко получить из маски с установленным N-ным битом, применив к ней операцию побитного инвертирования:
~(1<

SREG = SREG & (~ (1<<7)); или кратко: SREG &= ~ (1<<7);

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

#define OCIE0 1

Здесь #define – указание компилятору заменять в тексте программы сочетание символов "OCIE0" на число 1, то есть стандартное имя бита OCIE0, который входит в состав регистра TIMSK микроконтроллера Atmega64 на его порядковый номер в этом регистре. Благодаря этому установку бита OCIE0 в регистре TIMSK можно нагляднее записывать так:

TIMSK|=(1<

Устанавливать или сбрасывать несколько разрядов регистра одновременно можно, объединяя битовые маски в выражениях оператором логического "ИЛИ":

PORTA |= (1<<1)|(1<<4); // Установить выводы 1 и 4 порта A в единицу;
PORTA&=~((1<<2)|(1<<3)); // Выводы 2 и 3 порта A сбросить в ноль.

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

DAC0->CTRL |= DAC_CTRL_DIFF; // установка
DAC0->CTRL &= ~DAC_CTRL_DIFF; //сброс

§> Проверка разрядов регистра на ноль и единицу.

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

(REGISTR & (1<

Приведённое выражение можно использовать в условном операторе if (выражение) или операторе цикла while (выражение), которые относятся к группе логических, то есть воспринимают в качестве аргументов значения типа истина и ложь. Поскольку язык Си, приводя числовые значения к логическим, любые числа не равные нулю воспринимает как логическую истину, значение (REGISTR & (1< Если появляется необходимость при установленном бите N получить для нашего выражения логическое значение «ложь», достаточно дополнить его оператором логической инверсии в виде восклицательного знака - !(REGISTR & (1<

While (!(UCSRA & (1<

Здесь при сброшенном бите UDRE выражение (UCSRA & (1< !(UCSRA & (1<

§> Изменение состояния бита регистра на противоположное.

Эту, с позволения сказать, проблему с успехом решает логическая операция побитного "ИСКЛЮЧАЮЩЕГО ИЛИ” и соответствующий ей оператор Си, записываемый в виде символа " ^ ”. Правило "исключающего или" с двумя битами даёт "истину” тогда и только тогда, когда один из битов установлен, а другой сброшен. Не трудно убедиться, что этот оператор, применённый между битовой маской и регистром, скопирует в результат биты стоящие напротив нулевых битов маски без изменения и инвертирует расположенные напротив единичных. Например, если: reg=b0001 0110 и mask=b0000 1111, то reg^mask=b0001 1001. Таким способом можно менять состояние светодиода, подключенного к пятому биту порта A:

#define LED 5 // Заменять в программе сочетание символов LED на число 5 (вывод светодиода).

PORTA ^=(1<< LED); // Погасить светодиод, если он светится и наоборот.

§> Арифметика и логика языка Си.

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


Для более подробного знакомства с операциями над переменными и языком Си в целом, рекомендую книгу "Язык программирования Си" Б. Керниган, Д. Ритчи.

Преобразование типов переменных – это часть внутренней автоматической работы компилятора, происходящая в строгом соответствии с правилами языка программирования. Сам разработчик при написании программы в явном виде этим, как правило, не занимается. Однако, неаккуратное объявление типов переменных, или присвоение переменной значения превышающего допустимый диапазон, и даже неправильный формат записи константы, могут привести к потере данных и некорректной работе программы, при полном молчании компилятора.
Когда происходит и в чём заключается приведение типов? Таких ситуаций достаточно много. Рассмотрим наиболее опасные из них.

§> Преобразование типа выражения перед присвоением переменной.

В первом разделе мы обращали своё внимание на необходимость явного указания типа объявляемой переменной. Это позволяет компилятору зарезервировать за ней нужное количество адресного пространства и определить диапазон значений, которые она способна хранить. Тем не менее, мы не застрахованы он того, что в процессе выполнения программы произойдёт попытка записать в переменную значение свыше предельно допустимого. В самых грубых случаях компилятор выдаст нам сообщение о возможной ошибке. Например, при желании записать в переменную типа unsigned char (диапазон от 0 до 255) число 400:

Unsigned char a=400; // выдаст сообщение типа "integer conversion resulted in truncation”

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

Unsigned char x=200, y=200;
x=x+y;

При таком варианте, не смотря на то, что значение выражение (x+y) так же равно 400, никаких предупреждений со стороны компилятора уже не последует. А в переменную x запишется только младший байт числа 400, то есть 144. И здесь компилятор трудно в чём-то упрекнуть, ведь вместо явно проинициализированной переменной в выражении может быть использован, например, приёмный регистр шины UART, в котором может оказаться любое значение, принятое от внешнего устройства.
Другой пример в этом же духе – присвоение дробного значения переменной целого типа:

Float a=1.5; // Объявлена переменная с плавающей точкой.

b=a*b; // Ожидается, что в переменную b будет записано значение 4,5.

В результате в переменной b сохранится только целая часть результата a*b – число 4.

§> Преобразование результата выражения к типу наиболее точной переменной в выражении.

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

Float a =1.5; // Объявлена переменная a с плавающей точкой.
char b=3; // Объявлена целочисленная переменная.

В выражении (a*b) переменная float a имеет более высокий тип, потому что может сохранять любое целое значение из диапазона 0…255 типа char. Результат выражения (a*b) будет иметь тип float.
Типичный пример неожиданности для этого случая – попытка получить дробное число делением двух целочисленных:

Char a=3; // Объявлена целочисленная переменная.
char b=4; // Объявлена целочисленная переменная.
float c; // Объявлена переменная "c" с плавающей точкой для сохранения результата.
c=a/b; // Ожидается, что "c" будет равно 0,75 (¾).

В отличие от предыдущего примера, результат записывается в переменную способную хранить числа с плавающей точкой, однако компилятор в соответствии с правилом приведения, получив в результате деления число 0,75 приводит его к типу целочисленных операндов, отбросив дробную часть. В результате в переменную "c” будет записан ноль.
Более реалистичный пример из жизни – расчёт измеряемого напряжения из выходного кода АЦП:

Int ADC; // Двухбайтовая целочисленная переменная для хранения кода АЦП.
float U; // Переменная с плавающей точкой для сохранения значения напряжения.
U= ADC*(5/1024); // Расчёт напряжения.

Здесь упущено из виду то, что константа в Си, как и любая переменная, тоже имеет свой тип. Его желательно указывать явно или, используя соответствующую форму записи. Константы 5 и 1024 записаны без десятичной точки и будут восприняты языком Си как целочисленные. Как следствие, результат выражения (5/1024) тоже будет приведён к целому – 0 вместо ожидаемого 0,00489. Это не случилось бы при записи выражения в формате (5.0/1024).
Приведённых ошибок также можно избежать, используя оператор явного приведения типов выражений языка Си, который записывается в виде названия типа, заключённого в круглые скобки и воздействует на выражение стоящее после него. Этот оператор приводит результат выражения к явно указанному типу, не взирая на типы его операндов:

C= (float) a/b; // Ожидается, что "c" будет равно 0,75 (¾);
U= ADC * ((float)5/1024); // Расчёт напряжения.

§> Назначение функций.

Ещё древние программисты обратили своё внимание на один занимательный факт – зачастую программа вынуждена несколько раз выполнять ровно одну и ту же последовательность действий. Именно тогда родилась идея при достаточно большом наборе таких действий и их повторов, с целью экономии программной памяти, оформлять их в виде отдельной группы, а затем при необходимости просто отправлять программу на её выполнение. Такой обособленный кусок кода в Си как раз и называется функцией. Само название термина "функция” исконно отражает другое свойство некоторых функций – способность (подобно функциям математическим) преобразовывать по заданному алгоритму некие входные данные. Но этом немного позже.

Другое назначение функции, полностью отражющее её название – это выделение в отдельную группу действий связанных одной общей целью, например, функция инициализации портов или функция опроса клавиатуры. Это и есть одно из дополнительных предназначений функции.
Такие функции могут вызываться программой только один раз. Зачем же тогда они нужны? Для обоснования такого подхода в литературе часто приводится фраза неизвестного, но по всей видимости, очень авторитетного древнеримского программиста: " Разделяй и властвуй!”. И действительно, программа, оформленная в виде целевых функциональных блоков гораздо проще для понимания, отладки и последующей модификации, чем набор отдельных, разрозненных по назначению кусков кода.
Обобщая сказанное, можно сформулировать формальные предпосылки к созданию функций в программе, это:

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

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

§> Структура и оформление функций.

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

Тип выходной переменной Имя функции (Типы входных переменных и их имена через запятую)

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

Void имя функции (void)

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

Initialization ();

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

Void initialization (void)
{
DDRA=0xFF; // PORTA на выход.
DDRB|=(1<<0)| (1<<3)| (1<<4); // PB0, PB3, PB4 на выход.
DDRC=0xF0; // Старшая тетрада PORTC на выход.
}

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

§> Обработка параметров функцией.

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

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

InitUart (8, 2);

После этого внутри функции переменным с именами FrameLength и StopBit присвоятся конкретные значении 8 и 2, которые можно использовать, например, для настройки длинны посылки модуля UART и количества его стоп-битов:

Void initUart (char FrameLength, char StopBit)
{
if (FrameLength==8) UCSR0C|=((1<<1)|(1<<2));
if (StopBit==2) UCSR0C|=(1<<3);
}

§> Специализированные функции.

Мы рассмотрели функции, задаваемые самим пользователем. Помимо них в любой программе присутствуют функции, которые выполняют специализированные задачи и должны быть оформлены по особым правилам.
Самая главная функция такого рода, как это видно и самого её названия это функция main. Она характеризуется тем, что выполнение передаётся на неё самим микроконтроллером при подаче питания или после перезагрузки, то есть, именно с неё и начинается работа любой программы. Еще одно свойство функции main состоит в том, что при её выполнении до конца программа автоматически перейдёт на её же начало, то есть она выполняется по циклу, если внутри её самим пользователем специально не был организован бесконечный цикл.
Ещё один вариант системных функций – обработчики прерываний. Их так же невозможно вызвать программно. Микроконтроллер самостоятельно передаёт управление на них в случае возникновения особых аппаратных состояний – условий вызова прерываний.

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

§> Общая структура простейшей программы. Инициализация, фон.

При рассмотрении программы на уровне языка Си можно сказать, что она начинает свою работу с первой строки функции main (строка 001 на рисунке):

Структкра программы на Си
Далее последовательно выполняются строки 002, 003, 004, объёдинённые одним общим свойством: программа проходит по ним только один раз, при запуске микроконтроллера. Эту часть программы принято называть инициализационной. Инициализационная часть - законное место для размещения действий по подготовке периферии микроконтроллера к работе с заданными параметрами - настройки портов на вход или выход, начальной инициализации таймеров, задания скорости и формата кадра UART и так далее для всего, что планируется использовать в дальнейшем.

Поскольку любая программа предназначена для непрерывной работы, нормальный режим её функционирования - это безостановочное повторение по кругу содержимого бесконечного цикла. На практике такой цикл чаще всего реализуется с помощью конструкции while(1) { }, предназначенной для многократного выполнения действий, размещённых внутри её фигурных скобок. Содержимое бесконечного цикла программы называется фоном. Именно здесь происходит основная часть работы по проверке состояния аппаратной части и соответствующее воздействие на неё для получения нужного результата.

Рассмотрим описанную структуру программы на простейшем примере. Пусть необходимо: отправлять по шине UART символ *, пока кнопка на выводе PA0 находится в нажатом состоянии (нулевой уровень сигнала). Программа в данном случае (без лишних процедур по подавлению дребезга кнопки и прочего) может выглядеть так:

Void main (void)
{
PORTA|=(1<<0); // Притянуть вход кнопки PORTA.0 внутренним pull-up резистором.

UCSRB = (1< while (1)
{
if (! (PINA & (1<<0))) // Если кнопка нажата...
{
while(! (UCSRA & (1< UDR = " * "; // Отправить *.
}
// другие команды фона:
00N
00N+1
...
}
}

Здесь конструкция if (...), расположенная в фоне программы проводит бесконечные опросы входного регистра PINA и проверку вывода PA0 на наличие низкого уровня. Далее выполняются другие действия фонового процесса, обозначенные строками 00N, 00N+1 и так далее.

Какие факторы, применительно к данной программе, определяют самые важные параметры её работы - надёжность и быстродействие?

Из примера видно, что частота опроса входа PA.0 определяется длительностью выполнения команд фона, ведь прежде чем в очередной раз опросить кнопку, микроконтроллер должен выполнить следующие за этим строки 00N, 00N+1 и т. д. Очевидно, что надёжность фиксации внешнего события (нажатия на кнопку) в данном случае будет зависеть от соотношения длительности воздействия этого события к периоду его детектирования. Длительность фона в данной программе наверняка будет во много раз меньше длительности удержания кнопки, которое на практике составляет несколько десятков миллисекунд. Однако при разрастании фоновой части программы и малом времени внешнего воздействия, надёжность его отслеживания в определённый момент резко снизится. Что бы этого не произошло, а также для снижения времени реакции программы на внешнее событие, используется система прерываний.

§> Прерывания.

Как работает механизм прерываний? Очень просто, особенно на уровне языка Си!

В архитектуру микроконтроллеров AVR, впрочем как и любых других, на аппаратном уровне заложена способность отслеживать определённые "интересные состояния железа” и устанавливать при этом соответствующие биты-признаки. Такие состояния называются условиями возникновения прерываний, а устанавливаемые признаки - флагами прерываний. В процессе работы, микроконтроллер непрерывно отслеживает состояние этих флагов. При обнаружении любого установленного флага прерывания, при условии, что оно разрешено включением соответствующего бита, а также установлен бит глобального разрешения прерываний (№7 в регистре SREG для AVR), выполнение основной части программы будет временно приостановлено (прервано).

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

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

1. Разрешить использование прерываний в программе.
2. Разрешить вызов интересующего нас прерывания специальным битом в соответствующем регистре. Каким именно и где подскажет описание на микроконтроллер.
3. Создать условия для возникновения прерывания, например, если это переполнение таймера, то банально запустить его. Если это прерывание по изменению состояния внешнего вывода то задать нужные условия для этого (фронт, срез или нулевой уровень).
4. Разместить в программе обработчик прерывания, оформив его в соответствии с требованиями компилятора.

Применительно к нашему примеру организовать отправку в UART по низкому уровню на входе кнопки, можно используя так называемое внешнее прерывание INT0. Данное прерывание вызывается по фронту, срезу или нулевому уровню на выводе INT0.

Перенесем кнопку на вывод PD.2 с альтернативной функцией INT0. В инициализационной части программы разрешим прерывания глобально и INT0 конкретно. Микроконтроллер по-умолчанию настроен на формирование прерывания INT по низкому уровню входного сигнала, поэтому дополнительных настроек не потребуется. Остаётся объявить за пределами функции main обработчик INT0, отправляющий в UART символ *:

Void main (void)
{
PORTD|=(1<<2); // Притянуть вход кнопки PORTD.2 внутренним pull-up.
UBRRL=51; // Скорость UART – 9600 bps.
UCSRB = (1< SREG|=(1<<7); // Разрешить прерывания.
GICR|=(1< while (1){}
}

#pragma vector=INT0_vect // Обработчик прерывания INT0/
__interrupt void INT0_INTPT()
{
if (! (PIND & (1<<2))) {while(! (UCSRA & (1< }

Здесь обработчик прерывания объявлен в формате компилятора IAR. Принципиально в нём только имя вектора прерывания - INT0_vect, компилятор заменяет его на адрес памяти программ, на который передаётся выполнение программы при возникновении данного прерывания. Имя самого обработчика INT0_INTPT выбирается произвольно. Названия векторов всех возможных прерываний для данного МК описаны в h-файлах.

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

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

Взято с сайта
http://eugenemcu.ru/

Микроконтроллеры для котят

Всем Мяу, котаны:)

Как-то раз от меня ушла кошка:(Ну и чем валерьянку лопать, я решил заняться делом, так сказать «на благо Родине». Давно уж хотел цифровыми устройствами заняться, да времени не было (сами понимаете, то спать, то с кошкой по крышам гулять), а тут как раз время-то и появилось. Ну-с начнём..)

Всё как обычно начинается с выбора. Ну вроде выбор-то небольшой PIC, да AVR. Последние мне как-то больше приглянулись. Нужен был ещё и USB программатор за неимением других портов на компьютере, от цены которого у меня чуть хвост не отвалился. Ещё Arduino есть - зверёк такой. Его и программировать по USB можно. Ну, думаю, "то что доктор прописал". В селе нашем его только через интернет-магазин достать можно. Нашёл, где по-выгодней, чуть не купил и... ОПА! Смотрю - STM32VL-Discovery. Что за зверь такой? Хм, STM32.. Что-то слышал краем уха.. А от характеристик усы дыбом, честно!

А лап-то у неё сколько!

Итак, попорядку:

  • У Arduino 14 цифровых портов ввода/вывода и 6 аналоговых входов. У STM32VL-Discovery 45 цифровых входа/выхода 10 из которых по желанию превращаются в аналоговые входы.
  • У Arduino 32 Кб для хранения программы и 2 Кб ОЗУ. У STM32VL-Discovery 64 Кб для хранения программ и 8 Кб ОЗУ.
  • У Arduino тактовая частота 16 МГц у STM32VL-Discovery же 24 МГц.
  • Любой микроконтроллер STM32 можно заменить другим STM32, но с лучшими характеристиками, без изменения схемы
  • STM32 можно программировать без программатора по COM порту (об этом чуть позже)
  • Цена Arduino на момент написания статьи ~1300 рублей, STM32VL-Discovery ~600 рублей. Это ж дешевле более чем в 2 раза!

А что дальше? В STM32VL-Discovery есть встроенный программатор/отладчик, который лёгким движением лапы (снятием перемычек) может программировать и отлаживать (отладка очень уж вещь полезная, но об этом чуть позже) микроконтроллеры STM32 за пределами платы. С Arduino такое не прокатит. То есть используя STM32VL-Discovery мы и деньги экономим и получаем большую производительность и свободу творчества:)

Да и сами микроконтроллеры STM32 выглядят привлекательней остальных:

STM32F100C4T6B ATtiny24A-SSU PIC16F688-I/SL STM32F103RET6 ATmega1284P-PU PIC18F4550-I/PT
Средняя цена, руб 60 65 60 240 330 220
Тактовая частота, МГц 24 20 20 72 20 48
Flash память, Кбайт 16 2 4 512 128 16
RAM, Байт 4096 128 256 65536 16384 2048
USART, шт 2 0 0 5 2 0
SPI, шт 1 1 0 3 1 1
АЦП, шт 16x12Bit 8x10Bit 8x10Bit 16х12Bit 8x10Bit 13x10Bit
ЦАП, шт 1x12Bit 0 0 2х12Bit 0 0
Количество линий ввода/вывода, шт 37 12 12 51 32 35

А ещё STM32 32-х разрядные, а это означает возможность работы с 32-х битными данными за один такт. AVR и PIC этим не похвастаются.

Ну что, котаны, убедил? Тогда начнём курс молодого бойца цифровика!)

Как оно работает? Из чего состоит? Что умеет?

Как известно, все коты очень любознательные, а радиокоты особенно!

Микроконтроллер - это микросхема сочетающая в себе функции процессора, периферии, имеющая ОЗУ, flash память. Как компьютер, только меньше!

Проведём аналогию: компьютером управляет операционная система, а микроконтроллером «прошивка», которую пишете Вы; операционная система компьютера хранится на жёстком диске, «прошивка» микроконтроллера в его flash памяти; функции ОЗУ схожи - хранение изменяющихся данных во время выполнения программы. А ещё у МК есть различные периферийные устройства, такие как АЦП и ЦАП например.

МК общается с внешним миром при помощи лап на его корпусе (не таких как у котов, конечно, а металлических). Но не все из них управляются программой, есть выводы питания, вывод сброса, выводы питания периферии, вывод резервного питания. А те, которые управляются программой делятся на группы называемые «порты». Все эти управляемые выводы называются 2-мя буквами и цифрой. Например PA1: P - порт, А - порт «А», 1 - номер вывода этого порта.

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

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

  • Цифровой вход - вход, значение которого (логическая 1 или 0) можно считывать программой. Если напяжение на входе 0, то значение равно 0, если на входе напяжение равное напрядению питания, то значение входа 1. Третьего не дано. Можно сделать с подтягивающим резистором либо к питанию, либо к массе
  • Аналоговый вход - вход значение которого можно считывать программой, но значений может быть много - целых 4096. А точнее от 0 если на входе напяжение 0 относительно минуса питания микроконтроллера до 4095, если на входе напряжение равное напряжению питания. Все эти преобразования делает АЦП - аналогово-цифровой преобразователь, при помощи его можно например измерять напряжение на терморезисторе и узнавать температуру или измерять напяжение на фоторезисторе и узнавать яркость попадающего на него света... Ну много чего можно придумать, если фантазия есть:) Если питать микроконтроллер от 3В, то 0В = 0, а 3В = 4096, значит 3/4096=0.000732421, т.е. при изменении напяжения на входе на 0.000732421В значение входа в программе меняется на 1. Не так-то всё и сложно, да? Идём дальше
  • Цифровой вход в режиме альтернативной функции - вход для работы с периферией. Например вход для таймера или вход для какого-нибудь интерфейса. Из программы значение этого входа считать нельзя. В программе можно например считать данные полученные по этому выводу каким-нибудь интерфейсом.

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

  • Выход. Просто выход. Обычный цифровой выход. На выводе либо напрядение питания (логическая 1) либо на выводе нет напряжения (логическая 0). Всё просто.
  • Выход в режиме альтернативной функции - выход управляемый периферией. Этим выходом нельзя управлять из программы, но программой можно заставить управлять этим выводом например интерфейс.

Но не все выводы можно назначать «как захочется». Для того, что бы узнать, что можно, а что нельзя нужно посмотреть документацию (таблица 4) или воспользоваться программой MicroXplorer.

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

Ещё есть выводы BOOT 0 и BOOT 1 . Эти выводы не относятся к портам, они служат для управления загрузкой микроконтроллера. Если во время подачи питания на выводе BOOT 0 логический ноль (вывод соединен с общей точкой), то микроконтроллер выполняет программу загруженную во flash память, т.е. Вашу прошивку. Если во время подачи питания на выводе BOOT 0 логическая еденица (вывод соединен с питанием микроконтроллера), а на выводе BOOT 1 логический ноль, то микроконтроллер выполняет не Вашу прошивку, а записанный на заводе загрузчик. Запомните это! Вы будете часто пользоваться этим в процессе работы с микроконтроллерами STM32! Иногда загрузка записанного с завода загрузчика - единственный способ записать/изменить прошивку микроконтроллера. Это бывает например при конфигурировании в прошивке выводов, к которым подключается программатор или при прошивке микроконтроллера без использования программатора. Так что настоятельно рекомендую при проектировании печатной платы эти выводы (или хотя бы BOOT 0) распологать в удобном месте.

Вот разобрались:) Теперь знаем что такое микроконтроллер, из чего он состоит. Сейчас узнаем ещё о некоторых премудростях и перейдём к самому интересному - практике!

Программа в микроконтроллере выполняется пошагово. Один такт процессора - один шаг программы.

Например пусть перемигивается красная и зелёная лампочки, пока НЕ нажата кнопка. Длительность каждой лампы - 5 секунд. Вот алгоритм:

  1. Проверяем, на входе с кнопкой есть напряжение? (кнопка замыкает вывод микроконтроллера на + питания)
  2. Если нет напряжения, то загорается красная лампочка на 5 секунд, зелёная тухнет, если есть напряжение, то начинаем всё сначала
  3. Снова проверяем
  4. Если нет напряжение, то загорается зелёная лампочка на 5 секунд, красная тухнет, если есть напряжение, то начинаем всё сначала
  5. Начинаем сначала

СТОП! А если я нажму кнопку, пока горит лампочка? То ничего не произойдёт! Потому что программа выполняется пошагово, а шаг с проверкой нажатия кнопки находится в момент переключения лампочек.
Вот именно для таких случаев есть такая вещь, как прерывания

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

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

Встаём на лапы!

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

Отлично на первых порах иметь отладочную плату, например STM32VL-Discovery, но если жаба душит или всё-таки нехватает на колбасу, то можно обойтись и одним микроконтроллером и преобразователем интерфейсов RS-232 ->UART (напр. MAX3232) или USB ->UART (напр. FT232RL). В этом случае в 100 рублей можно вполне уложиться, но придётся делать печатную плату и паять минимум 48 выводов шириной 0,3 мм с зазором 0,2 мм. Я предупреждал.

Сначала нужно естественно прикошачить отладочную плату или контроллер к компьютеру.

Если у Вас отладочная плата:

С отладочной платой, конечно проще. Берём шнурок Mini-USB и соединяем плату с компьютером, все драйверы должны поставиться автоматически. Увидеть STMicroelectronics STLink dongle в диспетчере устройств - хороший знак! Ну а если что-то пошло не так и ничего не вышло - не надо царапать диван, нужно просто зайти сюда и установить STM32 ST-LINK utility .

Ну а если Вы счастливый обладатель компьютера под управлением Windows 8, то перед проведением вышеописанных действий нужно сделать так: Параметры -> Изменение параметров компьютера -> Общие -> Особые варианты загрузки и выбрать параметр Отключение проверки подписи драйверов .

Если у Вас микроконтроллер:

Если у Вас один микроконтроллер, то у Вас должны быть прямые лапы. Но я в Вас не сомневаюсь!

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

Рабочий минимум на схеме ниже:

Но это неинтересный минимум.

Добавьте светодиодов и кнопок (не забудьте про выводы BOOT), например так

А вот с пайкой этой блохи могут возникнуть проблемы. Но я надеюсь, не возникнут. Я накошачился паять её своим любимым советским 25 Вт паяльником с шириной жала в 3/4 ширины контроллера. У меня больше проблем с изготовлением печатной платы... ну тут уж у каждого своя технология.

И переходник нужно сделать на UART по документации к той микросхеме, которую купили.

Соединяем выводы TxD и RxD на печатной плате с выводами RxD и TxD соответственно переходника. Не забываем про общую точку и питание всего этого.

Выбор и установка ПО

Пользоваться мы будем средой разработки CooCox IDE , но это не просто так, а по нескольким причинам:

  • Во-первых это свободно распространяемое ПО. А это значит, что Ваша карма будет чиста
  • На мой взгляд (да и не только на мой) эта среда разработки удобнее остаальных
  • Позволяет использовать отладку
  • Много примеров, которые можно загружать в среду разработки (полезно для котят и не только)

Среда разработки - это программа для написания кода, компилятор, отладчик в одном. Удобненько:) Но если какому-то суровому Челябинскому коту удобнее писать код (в блокноте например), компилировать и прошивать разными программами - я не против, тогда Вам пригодится STM32 ST-LINK utilit для загрузки прошивки в микроконтроллер. Хозяин барин, как говорится.

Эта среда разработки основана на многим известном Eclipse.

  1. Идём сюда
  2. Тыкаем Download through CoCenter (Recommend)
  3. Вводим адрес эл.почты (можно от балды, он там «для галочки»)
  4. После загрузки устанавливаем этот самый CoCenter
  5. В первой строчке, где написано CooCox CoIDE тыкаем Download
  6. После того, как загрузка закончится, то вместо Download будет Install . Сюда и жмём
  7. Идём сюда
  8. Справа в колонке Download скачиваем файл который .exe. Устанавливаем его.
  9. Открываем сам CooCox CoIDE , вкладка Project , Select Toolchain Path .
  10. Указываем путь к файлу arm-none-eabi-gcc.exe (это мы установили в п.8, путь приблизительно такой: D:Program Files (x86)GNU Tools ARM Embedded4.7 2013q1bin)
  11. Снова открываем CoIDE , нажимаем View -> Configuration , открываем вкладку Debugger и делаем так [фото]
  12. Радуемся, потому что теперь мы можем написать программу и прошить её в микроконтроллер! Чем мы и займёмся.

Если у Вас вариант без отладочной платы/программатора, то для загрузки программы в МК понадобится программка Flash Loader Demonstrator которая находится

Находим общий язык

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

Проект состоит из файлов с расширениями .c и .h . В первых находятся функции во вторых названия используемых функций и константы например. Так уж заведено. Самый главный файл, в котором находится код программы main.c . Для использования различных функций нужно подключать библиотеки с этими функциями. Подключаются они записью #include "название_библиотеки" ну библиотеки естественно должны быть в проекте. Подключают их в самом начале файла.

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

тип_возвращаемой_переменной имя_функции (тип_переменной)
{
Тело функции
}

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

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

тип_возвращаемой_переменной имя_функции (тип_переменной);

Ах, да, забыл самое главное! В конце каждой строки должна быть точка с запятой!

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

При запуске, первой всегда выполняется функция main() .

Ну с функциями вроде разобрались, понимание придёт только с практикой.

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

  • INT - переменная этого типа может быть только целым числом от -2147483648 до 2147483647
  • FLOAT - переменная этого типа число с точностью до 7 разрядов от ±1,5*10-45 до ±3,4*1033
  • DOUBLE - число с точностью до 16 разрядов от ±5*10-324 до ±1,7*10306
  • ULONG - тоже целое число, но от 0 до 18446744073709551615
  • LONG - целое от -9223372036854775808 до 9223372036854775807
  • CHAR - один символ
  • BOOL - логическая переменная. Она может иметь только 2 значения: истина (true) или ложь (false)

Строку (слово, предложение) можно представить как массив из символов типа char. Например:

char stroka = "Слово";

Здесь квадратных скобках - количество символов в строке, «stroka» - название массива.

Перед использованием переменной её нужно обязательно объявить. (просто указать тип переменной и имя)

  • + - сложение.
  • - - вычитание.
  • * - умножение.
  • / - деление.
  • = - присвоение переменной значения.

Например выражение a=b+c значит присвоить переменной a значение суммы значений переменных b и c .

  • ++ - инкремент. Увеличение значения переменной на 1
  • -- - декремент. Уменьшение значения переменной на 1

Например выражение a++ значит увеличить значение переменной a на 1 (то же самое, что и a=a+1 )

  • == - сравнение, знак «равно». (НЕ ПУТАТЬ С ПРИСВОЕНИЕМ)
  • != - сравнение, знак «не равно».
  • < - сравнение, знак «меньше».
  • <= - сравнение, знак «меньше или равно».
  • > - сравнение, знак «больше».
  • >= - сравнение, знак «больше или равно».

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

  • % - остаток от деления

Например если a=5 , b=3 , то значение выражения a%b будет равно 2 (т.к. 5/3=1 (ост.2))

  • << - побитовый сдвиг влево. Не вдаваясь в подробности значение выражения a< на языке Си будет равно выражению a*2 b
  • >> - побитовый сдвиг вправо. Выражение a>>b в программе равносильно выражению a/2 b
  • & - логическое И .
  • | - логическое ИЛИ .
  • ~ - инвертирование.

Чуть не забыл рассказать про циклы. Основные:

while(условие) {

тело цикла

Тело цикла (всё что в фигурных скобках) выполняется, когда условие истинно (пока условие не станет ложным).

for (начальное_значение; цикл_выполняется_до, шаг) {

тело цикла

Начальное_значение - начальное значение счётчика

Цикл_выполняется_до - до достижения какого значения выполняется цикл

Шаг - с каким шагом счетчик считает

Например

for (i=0; i<10, i++) {

тело цикла

Здесь начальное значение переменной i равно 0, цикл выполняется, пока значение переменной i меньше 10, при каждом выполнении цикла к переменной i прибавляется 1. Так же можно изменять значение переменной прямо в цикле.

if (условие){

тело 1

} else {

тело 2

В усовном переходе «тело 1» выполняется, если условие истинно и выполняется «тело 2», если условие ложно. Ещё есть такой вариант:

if (условие 1){

} else if (условие 2) {

В этом случае «тело 1» выполняется, если истинно «условие 1», «тело 2» выполняется, если истинно «условие 2». Таких условий может быть сколько угодно, так же может быть одно else.

Условия могут быть простыми и составными: простые - одно логическое выражение, а составное - несколько логических выражений соединённых знаком & (условия истинно, когда все условия соединённые этим знаком истинны) или | (условие истинно, если хотябы одно условие соединённое этим знаком истинно).

Ещё полезная вещь - комментарии. Помогут разобраться в забытом проекте:) или просто что бы что-то не забыть. Комментировать можно или после знаков // и до конца строки или начинаются знаками /* и заканчиваются */ , в таком случае комментарий может быть любое количество строк. На размер программы комментарии не влияют.

Ну вот, из основного вроде всё. На первое время хватит (до написания следующей части статьи)

Первая программа

Не будем отступать от традиций (а то мало ли) и начнём с Hello World. А по пути будем продолжать знакомиться с микроконтроллером и так сказать получать опыт.

Открываем среду разработки:

Нажимаем Browse in Repository

Выбираем ST

Потом мы увидим список подключаемых библиотек.

Для нашей простенькой программы нам понадобится: CMSIS core , CMSIS Boot , RCC , GPIO .

Библиотеки CMSIS core и CMSIS Boot - системные, их нужно подключать обязательно

Библиотека RCC для работы с системой тактирования

Библиотека GPIO для работы с портами ввода-вывода

Теперь слева в окне Project открываем файл main.c .

Сначала нужно подключить наши библиотеки (CMSIS подключать не нужно).

Идём в самое начало программы и добавляем строчки:

#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"

void Delay(int i) {
for (; i != 0; i--);
}

Так. Тут по порядку, функция ничего не возвращает, по этому void , название функции Delay , сразу объявляем переменную i типа int . В фигурных скобках тело функции - цикл for . Это его строчная запись. Начальное значение i мы не изменяем, цикл выполняется, пока i не равна нулю (как i становится равна нулю, цикл прекращается, функция «выключаеся»). С каждым выполнением тела цикла (тактом) переменная i уменьшается на 1. Т.е. суть цикла - просто повториться количество раз равное i . Пока выполняется цикл время идёт, происходит задержка.

Какой порт ответственный за какой вывод можно посмотреть в документации к МК:

Для тактирования порта С добавляем строчку:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC , ENABLE);

Добавляем в прогармму строчку:

GPIO_InitTypeDef GPIO_Init1;

Этой строчкой мы объявили структуру GPIO_InitTypeDef - дали ей название GPIO_Init для использования в нашей программе далее.

Какие в этой структуре можно настроить параметры и какой вид они имеют, смотрим всё в том же stm32f10x_gpio.h :

Теперь чтобы настроить параметры выводов при помощи структуры нужно написать её название, поставить точку и появится окошечко в котором эти параметры указаны

Дважды щёлкаем по одному из них, и он появляется в строке, далее ставим = (присвоить) и прописываем значение из stm32f10x_gpio.h

Так же поступаем со всеми параметрами. Не забываем точку с запятой в конце каждой строки!

GPIO_Init(GPIOC , &GPIO_Init);

Теперь будем мигать! Мигать мы будем циклично, сделаем зацикливание в цикле while. Условие цикла будет 1. Еденица - всегда истина, нуль - всегда ложь.. такова се ля ви..

Чтобы подать ток на вывод нужно установить бит, чтобы выключить вывод нужно сбросить бит. Как это делать - всё в том же stm32f10x_gpio.h :

Делаем так:

while (1){

GPIO_SetBits(GPIOC, GPIO_Pin_9);

Delay (200000);

GPIO_ResetBits(GPIOC, GPIO_Pin_9);

Delay (200000);

1 всегда истина, значит цикл будет зацикливание.

GPIO_SetBits - функция установки бита

GPIO_ResetBits - функция сброса бита

Delay (200000) - на этой строчке выполнение программы переходит в функцию Delay , в ту самую, в которой цикл for . Число 200000 в скобках - передаётся в эту функцию, как переменная i . (помним строчку void Delay(int i) ?) и выполняется тот самый цикл в этой функции, все 200000 раз. Это быстро:) после окончания работы цикла for функция D elay заканчивает свою работу, т.к. она void , то она ничего не возвращает и программа продолжает выполняется дальше.

Т.к. while зациклен, то включение светодиода, задержка, выключение светодиода, задержка будут выполняться бесконечно циклично. Пока не выключится питание или не произойдёт прерывание (об этом в следующей статье).

Ну вот, первая программа готова. Теперь нажимаем F7, программа компилируется.

Теперь если у Вас отладочная плата, то подключаем её при помощи USB шнурка и нажимаем Download Code To Flash . Радуемся выполненной работе и полученным знаниям:)

А если у Вас не отладочная плата, то подключите к своей плате переходник сделаный ранее, а переходник к COM-порту компьютера. Далее соедините вывод BOOT 0 c плюсом питания микроконтроллера и включите питание микроконтроллера. Тем самым микроконтроллер войдет в режим прошивки. Вообще процедура прошивки не сложная. Нужно просто следовать указаниям приложения Flash Loader Demonstrator . Сначала указываем номер COM-порта, через который у Вас подключен микроконтроллер и скорость. Для воизбежании сбоев, скорость лучше выбрать поменьше

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

После нажатия «Next», Вы увидите страницу с адресацией памяти. Она нам не понадобится.

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

Для прошивки выбираем Download to device и в поле Download from file выбираем компилированный.hex файл, который находится в папке CooCox -> CooIDE -> workspace -> имя_проекта -> имя_проекта -> Debug -> Bin . После снова нажимаем «Next».

После того, как увидим такое окно:

Отключаем питание микроконтроллера, закрываем Flash Loader Demonstrator , отключаем переходник, и включаем микроконтроллер в обычном режиме (когда при включении вывод BOOT 0 соединен с минусом питания микроконтроллера). Радуемся!

Итак, теперь мы знаем, чем микроконтроллеры STM лучше других, знаем как работает микроконтроллер, умеем прошивать микроконтроллер в отладочной плате и в своей плате, знаем основы языка Си, которые нужны для программирования STM32, получили опыт работы с микроконтроллером (надеюсь положительный) и самое главное, теперь Вы можете воплотить свои идеи цифровых устройств в жизнь (и поведать о них, на нашем любимом РадиоКоте)! Пусть пока ещё простенькие, но всё навёрстывается с опытом. А я постараюсь в следующих статьях рассказать об АЦП, ЦАП, прерываниях, использовании отладки и других полезностях.

Как вам эта статья?

Все картинки в этой статье кликабельны.

Микроконтроллеры содержат микропроцессорное ядро ARM , точнее ARM Cortex-M. Это ядро присуще не только микроконтроллерам STM32, оно существует само по себе, и на его основе выпускается множество микроконтроллеров от разных производителей.

Тогда находим этот микроконтроллер в списке слева и устанавливаем соответствующий пакет DFP:

Можно заметить, что среди установленных пакетов есть CMSIS. CMSIS - это библиотека для ядра Cortex-M, общая для всех микроконтроллеров. Библиотека разрабатывается фирмой ARM и доступна для скачивания с официального сайта после регистрации. Можно было бы не устанавливать этот пакет, а пользоваться официальным выпуском библиотеки, но это дополнительные сложности.

Закрываем менеджер пакетов и запускаем Keil uVision5 (произносится мю-вижен):

Keil uVision5 - это часть MDK-ARM, графический интерфейс среды, который включает редактор кода:

  1. Кодировка UTF-8.
  2. Правая граница кода в 80 символов.
  3. Отступы по 4 пробела.

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

Теперь создаем проект. Для этого выбираем меню «Project -> New uVision Project…». В открывшемся окне выбираем расположение и имя проекта. Для проекта лучше создать отдельную папку и сохранить проект туда.

После сохранения появится окно выбора устройства. Выбираем нужный микроконтроллер и нажимаем «ОК». Если бы мы не установили нужный пакет, то микроконтроллера не было бы в списке:

В следующем окне предстоит выбрать компоненты, которые будут использоваться в проекте. Необходимо выбрать «CMSIS:CORE» и «Device:Startup»:

После нажатия «OK» процесс создания проекта завершится.

В дальнейшем вы всегда сможете запустить окно выбора компонентов, чтобы добавить или удалить их. Для этого надо выбрать меню «Project -> Manage -> Run-Time Evironment…».

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

После создания проекта описанным способом, в окне справа вы увидите следующую структуру проекта:

Здесь мы видим название проекта «example», цель проекта «Target 1», пустую группу файлов «Source Group 1», компоненты CMSIS и Device.

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

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

В структуре мы видим два файла. Один с расширением «s». Он содержит исходный код на языке ассемблера. Другой с расширением «с». Он содержит исходный код на языке Си.

Собрать проект и получить файл прошивки можно нажав клавишу F7. Но в таком виде проект не будет собран и вы получите ошибку, потому что отсутствует функция «main()».

Функция «main()» - это точка входа в вашу программу, то с чего начинается программа. Ее наличие обязательно если вы пишите программу на языке Си.

Давайте создадим эту функцию. Кликнем на группе «Source Group 1» правой кнопкой и выберем «Add New Item to ‘Source Group 1’…» (перевод: добавить новый элемент в группу ‘Source Group 1’). Создадим файл «main.c»:

В созданный файл добавим код:

Int main() { return 0; }

В конец файла стоит добавить пустую строку, иначе при сборке вы получите предупреждение «warning: #1-D: last line of file ends without a newline».

Теперь проект можно собрать клавишей F7. В результате вы получите файл «Objects\example.axf» (по умолчанию имя файла совпадает с именем проекта). Файл располагается в папке с проектом.

Обычно разработчику требуется файл прошивки в формате Intel HEX . Чтобы получить его, надо произвести настройку цели. Чтобы увидеть настройки цели нажмите Alt-F7, перейдите на вкладку «Output» и выберите «Create HEX File».

После очередной сборки вы получите файл «Objects\example.hex».

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

Запустим выбор компонентов с помощью меню «Project -> Manage -> Run-Time Evironment…» и выберем компонент «Device:STM32Cube Hal:GPIO».

В нижней части окна мы увидим неудовлетворенную зависимость «Device:STM32Cube Hal:Common». Выберем этот компонент и увидим еще больший список зависимостей. Необходимо выбрать все требуемые зависимости:

  • Device:STM32Cube Hal:Common
  • Device:STM32Cube Hal:RCC
  • Device:STM32Cube Hal:PWR
  • Device:STM32Cube Hal:Cortex
  • Device:STM32Cube Framework:Classic

STM32Cube - это библиотека, которую предоставляет STMicroelectronics.

При выборе компонентов мы выбираем какие возможности этой библиотеки использовать.

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

Библиотека STM32Cube многоуровневая, то есть включает в себя множество промежуточных библиотек. Одна из промежуточных библиотек называется STM32Cube HAL, или просто HAL. Она поделена на модули и каждый модуль соответствует какому-нибудь периферийному устройству. Название модуля совпадает с названием устройства, например, имеется модуль GPIO.

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

Для начала подключим HAL в нашей программе, добавив строчку перед определением функции «main()»:

#include "stm32f4xx_hal.h"

В самом начале функции «main()» вызовем функцию «HAL_Init()», которая инициализирует библиотеку.

Таким образом мы получим следующий код в файле «main.c»:

#include "stm32f4xx_hal.h" int main() { HAL_Init(); return 0; }

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

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

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

#include "stm32f4xx_hal.h" int main() { HAL_Init(); // Разрешить тактирование порта A. __HAL_RCC_GPIOA_CLK_ENABLE(); // Настройки порта. GPIO_InitTypeDef s; s.Pin = GPIO_PIN_0; // Вывод 0. s.Mode = GPIO_MODE_OUTPUT_PP; // Цифровой выход. s.Pull = GPIO_NOPULL; // Без подтяжки. s.Speed = GPIO_SPEED_FREQ_VERY_HIGH; // Максимальная скорость. // Настроить вывод 0 порт A. HAL_GPIO_Init(GPIOA, &s); // Бесконечно переключать состояние порта с максимальной скоростью. while(1) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); } //return 0; } void SysTick_Handler(void) { HAL_IncTick(); }

Ссылки

  1. Скринкаст «Eclipse и GNU Tools для разработки под ARM-микроконтроллеры «.
  2. Микроконтроллер STM32F407VG .
  3. Отладочная плата STM32F4-Discovery .
  4. Библиотека STM32CubeF4 .

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

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

В этой статье, путь от фразы «Да, я хочу попробовать!» до радостного подмигивания светодиода, будет значительно длиннее чем у других авторов. Я постараюсь раскрыть аспекты программирования микроконтроллеров, которые прячутся за использованием библиотечных функций и готовых примеров.
Если вы намерены серьезно изучать программирование микроконтроллеров данная статья для вас. Возможно, она может заинтересовать и тех, кто вдоволь наигрался с Arduino и хочет получить в свои руки все аппаратные возможности железа.

Выбор микроконтроллера

Многие могут сказать, что начинать изучение микроконтроллеров лучше с AVR, PIC, 8051 или чего-то еще. Вопрос многогранный и спорный. Я знаю достаточно примеров, когда люди изучив Cortex-M, программировали AVR, ARM7 и т.д. Сам же я начинал с Cortex-M3. Если перед вами стоит определенная задача, в интернете достаточно много информации со сравнением различных типов микроконтроллеров и решаемых с их помощью задач. На хабре этот вопрос тоже поднимался, например .

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

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

Что необходимо для изучения?

К сожалению, для начала программирования МК не достаточно одного лишь ПК. Придется где-то раздобыть демонстрационную плату и программатор. Хотя это и уменьшает конкуренцию на рынке труда.

Сам я использую демонстрационную плату STM3220G-EVAL и программатор J-Link PRO . Но для начала, будет вполне достаточно STM32F4DISCOVERY , которую можно купить без особых проблем за небольшую сумму.

Все примеры будут именно для отладочной платы STM32F4DISCOVERY . На данном этапе нам будет совершенно не важно, что этой плате стоит МК на базе ядра Cortex-M4. В ближайшее время мы не будем использовать его особенности и преимущества над Cortex-M3. А как там будет дальше - посмотрим.

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

Среда разработки

Как уже неоднократно упоминалось, для ARM микроконтроллеров существует достаточное количество сред разработки, как платных так и не очень. И снова хочется опустить полемику по этому поводу. Я использую IAR Embedded Workbench for ARM 6.60 . Все примеры будут именно в этой среде. Если вам по душе (или в вашей организации используется) что-то другое (Keil, Eclipse, CCS, CooCoc и т.д.) то это вам тоже не очень помешает. На особенности, связанные именно со средой разработки, я буду обращать отдельное внимание.

Почему платная среда разработки?

Возможно, кто-то будет не совсем доволен тем, что я предлагаю использовать платную среду разработки, но в IAR есть возможность получить временную лицензию без ограничения функционала, либо безлимитную лицензию с ограничением по размеру кода (32КБ для МК это очень много).
Помимо этого, сразу замечу, что для некоторых МК не существует бесплатных сред разработки. И к сожалению эти МК в некоторых областях незаменимы.


Процесс установки я описывать не буду.

С чего начать?

Создание проекта
Для начала создадим пустой проект. IAR позволяет создать проекты на ASM, C и C++. Мы будем использовать C.

Перед нами появится пустой проект с main файлом.

Теперь необходимо настроить проект для начала работы с «нашим» МК и отладчиком. На плате STM32F4DISCOVERY установлен MK STM32F407VG . Его необходимо выбрать в свойствах проекта (General Options->Target->Device):

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

После этого необходимо настроить отладчик. Отладка программы происходит непосредственно «в железе». Производится это с помощью JTAG отладчика. Более подробнее ознакомиться с тем, как это происходит можно на Википедии . На плату STM32F4DISCOVERY интегрирован отладчик ST-LINK/V2. Для работы с отладчиком необходимо выбрать его драйвер в меню Debugger->Setup->Driver . Так же необходимо указать, что отладка должна производиться непосредственно в железе. Для этого необходимо поставить флаг Debugger->Download->Use flash loader(s)


Для тех, кто увидел слово Simulator

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

Теперь проект готов для работы (программирования, заливки и отладки).

«ТЗ» для первого проекта
Подведем промежуточный итог: МК и отладочная плата выбраны, проект подготовлен. Пора определиться с задачей.

Не будем отходить от классики. Первым проектом будет мигающий светодиод. Благо на плате их предостаточно.Что же это означает с точки зрения программирования? Первым делом необходимо изучить принципиальную схему демонстрационной платы и понять как «заводится» светодиод.
доступен на сайте производителя. В данном описании даже есть отдельный раздел про светодиоды на плате -4.4 LEDs . Для примера, будем использовать User LD3 . Найдем его на схеме:

Простейший анализ схемы говорит о том, что для того, что бы «зажечь» светодиод необходимо на пин МК подать «1» (которая для данного МК соответствует 3.3В). Выключение производится подачей на этот пин «0». На схеме этот пин обозначается PD13 (это, наверное, самая важная информация из этого документа).

В итоге, мы можем написать «ТЗ» для нашей первой программы:
Программа для МК должна переводить состояние пина МК PD13 из состояния «0» в состояние «1» и обратно с некоторой периодичностью, различимой для человеческого глаза (важное замечание, если моргать светодиодом слишком часто глаз может этого не различить).

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

Начнем с того, что любой МК включает ядро, память и периферийные блоки. Думаю, что с памятью пока все понятно. Упомяну лишь, в STM32 есть флеш память в которой хранится программа МК (в общем случае это не верное утверждение, программа может храниться во внешней энергонезависимой памяти, но пока это опустим) и другие данные, в том числе и пользовательские. Так же есть SRAM - оперативная память.

Ядро - часть микроконтроллера, осуществляющая выполнение одного потока команд. В нашем МК тип ядра - Cortex-M4. Ядро МК можно сравнить с процессором в ПК. Оно умеет только выполнять команды и передавать данные другим блокам (в этом сравнении не учитываются процессоры с интегрированными графическими ускорителями).
При этом производитель МК не разрабатывает ядро. Ядро покупается у компании ARM Limited . Главное отличие между различными МК - в периферии.

Периферийные блоки - блоки осуществляющие взаимодействие с «внешним миром» или выполняющие специфические функции, недоступные ядру МК. Современные МК (в том числе и STM32) содержат огромный спектр периферийных блоков. Периферийные блоки предназначены для решения различных задач, от считывания значения напряжения с аналогового входа МК до передачи данных внешним устройствам по шине SPI.
В отличии от ядра МК периферийные блоки не выполняют инструкции. Они лишь выполняют команды ядра. При этом участие ядра при выполнении команды не требуется.

Пример

В качестве примера можно привести блок UART, который предназначен для приема и передачи данных от МК внешним устройствам. От ядра необходимо лишь сконфигурировать блок и отдать ему данные для передачи. После этого ядро может дальше выполнять инструкции. На плечи же периферийного блока ложится управление соответствующим выводом МК для передачи данных в соответствии с протоколом. Периферийный блок сам переводит выход МК в необходимое состояние «0» или «1» в нужный момент времени, осуществляя передачу.

Взаимодействие ядра с периферийным блоком
Взаимодействие ядра МК с периферийным блоком осуществляется с помощью спецрегистров (есть еще взаимодействие через механизм прерываний и DMA, но об этом в следующих постах). С точки зрения ядра это просто участок памяти с определенным адресом, вот только на самом деле это не так . Запись данных в спецрегистр эквивалентна передаче команды или данных периферийному блоку. Считывание - получение данных от блока или считывание его состояния. Описание периферийных блоков и их спецрегистров занимает львиную долю описания МК.

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

Спецрегистры обычно разделены на битовые поля. Один (или несколько) бит управляют определенным параметром периферийного блока, обычно независимо. Например, разные биты одного регистра управляют состоянием разных выходов МК.

Вспоминаем С
Если вы гуру в языке C, то можете смело пропускать данный раздел. Он предназначен в первую очередь для тех, кого учили (или ктоучился сам) программировать для ПК. Опыт показывает, что люди часто не помнят важных команд. Здесь я вкратце напомню про побитовые операции и работу напрямую с памятью по ее адресу.

Запись данных по адресу в памяти

Предположим, что читая описание периферийного блока, мы поняли, что для его корректной работы необходимо записать в него число 0x3B. Адрес спецрегистра 0x60004012. Регистр 32-битный.
Если вы сразу не знаете как это сделать, попробую описать цепочку рассуждений для получения правильной команды.

Значение 0x60004012 есть не что иное, как значение указателя на ячейку памяти. Нужно именно это и указать в нашей программе, тоесть сделать преобразование типов согласно синтаксису языка C:

(unsigned long*)(0x60004012)

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

*(unsigned long*)(0x60004012) = 0x3B;

Установка произвольных бит в 1

Предположим, что необходимо установить «1» в 7 и 1 биты по адресу 0x60004012, при этом не изменив значение всех остальных бит в регистре. Для этого необходимо использовать бинарную операцию |. Сразу приведу правильный ответ:

*(unsigned long*)(0x60004012) |= 0x82;

Обратите внимание на 2 факта. Биты считаются с нулевого, а не с первого. Данная операция на самом деле занимает неменее 3 тактов - считывание значения, модификация, запись. Иногда это не допустимо, поскольку между считыванием и записью значение одного из бит, которые нам запрещено изменять, могло быть изменено периферийным блоком. Незабывайте про эту особенность, иначе могут полезть баги, которые крайне сложно отловить.

Установка произвольных бит в 0

Предположим, что необходимо установить «0» в 7 и 1 биты по адресу 0x60004012, при этом не изменив значение всех остальных бит в регистре. Для этого необходимо использовать бинарную операцию &. Сразу приведу правильный ответ:

*(unsigned long*)(0x60004012) &= 0xFFFFFF7D;

Или его более простою запись (не переживайте за лишнюю операцию, компилятор все заранее посчитает даже при минимальной оптимизации):

*(unsigned long*)(0x60004012) &= (~0x82);

Некоторые особенности программ для МК
Здесь я постараюсь описать некоторые особенности программ для МК, которые важно помнить. Вещи достаточно очевидные, но все же.
У программы нет конца
В отличии от большинства программ для ПК, программа для МК не должна заканчиваться, НИКОГДА! А что собственно должен будет делать МК после завершения вашей программы? Вопрос, практически, риторический. Поэтому не забываем убедиться в том, что вы не забыли вечный цикл. При желании, можно перевести МК в режим сна.
Пользуйтесь целочисленными переменными
Не смотря на то, что мы используем МК с ядром Cortex-M4, который аппаратно выполняет операции над числами с плавающей точкой, советую вам отказаться от их использования. В МК без поддержки таких операций время вычислений будет просто огромным.
Откажитесь от динамического выделения памяти
Это только совет. Причина проста - памяти мало. Я не раз встречался с библиотеками, в которых были «медленные утечки» памяти. Было очень неприятно, когда после нескольких недель стабильной работы МК зависал с ошибкой. Лучше заранее продумать архитектуру своей программы так, чтобы не пришлось использовать динамическое выделение памяти.
Если же все-таки хочется использовать - внимательно изучите работу менеджера памяти или пишите свой.

Приступаем к работе!

Работа над программой для МК всегда начинается с чтения документации. Для нашего МК доступен на сайте производителя. Страниц много, но все читать пока не нужно. Как уже было сказано, большую часть документации составляет описание периферийных блоков и их регистров. Так же хочу обратить внимание на то, что этот Reference Manual написан не для одного МК, а для нескольких линеек. Это говорит о том, что код будет переносим при переходе на другие МК в этих линейках (если конечно не пытаться использовать периферийные блоки которых нет в используемом МК).

В первую очередь необходимо определиться с какими блоками предстоит работать. Для это достаточно изучит разделы Introduction и Main features .

Непосредственное управление состоянием пинов МК осуществляется с помощью блока GPIO. Как указано в документации в МК STM32 может быть до 11 независимых блоков GPIO. Различные периферийные блоки GPIO принято называть портами. Порты обозначаются буквам от A до K. Каждый порт может содержать до 16 пинов. Как мы отметили ранее, светодиод подключается к пину PD13. Это означает, что управление этим пином осуществляется периферийным блоком GPIO порт D. Номер пина 13.

Ни каких других периферийных блоков на это раз нам не понадобится.

Управление тактированием периферийных блоков
Для снижения электропотребления МК практически все периферийные блоки после включения МК отключены. Включение/выключение блока производится подачей/прекращением подачи тактового сигнала на его вход. Для корректной работы, необходимо сконфигурировать контроллер тактового сигнала МК, чтобы необходимому периферийному блоку поступал тактовый сигнал.
Важно: Периферийный блок не может начать работу сразу после включения тактового сигнала. Необходимо подождать несколько тактов пока он «запустится». Люди, использующие библиотеки для периферийных устройств, зачастую даже не знают об этой особенности.

За включение тактирования периферийных блоков отвечают регистры RCC XXX peripheral clock enable register .На месте XXX могут стоять шины AHB1, AHB2, AHB3, APB1 и APB2. После внимательного изучения описания соответствующих регистров, можно сделать вывод о том, тактирование периферийного блока GPIOD включается установкой «1» в третий бит регистра RCC AHB1 peripheral clock enable register (RCC_AHB1ENR) :

Теперь необходимо разобраться с тем, как узнать адрес самого регистра RCC_AHB1ENR .

Замечание: Описание системы тактирования МК STM32 достойно отдельной статьи. Если у читателей возникнет желание, я подробнее освещу этот раздел в одной из следующих статей.

Определение адресов спецрегистров
Определение адресов спецрегистров необходимо начинать с чтения раздела Memory map в Reference manual. Можно заметить, что каждому блоку выделен свой участок адресного пространства. Например, для блока RCC это участок 0x4002 3800 - 0x4002 3BFF:

Для получения адреса регистра, необходимо к начальному значению адресного пространства блока RCC прибавить Addr. offset нужного регистра. Addres offset указывается и в описании регистра (см. скриншот выше).

В итоге, мы определили адрес регистра RCC_AHB1ENR - 0x4002 3830.

Блок GPIO
Для общего ознакомления с блоком GPIO я настоятельно рекомендую полностью прочитать соответствующий раздел Reference Manual. Пока можно не особо обращать внимание на Alternate mode . Это оставим на потом.

Сейчас же наша задача научиться управлять состоянием пинов МК. Перейдем сразу к описанию регистров GPIO.

Режим работы
В первую очередь необходимо установить режим работы 13 пина порта D как General purpose output mode , что означает что блок GPIO будет управлять состоянием пина МК. Управление режимом работы пинов МК производитсяс помощью регистра GPIO port mode register (GPIOx_MODER) (x = A..I/J/K) :

Как видно из описания для совершения требуемой нам настройки необходимо записать значение 01b в 26-27 биты регистра GPIOx_MODER . Адрес регистра можно определить тем же методом, что описан выше.

Настройка параметров работы выходных пинов порта GPIO
Блок GPIO позволяет применить дополнительные настройки для выходных пинов порта. Данные настройки производятся в регистрах:
  • GPIO port output type register (GPIOx_OTYPER) - задается тип выхода push-pull или open-drain
  • GPIO port output speed register (GPIOx_OSPEEDR) - задается скорость работы выхода
Мы не будем менять данных параметров, поскольку нас вполне устраивают значения по умолчанию.
Установка значения на пине МК
Наконец-то мы подошли к моменту управления состоянием выхода МК. Для утановки выходного значения на определенном пине МК есть два метода.

Используем регистр GPIO port bit set/reset register (GPIOx_BSRR)

Запись «0» или «1» в биты 0-16 приводят к соответствующему изменению состояния пинов порта. Для того, чтобы установить определенное значение на выходе одного или нескольких пинов МК и не изменить состояния остальных, необходимо будет пользоваться операцией модификации отдельных бит. Такая операция выполняется не менее чем за 3 такта. Если же необходимо в часть битов записать 1, а в другие 0, то понадобится не менее 4 тактов. Данный метод предпочтительнее всего использовать для изменения состояния выхода на противоположное, если его изначальное состояние не известно.

GPIO port bit set/reset register (GPIOx_BSRR)

В отличии от предыдущего метода, запись 0 в любой из битов данного регистра не приведет ни к чему (да и вообще, все биты write-only!). Запись 1 в биты 0-15 приведет к установке «1» на соответствующем выходе МК. Запись 1 в биты 16-31 приведет к установке «0» на соответствующем выходе МК. Этот метод предпочтительнее предыдущего, если необходимо установить определенное значение на пине «МК», а не изменить его.

Зажигаем светодиод!
Найдя адреса всех необходимых регистров, можно написать программу, которая включает светодиод:
void main() { //Enable port D clocking *(unsigned long*)(0x40023830) |= 0x8; //little delay for GPIOD get ready volatile unsigned long i=0; i++; i++; i++; i=0; //Set PD13 as General purpose output *(unsigned long*)(0x40020C00) = (*(unsigned long*)(0x40020C00)& (~0x0C000000)) | (0x04000000); //Turn LED ON! *(unsigned long*)(0x40020C14) |= 0x2000; while(1); }
Можно компилировать (Project->Compile ) и заливать (Project->Download->Download active application ). Или запустить отладку (Project->Dpwnload and Debug ) и начать выполнение (F5).
Светодиод загорелся!
Мигаем светодиодом
Мигание светодиода есть ни что иное, как попеременное включение и выключение с задержкой между этими действиями. Самый простой способ - поместить включение и выключение в вечный цикл, а между ними вставить задержку.
void main() { //Enable port D clocking *(unsigned long*)(0x40023830) |= 0x8; //little delay for GPIOD get ready volatile unsigned long i=0; i++; i++; i++; i=0; //Set PD13 as General purpose output *(unsigned long*)(0x40020C00) = (*(unsigned long*)(0x40020C00)& (~0x0C000000)) | (0x04000000); while(1) { //Turn LED ON *(unsigned long*)(0x40020C14) |= 0x2000; //Delay for(i=0; i<1000000 ;++i); //Turn LED OFF *(unsigned long*)(0x40020C14) &= ~0x2000; //Delay for(i=0; i<1000000 ;++i); } }
Значение 1000000 в задержке подобрано экспериментально так, чтобы период мигания светодиода был различим глазом, но и не был слишком велик.
Оптимизируем алгоритм
Минусом выбранного подхода миганием светодиодом является то, что ядро МК большую часть времени проводит в пустых циклах, хотя мог бы заниматься чем-нибудь полезным (в нашем примере других задач нет, но в будущем они появятся).

Для того, чтобы этого избежать, обычно используется счетчик циклов, а переключение состояние пина МК происходит при прохождении программы определенного числа циклов.
void main() { //Enable port D clocking *(unsigned long*)(0x40023830) |= 0x8; //little delay for GPIOD get ready volatile unsigned long i=0; i++; i++; i++; i=0; //Set PD13 as General purpose output *(unsigned long*)(0x40020C00) = (*(unsigned long*)(0x40020C00)& (~0x0C000000)) | (0x04000000); while(1) { i++; if(!(i%2000000)) { //Turn LED ON *(unsigned long*)(0x40020С14) |= 0x2020; } else if(!(i%1000000)) { //Turn LED OFF *(unsigned long*)(0x40020С14) &= ~0x2000; } } }
Но и тут не обойдется без проблем, с изменением количества команд выполняемых внутри цикла, будет меняться период мигания светодиодом (или период выполнения других команд в цикле). Но на данном этапе мы не можем с этим бороться.

Немного об отладке
IAR позволяет осуществлять отладку приложения непосредственно в железе. Все выглядит практически так же, как и отладка приложения для ПК. Есть режим пошагового выполнения, входа в функцию, просмотр значения переменных (В режиме отладки View->Watch->Watch1/4 ).

Но помимо этого, присутствует возможность просмотра значений регистров ядра, спецрегистров периферийных блоков (View->Register) и т.п.
Я настоятельно рекомендую ознакомиться с возможностями дебаггера во время изучения программирования МК.

Несколько слов в заключение

Возможно, многие скажут, что ручное прописывание адресов в программе это не правильно, поскольку производитель предоставляет файлы с определениями регистров и битовых полей, библиотеки для работы с периферией и другие инструменты, облегчающие жизнь разработчику. Я с этим полностью согласен, но все равно считаю, что первые шаги в программировании МК необходимо делать перекапывая документацию к вручную, самостоятельно определяя необходимые регистры и битовые поля. В дальнейшем этим можно не пользоваться, но уметь нужно обязательно.
Приведу лишь несколько причин для этого утверждения:
  • В библиотеках от производителя иногда встречаются ошибки! Я один раз чуть не сорвал срок проекта из-за этого. Несколько раз перепаивал чип, думая, сто повредил кристалл при пайке (до этого такое случалось). А проблема заключалась в том, что в библиотеке был неверно прописан адрес спецрегистра. Обычно такое случается с МК или линейками МК только вышедшими на рынок.
  • Библиотеки для работы спериферией некоторых производителей не реализуют всех возможностей периферийных блоков. Особенно этим грешилb Luminary Micro , которых в последствии выкупили TI. Приходилось писать инициализацию периферии вручную.
  • Многие привыкают начинать программирование МК с изучения примеров. Я считаю, что сперва необходимо определиться с тем, что позволяет реализовать МК. Это можнопонять только прочитав документацию. Если чего-то нет в примерах, это не значит, что железоэто не поддерживает. Последний пример - аппаратная поддерка PTP STM32. В сети, конечно, можно кое-что найти, но это не входит в стандартный набор от производителя.
  • Драйверы периферийных блоков некоторых производителей настолько не оптимизированы, что на переключение состояния пина средствами библиотеки тратится до 20 тактов. Это непозволительная роскошь для некоторых задач.

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

Одна из причин популярности микроконтроллеров STM32 производства STMicroelectronics – разнообразие инструментов разработки и отладки. Это касается как аппаратных, так и программных средств. Существует возможность создания и отладки резидентного ПО для STM32 без материальных затрат с помощью набора бесплатных программ . В статье дан обзор наиболее значимых бесплатных программных инструментов разработки: ST MCU Finder, STM32CubeMX, SW4STM32, STM32 Studio .

Компания STMicroelectronics (ST) является крупнейшим производителем микроконтроллеров в мире, при этом большая часть приходится на семейства STM32. При разработке новых линеек контроллеров STMicroelectronics преследует несколько стратегических целей:

  • повышение производительности;
  • повышение уровня интеграции: рост объема памяти, расширение перечня периферии;
  • снижение потребления;
  • снижение стоимости.

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

Дадим краткую характеристику семействам микроконтроллеров STM32 от ST.

Малопотребляющие микроконтроллеры семейств STM32L. Данная группа объединяет семейства, ориентированные, в первую очередь, на достижение минимального уровня потребления. Для этого используются различные методы: динамическое управление напряжением питания, гибкая система тактирования, специализированная периферия (LP-Timer, LP-UART), развитая система режимов пониженного потребления и так далее.

Базовые семейства STM32F0, STM32F1, STM32F3. Данная группа включает семейства со сбалансированными характеристиками и компромиссным значением производительности/потребления/цены.

В свою очередь отдельные пакеты STMCube включают:

  • аппаратно независимые библиотеки HAL для работы с аппаратными средствами микроконтроллеров;
  • библиотеки промежуточного уровня. Например, в состав самого развитого пакета ПО STM32CubeF7 входят следующие библиотеки и стеки: CMSIS-RTOS на базе FreeRTOS, стек TCP/IP на базе LwIP, файловая система FAT на базе FatFs с поддержкой NAND Flash, StemWin – графический стек на базе SEGGER emWin, полный стек USB (Host и Device). Для ряда семейств доступна библиотека Touch Library для сенсорных приложений;
  • примеры и шаблоны проектов для различных сред и отладочных наборов (Discovery, Nucleo, Evaluation Boards).

Чтобы понять, как происходит взаимодействие между составляющими программной платформы STM32Cube, следует обратиться к примеру, представленному на рисунке 9. В этом примере пользователь конфигурирует микроконтроллер STM32F429ZIT с помощью STM32CubeMX. После окончания визуальной настройки (выводов, тактирования и прочего) STM32CubeMX генерирует С-код, для этого используются библиотеки из программного пакета STM32CubeF4. В результате пользователь получает завершенный С-проект, сформированный для конкретной интегрированной среды разработки: IAR™ EWARM, Keil™MDK-ARM, Atollic® TrueSTUDIO и AC6 System Workbench (SW4STM32). В этот проект уже включены все необходимые библиотеки и файлы.

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

  • генерируемый С-код охватывает только настройку блоков и периферии микроконтроллера. Это значит, что алгоритмическую часть программы автоматически сгенерировать нельзя, ее нужно будет дописать вручную;
  • STM32CubeMX поможет создать только стартовую конфигурацию. Иногда в процессе работы пользователю требуется изменить частоту работы периферийного блока или изменить конфигурацию вывода. Все это придется прописывать самостоятельно;
  • для генерации кода используются стандартные, разработанные ST, библиотеки нижнего уровня (HAL и LL) и промежуточного уровня, например, StemWin или STM32_USB_Device_Library;
  • в процессе генерации С-файл выстраивается таким образом, что для пользователя выделяются специальные секции, в которые он может помещать свой код. Если пользовательский код окажется вне этих рамок – он будет затерт при следующих генерациях;
  • существуют и другие ограничения для отдельных блоков, для более подробного ознакомления с которыми следует обратиться к руководству по STM32CubeMX.

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

Создание простейшей программы с помощью STM32CubeMX

Рассмотрим подробнее создание скелета проекта в среде STM32CubeMX . Для начала требуется скачать саму среду STM32CubeMX. Это можно сделать абсолютно бесплатно с сайта ST. После установки на диске пользователя будут размещены как сам STM32CubeMX, так и папки с библиотеками STM32Cube.

Процесс создания скелета проекта выполняется по шагам.

Шаг первый. Скачивание актуальных версий библиотек с помощью специальной утилиты. Для этого вначале нужно настроить параметры сети (Help → Updater Settings) и далее запустить автоматическое обновление (Help → Check for Updates). Если ПК не подключен к сети – обновлять библиотеки придется вручную.

Шаг второй. После запуска STM32CubeMX на стартовом экране или в меню “File” необходимо создать новый проект, нажав “New Project”. Далее STM32CubeMX предложит выбрать целевую платформу: контроллер с заданными параметрами или отладочную плату. На рисунке 10 в качестве примера демонстрируется, как встроенный поиск подобрал список контроллеров по параметрам: семейство STM32F4, корпус TQFP100, объем Flash не менее 592 кбайт, ОЗУ более 214 кбайт.

Шаг третий. На третьем этапе разработчику предстоит определить назначение выводов с помощью Pin Wizard (рисунок 11). Данная утилита помогает создавать требуемую конфигурацию и проверять ее на ошибки. Стоит отметить и удобную систему подсветки, например, системные выводы закрашиваются бледно-желтым цветом.

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

Шаг пятый. Создание С-кода начинается с выбора целевой интегрированной среды в настройках проекта (Project → Settings). В настоящий момент к услугам пользователя предлагаются: IAR™ EWARM, Keil™MDK-ARM, Atollic® TrueSTUDIO и AC6 System Workbench (SW4STM32) (рисунок 13). Далее на вкладке Code Generator следует определиться с составом копируемых в директорию проекта библиотек, настройками обработки пользовательского кода при регенерации (например, сохранять или удалять), настройками использования HAL (рисунок 13).

Для более детальной настройки генератора следует перейти на вкладку Advanced Settings (рисунок 14). Главной особенностью С-генератора в STM32CubeMX является возможность использования как HAL-, так и LL-драйверов. На этом моменте следует остановиться подробнее.

HAL – набор абстрактных драйверов, обеспечивающих максимальную кроссплатформенность среди контроллеров STM32. При этом некоторые драйверы являются абсолютно универсальными (подходят ко всем контроллерам STM32), а часть применима только к отдельным линейкам с соответствующими периферийными блоками (например, блоками шифрования). Главными достоинствами HAL являются :

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

Вместе с тем, у HAL есть и недостатки: значительный объем кода, недостаточная оптимизация выполнения задач, сравнительно малое быстродействие. Если эти недостатки являются критичными, то следует использовать LL-драйверы.

Low Layer APIs (LL) – аппаратно зависимые драйверы, позволяющие напрямую работать с периферией контроллера, в том числе – использовать inline-функции и выполнять атомарный доступ к регистрам . Такой подход не требует значительных затрат памяти, функции получаются максимально короткими и эффективными по скорости. Очевидными недостатками LL-драйверов являются снижение совместимости кода при переходе от одного контроллера к другому и необходимость глубокого знания особенностей архитектуры контроллера.

В рамках одно и того же проекта на STM32CubeMX можно одновременно использовать как HAL так и LL, но для разных периферийных блоков. Например, на рисунке 15 показаны настройки С-генератора, при которых для UART/TIM/RTC используются LL-драйверы, а для остальных блоков – HAL.

Шаг шестой. После настройки проекта следует выполнить генерацию кода, зайдя в меню Project → Generate Code. В результате этого в указанной директории проекта будет сгенерирован скелет проекта для заданной среды разработки.

Иногда возникает необходимость миграции проекта с одной платформы на другую. С помощью STM32CubeMX это можно сделать с минимальными затратами времени.

Миграция проектов с помощью STM32CubeMX

Для миграции проекта с одной платформы на другую используется дополнительная утилита File → Import Project (рисунок 15). В ней требуется указать тип нового микроконтроллера и режим миграции. После этого программа автоматически генерирует новый код либо, при наличии неполной совместимости ядер, указывает на возникшие сложности, предлагая пользователю их устранить.

Несовместимость, обнаруживаемая при миграции, может быть устранимой и неустранимой. Неустранимый случай возникает, когда состав периферии контроллеров значительно отличается. Например, ранее использовался модуль Ethernet, который отсутствует на новом МК (рисунок 15). Очевидно, что в таком случае миграция невозможна.

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

После получения итогового скелета проекта останется добавить пользовательскую алгоритмическую часть кода, провести компиляцию и выполнить отладку. Для этого используются специализированные среды. Среда SW4STM32 для STM32 производства компании AC6 позволяет делать это абсолютно бесплатно.

AC6 System Workbench – бесплатная IDE для STM32

Для редактирования, компиляции и отладки программ предназначены специальные интегрированные среды IDE. Большая часть из них является коммерческими продуктами (IAR™ EWARM, Keil™MDK-ARM, Atollic® TrueSTUDIO и другие), но есть и бесплатные инструменты, например, System Workbench производства компании AC6. В рамках системы названий STMicroelectronics данная IDE носит название SW4STM32 .

Интегрированная среда SW4STM32 предназначена для работы с микроконтроллерами STM32. Она основана на базе платформы Eclipse и является бесплатной и кроссплатформенной. Основными ее достоинствами являются :

  • поддержка работы с микроконтроллерами STM32, аппаратными отладочными наборами (STM32 Nucleo, Discovery и Evaluation boards), с программными библиотеками (Standard Peripheral library и STM32Cube HAL);
  • отсутствие ограничений на объем программного кода;
  • бесплатный компилятор GCC C/C++;
  • свободный отладчик GDB (проект GNU);
  • открытая платформа Eclipse IDE с поддержкой групповой разработки встраиваемого ПО с системой контроля версий SVN/GIT;
  • совместимость с плагинами Eclipse;
  • поддержка ST-LINK;
  • мультиплатформенность и совместимость с Windows®, Linux и OS X®.

С помощью SW4STM32 можно редактировать, компилировать и отлаживать программы для STM32. Для этого удобно использовать скелеты программ, создаваемые в STM32CubeMX. Для их импорта следует выполнить простейшие операции: выбрать меню File -> Import, назначить тип импорта «Existing Projects into Workspace», указать директорию проекта, выбрать сам проект и нажать Finish.

При работе с проектами, созданными в STM32CubeMX, следует размещать пользовательский код в специальных секциях:

/*USER CODE BEGIN…*/

/*USER CODE END…*/

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

  • при перенастройке используемого МК;
  • при миграции с одного МК на другой.

Таким образом, при работе в связке STM32CubeMX + SW4STM32 пользователь может в любой момент перенастроить контроллер и произвести миграцию с сохранением пользовательского кода при минимальных затратах времени.

При отладке программ в SW4STM32 доступна возможность широкого мониторинга состояния памяти, регистров, переменных. Также среда имеет поддержку точек останова (рисунок 17). Для запуска процесса отладки требуется нажать значок “Debug” (в виде жука), выбрать тип проекта “Ac6 STM32 C/C++ Application”, определить тип отладчика и интерфейс, нажать кнопку “OK”.

SW4STM32 имеет поддержку систем контроля версий SVN/GIT. Это важно для больших проектов, над которыми работает несколько разработчиков. Система контроля версий позволяет: регистрировать все изменения, производимые в проекте; сравнивать версии проектов; восстанавливать предыдущие версии; разрешать конфликты при работе нескольких программистов над одним файлом; вести параллельно несколько версий и так далее.

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

Рассмотрим последовательность действий при подключении проекта к SVN (рисунок 18).

  • в открытом проекте щелкнуть по его названию на панели директорий правой кнопкой мыши и пройти Team → Share Project(s) (рисунок 18а);
  • выбрать тип системы SVN/GIT и нажать “Next” (рисунок 18б);
  • выбрать директорию для SVN и нажать “Next” (рисунок 18в);
  • выбрать директорию хранения проекта в SVN нажать “Finish” (рисунок 18г);
  • на вкладке “General” (рисунок 18д) выбрать URL-адрес SVN, метку для репозитория, имя пользователя, пароль, нажать “Next”;
  • ввести комментарий для проекта, выбрать файл, помещаемый под контроль SVN, нажать “OK” (рисунок 18е).

В дальнейшем для синхронизации файла или всего проекта необходимо щелкнуть по его названию на панели директорий правой кнопкой мыши и выбрать Team → Commit. В открывшемся окне следует написать пояснение к изменениям и нажать “OK”.

Для отключения SVN следует использовать команду Team → Disconnect.

Для импорта проекта из SVN применяется команда меню Import → SVN → Project from SVN. Далее требуется выполнить ряд настроек по импорту во всплывающих диалоговых окнах.

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

  • отсутствие встроенного симулятора;
  • компилятор GCC проигрывает своим коммерческим собратьям по объему кода и быстродействию;
  • поддержка SW4STM32 со стороны разработчиков не будет столь оперативной, как в случае с платными средами.

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

Отладка кода может производиться не только в SW4STM32, но с помощью дополнительных средств. Рассмотрим некоторые из них.

STMStudio – простой способ отладки приложений на STM32

STM Studio – фирменная утилита производства компании STMicroelectronics, которая помогает проводить отладку программы и позволяет отслеживать значения пользовательских переменных при выполнении кода в реальном времени. Данная программа запускается под ОС Windows и использует отладчик ST-LINK для связи с микроконтроллером.

STM Studio имеет следующие возможности :

  • чтение переменных из ОЗУ «на лету», без влияния на работу пользовательской программы;
  • использование исполнительных файлов.elf, .out, .axf для импорта переменных;
  • вывод значений переменных в табличной и графической форме;
  • графический вывод в виде графиков или диаграмм;
  • возможность вывода зависимостей переменных, когда одна из переменных откладывается по оси Х, а вторая – по оси Y;
  • логирование данных в файл для последующего просмотра.

Окно STM Studio состоит из нескольких панелей (рисунок 19).

Работа с STM Studio начинается с импорта переменных. Для этого в программу необходимо загрузить тот же исполнительный файл, что находится в самом микроконтроллере. Для этого подойдут следующие форматы, которые генерируются при компиляции: .elf, .out, .axf. Далее необходимо выполнить команду File → Import variables. В диалоговом окне при выборе пункта “Expand table elements” пользователь сможет вручную выбрать из предлагаемой таблицы любые глобальные переменные. Для запуска отладки необходимо выполнить команду “Run”.

Как говорилось выше, STM Studio позволяет отображать переменные в трех формах: в виде текста, диаграммы и графика (рисунок 20). Настройка типа отображения может быть изменена в любое время. Кроме того, все данные дополнительно записываются в лог-файл для дальнейшего анализа. Интересной особенностью STM Studio является возможность отображения зависимостей одних переменных от других, а также построения выражений из переменных.

Популярным средством передачи отладочной информации является использование консоли и функции вывода printf().

Реализация терминального вывода printf() через USART

Использование стандартной функции printf() – один из наиболее популярных методов вывода отладочной информации. С помощью данной функции вывода пользователь может передавать любые данные на консоль среды разработки или терминал. Большинство интегрированных сред поддерживает эту возможность. При использовании STM32 есть два способа реализации этого метода: традиционный, с помощью UART, и дополнительный, через SWO-интерфейс с помощью отладчика ST-LINK. Реализация каждого из них максимально упрощается при использовании STM32CubeMX и SW4STM32.

Рассмотрим вначале первый вариант реализации – через UART. Для этого придется выполнить следующую последовательность действий:

  • обеспечить аппаратное подключение к ПК;
  • выполнить настройку UART в среде STM32CubeMX;
  • реализовать саму функцию printf() в среде SW4STM32.

Подключение к ПК можно выполнить тремя путями: через COM-порт и микросхему приемопередатчика RS-232; через USB-порт и микросхему конвертера UART-USB (например, FT232); с помощью USB-интерфейса отладчика ST-LINK. Вне зависимости от того, какой способ выбран, далее необходимо сконфигурировать аппаратный UART.

C помощью STM32CubeMX настройка UART выполняется в несколько кликов (рисунок 21). Сначала на вкладке Pin Wizard соответствующие выводы контроллера переводятся в режим работы UART. Далее во вкладке “Configuration” настраиваются параметры UART: тип обмена, скорость, наличие стоп-битов и так далее. После этого генерируется С-код.

В среде SW4STM32 необходимо подключить стандартную библиотеку и определить функции _io_putchar() и _write(), например, так:

/*USER CODE BEGIN Includes*/

#include

/*USER CODE END Includes*/

/*USER CODE BEGIN 1*/

int __io_putchar(int ch)

c = ch & 0x00FF;

HAL_UART_Transmit(&huart2,&*c,1,10);

int _write(int file, char *ptr, int len)

for (DataIdx = 0; DataIdx < len; DataIdx++)

Достоинствами такого подхода к передаче отладочной информации можно считать:

  • использование интерфейса UART, который присутствует во всех микроконтроллерах STM32 без исключения;
  • простоту настройки и привычность для программистов. Можно использовать старые наработки из проектов с другими контроллерами;
  • отсутствие сложного аппаратного обеспечения (за исключением моста UART-USB или приемопередатчика RS-232);
  • отсутствие сложного ПО. Работа производится со всеми IDE или терминальными программами.

Однако есть у этого метода и недостатки. Во-первых, придется пожертвовать каналом UART для отладки. А во-вторых, такая реализация влияет на работу контроллера, так как занимает ядро для обработки кода функции printf(). В случае с STM32 есть более специализированный, а главное – простой способ, который не занимает ресурсы микроконтроллера – использование связки SWO и ST-LINK.

Реализация терминального вывода printf() через SWO

При использовании связки SWO и ST-LINK создание терминального ввода/вывода оказывается еще проще, чем в рассмотренном выше методе с аппаратным UART. В этом случае связь с ПК осуществляется через интерфейс SWO и USB-интерфейс, используемый в ST-LINK. Последовательность действий остается примерно той же, что и в предыдущем случае.

Сначала с помощью STM32CubeMX происходит настройка выводов SWO-интерфейса во вкладках “Pin Wizard” и “Configuration” (рисунок 22). После этого происходит перегенерация кода для среды разработки.

Следующий шаг заключается в написании кода обработчика __io_putchar(int ch), например, так:

/*USER CODE BEGIN 1*/

int __io_putchar(int ch)

ITM_SendChar(ch);

/*USER CODE END 1*/

Для отладки удобно использовать утилиту STLink Utility (рисунок 23).

Достоинства метода:

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

Из недостатков этого метода реализации можно отметить аппаратную зависимость, так как требуется наличие ST-LINK.

Заключение

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

Важным преимуществом STM32 является наличие развитой системы средств отладки. К услугам разработчиков предлагается более ста отладочных плат (Nucleo, Discovery, Evaluation Boards). Еще большим подспорьем для программистов станет наличие полного набора бесплатного прикладного ПО для создания, компиляции и отладки программного кода:

ST MCU Finder – приложение для смартфонов, помогающее выбрать наиболее оптимальный МК для конкретного приложения;

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

SW4STM32 – кросплатформенная интегрированная среда разработки встраиваемого ПО для микроконтроллеров STM32.

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

ST-LINK Utility позволяет совместно с программатором ST-Link выполнять ввод и вывод отладочной информации через SWO-интерфейс.

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

Литература

  1. Data brief. NUCLEO-XXXXKX. STM32 Nucleo-32 board. Rev 3. ST Microelectronics, 2016.
  2. Data brief. NUCLEO-XXXXRX. STM32 Nucleo-64 board. Rev 8. ST Microelectronics, 2016.
  3. Data brief. NUCLEO-XXXXZX. STM32 Nucleo-144 board. Rev 5. ST Microelectronics, 2017.
  4. UM1718. User manual. STM32CubeMX for STM32 configuration and initialization C code generation. Rev 18. ST Microelectronics, 2017.
  5. Виктор Чистяков. CUBEMX И WORKBENCH: СОЗДАНИЕ ПРОЕКТА НА БАЗЕ STM32 С ПОМОЩЬЮ БЕСПЛАТНОГО ПО. Новости Электроники №2/2016.
  6. UM1884. User manual. Description of STM32L4 HAL and Low-layer drivers. Rev 5. ST Microelectronics, 2016.
  7. UM1025. User manual. Getting started with STM-STUDIO. Rev6. ST Microelectronics, 2013.
  8. UM0892.User manual STM32 ST-LINK utility software description. Rev 22. ST Microelectronics, 2016.

О компании ST Microelectronics