Programming microcontrollers stm32 in C language. ARM. STM32 quick start. STM32 firmware using ST-Link programmer under Windows

Getting started with any thing is best to start with the instructions. In some cases, everything is clear and so, in others - "hmm, nothing works, it looks like you still need to read the instructions." Microcontrollers are rather complex devices, and without reading the documentation you certainly won’t do anything useful with them, although ...

After some AVR ok, you can experience a slight shock from the number of different PDF ok for STM32 microcontrollers. Where to look first? How to use it? What else is happening?? At first glance, nothing is clear. Therefore, I decided to make a small overview of the world of documentation for these wonderful microcontrollers. I will put special emphasis on STM32F103C8T6, since I plan to write a few lessons on using this particular pebble.

The main documents for STMs are as follows:

  1. Datasheet
  2. Reference manual
  3. Programming Manual
  4. Errata Sheet

Datasheet

The Datasheet contains information about the presence of certain peripherals in a particular MK, pinout, electrical characteristics and chip marking for STM32F103x8 and STM32F103xB, that is, for these ones, which are circled in red:

Not sour, one datasheet for 8 microcontrollers.

Basics in the Datasheet

First of all, you need to pay attention to the section 7. Ordering information scheme, in which it is indicated, then denotes each character in the marking. For example, for STM32F103C8T6: LQFP-48 package, 64Kb flash, temperature range -40 to 85 °C.

The main difference between microcontrollers from different columns is the number of legs and the amount of flush, everything else is the same. A small exception is the first version column Tx: these microcontrollers have fewer SPI, I2C and USART modules. Peripherals are numbered from one: that is, if in STM32F103 Cx we have 2 SPIs, then they have the names SPI1 and SPI2, and in STM32F103 Tx we only have SPI1. Since we have the Datasheet for STM32F103x8 and STM32F103xB microcontrollers, this table is valid only for these models. For example STM32F103 C8 or STM32F103 CB correspond to this table, and STM32F103 C6 No, there is a separate datasheet for it.

In chapter 2.2 Full compatibility throughout the family it says that STM32F103xx devices are software, functional and pin-to-pin (for the same cases) compatible.

In the reference manual, there is a division into the following "types" of microcontrollers: STM32F103x4 and STM32F103x6 are designated as low-density devices , STM32F103x8 and STM32F103xB as medium-density devices , STM32F103xC, STM32F103xD and STM32F103xE as high-density devices . Low-density devices have fewer Flash and RAM memories, timers, and peripherals. High-density devices have more Flash and RAM memory, as well as additional peripherals such as SDIO, FSMC, I2S and DAC, while remaining fully compatible with other members of the STM32F103xx family. That is, if at some stage of development it became clear that the selected microcontroller is not enough to implement all the possibilities, then you can painlessly choose a more sophisticated stone without having to rewrite all existing software, while if the new stone is in the same package, then there is no need to re-lay the printed circuit board.

Reference manual

Let's go further. Reference manual (reference manual) contains a detailed description of all peripherals, registers, offsets, and so on. This is the main document that is used when creating firmware for the microcontroller. The reference manual has been compiled for a large group of microcontrollers, in our case for all STM32F10xxx, namely STM32F101xx, STM32F102xx, STM32F103xx and STM32F105xx / STM32F107xx. But STM32F100xx are not included in this RM, they have their own.

Key points in the Reference manual

As mentioned above, in the reference manual there is a division into the following "types" of microcontrollers: low-, medium-, high-density and connectivity
line. IN 2.3 Glossary clarified who is who:

  • low-density devices these are STM32F101xx, STM32F102xx and STM32F103xx microcontrollers which have flash memory size between 16 and 32 Kbytes.
  • Medium-density devices these are STM32F101xx, STM32F102xx and STM32F103xx, flash size between 64 and 128 Kbytes.
  • high-density devices these are STM32F101xx and STM32F103xx, flash size between 256 and 512 Kbytes.
  • XL-density devices these are STM32F101xx and STM32F103xx, flash size between 768 Kbytes and 1 Mbyte.
  • Connectivity line devices these are STM32F105xx and STM32F107xx microcontrollers.

Our STM32F103C8T6 is a Medium-density device. It will be useful to know this when studying peripherals, for example, there are separate sections on RCC for Low-, medium-, high- and XL-density devices, and Connectivity line devices.

Programming Manual

The Programming Manual is not an essential document at the very beginning of acquaintance with STMs, but it is very important when studying these microcontrollers in depth. It contains information about the processor core, instruction set, and kernel peripherals. Moreover, this is not the same peripheral that is described in the Reference manual. It includes:

  • System timer - system timer
  • Nested vectored interrupt controller - priority interrupt controller
  • System control block
  • Memory protection unit

As soon as we start getting acquainted with interrupts in STM32, we need a section 4.3 Nested vectored interrupt controller (NVIC). Well, the system timer is a very cool thing that will be useful in some RTOS or for creating software timers.

Errata Sheet

Errata Sheet is a collection of all known hardware glitches and jambs of microcontrollers and tips on how to get around them. Pretty funny document 🙂 Before using any peripherals, I advise you to take a look at the courts. This can help reduce the number of lost nerve cells when debugging your miracle firmware, which doesn’t want to work in any way 🙂

§> General questions. Variables declared by the user.

So, the C language is a typical representative of abstract programming languages, which means that it is not at all interested in what kind of information we will process, whether it be the contents of a computer file or internal microcontroller control registers.


The main programming object for classical C is the variable. It can be a single variable or a group of specially related variables, such as an array or structure. In fact, a variable is a kind of storage for a number that has its own unique name and a valid range of values, beyond which it is highly undesirable to go. And the first thing we must do before we start using the variable name in the program text is to familiarize the program with its properties. In the C language, this process is called variable declaration.

Why do you need to declare variables?

Although the C language is abstract, the microcontroller used by the developer is, as a rule, quite specific and has its own memory address space with specified properties, where the declared variable will be stored. The declaration, in addition to assigning a name to a variable, forces the compiler to place it at a specific address in the memory of the microcontroller (which one we are not interested in in most cases).

How should variables be declared?

The declaration rule can be formulated as follows: before we first use the name of a variable in the text of our program, we need to place its declaration in the following format:

typename; // Variable with name "name" and type "type".

Here: type is the so-called variable type identifier from a certain set of standard types;
name - an arbitrary variable name, as long as it does not start with a digit, consists only of Latin characters, and does not coincide with the service words of the C language (the list of which is not so long, in order to encounter such a situation, you really need to try very hard).

What is a type identifier and why mention it?

To store a variable, the microcontroller uses memory cells, the size of which is determined by its capacity. For example, microcontrollers of the AVR family are 8-bit, which means that they use memory cells with a size of one byte for storing data, which are capable of storing 256 different numerical values. If the expected values ​​of a variable can exceed this number, then two or more memory cells will be needed to store it. Since C, strictly speaking, does not represent what values ​​we plan to assign to a variable, it asks us to specify its type, which just determines the allowable range of values. This is necessary in order not to reserve an excessive or unacceptably small amount of memory for it, as well as to warn us when we try to assign too large a value to a variable that cannot store it. For 8-bit microcontrollers, the most commonly used integer data types are as follows:

Capable of storing only positive values ​​(unsigned):
unsigned char - occupies one byte of memory, values ​​0...255
unsigned int - two bytes, values ​​0...65535
unsigned long - four bytes, from 0 to (2^32)-1
capable of storing signed (signed) values:
signed char - occupies one byte of memory, from -128...127
signed int - two bytes, values ​​-32768...32767
signed long - requires four bytes, values ​​-(2^31) to (2^31)

The keyword "unsigned" can generally be omitted, because in C, by default, a type for which this attribute is not specified is considered unsigned.
To work with fractional numbers, C provides floating point types:

Float - 32 bits, values ​​from ±1.18E-38 to ±3.39E+38
double – 32 (±1.18E-38…±3.39E+38) or 64 bits (±2.23E-308…±1.79E+308) depending on compiler settings.

Note: The size of memory for storing variables of these types and the range of valid values ​​may vary slightly depending on the development environment or family of microcontrollers.

In order for a variable to already have a specific value before starting to use it, an initializer is often added to the declaration: an equals sign (in C it is an assignment operator) and the initial value of the variable.

For example:

IntA=100; // Variable named "A" of type int and initial value equal to 100.

Practical example: let's say we are going to write a program that blinks the LED 5 times. To count the number of blinks, you need a variable whose value will obviously never be negative and will not go beyond the range from 0 to 255, which means that in this case it will be quite enough to use a one-byte char type:

§> Variable scope.

The source of much confusion for beginners is a property of the language called the scope of a declared variable. The C language has the ability to limit the action of a variable to a specific area of ​​​​program code, while in other parts of the program it becomes inaccessible, which frees up memory that can be used by other variables in other parts of the program. Such variables are called local variables, and using them is the main way to get economical code.

At the initial stage of training, it is desirable to declare all variables as global ones. To do this, their declarations must be placed at the very beginning of the program before and outside of any functions. In this case, you can be sure that they will be available for work anywhere in the program within the current file.

§> Variable allocation area.

As you know, microcontrollers of the AVR family contain three memory areas implemented using different technologies. Each of them has its own purpose and address space, numbered from zero to the maximum value for a particular model:


RAM, non-volatile EEPROM memory can be used to store user variables, and FLASH memory of the microcontroller can also be used to store constants, the value of which cannot be changed during program operation.

To begin with, it is useful to know that variables declared by the user without the use of special keywords like _eeprom or _flash are located in the RAM of the microcontroller, in the form of one or more cells of static SRAM. In the course of operation, they are periodically copied to the fast register memory RON, which directly interacts with the arithmetic-logical block ALU of the microcontroller.
The issues of placing variables inside RAM, as a rule, are of interest only in the context of program performance.

§> Special purpose registers of the SFR microcontroller.

So, we briefly reviewed the declaration of variables intended for organizing the computational process, which have little to do with the specifics of the MC hardware.

Management and control of the operation of the microcontroller and its individual internal modules is carried out by writing and reading special cells-registers in the service area of ​​\u200b\u200bRAM memory - special-purpose registers (Special Function Register, hereinafter simply SFR).

The basic idea behind using C to program microcontrollers is this: special value registers are C variables just like those declared by the user. These variables can be assigned values, controlling the operation of the microcontroller, or read them, thus obtaining information about its current state. It is not necessary to declare microcontroller registers like user variables for several reasons. First, their size is known in advance: in AVR C, these are unsigned 8-bit variables. Secondly, SFRs have strictly defined names and addresses in memory, being the so-called I / O registers.

Nevertheless, it is necessary to familiarize the program with special purpose registers, and this is done by connecting the so-called header files.

At the beginning of any C program, we can see lines like:

#include "file1.h" // Include the contents of the file "file1.h" in the code.

#include is a directive (indication) that causes the development environment to place the contents of a file named file1.h at a given location in the program. Files with the .h extension are called header or h files. The developer can create his own h-files and place them, given the content, anywhere in the program. However, in order to familiarize the program with SFR for a given type of microcontroller, it is necessary to include quite specific header files. Their names and number depend on the specific development environment and the type of microcontroller used, so, for example, in IAR for Atmega64, it is enough to write the lines:

#include "iom64"
#include "inavr.h"

After including the necessary h-files in the text, the program will recognize the SFR names mentioned in it, for example, the status register of the AVR microcontroller with the name SREG, the receive / transmit buffer of the UART module - UDR, and so on.

A blank program for IAR, which does nothing, but no longer "swears" at the names of special-purpose registers of the Atmega16 microcontroller, should look like this:

#include "iom16.h"
#include "inavr.h"
unsigned char ChisloMiganiy=0;
void main(void)
{
// Here we will place a program that uses the variable ChisloMiganiy
// and any Atmega16 registers whose names are specified in the iom16.h file.
}

I would like to hope that the reader is familiar with the rules for formatting comments in the text of the program. These are notes that are ignored by the C language and are not considered part of the program code if they are written on one or more lines enclosed between the characters /* and */, or on a single line beginning with the sequence //.

§> Overview of standard operations with registers.

It's time to move on to more serious operations on registers and program variables. Managing the operation of the microcontroller in most cases comes down to the following simple set of actions with its registers:

1. Writing to the register of the required value.
2. Reading a register value.
3. Setting the required bits of the register to one.
4. Resetting the bits of the register to zero.
5. Checking the bit for a logical one or a logical zero.
6. Changing the logical state of the register bit to the opposite.

All of these actions involve the C assignment operator, written as an equal sign. The principle of operation of the operator is primitively simple - it writes to the register or variable located to the left of it, the value of what is written to the right. On the right can be a constant, another case, a variable, or an expression consisting of them, for example:

A = 16; // Set variable A to 16;
A=B; // Read the value of variable B and assign this value to variable A;
A=B+10; // Read the value of variable B, add 10 to the read value, assign the result to variable A (the value of variable B does not change).

§> Writing and reading registers.

It can be seen from the considered examples that the assignment operator by itself solves the first two tasks - writing and reading register values. For example, to send a byte by the AVR microcontroller via the UART bus, it is enough to write it to the transmitting register with the name UDR:

UDR=8; // Send number 8 via UART;

To get the byte received via UART, it is enough to read it from the UDR register:

§> Set register bits.

The C language does not include commands for direct reset or setting of bits of a variable, however, there are bitwise logical operations "AND" and "OR" that are successfully used for these purposes.
The bitwise logical operator "OR" is written as a vertical bar - "|" and can be executed between two variables, as well as between a variable and a constant. Let me remind you that the "OR" operation on two bits results in a one bit if at least one of the original bits is in the one state. Thus, for any bit, a logical "OR" with a "1" will result in a "1", regardless of the state of that bit, and an "OR" with a logical "0" will leave the original bit's state unchanged. This property allows you to use the "OR" operation to set the Nth bit in the register. To do this, you need to calculate a constant with a single Nth bit using the 2^N formula, which is called a bit mask, and perform a logical "OR" between it and the register, for example, to set bit No. 7 in the SREG register:

(SREG | 128) - this expression reads the SREG register and sets the seventh bit in the read value, then put the sufficiently changed value back into the SREG register:

SREG = SREG | 128; // Set bit #7 of register SREG.

Such work with the register is usually called "reading - modifying - writing", in contrast to simple assignment, it saves the state of the remaining bits without changing.
The given program code, by setting the seventh bit in the SREG register, performs quite meaningful work - it allows the microcontroller to process software interrupts. The only drawback of such a notation is that it is not easy to guess the set seventh bit in constant 128, so more often the mask for the Nth bit is written in the following form:

(1<

SREG = SREG | (1<<7);

Or even simpler using the short form of the C language:

SREG |= (1<<7);

Which means - take the content to the right of the equals sign, perform the operation between it and the register on the left that precedes the equals sign and write the result to the register or variable on the left.

§> Reset bits in registers.

Another logical operation of the C language is the bitwise "AND", written as the symbol "&". As you know, the operation of logical "AND", in relation to two bits, gives a unit if and only if both source bits have a single value, this allows you to use it to reset bits in registers. In this case, a bit mask is used, in which all bits are ones, except for zero at the position of the one being reset. It can be easily obtained from the mask with the Nth bit set by applying the bitwise inversion operation to it:
~(1<

SREG = SREG&(~(1<<7)); или кратко: SREG &= ~ (1<<7);

In the header file mentioned earlier for a specific microcontroller, the standard names of special-purpose register bits are given, for example:

#define OCIE0 1

Here #define is an instruction to the compiler to replace the combination of characters "OCIE0" in the program text with the number 1, that is, the standard name of the OCIE0 bit, which is part of the TIMSK register of the Atmega64 microcontroller, with its serial number in this register. Due to this, setting the OCIE0 bit in the TIMSK register can be more clearly written as follows:

TIMSK|=(1<

You can set or reset several bits of the register at the same time by combining bit masks in expressions with the logical "OR" operator:

PORT |= (1<<1)|(1<<4); // Установить выводы 1 и 4 порта A в единицу;
PORTA&=~((1<<2)|(1<<3)); // Выводы 2 и 3 порта A сбросить в ноль.

An example of usage with registers defined in CMSIS:

DAC0->CTRL |= DAC_CTRL_DIFF; // installation
DAC0->CTRL &= ~DAC_CTRL_DIFF; // reset

§> Check register bits for zero and one.

Special-purpose registers of microcontrollers contain a lot of attribute bits, the so-called "flags" that notify the program about the current state of the microcontroller and its individual modules. Checking the logic level of the flag comes down to selecting an expression that becomes true or false, depending on whether it is set or this digit in the register is reset. This expression can be a logical "AND" between the register and the mask with the N bit set at the position of the bit being checked:

(REGISTER & (1<

The above expression can be used in a conditional if statement (expression) or a while loop statement (expression), which belong to the logical group, that is, they accept true and false values ​​as arguments. Since the C language, when converting numeric values ​​to booleans, treats any numbers that are not equal to zero as a logical truth, the value (REGISTR & (1<If it becomes necessary to get the logical value “false” for our expression with the N bit set, it is enough to supplement it with a logical inversion operator in the form of an exclamation mark -! (REGISTR & (1<

While (!(UCSRA & (1<

Here, with the UDRE bit cleared, the expression (UCSRA & (1<!(UCSRA & (1<

§> Changing the state of a register bit to the opposite.

This, so to speak, problem is successfully solved by the logical operation of the bitwise "EXCLUSIVE OR" and the corresponding C operator, written as the symbol "^". The XOR rule with two bits is true if and only if one of the bits is set and the other is clear. It is not hard to see that this operator, applied between the bitmask and the register, will copy the bits opposite the zero bits into the result. masks are unchanged and inverts those located opposite the ones that are 1. For example, if: reg=b0001 0110 and mask=b0000 1111, then reg^mask=b0001 1001. In this way, you can change the state of the LED connected to the fifth bit of port A:

#define LED 5 // Replace the combination of LED characters in the program with the number 5 (LED output).

PORTA ^=(1<< LED); // Погасить светодиод, если он светится и наоборот.

§> Arithmetic and logic of C language.

We examined a typical set of operations used when working with microcontroller registers. In addition to them, the language has a number of simple arithmetic and logical operations in the arsenal, the descriptions of which can be found in any C reference book, for example:


For a more detailed acquaintance with operations on variables and the C language in general, I recommend the book "The C Programming Language" by B. Kernigan, D. Ritchie.

Variable type conversion is part of the internal automatic work of the compiler, occurring in strict accordance with the rules of the programming language. The developer himself, when writing a program in an explicit form, usually does not do this. However, inaccurate declaration of variable types, or assigning a value to a variable that exceeds the allowable range, and even an incorrect format for writing a constant, can lead to data loss and incorrect program operation, with the compiler completely silent.
When does type casting occur and what does it consist of? There are many such situations. Consider the most dangerous of them.

§> Converting the type of an expression before assigning it to a variable.

In the first section, we turned our attention to the need to explicitly specify the type of the declared variable. This allows the compiler to reserve just the right amount of address space for it and determine the range of values ​​it can store. However, we are not insured that during the execution of the program there will be an attempt to write to the variable a value that exceeds the maximum allowable value. In the worst cases, the compiler will give us a message about a possible error. For example, if you want to write the number 400 into a variable of type unsigned char (range from 0 to 255):

Unsigned char a=400; // gives a message like "integer conversion resulted in truncation”

The compiler warns us that an attempt has been made to write a numeric value that requires two bytes to store (400 is 1 in the high byte and 144 in the low byte) into a one-byte variable. However, in cases where the expression to be assigned contains variables, and the compiler might notice a possible loss of data, it relieves itself of this responsibility, for example:

Unsigned char x=200, y=200;
x=x+y;

With this option, despite the fact that the value of the expression (x + y) is also equal to 400, no warnings from the compiler will follow. And only the low byte of the number 400, that is, 144, will be written to the variable x. And here it is difficult to blame the compiler for something, because instead of an explicitly initialized variable in the expression, for example, the receiving register of the UART bus can be used, which can contain any value, received from an external device.
Another example in the same vein is assigning a fractional value to an integer type variable:

Float a=1.5; // Declared floating point variable.

b=a*b; // Expect b to be set to 4.5.

As a result, only the integer part of the result a*b, the number 4, will be stored in the variable b.

§> Convert the result of an expression to the type of the most precise variable in the expression.

In such a conversion, the compiler is guided by the following rule: before the evaluation of the expression begins, operators with a “lower” type are promoted to “higher” ones, while the result is also cast to the “higher” type. What type should be considered "highest"? One that can store any valid value of another type without loss of precision. So, in the previous example:

Float a=1.5; // The variable a is declared as a floating point.
charb=3; // Integer variable declared.

In the expression (a*b), the variable float a has a higher type because it can store any integer value in the range 0...255 of type char. The result of the expression (a*b) will be of type float.
A typical example of a surprise for this case is an attempt to get a fractional number by dividing two integers:

Chara=3; // Integer variable declared.
charb=4; // Integer variable declared.
float c; // Declared floating point variable "c" to store the result.
c=a/b; // "c" is expected to be 0.75 (¾).

Unlike the previous example, the result is written to a variable capable of storing floating-point numbers, however, the compiler, in accordance with the casting rule, having received the number 0.75 as a result of division, converts it to the type of integer operands, discarding the fractional part. As a result, zero will be written to the variable "c".
A more realistic example from life is the calculation of the measured voltage from the ADC output code:

IntADC; // Two-byte integer variable to store the ADC code.
floatU; // Floating point variable to store the voltage value.
U=ADC*(5/1024); // Voltage calculation.

What is overlooked here is that a constant in C, like any variable, also has its own type. It is desirable to indicate it explicitly or using the appropriate notation. The constants 5 and 1024 are written without a decimal point and will be treated by the C language as integers. As a consequence, the result of the expression (5/1024) will also be reduced to an integer - 0 instead of the expected 0.00489. This would not happen if the expression was written in the format (5.0/1024).
The above errors can also be avoided by using the explicit type conversion operator of C expressions, which is written as a type name enclosed in parentheses and affects the expression after it. This operator casts the result of an expression to an explicit type, regardless of the types of its operands:

C= (float) a/b; // "c" is expected to be 0.75 (¾);
U= ADC * ((float)5/1024); // Voltage calculation.

§> Purpose of functions.

Even the ancient programmers turned their attention to one interesting fact - often the program is forced to perform exactly the same sequence of actions several times. It was then that the idea was born with a sufficiently large set of such actions and their repetitions, in order to save program memory, arrange them as a separate group, and then, if necessary, simply send the program for its execution. Such a separate piece of code in C is called a function. The very name of the term "function" originally reflects another property of some functions - the ability (like mathematical functions) to transform certain input data according to a given algorithm. But this is a little later.

Another purpose of the function, which fully reflects its name, is the allocation to a separate group of actions related to one common goal, for example, the port initialization function or the keyboard polling function. This is one of the additional purposes of the function.
Such functions can be called by the program only once. Why then are they needed? To justify this approach, the literature often cites the phrase of an unknown, but apparently very authoritative ancient Roman programmer: "Divide and conquer!". Indeed, a program designed as target functional blocks is much easier to understand, debug and then modify than a set of separate pieces of code separated by purpose.
Summarizing the above, we can formulate the formal prerequisites for creating functions in the program, these are:

1. The presence of the same, sufficiently large and repeatedly repeated sets of actions.
2. The desire to structure the program in the form of separate blocks with a common functional purpose.

Here we must immediately make an important caveat. The fact is that with each transition to a function and return from it, the microcontroller is forced to save some system data, for example, the address of the program from which the transition to the function occurred, and this requires additional temporary resources. It is necessary to take this fact into account and try not to produce many short functions in the program, if it is possible to combine them into one common one.

§> Structure and design of functions.

In any function, it is structurally easy to distinguish two components: the header and the body of the function.
The header is the very first line of any view function:

Output variable type Function name (Types of input variables and their names separated by commas)

Let's temporarily skip the consideration of the contents of the header before and after the name and consider functions that do not process any data. They are designed only to perform certain actions. In the headers of such functions, you need to specify the names of the empty type - void (English vacuum, emptiness):

Void function name (void)

As a name, you can use any word that reflects the meaning of the actions performed by the function, as long as it does not begin with a number. Already now we can call the execution of our function from anywhere in the program. To do this, you need to write the name of the function, parentheses and a semicolon symbol. For example, a function with the title:
void initialization (void)
can be called like this:

initialization();

The body of a function is a set of commands located between the first opening curly brace after the header and its corresponding closing curly brace. Explanation: Sets of actions inside curly braces are called blocks in C. They logically link several single actions into one complex one, which is either completely executed or completely ignored depending on the context of the program. In this case, the function body is a block of commands that the function must execute from beginning to end. Thus, the program, having encountered a transition to a function, will execute the contents of the block and, following the last closing bracket, will return to where it was called from.
For example, a function:

Void initialization (void)
{
DDRA=0xFF; // PORTA to exit.
DDRB|=(1<<0)| (1<<3)| (1<<4); // PB0, PB3, PB4 на выход.
DDRC=0xF0; // High tetrad PORTC to the output.
}

Having initialized the direction of the pins of ports A, B and C, it will return to the next line after its call.
To begin with, it is also important to know that in the text of the program, each function must be located separately, that is, one function cannot be inside another or partially overlap it.

§> Handling of parameters by a function.

All functions can process and change the values ​​of specialized registers of the microcontroller and the so-called global variables, that is, those that are declared by the user at the very beginning of the program outside of any functions. In addition, it is possible to pass data to a function for processing directly at the time of its call. It's just convenient and nothing more.
This data is called the parameters passed to the function. They must be listed, separated by commas, along with their types, in the function header, inside parentheses after the function name:

This design of the header will mean that the function is able to take as parameters two numbers of the char type with the names FrameLength and StopBit. Now, when calling a function, the compiler will not allow parentheses to be left empty and will require specific values ​​to be passed, separated by commas, for example:

InitUart(8, 2);

After that, inside the function, variables named FrameLength and StopBit will be assigned specific values ​​8 and 2, which can be used, for example, to set the length of the UART module send and the number of its stop bits:

Void initUart(char FrameLength, char StopBit)
{
if (FrameLength==8) UCSR0C|=((1<<1)|(1<<2));
if (StopBit==2) UCSR0C|=(1<<3);
}

§> Specialized functions.

We have considered the functions set by the user himself. In addition to them, in any program there are functions that perform specialized tasks and must be designed according to special rules.
The most important function of this kind, as you can see from its very name, is the main function. It is characterized by the fact that the execution is transferred to it by the microcontroller itself when power is applied or after a reboot, that is, it is from it that the work of any program begins. Another property of the main function is that when it is executed to the end, the program will automatically go to its own beginning, that is, it is executed in a loop, unless an infinite loop was specially organized inside it by the user himself.
Another variant of system functions is interrupt handlers. They also cannot be called programmatically. The microcontroller independently transfers control to them in case of occurrence of special hardware states - conditions for calling interrupts.

Obviously, any program is a set of actions described in accordance with the rules of the programming language and intended for execution by a specific device, in this case, a microcontroller. What is the order of performing these actions in practice, we will try to understand in this section.

§> General structure of the simplest program. Initialization, background.

When considering a program at the level of the C language, we can say that it starts its work from the first line of the main function (line 001 in the figure):

Structure of a C program
Next, lines 002, 003, 004 are sequentially executed, united by one common property: the program passes through them only once, when the microcontroller is started. This part of the program is called the initialization part. The initialization part is a legitimate place to place actions to prepare the microcontroller peripherals to work with the specified parameters - setting ports for input or output, initial initialization of timers, setting the speed and UART frame format, and so on for everything that is planned to be used in the future.

Since any program is designed to run continuously, its normal mode of operation is non-stop repetition of the contents of an infinite loop around the circle. In practice, such a loop is most often implemented using the while(1) ( ) construct, designed to repeatedly perform the actions placed inside its curly braces. The content of an infinite program loop is called the background. It is here that the main part of the work on checking the state of the hardware and the corresponding impact on it to obtain the desired result takes place.

Let's consider the described program structure on the simplest example. Let it be necessary: ​​to send the symbol * over the UART bus while the button on the PA0 pin is pressed (zero signal level). The program in this case (without unnecessary procedures to suppress button bounce and other things) may look like this:

Void main (void)
{
PORTA|=(1<<0); // Притянуть вход кнопки PORTA.0 внутренним pull-up резистором.

UCSRB = (1<while(1)
{
if (! (PINA & (1<<0))) // Если кнопка нажата...
{
while(! (UCSRA & (1<UDR="*"; // Send *.
}
// other background commands:
00N
00N+1
...
}
}

Here, the if (...) construct, located in the background of the program, conducts endless polls of the PINA input register and checks the PA0 output for a low level. Next, other actions of the background process are performed, indicated by the lines 00N, 00N+1, and so on.

What factors, in relation to this program, determine the most important parameters of its work - reliability and speed?

The example shows that the polling frequency of the PA.0 input is determined by the duration of the execution of the background commands, because before polling the button again, the microcontroller must execute the following lines 00N, 00N + 1, etc. Obviously, the reliability of fixing an external event (pressing the button) in this case will depend on the ratio of the duration of the impact of this event to the period of its detection. The duration of the background in this program will certainly be many times less than the duration of holding the button, which in practice is several tens of milliseconds. However, with the growth of the background part of the program and the short time of external influence, the reliability of its tracking at a certain moment will sharply decrease. To prevent this from happening, and also to reduce the reaction time of the program to an external event, the interrupt system is used.

§> Interrupts.

How does the interrupt mechanism work? Very simple, especially at the C level!

The architecture of AVR microcontrollers, however, like any other, at the hardware level, has the ability to track certain “interesting states of iron” and set the corresponding sign bits. Such states are called interrupt conditions, and the signs that are set are interrupt flags. the microcontroller continuously monitors the state of these flags.If any set interrupt flag is detected, provided that it is enabled by turning on the corresponding bit, and the global interrupt enable bit (#7 in the SREG register for the AVR) is set, the execution of the main part of the program will be temporarily suspended (interrupted ).

Since an interrupt can occur while executing any arbitrary background instruction, its address is stored in the so-called software stack. After that, the execution is given to a part of the program, specially written by the developer to respond to the event that caused this interrupt. This small part of the program is called the interrupt handler. When the handler is executed to the end, the program, using the address stored in the program stack, will return to the place from which it was called to handle this interrupt.

What is the programmer's role in this process? When developing in C, it is minimized.
Some of the actions, such as monitoring interrupt flags, are implemented at the hardware level. The other part, for example, protection against changes in the handler of the SREG status register important for the program, saving the program address on the stack, and much more, the compiler takes over.
The only thing left to do is:

1. Allow the use of interrupts in the program.
2. Allow the call of the interrupt of interest to us with a special bit in the corresponding register. How exactly and where will tell the description on the microcontroller.
3. Create conditions for the occurrence of an interrupt, for example, if this is a timer overflow, then it is trite to start it. If this is an interrupt for changing the state of an external pin, then set the necessary conditions for this (front, cut or zero level).
4. Place an interrupt handler in the program, arranging it in accordance with the requirements of the compiler.

In relation to our example, it is possible to organize sending to the UART at a low level at the button input using the so-called external interrupt INT0. This interrupt is triggered by a rising, falling or zero level on the INT0 pin.

Let's move the button to the output PD.2 with an alternative function INT0. In the initialization part of the program, let's enable interrupts globally and INT0 specifically. By default, the microcontroller is configured to generate an INT interrupt on a low input signal level, so no additional settings are required. It remains to declare outside the main function the INT0 handler that sends the * character to the UART:

Void main (void)
{
PORTD|=(1<<2); // Притянуть вход кнопки PORTD.2 внутренним pull-up.
UBRRL=51; // UART speed - 9600 bps.
UCSRB = (1<SREG|=(1<<7); // Разрешить прерывания.
GICR|=(1<while (1)()
}

#pragma vector=INT0_vect // INT0 interrupt handler/
__interrupt void INT0_INTPT()
{
if (! (PIND & (1<<2))) {while(! (UCSRA & (1< }

Here the interrupt handler is declared in IAR compiler format. Basically, it contains only the name of the interrupt vector - INT0_vect, the compiler replaces it with the program memory address, to which the program execution is transferred when this interrupt occurs. The name of the INT0_INTPT handler itself is chosen arbitrarily. The names of the vectors of all possible interrupts for this MK are described in h-files.

Now the response time to a button press does not depend on the duration of the program background and is several microcontroller cycles, and the probability of missing this event is zero. Thus, an interrupt is a great way to react to an event that needs to be handled immediately. This is its main purpose.

I would like to immediately mention one unspoken rule regarding interrupt handlers, although this is a rather narrow question. They should only place what is actually needed to respond quickly to an interrupt. All other actions that can be postponed must be placed in the background.
What is it connected with?
If the events that cause the interrupt occur frequently enough, then the too long handler may not have time to complete before the next interrupt occurs. And this is fraught with unpleasant consequences in the form of data loss and violation of the normal sequence of actions. For example, if you need to receive a certain array of bytes via UART, then in the handler that is called after receiving each of them, you should not closely study the received data, but only rewrite them from a pre-prepared array. And after receiving the last of them, you can set the corresponding sign in the handler (they say, everything is accepted) and in the background, having found it, calmly study the entire received array.

Taken from the site
http://eugenemcu.ru/

STM32 clock system.

Today we will talk about the clocking system of STM 32 microcontrollers. If you still don’t know what a clock is, a frequency and didn’t touch the clocking system at all before,. Although this link discusses the AVR microcontroller clocking system, the concepts defined in the lesson by reference are also applicable to the STM 32 microcontroller clocking system.

So let's get started!

We will consider the clocking system using the example of the STM 32F 303VCT 6 microcontroller, which is installed in the STM 32 F 3 DISCOVERY debug board.

Let's take a look at the general structure of the clocking system:

As we can see, the STM 32 clocking system is an order of magnitude more complicated than the microcontroller clocking system. AVR, despite the fact that The figure shows only the main part of it.

Let's figure it out!

The diagram should be viewed from left to right. First, we must select the controller's main clock source. We will choose between HSI and HSE.

HSE-External high frequency generator. The clock source for it is an external clock signal ( Input frequency), which, as we see from the diagram, can be from 4 to 32 MHz. It can be a quartz resonator, a clock generator, and so on.

HSI - Internal high frequency generator. In microcontrollers STM 32 F 3 is an RC circuit with a frequency of 8 MHz. The accuracy is much lower than the external HSE generator.

Each of these clock sources can be connected to PLL. However, before being fed to the PLL, the signal from the HSI will be reduced by a factor of 2. The HSE signal, in turn, can be applied to the PLL without changes, or be reduced by a certain number of times, at the request of the user.

PLL Clock - Phase Locked Loop System (PLL). Allows you to multiply the input HSI or HSE signal by the required number of times.

With PLL, the signal can be fed to the system bus, the maximum frequency of which is 72MHz. Or, the HSE or HSI signal can be applied directly to the system bus, i.e. without PLL conversion.

The system clock, SYSCLK, clocks all the main buses of the microcontroller, through the appropriate dividers, as we see in the diagram above. Note that the maximum clock speed of some buses is below SYSCLK . Therefore, before applying the clock signal SYSCLK to the bus, you should divide it with an appropriate divider. If this is not done, the microcontroller will freeze.

To adjust the clocking, you can resort to manual editing of registers, or use library functions. We will use the library.

Set up our debug board STM 32 F 3 DISCOVERY to work with a clock frequency of 72 MHz.

Let's create and set up a project in Keil uVision. .

Let's add the following code:

#include "stm32f30x_gpio.h" #include "stm32f30x_rcc.h" void InitRCC() ( RCC_HSEConfig(RCC_HSE_ON); //Enable HSE while(RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET) ; //Waiting for HSE //Set Flash latency FLASH ->ACR |= FLASH_ACR_PRFTBE; FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY); FLASH->ACR |= (uint32_t)((uint8_t)0x02); RCC_PREDIV1Config(RCC_PREDIV1_Div1);//PREDIV 1 Divider = 1 RCC_PLLConfig(RCC_PLLSource_PREDIV1,RCC_PLLMul_9);//Set PREDIV1 as source for PLL,And set PLLMUL=9 RCC_PLLCmd(ENABLE);//Enable PLL while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) ;//Waiting for PLL RCC_SYSCLKConfig( RCC_SYSCLKSource_PLLCLK);//Set PLL as SYSCLK Soucre RCC_HSICmd(DISABLE);//Disable HSI ) int main(void) ( RCC_ClocksTypeDef RCC_Clocks; InitRCC(); RCC_GetClocksFreq (&RCC_Clocks); __NOP (); while (1) ( ) )

#include "stm32f30x_gpio.h"

#include "stm32f30x_rcc.h"

void InitRCC()

RCC_HSEConfig(RCC_HSE_ON) ; //Enable HSE

while (RCC_GetFlagStatus (RCC_FLAG_HSERDY ) == RESET ) ; //Waiting for HSE

//Set flash latency

FLASH -> ACR |= FLASH_ACR_PRFTBE ;

FLASH -> ACR &= (uint32_t ) ((uint32_t ) ~ FLASH_ACR_LATENCY ) ;

FLASH -> ACR |= (uint32_t ) ((uint8_t ) 0x02 ) ;

RCC_PREDIV1Config (RCC_PREDIV1_Div1 ) ; //PREDIV 1 Divider = 1

RCC_PLLConfig(RCC_PLLSource_PREDIV1 , RCC_PLLMul_9 ) ; //Set PREDIV1 as source for PLL,And set PLLMUL=9

RCC_PLLCmd(ENABLE) ; //Enable PLL

while (RCC_GetFlagStatus (RCC_FLAG_PLLRDY ) == RESET ) ; //Waiting for PLL

RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK) ; //Set PLL as SYSCLK Soucre

RCC_HSICmd(DISABLE) ; //Disable HSI

int main(void )

RCC_ClocksTypeDef RCC_Clocks ;

initRCC();

RCC_GetClocksFreq(&RCC_Clocks) ;

NOP () ;

while (1)

In the main function, the structure is declared RCC _ ClocksTypeDef. This structure contains fields that reflect the current clock frequency of certain parts of the controller.

Then the InitRCC function is called in the main function, which sets the controller clock. Let's consider it in more detail.

team RCC _ HSEConfig (RCC _ HSE _ ON ), we enable HSE . It takes time to enable it, so you need to wait until the flag is set RCC _ FLAG _ HSERDY . We do it in a loop while (RCC _ GetFlagStatus (RCC _ FLAG _ HSERDY ) == RESET ) .

Then we adjust the flash delay. This must be done when operating the system bus at frequencies above 36 MHz!

After setting the delay, select the prescaler PLL. team RCC _ PREDIV 1 config (RCC _ PREDIV 1_ Div 1) we set the prescaler to 1. With the command RCC _ PLLConfig (RCC _ PLLSource _ PREDIV 1, RCC _ PLLMul _9 ) select HSE as the frequency source for the PLL and select a multiplication of 9. It remains only to turn on the PLL with the command RCC _ PLLCmd (ENABLE ), and wait for the flag to be set RCC _ FLAG _ PLLRDY,in the while loop. Thus, we provide the necessary time delay for turning on the PLL. After that, we select PLL as the source of the system frequency SYSCLK with the command RCC _ SYSCLKConfig (RCC _ SYSCLKSource _ PLLCLK ). We will not touch the bus prescalers, so the AHB, APB 1, APB 2 buses will operate at frequencies of 72.36 and 72 MHz, respectively.

It remains only to turn off the internal RC chain with the command RCC _ HSICmd (DISABLE ).

After executing the InitRCC function, in the main firmware cycle, fill in the structure RCC _ ClocksTypeDef, which will allow us to know if we have configured the clock system correctly. We do it as a team RCC_GetClocksFreq(&RCC_Clocks).

You can view the controller clock values ​​in debug mode by setting a breakpoint on the command __ NOP () which means empty command. This command is often added for ease of debugging.

Connecting the debug board STM32 F3 DISCOVERY, assemble the firmware, flash the board, and finally enter the debug mode by pressing the Start / Stop debug session button (Ctrl + F 5). By setting a breakpoint on a function __ NOP, and adding the RCC _Clocks structure to Watch , we start the firmware execution by pressing F 5. As a result, we see:

The frequencies are set correctly, and the microcontroller is now running at 72 MHz.

So, as you learned from today's tutorial, the STM 32 clock system is powerful and flexible enough to meet the needs of your projects. Taking the time to set it up - you will achieve excellent results!

Thank you for your attention! Your questions as usual in the comments!

Any copying, reproduction, quoting of the material, or parts of it, is permitted only with the written consent of the MKPROG .RU administration. Illegal copying, quoting, reproduction is punishable by law!

Published on 09.08.2016

Microcontrollers STM32 are becoming increasingly popular due to their power, fairly heterogeneous peripherals, and their flexibility. We will begin to study using a budget test board, the cost of which does not exceed $ 2 (from the Chinese). We also need ST Link programmer, the cost of which is about $ 2.5 (from the Chinese). Such amounts of expenses are available to both students and schoolchildren, so I suggest starting with this budget option.


This microcontroller is not the most powerful among STM32 but not the weakest either. There are various boards STM32, including discovery which cost around $20. On such boards, almost everything is the same as on our board, plus a programmer. In our case, we will use the programmer separately.

Microcontroller STM32F103C8. Characteristics

  • ARM Core 32-bit Cortex-M3
  • Maximum frequency 72MHz
  • 64Kb Flash memory for programs
  • 20Kb SRAM
  • Power 2.0 ... 3.3V
  • 2 x 12-bit ADC (0…3.6V)
  • DMA controller
  • 37 I/O 5V tolerant
  • 4 16-bit timers
  • 2 watchdog timers
  • I2C - 2 buses
  • USART - 3 buses
  • SPI - 2 buses
  • USB 2.0 full-speed interface
  • RTC - built-in clock

On the STM32F103C8 board are available

  • Bring out ports A0-A12, B0-B1, B3-B15, C13-C15
  • Micro USB through which you can feed the board. The board has a 3.3V voltage regulator. 3.3V or 5V power can be supplied to the corresponding pins on the board.
  • Button reset
  • Two jumpers BOOT0 And BOOT1. We will use during the firmware through UART.
  • Two quartz 8 MHz and 32768 Hz. The microcontroller has a frequency multiplier, so on 8 MHz quartz we can reach the maximum controller frequency of 72 MHz.
  • Two LEDs. PWR- indicates power supply. PC13- connected to the output C13.
  • Connector for programmer ST Link.

So, let's start by trying to flash the microcontroller. This can be done via USART, or using a programmer ST Link.

You can download a test file for firmware. The program flashes the LED on the board.

STM32 firmware using USB-Uart adapter under Windows

In system memory STM32 There is Bootloader. Bootloader is written at the production stage and any microcontroller STM32 can be programmed via interface USART using a USART-USB adapter. Such adapters are most often made on the basis of popular microcircuits. FT232RL. First of all, connect the adapter to the computer and install the drivers (if required). Drivers can be downloaded from the manufacturer's website FT232RL– ftdichip.com. Need to download drivers VCP(virtual com port). After installing the drivers, a virtual serial port should appear on the computer.


We connect RX And TX outputs to corresponding conclusions USART1 microcontroller. RX connect the adapter to TX microcontroller (A9). TX connect the adapter to RX microcontroller (A10). Since USART-USB has 3.3V power outputs, let's supply power to the board from it.

To put the microcontroller into programming mode, you need to set the pins BOOT0 And BOOT1 to the desired state and reload it with the button reset or turn off and turn on the power of the microcontroller. For this we have jumpers. Different combinations drive the microcontroller into different modes. We are only interested in one mode. To do this, the microcontroller at the output BOOT0 should be a logical unit, and on the output BOOT1- logical zero. On the board, this is the following position of the jumpers:

After pressing the button reset or disconnecting and connecting the power, the microcontroller must enter the programming mode.

Firmware software

If we use a USB-UART adapter, the port name will be something like this /dev/ttyUSB0

Get information about the chip

Result:

Reading from the chip to the dump.bin file

sudo stm32flash -r dump.bin /dev/ttyUSB0

We write to the chip

sudo stm32flash -w dump.bin -v -g 0x0 /dev/ttyUSB0

Result:

Stm32flash 0.4 http://stm32flash.googlecode.com/ Using Parser: Raw BINARY Interface serial_posix: 57600 8E1 Version: 0x22 Option 1: 0x00 Option 2: 0x00 Device ID: 0x0410 (Medium-density) - RAM: 20KiB (512b reserved by bootloader) - Flash: 128KiB (sector size: 4x1024) - Option RAM: 16b - System RAM: 2KiB Write to memory Erasing memory Wrote and verified address 0x08012900 (100.00%) Done. Starting execution at address 0x08000000... done.

STM32 firmware using ST-Link programmer under Windows

When using the programmer ST Link conclusions BOOT0 And BOOT1 are not used and must be in the standard position for normal operation of the controller.

(Book in Russian)

STM32 marking

device familyproduct typedevice subfamilyPin countFlash memory sizepackagetemperature range
STM32 =
ARM-based 32-bit microcontroller
F = General purpose
L = Ultra low power
TS=TouchScreen
W = wireless system-on-chip
60 = multitouch resistive
103 = performance line
F=20pins
G = 28 pins
K = 32 pins
T=36 pins
H=40pins
C=48/49 pins
R = 64 pins
O = 90 pins
V = 100 pins
Z = 144 pins
I = 176 pins
B = 208 pins
N = 216 pins
4 = 16Kbytes of Flash memory
6 = 32Kbytes of Flash memory
8 = 64Kbytes of Flash memory
B = 128 Kbytes of Flash memory
Z = 192 Kbytes of Flash memory
C = 256 Kbytes of Flash memory
D = 384 Kbytes of Flash memory
E = 512 Kbytes of Flash memory
F = 768 Kbytes of Flash memory
G = 1024 Kbytes of Flash memory
I = 2048 Kbytes of Flash memory
H=UFBGA
N=TFBGA
P = TSSOP
T=LQFP
U = V/UFQFPN
Y=WLCSP
6 = Industrial temperature range, -40…+85 °C.
7 = Industrial temperature range, -40…+ 105 °C.
STM32F103 C8 T6

How to remove write / read protection?

If you received a board with STM32F103, but the programmer does not see it, this means that the Chinese have protected the Flash memory of the microcontroller. The question "why?" let's leave it unattended. To unlock, we connect the UART adapter, we will program through it. We set the jumpers for programming and let's go:

I will do this from under Ubuntu using the stm32flash utility.

1. Check if the microcontroller is visible:

sudo stm32flash /dev/ttyUSB0

Should get something like this:

Stm32flash 0.4 http://stm32flash.googlecode.com/ Interface serial_posix: 57600 8E1 Version: 0x22 Option 1: 0x00 Option 2: 0x00 Device ID: 0x0410 (Medium-density) - RAM: 20KiB (512b reserved by bootloader) - Flash: 128KiB (sector size: 4x1024) - Option RAM: 16b - System RAM: 2KiB

2. Remove read protection and then write protection:

sudo stm32flash -k /dev/ttyUSB0 stm32flash 0.4 http://stm32flash.googlecode.com/ Interface serial_posix: 57600 8E1 Version: 0x22 Option 1: 0x00 Option 2: 0x00 Device ID: 0x0410 (Medium-density) - RAM: 20KiB ( 512b reserved by bootloader) - Flash: 128KiB (sector size: 4x1024) - Option RAM: 16b - System RAM: 2KiB Read-UnProtecting flash Done. sudo stm32flash -u /dev/ttyUSB0 stm32flash 0.4 http://stm32flash.googlecode.com/ Interface serial_posix: 57600 8E1 Version: 0x22 Option 1: 0x00 Option 2: 0x00 Device ID: 0x0410 (Medium-density) - RAM: 20KiB ( 512b reserved by bootloader) - Flash: 128KiB (sector size: 4x1024) - Option RAM: 16b - System RAM: 2KiB Write-unprotecting flash Done.

Now you can work normally with the microcontroller.

Once, when I drove into another rented apartment, I encountered a certain inconvenience, which was quite annoying: the light switch in the main room was behind a wall cabinet, which was screwed to the wall, and its rearrangement was impossible. This required a lot of time and effort. I wanted to solve this problem very much and one thought came to my mind: to make a remote control for lighting control!

It was from the idea of ​​​​creating my own remote control to control the light in the room that my passion for electronics, microcontrollers and various radio devices began.

After that, I began to study this topic, get acquainted with the basics of electronics, examples of devices, learn how people implement such devices. After searching for information on where to start studying microcontrollers, I learned about what Arduino is, what they are eaten with, and how to work with them. An easy solution looked very attractive, because as far as I understood at that time, the code is assembled in one or two. But having concluded that I would not know what was going on inside the microcontroller outside of the Arduino sketches, I decided to look for a more interesting option, which meant deep study and immersion in the wilds of microcontroller technology.

The company I work for has a development department, and I decided to turn to the engineers to guide me on the right path and show me where to start solving my problem. I was strongly dissuaded from studying Arduino, and in my hands was an unknown and incomprehensible green scarf on which there were inscriptions, letters, various electronic components.

All this for me at that time seemed incomprehensibly difficult, and I even came to some confusion, but I was not going to refuse the implementation of the task. So I got acquainted with the STM32 family of microcontrollers and the STM32F0-Discovery board, after studying which I would like to build my device for the purposes I need.

To my great surprise, such a large community, articles, examples, various materials on STM were not in the same abundance as for Arduino. Of course, if you search there are many articles "for beginners" that describe how and where to start. But at that moment it seemed to me that all this was very complicated, many details were not told that were interesting for the inquisitive mind of a beginner, things. Although many articles were characterized as “training for the smallest”, it was not always possible to achieve the required result with their help, even with ready-made code examples. That is why I decided to write a small series of articles on programming on STM32 in the light of the implementation of a specific idea: a lighting control panel in a room.

Why not AVR/Arduino?

Anticipating statements that it would be too early for an inexperienced beginner to immediately rush into the study of such a complex MK as STM32 - I will tell you why I decided to go this way without delving into and not getting acquainted with the Atmel processor family and not even considering Arduino as an option .

Firstly, the price-functionality ratio played a decisive role, the difference can be seen even between one of the cheapest and simplest MKs from ST and the rather "fat" ATMega:


After I saw significant differences between the price and capabilities of AVR and STM32, I decided that I would not use AVR in my development =)

Secondly, I first tried to determine for myself the set of skills that I would have received by the time I achieve the required result. If I decided to use Arduino, it would be enough for me to copy ready-made libraries, throw in a sketch and voila. But understanding how digital buses work, how a radio transmitter works, how it is all configured and used - in this situation, I would never have come. For myself, I chose the most difficult and thorny path so that on the way to achieve the result I would get the maximum experience and knowledge.

Thirdly, any STM32 can be replaced by another STM32, but with better performance. And without changing the wiring diagram.

Fourth, people involved in professional development tend to use 32-bit MCUs more often, and most often these are models from NXP, Texas Instruments and ST Microelectronics. Yes, and I could at any time approach my engineers from the development department and find out how to solve a particular problem and get advice on issues of interest to me.

Why is it worth starting the study of STM32 microcontrollers using the Discovery board?

As you already understood, we will begin our acquaintance and study of the STM32 microcontroller with you, dear readers, by using the Discovery board. Why exactly Discovery, and not your own board?

What do we need for development besides the Discovery board?

In our work with the Discovery board, we will need a number of other irreplaceable things that we cannot do without:

Let's get down to the initial setup and preparation of the IDE for work!

After the installation file of our IDE is downloaded, you can proceed with the installation. Follow the instructions of the installer to complete the installation process. After all the files necessary for work are copied, the installer window for software packages for development will appear Pack Installer. This installer contains low-level libraries, Middleware, sample programs that are regularly added to and updated.


To start working with our board, we need to install a number of packages necessary for work and need to find a microcontroller with which we will work. You can also use the search at the top of the window. After we have found our MK, we click on it in the second half of the window and we need to install the following list of libraries:
  1. Keil::STM32F0xx_DFP- a complete software package for a specific family of microcontrollers, including manuals, datasheets, SVD files, libraries from the manufacturer.
  2. ARM::CMIS- Cortex Microcontroller Software Interface Standard package, which includes a complete set of libraries from ARM to support the Cortex core.
  3. Keil::ARM_Compiler is the latest version of the compiler for ARM.
After installing the required packs, you can proceed to setting up the IDE and our debugger / programmer. To do this, we need to open the main Keil window and create a new project.


To do this, go to the menu Project -> New uVision Project and select the folder where we will save our project.

After Keil will ask us which MK will be used in the project. We select the MK we need and click OK.


And again, a window already familiar to us will appear in which we can connect the modules of interest to us to the project. For our project, we need two modules:
  1. CMSIS Library Core, which declares settings, register addresses and much more of what is necessary for the operation of our MK.
  2. Startup file, which is responsible for the initial initialization of the MK at startup, the declaration of vectors and interrupt handlers, and much more.
If all the dependencies of the plug-ins are satisfied, the manager will signal this to us in green:


After we press the key OK we can start creating our project.

In order to configure the project parameters and set up our programmer, you need to right-click on Target 1 open the corresponding menu.


In the main menu of the project, set the parameter Xtal into meaning 8.0 MHz . This parameter is responsible for the frequency of the quartz oscillator of our MK:


Next, we move on to setting up our programmer / debugger. Click on the tab in the same window debug and select in the field use parameter ST-Link Debugger and go to settings:


In the settings, we should see the model of our ST-Link installed on the board, its serial number, HW version and IDCODE of the MK that we will be flashing:

For convenience, you can configure the parameter responsible for the MK to be reset automatically after flashing. To do this, check the box Reset and Run.


After that, we need to set up another option that will allow us to write Russian-language comments to the code of our projects. We press the button Configuration and in the opened menu in the field Encoding choose Russian Windows-1251 .


All. Our IDE and programmer are ready to go!

Keil has a convenient project navigator in which we can see the project structure, reference materials necessary for work, including those that we have already downloaded to our computer before (Discovery scheme, datasheet, reference manual), list functions used in the project and templates for quick insertion of different language constructs of the programming language.


Rename the folder in the project structure from Source Group 1 on App/User , thus denoting that in this folder we will have user program files:


Let's add the main program file through the project navigator by running the command Add New Item To Group “App/User” .


You must select from the proposed list C File (.c) and give it a name main.c :


The created file will be automatically added to the project structure and opened in the main program window.

Well, now we can start creating our program.

First of all, we need to connect the header document of our family of microcontrollers to our executable file. Add to file main.c lines of the following content, this program will make our LEDs blink alternately:

/* Header file for our family of microcontrollers*/ #include "stm32f0xx.h" /* Main program body */ int main(void) ( /* Enable clocking on the GPIO port */ RCC->AHBENR |= RCC_AHBENR_GPIOCEN; /* Configure PC8 and PC9 ports operation mode in Output*/ GPIOC ->MODER = 0x50000; /* Set Output type to Push-Pull mode */ GPIOC->OTYPER = 0; /* Set port speed to Low */ GPIOC->OSPEEDR = 0; while(1) ( /* Turn on PC8 LED, turn off PC9 */ GPIOC->ODR = 0x100; for (int i=0; i<500000; i++){} // Искусственная задержка /* Зажигаем светодиод PC9, гасим PC8 */ GPIOC->ODR=0x200; for (int i=0; i<500000; i++){} // Искусственная задержка } }
After we have written our program, it is time to compile the code and upload the firmware to our MK. To compile the code and download, you can use this menu:


Team Build (or hot key F7) will compile the code, and if there were no errors, the program will display the following message in the compilation log stating that there are no errors and warnings:


Team load (or hot key F8) will load the compiled code into our MK and automatically send it for execution:


After downloading the code, we will see how the LEDs began to blink at regular intervals.


Hooray! We have taken the first step in mastering STM32 microcontrollers! In we will analyze what bit and logical operations are, how to use them and learn about one very useful utility for working with MK, but for now we can enjoy how cheerfully the LEDs on our Discovery board blink.)