Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

В the first part I tried to tell hobby electronics engineers who grew out of Arduino pants how and why they should read datasheets and other documentation for microcontrollers. The text turned out to be large, so I promised to show practical examples in a separate article. Well, he called himself a loader ...

Today I will show you how to use datasheets to solve fairly simple, but necessary tasks for many projects on STM32 (Blue Pill) and STM8 controllers. All demo projects are dedicated to my favorite LEDs, we will light them in large quantities, for which we will have to use all sorts of interesting peripherals.

The text again turned out to be huge, so for convenience I’m making the content:

STM32 Blue Pill: 16 LEDs with DM634 driver
STM8: Setting Up Six PWM Pins
STM8: 8 RGB LEDs on three pins, interrupts

Disclaimer: I am not an engineer, I do not pretend to have deep knowledge in electronics, the article is intended for amateurs like me. In fact, I considered myself two years ago as the target audience. If someone had told me then that it’s not scary to read datasheets for an unfamiliar chip, I would not have spent a lot of time looking for some pieces of code on the Internet and inventing crutches with scissors and adhesive plaster.

The focus of this article is datasheets, not drafts, so the code may not be too polished and often a crutch. The projects themselves are very simple, although they are suitable for the first acquaintance with a new chip.

I hope that my article will help someone at a similar stage of diving into a hobby.

STM32

16 LEDs with DM634 and SPI

Small project using Blue Pill (STM32F103C8T6) and DM634 LED driver. With the help of datasheets, we will deal with the driver, STM IO ports and configure SPI.

DM634

Taiwanese chip with 16 x 16-bit PWM outputs, can be chained. The younger 12-bit model is known from a domestic project light pack. At one time, choosing between DM63x and the well-known TLC5940, I settled on DM for several reasons: 1) TLC on Aliexpress is definitely fake, but this one is not; 2) DM has an autonomous PWM with its own frequency generator; 3) it could be bought inexpensively in Moscow, and not wait for a parcel from Ali. And, of course, it was interesting to learn how to control the chip yourself, and not use a ready-made library. Chips are now mainly presented in the SSOP24 package, they are easy to solder on the adapter.

Since the manufacturer is Taiwanese, datasheet to the chip is written in Chinese English, which means it will be fun. First look at the pinoutPin connection) to understand which leg to connect what, and a description of the pins (Pin Description). 16 pins:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
Sink DC sources (open drain)

Zinc / open-drain output - stock; source of incoming current; an output connected to ground in an active state - LEDs are connected to the driver by cathodes. Electrically, this is, of course, no "open drain" (open drain), but in datasheets such a designation for outputs in drain mode is often found.

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
External resistors between REXT and GND to set the output current value

A reference resistor is installed between the REXT pin and ground, which controls the internal resistance of the outputs, see the graph on page 9 of the datasheet. In the DM634, this resistance can also be controlled by software by setting the overall brightness (global brightness); I won’t go into details in this article, I’ll just put a 2.2 - 3 kOhm resistor here.

To understand how to control the chip, let's look at the description of the device interface:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

Yeah, here it is, Chinese English in all its glory. It is problematic to translate this, you can understand it if you wish, but there is another way - to look at how the datasheet describes the connection to the functionally close TLC5940:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
… Only three pins are required to enter data into the device. The rising edge of the SCLK signal shifts the data from the SIN pin to the internal register. After all data has been loaded, a short high XLAT signal latches the serially transferred data in internal registers. Internal registers are gates triggered by the XLAT signal level. All data is transmitted MSB first.

Latch - latch / latch / latch.
rising edge is the leading edge of the pulse
MSB first – the most significant (leftmost) bit forward.
to clock data – transmit data sequentially (bit by bit).

Word latch often found in the documentation for chips and is translated in a variety of ways, so for understanding I will allow myself

small educational programThe LED driver is essentially a shift register. "Shift" (shift) in the name - bit-by-bit movement of data inside the device: each new bit shoved inside pushes the entire chain forward in front of it. Since no one wants to observe the chaotic blinking of LEDs during the shift, the process takes place in buffer registers separated from the workers by a shutter (latch) is a kind of dressing room where the bits line up in the desired sequence. When everything is ready, the shutter opens and the bits go to work, replacing the previous batch. Word latch in the documentation for microcircuits almost always implies such a damper, in whatever combinations it is used.

