This year during Double Eleven, I got a 3D printer and downloaded several models online, and the printing effect was very good:
However, always downloading models designed by others and printing them, I felt I wasn’t utilizing my design skills. Therefore, I decided to design my own 3D models.
After browsing some science popularization articles online, I found that 3D model design can be roughly divided into two categories:
-
Using modeling software like ZBrush and Blender to create artistic models, mainly for movies, games, figurines, etc.;
-
Using industrial modeling software like SolidWorks and Fusion360 to create precise industrial product models, which can design furniture, daily necessities, robots, etc.
Considering my limited artistic talent, I naturally chose the more challenging industrial design. Therefore, in order to design 3D models and print them with a 3D printer, I decided to master SolidWorks, an industrial design software.
I searched for SolidWorks tutorials on Bilibili and found that Teacher Aqi’s SolidWorks tutorial was very popular. I spent a week quickly learning and successfully designed a small drawer and a shaking fan:
To summarize, the core of SolidWorks modeling is to master sketching, creating parts, and finally using assemblies to complete the assembly and simulate mechanical movement. After checking to ensure there are no interferences, the final 3D model is completed.
SolidWorks, easily mastered!
Next, I want to challenge the high difficulty of robot design. Although my ultimate goal is a humanoid robot, I still need to follow the principle of gradual progress, starting with an entry-level robotic arm. Luckily, Teacher Aqi also has a paid robotic arm course, so I spent another week quickly learning and successfully completed the robotic arm model:
Compared to the original robotic arm course, the main modifications are as follows:
-
Removed the synchronous belt wheel and changed to gear drive;
-
Simplified the gripper a bit, using 3 gears to drive.
Unfortunately, although this tutorial can achieve the final robotic arm design, it does not complete the MCU control and software operation functions. However, this is our professional field.
There are many examples of controlling robotic arms on Bilibili. The most common method is to connect via USB to a computer or use a mobile phone for operation. However, using a mouse or touchscreen to control the servo can only be used during the development phase; as a product, it is completely unqualified. To easily control the robotic arm, a wireless controller must be used. Therefore, I summarized the product requirements for the robotic arm as follows:
-
Use a wireless controller to remotely control the robotic arm without the need for a computer or mobile phone as a host intermediary;
-
Must have a built-in MCU master control chip and support wireless connection;
-
If using Wi-Fi connection, it needs to connect to an existing wireless network and configure the IP, which is not as convenient as Bluetooth.
So, the final solution is to use a Bluetooth controller to connect to the MCU master control to control the robotic arm.
Now the question arises, which MCU chip is the best?
First, we exclude the ancient 51, and the remaining contenders include:
-
STM32: Uses ARM Cortex-M 32-bit CPU;
-
Arduino: Uses AVR microcontroller, also has ARM products;
-
Pico: Self-developed MCU from Raspberry Pi;
-
ESP32: Domestic MCU, uses LX7 or RISC-V 32-bit CPU.
After comparing the above products, I decided to choose ESP32 in the end because it is not only a domestic product but also integrates Wi-Fi and Bluetooth in one chip, along with various peripheral communication protocols, offering excellent cost performance! This means that no additional modules are needed to achieve Bluetooth connectivity directly!
I found the cheapest ESP32C3 Mini development board on Taobao, priced at only 10-20 yuan, which can easily beat STM32, Arduino, and Pico:
Therefore, the MCU master control will choose ESP32C3, plus a servo driver board PCA9685 and a Bluetooth controller (model to be determined).
Here, let me explain why we need a separate servo driver board. Because we are using very cheap analog servos that are controlled by PWM signals. Although the ESP32 can output PWM signals, its internal PWM pins are limited, while the PCA9685 can output 16 channels of PWM signals simultaneously, meaning it can drive 16 servos at the same time! If more servos need to be driven, it can also be cascaded for expansion. In addition, driving multiple servos requires stable power supply to the servos, which cannot be drawn from the ESP32 board. The PCA9685 driver board comes with a power input, saving us from having to find a separate power supply board.
The principle of controlling servos with ESP32 + PCA9685 is as follows:
The ESP32 sends control commands to the PCA9685 via the I2C interface, and the PCA9685 generates PWM signals based on the control commands to control the servos. This way, when we write the program, we only need to send control commands without using timers to generate PWM signals, which greatly simplifies the development of control programs.
Next, we need to choose a Bluetooth controller. After carefully reading the relevant documents of the ESP32C3, I found that the Bluetooth protocol is quite complex. The earliest Bluetooth 1.0 was born in 1999, while the widely used Bluetooth 4.x and 5.x were born in 2010 and 2016 respectively. The significant difference between these versions is the introduction of Bluetooth Low Energy (BLE), while traditional Bluetooth is referred to as Classic Bluetooth (BR/EDR). These two protocols have almost no overlap in the protocol stack, and can be considered as two different protocols.
Therefore, when it comes to supporting Bluetooth-specific hardware, it can support any one or more of the following protocols:
-
BR/EDR 4.x;
-
BR/EDR 5.x;
-
BLE 4.x;
-
BLE 5.x.
Mobile phones and computers typically support both BR/EDR and BLE (i.e., dual-mode Bluetooth), but Bluetooth peripherals like mice, headsets, alarms, etc., generally support only one of the protocols. According to the official selection manual of the ESP32, the ESP32C3 only supports BLE 5.0, so we must choose a Bluetooth controller that supports BLE 5.0. Currently, most game controllers on the market only support classic Bluetooth. I asked around on Taobao’s customer service, and the only one that received a positive answer is a controller called “Gai Shi Xiao Ji”, so I finally chose their “Qi Xing Star Wireless Controller” for under 100 yuan:
Software Development Environment
For ESP32, the available development environments are as follows:
-
Using the official ESP-IDF with Eclipse or VSCode, using GCC as the compiler;
-
Using Arduino IDE, using simplified C++;
-
Using MicroPython.
If using MicroPython, we usually develop at the application layer, which requires the firmware to contain the basic system and MicroPython runtime environment. The biggest problem with using MicroPython is that if the hardware we want to use does not have existing drivers, we still need to write a calling interface for MicroPython using C. In addition, the running efficiency of MicroPython is much worse than that of C.
The biggest advantage of using Arduino IDE for development is that it can use Arduino’s rich third-party libraries, but the downside is that Arduino itself is too simple, to the point that it doesn’t even have an operating system. It’s easy to get started but not convenient for developing complex programs. The ESP32 official development environment, ESP-IDF, uses CMake and GCC for compilation, running on FreeRTOS, which is the most difficult to develop but also the most flexible, as we can fully control the underlying hardware.
Based on the above analysis, we choose the highest difficulty and use the ESP-IDF and VSCode development environment to write C code directly. There is no need to use C++ because the advanced features of C++ such as virtual functions and templates are rarely used when controlling hardware.
Connecting the Bluetooth Controller
In the Bluetooth protocol, the two devices that connect with each other are called Client and Server, but contrary to the Client/Server of the Internet, the Bluetooth Client is the PC or mobile phone, and the Server is peripherals like headphones.Another better way to say this is the Host (Central) and Peripheral, where the Host is the PC or mobile phone, and the Peripheral is keyboards, mice, controllers, sensors, etc.A Host can connect to multiple Peripherals via Bluetooth simultaneously, but a Peripheral can only connect to one Host at a time.
The process of establishing a Bluetooth connection is as follows:
-
The Peripheral must actively broadcast;
-
The Host must actively initiate a scan, and after discovering the broadcasting Peripheral, send a connection request;
-
The Peripheral accepts the request and establishes a connection through pairing, starting communication;
-
During the period from connection establishment to disconnection, the Peripheral no longer broadcasts.
The Bluetooth BLE protocol is actually much simpler than the classic Bluetooth BR/EDR. The BLE protocol defines the services (Service) that the Peripheral can provide, and each service can enumerate characteristics (Characteristic), which can be categorized as readable, writable, subscribable, etc. For instance, a temperature sensor’s characteristic like temperature is readable, while a clock’s characteristic like current time is both readable and writable, and a controller’s control button is subscribable, meaning that when a user presses a button, the Host receives a subscription message. These services and characteristics are identified by UUIDs, and the Bluetooth standards organization defines many common IDs to identify keyboards, mice, controllers, temperature, humidity, heart rate, and various sensors. To control the controller, we assume that the preset ID from the manufacturer complies with the standard, so we only need to read the input from the controller based on the standard ID.
Microsoft officially provides a Bluetooth LE Explorer
that can be directly installed from the Windows app store, which allows us to conveniently view the information of the Bluetooth BLE controller:
The role of the ESP32C3 chip is the Host, so we need to start the Bluetooth function of the ESP32C3 in Host mode. We can also start the Bluetooth of the ESP32 in Peripheral mode; in Peripheral mode, we can actually develop a Bluetooth controller or any Bluetooth Peripheral based on the ESP32.
There is very little information available online about connecting the ESP32 to a Bluetooth controller, but the good news is that the ESP-IDF comes with an example of connecting HID peripherals in Host mode called esp-hid-host
. HID refers to Human Interface Devices, which include controllers, mice, and keyboards. By creating a new ESP-IDF project and choosing to use this template, we get a program that automatically connects to the controller.
According to the official documentation of the ESP32, it supports two software protocol stacks:
-
Bluedroid: This is a Bluetooth protocol stack open-sourced by the Android system, supporting BR/EDR and BLE;
-
Nimble: This is an open-source Bluetooth protocol stack from Apache, which only supports BLE.
This example defaults to using Bluedroid, but it can be changed to Nimble. Since Bluedroid is already working, I didn’t bother testing Nimble.
After turning on the controller and enabling pairing mode (actively broadcasting), running the code can automatically scan and find the controller named Xbox Wireless Controller
, then automatically pair and connect. Next, it will continuously print the inputs from the controller:
00 80 ff 7f 00 80 ff 7f 00 00 00 00 00 00 00 00
Each time the input data received is 16 bytes, and it is speculated that these 16 bytes contain the status of all buttons on the controller. By simply pressing different buttons and joysticks on the controller, we can observe changes in the 16-byte input, confirming that the data for each byte is as follows:
-
Bytes 0-3: X and Y axis data of the left joystick;
-
Bytes 4-7: X and Y axis data of the right joystick;
-
Bytes 8-9: Pressure of the left upper L2 button;
-
Bytes 10-11: Pressure of the right upper R2 button;
-
Byte 13: A, B, X, Y, L1, R1 buttons;
-
Byte 14: View and Menu buttons;
-
Other bytes: Not concerned.
The X and Y axis data of the joystick are represented by two little-endian uint16
values, with a range of 0
to 0xffff
:
When the joystick is in the center, the X coordinate is 0x8000
, and the Y coordinate is 0x7fff
. This is a bit strange, as there is only a difference of 1
.
The pressure of the L2 button is represented by two uint8
values. The first uint8
has a range of 0
to 0xff
, representing precise pressure, while the second uint8
has a range of 0
to 3
, representing a coarser pressure. The same applies to R2.
The encoding for buttons A, B, X, Y, L1, and R1 is as follows:
-
A = 0x01
-
B = 0x02
-
X = 0x08
-
Y = 0x10
-
L1 = 0x40
-
R1 = 0x80
The encoding for the View and Menu buttons is as follows:
-
View = 0x04
-
Menu = 0x08
Thus, we can directly parse these 16 bytes as follows:
typedef struct __attribute__((packed)) { // __attribute__((packed)) is a gcc extension that indicates compact fields without memory alignment
uint16_t left_stick_x; // left joystick
uint16_t left_stick_y;
uint16_t right_stick_x; // right joystick
uint16_t right_stick_y;
uint8_t left_trigger; // left upper L2 button
uint8_t left_trigger_level;
uint8_t right_trigger; // right upper R2 button
uint8_t right_trigger_level;
uint8_t any_1;
uint8_t buttons; // A, B, X, Y, L1, R1 buttons
uint8_t ex_buttons; // VIEW, MENU buttons
uint8_t any_2;
} ctrl_input_data_t;
Next, we will make the following modifications based on the example code.
The default example code automatically connects to the last scanned HID input device. We will filter by name and only connect to the device named Xbox Wireless Controller
. Note that this name is seen through Bluetooth LE Explorer, so if your controller has a different name, change it to the actual controller name.
Then define the connection status:
enum CTRL_STATE
{
SCANNING, // scanning
OPENING, // opening
INPUT // normal input
};
When the controller is not scanned, the default example code will automatically stop running. We will change it to pause for 5 seconds and continue scanning, with an infinite loop not exiting:
void hid_scan_task(void *pvParameters)
{
for (;;) {
vTaskDelay(5000 / portTICK_PERIOD_MS);
if (ctrl_state == SCANNING) {
hid_scan();
}
}
}
Once connected to the controller, we notice that the frequency of receiving controller data is very fast, so we need to sample the input data at intervals of 200ms, meaning we only accept input 5 times per second:
case ESP_HIDH_INPUT_EVENT: {
// process controller input:
const uint8_t *bda = esp_hidh_dev_bda_get(param->input.dev);
if (param->input.length == CTRL_INPUT_DATA_LENGTH) {
// sample input every 200ms:
int64_t now = esp_timer_get_time();
if (now - ctrl_input_time > 200000) {
// copy input data and send message to queue:
memcpy(&ctrl_input_data, (char *) param->input.data, CTRL_INPUT_DATA_LENGTH);
xQueueSend(ctrl_input_queue, &ctrl_input_data, 0); // if the previous input data has not been processed, do not wait and discard directly
ctrl_input_time = now;
}
}
break;
}
This way, we can take the controller input from the queue and process it in another task:
void control_task(void *pvParameters)
{
// take the controller signal from the queue and control the servo:
ctrl_input_data_t input;
for (;;) {
if (pdTRUE == xQueueReceive(ctrl_input_queue, &input, 0)) {
// TODO: process input
}
// after reading input once, wait 100ms, allowing only 10 inputs per second
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
Now, the Bluetooth controller connection and input issues are basically resolved. The next step is to prepare to drive the PCA9685 via the I2C interface and control the servos.
After searching online, I found a driver that uses PCA9685 to control LED lights. After a bit of modification, it can be turned into a driver to control servos, with the core functions as follows:
// Set the frequency of PCA9685:
void pca9685_set_freq(uint16_t freq)
{
// Set prescaler
// calculation on page 25 of datasheet
uint8_t prescale_val = CLOCK_FREQ / (4096 * freq) - 1;
// The PRE_SCALE register can only be set when the SLEEP bit of MODE1 register is set to logic 1.
uint8_t mode1Reg;
uint8_t any;
ESP_ERROR_CHECK(generic_read_two_i2c_register(PCA9685_MODE1_REG, &mode1Reg, &any));
mode1Reg = (mode1Reg & ~PCA9685_MODE1_RESTART) | PCA9685_MODE1_SLEEP;
generic_write_i2c_register(PCA9685_MODE1_REG, mode1Reg);
ESP_ERROR_CHECK(generic_write_i2c_register(PCA9685_PRESCALE_REG, prescale_val));
// It takes 500us max for the oscillator to be up and running once SLEEP bit has been set to logic 0.
mode1Reg = (mode1Reg & ~PCA9685_MODE1_SLEEP) | PCA9685_MODE1_RESTART;
ESP_ERROR_CHECK(generic_write_i2c_register(PCA9685_MODE1_REG, mode1Reg));
vTaskDelay(5 / portTICK_PERIOD_MS);
}
// Set PWM for specified channel:
void pca9685_set_channel_pwm(uint8_t channel, uint16_t pwm)
{
uint8_t pinAddress = PCA9685_LED0_REG + (channel << 2);
ESP_LOGI(TAG, "set channel %d (addr = %d) pwm: %d", channel, pinAddress, pwm);
ESP_ERROR_CHECK(generic_write_i2c_register_two_words(pinAddress, 0, pwm));
}
The principle of controlling servos using PWM signals is very simple. Taking the commonly used 180° servo as an example, it requires a PWM signal period of 20ms, meaning a frequency of 50Hz. The duration of the high level determines the deflection angle of the servo:
-
High level duration of 0.5ms, low level duration of 20-0.5=19.5ms, the servo rotates to 0°;
-
High level duration of 1.5ms, low level duration of 20-1.5=18.5ms, the servo rotates to 90°;
-
High level duration of 2.5ms, low level duration of 20-2.5=17.5ms, the servo rotates to 180°.
The range of high level duration is between 0.5ms and 2.5ms, allowing us to control the servo from 0° to 180°.
The PCA9685 outputs PWM signals based on two registers for each servo channel, which store the counter periods for open
(high level on) and close
(low level on). The counter counts from 0 to 4095 repeatedly. Assuming open=0
and close=2047
, we will get the following signal for each counting period:
The signal above has a duty cycle of exactly 50%.
If open=0
and close=1023
, we can obtain a PWM signal with a duty cycle of 25%:
If open=100
and close=1123
, the output PWM signal still has a duty cycle of 25%, but the phase has changed:
Some controls that require phase adjustments can set open
. Here, since we are controlling the servo, we do not need phase offset, so open
is always set to 0
, and we can easily calculate the duty cycle based on close
:
-
To control the servo to 0°, the duty cycle is
0.5/20
, calculateclose=0.5*4096/20-1=101
; -
To control the servo to 90°, the duty cycle is
1.5/20
, calculateclose=1.5*4096/20-1=306
; -
To control the servo to 180°, the duty cycle is
2.5/20
, calculateclose=2.5*4096/20-1=511
; -
To control the servo to x°, the duty cycle is
(2.5-0.5)*x/180+0.5
, calculateclose=((2.5-0.5)*x/180+0.5)*4096/20-1
.
Therefore, we input open=0
, and close
is the integer value calculated based on the target angle, which can control the angle of the servo (note: there may be slight errors).
The next issue is that the PWM signal period required by the servo is 50Hz, while the PCA9685 has a built-in clock frequency of 25MHz. Therefore, the output PWM signal period is 25M/4096=6.1KHz
, which evidently does not meet the 50Hz requirement.
The solution to this problem is to use a prescaler; a 2x prescaler can reduce 25MHz to 12.5MHz, and a 4x prescaler can reduce it to 6.25MHz, while we need a frequency of 50*4096=204.8KHz
, leading to a prescaler calculation of 25M/204.8K≈122
.
Thus, when we call pca9685_set_freq(50)
to set a frequency of 50Hz, we do not write 50
to PCA9685, but first calculate the value of the prescaler and then write this value to the PCA9685 register. When we write 122
, the internal clock frequency of PCA9685 remains at 25MHz, but it counts only once every 122 clock cycles. Completing the count from 0
to 4095
requires 122*4096=499712≈500K
clock cycles, which corresponds exactly to 25M/500K=50Hz
.
Digital chips’ internal registers are integers, and we cannot precisely control the frequency to 50Hz. The prescaler calculation formula provided in the PCA9685 official manual is:
Finally, we calculate the PWM close
based on the input servo angle and set the corresponding register of the PCA9685:
// Input servo angle 0~180:
void set_servo_pwm(uint8_t channel, int16_t angle)
{
// Calculate duty cycle:
uint32_t pwm = RA_SERVO_PWM_RANGE * (uint32_t)angle / RA_SERVO_ANGLE_RANGE + RA_SERVO_PWM_MIN;
// Set duty cycle:
pca9685_set_channel_pwm(channel, (uint16_t) pwm);
}
After debugging, I uploaded it to the ESP32C3, powered it on for testing, and if everything is normal, I can install the servo and board into the robotic arm.
Now, let’s see the final effect of using the Bluetooth controller to remotely control the robotic arm: