STM32 Course - redwolf-digital/ENE_Embedded_Lab GitHub Wiki

BEFORE START

Before starting, it is important to note that firmware development for most embedded devices is generally done using C or C++ and does not typically support high-level languages such as Python or Java. Therefore, learners are required to have a basic understanding of C and/or C++ beforehand. For those whose foundation is not strong enough, recommended learning resources can be found at: W3schools - C Tutorial

for about software, it is necessary to install the following programs:

and some hardware:

  • Any STM32 Develop board
  • ST-Link debugger V2 or above (optional)

Warning

debugger request if your develop board is not included or built-in debugger

How to install STM32CubeIDE

1. Prepare the installer

Download software from offical website

and download according to the OS you are using.

After accepting the terms of use, for those who do not have an ST account yet, please register first. You can use either your personal email or your university email.

After this, STMicroelectronics will send a download link for the program to the email you registered with.

Caution

MyST Account is required to download services within the STM32CubeIDE program. Therefore, you need to register for it.

2. Install

After opening the installation file, click Next continuously until you reach the installation location selection page. Choose the drive where you want to install the program.

Tip

However, based on my experience, I recommend installing it on Drive C to avoid driver issues. But if there is not enough space, it’s OK for other locations.

In the check box, make sure the ST-Link Driver option is selected. After that, you can click Install to proceed..

One more thing

After you open the STM32CubeIDE program, it will ask where you want to use the workspace to save your projects. By default, the workspace will be located at C:\Users\<YOUR_USER>\Documents\STMworkspace You can choose to change it or leave it as is. Once you've finished setting it up, click the Launch button. If you don't want this page to appear again, check the box labeled Use this as the default and do not ask again

About F411

ST Microelectronics is one of the companies that manufactures microcontrollers, similar to others like Microchip/Atmel (PIC, Atmega), NXP, Texas Instruments, or Analog Devices (DSP). It is widely popular in the industrial sector. ST also offers a variety of ARM-based controllers in different series, including general-purpose, high-performance, and low-power models.

In this class, we will focus on the STM32F411, an ARM-based microcontroller with a main CPU that features the Arm® Cortex®-M4 with Floating-Point Unit (FPU). When compared to the specifications of the Arduino Uno (Atmega328p), the comparison is as follows:

STM32F411 STM32F103 Arduino uno (Atmega328p)
Processor ARM Cortex-M4(32-bit) ARM Cortex-M3(32-bit) AVR RISC(8-bit)
Max clock speed 100 MHz 72 MHz 16 MHz
RAM 128 KB 20 KB 2 KB
FLASH 512 KB 64 KB 32 KB
FPU
DMA ✅ 12 channels ✅ 7 channels
UART/USART 4 5 1
SPI 3 2 1
I2C 3 2 1
Timers 14 11 3
ADC 1 Core (12 bit) 16 channels 2 Core (12 bit) 16 channels 1 Core (10 bit) 6 channels
GPIO 50 37 23
USB 1 OTG Full speed 1 Full speed
CAN 1 1
SDIO 1
RTC 1 1 1
Analog comparator 1
WDT 1 1 1
Crypto/CRC 1 1

Software over view

  1. tools bar
  2. Project explorer
  3. Text editer workspace
  4. Debug console

create first project

STEP 1:

File > New > STM32 Project

STEP 2:
After the program displays this pop-up, select the microcontroller model that matches the one you are using. In this case, it is the STM32F411ECU6. Enter this in the Commercial part number select and then click Next

Note

If you are not logged in, the program will prompt you to log in to download the services and libraries for the specific chip. Please log in to complete the process.

STEP 3:
In this step, name your project as desired and click Finish In this case, the project will be named FirstProject.

Note

If a pop-up regarding the terms of use appears, don't worry. Simply click "Accept" to proceed.

DONE

Clock structure and config

Before configuring the clock, we need to understand a bit about the internal structure of the chip. Refer to the stm32f411_datasheet on page 16 to get a general idea of how components inside the chip are connected to various buses. This understanding will make it easier to work with the chip. Every microcontroller (MCU) or microprocessor (MPU) includes such information, which can be found in the datasheet provided by the chip manufacturer.