So, data transfer to DM634 is carried out as follows: set the DAI input to the value of the high bit of the far LED, pull DCK up and down; set the DAI input to the value of the next bit, pull DCK; and so on until all bits have been transmitted (clocked in), after which we pull LAT. This can be done manuallybit bang), but it’s better to use the specially sharpened SPI interface for this, since it is presented on our STM32 in two copies.

Blue Tablet STM32F103

Introductory: STM32 controllers are much more complicated than Atmega328 than they can be scary. At the same time, for reasons of energy saving, almost all peripherals are disabled at the start, and the clock frequency is 8 MHz from an internal source. Fortunately, the STM programmers wrote a code that brings the chip to the “calculated” 72 MHz, and the authors of all the IDEs I know included it in the initialization procedure, so we don’t need to clock (but you can if you really want). But you have to turn on the peripherals.

Documentation: The popular STM32F103C8T6 chip is installed on Blue Pill, there are two useful documents for it:

In the datasheet, we may be interested in:

  • Pinouts - chip pinouts - in case we decide to make boards ourselves;
  • Memory Map - a memory map for a specific chip. The Reference Manual has a map for the entire line, it mentions registers that are not on ours.
  • Pin Definitions table - listing the main and alternative pin functions; for the “blue pill” on the Internet, you can find more convenient pictures with a list of pins and their functions. Therefore, we immediately google Blue Pill pinout and keep this picture at hand:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
NB: there was an error in the picture from the Internet, noticed in the comments, for which thanks. The picture has been replaced, but this is a lesson - it is better to check information not from datasheets.

We remove the datasheet, open the Reference Manual, from now on we use only it.
Procedure: deal with standard input / output, configure SPI, turn on the necessary peripherals.

Input Output

On the Atmega328, I/O is extremely simple, which is why the abundance of STM32 options can be confusing. Now we only need conclusions, but even there are four options:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
Open Drain Output, Push-Pull Output, Push-Pull Alternate, Open-Drain Alternate

"Pull-push" (push pull) - the usual output from the Arduino, the pin can be either HIGH or LOW. But with the "open drain" arise difficulties, although in fact everything is simple here:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
Output configuration / when port is assigned to output: / output buffer enabled: / – open drain mode: "0" in the output register enables N-MOS, "1" in the output register leaves the port in Hi-Z mode (P-MOS is not activated ) / - push-pull mode: "0" in the output register activates N-MOS, "1" in the output register activates P-MOS.

All open drain difference (open drain) from "push-pull" (push pull) is that in the first pin it cannot take the HIGH state: when a unit is written to the output register, it goes into high resistance mode (high impedance, Hi-Z). When writing zero, the pin in both modes behaves the same, both logically and electrically.

In normal output mode, the pin simply translates the contents of the output register. In "alternative" it is controlled by the corresponding peripheral (see 9.1.4):

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
If the port bit is configured as an alternate function output, the output register is disabled and the pin is connected to the output signal of the peripheral.

The alternative functionality of each pin is described in Pin Definitions The datasheet is on the downloaded picture. When asked what to do if the pin has several alternative functions, the answer is given by a footnote in the datasheet:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
If multiple peripherals use the same pin, to avoid conflict between alternative functions, only one peripheral should be used at a time, switching using the Peripheral Clock Enable bit (in the corresponding RCC register).

Finally, pins in output mode also have a clock speed. This is another energy saving feature, in our case we just set it to the maximum and forget it.

So: we are using SPI, which means that two pins (with data and with a clock signal) should be “alternative push-pull function”, and one more (LAT) should be “normal push-pull”. But before assigning them, let's deal with SPI.

SPI

Another little hack

