Skip to main content

NuttX for Motor Control and Sensing: IMU Integration and Wireless Data Streaming

·10 mins·
NuttX Apache ESP32 ESP32-C6 Motor IMU
Author
Filipe Cavalcanti
Embedded Software Engineer at Espressif
Table of Contents
This article extends the NuttX motor control system with inertial measurement unit (IMU) sensor integration for vibration measurement and wireless data streaming capabilities using the ESP32-C6 Wi-Fi connectivity.

Introduction
#

In the first part of this series, we built a real-time motor control and sensing system using NuttX RTOS on the ESP32-C6. We covered DC motor control using the MCPWM Peripheral, potentiometer-based speed adjustment via ADC, and speed measurement using a quadrature encoder. This setup provided a foundation for embedded motor control. Now, we start working on sensor integration.

In this second and final part, we extend the system with two major features:

  • Integration of the MPU6050 IMU sensor to measure vibrations
  • Wireless data streaming using the ESP32-C6 Wi-Fi capabilities, enabling real-time sensor data transfer to a host computer for visualization and analysis

By the end of this article, you will have a complete, connected motor control and sensing platform. The ESP32-C6 will not only control and monitor the motor, but also capture vibration data and stream it wirelessly for live plotting and further analysis on the receiver side. This demonstrates the power of NuttX and Espressif devices for building advanced, networked embedded applications on modern microcontrollers.

Hardware Requirements
#

This second part introduces two new elements to enhance the system:

  • Wi-Fi connectivity: The ESP32-C6 onboard Wi-Fi module enables wireless data streaming. A host computer with network connectivity is required to receive and visualize the sensor data in real time.

  • GY521 IMU module (MPU6050): Provides 3-axis accelerometer and gyroscope data for vibration measurement. Connects to the ESP32-C6 via I2C.

Set Up the Build Environment
#

Building wireless capabilities requires that we clear our environment, since some new files will have to be added to our build system. The following snippet shows how to clear the current NuttX build and merge the motor_sensing defconfig, allowing us to continue building from the first article scenario.

make distclean
./tools/configure.sh esp32c6-devkitc:nsh
kconfig-merge -m .config ../apps/external/motor_sensing/config/defconfig
make olddefconfig

Make sure you have the nuttx-esp-motor-sensing repository linked to nuttx-apps.

Wi-Fi Setup
#

To enable wireless data streaming, we need to configure the ESP32-C6 Wi-Fi settings. NuttX provides a ready-to-use Wi-Fi configuration that we can merge with our existing setup. We are going to use that instead of manually selecting all Wi-Fi-related options through menuconfig.

Use the following command to merge the Wi-Fi configuration with the current setup:

kconfig-merge -m .config boards/risc-v/esp32c6/esp32c6-devkitc/configs/wifi/defconfig
make olddefconfig

This command merges the Wi-Fi-specific configuration options into your current .config file, enabling networking support, Wi-Fi drivers, and socket APIs needed for wireless communication.

Now run make to build the firmware.

Wi-Fi Network Configuration
#

With Wi-Fi support enabled, you’ll need to configure the network connection. In the NuttX shell, use the wapi command to connect to your Wi-Fi network:

nsh> wapi psk wlan0 <mynetwork> <mypassword>
nsh> wapi essid wlan0 <mynetwork>
nsh> renew wlan0

Replace mynetwork and mypassword with your actual Wi-Fi network credentials. The renew command requests an IP address from your router using DHCP.

Once connected, verify your network configuration using ifconfig:

nsh> ifconfig
wlan0	Link encap:Ethernet HWaddr 60:ff:ff:ff:ff:ff at RUNNING mtu 576
	inet addr:10.42.0.199 DRaddr:10.42.0.1 Mask:255.255.255.0

A successful connection will show an assigned IP address. You can now ping a website to test the connection.

IMU (MPU6050) Setup
#

Measuring vibrations requires that we integrate the MPU6050 IMU with the ESP32-C6 using the I2C interface.

