Controlling a Stepper Motor using a WPF application



I started off this project wanting to dig deeper into using Serial Communication to control an Arduino-connected actuator. Previously, I had seen certain projects switching LED’s on and off using WinForms. Although WinForms is great, I wanted to go a step above that. My goal was to create an interactive UI, which was a little more intuitive to use. In this case, creating a dial-like interface that the user could move would help them visualize clearly what direction they want their motor to point to. As opposed to typing in an angular value in a text box, which is decidedly less intuitive. Since WinForms is limited, I knew I had to use WPF (Windows Presentation Foundation). I’ve seen people build amazing WPF apps on Youtube, and thought it would fit this project. Also, it would serve as hands-on practice with XAML and C#. (Since I will be needing to use them in a later project).

Required Components:

  • 28BYJ-48 Stepper Motor

  • ULN2003 Motor Driver Board

  • Arduino UNO (or any other version)

  • Power Bank (5V) or any other appropriate 5 V power supply

  • Wires

  • Arrow-like object (optional, this was used to place on the stepper motor to note direction)

Schematic Diagram:

The ULN2003 driver board and Stepper motor fritzing part were obtained from [6] and [7].

Project Setup

In this project, I used the 28BYJ-48 step motor along with a ULN2003 Motor Driver Board and Arduino UNO. A powerbank was used to supply power to the ULN2003 board, as it would supply sufficient current. The Arduino Board gets its power from the USB port. The stepper motor is fitted with a tiny “hand” so that you can see clearly where it is pointing. The setup is shown below.


Here is a GIF showing the demo of this project. Once a dial position has been set, and the button has been clicked, the stepper motor moves to the corresponding location.



Controlling the Stepper Motor with Arduino

The number of steps needed to complete a revolution in full-step mode is 2048 (Info obtained from [1]). If a user defines an angle they want, the number of steps needed to take are calculated. The equation is shown below:


Number of Steps=(2048/360)*Angle


An instance for the stepper motor was created using the code below, take care of the order in which IN1-4 are typed.


//change this to the number of steps on your motor
#define stepsPerRevolution 2048 //FullStep Mode
// create an instance of the stepper class, specifying
// the number of steps of the motor and the pins it's
// attached to
/* IN1 => PIN 8
   IN3 => PIN 10
   IN2 => PIN 9
   IN4 => PIN 11 

   IN1-4 correspond to the 4 pins on the  ULN2003 driver board.
   Take care of the order in which you type the PIN numbers.
   */
Stepper myStepper(stepsPerRevolution, 8,10,9 ,11);
The code used to rotate the stepper motor is shown below. The value 0.176 corresponds to the step angle in full-step mode [1].

void rotateit(){
      int steps=data/0.176; //Calculates the number of steps needed to rotate the motor by defined angle
      myStepper.setSpeed(6); //
      //Serial.print(steps); //Prints number of steps to Serial Monitor
      myStepper.step(steps); //Rotates the motor by the number of steps 
      delay(1000); //delays by 5 second
      //Motor stops running until user defines new angle
    
      }

Serial Communication on Arduino Side

The code provided in Example 2 in [2] helped greatly in writing code which allowed the Arduino to receive several characters. The end of string was denoted by the ‘$’ symbol. So if the user typed ‘45 $’, the string would end and would be stored in an array. The code then replaces $ with ‘\0’, and then converts the string to an integer value. The baud rate was set 9600, same as the one set in the WPF application. The following variables were set:
  • numChars was the length of the array
  • rc was the array that would store the incoming serial data.
  • newData was a boolean that changed to false once the received characters were sent to the stepper motor

const byte numChars =15; //the length of the array will be 15 because I'm only sending a few characters 
char rc[numChars];// an array to store the received data

boolean newData=false; //status variable to check if data was received and sent. (True when data is received and not sent to motor yet)
int data; //will store user-defined angle in integers
There are two functions used for the Serial Communication namely : receiveit() and printit()