SPI or Serial Peripheral Interface (serial peripheral interface) is a simple and very effective interface for communicating MK with other MKs and the outside world in general. The principle of its operation has already been described above, where about the Chinese LED driver (see section 25 in the reference manual). SPI can operate in master ("master") and slave ("slave") modes. SPI has four basic channels, of which not all may be involved:

  • MOSI, Master Output / Slave Input: this pin sends data in master mode, and receives data in slave mode;
  • MISO, Master Input / Slave Output: on the contrary, in the master it receives, in the slave it gives;
  • SCK, Serial Clock: sets the frequency of data transmission in the master or receives a clock signal in the slave. Essentially, beats the beats;
  • SS, Slave Select: with this channel, the slave knows that they want something from him. On STM32 it is called NSS, where N = negative, i.e. the controller becomes a slave if this channel has a ground. It combos well with the Open Drain Output mode, but that's another story.

Like everything else, SPI on STM32 is rich in functionality, which makes it somewhat difficult to understand. For example, it can work not only with SPI, but also with an I2S interface, and their descriptions are mixed in the documentation, you need to cut off the excess in a timely manner. Our task is extremely simple: you just need to give data using only MOSI and SCK. We go to section 25.3.4 (half-duplex communication, half-duplex communication), where we find 1 clock and 1 unidirectional data wire (1 clock and 1 unidirectional data stream):

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
In this mode, the application uses SPI in either transmit-only or receive-only mode. / The transmit-only mode is similar to duplex mode: data is transmitted on the transmit pin (MOSI in master mode or MISO in slave mode), while the receive pin (MISO or MOSI respectively) can be used as a normal I/O pin. In this case, it is enough for the application to ignore the Rx buffer (if it is read, there will be no transmitted data).

Great, the MISO pin is free, let's connect the LAT signal to it. Let's deal with Slave Select, which can be controlled programmatically on STM32, which is extremely convenient. We read the paragraph of the same name in section 25.3.1 SPI General Description:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
NSS software control (SSM = 1) / Slave selection information is contained in the SSI bit of the SPI_CR1 register. The external NSS pin is left free for other application needs.

It's time to write to the registers. I decided to use SPI2, we are looking for its base address in the datasheet - in section 3.3 Memory Map (Memory Map):

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

Well, let's start:

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

We open section 25.3.3 with the telling title "Configuring SPI in master mode":

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

1. Set the serial interface clock with the BR[2:0] bits in the SPI_CR1 register.

The registers are collected in the reference manual section of the same name. Address shift (address offset) CR1 has 0x00, by default all bits are cleared (reset value 0x0000):

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

The BR bits set the controller clock divider, thus determining the frequency at which the SPI will operate. The STM32 frequency will be 72 MHz, the LED driver, according to its datasheet, operates at a frequency of up to 25 MHz, so we need to divide by four (BR[2:0] = 001).

#define _SPI_CR1 0x00

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

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

2. Set the CPOL and CPHA bits to define the relationship between data transfer and serial interface clock (see diagram on page 240)

Since we are reading a datasheet here, and not looking at schematics, let's take a closer look at the textual description of the CPOL and CPHA bits on page 704 (SPI General Description):

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
Clock phase and polarity
Using the CPOL and CPHA bits of the SPI_CR1 register, you can programmatically select four options for timing ratios. The CPOL (Clock Polarity) bit controls the state of the clock signal when no data is being transmitted. This bit controls master and slave modes. If CPOL is reset, the SCK pin is low at rest. If the CPOL bit is set, the SCK pin is high when idle.
If the CPHA (Clock Phase) bit is set, the MSB trap strobe is the second edge of the SCK signal (falling if CPOL is cleared, or rising edge if CPOL is set). The data is latched on the second clock change. If the CPHA bit is cleared, the rising edge of the SCK signal (falling edge if CPOL is set, or rising edge if CPOL is clear) serves as the high bit trap strobe. Data is latched on the first clock change.

Having tasted this knowledge, we come to the conclusion that both bits must remain zero, because we want the SCK signal to remain low when not in use, and data to be transmitted on the rising edge of the pulse (see fig. rising edge in datasheet DM634).

By the way, here we first encountered a feature of the vocabulary in the ST datasheets: in them the phrase “reset the bit to zero” is written to reset a bitAnd not to clear a bit, like, for example, Atmega.

