Хабрахабр

Читаем даташиты 2: SPI на STM32; ШИМ, таймеры и прерывания на STM8

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

Все демо-проекты посвящены моим любимым светодиодам, зажигать мы их будем в больших количествах, для чего придется задействовать всякую интересную периферию. Сегодня я покажу, как с помощью даташитов решить довольно простые, но необходимые для множества проектов задачи на контроллерах STM32 (Blue Pill) и STM8.

Текст опять получился огромный, поэтому для удобства делаю содержание:

STM32 Blue Pill: 16 светодиодов с драйвером DM634
STM8: Настраиваем шесть выводов ШИМ
STM8: 8 RGB-светодиодов на трех пинах, прерывания

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

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

Надеюсь, что моя статья поможет кому-то на похожем этапе погружения в хобби.

С помощью даташитов разберемся с драйвером, IO-портами STM и настроим SPI. Небольшой проект с использованием Blue Pill (STM32F103C8T6) и светодиодного драйвера DM634.

DM634

Младшая 12-битная модель известна по отечественному проекту Lightpack. Тайваньский чип с 16-ю 16-битными ШИМ-выходами, можно соединять в цепочки. Ну и, конечно, было интересно самому научиться управлять чипом, а не использовать готовую библиотеку. В свое время, выбирая между DM63x и хорошо известным TLC5940, остановился на DM по нескольким причинам: 1) TLC на Алиэкспрессе точно поддельный, а этот – нет; 2) у DM автономный ШИМ со своим генератором частоты; 3) его можно было недорого купить в Москве, а не ждать посылки с Али. Чипы сейчас в основном представлены в корпусе SSOP24, их несложно припаять на переходник.

Сперва смотрим на распиновку (Pin Connection), чтобы понять, к какой ноге что подключать, и описание пинов (Pin Description). Поскольку производитель тайваньский, даташит к чипу написан на китайском английском, а значит, будет весело. 16 выводов:


Источники втекающего постоянного тока (открытый сток)

Электрически это, конечно, никакой не «открытый сток» (open drain), но в даташитах такое обозначение для выводов в режиме стока встречается часто. Sink / Open-drain output – сток; источник втекающего тока; выход, в активном состоянии подключенный к земле, – светодиоды к драйверу подключаются катодами.


Внешние резисторы между REXT и GND для установки значения выходного тока

график на стр. Между пином REXT и землей устанавливается референсный резистор, контролирующий внутреннее сопротивление выходов, см. В DM634 этим сопротивлением можно также управлять программно, устанавливая общую яркость (global brightness); в этой статье вдаваться в подробности не буду, просто поставлю сюда резистор на 2. 9 даташита. 2 – 3 кОм.

Чтобы понять, как управлять чипом, посмотрим на описание интерфейса устройства:

Перевести это проблематично, понять при желании можно, но есть другой путь – взглянуть, как описывается подключение в даташите к функционально близкому TLC5940: Ага, вот он, китайский английский во всей красе.

Передний фронт сигнала SCLK сдвигает данные с пина SIN во внутренний регистр.
… Для ввода данных в устройство требуются только три пина. Внутренние регистры – срабатывающие по уровню сигнала XLAT задвижки. После того, как все данные загружены, короткий высокий сигнал XLAT фиксирует последовательно переданные данные во внутренних регистрах. Все данные передаются старшим битом вперед.

Latch – задвижка/защелка/фиксатор.
Rising edge – передний фронт импульса
MSB first – старшим (крайним левым) битом вперед.
to clock data – передавать данные последовательно (побитно).

Слово latch часто встречается в документации к чипам и переводится разнообразно, поэтому для понимания позволю себе

небольшой ликбез

«Сдвиг» (shift) в названии – побитное перемещение данных внутри устройства: каждый новый засунутый внутрь бит пихает всю цепочку перед собой вперед. LED-драйвер – по сути сдвиговый регистр. Когда все готово, заслонка открывается, и биты отправляются работать, заменяя предыдущую партию. Поскольку во время сдвига никто не хочет наблюдать хаотичное мигание светодиодов, процесс происходит в буферных регистрах, отделенных от рабочих заслонкой (latch) – это своего рода предбанник, где биты выстраиваются в нужную последовательность. Слово latch в документации к микросхемам почти всегда подразумевает такую заслонку, в каких бы сочетаниях оно ни использовалось.

Это можно сделать вручную (bit-bang), но лучше воспользоваться специально под это заточенным интерфейсом SPI, благо он представлен на нашем STM32 в двух экземплярах. Итак, передача данных в DM634 осуществляется так: выставляем вход DAI в значение старшего бита дальнего светодиода, дергаем DCK вверх-вниз; выставляем вход DAI в значение следующего бита, дергаем DCK; и так далее, пока все биты не будут переданы (clocked in), после чего дергаем LAT.

Синяя Таблетка STM32F103

При этом из соображений энергосбережения на старте у них отключена почти вся периферия, а тактовая частота составляет 8 МГц от внутреннего источника. Вводные: контроллеры STM32 – значительно сложнее Atmega328, чем могут пугать. А вот включить периферию придется. К счастью, программисты STM написали код, доводящий чип до «расчетных» 72 МГц, а авторы всех известных мне IDE включили его в процедуру инициализации, поэтому тактировать нам не нужно (но можно, если очень хочется).

Документация: на Blue Pill установлен популярный чип STM32F103C8T6, к нему есть два полезных документа:

  • Data Sheet для микроконтроллеров STM32F103x8 и STM32F103xB;
  • Reference Manual для всей линейки STM32F103 и не только.

