Начнём с того, что вообще такое порт ввода-вывода. Для настройки режима работы или изменения состояния линии ввода-вывода обычно достаточно одного-двух битов, а регистры в контроллерах, как правило, восьми, шестнадцати или даже 32-х битные (как в stm32). Выделять для каждой линии ввода-вывода свой отдельный регистр — довольно расточительно, вот и придумали группировать в одном регистре биты управления сразу несколькими линиями, а группы линий, имеющие общие регистры управления, называть портами.
В контроллерах stm32 в порт может быть сгруппировано до 16-ти линий. Именуются порты обычно буквами латинского алфавита: PORTA, PORTB, PORTC …
Теперь про сами линии ввода-вывода. Линии ввода-вывода могут быть общего назначения (GPIO — general purpose input output) или иметь какое-то специальное назначение (например, специальные линии модулей встроенной периферии: MOSI/MISO для модуля SPI, D+/D- для модуля USB …). Как правило, линии делают конфигурируемыми, то есть такими, которые, в зависимости от настроек, могут использоваться или как GPIO, или как специальные. Типовую внутреннюю структуру канала ввода-вывода контроллера stm32 можно видеть на рисунке ниже.
Всего возможно 8 конфигураций линий ввода-вывода:
- Выход GPIO:
- push-pull (работают оба выходных транзистора)
- open-drain (работает только N-FET, а P-FET всегда разомкнут)
- Альтернативный выход (выход периферийного модуля):
-
- push-pull
- open-drain
- Вход:
- Analog, высокоимпедансный вход (подтягивающие резисторы и триггер Шмитта отключены, используется для АЦП)
- Floating, высокоимпедансный вход (подтягивающие резисторы отключены, триггер Шмидта включен)
- Pull-up, вход с подтяжкой к питанию (через специальный встроенный резистор)
- Pull-down, вход с подтяжкой к «земле» (через специальный встроенный резистор)
Конфигурация определяется регистрами GPIOx_CRL (первые 8 линий порта x) и GPIOx_CRH (вторые 8 линий порта x). В этих регистрах на каждую линию порта выделено по 4 бита, из которых 2 называются MODEy[1:0], а другие 2 — CONFy[1:0], где y — номер линии порта.
Кроме самих режимов, при конфигурировании линии в качестве выхода, сочетанием битов MODE[1:0] задаётся максимальная скорость переключения.
При конфигурировании линии в качестве входа с подтяжкой, в настройке принимает участие также выходной регистр порта (GPIOx_ODR). Соответствующий конфигурируемой линии бит регистра GPIOx_ODR в этом случае определяет «направление» подтяжки (к питанию или к «земле»).
По умолчанию (то есть после ресета) все линии настроены как «floating input».
Наглядно, всё то о чём мы сейчас говорили, показано в табличке ниже:
Режим | CNF1 | CNF0 | MODE1 | MODE0 | GPIOx_ODR | |
General purpose output |
push-pull | 0 | 0 | 01 — max speed 10 MHz 10 — max speed 2 MHz 11 — max speed 50 MHz |
0 или 1 | |
open-drain | 1 | 0 или 1 | ||||
Alternate function output |
push-pull | 1 | 0 | не важно | ||
open-drain | 1 | не важно | ||||
Input | analog | 0 | 0 | 00 | не важно | |
floating | 1 | не важно | ||||
pull-down | 1 | 0 | 0 | |||
pull-up | 1 |
Итак, порт мы настроили, что дальше? Дальше надо как-то линиями этого порта рулить.
Когда порт настроен для работы со встроенной периферией, эта периферия либо сама рулит нужными линиями, либо мы ими рулим, но через регистры этой самой периферии. Про это я писать пока не буду (про периферийные модули разговор особый). А пока рассмотрим как можно работать с линиями порта напрямую.
Считать состояние линий портов можно через Port Input Data Registers, которые коротко именуются GPIOx_IDR (x — буква порта, A, B, C и так далее). В младшие 16 бит каждого из этих регистров читаются состояния линий соответствующего порта, а старшие 16 бит — не используются.
Когда линии порта настроены как выходы GPIO, то управлять состоянием этих выходов можно через Port Output Data Registers, которые кратко именуются GPIOx_ODR (x — буква порта, A, B, C и так далее). То есть, те значения, которые записываются в эти регистры, появляются и на соответствующих выходных линиях. В этих регистрах также используются только младшие 16 бит.
Кроме того, как я выше уже писал, регистры GPIOx_ODR используются при конфигурировании входов с подтяжкой к питанию или «земле».
Помимо описанных выше регистров, для каждого порта выделены ещё два регистра, которые позволяют обойтись одной операцией для установки или сброса битов в выходных регистрах порта. Эти регистры называются GPIOx_BSRR (Port Bit Set/Reset Register) и GPIOx_BRR (Port Bit Reset Register), где x — буква порта.
Суть тут в следующем. Все регистры управления портами доступны только в виде слова целиком (то есть тут нет доступа к отдельным битам). Поэтому для изменения каких-либо отдельных битов, нужно сначала прочитать регистр, потом его модифицировать, а потом записать обратно. При этом между чтением и записью может произойти прерывание, в результате обработки которого, какие-то значения в регистрах могут поменяться. Мы эти изменения при перезаписи регистра потеряем. Чтобы этого избежать — можно каждый раз выключать прерывания на время модификации
выходных регистров порта, но это путь долгий и неэффективный.
Вместо этого как раз и придумали регистры GPIOx_BSRR и GPIOx_BRR. Эти регистры позволяют без всякого выключения прерываний осуществлять «атомарные» (то есть происходящие в одно действие) операции с отдельными битами регистров GPIOx_ODR.
Запись единицы в какой-либо из младших 16-ти бит регистра GPIOx_BSRR приводит к «атомарной» установке соответствующего бита регистра GPIOx_ODR в единицу, а запись единицы в какой-либо из старших 16-ти битов регистра GPIOx_BSRR приводит к «атомарному» сбросу соотвветствующего бита регистра GPIOx_ODR в ноль. Установка бита имеет приоритет над сбросом, то есть если установить в единицу одновременно два бита регистра GPIOx_BSRR, отвечающие за сброс и установку одного и того же бита регистра GPIOx_ODR, то в результате выполнения операции произойдёт установка этого бита в единицу.
Регистр GPIOx_BRR работает аналогично, за исключением того, что через этот регистр возможен только сброс битов регистра GPIOx_ODR и невозможна их установка.
Наверняка у некоторых читателей возникнет вопрос: «А в чём разница между использованием регистров GPIOx_BSRR + GPIOx_BRR и методом bit-banding?» Разница тут в том, что с помощью метода bit-banding за одну операцию можно изменить только один бит регистра GPIOx_ODR, а с помощью регистров GPIOx_BSRR, GPIOx_BRR можно изменить сразу несколько битов. В регистры GPIOx_BSRR, GPIOx_BRR мы записываем сразу целое слово и соответствующая реакция на эту запись появится сразу у всех битов регистра GPIOx_ODR для которых в этом слове установлены единицы.
Надеюсь с этим всё понятно (если нет, то спрашивайте на форуме), поэтому пойдём дальше.
Ещё одной интересной особенностью при работе с портами контроллеров STM32 является наличие механизма защиты отконфигурации порта от изменения. Для этого тоже существуют специальные регистры — GPIOx_LCKR (Port Configuration Lock Registers), где x — буква порта. Работает этот механизм следующим образом. Младшие 16 бит регистра GPIOx_LCKR используются для выбора линий порта, конфигурацию которых необходимо залочить (выбор осуществляется установкой соответствующего бита в единицу), а потом специальной последовательностью действий над 17-м битом осуществляется залочка этих выбранных конфигураций. Последовательность такая: записать 1, записать 0, записать 1, считать 0, считать 1. Правильный результат двух последних операций (чтение) говорит о том, что защита успешно включена. После включения защиты отменить её уже нельзя и выбранные конфигурации останутся залоченными до перезагрузки контроллера.
Главный документ, который стоит почитать чтобы узнать подробности (например, где какие биты расположены в описанных регистрах, адреса регистров и тому подобное) — это как обычно Reference Manual RM0008 (CD00171190.pdf). В данном случае копаем раздел 9 этого мануала (General-purpose and alternate function).
В качестве примера приведу программку, которая будет контролировать уровень сигнала на входе PA9 (скажем, на него у нас прицеплена кнопка) и изменять, в зависимости от этого уровня, уровень сигнала на выходе PA8 (сюда можно через резистор подключить светодиод). То есть при подаче единицы на вход PA9 — на выходе PA8 тоже будет устанавливаться единица, а при подаче нуля на вход PA9 — на выходе PA8 будет устанавливаться ноль.
;--------------------------------------------------------- ; blocks addresses SAR EQU 0x42000000 ; Start Alias Region FLASH EQU 0x40022000 RCC EQU 0x40021000 PORTA EQU 0x40010800 ; registers addresses FLASH_ACR EQU FLASH+0x0 RCC_CR EQU RCC+0x0 RCC_CFGR EQU RCC+0x4 RCC_APB2ENR EQU RCC+0x18 GPIOA_CRH EQU PORTA+0x4 ; регистр конфигурации старшей половины PortA (8-15) GPIOA_IDR EQU PORTA+0x8 ; входной регистр PORTA GPIOA_ODR EQU PORTA+0xC ; выходной регистр PORTA GPIOA_BSRR EQU PORTA+0x10 ; регистр установки/сброса отдельных бит PORTA ; bits numbers LAT1 EQU 1 HSEON EQU 16 HSERDY EQU 17 PLLSRC EQU 16 PLLMUL EQU 18 PPRE2 EQU 11 PLLON EQU 24 PLLRDY EQU 25 SW1 EQU 1 AFIOEN EQU 0 IOPAEN EQU 2 MCO EQU 24 MODE8 EQU 0 ; биты MODE для PA8 - это 0-й и 1-й CNF8 EQU 2 ; биты CNF для PA8 - это 2-й и 3-й MODE9 EQU 4 ; биты MODE для PA9 - это 4-й и 5-й CNF9 EQU 6 ; биты CNF для PA9 - это 6-й и 7-й AREA STACK, NOINIT, READWRITE SPACE 0x400 Stack_top AREA RESET, DATA, READONLY dcd Stack_top dcd Program_start AREA PROGRAM, CODE, READONLY ENTRY Program_start InitClock mov r9, #1 ; устанавливаем Latency = 2 ldr r0, =SAR+(FLASH_ACR&0x00FFFFFF)*0x20+LAT1*4 str r9,[r0] ; включаем HSE ldr r0, =SAR+(RCC_CR&0x00FFFFFF)*0x20+HSEON*4 str r9,[r0] ; ждём появления флага HSERDY ldr r0, =RCC_CR ; загружаем в r0 адрес регистра RCC_CR wait_hserdy ldr r10,[r0] ; читаем регистр RCC_CR ; проверяем, равен ли бит HSERDY единице (&0x20000) tst r10,#(1<<HSERDY) beq wait_hserdy ; выбираем HSE источником для PLL, устанавливаем множитель = 9, ; предделитель для APB2 (/2), MCO - no clock (оно так и по умолчанию, это просто чтоб внимание обратить) ldr r0,=RCC_CFGR ; загружаем в r0 адрес регистра RCC_CFGR ldr r10,=(1<<PLLSRC)+(7<<PLLMUL)+(4<<PPRE2)+(0<<MCO) str r10,[r0] ; включаем PLL ldr r0,=SAR+(RCC_CR&0x00FFFFFF)*0x20+PLLON*4 str r9,[r0] ; ждём появления флага PLLRDY ldr r0, =RCC_CR ; загружаем адрес регистра RCC_CR wait_pllrdy ldr r10,[r0] ; читаем RCC_CR ; проверяем, равен ли бит PLLRDY единице (&0x2000000) tst r10, #(1<<PLLRDY) beq wait_pllrdy ; выбираем PLL в качестве SYSCLK ldr r0,=SAR+(RCC_CFGR&0x00FFFFFF)*0x20+SW1*4 str r9,[r0] ; включаем тактирование PORTA ldr r0,=SAR+(RCC_APB2ENR&0x00FFFFFF)*0x20+IOPAEN*4 str r9,[r0] ; конфигурируем PORTA (PA8 - выход, PA9 - вход) ldr r0,=GPIOA_CRH ; загружаем в r0 адрес регистра GPIOA_CRH ldr r10,[r0] ; читаем регистр GPIOA_CRH в r10 ; устанавливаем PA8 MODE=11 (output max speed = 50MHz), PA8 CNF=00 (output push-pull) ; PA9 MODE=00 (input), PA9 CNF=01 (input floating, т.е Hi-Z) ; ногу PA9 можно было и не настраивать, поскольку все ного после ресета по умолчанию настроены ; на вход Hi-Z, так что это просто чтобы внимание обратить ldr r11,=(3<<MODE8)+(0<<CNF8)+(0<<MODE9)+(1<<CNF9) bfi r10, r11, #0, #8 ; копируем 8 младших бит регистра r11 в r10 str r10,[r0] ; пишем регистр r10 в регистр GPIOA_CRH ;------------------ ; делаем что-то полезное Work nop ; это чтобы не получить warning: A1581W: Added 2 bytes of padding... ldr r0, =GPIOA_IDR ; в r0 - адрес регистра входов ldr r10,[r0] ; читаем входы (регистр IDR) tst r10,#(1<<9) ; test 9 bit (проверяем уровень на 9-й ноге) beq Null ; если ноль - прыгаем One ldr r0,=GPIOA_BSRR ; загружаем в r0 адрес регистра GPIOA_BSRR ldr r10,=(1<<8) ; загружаем в r10 0x100 (установленный 8-й бит, set 8-th bit in GPIOA_ODR) str r10,[r0] ; пишем регистр r10 в GPIOA_BSRR b Work Null ldr r0,=GPIOA_BSRR ; загружаем в r0 адрес регистра GPIOA_BSRR ldr r10,=(1<<24) ; загружаем в r10 0x1000000 (установленный 24-й бит, reset 8-th bit in GPIOA_ODR) str r10,[r0] ; пишем регистр r10 в GPIOA_BSRR b Work END ;--------------------------------------------------------- |
Код конечно не оптимальный, но, надеюсь, максимально понятный. Если вопросы всё же остались — задавайте на форуме.
- Часть 1. Установка MDK, создание проекта, основы Keil uVision
- Часть 2. Команды и директивы ассемблера, структура и синтаксис программы. Первая программа для STM32
- Часть 3. Карта памяти контроллеров STM32, методы работы с памятью
- Часть 4. Регистры, старт и режимы работы контроллеров STM32
- Часть 5. Как залить прошивку в контроллер
- Часть 6. Настройка системы тактирования
- Часть 7. Работа с портами ввода-вывода
- Часть 8. Процедуры на ассемблере для STM32
- Часть 9. Система прерываний
- Часть 10. CMSIS, использование стандартных библиотек и функций
- Часть 11. Подключение и использование драйверов из пакета StdPeriph
- Часть 12. Работа с модулями USART и UART.
- Часть 13. Работа с модулями ADC
- Часть 14. Использование DMA
- Часть 15. Таймеры. Глава 1 — Введение. Простейшие таймеры
- Часть 15. Таймеры. Глава 2 — Таймеры общего назначения TIM9 — TIM14
- Часть 15. Таймеры. Глава 3 — Таймеры общего назначения TIM2 — TIM5
- Часть 15. Таймеры. Глава 4 — Продвинутые таймеры TIM1, TIM8
- Часть 16. Создание устройства USB HID в Keil uVision при помощи библиотечного компонента USB
- Приложение 1. Набор инструкций THUMB-2 и особенности их использования
- Приложение 2. Таблица векторов прерываний для семейств STM32F101, STM32F102, STM32F103
- Приложение 3. Драйвера и функции библиотеки StdPeriph