
I Jeg prøvede at forklare hobbyelektronikere, der er vokset ud af Arduino, hvorfor og hvordan de bør læse datablade og anden dokumentation for mikrokontrollere. Teksten blev lang, så jeg lovede at vise praktiske eksempler i en separat artikel. Nå, nu hedder det så...
I dag vil jeg vise, hvordan man ved hjælp af datablade løser ret simple, men nødvendige opgaver for mange projekter på STM32 (Blue Pill) og STM8 controllere. Alle demo-projekterne handler om mine yndlings LED'er, som vi vil tænde i store mængder, hvilket kræver interessant periferiudstyr.
Teksten er igen blevet enorm, så for nemheds skyld laver jeg et indholdsforklaring:
Ansvarsfraskrivelse: Jeg er ikke ingeniør og gør ikke krav på dyb viden inden for elektronik; artiklen er beregnet til ligesindede amatører som mig selv. Faktisk har jeg overvejet mig selv for to år siden som målgruppe. Hvis nogen havde fortalt mig dengang, at det ikke er skræmmende at læse datablade for en ukendt chip, ville jeg ikke have spildt en masse tid på at lede efter stykker kode på internettet og finde på løsninger med saks og plaster.
I centrum af denne artikel er datablade og ikke projekter, så koden kan være lidt ustruktureret og ofte improviseret. Selve projekterne er meget enkle, selvom de er gode til første bekendtskab med en ny chip.
Jeg håber, at min artikel kan hjælpe nogen på et lignende trin i deres hobbyindtræden.
STM32
16 LED'er med DM634 og SPI
Et lille projekt med brug af Blue Pill (STM32F103C8T6) og LED-driveren DM634. Med datablade vil vi forstå driveren, STM IO-porte og indstille SPI.
DM634
Taiwansk chip med 16 16-bit PWM-udgange, kan forbindes i kæder. Den mindre 12-bit model er kendt fra et indenlandsk projekt. . Da jeg skulle vælge mellem DM63x og den velkendte TLC5940, valgte jeg DM af flere grunde: 1) TLC på AliExpress er helt sikkert en kopi, mens denne ikke er; 2) DM har en uafhængig PWM med sin egen frekvensgenerator; 3) den kunne købes billigt i Moskva, uden at jeg skulle vente på en pakke fra Ali. Og selvfølgelig var det interessant at lære at styre chippen selv, fremfor at bruge et færdigt bibliotek. Chips findes i øjeblikket primært i SSOP24-kabinetter, som er lette at lodde på en adapter.
Da producenten er taiwansk, er skrevet på kinesisk engelsk, så det bliver sjovt. Først ser vi på pinout (Pin Connection), for at forstå, hvilken ben der skal tilsluttes hvor, og beskrivelsen af pins (Pin Description). 16 udgange:

Kilder til indkommende jævnstrøm (åben kollektor)
Sink / Open-drain output – kollektor; kilde til indkommende strøm; udgang, der i aktiv tilstand er tilsluttet jorden, – lysdioderne forbindes til driveren med katoder. Elektrisk set er dette selvfølgelig ikke en "åben kollektor" (open drain), men i databladene er denne betegnelse ofte brugt for udgange i kollektortilstand.
![]()
Eksterne modstande mellem REXT og GND for at fastsætte værdien af udgangsstrømmen
Mellem pin REXT og jorden sættes en reference-modstand, som kontrollerer den interne modstand af udgangene, se graf på side 9 i databladet. I DM634 kan denne modstand også styres programmæssigt, således at den samlede lysstyrke (global brightness); i denne artikel vil jeg ikke gå ind i detaljerne, jeg sætter bare en modstand på 2,2 – 3 kΩ her.
For at forstå, hvordan man styrer chippen, lad os se på beskrivelsen af enhedens interface:

Ah, her er det, det kinesiske engelsk i al sin pragt. At oversætte dette er problematisk, men det kan forstås ved lidt vilje, men der er en anden vej – lad os se, hvordan tilslutningen beskrives i databladet for den funktionelt nært beslægtede TLC5940:

… For at sende data til enheden kræves der kun tre pins. Den stigende kant af SCLK-signalet skifter data fra pin SIN til det interne register. Når alle data er indlæst, fastholder et kort højt signal XLAT sekventielt de overførte data i de interne registre. De interne registrer er flip-flops, som trigges af signalet XLAT. Alle data sendes med den mest betydende bit først.
Latch – flip-flop/lås.
Rising edge – den stigende kant af impulsen
MSB first – med den mest signifikante (yderste venstre) bit frem.
til at klokke data – at sende data sekventielt (bit for bit).
Ordet latch ses ofte i dokumentationen til chips og oversættes forskelligt, så for forståelsens skyld vil jeg tillade mig
en lille lektionLED-driver – er i bund og grund et skifteregister. "Skift" (shift) i navnet – er bitflytning af data indenfor en enhed: hver ny bit, der skubbes ind, skubber hele kæden foran sig fremad. Da ingen ønsker at observere tilfældige blinkende lysdioder under skiftet, sker processen i bufferregistre, adskilt fra de aktive ved en skillevæg (latch) – dette er en slags venteværelse, hvor bittene bliver arrangeret i den ønskede rækkefølge. Når alt er klar, åbnes skillevæggen, og bittene sendes til arbejde, og erstatter den tidligere batch. Ordet latch i dokumentationen til mikroskeme refererer næsten altid til en sådan skillevæg, uanset hvilke kombinationer det måtte bruges i.
Sådan overføres data i DM634: vi sætter input DAI til værdien af den mest signifikante bit af den fjerneste LED, trækker DCK op og ned; vi sætter input DAI til værdien af den næste bit, trækker DCK; og så videre, indtil alle bit er sendt (clocked in), hvorefter vi trækker LAT. Dette kan gøres manuelt (bit-bang), men det er bedre at bruge et interface specielt designet til dette, som heldigvis er tilgængeligt på vores STM32 i to eksemplarer.
Den blå tablet STM32F103
Indledning: STM32-controllere er betydeligt mere komplekse end Atmega328, end man måske skulle tro. På grund af energibesparende hensyn er næsten al periferien deaktiveret i starten, og clockfrekvensen er 8 MHz fra den interne kilde. Heldigvis har STM-programmørerne skrevet kode, der bringer chippen op til de "beregnede" 72 MHz, og forfatterne til alle de IDE'er, jeg kender til, har inkluderet det i initialiseringsproceduren, så vi behøver ikke at takte (men ). Men at aktivere periferien bliver vi nødt til.
Dokumentation: på Blue Pill er der en populær chip STM32F103C8T6, som der er to nyttige dokumenter til:
- for mikrocontrollere STM32F103x8 og STM32F103xB;
- for hele linjen af STM32F103 og mere.
I dataarket kan vi være interesserede i:
- Pinouts – pin-udgange til chipsene – i tilfælde af at vi beslutter at lave printplader selv;
- Hukommelseskort – hukommelseskort til en specifik chip. I Referencehåndbogen findes der et kort for hele serien, der nævner registre, som ikke er i vores.
- Pin Definitions Tabel – liste over hoved- og alternative funktioner for pins; for "den blå pille" kan man finde mere brugervenlige billeder med en liste over pins og deres funktioner på internettet. Så lad os straks Google Blue Pill pinout og have et sådant billede ved hånden:

NB: billedet fra internettet havde en fejl, som blev bemærket i kommentarerne, tak for det. Billedet er blevet udskiftet, men dette er en lektion – information, der ikke er fra databladene, bør man bedre kontrollere.
Vi fjerner databladet, åbner Referencehåndbogen, og fra nu af bruger vi kun den.
Handlingsrækkefølge: vi klarer standard input/output, konfigurerer SPI, aktiverer den ønskede periferi.
Input-output
På Atmega328 er input-output implementeret ganske enkelt, hvilket kan forvirre på grund af overfloden af muligheder med STM32. Lige nu har vi kun brug for udgange, men der er endda fire varianter af dem:

output med åbent kolektor, push-pull output, alternativ push-pull, alternativ åbent kolektor
Push-pull (push-pull) – den velkendte udgang fra Arduino, pin kan tage værdien enten HIGH eller LOW. Men med "åben kollektor" opstår der , men faktisk er det hele ret simpelt:


Udgiftskonfiguration / når porten er tildelt til output: / outputbuffer er aktiveret: / – åben kollektor tilstand: "0" i udgangsregisteret aktiverer N-MOS, "1" i udgangsregisteret lader porten være i Hi-Z tilstand (P-MOS aktiveres ikke) / – push-pull tilstand: "0" i udgangsregisteret aktiverer N-MOS, "1" i udgangsregisteret aktiverer P-MOS.
Den eneste forskel mellem åben kollektor (open drain) og push-pull (push-pull) er, at i den første kan pin ikke tage tilstanden HIGH: når man skriver en enhed i udgangsregisteret, går den i høj modstandstilstand (high impedance, Hi-Z). Når man skriver nul, opfører pin sig ens i begge tilstande, både logisk og elektrisk.
I normal output tilstand transmittes indholdet af udgangsregisteret simpelt hen. I "alternativ" styres den af den tilhørende periferi (se 9.1.4):
![]()
Hvis portbit er konfigureret som output af alternativ funktion, deaktiveres udgangsregisteret, og pin tilsluttes udgangssignalet fra periferien.
Hver pins alternative funktion er beskrevet i Pin Definitions Databladet findes på det downloadede billede. Til spørgsmålet om, hvad man skal gøre, hvis en pin har flere alternative funktioner, giver en fodnote i databladet svaret:
![]()
Hvis flere perifere blokke bruger den samme pin, bør der kun anvendes én perifere blok ad gangen for at undgå konflikt mellem de alternative funktioner, og dette styres via et aktiveringsbit i periferiens takttype (i den relevante RCC-register).
Endelig har pins i outputtilstand også taktfrekvens. Dette er endnu en funktion for energibesparelse; i vores tilfælde sætter vi det bare til maksimum og glemmer det.
Så: vi bruger SPI, så to pins (data og clock) skal være 'alternativ funktion trækkes/skubbes', og en anden (LAT) er 'almindelig trækkes/skubbes'. Men før vi definerer dem, lad os se nærmere på SPI.
SPI
En lille lektion
SPI eller Serial Peripheral Interface (seriell perifer grænseflade) er en enkel og meget effektiv grænseflade til kommunikation mellem mikrocontrollere (MCU) og omkringliggende komponenter. Dets funktionsprincip er allerede beskrevet ovenfor, hvor det drejede sig om den kinesiske LED-driver (se brugsmanualen, afsnit 25). SPI kan fungere i både master- og slave-tilstand. SPI har fire grundlæggende kanaler, hvoraf ikke alle nødvendigvis er aktiveret:
- MOSI, Master Output / Slave Input: denne pin transmitterer data i mastertilstand og modtager data i slavemodus;
- MISO, Master Input / Slave Output: omvendt, modtager data i mastertilstand og transmitterer data i slavemodus;
- SCK, Serial Clock: bestemmer frekvensen for dataproduktion i mastertilstand eller modtager taktsignal i slavemodus. I bund og grund timmer den bits;
- SS, Slave Select: gennem denne kanal kan slaven vide, at der er en anmodning. I STM32 kaldes det NSS, hvor N = negativ, dvs. controlleren bliver slave, hvis der er jord på denne kanal. Det fungerer godt sammen med Open Drain Output-tilstand, men det er en anden historie.
Som alt andet er SPI på STM32 rigt på funktioner, hvilket gør forståelsen lidt kompleks. For eksempel kan det ikke kun arbejde med SPI, men også med I2S-interface, og i dokumentationen er deres beskrivelser blandet sammen; man skal skære det overflødige væk i tide. Vores opgave er dog ekstremt enkel: vi skal bare transmittere data ved at bruge kun MOSI og SCK. Lad os gå til afsnit 25.3.4 (halv-dupleks kommunikation), hvor vi finder: 1 clock and 1 unidirectional data wire (1 taktsignal og 1 unidirectional datalinje):

I denne tilstand bruger applikationen SPI enten i kun transmissions- eller kun modtagelsesmode. / Kun transmissionsmode ligner duplexmode: data sendes via transmissionspinen (MOSI i mastermode eller MISO i slavemodel), og modtagspinen (MISO eller MOSI henholdsvis) kan bruges som en almindelig I/O-pin. I dette tilfælde er det tilstrækkeligt for applikationen at ignorere Rx-pufferten (hvis man læser den, vil der ikke være overførte data).
Fint, MISO-pinen er ledig, lad os forbinde LAT-signalet til den. Vi skal finde ud af Slave Select, som kan styres programmatisk i STM32, hvilket er utroligt praktisk. Vi læser det tilsvarende afsnit i sektion 25.3.1 SPI Generel Beskrivelse:

Programmatisk kontrol af NSS (SSM = 1) / Oplysninger om valg af slave er indeholdt i SSI-biten i SPI_CR1-registeret. Den eksterne NSS-pin forbliver ledig til andre formål i applikationen.
Det er tid til at skrive til registrene. Jeg har besluttet at bruge SPI2, lad os finde dens basaddress i databladet – i sektion 3.3 Memory Map:
![]()
Og så begynder vi:
#define _SPI2_(mem_offset) (*(volatile uint32_t *)(0x40003800 + (mem_offset)))Lad os åbne sektion 25.3.3 med den sigende titel "Indstilling af SPI i mastermode":

1. Indstil klokfrekvensen for seriel interface med bitene BR[2:0] i SPI_CR1-registeret.
Registrene findes i det tilsvarende afsnit i reference manualen. Adresseoffset (Address offset) i CR1 – 0x00, som standard er alle bit nulstillet (Reset value 0x0000):

