В начало | Зарегистрироваться | Заказать наши киты почтой
 
 
 
 

Программирование микроконтроллеров на языке C. Часть 2

📆12 марта 2023   ✒️erbol   🔎2.185   💬0  
Программирование микроконтроллеров на языке C. Часть 2

Добрый день, уважаемые камрады-датагорцы! Сегодня, рассмотрев некоторые общие моменты, мы займёмся программированием.

Общие сведения

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

Что читать?

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

В качестве учебника по Си рекомендую книгу Брайана Кернигана и Денниса Ритчи «Язык программирования Си», изложенную в простой и доходчивой форме.

Касательно МК достаточно, как мне кажется, вдумчивого изучения технической документации — даташитов, спецификаций и мануалов. Те из вас, кто хочет глубже вникнуть в детали работы ядра nRF52832 и STM32F401, могут обратиться к Cortex-M4 User Guide и Reference Manual.
Все вышеупомянутые источники выложены в архив статьи.

Кроме того, крайне желательно ознакомиться хотя бы с описательной частью нашей серии статей «Ассемблер...».

Термины и понятия

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

Компиляция, в общем смысле — перевод кода с-файлов в ассемблерный с последующими компоновкой в единый отладочный elf- и преобразованием его в загрузочный hex-файл.
Препроцессинг — предваряющий компиляцию этап, в ходе которого препроцессор осуществляет подготовительную работу, в т.ч. исполняет адресованные ему директивы, производит замену в коде макроопределений на значения определяемых ими констант и пр.
Директива — указание препроцессору, помеченное символом «#».
Заголовочный (или хэдер-) файл — файл с расширением h, содержащий вспомогательную информацию. Подключается к компиляции посредством директивы #include, размещаемой в самом верху с-файла, который использует данный хэдер-файл.
#include <имя хэдер-файла>
или
#include "имя хэдер-файла"

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

Типы данных

Любая программа оперирует различными числовыми данными: целочисленными или вещественными, знаковыми или беззнаковыми, т.е. принимающими и положительные и отрицательные или только положительные значения, соответственно. Текущее значение некоторых из них требует временного хранения, для чего выделяется место в оперативной памяти. При этом, длина регистров ОЗУ может быть разной, например, для ATmega8 она составляет 8, а для STM32F401 и nRF52832 — 32 бита. Если использовать в программе формулировки вроде «выделить 2 регистра памяти», в случае с AVR-8 это будет означать 16, а с Cortex-M4 — 64 бита. Очевидно, о переносимости такого кода не может быть и речи, поэтому отталкиваются от длины не регистров, а самих данных, в связи с чем введено понятие «тип данных».

Стандартом установлены следующие базовые типы целочисленных и вещественных данных:
void, означающий отсутствие значения данных,
char — для целочисленных 8-битных данных,
int — для целочисленных данных, длина которых составляет 16 или 32 бита, в зависимости от конкретной среды программирования,
float — для чисел с плавающей точкой одинарной точности,
double — для чисел с плавающей точкой двойной точности.

Затем, посредством квалификаторов знака (signed и unsigned) и длины (short и long), осуществляется более строгая типизация. Подробная информация по указанным типам данных приведена в Таблице 1.

Таблица 1. Типы данных

Обращу ваше внимание на несколько моментов:
а) Тип char, в зависимости от реализации языка и настроек компилятора, может быть знаковым или беззнаковым. В нашем случае он — беззнаковый.
б) Тип int — всегда знаковый.
в) В той или иной реализации языка минимальные значения знаковых целочисленных данных могут отличаться на единицу, например, -127, вместо -128 для signed char или -32767 вместо -32768 для short. Конкретные значения определяются в файле limits.h тулчейна. В нашем случае действительны значения, приведённые в Таблице 1.
г) Граничные значения для вещественных чисел определены в файле float.h тулчейна.

Возможно, кому-то из вас больше придётся по душе производная от базовых линейка типов целочисленных данных вида intN_t/uintN_t, определённых в заголовочном файле stdint.h тулчейна, который необходимо подключить к компиляции при использования линейки. Соответствие указанных типов вышеприведённым представлено в Таблице 2.
Программирование микроконтроллеров на языке C. Часть 2
Таблица 2. Соответствие типов данных

Следует упомянуть ещё одни тип данных, предназначенный для отражения логических значений — _Bool. С целью совместимости с С++, для типа _Bool в файле stdbol.h тулчейна определена производная bool, а также даны макроопределения её значений: false (для 0) и true (для 1).

Теперь, о представлении данных в программе.
Константы, т.е. данные не меняющие своего значения, можно прописывать непосредственно в коде, при этом для удобочитаемости и гибкости последнего в некоторых случаях желательно давать им макроопределения, о чём — чуть ниже. По умолчанию размерность констант — int, но при необходимости можно задать длину явно с помощью соответствующего суффикса, следующего сразу после значения константы: 65535UI (Unsigned Int), 16000000UL (Unsigned Long) и т.п.

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

При этом, существует несколько гласных и негласных правил, которых мы и будем придерживаться в последующем:
1. Выбор типа переменной определяется диапазоном значений, который она может принимать по ходу исполнения программы.
2. Имя переменной является аналогом метки в ассемблере и служит, с одной стороны, для идентификации компилятором адреса переменной в ОЗУ. С другой стороны, по имени программист различает те или иные данные, поэтому желательно, чтобы оно несло полезную смысловую нагрузку. Простое имя оформляется прописными буквами, а в для составного применяется так называемый «Camel style», когда первая составляющая пишется полностью прописными, а все последующие начинаются с заглавной. Для переменных, хранящих одинаковые по назначению данные, можно использовать нумерацию через символ подчёркивания.
3. Во избежание попадания «мусора» при подаче питания/сбросе желательно инициализировать переменную при объявлении, т.е. присвоить ей посредством оператора «=» (или записать в неё) начальное значение. Чаще всего это — 0.