Typically, the controllers or peripheral controllers within a chip are managed by a clock signal, which can originate from either an external or internal clock source, depending on your configuration. The complexity or functionality depends on the structure of the clock distribution system within the chip. For the STM32F411, this is referenced in the Reference Manual from the file stm32f411_ReferenceManual_RM0383 or RM0383, specifically on page 93.

You'll notice that the clock distribution system is filled with a complex network of connections. If you were to manually control it using registers, this page would be critical as it specifies which bits need to be set in the registers. However, to simplify the process and reduce the complexity, the STM32CubeIDE program provides a menu for configuring and calculating the values for PLL Config automatically, making it much easier to work with.

Before moving to the next step, there’s one more important thing to understand the schematic diagram of the board you're working with. Whether it's a custom-designed board or a purchased one, the schematic or block diagram of the system is always essential. It helps us understand what components are connected within the system.

As you can see, there are two oscillators, Y1 and Y2, connected to the system. Y1 is connected to PC14 and PC15 with a frequency of 32.768 kHz, acting as a Low-Speed Clock (LSE) for the internal Real Time Clock (RTC) module used for timekeeping. Meanwhile, Y2 is a High-Speed Clock (HSE) with a frequency of 25 MHz, connected to PH0 and PH1.

STEP1
Categories > RCC > High Speed Clock(HSE) > Crystal/Ceramic Resonator

STEP2
Categories > SYS > Debug > Serial Wire

Warning

In the SYS configuration, if you don't enable Debug, you won't be able to test your code. In some worst-case scenarios, you might lose the ability to reprogram chip. Fixing this issue could require special tools and software, ensure that Debug is enabled. However, if you’re creating a product for sale or only need to program the chip only one time, you can disable this feature.

STEP3

  1. Verify that the input frequency is set to 25 MHz according to the schematic.
  2. In the PLL Source Mux, select HSE to use the external oscillator.
  3. In the System Clock Mux, select PLLCLK as the system clock.
  4. HCLK is the main clock for the CPU and the system, and in this case, it will be set to the maximum frequency of 100 MHz.
  5. After pressing Enter, the system will automatically calculate the clock signal values for various components.

STEP4
After completing the configuration, click the Device Configuration Tool Code Generation button on the toolbar to generate the basic draft code along with the configuration code you have set. The code generated by the program is called the skeleton code. From here, you will need to write additional functions yourself to implement your desired features.

Your code will be located at :

[YOUR_PROJECT] > Core > Src > main.c

Src : Contains the source files, including the main program file (main.c) and any auto-generated initialization functions.
Inc : Contains the header files, where function prototypes and configurations are declared.

DONE

Note

For those having issues with code generation, try logging in first by ?navigating to:
Help > STM32Cube updates > Connection to myST
Then, log in using the account you previously registered.
If the login fails, try again or switch to a different internet connection.

About HAL

HAL (Hardware Abstraction Layer) is a library developed by STMicroelectronics to simplify firmware development for STM32 microcontrollers. HAL acts as an intermediate layer between hardware and user software, making it easier to write portable and maintainable code.

HAL VS Low Level (LL)

Feature HAL LL
Ease of use
High performance
Readable and maintainable code
Low power consumption
Memory Usage More due to library overhead Less, as it avoids unnecessary abstraction
Flexibility Good for general applications Best for performance-critical applications

GPIO

GPIO (General-Purpose Input/Output) refers to the input and output pins that can be configured to operate in various modes on the STM32F411EC microcontroller. These pins allow efficient control of external devices such as LEDs, buttons, and various modules. The STM32F411EC GPIO can operate in four main modes:

  • Input Mode – Used to receive external signals
  • Output Mode – Used to send signals to external devices
  • Alternate Function Mode – Used for peripheral communication such as UART, SPI, I2C
  • Analog Mode – Used for ADC (Analog-to-Digital Converter)

Caution