Bitene BR indstiller divideren for controllerens klokfrekvens, og bestemmer dermed den frekvens, som SPI vil fungere på. STM32-frekvensen vil være 72 MHz, LED-driveren, ifølge dens datablad, fungerer med en frekvens på op til 25 MHz, så vi skal dividere med fire (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. Indstil bitene CPOL og CPHA for at bestemme forholdet mellem dataoverførsel og timing af den serielle interface (se diagrammet på side 240)
Da vi her læser databladet og ikke ser på diagrammer, lad os bedre studere den tekstlige beskrivelse af bitene CPOL og CPHA på side 704 (SPI Generel Beskrivelse):

Fase og polaritet af kloksignalet
Med bitene CPOL og CPHA i SPI_CR1-registeret kan man programmatisk vælge mellem fire tidsforhold. Bit CPOL (kloksignalets polaritet) kontrollerer tilstanden af kloksignalet, når data ikke overføres. Dette bit styrer master- og slave-modes. Hvis CPOL nulstilles, er SCK-pinen i hvilende tilstand i lavt niveau. Hvis bit CPOL er sat, er SCK-pinen i hvilende tilstand i højt niveau.
Hvis CPHA-bittet (fase på clock signalet) er indstillet, fungerer den anden kant af SCK-signalet (faldende, hvis CPOL er nulstillet, eller stigende, hvis CPOL er indstillet) som strobe for den mest signifikante bit. Daten registreres ved den anden ændring af clock signalet. Hvis CPHA-bittet er nulstillet, fungerer den fremadgående kant af SCK-signalet (faldende, hvis CPOL er indstillet, eller stigende, hvis CPOL er nulstillet) som strobe for den mest signifikante bit. Daten registreres ved den første ændring af clock signalet.
Når vi dykker ned i disse oplysninger, konkluderer vi, at begge bits skal forblive nul, da vi ønsker, at SCK-signalet forbliver lavt, når det ikke er i brug, og data skal overføres ved den fremadgående kant af impulsen (se Rising Edge i DM634-datasheetet).
Forresten, her stødte vi for første gang på en særegenhed ved terminologien i ST-datasheets: i dem skriver man frasen «reset a bit» – at nulstille en bit, ikke at rydde en bit, som for eksempel i Atmega.
3. Indstil DFF-bittet for at bestemme 8-bit eller 16-bit data blok format
Jeg tog med vilje den 16-bit DM634 for ikke at bekymre mig om at overføre 12-bit PWM-data, som i DM633. Det giver mening at sætte DFF til én:
#define DFF 0x0800
_SPI2_ (_SPI_CR1) |= DFF; // 16-bit mode4. Konfigurer LSBFIRST-bittet i SPI_CR1-registeret for at bestemme blokformatet
LSBFIRST, som navnet antyder, indstiller transmissionen til at starte med den mindste bit. Men DM634 ønsker at modtage data, der begynder med den mest signifikante bit. Derfor lader vi det være nulstillet.
5. I hardwaretilstand, hvis indgang fra NSS-pinnen er påkrævet, skal du tage NSS-pinnen højt under hele byte-overførselssekvensen. I programmodesæt NSS, skal du indstille SSM og SSI-bits i SPI_CR1-registeret. Hvis NSS-pinnen skal fungere som udgang, skal du kun indstille SSOE-bittet.
Vi indstiller SSM og SSI for at glemme hardwaretilstand NSS:
#define SSI 0x0100
#define SSM 0x0200
_SPI2_ (_SPI_CR1) |= SSM | SSI; //enable software control of SS, SS high6. Både MSTR og SPE bits skal være indstillet (de forbliver indstillet, så længe der påføres højsignal på NSS)
Disse bits definerer faktisk vores SPI som master og aktiverer det:
#define MSTR 0x0004
#define SPE 0x0040
_SPI2_ (_SPI_CR1) |= MSTR; //SPI master
//когда все готово, включаем SPI
_SPI2_ (_SPI_CR1) |= SPE;SPI er konfigureret, lad os straks skrive funktioner, der sender bytes til driveren. Fortsæt med at læse 25.3.3 «Indstilling af SPI i master-tilstand»:

Dataoverførselsrækkefølge
Overførslen begynder, når en byte er indtastet i Tx-bufferet.
Dataen i byte indlæses i skiftregisteret i parallelt mode (fra den interne bus) under overførslen af den første bit, hvorefter den overføres i seriel I MOSI-tilstand, er den første eller sidste bit fremad, afhængigt af indstillingen af LSBFIRST-bit i CPI_CR1-registeret. TXE-flaget sættes efter dataoverførsel fra Tx-bufferet til skiftregistrets, og et interrupt oprettes, hvis TXEIE-bit er sat i CPI_CR1-registeret.
Jeg har fremhævet nogle ord i oversættelsen for at påpege en særlig egenskab ved SPI-implementeringen i STM controllere. På Atmega sættes TXE-flaget (Tx Empty, Tx er tomt og klar til at modtage data) kun når hele byte'en er sendt ud. Her sættes dette flag, når byte'en er blevet skubbet ind i det interne skiftregister. Da det skubbes ind med alle bits samtidig (parallelt), og data derefter sendes sekventielt, sættes TXE, inden byte'en er helt sendt. Dette er vigtigt, da vi i vores LED-driver skal aktivere LAT-pinen efter afsendelse af alle data, altså vil kun TXE-flaget ikke være tilstrækkeligt.
Det betyder, at vi har brug for et ekstra flag. Lad os kigge i 25.3.7 – "Statusflader":

<…>

BUSY-flaget
BSY-flaget sættes og nulstilles hardwaremæssigt (skrift i det påvirker intet). BSY-flaget viser status for SPI-kommunikationslaget.
Det nulstilles:
når overførslen er afsluttet (bortset fra master-tilstand, hvis overførslen er kontinuerlig)
når SPI er deaktiveret
når der er en master-tilstandsfejl (MODF=1)
Hvis overførslen ikke er kontrolleret, nulstilles BSY-flaget mellem hver dataoverførsel.
Okay, det kan være nyttigt. Lad os finde ud af, hvor Tx-bufferet er. Til dette læser vi "SPI Data Register":

Bits 15:0 DR[15:0] Dataregister
Modtagne data eller data til overførsel.
Dataregisteret er opdelt i to buffere – en til skrivning (overførselsbuffer) og en anden til læsning (modtagerbuffer). Skrift i dataregisteret skriver til Tx-bufferet, mens læsning fra dataregisteret returnerer værdien, der findes i Rx-bufferet.
Og her er statusregisteret, hvor TXE- og BSY-fladerne findes:

Skriv:
#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
}Da vi skal sende 16 gange to bytes, svarende til antallet af udgange i LED-driveren, ser det således ud:
void sendLEDdata()
{
LAT_low();
uint8_t k = 16;
do
{ k--;
dm_shift16(leds[k]);
} while (k);
while (_SPI2_(_SPI_SR) & BSY); // afslut transmission
LAT_pulse();
}Men vi kan endnu ikke styre LAT-pinen, så lad os vende tilbage til I/O.
Tiller pinner
Registrene i STM32F1, der styrer tilstanden af benene, er ret usædvanlige. Det er klart, at de er flere end hos ATMega, men de adskiller sig også fra andre STM-chips. Sektion 9.1 Generel beskrivelse af GPIO:

Hver af de generelle input/output-porte (GPIO) har to 32-bit konfigurationsregistre (GPIOx_CRL og GPIOx_CRH), to 32-bit dataregistre (GPIOx_IDR og GPIOx_ODR), et 32-bit set/reset-register (GPIOx_BSRR), et 16-bit reset-register (GPIOx_BRR) og et 32-bit låseregister (GPIOx_LCKR).
De er usædvanlige og også ret besværlige; de første to registre fordi 16 ben i porten er spredt ud over dem i et format "fire bit per gruppe". Det vil sige, at benene fra nul til syv sidder i CRL, mens de øvrige er i CRH. De andre registre kan dog gemme bittene for alle benene i porten – ofte forbliver de halvt "reserverede".
For enkelheds skyld begynder vi med slutningen af listen.
Vi får ikke brug for låseregistret.
Set/reset registrene er ret sjove, da de delvist duplikerer hinanden: man kan skrive alt i BSRR, hvor de øverste 16 bits vil nulstille benet til nul, mens de nederste vil sætte det til 1, eller man kan også bruge BRR, hvis nederste 16 bits kun nulstiller benet. Jeg foretrækker den anden mulighed. Disse registre er vigtige, da de sikrer atomisk adgang til benene:

![]()
Atomisk opsætning eller nulstilling
Det er ikke nødvendigt at deaktivere afbrydelser, når man programmerer GPIOx_ODR på bit-niveau: man kan ændre en eller flere bits med en atomisk skriveoperation til APB2. Dette opnås ved at skrive "1" til set/reset-registeret (GPIOx_BSRR eller, kun for nulstilling, til GPIOx_BRR) for den bit, der skal ændres. De andre bits forbliver uændrede.
Dataregistrene har ret sigende navne – IDR = Input Retningsregister, input register; ODR = Output Retningsregister, output register. Vi får ikke brug for dem i dette projekt.
Og til sidst, kontrolregistrene. Da vi er interesseret i benene fra det andet SPI, nemlig PB13, PB14 og PB15, kigger vi straks på CRH:

Og vi ser, at vi skal skrive noget i bits 20 til 31.
Vi har allerede tidligere været inde på, hvad vi ønsker fra pins, så her vil jeg undlade et screenshot og blot sige, at MODE angiver retningen (input, hvis begge bits er sat til 0) og hastigheden på pinnen (vi har brug for 50MHz, dvs. begge pins i '1'), mens CNF angiver tilstanden: normal 'træk og skub' – 00, 'alternativ' – 10. Som vi ser ovenfor, er der for alle pins angivet den tredje nederste bit (CNF0), som sætter dem i tilstand flydende input.
Da jeg planlægger at lave noget mere med denne chip, har jeg for nemhedens skyld defineret alle mulige værdier for MODE og CNF både for de nederste og øverste kontrolregistre.
Sådan er det.
#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 0x20000000Vores pins er på port B (basisadresse – 0x40010C00), kode:
#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;Og tilsvarende kan vi skrive defines for LAT, som vil styre registrene BRR og 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 er bare på inertien, som det altid har været, så lad det forblive sådan)
Nu fungerer alt rigtig godt, men det virker ikke. Fordi dette er STM32, her sparer man på elektriciteten, så det er nødvendigt at aktivere takten til den nødvendige periferi.
Aktiver takten
Det er klokkerne, der håndterer takten, også kaldet Clock. Og vi har allerede set forkortelsen RCC. Lad os finde det i dokumentationen: det er Reset and Clock Control.
Som nævnt ovenfor, heldigvis har folkene fra STM gjort det sværeste i forhold til takten for os, for hvilket de skal have stor tak (jeg giver endnu en gang et link til , så det bliver klart, hvor kompliceret det er). Vi har blot brug for registrene, der håndterer aktiveringen af periferiens takt (Peripheral Clock Enable Registers). Først finder vi basisadressen for RCC, som er i starten af 'Hukommelseskortet':
![]()
#define _RCC_(mem_offset) (*(volatile uint32_t *)(0x40021000 + (mem_offset)))Derefter kan vi enten klikke på linket for at forsøge at finde noget i tabellen, eller meget bedre, hurtigt gennemgå beskrivelsen af aktiverende registre fra sektionerne om aktiveringsregistre. Her finder vi RCC_APB1ENR og RCC_APB2ENR:


Og i dem, henholdsvis, bits, der aktiverer takten til SPI2, IOPB (I/O Port B) og alternative funktioner (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;Den endelige kode kan findes .
Hvis du har mulighed for at teste og lyst, så tilslut DM634 således: DAI til PB15, DCK til PB13, LAT til PB14. Forsyn driveren med 5 volt, og glem ikke at forene jorderne.

STM8 PWM
PWM på STM8
Da jeg først planlagde denne artikel, besluttede jeg at forsøge at mestre en funktion fra en ukendt chip kun ved hjælp af databladet, så jeg ikke endte som en skomager uden sko. STM8 var ideel til denne rolle: for det første havde jeg et par kinesiske boards med STM8S103, og for det andet er den ikke så populær, hvilket gør fristelsen til at søge løsninger på nettet begrænset af manglen på netop disse løsninger.
Der er også et og , i den første pinout og adresser på registrene, i den anden – alt det andet. STM8 programmeres i C i en ganske grim IDE .
Kroning og input-output
Som standard arbejder STM8 på en frekvens på 2 MHz, det skal straks rettes.

Takt signal HSI (High-Speed Internal)
Takt signal HSI kommer fra en intern 16 MHz RC-generator med en programmerbar divisor (fra 1 til 8). Det angives i kilderegistret for taktsignalet (CLK_CKDIVR).
Bemærk: ved opstart vælges den ledende kilde til taktsignalet HSI RC-generatoren med divisor 8.
Vi finder adressen på registreret i databladet, beskrivelsen i refman, og ser at registret skal nulstilles:
#define CLK_CKDIVR *(volatile uint8_t *)0x0050C6
CLK_CKDIVR &= ~(0x18);Da vi planlægger at aktivere PWM og tilslutte lysdioder, ser vi på pinout:

Chippens størrelse er lille, mange funktioner er tilknyttet de samme pins. Det, der står i firkantede parenteser, er "alternativ funktionalitet", som skiftes med "options bytes" (option bytes) – noget der ligner fuse'en på Atmega. Deres værdier kan ændres programmæssigt, men det er ikke nødvendigt, da der kun aktiveres ny funktionalitet efter genstart. Det er nemmere at bruge ST Visual Programmer (som downloades sammen med Visual Develop), som kan ændre disse bytes. I pinout kan man se, at outputtene CH1 og CH2 fra den første timer er skjult i firkantede parenteser; det er nødvendigt at angive bitene AFR1 og AFR0 i STVP, hvorved den anden også flytter output CH1 fra den anden timer fra PD4 til PC5.
Dermed vil 6 pins styre lysdioderne: PC6, PC7 og PC3 for den første timer, PC5, PD3 og PA3 for den anden.
Opsætningen af selve input-output pins på STM8 er simplere og mere logisk end på STM32:
- den velkendte registreringsdata DDR (Data Direction Register): 1 = output;
- den første kontrolregister CR1 ved output sætter "push-pull" mode (1) eller åben kollektortilstand (0); da jeg tilslutter lysdioder til chippen med katoder, lader jeg her nuller være;
- det andet kontrolregister CR2 ved output sætter taktfrekvensen: 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)); //fastPWM Konfiguration
Lad os først definere termerne:
- PWM Frekvens – frekvensen, hvormed timeren tikker;
- Auto-reload, AR – den automatisk indlæste værdi, som timeren vil tælle til (impulsperioden);
- Update Event, UEV – den begivenhed, der sker, når timeren når AR;
- PWM Duty Cycle – arbejdsforholdet for PWM, ofte kaldet "duty cycle";
- Capture/Compare Værdi – værdien for capture/compare, som timeren når, for at udløse en handling (i tilfælde af PWM – inverterer outputsignalet);
- Preload Værdi – den forudindlæste værdi. Sammenligningsværdi kan ikke ændres, mens timeren tikker, ellers vil PWM-cyklussen gå i stykker. Derfor gemmes de nye værdier i en buffer og hentes derfra, når timeren når slutningen af optællingen og nulstilles;
- Edge-aligned og Center-aligned modes – justering ved kanten og i centrum, det samme som ATMELs Fast PWM og Phase-correct PWM.
- OCiREF, Output Compare Referencen Signal – reference output signal, faktisk det, som i PWM-tilstand vises på den tilsvarende pin.
Som allerede tydeligt fremgår af pinout, har to timere PWM-funktioner – den første og den anden. Begge er 16-bit, den første har mange ekstra funktioner (især kan den tælle både op og ned). Vi skal have, at begge fungerer ens, derfor besluttede jeg at starte med den bevidst mere enklere anden for ikke ved et uheld at bruge noget, der ikke er til stede. Et problem er, at beskrivelsen af PWM-funktionaliteten for alle timere i referencehåndbogen findes i kapitlet om den første timer (17.5.7 PWM Mode), så vi hele tiden skal hoppe frem og tilbage i dokumentet.
PWM på STM8 har en vigtig fordel i forhold til PWM på Atmega:

PWM med edge-alignment
Bunden-op tælling konfiguration
Bund-op tællingen er aktiv, hvis DIR biten i TIM_CR1 registeret er nulstillet
Eksempel
Eksemplet bruger den første PWM-tilstand. PWM referencen signal OCiREF holdes højt, så længe TIM1_CNT < TIM1_CCRi. Ellers falder den til lavt niveau. Hvis sammenligningsværdien i TIM1_CCRi registeret er større end den automatisk indlæste værdi (TIM1_ARR registeret), holdes signalet OCiREF på 1. Hvis sammenligningsværdien er 0, holdes OCiREF på nul.…
Timeren STM8 under update event tjekker først sammenligningsværdi, og derefter giver den referencen signal. På Atmega timeren kører først, og så sammenligner den, hvilket resulterer i sammenligningsværdi == 0 Ved udgangen får vi en nål, som vi skal finde ud af at håndtere (for eksempel ved programmatisk at invertere logikken).
Så hvad vil vi gøre: 8-bit PWM (AR == 255), tæller vi opad, justering ved grænsen. Da lampene er tilsluttet chippen med katoderne, skal PWM'en udgive 0 (LED'en lyser) indtil sammenligningsværdi og 1 efter.
Vi har allerede læst om nogle PWM-tilstand, så vi finder den nødvendige register for den anden timer ved at søge i referencehåndbogen med denne sætning (18.6.8 – TIMx_CCMR1):