3. Set the DFF bit to determine the 8-bit or 16-bit data block format

I specifically took a 16-bit DM634 so as not to bother with the transfer of 12-bit PWM data, like the DM633. DFF makes sense to put in unity:

#define DFF         0x0800

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

4. Configure the LSBFIRST bit in the SPI_CR1 register to define the block format

LSBFIRST, as its name implies, sets up the transmission with the least significant bit first. But the DM634 wants to receive data MSB first. Therefore, we leave it reset.

5. In hardware mode, if input from the NSS pin is required, drive the NSS pin high during the entire byte transfer sequence. In NSS program mode, set the SSM and SSI bits in the SPI_CR1 register. If the NSS pin is to be output, only the SSOE bit needs to be set.

Install SSM and SSI to forget about NSS hardware mode:

#define SSI         0x0100
#define SSM         0x0200

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

6. The MSTR and SPE bits must be set (they only remain set if the NSS is high)

Actually, with these bits we assign our SPI as a master and turn it on:

#define MSTR        0x0004
#define SPE         0x0040

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

SPI is configured, let's immediately write functions that send bytes to the driver. Continue reading 25.3.3 "Configuring SPI in Master Mode":

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
Data transfer procedure
The transfer begins when a byte is written to the Tx buffer.
The data byte is loaded into the shift register at parallel mode (from the internal bus) during the transmission of the first bit, after which it is transmitted in consistent MOSI pin mode, first or last bit forward depending on the setting of the LSBFIRST bit in the CPI_CR1 register. TXE flag is set after data transmission from Tx buffer to shift register, and an interrupt is generated if the TXEIE bit in the CPI_CR1 register is set.

I highlighted a few words in the translation to draw attention to one feature of the implementation of SPI in STM controllers. On the Atmega, the TXE flag (Tx Empty, Tx is empty and ready to receive data) is set only after the entire byte has been sent out. And here this flag is set after the byte has been shoved into the internal shift register. Since it is shoved there with all the bits at the same time (in parallel), and then the data is transmitted sequentially, TXE is set before the byte is completely sent. This is important because in the case of our LED driver, we need to pull the LAT pin after sending all data, i.e. only the TXE flag will not be enough for us.

Which means we need another flag. Let's look at 25.3.7 - "Status Flags":

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
<...>
Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
BUSY flag
The BSY flag is set and cleared by hardware (writing to it has no effect). The BSY flag indicates the state of the SPI communication layer.
It resets:
when the transfer is completed (except in master mode if the transfer is continuous)
when SPI is disabled
when a master mode error occurs (MODF=1)
If the transmission is not continuous, the BSY flag is cleared between each data transmission.

Okay, it'll come in handy. Find out where the Tx buffer is located. To do this, read the "SPI Data Register":

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
Bits 15:0 DR[15:0] Data Register
Data received or data to be transmitted.
The data register is divided into two buffers, one for writing (transmit buffer) and one for reading (receive buffer). A write to the data register writes to the Tx buffer, and a read from the data register will return the value contained in the Rx buffer.

Well, the status register, where there are TXE and BSY flags:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

We write:

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

Well, since we need to transfer 16 times two bytes, according to the number of outputs of the LED driver, something like this:

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();
}

But we don't know how to pull the LAT pin yet, so let's go back to I/O.

Assign pins

In STM32F1, the registers responsible for the state of the pins are rather unusual. It is clear that there are more of them than Atmega, but they are also different from other STM chips. Section 9.1 GPIO General Description:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
Each of the general purpose I/O ports (GPIO) has two 32-bit configuration registers (GPIOx_CRL and GPIOx_CRH), two 32-bit data registers (GPIOx_IDR and GPIOx_ODR), a 32-bit set/reset register (GPIOx_BSRR), a 16-bit reset register (GPIOx_BRR), and a 32-bit blocking register (GPIOx_LCKR).

Unusual, and also rather inconvenient, are the first two registers here, because the 16 pins of the port are scattered across them in a “four bits per brother” format. Those. pins XNUMX through XNUMX are in CRL, and the rest are in CRH. At the same time, the remaining registers successfully fit the bits of all the port pins - often remaining half "reserved".

