Часть 1
Часть 2
Часть 3
Часть 4.1

Пролог


Мнения были разные по поводу разбора кода и его необходимости вообще. Я постарался в данной статье реализовать метод «золотого сечения», поэтому:
а) в конце статьи будет приложен исходник экспертам дальше не читать
б) приведу алгоритм работы и разберу его
в) объясню как пользоваться библиотеками SPL
г) в объеме статьи расскажу как пользоваться определенной периферией, покажу реализацию работы с ней в коде
д) отдельным пунктом опишу работу с ILI9341, т.к. тема довольно разжевана, то просто расскажу о главном — как обдумано реализовать функцию инициализации (в интернете видел лишь код с фразой: «вот рабочая инициализация, копируйте и не вдумывайтесь что это») и запустить его через аппаратный SPI.

Слишком подробный разбор кода вы тут не увидите, все будет в меру, иначе мне придется написать книгу страниц так в 200-250. Поэтому изучайте даташиты и прочую документацию (ссылки будут) перед тем, как приступать к написанию программы. Те, кто первый раз сядет за МК — не бойтесь, если возникнут вопросы я вам подскажу и помогу, так что данный код вы осилите.

Какую среду разработки и можно ли облегчить жизнь библиотеками?


Скажу сразукод в данной статье не «родной», но опробован и работает! Т.к. IAR не самый удобный софт для новичков, да и как-то его многие обходят стороной из-за слишком большого функционала или еще каких-то причин, то было принято решение для учебной статьи перенести проект в «кокос» (CooCoxIDE 1.7.7). Так же я решил отойти от регистров и написать код на SPL.

SPL — это стандартные библиотеки от ST для работы с периферией МК STM32. Сейчас на замену вышли новые библиотеки HAL, но пока они не вытеснили первые во многих проектах. Плюсы и достоинства можете вычитать в гугле.
Я выбрал SPL — т.к. мне больше нравится конечный код после «жесткой оптимизации», они интуитивно понятнее, HAL более приспособлены для работы с различными RTOS. Это сугубо мое мнение, я не прошу никого принимать это на веру и рассказывать в комментариях о том, что лучше или хуже, не стоит.

Как я выше писал — был переход с регистров на библиотеки, в чем разница:

Плюсы:
а) читаемость кода написанного с использованием SPL просто отличная, а в статьях с уклоном под обучение это самое главное;
б) скорость написания кода намного выше, мне потребовалось 1,5 часа чтобы под имеющийся алгоритм написать код с нуля и отладить его;
в) более легкий переезд с камня на камень, ну вдруг у вас нету STM32F103RBT6, а есть STM32F105VCT6 и вы на нем будете собирать.

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

Мой итог:
Можно смело работать в CoIDE и писать программы с использованием SPL ровно до уровня пока вы считаете себя любителем. Как только у вас появляется цель проектировать серьезные устройства или начать зарабатывать деньги электроникой — тут же пересаживайтесь на IAR и курите даташиты с их регистрами.

Составляем алгоритм нашей программы


Для начала прикинем хрен к носу общую функциональную структуру устройства:


Рисунок 1 — Блок-схема для программы управления

Теперь мы видимо что должна измерить наша часть устройства и как поступить с полученными данными. Все измеренные напряжения и ток вывести на TFT панель, определить попадает ли напряжение в нормальный диапазон (согласно ГОСТу), а в конце перемножить выходной ток с выходным напряжением и получить мощность потребления нагрузкой. Вроде бы все просто и понятно.
Так же наша плата должна измерить температуру, вывести ее на экран, в случае превышения аварийных 85 оС должна отключить устройство путем подачи лог. 1 на ноги SD драйвера IR2110 в силовых платах. А так же по результатам измеренной температуры должно регулироваться значение скважности ШИМа, управляющего куллерами.
Все аварийные состояния должны так же отображаться на светодиодной индикации, которая служит для отображения «серьезных вещей» неполадок в системе или выход из строя устройства.
Функционал данного блока не сложный, а значит большого труда в реализации его не возникнет.

Что необходимо сделать перед тем, как продолжить изучать статью?


1) Скачать софт с которым мы будем работать. Это будет CooCoxIDE 1.7.7, ниже я приложил ссылочку на свой ЯД, где вы можете скачать папку с данной версией программы, уже с установленными в ней SPL, а так же с уже скаченным компилятором чтобы не мучились и не искали. Все для старта работы там есть:
CooCoxIDE 1.7.7

2) Скачать священный манускрип под названием даташит на STM32F103:
Datasheet STM32F103

3) Скачать не менее священное писание под названием референс мануал, который гуглится как RM0008 и имеет эпичный объем в 1128 страниц. Это самое объемное научное писание, которое я осилил, правда были еще все тома Ландау-Лившица, но там несколько книг все таки… Я к чему — советую обращаться к данному файлу при любых возникающих непонятнках и курьезах.
RM0008

4) Еще очень настоятельно советую блог одного паренька с уровнем знаний по ST — Бог, у него там разжевана переферия для новичков и достаточно сложные и интересные задачи:
Николай Горбунов