Для примера, объявление двух переменных для хранения текущего 16-битного значения времени двух разных процессов, может выглядеть так:
unsigned short timeValue_1 = 0;
unsigned short timeValue_2 = 0;

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

Дополнительно к уже сказанному советую поискать информацию о приведении типов данных.

Арифметические и логические операции. Битовые маски

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

В Си приняты следующие операторы арифметических и логических операций:

Таблица 3. Операторы арифметических и логических операций

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

Рисунок 1. Синтаксис операции

При этом, понятие «Регистр» относится и к регистрам ОЗУ, т.е. вся нижеследующая информация по операциям справедлива и для переменных. Понимать запись на Рисунке 1 следует так: «Считать текущее значение регистра (этап 1), произвести операцию между этим значением и заданным числом (этап 2), записать получившийся результат обратно в регистр (этап 3)».

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

Рисунок 2. Свёрнутая форма синтаксиса операции

В этом случае, как видите, оператор и символ присваивания пишутся слитно.

Ниже представлены примеры операций с упомянутыми выше переменными:
/* Арифметические операции */
timeValue_1 -= 2;
timeValue_2 += 5;
timeValue_1 *= 8;
timeValue_2 /= 4;
/* Логические операции */
timeValue_1 >>= 8;
timeValue_2 <<= 4;

Инкремент (увеличение 1) и декремент (уменьшение на 1) значения переменной можно осуществлять в ещё более короткой форме:
/* Инкремент */
timeValue_1++;
/* Декремент */
timeValue_1--;

При инициализации модуля МК (порт ввода/вывода, таймер, АЦП и пр.) в регистры его настроек записывается то или иное число, двоичное представление которого — нули и единицы в требуемых битах регистра. В ходе работы может понадобиться изменить значение определённого бита, не меняя состояния других. Кроме того, иногда переменную используют как хранилище флагов, когда значение каждого из её битов отражает состояние отдельного процесса и должно, при необходимости, изменяться программистом соответствующим образом. Во всех указанных случаях удобнее использовать комбинацию логических операций и битовых масок.

Напомню, что маска числа с единицей в k-м бите и нулями в остальных выглядит так:
(1 << k)

Особо отмечу следующий нюанс. Единица в маске сама по себе является константой, а дефолтная размерность её, как говорилось выше — int, т.е. 16 или 32 бита. В случаях реализации языка или компилятора, когда длина int по умолчанию составляет 16 бит, попытка оформить маску с единицей в 16-м и выше битах приведёт к формированию некорректной маски, для исключения чего к единице добавляют соответствующий суффикс (UL или ULL).

Чтобы получить число с нулём в k-м бите и единицами в остальных, достаточно инвертировать предыдущую маску:
~(1 << k)

Для случая с единицами в нескольких битах необходимо произвести операцию ИЛИ между соответствующими масками:
/* Число с единицами в битах k, m, n и нулями в остальных */
(1 << k) | (1 << m) | (1 << n)
/* Инверсия предыдущего числа */
~((1 << k) | (1 << m) | (1 << n))

Далее между текущим значением регистра и маской применяется логическая операция ИЛИ, И либо ИСКЛЮЧАЮЩЕЕ ИЛИ в случаях когда значение требуемого бита надо установить в 1, сбросить в 0 либо поменять на противоположное, соответственно:
/* Установить в единицу 4-й бит регистра */
Регистр |= (1 << 4);
/* Сбросить в ноль 2-й и 6-й биты регистра */
Регистр &= ~((1 << 2) | (1 << 6));
/* Поменять на противоположное значение 5-го бита регистра */
Регистр ^= (1 << 5);
/* Объявить переменную flags и */
unsigned int flags = 0; 
/* установить в единицу 7-й, 3-й и 0-й биты */
flags |= (1 << 7) | (1 << 3) | (1 << 0);
/* сбросить в ноль 3-й бит */
flags &= ~(1 << 3);
/* поменять на противоположное значение 7-го бита */
flags ^= (1 << 7);

Позже, мы будем активно использовать ещё одну возможность, которую даёт комбинация логической операции И с маской. Чтобы определить состояние k-го бита регистра, достаточно произвести указанную операцию между текущим значением регистра и маской с единицей в искомом бите.
РЕГИСТР & (1 << k)

Результат будет ненулевым только, если k-й бит находится в состоянии логической единицы.

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

Макроопределения, макросы, функции

Из примеров с переменной flags выше ясно только то, что мы меняем состояние битов 7, 3 и 0, однако можно повысить их информативность, а заодно и читабельность, если посредством директивы #define препроцессора дать номерам битов условные названия или макроопределения.

Предположим, что значения 1/0 указанных битов отражают состояние ВКЛЮЧЕНО/ВЫКЛЮЧЕНО:
• бит 7 — мотора,
• бит 3 — светодиода,
• бит 0 — реле.

Тогда, дав соответствующие макроопределения,
#define MOTOR_STATE_FLAG 7
#define LED_STATE_FLAG 3
#define RELAY_STATE_FLAG 0

можно переписать код следующим образом:
unsigned int flags = 0; 
flags |= (1 << MOTOR_STATE_FLAG) | (1 << LED_STATE_FLAG) | (1 << RELAY_STATE_FLAG);
flags &= ~(1 << LED_STATE_FLAG);
flags ^= (1 << MOTOR_STATE_FLAG);

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

Макрос — практически то же, что и макроопределение, только по отношению к фрагменту кода. Оформим макросы изменения значений битов (всех вместе взятых и каждого по отдельности):
#define SET_ALL_FLAGS flags |= (1 << MOTOR_STATE_FLAG) | (1 << LED_STATE_FLAG) | (1 << RELAY_STATE_FLAG)
#define CLEAR_ALL_FLAGS flags &= ~((1 << MOTOR_STATE_FLAG) | (1 << LED_STATE_FLAG) | (1 << RELAY_STATE_FLAG))
#define TOGGLE_ALL_FLAGS flags ^= (1 << MOTOR_STATE_FLAG) | (1 << LED_STATE_FLAG) | (1 << RELAY_STATE_FLAG)