В даташите нам могут быть интересны:

  • Pinouts – распиновки чипов – на тот случай, если мы решим делать платы сами;
  • Memory Map – карта памяти для конкретного чипа. В Reference Manual есть карта для всей линейки, в ней упомянуты регистры, которых нет на нашем.
  • Таблица Pin Definitions – перечисление основных и альтернативных функций пинов; для «синей таблетки» в интернете можно найти более удобные картинки со списком пинов и их функциями. Поэтому немедленно гуглим Blue Pill pinout и держим вот такую картинку под рукой:

Даташит убираем, открываем Reference Manual, отныне пользуемся только им.
Порядок действий: разбираемся со стандартным вводом/выводом, настраиваем SPI, включаем нужную периферию.

Ввод-вывод

Сейчас нам нужны только выводы, но даже их имеется четыре варианта: На Atmega328 ввод-вывод реализован предельно просто, из-за чего обилие опций STM32 может сбить с толку.


вывод с открытым стоком, вывод «тяни-толкай», альтернативный «тяни-толкай», альтернативный открытый сток

А вот с «открытым стоком» возникают сложности, хотя на самом деле тут все просто: «Тяни-толкай» (push-pull) – привычный вывод с Ардуины, пин может принимать значение либо HIGH, либо LOW.



Конфигурация вывода / когда порт назначен на вывод: / включен буфер вывода: / – режим открытого стока: «0» в выводном регистре активирует N-MOS, «1» в выводном регистре оставляет порт в режиме Hi-Z (P-MOS не активируется) / – режим «тяни-толкай»: «0» в выводном регистре активирует N-MOS, «1» в выводном регистре активирует P-MOS.

При записи нуля пин в обоих режимах ведет себя одинаково, как логически, так и электрически. Все отличие открытого стока (open drain) от «тяни-толкай» (push-pull) состоит в том, что в первом пин не может принять состояние HIGH: при записи единицы в выводной регистр он переходит в режим высокого сопротивления (high impedance, Hi-Z).

В «альтернативном» им управляет соответствующая периферия (см. В обычном режиме вывода пин просто транслирует содержимое выводного регистра. 1. 9. 4):


Если бит порта сконфигурирован как вывод альтернативной функции, выводной регистр отключается, а пин подключается к выводному сигналу периферии

На вопрос, что делать, если у пина несколько альтернативных функций, ответ дает сноска в даташите: Альтернативный функционал каждого пина описан в Pin Definitions даташита и есть на скачанной картинке.


Если несколько периферийных блоков используют один и тот же пин, во избежание конфликта между альтернативными функциями одновременно следует использовать только один периферийный блок, переключаясь с помощью бита активации тактирования периферии (в соответствующем регистре RCC).

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

Но прежде, чем их назначать, разберемся со SPI. Итак: мы используем SPI, значит, два пина (с данными и с тактовым сигналом) должны быть «альтернативная функция тяни-толкай», а еще один (LAT) – «обычный тяни-толкай».

SPI

Еще небольшой ликбез

Принцип его работы уже описан выше, там, где про китайский LED-драйвер (в reference manual см раздел 25). SPI или Serial Peripherial Interface (последовательный периферийный интерфейс) – простой и весьма эффективный интерфейс для связи МК с другими МК и вообще внешним миром. У SPI есть четыре базовых канала, из которых задействованы могут быть не все: SPI может работать в режиме мастера («хозяина») и слейва («раба»).

  • MOSI, Master Output / Slave Input: этот пин в режиме мастера отдает, а в режиме слейва принимает данные;
  • MISO, Master Input / Slave Output: наоборот, в мастере принимает, в слейве – отдает;
  • SCK, Serial Clock: задает частоту передачи данных в мастере или принимает тактовый сигнал в слейве. По сути, отбивает биты;
  • SS, Slave Select: с помощью этого канала слейв узнает, что от него что-то хотят. На STM32 называется NSS, где N = negative, т.е. контроллер становится слейвом, если в этом канале земля. Хорошо комбится с режимом Open Drain Output, но это другая история.

Например, он умеет работать не только SPI, но и I2S-интерфейсом, причем в документации их описания идут вперемешку, надо своевременно отсекать лишнее. Как и все остальное, SPI на STM32 богат функционалом, что несколько осложняет его понимание. Идем в раздел 25. У нас же задача крайне простая: надо всего лишь отдавать данные, задействуя только MOSI и SCK. 4 (half-duplex communication, полудуплексная связь), где находим 1 clock and 1 unidirectional data wire (1 тактовый сигнал и 1 однонаправленный поток данных): 3.

/ Режим только передачи похож на дуплексный режим: данные передаются по передающему пину (MOSI в режиме мастера или MISO в режиме слейва), а принимающий пин (MISO или MOSI соответственно) может использоваться как обычный пин ввода-вывода.
В этом режиме приложение использует SPI либо в режиме только передачи, либо только приема. В этом случае приложению достаточно игнорировать буфер Rx (если его прочитать, там не будет переданных данных).

Разберемся со Slave Select, которым на STM32 можно управлять программно, что необычайно удобно. Отлично, пин MISO у нас освободился, подключим к нему сигнал LAT. 3. Читаем одноименный абзац раздела 25. 1 SPI General Description:

Внешний пин NSS остается свободным для других нужд приложения.
Программное управление NSS (SSM = 1) / Информация о выборе слейва содержится в бите SSI регистра SPI_CR1.

Я решил использовать SPI2, ищем в даташите его базовый адрес – в разделе 3. Пора писать в регистры. 3 Memory Map (Карта памяти):

