Servo Motor Control with a Potentiometer using the STM32F407 Discovery Kit

Servo Motor Control with a Potentiometer using the STM32F407 Discovery Kit

·

11 min read

Introduction

Hi! đŸ˜„Today our aim will be to use the ADC peripheral available on the STM32F407 Discovery Kit. It is used to convert the analog value given out by a sensor to digital values into a specific resolution (12-bit, 8-bit, 10-bit, etc.) configured by the programmer. We will continue our tradition and use Bare Metal Programming to achieve this task as well. A potentiometer will act as our source of analog values to be converted, whose output will be connected to a GPIO pin on our development kit, through which we will process the data. But, we will not just be using an ADC in this project, a servo motor (sg90) will also be used to show how the output from the ADC can be mapped to other devices and how those values can be used to control them.

Working Principle

  • ADC and Potentiometer

image.png

The potentiometer is basically a variable resistor. It has a total of 3 Terminals, where the outermost terminals need to be connected to VCC and GND, and the terminal in the middle will give us our analog output. It consists of a knob that is turned to control the resistance value of the potentiometer, this value of resistance will, in turn, affect the voltage drop across it, and that voltage will act as our analog value which needs to be converted into a digital form.

Now let's talk about the ADC. The STM32F407 Discovery Kit consists of 3 ADCs, each of these has 16 Independent Channels which can convert various analog inputs into their corresponding digital values. All of these ADC are successive approximation type and can be configured in 12-bit, 10-bit, 8-bit, and 6-bit resolution modes. The conversion takes place based on the following formula,

$$ D = (V_{Ref})/((2^n) -1) $$

Where,

n - The resolution of the ADC

\(V_{Ref}\) - The reference voltage for the ADC

D - The Digital Level or Value

The Digital Value obtained above lies in a specific range that depends on the resolution of the ADC. For example, in our case, we will be using ADC1 in 12-bit resolution mode, so the Digital Result will lie in the range of 0 to 4095 which is determined by \(2^n - 1\).

Another important factor of the ADC is its sampling rate. The sampling frequency always needs to be greater than twice the message frequency to accurately capture and convert the analog signal, according to the Nyquist Criteria. This factor determines how many samples are taken per second, which in turn will affect both the speed and accuracy of the conversion.

  • Servo Motor

130080296-e2cd51da-600a-4584-85c7-ff7283a5eb2a.png

Here, I will be using the sg90 servo motor, which has a 180 degree range of motion. We can control the position of this motor by sending the PWM pulses to the control signal pin, which is shown in the figure above. The VCC and GND are connected to the 5V and GND of the microcontroller respectively. For detailed connections, please visit my GitHub repo, by clicking here.

As seen in the figure below, the time period of the pulse to be generated needs to be 20ms, or in other words, it must have a frequency of 50 Hz. Out of that period, the T on (The time duration for which the state must be HIGH) should be 1ms for moving the motor to the 0 degree position and 2ms for moving the motor to the 180 degrees position.

130080344-4e280f41-d8cb-43d3-a9fe-013cfe64d3b6.png

Note: While testing the motor, I could not achieve the full range of its motion by sending pulses with pulse widths between 1ms and 2ms. So, by trial and error, I figured out the actual range of motion can be achieved by sending pulses with pulse-widths between 0.5ms and 2.5ms for the 0 degree and 180 degrees position respectively. This is what I have used in my code as well, as you will see in the latter part of this blog.

Components Required

  • STM32F4 based microcontroller

  • An sg90 servo motor

  • A potentiometer

  • A few Jumper wires

After gathering all the aforementioned components we are ready to start programming our microcontroller đŸ˜€.

Programming the MCU

I have used bare-metal programming for the coding, which involves Bitwise AND/OR/XOR operations on the SFRs (Special Function Registers) present on the MCU.

1. Including the necessary Header Files

#include "stm32f4xx.h"
#define ARM_MATH_CM4

These are the header files included at the top of our main.c file. The stm32f4xx.h is the one for our microcontroller and the ARM_MATH_CM4 is for any math operations that we perform in our code.

2. Declaring the User-Defined Functions and Variables

void GPIO_Init(void);
void ADC1_Init(void);
void TIM2_Init(void);
void TIM4_ms_Delay(uint32_t delay);
int u;

The GPIO_Init(void) function is used to configure all the GPIO pins that we will require to use the ADC and control our servo motor, which will be PC0 and PA5 respectively. Then in the ADC1_Init(void) function, we configure the various parameters of the ADC and initialize it. After that, the TIM2_Init(void) function is used to generate the PWM pulses that will be used for the servo motor control. Lastly, the TIM4_ms_Delay(uint32_t delay) is used to provide a delay in milliseconds as the name suggests, for the actuation of the servo motor after it has been instructed to move. The int u variable will store the digital values given out by our ADC1 which will be mapped to the pulse width of the PWM signal that has to be used to control the servo motor.

