Our Pololu QTR-8RC sensor has taken a lot of work to get consistent readings and line detection working from the Pi directly, we’ve persevered with it and IT WORKS! We’ve yet to find a good working example running directly from the Raspberry Pi so we hope this post will help others who are also trying to use the module. We now have consistent readings and calibration running directly off GPIO and Python. We have the code interfaced to our PID and motor control code and now Mr Bit is navigating our basic line track.
Firstly the mounting onto Mr Bit, we designed a basic bracket and printed it on our 3d printer, we have strengthened the bracket so it won’t shake with fluted joints and have created a slot on the arm of the bracket so we can fix it with 2 bolts – allowing for extension as we’d like to test with it extended at the front of Mr Bit.

Drawing the bracket in Tinkercad
The wires fit through the bracket and under the pi, coming out at the side to plug into the GPIO. We learned how to crimp wires and insert into headers and pins over the past few weeks and we’ve wired 8 wires into a single block for all but the VCC which is positioned away from the rest of the GPIO pins we’re using.

11 wires from the qtr-8rc plug directly into the GPIO (underneath the GrovePi+)
This single block is an achievement in itself for us and we’re glad to have discovered/learnt crimping – it means we can very easily plug and unplug the sensor (we’ve had to cut the plastic housing of the block to fit underneath the GrovePi which we’re using for our other sensors etc).