Ну и начинаем:

#define _SPI2_(mem_offset) (*(volatile uint32_t *)(0x40003800 + (mem_offset)))

3. Открываем раздел 25. 3 с говорящим названием «Настройка SPI в режиме мастер»:

Установите тактовую частоту последовательного интерфейса битами BR[2:0] в регистре SPI_CR1. 1.

Сдвиг адреса (Address offset) у CR1 – 0x00, по умолчанию все биты сброшены (Reset value 0x0000): Регистры собраны в одноименном разделе reference manual.

Частота STM32 у нас будет 72 МГц, LED-драйвер, согласно его даташиту, работает с частотой до 25 МГц, таким образом, делить надо на четыре (BR[2:0] = 001). Биты BR устанавливают делитель тактовой частоты контроллера, определяя таким образом частоту, на которой будет работать SPI.

#define _SPI_CR1 0x00 #define BR_0 0x0008
#define BR_1 0x0010
#define BR_2 0x0020 _SPI2_ (_SPI_CR1) |= BR_0;// pclk/4

Установите биты CPOL и CPHA, чтобы определить отношения между передачей данных и тактированием последовательного интерфейса (см. 2. схему на стр 240)

704 (SPI General Description): Поскольку мы тут читаем даташит, а не рассматриваем схемы, давайте лучше изучим текстовое описание битов CPOL и CPHA на стр.

Бит CPOL (полярность тактового сигнала) управляет состоянием тактового сигнала, когда данные не передаются.
Фаза и полярность тактового сигнала
С помощью битов CPOL и CPHA регистра SPI_CR1 можно программно выбрать четыре варианта отношений таймингов. Если CPOL сброшен, пин SCK в режиме покоя находится в низком уровне. Этот бит управляет режимами мастер и слейв. Данные фиксируются по второму изменению тактового сигнала. Если бит CPOL установлен, пин SCK в режиме покоя находится в высоком уровне.
Если установлен бит CPHA (фаза тактового сигнала), стробом-ловушкой старшего бита выступает второй фронт сигнала SCK (нисходящий, если CPOL сброшен, или восходящий, если CPOL установлен). Данные фиксируются по первому изменению тактового сигнала. Если бит CPHA сброшен, стробом-ловушкой старшего бита выступает передний фронт сигнала SCK (нисходящий, если CPOL установлен, или восходящий, если CPOL сброшен).

нам надо, чтобы сигнал SCK оставался низким, когда не используется, а данные передавались по переднему фронту импульса (см. Вкурив в эти знания, приходим к выводу, что оба бита должны остаться нулями, т.к. Rising Edge в даташите DM634).

Кстати, здесь мы впервые столкнулись с особенностью лексики в даташитах ST: в них фраза «сбросить бит в ноль» – пишется to reset a bit, а не to clear a bit, как, например, у Атмеги.

Установите бит DFF для определения 8-битного или 16-битного формата блока данных 3.

DFF имеет смысл поставить в единицу: Я специально взял 16-битный DM634, чтобы не заморачиваться с передачей 12-битных данных ШИМ, как у DM633.

#define DFF 0x0800 _SPI2_ (_SPI_CR1) |= DFF; // 16-bit mode

Сконфигурируйте бит LSBFIRST в регистре SPI_CR1 для определения формата блока 4.

Но DM634 хочет получать данные, начиная со старшего бита. LSBFIRST, как видно из его названия, настраивает передачу младшим битом вперед. Поэтому оставляем сброшенным.

В аппаратном режиме, если требуется ввод с пина NSS, подавайте на пин NSS высокий сигнал во время всей последовательности передачи байтов. 5. Если пин NSS должен работать на вывод, надо установить только бит SSOE. В программном режиме NSS установите биты SSM и SSI в регистре SPI_CR1.

Устанавливаем SSM и SSI, чтобы забыть про аппаратный режим NSS:

#define SSI 0x0100
#define SSM 0x0200 _SPI2_ (_SPI_CR1) |= SSM | SSI; //enable software control of SS, SS high

Должны быть установлены биты MSTR и SPE (они остаются установленными только если на NSS подается высокий сигнал) 6.

Собственно, этими битами мы назначаем наш SPI мастером и включаем его:

#define MSTR 0x0004
#define SPE 0x0040 _SPI2_ (_SPI_CR1) |= MSTR; //SPI master
//когда все готово, включаем SPI
_SPI2_ (_SPI_CR1) |= SPE;

Продолжаем читать 25. SPI настроен, давайте сразу напишем функции, отправляющие байты драйверу. 3 «Настройка SPI в режиме мастер»: 3.

Флаг TXE устанавливается после передачи данных из буфера Tx в сдвиговый регистр, а также создается прерывание, если установлен бит TXEIE в регистре CPI_CR1.
Порядок передачи данных
Передача начинается когда в буфер Tx записывается байт.
Байт данных загружается в сдвиговый регистр в параллельном режиме (из внутренней шины) во время передачи первого бита, после чего передается в последовательном режиме пину MOSI, первым или последним битом вперед в зависимости от установки бита LSBFIRST в регистре CPI_CR1.

На Атмеге флаг TXE (Tx Empty, Tx пуст и готов принимать данные) устанавливается только после того, как весь байт отправился наружу. Я выделил несколько слов в переводе, чтобы обратить внимание на одну особенность реализации SPI в контроллерах STM. Поскольку пихается он туда всеми битами одновременно (параллельно), а дальше данные передаются последовательно, TXE устанавливается до того, как байт полностью отправится. А здесь этот флаг устанавливается после того, как байт оказался засунут во внутренний сдвиговый регистр. в случае нашего LED-драйвера нам надо дернуть пин LAT после отправки всех данных, т.е. Это важно, т.к. только флага TXE нам будет недостаточно.

