
В Опитах се да разкажа на хобби електронците, които са израснали от Arduino, как и защо е важно да четат спецификациите и друга документация за микроконтролерите. Текстът стана дълъг, затова обещах да покажа практическите примери в отделна статия. Е, добре, нарекох се гъба…
Днес ще покажа как с помощта на спецификациите да решим доста прости, но необходими задачи за множество проекти на контролерите 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 – сток; източник на входящ ток; изход, в активното състояние свързан към земята, – светодиодите се свързват с драйвера с катодите. Елeктрически, това не е никакъв „открит сток“ (отворен дренаж), но в даташитите такава обозначение за изводите в режим на сток често се среща.
![]()
Външни резистори между REXT и GND за задаване на стойността на изходящия ток
Между пина REXT и земята се поставя референтен резистор, който контролира вътрешното съпротивление на изходите, виж график на стр. 9 на даташита. В DM634 с това съпротивление също може да се управлява програмно, задавайки обща яркост (global brightness); в тази статия няма да задълбавам в подробности, просто ще поставя тук резистор от 2.2 – 3 kОм.
За да разбера как да управлявам чипа, ще погледнем описанието на интерфейса на устройството:

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

… За въвеждане на данни в устройството са нужни само три пина. Предният фронт на сигнала SCLK прехвърля данните от пина SIN във вътрешния регистър. След като всички данни са заредени, кратък висок сигнал XLAT фиксира последователно предадените данни във вътрешните регистри. Вътрешните регистри са задействани от нивото на сигнала XLAT на задейките. Всички данни се предават с най-старшия бит напред.
Latch – задейка/блокировка.
Rising edge – преден фронт на импулса
MSB first – с старшия (крайния ляв) бит напред.
да часови данни – да предаваме данни последователно (по битове).
Думата превключвател често се среща в документацията за чипове и се превежда по различен начин, затова за разбиране ще си позволя
малко обучениеLED драйвер – по същество е сменяем регистър. "Сменяем" (смести) в името означава побитово премизване на данни вътре в устройството: всеки нов бит, вкаран вътре, избутва цялата верига напред. Тъй като по време на смяната никой не иска да наблюдава хаотичното мигане на светодиодите, процесът се извършва в буферни регистри, отделени от работния механизъм с клапа (превключвател) – това е своеобразен преддверие, където битовете се подреждат в нужната последователност. Когато всичко е готово, клапата се отваря и битовете започват да работят, заменяйки предишната партия. Думата превключвател в документацията за микросхеми почти винаги подразбира такава клапа, независимо от комбинацията, в която се използва.
И така, предаването на данни в DM634 се извършва по следния начин: задаваме вход DAI на стойност на старшия бит на далечния светодиод, дърпаме DCK нагоре-надолу; задаваме вход DAI на стойността на следващия бит, дърпаме DCK; и така нататък, докато всички битове не бъдат предадени (clocked in), след което дърпаме 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.
Всичките различия на отворения сток (отворен дренаж) от "тегли-натиска" (push-pull) се състоят в това, че в първия пин не може да приеме състояние HIGH: при записване на единица в изходния регистър, той преминава в режим на високо съпротивление (high impedance, Hi-Z). При записване на нула пинът и в двата режима се държи по един и същ начин, както логически, така и електрически.
В нормален режим на изхода пинът просто транслира съдържанието на изходния регистър. В "алтернативния" им управлява съответната периферия (вж. 9.1.4):
![]()
Ако битът на порта е конфигуриран като изход на алтернативна функция, изходният регистър се изключва, а пинът се свързва с изходния сигнал на периферията.
Алтернативният функционал на всеки пин е описан в Описания на пиновете Техническият лист е на изтегленото изображение. На въпроса какво да направим, ако пинът има няколко алтернативни функции, отговорът дава бележка в техническия лист:
![]()
Ако няколко периферни блока използват един и същ пин, за да избегнете конфликти между алтернативните функции, трябва да се използва само един периферен блок, превключвайки с помощта на бит за активиране на тактоването на периферията (в съответния регистър RCC).
Накрая, пиновете в режим на изход имат допълнителна скорост на тактоване. Това е още една функция за пестене на енергия, в нашия случай просто настройваме на максимум и забравяме.
И така: използваме SPI, което означава, че два пина (с данни и с тактов сигнал) трябва да са „алтернативна функция да теглиш и буташ“, а още един (LAT) – „обикновен да теглиш и буташ“. Но преди да ги назначим, да разберем SPI.
SPI
Още един малък ликбез
SPI или Serial Peripheral Interface (последователен периферен интерфейс) – прост и много ефективен интерфейс за свързване на микроконтролери с други микроконтролери и изобщо външния свят. Принципът на работа е описан по-горе, там, където споменаваме китайския LED драйвер (в референтния наръчник вижте раздел 25). SPI може да работи в режим на майстор („господар“) и слейв („роб“). SPI разполага с четири основни канала, от които не всички могат да бъдат активирани:
- MOSI, Master Output / Slave Input: този пин в режим на майстор предава, а в режим на слейв приема данни;
- MISO, Master Input / Slave Output: обратно, в майстора приема, а в слейва – предава;
- SCK, Serial Clock: задава честотата на предаване на данни в майстора или приема тактов сигнал в слейва. По същество отбелязва битовете;
- SS, Slave Select: чрез този канал слейвът разбира, че нещо му се иска. На STM32 се нарича NSS, където N = отрицателен, т.е. контролерът става слейв, ако в този канал е земя. Добре се комбинира с режима Open Drain Output, но това е друга история.
Както всичко останало, SPI на STM32 е богат на функционалност, което малко усложнява разбирането му. Например, той може да работи не само в SPI режим, но и с I2S интерфейс, като в документацията описанията им вървят смесени, трябва да отделяте ненужното навреме. Нашата задача е изключително проста: просто трябва да предаваме данни, използвайки само MOSI и SCK. Отиваме в раздел 25.3.4 (полудуплексна комуникация), където намираме 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 вDatasheet DM634).
Между другото, тук за първи път се сблъскваме с особеност на лексиката в даташитите на ST: в тях изразът „да нулирате бит“ се пише to reset a bit, а не to clear a bit, както е например при Атмега.
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); // 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 = Вход Регистрите за посока, регистра за вход; ODR = Регистри за посока, регистра за изход. В текущия проект те няма да са ни нужни. И накрая, управляващите регистри. Тъй като ни интересуват пиновете на втория SPI, а именно PB13, PB14 и PB15, веднага поглеждаме на CRH:
И виждаме, че ще трябва да напишем нещо в битовете от 20-ти до 31-ви.

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