5) И последнее… бежим скачивать очень полезную программку STM32CubeMXОфициальный сайт ST

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

Собираем проект


1) Выбираем переферию с которой нам предстоит работать


Рисунок 2 — Репозиторий для выбора переферии, которую будем использовать

Теперь вкратце кто и за что отвечает:

а) CMSIS core — первым делом ставим галочку именно на эту библиотеку, сразу подключится еще несколько библиотек. Это минимум с которым можно работать. Данная библиотека как следует из ее названия подключает «ядро», базовые функции через которые будет работать остальная периферия

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

в) GPIO — библиотека работы с портами ввода/вывода. Так же она позволяет провести все первичные настройки для остальной периферии.

г) DMA — эта библиотека позволяет работать с прямым доступом к памяти. Очень подробно это можно прочитать в интернете — тема разжевана, для новичков пока достаточно понять, что данный принцип позволяет повысить скорость работы всего устройства.

д) SPI — библиотека для работы с интерфейсом SPI, позволяющий обмениваться данными с кучей устройств. В нашем случае через него мы общаемся с TFT экраном на ILI9341.

е) TIM — библиотека для работы с таймера. Тут думаю все понятно, она позволит нам запустить ШИМ для управления куллерами и конечно же для реализации задержек и генерации прерываний.

ж) ADC — библиотека для работы с нашей главной периферией аналогово-цифровым преобразователем (АЦП), которая измеряет все наши напряжения и ток.

Переходим к написанию программы


Я не собирался давать уроки по С, но так как я ориентируюсь на аудиторию людей, которые боялись взяться за изучение МК и мне хочется их подтолкнуть, то основные моменты буду описывать. Первые 2 команды:

1) define — это команда, которая служит для подстановки одного значения вместо другого. Половина тут не поняли, как и я, когда изучал)

Пример 1:

У нас программа, которая зажигает светодиод командой GPIO_SetBits(GPIOA, GPIO_Pin_1); и выключает его командой GPIO_ResetBits(GPIOA, GPIO_Pin_1);. Сейчас вам не надо задумываться над этими командами. Как видите команда достаточно сложная и в процессе написания кода вам не хочется ее 100 раз переписывать, и тем более не хочется вспоминать каждый раз как включать/выключать светодиод. Поэтому мы поступаем следующим образом, пишем:

#define LED_ON        GPIO_SetBits(GPIOA, GPIO_Pin_1)
#define LED_OFF       GPIO_ResetBits(GPIOA, GPIO_Pin_1)


Что мы сделали? А упростили себе жизнь. Теперь каждый раз, когда мы будем писать в тексте программы LED_ON; (точка с запятой обязательна), то будет происходить замена данной фразы на команду GPIO_SetBits(GPIOA, GPIO_Pin_1); Тоже самое с выключением.

Пример 2:

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

#define Year   365


Стоит заметить, что так как 365 просто константа, а не команда, то после нее не надо ставить точку с запятой. Иначе будет вставлять не 365, а 365; и при использование в той же формуле будет воспринято как ошибка.

Команда #define X1 Y2 просто выполняет в коде замену всех X1 на Y2.

Надеюсь тут вопросов не осталось и переходим к более простой, но пожалуй самой важной команде:

2) include — она позволяет нам прикрепить к нашему коду другие файлы и библиотеки.

Пример 1:

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

#include "stm32f10x_conf.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_spi.h"

#include "TFT ILI9341.h"



Чтобы понять откуда мы взяли это непонятные файл надо посмотреть в дерево проекта:

Рисунок 3 — Дерево проекта с необходимыми библиотеками

Как видим все, что мы прикрепили есть в этом «дереве». Это библиотека CMSIS и прочие. Тут стоит обратить внимание на четыре момента:

а) думаю момент с подключение понятен, но уточню — #include «имя файла» Именно так надо включать файл, указывая его имя в скобках и не надо ставить точку с запятой в конце строки.

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

в) есть 2 файла при использование библиотек SPL, которые подключаются всегда в main.

#include "stm32f10x_conf.h"
#include "stm32f10x.h"


г) как видно файл font.h я не подключил в основной файл main, т.к. он у меня подключен в файле библиотеки TFT ILI9341.h Почему так? FONT — библиотека с шрифтами и используется только в функциях работы с TFT панелью. Чтобы не загромождать основной файл main я прикрепил шрифты с помощью #include уже внутри файла TFT ILI9341.h.

3) Отличия файлов с разришениями .h и .c

Любая приличная библиотека состоит двух файлов, один из них имеет разрешение .c В этом файле собраны все функции и их реализации. Файл с данным расширением является основной для файла .h и поэтому прикрепляется внутри последнего.
Вторая часть библиотеки файл .h содержит как раз все необходимые инклуды, как например FONT для TFT ILI9341. Так же в ней описаны все define, объявлены константы. Для работы с данной библиотекой, как вы видели выше — мы подключаем именно этот файл, а файл .c идет «хвостом» внутри .h

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




Тактирование микроконтроллеров STM32



