STM32 — GPIO. Первая программа
Итак, я уже достаточно наигрался с STM32, чтобы потихоньку начать писать статьи про него. В этой статье я рассмотрю работу с GPIO (входы/выходы общего назначения). Это основа для работы со всей остальной периферией.
На моей отладочной плате установлен процессор STM32F103VET6 в корпусе LQFP100. Он имеет 80 GPIO выводов. Они имеют следующие обозначения:
- PA0..PA15;
- PB0..PB15;
- PC0..PC15;
- PD0..PD15;
- PE0..PE15.
Если вывод предполагается использовать как самостоятельный вход/выход, то можно использовать практически любой из имеющихся. Соответствие пинам микроконтроллера можно посмотреть в даташите:

Конечно это нам пригодится, если мы самостоятельно разводим плату под процессор. В нашем же случае все необходимые выводы выведены на контактные гребёнки отладочной платы.
Вообще, очень часто придётся обращаться к даташиту на микроконтроллер, так как там можно найти много полезной информации. Скачать его можно на официальном сайте STMicroelectronics.
Каждому выводу можно установить определённый режим работы. У ARM их аж 8:
Режим | Функция | Примечание |
---|---|---|
GPIO_Mode_AIN (Analog Input) | Вход | Аналоговый |
GPIO_Mode_IN_FLOATING (Input float) | Вход | Плавающий (без подтяжки) |
GPIO_Mode_IPD (Input Pull-down) | Вход | Подтяжка к земле |
GPIO_Mode_IPU (Input Pull-up) | Вход | Подтяжка к питанию |
GPIO_Mode_Out_OD (Output Open Drain) | Выход | С открытым стоком |
GPIO_Mode_Out_PP (Output Push-Pull) | Выход | Двухтактный |
GPIO_Mode_AF_OD (Alternate Function Open Drain) | Альтернативная функция | С открытым стоком |
GPIO_Mode_AF_PP (Alternate Function Push-Pull) | Альтернативная функция | Двухтактный |
Режим альтернативной функции позволяет использовать вывод для внутренней периферии (например, как USART вход/выход). Соответствие выводов функциям можно посмотреть в том же даташите, в таблице №5.

Первые 6 столбцов Pins показывают соответствие ногам микроконтроллера в разных корпусах.
- Pin name — название пина.
- Type — тип: I — вход, O — выход, S — питание.
- I/O Level — уровень сигнала: FT означает, что данный пин позволяет подавать на себя 5 вольт. Иначе — не более 3.3 вольт.
- Main function — главная функция, которая устанавливается на пине после включения/сброса.
- Alternate functions: default — альтернативная функция вывода.
- Alternate functions: remap — если по каким-то причинам вы не можете использовать функцию на её «родном» пине, то функцию можно «заремапить» на другой пин. В этом столбце как раз указывается дополнительная функция после ремапа.
Например, рассмотрим контакт PC10:
Он находится на 78 пине чипа (корпус LQFP100), может работать как вход/выход, толерантен к 5 вольтам, при необходимости его можно использовать как TX-вывод UART4 или сделать ремап для TX USART3.
Ещё, что куда, можно посмотреть на следующей картинке (кликабельна):

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

Задача 1
Попробуем просто помигать светодиодами, которые находятся на плате.
Для начала посмотрим, куда они подключены:
Итак, это пины PC6, PC7, PD13 и PD6. Будем мигать светодиодами по-очереди. Для начала необходимо включить тактирование используемых портов, без этого ничего работать не будет. Для этого заведём функцию RCC_Configuration. Сразу же там вызовем функцию SystemInit для начальной инициализации микроконтроллера и установки частот.
void RCC_Configuration(void) { SystemInit(); }
Теперь собственно включаем тактирование. Для этого сначала рассмотрим следующую схему:

