In this lab we set up two VL53L1X Time-of-Flight distance sensors and an ICM-20948 IMU, learning to run both ToF sensors simultaneously (in my case, using XSHUT address reassignment), and sending that data over Bluetooth.
Since the default I2C address as 0x52, 8-bit (according to the datasheet), it'll be 0x29 in the 7-bit format that Arduino uses; essentially it means when the I2C scanner is run, it reports the sensor at address 0x29 (which it did, yay).
Both VL53L1X sensors have the same hardwired I2C address (0x29), so they cannot both be active on the bus at the same time without causing problems. I chose the XSHUT approach over toggling sensors on and off each read. So when you boot it up, the second sensor's XSHUT pin (wired to A2) is held LOW, keeping it off the bus. The first sensor initializes at 0x29, then its address is changed to 0x32. The XSHUT pin is then set HIGH, bringing the second sensor back online at the now-free 0x29. This only needs to happen once at startup, after which both sensors work perfectly. The alternative, which would be continuously enabling and disabling sensors using each an XSHUT would add latency and complexity every single cycle, which kind of defeats the purpose of fast sampling.
I plan to mount both ToF sensors on the front of the robot. This may be be a bad idea that I change later, but for now I want to have one configured for short-range mode and one for long-range mode. The short-range sensor will handle precise, close-distance obstacle detection so I can have really fast and accurate collision avoidance, while the long-range sensor will maybe provide earlier detection of objects farther ahead, which lets me have smoother speed control and actual path planning. Notably, obstacles that could still be missed include objects approaching from the rear or sides, also short obstacles beneath the sensor beam, or thin objects like wires that fall between where it measures in the field of view. Also, any targets beyond the maximum eff. range of the long distance ToF.
The Artemis connects to the QWIIC breakout board, which provides SDA/SCL/3.3V/GND to both ToF sensors and the IMU with chained QWIIC connectors through a center piece. The second ToF sensor's XSHUT pin has an additional wire soldered from it to pin A2 on the Artemis. This pin gave me a large headache, as originally I wanted it to go to A5, then I accidentally soldered it shut, then I tried to do A4 only to realize there is no A4, and then finally cut that off and used A2. Wire lengths were chosen with chassis placement in mind, trying to give myself as much space to work with when assembling later, especially a good length for the ToF sensors that have to go in the front.
The first ToF sensor was connected to the QWIIC breakout board and tested with the SparkFun ReadDistance example.
The VL53L1X has three distance modes. Short mode has a maximum range of a pretty short 1.3m but is less affected by ambient light and provides faster, more reliable readings at close range. Medium mode extends to 3m, and Long mode reaches 4m but is more susceptible to ambient light interference and has slower ranging times. I chose to work with Short mode for now since it's more important for the final robot. I may change my mind on the placement of the second sensor, so I don't want to waste time working with it until I'm certain. But anyway, our main task is object collision prevention so having a good, well calibrated, fast sensor, then that's the best way to work towards our goal.
I tested the sensor accuracy and repeatability by placing a flat surface at known distances and collecting a reading at each distance. The sensor was tested at 100mm intervals from 100mm to 1000mm. I didn't have a ruler but printer paper is 29.7cm, so I folded it into thirds, flattened them, and laid 4 out in a line. I used a box and moved it away in those increments.
To use two ToF sensors simultaneously, I followed the process I outlined in prelab. There we some issues for many days as I worked on this, as often it would fail to detect the second sensor. It was at this time I rewired it from A5 to A2, because the connection to A5 was lacking and then completely broken when I tried to fix it.
// XSHUT Mech
pinMode(XSHUT_PIN, OUTPUT);
digitalWrite(XSHUT_PIN, LOW); // keeps sensor 2 off
delay(10);
tof1.begin(); // sensor 1 start at 0x29
tof1.setDistanceModeShort();
tof1.setI2CAddress(0x32); // move sensor 1 to 0x32
digitalWrite(XSHUT_PIN, HIGH); // sensor 2 turns on at 0x29, we leave it there
delay(10);
tof2.begin();
tof2.setDistanceModeShort();
In order to measure how fast the main loop runs, I'm having the code print the Artemis clock as fast as possible and only prints ToF data when checkForDataReady() returns true. The loop itself runs extremely fast without the data since it's none blocking. As expected, the limitation is the ToF sensor ranging time. In Short mode, each sensor produces a new reading roughly every ~50ms (about 20 readings per second per sensor) which seems fast until you consider that the IMU samples much faster. The key with this model is that it doesn't check if the info is there, it just runs regardless.
while (millis() - start < 10000 && tof_count < BUF_LEN) {
bool got1 = false, got2 = false;
if (tof1.checkForDataReady()) {
tof1_buf[tof_count] = tof1.getDistance();
tof1.clearInterrupt();
got1 = true;
}
if (tof2.checkForDataReady()) {
tof2_buf[tof_count] = tof2.getDistance();
tof2.clearInterrupt();
got2 = true;
}
if (got1 || got2) {
tof_time_buf[tof_count] = millis();
tof_count++;
}
}
Time-stamped ToF data from both sensors was recorded into on-board buffers for 10 seconds, then transmitted over BLE to the laptop. Then, the data was parsed in the Jupyter notification handler and plotted with matplotlib.
# Jupyter: receive and parse ToF data
tof_time = []; tof1_data = []; tof2_data = []
def tof_handler(uuid, notif):
s = ble.bytearray_to_string(notif).strip()
if s == "TOF_DONE":
print("TOF done, received:", len(tof_time))
return
parts = s.split("|")
t = int(parts[0].split(":")[1])
a = int(parts[1].split(":")[1])
b = int(parts[2].split(":")[1])
tof_time.append(t); tof1_data.append(a); tof2_data.append(b)
ble.start_notify(ble.uuid["RX_STRING"], tof_handler)
ble.send_command(CMD.SEND_TOF, "")
IMU data (accelerometer and gyroscope) was recorded into buffers for 10 seconds and sent over BLE using the same buffered approach. The notification handler parses the T:|AX:|AY:|AZ:|GX:|GY:|GZ: format into separate lists.
# Jupyter: receive and parse IMU data
imu_time = []
ax_data = []; ay_data = []; az_data = []
gx_data = []; gy_data = []; gz_data = []
def imu_handler(uuid, notif):
s = ble.bytearray_to_string(notif).strip()
if s == "IMU_DONE":
print("IMU done, received:", len(imu_time))
return
parts = s.split("|")
vals = {}
for p in parts:
k, v = p.split(":")
vals[k] = float(v)
imu_time.append(int(vals["T"]))
ax_data.append(vals["AX"]); ay_data.append(vals["AY"])
az_data.append(vals["AZ"]); gx_data.append(vals["GX"])
gy_data.append(vals["GY"]); gz_data.append(vals["GZ"])
ble.start_notify(ble.uuid["RX_STRING"], imu_handler)
ble.send_command(CMD.SEND_IMU, "")
The buffered approach we were introduced to in Lab 1 was reused here and works quite well. However, for actual movement it needs to be different from this, which was optimized for collecting data. The sensors are sampled as fast as they produce data and THEN the results are stored in RAM, and then the entire dataset is transmitted over BLE AFTER recording ends. This notably avoids the BLE bottleneck during data collection and ensures maximum sampling rate but would NOT work real time. As an aside, for this lab I focused on short mode since I wasn't sure if I'd need long mode and didn't want to waste time calibrating if I change my mind. I do think my plan is solid, but I would like to note that almost no one else I've seen has something similar, which makes me doubtful.
Collaborations: As I turned this lab in late, I was able to observe the structure and problem solving of my friend's websites including Rajarshi Das, Clarence Dagins, and also past students like Brooke Hudson. I had to do a lot of debugging with GPT and Claude as combining my codes did not work well. I also used it to fix my pin issue, and to format the HTML of the website.