Посмотрим в 25. А это значит, что нам нужен еще какой-то флаг. 7 – «Флаги статусов»: 3.

Флаг BSY показывает состояние коммуникативного слоя SPI.
Он сбрасывается:
когда передача завершена (кроме режима мастера, если передача непрерывна)
когда SPI отключен
когда происходит ошибка режима мастера (MODF=1)
Если передача не непрерывна, флаг BSY сброшен между каждой передачей данных
<...>

Флаг BUSY
Флаг BSY устанавливается и сбрасывается аппаратно (запись в него ни на что не влияет).

Выясняем, где находится буфер Tx. Окей, пригодится. Для этого читаем «Регистр данных SPI»:

Запись в регистр данных пишет в буфер Tx, а чтение из регистра данных вернет значение, содержащееся в буфере Rx.
Биты 15:0 DR[15:0] Регистр данных
Полученные данные или данные для передачи.
Регистр данных разделен на два буфера – один для записи (буфер передачи) и второй для чтения (буфер приема).

Ну и регистр статусов, где найдутся флаги TXE и BSY:

Пишем:

#define _SPI_DR 0x0C
#define _SPI_SR 0x08 #define BSY 0x0080
#define TXE 0x0002 void dm_shift16(uint16_t value)
{ _SPI2_(_SPI_DR) = value; //send 2 bytes while (!(_SPI2_(_SPI_SR) & TXE)); //wait until they're sent
}

Ну а поскольку нам надо передать 16 раз по два байта, по числу выходов LED-драйвера, то как-то так:

void sendLEDdata()
while (k); while (_SPI2_(_SPI_SR) & BSY); // finish transmission LAT_pulse();
}

Но мы пока не умеем дергать пин LAT, поэтому вернемся в I/O.

Назначаем пины

Понятно, что их больше, чем у Атмеги, но они еще и отличаются от других чипов STM. У STM32F1 регистры, отвечающие за состояние пинов, довольно необычны. 1 Общее описание GPIO: Раздел 9.


Каждый из портов ввода/вывода общего назначения (GPIO) обладает двумя 32-битными регистрами конфигурации (GPIOx_CRL и GPIOx_CRH), двумя 32-битными регистрами данных (GPIOx_IDR и GPIOx_ODR), 32-битным регистром установки/сброса (GPIOx_BSRR), 16-битным регистром сброса (GPIOx_BRR) и 32-битным блокирующим регистром (GPIOx_LCKR).

Т.е. Необычны, а также довольно неудобны, здесь первые два регистра, потому что 16 пинов порта разбросаны по ним в формате «по четыре бита на брата». При этом остальные регистры успешно умещают в себя биты всех пинов порта – часто оставаясь наполовину «зарезервированными». пины с нулевого по седьмой сидят в CRL, а остальные – в CRH.

Для простоты начнем с конца списка.

Блокирующий регистр нам не потребуется.

Мне по душе второй вариант. Регистры установки и сброса довольно забавны тем, что частично дублируют друг друга: можно все писать только в BSRR, где старшие 16 битов будут сбрасывать пин в ноль, а младшие – устанавливать в 1, либо использовать также BRR, младшие 16 битов которого только сбрасывают пин. Эти регистры важны тем, что обеспечивают атомарный доступ к пинам:

Это достигается записью «1» в регистр установки/сброса (GPIOx_BSRR или, только для сброса, в GPIOx_BRR) бита, который требуется изменить.

Атомарная установка или сброс
Не нужно отключать прерывания при программировании GPIOx_ODR на битовом уровне: можно изменять один или несколько битов одной атомарной операцией записи APB2. Прочие биты останутся неизменными.

В нынешнем проекте они нам не потребуются. Регистры данных имеют вполне говорящие названия – IDR = Input Direction Register, регистр ввода; ODR = Output Direction Register, регистр вывода.

Поскольку нам интересны пины второго SPI, а именно PB13, PB14 и PB15, сразу смотрим на CRH: Ну и, наконец, управляющие регистры.

И видим, что надо будет что-то написать в биты с 20-го по 31-й.