STM32, MCU, or even sensors nowadays mostly operate at a 3.3V voltage level. Although some datasheets state that certain GPIOs are 5V tolerant, I recommend using logic levels that do not exceed 3.3V to prevent potential issues. If the peripheral device or module operates at 5V logic, a logic level converter should be used to adjust the voltage levels accordingly.

GPIO Input

Basic read GPIO example.

REGISTER based

This function initializes a GPIO pin, but if you've already set up GPIOs in your system initialization, you may skip this step. However, it's necessary if you haven't configured it initially or are using a different development environment such as Keil.

From the Reference Manual RM0383, section 8.3.9, on page 152, we can get a general idea of the necessary configurations. The basic setup can be done as follows:

Step 1: Enable the GPIOx clock  
Step 2: Configure GPIOxn as an input  
Step 3: Operate the GPIO  

Example : Set PB1 as input

int  count;

void GPIOB_Config(void) {
    // step 1 : enable GPIOB clock
    RCC -> AHB1ENR |= (0x01 << 1);
    // step 2 : Config PB1 as input
    GPIOB -> MODER &= ~(0x03 << (1 * 2)); 
}

unsigned char Read_PB1(void) {
    return (GPIOB -> IDR & (0x01 << 1)) ? 1 : 0;
}

int main(void) {

    GPIOB_Config();

    while(1) {

        if(Read_PB1() == 1) {
            count++;
        }

    }
}

Step 1
Since GPIO requires a clock for signal sampling, we need to enable the clock for GPIO. GPIOB is connected to the AHB1 bus, so we must refer to the section on Clock Configuration, specifically on page 117, section 6.3.9, RCC AHB1 peripheral clock enable register (RCC_AHB1ENR).
so we will set bit 1 to 1

Step 2
After that, set the mode of the GPIO as needed.

Step 3
Read the status of the GPIO by accessing the IDR Register.

Step 4 Upload and debug
In the toolbar, if you click "build" (the hammer icon) and there are no errors, click "Debug" (the green bug icon) and wait for the program to upload to the microcontroller.

Step 5
After that, add the variable count in Live expressions and press the Resume button in the toolbar or F8 to start debugging.

circuit in test

HAL based

In <project name>.ioc, in Pinout view, click on pin PB1, select input, and then click Device Configuration Tool Code Generation.

Example : Read PB1

int count;

int main(void) {
    
    while(1) {
        if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == 1) {
            count++;
	    }
    }
}

debug and try again.

GPIO Input pull-up/pull-down

From the previous section, we can see that the system is unstable due to the floating input, which makes it sensitive to external noise and the parasitic capacitance within the CMOS circuit. This causes the system to be unable to determine whether the signal is 1 or 0, leading to unexpected behavior.
Enabling Pull-up or Pull-down helps prevent the input from floating, making the input pin more stable. The Pull-up or Pull-down circuit can be implemented using an external resistor or by enabling the internal resistor in the microcontroller.

According to section 8.4.4, on page 158, set bits 3:2 in the GPIOx_PUPDR register to 0x01 to enable the internal pull-up resistor.

REGISTER based

Example : Set PB1 as input with enable internal pull-up resistor

int  count;

void GPIOB_Config(void) {
    // step 1 : enable GPIOB clock
    RCC -> AHB1ENR |= (0x01 << 1);
    // step 2 : Config PB1 as input
    GPIOB -> MODER &= ~(0x03 << (1 * 2)); 
    // Optional: Enable pull-up or pull-down if needed
    GPIOB->PUPDR |= (0x01 << (1 * 2));
}

unsigned char Read_PB1(void) {
    return (GPIOB -> IDR & (1 << 1)) ? 1 : 0;
}

int main(void) {

    GPIOB_Config();

    while(1) {

        if(Read_PB1() == 0) {
            count++;
        }

    }
}

debug and try again.

HAL based

In <project name>.ioc, in System core > GPIO, in configuration options GPIO Pull-up/Pull-down > Pull-up, and then click Device Configuration Tool Code Generation.

Example : Read PB1 with enable internal pull-up resistor

int count;

int main(void) {
    
    while(1) {
        if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == 0) {
            count++;
	    }
    }
}

debug and try again.