#define SET_MOTOR_STATE_FLAG flags |= (1 << MOTOR_STATE_FLAG)
#define CLEAR_MOTOR_STATE_FLAG flags &= ~(1 << MOTOR_STATE_FLAG)
#define TOGGLE_MOTOR_STATE_FLAG flags ^= (1 << MOTOR_STATE_FLAG)

#define SET_LED_STATE_FLAG flags |= (1 << LED_STATE_FLAG)
#define CLEAR_LED_STATE_FLAG flags &= ~(1 << LED_STATE_FLAG)
#define TOGGLE_LED_STATE_FLAG flags ^= (1 << LED_STATE_FLAG)

#define SET_RELAY_STATE_FLAG flags |= (1 << RELAY_STATE_FLAG)
#define CLEAR_RELAY_STATE_FLAG flags &= ~(1 << RELAY_STATE_FLAG)
#define TOGGLE_RELAY_STATE_FLAG flags ^= (1 << LED_STATE_FLAG)

и перепишем наш код:
unsigned int flags = 0; 
SET_ALL_FLAGS;
CLEAR_LED_STATE_FLAG;
TOGGLE_RELAY_STATE_FLAG;

Макросы, в отличие от макроопределений, могут принимать аргументы, что делает их использование более гибким. Пропишем макросы, принимающие в качестве аргументов имя регистра и номер бита, а затем меняющие значение последнего соответствующим образом:
#define SET_BIT(arg1, arg2) arg1 |= (1 << arg2)
#define CLEAR_BIT(arg1, arg2) arg1 &= ~(1 << arg2)
#define TOGGLE_BIT(arg1, arg2) arg1 ^= (1 << arg2)

Тогда, код примера примет следующий вид:
unsigned int flags = 0; 
SET_ALL_FLAGS;
CLEAR_BIT(flags, LED_STATE_FLAG);
TOGGLE_BIT(flags, RELAY_STATE_FLAG);

Чтобы не перегружать с-файл, макроопределения и макросы обычно помещают в связанный хэдер-файл. В отношении их оформления также существуют неписанные правила:
1. Имена макроопределений макросов пишутся заглавными буквами.
2. Элементы составного имени отделяются знаком подчеркивания.
3. В именах сходных по назначению макроопределений можно использовать нумерацию через символ подчёркивания.

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

Прописывается функция в с-файле и синтаксис её, в общем случае, следующий:
тип возвращаемого аргумента имя функции(тип и имя принимаемого аргумента)
{
 return возвращаемый аргумент;
}

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

Помимо вышесказанного, в одноимённом хэдер-файле необходимо объявить прототип функции, который фактически дублирует её заголовок:
тип возвращаемого аргумента имя функции(тип и имя принимаемого аргумента);

Вызов функции осуществляется через её имя:
/* Вызов функции не имеющей аргументов */
function_1();
/* Вызов функции с возвращаемым аргументом */
int a = function_2();
/* Вызов функции с принимаемым аргументом */
int b = 5;
function_3(b);

Следует иметь в виду, что в Си реализована передача в функцию по значению. Это означает, что в функцию function_3() выше передаётся значение переменной b (т.е. число 5), а не сама она и работа функции никак не затронет состояние переменной.

Структура проекта и назначение его файлов

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

Рисунок 3. Структура проекта для МК с ядром а) AVR-8 и б) Cortex-M4

Папка .vscode создаётся автоматически, как только вы вносите какое-либо изменение в текущие настройки редактора через File/Preferences или функциональную клавишу F1. На данный момент она содержит единственный файл — c_cpp_properties.json, куда, среди прочего, внесены пути ко включаемым при компиляции папкам. В дальнейшем .vscode может пополнятся и другими файлами, если вы надумаете, например, поменять цвет фона или размер шрифта.

В папке inc мы будем хранить заголовочные h-файлы, а в src — с-файлы с исходным кодом.

LinkerScript.ld

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

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

Разработчик ARM лишь ограничивает область памяти, а каждый производитель МК на базе этого ядра сам определяет её конкретные размеры и начальный адрес в пределах заданной области, по причине чего, видимо, в скриптах тулчейна указанные данные отсутствуют. Учитывая изложенное, к шаблонным файлам для Cortex-M4 добавлен LinkerScript.ld, чтобы пользователь мог контролировать параметры памяти для МК, программируемого в данный момент. Фрагмент скрипта, который подлежит корректировке при смене МК, выглядит, в общем случае, так:
MEMORY
{
 RAM (): ORIGIN = начальный адрес ОЗУ, LENGTH = размер ОЗУ
 FLASH (): ORIGIN = начальный адрес флэш-памяти, LENGTH = размер флэш-памяти
}

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

Что касается размеров памяти (LENGTH), в случае с nRF информацию следует искать в спецификации. Чтобы получить аналогичные данные по STM32, необходимо перейти по ссылке и выбрать интересующее вас ядро, например, Cortex-M4, как показано на Рисунке 4.

Рисунок 4. Выбор ядра МК

Далее, в таблице на открывшейся странице кликаете по нужной линейке

Рисунок 5. Выбор линейки микроконтроллеров

и по диаграмме на следующей странице определяете размер обоих типов памяти вашего МК.

Рисунок 6. Определение размеров памяти МК

Makefile и variables.mk

Два основных правила из Makefile обуславливают компиляцию/компоновку файлов проекта (правило all) и загрузку hex-файла в МК (правило upload). Правило size предназначено для вывода в консоль информации о том, сколько памяти (флэш и ОЗУ) потребуется для размещения и работы вашей программы.

