I’ve been experimenting with the idea of using a Neato LIDAR module paired with a BeagleBone Black to form the core of an autonomous wheeled robot. This would be done using a SLAM algorithm running on the the BeagleBone Black, with the LIDAR providing the robot’s vision. So far, I’ve been able to hook up the LIDAR to one of the serial ports of the BeagleBone and read the ranging data that it outputs.
Enabling a Serial Port on the BeagleBone Black
On the BeagleBone Black, each I/O pin can be used for multiple purposes (but not at the same time). This means that before using a pin, we need to be sure it is in the correct mode. The mode can be set using device tree overlays. In this case I just wanted to enable the RX and TX pins for the 2nd serial port, which was easy to do by running the following commands from a terminal connection to the BeagleBone. Note that I’m using a Revision C BeagleBone Black with the Debian OS that came pre-installed.
cd /sys/devices/bone_capemgr.*
echo BB-UART2 > slots
After doing this, I ran cat slots
to verify that it had been setup correctly. I also used dmesg
to check the logs and make sure there wasn’t anything unusual.
I originally tried this using the 1st serial port (BB-UART1), and while everything looked fine initially, things didn’t seem quite right when I ran the following commands to check the pin modes.
cd /sys/kernel/debug/pinctrl/44e10800.pinmux
cat pingroups
cat pins
Running cat pingroups
told me which pin numbers were being used for the serial port, and cat pins
provided a list of the pins along with the mode that each one was in. For BB-UART1, it turned out that both the RX and TX pins were set to 0x20, which is pin mode 0, in input mode (note: reference the BeagleBone Black documentation for an explanation of the different pin modes). This didn’t seem right, as the TX pin should be in output mode. When I switched to BB-UART2, the pin modes for the RX and TX pins were 0x21 (input, mode 1), and 0x01 (output, mode 1), which is what I expected.
The Neato LIDAR uses a baud rate of 115200, so it is important to set that correctly like so:
stty -F /dev/ttyO2 115200
Wiring the Neato LIDAR
I connected the 4-wire strip from the Neato LIDAR to the BeagleBone Black as follows (red wire last):
Neato LIDAR | BeagleBone Black |
---|---|
Black Wire (GND) | P9_1 or P9_2 (both ground) |
Orange Wire (LDS_TX) | P9_22 (uart2_rxd) |
Brown Wire (LDS_RX) | P9_21 (uart2_txd) |
Red Wire (+5V) | P9_5 or P9_6 (both VDD_5V) |
Connecting the motor directly to a 3V power supply will cause it to spin at a speed within the acceptable range. Later on, I plan to drive the motor with a PWM signal and use the rpm data provided by the serial connection to keep the speed consistent, but this is good enough for now.
Testing the Serial Connection
At this point, I ran cat /dev/ttyO2
and spun the LIDAR by hand to verify that the connection was working. Of course, this only produced random characters since the data was encoded. Next, I used screen /dev/ttyO2 115200
to open a serial connection that would allow me to send commands to the LIDAR. For example, typing getversion
returns some basic information about the unit. There are a number of other useful commands for testing and calibration, which can be found by typing help
. Be warned that some of these might brick the device if used improperly. There is also some documentation for this on the xv11hacking website.
Reading Ranging Data
To view the data as hex values, I ran the following Python script while spinning the LIDAR motor manually.
print("Hello")
with open("/dev/ttyO2", "rb") as f:
byte = f.read(1)
count = 1
while byte != "" and count <= 22*100:
print(byte.encode('hex') + ":")
byte = f.read(1)
count += 1
print("Goodbye")
This produced the expected output of 22 bytes per packet, as detailed here. There were a few bad packets, but not enough to have much of an effect. However, the 4 data reading sections of each packet were invalid. Most of the data reading hex values were 50 80 00 00
, but there were also a few that were 52 80 00 00
. The second byte’s value of 80 is what indicates that the data is invalid in both of these cases. The first byte is apparently an error code, but I haven’t been able to find out what the error codes 50 and 52 mean.
After a bit of head scratching, I finally figured out that the data was invalid because the LIDAR was not spinning at the proper rate. Powering the LIDAR’s motor with 3V and updating my Python script to capture 1000 packets instead of 100 produced good data. Capturing only 100 packets simply didn’t give the LIDAR time to get up to speed.
Valid Sample Data:
fa:a0:27:4b:97:01:bb:01:97:01:b4:00:98:01:53:00:99:01:93:00:4e:28:
fa:a1:35:4b:9a:01:b9:01:9a:01:8b:02:9a:01:71:02:9b:01:8b:02:a6:5f:
fa:a2:35:4b:9c:01:90:02:9d:01:89:02:9e:01:72:02:a0:01:8a:02:d8:16:
fa:a3:35:4b:a1:01:85:02:a2:01:7d:02:a4:01:76:02:a7:01:6a:02:aa:16:
fa:a4:35:4b:a8:01:5c:02:aa:01:78:02:ad:01:6f:02:af:01:4b:02:bb:10:
fa:a5:35:4b:b2:01:57:02:b4:01:61:02:a8:01:c3:00:97:01:9a:00:96:0b:
fa:a6:35:4b:35:80:00:00:79:01:ab:00:21:80:ab:00:61:01:f9:00:7a:08:
fa:a7:5b:4b:55:01:26:01:4a:01:44:01:41:01:41:01:37:01:45:01:78:16:
fa:a8:5b:4b:2f:01:77:01:26:01:8b:01:1f:01:9b:01:17:01:a4:01:23:18:
Visualizing the Data
The only thing left at this point was to graph some of the sample data so that I could determine how accurate the Neato LIDAR’s readings were. For this part I used a Python script on my laptop.
import numpy as np
import matplotlib.pyplot as plt
def getDist(lsb, msb):
if (msb == "80" or msb == "70"):
return False
return int(msb + lsb, 16)/1000.
def addPoint(dist, theta, x, y):
if (dist != False and dist < 6):
print(dist)
x.append(dist*np.cos((theta+90) * np.pi/180))
y.append(dist*np.sin((theta+90) * np.pi/180))
x=[]
y=[]
with open("C:\Users\Alex\Desktop\lidardata.txt") as f:
line = f.readline()
while line != "":
bytes = line.strip(':n').split(':')
if (len(bytes) == 22):
theta = (int(bytes[1], 16) - 160) * 4
# print(line)
print(theta)
addPoint(getDist(bytes[4], bytes[5]), theta, x, y)
addPoint(getDist(bytes[8], bytes[9]), theta + 1, x, y)
addPoint(getDist(bytes[12], bytes[13]), theta + 2, x, y)
addPoint(getDist(bytes[16], bytes[17]), theta + 3, x, y)
line = f.readline()
fig = plt.figure()
ax = fig.gca()
ax.set_xticks(np.arange(-10,10,0.2))
ax.set_yticks(np.arange(-10,10.,0.2))
ax.set_aspect('equal')
plt.scatter(x,y)
plt.grid()
plt.show()
Running this script against a small sample of the data collected earlier yielded the graph below. Some sections were missed, but the measurements that were captured are surprisingly accurate (plus or minus a few centimeters in most cases). Note that the measurements shown on the graph are in meters.
Further Reading
For more information about the Neato LIDAR sensor, I recommend that you read the following posts:
- xv11hacking – LIDAR Sensor
- Neato’s LiDAR Module – Wayne’s Tinkering Page
- Steven’s Robotics Blog: Neato XV-11 LDS menu test
For more information about the BeagleBone Black serial ports, see the following links. Some the information in these posts may only be applicable to certain versions of the BeagleBone Black.
- Exporting and Unexporting an Overlay | Introduction to the BeagleBone Black Device Tree | Adafruit Learning System
- Beaglebone Coding 101: Using the Serial and Analog Pins | GigaMegaBlog
- BeagleBone: Serial Ports and XBees – Jerome Bernard Blog
- Enable serial/UART/tty on BeagleBone Black | Hipstercircuits
Permalink
I tried to replicate your experiment as I’m interested in building a similar robot. I was able to communicate with the Lidar and receive the correct header (info about the Lidar), but I have problems getting valid data packets: most of the packets have 22 bytes, I get the ‘fa’ correctly, indexes are ok, but the speed and the 4 groups of data as invalid, the speed is either too low or too high. I assume that the speed may not be correct, but I tried multiple Lidar’s and various sources of 3.3V for the motor.
Do you have any idea what I may do wrong or what else to try?
I’m very beginner in Linux, so I may need beginner type instructions for Linux.
Permalink
I’m not sure how many data packets you are capturing total, but one thing that you could try is capturing more packets to see if it just needs time to get up to speed and stabilize. I initially was capturing only 100 packets, but that’s only a little over one revolution so it didn’t work well. When I captured 1000 packets and checked the data towards the end it was fine. Depending on your situation it’s possible that you might need more than that.
If that still doesn’t work, you might need to PWM the LIDAR motor to get it to run at the correct speed. Going even further, you could write some code to adjust the PWM signal based on the speed data that the LIDAR sends over the serial connection in order to hold the RPMs steady. Just keep in mind that the PWM signal from the BeagleBone Black probably can’t drive the motor directly. I am planning to write a blog post on this topic at some point in the future.
Note: I also get an occasional packet that doesn’t have 22 bytes. It doesn’t cause much of an issue as I just skip over those.
Permalink
Your first script with some modifications of my own:
“`
import serial
COM_PORT = “/dev/cu.usbserial-A102GPP8” # I’m on Mac OS X
BAUD_RATE = 115200
HEADER_BYTE = ‘fa:’
PACKET_BYTE_COUNT = 22
def main(num_data = 1000, file_name = “lidar_data.dat”):
print(“Establishing connection to XV-11 LiDAR via ” + COM_PORT)
lidar_serial = serial.Serial(port = COM_PORT, baudrate = BAUD_RATE, timeout = 1)
lidar_data = list()
count = 1
hex_reading = ”
while iter_counter <= num_data:
raw_data = str()
while hex_reading != HEADER_BYTE: # Wait for a header byte
byte = lidar_serial.read(1)
hex_reading = get_hex(byte)
raw_data += hex_reading
byte_counter = 1 # Initialize counter
while byte_counter < PACKET_BYTE_COUNT:
byte = lidar_serial.read(1)
hex_reading = get_hex(byte)
# Abandon this packet if you encounter another header byte too soon
if hex_reading == HEADER_BYTE:
break
raw_data += hex_reading
byte_counter += 1
if len(raw_data) == 3*PACKET_BYTE_COUNT:
# This is a proper packet, use it
lidar_data.append(raw_data)
print raw_data
iter_counter += 1
print("Goodbye")
return lidar_data
“`
Permalink
Oops, something’s missing
def get_hex(byte):
return byte.encode(‘hex’) + “:”
Permalink
And `count` should be `iter_counter ` asdf