GPIO Input Interrupt

Input Interrupt is a mechanism in microcontrollers that allows the CPU to respond to external inputs without continuously looping to check the input status (Polling). It can trigger a special function to handle the event immediately without waiting for the loop cycle. This helps reduce power consumption in some chips, such as when receiving input from a user/sensor or waking up the MCU from Sleep mode.

REGISTER based

Example : Read PB1 interrupt with enable internal pull-up resistor and detect only falling signal

void EXTI1_IRQHandler(void) {
	if(EXTI -> PR & EXTI_PR_PR1) {
		// Clear the pending bit (In this example, macros are used instead of bitwise operations.)
		EXTI -> PR |= EXTI_PR_PR1;

		// Your code begin here
		count++;
	}
}

void GPIOB_Config(void) {
    // step 1 : enable GPIOB clock
    RCC -> AHB1ENR |= (0x01 << 1);
    // step 2 : Config PB1 as input
    GPIOB -> MODER &= ~(0x03 << (1 * 2));
    // step 3 Optional: Enable pull-up or pull-down if needed
    GPIOB -> PUPDR |= (0x01 << (1 * 2));

    // step 4 : Enable SYSCFG clock for EXTI
    RCC -> APB2ENR |= (0x01 << (1* 14));
    // step 5 : Map EXTI1 to PB1
    SYSCFG -> EXTICR[0] &= ~(0x01 << (1 * 4));	// Clear EXTI1 config
    SYSCFG -> EXTICR[0] |= (0x01 << (1 * 4));	// Set EXTI1 to PB1
    // step 6 : Enable EXTI1 and set detection type
    EXTI -> IMR |= (0x01 << (1 * 1)); 	// Enable EXTI1 interrupt
    EXTI -> FTSR |= (0x01 << (1 * 1));	// Enable falling edge
    // step 7 : Call NVIC IRQ
    NVIC_EnableIRQ(EXTI1_IRQn);		    // Enable EXTI1 interrupt in NVIC
    NVIC_SetPriority(EXTI1_IRQn, 0);    // Set priority (Optional)
}

int main(void) {

    GPIOB_Config();

    while(1){

    }
}

Step 1 to 3 : are the same as the previous steps.

Step 4 :
Since EXTI is connected to the APB2 bus, similar to GPIO, it is necessary to enable the clock for EXTI as well. This can be done through the RCC Register.

Step 5 :
Connect PB1 to EXTI1 by using SYSCFG_EXTICR[0].

Step 6 :
Enable the interrupt by using the interrupt mask and choose the type of detection you want. Both edge types can be enabled, but in this case, only the Falling edge will be enabled.

Step 7 :
Call the NVIC API function and set the priority (though it’s optional, setting the priority is recommended in cases where the interrupt priority is important for certain tasks).

HAL based

Step 1 :
Select PB1 as GPIO_EXTI1.

Step 2 :
In the GPIO Configuration, set the GPIO Mode to External Interrupt Mode with Falling Edge Trigger Detection and set GPIO Pull-up/Pull-down to Pull-up.

Step 3 :
In the NVIC options, enable the EXTI line1 interrupt to activate the interrupt.

And don't forget to check the Code generation section. Once everything is set up correctly, you can proceed to generate the code.

Code Example :

int count;

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    // Your code for interrupt event begin here
	count++;
}


int main(void) {

    while(1) {

    }
}

GPIO Output

Since GPIO (General-Purpose Input/Output), as its name suggests, has both input and output, its output is driven by P-MOS and N-MOS transistors, as shown in the GPIO structure diagram. The output configuration can be set in two modes:

  1. Push-Pull Mode
  2. Open-Drain Mode

REGISTER based

setup step

1. Enable GPIO Clock
2. Configure GPIO Mode
3. Set Output Type
4. Configure Output Speed
5. Config Pull-up/Pull-down (if needed)
6. Write Output Data

Code Example :