Исполнение любого правила запускается из консоли командой «make имя правила» + Enter. Поскольку правило all расположено первым по ходу, его имя в команде можно опустить, набрав лишь «make» + Enter. Чтобы избежать повторений в тексте, договоримся, что в дальнейшем словосочетание «компилировать программу» будет означать для нас команду из консоли «make» + Enter, а «загрузить программу» — «make upload» + Enter.

В variables.mk даны макроопределения, используемые в Makefile, первое из которых — макроопределение FILE имени проекта и, соответственно, hex-файла. В принципе, можно от проекта к проекту не менять его текущее значение «projectName», но, тогда со временем у вас накопится куча прошивок с одним и тем же именем, что, согласитесь, не очень удобно. Поэтому, изменим значение FILE на «first».

Помимо упомянутых выше, в Makefile для AVR-8 прописаны правила записи (fuses_write) или чтения (fuses_read) фьюзов и мы, пользуясь случаем, опробуем первое из них.
Дефолтная частота тактирования процессора указанного МК составляет 1МГц, что вызывает ряд неудобств:
• Загрузка hex-файла происходит медленно, а в некоторых случаях программатор USBasp может и не «увидеть» МК, требуя изменить частоту либо установить джампер J2, входящий в его состав.
• Процент ошибок при обмене данными по протоколу UART на частоте 1 МГц — высоковат.

Учитывая изложенное, повысим частоту до 8 МГц, для чего перейдём в калькулятор фьюзов и убедимся, что соответствующее значение младшего фьюз-байта составляет 0xC4.

Рисунок 7. Значения фьюз-байтов ATmega8 при частоте тактирования 8 Мгц

Затем, изменим значение макроопределения в строке 61 файла variables.mk на вышеуказанное
LOW_BYTE = 0xC4

и перезапишем фьюзы командой из консоли «make fuses_write» + Enter, получив там же сообщение об успешной записи
avrdude.exe: safemode: Fuses OK (E:FF, H:D9, L:C4)

main.c

Файл main.c из папки src является обязательным элементом и основой любой программы, поэтому детально разберём его начальное содержимое.
#include <avr/io.h> // "stm32f4xx.h", "nrf.h"

void SystemInit() // для STM32F401 и nRF52832
{
 
}

int main(void)
{
 /* Код, исполняемый один раз*/ 

 while(1)
 {
 /* Код, исполняемый в цикле */
 }
}

1. Первой строкой включаются заголовочные файлы, которые прямо или опосредованно содержат адреса регистров МК, макроопределения их битов и прочую информацию.
2. Те из вас, кто ознакомился с «Ассемблер...», в курсе, что любая программа начинается с размещения векторов прерываний и указания на вершину стека. В Си программист освобождён от этого, поскольку код указанных процедур уже прописан в startup-файле и автоматически помещается в самом верху вашей программы в ходе компиляции. При подаче питания/сбросе осуществляется переход по вектору RESET к основной функции main() либо напрямую (для ATmega8), либо через функцию SystemInit(), по причине чего последняя и присутствует в main.c для STM32F401 и nRF52832. Присутствует и ладно, тем более, что впоследствии мы используем её с выгодой для себя.
3. Тип возвращаемого и принимаемого аргументов функции main() в нашем случае — всегда int и void, соответственно.
4. Как следует из комментариев, код, размещённый в main() до цикла while(), исполняется процессором один раз. Чаще всего это — первоначальные настройки модулей МК, используемых в данном проекте. Инструкции, которые мы прописываем внутри фигурных скобок цикла while(), процессор исполняет по кругу, временно покидая его лишь для обработки прерываний или перехода к вызываемым функциям.
5. Комментарии к коду реализуются двумя способами: предваряются символом «//», если комментарий умещается в одну строку
// комментарий в одну строку 

либо заключаются между символами «/*» и «*/» при комментировании блока строк.
/*
 комментарий 
 в
 несколько 
 строк
*/

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

Обмен данными с МК

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

1. Подключите USB-UART адаптер к компьютеру. На борту макетной платы nRF52832 имеется собственный адаптер, поэтому её достаточно подключить напрямую.
2. Выясните в диспетчере устройств Windows номер выделенного под адаптер COM-порта.
3. Пройдите во вкладку Serial monitor в нижней части VS Code и выберите порт вашего адаптера из списка «Port», а также скорость 9600 — из списка «Baud rate». Соединение с адаптером устанавливается нажатием кнопки «Start monitoring».

Рисунок 8. Настройки последовательного порта

4. Скопируйте для вашего МК из архива статьи файлы Serial.c и Serial.h в папки проекта src и inc, соответственно. Библиотека Serial содержит две нужные нам функции:
serialBegin() инициализирует протокол UART и прописывается до цикла while() функции main().
serialPrintln() передаёт в последовательный порт строку, заключённую в кавычки, и размещается в том месте кода, где требуется вывод информации.
5. Включите библиотеку в компиляцию и добавьте, для примера, в функцию main() следующий код:
#include “Serial.h”
 
int main()
{
 serialBegin();
 serialPrintln("Hello World!");
 
 while(1)
 {
 
 }
}

6. Соедините вывод TX микроконтроллера (PD1 – для ATmega8, PA2 или A2 – для макетной платы STM32F401) с аналогичным выводом адаптера, скомпилируйте программу, загрузите её в МК и убедитесь, что строка успешно передана в последовательный порт.

Рисунок 9. Отражение данных в мониторе порта

7. Вывод в монитор численных значений потребует применения функций библиотеки stdio тулчейна. Для ATmega8 указанная библиотека обнаруживается автоматически, а вот в случае с Cortex-M4 для успешного её поиска необходимо в файле c_cpp_properties.json после определения стандарта языка добавить через запятую путь к компилятору. В моём случае это выглядит следующим образом:
"cStandard": "c99",
"compilerPath": "D:/GNU/ARM/armGnuToolchain/bin/arm-none-eabi-gcc.exe"