110: Første PWM-tilstand – ved tælling opad, er første kanal aktiv, så længe TIMx_CNT < TIMx_CCR1. Ellers er første kanal ikke aktiv. [videre i dokumentet er der en fejlbehæftet kopiering fra timer 1]
111: Anden PWM-tilstand – ved tælling opad, er første kanal ikke aktiv, så længe TIMx_CNT < TIMx_CCR1. Ellers er første kanal aktiv.
Da lysdioderne er tilsluttet MCU med katoderne, passer den anden tilstand os (den første også, men vi kender det ikke endnu).

Bit 3 OC1PE: Aktivér preload-output 1
0: Preload-registeret på TIMx_CCR1 er slukket. Det er muligt at skrive til TIMx_CCR1 når som helst. Den nye værdi virker straks.
1: Preload-registeret på TIMx_CCR1 er tændt. Læsning/skriveoperationer refererer til preload-registeret. Den forudindlæste værdi TIMx_CCR1 indlæses i skygge-registeret ved hver opdateringshændelse.
*Bemærk: For at PWM-tilstanden fungerer korrekt, skal preload-registre være tændt. Dette er ikke nødvendigt i enkelt signal-tilstand (bit OPM er indstillet i registeret TIMx_CR1).
Okay, vi tænder for alt, hvad der er nødvendigt, for de tre kanaler i den anden timer:
#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 består af to otte-bit registre, her er det simpelt:
#define TIM2_ARRH *(volatile uint8_t *)0x00530F
#define TIM2_ARRL *(volatile uint8_t *)0x005310
TIM2_ARRH = 0;
TIM2_ARRL = 255;Den anden timer kan kun tælle opad, justering ved grænsen, der skal ikke ændres noget. Vi indstiller frekvensdeleren, for eksempel til 256. Frekvensdeleren for den anden timer indstilles i registeret TIM2_PSCR og er en potens af to:
#define TIM2_PSCR *(volatile uint8_t *)0x00530E
TIM2_PSCR = 8;Nu skal udgange og selve den anden timer aktiveres. Den første opgave løses med registrene Capture/Compare Aktiver: der er to af dem, tre kanaler er spredt asymmetrisk.
#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;Her kan vi også se, at vi kan ændre signalpolariteten, dvs. det var i princippet også muligt at bruge PWM Mode 1. Vi skriver:

#define TIM2_CR1 *(volatile uint8_t *)0x005300
TIM2_CR1 |= 1;Og endelig, vi starter timeren i registeret TIMx_CR1: Vi vil skrive en simpel analog AnalogWrite(), der vil overføre timerens værdier til sammenligning. Registrene hedder forudsigeligt,, to hver kanal er der to: de lavere 8 bit i TIM2_CCRxL og de højere i TIM2_CCRxH. Da vi har indført 8-bit PWM, er det tilstrækkeligt kun at skrive de lavere bits:
#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;
}Den opmærksomme læser vil bemærke, at vi har fået en let defekt PWM, der ikke kan give 100% duty cycle (ved det maksimale værdi 255 inverteres signalet i en timercyklus). For lysdioder er det ikke vigtigt, og den opmærksomme læser kan selv gætte, hvordan dette kan rettes.
PWM på den anden timer fungerer, lad os gå videre til den første.
Den første timer har præcis de samme bits i de samme registre (bare de bits, der var "reservede", i den anden timer, bruges aktivt i den første til forskellige avancerede ting). Så det er tilstrækkeligt at finde adresserne til disse registre i datablade og kopiere koden. Og ændre værdien af frekvensdelingen, da den første timer ønsker at modtage et præcist 16-bit værdi i to registre, ikke en potens af to. Prescaler High og Lav. Vi gør alt dette og… den første timer fungerer ikke. Hvad er problemet?
Problemet kan kun løses ved at gennemgå hele afsnittet om kontrolregistre for timer 1, hvor vi leder efter den, som ikke findes i den anden timer. Der vil være en. 17.7.30 Break register (TIM1_BKR), hvor der er en bit:
![]()
Aktiver hovedudgangen
#define TIM1_BKR *(volatile uint8_t *)0x00526D
TIM1_BKR = (1<<7);Nu virker det helt sikkert, koden .

STM8 Multiplex
Multiplexering på STM8
Det tredje mini-projekt består i at tilslutte otte RGB-lysdiode til den anden timer i PWM-tilstand og få dem til at vise forskellige farver. I bunden ligger konceptet LED-multiplexering, som indebærer, at hvis vi tændere og slukker lysdioderne meget, meget hurtigt, vil vi have indtryk af, at de lyser konstant (persistence of vision, inerti i visuel opfattelse). Engang lavede jeg .
Algoritmen ser således ud:
- tilslut anoden til den første RGB LED;
- tændte den ved at sende de nødvendige signaler til katoderne;
- ventede på enden af PWM-cyklussen;
- tilsluttede anoden til den anden RGB LED;
- tændte den…
Og så videre. Selvfølgelig kræver det, at tilslutningen af anoden og "tændingen" af lysdioden sker samtidig. Nå, eller næsten. Under alle omstændigheder skal vi skrive en kode, der vil sende værdier til tre kanaler på den anden timer, ændre dem ved at nå UEV og samtidig ændre den aktive RGB-lysdiode.
Da skiftet af LED sker automatisk, skal der oprettes en "videohukommelse", hvorfra afbryderen får data. Dette er et simpelt array:
uint8_t colors[8][3];For at ændre farven på en bestemt LED er det nok at skrive de ønskede værdier i dette array. Variablen, der holder styr på den aktive LED, vil være
uint8_t cnt;Demux
Til korrekt multiplexing har vi, lidt ironisk, brug for demultiplexoren CD74HC238. Demultiplexoren er en chip, der hardware-implementerer operatoren <<. Gennem tre indgangsben (bit 0, 1 og 2) indtaster vi et 3-bit tal X, og den aktiverer output nummeret (1<<X). De øvrige indgange på chippen bruges til at skalere hele konstruktionen. Vi har brug for denne chip, ikke kun for at reducere antallet af ben på mikrokontrolleren, men også for sikkerheden – for ikke ved et uheld at tænde flere LEDs, end det er muligt, og dermed brænde MKu. Chippen er billig, og den er værd at have liggende i hjemmets værktøjskasse.
CD74HC238 vil sørge for at tilføre strøm til anoden af den ønskede LED. I en fuld multiplex ville den tilføre strøm til søjlen gennem en P-MOSFET, men i denne demo kan vi gøre det direkte, da den trækker 20 mA, ifølge absolute maximum ratings datasheetet. Fra har vi brug for pinout og denne huskeblads:

H = højt spændingsniveau, L = lavt spændingsniveau, X – ligegyldigt
Vi tilslutter E2 og E1 til jord, E3, A0, A1 og A3 til benene PD5, PC3, PC4 og PC5 på STM8. Da tabellen ovenfor indeholder både lave og høje niveauer, skal vi indstille disse ben som push-pull-output.
PWM
PWM på den anden timer konfigureres på samme måde som i den tidligere historie, med to forskelle:
For det første skal vi aktivere afbrydelsen for Update Event (UEV), som vil kalde funktionen, der skifter den aktive LED. Dette gøres ved at ændre biten Update Interrupt Enable i en register med det talende navn

Interrupt Enable Register
#define TIM2_IER *(volatile uint8_t *)0x005303
//enable interrupt
TIM2_IER = 1;Den anden forskel handler om fænomenet multiplexing kaldet ghosting – parasitær lysudstråling fra dioder. I vores tilfælde kan det opstå, fordi timeren, der kalder et afbrydelse på UEV, fortsætter med at tælle, og afbrydelseshåndtereren ikke når at skifte LED, før timeren allerede begynder at sende noget til udgangene. For at bekæmpe dette må vi invertere logikken (0 = maksimal lysstyrke, 255 = intet lyser) og undgå yderlige værdier af duty cycle. Dvs. at sikre, at efter UEV slukker lysdioderne helt i en takt af PWM.
Ændrer polariteten:
//set polarity
TIM2_CCER1 |= (CC1P | CC2P);
TIM2_CCER2 |= CC3P;Undgå at sætte r, g og b til 255 og husk at invertere dem under brug.
Afbrydelser
Essensen af en afbrydelse er, at chippen under visse omstændigheder stopper med at udføre hovedprogrammet og kalder en ekstern funktion. Afbrydelser opstår på grund af eksterne eller interne påvirkninger, herunder fra timeren.
Da vi første gang oprettede et projekt i ST Visual Develop, fik vi udover main.c et vindue med den mystiske fil stm8_interrupt_vector.c, der automatisk blev inkluderet i projektet. I denne fil er der knyttet en funktion til hver afbrydelse. NonHandledInterrupt. Vi skal knytte vores funktion til den ønskede afbrydelse.
I databladet er der en tabel med afbrydelsesvektorer, hvor vi finder det nødvendige:

13 TIM2 opdatering/overløb
14 TIM2 fangst/sammenligning
Vi skal ændre LED ved UEV, så vi har brug for afbrydelse nr. 13.
Følgelig, for det første, i filen stm8_interrupt_vector.c ændrer vi navnet på funktionen, der som standard håndterer afbrydelse nr. 13 (IRQ13) til vores eget:
{0x82, TIM2_Overflow}, /* irq13 */For det andet må vi oprette en fil main.h med følgende indhold:
#ifndef __MAIN_H
#define __MAIN_H
@far @interrupt void TIM2_Overflow (void);
#endifOg endelig skal vi definere denne funktion i din main.c:
@far @interrupt void TIM2_Overflow (void)
{
PD_ODR &= ~(1<<5); // slukker demultiplexeren
PC_ODR = (cnt<<3); // skriver det nye værdi til demultiplexeren
PD_ODR |= (1<<5); // tænder demultiplexeren
TIM2_SR1 = 0; // nulstiller flaget for Update Interrupt Pending
cnt++;
cnt &= 7; // flytter LED-tælleren
TIM2_CCR1L = ~colors[cnt][0]; // sender inverterede værdier til bufferen
TIM2_CCR2L = ~colors[cnt][1]; // til næste PWM-cyklus
TIM2_CCR3L = ~colors[cnt][2]; //
return;
}Det er tilbage at aktivere afbrydelserne. Dette gøres med en assembler-kommando rim – den skal findes i :
//enable interrupts
_asm("rim");En anden assembler-kommando – sim – slår afbrydelser fra. De skal deaktiveres, mens nye værdier skrives til "videohukommelsen", for at en afbrydelse, der opstår på et uheldigt tidspunkt, ikke beskadiger arrayet.
Hele koden – .

Hvis denne artikel kan være til nytte for nogen, så har jeg ikke skrevet den forgæves. Jeg ser frem til kommentarer og bemærkninger og vil forsøge at svare på alle.
Kilde: habr.com