void GPIOC_Config(void) {
	// Enable GPIOC Clock
	RCC -> AHB1ENR |= (0x01 << 2);
	// Set PC13 as output
	GPIOC -> MODER &= ~(0x03 << 26); 	// Clear bit 27:26 bit
	GPIOC -> MODER |= (0x01 << 26);		// Set bit 27:26 bit to b01
	// Set output type
	GPIOC -> OTYPER &= ~(0x01 << 13);
	// Set GPIO Speed (optional)
	GPIOC -> OSPEEDR &= ~(0x03 << 26);
	// Disable pull-up/pull-down
	GPIOC -> PUPDR &= ~(0x03 << 26);
}

int main(void) {
    GPIOC_Config();

    while(1) {
        GPIOC -> ODR ^= (1 << 13);
        for(uint32_t i=0; i<=10000000; i++) {
            __NOP();
        }
    }
}

Step 1 :
Enable the clock for GPIOC.

Step 2 :
Configure the mode using the MODER register.

Step 3 :
Set the output type using the OTYPER register.

Step 4 :
Set the GPIO speed using the OSPEEDR register. The GPIO speed affects signal overshoot during state transitions—higher speed increases overshoot, which may impact certain devices.

Step 5 :
Disable Pull-up/Pull-down using the PUPDR register, as the system does not require it.

Step 6 :
Control the GPIO state (ON/OFF) using the ODR register.

HAL based

Step 1 :
Select PC13 as GPIO_Output.

Step 2 :
In the GPIO Configuration, set the GPIO output level to Low, GPIO Mode to Output Push Pull, GPIO Pull-up/Pull-down to No pull-up and no pull-down and set Maximum output speed to Low

Code Example :

int main(void) {

    while(1) {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
        HAL_Delay(500);	// Delay 500ms
    }
}

Multiple GPIO Output control in one time

Sometimes, it is necessary to control multiple GPIO pins simultaneously to reduce signal skew or achieve higher speed. This is useful for applications such as:

  • 7-Segment Display Control
  • Parallel Data Transmission (e.g., LCD, ROM, RAM, etc.)

REGISTER based

void GPIOA_Config(void) {
	// Enable GPIOC Clock
	RCC -> AHB1ENR |= (0x01 << 0);
	// Set PA1,2,3,4,5 as output
	GPIOA -> MODER &= ~(0xFFC); 	// Clear bit 11:2 bit
	GPIOA -> MODER |= (0x554);		// Set bit 11:2 bit to b01
	// Set output type
	GPIOA -> OTYPER &= ~(0x3E);     // Set bit 5:1
	// Set GPIO Speed (optional)
	GPIOA -> OSPEEDR &= ~(0xFFC);   
	// Disable pull-up/pull-down
	GPIOA -> PUPDR &= ~(0xFFC);
}

void basic_Delay(void) {
	for(uint32_t i=0; i<=10000000; i++) {
		__NOP();
	}
}


int main(void) {
    GPIOA_Config();

    while(1) {
        // Set PA1,2,3,4,5 to high
        GPIOA -> ODR |= (0x1F << 1);
        basic_Delay();
        // Set PA3 to low
        GPIOA -> ODR &= ~(0x01 << 3);
        basic_Delay();
        // toggle PA1,2,3,4,5
        GPIOA -> ODR ^= (0x1F << 1);
        basic_Delay();
        // toggle PA PA1,2,3,4,5
        GPIOA -> ODR ^= (0x1F << 1);
        basic_Delay();
    }
}

REGISTER based

Configure PA1, PA2, PA3, PA4, and PA5 as output according to the previous HAL configuration steps, then proceed to generate the code.

HAL based

int main(void) {

    while(1) {
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5, SET);
        HAL_Delay(500);

        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, RESET);
        HAL_Delay(500);

        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5);
        HAL_Delay(1000);
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5);
        HAL_Delay(1000);
    }
}

circuit in test

TIMER

The TIMER module in the microcontroller is a critical feature used to manage time-based operations within the system. It allows precise control over various time-related actions, such as generating PWM signals, measuring time intervals, creating delays, and capturing events.

Key Features

High Performance : The timers in STM32F411CE support up to 32-bit resolution, allowing for precise time measurement and flexibility in handling long time intervals.