Схема устройства

Проверять работу написанных программ мы будем с помощью устройства, схема которого приведена на Рисунке 10, а распиновка для каждого из рассматриваемых нами МК — в Таблице 4.

Рисунок 10. Схема устройства для проверки кода

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


Таблица 4. Распиновка устройства

В случае с макетными платами задействованы имеющиеся на борту:
• Пары светодиодов и кнопок, которые подключены к пинам P0.17-P0.18 и P0.13-P0.14, соответственно — для nRF52832.
• Светодиод, подключённый к пину PC13 (C13) — для STM32F401.

Порт ввода-вывода

На каждый из пинов МК, в связи с ограниченностью их количества, возлагается несколько функций, дефолтная из которых, обычно — вход/выход для чтение из-вне/записи во-вне дискретного значения: логических 1 или 0. При этом, пины объединяются в порты ввода-вывода (GPIO), количество которых зависит от комплектации МК, в частности:
• для ATmega8 — 3 порта: B (пины PB0-PB7), C (пины PC0-PC6) и D (пины PD0-PD7),
• для STM32F401 (QFPN48) — 3 порта: A (пины PA0-PA15), B (пины PB0-PB10, PB12-PB15) и C (пины PC13-PC15),
• для nRF52832 — один порт, который и составляют имеющиеся 32 пина P0.0-P0.31.

Работу порта регулируют несколько регистров, через которые:
а) Устанавливается направление пина (вход или выход).
б) Пин подтягивается к питанию или опускается к земле, если есть такая необходимость.
в) Обеспечивается запись логических 1 или 0 на пин, настроенный как выход.
г) Считывается текущее логическое состояние пина.

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

Настроим выводы LED_1-LED_4 МК нашего устройства как выходы и оформим код управления светодиодами, для чего создадим файлы leds.h и leds.c в папках inc и src, соответственно.

Содержимое заголовочных файлов будет иметь следующий вид:
ATmega8
#ifndef LEDS_H_
#define LEDS_H_

#include <avr/io.h>

/* Макроопределения и макросы */
// запись на пин PD6 логического 0 
#define LED_1_ON PORTD &= ~(1 << PD6)
// запись на пин PD6 логической 1 
#define LED_1_OFF PORTD |= (1 << PD6)
// изменение состояния пина PD6 на противоположное 
#define LED_1_TOGGLE PORTD ^= (1 << PD6)

// запись на пин PD7 логического 0 
#define LED_2_ON PORTD &= ~(1 << PD7)
// запись на пин PD7 логической 1 
#define LED_2_OFF PORTD |= (1 << PD7)
// изменение состояния пина PD7 на противоположное 
#define LED_2_TOGGLE PORTD ^= (1 << PD7)

// запись на пин PB1 логического 0 
#define LED_3_ON PORTB &= ~(1 << PB1)
// запись на пин PB1 логической 1 
#define LED_3_OFF PORTB |= (1 << PB1)
// изменение состояния пина PB1 на противоположное 
#define LED_3_TOGGLE PORTB ^= (1 << PB1)

// запись на пин PB2 логического 0 
#define LED_4_ON PORTB &= ~(1 << PB2)
// запись на пин PB2 логической 1 
#define LED_4_OFF PORTB |= (1 << PB2)
// изменение состояния пина PB2 на противоположное 
#define LED_4_TOGGLE PORTB ^= (1 << PB2)

/* Прототип функции инициализации пинов */
void ledsInit();

#endif

STM32F401
#ifndef LEDS_H_
#define LEDS_H_

#include "stm32f4xx.h"

/* Макроопределения и макросы */
// запись на пин PC13 логического 0 
#define LED_1_ON GPIOC->ODR &= ~GPIO_ODR_ODR_13
// запись на пин PC13 логической 1 
#define LED_1_OFF GPIOC->ODR |= GPIO_ODR_ODR_13
// изменение состояния пина PC13 на противоположное 
#define LED_1_TOGGLE GPIOC->ODR ^= GPIO_ODR_ODR_13

// запись на пин PC14 логического 0 
#define LED_2_ON GPIOC->ODR &= ~GPIO_ODR_ODR_14
// запись на пин PC14 логической 1 
#define LED_2_OFF GPIOC->ODR |= GPIO_ODR_ODR_14
// изменение состояния пина PC13 на противоположное 
#define LED_2_TOGGLE GPIOC->ODR ^= GPIO_ODR_ODR_14

// запись на пин PB1 логического 0 
#define LED_3_ON GPIOB->ODR &= ~GPIO_ODR_ODR_1
// запись на пин PB1 логической 1 
#define LED_3_OFF GPIOB->ODR |= GPIO_ODR_ODR_1
// изменение состояния пина PB1 на противоположное 
#define LED_3_TOGGLE GPIOB->ODR ^= GPIO_ODR_ODR_1

// запись на пин PB2 логического 0 
#define LED_4_ON GPIOB->ODR &= ~GPIO_ODR_ODR_2
// запись на пин PB2 логической 1 
#define LED_4_OFF GPIOB->ODR |= GPIO_ODR_ODR_2
// изменение состояния пина PB2 на противоположное 
#define LED_4_TOGGLE GPIOB->ODR ^= GPIO_ODR_ODR_2

/* Прототип функции инициализации пинов */
void ledsInit();

#endif

nRF52832
#ifndef LEDS_H_
#define LEDS_H_

#include "nrf.h"