The receiveit() function checks if a user sent a character/data to the Serial Port. This is done by using Serial.available()>0 . It checks if newData is false so that it can start storing the received characters in the rc array. At each iteration, it checks if the received character is $, if it isn't then it continues adding the characters to the array. The index is incremented each time, until it is equal to or greater than the maximum array length.

When $ is at the end of the string, it is replaced by ‘\0’, and the status newData is set to true, which signifies that we are ready to print the data and send it to the stepper motor.

void receiveit(){
  char readit; //Temporarily stores characters in this variable obtained from the Serial Port
  static int i=0; //updates index value each time a new characters is added to the rc array
        
  while(Serial.available()>0){
    readit=Serial.read(); //gets characters from the Serial Port
    //If no previous data was left unsent
  if(newData==false){
 
    if(readit!='$'){ //checks to see if the string hasn't ended, $ will be the symbol that signifies the end of the string
    rc[i]=readit; //adds character to the end of the array
    i++; //updates the indexh
    if(i>=numChars){ //if array length is equal or greater than the max array length
      i=numChars-1; //Will overwrite at the end of the array
      }
    }

    else{ //if string has ended because there was the $ at the end
     rc[i]='\0'; //ends array and removes the $
     i=0; //Resets the position to the beginning of the array
     newData=true; // set to true to signify Data was received and has to be sent
      
      }
    }
  } 
 }
The printit() function is then run. It converts the data to integer and runs the rotateit() function.

  void printit(){ 
    if(newData==true){  //if data was received and hasnt been sent yet
      data=atoi(rc);//Converts to Integer      
        // Serial.print("Data as Number ... ");   
        //Serial.println(data);   //Print the angle number to the Serial Monitor        
        rotateit(); //Sends to Stepper Motor
        newData = false; //Allows for more characters to be received 
      }     
    }

WPF Side of Things

To create an interactive user interface, the UserControl class was used. The behaviour of the application was shown in the beginning of the post. Before explaining the code, I’d like to mention that the code was inspired from [3]. However, my code doesn’t contain any Binding Elements, thus it varies quite a bit from the code given in [3] and [4].

A TextBox and Button and two ellipses were created —— A smaller ellipse (our interactive dial) is named Ellipse2. It sits ontop of the 2nd much larger Ellipse. All of these items sit inside a Grid which is enclosed inside fixed-size Grid. The enclosed Grid (given the name Science) can register two events namely MouseMove and MouseLeftButtonUp. This is when the user moves around the mouse in the grid and lifts their finger from the left button. The code-behind is explained later in this post.
<Grid 
Name="Science"
MouseMove="Science_MouseMove"
MouseLeftButtonUp="Science_MouseLeftButtonUp">... </Grid>
The Ellipse2 can register 1 event, which is when the user presses down the left mouse button (MouseLeftButtonDown).
<Ellipse
MouseLeftButtonDown="Ellipse2_MouseLeftButtonDown"
Name="Ellipse2"
Fill="Blue".... />
The button’s XAML code snippet is shown below. Once the user clicks on the button, it triggers a function.
<Button
x:Name="ButtonSerial"
Click="ButtonSerial_Click" 
FontSize="24"
Content="Send to Arduino?" .../>
The TextBox XAML code snippet is shown below. It is given the name “Temp”.
<TextBox
Name="Temp"
FontSize="45" ...>...</TextBox>

The XAML code for the Window involves bringing your UserControl namespace. This code snippet was inspired from [5]. Since the UserControl exists in the same namespace, StepperMotor_DialWPF, we just use that. The reference is called using the line of code below.

xmlns:local="clr-namespace:StepperMotor_DialWPF"
The local prefix is used to bring UserControl1’s design to the main Window. I placed the Horizontal and Vertical Alignment to center, so that it is displayed properly in the Window. This line of code is placed before </Grid>
<local:UserControl1			
HorizontalAlignment="Center"
VerticalAlignment="Center" />

WPF Code-behind

