CNC MPG Pendant with Arduino Nano and LCD display

In this article, I show you how to connect this CNC MPG pendant to an Arduino Nano and use it for controlling anything. The original purpose of this controller is to be used with CNC machines, but you can control anything else which can be controlled with a number. To make the demonstration more spectacular, I added a 2004 LCD to the circuit and printed the position values of the X, Y and Z axes and the value of the multiplier. I bought this controller because I want to upgrade my microscope controller, but before incorporating it into that system, I want to make sure that it works as I expect.

 

Introduction

This simple CNC controller has a lot of features, and with a microcontroller, it can be turned into a powerful controller. It has a pulser wheel with 100 detents, and each detent is indicated with a number. This allows a precise and traceable positioning. For faster positioning, the handwheel has a tiny handle which allows the user to rotate the wheel faster. The rotary wheel is operated at 5 V and it has four outputs: A, B, A- and B-. For an Arduino-like microcontroller, one can just use the A and B pins, and the wheel will work just fine. However, one can use a differential receiver and connect the A, A- and B, B- pairs to the corresponding inputs of the receiver. Then, if a common-mode noise is present in both signals (A, A-, for example), the differential receiver is going to ignore that noise, and it will output a pure signal. This technique is used with devices with long cables or in electrically noisy environments.

Above the wheel, there are two rotary switches. One is used to select the active axis and the other is used to apply a multiplier. The axis selector has 7 options: X, Y, Z, 4, 5, 6 and OFF. The OFF, X, Y and Z are obvious, and the 4, 5, 6 can be any randomly selected axis (in case you use it for a CNC-like machine). The multipliers are also trivial. 1x means that 1 click on the encoder wheel should be 1 unit of steps should be in your code, 10x means 10 units of steps, and 100x means 100 units of steps. One must be aware that the controller won’t emit 10x or 100x more pulses for a single click. This has to be handled in code. When the code detects that the user selected, let’s say, the 10x multiplier, then the code should change the value of the increments to 10. And of course, this value can be anything. If you want to make it 3.1415, then you can just code it to be that value.

It is important to know, that none of these switches have an effect until the enable button on the side is pressed. The enable button shorts the rotary switch circuit to ground, and by completing the circuit, the microcontroller will see a change in the signals.

Above these two rotary switches, there is an emergency button. It does not disable anything on the controller itself, but it sends a signal to the microcontroller by shorting a certain pin to ground when pressed. Similar to the axis and multiplier selector switches, the emergency switch should also be handled in software when using a microcontroller.

 
 

Circuit

The circuit does not need too much explanation; it works with any 5V-tolerant microcontroller “straight out of the box”. If you use a more modern, 3.3V-tolerant only MCU, then you will need level shifters. But this demo is with an Arduino Nano, which is 5V-tolerant.

I connected all cables to the microcontroller, except for A-, B-, and the not used red-black cable. The wiring diagram is unnecessary because I named the pins in my code properly, so the only skill needed is just some comprehension. I set up the circuit in a breadboard. The wires from the controller were tinned, so I could plug them into the breadboard easily.

Regarding the display, since it is an I2C display, its pin connections are also trivial. The I2C on this microcontroller is A4 - SDA and A5 - SCL. VCC is obviously +5V, and GND is obviously GND. I also used pull-up resistors on the I2C line, because I was not sure if the board I used had them. For example, when I tested a similar circuit with the “GM12864-59N VER2.0” display, I needed the pull-up resistors; otherwise, the display would not work.

One could save quite a lot of pins on the Arduino by reading the axis selector and multiplier selector switches with an I2C IO expander circuit, such as the MCP23017/8 (SMD/Through hole). One would only need to connect the chip to the I2C bus, which is just 2 pins instead of the total 9 pins.

Since checking these pins using an IO expander does not need a high-speed code and circuit, it is OK to “outsource” this task to the IO expander instead of directly reading the pins with the GPIO pins.

The handwheel is a tiny bit more than a simple encoder. It has 4 output pins instead of just two. This is because of the previously mentioned differential output.

The rotary switches sit on the same PCB. We can see all the wire connections for the multipliers and axes at the bottom of the PCB. There is one peculiar wire, which is marked with “C” and it has a black and yellow insulation. This wire goes from the PCB to the enable switch. Then, on the other side of the enable switch, there is an orange-black wire which is connected to the ground of the Arduino. So, now, by looking at the PCB and the connections, we can see how the enable switch lets the multiplier and axis selector switches to be detected only when the enable switch is pressed. Without the switch being pressed, the circuit is an open circuit, so the Arduino cannot detect any changes.

 
 