Multiple Operating Modes : STM32F411CE supports various timer modes, such as :

  • Basic Timer : Used for simple time delays and event counting.
  • PWM Mode : For generating PWM signals to control external devices like motors.
  • Input Capture : Captures external signal edges to measure frequency or period.

Power-Efficient Operation : The timers can operate in Sleep or Standby modes to save power while still performing essential timing tasks.

Just timer

The Basic Timer in the microcontroller is a simple designed for basic time management tasks. It is ideal for generating precise time delays or counting events without the need for complex configurations. Basic Timers are particularly useful in applications where simplicity and efficiency are required.

HAL based

In this experiment, an example will be given by setting the timer to 5 microseconds, calculated as follows.

[ Example ] Set timer to 5 uS. 

Frequency for 5 uS is 
Frequency = 1/T 
Frequency = 1/(5*10^-6) = 200000 -> 200kHz

Prescaler = (System Clk / Timer target in Hz) - 1
Prescaler = (100 MHz / 200 kHz) - 1
          = ((100*10^6)/(200*10^3)) - 1
Prescaler = 499
TIM3 > check Internal Clock option
set :
    Prescaler        -> 500-1
    Counter Priode   -> 1

Prescaler is a frequency divider of the clock input of the timer, which is useful for increasing or decreasing the timing interval.
Counter Priode Counter Period or Auto Reload is the maximum value before the counter resets itself, and its value increases when the timer completes one cycle.

int main(void) {
    
    HAL_TIM_Base_Start(&htim2);

    while(1) {
        if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) == SET) {
            __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);

            counter++;

            if(counter == 200000) {

                HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);

                counter_Sec++;
                counter = 0;
            }
        }
    }
}

Timer interrupt

This method for setting up the timer’s time will be done the same as the previous steps, with the following additional steps.

Step 1:

Step 2:

Step 3:

The code will be as follows.

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim) {
	if(htim -> Instance == TIM2) {
		// Your code start here
	}
}

void main() {
    
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    
    MX_TIM2_Init();

    // Add for start timer interrupt
    HAL_TIM_Base_Start_IT(&htim2);

}

UART

UART (Universal Asynchronous Receiver-Transmitter) is one of the most commonly used serial communication protocols in embedded systems, microcontrollers, and various communication modules such as Bluetooth, GPS, and Wi-Fi modules.

How UART Works
UART converts data between Parallel Data (inside the microcontroller) and Serial Data (transmitted to external devices) without using a shared clock signal (asynchronous). Data is sent one bit at a time, starting with a Start Bit, followed by Data Bits, an optional Parity Bit for error checking, and ending with one or more Stop Bits to indicate the end of the transmission.

Baud Rate (Communication Speed)
The baud rate defines the communication speed, measured in bits per second (bps), such as 9600, 115200, etc. Both transmitter and receiver must be configured with the same baud rate to communicate correctly.

Limitations of UART

  • Point-to-point communication only (between two devices)
  • Limited communication distance (due to electrical signal degradation)
  • Both devices must use the same baud rate settings

Setup project

In the example, data will be transmitted and received via UART1 using the polling method.

Categories > Connectivity > USART1
Mode > Asynchronous

Baud Rate > 115200 Bits/s
Word Length > 8 bits (include Parity)
Parity > None
Stop bits > 1 

Basic Tx data

#include "main.h"

uint8_t TxData[] = "HELLO WORLD\n";

UART_HandleTypeDef huart1;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);

int main(void) {

    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();

    while(1) {
        // Tx data
        HAL_UART_Transmit(&huart1, TxData, sizeof(TxData)-1, 1000);
	    HAL_Delay(250);
    }
}

result

Rx data

For receiving UART data, the function HAL_UART_Receive(huart, pData, Size, Timeout) can be used. However, in many cases, using the polling method is not very efficient, as it requires looping the code to wait for incoming data. Often, data arrives randomly, and if the data is not read in time, it may be lost. Therefore, in this lab, the interrupt method will be introduced to allow more efficient data reception in the future.

