From AVR to STM32: An introduction to ‘Bare Metal’-programming the Arm-Cortex-M4(R) MCUs (Lesson 10: I2C)

Abstract

This uni of our online tutorial will cover the implementation, configuration and usage of I2C communication with the STM32F4 MCUs. Later we will show practical examples, that can be used for driving an SH1106 OLED display for example or a 24C65 external EEPROM.

I2C basics

I2C (spoken “I square C” for inter-integrated circuit) has been developed by Philips Company in the beginning of the 1980s. It is a two-wire serial bus system with one master and one or multiple slaves connected to a common bus. With ATMEL this is called TWI (“two wire interface” due to copyright reasons).

SCL (Serial Clock) is a permanent clock signal and SDA (Serial DAta) is the data line. Each slave is identified by a unique 7-bit address. Thius often is coded as

uint8_t device_addr = 0x78; //I2C_DEVICE_ADDRESS; here for OLED SH1106

The remaining bit (LSBit) indicates to the slave either the it should receive data from or transmit data to the master.

In practical circuits it is important that the two lines (SCL and SDA) must be pulled up with resistors (2.7 to 4.7 kOhms) to VDD to make sure that the ports of the ICs used for master and slave can pull down the lines when data or clock is transmitted. This system generates the mandatory voltage swing between 0V and VDD on the 2 lines.

I2C on STM32F4

I2C uses ports by applying “alternate function” mode (AF) to the respective pins. In this demonstration we will use ports B6 as SCL and B9 as SDA.

First we have to enable the related clock for I2C communication:

As we want to activate I2C interface 1 (I2C1) we have to set high the corresponding clock by setting bit 21 as “1”:

RCC->APB1ENR |= (1 << 21);

The following sequence does the port definition as alternate function with open drain output (we are about to use pull up resistors, please remember!)

//Setup I2C PB6 and PB9 pins
RCC->AHB1ENR |= (1 << 1);
GPIOB->MODER &= ~(3 << (6 << 1)); //PB6 as SCK
GPIOB->MODER |= (2 << (6 << 1)); //Alternate function
GPIOB->OTYPER |= (1 << 6); //open-drain
GPIOB->MODER &= ~(3 << (9 << 1)); //PB9 as SDA
GPIOB->MODER |= (2 << (9 << 1)); //Alternate function
GPIOB->OTYPER |= (1 << 9); //open-drain

Based on the table above and having prepared PB6 and PB9 for AF use we have to program AF4 (0b0100) for PB6 in AFRL (aka AFR[0] in CMSIS) and PB9 in AFRH (aka AFR[1] in CMSIS):

//Choose AF4 for I2C1 in Alternate Function registers
GPIOB->AFR[0] |= (4 << (6 << 2)); // for PB6 SCK
GPIOB->AFR[1] |= (4 << ((9-8) << 2)); // for PB9 SDA

The next steps perform a software reset of the register and subsequently reset this I2C configuration register:

//Reset and clear register
I2C1->CR1 = I2C_CR1_SWRST;
I2C1->CR1 = 0;

The last steps are defining the clock frequency for the I2C bus based on APB hub frequency and enabling I2C interface:

//Set I2C clock
I2C1->CR2 |= (10 << 0); //10Mhz peripheral clock
I2C1->CCR |= (50 << 0);
I2C1->TRISE |= (11 << 0); //Set TRISE to 11 eq. 100khz
I2C1->CR1 |= I2C_CR1_PE; //Enable i2c
//I2C init procedure accomplished. 

As you can see from CR2 register description that peripheral is defined in I2C->CR2 register (bits 5:0) and bits 11:0 in CCR register. It is strongly recommended to take a view in the reference manual chapters 18.6.2 (CR2 register) and 18.6.8 (CCR register). The same is valid for TRISE definition:

Sending and receiving data via I2C

Sending 1 byte of data

Before we define the a function for sending 1 byte of data we may look like first to 2 functions to  be defined to start and stop an I2C data transfer:

void i2c_start(void) 
{
    I2C1->CR1 |= (1 << 8);
    while(!(I2C1->SR1 & (1 << 0)));
}

void i2c_stop(void) 
{
    I2C1->CR1 |= (1 << 9);
    while(!(I2C1->SR2 & I2C_SR2_BUSY));
}

Next we program the transmit function:

void i2c_write_1byte(uint8_t regaddr, uint8_t data) 
{
    //Start signal
    i2c_start();

    //Send chipaddr to be targeted
    I2C1->DR = DeviceAddr;
    while (!(I2C1->SR1 & I2C_SR1_ADDR)); //Wait until transfer done
    //Perform one read to clear flags etc.
    (void)I2C1->SR2; //Clear address register

    //Send operation type/register 
    I2C1->DR = regaddr;
    while (!(I2C1->SR1 & I2C_SR1_BTF)); //Wait until transfer done

    //Send data
    I2C1->DR = data;
    while (!(I2C1->SR1 & I2C_SR1_BTF)); //Wait until transfer done

    //Send stop signal
    i2c_stop();
}

When we wish to  write more than one byte we have to hand over a pointer to the data we want to transmit as well as the number ob bytes to be transmitted:

//Multiple number of bytes (2+)
void i2c_write_nbytes(uint8_t *data, uint8_t n) 
{
    int t1 = 0;

    //Send start signal
    i2c_start();

    //Send device address
    I2C1->DR = device_addr; 
    while (!(I2C1->SR1 & I2C_SR1_ADDR));
    //Perform one dummy read to clear flags
    (void)I2C1->SR2;

    for(t1 = 0; t1 < n; t1++)
    {
        //Send data
        I2C1->DR = data[t1];
        while (!(I2C1->SR1 & I2C_SR1_BTF));
    } 

    //Send stop signal
    i2c_stop();
}

And if you wish to receive data from the I2C system, here 2 examples how to do this. The 1st one calls a one-byte address and returns the 8 bit value for register, the 2nd one can address a word register (16 bits) an return the containing byte for example in EEPROMs like the 24C65 IC.

//Address a byte register, return one byte
uint8_t i2c_read1(uint8_t regaddr) 
{
    uint8_t reg;

    //Start communication
    i2c_start();

    //Send device address
    I2C1->DR = DeviceAddr;
    while (!(I2C1->SR1 & I2C_SR1_ADDR));
    //Dummy read to clear flags
    (void)I2C1->SR2; //Clear addr register

    //Send operation type/register 
    I2C1->DR = regaddr;
    while (!(I2C1->SR1 & I2C_SR1_BTF));

    //Restart by sending stop & start sig
    i2c_stop();
    i2c_start();

    //Do it again!
    I2C1->DR = DeviceAddr | 0x01; // read
    while (!(I2C1->SR1 & I2C_SR1_ADDR));
    (void)I2C1->SR2; //Dummy read

    //Wait until data arrived in receive buffer
    while (!(I2C1->SR1 & I2C_SR1_RXNE));
    //Read value
    reg = (uint8_t)I2C1->DR;

    //Send stop signal
    i2c_stop();

    return reg;
}

//Address a word register, return one byte
uint8_t i2c_read2(uint16_t regaddr) 
{
    int8_t reg;
    int16_t r_msb, r_lsb;

    r_msb = (regaddr & 0xFF00) >> 8;
    r_lsb = regaddr & 0x00FF;

    //Start communication
    i2c_start();

    //Send device address
    I2C1->DR = device_addr;
    while (!(I2C1->SR1 & I2C_SR1_ADDR));
    //Dummy read to clear flags
    (void)I2C1->SR2; //Clear addr register

    //Send operation type/register MSB
    I2C1->DR = r_msb;
    while (!(I2C1->SR1 & I2C_SR1_BTF));

    //Send operation type/register LSB
    I2C1->DR = r_lsb;
    while (!(I2C1->SR1 & I2C_SR1_BTF));

    //Restart by sending stop & start sig
    i2c_stop();
    i2c_start();

    //Repeat
    I2C1->DR = device_addr | 0x01; // read
    while (!(I2C1->SR1 & I2C_SR1_ADDR));
    (void)I2C1->SR2;

    //Wait until data arrived in receive buffer
    while (!(I2C1->SR1 & I2C_SR1_RXNE));
    //Read value
    reg = I2C1->DR;

    //Send stop signal
    i2c_stop();

    return reg;
}