Приступаем к самому ответственному и хитрому пункту. Тактирование STMок происходит не как у AVR и пиков. Там просто берется частота кварца и гонится в камень.

В STM32 делается так:

а) МК всегда запускается от HSI — это внутренний генератор на 8 МГц
б) Потом МК переходит на HSE — это внешний кварцевый резонатор
в) Далее сигнал идет на PLL, где частота умножается и идет на периферию.

Именно из-за последнего пункта, когда я переходил на STM-ки у меня возник ступор: кварц я подключил на 8 МГц, а работает все на 72 МГц. Поэтому типичный кварцы это 8, 16, 24 МГц. Дальше частота умножается внутри микроконтроллера.
Все это можно увидеть на следующей схеме из даташита, находится она на странице 14. Именно поэтому я включил данный манускрип в обязательные)


Рисунок 4 — Схема тактирования периферии микроконтроллера STM32

Еще прошу обратить внимание, что когда тактовая частота берется с PLL (множителя частоты) и потом распределяется по периферии и устанавливается с помощью делителя. Есть две шины: APB2 и APB1, на каждой висит определенная периферия. У каждой шины есть ограничение по частоте: 72 МГц у APB2 и 36 МГц у APB1, то есть на 1-й шине частота равна 1/2 от тактовой с PLL.

Приведу пример: у SPI1 питание идет с шины APB2 с максимальной частотой 72 МГц, а SPI2 висит на шине APB1 с частотой 36 МГц и из этого следует, что скорость SPI2 будет ниже. Это стоит учитывать!

Теперь обратимся к функции, которая выполняет все настройки шин. За тактирование отвечает библиотека RCC, поэтому функцию нужно искать в файле stm32f10x_rcc.h, который мы подключили к нашему проекту в самом начале.

void RCC_Configuration(void)
{
    
    RCC_DeInit();                                               // Выполняем сброс reset 
    RCC_HSEConfig(RCC_HSE_ON);                                  // Включаем тактирование HSE (от кварца) 
    HSEStartUpStatus = RCC_WaitForHSEStartUp();                 // Ждем пока частота кварца стабилизируется

    if (HSEStartUpStatus == SUCCESS)                            // Если все отлично, то переходим на кварц
    {
        
        RCC_HCLKConfig(RCC_SYSCLK_Div1);                        // Выставляет делитель в 1 (Div1), системная частота становится 72 МГц
        RCC_PCLK2Config(RCC_HCLK_Div1);                         // Запитываем APB2 от тактовой частоты PLL в 72 МГц
        RCC_PCLK1Config(RCC_HCLK_Div1);                         // Запитываем APB1 от тактовой частоты PLL в 72 МГц
        RCC_ADCCLKConfig(RCC_PCLK2_Div2);                       // Ставим делитель на 2 (Div2) и получаем частоту 36 МГц вместо 72 МГц
        RCC_PLLConfig(RCC_PLLSource_PREDIV1, RCC_PLLMul_9);     // Устанавливаем множитель 9, получаем 8 МГц * 9 = 72 МГц на PLL
        
        RCC_PLLCmd(ENABLE);                                     // Включаем PLL
        
        while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) {}  // Ждем пока PLL устаканится
        
        RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);              // Выбираем PLL как источник тактового сигнала

        
        while (RCC_GetSYSCLKSource() != 0x08) {}                // Ждем пока PLL установится как источник тактирования
    }
}


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

/*************************************** Тактирование процессора **********************************/

void RCC_Configuration(void);
ErrorStatus HSEStartUpStatus;
RCC_ClocksTypeDef RCC_Clocks;


void RCC_Configuration(void)
{
    
    RCC_DeInit();                                               // Выполняем сброс reset 
    RCC_HSEConfig(RCC_HSE_ON);                                  // Включаем тактирование HSE (от кварца) 
    HSEStartUpStatus = RCC_WaitForHSEStartUp();                 // Ждем пока частота кварца стабилизируется

    if (HSEStartUpStatus == SUCCESS)                            // Если все отлично, то переходим на кварц
    {
        
        RCC_HCLKConfig(RCC_SYSCLK_Div1);                        // Выставляет делитель в 1 (Div1) и системная частота становится 72 МГц
        RCC_PCLK2Config(RCC_HCLK_Div1);                         // Запитываем APB2 от тактовой частоты PLL в 72 МГц
        RCC_PCLK1Config(RCC_HCLK_Div1);                         // Запитываем APB1 от тактовой частоты PLL в 72 МГц
        RCC_ADCCLKConfig(RCC_PCLK2_Div2);                       // Ставим делитель на 2 (Div2) и получаем частоту 36 МГц вместо входящей 72 МГц
        RCC_PLLConfig(RCC_PLLSource_PREDIV1, RCC_PLLMul_9);     // Устанавливаем множитель частоты 9, получаем 8 МГц * 9 = 72 МГц на PLL
        
        RCC_PLLCmd(ENABLE);                                     // Включаем PLL
        
        while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) {}  // Ждем пока PLL устаканится
        
        RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);              // Выбираем PLL как источник тактового сигнала

        
        while (RCC_GetSYSCLKSource() != 0x08) {}                // Ждем пока PLL установится как источник тактирования
    }
}