На ней мы видим три основные шины:
- Advanced High Performance Bus (AHB);
- Low speed Advanced Peripherial Bus (APB1);
- High speed Advanced Peripherial Bus (APB2).
Шины AHB и APB2 высокоскоростные и могут работать на частотах вплоть до 72 МГц. Шина APB1 медленнее, её частота до 36 МГц. Для включения тактирования устройств на шине, существуют соответственно функции RCC_AHBPeriphClockCmd, RCC_APB1PeriphClockCmd и RCC_APB2PeriphClockCmd.
По картинке видно, что GPIO порты подключены к шине APB2, поэтому будем использовать последнюю функцию.
void RCC_Configuration(void) { SystemInit(); RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD , ENABLE); }
Рекомендуют для защиты все неиспользуемые выходы переводить в режим аналогового входа (Configure all unused GPIO port pins in Analog Input mode (floating input trigger OFF), this will reduce the power consumption and increase the device immunity against EMI/EMC). Для этого просто необходимо включить тактирование для всех GPIO и включить их на вход. Это необходимо сделать перед настройкой всех остальных пинов.:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_Init(GPIOD, &GPIO_InitStructure); GPIO_Init(GPIOE, &GPIO_InitStructure);
Всё, тактирование включено. Теперь необходимо настроить выводы. Для удобства это будем делать в функции GPIO_Configuration.
Объявим глобальную переменную:
GPIO_InitTypeDef GPIO_InitStructure;
И теперь, используя её, зададим параметры пина PC6:
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; //шестой пин GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //двухтактный выход GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //скорость 50 МГц (доступны также 2 и 10) GPIO_Init(GPIOC, &GPIO_InitStructure); //записываем информацию в регистр
В итоге вся функция будет выглядеть так:
void GPIO_Configuration() { GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOD, &GPIO_InitStructure); }
Теперь давайте заглянем в программу MicroXplorer. Весь код для инициализации интерфейсов можно сгенерировать с помощью него. Для этого указываем, какие пины мы хотим использовать и в каком режиме (вход/выход).

Затем, переходим на вкладку «Configuration» и жмём кнопку «GPIO». Там указываем для наших пинов скорость работы и какой именно режим выхода мы хотим получить. Затем жмём OK.