For simplicity, let's start at the end of the list.

We do not need a blocking register.

The set and reset registers are quite funny in that they partially duplicate each other: you can write everything only in BSRR, where the upper 16 bits will reset the pin to zero, and the lower ones will be set to 1, or you can also use BRR, the lower 16 bits of which only reset the pin . I like the second option. These registers are important because they provide atomic access to the pins:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
Atomic install or reset
You do not need to disable interrupts when programming GPIOx_ODR at the bit level: you can change one or more bits with a single APB2 atomic write operation. This is achieved by writing a "1" to the set/reset register (GPIOx_BSRR or, for reset only, GPIOx_BRR) of the bit to be changed. Other bits will remain unchanged.

Data registers have quite speaking names - IDR = Input Direction Register, input register; ODR= output Direction Register, output register. In the current project, we do not need them.

And finally, the control registers. Since we are interested in the pins of the second SPI, namely PB13, PB14 and PB15, we immediately look at CRH:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

And we see that it will be necessary to write something in bits from the 20th to the 31st.

We already figured out what we want from the pins above, so here I will do without a screenshot, just say that MODE sets the direction (input if both bits are set to 0) and the speed of the pin (we need 50MHz, i.e. both pin to "1"), and CNF sets the mode: normal "push-pull" - 00, "alternative" - ​​10. By default, as we can see above, all pins have the third bit from the bottom (CNF0), it sets them to mode floating input.

Since I plan to do something else with this chip, for simplicity, I generally defined all possible MODE and CNF values ​​for both the lower and upper control registers.

Somehow like this

#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

Our pins are on port B (base address - 0x40010C00), code:

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

And, accordingly, you can write defines for LAT, which will twitch the BRR and BSRR registers:

/*** 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 just by inertia, somehow it always was, let it stay for yourself)

Now everything is great, it just doesn't work. Because this is STM32, they save electricity here, which means that you need to turn on the clocking of the necessary peripherals.

Turn on clocking

The clock is responsible for clocking, they are also Clock. And we could already notice the abbreviation RCC. We are looking for it in the documentation: this is Reset and Clock Control (Management of reset and clocking).

As mentioned above, fortunately, the people from STM did the most difficult part of the clocking topic for us, for which many thanks to them (once again I will give a link to Di Halt's websiteto make it clear how confused it is). We only need registers responsible for enabling peripheral clocking (Peripheral Clock Enable Registers). First, let's find the base address of the RCC, it is at the very beginning of the "Memory Card":

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

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

And then either click on the link where to try to find something in the table, or, much better, go over the descriptions of the including registers from the sections about enable registers. Where do we find RCC_APB1ENR and RCC_APB2ENR:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

And in them, respectively, bits that include the clocking of SPI2, IOPB (I / O Port B) and alternative functions (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;

The final code can be found here.

If there is an opportunity and desire to test, then we connect DM634 like this: DAI to PB15, DCK to PB13, LAT to PB14. We feed the driver from 5 volts, do not forget to combine the grounds.

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

STM8 PWM

PWM on STM8

When I was just planning this article, I decided, for example, to try to master some functionality of an unfamiliar chip with the help of only a datasheet, so that a shoemaker would not turn out without boots. STM8 was perfect for this role: firstly, I had a couple of Chinese boards with STM8S103, and secondly, it is not very popular, and therefore the temptation to read and find a solution on the Internet rests on the absence of these same solutions.

The chip also has datasheet и reference manual RM0016, in the first pinout and register addresses, in the second - everything else. Programming STM8 in C in a ugly IDE ST Visual Develop.

Clocking and I/O

By default, STM8 operates at a frequency of 2 MHz, this must be corrected immediately.

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
HSI Clock (High Internal)
The HSI clock is derived from an internal 16 MHz RC oscillator with a programmable divider (1 to 8). It is set in the clock divider register (CLK_CKDIVR).
Note: HSI RC oscillator with a divider of 8 is selected as the master clock source at startup.

We find the address of the register in the datasheet, the description in refman and see that the register needs to be cleared:

#define CLK_CKDIVR *(volatile uint8_t *)0x0050C6

CLK_CKDIVR &= ~(0x18);

Since we are going to run PWM and connect LEDs, let's look at the pinout:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

The chip is small, many functions are suspended on the same pins. What is in square brackets is “alternative functionality”, it is switched by “option bytes” (option bytes) - something like Atmega fuses. You can change their values ​​programmatically, but it is not necessary, because. The new functionality is activated only after a reboot. It's easier to use ST Visual Programmer (downloaded with Visual Develop), which can change these bytes. The pinout shows that the outputs CH1 and CH2 of the first timer are hidden in square brackets; it is necessary to set the AFR1 and AFR0 bits in STVP, and the second one will also transfer the output CH1 of the second timer from PD4 to PC5.

Thus, 6 pins will control the LEDs: PC6, PC7 and PC3 for the first timer, PC5, PD3 and PA3 for the second.

Setting up the I/O pins themselves on the STM8 is simpler and more logical than on the STM32:

  • Atmega-familiar data direction register DDR (Data Direction Register): 1 = output;
  • the first control register CR1, when output, sets the push-pull mode (1) or open drain (0); since I connect the LEDs to the chip with cathodes, I leave zeros here;
  • the second control register CR2 sets the clock speed when outputting: 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

PWM setting

First, let's define terms:

  • PWM Frequency – frequency with which the timer ticks;
  • Auto-reload, AR – autoloaded value, up to which the timer will count (pulse period);
  • Update Event, UEV – an event that occurs when the timer has counted to AR;
  • PWM Duty Cycle - PWM duty cycle, often called "duty cycle";
  • Capture/Compare Value – value to capture/compare, counting up to which the timer will do something (in the case of PWM, it inverts the output signal);
  • preload value – preloaded value. compare value cannot change while the timer is ticking, otherwise the PWM cycle will break. Therefore, new transmitted values ​​are placed in the buffer and pulled out when the timer reaches the end of the countdown and is reset;
  • Edge-aligned и Center-aligned modes – alignment on the border and in the center, the same as atmelovskie Fast PWM и Phase-correct PWM.
  • OCiREF, Output Compare Reference Signal - the reference output signal, in fact, what appears on the corresponding pin in PWM mode.

As is already clear from the pinout, two timers have PWM capabilities - the first and second. Both are 16-bit, the first one has a lot of additional features (in particular, it can count both up and down). We need both to work the same way, so I decided to start with the obviously poorer second one, so as not to accidentally use something that is not in it. Some problem is that the description of the PWM functionality of all timers in the reference manual is in the chapter about the first timer (17.5.7 PWM Mode), so you have to jump back and forth through the document all the time.

PWM on STM8 has an important advantage over Atmega PWM:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
PWM with edge alignment
Account configuration from bottom to top
Up counting is active if the DIR bit in the TIM_CR1 register is clear
Example
The example uses the first PWM mode. The PWM reference signal OCiREF is held high as long as TIM1_CNT < TIM1_CCRi. Otherwise, it takes a low level. If the value to compare in the TIM1_CCRi register is greater than the autoload value (TIM1_ARR register), the OCiREF signal is held at 1. If the comparison value is 0, OCiREF is held at zero....

STM8 timer during update event checks first compare value, and only then produces a reference signal. In Atmega, the timer first shivers, and then compares, as a result of which, when compare value == 0 the output is a needle that must be dealt with somehow (for example, by programmatically inverting the logic).

So what we want to do: 8-bit PWM (AR == 255), counting from bottom to top, alignment along the border. Since the bulbs are connected to the chip by cathodes, the PWM should output 0 (LED on) until compare value and 1 after.

We have already read about some PWM mode, so we find the desired register of the second timer by searching in the reference manual for this phrase (18.6.8 - TIMx_CCMR1):

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
110: First PWM mode - when counting from bottom to top, the first channel is active as long as TIMx_CNT < TIMx_CCR1. Otherwise, the first channel is inactive. [further in the document, erroneous copy-paste from timer 1] 111: Second PWM mode - when counting from bottom to top, the first channel is inactive until TIMx_CNT < TIMx_CCR1. Otherwise, the first channel is active.

Since the LEDs are connected to the MK with cathodes, the second mode suits us (the first one too, but we don’t know this yet).

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
Bit 3 OC1PE: Enable Preloading Output 1
0: Preload register on TIMx_CCR1 disabled. You can write to TIMx_CCR1 at any time. The new value works immediately.
1: Preload register on TIMx_CCR1 enabled. Read/write operations access the preload register. The preloaded value of TIMx_CCR1 is loaded into the shadow register during each update event.
*Note: Preload registers must be enabled for PWM mode to work properly. This is optional in single signal mode (the OPM bit is set in the TIMx_CR1 register).

Okay, turn on everything you need for the three channels of the second 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 consists of two eight-bit registers, everything is simple here:

#define TIM2_ARRH  *(volatile uint8_t *)0x00530F
#define TIM2_ARRL  *(volatile uint8_t *)0x005310

TIM2_ARRH = 0;
TIM2_ARRL = 255;

The second timer can only count from bottom to top, alignment on the border, nothing needs to be changed. Set the frequency divider, for example, to 256. For the second timer, the divider is set in the TIM2_PSCR register and is a power of two:

#define TIM2_PSCR  *(volatile uint8_t *)0x00530E

TIM2_PSCR = 8;

It remains to turn on the conclusions and the second timer itself. The first task is solved by registers Capture/Compare Enable: there are two of them, three channels are scattered asymmetrically over them. Here we can also learn that it is possible to change the polarity of the signal, i.e. in principle, PWM Mode 1 could also be used. We write:

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

And finally, we start the timer in the TIMx_CR1 register:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

#define TIM2_CR1   *(volatile uint8_t *)0x005300

TIM2_CR1 |= 1;

Let's write a simple analogue of AnalogWrite (), which will pass the actual values ​​​​to the timer for comparison. The registers are predictably named Capture/Compare registers, there are two of them for each channel: the low 8 bits in TIM2_CCRxL and the high bits in TIM2_CCRxH. Since we started 8-bit PWM, it is enough to write only the low 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;
}

The attentive reader will notice that we have a slightly defective PWM, unable to give out 100% fill (at a maximum value of 255, the signal is inverted by one timer cycle). For LEDs, this does not play a role, and the attentive reader already guesses how to fix it.

PWM on the second timer works, go to the first.

The first timer has exactly the same bits in the same registers (it's just that those bits that remained "reserved" in the second timer are actively used for all sorts of advanced things in the first). Therefore, it is enough to find the addresses of the same registers in the datasheet and copy the code. Well, change the value of the frequency divider, because. the first timer wants to get not a power of two, but an exact 16-bit value in two registers Prescaler High и low. We do everything and ... the first timer does not work. What's the matter?

The only way to solve the problem is by looking at the entire section about the control registers of timer 1, where we are looking for one that the second timer does not have. there will be 17.7.30 Break register (TIM1_BKR), where there is a bit like this:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
Enable main output

#define TIM1_BKR   *(volatile uint8_t *)0x00526D

TIM1_BKR = (1<<7);

That's all for now, the code ibid..

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

STM8 Multiplex

Multiplexing on STM8

The third mini-project is to connect eight RGB LEDs to the second timer in PWM mode and make them show different colors. It is based on the concept of LED multiplexing, which consists in the fact that if you turn on and off the LEDs very, very quickly, it will seem to us that they are constantly on (persistence of vision, inertia of visual perception). I once did something like that on arduino.

The algorithm of work looks like this:

  • connected the anode of the first RGB LED;
  • lit it, giving the necessary signals to the cathodes;
  • waited for the end of the PWM cycle;
  • connected the anode of the second RGB LED;
  • lit it...

Well, etc. Of course, for beautiful work, it is required that the anode connection and the “ignition” of the LED occur simultaneously. Well, almost. In any case, we need to write a code that will output values ​​​​in three channels of the second timer, change them when UEV is reached and at the same time change the currently active RGB LED.

Since the LED switching is automatic, we need to create "video memory" from where the interrupt handler will receive data. This is a simple array:

uint8_t colors[8][3];

In order to change the color of a particular LED, it will be enough to write the necessary values ​​\uXNUMXb\uXNUMXbinto this array. And the variable will be responsible for the number of the active LED

uint8_t cnt;

Demux

For proper multiplexing, we need, oddly enough, the CD74HC238 demultiplexer. Demultiplexer - a chip that implements the operator in hardware <<. Through three input pins (bits 0, 1 and 2) we feed him a three-bit number X, and in response he activates the output number (1<<X). The remaining inputs of the chip are used to scale the entire design. We need this chip not only to reduce the number of occupied pins of the microcontroller, but also for safety - so as not to accidentally turn on more LEDs than possible and not burn the MK. The chip costs a penny, it should always be kept in the first-aid kit at home.

CD74HC238 will be responsible for supplying voltage to the anode of the desired LED. In a full-fledged multiplex, it would supply voltage to the column through the P-MOSFET, but in this demo, you can do it directly, because. it pulls 20mA, according to absolute maximum ratings in the datasheet. From datasheet CD74HC238 we need a pinout and this cheat sheet:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
H = high voltage level, L = low voltage level, X - don't care

We connect E2 and E1 to ground, E3, A0, A1 and A3 to pins PD5, PC3, PC4 and PC5 of STM8. Since the table above contains both low and high levels, we set up these pins as push-pull pins.

PWM

PWM on the second timer is configured in the same way as in the previous story, with two differences:

First, we need to enable the interrupt on Update Event (UEV) which will call a function to toggle the active LED. This is done by changing the bit Update Interrupt Enable in a register with a speaking name

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
Interrupt enable register

#define TIM2_IER   *(volatile uint8_t *)0x005303

//enable interrupt
TIM2_IER = 1;

The second difference is connected with such a phenomenon of multiplexing as ghosting - parasitic glow of diodes. In our case, it may appear due to the fact that the timer, having caused an interrupt on the UEV, goes on ticking, and the interrupt handler does not have time to switch the LED before the timer starts writing something to the outputs. To combat this, you will have to invert the logic (0 = maximum brightness, 255 = nothing is on) and not allow extreme duty cycle values. Those. ensure that after UEV the LEDs are completely extinguished for one PWM cycle.

Change the polarity:

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

Avoid setting r, g and b to 255 and remember to invert them when using.

Interrupts

The essence of the interrupt is that, under certain circumstances, the chip stops executing the main program and calls some external function. Interrupts occur due to external or internal influences, including from the timer.

When we first created a project in ST Visual Develop, apart from main.c we got a window with a mysterious file stm8_interrupt_vector.cautomatically included in the project. In this file, a function is attached to each interrupt NonHandledInterrupt. We need to bind our function to the desired interrupt.

The datasheet has a table of interrupt vectors, where we find the ones we need:

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8
13 TIM2 update/overflow
14 TIM2 Capture/Compare

We need to change the LED at UEV, so interrupt #13 is needed.

Accordingly, firstly, in the file stm8_interrupt_vector.c change the name of the function responsible for interrupt number 13 (IRQ13) by default to our own:

{0x82, TIM2_Overflow}, /* irq13 */

Secondly, we will have to create a file main.h such content:

#ifndef __MAIN_H
#define __MAIN_H

@far @interrupt void TIM2_Overflow (void);
#endif

And finally, write this function in your 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;
}

It remains to enable interrupts. This is done with an assembler command. rim - you will have to look for it in Programming Manual:

//enable interrupts
_asm("rim");

Another assembler instruction - sim - Turns off interrupts. They must be disabled while new values ​​are being written to the "video memory" so that an interrupt caused at an unfortunate moment does not spoil the array.

All code - on Github.

Read datasheets 2: SPI on STM32; PWM, timers and interrupts on STM8

If at least someone this article is useful, then I did not write it in vain. I will be glad to comments and remarks, I will try to answer all.

Source: habr.com

Add a comment