Controller Redesign
So-called “pendants” are used by most CNC milling machine operators to make manual adjustments. In this article, Brian discusses his project, building a comprehensive CNC pendant around a new rotary encoder. The pendant supports the Mach3 CNC controller software program.
Just before retiring 10 years ago, I built a homebrew CNC (computer numerical control) router for my workshop. I’d been lucky enough to have access to a machine shop, and was friends with the machinist while employed at Dalhousie University. I knew I was going to continue designing various projects, and wanted to be able to do my own machining for cabinets and other work. Figure 1 shows the mechanical part of that CNC machine—milling out the front panel for the project that was the subject of my last column “A Digital Amplifier for TVs” (Circuit Cellar 372, July 2021) [1].
![Figure 1 The homemade CNC router that I built about 10 years ago. Here it is milling out the front panel for the project that was the subject of my July column. [1]](https://i0.wp.com/circuitcellar.com/wp-content/uploads/2021/09/374_Millier_Figure_1.jpg?resize=810%2C611&ssl=1)
The homemade CNC router that I built about 10 years ago. Here it is milling out the front panel for the project that was the subject of my July column. [1]
In case any of you are unfamiliar with it, CNC milling is a machining process that progressively removes material from a workpiece, to produce a custom-designed part or product. It uses computerized controls and rotating, multi-point cutting tools, and is suitable for machining metal, plastic, glass and wood.
While the CNC machine is basically computer controlled, you have to maneuver the “spindle” (the actual router motor) into a “home” position manually, and also perform various manual operations in between running the various “G-Code” programs. Those programs tell the machine where to move, how fast, and what path to follow.
I use the Mach3 CNC controller program that runs on a PC. You can perform these manual functions using the mouse and keyboard, in conjunction with the computer’s LCD monitor. However, the computer—and its associated screen, keyboard and so forth—are located a few feet away from the CNC machine, for practical reasons. So, closely monitoring the position of the spindle while adjusting it manually is difficult when using the mouse/keyboard and computer monitor.
Most CNC milling machine operators use what is called a “pendant” for manual adjustments. Not long after building the CNC mill, I also built such a pendant, which I have been using for about 10 years. Recently, however, I came across what I considered to be an exquisite rotary encoder assembly. It looked like it was built for industrial use. It also reminded me of the sophisticated frequency-tuning dials found on high-end amateur radio transceivers (such as Collins transceivers), which I had seen way back in the 1960s. It was sold by Banggood for under $30 [2]. Figure 2 shows this encoder/knob. I could see a few uses for it—an improved CNC pendant, and the jog wheel for a transport controller in my music studio. In this article, I’ll describe a comprehensive CNC pendant that I built around this rotary encoder.

One factor that led me to design a new CNC pendant was the availability of this excellent rotary encoder/dial assembly at a reasonable cost.
CNC PENDANT FUNCTIONALITY
My CNC pendant performs three main functions:
— ADVERTISMENT—
—Advertise Here—
1) Jogging the router spindle in 3 planes, at low or high (user-selected) speeds. This enables me to move the spindle to approximately a “home” position.
2) Precision movement of the spindle in small steps, using what is known as the manual pulse generator (MPG) function.
3) Control of 10 commonly used functions normally performed on Mach3’s screen, using the mouse or keyboard shortcuts. Example functions are Zeroing X, Y, Z, Cycle Start, Stop, Rewind and Spindle On/Off.
The Mach3 application can be configured for several ways to interface the PC to the CNC machine controller [3]. In Mach3’s early days, the most common and reliable method was to use the PC’s parallel printer port. The parallel printer port contained enough digital I/O pins to send step commands to each of 4 axes, turn the spindle on/off and monitor limit switches. PC software could manipulate these parallel port pins accurately in terms of time, which is critical in CNC motor control. This is the method I use, but that’s only possible because I have an older PC that contains a parallel printer port on the motherboard. More recently, Mach3 can be configured to use a USB interface to the CNC machine.
Regardless of which of the above methods is used to interface the PC running Mach3 to the CNC controller, the interface between the PC and the CNC pendant is independent. The most common method is to use a keyboard emulation, and let the pendant send keystrokes to the PC to match the keyboard shortcuts or “hotkeys” that are defined by Mach3. Older PCs contained a PS/2 interface for the keyboard. The computer running my CNC machine has both PS/2 and USB ports for keyboard use, and my first pendant project used the PS/2 interface. For this upgraded pendant, I chose to go with the USB port and use the USB HID keyboard emulation that’s available on the PJRC’s Teensy 3.2 microcontroller (MCU) module [4].
Controlling the motion of the spindle in all 3 axes is done in two ways, as outlined in 1) and 2) earlier. For both slow and fast jogging—which are continuous motions—I used a 4-switch membrane assembly, slow and fast in each of the 2 directions. For precise spindle location adjustments, the MPG function is used. This allows you to move the spindle in discrete steps, with 5 different resolutions. I have set up my CNC mill for Imperial measurement, so my MPG steps vary from 0.0001” to 1.0“ in five steps. This is where the nice rotary encoder shown in Figure 2 is used. It has a resolution of 100 quadrature pulses per revolution, and there is a nice “click” detent at each one. All the other functions are triggered by pressing standard momentary-contact push-button switches.
TFT SCREEN ADDED
I added one customized feature to my pendant design. This arose from past experience, when I couldn’t always remember which direction each of the Jog buttons defined. Logically, one would expect that the direction of motion for each button would be labeled on the pendant’s panel, but I used only 4 buttons to define slow/fast jogs in 2 directions, not 4 separate buttons for each axis. Also, I use several different PC applications to generate the G-Code needed for CNC milling.
Unfortunately, they don’t all define the Cartesian coordinates the same way. So, for example, one such application would define a positive Z value as having the spindle above the cutting surface of the workpiece, whereas with another, the spindle would be cutting into the material. To compensate for these differences, one must use Mach3’s Configuration > Homing > Limits menu. Practically speaking, I do this by defining a different Mach3 Profile for each of the different programs that I use to generate G-Code. While selecting the correct profile takes care of translating the G-Code file instructions into motion in the correct direction, it does not adjust the way in which the pendant works. So, depending upon the profile, the Up/Down Jog buttons may move the spindle either up or down.
To eliminate this confusion (as well as other things), I added a small TFT screen [5] to the pendant to display the following information:
1) The TFT displays the current Profile in use. A Profile switch, located by itself on the side of the pendant, is used to synchronize the TFT display’s Profile to that chosen in Mach3 when that application is started.
2) There are only 4 Jog buttons (slow and fast in two directions). I chose to have 3 Axis-Select buttons, and use the same 4 Jog buttons for all 3 axes. The TFT displays the current axis, using a large font that is hard to overlook. It also displays which direction each of the 4 buttons defines for the chosen Profile.
3) As described earlier, the MPG function, handled by the rotary encoder, allows the spindle to be moved in precise discrete increments. The absolute spindle position relative to home position is displayed on the PC’s monitor by the Mach3 application. However, if I want to move the spindle position manually using the MPG, I thought it would be nice to also have a display showing me how much I have moved the spindle manually, in a relative sense. To achieve this, whenever either of the X, Y and Z Axis-Select buttons is pressed and held for more than 1 second, the corresponding coordinate reading on the TFT display gets zeroed out. This allows you to perform a manual movement using MPG, without looking at the Mach3’s Absolute coordinate display and doing the math.
4) 5 different MPG increments are available, with a button to toggle among them. The TFT display indicates which incremental resolution is active. Since this command merely cycles among the 5 values, you have to make sure that at start-up, the Mach3’s MPG increment reading matches the one shown on the pendant’s display.
Figure 3 shows a close-up of the TFT display in use. The coordinate readouts for feature 3) above are the three “Local” values shown at the bottom of the display.
— ADVERTISMENT—
—Advertise Here—