Я думаю тактирования я более менее описал подробно, теперь необходимо разбирать периферию.

Работа с портами ввода/вывода


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

а) Светодиод №1 (LED1) подключен к PB12
б) Светодиод №2 (LED2) подключен к PB13
в) Светодиод №3 (LED3) подключен к PB14
г) Светодиод №4 (LED4) подключен к PB15
д) Светодиод №5 (LED5) подключен к PС6


Что это значит… У STM-ок есть порты, они имеют 16 выводов от 0 до 15. Правда есть исключения, некоторые порты не всегда имеют 16 ногу, а могут, например, лишь 4 или 5. Обозначение PB12 означает что это порт B и 12-й вывод. Теперь открываем скаченный ранее STM32CubeMX и смотрим где эти ноги находятся и удобно ли нам их будет разводить.

Рисунок 5 — Выбор расположения переферии в программе STM32CubeMX

Огромная прелесть STM в том, что их «гибкость» позволяет использовать любые ноги для ввода-вывода, а вся периферия может быть переведена на альтернативные ноги (запасной вариант), так называемый Remap. Все это позволяет очень качественно, быстро и удобно разводить плату, поэтому новичкам учиться пользоваться всем тем, что дают нам разработчики ST.

Теперь нам необходимо работать с индикацией, то есть зажигать светодиоды в определенных ситуациях. Теперь мы идем и смотрим как это делать, нам надо устанавливать наш вывод в лог.1, т.к. аноды подключены к МК, а катоды с «минусу» схемы. Для этого открываем файл stm32f10x_gpio.h и листаем вниз файла, там список всех доступных функций:

Рисунок 6 — Функции доступные при работе с GPIO

Там мы видим функции установки и сброса состояния вывода:
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);


Как работать с такими функциями: копируем ее, видимо что первая запись в скобках GPIO_TypeDef* GPIOx, как понятно вместо «х» необходимо указать порт, у нас это порты В и С, получим GPIOB. Тут вспоминаем функции из начала статьи, которые я просил особо не вкуривать, тут уже пора). Смотрим на вторую часть в скобках uint16_t GPIO_Pin, тут видим переменную GPIO_Pin, так же видим, что тип этой переменной беззнаковая величина размером 16 бит, то есть 216 или же 65536. Но нам столько не нужно, я думаю понятно, что тут нам необходимо указать номер вывода (пина) от 0 до 15. В итоге получим GPIO_Pin_12. Учитывая все это пишем код:
GPIO_SetBits(GPIOB, GPIO_Pin_12);            // Зажигаем светодиод № 1
GPIO_SetBits(GPIOB, GPIO_Pin_13);            // Зажигаем светодиод № 2
GPIO_SetBits(GPIOB, GPIO_Pin_14);            // Зажигаем светодиод № 3
GPIO_SetBits(GPIOB, GPIO_Pin_15);            // Зажигаем светодиод № 4
GPIO_SetBits(GPIOC, GPIO_Pin_6);             // Зажигаем светодиод № 5


Как видите каждый раз вспоминать к какой ноге мы подключили светодиод, писать лишние буквы, когда код хотя бы в 5 000 строк это уже ой как существенно)) Поэтому вспоминаем замечательное свойство команды #define и модифицируем наш код:
#define LED1_ON      GPIO_SetBits(GPIOB, GPIO_Pin_12)            // Зажигаем светодиод № 1
#define LED2_ON      GPIO_SetBits(GPIOB, GPIO_Pin_13)            // Зажигаем светодиод № 2
#define LED3_ON      GPIO_SetBits(GPIOB, GPIO_Pin_14)            // Зажигаем светодиод № 3
#define LED4_ON      GPIO_SetBits(GPIOB, GPIO_Pin_15)            // Зажигаем светодиод № 4
#define LED5_ON      GPIO_SetBits(GPIOC, GPIO_Pin_6)             // Зажигаем светодиод № 5


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

#define LED1_ON       GPIO_SetBits(GPIOB, GPIO_Pin_12)              // Зажигаем светодиод № 1
#define LED2_ON       GPIO_SetBits(GPIOB, GPIO_Pin_13)              // Зажигаем светодиод № 2
#define LED3_ON       GPIO_SetBits(GPIOB, GPIO_Pin_14)              // Зажигаем светодиод № 3
#define LED4_ON       GPIO_SetBits(GPIOB, GPIO_Pin_15)              // Зажигаем светодиод № 4
#define LED5_ON       GPIO_SetBits(GPIOC, GPIO_Pin_6)               // Зажигаем светодиод № 5
	
#define LED1_OFF      GPIO_ResetBits(GPIOB, GPIO_Pin_12)            // Гасим светодиод № 1
#define LED2_OFF      GPIO_ResetBits(GPIOB, GPIO_Pin_13)            // Гасим светодиод № 2
#define LED3_OFF      GPIO_ResetBits(GPIOB, GPIO_Pin_14)            // Гасим светодиод № 3
#define LED4_OFF      GPIO_ResetBits(GPIOB, GPIO_Pin_15)            // Гасим светодиод № 4
#define LED5_OFF      GPIO_ResetBits(GPIOC, GPIO_Pin_6)             // Гасим светодиод № 5