оба пина в «1»), а CNF задает режим: обычный «тяни-толкай» – 00, «альтернативный» – 10. Мы уже выше разобрались с тем, что мы хотим от пинов, поэтому тут я обойдусь без скриншота, просто скажу, что MODE задает направление (ввод, если оба бита выставлены в 0) и скорость пина (нам нужно 50MHz, т.е. По умолчанию, как мы видим выше, у всех пинов прописан третий снизу бит (CNF0), он устанавливает их в режим floating input.

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

Ну вот как-то так

#define CNF0_0 0x00000004
#define CNF0_1 0x00000008
#define CNF1_0 0x00000040
#define CNF1_1 0x00000080
#define CNF2_0 0x00000400
#define CNF2_1 0x00000800
#define CNF3_0 0x00004000
#define CNF3_1 0x00008000
#define CNF4_0 0x00040000
#define CNF4_1 0x00080000
#define CNF5_0 0x00400000
#define CNF5_1 0x00800000
#define CNF6_0 0x04000000
#define CNF6_1 0x08000000
#define CNF7_0 0x40000000
#define CNF7_1 0x80000000
#define CNF8_0 0x00000004
#define CNF8_1 0x00000008
#define CNF9_0 0x00000040
#define CNF9_1 0x00000080
#define CNF10_0 0x00000400
#define CNF10_1 0x00000800
#define CNF11_0 0x00004000
#define CNF11_1 0x00008000
#define CNF12_0 0x00040000
#define CNF12_1 0x00080000
#define CNF13_0 0x00400000
#define CNF13_1 0x00800000
#define CNF14_0 0x04000000
#define CNF14_1 0x08000000
#define CNF15_0 0x40000000
#define CNF15_1 0x80000000 #define MODE0_0 0x00000001
#define MODE0_1 0x00000002
#define MODE1_0 0x00000010
#define MODE1_1 0x00000020
#define MODE2_0 0x00000100
#define MODE2_1 0x00000200
#define MODE3_0 0x00001000
#define MODE3_1 0x00002000
#define MODE4_0 0x00010000
#define MODE4_1 0x00020000
#define MODE5_0 0x00100000
#define MODE5_1 0x00200000
#define MODE6_0 0x01000000
#define MODE6_1 0x02000000
#define MODE7_0 0x10000000
#define MODE7_1 0x20000000
#define MODE8_0 0x00000001
#define MODE8_1 0x00000002
#define MODE9_0 0x00000010
#define MODE9_1 0x00000020
#define MODE10_0 0x00000100
#define MODE10_1 0x00000200
#define MODE11_0 0x00001000
#define MODE11_1 0x00002000
#define MODE12_0 0x00010000
#define MODE12_1 0x00020000
#define MODE13_0 0x00100000
#define MODE13_1 0x00200000
#define MODE14_0 0x01000000
#define MODE14_1 0x02000000
#define MODE15_0 0x10000000
#define MODE15_1 0x20000000

Наши пины находятся на порту B (базовый адрес – 0x40010C00), код:

#define _PORTB_(mem_offset) (*(volatile uint32_t *)(0x40010C00 + (mem_offset))) #define _BRR 0x14
#define _BSRR 0x10
#define _CRL 0x00
#define _CRH 0x04 //используем стандартный SPI2: MOSI на B15, CLK на B13
//LAT пусть будет на неиспользуемом MISO – B14 //очищаем дефолтный бит, он нам точно не нужен
_PORTB_ (_CRH) &= ~(CNF15_0 | CNF14_0 | CNF13_0 | CNF12_0); //альтернативные функции для MOSI и SCK
_PORTB_ (_CRH) |= CNF15_1 | CNF13_1; //50 МГц, MODE = 11
_PORTB_ (_CRH) |= MODE15_1 | MODE15_0 | MODE14_1 | MODE14_0 | MODE13_1 | MODE13_0;

И, соответственно, можно написать дефайны для LAT, который будет дергаться регистрами BRR и BSRR:

/*** LAT pulse – high, then low */
#define LAT_pulse() _PORTB_(_BSRR) = (1<<14); _PORTB_(_BRR) = (1<<14) #define LAT_low() _PORTB_(_BRR) = (1<<14)

(LAT_low просто по инерции, как-то всегда было, пусть себе останется)

Потому что это STM32, тут экономят электричество, а значит, надо включить тактирование нужной периферии. Теперь все уже здорово, только не работает.

Включаем тактирование

И мы уже могли заметить аббревиатуру RCC. За тактирование отвечают часики, они же Clock. Ищем ее в документации: это Reset and Clock Control (Управление сбросом и тактированием).

Нам нужны всего лишь регистры, отвечающие за включение тактирования периферии (Peripheral Clock Enable Registers). Как выше было сказано, к счастью, самое сложное из темы тактирования за нас сделали люди из STM, за что им большое спасибо (еще раз дам ссылку на сайт Di Halt'а, чтобы было понятно, насколько это заморочено). Для начала найдем базовый адрес RCC, он в самом начале «Карты памяти»:

#define _RCC_(mem_offset) (*(volatile uint32_t *)(0x40021000 + (mem_offset)))

Где мы найдем RCC_APB1ENR и RCC_APB2ENR: А дальше либо кликнуть по ссылке, где пытаться в табличке что-то найти, либо, гораздо лучше, пробежаться по описаниям включающих регистров из разделов про enable registers.


И в них, соответственно, биты, включающие тактирование SPI2, IOPB (I/O Port B) и альтернативных функций (AFIO).

#define _APB2ENR 0x18
#define _APB1ENR 0x1C #define IOPBEN 0x0008
#define SPI2EN 0x4000
#define AFIOEN 0x0001 //включаем тактирование порта B и альт. функций
_RCC_(_APB2ENR) |= IOPBEN | AFIOEN; //включаем тактирование SPI2
_RCC_(_APB1ENR) |= SPI2EN;

Финальный код можно найти тут.

Питаем драйвер от 5 вольт, не забываем объединить земли. Если есть возможность и желание потестить, то подключаем DM634 так: DAI к PB15, DCK к PB13, LAT к PB14.

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

Программируется STM8 на C в страшненькой IDE ST Visual Desktop. К чипу также есть даташит и reference manual RM0016, в первом распиновка и адреса регистров, во втором – все остальное.

Тактирование и ввод-вывод

По умолчанию STM8 работает на частоте 2 МГц, это надо сразу исправить.

Он задается в регистре делителя тактового сигнала (CLK_CKDIVR).
Примечание: на старте ведущим источником тактового сигнала выбирается HSI RC-генератор с делителем 8.
Тактовый сигнал HSI (скоростной внутренний)
Тактовый сигнал HSI получается от внутреннего 16-МГц RC-генератора с программируемым делителем (от 1 до 8).

Находим адрес регистра в даташите, описание в refman и видим, что регистр надо очистить:

#define CLK_CKDIVR *(volatile uint8_t *)0x0050C6 CLK_CKDIVR &= ~(0x18);

Поскольку мы собираемся запускать ШИМ и подключать светодиоды, смотрим распиновку:

То, что в квадратных скобках – «альтернативный функционал», он переключается «байтами опций» (option bytes) – что-то вроде фьюзов Атмеги. Чип маленький, многие функции подвешены на одни и те же пины. активируется новый функционал только после перезагрузки. Менять их значения можно программно, но не нужно, т.к. В распиновке видно, что выводы CH1 и CH2 первого таймера спрятаны в квадратные скобки; надо в STVP проставить биты AFR1 и AFR0, причем второй также перенесет вывод CH1 второго таймера с PD4 на PC5. Проще воспользоваться ST Visual Programmer (качается вместе с Visual Desktop), умеющим менять эти байты.

Таким образом, управлять светодиодами будут 6 пинов: PC6, PC7 и PC3 для первого таймера, PC5, PD3 и PA3 для второго.

Настройка самих пинов ввода-вывода на STM8 проще и логичнее, чем на STM32:

  • знакомый по Atmega регистр направления данных DDR (Data Direction Register): 1 = вывод;
  • первый контрольный регистр CR1 при выводе задает режим «тяни-толкай» (1) или открытый сток (0); поскольку я подключаю светодиоды к чипу катодами, оставляю тут нули;
  • второй контрольный регистр CR2 при выводе задает скорость тактирования: 1 = 10 МГц

#define PA_DDR *(volatile uint8_t *)0x005002
#define PA_CR2 *(volatile uint8_t *)0x005004
#define PD_DDR *(volatile uint8_t *)0x005011
#define PD_CR2 *(volatile uint8_t *)0x005013
#define PC_DDR *(volatile uint8_t *)0x00500C
#define PC_CR2 *(volatile uint8_t *)0x00500E PA_DDR = (1<<3); //output
PA_CR2 |= (1<<3); //fast
PD_DDR = (1<<3); //output
PD_CR2 |= (1<<3); //fast
PC_DDR = ((1<<3) | (1<<5) | (1<<6) | (1<<7)); //output
PC_CR2 |= ((1<<3) | (1<<5) | (1<<6) | (1<<7)); //fast

Настройка ШИМ

Для начала определимся с терминами:

  • PWM Frequency – частота, с которой тикает таймер;
  • Auto-reload, AR – автозагружаемое значение, до которого будет считать таймер (период импульса);
  • Update Event, UEV – событие, случающееся, когда таймер досчитал до AR;
  • PWM Duty Cycle – коэффициент заполнения ШИМ, часто называют «скважностью»;
  • Capture/Compare Value – значение для захвата/сравнения, досчитав до которого таймер что-то сделает (в случае ШИМ – инвертирует выходной сигнал);
  • Preload Value – предзагруженное значение. Compare value не может меняться, пока таймер тикает, иначе цикл ШИМ поломается. Поэтому новые передаваемые значения помещаются в буфер и вытаскиваются оттуда, когда таймер достигает конца отсчета и сбрасывается;
  • Edge-aligned и Center-aligned modes – выравнивание по границе и по центру, то же, что атмеловские Fast PWM и Phase-correct PWM.
  • OCiREF, Output Compare Reference Signal – референсный выводной сигнал, собственно, то, что в режиме ШИМ оказывается на соответствующем пине.

Оба 16-битные, первый обладает массой дополнительных фич (в частности, умеет считать и вверх, и вниз). Как уже ясно из распиновки, возможности ШИМ есть у двух таймеров – первого и второго. Некоторая проблема состоит в том, что описание функционала ШИМ всех таймеров в reference manual находится в главе про первый таймер (17. Нам надо, чтобы оба работали одинаково, поэтому я решил начать с заведомо более бедного второго, чтобы случайно не использовать что-то, чего в нем нет. 7 PWM Mode), поэтому приходится все время прыгать туда-сюда по документу. 5.

ШИМ на STM8 обладает важным преимуществом над ШИМ Атмеги:

Референсный сигнал ШИМ OCiREF удерживается в высоком уровне, пока TIM1_CNT < TIM1_CCRi.
ШИМ с выравниванием по границе
Конфигурация счета снизу вверх
Счет снизу вверх активен, если бит DIR в регистре TIM_CR1 сброшен
Пример
Пример использует первый режим ШИМ. Если значение для сравнение в регистре TIM1_CCRi больше, чем автозагружаемое значение (регистр TIM1_ARR), сигнал OCiREF удерживается в 1. Иначе он принимает низкий уровень. Если значение для сравнения равно 0, OCiREF удерживается на нуле.

У Атмеги таймер сперва шарашит, а потом сравнивает, в результате чего при compare value == 0 на выходе получается игла, с которой надо как-то бороться (например, программно инвертируя логику). Таймер STM8 во время update event сперва проверяет compare value, и лишь потом выдает референсный сигнал.

Поскольку лампочки подключены к чипу катодами, ШИМ должен выдавать 0 (LED горит) до compare value и 1 после. Итак, что мы хотим сделать: 8-битный ШИМ (AR == 255), считаем снизу вверх, выравнивание по границе.

6. Мы уже прочитали про некие PWM mode, поэтому находим нужный регистр второго таймера поиском в reference manual по этой фразе (18. 8 – TIMx_CCMR1):

В противном случае первый канал неактивен.
110: Первый режим ШИМ – при счете снизу вверх, первый канал активен, пока TIMx_CNT < TIMx_CCR1. В противном случае первый канал активен. [дальше в документе ошибочный копипаст из таймера 1]
111: Второй режим ШИМ – при счете снизу вверх, первый канал неактивен, пока TIMx_CNT < TIMx_CCR1.

Поскольку светодиоды подключены к МК катодами, нам подходит второй режим (первый тоже, но мы пока этого не знаем).

Писать в TIMx_CCR1 можно в любое время.
Бит 3 OC1PE: Включить предзагрузку вывода 1
0: Регистр предзагрузки на TIMx_CCR1 выключен. Операции чтения/записи обращаются к регистру предзагрузки. Новое значение работает сразу.
1: Регистр предзагрузки на TIMx_CCR1 включен. Это необязательно в режиме одиночного сигнала (в регистре TIMx_CR1 установлен бит OPM).
Предзагруженное значение TIMx_CCR1 загружается в теневой регистр во время каждого события обновления.
*Примечание: для правильной работы режима ШИМ регистры предзагрузки должны быть включены.

Окей, включаем все, что нужно, для трех каналов второго таймера:

#define TIM2_CCMR1 *(volatile uint8_t *)0x005307
#define TIM2_CCMR2 *(volatile uint8_t *)0x005308
#define TIM2_CCMR3 *(volatile uint8_t *)0x005309 #define PWM_MODE2 0x70 //PWM mode 2, 0b01110000
#define OCxPE 0x08 //preload enable TIM2_CCMR1 = (PWM_MODE2 | OCxPE);
TIM2_CCMR2 = (PWM_MODE2 | OCxPE);
TIM2_CCMR3 = (PWM_MODE2 | OCxPE);

AR состоит из двух восьмибитных регистров, тут все просто:

#define TIM2_ARRH *(volatile uint8_t *)0x00530F
#define TIM2_ARRL *(volatile uint8_t *)0x005310 TIM2_ARRH = 0;
TIM2_ARRL = 255;

Установим делитель частоты, например, в 256. Второй таймер умеет считать только снизу-вверх, выравнивание по границе, менять ничего не надо. У второго таймера делитель выставляется в регистре TIM2_PSCR и представляет собой степень двойки:

#define TIM2_PSCR *(volatile uint8_t *)0x00530E TIM2_PSCR = 8;

Первая задача решается регистрами Capture/Compare Enable: их два, три канала по ним разбросаны несимметрично. Осталось включить выводы и сам второй таймер. в принципе можно было использовать и PWM Mode 1. Здесь мы также можем узнать, что можно менять полярность сигнала, т.е. Пишем:

#define TIM2_CCER1 *(volatile uint8_t *)0x00530A
#define TIM2_CCER2 *(volatile uint8_t *)0x00530B #define CC1E (1<<0) // CCER1
#define CC2E (1<<4) // CCER1
#define CC3E (1<<0) // CCER2 TIM2_CCER1 = (CC1E | CC2E);
TIM2_CCER2 = CC3E;

Ну и, наконец, запускаем таймер в регистре TIMx_CR1:

#define TIM2_CR1 *(volatile uint8_t *)0x005300 TIM2_CR1 |= 1;

Регистры предсказуемо называются Capture/Compare registers, их по два на каждый канал: младшие 8 бит в TIM2_CCRxL и старшие в TIM2_CCRxH. Напишем простенький аналог AnalogWrite(), который будет передавать таймеру собственно значения для сравнения. Поскольку мы завели 8-битный ШИМ, достаточно писать только младшие биты:

#define TIM2_CCR1L *(volatile uint8_t *)0x005312
#define TIM2_CCR2L *(volatile uint8_t *)0x005314
#define TIM2_CCR3L *(volatile uint8_t *)0x005316 void setRGBled(uint8_t r, uint8_t g, uint8_t b)
{ TIM2_CCR1L = r; TIM2_CCR2L = g; TIM2_CCR3L = b;
}

Для светодиодов это не играет роли, а внимательный читатель уже сам догадывается, как это исправить. Внимательный читатель заметит, что у нас получился слегка бракованный ШИМ, неспособный выдать 100% заполнение (при максимальном значении 255 сигнал инвертируется на один цикл таймера).

ШИМ на втором таймере работает, переходим к первому.

Поэтому достаточно найти адреса этих же регистров в даташите и скопировать код. Первый таймер обладает ровно теми же битами в таких же регистрах (просто те биты, что оставались «зарезервированы» во втором таймере, в первом активно используются для всяких продвинутых штук). первый таймер хочет получить не степень двойки, а точное 16-битное значение в два регистра Prescaler High и Low. Ну и поменять значение делителя частоты, т.к. В чем дело? Все делаем и… первый таймер не работает.

Найдется 17. Решить проблему можно только путем просмотра всего раздела про управляющие регистры таймера 1, где ищем тот, которого нет у второго таймера. 30 Break register (TIM1_BKR), где есть такой бит: 7.


Включить главный вывод

#define TIM1_BKR *(volatile uint8_t *)0x00526D TIM1_BKR = (1<<7);

Вот теперь точно все, код там же.

В основе – концепция LED-мультиплексинга, состоящая в том, что если очень-очень быстро зажигать и гасить светодиоды, нам будет казаться, что они горят постоянно (persistence of vision, инерция зрительного восприятия). Третий мини-проект состоит в том, чтобы подключить к второму таймеру в режиме ШИМ восемь RGB-светодиодов и заставить их показывать разные цвета. Когда-то я делал что-то такое на Ардуине.

Алгоритм работы выглядит так:

  • подключили анод первого RGB LED;
  • зажгли его, подав нужные сигналы на катоды;
  • дождались конца цикла ШИМ;
  • подключили анод второго RGB LED;
  • зажгли его...

Разумеется, для красивой работы требуется, чтобы подключение анода и «зажигание» светодиода происходили одновременно. Ну и т.д. В любом случае, нам надо написать код, который будет в три канала второго таймера выдавать значения, при достижении UEV менять их и одновременно менять активный в данный момент RGB-светодиод. Ну или почти.

Это простой массив: Поскольку переключение LED выполняется автоматически, нужно создать «видеопамять», откуда обработчик прерывания будет получать данные.

uint8_t colors[8][3];

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

uint8_t cnt;

Демукс

Демультиплексор – чип, аппаратно реализующий оператор <<. Для правильного мультиплексинга нам потребуется, как ни странно, демультиплексор CD74HC238. Остальные входы чипа используются для масштабирования всей конструкции. Через три входных пина (биты 0, 1 и 2) мы скармливаем ему трехбитное число X, а он в ответ активирует выход номер (1<<X). Чип стоит копейки, его стоит держать в домашней аптечке всегда. Этот чип нам нужен не только для сокращения числа занятых пинов микроконтроллера, но и для безопасности – чтобы случайно не врубить больше светодиодов, чем можно, и не сжечь МК.

В полноценном мультиплексе он бы подавал напряжение на столбец через P-MOSFET, но в этом демо можно и напрямую, т.к. CD74HC238 у нас будет отвечать за то, чтобы подавать напряжение к аноду нужного светодиода. Из даташита CD74HC238 нам потребуется распиновка и вот эта шпаргалка: он тянет 20 мА, согласно absolute maximum ratings в даташите.


H = высокий уровень напряжения, L = низкий уровень напряжения, X – все равно

Поскольку таблица выше содержит и низкий, и высокий уровни, настраиваем эти пины как push-pull выводы. Подключаем E2 и E1 к земле, E3, A0, A1 и A3 к пинам PD5, PC3, PC4 и PC5 STM8.

ШИМ

ШИМ на втором таймере настраивается так же, как в предыдущей истории, с двумя отличиями:

Делается это изменением бита Update Interrupt Enable в регистре с говорящим названием Во-первых, нам надо включить прерывание на Update Event (UEV), которое будет вызывать функцию, переключающую активный LED.


Регистр включения прерываний

#define TIM2_IER *(volatile uint8_t *)0x005303 //enable interrupt
TIM2_IER = 1;

В нашем случае оно может появитсья из-за того, что таймер, вызвав прерывание на UEV, идет тикать дальше, и обработчик прерывания не успевает переключить LED прежде чем таймер уже начнет что-то писать в выводы. Второе отличие связано с таким явлением мультиплексинга, как ghosting – паразитное свечение диодов. Т.е. Для борьбы с этим придется инвертировать логику (0 = максимальная яркость, 255 = ничего не горит) и не допускать крайних значений скважности. добиться того, чтобы после UEV светодиоды полностью гасли на один такт ШИМ.

Меняем полярность:

//set polarity TIM2_CCER1 |= (CC1P | CC2P); TIM2_CCER2 |= CC3P;

Избегаем установки r, g и b в 255 и не забываем их инвертировать при использовании.

Прерывания

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

В этом файле на каждое прерывание привязана функция NonHandledInterrupt. Когда мы в первый раз создали проект в ST Visual Desktop, то кроме main.c мы получили окно с загадочным файлом stm8_interrupt_vector.c, автоматически включенным в проект. Нам надо привязать свою функцию к нужному прерыванию.

В даташите есть таблица векторов прерываний, где мы находим нужные:


13 TIM2 обновление/переполнение
14 TIM2 захват/сравнение

Нам надо менять LED при UEV, так что нужно прерывание №13.

Соответственно, во-первых, в файле stm8_interrupt_vector.c меняем имя функции, отвечающей за прерывание №13 (IRQ13) по умолчанию на свое:

{0x82, TIM2_Overflow}, /* irq13 */

Во-вторых, нам придется создать файл main.h такого содержания:

#ifndef __MAIN_H
#define __MAIN_H @far @interrupt void TIM2_Overflow (void);
#endif

Ну и, наконец, прописать эту функцию в своем main.c:

@far @interrupt void TIM2_Overflow (void)
{ PD_ODR &= ~(1<<5); // вырубаем демультиплексор PC_ODR = (cnt<<3); // записываем в демультиплексор новое значение PD_ODR |= (1<<5); // включаем демультиплексор TIM2_SR1 = 0; // сбрасываем флаг Update Interrupt Pending cnt++; cnt &= 7; // двигаем счетчик LED TIM2_CCR1L = ~colors[cnt][0]; // передаем в буфер инвертированные значения TIM2_CCR2L = ~colors[cnt][1]; // для следующего цикла ШИМ TIM2_CCR3L = ~colors[cnt][2]; // return;
}

Делается это ассемблерной командой rim – искать ее придется в Programming Manual: Осталось включить прерывания.

//enable interrupts
_asm("rim");

Их надо отключать на время записи новых значений в «видеопамять», чтобы вызванное в неудачный момент прерывание не испортило массив. Другая ассемблерная команда – sim – выключает прерывания.

Весь код – на Гитхабе.

Буду рад комментариям и замечаниям, постараюсь ответить на все. Если хоть кому-то эта статья пригодится, значит, я не зря ее писал.

Показать больше

Похожие публикации

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

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

Кнопка «Наверх»