3. Defining the User-Defined Functions

  • First, let's take a look at GPIO_Init(),
void GPIO_Init(){
    //PC0 is connected to ADC1 IN10
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN;// Enable GPIOC clock
    GPIOC->MODER |= GPIO_MODER_MODER0; // Set PC0 to Analog mode

    RCC->AHB1ENR |= 1; //Enable GPIOA clock
    GPIOA->AFR[0] |= 0x00100000; // Select the PA5 pin in alternate function mode
    GPIOA->MODER |= 0x00000800; //Set the PA5 pin alternate function
}

Capture.PNG

Capture3.PNG

Screenshot 2021-09-20 165818.png

Screenshot 2021-09-20 170228.png

Here, we first enable the clock signal for GPIO Port C by setting the GPIOCEN bit to 1 in the RCC->AHB1ENR register. Then we set PC0 to Analog mode by writing 11 to the MODER0[1:0] bits in the GPIOC->MODER register. We are going to use PC0 for our analog input pin since it is connected to ADC1 IN10 that is channel 10 of ADC1 on our MCU.

Then, for our PWM generation, we configure PA5 in Alternate Function Mode, because one of its alternate functions is Channel 1 of Timer 2. So, we enable the clock signal for GPIO Port A by setting the GPIOAEN bit to 1 in the RCC->AHB1ENR register. After that, we set PA5 in alternate function mode by writing 10 to the MODER5[1:0] bits in the GPIOA->MODER register. Then we write 0001 to AFRL5[3:0] bits in the GPIOA->AFR[0] register. The pictures describing these registers are given above.

  • Now, comes the ADC1_Init() function,
void ADC1_Init(){
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;//Enable ADC clock
    //APB2 clock is not divided so it runs at 16MHz
    ADC->CCR |= 0<<16; // PCLK2 divide by 2, so the clock frequency is 8MHz
    ADC1->CR2 |= ADC_CR2_CONT; // Setting the Continuous Conversion Mode
    ADC1->CR2 |= ADC_CR2_ADON; // Enabling the ADC
    ADC1->CR2 &= ~ADC_CR2_ALIGN; //setting the alignment to the right
    ADC1->CR1 &= ~ADC_CR1_RES; // Selecting 12-bit resolution
    ADC1->SMPR1 |= (ADC_SMPR2_SMP0_0 | ADC_SMPR2_SMP0_2); // 112 Sampling cycle selection
    ADC1->SQR3 |= 10<<0; // Selecting channel 10
}

image.png

image.png

image.png

image.png

image.png

image.png

image.png

Here, we first enable the clock for ADC1 by writing a 1 to the ADC1EN bit in the RCC->APB2ENR register, and since we don't divide the APB2 clock it will work at 16MHz. Then we start configuring the ADC. For that, first, we divide the PCLK2 by 2 so that, the ADC1 will run at 8MHz. This is done by writing a 0 to the ADCPRE bits in the ADC->CCR register, which signifies clock division by 2. Then we set the Continuous Conversion Mode for ADC1 by writing a 1 to the CONT bit in the ADC1->CR2 register. After that, we Enable the ADC and align our data to the right by writing a 1 to the ADON bit and a 0 to the ALIGN bit in the ADC1->CR2 register respectively. For selecting our ADC resolution we write a 00 to the RES[1:0] bits in the ADC1->CR1 register, which signifies the 12-bit resolution mode. Next, we need to configure the sampling rate for our ADC which is done by writing 101 to the SMP10[2:0] bits in the ADC1->SMPR1 register which signifies a sampling rate of 112 cycles. Then, in the end, we set the ADC channel conversion sequence by setting it as first in the regular sequence. This is done by writing 10 (since our channel number is 10) to the SQ1[4:0] bits in the ADC1->SQR3 register.

  • Now, let's look at void TIM2_Init(),
void TIM2_Init(){
    RCC->APB1ENR |=1;
    TIM2->PSC = 16-1; //Setting the clock frequency to 1MHz.
    TIM2->ARR = 20000; // Total period of the timer
    TIM2->CNT = 0;
    TIM2->CCMR1 = 0x0060; //PWM mode for the timer
    TIM2->CCER |= 1; //Enable channel 1 as output
    TIM2->CCR1 = 500; // Pulse width for PWM
}

Here, we start by enabling the clock for the Timer 2, because we will be using it to generate our pulses. This is done by writing a 1 to the TIM2EN bit of the RCC->APB1ENR register. Then, we set the Pre-scalar to 16-1 to reduce the clock frequency to 1MHz from 16MHz, by writing the value to the TIM2->PSC register. After that, we set the time period of the pulse to be generated by assigning the corresponding value to the TIM2->ARR register, which is 20,000 in this case since we want the time period to be 20ms. Each timer tick corresponds to (1 / (1 MHz)), which is 1 micro-second, and since we require the time period to be 20ms the TIM2->ARR register is loaded with 20x1000 micro-seconds, which is 20 milliseconds. Then, we initialize the TIM2-CNT to zero.