The TFT readout in action. I chose a 2.2″ TFT with an ILI9341 controller, because this display has a well-supported driver library and is low in cost.
USB HID KEYBOARD EMULATION
Many current MCUs don’t contain a native USB port function. When such MCUs are mounted on a development board of some sort, the Arduino for example, the board may contain a USB socket. Generally, that USB port is handled by a USB-serial port emulator chip, such as those made by FTDI, Silicon Labs and other manufacturers. These chips only perform serial port emulation and are not programmed to act as a USB HID keyboard.
Some MCUs do contain a native USB port. However, besides the existence of this physical port contained in the MCU, you also need access to a USB stack library that handles the HID keyboard profile. The USB stack is a complex driver to implement. Unless you are a pro, you don’t want to try writing this code yourself! The Microchip Technology (formerly Atmel) ATMega32U4 (in the AVR 8-bit MCU family) was one of the early MCUs to include a native USB port. Just as important, the USB stack driver with HID keyboard profile has been available for the Arduino IDE for many years.
I’ve been using Teensy MCU modules for more than 4 years. This series of tiny modules began with the 8-bit ATMega32U4 MCU, and has since expanded to much more powerful 32-bit Arm MCUs. A Teensyduino plug-in is available for the Arduino IDE. It allows you to develop programs in the Arduino IDE for all members of the Teensy family, from the early 8-bit MCUs up to the newest 600 MHz NXP (formerly Freescale) iMXRT1062 MCUs.
All the Teensy MCUs have a native USB port, and a Teensy USB stack is available for many different USB profiles (HID Keyboard, Mouse, Joystick, MIDI, Serial, Audio, Touchscreen). I chose a lower-end Teensy 3.2 module [2] for this project, since I had some on hand, but I believe the less-expensive Teensy LC could have been used instead.
To use the USB HID keyboard profile in your program, you don’t have to add a #include
directive at the top of your program pointing to a USB driver library file. However, you must select the proper option in the Tools > USB Type menu in the Arduino IDE. You must pick the “Keyboard” option here. Note that several other options are available, containing Keyboard along with other USB profiles.
During program development, I used “Keyboard + Serial” so I could debug using a COM port and the Serial.println()
command. However, when I went to try out the pendant on my CNC machine’s older Windows XP computer, it would not recognize it. The Keyboard + Serial profile is a “combo” or “composite” device that Windows XP did not support. So, to deploy the project on the CNC machine’s computer, I had to switch the USB type to Keyboard, and re-build/flash the project.
CIRCUITRY
The schematic of the project is shown in Figure 4. The Teensy 3.2 module is shown on the left side of the figure. Power for the Teensy 3.2 and the rest of the circuitry is supplied by the USB socket on the Teensy 3.2 module. The USB port doesn’t show up on the Teensy 3.2 symbol, because it is a micro-USB socket on the top of the board, and is not among the DIL GPIO/power pins that are included in the library’s symbol.

Schematic for the unit. Besides the Teensy 3.2 MCU board and TFT display, the circuitry only consists of switches and the Banggood rotary encoder. (CLICK TO ENLARGE)
The TFT display is a 2.2″ model that uses an Ilitek ILI9341 controller IC and a SPI interface. Mine came from PJRC, but they don’t carry them any longer. However, a link to a source of them can be found in RESOURCES at the end of this article [4]. These displays require a 5V power supply, but all their control signals must be at 3.3V levels. The Teensy 3.2 MCU uses 3.3V logic levels, so this is not a problem. The 13 push-button switches used for the project connect to 13 Teensy GPIO ports. All these GPIO pins have internal pull-ups set in software, so no pull-up resistors are needed.
For the Jog function, I guess I could have used 6 switches to handle the two directions for each of 3 axes, plus another switch to be held down for fast speed. Instead, I used a single 4-button membrane switch assembly with 3 other push buttons to select the axis. The membrane switch came from Adafruit, and its pinout is counter-intuitive:
Pin 1 (black marking): Common
Pin 2: SW 2
Pin 3: SW 1
Pin 4: SW 4
Pin 5: SW 3
I found out the hard way that these membrane switches do not present 0Ω resistance when pressed, but rather, a value around 10Ω to 20Ω. When I was trying to figure out the pinout using the continuity setting of my digital voltmeter (DVM), some of the switches were just beyond the threshold of what my DVM would consider good continuity, and the DVM wouldn’t beep.
The large, red, Emergency Stop (E-Stop) button is not monitored/connected to the Teensy MCU at all. The Geckodrive G540 motor controller that I use calls for an NC switch for E-Stop, but instead, I wired up my six NC limit switches in series and connected them up to the E-Stop input. The large, red switch that I had on hand was NO. To use it for the E-Stop function, I connected it to another input pin on the G540, which could be configured to use an NO switch for the E-Stop function. I acknowledge that the proper, most safe way to implement an E-Stop function is to use an NC switch, because this will be a fail-safe if either the switch, the wiring or the plug/socket fails.
The large rotary encoder/knob assembly from Banggood [3] is different from the common, panel-mounted rotary encoders that one normally uses. Rather than just using two mechanical switches arranged to produce quadrature signals, this encoder requires a power supply and produces logic level signals on its A and B outputs. Observing the A and B signals on an oscilloscope, you see nice steep transitions on these signals with no contact bounce present. Figure 5 shows these two signals as the encoder is turned. These encoders are sealed, so I can’t see what’s inside, but I assume it is an optical encoder with internal signal conditioning.
Along with the A and B outputs, there are also

which are complementary signals. The presence of these outputs indicates to me that this encoder provides differential signals, which are more commonly used in industrial equipment where long cable runs in noisy environments are common. Unlike the common rotary encoders that I’ve used in the past, this one doesn’t require pull-up resistors—neither external nor internal MCU pull-ups.
— ADVERTISMENT—
—Advertise Here—
Figure 6 shows the interior of the cabinet. The Teensy 3.2 MCU is mounted on the other side of the circuit board, along with the few other passive components used in the circuit. Figure 7 shows the front panel of the finished unit. I was careful to take a picture of it before I started using it. My older pendant looks anything but new, having been splattered with cooling fluid, metal grindings and sawdust.

Photo of the finished project—before it became covered with metal shavings, cutting fluid and finger prints! The 4-button membrane switch controls jogging in one axis at a time, at both low and high speed. The unit connects to the PC for power and communication via the micro-USB cable at the upper right.
SOFTWARE
If you are not already using it, you must first install the Arduino IDE software. To use with Teensy modules, you should download the Arduino IDE from www.arduino.cc. After you have downloaded and run this application once to ensure it is working properly, you then want to download the Teensyduino add-on, available at:
http://pjrc.com/teensy/td_download.html [5](This link is not immediately obvious on PJRC’s website’s main page.)
This will give you an installer program that will install and integrate the Teensyduino add-on to the Arduino IDE. As part of the installation process, the installer will prompt you to select which Teensy libraries you want to have loaded. Several are available—many of which are equivalent to or upgrades of existing Arduino libraries—for a host of peripheral devices.
In general, just select the option of installing all of them. In the future, if you change or customize some of these libraries yourself, you may want to exclude them during subsequent installs of newer versions of the Teensyduino add-on.
Because Teensyduino is closely coupled to the Arduino IDE, as new versions of the Arduino IDE become available, new versions of Teensysduino will also become available shortly thereafter. The PJRC installer webpage will clearly state which version(s) of the Arduino IDE are compatible with the current Teensyduino release. I am currently using Arduino V1.8.9 and the corresponding Teensyduino 1.5.2, both of which are fairly new, but not the latest versions.
Both the Arduino IDE and Teensyduino are available for PCs, Macs and Linux machines, including but not limited to the Raspberry Pi. Although I own a Mac Mini and several Raspberry Pis, I use Arduino/Teensyduino exclusively on the PC.
SIMPLE PROCESS
The software for this project was not too involved. When you define the USB type as Keyboard, as mentioned earlier, the process of sending keystrokes to the PC is simple. It doesn’t require you to load a library or specifically instantiate any class. All that must be done is to use the Keyboard.begin()
statement at the start of your program and use the Key.press(keyname)
statement whenever you want a keystroke sent. The “keynames” for all the keys on the PC keyboard have been pre-defined in the form “KEY_x”, where x is either the alphanumeric character designation of the key, “Fx” for the Function keys or SHIFT, ALT, CTRL for the control keys. They also have LEFT and RIGHT designations, since the PC has different scan-codes for each one.
If the keystroke you want to send is a LEFT CTRL s, for example, you first issue a Keyboard.press(KEY_LEFT_CTRL)
command, delay a bit (50ms or so) and then issue a Keyboard.press(KEY_s)
command. When you want to indicate that you’ve released a key or key combination, you use the Keyboard.release(keyname)
or just a Keyboard.releaseAll()
command.
The program consists mainly of the normal Arduino set-up
and Loop()
functions. In set-up
, you first configure the TFT screen by defining the pins used for the TFT’s CS and DC lines, clearing the screen, and putting up a “splash” screen for a few seconds. Then you configure all 17 GPIO pins used for the switches as INPUT_PULLUP
. Then you display the initial MPG, Profile and coordinate values on the TFT display. The loop()
function merely monitors the state of all the switches, and sends out the correct keystrokes. It also checks the state of the rotary encoder and issues the proper MPG increment commands.
The three X, Y, and Z switches are treated differently. They don’t send out any keystrokes as such, but instead define the value of the CurrentAxis
variable. The value of that variable is then used to pick the proper keystrokes to send, for both the Jog and MPG commands, to match the currently selected axis.
Personally, I see one big advantage of the Arduino IDE is that I can re-use C code that I’ve written for older 8-bit Microchip AVR MCU projects for similar or updated projects for which I now use either Teensy or Espressif Systems ESP8266/32 modules. In this case, I was able to re-use much of the C code that I’d written for my older version of this project, running on the Microchip ATMega32U4 MCU. Had it not been for the limited front panel space available on the enclosure I picked, I could have easily implemented more functions (such as more push-button switches), because the Teensy modules have more GPIO lines available than the ATMega32U4 had.
MACH3 CONFIGURATION
The keystrokes that I send to Mach3 are shown in Table 1. Most of them are defaults—the 12 Jog commands, for instance. However, I had to define some of them myself. To get Mach3 to recognize these keystrokes correctly, I used a utility program called MachScreen that comes with Mach3. This utility allows you to customize many aspects of the Mach3 user interface. However, all I wanted to do was define some hotkeys, and this can be achieved using the View > Hotkeys menu. Figure 8 is a screen capture of this view showing the hotkey definitions. Since I use the same hotkey definitions as I had used for my original pendant project 10 years ago, I can’t remember which (if any) of the hotkeys I had to define or re-define. Therefore, one should run the MachScreen program to ensure that the hotkeys are defined to match the keystrokes shown in Table 1.