/* Макроопределения и макросы */
// запись на пин P0.17 логического 0 
#define LED_1_ON NRF_GPIO->OUT &= ~(GPIO_OUT_PIN17_High << GPIO_OUT_PIN17_Pos)
// запись на пин P0.17 логической 1 
#define LED_1_OFF NRF_GPIO->OUT |= (GPIO_OUT_PIN17_High << GPIO_OUT_PIN17_Pos)
// изменение состояния пина P0.17 на противоположное 
#define LED_1_TOGGLE NRF_GPIO->OUT ^= (GPIO_OUT_PIN17_High << GPIO_OUT_PIN17_Pos)

// запись на пин P0.18 логического 0 
#define LED_2_ON NRF_GPIO->OUT &= ~(GPIO_OUT_PIN18_High << GPIO_OUT_PIN18_Pos)
// запись на пин P0.18 логической 1 
#define LED_2_OFF NRF_GPIO->OUT |= (GPIO_OUT_PIN18_High << GPIO_OUT_PIN18_Pos)
// изменение состояния пина P0.18 на противоположное 
#define LED_2_TOGGLE NRF_GPIO->OUT ^= (GPIO_OUT_PIN18_High << GPIO_OUT_PIN18_Pos)

// запись на пин P0.15 логического 0 
#define LED_3_ON NRF_GPIO->OUT &= ~(GPIO_OUT_PIN15_High << GPIO_OUT_PIN15_Pos)
// запись на пин P0.15 логической 1 
#define LED_3_OFF NRF_GPIO->OUT |= (GPIO_OUT_PIN15_High << GPIO_OUT_PIN15_Pos)
// изменение состояния пина P0.15 на противоположное 
#define LED_3_TOGGLE NRF_GPIO->OUT ^= (GPIO_OUT_PIN15_High << GPIO_OUT_PIN15_Pos)

// запись на пин P0.16 логического 0 
#define LED_4_ON NRF_GPIO->OUT &= ~(GPIO_OUT_PIN16_High << GPIO_OUT_PIN16_Pos)
// запись на пин P0.16 логической 1 
#define LED_4_OFF NRF_GPIO->OUT |= (GPIO_OUT_PIN16_High << GPIO_OUT_PIN16_Pos)
// изменение состояния пина P0.16 на противоположное 
#define LED_4_TOGGLE NRF_GPIO->OUT ^= (GPIO_OUT_PIN16_High << GPIO_OUT_PIN16_Pos)

/* Прототип функции инициализации пинов */
void ledsInit();

#endif

Как видите:
1. Каждый хэдер-файл начинает и завершает конструкция директив #ifndef-#define-#endif, призванная в больших проектах исключить многократное включение одного и того же заголовочного файла и, как следствие, ошибку переопределения.
2. Из main.c в leds.h перенесено подключение заголовочного файла (avr/io.h, stm32f4xx.h, nrf.h), поскольку теперь адресами регистров и макроопределениями их битов будет оперировать библиотека leds, а основная функция — использовать лишь макросы и функции библиотеки. Строго говоря, у main.c останется доступ к указанному файлу, но опосредованно, через хэдер-файл библиотеки.
3. Включение, выключение переключение светодиодов реализованы через макросы, хотя можно было прописать их в виде функций. Однако, в случаях, когда фрагмент кода состоит из одной строки, макрос предпочтительнее, т.к. его исполнение занимает у процессора меньше времени, в связи с отсутствием инструкций перехода и возврата.
4. Из-за того, что светодиоды в нашем устройстве — активно-низкие, в макросах применена обратная логика, т.е. для включения светодиода соответствующий пин переводится в состояние 0, а для выключения — в 1.

C-файл библиотеки будет содержать единственную функцию, инициализирующую пины как выходы.
ATmega8
#include "leds.h"

/* Функция инициализации пинов */
void ledsInit()
{ 
 // Настроить пины PD7 и PD6 как выходы 
 DDRD |= (1 << PD7) | (1 << PD6);
 // Настроить пины PB2 и PB1 как выходы 
 DDRB |= (1 << PB2) | (1 << PB1);
 // Выключить светодиоды
 LED_1_OFF;
 LED_2_OFF;
 LED_3_OFF;
 LED_4_OFF;
}

STM32F401
#include "leds.h"

/* Функция инициализации пинов */
void ledsInit()
{ 
 // Включить тактирование порта B
 RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
 // Настроить пины PB2 и PB1 как выходы
 GPIOB->MODER |= GPIO_MODER_MODER2_0 | GPIO_MODER_MODER1_0;

 // Включить тактирование порта C
 RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN;
 // Настроить пины PC14 и PC13 как выходы
 GPIOC->MODER |= GPIO_MODER_MODER14_0 | GPIO_MODER_MODER13_0;

 // Выключить светодиоды
 LED_1_OFF;
 LED_2_OFF;
 LED_3_OFF;
 LED_4_OFF; 
}

nRF52832
#include "leds.h"

/* Функция инициализации пинов */
void ledsInit()
{ 
 // Настроить пины P0.15-P0.18 как выходы
 NRF_GPIO->PIN_CNF[15] |= (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos);
 NRF_GPIO->PIN_CNF[16] |= (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos);
 NRF_GPIO->PIN_CNF[17] |= (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos);
 NRF_GPIO->PIN_CNF[18] |= (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos); 

 // Выключить светодиоды
 LED_1_OFF;
 LED_2_OFF;
 LED_3_OFF;
 LED_4_OFF; 
}

Отмечу, что:
а) Первой строкой c-файла библиотеки всегда подключается парный хэдер-файл.
б) При наборе имён модулей (например, GPIOB для STM32F401 и NRF_GPIO для nRF52832), регистров (в частности, DDRB для ATmega8, MODER для STM32F401, PIN_CNF для nRF52832), масок и битов всплывает список подсказки, что облегчает и ускоряет процесс. Кроме того, указанные имена очень хорошо коррелируют с таковыми в технической документации, так что вы быстро разберётесь с этими страшным символам и привыкнете к ним.
в) Опять-таки, по причине активно-низких светодиодов функция инициализации содержит строки их выключения, т.к. дефолтный логический уровень сигнала на пине, настроенном как выход — 0, что, как вы помните, включает светодиод.