Capture4.PNG

After that, we configure PWM Mode 1 for TImer 2 - Channel 1, and make Channel 1 an output. For that, we first write 0110 to the OC1M bits of the TIM2->CCMR1 register.

Screenshot 2021-09-20 172206.png

Screenshot 2021-09-20 172308.png

Then, we enable the PWM output on Channel 1 by writing a 1 to the CC1E bit in the TIM2->CCER register.

Screenshot 2021-09-20 172613.png

Screenshot 2021-09-20 172711.png

Finally, we set the initial Pulse-Width or initial Angle for the motor, by setting the TIM2->CCR1 register. Here, I have chosen the 0 degrees position as the initial position for my servo motor, by assigning a value of 500 or 0.5ms to the TIM2->CCR1 register.

  • Then, comes the void TIM4_ms_Delay(uint32_t delay) function, which handles any time delay requirements that are needed, in the main function.
void TIM4_ms_Delay(uint32_t delay){
    RCC->APB1ENR |= 1<<2; //Start the clock for the timer peripheral
    TIM4->PSC = 16000-1; //Setting the clock frequency to 1kHz.
    TIM4->ARR = (delay); // Total period of the timer
    TIM4->CNT = 0;
    TIM4->CR1 |= 1; //Start the Timer
    while(!(TIM4->SR & TIM_SR_UIF)){} //Polling the update interrupt flag
    TIM4->SR &= ~(0x0001); //Reset the update interrupt flag
}

Most of the configurations done here are very similar to the ones I described above for the function void TIM2_Init(). Here, we enable the clock for the Timer 4, and set the Pre-scalar to 16000-1 to reduce the clock speed to 1 KHz, which means one clock tick corresponds to 1ms. After that, we assign the desired delay value to the TIM2->ARR register, initialize the count to zero and start the timer. Then we poll the UIF or the Update Interrupt Flag, which is set after the desired amount of clock ticks provided in the TIM2->ARR register are done, or in other words when the timer overflows. After the UIF flag is set we clear it from the TIM2->SR register and exit the function.

Capture8.PNG

Capture9.PNG

4. Writing the main Function

int main(void){
    RCC->CFGR |= 0<<10; // set APB1 = 16 MHz
    GPIO_Init();
    ADC1_Init();
    TIM2_Init();
    TIM2->CR1 |= 1;
    while(1){
        ADC1->CR2 |= ADC_CR2_SWSTART; //Starting ADC conversion
        while (!(ADC1->SR & ADC_SR_EOC)){}
        u = ADC1->DR;
        TIM2->CCR1 = (int)(((u/4095)*2000)+500);
        TIM4_ms_Delay(50);
    }
}

Here, we first set the APB1 clock speed to 16 MHz, by writing a zero to the PPRE[3:0] bits in the RCC->CFGR register, and then we call all the initialization functions we defined above.

Capture10.PNG

Capture11.PNG

Then, we start the Timer 2 by writing a 1 to the CEN bit in the TIM2->CR1 register. By, doing this we start getting the PWM pulses with a pulse width of 0.5ms on PA5.

Capture6.PNG

Capture7.PNG

After that, we enter the infinite while loop, here we start the ADC conversion by writing a 1 to the SWSTART bit in the ADC1->CR2 register. After that, we poll the EOC flag in the ADC1->SR register, since it signifies the End of Conversion. Then we read the converted value from the ADC1->DR register and store it in the variable u which we had defined earlier.

image.png

image.png

Our next step is to map the value we obtained from the pot to the range of pulse width required to control the servo motor. To do that we use the following line, TIM2->CCR1 = (int)(((u/4095)*2000)+500);. In this, first, we are converting our input to a range of 0 to 1 by dividing u by 4095 (Since it's a 12-bit ADC). Then, we multiply the value by 2000 since the servo motor has a control range of 500 to 2500, the latter representing the 180 degrees position and the former representing the 0 degrees position respectively. So, when we subtract 2500 from 500 we get 2000. After that, we add 500 to the resulting value to ensure that the servo motor receives a pulse-width value that is within its control range. This calculated value is finally given to TIM2->CCR1 register by converting it into the corresponding int value. A delay of 50ms is given for the motor to acknowledge the change in the pulse-width and move to the desired position.

We keep repeating this process indefinitely, and hence we are able to achieve control over the movement of a servo motor.

Conclusion

With that, we have been able to successfully control the sg90 servo motor by turning the knob of a potentiometerđŸ˜„.

I hope you found this article useful and easy to follow alongđŸ˜€. All the codes used here are present in my GitHub repository, which you can access by clicking here.

And, the demo video for this tutorial can be found here.

IMG_20210929_215036.jpg

Â