Enable I2C and MPU6050 Driver in NuttX
#

To enable the MPU6050 IMU, start by running make menuconfig in the NuttX project directory. Navigate to System Type → Peripheral Support and enable I2C0. On ESP32-C6 this should set GPIOs 6 and 5 as SCL and SDA pins, respectively. To verify, enter System Type → I2C Configuration to see the designated GPIOs.

Then, go to Device Drivers → Sensor Device Support and enable the Invensense MPU60x0 Sensor support driver. Make sure to also select the MPU60x0 I2C Interface option, instead of SPI. Once these options are set, save your configuration and proceed to build the firmware.

Verify Device Registration
#

To verify the IMU properly registered in the system, connect the IMU to the board and then build and flash the board:

make
make flash ESPTOOL_BINDIR=./ ESPTOOL_PORT=/dev/ttyUSB0

Open a serial terminal and check the available devices. You should now see /dev/imu0 among the other character drivers.

nsh> ls /dev
/dev:
 adc0
 console
 imu0
 motor0
 null
 pcnt0
 qe0
 random
 ttyS0
 zero

IMU Application
#

Now that we have our devices configured, we need an application that is able to read the accelerometer data and send it to our computer, wirelessly.

To accomplish this, we’ll need to create another program similar to motor_sensing of the first article. This program will read accelerometer data and open a socket that will be used to send the data.

The data transmitting part of this software will use a simple client/server socket, which is a widely available example for socket communication.

First, we need some definitions for the MPU6050 and for the socket:

#define REG_LOW_MASK    0xFF00
#define REG_HIGH_MASK   0x00FF
#define MPU6050_AFS_SEL 4096.0f   /* Accel scale factor */
#define SAMPLE_RATE_MS  20        /* 50 Hz sample rate */
#define TCP_PORT        5000      /* TCP port to send data */
#define MAX_MSG_SIZE    64        /* Maximum message size */
#define MAX_CLIENTS     1         /* Maximum number of clients */

Next, we need a data structure to hold the MPU6050 data. It must match the structure provided in the MPU6050 driver, which includes accelerometer, gyroscope, and temperature data.

struct mpu6050_imu_msg
{
  int16_t acc_x;
  int16_t acc_y;
  int16_t acc_z;
  int16_t temp;
  int16_t gyro_x;
  int16_t gyro_y;
  int16_t gyro_z;
};

This structure allows us to read the IMU through its file descriptor, which should be opened in the main function. On success, the raw IMU data must be converted to a usable value in units of g, which is achieved by concatenating the MSB and LSB registers to obtain a 16-bit integer and dividing the value by the AFS_SEL value, which is a scaling factor determined by the precision setting. More details about this can be found in the MPU6050 datasheet.

For the conversion function, we also need a simple structure to hold the processed accelerometer data:

struct sensor_accel
{
  float x;
  float y;
  float z;
};

The IMU reading function converts the raw sensor data to acceleration values in g-force units:

static void read_mpu6050(int fd, struct sensor_accel *acc_data)
{
  struct mpu6050_imu_msg raw_imu;
  int16_t raw_data[7];
  memset(&raw_imu, 0, sizeof(raw_imu));

  int ret = read(fd, &raw_data, sizeof(raw_data));
  if (ret != sizeof(raw_data))
    {
      printf("Failed to read IMU data\n");
      acc_data->x = 0;
      acc_data->y = 0;
      acc_data->z = 0;
      return;
    }

  /* Convert raw data */
  raw_imu.acc_x = ((raw_data[0] & REG_HIGH_MASK) << 8) +
                   ((raw_data[0] & REG_LOW_MASK) >> 8);
  raw_imu.acc_y = ((raw_data[1] & REG_HIGH_MASK) << 8) +
                   ((raw_data[1] & REG_LOW_MASK) >> 8);
  raw_imu.acc_z = ((raw_data[2] & REG_HIGH_MASK) << 8) +
                   ((raw_data[2] & REG_LOW_MASK) >> 8);

  /* Convert to g force */
  acc_data->x = raw_imu.acc_x / MPU6050_AFS_SEL;
  acc_data->y = raw_imu.acc_y / MPU6050_AFS_SEL;
  acc_data->z = raw_imu.acc_z / MPU6050_AFS_SEL;
}

