STM32 с нуля. 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 бита на одну ножку. Открываем даташит и видим:

Режимы работы портов ввода-вывода в STM32

Например, надо нам настроить ножку PB5. Идем в регистр GPIOB->CRL и выставляем сответствующие биты так как нам требуется (на картинке 32-х битный регистр CRL). Для PB5 это биты:

STM32. Настройка портов ввода-вывода

После восьмибиток может показаться все достаточно сложным и каким то корявым, но на самом деле реализовано все довольно изящно =). Посмотрим, что тут есть еще.

Выходной регистр 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. Так что лезем ковырять библиотеку. За 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. Нетрудно догадаться, что первая отвечает за номер ножки порта, которую мы хотим настроить, вторая – за скорость работы порта, ну и третья, собственно, за режим работы. Таким образом, для настройки вывода нам всего лишь нужно объявить переменную типа структуры и заполнить ее поля нужными значениями. Все возможные значения полей тут же – в 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 и находим там тучу различных функций для работы с STM32 GPIO. Рассмотрим функцию GPIO_Init() (код приводить не буду, все в файле библиотеки). Так вот, эта функция как раз и связывает нашу созданную структуру и конкретные регистры контроллера. То есть мы передаем в эту функцию переменную, в соответствии с которой выставляются нужные биты нужных регистров микроконтроллера. Все очень просто, но от этого не менее гениально. Поковыряйте еще файлы библиотеки. Там функции на любой случай есть ) Кстати очень удобно – перед функцией идет описание переменных, которые она принимает и возвращает, а также описание собственно того, что эта функция призвана делать. Так что, разобраться несложно, но надо немного дружить с английским. Хотя без этого никуда;)

Отвлечемся ненадолго от портов ввода-вывода и обсудим один довольно тонкий момент. Чтобы использовать порты, либо любую другую периферию, ОБЯЗАТЕЛЬНО надо включить тактирование. И порты, и периферия изначально отключены от тактирования, так что без этого действия ничего не заведется. Программа скомпилируется, но на деле работать ничего не будет. За тактирование в SPL отвечают файлы stm32f10x_rcc.c и stm32f10x_rcc.h. Не забывайте добавлять их в проект.

Давайте уже перейдем к программированию. Как это принято, заставим диодик помигать ) Чтобы получше разобраться с Standard Peripheral Library немножко усложним обычное мигание диодом – будем опрашивать кнопку, и если она нажата – диод загорается, иначе – гаснет. Запускаем Keil, создаем проект, добавляем все нужные файлы, не забываем про CMSIS. Из SPL для этого проекта нам понадобятся 4 файла, уже упомянутые выше. Создание нового проекта описано в предыдущей статье учебного курса. Также там можно найти ссылки на библиотеки )

Итак, код:

/****************************gpio.c*********************************/
//Подключаем все нужные файлы
#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);
        }
    } 
} 
/****************************End of file****************************/

Кстати, возможно кто-то обратит внимание на наличие скобок {  }, несмотря на всего лишь одну инструкцию  в теле if и else. А это уже привычка) Очень рекомендуется так писать, особенно при разработке крупных проектов. При дописывании/исправлении программы невнимательный программист может не обратить внимания на отсутствие скобок и дописать вторую инструкцию, которая, как вы понимаете, уже окажется все блока if или else. Та же тема с циклами. Когда над проектом работает много народу, нет никаких гарантий, что кто-нибудь не окажется невнимательным, так что, чтобы не тратить минуты/часы на последующие поиски косяка, рекомендую ставить эти скобки всегда ) Хотя может, кто-то и не согласится с такой логикой.

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

Функция GPIO_StructInit(&port) – принимает в качестве аргумента адрес переменной port.

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

Еще две функции, которые мы использовали:

  • GPIO_SetBits(GPIOA, GPIO_Pin_0);
  • GPIO_ResetBits(GPIOA, GPIO_Pin_0);

Ну вы и так догадались для чего они 😉

Итак мы закончили рассматривать STM32 порты ввода-вывода. В следующей статье познакомимся со средствами Keil’а для отладки.

Понравилась статья? Поделись с друзьями!

