LINUX.ORG.RU

Осваиваем STM32 снизу: часть 3 - мигаем светодиодом

 ,


0

1

Часть 1 Часть 2 Часть 3 Часть 4 Часть 5 Часть 6 Часть 7 Часть 8 Часть 9

Часть 3: мигаем светодиодом

Мигание светодиодом это традиционный hello world для микроконтроллеров. Это один из самых простых способов взаимодействия с окружающей средой без помощи отладчика. В этой части именно этим мы и займёмся.

Сразу оговоримся, что эта часть и далее уже очень сильно зависят от конкретного процессора и даже платы. Все адреса приведены со ссылками на reference manual, что должно помочь в переводе кода на другие процессоры.

На плате blue pill синий светодиод подключен анодом (плюсом) к напряжению на 3.3 вольта, а катодом (минусом) к выводу микроконтролера, обозначенному C13 / 2 / PC13 / TAMPERRTC. Большинство выводов микроконтроллера способны выполнять разные задачи, отсюда и несколько названий. Теперь откроем Datasheet на микроконтроллер, и найдём этот вывод там. На плате видно, что у микросхемы имеется 48 выводов, по 12 на каждой стороне. Этому соответствует тип корпуса LQFP48, его можно увидеть в разделе 3, изображении 8. Там же видно, что у этого корпуса действительно есть вывод с номером 2 и функционалом PC13-TAMPER-RTC. Соответствие между платы и datasheet контролера мы провели и теперь точно знаем, с каким выводом будем работать.

Для того, чтобы реализовать этот функционал, микроконтроллер имеет набор GPIO портов. Порт может быть сконфигурирован в режиме входа (input), в этом случае он определяет напряжение между соответствующим выводом и «землёй» в бинарном виде (0 это отсутствие напряжения, 1 это номинальное напряжение). Но нас интересует конфигурация порта в режиме выхода (output). В этом случае микроконтроллер для состояния 0 подключает вывод к земле, а для состояния 1 либо соединяет вывод с источником напряжения в режиме двухтактного выхода (push-pull output), либо отключает вывод от схемы в режиме выхода с открытым коллектором (open drain output).

В применении к нашей схеме 0 соединит выход микроконтроллера с землёй, а значит и катод светодиода. Через светодиод потечёт ток и он включится. 1 в любом режиме отключит светодиод.

Интересующий нас вывод микроконтроллера называется PC13, это можно раскрыть как Port C 13. У микроконтроллера имеется несколько портов ввода-вывода и каждый порт способен работать с несколькими выводами. Другое обозначение порта C это GPIOC. Если посмотреть в Reference Manual, разделе 3.1 изображение 1 (System Architecture), можно увидеть, что устройство GPIOC подключено через шину APB2 (Advanced Peripheral Bus). Это важно понимать, чтобы потом найти нужные пункты в документации.

При загрузке у микроконтроллера отключены практически все периферийные устройства. Поэтому первое, что нужно сделать, это включить порт GPIOC. Далее нужно настроить вывод C13 как выход с открытым коллектором. По сути это все подготовительные действия. После этого можно управлять выводом C13, включая и выключая светодиод. Для задержки между включением и выключением будем использовать «пустой» цикл, который займёт процессор работой на нужное число миллисекунд.

Проект возьмём из второй части, переименовав loop в blink.

Итак первое, что нужно сделать, это включить порт GPIOC. Для включения периферийных устройств в микроконтроллере имеется RCC (reset and clock control, управление сбросом и тактированием). Взаимодействие с RCC осуществляется через запись нужных значений в регистры RCC. Подробное описание этих регистров можно почитать в Reference Manual, разделе 8.3. В разделе 8.3.7 описан регистр RCC_APB2ENR, включающий устройства на шине APB2. В частности бит 4 IOPCEN отвечает именно за порт ввода-вывода C. Регистр RCC_APB2ENR имеет смещение 0x18 от начала адресного пространства, выделенного для RCC. Если посмотреть в Reference Manual, разделе 3.3, можно найти, что для RCC выделено адресное пространство 0x4002_1000-0x4002_13FF. Резюмируя: нам нужно проставить единицу в 4-й бит по адресу 0x4002_1000 + 0x18.

На языке ассемблера это действие будет записываться следующим образом:

// enable I/O port C clock
ldr r0, =0x40021000 + 0x18 // RCC_APB2ENR
ldr r1, [r0]
orr r1, 1 << 4 // IOPCEN
str r1, [r0]

Этому коду соответствует псевдокод

// enable I/O port C clock
r0 := 0x4002_1000 + 0x18;
r1 := memory[r0];
r1[4] := 1;
memory[r0] := r1;

После этого нужно настроить вывод С13. В reference manual, разделе 9.2.2 описан регистр GPIOx_CRH со смещением 0x04, который позволяет настраивать этот вывод: биты 20 и 21 с названием MODE13 и биты 22 и 23 с названием CNF13. В разделе 9.1, таблице 21 подробно описаны все сочетания значений. Для нашего случая нужно установить CNF1 в 0, CNF0 в 1, MODE1 в 1, MODE0 в 0. Это настроит порт в режим general purpose output, open-drain, maximum output speed 2 MHz. В reference manual, разделе 3.3 можно найти адреса для GPIO Port C: 0x4001 1000 - 0x4001 13FF. Таким образом нам нужно модифицировать машинное слово по адресу 0x4001 1000 + 0x04.

Для наглядности приведём то, что нам нужно сделать. ? означает, что мы не меняем этот бит.

                        +------------------------------ CNF13, bit 1
                        |+----------------------------- CNF13, bit 0
                        ||+---------------------------- MODE13, bit 1
                        |||+--------------------------- MODE13, bit 0
                        ||||
0x4001 1004: ???? ????  0110 ????  ???? ????  ???? ????

На ассемблере это запишется так:

// configure PC13 as open-drain output with 10 MHz speed
ldr r0, =0x40011000 + 0x04 // GPIOC_CRH
ldr r1, [r0]
bic r1, 1 << 20 // MODE13:0
orr r1, 1 << 21 // MODE13:1
orr r1, 1 << 22 // CNF13:0
bic r1, 1 << 23 // CNF13:1
str r1, [r0]

Этому коду соответствует псевдокод

// configure PC13 as open-drain output with 10 MHz speed
r0 := 0x4001_1000 + 0x04;
r1 := memory[r0];
r1[20] := 0;
r1[21] := 1;
r1[22] := 1;
r1[23] := 0;
memory[r0] := r1;

Этого кода хватает, чтобы включить светодиод (т.к. по умолчанию порт имеет состояние 0).

Для того, чтобы светодиод начал мигать, нужно ещё немножко кода. Управление портом выхода осуществляется через регистр GPIOx_ODR, имеющий смещение 0x0c. Порту 13, как несложно догадаться, соответствует бит 13. Для задержки используем пустой цикл из миллиона итераций, он занимает примерно половину секунды.

В итоге у нас получился такой код для мигания:

blink_loop:

// wait loop
ldr r0, =1000000
delay_loop:
subs r0, 1
bne delay_loop

// toggle PC13
ldr r0, =0x40011000 + 0x0c // GPIOC_ODR
ldr r1, [r0]
eor r1, 1 << 13 // ODR13
str r1, [r0]

b blink_loop

Ему соответствует псевдокод

blink_loop:

// wait loop
r0 := 1000000;
delay_loop:
r0 := r0 - 1;
if r0 != 0 goto delay_loop;

// toggle PC13
r0 := 0x4001_1000 + 0x0c;
r1 := memory[r0];
r1[13] := ~r1[13];
memory[r0] := r1;

goto blink_loop;

Прошиваем его командой make flash и если всё сделано правильно, то синий светодиод замигает.

Полный код доступен на гитхабе.

★★★★

Проверено: hobbit ()
Последнее исправление: hobbit (всего исправлений: 3)

В тексте:

Это настроит порт в режим general purpose output, open-drain, maximum output speed 2 MHz.

В коде:

// configure PC13 as open-drain output with 10 MHz speed

Если я нигде не промахнулся с подсчетом бит, то коммент в коде ошибочен, а текст - корректен

Barracuda72 ★★
()

По поводу «миллион итераций занимает примерно полсекунды», если кому интересно.

subs выполняется за 1 такт, bne - за 1+2 (1 на вычисление условия, 2 на перезагрузку конвейера если переход выполняется). Итого 4 такта за итерацию цикла или 4 миллиона тактов за весь цикл.

По умолчанию STM32F103xxxx тактуется от внутреннего источника на 8 MHz +/- 1%, если я верно понял (подробности чудес с разными источниками и всякими предделителями оставлю автору, вероятно, в дальнейших статьях он это расскажет или уже рассказал).

Вот и выходит примерно полсекунды.

У меня, правда, не прокатило - полсекунды проходят за ~1333333 итераций цикла. Либо настройки платы по умолчанию другие, либо китайский 1% отклонения очень уж китайский. Почему и полез считать.

Barracuda72 ★★
()
Ответ на: комментарий от Barracuda72

Хотя нет, доки я мугичкой читал, видимо.

Taken branches with an immediate are normally 1 cycle of pipeline reload (2 cycles total)

Т.е. bne в примере занимает 1+1 = 2 такта, итерация цикла - 3 такта и в итоге выходит 3 миллиона тактов за весь цикл из 1000000 итераций.

Значит, для почти точных «полсекунды» нужны как раз 1333333 итераций. Все с моей китайской платой нормально.

Выдохнул

Barracuda72 ★★
()
Ответ на: комментарий от Barracuda72

В 6 части я эти подсчёты тоже сделал, 375 мс вышло (на миллион итераций). Про расхождения в тексте и комментарии спасибо, исправлю.

vbr ★★★★
() автор топика
Последнее исправление: vbr (всего исправлений: 2)
Ответ на: комментарий от vbr

Ага, значит, я просто забежал вперед паровоза. Удивился, что диод мигает заметно быстрее рассчетного, вот и решил проверить.

Так-то получается, что можно просто однотактовый nop добавить в цикл ожидания и выйдет ровно 500 мс на миллион итераций. Чтоб уж красивое круглое число не портить.

Barracuda72 ★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.