Since everytime the angle value changes, we want to render Ellipse2 to the relevant angular position, one must use the RenderTransform element to update the UserControl. Firstly the rotation axis coordinates should be defined, also known as RenderTransformOrigin. I found the appropriate RenderTransformOrigin coordinates by moving it around in SharpDevelop until it met the centre of the bigger Ellipse. The values were 0.4471, 4.644. These could be different if your grid size ellipse size were different. The code below shows the way the Ellipse2 is rotated using RenderTransform.
private RotateTransform rotation2;
rotation2=new RotateTransform(angle,0.4471,4.6444);
Ellipse2.RenderTransform=rotation2;

Initially, Ellipse2 is pointing upwards, which in Cartesian coordinates would have an angular position of 90 degrees. But the RenderTransform element believes it is at an angle of 0. So each time the angle is calculated, 90 degrees has to be added. Likewise, clockwise directions are positive according to RenderTransform unlike Cartesian convention. This is also accounted for.

The angle is calculated by first finding the centre position of the Grid, which is stored in the pointorigin variable. Then, when the mouse is moving, the mouse pointer coordinates are stored in point2. The adjacent length and opposite side lengths are calculated using the equations shown in the code snippet. Finally, the angle is calculated using the inverse of tan.

pointorigin.Y=Science.Margin.Top+Science.Height/2;
pointorigin.X=Science.Margin.Left+Science.Width/2;
Point point2=Mouse.GetPosition(this);
double adj_length=-point2.X+pointorigin.X;
double opp_length=-point2.Y+pointorigin.Y; 
angle=Math.Round(-(+90-Math.Atan2(opp_length,adj_length)*180/Math.PI)); 
An important thing to take care of is the initial angle of your stepper motor. Perhaps, at the beginning of your demo, your stepper motor may be pointing in a different orientation. This initial orientation has to be offset when you choose a second angular position for the motor to move to. In subsequent runs, the motor’s current angular position will always be taken into account when moving it to another angular position. This offset angular value is stored in the variable initialangle.

As mentioned earlier, there are 4 event triggers:

  • When the left mouse button is pressed (Ellipse2_MouseLeftButtonDown())
  • When the mouse is moving (Science_MouseMove())
  • When the left mouse button is no longer being pressed (Science_MouseLeftButtonUp())
  • When the button is pressed. ( ButtonSerial_Click () )
When the button is pressed, the initial angular position/current angular position of the motor is subtracted from the calculated angular position of Ellipse2. It then tries to open the port and sends the angle value to the Serial Port.

void ButtonSerial_Click(object sender, RoutedEventArgs e)
		{	//Once the button "Send to Arduino" is clicked, 
			//the current angular position of the ellipse is sent via Serial communication
			
			//the temporary variable subtracts the initial angle position, so that it is compensated for
			// when the motor starts moving. 
			//Also at each iteration, the previous position of the motor is stored in initialangle, and is compensated for at each iteration
			var temporary=Math.Round(((-rotation2.Angle+90)-initialangle));
			
			//tries opening and writing to the port
			try{
				port1.Open(); //Opens port
				if(port1.IsOpen){
				 port1.Write(temporary+" $"); //Writes to the port and appends a $ symbol b/c it signifies end of string for the Arduino code
				 
				 //Shows a Message once the angle is sent sucessfully
				 MessageBox.Show("Sent "+temporary+" $ to arduino. Initial angle was :"+initialangle);
				 port1.Close(); //closes the port
				}
			}catch(Exception ex){MessageBox.Show(ex.Message); //if any exception arises, it lets the user know			
			}
			initialangle=0; //resets initial angle to zero
			initialangle=(90-rotation2.Angle); //takes into account the current angular position of the stepper motor
		}