Теперь, в main.c нужно подключить библиотеку и можно помигать светодиодом LED_3, к примеру:
#include “leds.h”

void SystemInit() // для STM32F401 и nRF52832
{
 
}

int main()
{
 ledsInit();

 while(1)
 {
 LED_3_TOGGLE;
 }
}

Однако, после компиляции программы и загрузки её в МК вы вряд ли увидите блинк без осциллографа, поскольку текущая частота тактирования высока (8 МГц — для ATmega8, 16 МГц — для STM32F401, 64 МГц — для nRF52832), а в цикле while() основной функции — лишь одна строка кода, которая и исполняется с соответствующей периодичностью.

Учитывая изложенное, привяжем включение/выключение светодиодов LED_1 — LED_3 к состоянию кнопок по следующему алгоритму:
Если нажата только кнопка BUTTON_1, включить светодиод LED_1, а остальные выключить
Если же нажата только кнопка BUTTON_2, включить светодиод LED_2, а остальные выключить
Если же нажаты обе кнопки, включить светодиод LED_3, а остальные выключить
Во всех остальных случаях выключить все светодиоды

Как видите, алгоритм предусматривает ветвление программы, для организации которого применяют конструкцию if-else if-else, имеющую, в общем случае, следующий синтаксис:
if(выражение 1) /* ветка 1 */
 код, исполняемый при истинности выражения 1
else if(выражение 2) /* ветка 2 */
 код, исполняемый при истинности выражения 2
...
else if(выражение n-1) /* ветка n-1 */
 код, исполняемый при истинности выражения n-1
else // /* ветка n */
 код, исполняемый когда выражения все предыдущие выражения — ложны

При этом:
а) Инструкция if может использоваться и самостоятельно.
б) Выражение формируется посредством операторов отношения или сравнения на равенство:
> — больше,
>= — больше или равно,
< — меньше,
<= — меньше или равно,
== — равно,
!= — не равно.
в) Если выражение содержит несколько условий, их комбинируют с помощью логических операторов:
&& — для случая, когда должны выполняться все условия,
|| — для случая, когда должно выполняться хотя бы одно из условий,
г) Если код ветки состоит более, чем из одной строки, его заключают в фигурные скобки.

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

Создадим библиотеку buttons и пропишем в её с-файле функцию инициализации пинов подключения кнопок:
ATmega8
#include "buttons.h"

void buttonsInit()
{
 // Настроить пины PD3 и PD2 как входы
 DDRD &= ~((1 << PD3) | (1 << PD2));
 // и подтянуть к питанию
 PORTD |= (1 << PD3) | (1 << PD2);
}

STM32F401
#include "buttons.h"

void buttonsInit()
{
 // Включить тактирование порта A
 RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
 // Настроить пины PA10 и PA9 как входы
 GPIOA->MODER &= ~(GPIO_MODER_MODER10 | GPIO_MODER_MODER9);
 // и подтянуть к питанию
 GPIOA->PUPDR |= GPIO_PUPDR_PUPDR10_0 | GPIO_PUPDR_PUPDR9_0;
}

nRF52832
#include "buttons.h"

void buttonsInit()
{
 // Настроить как вход и подтянуть к питанию пин P0.13
 NRF_GPIO->PIN_CNF[13] = (GPIO_PIN_CNF_DIR_Input << GPIO_PIN_CNF_DIR_Pos) | (GPIO_PIN_CNF_PULL_Pullup << GPIO_PIN_CNF_PULL_Pos);
 // Настроить как вход и подтянуть к питанию пин P0.14
 NRF_GPIO->PIN_CNF[14] = (GPIO_PIN_CNF_DIR_Input << GPIO_PIN_CNF_DIR_Pos) | (GPIO_PIN_CNF_PULL_Pullup << GPIO_PIN_CNF_PULL_Pos);
}

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

Текущее состояние пинов портов ввода/вывода отражают регистры:
PINB, PINC и PIND — для ATmega8,
IDR модуля GPIO — для STM32F401,
IN модуля GPIO — для nRF52832.

Чтобы знать, нажата или отпущена та или иная кнопка в данный момент, оформим в хэдер-файлах библиотеки макросы посредством упоминавшихся выше операторов.
ATmega8
#ifndef BUTTONS_H_
#define BUTTONS_H_

#include <avr/io.h>

#define BUTTON_1_PUSHED (PIND & (1 << PD2)) == 0
#define BUTTON_1_RELEASED (PIND & (1 << PD2)) != 0
#define BUTTON_2_PUSHED (PIND & (1 << PD3)) == 0
#define BUTTON_2_RELEASED (PIND & (1 << PD2)) != 0

/* Прототип функции инициализации пинов */
void buttonsInit();

#endif

STM32F401
#ifndef BUTTONS_H_
#define BUTTONS_H_

#include "stm32f4xx.h"

#define BUTTON_1_PUSHED (GPIOA->IDR & GPIO_IDR_IDR_9) == 0
#define BUTTON_1_RELEASED (GPIOA->IDR & GPIO_IDR_IDR_9) != 0
#define BUTTON_2_PUSHED (GPIOA->IDR & GPIO_IDR_IDR_10) == 0
#define BUTTON_2_RELEASED (GPIOA->IDR & GPIO_IDR_IDR_10) != 0

/* Прототип функции инициализации пинов */
void buttonsInit();

#endif

nRF52832
#ifndef BUTTONS_H_
#define BUTTONS_H_

#include "nrf.h"