STM32 с нуля. GPIO. Порты ввода-вывода.: 58 комментариев
  1. Добрый день! Я вижу у вас в Keil комментарии на русском языке. У меня при переключении на русский Keil печатает какую-то абракадабру. Может где-то в настройках надо что-то задать, не подскажете? Я так и не нашел.

    • Попробуй зайти в Edit -> Configuration -> Colors & Fonts -> «8051: Editor C Files»
      параметр «//Comment» и установить ему шрифт «FixedSys»

  2. Добрый день!
    Попробовал скомпилировать получил 44 варнинга и ошибку
    stm32f10x_rcc.c(1451): warning: #223-D: function «assert_param» declared implicitly
    а вот ошибка
    .\p1.axf: Error: L6218E: Undefined symbol assert_param (referred from stm32f10x_gpio.o).
    Подскажите, что не так.
    Спасибо

  3. В проекте меняю используемый чип с 103 на 107 — возникает ошибка при отладке… меняю назад на 103 — все хорошо… Как-то можно данную проблему победить?

    • В Кейле просто встроенный отладчик нормально работает только для stm32f103, поэтому тут только один вариант — отладка в железе

  4. Подскажите пожалуйста как например вывод PB03 настроить как цифровой вход с подтяжкой на землю и/или с подтяжкой к питанию.

    • Надо поле структуры GPIO_Mode задавать, вот возможные значения: GPIO_Mode_AIN, GPIO_Mode_IN_FLOATING, GPIO_Mode_IPD, GPIO_Mode_IPU, GPIO_Mode_Out_OD, GPIO_Mode_Out_PP, GPIO_Mode_AF_OD,
      GPIO_Mode_AF_PP

      IPD, например, вход с подтяжкой вниз, IPU — подтяжка вверх

  5. А как это сделать не используя SPL, через CMSIS.

    GPIOB->CRL &=~GPIO_CRH_MODE3_0;

    GPIOB->CRL |= GPIO_CRH_CNF3_1;

    • Вот, например для входа с подтяжкой вниз:
      GPIOB->CRL |= GPIO_CRH_CNF3_1;
      Бит CNF1 в единицу.
      И в GPIOB_ODR надо выставить в ноль третий бит.

  6. Я делаю вот так:
    GPIOA->CRL &= ~GPIO_CRL_MODE5;
    GPIOA->CRL |= GPIO_CRL_CNF5_1;
    GPIOA->ODR = 0xFFFF; //в pull-up;

    Замеряю напряжение на выводе PA05, оно равно 0 :(. Я так предпологаю что там должно быит напряжение питания?

  7. Теперь делаю так:

    RCC->APB2ENR|= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN;
    RCC->CFGR |= RCC_CFGR_MCO_SYSCLK;

    GPIOA->CRH |= GPIO_CRH_MODE11_1; // Настраиваем на выход
    GPIOA->CRH &= ~GPIO_CRH_CNF11; // Симметричный выход

    GPIOB->CRL &= ~GPIO_CRL_MODE0; // Настраиваем на вход
    GPIOB->CRL |= GPIO_CRL_CNF0_1; // С подтяжкой 0x44444448
    GPIOB->ODR = 0xFFFF;

    НО если вместо GPIO_CRL_CNF0_1 подставить 0x44444448 то все работает! Что это может быть? подскажите?

  8. Скидывать не буду, подсказали:)

    »
    После сброса конфиг выглядит так :
    CNF 01 MODE 00 — Input float
    Ты ставишь CNF0_1 получаешь :
    CNF11 MODE 00
    Что соответствует Reserved а не Input c pullup .»

    Но все равно спс! 🙂

  9. Здравствуйте.Пытаюсь делать в точности так,как Вы описываете. Код с этой страницы. Компиляция проходит успешно. Запускаю симулятор — выдаётся ошибка » *** error 65: access violation at 0x40021000 : no ‘read’ permission » . Контроллер stm32f103cb
    Подскажите, как справиться с этой проблемой.

  10. контроллер:
    в меню options for Target `STM32` вкладка Device
    указан STM32F103CB

    Настройки отладчика:
    в меню options for Target `STM32` вкладка Debug:

    точка у Use Simulator
    пустой квадрат у Limit Speed to Real-Time
    галка у Load Application at Startup
    галка у Run to main()
    Initialization File — пустая строка
    Restore Debug Session Setting — во всех квадратах галки
    CPU DILL: SARMCM3.DLL
    Pframetr: -REMAP
    Dialog DLL: DCM.DLL
    Parametr:-pCM3

    Что не так?

  11. Схема подключения , похожа на http://parshev.files.wordpress.com/2011/11/button_bb1.png?w=500&h=416

    Вот мой код
    //
    #include
    #include «stm32f30x_gpio.c»
    //

    void initAll()
    {
    GPIO_InitTypeDef port;
    //
    RCC_AHBPeriphResetCmd(RCC_AHBPeriph_GPIOA, ENABLE);
    //
    GPIO_StructInit(&port);
    //
    port.GPIO_Mode=GPIO_Mode_IN;
    port.GPIO_PuPd=GPIO_PuPd_DOWN;
    port.GPIO_Pin=GPIO_Pin_3;
    port.GPIO_Speed=GPIO_Speed_2MHz;
    GPIO_Init(GPIOA,&port);
    //
    port.GPIO_Mode=GPIO_Mode_OUT;
    port.GPIO_OType=GPIO_OType_PP;
    port.GPIO_PuPd=GPIO_PuPd_NOPULL;
    port.GPIO_Pin=GPIO_Pin_2;
    port.GPIO_Speed=GPIO_Speed_2MHz;
    GPIO_Init(GPIOA,&port);
    }
    int main()
    {
    uint8_t buttonState = 0;
    initAll();
    while(1)
    {
    buttonState=GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3);

    if (buttonState == 1 )
    {
    GPIO_SetBits(GPIOA,GPIO_Pin_2);
    }
    else
    {
    GPIO_ResetBits(GPIOA,GPIO_Pin_2);
    }
    }
    }

    Почему-то у меня моргают и диоды на плате…не понятно. Помогите разобраться

  12. Плата STM32F3Discovery. Диод(обычный красный) подключен одной ногой на GND платы, второй через резистор на PA3. Кнопка к PA2 и на +3В c платы — и ничего не светиться.
    Подключаю кнопку к +5В и диод светиться но в месте с ним еще светятся голубые диоды на плате(бледно, но светятся). В чем может быть дело?
    Делал то же самое но из примеров от STM с EXTI там все работает на +3В.

  13. Да да РА3 кнопка, ошибся. Где прочитать про то как тестировать. И из-за этого у меня не работает?

    • не работает из-за тактирования. Надо RCC_AHBPeriphClockCmd() команду использовать для включения тактирования

  14. Спасибо, невнимательно читал(
    Хочу попробовать сделать все уроки по ардуино но на стм32.

  15. Здравствуйте. При подключении «GPIO_STM32F4xx.h» и «stm32f4xx_gpio.h» проект не компилится.
    Там одинаковые переменные. Как быть?
    C:\Keil_v5\ARM\PACK\Keil\STM32F4xx_DFP\1.0.8\RTE_Driver\GPIO_STM32F4xx.h(57): error: #40: expected an identifier GPIO_AF_TIM1 = 1,

    Вот такого вида ошибки.

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

  16. Подскажите пожалуйста, пытаюсь сделать это задание. Сижу и пытаюсь понять где накосячил. Смысл какой — у меня процессор не 103 а 407 и пытаясь адаптировать под него эту мигалку, при компиляции получаю ошибку «MainCode\main.c(11): error: #20: identifier «GPIO_Mode_IPD» is undefined». Залез в «stm32f4xx_gpio.h», а там вообще не так много перечислений режимов работы. И тут возник вопрос — а может у меня процессор так не умеет или надо как-то по другому это делать. Среда разработки Keil 4.73, процессор выбран верно, SPL качал с сайта производителя(еле нашёл где это вообще скачивается).

  17. если настроить вывод как «Output push-pull», куда будет подтяжка к «+» или к «-» ?

  18. Да, то что двухтактный я понимаю, извиняюсь я неполноценно задал вопрос:
    допустим
    gpio.GPIO_Mode = GPIO_Mode_Out_PP; //Выход Push-Pull
    и дальше пошли настраивать остальную периферию.
    Как будет вести себя вывод в это время ?
    ведь в регистр мы еще не записали
    ни GPIOC->BSRR=GPIO_BSRR_BS9; // «1»
    ни GPIOC->BSRR=GPIO_BSRR_BR9; // «0»

  19. Здравствуйте.
    У меня такой вопрос:
    У меня есть 8 бит данных.
    У регистра ODR свободны первые 16 разрядов.
    Могу ли я записать 8 бит данных в регистр ODR в ячейки с 8-ой по 15-ую, и как это сделать, не используя побитовую запись через BSRR?

    Например, если записывать так:
    uint8_t word = 3;
    GPIOA->ODR = word;
    То запись будет произведена в первые 8 разрядов ODR. А мне нужны с 8-го по 15-ый.

  20. Здравствуйте!
    При компиляции программы, возникают следующие ошибки.
    Контроллер в проекте выбрал такой же как у Вас.
    Почему может возникать эта ошибка? Пути к файлам указаны верно. Тестовая программа компилировалась.
    В этой же как будто не виден файл «stm32f10x_gpio.c», но *.h файл подключен.
    test.axf: Error: L6218E: Undefined symbol GPIO_Init (referred from test.o).
    test.axf: Error: L6218E: Undefined symbol GPIO_ReadInputDataBit (referred from test.o).
    test.axf: Error: L6218E: Undefined symbol GPIO_ResetBits (referred from test.o).
    test.axf: Error: L6218E: Undefined symbol GPIO_SetBits (referred from test.o).
    test.axf: Error: L6218E: Undefined symbol GPIO_StructInit (referred from test.o).
    Target not created

    • Эх, удалить уже нельзя, нашел свой косяк. Подключить то — подключил, а в проект добавить забыл.

  21. Здравствуйте. Вы пишите в уроке
    «//Отметим только что один из параметров – указатель(!) на
    //нашу структуру
    GPIO_Init(GPIOA, &port);»
    Но разве «&» это указатель, а не адрес?

    • Да, & — это операция взятия адреса. А переменная, в которой хранится адрес на другую переменную является указателем ) Просто здесь мы не создавали отдельную переменную-указатель для хранения адреса, а передали его сразу в функцию через &.

  22. Доброго времени суток!
    Вопрос у меня такой, надеюсь поймете меня. может не совсем в тему. Вобщем у меня кнопка(физическая в приборе привязана к ножке микроконтроллера. когда я на нее нажимаю, прибор должен включиться. Т.е я должен опрашивать ножку контроллера, не нажата ли кнопка. если кнопка нажата, то я должен подать логическую единицу на другую ногу мк и зафиксировать что прибор сейчас включен. т.к при повторном нажатии на кнопку я его должен отключить. можете подсказать? как это реализовать программно.

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

  23. Добрый день.
    Спасибо за уроки, у меня stm32f103c8 как изменить Ваш код , что-бы зажечь светодиод на РС13 (установленный на плате)?
    /****************************gpio.c*********************************/
    //Подключаем все нужные файлы
    #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_GPIOC, ENABLE);

    //А про эту функцию мы уже говорили
    //Отметим только что один из параметров – указатель(!) на
    //нашу структуру
    GPIO_Init(GPIOC, &port);
    //Настраиваем вывод, на котором будет висеть диодик – PA0
    port.GPIO_Mode = GPIO_Mode_Out_PP;
    port.GPIO_Pin = GPIO_Pin_13;
    port.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOC, &port);
    }
    /*******************************************************************/
    int main()
    {
    //Объявляем переменную для хранения состояния кнопки

    initAll();

    while(1)
    {

    GPIO_SetBits(GPIOC, GPIO_Pin_13);

    //GPIO_ResetBits(GPIOC, GPIO_Pin_13);//

    }
    }

    /****************************End of file****************************/

    • Вроде бы все верно в коде, кроме одного момента в инициализации:

      GPIO_Init(GPIOC, &port);
      //Настраиваем вывод, на котором будет висеть диодик – PA0
      port.GPIO_Mode = GPIO_Mode_Out_PP;
      port.GPIO_Pin = GPIO_Pin_13;
      port.GPIO_Speed = GPIO_Speed_2MHz;
      GPIO_Init(GPIOC, &port);

      Здесь первым делом вызывается функция иницилазации — до(!) того, как поля структуры port были заполнены нужными данными.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *