BLDC is a type of motor I’ve encountered during my recent studies. It differs greatly from the commonly used TT motors and DC encoder motors. Its most prominent advantage is high precision, enabling highly accurate positioning. That’s why it is widely used in robotics and industrial control. This does not mean brushless motors are absent from regular consumer applications; on the contrary, they are quite common in consumer products such as electric toothbrushes, hair dryers, and pan-tilt security cameras.
My project was inspired by pan‑tilt security cameras. I happened to have the motor assembly from a reCamera Gimbal on hand, but it came without a controller. I repurposed it and used two MCUs to implement the motor controller and the recognizer respectively.
The motor controller here is not a motor driver, since the motor I have already integrates the driver. However, it is driven via the CAN bus, which means I need a CAN-capable device along with a transceiver. Fortunately, the newly released XIAO STM32 series from Seeed Studio comes with CAN support and an onboard transceiver, and no terminating resistor configuration is required! Therefore, I decided to use the XIAO STM32C5 as the device to send CAN signals, serving as the motor controller. Meanwhile, to keep the form factor compact, I picked another MCU from the XIAO family — the camera-equipped XIAO ESP32S3 Sense, even though it runs really hot.
The two XIAO-series MCUs communicate with each other over UART. Besides, to enable remote wireless viewing, I use Wi‑Fi to transmit the video stream from the XIAO ESP32S3 Sense, so I can watch it on a mobile device. After sorting out this plan, I started implementation. Below are my implementation steps, and I hope they can give you some ideas.
HardwareThe most expensive component in this build is the motor, not the XIAO boards. Fortunately, I repurposed discarded parts, so the motor cost me almost nothing. If you need to buy a motor and will only use it occasionally, you can opt for low-cost CAN-compatible motors.
Apart from the motor, you will need one XIAO ESP32S3 Sense and one XIAO STM32C5, as well as an XT30 adapter board with CAN. This adapter combines power lines and CAN bus communication lines, which connects to a single cable carrying both CAN signals and power.
Next comes the power supply. A 12 V DC supply powers the motor, and a 5 V DC supply powers the XIAO boards. Ideally, you can use one 12 V DC supply paired with a 12 V-to-5 V converter to run everything from a single source. I only had an adjustable power supply, so the XIAO modules are powered via USB instead. It is important to maintain a common ground.
For this project, I split image processing and motor control between the two XIAO boards. The XIAO ESP32S3 Sense is developed with ESP-IDF and connected to an OV2640 camera. It captures frames, performs on-device face detection, and streams video along with a control panel to a web browser over Wi‑Fi. Once you open the webpage, you can view detection bounding boxes, manually adjust the gimbal using virtual joysticks and sliders, or enable AUTO mode. In AUTO mode, the program calculates where the gimbal should rotate based on the face’s position in the frame.
The XIAO STM32C5 is programmed under PlatformIO IDE with Zephyr RTOS. This board does not process video. Its main jobs are receiving target angles, reading motor feedback, and driving the yaw and pitch motors over the CAN bus. One easy-to-miss detail here: the CAN transceiver has an STB standby pin. The program must pull PB14 low first for the transceiver to function properly.
D6 and D7 are cross-wired as UART pins between the two boards. The ESP32‑S3 sends target angles to the STM32C5, and the STM32C5 sends back the motors’ actual angles and status. This is how the motor angles shown on the webpage are obtained. The whole workflow can be summarized simply: the ESP32‑S3 “sees” and decides the target position, while the STM32C5 commands the motors to turn and feeds back the actual movement result.
void app_main()
{
esp_err_t e = nvs_flash_init();
if (e == ESP_ERR_NVS_NO_FREE_PAGES || e == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
nvs_flash_erase();
nvs_flash_init();
}
frame_mux = xSemaphoreCreateMutex();
control_mux = xSemaphoreCreateMutex();
uart_tx_queue = xQueueCreate(16, sizeof(QueuedTarget));
ESP_ERROR_CHECK((frame_mux && control_mux && uart_tx_queue) ? ESP_OK : ESP_ERR_NO_MEM);
uart_config_t u{.baud_rate = 115200, .data_bits = UART_DATA_8_BITS, .parity = UART_PARITY_DISABLE, .stop_bits = UART_STOP_BITS_1, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, .source_clk = UART_SCLK_DEFAULT};
ESP_ERROR_CHECK(uart_driver_install(UART_NUM_1, 2048, 0, 0, nullptr, 0));
ESP_ERROR_CHECK(uart_param_config(UART_NUM_1, &u));
ESP_ERROR_CHECK(uart_set_pin(UART_NUM_1, GPIO_NUM_43, GPIO_NUM_44,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_LOGI(TAG, "BUILD uart-duplex-v3: UART1 D6/GPIO43 TX D7/GPIO44 RX 115200 8N1");
ESP_ERROR_CHECK(camera_init());
web_init();
xTaskCreatePinnedToCore(uart_tx_task, "uart_tx", 3072, nullptr, 7, nullptr, 1);
xTaskCreatePinnedToCore(uart_rx_task, "uart_rx", 4096, nullptr, 7, nullptr, 1);
xTaskCreatePinnedToCore(capture, "capture", 4096, nullptr, 5, nullptr, 0);
xTaskCreatePinnedToCore(infer, "face_detect", 12288, nullptr, 4, nullptr, 1);
}Reference tutorial :
3D ShellFor the 3D-printed enclosure around the motors: I am using the original housing of the reCamera Gimbal, so it fits my motors directly. The STL files for this housing are open-source online, which means you can print them yourself. Alternatively, you can measure the outer dimensions of your two motors and design a custom enclosure from scratch, though this will take quite a lot of time.
Picture1
Picture2














Comments