Keystrokes sent to the Mach3. Most are defaults, but I defined some myself for a previous project. Use the MachScreen utility program to ensure that the hotkey definitions match the keystrokes.
Similarly, the MPG function must synchronize with the way that Mach3 has defined it. Figure 9 is a screen capture of Mach3’s “on-screen pendant.” For the MPG function to work, the Jog mode must be set to MPG. Similarly, each time you start up Mach3, it will restore the jog step (which I call MPG increment) to the setting it had when Mach3 was last shut down. Therefore, you must toggle the Cycle Jog Step button to match this increment to the setting displayed on the pendant. Alternately, just ignore the pendant’s MPG increment display, and use the PC’s setting. My pendant has both Slow Jog and Fast Jog speeds. The Fast Jog speed is set within Mach3 and the ratio of Fast to Slow is set using the Slow Jog Rate buttons/window, as shown in Figure 9.
RESOURCES
References:
(Note: some of these reference numbers don’t match with what’s in the print article. We apologize for any confusion.)
[1] “A Digital Amplifier for TVs” (Circuit Cellar 372, July 2021)
[2] Rotary Encoder/Knob Assembly:
https://www.banggood.com/60mm-Pulser-Handwheel-Handle-Kit-5V-Manual-Pulse-Generator-CNC-Machine-Rotary-Encoder-Handwheel-p-1084181.html?rmmds=myorder&cur_warehouse=CN
[3] Mach3 CNC Software Tutorial:
https://cncphilosophy.com/mach3-cnc-software-tutorial
[4] Teensy 3.2 MCU Module:
https://www.pjrc.com/store/teensy32.html
[5] 2.2” TFT Touchscreen Display:
https://www.amazon.ca/Wondiwe-Inch-Serial-Display-Module/dp/B08MQ1V39Y/ref=sr_1_6?dchild=1&keywords=2.2%22+TFT+display&qid=1604956318&sr=8-6
[6] Teensyduino add-on, available at http://pjrc.com/teensy/td_download.html
(This link is not immediately obvious on PJRC’s web-site’s main page.)
Adafruit | www.adafruit.com
Arduino | www.arduino.cc
Espressif Systems | www.espressif.com
Geckodrive | www.geckodrive.com
Microchip Technology | www.microchip.com
PJRC | www.pjrc.com
PUBLISHED IN CIRCUIT CELLAR MAGAZINE • SEPTEMBER 2021 #374 – Get a PDF of the issue
Sponsor this ArticleBrian Millier runs Computer Interface Consultants. He was an instrumentation engineer in the Department of Chemistry at Dalhousie University (Halifax, NS, Canada) for 29 years.