Line Robot - Line following


Visit this page to learn how to use Your mobile as a Bluetooth terminal.


Before studying line following we have to learn another function which we managed to avoid by now, but line sensor will not work without it. We already mentioned Arduino's setup() function and its ML-R analogon, which we called "constructor". As it turns out, this concept is not good enough for the functions like loop(), which may be started, run many times, stopped, then started again, etc. There may be some tasks which should be done each time the loop() (or similar functions) runs the first time, not just when the whole program starts. We may want to set some variables, start measurements, etc.

An example. Imagine we wanted variable "a" to have value "2" when the function loop() is run the first time. Provided global variables (declared outside any functions) are allowed, this is easy:

int a = 2;

void loop(){
In the first run, a will be available inside loop() and its value will be 2. However, global variables are a no-no for any decent programmer and we will not be using them. Instead, ML-R offers function setup(), which returns "true" value during the first run of any function similar to loop() and "false" later. The solution to our problem is here, in terms of setup():
void loop(){
	int a = 0;
	if (setup())
		a = 2;

Line following, a crude algorithm

Line following is a complicated task. In fact, it can be quite challenging. For now, we will consider only the simplest possibilities and our simple program will be able to track these simple lines.

Prepare a white (or some other bright) surface. Stick a black stripe, at least half a meter long. Even better, make a closed loop. The objective is to program the robot to move along the stripe.

Enter command "ref" to see how the sensor measures black and white values. If You put the stripe below the sensor, the results will look like in the picture left.

Values will not be the same, but You will notice that 1 or 2 adjacent numbers are smaller than the rest. Bigger reflected infrared light (white) yields higher number and smaller reflected (black) smaller numbers. Here, numbers around 300 designate the black stripe, which is under 3rd sensor. If we want the robot to follow the line, we will always force it to turn in a way that the line is in the middle of the sensor.

Find smallest values of all sensors (black stripe below) and highest (white below), when the surface is at normal distance (robot's wheels in contact with the surface). Find averages for each sensor. For example, if 360 is black and 780 white, average will be (780 + 360) / 2 = 570. These will be our test values. All the readings below them (for each sensor) we will declare as black and white above. The code follows:

void loop(){
	if (brightness(0) < 300 || brightness(1) < 490 || brightness(2) < 450 || brightness(3) < 480)
		go(100, 10);
	else if (brightness(5) < 425 || brightness(6) < 410 || brightness(7) < 480 || brightness(8) < 340)
		go(10, 100);
		go(80, 80);
Remember that C++ count from 0, not 1. Enter Your values, not the ones above. For example, compare sensor 0 against Your average. Therefore, not 300 but 380 (if Your average is 380): reading(0) < 380.


  1. brightness(0) measures surface's brightness. In this example, 0 inside the brackets denotes phototransistor number 0, first of 9 (or 8). There are labels on the sensor that indicate transistors.
  2. "||" in C++ means "or".
  3. If any of the rightmost 4 sensors (0, 1, 2, or 3) see the black line (brightness(0) < 300 ||...), turn right (go(100, 10);).
  4. Otherwise, if any of the leftmost 4 sensors (5, 6, 7, or 8) see the black line (brightness(5) < 425 ||...), turn left (go(10, 100);).
  5. Otherwise (no line left and no right), go straight ahead (go(80, 80));

Start the program by entering command "loo". Change 10 and 100 into some other values till You get the best line tracking. You can use negative values (go backwards). Again, this is not a very good program for line following but it very simple and shows the concept.

Line following, an improved algorithm

We want the robot to follow the line smoothly and want the line to always be in the middle of the sensor. In that case, when same challenging situations emerges, the robot will be in the best position to overcome it.

Obviously, it is not good to turn vigorously when the robot's center is close to the line. Doing so, the robot will overshoot the centre and the result will be oscillatory motion. An initial solution is here:

void loop(){
	const uint16_t LIMIT[] = { 300, 490, 450, 480, 470, 425, 410, 480, 340 };

	for (uint8_t i = 0; i < 9; i++)
		if (brightness(i) < LIMIT[i]) {
			go(60 + (i - 4) * 15, 60 - (i - 4) * 15);
  1. "const uint16_t..." - this is an array, a variable that can hold a list of values. If this is not familiar, consult an elementary C++ tutorial.
  2. "for..." - a basic loop. Does it look strange? In that case, C++ tutorial will help. This loop simplifies the code as only 1 "if" is needed.
  3. "if" body - to see how this logic works, consider cases when i = 0 (line under the leftmost transistor), i = 4 (line in the middle), and i = 8 (under rightmost). When the line is closer to the robot's center, turning will be gentler.

This was better, but there are still problems. If the line is wider, 2 or more transistors may be activated, but only the first from the left will be taken into account. Also, there are discrete jumps between 2 adjacent sensors. Either first triggers action, or second, although there are many line positions between them. But first, let's address another problem: variations in measurements.


Due to different illuminations and different transistors' sensitivities, each of the 9 sensors produces a different measurement, as we already noticed. There will be no problem when everything is fine, but bad things happen: irregular external light, bumpers (which change surface's distance), a poorly reflective tape. It is the best to calibrate the sensors on each site where competition will take place.

Calibration could be performed in a separate function and the results saved for later in flash so that power switched off will not erase them. Here, we will show a simpler case - auto-calibration while the robot is moving:

void loop(){
	static uint16_t bright[9], dark[9];                                          // Static values are preserved between passes. Darkest and brightest readings for each sensor.

	for (uint8_t i = 0; i < 9; i++)                                      // Store approximate middle values.
		bright[i] = 570, dark[i] = 570;

	bool found = false;                                                  // If line found, start the motors and stop searching.
	for (uint8_t i = 0; i < 9; i++) {
		uint16_t reading = brightness(i);                           // Take a reading once, use many times later.

		if (reading < (bright[i] + dark[i]) * 0.5 &amp;&amp; !found)  // Use mid-value (*0.5) between extremes.
			go(60 + (i - 4) * 15, 60 - (i - 4) * 15), found = true;

		if (reading > bright[i])                                             // If current value is out of the present range, extend the stored extremes.
			bright[i] = reading;
		if (reading < dark[i] &amp;&amp; reading != 0)
			dark[i] = reading;
In this example, we put comments after "//" - C++ style comments. Additional explanation:
  1. "static" - use C++ tutorial. Variables declared static will retain its values between calls of the function they are in.
  2. "uint8_t" - C++ tutorial. You can safely change "uint8_t" into "int" almost everywhere in these lessons. It is a very short integer, having values between 0 and 255.
  3. "bool" - variable of this type can be only "true" or "false".
You are advised to study this code until You grasp the idea.

Line following using mrm-ref-can's firmware

MRMS reflectance sensors 8x, CAN, analog, I2C (mrm-ref-can8) and MRMS reflectance sensors 9x, CAN, analog, I2C (mrm-ref-can) are quite useful sensors for line following. By now we have not been using some of the built-in capabilities but will do so now. The rest of the lesson shows the best practice in line following.

Calibration in firmware

If we do the calibration in the Arduino code, we will have some tasks:

  • to develop the algorithm,
  • to store the calibration permanently,
  • to retrieve the stored data on each robot's start.
A lot of work. So why not using the calibration that is built in sensors' ARM firmware? Let's check this option. Note this command in the menu:

cal - Calibrate refl.

If we choose it, mrm-ref-can's LED will be turned off for 5 seconds and will turn on for 1 second after that. These 2 events mark begin and end of the calibration process, during which should the robot go over black line and white area (moved by hand) in the way that is expected during the run. Minimum and maximum for every of the 9 transistors will be stored in board's flash and will be retained when the power is switched off.

How can we use the calibration data? We have 2 options.

Local calibration data

The data are now in sensor and we can store them in Arduino driver library in order not to ask every time the sensor to deliver them because that would slow down the program a little. There is a command for this purpose:

mrm_ref_can->calibrationDataRequest(0, true); // Read line sensor's calibration data.
First parameter, 0, chooses sensor 0. Second, "true", means that the function will block program flow until the sensor sends the data back to Arduino MRMS ESP32: Arduino, IMU, eFuse, BT, WiFi, CAN Bus (mrm-esp32). If the choice was "false", there would be no delay, but the next program statements would not be able to use the correct calibration data. They would be ready only after exchanging appropriate CAN Bus messages later. To use a blocking call is usually a poor choice, but here it is all right, as the function is called only once, and it would not be good that the robot starts following a line without the calibration data.

If we want to use the data, they can be recalled from local Arduino library, using a command, in this example to read the data (value in the middle between extreme bright and dark) for transistor 7:

Now we can compare analog readings to the calibration data and decide if the particular transistor detects black or white. For example, function dark(receiverNumberInSensor, deviceNumber), will return a boolean value as a result of comparing value of receiverNumberInSensor to the stored calibration value for that transistor. To get the value for transistor 7, the call will be dark(7) and will return true or false. deviceNumber is optional and is 0 if not specified.

No local data

The other option is to leave the calibration data in the sensor and read digital values. Each transistor's reading will be compared to the stored calibration data and the result will be digital: dark or not. This option will bring some benefits.

  • Much less messages are needed to convey the data: 1 short instead of 3 long. That will increase frequency of data exchange 3 times. Using firmware's default setup, the messages arrive each 3 or 4 ms, yielding frequency of at least 250 readings per second. If the robot goes 20 cm/sec., it will get a fresh reading every 0.08 mm.
  • Less local program logic is needed and less local processing time.

Line center

By using digital values it is not possible to calculate exact position of the line. To overcome this, the same message carrying digital values transfers line center as an analog value. If the line is under transistor 1, the value will be 1000, under transistor 2 it will be 2000, and so on until transistor 9, where it will be 9000. If no line detected (all sensors white), the value will be 0. If the line is between adjacent transistors, the value will be interpolated. For example, being 1/3 of the way between transistors 1 and 2, the value will be 1333. We will use this value in the program below.


Lets change function lineFollow() it the following way:

void RobotLine::lineFollow() {
	static float lastLineCenter = 0;

	float lineCenterNow = lineCenter();                        // Result: -50 to 50.
	if (lineCenterNow < -40 || lineCenterNow > 40) {		// No line under inner sensors, including lost line.
									// Choice depending on the line center the last time it was detected, stored in variable lastLineCenter.
		if (lastLineCenter < -15) 			// Lost line right or line far right.
			go(127, -127); 		// Rotate in place.
		else if (lastLineCenter > 15) 		// Lost line left or line far left.
			go(-127, 127); 		// Rotate in place.
		else 								// Line was around the robot's center when lost. Therefore, it was interrupted
			go(127, 127); 		// Go straight ahead.
	else { 									// Follow line
									// Maximum speed of the faster motor, decrease the other one.
		go(lineCenterNow < 0 ? 127 : 127 - lineCenterNow * 3, lineCenterNow < 0 ? 127 + lineCenterNow * 3 : 127);
		lastLineCenter = lineCenterNow; 			// Remember the line.
The comments are in the code. You can change maximum speed (127) or change how much the robot turns (by changing "* 3" part). We can start the program with menu's command:

lin - FollowLine.

Note that this is another function, no more loop(), but its behaviour is similar: always runs constantly, till stopped.

Next steps

You can, and should, continue experimenting with line. To be able to solve different problem, we will need some new hardware. Therefore, in the next lesson we will continue building the robot.