ШИМ с изравняване по ръба
Конфигурация на броене отдолу нагоре
Броенето отдолу нагоре е активно, ако бит DIR в регистъра TIM_CR1 е нулиран
Пример
Примерът използва първия режим на ШИМ. Референтният сигнал на ШИМ OCiREF се поддържа на високо ниво, докато TIM1_CNT < TIM1_CCRi. В противен случай той преминава на ниско ниво. Ако стойността за сравнение в регистъра TIM1_CCRi е по-голяма от автоматично презарежданото значение (регистър TIM1_ARR), сигналът OCiREF се поддържа на 1. Ако стойността за сравнение е 0, OCiREF остава на нула.…
Таймерът STM8 по време на събитие на актуализация първо проверява стойността за сравнение, и само след това издава референтния сигнал. При Атмега таймерът първо работи, а след това сравнява, в резултат на което при стойност за сравнение == 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 Enable: има два от тях, три канала са разпръснати несиметрично. Тук можем също да разберем, че можем да променим полярността на сигнала, т.е. всъщност бихме могли да използваме и 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 регистри, по два на все канали: младшие 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 и Low. Правим всичко и… първият таймер не работи. Каква е причината?
Проблемът може да бъде решен само чрез преглед на целия раздел за управляващите регистри на таймер 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 мултиплексиране, при която, ако много бързо включваме и изключваме светодиодите, ще изглежда, че те светят постоянно (persistence of vision, инерция на зрителното възприятие). Някога правих .
Алгоритъмът работи така:
- свързахме анода на първия 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