We only want the Ellipse2 element to move around once the user has clicked the left button. Otherwise the ellipse would be moving all the time. So when the left mouse buttton is pressed, we set left button to true. When the user lifts their finger from the left mouse button, the status changes to false.
void Ellipse2_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
		{  	//Event triggered when the smaller ellipse is clicked onto with the left button
			//sets leftbutton to true
  			leftbutton=true;
  			 
		}
        
        void Science_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
		{	
			if(initializestatus==false){
				initializestatus=true; //After button is no longer pressed, the initial angle is marked at wherever the dial is pointing
				MessageBox.Show("Initial Dial position has been marked!");//Message informs the user that angle was marked
			}
			leftbutton=false; //left button is no longer pressed, status changes
			Temp.Text=(-rotation2.Angle+90).ToString(); //displays current angle in the TextBox
		}
        
While the mouse is moving, we calculate the angle value. Then we use RenderTransform to move the Ellipse to that position. In the Science_MouseMove() function, we also lock in the initial angular position of the motor in real life. This is only done once. Each time we calculate the angle, it is displayed in a TextBox called Temp.
void Science_MouseMove(object sender, MouseEventArgs e)
		{   //Anytime the mouse moves around in the grid region, this function is triggered
			
			//stores the x and y coordinates of the mouse pointer
			Point point2=Mouse.GetPosition(this);
			//To find the inverse tangent, the opposite and adjacent lengths are found
			//Using the centre of the bigger circle and the current position of the mouse pointer
			double adj_length=-point2.X+pointorigin.X;
			double opp_length=-point2.Y+pointorigin.Y;
			
			//If the initial angle hasn't been set yet as in initializestatus=false
			// AND left button was previously pressed, the condition starts
			if(initializestatus==false&&leftbutton==true){
				
				//calculates the inverse tangent angle in degrees and subtracts from 90
				// because the program doesnt not adhere to cartesian coordinate rules currently
				//Also it is set to negative because clockwise is -ve
				angle=Math.Round(-(+90-Math.Atan2(opp_length,adj_length)*180/Math.PI));
				//Uses RotateTransform to rotate the ellipse by calculated angle
				//the RenderTransform origin was set to the origin of the bigger circle (0.4471,4.6444)
				rotation2=new RotateTransform(angle,0.4471,4.6444);
				//This renders the ellipse's new position on the WIndow
				Ellipse2.RenderTransform=rotation2;
				//Displays the position of the dial (in degrees) in the Left Text Box
				Temp.Text= (-rotation2.Angle+90).ToString();
				initialangle=(-rotation2.Angle+90); //Stores the initial angle value (+90 is added to cater for the cartesian coordinate rules)
			}
			//After getting the initial angle, the following bit of code is run instead
			else if(leftbutton){
				//This bit of code is explained above
				angle=Math.Round(-(+90-Math.Atan2(opp_length,adj_length)*180/Math.PI));
				rotation2=new RotateTransform(angle,0.4471,4.6444);				
				Ellipse2.RenderTransform=rotation2;
				Temp.Text= (-rotation2.Angle+90).ToString();
			}
		}
Hopefully, you now have an idea on how one can link an Arduino board with a WPF application. In the future, I will be trying out more projects using this method because I found this very useful and intuitive to use. Perhaps, I may play around with different elements in WPF e.g Binding Elements etc... Thank you for taking the time to read this post! ^_^ All the codes are available on my GitHub repository. References used:

[1]https://www.makerguides.com/28byj-48-stepper-motor-arduino-tutorial/

[2]https://forum.arduino.cc/index.php?topic=396450.0

[3]https://blog.jerrynixon.com/2012/12/walkthrough-building-sweet-dial-in-xaml.html

[4]https://www.youtube.com/watch?v=m-EqckhJNvI

[5]https://www.wpf-tutorial.com/usercontrols-and-customcontrols/creating-using-a-usercontrol/

[6]ULN2003 Driver Board

[7]28BYJ-48 Stepper Motor


Comments

Popular posts from this blog

Fixing the "A software problem has caused Meshmixer to close unexpectedly" Problem

Making an 8DOF Quadruped

Digital Sculpting with MeshMixer...