Each time data arrives, the MCU will jump to read the data and store it in the buffer. Therefore, we don’t have to worry about missing the data. However, the downside is that if too much data comes in, it can create a heavy load on the MCU.

Setting project

Example Code

#include "main.h"

uint8_t TxData[] = "HELLO WORLD\n";
uint8_t RxData[sizeof(TxData)];

UART_HandleTypeDef huart1;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);


void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
	HAL_UART_Receive_IT(&huart1, RxData, sizeof(TxData));
}


int main(void) {

    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();


    // Start Rx data
    HAL_UART_Receive_IT(&huart1, RxData, sizeof(TxData));


    while(1) {
        HAL_UART_Transmit(&huart1, TxData, sizeof(TxData)-1, 1000);
	    HAL_Delay(250);
    } 
}

result

In this case, we already know the length of the message, so this method works well. However, in many situations, the length of the incoming data is unknown, such as data from GPS modules, ADS-B, or various control devices. Therefore, the method for receiving data with unknown length will be covered in the STM32 Extra section for those who may need to use it.

Q/A : Many people may wonder why we need to use HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart).
The reason is that HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) is the function called by the HAL every time data is received. However, handling the data whether processing it, storing it in a buffer, or forwarding it elsewhere is entirely up to us. So, you can think of HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) as saying:
"The data has arrived, now what do you want to do with it?"

UART over USB

UART over USB, also known as Virtual COM Port, is another method of communication using USB hardware. Normally, when UART is used to communicate between a computer and a device, a converter such as a USB to TTL chip like FTDI232 or CP2102 is required to translate the data. However, some STM32 models natively support USB and offer various communication options, eliminating the need for additional hardware converters like in other chipsets.

Setup the project

Step 1 :

Categories > Connectivity > USB_OTG_FS

Mode > Device only

Step 2 :

Middleware > USB_DEVICE

Class For FS IP > Communication Device class (Virtual Port Com)
PRODUCT_STRING > "Name device up to you"

Sometimes, using USB hardware can limit the available main clock frequencies because USB requires a 48 MHz clock to function properly. This can cause issues with the PLL (Phase-Locked Loop) not being able to compute the frequency correctly. As a basic recommendation, using a 72 MHz clock 👍 is suggested for simplicity and to avoid any complications.

After generating the code, try debugging. If you connect the USB to the computer, you should see the USB Serial Device in the Device Manager. The COM port number will depend on the computer you're using.

Start Tx

Example Code

#include "main.h"
#include "usb_device.h"

#include "usbd_cdc_if.h"
#include "string.h"


char *data = "HELLO WORLD\n";


void SystemClock_Config(void);
static void MX_GPIO_Init(void);

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USB_DEVICE_Init();

    while(1) {


        CDC_Transmit_FS((uint8_t *)data, strlen(data));
	    HAL_Delay(1000);


    }
}

result

To view the data, you can use a Serial Monitor or Serial Debug program. In this case, the program CoolTerm will be used, but you can also use PUTTY as an alternative.

Note

You will notice that when using Virtual COM Port, the baud rate can be set to any value. This is because it is a simulation, not a true hardware UART. As a result, there is no need for hardware synchronization, allowing you to use any baud rate without issues.

Start Rx

the function CDC_Receive_FS is a static function and hence can not be used outside that file.
We want to do something simple with a function. In this example, it will be an Echo, where whatever you type will be sent back.

Step 1:
In main.c, create a global buffer to store the received data. The size of the buffer will directly affect the memory space used on the chip.

#include "main.h"
#include "usb_device.h"

#include "usbd_cdc_if.h"
#include "string.h"


// Create global buffer
uint8_t USB_RxBuff[64];


void SystemClock_Config(void);
static void MX_GPIO_Init(void);

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USB_DEVICE_Init();

    while(1) {

    }
}

Step 2 :

Project > USB_DEVICE > APP > usbd_cdc.if.c

In this step, it will be in the file usbd_cdc_if.c and will use the extern Buffer that was created earlier.

Tip

extern is used to tell the compiler that a variable or function declared in the program will be defined (or declared) in other files that are not in the current file, allowing the use of variables or functions declared in other files.