Теперь для простого зажигания светодиода достаточно написать LED1_ON; Чтобы его выключить пишем так же LED1_OFF;

Вроде все просто? А нет! Остался последний момент, которые надо было показать в начале, но он бы половину людей возможно отпугнул бы. Хотя он и простой, но работоспособность зависит именно от него — это инициализация периферии GPIO. Это необходимо, чтобы указать порту откуда брать ему тактовую частоту, на какой работать и в каком режиме. Все это делается в той же библиотеке stm32f10x_gpio.h, но теперь к ней необходимо еще и библиотеку тактирования stm32f10x_rcc.h подключить.
Перед тем как вообще что-то делать с любой периферией необходимо включить ее тактирование, делается это в stm32f10x_rcc.h, идем туда и смотрим какой функцией это сделать, их список так же в конце файла:

Рисунок 7 — Функции тактирования

Тут мы видим знакомые нам APB2 и APB1, это шины к которым подсоединены наши порты. В данном случае и С и В сидят на APB2, эту функция я выделил на скрине. Она простейшая: в первой части необходимо написать название периферии, во второй указать статус. Статуса может быть два: ENABLE (включено) и DISABLE. Хз почему, но принципиально писать большими буквами статус, иначе Кокос не подсветит текст.
Теперь необходимо откуда-то взять правильное название периферии. Поэтому идем в файл библиотеки stm32f10x_rcc.h и жмет Ctrl + F — это поиск по файлу, пишем в него RCC_APB2Periph и жмем Find. И несколько раз тыкаем Find пока не дойдем до такого списка, где указаны все состояния, которые может принимать данная надпись. Выбираем нужную периферию:

Рисунок 8 — Поиск значения функции по библиотеки

И так… как включить тактирование разобрались, получили мы такую строку, вернее 2 строки, т.к. используем два порта В и С:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);


Все, с библиотекой тактирования пока закончим и переходим к файлу stm32f10x_gpio.h Идем в конец и ищем функцию инициализации, обычно они называются Init, тут сплошное капитанство если знаешь английский хоть немного. Дальше копируем имя функции GPIO_Init и дальше по стандарту Ctrl + F:

Рисунок 9 — Вот так выглядит функция в списке

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

Рисунок 10 — Функция инициализации

так она выглядит и состоит из 3-х составляющих:
а) GPIO_Pin — тут указываем как вывод мы настраиваем
б) GPIO_Speed — тут указываем скорость/частоту максимальную на которой может работать нога контроллера
в) GPIO_Mode — устанавливаем режим работы ноги

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

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);                          // Включаем тактирование порта

GPIO_InitTypeDef led;                                                          // Даем имя нашей инициализации led

led.GPIO_Pin = (GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15) ;       // Указываем выводы которые подчинятся этой настройке
led.GPIO_Speed = GPIO_Speed_2MHz;                                              // Указываем максимальную частоту работы
led.GPIO_Mode = GPIO_Mode_Out_PP;                                              // Указываем тип настройки ног, тут выход push-pull
GPIO_Init(GPIOB, &led);                                                        // Запускаем инициализацию


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

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

#include "stm32f10x_conf.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_spi.h"

#include "TFT ILI9341.h"


#define LED1_ON       GPIO_SetBits(GPIOB, GPIO_Pin_12)              // Зажигаем светодиод № 1
#define LED2_ON       GPIO_SetBits(GPIOB, GPIO_Pin_13)              // Зажигаем светодиод № 2
#define LED3_ON       GPIO_SetBits(GPIOB, GPIO_Pin_14)              // Зажигаем светодиод № 3
#define LED4_ON       GPIO_SetBits(GPIOB, GPIO_Pin_15)              // Зажигаем светодиод № 4
#define LED5_ON       GPIO_SetBits(GPIOC, GPIO_Pin_6)               // Зажигаем светодиод № 5
          	
#define LED1_OFF      GPIO_ResetBits(GPIOB, GPIO_Pin_12)            // Гасим светодиод № 1
#define LED2_OFF      GPIO_ResetBits(GPIOB, GPIO_Pin_13)            // Гасим светодиод № 2
#define LED3_OFF      GPIO_ResetBits(GPIOB, GPIO_Pin_14)            // Гасим светодиод № 3
#define LED4_OFF      GPIO_ResetBits(GPIOB, GPIO_Pin_15)            // Гасим светодиод № 4
#define LED5_OFF      GPIO_ResetBits(GPIOC, GPIO_Pin_6)             // Гасим светодиод № 5



/****************************************************************************************************************/
/********************** Функции используемы для работы **********************************************************/
/****************************************************************************************************************/

/*************************************** Тактирование процессора **********************************/

void RCC_Configuration(void);
ErrorStatus HSEStartUpStatus;
RCC_ClocksTypeDef RCC_Clocks;