Now, we create our main function which opens the IMU file descriptor, reads it and also establishes network communication.

The main function performs the following steps:

  1. Opens and checks if the IMU device is available
  2. Creates a TCP socket and waits for a connection from client
  3. Once a connection is established, accepts the client and starts reading IMU data
  4. Sends the data over the network connection
  5. Monitors for client disconnections and waits for reconnection when needed

This process repeats every 20 ms, as defined by the SAMPLE_RATE_MS value.

The following code is also available in the example repository.

int main(int argc, FAR char *argv[])
{
  int fd;
  struct sensor_accel acc_data;
  char msg_buffer[MAX_MSG_SIZE];
  
  /* Socket variables */
  int server_fd, client_fd;
  struct sockaddr_in server_addr, client_addr;
  socklen_t client_len = sizeof(client_addr);
  int opt = 1;
  
  printf("MPU60x0 Accelerometer Test\n");
  printf("Sample Rate: %d ms (%d Hz)\n", SAMPLE_RATE_MS, 1000/SAMPLE_RATE_MS);
  printf("TCP server starting on port %d\n", TCP_PORT);

  /* Open IMU device */
  fd = open("/dev/imu0", O_RDONLY);
  if (fd < 0)
    {
      printf("Failed to open imu0\n");
      return EXIT_FAILURE;
    }

  /* Create TCP socket */
  server_fd = socket(AF_INET, SOCK_STREAM, 0);
  if (server_fd < 0)
    {
      printf("Failed to create socket\n");
      close(fd);
      return EXIT_FAILURE;
    }

  /* Set socket options */
  if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR,
                 &opt, sizeof(opt)) < 0)
    {
      printf("Failed to set socket options\n");
      close(fd);
      close(server_fd);
      return EXIT_FAILURE;
    }

  /* Configure server address */
  memset(&server_addr, 0, sizeof(server_addr));
  server_addr.sin_family = AF_INET;
  server_addr.sin_addr.s_addr = INADDR_ANY;
  server_addr.sin_port = htons(TCP_PORT);

  /* Bind socket */
  if (bind(server_fd, (struct sockaddr *)&server_addr,
           sizeof(server_addr)) < 0)
    {
      printf("Failed to bind socket\n");
      close(fd);
      close(server_fd);
      return EXIT_FAILURE;
    }

  /* Listen for connections */
  if (listen(server_fd, MAX_CLIENTS) < 0)
    {
      printf("Failed to listen\n");
      close(fd);
      close(server_fd);
      return EXIT_FAILURE;
    }

  printf("Waiting for client connection...\n");
  
  /* Accept client connection */
  client_fd = accept(server_fd, (struct sockaddr *)&client_addr,
                    &client_len);
  if (client_fd < 0)
    {
      printf("Failed to accept client\n");
      close(fd);
      close(server_fd);
      return EXIT_FAILURE;
    }

  printf("Client connected\n");

  while (1)
    {
      read_mpu6050(fd, &acc_data);

      /* Format data as string with newline */
      snprintf(msg_buffer, MAX_MSG_SIZE, "%.3f,%.3f,%.3f\n",
               acc_data.x, acc_data.y, acc_data.z);

      /* Send data over TCP */
      ssize_t bytes_sent = send(client_fd, msg_buffer,
                               strlen(msg_buffer), 0);
      
      /* Check if client disconnected */
      if (bytes_sent <= 0)
        {
          printf("Client disconnected, waiting for new connection...\n");
          close(client_fd);
          
          /* Wait for new client */
          client_fd = accept(server_fd, (struct sockaddr *)&client_addr,
                           &client_len);
          if (client_fd < 0)
            {
              printf("Failed to accept new client\n");
              break;
            }
          printf("New client connected\n");
          continue;
        }

      printf("Accel (g): X=%.3f Y=%.3f Z=%.3f\n",
             acc_data.x, acc_data.y, acc_data.z);

      usleep(SAMPLE_RATE_MS * 1000);
    }

  /* Cleanup */
  close(fd);
  close(client_fd);
  close(server_fd);
  return EXIT_SUCCESS;
}

Client-Side Data Acquisition
#

Now that we have everything set up on our ESP32-C6, we just need a simple program to wirelessly connect to our board and obtain this data.

This client program can be built in many ways using Python’s sockets module, C sockets or even a bash script. For the sake of simplicity, we’ll use a bash script. The following snippet connects to our server using netcat and prints the received data.

#!/bin/bash

# Default connection parameters
HOST="10.42.0.199"
PORT=5000

# Check if custom host/port provided
if [ $# -eq 2 ]; then
    HOST=$1
    PORT=$2
fi

echo "Connecting to IMU server at $HOST:$PORT..."

# Connect to server and process data
nc $HOST $PORT | while IFS="," read -r x y z; do
    printf "X: %6s  Y: %6s  Z: %6s\r" "$x" "$y" "$z"
done

echo -e "\nConnection closed." 

Testing Application
#

With the IMU and socket program ready, we enable it through menuconfig on Application Configuration → Motor Sensing Apps → Motor IMU Data Example, build and flash the board.

The imu program should now be available on the NuttShell. Make sure you are connected to a Wi-Fi network.

Execute the IMU program on NuttX:

nsh> imu
MPU60x0 Accelerometer Test
Sample Rate: 20 ms (50 Hz)
TCP server starting on port 5000
Waiting for client connection...

Now run the client-side bash script. You should see data being received:

$ ./imu_client.sh 
Connecting to IMU server at 10.42.0.199:5000...
X:  1.168  Y: -0.077  Z:  0.145

Exiting the bash script and restarting should keep the data flowing with no issues.

Practical Use
#

To fulfill the initial proposal of measuring motor vibrations, we can now change the motor speed by moving the potentiometer and log this data for analysis.

The following figure shows the results of a simple test where the motor was stopped, then speed gradually increased and decreased until a full stop.

The data and script used to generate the plots are available on the example repository.

Conclusion
#

In this second part of the NuttX motor control series, we successfully transformed a basic motor control system into a complete networked sensing platform. Building on the foundation established in the first article, we’ve now added sensing and connectivity capabilities.

In summary, the achievements in this article include:

  • Wi-Fi connectivity integration: Configured and connected the ESP32-C6 to wireless networks using NuttX’s networking stack
  • IMU sensor integration: Implemented MPU6050 accelerometer support via I2C for vibration measurement
  • Wireless data streaming: Created a TCP socket-based system for streaming sensor data from the embedded device to a host computer
  • Cross-platform communication: Developed both embedded firmware and host-side client scripts for data exchange

The combination of both articles demonstrates how NuttX can be used to build advanced embedded systems that bridge the gap between traditional motor control and modern IoT applications. We started with basic motor control using familiar peripherals and evolved into a comprehensive system capable of wireless sensor data streaming, creating a complete embedded IoT platform.

Related Resources#

Related

NuttX for Motor Control and Sensing: MCPWM and DC Motor Control
·12 mins
NuttX Apache ESP32 ESP32-C6 Motor IMU
This article demonstrates how to implement motor control and speed sensing on an ESP32-C6 using NuttX RTOS. It covers setting up MCPWM for motor control, ADC for potentiometer reading, and quadrature encoder for speed measurement. The implementation showcases NuttX’s real-time capabilities through a practical example using common peripherals and sensors.
Testing applications with Pytest and NuttX
·11 mins
NuttX Apache ESP32 Pytest Testing
Getting Started with NuttX and ESP32
·8 mins
NuttX Apache ESP32 POSIX Linux