Pull to refresh
124.11
Skyeng
Крутейшая edtech-команда страны. Удаленная работа

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

Reading time 24 min
Views 36K


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


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


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


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


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


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


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


STM32


16 светодиодов c DM634 и SPI


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


DM634


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


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



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


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



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


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


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



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



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


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


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

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

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


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


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


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


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

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


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


NB: на картинке из интернета была ошибка, подмеченная в комментариях, за что спасибо. Картинка заменена, но это урок — информацию не из даташитов лучше проверять.


Даташит убираем, открываем 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). При записи нуля пин в обоих режимах ведет себя одинаково, как логически, так и электрически.


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



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


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



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


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


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


SPI


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

SPI или Serial Peripherial Interface (последовательный периферийный интерфейс) – простой и весьма эффективный интерфейс для связи МК с другими МК и вообще внешним миром. Принцип его работы уже описан выше, там, где про китайский LED-драйвер (в reference manual см раздел 25). 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 на STM32 богат функционалом, что несколько осложняет его понимание. Например, он умеет работать не только SPI, но и I2S-интерфейсом, причем в документации их описания идут вперемешку, надо своевременно отсекать лишнее. У нас же задача крайне простая: надо всего лишь отдавать данные, задействуя только MOSI и SCK. Идем в раздел 25.3.4 (half-duplex communication, полудуплексная связь), где находим 1 clock and 1 unidirectional data wire (1 тактовый сигнал и 1 однонаправленный поток данных):



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


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



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


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



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


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

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



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


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



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


#define _SPI_CR1 0x00

#define BR_0        0x0008
#define BR_1        0x0010
#define BR_2        0x0020

_SPI2_ (_SPI_CR1) |= BR_0;// pclk/4

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


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



Фаза и полярность тактового сигнала
С помощью битов CPOL и CPHA регистра SPI_CR1 можно программно выбрать четыре варианта отношений таймингов. Бит CPOL (полярность тактового сигнала) управляет состоянием тактового сигнала, когда данные не передаются. Этот бит управляет режимами мастер и слейв. Если 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, как, например, у Атмеги.


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


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


#define DFF         0x0800

_SPI2_ (_SPI_CR1) |= DFF; // 16-bit mode

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


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


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


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


#define SSI         0x0100
#define SSM         0x0200

_SPI2_ (_SPI_CR1) |= SSM | SSI; //enable software control of SS, SS high

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


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


#define MSTR        0x0004
#define SPE         0x0040

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

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



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


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


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



<...>

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


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



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


Ну и регистр статусов, где найдутся флаги 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()
{
    LAT_low();
    uint8_t k = 16;
    do
    {   k--;
        dm_shift16(leds[k]);
    } while (k);

    while (_SPI2_(_SPI_SR) & BSY); // finish transmission

    LAT_pulse();
}

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


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


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



Каждый из портов ввода/вывода общего назначения (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 битов которого только сбрасывают пин. Мне по душе второй вариант. Эти регистры важны тем, что обеспечивают атомарный доступ к пинам:




Атомарная установка или сброс
Не нужно отключать прерывания при программировании GPIOx_ODR на битовом уровне: можно изменять один или несколько битов одной атомарной операцией записи APB2. Это достигается записью «1» в регистр установки/сброса (GPIOx_BSRR или, только для сброса, в GPIOx_BRR) бита, который требуется изменить. Прочие биты останутся неизменными.


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


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



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


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


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


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


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



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

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




И в них, соответственно, биты, включающие тактирование 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;

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


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



STM8 PWM


ШИМ на STM8


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


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


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


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



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


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


#define CLK_CKDIVR *(volatile uint8_t *)0x0050C6

CLK_CKDIVR &= ~(0x18);

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



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


Таким образом, управлять светодиодами будут 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.5.7 PWM Mode), поэтому приходится все время прыгать туда-сюда по документу.


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



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


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


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


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



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


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



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

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


#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;

Напишем простенький аналог AnalogWrite(), который будет передавать таймеру собственно значения для сравнения. Регистры предсказуемо называются Capture/Compare registers, их по два на каждый канал: младшие 8 бит в TIM2_CCRxL и старшие в TIM2_CCRxH. Поскольку мы завели 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. Все делаем и… первый таймер не работает. В чем дело?


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



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


#define TIM1_BKR   *(volatile uint8_t *)0x00526D

TIM1_BKR = (1<<7);

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



STM8 Multiplex


Мультиплексинг на STM8


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


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


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

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


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


uint8_t colors[8][3];

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


uint8_t cnt;

Демукс


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


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



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


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


ШИМ


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


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



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


#define TIM2_IER   *(volatile uint8_t *)0x005303

//enable interrupt
TIM2_IER = 1;

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


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


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

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


Прерывания


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


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


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



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 – выключает прерывания. Их надо отключать на время записи новых значений в «видеопамять», чтобы вызванное в неудачный момент прерывание не испортило массив.


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



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

Tags:
Hubs:
+39
Comments 26
Comments Comments 26

Articles

Information

Website
www.skyeng.team
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия
Representative
Alisa Kruglova