#define BUTTON_1_PUSHED (NRF_GPIO->IN & (GPIO_IN_PIN14_High << GPIO_IN_PIN14_Pos)) == 0
#define BUTTON_1_RELEASED (NRF_GPIO->IN & (GPIO_IN_PIN14_High << GPIO_IN_PIN14_Pos)) != 0
#define BUTTON_2_PUSHED (NRF_GPIO->IN & (GPIO_IN_PIN13_High << GPIO_IN_PIN13_Pos)) == 0
#define BUTTON_2_RELEASED (NRF_GPIO->IN & (GPIO_IN_PIN13_High << GPIO_IN_PIN13_Pos)) != 0

/* Прототип функции инициализации пинов */
void buttonsInit();

#endif

Осталось, следуя алгоритму, прописать код main.c:
#include "leds.h"
#include "buttons.h"

void SystemInit() // для STM32F401 и nRF52832
{
 
}

int main()
{
 ledsInit();
 buttonsInit();

 while(1)
 {
 // Если нажата только кнопка BUTTON_1, 
 if(BUTTON_1_PUSHED && BUTTON_2_RELEASED)
 {
 // включить светодиод LED_1, а остальные выключить 
 LED_1_ON; 
 LED_2_OFF; 
 LED_3_OFF; 
 }
 // Если же нажата только кнопка BUTTON_2, 
 else if(BUTTON_1_RELEASED && BUTTON_2_PUSHED)
 {
 // включить светодиод LED_2, а остальные выключить
 LED_1_OFF; 
 LED_2_ON; 
 LED_3_OFF; 
 }
 // Если же нажаты обе кнопки, 
 else if(BUTTON_1_PUSHED && BUTTON_2_PUSHED)
 {
 // включить светодиод LED_3, а остальные выключить
 LED_1_OFF; 
 LED_2_OFF; 
 LED_3_ON; 
 } 
 // Во всех остальных случаях выключить все светодиоды
 else 
 {
 LED_1_OFF; 
 LED_2_OFF; 
 LED_3_OFF; 
 } 
 }
}


Файлы

🎁Папки проектов и коды в zip  82.4 Kb ⇣ 27

Ссылки

Atmega8a даташиты, мануалы
Cortex M4 даташиты, мануалы
nRF52832 даташиты, мануалы
STM32F401 даташиты, мануалы
Подборка книг по языкам, ОС, алгоритмам, компиляторам и т.п. Включая «Язык программирования Си» Кернигана и Ритчи.

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

Камрад, рассмотри датагорские рекомендации

🌼 Полезные и проверенные железяки, можно брать

Опробовано в лаборатории редакции или читателями.




 

Читательское голосование

Нравится

Статью одобрили 13 читателей.

Для участия в голосовании зарегистрируйтесь и войдите на сайт с вашими логином и паролем.
 

Поделись с друзьями!

 

 

Связанные материалы

 

Схема на Датагоре. Новая статья Программирование микроконтроллеров в AtmelStudio 6. Часть 1. Первые шаги... Современное радиолюбительство невозможно представить без микроконтроллеров, и это очевидно. В...
Схема на Датагоре. Новая статья Программирование микроконтроллеров в AtmelStudio 6. Часть 2. Одна программа на разных языках.... Для радиолюбителей, которые до определенного времени не использовали микроконтроллеры в своих...
Схема на Датагоре. Новая статья Программирование на языке С для AVR и PIC микроконтроллеров. Шпак Ю.А.... Программирование на языке С для AVR и PIC микроконтроллеров. Шпак Ю.А. Издательство "МК -...
Схема на Датагоре. Новая статья Микроконтроллеры AVR семейств Tiny и Mega фирмы ATMEL, Евстифеев А.В.... Издательство: Додэка XXI [М.], 560 стр. 2005 г. Книга посвящена вопросам практического применения...
Схема на Датагоре. Новая статья Программирование микроконтроллеров на языке C. Часть 1... Приветствую всех жителей и гостей Датагор.ру! Года полтора назад мы с Радиком Галимовым, более...
Схема на Датагоре. Новая статья Программа обмена по RS-232 на языке C# в среде Microsoft Visual Studio... Сограждане, применяющие микроконтроллеры, часто испытывают потребность использования функций...
Схема на Датагоре. Новая статья Программирование микроконтроллеров ATmega... Хеллоу ВСЕМ!!! Итак, как я и обещал, расскажу как залить программу в МК. Для этого нам понадобится...
Схема на Датагоре. Новая статья Програмирование в AVR Studio 5 с самого начала. Часть 1... Каждый человек, который только начинает осваивать программирование микроконтроллеров, да и вообще...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 1. Начало пути... Приветствую всех сограждан и читателей журнала Датагор! Пользуясь кучей времени, предоставленной...
Схема на Датагоре. Новая статья Блок питания с защитой + микроконтроллер ATMEGA16, ATMEGA8535, PIC16F877. Часть первая, лирическая... Вниманию сограждан Датагорода предлагаю мой вариант лабораторного блока питания с...
Схема на Датагоре. Новая статья Аудио ЦАП DAC. Поделки начинающего цапостроителя. Часть 18:1. Рулим ЦАП AK4399 с помощью Arduino... Я долго колебался – писать эту статью или нет. Это решение нужно было принять — основываясь на...
Схема на Датагоре. Новая статья Грызём микроконтроллеры. Урок 1. Моргаем 8-ю светодиодами. CodeVision, Proteus, ISIS... Эту статью (а точнее цикл статей) я решил полностью посвятить микроконтроллерам фирмы Atmel....
 

Комментарии, вопросы, ответы, дополнения, отзывы

 

Добавить комментарий, вопрос, отзыв 💬

Камрады, будьте дружелюбны, соблюдайте правила!

  • Смайлы и люди
    Животные и природа
    Еда и напитки
    Активность
    Путешествия и места
    Предметы
    Символы
    Флаги
 
 
В начало | Зарегистрироваться | Заказать наши киты почтой