April 10th, 2024
The Project
The objective of this project was to develop a microcontroller-based device capable of rapid 3D spatial mapping. Using a Time-of-Flight (ToF) sensor attached to a stepper motor, the device achieves precise 360° spatial scanning and visualizes the captured data using Python libraries and PC connectivity.
My Role
This project was completed entirely independently. My responsibilities included programming the ARM architecture-based microcontroller, wiring and connecting the hardware, and conducting thorough testing.
Key Features
-
Rapid 3D Spatial Mapping: Utilizes the VL53L1X ToF sensor for precise distance measurements up to 4 meters, with a high ranging frequency of up to 50Hz.
-
Intuitive Operation: Features a simplified user interface with dedicated buttons for starting and resetting the scanner.
-
Full 360° Scanning: Employs a 28BYJ-48 Stepper Motor with 2048-step precision, controlled in Full Step mode, for accurate positioning and smooth operation.
-
Advanced Data Capture: Ensures efficient I²C communication for seamless data transfer and enhanced data capture.
-
PC Connectivity: Establishes serial communication with a Python interface for easy data transfer and visualization.
-
Microcontroller Efficiency: Powered by an MSP432E401Y microcontroller operating at 60MHz for optimized performance.
Programming the Microcontroller
The microcontroller and its functionality were programmed in C using the Keil software development environment, designed specifically for ARM Cortex-M based microcontroller devices. The program instructs the microcontroller to wait for a button input and then rotate the stepper motor 360°. Every 11.25°, the ToF sensor takes a scan. On the next button input, the stepper motor rotates 360° in the opposite direction, taking scans at the same intervals. Here is a large portion of the code used.
//*************************************************//
//*************** Output Function *****************//
//*************************************************//
// Variable Definitions
int on = 0; // Variable to determine if user wants to start scan, 1 to start scan
int CW = 1; // CW = 0 means clockwise, CW = 1 means counter clockwise
uint16_t z_coord = 0; // Initial z coordinate that will be changed after every full scan
uint16_t z_interval = 100; // Z interval is defined by the distance change after each step in mm [CHANGE FOR NEW INTERVAL]
float degree = 0; // Degree value that is changed every measurement
while(1) {
int input = GPIO_PORTM_DATA_R &= 0xF;
if((input) == 1){
SysTick_Wait10ms(5);
while((GPIO_PORTM_DATA_R &= 0xF) != 0){}
if((input & 0b00000001)==1){ // Use polling to check for button input
if (on==0){ // If on == 0, set on = 1, and vice-versa
on = 1;
} else {
on = 0;
}
}
SysTick_Wait10ms(5);
}
if ((on==1)){ // if on and step count is less than 360 degree rotation
int rotation_Step = 512;
GPIO_PORTF_DATA_R ^= 0b00010000; // Turn on PF4 LED (indication that currently in scan)
if (CW == 0){
// Clockwise
degree = 0;
for (int i=0; i<=rotation_Step; i++){ // rotate for 360 degrees
uint32_t delay = 1;
if((i%(rotation_Step/32)) == 0){ // If at 11.25 degrees, measure distance
GPIO_PORTF_DATA_R ^= 0b00000001; // Turn on PF0 LED (indication that measurement is being taken)
GPIO_PORTF_DATA_R ^= 0b00000010; // Turn on PF1 for bus speed inspection
status = VL53L1X_GetDistance(dev, &Distance); // Capture measured distance value
status = VL53L1X_ClearInterrupt(dev); // Clear interrupt has to be called to enable next interrupt
// print the resulted readings to UART [Distance, Z Coordinate, Degree]
sprintf(printf_buffer,"W,%u,%u,%f\r\n", Distance, z_coord, degree);
UART_printf(printf_buffer);
degree += 11.25;
}
// Spin motor
GPIO_PORTH_DATA_R = 0b00000011;
SysTick_Wait10ms(delay);
GPIO_PORTH_DATA_R = 0b00000110;
SysTick_Wait10ms(delay);
GPIO_PORTH_DATA_R = 0b00001100;
SysTick_Wait10ms(delay);
GPIO_PORTH_DATA_R = 0b00001001;
SysTick_Wait10ms(delay);
if((i%(rotation_Step/32)) == 0) {
GPIO_PORTF_DATA_R ^= 0b00000001; // Turn off PF0 LED
GPIO_PORTF_DATA_R ^= 0b00000010; // Turn off PF1 for bus speed
}
}
CW = 1; // Switch directions
} else {
// Counter Clockwise
degree = 348.75;
for (int i=0; i<=rotation_Step; i++){ // rotate for 360 degrees
uint32_t delay = 1;
if((i%(rotation_Step/32)) == 0){ // If at 11.25 degrees, measure distance
GPIO_PORTF_DATA_R ^= 0b00000001; // Turn on PF0 LED (indication that measurement is being taken)
status = VL53L1X_GetDistance(dev, &Distance); // Capture measured distance value
status = VL53L1X_ClearInterrupt(dev); // Clear interrupt has to be called to enable next interrupt
// print the resulted readings to UART [Distance, Z Coordinate, Degree]
sprintf(printf_buffer,"W,%u,%u,%f\r\n", Distance, z_coord, degree);
UART_printf(printf_buffer);
degree -= 11.25;
}
// Spin motor
GPIO_PORTH_DATA_R = 0b00001001;
SysTick_Wait10ms(delay);
GPIO_PORTH_DATA_R = 0b00001100;
SysTick_Wait10ms(delay);
GPIO_PORTH_DATA_R = 0b00000110;
SysTick_Wait10ms(delay);
GPIO_PORTH_DATA_R = 0b00000011;
SysTick_Wait10ms(delay);
if((i%(rotation_Step/32)) == 0) {
GPIO_PORTF_DATA_R ^= 0b00000001; // Turn off PF0 LED
}
}
CW = 0; // Switch directions
}
GPIO_PORTF_DATA_R ^= 0b00010000; // Turn off PF4 LED
on = 0; // Turn off
z_coord += z_interval; // Adjust z coordinate
}
}
UART and Python Integration
Via UART communication, the PC can read the output from the microcontroller. A Python program then manipulates this data. The microcontroller sends each scan's data in the format [Distance, Z Coordinate, Degree], where the distance is the measurement from the ToF sensor to a surface, and the Z Coordinate is the horizontal displacement from the last scanned spot. The Python program uses the Open3D library to create a point cloud from the scan data, constructing a visual representation by connecting each frame with lines. This segment of the final program below covers the UART communication with the microcontroller.
# Final Program
# Written by Ayaan. K
# 2024-04-01
import serial
import math
import numpy as np
import open3d as o3d
filename = "coordinates.xyz"
s = serial.Serial()
s.baudrate = 115200
s.port = 'COM3'
s.timeout = 10
s.open()
print("Opening: " + s.name)
# reset the buffers of the UART port to delete the remaining data in the buffers
s.reset_output_buffer()
s.reset_input_buffer()
# wait for user's signal to start the program
input("Press Enter to start communication...")
# send the character 's' to MCU via UART
# This will signal MCU to start the transmission
s.write(b's')
data = []
matchflag = True
setsOfScan = 3 # change depending on how many scans going to do, 3 for demo
w_count = 0
# recieve characters from UART of MCU
while (matchflag == True):
x = s.readline() # read one byte
dec = x.decode()
print(dec)
if (len(dec)>0):
if (dec[0] == 'W'): # if leading character is 'W' add to data array
data.append(dec)
w_count += 1
if (dec[0] == "X"): # if want to implement stop button in the future
matchflag = False
if (w_count == (setsOfScan*32)):
matchflag = False
# the encode() and decode() function are needed to convert string to bytes
# because pyserial library functions work with type "bytes"
#close the port
print("Closing: " + s.name)
s.close()
This segment covers the data manipulation into readable point cloud data for Open3D.
print(data)
# Parse each item in data array to get [Distance, Z Coordinate, Degree]
pcoords = []
for item in data:
subitem = item.split("\r")
coord = subitem[0].split(",")
pcoords.append([int(coord[1]), int(coord[2]), float(coord[3])])
print(pcoords)
# Create new array and add [x,y,z] coordinates for each point
coords = []
for coord in pcoords:
print("Degree: ", coord[2])
temp = [coord[0]* math.cos(math.radians(coord[2])), coord[0] * math.sin(math.radians(coord[2])), coord[1]]
coords.append(temp)
print(coords)
# Flip every odd set because every odd set is taken in a different direction
# Iterate over the list in steps of 32
for i in range(0, len(coords), 32):
# Check if the current set is odd
if i // 32 % 2 == 1: # If the index divided by 32 has a remainder of 1, it's an odd set
# Reverse the sublist of 32 values
coords[i:i+32] = reversed(coords[i:i+32])
print(coords)
f = open(filename, "w")
# Write data to .xyz file
for coord in coords:
f.write("{x} {y} {z}\n".format(x = coord[0], y = coord[1], z = coord[2]))
f.close()
print("Done")
This final segment is the Open3D processing of the data and the creation of the visual render.
# Get Point cloud data from file
point_cloud_data = o3d.io.read_point_cloud(filename, format="xyz")
# Print out data to make sure it exists
print("Point cloud array: ")
print(np.asarray(point_cloud_data.points))
# Display the data
print("Spawining separate window with point cloud: ")
o3d.visualization.draw_geometries([point_cloud_data])
# Define connections
lines = []
y = 0
num_scans = 32
for x in range(len(coords)):
print(y, ((len(coords)/num_scans)-1))
if (((x+1)%num_scans) != 0) or (x==0):
print("General case: ", x)
lines.append([[x], [x+1]])
else:
print("Special Case: ", x)
lines.append([[x], [0+(y*num_scans)]])
y += 1
print(y)
if(y < ((len(coords)/num_scans)-1)):
print("Connecting Lines: ", x, (x+num_scans))
lines.append([[x], [x+num_scans]])
line_set = o3d.geometry.LineSet(points=o3d.utility.Vector3dVector(np.asarray(point_cloud_data.points)),
lines=o3d.utility.Vector2iVector(lines))
print("Spawining separate window with lines")
o3d.visualization.draw_geometries([line_set])
Hardware
Constructing the device required a thorough understanding of the microcontroller. Below is the circuit schematic used in the project.

Final Product
Below are images comparing the line visualizations created using Open3D with photographs of the actual area. Also included is an in-depth technical report and datasheet of the final product.