In lines approximately 51-53, write it as follows:

extern uint8_t USB_RxBuff[64];

Make a small modification to the CDC_Receive_FS function as follows:

static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len){
  /* USER CODE BEGIN 6 */
  USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
  USBD_CDC_ReceivePacket(&hUsbDeviceFS);


  // modify code begin
  memset (USB_RxBuff, 0x00, sizeof(USB_RxBuff));  // clear the buffer
  uint8_t len = (uint8_t)*Len;
  memcpy(USB_RxBuff, Buf, len);  // copy the data to the buffer
  CDC_Transmit_FS(USB_RxBuff, strlen((char *)USB_RxBuff)); // send data in buffer to VCP
  memset(Buf, 0x00, len);   // clear the Buf also


  return (USBD_OK);
  /* USER CODE END 6 */
}

result
When you type any text, it should return that text to the terminal.

Analog to Digital (ADC)

ADC stands for Analog-to-Digital Converter. It is a circuit or device that converts an analog signal (a continuous signal like voltage) into a digital signal (a number that can be processed by a microcontroller or computer).

conversion equation

$$V = \frac{\text{ADC}_{\text{value}}}{2^n - 1} \times V_{\text{ref}}$$

Where:

  • V = Calculated voltage
  • ADC_value = Value read from the ADC
  • n = ADC resolution in bits (e.g., 10-bit, 12-bit, etc.)
  • V_ref = Reference voltage

Setup

In this example, the operation is based on polling and continuous conversion mode.

Analog > ADC1 > IN0(PA0) > continuous conversion mode [Enable]

Code

#include "main.h"


uint16_t ADC_READ = 0;


ADC_HandleTypeDef hadc1;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_ADC1_Init(void);

int main(void) {

    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_ADC1_Init();

    HAL_ADC_Start(&hadc1);

    while (1) {


        HAL_ADC_Start(&hadc1);
        HAL_ADC_PollForConversion(&hadc1, 1);
        ADC_READ = HAL_ADC_GetValue(&hadc1);
        HAL_Delay(1);


    }
}

PWM

PWM stands for Pulse Width Modulation. It is a technique used to control power delivery or simulate analog output using a digital signal that switches between ON and OFF states.

PWM generates a square wave signal with a constant frequency but varies the amount of time the signal stays ON, known as the duty cycle.

Duty Cycle (%) The percentage of one cycle in which the signal is ON.

Examples:

0% → Always OFF
50% → ON half the time
100% → Always ON

equation

equation 1 :

$$\text{TIM\_CLK} = \frac{\text{APB\_TIM}}{\text{Prescaler}}$$

equation 2 :

$$\text{Frequency} = \frac{\text{TIM\_CLK}}{\text{ARR}}$$

equation 3 :

$$\text{DUTY\%} = \frac{\text{CCRx}}{\text{ARR}} \times 100$$

in this examples PWM frequency is 24kHz and input data is 12bit and APB_TIM clock is 100MHz
Step 1:

$$\text{Frequency} = \frac{\text{TIM\_CLK}}{\text{ARR}}$$ $$24\,\text{kHz} = \frac{\text{TIM\_CLK}}{2^{12}}$$ $$\text{TIM\_CLK} = 98.304\ \text{MHz}$$

Step 2 :

$$\text{TIM\_CLK} = \frac{\text{APB\_TIM}}{\text{Prescaler}}$$ $$98.304\ \text{MHz} = \frac{100\ \text{MHz}}{\text{Prescaler}}$$ $$\text{Prescaler} = 1.017$$

summary

Prescaler = 1
ARR = 4096

Setup

#include "main.h"


uint16_t PWM_VALUE = 0;


TIM_HandleTypeDef htim1;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_TIM1_Init(void);

int main(void) {

    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM1_Init();

    while(1) {
        
        TIM1 -> CCR1 = (PWM_VALUE << 4);
	    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);

    }
}

Since the input data is 12-bit while the CCR1 is 16-bit, it needs to be shifted left by 4 bits to fill the MSB.

⚠️ **GitHub.com Fallback** ⚠️