Full code example can be downloaded here.

What can be seen as a useful homework: Reprogram the functions in a way that you can write one byte to a word register for EEPROMs like 24C65.

Thanks and hope to see u again!

vy 73 de Peter (DK7IH)

From AVR to STM32: An introduction to ‘Bare Metal’-programming the Arm-Cortex-M4(R) MCUs (Lesson 9: PWM)

by Peter Baier (DK7IH)

Abstract

In this article we will discuss pulse width modulation (PWM), how the respective signals are produced and how we can use them to control motor, light bulbs, LEDs etc.

PWM in short

When current consuming devices like motors, light bulbs, LEDs etc. have to be controlled concerning their intensity you can either control the voltage driving the device (using Ohms law) or you can control the energy transmitted per time unit that powers the device. PWM uses the latter possibility.

PWM takes advantage of square waves, continously “flipping” between two extreme values: Max. power and zero power. The longer the time power is transferred (compared to the time slice where no power is transferred, the greater the total energy coming to the connected device will be:

You can see three different waveforms with different so called duty cycles. No. 1 features relative low “on” time and a very much larger time span “off”. Thus the total amount of energy transferred to the connected device is fairly low. No. 2 has about 50% on- vs. off-time each and no. 3 has a large amount of “on” time and relative low time “off”. In the latter case energy brought to the load is higher than in the other two examples.

One advantage of this method is that the driving circuit dissipates very much lower amounts of thermal energy because its resistance is nearly equal to zero when switched and very high when not conducting. Thus, according to Ohms law and power calculation (P = V x I in DC circuits), the amount of thermal energy produced by the semiconductor is minimized.

Usually for practical applications you can not connect the load directly to the microcontroller because it an output port is not capable of driving more than some milliamps. Deduced from this fact one or more driver stages have to be involved. We will talk about this issue later.

PWM generation in the STM32F4

PWM in an MCU is generated by using a timer. This is the ideal tool because PWM has got a lot of things in common with timing as you can see from the graph above.

To achieve our goal we first have to perform a setup sequence for a timer if we intend using it for PWM. In the following example we will use timer 3 (TIM3) in the STM32 MCU device to drive output port PA6 with an adjustable waveform between 0 and 100% duty cycle.

//////////////////////////////////////////
//Setup TIMER3 for PWM control
//////////////////////////////////////////
RCC->AHB1ENR |= (1 << 0); //Enable GPIOA clock
GPIOA->MODER |= (0x02 << 12); //Set PA6 to AF mode
GPIOA->AFR[0] |= (0b0010 << 24); //Choose Timer3 as AF2 for Pin PA6 LED
RCC->APB1ENR |= (1 << 1); //Enable TIM3 clock (Bit1)

Enabling the respective GPIO port (here “A”), setting PA6 to “alternate function” (AF) mode, setting PA6 as AF mapped to TIM3 are the first things to be done. Here the procedure is the same as described before. Check out the “alternate function” table in datasheet starting from p. 62, check out the device you want to have the application for (TIM3 in this case the 3rd column in that table) and check out the port that is going to be used:

As we intend to use PA6 we have to select AFRL6 in “alternate function” register AFRL which is named in CMSIS as AFR[0]. Don’t get confused, register names are sometimes not matching data sheet or reference manual by 100%!

(Source: STM32F4 reference manual p. 285)

The last line of our code example above switches TIM3 clock to “on” so we can use it later.

The next 4 lines refer to the timer settings which have to be defined:

TIM3->PSC = 499; //fCK_PSC / (PSC[15:0] + 1) -> 50 Mhz / (499 + 1) = 100 khz timer clock speed
TIM3->ARR = MAXPERIOD; //Set period max. value
TIM3->CCR1 = duty_cycle;; //Set duty cycle
TIM3->CCMR1 |= (0x06 << 4); //Set OC1 mode as PWM (0b110 (0x06) in Bits 6:4)

First we set the prescaler that is deduced from system clock speed. In the example SYSCLK is set to 100MHz. To learn how to set SYSCLK please refer to unit 3. The prescaler determines the clock rate the counter counts up (or down if you use a downcounter). The respective formula is given in the comment line.

The next register is the ARR (auto reload register) register that sets the “ceiling” the counter will count to before it will be reset. We use 255 as max. value.

In CCR1 register a value is preloaded that sets the initial setting of the timer. That is the value the timer starts to count with. Here we use it to define the current duty cycle.

With CCMR1 we set the timer “output compare” mode:

By writing 0b110 (dec. 6) we define “110”: “PWM mode 1” for this counter.

The next block of instructions defines compare mode and interrupt status:

TIM3->CCMR1 |= (1 << 3); //Enable OC1 preload (Bit3)
TIM3->CCER |= (1 << 0); //Enable capture/compare CH1 output
(TIM3->DIER |= (1 << 0); //Enable update interrupt)

First instruction OC1PE: sets “Output compare 1 preload enable” in CCMR1 enabling us to use the preload register.

Second instruction hands the signal out on the corresponding output pin.

Last instruction (in parenthesis) enables triggering the interrupt when TIM6 or 7 are used. Here, with TIM3, this is not necessary.

By the end of the setup sequence we have to activate interrupt usage and activate TIM3.

NVIC_SetPriority(TIM3_IRQn, 2); // Priority level 2
NVIC_EnableIRQ(TIM3_IRQn); //Enable TIM3 IRQ via NVIC
TIM3->CR1 |= (1 << 0); //Switch on TIM3

As always the handler function has to be defined and declared correctly. If you compile using “C++” option you must declare/define the function as extern”C” to get the correct idnetifier of the handler function. The only use of this function is to clear interrupt status after timer overflow has occurred and interrupt has been fired.

//////////////////////////////////
// TIM3 INT Handler (PWM)       //
//////////////////////////////////
extern "C" void TIM3_IRQHandler(void)
{
    //Clear interrupt status
    if((TIM3->DIER & 0x01) && (TIM3->SR & 0x01))
    {
        TIM3->SR &= ~(1U << 0);
    }
}

Setting the duty cycle

Setting the desired duty cycle depends on the driver stage you are about to use, i. e. inverting vs. non-inverting. You can either try:

TIM3->CCR1 = duty_cycle; //Set duty cycle

or

TIM3->CCR1 = 255 - duty_cycle; //Set duty cycle

A word about driver stages

As we pointed out before, you can not connect most loads directly to the output pins. A driver stage is mandatory in this case. These stages can be realized either by using bipolar or field effect transistors. Or you can use integrated circuits. Some examples can be found just by using a search engine.

If you intend using a MOSFET driver it is important to keep in mind that with most power MOSFETs gate voltage must be very much higher than the 3.3 Volts you can expect from the MCU’s output port to drive the semiconductor adequately. In these cases you can use a “low voltage” MOSFET specially intended for these purposes or you can switch in an intermediate driver stage equipped with a bipolar transistor. This is due to the fact that bipolar transistors are current driven wheres MOSFETs are voltage driven. The bipolar semiconductor will switch with a base current around some microamperes and then switch the voltage of the MOSFET gate provided the voltage in your application is high enough.

So, that’s all for today. See you later and “thanks for watching”!

73 de Peter (DK7IH)

 

 

 

From AVR to STM32: An introduction to ‘Bare Metal’-programming the Arm-Cortex-M4(R) MCUs (Lesson 8: Rotary Encoder)

by Peter Baier (DK7IH)

Abstract

Today’s lesson will cover a topic that the software developer will encounter sooner or later: The need to decode the signals from a rotary encoder. We will discuss that and deliver a simple and easy-to-use solution for this problem.

Theory

A rotary encoder is a set of two mechanical or optical switches driven by a rotating axle, a “shaft”. Both switches are connected to the same GND potential, thus these encoders have two outputs (usually called “A” and “”B”) and a share common ground (GND). Sometimes they also are equipped with an additional push button (S):


Function in brief

When the encoder is rotated, the two switches A and B change their status from “open” to “closed” with a certain phase delay bet ween outputs A and B:

You can see from the picture that clockwise rotation (CW) is present when signal “A” is high and signal “B” presents a rising edge while signal “A” preserves its status (1). In contrast, when the shaft is turned counterclockwise (CCW), signal “A” is high again but “B” shows a falling edge (2). By decoding these two signals as a function of time you can clearly distinguish between CW and CCW rotation.

Reading the rotary encoder in software

Polling the two output lines of the encoder and subsequent decoding is one method to find out about rotor’s status but that wastes a large amount of processor time. The best way to get the rotation direction of this type of encoder is to use interrupts. Signal “A” triggers an interrupt and simultaneously signal “B” is measured.

For example you can trigger an interrupt when “A” represents a falling edge, meaning that it has been “high” right before, and signal “B” will not have changed its status yet. So you can clearly discern the direction of the rotating shaft.

Reading a rotary encoder with STM32F4

Due to the fact that these encoders are widely used in modern electronics, the STM32F4 has integrated functions to read out such devices. But we will use a different method, the well known one from AVRs. That means using something that is called “pin change interrupt”. This type of triggering interrupts also is available in the STM32 MCUs when you write a piece of code for it.

Step 1: Define the input ports and the interrupt

In this example we want to read out a rotary encoder that is connected to pin PB0 and PB1 of an STM32F411 on a “Black Pill”-board. Thus we have to define ports first:

//Set PB0, PB1 as input pins
RCC->AHB1ENR |= (1 << 1); //GPIOB power up
GPIOB->MODER &= ~((3 << (0 << 1))|(3 << (1 << 1))); //PB0 und PB1 Input
GPIOB->PUPDR |= (1 << (0 << 1))|(1 << (1 << 1)); //Pullup PB0 und PB1

First we power up port GPIOB, second we set PB0 and PB1 as inputs by writing 0b11 (dec. 3) into the respective bits of the MODER register. Next step: We set pull-up resistors because the input pins must have some electrical potential to be decoded when pulled to ground or left open.

Interrupt programming

Now we enable the System Configuration Controller in APB2ENR register (Bit 14), which, among other functions, controls the interrupt mapping from GPIO pins to the desired interrupt sources.

 RCC->APB2ENR |= (1 << 14); //Enable SYSCFG clock (APB2ENR: bit 14)

The next 3 steps are related directly to the interrupt function we are about to use. The first line involves the EXTI0 (external interrupt 0) mapped  to PB0. This is an essential step we have to explain more detailed:

In the STM32F4 MCU external interrupts are organized in so called “lines”. There are 16 of them. The special thing, if you want to say so, for former AVR developers is that all pins having the same number of the various MCU ports share the same “line”. This means, for example, that PA0, PB0, PC0 etc. share the same multiplexed interrupt bus and therefore only one external pin of this set can be used to trigger an interrupt in the respective line. There is a minor exception from this rule (concerning PA5 iirc) but generally this rule sticks for the whole system of external interrupts.

To map a certain pin to an interrupt the EXTICR registers have been designed. In the reference manual you can find them defined beginning with page 291. There are 4 of them:

  • EXTICR1 (called EXTICR[0] in CMSIS header file)
  • EXTICR2 (called EXTICR[1] in CMSIS header file)
  • EXTICR3 (called EXTICR[2] in CMSIS header file)
  • EXTICR4 (called EXTICR[3] in CMSIS header file)

As you can see, once again the identifiers in STM32F4 software package are different from datasheet! So care must be taken!

As an example (and for our code here) we take a closer look on EXTICR1 (EXTICR[0]).

This definitely requires explanation: As we desire to use a pin with number “0” in its name, we have to use the group of pins (“line”) with “0” in the description. This line is represented in the EXTI0 bits [3:0] which also are bits 3:0 of this register. As it is PB0 we want to trigger the interrupt, we have to take the second line from the text block: 0001: PB[x]. This is because our “x” here is 0, so we write 0b0001 into the first 4 bits of this register:

SYSCFG->EXTICR[0] |= 0x0001; //Write 0001 to map PB0 to EXTI0

The remaining instructions define that we want to set up something in AVRs we would call “pin change interrupt”:

EXTI->RTSR |= 0x00001; //Enable rising edge trigger on EXTI0

And last we have to exclude EXTI0 from the interrupts that are not fired on an event:

EXTI->IMR |= 0x01; //Mask EXTI0 in IMR register

The two remaining steps are defining interrupt priority and activating the interrupt:

NVIC_SetPriority(EXTI0_IRQn, 1); //Set Priority

Please note: Priority in interrupts is defined in a way, that the low numbers stand for high priority!

NVIC_EnableIRQ(EXTI0_IRQn); //Enable EXT0 IRQ from NVIC

So, again we are ready to go!

Last we have to set up the function that handles the interrupt once it is fired and deals with the pin data from the encoder: Declaration first

extern "C" void EXTI0_IRQHandler(void);
int16_t rotate = 0;
int16_t laststate = 0; //Last state of rotary encoder

and definition:

extern "C" void EXTI0_IRQHandler(void)
{
    uint16_t state;

    //Check first if the interrupt is triggered by EXTI0
    if(EXTI->PR & (1 << 0))
    {
        state = GPIOC->IDR & 0x03; //Read pins
        if(state & 1)
        {
            if(state & 2)
            {
                 //Turn CW
            }
            else
            {
                //Turn CCW
            }
        } 

        //Clear pending bit
        EXTI->PR = (1 << 14);
}

Here again we have to declare the function as ‘extern “C”‘ because when compiled under C++ settings the handler name will be altered by the compiler therefore the handler will not be recognized and your program will crash as soon as the interrupt is triggered.

Thanks for being here! Hope you enjoyed this lesson!

vy 73 de Peter (DK7IH)

 

 

 

 

 

From AVR to STM32: An introduction to ‘Bare Metal’-programming the Arm-Cortex-M4(R) MCUs (Lesson 7: ADC (I))

by Peter Baier (DK7IH)

Abstract

This unit will cover the basic usage of the Analog-Digital-Converter (ADC) in the STM32F4 MCU. We will develop a simple procedure to read multiple ADC values from various input pins and have these data ready for further processing.

ADC in STM32F4 in brief

The Analog-Digital-Converter in the STM32F4 MCU uses the basic principles of function like most ADCs in other digital circuits: The process of successive approximation. An internal generator (a Digital-Analog-Converter) produces a variable DC signal with different voltage and this is compared to the input signal in a way that in the end the voltages of the two sources (internal DC and external signal voltage) match, so they are equal. By reading the respective DAC data we get the digital equivalent of the externally applied voltage. This process involves a certain time (thus defining a sampling rate) and varies in bit width of the detecting process.

In contrast to AVRs, where the ADC usually has 10bits width, the ADC in the STM32F4 unit can be switched to a maximum of 12 bits processing width. There are up to 3 different ADCs in one MCU depending on the exact model you have in use. They are numbered as “ADC1”, “ADC2” and “ADC3”, if present. They can be connected to certain input pins but not all combinations are possible.

Setting up the ADC

As always to start with a functional unit, this unit inside the MCU has to be configured before we can use it. First we have to have the reference manual at hand. The “ADC”-section starts with page 388. The registers can be found beginning from page 415. Lets directly stick to that part because from the vast set of possibilities to make use of the ADC we will select a really simple approach to avoid confusion.

Some general annotations before we start:

You as a developer can assign certain pins to certain ADC. But not all combinations are possible as mentioned before. They are limited as shown in the following table:

This table is deduced from an entry in datasheet, the “Pinouts and pin description” section table starting on page 50. You can learn how a given input pin can be assigned to a certain ADC channel.In the “additional functions” sections you can see the pins assignable to certain ADC channels.

For ADC1 we, to start in in simple way into this more or less complex field, want to use pin PB0 as an input for analog voltage. From the config table above we deduce that PB0 can either be assigned to ADC1 or ADC2 as input channel #8. We will use ADC1. The further steps of configuration are:

//Port config
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN; //Power up PORTB
GPIOB->MODER |= (3 << (0 << 1)); //Set PB0 to analog mode

As you can see PB0 is switched to analog mode which requires writing 0b11 to the respective bits in MODER register.

Next the ADC will have to be set up:

//ADC config sequence
RCC->APB2ENR |= (1 << 8); //Enable ADC1 clock (Bit8) 
ADC1->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8)
ADC1->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00)
ADC1->SQR1 |= (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion
ADC1->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
ADC1->SQR3 |= (0b01 << 3); //1st conversion in regular sequence: SQ1: PB0 as ADC1_IN8
ADC1->CR2 &= ~(1 << 1); //Single conversion
ADC1->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0
ADC1->SMPR2 |= (7 << 0); //Sampling rate 480 cycles. 16MHz bus clock for ADC. 1/16MHz = 62.5ns. 480*62.5ns=30us
ADC1->CR2 |= (1 << 0); //Switch on ADC1

The first 3 lines switch the ADC clock signal to “on” (thus power the ADC, disables scan mode which means that the ADC will not automatically run thru the configured channels and do (unnecessary) conversion for our simple example and we set the resolution to the maximum of 12 bits.

Steps from #4 (“Set number of conversions…“) and beyond have to be explained in a more detailed way:

In the STM32F4 ADC the signals to be used for conversions of analog signals are organized in so called “sequences”. There are “regular” and “injected” sequences (we are about to define a “regular” one) and they can contain up to 16 different inputs served sequentially. So, a “chain” must contain at least 1 and may contain 16 channels max.

How many channels are held in a sequence is defined in the SQR1 register (“sequence register”) , namely in bits 23:20:

Because we only want to read one input by one time, we write decimal “1” into this set of bits:

ADC1->SQR1 |= (1 << 20); 

The next step is to fix what channel will be used (first, if you have more). The sequence to be processed is defined beginning with SQR3, SQR2 and the remaining bits of SQR1 register. As we only have conversion channel projected we directly got to SQR3 register:

Here bits 4:0 (SQ1) define the first (and in our example only) channel to be read.

ADC1->SQR3 |= (0b01 << 3); 

As we want to use channel 8 we shift 0b01 by 3 bits to the left. Another possible (maybe even better) notation would have been “0b01000”. This says that channel 8 will be assigned as first (and int this case only) channel to be processed.

The remaining 4 lines of code define the way of conversion (we perform only a single conversion when called), define adjustment of the data bits (right alignment, so we don’t have to care about actual number of bits appearing after the conversion) we set a sampling rate and, last but not least, we switch on the ADC. Please refer to the reference manual to make clear what these bits are intended for in the respective register!

With this sequence of defintiions the ADC now is “ready to use”.

To read an analog value from PB0 the ADC’s “DR” (data register) has to be read. First we start a conversion by writing the value of “1” into bit 30 of ADC->CR2 register and then get the corresponding digital value from the “DR” register.

uint16_t adcval = 0;
ADC1->CR2 |= (1 << 30); //Start 1st conversion SWSTART
while(!(ADC1->SR & (1 << 1))); //Wait until conversion is complete
adcval = ADC1->DR; //Read value from register

Using multiple input pins

In case you wish to extend this example to, let’s say, two different input pins this can be easily done. You have to redefine the start of the sequence (actually we only have one channel to read, so there is no “sequence”). Let’s say we want to read channel 8 AND channel 9 (PB0 and PB1) of port B.

First: Don’t forget to set PB1 as previously done with PB0 by the begin of the initialization sequence:

GPIOB->MODER |= (3 << (1 << 1)); //Set PB1 to analog mode

Then, before reading a a channel we must reset the respective “begin” of the sequence in SQR3 register. For PB0 we have shown it before, for PB1 it is slightly different:

ADC1->SQR3 = (0b01001); //Set 1st conversion in regular sequence: SQ1: PB1 as ADC1_IN9

After that you can again read the ADC, this time connected to PB9.

Hint: Please note the the usual “|=” operator frequently used for register definition does not apply here! This is because if you try to reset the bits by simply “or-ing” them, their total value will remain unchanged in this case. If you have multiple bits and want to redefine only some of them,  first use the “&= ~” operator to reset the bits you are about to modify.

Example code

On my Github repo you can find two files. One to demonstrate the reading of the internal temperature sensor, the second one to show the usage of PB0 as analog input like discussed before. For this example a 4×20 LCD module has been connected to the MCU to give you a detailed reading of data.

Thanks for today an c u!

73 de Peter (DK7IH)

From AVR to STM32: An introduction to ‘Bare Metal’-programming the Arm-Cortex-M4(R) MCUs (Lesson 6: SPI (I))

by Peter Baier (DK7IH)

SPI (serial peripheral interface) is a communication interface between electronic devices like microcontrollers, displays, DDS generators and many other electronic circuits. It is widely used, thus the manufacturers of microcontrollers have implemented predefined SPI functions into their hardware. On the other hand, it is possible (and easy) to write these functions on your own. Sometimes this is the even preferably way because debugging is easier and faster. But today we will focus on using the integrated SPI hardware found in the STM32 MCUs.

SPI: The basics in brief

SPI uses 3 or 4 lines between the so called “host” (often called ‘master’) and the peripheral device (often named “slave”).

(from Wikipedia)

In the datasheet of any SPI device you can find something that is called “timing diagram”. They all look much or less the same, SPI is standardized to a very high degree.

AD9834 DDS SPI Timing diagram

(from datasheet AD9834, an Analog Devices DDS synthesizer)

The first line represents the so called “clock signal”, it switches from low to high, indicating that the current data bit is to be read right in that very moment (line #3). The data stream that goes from master to slave is called MOSI” (“master out, slave in”).

The remaining signal to be explained is called “CS” (for “chip select”, sometimes named differently like “FSYNC” in that case or “NSS” for “negative slave select” in ST’s language). It indicates that the master has got data now for the peripheral device and that it should listen to it. If you have more than one “slave” device in your circuitry you must have a separate CS line for every “slave” in case you do not use cascaded slaves. CS, by the way, is an “active low” signal line.

A fourth line is the “MISO” (master in slave out) line which transfers data from “slave” to “master”, if requested.

Setting up SPI in the STM32 microcontroller

There are up to 3 SPI devices in the STM32F4 MCU and they are controlled, again, using the “alternate function” mapping.

Hint: A warning is given in the reference manual by STM because some SPI pins interfere with other essential functions like the JTAG interface for example. See page 873 of the reference manual for details!

In our following code discussion we will use SPI2 which is not prone for any of these conflicts.

Setting up the peripheral SPI structure

Here you can recognize that the relevant pins are PB15:PB12. Thus we have to start with powering and clocking the respective port, i. e. GPIOB as well as clocking the SPI

//////////////////////////////////////////
// Setup SPI
//////////////////////////////////////////
//PB12:CS; PB13:SCK; PB14:MISO; PB15:MOSI
RCC->AHB1ENR |= (1 << 1); //GPIOB power clock enable
RCC->APB1ENR |= (1 << 14); //Enable SPI2 clock, bit 12 in APB2ENR

Next we set up port B:

//Alternate function ports
GPIOB->MODER &= ~(0b11111111U << (12 << 1)); //Reset bits 15:12
GPIOB->MODER |= (0b01 << (12 << 1)); //PB12 (CS) as output
GPIOB->MODER |= (0b101010U << (13 << 1)); //Set bits 15:13 to 0b101010 for alternate function (AF5)
GPIOB->OSPEEDR |= (0b11111111 << (12 << 1)); //Speed vy hi PB15:PB12

//Set AF5 (0x05) for SPI2 in AF registers (PB13:PB15)
GPIOB->AFR[1] |= (0x05 << (20)); //PB13
GPIOB->AFR[1] |= (0x05 << (24)); //PB14
GPIOB->AFR[1] |= (0x05 << (28)); //PB15

There is some confusion about the CS line in many publications. It is recognized that SPI is little bit faulty in the STM32s. This affects CS (NSS) signal. A workaround is to define this line as “normal” output and switch it by software when transmitting data.

The second block sets the “alternate function registers” according to the table above an the AFR[1] register. If you aren’t quite clear about this technique, please refer to lesson 4 where AF usage is explained in detail.

After having done port configuration the last remaining step is to specify the working conditions  for the SPI interface:

//Set SPI2 properties
SPI2->CR1 |= (1 << 2); //Master mode
SPI2->CR1 |= SPI_CR1_SSM; //Software slave management enabled
SPI2->CR1 |= SPI_CR1_SSI; //Internal slave select The value of this bit is 
//forced onto the NSS pin and the IO value of the NSS pin is ignored.
SPI2->CR1 |= SPI_CR1_SPE; //SPI2 enable

With this you are done with preparing the SPI interface and can start transmitting data:

void spi_write(uint8_t data)
{
    GPIOB->ODR &= ~(1 << 12); //CS low
    SPI2->DR = data; //Write data to SPI interface
    while (!(SPI2->SR & (1 << 1))); //Wait till TX buf is clear
    GPIOB->ODR |= (1 << 12); //CS high
} 

As code examples there are three files to illustrate basic SPI application:

#1: The basic procedure that can be seen above and that has been explained (hopefully) to the details (Link to file)

#2 An example that shows the basic driving functions for a typical SPI LCD (IL9341). This code lacks extensive functions, it just holds an initialization and a clearscreen routine. (Link to file)

#3 A full display driver for the ILI9341 containing all the functions you need to drive this display in text mode (including various character sizes) (Link to file)

Have a good one! 73 de Peter (DK7IH)

 

 

 

 

 

From AVR to STM32: An introduction to ‘Bare Metal’-programming the Arm-Cortex-M4(R) MCUs (Lesson 5: Timers and Interrupts)

by Peter Baier (DK7IH)

Abstract

This unit will cover the basic usage of timers and related interrupts, a crucial topic in the world of microcontrollers.

Introduction

The STM32/Arm Cortex(R)-M4 MCUs contain a large number of timers:

  • Basic timers
  • General-purpose timers
  • Advanced-control timers

There are up to 14 timers in an STM32-MCU that are fully independent and “do not share any system resources” (according to reference manual), thus there are quite a lot timers to chose from.

In this unit we will cover the general aspects of usage of a general purpose timer, detailed description and enhanced possibilities will  be subject for later discussion. As timers in most cases go along with usage of interrupts, we will take a look to them as well.

Setting up a timer

We want to set up a timer to count seconds in our application. Because usage of timers depends on the correct setting of the system clock we have to set this up correctly before we go on. Please refer to lesson #3 to learn about system clock settings.

First we set up timer #2 (TIM2), a 32-bit timer, that can be used as a up, down, up/down auto-reload
counter and is labeled as general purpose timer. Information in reference manual can be taken beginning from p. 589.

First step is to power up TIM2, which is defined by bit 0 of APB1ENR register:

//Enable TIM2 clock (Bit 0)
RCC->APB1ENR |= (1 << 0);

Next we do some calculations to set TIM2 adequately:

//Timer calculation
TIM2->PSC = 100000; //Divide system clock (f=100MHz) by 100000 -> update frequency = 1000/s
TIM2->ARR = 500; //Define timer overrun based on auto-reload-register to happen after 500ms

“PSC” is the prescaler used to divide system clock rate by a given factor to make the timer count.  It will increase the timer by 1 every number of clock ticks defined in “PSC”.

ARR is the register that contains the upper (or lower, if you are downcounting) margin of the counter. When this limit is exceeded an interrupt is fired, if activated.

Thus the interrupt has to be addressed and enabled before firing it:

//Interrupt definition
TIM2->DIER |= (1 << 0); //Update of Interrupt enable
NVIC_SetPriority(TIM2_IRQn, 2); //Priority level 2 for this event
NVIC_EnableIRQ(TIM2_IRQn); //Enable TIM2 IRQ from NVIC

To end the sequence correctly TIM2 must be switched on:

 TIM2->CR1 |= (1 << 0); //Enable Timer 2 (CEN, bit0)

Handling interrupt condition

As known from AVRs, a function must be defined that handles the interrupt.

Declaration (1st) and definition (2nd):

extern "C" void TIM2_IRQHandler(void); //IRQ-Handler timer2
...
extern "C" void TIM2_IRQHandler(void)//IRQ-Handler for TIM2
{
    if (TIM2->SR & TIM_SR_UIF) //Toggle LED on update event every 500ms
    {
        GPIOD->ODR ^= (1 << 15); //Blue LED
    }
    TIM2->SR = 0x0; //Reset status register
}

Note that this handler function must be declared AND defined as ‘extern “C”‘. When your compiler is set to C++ code generation mode the name of the handler function otherwise will be mangled according to C++ rules and get a different spelling, thus won’t be recognized when the code is executed and therefore the function call will end up anywhere in nowhere-land.

Additional information: If you don’t want to use an interrupt, you can also poll TIM2->CNT register while your program is in the infinite loop structure:

TIM2->ARR = 0xFFFF;
...
while(1)
{
    if(TIM2->CNT >= 500)
    {
        GPIOD->ODR ^= (1 << 15); //Blue LED
        TIM2->CNT = 0;
    } 
}


TIMx-ARR must be preset to a value that is able to be reached, because if the counter gets above the value of TIM2->ARR register TIMx->CNT will be reset to 0 and your if(..)-request never will trigger. If this method makes sense in all is up to the reader to decide!

The full code for this example can be downloaded here.

Hope, this unit provided some benefit and say CU later!

73 de Peter

 

Migrating from AVR 8-bit controllers to STM32 MCUs (Lesson 4: Alternate functions)

by Peter Baier (DK7IH)

Abstract

In this chapter of our online course we will see how Alternate Functions (AF) in the STM32 MCUs are programmed and used. Later we will measure the clock frequency by activating the respective “AF” and put out main clock frequency via an external pin to measure it.

Alternate Functions in the STM32 MCU

As in AVRs in STM32 MCUs as well, you can assign different functions to a number of the controller’s pins. In STM32 this assignment is, as in AVRs, not 100% deliberate, certain pins are reserved for certain functions. And one pin can be assigned to more than one function but only one by a time.

Assigning a pin to an alternate function is a two-step process.

First, you have to configure a pin as “AF”:


Second, you have to define what alternate function is assigned to that respective pin. To determine this selection, there is a large table to be found in datasheet, called the “Alternate Function Table“. You can find that beginning on p. 62 and ending on p. 70 of the datasheet.

Just have look to a small excerpt of this very large table:

With this example we want to assign Channel 3 of Timer 2 to pin PA2:

First we have to define PA2 as “alternate function” using MODER register:

GPIOA->MODER |= (0b10 << (2 << 1));

Next step is that we have to look up the respective “AF”-code in the alternate function table. This is “AF1” for our intended purpose.

To map this “AF1” to pin PA2 another register (or to say more exactly “one of 2 possible registers”!) is needed: The GPIO alternate function registers (AFRL and AFRH):

For every port in the MCU these two register exist. To map PA2 to AF1 as in our example, we have to use the bit mapping underneath the register description:

AF1 is represented by bit pattern 0b0001. So we set port A (x), bit 2 (y) by writing 0b0001 into this register:

GPIOA->AFR[0] |= (0b0001 << 4);

Explanation:

  • 0b0001: Bit pattern to be set,
  • 4: nmuber of bits to be shifted (must end up in bit 7:4)

Hint: You will often find that register labels in the respective .h-file are inconsistent. The two branches of AFR-register (AFRL and AFRH) in this case are not named as they appear in  reference manual but are named AFR[0] and AFR[1]. Sometimes it just helps to check the include file for this if compile fails due to a non-recognized identifier.

Example

For a practical example of how to use the AF register, we will get back to the last lesson, where we dealt with clocking the CPU. STM32 MCU incorporates a technique to connect some of the clocks to an external pin and therefore measure its frequency. We will take advantage of this.

Basic information: In the STM32 it is possible to hand out the clock signal to a pin. This is called “MCO” (master clock out). When you check the AF-table on p. 62 of the datasheet, you will recognize this portion:

With this information we can deduce that activating MCO to PA8 requires switching AF0 in AFR[0] register because AF0 is in the first block of possible AF address space (from AF0 to AF7).

The respective code is as follows:

//Turn on GPIOA
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; //RCC->AHB1ENR |= (1<<0);

//Set PA8 as MCO pin
RCC->CFGR |= (0b11 << 21); //MCO1: Microcontroller clock output 1 11: PLL clock selected
RCC->CFGR |= (0b100 <<24); //Divide f.out by 2
GPIOA->MODER |= (2 << (8 << 1)); //PA8 as AF
GPIOA->OSPEEDR |= (3 << (8 << 1)); //HiSpeed
GPIOA->AFR[1] = 0; //0b0000

Explanation:

After activation of GPIOA the next code line sets bit22 of the RCC-CFGR register. This will put out the PLL clock generated signal to MCO output.

After that we divide the frequency by 2 which is basically not necessary in this case because the pin can handle f.max=100MHz. So, this is just for demonstration.

Next line (#3 in this sequence) activates AF for PA8.

Line #4 sets output port speed to max. value.

The last line in the sequence maps MCO output PA8 by activating AF0. This means writing 0b000 to the first 4 bits of AFR[0] register.

With this you’re done again.

The example can be downloaded from my Github repo.

When connected to a spectrum analyzer you can identify a 50MHz signal on the screen (50MHz = 100Mhz / 2):

Thanks for attending this lesson! 🙂

73 de Peter (DK7IH

 

From AVR to STM32: An introduction to ‘Bare Metal’-programming the Arm-Cortex-M4(R) MCUs (Lesson 3: Setting system clock)

by Peter Baier (DK7IH)

With AVRs clocking was easy. Just set some fuse bits (but the right ones with the right values!!!!) , install external clock (optional) and go for it! With STM32s it is, as you might think, its alittle bit more complex.

Various sources for the clock signal

In the STM32F4 MCU there are 3 possible way of generating a clock signal:

  • Internal High-Speed-Oscillator (HSI) with 16MHz fixed frequency
  • External High-Speed-Oscillator (HSE) normally from 8 to 25 MHz frequency (crystal based)
  • PLL frequency derived from either HSI or HSE clock signal.

When you startup the system by supplying VDD the internal oscillator (HSI) is switched as clock source. Which clock you are about to use afterwards is defined in RCC->CR (Clock Control Register):

You can see two bits that work identically for HSI respectively HSE 16 bits apart in the same register: An “ON”-bit and a corresponding “RDY”-bit. First you power the oscillator of your choice by setting the “ON” bit to “1”:

HSI on:

RCC->CR |= (1 << 0);

HSE on:

RCC->CR |= (1 << 16);

After being switched “on” these oscillators need a short period of time until they are ready for use. This is checked in a loop by polling the respective flag bit:

while ((RCC->CR & (1 << 1) == 0); //Check HSI ready
while ((RCC->CR & (17 << 1) == 0); //Check HSE ready

Using PLL as clock source

Besides using HSI oder HSE stand-alone, STM32F4 MCU allows the developer to apply an onboard PLL synthesizer to work as the primary clock source for the system. This might lead to a mac. clock frequency of 168 MHz. All settings are done by software, so you are not in danger, like with the AVRs, to “defuse” a controller. Software definition implies several steps because PLL setting involves definition of various parameters.

These parameters are named by the Alphabet, starting with PLLM, PLLN, PLLP and PLLQ.

Their frequencies are derived successively from the respective predecessor, the chain starts with PLLM. Provided we use 8 MHz HSE as input source, we have to modify PLLCFGR register. These steps are defined as:

//Set PLLM
RCC->PLLCFGR &= ~0x3F; //1st reset bits
RCC->PLLCFGR |= 4;     //2nd define VCO input frequency 
                       //= PLL input clock frequency (f.HSE) / PLLM with 2 ≤ PLLM ≤ 63 
                       //-> f.VCO.in = 8MHz / 4 = 2MHz

VCO input frequency is derived from HSE and divided by 4. This leads to f.VCO.in = 2 MHz which is fine because the boundaries are 1MHz <= f.VCO <= 2MHz (Ref. manual pages 163, 164).

Next is PLLN which is a multiplication and leading to VCO output frequency::

//Set PLLN: PPLLN defines VCO out frequency
RCC->PLLCFGR &= ~0x7FC0; //1st Reset bits 14:6
RCC->PLLCFGR |= (100 << 6); //2nd define f.VCO.out = 
                             //f.VCO.in * 100 = 200MHz

With PLLP we define the PLL output frequency for modules like the USB interface. Certain limitations are to obeyed here, e. g. for USB frequency must be exactly 48MHz.

//Set PLLQ. PLLQ = division factor for USB OTG FS, SDIO and random number generator clocks
RCC->PLLCFGR &= ~(0b1111 << 24); //Reset bits 27:24
RCC->PLLCFGR |= (8 << 24); //PLL-Q: f.VCO.out / 8 = 25MHz

Nest step: We have to activate the PLL:

RCC->CR |= (1 << 24); //Activate PLL, Bit 24
while ((RCC->CR & (1 << 25)) == 0); //Wait until PLL is ready Bit 25

And finally the frequencies for the various bus systems and bridges must be set. For limitations see reference manual!

                            //Division of clock signal for bus system
RCC->CFGR |= (0b1001 << 4) //AHB divider: 100MHz / 4 = 25 MHz
| (0b100 << 10) //APB1 divider: /2
| (0b100 << 13); //APB2 divider: /2

Finally, not to be forgotten, PLL must be set as clock source for the MCU:

RCC->CFGR |= 0b10; //Switching to PLL clock source

Then you are ready to go!

Just another annotation: Onboard flash memory has to be configured as well. In Flash access control register (FLASH_ACR) the waitstates for FLASH memory have to be set. My rule of the thumb is:

  • 1 WS for f=50MHz
  • 2 WS for f>=100MHz
 FLASH->ACR |= 0b010; //2 wait state for 100 MHz

If you want to experiment, write a short program that makes a LED blink and test it with various PLL settings up to 168 MHz. Just check if the program works. If the software hangs try to increase waitstates!

A full demo code for the Diymore STM32F4 (F407) board can be found on my Github repo: Link.

Hope you enjoyed the lesson. C U!

73 de Peter (DK7IH)

 

 

 

 

 

From AVR to STM32: An introduction to ‘Bare Metal’-programming the Arm-Cortex-M4(R) MCUs (Lesson 2: Register usage and port configuration)

Abstract

In this lesson we will talk about port configuration in the STM32F4 MCU. First a closer look to the registers will be taken and then we will cover first output port configuration. By the end of this lesson input port config and usage will be covered.

Handling registers

When reading and writing data in the microcontroller, in many cases registers have to be accessed.

Like in AVRs registers have names (“abbreviations” or acronyms to say more precisely) also in STM32 MCUs. They are defined in a large header file you can find in the CMSIS-folder in your working environment. These acronyms often correspond 100% with the names in datasheet or reference manual. Sometimes they don’t. We will give hints when this is the case with a certain register.

Registers are part of an overlaying structure. For example the structure “General-purpose I/Os (GPIO)” contains register that are called “MODER” (mode register), “ODR” (output data register), “IDR” (input data register) an so on.

In C++ registers are accessed via the “->” operator:

GPIOA->MODER = 0x001 assigns the value of “1” to the mode register of port A. The “->” operator is equivalent to the “.” operator but that can not be used here.

All registers are declared and defined in the various header files and C-definitions for the respective MCU model.

Accessing registers by address

In the rare cases where no acronym exists and a register is not declared/defined in CMSIS file you have to access this location by its memory address. So this hint might be helpful.

Example: Reading flash memory size from its respective register:

In an STM32F411 flash memory size is stored as read-only value in 16-bit register with address 0x1FFF7A22. To read it you can use the following code:

uint16_t (*flashsize) = (uint16_t*)(0x1FFF7A22);

Explanation: A 16-bit pointer variable named *flashsize is declared and defined as the content of memory cell 0x1FFF7A22. The content of this memory cell then is read as a 16 bit variable.

(Source STM32F411 datasheet, p. 839)

Using I/O ports

The main difference basic concept for STM32 MCUs compared to AVRs is that the various units in the processor all have to be powered up separately because this saves energy consumed by the unused ports of the MCU. If you don’t use it, switch it off! Or, to say in more exactly: Don’t switch it on!

This concept at first might irritate the experienced AVR developer because a software will simply not work because you forgot to power up the respective port, module or whatever by accident.

Powering up a port

In the STM32F4 there are a number of registers that define the power and clocking status of the hardware. The registers that are needed here are called the RCC-register section (Reset and Clock Control). In the Reference manual you will find them beginning with page 161.

The register that is needed from this section if you want to power up an I/O port is called the AHB1ENR-Register which stands for APB1 peripheral clock enable register. APB1 is one of the bus systems of the MCU.

Hint: When you see an underscore “_” in the STM reference manual when describing register access, you will code the “->”-operator in your “real world” program. The “->”-operator in this example refers to a member called “AHB1ENR” of the “RCC” struct. An equivalent expression would be (*RCC).AHB1ENR. But as nobody does that we won’t do it either as mentioned before.

Register AHB1ENR is the register that is used to power up ceratin parts of the MCU. Among are all the GPIOs (ports) from A to K max. (depending on the MCU you have in use).

The instruction to power up port A for example can look like

RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

The bold “A” marks the port ID and the numeric constant defined in the .h-file for this MCU named

#include “stm32f4xx.h” in a subfolder of the “CMSIS”-folder that has been installed with the programming software.

Or the instruction can be more cryptic in the style of a bit shift operation:

RCC->AHB1ENR |= (1 << 0);

No matter which syntax you use, you set bit 0 of the register to “1” which enables the port (see register description above!).

With this instruction your port is powered up. Next you have to define the port’s function:

Tell the port what it is intended for

All ports are defined in the GPIO structure (General Purpose Input Output) of the MCU model. Every port has its own GPIO, beginning with GPIOA, GPIOB etc.

Port mode register (MODER)

With this register you can define a specific pin of port as:

  • Input
  • Output
  • Alternate Function
  • Analog mode

Which might be new to AVR users is the so called “alternate function” mode. Many ports, like in the AVR environment, have special functions besides their normal port functionality. We will talk about that topic later because it requires some deeper understanding.

Every port has a 32-bit register to define the function of every pin. The register used is called “MODER” (“mode register”). For port A it is called GPIOA->MODER e. g.:

 

Every pin, in contrast to AVRs, is defined by a 2-bit structure. The respective bit patterns are:

(Source: Internet, unknown)

If you want to set a respective pin to be set as output pin you must write “01b” to the respective bits. BTW: all register are initialized to 0 at start up.

Examples
GPIOA->MODER |= (1  <<  (3 << 1)); 

sets pin 3 as output. The inner set of parenthesis multiplies the pin bit (3) by 2 (due to the 2-bit structure) by shifting it on bit left (i. e. *2) and writes the value 1 into that position. This pin is configured as output now.

GPIOA->MODER |= (3  <<  (5 << 1)); 

sets pin 5 to analog mode (11b). Which function then exactly is meant has to defined later in a further step. The inner set of parenthesis multiplies the pin bit (5) by 2 again (due to the 2-bit structure) and then writes the value 3 (11b) into that position.

Hint: There also “magic words” for the definitions. You can sometimes see them in example code and find them in the .h-file called “stm32f4xx.h”. We will use register based bit shift operations here.

Defining electrical characteristics of an output port (PUPDR)

This step defines the way of how the port drivers are switched using the PUPDR (pull-up/pull-down-register). This is important when using a pin as input source. You have 3 options available:

00b is neither pull-up or down so the output runs free, 01b pulls the port up to VDD when set to “1”, 10b pulls it down to GND when set. Normally you can leave out this step.

Setting port speed (OSPEEDR)

As a beginner to STMs you don’t have to care too much about port speed and don’t need to define expressively. But here are the definitions for the various speed levels:

Because “low speed” is set with 00b you don’t have to define anything if you want to use the respective port with this speed.

Setting output driver type (OTYPER)

With the “output type register” your options are to select between Push-pull and open drain. Usually we use predefined state 00b.

Let’s sum up: If you intend to set port E pin 0 for example as output the following 2 lines of code are suffice:

//Turn on the GPIOE peripheral
RCC->AHB1ENR |= (1 << 0);

//Put pin PE0 in general purpose output mode
GPIOE->MODER |= (1 << 0);

If you wish to have it in full with all the options available, you might like this example:

//Turn on the GPIOE peripheral
RCC->AHB1ENR |= (1 << 4);

//Put pin PE0 in general purpose output mode (2 bits count!)
GPIOE->MODER |= (1 << (0 << 1));

//Put pin PE0 in general purpose output mode pull UP (2 bits count!)
GPIOE->PUPDR |= (1 << (0 << 1));

//Set speed to high (2 bits count!)
GPIOE->OSPEEDR |= (3 << (0 << 1));

//Set output type to push pull (1 bit count!)
GPIOE->OTYPER |= (0 << 1);

Using ports as output

Every port has two registers to control data flow: The “ODR” (out data register) and the “IDR” (in data register”). For setting a port we use the ODR:

As you can recognize this register has a width of 32 bits as well but only 16 bits (15:0) are used. This makes sense because every port has got 16 pins.

//Reset pin
GPIOE->ODR &= ~(1 << 0);

//Set pin
GPIOE->ODR |= (1 << 0);

This coding might look familiar to AVR programmers.

Full code example

To come to end of today’s lesson, a full example of setting ports with the STM32F4 can be found on my Github repo.

Using ports as input

The code sequence for reading a key looks like one mentioned below. PE4 will be selected as input. This code applies for the F407VGT board by “DIYmore”, for other hardware it is subject to adaption respectively.

//Pin PE4 must be set to 'input' mode with pull-up.
GPIOE->MODER &= ~(3 << (BTNPIN << 1)); //Set to 00b -> Input mode
GPIOE->PUPDR &= ~(3 << (BTNPIN << 1)); //Reset to 00b first 
GPIOE->PUPDR |= (1 << (BTNPIN << 1)); //Set to 01b -> Pull up

Decoding input pin is done as follows:

#define BTNPIN 15 //PD15
...
pin_input = ~(GPIOD->IDR); //"0" means "pressed"!
if(pin_input & (1 << BUTTON_PIN))
{
    GPIOE->ODR &= ~(1 << 0);
}
else
{ 
    GPIOE->ODR |= (1 << 0);
}

The full example can be downloaded here.

Have fun programming and thanks for attending this lesson!

Peter (DK7IH)

 

From AVR to STM32: An introduction to ‘Bare Metal’-programming the Arm-Cortex-M4(R) MCUs (Lesson 1: The Basics)

Abstract

This online course will provide the interested hobbyist software developer with the basic understanding of the steps that must be taken when migrating from the more or less “easy going”-oriented AVR 8-bit-MCU family to the more sophisticated STM32-ARM-Cortex devices. It is also designed for those who are coming from other platforms like PIC-controllers for example.

In this course we will focus on the STM32F4-family (ARM-Cortex-M4), a group of controllers that has been labeled by the manufacturer as “high performance” types.

During this lesson you will learn how to set up the developing environment, connect it to your hardware and do some basic port set up exercise to configure ports as out and input.

STM32 – What is it about?

The STM32 controllers are based on a microprocessor design developed by the “ARM” company. “ARM” is an abbreviation of the term “Acorn Risc Machines” which refers to the UK based ACORN microprocessor company that issued the first models of this architecture in the early 80s of the last century.

Like the AVRs these ARM processors have been designed as Reduces Instruction Set Computers. They process 32 bit in parallel, most of the registers are 32-bit wide but some also have 16-bit width. Ports have a bit widths of 16 bits. Clock rates are up to 168 MHz.

Another story: ARM itself does not produce chips but only issues a license for other companies developing their own processors based on the ARM model. Thus you can buy ARM processors from a wide range of manufacturers wheres STM is only one of them.

ARM processors offer a wider range of functions, are more complex and have more integrated functions than the AVR controllers. Thus programming these modules is not that easy compared to the ATMEL products. We will cover the basic hurdles to be taken by beginners.

Get Hardware

There are lots of various “Breakout”- or “Discovery”-Boards you can purchase. Many people use the original STM32F4 “Disco” board. Other affordable hardware comes from China based vendors where you can buy the STM32F407VET6 blackboard for example that contains extra flash memory, a socket for the NRF24L01 wireless module and some other features.

 

This board is definitely a good start to dive into the world of STM32F4.

If you prefer a smaller versions of hardware you can purchase the so-called “Black Pill“-boards that contain an STM411 MCU with fewer pins and some features missing (among them the DAC) that the larger units have on board.

ISP Programming

A known from the AVR world, ISP (in system programming) is also present with the STM32 MCUs. So, further on you need an ISP  programmer at least for most boards except from e. g. the STM original “Disco” board. The “Disco” has a compatible programming interface on board.

The required ST V2 Programmer device also is available via the internet.

All boards have 4 pins to connect the USB programmer device.

  • VDD (+3.3V)
  • GND
  • SWDIO
  • SWCLK

If you are searching where to connect your board if this hasn’t got a standard interface connector you can switch to the “Board” menu of STM32-Base Website.

The easiest way to “flash” your device with software is to use the SWDIO- (PA13) and SWCLK-Line (PA14). The programmer also is able to power your board but not necessarily the peripheral hardware in every case. USB power supply is limited to 0.5A (or 0.1 A in low power devices).

As programming software there is STM-Cube-Programmer available.

 

Software design: The 2 ways of programming: “Bare metal” vs. “HAL”

When you remember AVRs, you have two choices to produce software for these controllers:

  • Programming in Assembler, C or another language and writing all the necessary program code on your own, or
  • using the ARDUINO environment and taking advantage of a lot of predefined functions that make programming more easy.

In the STM-world you also have two ways similar to that. On one hand you can use a large library called “HAL” which stand for “hardware abstraction layer”. This decouples the MCU’s hardware from your programming environment. The software developer communicates with some sort of interface that provides lots of predefined functions for the daily programming routine. The code reads a little bit like “Arduino code”.

The other way is called “bare metal” programming where the software engineer has to create all the necessary functions for himself.

We will cover this way of programming because it gives you a better understanding of what is behind the curtain of the MCUs we use here. This method also is called “register programming”. It is the same type of coding that the non-Arduino developers know from the AVR-world when designing software in C or another language.

Choosing a programming environment

(A) Chose an editor

There are lots of integrated programming suites and IDEs (user interfaces) on the market. When you use a search engine (“stm32 programming environment”) you will find items like “KEIL”, “STM32CUBE”, “CooCox” etc. etc. You can use them, for sure.

But there is a much easier and leaner way. Choose your well-known programming editor you have used so far. This approach is the best idea because you are already familiar with. If you haven’t got one yet, the author’s recommendation is Geany.

(B) Install a software environment

With STM32F4 MCUs the process of generating an executable code that can be uploaded to the MCU involves a little bit more effort than in the AVR world. Follow the steps mentioned subsequently:

First, create a folder called “ARM” on your local drive. That will be your workspace.

Second download the GCC ARM Compiler suite from this website. Unpack and install into a new folder called “tools”. This will match with the naming used in the second step.

Third you need software that defines the working environment for you respective STM32 MCU.

New: The need for a “startup-file”

Before you can start your own software running in the MCU, above all, a piece of very special code is required. This code is a so called “Startup” file. It exists for a very simple reason: The memory structure of the various STM32 MCUs varies between the single models. Therefore the respective memory model has to be specified in software. This is done by a file that is called “Start-Up file”.

It defines the different sections of memory, interrupt vectors  etc. for the processor that your code can access. In addition it fixes the jump to a function called SystemInit() that is called before your main() function is accessed. In SystemInit() basic settings like the clocking of the MCU are defined. After that process is terminated, your main()-code will be executed.

Usually the Startup-file is an assembler file, sometimes it can also be a C-file. This file depends on the exact processor subtype you are using.

Get the whole environment at once

To avoid bothering with this and all the more or less complex prerequisites you can download a fully featured software package from STM32-Base website. This website also contains information about the “Discovery”- or “Breakout”-Boards and other hardware. Follow the instructions mentioned under the Setup site of that website. It will give you a full working environment to start with. By the end of the install procedure you must still add 2 symbolic links. Using Linux the command for this is “ln -s /full/path/STM32-base STM32-base” etc.

After this you will have installed a working programming environment and can start working with your own .c-file stored in the /templates/src folder named main.c.

If you are already sophisticated you can adapt the various makefiles by changing the source name and other settings. If you are not, leave everything as it is.

(C) Recommended reading

If you got used to avoiding intense reading manuals like when you lived in the AVR world: Forget it! As STM32 MCUs are much more complex you won’t get along by “try and error”. RTFM (read the f…. manuals!) is mandatory now.

You will need at least:

The STM32F4 datasheet which contains the basic features about the MCU you have in use (Collection of STM32F4 datasheets)

The STM32F4 Reference Manual that gives a detailed description of the various functions, the registers involved etc. Don’t worry. About 1700 pages are waiting to be explored. No worries, it sounds more complicated than it is!

Next lesson: Port configuration.

Have fun programming and thanks for attending this lesson!

Peter (DK7IH)