Color Signal Function
Red +5V Encoder
Black GND
Green A
White B
Purple A-
Purple-Black B-
Yellow X Axis selector
Yellow-Black Y
Brown Z
Brown-Black 4
Pink 5
Pink-Black 6
Gray x1 Multiplier selector
Gray-Black x10
Orange x100
Orange-Black GND Common
Green-Black LED+ (+5V) LED indicator
White-Black LED- (GND)
Blue C Emergency switch
Blue-Black CN
Red-Black Not used
 

Differential signals

I want to dedicate a short section to the differential signals because understanding them is a crucial part of understanding the encoder. However, I must emphasise that I do not use them as of now, because using the A and B signals only is sufficient for my use case.

So, the very basic principles of an ordinary encoder are that when it is rotated, it emits two, 90° phase-shifted pulses (square waves), based on whose pulse rises first, we can distinguish between clockwise and counterclockwise rotation. In this encoder’s case, when the encoder is rotated clockwise, A rises while B is still low, and then A falls when B is still high. Consequently, when the encoder is rotated counterclockwise, A rises while B is already high, and A falls when B is already low. So, depending on the sign of the phase shifting, we can extract the direction of the rotation. This logic can be transferred into code, and it is fairly simple.

The behaviour of the inverted pulses (A- and B-) is basically the same. The only difference is that their signal is inverted.

The reason why we have the inverted signals is the following. As I said in the introduction, it is used for getting rid of noisy signals and dodgy edges. When a pair of differential signals is processed by a differential receiver, the receiver ideally will output a clear pulse even if the input pulses are noisy.

 
 
 

Coding

In this section, I discuss some of the most important code snippets. If you want the full code, either you can follow my step-by-step explanation in the video, or become a channel member and get the code from my page for supporters.

As I said, there is no need for a wiring diagram, because I named the pin connections in a very obvious way, and I even added comments to the code. As you can see, I spared the I2C pins (A4 and A5) as well as the SPI pins (10, 11, 12 and 13), so if someone needs an additional peripheral like a display or an IO expander, they can add it.

In this demo, I only used the I2C pins for the 2004 LCD. Let’s imagine, pins 10-13 are still available: You could hook up 4 stepper motor drivers to the microcontroller, and it could be used as a fully capable CNC controller. The four GPIOs can control the 4 step pins of the individual drivers, and the direction pins could be controlled from an IO expander.

Then, the next interesting part of the code is the interrupt handler. I actually got this part from DeeEmm’s code while searching for similar projects. The PIN B also has a very similar interrupt function (it differs only by two characters), but I only present the one for pin A for the sake of simplicity.

So, when the wheel produces a rising edge on pin A, the code enters this interrupt. Then we have a variable called reading. This is the combination of PIND and B00001100 with the logical AND operation. PIND refers to the port D’s IO register. To select the values of pin 2 and pin 3 from port D (these are pins 2 and 3), the value of PIND is masked with B00001100. If you read it from right to left, you can see that bit 2 and bit 3 are “1”, so when the logical AND operation is performed on PIND, only these two bits will return one if they are 1. Thus, we get the values of the PIND register.

Then an if-else if() block is coming. We check if both lines are high (reading == B00001100) and if the aFlag is high as well. The flag is set by the interrupt handler of pin B. So, when this function is triggered, the pin B’s interrupt has already run before. When the code proceeds, it decreases the number of clicks by a unit of steps which can be 1, 10 or 100x, based on the position of the multiplier rotary switch. Then, both a and b flags are reset.

Finally, we also check if we are in a so-called “pre-detent” state where A == 1 and B == 0 (see mask). This pattern indicates that the encoder is in a position where B will rise next if we keep turning in the direction that makes A lead.

 
const int pinA = 2;          //Encoder pin A (green), interrupt pin
const int pinB = 3;          //Encoder pin B (white), can be regular pin
const int pin1x = 4;         //1x multiplier (gray)
const int pin10x = 5;        //10x multiplier (gray-black)
const int pin100x = 6;       //100x multiplier (orange)
const int pinX = A0;         //X-axis selector (yellow)
const int pinY = A1;         //Y-axis selector (yellow-black)
const int pinZ = A2;         //Z-axis selector (brown)
const int pin4 = A3;         //4-axis selector (brown-black)
const int pin5 = 8;          //5-axis selector (pink)
const int pin6 = 9;          //6-axis selector (pink-black)
const int stopSwitch_C = 7;  //stop switch C (blue)
 
