Программирование ARM-контроллеров STM32 на ядре Cortex-M3. Часть 7. Работа с портами ввода-вывода

  1. Часть 1. Установка MDK, создание проекта, основы Keil uVision
  2. Часть 2. Команды и директивы ассемблера, структура и синтаксис программы. Первая программа для STM32.
  3. Часть 3. Карта памяти контроллеров STM32, методы работы с памятью.
  4. Часть 4. Регистры, старт и режимы работы контроллеров STM32.
  5. Часть 5. Как залить прошивку в контроллер.
  6. Часть 6. Настройка системы тактирования.
  7. Часть 7. Работа с портами ввода-вывода.
  8. Приложение 1. Набор инструкций THUMB-2 и особенности их использования.

Начнём с того, что вообще такое порт ввода-вывода. Для настройки режима работы или изменения состояния линии ввода-вывода обычно достаточно одного-двух битов, а регистры в контроллерах, как правило, восьми, шестнадцати или даже 32-х битные (как в stm32). Выделять для каждой линии ввода-вывода свой отдельный регистр — довольно расточительно, вот и придумали группировать в одном регистре биты управления сразу несколькими линиями, а группы линий, имеющие общие регистры управления, называть портами.

В контроллерах stm32 в порт может быть сгруппировано до 16-ти линий. Именуются порты обычно буквами латинского алфавита: PORTA, PORTB, PORTC …

Теперь про сами линии ввода-вывода. Линии ввода-вывода могут быть общего назначения (GPIO — general purpose input output) или иметь какое-то специальное назначение (например, специальные линии модулей встроенной периферии: MOSI/MISO для модуля SPI, D+/D- для модуля USB …). Как правило, линии делают конфигурируемыми, то есть такими, которые, в зависимости от настроек, могут использоваться или как GPIO, или как специальные. Типовую внутреннюю структуру канала ввода-вывода контроллера stm32 можно видеть на рисунке ниже.

Структурная схема канала ввода-вывода контроллера stm32

Всего возможно 8 конфигураций линий ввода-вывода:

  1. Выход GPIO:
    1. push-pull (работают оба выходных транзистора)
    2. open-drain (работает только N-FET, а P-FET всегда разомкнут)

  2. Альтернативный выход (выход периферийного модуля):
    1. push-pull
    2. open-drain
  3. Вход:
    1. Analog, высокоимпедансный вход (подтягивающие резисторы и триггер Шмитта отключены, используется для АЦП)
    2. Floating, высокоимпедансный вход (подтягивающие резисторы отключены, триггер Шмидта включен)
    3. Pull-up, вход с подтяжкой к питанию (через специальный встроенный резистор)
    4. 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. Часть 1. Установка MDK, создание проекта, основы Keil uVision
  2. Часть 2. Команды и директивы ассемблера, структура и синтаксис программы. Первая программа для STM32.
  3. Часть 3. Карта памяти контроллеров STM32, методы работы с памятью.
  4. Часть 4. Регистры, старт и режимы работы контроллеров STM32.
  5. Часть 5. Как залить прошивку в контроллер.
  6. Часть 6. Настройка системы тактирования.
  7. Часть 7. Работа с портами ввода-вывода.
  8. Приложение 1. Набор инструкций THUMB-2 и особенности их использования.

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