Introduction: 3D Printed RC Transmitter
Like most hobby 3d printing and electronics nerds, I have a very large list of projects I want to pursue that seems to be growing daily. So many, in fact, that I won't be able to accomplish them in one lifetime. When digging through this long list, I have noticed that many of these projects have several things in common: they will all require some form of teleoperation. Fortunately, that places a nice constraint on my priorities. I can't go about building anything before I have a tool to control it. Thus arises the foundations for this instructable.
I have designed an RC transmitter or two in the past, but as I learn more through experience, I keep returning to this project. With a continuous revision process in mind, this controller version incorporates a few helpful features. It is relatively easy to assemble and disassemble (while being fun to do so), so replacing parts is simple. The different components are standalone modules with QWIIC (I2C) daisy chaining, so they can be added, removed, or upgraded with very minimal code modification. The internal layout is several pieces, which makes it easy to redesign just a single bracket to work with a new component and cuts down on 3D print time for new parts. All of these features make this controller an excellent tool for developing your next project.
About halfway through the design phase of this project, I was approached by Playing With Fusion, who was interested in using my project to showcase some features of their newest microcontroller, the R3aktor SAMD21 Cortex M0 Dev Board. In turn, I was able to use their resources to learn PCB design, further simplifying the integration of different controller components. Many of the components for this project can be purchased from Playing With Fusion's website, playingwithfusion.com. As a disclosure, I am now an employee of Playing With Fusion.
This project will take you through a detailed description of how you can 3D print, build, and program your own radio controller to interface with any past, current, or future project on your list. For an alternative format, these instructions can be viewed here.
Supplies
Hardware
- (x4) m2x8 pan head screws
- (x4) m2 nuts
- (x14) m3x8 pan head screws
- (x10) m3x12 pan head screws
- (x24) m3 nuts
- (x8) m3x20 hex standoffs
- (x14) m3x16 countersunk screws
- (x2) m3x20 countersunk screws
Electronics
- (x1) PWF R3aktor M0 Logger (Link)
- (x2) PWF IFB-40001 I2C Encoders (Link)
- (x2) PWF IFB-40002 I2C Joysticks (Link)
- (x1) PWF IFB-40003 I2C Buttons (Link)
- (x2) PWF IFB-40004 I2C Switches (Link)
- (x1) PWF IFB-40005 Power Switch (Link)
- (x1) NRF24L01+PA+LNA RF Module
- (x1) 10uF Ceramic capacitor
- (x1) 1s 3.7v 2000mAh Battery
- (x7) QWIIC Cable (Link)
- (x1) male-to-male JST cable
- (x7) male-to-male pin header jumper cables
- (x1) row of 12 angled pin headers
- (x1) row of 16 angled pin headers
Step 1: 3D Print Parts
- (x1) Top
- (x1) Outer Wall
- (x1) Bottom
- (x4) Button
- (x1) Middle Button
- (x2) Encoder Knob
- (x1) Radio Enclosure
- (x1) Button Bracket
- (x1) Battery Enclosure
- (x1) Joystick Bracket - right
- (x1) Joystick Bracket - left
- (x1) Encoder Bracket
- (x1) Power Switch Bracket
- (x1) Toggle Switch Bracket - right
- (x1) Toggle Switch Bracket - left
- (x4) Small Spacer
- (x1) Switch Spacer - right
- (x1) Switch Spacer - left
- (x1) Lower Power Switch Spacer
- (x1) Upper Power Switch Spacer
- (x1) Lower Button Bracket Spacer
- (x1) Upper Button Bracket Spacer - right
- (x1) Upper Button Bracket Spacer - left
- (x16) Standoff
These files can be downloaded from Printables here.
Step 2: Solder PCB Components
Solder the 10uF ceramic capacitor to the positive and negative terminals of the radio module. Keep the form factor as small as possible so the radio module will fit in the 3D printed enclosure.
Solder the angled pin headers to the R3aktor board.
Gather all the Playing with Fusion PCBs and their components. Solder each component onto each board. Take special care with the power switch PCB and the toggle switch PCBs so that the components are as close to square to the PCB as possible.
Step 3: I2C Address Selection
The I2C bus requires each device to have a unique address. The firmware on each board handles this mostly, but a small change is needed to allow identical devices to coexist on the same bus.
Each PCB has a small "cuttable jumper" labeled ADR. Each Identical device needs to have a different configuration of jumpers selected. For example, joystick A has both jumpers 0 and 1 intact, and joystick B has jumper 0 cut and jumper 1 intact. See the above image.
This can be achieved by scraping one or both copper pads with a sharp hobby knife until the parallel pads become electrically disconnected. Using a multimeter to check these selectors for continuity may be helpful if you encounter errors during the programming phase of this project.
Step 4: Assembly
Gather 4 M2x8 pan head screws, 4 M2 nuts, the R3aktor, and the bottom. Place the R3aktor onto the raised cylinders in the center of the bottom. You may need to slide the USB-C port into the USB-C slot first, and then fit the R3aktor into place. Use the M2 hardware to secure the R3ktor to the bottom.
Step 5: Assembly
Assemble the button PCB to the button bracket using 2 m3x8 screws and 2 m3 nuts. Make sure the button PCB goes onto the right side of the bracket.
Step 6: Assembly
Assemble the joystick PCBs to the joystick bracket using 8 m3x12 screws and 8 m3 nuts. The QWIIC connectors should point away from the wall of the joystick bracket.
Step 7: Assembly
Assemble the encoder PCBs to the encoder bracket using 8 m3x8 screws and 8 m3 nuts.
Step 8: Assembly
Assemble the switch PCBs to the switch brackets using 4 m3x8 screws and 4 m3 nuts.
Step 9: Assembly
Assemble the power switch PCBs to the power switch bracket using 2 m3x12 screws and 2 m3 nuts.
Step 10: Assembly
Using 6 m3x16 countersunk screws, 2 m3x20 countersunk screws and 8 m3x20 hex standoffs, fasten the 3D printed standoffs to the 3D printed top of the controller. Use the longer screws for the upper-middle holes. Ensure the standoffs are all aligned properly in the square slots on the underside of the 3D printed top.
Step 11: Assembly
Insert the encoder knobs into the encoder knob holes.
Step 12: Assembly
Place the radio module in its place. Fit the radio module enclosure around it.
Step 13: Assembly
Slide two small spacers onto the standoffs to match the height of the radio module enclosure.
Step 14: Assembly
Place the assembled joystick modules on the standoffs.
Step 15: Assembly
Add the remaining two small spacers on the outside standoffs on top of the joystick mount. Place the encoder mount assembly and place it on the center standoffs. The encoder knobs will need to slide into the 3D-printed encoder knobs placed earlier. As the encoder mount assembly is slid into place, rotate the 3D-printed encoder knobs until the metal encoder knobs find their place.
Step 16: Assembly
Use 3 QWIIC cables to connect each one of the interface boards. The left joystick is connected to the left encoder is connected to the right encoder is connected to the right joystick.
Step 17: Assembly
Slide four more 3D-printed standoffs onto the hex standoffs. After this, slide the switch mount assemblies onto the standoffs. The QWIIC connectors on the PCBs should be pointed upwards (away from the 3D-printed top).
Step 18: Assembly
Add 3 more QWIIC cables to connect the left switch PCB to the left joystick PCB and the right joystick PCB to the right switch PCB. The third cable should be plugged into the right switch PCB. The other end will be plugged in later.
Step 19: Assembly
Slide the two mirrored switch spacers onto the 3D-printed standoffs. These are the last parts that need to go on this end of the controller, so only a small sliver of the standoff should be raised above the top surface of the spacers.
Step 20: Assembly
On the opposite side of the controller, place 3 spacers into their proper places on the 3D printed standoffs. Ensure the contour of the spacers matches the contour of the edge shape of the controller’s top, like in the above image.
Step 21: Assembly
Place the 3D-printed buttons into the button holes on the top. Add a QWIIC cable into the QWIIC connector on the button PCB, leaving the other end unconnected for now.
Step 22: Assembly
Place the last four standoffs onto the remaining hex standoffs. Also, plug the unconnected QWIIC cable from the switch PCB into the QWIIC connector on the button PCB.
Step 23: Assembly
Slide the switch mount assembly into place. It may require some maneuvering to get the switch to fit into the switch hole on the top. Once it is in place, add the two 3D-printed spacers.
Step 24: Assembly
Slide the battery enclosure into the last open place on the standoffs. Plug one end of the JST cable into the terminal on the right side of the switch.
Step 25: Assembly
Set the rechargeable battery into the battery enclosure. Feed the battery cable through an opening in the enclosure and plug it into the left terminal of the switch.
Step 26: Assembly
Utilize the flexibility of the outer wall to place it around the controller. It helps if the toggle switch levers are placed in their respective holes first, and then the other side is slid into place.
Step 27: Assembly
Make the remaining electrical connections. Plug the other end of the JST connector into the R3aktor battery JST terminal. Connect each pin of the radio module to its respective pin on the R3aktor (see the above connection diagram).
- R3aktor -> NRF24L01
- GND -> GND
- 3V3 -> VCC
- D09 -> CE
- D10 -> CSN
- SCK -> SCK
- MOSI -> MOSI
- MISO -> MISO
Step 28: Assembly
Plug the loose end of the last QWIIC cable into the R3aktor board.
Step 29: Assembly
Fit the 3D-printed bottom into place. Make sure all wires are stored safely inside the controller and are not pinched. Use 8 M3x16 countersunk screws to fasten the bottom into place.
Step 30: Assembly Complete
Congratulations! The controller is now ready for programming.
Step 31: Controller Programming
Connect the controller to a computer using a USB-C cable. Download and open the RC Transmitter code in the Arduino IDE. If you have not already, follow the instructions for setting up the Arduino IDE to work with the R3aktor board linked here. Once the code is opened, click on the RC_Transmitter.ino tab. Upload this code to the controller.
/***************************************************************************
* File Name: RC_Transmitter.ino
* Processor/Platform: PwFusion R3aktor M0 (tested)
* Development Environment: Arduino 2.1.1
*
* Designed to collect information from Playing with Fusion I2C interface boards
* and write them to a NRF24L01 radio.
*
* Copyright � 2023 Playing With Fusion, Inc.
* SOFTWARE LICENSE AGREEMENT: This code is released under the MIT License.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
* **************************************************************************
* REVISION HISTORY:
* Author Date Comments
* N. Johnson 2023Sep30 Original version
*
* Playing With Fusion, Inc. invests time and resources developing open-source
* code. Please support Playing With Fusion and continued open-source
* development by buying products from Playing With Fusion!
***************************************************************************/
// Include the radio module libraries
#include <RF24.h>
#include <RF24_config.h>
#include <nRF24L01.h>
#include <printf.h>
// Include the PwFusion I2C Interface board libraries
#include <PwFusion_I2C_Toggle_Arduino_Library.h>
#include <PwFusion_I2C_Joystick_Arduino_Library.h>
#include <PwFusion_I2C_Encoder_Arduino_Library.h>
#include <PwFusion_I2C_Buttons_Arduino_Library.h>
// Define the radio pins
#define CE_PIN 9
#define CSN_PIN 10
// Address definitions
uint8_t ADR_ENC_A = 0x01;
uint8_t ADR_ENC_B = 0x02;
uint8_t ADR_JOY_A = 0x03;
uint8_t ADR_JOY_B = 0x04;
uint8_t ADR_BTN_B = 0x05;
uint8_t ADR_BTN_A = 0x06;
uint8_t ADR_SW_A = 0x07;
uint8_t ADR_SW_B = 0x08;
// Create new instances of the objects for each I2C board
Joystick joyA;
Joystick joyB;
Encoder encA;
Encoder encB;
Buttons btnA;
Buttons btnB;
Switch swA;
Switch swB;
// Define the address for the radio
const uint64_t pipe = 0x01;
// Define an array for storing and sending data
uint8_t data[13];
// Initialize the radio object
RF24 radio(CE_PIN, CSN_PIN);
void setup() {
Serial.begin(9600);
// Initialize each I2C interface board object
joyA.begin(ADR_JOY_A);
joyB.begin(ADR_JOY_B);
encA.begin(ADR_ENC_A);
encB.begin(ADR_ENC_B);
btnA.begin(ADR_BTN_A);
btnB.begin(ADR_BTN_B);
swA.begin(ADR_SW_A);
swB.begin(ADR_SW_B);
// Start the radio
radio.begin();
radio.openWritingPipe(pipe);
}
void loop() {
// Store each piece of data from the I2C boards in the array
data[0] = joyA.getVRX();
data[1] = joyA.getVRY();
data[2] = joyA.getSW();
data[3] = joyB.getVRX();
data[4] = joyB.getVRY();
data[5] = joyB.getSW();
data[6] = encA.getBtnState();
data[7] = encA.getCount();
data[8] = encB.getBtnState();
data[9] = encB.getCount();
data[10] = btnA.getBtn();
data[11] = swA.getState();
data[12] = swB.getState();
//Print out the values of the interface boards to the Serial Monitor
Serial.print("JOY_A x: ");
Serial.print(data[0]);
Serial.print(" | ");
Serial.print("JOY_A y: ");
Serial.print(data[1]);
Serial.print(" | ");
Serial.print("JOY_A sw: ");
Serial.print(data[2]);
Serial.print(" | ");
Serial.print("JOY_B x: ");
Serial.print(data[3]);
Serial.print(" | ");
Serial.print("JOY_B y: ");
Serial.print(data[4]);
Serial.print(" | ");
Serial.print("JOY_B sw: ");
Serial.print(data[5]);
Serial.print(" | ");
Serial.print("ENC_A sw: ");
Serial.print(data[6]);
Serial.print(" | ");
Serial.print("ENC_A count: ");
Serial.print(data[7]);
Serial.print(" | ");
Serial.print("ENC_B sw: ");
Serial.print(data[8]);
Serial.print(" | ");
Serial.print("ENC_B count: ");
Serial.print(data[9]);
Serial.print(" | ");
Serial.print("BTN_A btn: ");
Serial.print(data[10]);
Serial.print(" | ");
Serial.print("SW_A state: ");
Serial.print(data[11]);
Serial.print(" | ");
Serial.print("SW_B state: ");
Serial.print(data[12]);
Serial.print(" | ");
Serial.println();
// Write the array to the radio address.
radio.write(&data, sizeof(data));
}
Step 32: Operation Instructions
To turn the controller on, flip the power switch. A light should appear to indicate the controller is receiving power. When charging the controller, the power switch must be in the on position.
Step 33: Receiver Programming
The controller must talk to a receiver to interface with any project. All the receiver needs to consist of is a controller board (like a r3aktor or an Arduino) and an nrf24l01 radio module. This controller board will receive a packet of data from the controller and store it in an array. Each array value represents the output values from a component within the controller. If the given code (RC_Transmitter.ino) was used, the array is structured like the following list:
data[0] = joystick A x-axis
data[1] = joystick A y-axis
data[2] = joystick A button
data[3] = joystick B x-axis
data[4] = joystick B y-axis
data[5] = joystick B button
data[6] = encoder A button
data[7] = encoder A count
data[8] = encoder B button
data[9] = encoder B count
data[10] = buttons state
data[11] = switch A state
data[12] = switch B state
See the next step for an example of this receiver code.
Step 34: Demo
There is no better way to demonstrate the functionality of this project than by seeing it working! The embedded video shows me controlling my current project. This project is still in the works (as evidenced by the wire ratsnets and the squealing gearbox), but still serves to show how simple it is to connect and how responsive the controller is.
The following code receives inputs from the transmitter and uses them to control the speeds and directions of drive motors.
// Include the required libraries for communication with the nrf module
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
// CE and CSN pin definitions
#define CE_PIN 7
#define CSN_PIN 8
// Motor pin definitions
#define Ap1 3
#define Ap2 5
#define Bp1 6
#define Bp2 9
#define Cp1 2
#define Cp2 4
// Dead zone for input values
int D_ZONE = 2;
// Joystick position definitions
float joyAx = 0;
float joyAy = 0;
float joyBx = 0;
float joyBy = 0;
// Define the address for RF communication
const uint64_t pipe = 0x01;
// Define the package size to receive.
uint8_t data[13];
// Define the new radio object
RF24 radio(CE_PIN, CSN_PIN);
void setup() {
pinMode(Ap1, OUTPUT);
pinMode(Ap2, OUTPUT);
pinMode(Bp1, OUTPUT);
pinMode(Bp2, OUTPUT);
pinMode(Cp1, OUTPUT);
pinMode(Cp2, OUTPUT);
// Make sure all motors are off
digitalWrite(Ap1, LOW);
digitalWrite(Ap2, LOW);
digitalWrite(Bp1, LOW);
digitalWrite(Bp2, LOW);
digitalWrite(Cp1, LOW);
digitalWrite(Cp2, LOW);
Serial.begin(9600);
delay(1000);
// Initialize the radio for recieving
Serial.println("Nrf24L01 Receiver Starting");
radio.begin();
radio.openReadingPipe(1,pipe);
radio.startListening();
}
void loop() {
// This demonstraits how to recieve the inputs from the RC Transmitter.
if ( radio.available() ) {
// Read the data from the radio and store it in the data array
radio.read(data, sizeof(data));
// Uncomment to see controller values in the Serial Monitor
// Print out each value from data to the Serial Monitor
// for (int i = 0; i < 13; i++) {
// Serial.print(data[i]);
// Serial.print("\t");
// }
// Serial.println();
// Retrieve, map, and assign the joystick position values from the data array
joyAx = map(data[0], 0, 255, -255, 255);
joyAy = map(data[1], 0, 255, -255, 255);
joyBx = map(data[3], 0, 255, -255, 255);
joyBy = map(data[4], 0, 255, -255, 255);
// Functions to set motor speeds and directions
set_motor_pwm(joyAy, Ap1, Ap2);
set_motor_pwm(joyBy, Bp1, Bp2);
set_motor(joyAx, Cp1, Cp2);
} else {
Serial.println("Transmitter unavailable");
}
}
void set_motor_pwm(int pwm, int pin1, int pin2) {
if (pwm < -2) {
analogWrite(pin1, -1 * pwm);
digitalWrite(pin2, LOW);
Serial.println("Forward");
} else if (pwm > 2) {
digitalWrite(pin1, LOW);
analogWrite(pin2, pwm);
Serial.println("Put it in reverse, Terry!");
} else {
digitalWrite(pin1, LOW);
digitalWrite(pin2, LOW);
}
}
void set_motor(int in, int pin1, int pin2) {
if (in < -10) {
digitalWrite(pin2, LOW);
digitalWrite(pin1, HIGH);
} else if (in > 10) {
digitalWrite(pin2, HIGH);
digitalWrite(pin1, LOW);
} else {
digitalWrite(pin2, LOW);
digitalWrite(pin1, LOW);
}
}
Step 35: Final Word
Thank you for taking the time to read through this instructable. I hope you were able to learn something or get a few new ideas for your next project.
Please leave a comment with any questions or suggestions! I will do my best to respond.