void encoderAInterrupt() 
{
  reading = PIND & B00001100; 
  if (reading == B00001100 && aFlag)
  {  
    numberOfClicks = numberOfClicks - clickMultiplier;
    bFlag = 0;
    aFlag = 0;
  } 
  else if (reading == B00000100)
  {
    bFlag = 1; 
  }
}
 

There are some more functions that worth some attention. One of them is the readSpeedSetting() function. This is a very simple function. The function is constantly called in the main loop(), and all it does is read the status of the three speed multiplier pins. Since they are defined as INPUT_PULLUP, their default state is HIGH. But when one of these three switches is selected, then their status becomes LOW because the rotary switch connects the corresponding circuit to the common (ground) rail, which then causes the GPIO pin to go LOW. The pins are mutually exclusive, so only one pin at a time can go LOW. Whenever one of these is LOW, the corresponding clickMultiplier value gets a new value, and then, in the previously introduced interrupt function, the value of the encoder clicks will be changed by this value. It is worth noting that we do not need to be so rigid with these multipliers. As long as you can distinguish the positions visually on the controller, you can assign any values. For example, the middle position of the rotary switch could be the 1x multiplier instead of 10x, and then instead of 1x, you could use 0.1x and instead of 100x, you could use 10x. Or something similar. Another reminder is that there is a hardware part that we cannot influence in the code: one must press the enable button to complete the circuit between the GPIO pin and the ground through the rotary switch. So if you change the switch but don’t press the enable button, the microcontroller won’t detect anything.

Note: the function in the code is a bit different from what I present here. Technically, it does the same, but this code looks cleaner; therefore, it is more suitable for presenting the principles of the circuit and the code.

Similarly to the speed settings, we can read the selected axis as well. This is actually a bit annoying because the zero position does not have any connections to the external world. It just cuts the circuit. But that is the same effect that you get when you release the enable button. So, there is no hardware distinction between releasing the enable button while one of the axes (except OFF) is selected and switching the rotary switch to OFF while the enable button is pressed.

Therefore, a software logic must be set up so that the axes are only manipulated when they are selected. When the enable button is released, the code defaults to OFF, and it behaves as it was no axes were selected.

The function returns a number from 0 to 6, where each number represents an axis or position. The code checks if any of the non-OFF positions (1-6) are active (pulled to LOW). If so, the raw value becomes the value of the corresponding axis, and the lastAxis is assigned to be this axis as well. When there is a non-zero axis selected, the code sets the offSince to 0 and exits the function. The rest of the code in this function is not performed.

However, if there’s no axis selected, or the enable button is released (these two are equivalent signal-wise), raw will be zero. In this case, the code skips the above-mentioned if() block and jumps to the next one and enters it since the offSince was or just recently became zero. Then the offSince is updated to millis(). In the last if() block, this will mean essentially no time passed, so millis() - offSince becomes zero. This will not pass the if()' block’s condition, so the code is skipped and the lastAxis is returned.

 
void readSpeedSetting()
{
  if (digitalRead(pin1x) == LOW) {
    clickMultiplier = 1;
  }
  if (digitalRead(pin10x) == LOW) {
    clickMultiplier = 10;
  }
  if (digitalRead(pin100x) == LOW) {
    clickMultiplier = 100;
  }
}
int readSelectedAxis() 
{
  static int lastAxis = 0;      
  static uint32_t offSince = 0; 
  const uint16_t OFF_MS = 20; 

  int raw = 0;
  if (digitalRead(pinX) == LOW) raw = 1;
  else if (digitalRead(pinY) == LOW) raw = 2;
  else if (digitalRead(pinZ) == LOW) raw = 3;
  else if (digitalRead(pin4) == LOW) raw = 4;
  else if (digitalRead(pin5) == LOW) raw = 5;
  else if (digitalRead(pin6) == LOW) raw = 6;

  if (raw != 0) 
  {   
    lastAxis = raw;
    offSince = 0;
    return raw;
  }

  if (offSince == 0) 
  {
  offSince = millis();
  }

  if (millis() - offSince >= OFF_MS) 
  {
    lastAxis = 0;
    return 0;
  }
  return lastAxis;
}

If you found this content useful, please consider joining my YouTube channel’s membership or leaving a donation.

Also, please consider using my affiliate links when buying relevant gadgets.

Next
Next

Light meter for analogue cameras