
В Опитах се да обясня на хоби електронците, израснали от Ардуино, как и защо трябва да четат даташитове и друга документация за микроконтролери. Текстът стана дълъг, затова обещах да покажа практически примери в отделна статия. Е, назовах се гъба...
Днес ще покажа как с помощта на даташитове да решим доста прости, но необходими задачи за множество проекти на контролери STM32 (Blue Pill) и STM8. Всички демонстрационни проекти са посветени на любимите ми светодиоди, които ще запаляме в големи количества, за което ще се наложи да използваме различна интересна периферия.
Текстът отново стана огромен, затова за удобство правя съдържание:
Дисклеймер: не съм инженер, не претендирам за дълбоки познания в електрониката, статията е предназначена за също толкова запалени любители. Всъщност, целевата аудитория, която имах предвид, съм самият аз отпреди две години. Ако някой ми беше казал тогава, че не е страшно да чета даташитове за непознат чип, нямаше да инвестирам толкова време в търсене на парчета код в интернет и изобретяване на импровизации с ножици и лепилни ленти.
В центъра на тази статия са даташитове, а не проекти, затова кодът може да не е особено подреден и често импровизиран. Самите проекти са много прости, въпреки че са подходящи за първо запознанство с нов чип.
Надявам се, че статията ми ще помогне на някого на подобен етап от запознаването с хобито.
STM32
16 светодиода с DM634 и SPI
Малък проект с използване на Blue Pill (STM32F103C8T6) и светодиоден драйвер DM634. С помощта на даташитове ще разгледаме драйвера, IO портовете на STM и ще настроим SPI.
DM634
Тайвански чип с 16 16-битови PWM изходи, който може да бъде свързван в вериги. По-младата 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
Rising edge – передний фронт импульса
MSB first – с най-значимия (краен ляв) бит напред.
за синхронизиране на данни – да се предават данни последователно (по бит).
Думата задвижка често се среща в документацията за чипове и се превежда по различни начини, затова за да се разбере, ще си позволя
да дам малко обяснениеLED драйвер – по същество е регистър с преместване. „Преместването“ (shift) в наименованието означава побитово преместване на данни вътре в устройството: всеки нов вкаран бит бута цялата верига напред. Тъй като по време на преместването никой не иска да наблюдава хаотично мигане на светодиоди, процесът се осъществява в буферни регистри, отделени от работния механизъм с клапа (задвижка) – това е нещо като преддверие, където битовете се подреждат в необходима последователност. Когато всичко е готово, клапата се отваря и битовете започват работа, заменяйки предишната партида. Думата задвижка в документацията за микросхеми почти винаги подразумява такава клапа, независимо от комбинациите, в които тя се използва.
И така, предаването на данни в DM634 се осъществява така: задаваме вход DAI на стойността на най-значимия бит на далечния светодиод, дърпаме DCK нагоре-надолу; задаваме вход DAI на стойността на следващия бит, дърпаме DCK; и така нататък, докато всички битове бъдат предадени (синхронизирани), след което дърпаме LAT. Това може да се направи ръчно (чрез bit-bang), но е по-добре да се използва специализирания интерфейс SPI, който имаме на STM32 в два экземпляра.
Синята Таблетка STM32F103
Въведение: контролерите STM32 – значително по-сложни от Atmega328, отколкото могат да изглеждат. В същото време от съображения за енергийна ефективност при стартиране почти всяка периферия е изключена, а тактовата честота е 8 MHz от вътрешния източник. За щастие, програмистите на STM написаха код, който довежда чипа до „разчетните“ 72 MHz, а авторите на всички известни IDE са го включили в процедурата за инициализация, така че не е нужно да тактуваме (но ). Но ще трябва да включим периферията.
Документация: на Blue Pill е инсталиран популярен чип STM32F103C8T6, за него има два полезни документа:
- за микроконтролерите STM32F103x8 и STM32F103xB;
- за целия набор STM32F103 и не само.
В даташита могат да ни интересуват:
- Pinouts – разпиновки на чиповете – за всеки случай, ако решим да правим платки сами;
- Паметна карта – карта памет за конкретен чип. В Референтния наръчник има карта за целия асортимент, в която са споменати регистрите, които не са налични при нас.
- Таблица с определения на пиновете – списък на основните и алтернативни функции на пиновете; за „синята таблетка“ в интернет могат да се намерят по-удобни изображения със списък на пиновете и техните функции. Така че веднага търсим Blue Pill pinout и държим такова изображение под ръка:

Забележка: на изображението от интернет имаше грешка, посочена в коментарите, за което благодаря. Изображението е заменено, но това е урок – информация, не от даташитовете, е най-добре да се проверява.
Премахваме даташита, отваряме Референтния наръчник и оттук нататък ползваме само него.
Последователност на действията: разбираме се със стандартния вход/изход, настройваме 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):
![]()
Ако битът на порта е конфигуриран като изход на алтернативна функция, изходният регистър се изключва, а пинът се свързва към изходния сигнал на периферията.
Алтернативната функционалност на всеки пин е описана в Определения на пиновете Техническите характеристики са в изтеглената картинка. Отговор на въпроса какво да правим, ако един и същи пин има няколко алтернативни функции, дава бележка в техническите характеристики:
![]()
Ако няколко периферни блока използват един и същи пин, за да избегнете конфликт между алтернативните функции, трябва да използвате само един периферен блок, превключвайки чрез бита за активиране на тактирования периферия (в съответния регистър RCC).
Накрая, пиновете в режим на изход имат и скорост на тактирование. Това е още една функция за енергоспестяване, в нашия случай просто я задаваме на максимум и забравяме.
И така: използваме SPI, следователно два пина (един за данните и един за тактовия сигнал) трябва да бъдат „алтернативна функция за изплъзване”, а друг (LAT) – „обикновен за изплъзване”. Но преди да ги зададем, да разгледаме SPI.
SPI
Още един малък урок
SPI или Serial Peripheral Interface (последователен периферен интерфейс) – прост и изключително ефективен интерфейс за свързване на микроконтролери с други микроконтролери и всъщност с външния свят. Принципът на работа му вече е описан по-горе, там където става дума за китайския LED драйвер (в справочното ръководство в раздел 25). 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 (полудуплексна комуникация), където намираме 1 часов сигнал и 1 еднопосочен поток данни (1 тактов сигнал и 1 однонаправлен поток данных):

В този режим приложението използва SPI, или в режим само предаване, или само прием. / Режимът на само предаване е подобен на дуплексния режим: данните се предават по предаващия пин (MOSI в режим на мастер или MISO в режим на слейв), а приемащият пин (MISO или MOSI съответно) може да се използва като стандартен входно-изходен пин. В този случай приложението просто трябва да игнорира Rx буфера (ако бъде прочетен, там няма да има предадени данни).
Страхотно, пинът MISO е свободен, ще свържем сигнала LAT към него. Ще разгледаме Slave Select, който на STM32 може да се управлява програмно, което е изключително удобно. Четем съответния параграф от раздел 25.3.1 SPI Общи характеристики:

Програмно управление на NSS (SSM = 1) / Информацията за избора на слейв се съдържа в битa SSI на регистъра SPI_CR1. Външният пин NSS остава свободен за други нужди на приложението.
Време е да пишем в регистрите. Реших да използвам SPI2, търсим в спецификацията основния му адрес – в раздел 3.3 Карта на паметта:
![]()
И започваме:
#define _SPI2_(mem_offset) (*(volatile uint32_t *)(0x40003800 + (mem_offset)))Отваряме раздел 25.3.3 с говорящото заглавие «Конфигуриране на SPI в режим мастер»:

1. Задайте тактовата честота на последователния интерфейс с битовете BR[2:0] в регистъра SPI_CR1.
Регистрите са събрани в едноименния раздел на упътването. Задвижването на адреса (Address offset) на CR1 – 0x00, по подразбиране всички битове са нулирани (Reset value 0x0000):

Битовете BR задават делителя на тактовата честота на контролера, определяйки по този начин честотата, при която ще работи SPI. Честотата на STM32 ще бъде 72 MHz, LED драйверът, според неговата спецификация, работи с честота до 25 MHz, така че делението трябва да е на четири (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 Общи характеристики):

Фаза и полярност на тактовия сигнал
С помощта на битовете 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, както, например, при АтMega.
3. Задайте бит DFF, за да определите 8-битния или 16-битния формат на блока данни
Специално избрах 16-битния DM634, за да не се затруднявам с предаването на 12-битови данни PWM, както при DM633. Има смисъл да зададете DFF в единица:
#define DFF 0x0800
_SPI2_ (_SPI_CR1) |= DFF; // 16-bit mode4. Конфигурирайте бита LSBFIRST в регистра SPI_CR1, за да определите формата на блока
LSBFIRST, както личи от името му, настройва предаването на младшия бит напред. Но DM634 иска да получава данни, започвайки от старшия бит. Затова оставяме нулирано.
5. В хардуерния режим, ако е необходимо въвеждане от пина 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); // завершить передачу
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 Регистрът за посока, регистър за вход; ODR = Output Регистрът за посока, регистър за изход. В настоящия проект не ни трябват.
И накрая, управляващите регистри. Понеже ни интересуват пиновете на втория SPI, а именно PB13, PB14 и PB15, веднага гледаме в CRH:

И виждаме, че трябва да напишем нещо в битовете от 20-и до 31-ви.
Вече разгледахме какво точно искаме от пиновете, затова тук ще се задоволя само с текста — MODE задава посоката (вход, ако и двата бита са зададени на 0) и скоростта на пина (нуждаем се от 50MHz, т.е. и двата пина на „1“), а CNF задава режима: стандартен „тик-так“ – 00, „алтернативен“ – 10. По подразбиране, каквото видяхме по-горе, за всички пинове е зададен третият бит отдолу (CNF0), който ги поставя в режим. плаващ вход.
Тъй като планирам да направя нещо друго с този чип, аз за простота дефинирах абсолютно всички възможни стойности на 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, за което им благодаря (отново ще дам линк на , за да стане ясно колко сложно е). Нуждаем се само от регистрите, отговарящи за включването на тактироването на периферията (Периферни регистри за включване на часовника). За начало ще намерим базовия адрес на RCC, който е в самото начало на „Картата на паметта“:
![]()
#define _RCC_(mem_offset) (*(volatile uint32_t *)(0x40021000 + (mem_offset)))А след това или да кликнем на линка, за да опитаме да намерим нещо в таблицата, или, което е много по-добре, да прегледаме описанията на включващите регистри от разделите за регистри за включване. Където ще намерим RCC_APB1ENR и RCC_APB2ENR:


И в тях, съответно, битове, включващи тактироването на SPI2, IOPB (I/O порт 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
Когато планирах тази статия, реших да опитам да овладея някаква функционалност на непознат чип, използвайки единствено datasheet, за да не съм обувчига без обувки. STM8 идеално подхожда на тази роля: от една страна, имам няколко китайски платки с STM8S103, а от друга страна, той не е много популярен, така че изкушението да потърся решение в интернет се натъква на липсата на такива решения.
Към чипа има също и , в първия е схемата и адресите на регистрите, във втория – всичко останало. STM8 се програмира на C в грозната IDE .
Тактуване и вход-изход
По подразбиране STM8 работи на честота 2 MHz, което трябва да се коригира веднага.

Тактовият сигнал HSI (високоскоростен вътрешен)
Тактовият сигнал HSI произтича от вътрешен 16 MHz RC генератор с програмираем делител (от 1 до 8). Той се задава в регистра на делителя на тактовия сигнал (CLK_CKDIVR).
Забележка: при стартиране, водещият източник на тактов сигнал е HSI RC генератор с делител 8.
Намираме адреса на регистра в datasheet, описанието в refman и виждаме, че регистра трябва да се изчисти:
#define CLK_CKDIVR *(volatile uint8_t *)0x0050C6
CLK_CKDIVR &= ~(0x18);Тъй като ще стартираме PWM и ще свържем светодиоди, гледаме схемата:

Чипът е малък, много функции са насочени към едни и същи изводи. Това, което е в квадратни скобки, е "алтернативна функционалност", която се превключва с "опционални байтове" (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 MHz
#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Настройка на ШИМ
Преди да започнем, да се изясним с термините:
- Честота на ШИМ – честота, с която работи таймерът;
- Авто-презареждане, AR – авто-презареждане на стойност, до която таймерът ще смята (период на импулса);
- Събитие за обновление, UEV – събитие, което се случва, когато таймерът достигне AR;
- Работен цикъл на ШИМ – коефициент на запълване на ШИМ, често наричан „скважност“;
- Стойност за захващане/сравнение – стойност за захващане/сравнение, до която таймерът нещо ще направи (в случай на ШИМ – инвертира изходния сигнал);
- Предварително заредена стойност. Стойността за сравнение не може да бъде променяна, докато таймерът работи, в противен случай цикълът на ШИМ ще се повреди. Затова новите предавани стойности се поставят в буфер и се изтеглят оттам, когато таймерът достигне края на отчитането и се нулира; Изравнени по ръб
- Централно изравнени режим и – изравняване по ръб и по център, същото като на АТмеловите Бърз ШИМ Фаза-коректен ШИМ и OCiREF, Output Compare Reference Signal.
- – референтен изходен сигнал, всъщност, това, което в режим ШИМ излиза на съответния пин. Както вече е ясно от разпиновката, ШИМ функцията е достъпна при два таймера – първият и вторият. И двата са 16-битни, като първият разполага с много допълнителни функции (в частност, може да брои и нагоре, и надолу). Трябва и двата да работят еднакво, затова реших да започна с очевидно по-простия втори, за да не употребя случайно нещо, което не е налично при него. Известна трудност е, че описанието на функционалността на ШИМ за всички таймери е в справочното ръководство в главата за първия таймер (17.5.7 Режим ШИМ), така че се налага постоянно да скочим насам-натам из документа.
ШИМ на STM8 има важно предимство пред ШИМ на Атмеги:
ШИМ с изравняване по ръб

Конфигурация на брой отдолу нагоре
Броят отдолу нагоре е активен, ако бит DIR в регистра TIM_CR1 е нулиран
Примерът използва първия режим на ШИМ. Референтният сигнал на ШИМ OCiREF се задържа на високо ниво, докато TIM1_CNT < TIM1_CCRi. В противен случай, той приема ниско ниво. Ако стойността за сравнение в регистра TIM1_CCRi е по-голяма от авто-презареждането (регистър TIM1_ARR), сигналът OCiREF се задържа на 1.
Пример
Ако стойността за сравнение е 0, OCiREF остава на нула. Таймерът STM8 по време на…
събитие на обновление първо проверява стойността за сравнение , и едва след това издава референтния сигнал. При Атмеги таймерът първо работи, а след това сравнява, в резултат на което пристойността за сравнение == 0 compare value == 0 На изхода получаваме игла, с която трябва да се борим по някакъв начин (например, инвертиране на логиката програмно).
И така, какво искаме да направим: 8-битен PWM (AR == 255), изчисляваме отдолу нагоре, подравняване по границата. Понеже лампичките са свързани към чипа с катоди, PWM трябва да издаде 0 (LED свети) до , и едва след това издава референтния сигнал. При Атмеги таймерът първо работи, а след това сравнява, в резултат на което при и 1 след.
Вече прочетохме за някои PWM режим, затова намираме необходимия регистър на втория таймер, търсейки в справочното ръководство по тази фраза (18.6.8 – TIMx_CCMR1):

110: Първи режим на PWM – при броене отдолу нагоре, първият канал е активен, докато TIMx_CNT < TIMx_CCR1. В противен случай първият канал не е активен. [по-нататък в документа грешен копипейст от таймера 1]
111: Втори режим на PWM – при броене отдолу нагоре, първият канал не е активен, докато TIMx_CNT < TIMx_CCR1. В противен случай първият канал е активен.
Понеже светодиодите са свързани към МК с катоди, вторият режим ни подхожда (първият също, но засега не знаем това).

Бит 3 OC1PE: Включи предподготовката на изход 1
0: Регистърът за предподготовка на TIMx_CCR1 е изключен. Може да се записва в TIMx_CCR1 по всяко време. Новата стойност заработва веднага.
1: Регистърът за предподготовка на TIMx_CCR1 е включен. Операциите на четене/запис се отнасят до регистъра за предподготовка. Предварително зададената стойност TIMx_CCR1 се зарежда в тъмен регистър при всяко обновление.
*Забележка: за правилното функциониране на режима PWM регистрите за предподготовка трябва да са включени. Това не е задължително при режима на единичен сигнал (бит OPM в регистъра TIMx_CR1 е задействан).
Окей, включваме всичко необходимо за трите канала на втория таймер:
#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 Активирай: те са два, три канала са разпределени несиметрично. Тук също можем да разберем, че можем да променим полярността на сигнала, т.е. всъщност можеше да се използва и режим PWM 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-битов PWM, е достатъчно да писем само младшите битове:
#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;
}Внимателният читател ще забележи, че получихме малко дефектен PWM, неспособен да достигне 100% запълване (при максималната стойност 255 сигналът се инвертира на един цикъл на таймера). За светодиодите това не е от значение, а внимателният читател вече сам стига до извода как да го поправи.
PWM на втория таймер работи, преминаваме към първия.
Първият таймер притежава точно същите битове в същите регистри (просто битовете, които оставаха „резервирани“ във втория таймер, в първия активно се използват за различни напреднали неща). Затова е достатъчно да намерим адресите на тези регистри в даташита и да копираме кода. И да променим стойността на делителя на честотата, тъй като първият таймер иска да получи не степен на две, а точно 16-битова стойност в два регистра Prescaler High и Ниско. Правим всичко и… първият таймер не работи. Каква е причината?
Проблемът може да бъде решен само чрез преглед на целия раздел за управляващите регистри на таймер 1, където търсим този, който липсва на втория таймер. Ще го намерим 17.7.30 Break register (TIM1_BKR), където има такъв бит:
![]()
Включи главния изход
#define TIM1_BKR *(volatile uint8_t *)0x00526D
TIM1_BKR = (1<<7);Ето сега всичко е точно, кодът .

STM8 Multiplex
Мултиплексиране на STM8
Третият мини-проект е свързан с това да свържем осем RGB-светодиода към втория таймер в PWM режим и да ги накараме да показват различни цветове. В основата – концепцията за LED мултиплексиране, която гласи, че ако много бързо включваме и изключваме светодиодите, ще ни се стори, че те светят постоянно (постоянство на зрението, инерция на зрителното възприятие). Някога правех .
Алгоритъмът на работа изглежда така:
- свързахме анода на първия RGB LED;
- включихме го, подавайки необходимите сигнали на катодите;
- изчакахме края на цикъла на PWM;
- свързахме анода на втория 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
