
В я попробовал рассказать хобби-электронщикам, выросшим из штанишек Ардуино, как и зачем им стоит читать даташиты и прочую документацию к микроконтроллерам. Текст получился большой, поэтому я пообещал практические примеры показать в отдельной статье. Ну что же, назвался груздем…
Сегодня я покажу, как с помощью даташитов решить довольно простые, но необходимые для множества проектов задачи на контроллерах STM32 (Blue Pill) и STM8. Все демо-проекты посвящены моим любимым светодиодам, зажигать мы их будем в больших количествах, для чего придется задействовать всякую интересную периферию.
Текст опять получился огромный, поэтому для удобства делаю содержание:
Дисклеймер: я не инженер, не претендую на глубокие познания в электронике, статья предназначена для таких же как я любителей. На самом деле, в качестве целевой аудитории я рассматривал самого себя двухлетней давности. Если бы мне кто-то тогда рассказал, что даташиты на незнакомый чип читать не страшно, я бы не потратил кучу времени на выискивание каких-то кусков кода в интернете и изобретение костылей с ножницами и лейкопластырем.
В центре этой статьи — даташиты, а не проекты, поэтому код может быть не слишком причесан и часто костылен. Сами проекты очень простые, хотя и годные для первого знакомства с новым чипом.
Надеюсь, что моя статья поможет кому-то на похожем этапе погружения в хобби.
STM32
16 светодиодов c DM634 и SPI
Небольшой проект с использованием Blue Pill (STM32F103C8T6) и светодиодного драйвера DM634. С помощью даташитов разберемся с драйвером, IO-портами STM и настроим SPI.
DM634
Тайваньский чип с 16-ю 16-битными ШИМ-выходами, можно соединять в цепочки. Младшая 12-битная модель известна по отечественному проекту . В свое время, выбирая между 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, к нему есть два полезных документа:
- для микроконтроллеров STM32F103x8 и STM32F103xB;
- для всей линейки 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/42. Установите биты 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 mode4. Сконфигурируйте бит 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 high6. Должны быть установлены биты 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, за что им большое спасибо (еще раз дам ссылку на , чтобы было понятно, насколько это заморочено). Нам нужны всего лишь регистры, отвечающие за включение тактирования периферии (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, а во-вторых, он не слишком популярен, а потому соблазн почитить и найти решение в интернете упирается в отсутствие этих самых решений.
К чипу также есть и , в первом распиновка и адреса регистров, во втором – все остальное. Программируется STM8 на C в страшненькой IDE .
Тактирование и ввод-вывод
По умолчанию 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 в даташите. Из нам потребуется распиновка и вот эта шпаргалка:

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 – искать ее придется в :
//enable interrupts
_asm("rim");Другая ассемблерная команда – sim – выключает прерывания. Их надо отключать на время записи новых значений в «видеопамять», чтобы вызванное в неудачный момент прерывание не испортило массив.
Весь код – .

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