После этого выбираем в меню Tools -> Generate code. В результате, в сохранённом файле mx_gpio.c мы увидим знакомый уже код включения тактирования и инициализации пинов.
Это мы только настроили все необходимые пины! Теперь переходим к основной части программы. Вызываем ранее созданные функции:
int main(void) { RCC_Configuration(); GPIO_Configuration(); }
И далее, в бесконечном цикле по-очереди включаем и выключаем пины (если быть точнее, подключаем их то к питанию, то к земле). Для этого служат функции GPIO_SetBits и GPIO_ResetBits:
int main(void) { RCC_Configuration(); GPIO_Configuration(); while (1) { GPIO_SetBits(GPIOC, GPIO_Pin_6); //Включаем D1 Delay(0xAFFFF); GPIO_SetBits(GPIOC, GPIO_Pin_7 ); //Включаем D2 GPIO_ResetBits(GPIOC, GPIO_Pin_6); //Выключаем D1 Delay(0xAFFFF); GPIO_SetBits(GPIOD, GPIO_Pin_13 ); //Включаем D3 GPIO_ResetBits(GPIOC, GPIO_Pin_7); //Выключаем D2 и т.д. Delay(0xAFFFF); GPIO_SetBits(GPIOD, GPIO_Pin_6 ); GPIO_ResetBits(GPIOD, GPIO_Pin_13); Delay(0xAFFFF); GPIO_ResetBits(GPIOD, GPIO_Pin_6); } }
Полностью код выглядит так:
#include "stm32f10x.h" GPIO_InitTypeDef GPIO_InitStructure; void RCC_Configuration(void); void GPIO_Configuration(void); void Delay(__IO uint32_t nCount); int main(void) { RCC_Configuration(); GPIO_Configuration(); while (1) { GPIO_SetBits(GPIOC, GPIO_Pin_6); Delay(0xAFFFF); GPIO_SetBits(GPIOC, GPIO_Pin_7 ); GPIO_ResetBits(GPIOC, GPIO_Pin_6); Delay(0xAFFFF); GPIO_SetBits(GPIOD, GPIO_Pin_13 ); GPIO_ResetBits(GPIOC, GPIO_Pin_7); Delay(0xAFFFF); GPIO_SetBits(GPIOD, GPIO_Pin_6 ); GPIO_ResetBits(GPIOD, GPIO_Pin_13); Delay(0xAFFFF); GPIO_ResetBits(GPIOD, GPIO_Pin_6); } } void RCC_Configuration(void) { SystemInit(); RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART1 |RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE, ENABLE); } void GPIO_Configuration() { GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOD, &GPIO_InitStructure); } void Delay(__IO uint32_t nCount) { for(; nCount != 0; nCount--); }
Ссылки на архивы с проектами есть в конце статьи. Скомпилировать можно под Keil.
Теперь рассмотрим процесс прошивки. У меня пока нет программатора ST-Link, но STM32 к счастью, можно шить через UART. На моей плате для этого имеется USB интерфейс (USB-UART конвертор).
Качаем Flash Loader Demonstrator. Подключаем плату к компьютеру, перед этим перемычку BOOT0 ставим в положение 1. Это режим загрузчика. В положении 0 данная перемычка включает режим выполнения пользовательской программы сразу после включения/сброса.

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

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

Здесь мы видим адреса страниц памяти, а также их состояние (защита от чтения/записи). Просто жмём Next.

Вот здесь уже предстоит выбрать, что мы хотим сделать с микроконтроллером, а точнее с его памятью. Можно очистить память (полностью или частично), загрузить программу, выгрузить программу из памяти в файл, установить защиту, либо произвольно отредактировать данные. Выбираем загрузку на устройство (Download to device). Выбираем hex-файл с прошивкой (находится в папке Obj каталога проекта). Галочка «Verify after download» позволяет проверить правильность записи программы. Если же поставить галочку «Jump to the user program», то программа будет выполнена сразу же после загрузки, даже без возвращения перемычки BOOT0 на место и перезагрузки контроллера.

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

Задача 2
Теперь попробуем написать программу, которая будет зажигать светодиоды при нажатии на кнопки. Светодиод D1 для кнопки S1, D2 для S2 и т.д.
Посмотрим подключение кнопок:

Они подключены соответственно на PE5, PE4, PE3, PE2. При этом, по-умолчанию пин подтянут резистором к питанию, а при нажатии на кнопку соединяется с землёй.
Включаем тактирование:
void RCC_Configuration(void) { SystemInit(); RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART1 |RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE, ENABLE); }
Настраиваем пины:
void GPIO_Configuration() { //Кнопки GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //вход с подтяжкой к питанию GPIO_Init(GPIOE, &GPIO_InitStructure); //светодиоды D1 и D2 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); //светодиоды D3 и D4 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOD, &GPIO_InitStructure); }
В основной программе будем проверять, нажата ли кнопка (на пине будет логический ноль) и в этом случае включать светодиод. Иначе — выключаем его. Выглядеть код для кнопки S1 будет так:
if (!GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_5)) { GPIO_SetBits(GPIOC, GPIO_Pin_6); } else { GPIO_ResetBits(GPIOC, GPIO_Pin_6); }
Весь код выглядит так:
#include "stm32f10x.h" GPIO_InitTypeDef GPIO_InitStructure; void RCC_Configuration(void); void GPIO_Configuration(void); void Delay(__IO uint32_t nCount); int main(void) { RCC_Configuration(); GPIO_Configuration(); while (1) { if (!GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_5)) { GPIO_SetBits(GPIOC, GPIO_Pin_6); } else { GPIO_ResetBits(GPIOC, GPIO_Pin_6); } if (!GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_4)) { GPIO_SetBits(GPIOC, GPIO_Pin_7); } else { GPIO_ResetBits(GPIOC, GPIO_Pin_7); } if (!GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_3)) { GPIO_SetBits(GPIOD, GPIO_Pin_13); } else { GPIO_ResetBits(GPIOD, GPIO_Pin_13); } if (!GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_2)) { GPIO_SetBits(GPIOD, GPIO_Pin_6); } else { GPIO_ResetBits(GPIOD, GPIO_Pin_6); } } } void RCC_Configuration(void) { SystemInit(); RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART1 |RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE, ENABLE); } void GPIO_Configuration() { //Кнопки GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //вход с подтяжкой к питанию GPIO_Init(GPIOE, &GPIO_InitStructure); //светодиоды D1 и D2 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); //светодиоды D3 и D4 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOD, &GPIO_InitStructure); } void Delay(__IO uint32_t nCount) { for(; nCount != 0; nCount--); }