void RCC_Configuration(void)
{

    RCC_DeInit();                                               // Выполняем сброс reset
    RCC_HSEConfig(RCC_HSE_ON);                                  // Включаем тактирование HSE (от кварца)
    HSEStartUpStatus = RCC_WaitForHSEStartUp();                 // Ждем пока частота кварца стабилизируется

    if (HSEStartUpStatus == SUCCESS)                            // Если все отлично, то переходим на кварц
    {

        RCC_HCLKConfig(RCC_SYSCLK_Div1);                        // Выставляет делитель в 1 (Div1) и системная частота становится 72 МГц
        RCC_PCLK2Config(RCC_HCLK_Div1);                         // Запитываем APB2 от тактовой частоты PLL в 72 МГц
        RCC_PCLK1Config(RCC_HCLK_Div1);                         // Запитываем APB1 от тактовой частоты PLL в 72 МГц
        RCC_ADCCLKConfig(RCC_PCLK2_Div2);                       // Ставим делитель на 2 (Div2) и получаем частоту 36 МГц вместо входящей 72 МГц
        RCC_PLLConfig(RCC_PLLSource_PREDIV1, RCC_PLLMul_9);     // Устанавливаем множитель частоты 9, получаем 8 МГц * 9 = 72 МГц на PLL

        RCC_PLLCmd(ENABLE);                                     // Включаем PLL

        while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) {}  // Ждем пока PLL устаканится

        RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);              // Выбираем PLL как источник тактового сигнала


        while (RCC_GetSYSCLKSource() != 0x08) {}                // Ждем пока PLL установится как источник тактирования
    }
}



/************************** Основное тело программы ***********************************************/
/**************************************************************************************************/
/**************************************************************************************************/

int main(void)
{
	 RCC_GetClocksFreq (&RCC_Clocks);
     RCC_Configuration();
     RCC_GetClocksFreq (&RCC_Clocks);
     


/************* Настройка портов *****************************************/     

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);                          // Включаем тактирование порта

GPIO_InitTypeDef led;                                                          // Даем имя нашей инициализации led

led.GPIO_Pin = (GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15) ;       // Указываем выводы которые подчинятся этой настройке
led.GPIO_Speed = GPIO_Speed_2MHz;                                              // Указываем максимальную частоту работы
led.GPIO_Mode = GPIO_Mode_Out_PP;                                              // Указываем тип настройки ног, тут выход push-pull
GPIO_Init(GPIOB, &led);                                                        // Запускаем инициализацию
          	
         

	
	


	while(1)
	{
		
		LED1_ON;
		LED2_OFF;
		LED3_ON;
		LED4_ON;
		LED5_OFF;

	}

}


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

Эпилог


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

Статья 4.3 с окончанием программного кода выйдет до Нового года, части с силовой схемотехникой будут уже в следующем году. Правда небольшая плюшка все таки будет — до НГ я планирую опубликовать еще одну статью не посвященную данному циклу про ИБП, но все таки имеющая некоторое отношение в плане схемотехники.
Статья будет называться — «Изготавливаем импульсный лабораторный блок питания 0-60В и 20А». Прототип у меня готов, обкатан и готов показаться в свет.

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

