Доброго времени суток! Сегодня мы займемся изучением GPIO. И, в первую очередь, давайте посмотрим в каких режимах могут работать порты ввода-вывода в STM32F10x. А режимов этих существует море, а именно:
- Input floating
- Input pull-up
- Input-pull-down
- Analog
- Output open-drain
- Output push-pull
- Alternate function push-pull
- Alternate function open-drain
То есть при работе на вход:
- Вход – Hi-Z
- Вход – подтяжка вверх
- Вход – подтяжка вниз
- Вход – аналоговый
При работе порта на выход имеем следующие варианты:
- Выход – с открытым коллектором
- Выход – двухтактный
- Альтернативные функции – выход типа "с открытым коллектором"
- Альтернативные функции – двухтактный выход
Вот кстати документация на STM32F103CB - STM32F103CB. В даташите есть внушительная таблица, в которой показано, какие альтернативные функции имеются у конкретного вывода. Вот, например, выводы PA9, PA10:
В столбце Default видим, какие функции будут выполнять эти пины при их настройке для работы в режиме Alternative function. То есть, настроив эти пины соответствующим образом они из просто PA9 и PA10 превратятся в Rx и Tx для USART1. А для чего же тогда столбец Remap? А это не что иное, как очень полезная функция ремаппинга портов. Благодаря ремапу, Tx USARTA ’а, например, может переместится с пина PA9 на PB6. Довольно часто эта функция оказывается невероятно полезной.
Ну с режимами вроде бы все более-менее понятно, пришло время окинуть взором регистры, которыми порты ввода-вывода управляются.
Раз уж только что обсудили в каких режимах могут существовать выводы STM32F10x, сразу же давайте разберемся, как же их можно собственно перевести в нужный режим. А для этого выделены два регистра – CRL и CRH. В первом конфигурируются выводы от 0 до 7, во втором, соответственно от 8 до 15. Регистры, как вы помните, 32-х разрядные. То есть на 8 выводов приходится 32 бита - получается 4 бита на одну ножку. Открываем даташит и видим:
Например, надо нам настроить ножку PB5. Идем в регистр GPIOB->CRL и выставляем соответствующие биты так как нам требуется (на картинке 32-х битный регистр CRL). Для PB5 это биты:
После 8-битных контроллеров все это может показаться сложным и нелогичным, но на самом деле реализовано все довольно изящно. Посмотрим, что тут есть еще...
- Выходной регистр GPIOx_ODR – напоминает регистр PORTx в AVR. Все что попадает в этот регистр сразу же попадает во внешний мир. Регистр 32-разрядный, а ножек всего 16. Как думаете, для чего используются оставшиеся 16? Все очень просто, биты регистра с 15 по 31 не используются вовсе )
- Входной регистр GPIOx_IDR – аналог PINx в AVR. Структура его похожа на упомянутую структуру ODR. Все, что появляется на входе микроконтроллера, сразу же оказывается во входном регистре IDR.
- Еще два полезных регистра GPIOx_BSSR и GPIOx_BRR. Они позволяют менять значения битов в регистре ODR напрямую, без использования привычных бит-масок. То есть, хочу я, например, выставить в единицу пятый бит ODR. Записываю единичку в пятый бит GPIOx_BSSR, и все, цель достигнута. Вдруг захотелось сбросить пятый бит ODR - единицу в 5 бит GPIOx_BRR и готово.
Итак, основные регистры рассмотрели, но, на самом-то деле, мы в наших примерах будем делать все иначе, используя Standard Peripheral Library. Так что идем изучать библиотеку.
Время традиционной вставки: поскольку компания STMicroelectronics прекратила поддержку библиотеки SPL, которая использовалась в этом курсе, я создал новый, посвященный работе уже с новыми инструментами, так что буду рад видеть вас там - STM32CubeMx. Кроме того, вот глобальная рубрика по STM32, а также статья на смежную тему из нового курса: STM32 GPIO. Настройка портов ввода-вывода в STM32CubeMx.
За GPIO в SPL отвечают файлы stm32f10x_gpio.h и stm32f10x_gpio.c.За конфигурацию портов отвечает структура GPIO_InitTypeDef:
typedef struct { uint16_t GPIO_Pin; /* Specifies the GPIO pins to be configured. This parameter can be any value of @ref GPIO_pins_define */ GPIOSpeed_TypeDef GPIO_Speed; /* Specifies the speed for the selected pins. This parameter can be a value of @ref GPIOSpeed_TypeDef */ GPIOMode_TypeDef GPIO_Mode; /* Specifies the operating mode for the selected pins. This parameter can be a value of @ref GPIOMode_TypeDef */ }GPIO_InitTypeDef;
Видим, что структура имеет три поля: GPIO_PIN, GPIO_Speed и GPIO_Mode. Нетрудно догадаться, что первая отвечает за номер ножки порта, которую мы хотим настроить, вторая – за скорость работы порта, ну и третья, собственно, за режим. Таким образом, для настройки вывода нам всего лишь нужно объявить переменную типа GPIO_InitTypeDef и заполнить ее поля нужными значениями. Все возможные значения полей тут же – в stm32f10x_gpio.h. Например,
typedef enum { GPIO_Mode_AIN = 0x0, GPIO_Mode_IN_FLOATING = 0x04, GPIO_Mode_IPD = 0x28, GPIO_Mode_IPU = 0x48, GPIO_Mode_Out_OD = 0x14, GPIO_Mode_Out_PP = 0x10, GPIO_Mode_AF_OD = 0x1C, GPIO_Mode_AF_PP = 0x18 }GPIOMode_TypeDef;
Все значения уже рассчитаны создателями SPL, так что для настройки какого-нибудь вывода для работы в режиме Output push-pull надо всего лишь в соответствующей структуре задать поле: GPIO_Mode = GPIO_Mode_Out_PP.
Ну вот, структура объявлена, поля заполнены как надо, что же дальше? Ведь мы всего лишь создали переменную. Причем тут регистры, микроконтроллеры и вообще электроника? Лезем в файл stm32f10x_gpio.c и находим там множество различных функций для работы с GPIO.
Рассмотрим функцию GPIO_Init() (код приводить не буду, все в файле библиотеки). Так вот, эта функция как раз и связывает нашу созданную структуру и конкретные регистры контроллера. То есть мы передаем в эту функцию переменную, в соответствии с которой выставляются нужные биты нужных регистров микроконтроллера. Все очень просто. Поковыряйте еще файлы библиотеки. Там функции на любой случай есть 🙂 Кстати очень удобно – перед функцией идет описание переменных, которые она принимает и возвращает, а также описание собственно того, что эта функция призвана делать. Так что, разобраться несложно.
Отвлечемся ненадолго от портов ввода-вывода и обсудим один довольно тонкий момент. Чтобы использовать порты, либо любую другую периферию, обязательно надо включить тактирование. И порты, и периферия изначально отключены от тактирования, так что без этого действия ничего не заведется. Программа скомпилируется, но на деле работать ничего не будет. За тактирование в SPL отвечают файлы stm32f10x_rcc.c и stm32f10x_rcc.h. Не забывайте добавлять их в проект.
Давайте уже перейдем к программированию. Как это принято, заставим диод помигать 👍 Чтобы лучше разобраться с Standard Peripheral Library немножко усложним обычное мигание диодом – будем опрашивать кнопку, и если она нажата – диод загорается, иначе – гаснет. Запускаем Keil, создаем проект, добавляем все нужные файлы, не забываем про CMSIS. Из SPL для этого проекта нам понадобятся 4 файла, уже упомянутые выше. Создание нового проекта описано в предыдущей статье учебного курса и также там можно найти ссылки на библиотеки.
Итак, код:
/***************************************************************************************/ //Подключаем все нужные файлы #include "stm32f10x.h" #include "stm32f10x_rcc.h" #include "stm32f10x_gpio.h" /***************************************************************************************/ //Тут будет вся инициализация всей использующейся периферии void initAll() { // Объявляем переменную port типа GPIO_InitTypeDef GPIO_InitTypeDef port; // Это функция из файла stm32f10x_rcc.c, включает тактирование на GPIOA // GPIOA сидит на шине APB2 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // Про эту функцию напишу чуть ниже GPIO_StructInit(&port); // Заполняем поля структуры нужными значениями // Первый вывод – вход для обработки нажатия кнопки – PA1 port.GPIO_Mode = GPIO_Mode_IPD; port.GPIO_Pin = GPIO_Pin_1; port.GPIO_Speed = GPIO_Speed_2MHz; // А про эту функцию мы уже говорили // Отметим только что один из параметров – указатель(!) на нашу структуру GPIO_Init(GPIOA, &port); // Настраиваем вывод, на котором будет висеть диод – PA0 port.GPIO_Mode = GPIO_Mode_Out_PP; port.GPIO_Pin = GPIO_Pin_0; port.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOA, &port); } /***************************************************************************************/ int main() { // Объявляем переменную для хранения состояния кнопки uint8_t buttonState = 0; initAll(); while(1) { // С помощью функции из SPL считываем из внешнего мира состояние кнопки buttonState = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1); if (buttonState == 1) { GPIO_SetBits(GPIOA, GPIO_Pin_0); } else { GPIO_ResetBits(GPIOA, GPIO_Pin_0); } } } /***************************************************************************************/
Кстати, возможно кто-то обратит внимание на наличие скобок { }, несмотря на всего лишь одну инструкцию в теле if и else. А это уже привычка ) Очень рекомендую так писать, особенно при разработке крупных проектов. При дописывании/исправлении программы невнимательный программист может не обратить внимания на отсутствие скобок и дописать вторую инструкцию, которая, как вы понимаете, уже окажется все блока if или else. Та же тема с циклами. Когда над проектом работает много народу, нет никаких гарантий, что кто-нибудь не окажется невнимательным, так что, чтобы не тратить минуты/часы на последующие поиски ошибки, рекомендую ставить эти скобки всегда.
Нажимаем F7, компилируем, и вот наша первая программа для STM готова. Вроде бы код довольно подробно откомментирован, так что поясню только пару моментов.
Функция GPIO_StructInit(&port) – принимает в качестве аргумента указатель на переменную port. Эта функция заполняет поля структуры, переданной ей в качестве аргумента значениями по умолчанию.
Еще две функции, которые мы использовали:
- GPIO_SetBits(GPIOA, GPIO_Pin_0);
- GPIO_ResetBits(GPIOA, GPIO_Pin_0);
Ну вы и так догадались для чего они 🙂 Итак, на этом сегодня заканчиваем, а в следующей статье познакомимся со средствами Keil’а для отладки наших программ!