We’ve tried to make the wiring as neat as possible, Mr Bit looking like a real robot!
The code we’ve written is python using wiringpi and we’ve (more-or-less) transposed the code from the Pololu QTR-8Rx CPP library – concentrating on the parts that are used specifically for the capacitor discharge version we have rather than the analog version. We previously posted about this in a basic form and now have calibration and read line functions working – there are a few tweaks as the time taken to run the python code on the Pi with all other system processes competing in the CPU means it’s not quite as responsive as a microcontroller.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
# Python QTR-8RC sensor library # Atuhor: Tom Broughton (@dpolymath) # Date: 20/03/2017 # # Reads the Pololu QTR-8RC IR sensor array # https://www.pololu.com/docs/pdf/0J12/QTR-8x.pdf # # Hardware setup (changing pins can be managed in init): # LEDON pin connected to GPIO 21 # 3V3 to Pi GPIO rail and GND (bypass soldered on qtr-8rc for 3V3 option) # Pins 22 - 29 to each IR LED/phototransitor pair # - This version does not work with PWM for LEDON_PIN # # ############################################################################# # MIT License # # Copyright (c) 2017 Tom Broughton # # Permission is hereby granted, free vof charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. ############################################################################### import wiringpi as wp class MrBit_QTR_8RC: """ Class for reading values from Pololu QT8-8RC sensor array. Requires wiringpi https://github.com/WiringPi/WiringPi-Python """ def __init__(self): """ Initialises class constants and variables - pins defined here. """ self.wp = wp self.wp.wiringPiSetup() self.LEDON_PIN = 21 self.SENSOR_PINS = [22, 26, 23, 27, 24, 28, 25, 29] self.NUM_SENSORS = len(self.SENSOR_PINS) self.CHARGE_TIME = 10 #us to charge the capacitors self.READING_TIMEOUT = 1000 #us, assume reading is black self.sensorValues = [] self.calibratedMax = [] self.calibratedMin = [] self.lastValue = 0 self.init_pins() def init_pins(self): """ Sets up the GPIO pins and also ensures the correct number of items in sensors values and calibration lists to store readings. """ for pin in self.SENSOR_PINS: self.sensorValues.append(0) self.calibratedMax.append(0) self.calibratedMin.append(0) self.wp.pullUpDnControl(pin, self.wp.PUD_DOWN) self.wp.pinMode(self.LEDON_PIN, self.wp.OUTPUT) def emitters_on(self): """ Turns the LEDON pin on so that the IR LEDs can be turned on. If there is nothing wired to LEDON emitters will always be on. Use emitters_on and emitters_off to conserve power consumption. """ self.wp.digitalWrite(self.LEDON_PIN, self.wp.HIGH) self.wp.delayMicroseconds(20) def emitters_off(self): """ Turns the LEDON pin off so that the IR LEDs can be turned off. If there is nothing wired to LEDON emitters will always be on. Use emitters_on and emitters_off to conserve power consumption. """ self.wp.digitalWrite(self.LEDON_PIN, self.wp.LOW) self.wp.delayMicroseconds(20) def print_sensor_values(self, values): """ Params: values - a list of sensor values to print Prints out the sensor and it's current recorded reading. """ for i in range(0, self.NUM_SENSORS): print("sensor %d, reading %d" % (i, values[i])) def initialise_calibration(self): """ Resets (inverse) max and min thresholds prior to calibration so that calibration readings can be correctly stored. """ for i in range(0, self.NUM_SENSORS): self.calibratedMax[i] = 0 self.calibratedMin[i] = self.READING_TIMEOUT def calibrate_sensors(self): """ Takes readings across all sensors and sets max and min readings typical use of this function is to call several times with delay such that a total of x seconds pass. (e.g. 100 calls, with 20ms delays = 2 seconds for calibration). When running this move the sensor over the line several times to calbriate contrasting surface. """ for j in range(0, 10): self.read_sensors() for i in range(0, self.NUM_SENSORS): if self.calibratedMax[i] < self.sensorValues[i]: self.calibratedMax[i] = self.sensorValues[i] if self.calibratedMin[i] > self.sensorValues[i] and self.sensorValues[i] > 30: self.calibratedMin[i] = self.sensorValues[i] def read_line(self): """ Reads all calibrated sensors and returns a value representing a position on a line. The values range from 0 - 7000, values == 0 and values == 7000 mean sensors are not on line and may have left the line from the right or left respectively. Values between 0 - 7000 refer to the position of sensor, 3500 referring to centre, lower val to the right and higher to the left (if following pin set up in init). """ self.read_calibrated() avg = 0 summ = 0 online = False for i in range(0, self.NUM_SENSORS): val = self.sensorValues[i] if val > 500: online = True if val > 50: multiplier = i * 1000 avg += val * multiplier summ += val if online == False: if self.lastValue < (self.NUM_SENSORS-1)*1000/2: return 0 else: return (self.NUM_SENSORS-1)*1000 self.lastValue = avg/summ return self.lastValue def read_calibrated(self): """ Reads the calibrated values for each sensor. """ self.read_sensors() print("uncalibrated readings") self.print_sensor_values(self.sensorValues) for i in range(0, self.NUM_SENSORS): denominator = self.calibratedMax[i] - self.calibratedMin[i] val = 0 if denominator != 0: val = (self.sensorValues[i] - self.calibratedMin[i]) * 1000 / denominator if val < 0: val = 0 elif val > 1000: val = 1000 self.sensorValues[i] = val print("calibrated readings") self.print_sensor_values(self.sensorValues) def read_sensors(self): """ Follows the Pololu guidance for reading capacitor discharge/sensors: 1. Set the I/O line to an output and drive it high. 2. Allow at least 10 us for the sensor output to rise. 3. Make the I/O line an input (high impedance). 4. Measure the time for the voltage to decay by waiting for the I/O line to go low. Stores values in sensor values list, higher vals = darker surfaces. """ for i in range(0, self.NUM_SENSORS): self.sensorValues[i] = self.READING_TIMEOUT for sensorPin in self.SENSOR_PINS: self.wp.pinMode(sensorPin, self.wp.OUTPUT) self.wp.digitalWrite(sensorPin, self.wp.HIGH) self.wp.delayMicroseconds(self.CHARGE_TIME) for sensorPin in self.SENSOR_PINS: self.wp.pinMode(sensorPin, self.wp.INPUT) #important: ensure pins are pulled down self.wp.digitalWrite(sensorPin, self.wp.LOW) startTime = self.wp.micros() while self.wp.micros() - startTime < self.READING_TIMEOUT: time = self.wp.micros() - startTime for i in range(0, self.NUM_SENSORS): if self.wp.digitalRead(self.SENSOR_PINS[i]) == 0 and time < self.sensorValues[i]: self.sensorValues[i] = time #Example ussage: if __name__ == "__main__": try: qtr = MrBit_QTR_8RC() approveCal = False while not approveCal: print("calibrating") qtr.initialise_calibration() qtr.emitters_on() for i in range(0, 250): qtr.calibrate_sensors() wp.delay(20) qtr.emitters_off() print "calibration complete" print "max vals" qtr.print_sensor_values(qtr.calibratedMax) print "calibration complete" print "min vals" qtr.print_sensor_values(qtr.calibratedMin) approved = raw_input("happy with calibrtion (Y/n)? ") if approved == "Y": approveCal = True except Exception as e: qtr.emitters_off() print str(e) try: while 1: qtr.emitters_on() print qtr.read_line() qtr.emitters_off() wp.delay(20) except KeyboardInterrupt: qtr.emitters_off() except Exception as e: print str(e) |
The read line function gives us a value between 1000 and 6999 when on the line, a 0 or 7000 when off and having left from the left or right of the sensor respectively. Firstly we run a calibration by spinning Mr Bit over the line and then a prompt on screen asks if we’re happy with the calibration, we’ve added this as 9 out of 10 times we are but then there are some outlying situations where the calibration is skewed so we’d rather check than rely on it being good every time.
After calibration we’re set to go. We have a set-point for our pid which is 3500 (centre of the sensor), a very low base speed and very low constants – in fact at the moment we’re just adjusting the Kp (proportional) value, setting the Kd sets Mr Bit off in jittery convulsions. We’ve only done a few tests so far and looking forward to working more on it this evening. Here he is in action:
We don’t want to end up coming off the course like at the and of that clip!
One of the issues we have at the moment is the course we’ve made. We did make one out of paper but it got chewed up too quickly. Rebelle and I went to Hobbycraft on Sunday and saw some white A1 foam board on offer and got 4 sheets of it. The board itself is great to lay out and draw a line on with insulation tape, the glossy coating means we should be able to peel off the tape and create other configurations. The problem is exactly this, the glossy surface, it’s too slippy and Mr Bit tends to slide over it, at speeds any faster than the lowest we can go he goes skidding around in circles – which is funny but makes him a bit dizzy. We’re looking to get or make some sticky wheels to add to our change set of wheels for different challenges.
Comments 2
Can you try with only 2 wheels design. 4 wheel will make harder turning. Plus if you have distance between sensors, wheels. Robot’s response time will catch and movements will be smoother. And sure with new SLT20 wheels will help to you too 🙂
Overall I only think about if robot’s mechanical design can be better, all movements will be better I guess.. Lower COG, and try with different wheel to wheel distances.
Author
Thanks for the suggestions Firat. We will see how many of those things we can try though I think lowering COG will need a redesign.
I like the 2 wheel idea not sure if we’ll be able to try that with this robot though.
Going to give the SLT20’s a go which arrived today! 🙂