Комментарии (32)


  1. farcaller
    27.12.2015 02:55

    Вроде и хорошо написано, доходчиво, с картинками, но код ваш — ужасен и некоторые идеи в нем тоже. Ну зачем вы «упрощаете» код препроцессором то? Есть же столько вариантов как этот код можно сломать!


    1. R4ABI
      27.12.2015 02:59

      Вот, началось) Какие идеи ужасны в инициализации и инверсии выходов? Конкретно показывайте что плохо и как сделали бы вы. Все остальное балабольство.

      P.S. В конце будет 2 варианта кода: по статье и мой оригинал, который обладает 70% функционалом от пром. версии.


      1. farcaller
        27.12.2015 03:13
        +2

        #define LED_ON GPIO_SetBits(GPIOA, GPIO_Pin_1);
        Не пишите так. Точка с запятой в конце делают это законченным выражением. Если очень надо завернуть код в #define — то напишите без. Но в разы лучше написать функцию.

        В контексте задачи, логичнее было бы объявить именованные константы с пинами LED — иначе очень сложно понять что на самом деле кроется за LED1_ON.

        Хз почему, но принципиально писать большими буквами статус
        потому что С регистро-зависимый язык?


        1. R4ABI
          28.12.2015 12:32

          Точка с запятой там лишняя, если глянуть код чуть дальше дефайнов (это видимо никто не сделал), то вызов осуществляется как LED1_ON; Точка с запятой осталась при переносе с SPL. Я даже больше скажу, такой вариант бы даже не скомпилил кокос.

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


  1. R4ABI
    27.12.2015 03:30

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

    Да, но IAR таки не обращает внимание на написания статуса, правда там есть отключение проверки синтаксиса, чтобы чепухой не отвлекало. За кокосом сидел пару раз, сейчас почему то внимание обратил, хотя таки компиляторы разные. Ну черт с ним, хочет с большой — написал)


    1. ncix
      27.12.2015 12:16

      >>Поэтому четкого ответа чем написание голой функции лучше дефайна я не увидел…

      Например, каждый раз используя этот define вы фактически дублируете кусок конечного кода. Это увеличивает объем программы, что критично в условиях весьма ограниченных ресурсов микроконтроллера.
      Во-вторых, это затрудняет отладку — не уверен что ваш отладчик покажет точку останова внутри макроса. Впрочем, с МК, он наверное вообще ничего не покажет без отладочной оснастки.


      1. R4ABI
        27.12.2015 14:19

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

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


        1. ncix
          27.12.2015 15:31
          +1

          Не так поняли. Обычно в таких случаях делают функцию, что-то типа

          void led_on(int led)
          {
          GPIO_SetBits(GPIOB, led);
          }

          Или класс
          class Led
          {
          public
          Led(gpio, pin);
          void on();
          void off();
          }

          Я понимаю что вы нашли для себя удобную конструкцию в языке и теперь хотите её применять везде. Конечно это ваше право. Но раз уж вы претендуете на учебный материал — зачем же учить новичков сразу неправильно?


          1. R4ABI
            27.12.2015 15:54

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

            Учебных материалов миллион и в каждом все у всех разное, т.к. в таких вопросах искать истину бесполезно. Читать и принимать за истину материал тоже не прошу) Кто считает полезным для себя — ознакомится и попробует, кто считает, что перерос и может сделать лучше — дай Бог! Я же только ЗА)

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


            1. ncix
              27.12.2015 22:49
              +1

              Я как-то на заре знакомства с С++ узнав про макросы тоже очень много чего с ними напридумывал. Я чувствовал что могу язык крутить любым боком, придумывать собственный синтаксис и писать на нём очень забавные вещи. Это была эйфория! Потом узнал про шаблоны (templates), — вместе с макросами от возможностей просто сносило крышу! Правда коллеги по-работе не сильно разделяли мой энтузиазм, просили в рабочий код такого не коммитить. Через какое-то время сам понял где кончается здравый смысл :)

              Кстати если интересно поглубже проникнуть в макропрограммирование и шаблоны — рекомендую почитать Александреску.


              1. R4ABI
                28.12.2015 01:49
                +1

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

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

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


    1. farcaller
      27.12.2015 12:30
      +2

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

      В смотрите на код как инженер-схемотехик, а я — как программист-прикладник. Так же как я могу собрать принципиальную схему на авось (ведь работает же!), так и я вижу в вашем коде вещи, от которых меня немного коробит.

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


      1. R4ABI
        27.12.2015 14:28
        +1

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

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

        3) Да, вы правы — я пытаюсь спрятать более «низкоуровневый код», но не просто спрятать самому, а научить новичков делать это самостоятельно. Это конечно не попытка выдать сие действо за истину, ну просто мне так удобно — открыв этот исходник через 20 лет, я за десять минут вспомню весь функционал и пойму как он работает без особого вникания в библиотеки и реализацию функций.


    1. GarryC
      27.12.2015 14:06

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


      1. R4ABI
        27.12.2015 14:32

        Согласен, в любом варианте будут недостатки, именно поэтому есть куча вариантов реализации. Ломать себя придется если взять такие «упрощения» из чужого кода и бездумно их использовать. Я же тут хочу людям объяснить как их реализовать самостоятельно, когда их можно использовать и когда лучше сделать отдельную библиотек. Когда человек сам это сделает, потом поймет, что это не всегда хорошо, то это будет повод «расти».
        Лентяям же лучше вообще не браться за электронику)


  1. solarplexus
    27.12.2015 19:17

    На работе использую CoIDE.
    Но, вот, бывали случаи, когда с обычной строчкой, наподобие «int x;» не компилилось, удаляешь ее, и все начинает работать. И таких мелких косяков накапливается просто горень! Хоть на IAR переходи!
    Кто-нибудь сталкивался с подобными проблемами?


    1. Mirn
      27.12.2015 19:29

      советую собрать свой eclipse+gcc,
      да если не знаком с настройкой эклипса и unix-way то это неделя, а если знаком то день.
      НО там на порядок больше настроек и не такая уродливая их реализация.
      Да и в целом среда поприятнее CoIDE и попродуманее именно для работы а не для скетчей ардуино.
      На работе и я и другие сами дошли до эклипса независимо.
      а IAR, там до сих пор IDE и редактор из 90ых (лет 7 не смотрел его)?


    1. R4ABI
      27.12.2015 21:05

      Поставил 1.7.7 пока не заметил багов каких либо, правда пользую активно только недельку


  1. solarplexus
    27.12.2015 19:32

    Т.е. своя сборка эклипса менее глючная?
    Все таки, можно ли утверждать, что CoIDE глючная?


    1. Mirn
      27.12.2015 19:36

      eclipse kepler + gcc использую
      не разу не глючила
      но испортить сборку, или проект легче неправильным использованием
      всё равно надо привыкать
      но каждый раз нарываясь на странности оказывается что виноват я а не эклипс =)
      один раз показалось что он завис и вылетел — но это я перемудрил с шаблоном и ещё с чем то — более миллиона ошибок съели всю память и винда аварйно завершила. Повторилось на чистом мейке без среды и без эклипса — терминал с ошибками взбесился.


  1. Dima_Sharihin
    27.12.2015 20:36

    Тот же STM32CubeMX позволяет давать выводам GPIO свои осознанные имена.
    И вместо макросов с LED_ON можно честно писать HAL_GPIO_WritePin(LD1_GPIO, LD1_PIN, PIN_ENABLE);


    1. R4ABI
      27.12.2015 21:14
      -1

      Использовать генератор кода — это хуже чем использовать ардуину. В таких людей камень кидать надо ИМХО.

      Кокос 1.7.7 последняя стабильная версия без особых багов, но там изначально стоит SPL. Тратить кучу времени чтобы впихнуть HAL — смысла нету. Ставить кокос 2.x — попробовал, пока еще сыровато. Да к HAL я вообще негативно отношусь, единственный момент когда к ним можно обратиться — использование какой нибудь RTOS.

      Зачем вместо банальщины LED1_ON; писать огромную команду, за которой придется каждый раз лазить? Если у кого-то есть куча времени и перфекционизм зашкаливает — может писать функцию. Я же пока не готов тратить кучу времени да обезьянью работу. Те, кто захочет кошерности кода вообще не будут использовать ни SPL, ни тем более HAL.


      1. Dima_Sharihin
        27.12.2015 21:28

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

        Мне после Linux все эти эмбеддедопроблемы кажутся терминальным маразмом (три модели драйверов для одной архитектуры процессора, три, карл!), но ничего не поделаешь, HAL позволяет не тратить часы в поисках подходящего прескалера, настроек таймеров и комбинаций пинов — любые сомнительные комбинации будут отметены еще на этапе проектирования.

        Использую Ac6 OpenSTM32 из под пингвина в связке с gcc.

        Не стал бы сравнивать STM32Cube и Ардуину. Ардуина скрывает реализацию в корне, корректнее ее сравнивать с mbed/nucleo. STM32Cube просто дает скелет будущего приложения, а все, что дальше, нужно писать точно так же (ну, почти), как и на голом железе


        1. R4ABI
          27.12.2015 22:45

          Если разок прочитать даташит и сделать 2-3 мелких проекта, то время на тактирование уходит 1-2 минуты)

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

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


          1. IronHead
            28.12.2015 10:05

            ST выпустило HAL после того, как подсмотрела эту штуку у Atmel, там она называется ASF. Но! У Атмела код, написанный на ASF можно переносить практически на любой микроконтроллер, начиная с atmega и даже некоторых attiny и заканчивая 32 битными sam. То есть там это реально работает, а у ST получился просто еще один вариант SPL.


            1. legiar
              28.12.2015 12:23

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

              Например, запуск SPI на F0 и F1 — через SPL будет совсем разный код, так как там и регистры разные, и названия функций тоже. А вот через HAL ничего менять не надо — подключил другой include и собирается, и, главное, работает.


              1. IronHead
                28.12.2015 12:29

                С STM32F103 на STM8S105 перенос будет работать?
                А вот на ASF один и тот же код и на ATMEGA и на SAM4S заработает.


                1. legiar
                  28.12.2015 13:19

                  Так как не видел ASF, поэтому не могу ничего об этом сказать. Спорить не буду, но вот не согласен с утверждением — «один и тот же код» — разрядность контроллеров разная, а про перефирию я вообще молчу.


  1. gorbln
    28.12.2015 11:46

    Банально, но вставлю свои пять копеек:
    Илья, спасибо за статью!
    Очень благодарен вам за подробное разъяснение не просто функций, которые вы используете (в примерах идущими обычно с капитанскими коментами вроде "// Эта функция зажигает светодиод"), а за разъяснение — откуда именно вы взяли эти идиотические конструкции типа «GPIO_Mode = GPIO_Mode_Out_PP». Мне это дело нереально выносило мозг, выкапывать эти названия из исходников и примеров.
    (отдельный луч ненависти отправляется разработчикам «кокоса» за нерабочие примеры. Примеры!!! Если уж это не работает, то я прям и не знаю...)


    1. R4ABI
      28.12.2015 12:36
      +1

      Во, как раз на разрешение таких вопросов и была нацелена статья. Именно они возникают, когда первый раз садишься за МК.
      Я некоторое время читал лекции по АСУ ТП и по факту тут просто попытка собрать и ответить на вопросы, которые мне задавали при писанине курсовых)
      Насколько помню у кокоса примеры только со светодиодами работают.


  1. ToSHiC
    29.12.2015 12:14

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


    1. R4ABI
      29.12.2015 13:10

      да, GCC. Можно и инлайн) У меня просто уже рефлекс развился в дефайнам в таких ситуациях. Кокос кстати и так перед компиляцией проверяет все динамические переменные и статику. Правда не во всех версиях, но в 1.7.6-1.7.8 есть точно.