EEEN21000
GROUP 30 Line Following Buggy
|
We had at least 4 technical demonstrations (TD) for this semester, assessing different parts of the buggy. They emphasised the hardware control, especially the motors and the software solutions to interface the microcontroller with other sensors and Bluetooth module.
TD | 1 | 2 | 3 | 4 |
---|---|---|---|---|
Marks | 100 | 97 | 100 | 80 |
On the software side, the key task was setting up the encoders and motors to work as intended. We settled down on a 20% duty cycle to keep it steady and avoid any translational inertia after stopping. PWM frequency set to 1000 Hz because the wheels were louder than the switches, why not? The heatsink was also not too hot to touch. We chose to use unipolar mode as this offered fewer switching losses.
The encoder was connected directly to the wheel shaft, and from there we could measure the speed of the rotating wheel. The QEI library encoded the pulses to X2 resolution, so we must scale it down to get the actual number of pulses. We also knew the number of pulses per revolution (CPR) was 256.
\begin{align} pulses &= \frac{pulses}{2} \\ rev &= \frac{pulses}{256} \\ distance &= rev \times 2 \pi r \\ velocity &= \frac{distance}{time} \\ \end{align}
One of the tasks was to make a square of 0.5 m in length. The hardest part was the square had to be in between the wheels. Even worst, we did not tune the task on the actual square at A16. The snippet below shows how we implemented the square sequence.
main.cpp
Ideally, all values in the linear vector and the rotation vector are 0.5 m and 90° respectively. However, ours looked like this
Note that the last one was more than 90° because the task was to trace the square back which was harder when your buggy did not move in a straight line. We used the bang-bang control approach to compensate for any tiny deviation between the two wheels, implemented in the Motor::forward.
motor.cpp
\begin{align} correction &= \frac{pulse_N}{pulse_M} \ where \ pulse_N > pulse_M \\ \end{align}
We received full marks on TD1 and this assured us of a good start to our journey in producing a winning buggy.
The Bluetooth module was easily configured by connecting to the correct RX, and TX pins. In this case, we used BufferedSerial to handle the communication between F401RE and HM-10. Infrared sensors were the tricky bit since we chose not to arrange them in one row but two instead. However, we will discuss the general process of configuration and calibration without considering our chosen arrangement.
It was easier to manage 6 sensors if they are connected to op-amp as this approach allowed calibration only using a potentiometer instead of software. Since this was not available to us, we had to calibrate the black and white level of the sensors.
The white level was calibrated by placing a white card under the sensors and toggling the sensors under normal conditions at a fixed height from the sensors. Does VDD matter? No, because reading was already normalised, you can do whatever scaling you want as long as you were comfortable with it. Here, we choose a saturation level of 10.
Recall that we had raw uncalibrated data (white level)
S1 | S2 | S3 | S4 | S5 | S6 |
---|---|---|---|---|---|
3.18998 | 3.26268 | 3.07483 | 3.12763 | 3.22799 | 3.70966 |
How big is the scale factor to make it 10?
S1 | S2 | S3 | S4 | S5 | S6 |
---|---|---|---|---|---|
3.13481 | 3.06497 | 3.25221 | 3.19731 | 3.09790 | 2.69567 |
Now, just apply a scale factor to each sensor to scale it to the desired maximum level.
Although we expected perfect results by calibration, there will be always external noise coming from sunlight and other sources. The issue needed to be addressed to avoid unreliable readings consisting of random errors.
\( reading_i = noise_i + emitter_i \)
Measurements of the analogue input when all emitter is off at the start of each read were very easy to implement. When this is done correctly, you get the best-intended readings from the sensor.
more...
Showcase some of the tests done to calibrate the sensors.
This part was where we realised how important normalised readings are for distance calculation. If all readings were normalised to a level, the distance formula simply became the weighted mean of sensor readings. From the sensor array, if we established a pivot point between sensors 3 and 4, we could assign a weight for every sensor based on its distance from the pivot as in the example below.
The solution was very simple and the equivalent formula was \(\ distance = \frac{\sum_{1}^{6} reading_i \times weight_i}{\sum_{1}^{6} reading_i} \)
When all components were put together, it was time to make sure all can work in sync to follow the white line on the track. The overall look at how the control algorithm work was outlined in the figure below.
The first controller scaled the deviations from the white line and outputted the target speed for the encoders. Then, a differential was created by using a target value and the output before to create a turning effect for the wheels. This differential then outputs the corresponding PWM outputs for each motor.
Another extra thing we added was an adaptive system response based on the position of the line, which manipulates the set point of the left and right controller to create a quicker turning response.
wheelcontrol.cpp
Other than the control algorithm, we simply added the turnaround code when the BLE signal was sent. The turnaround used the code created in TD1 with a little tweak to find the centre line instead of rotating 180. This method allows consistent turnaround even when the buggy is not perfectly aligned with the line before the turn.
In TD3, we only used proportional gain to drive the buggy. This time we try to tune the Kp and Kd gains to anticipate the motion of the buggy. Eventually, the buggy does not even need derivative gain to drive the buggy. It happened to be replacing printf with debugprint in the code improves the algorithm's performance.
Because we didn't bother to test it on the actual track, the fastest code failed to complete on the actual track on the heats day. We had to compromise speed to complete the track since we only had one try because we were disqualified from the first run for being late on the day.
In the real world situation, the derivative term is noisy, so we used a low-pass filter to smooth out the noise. The value of alpha in the filter is closer to 1 because we still need the high details of the term to be able to accurately predict the motion of the buggy. The low pass filter was implemented in the following code.
PID.cpp
more...
Showcase the filter behaviour with different alpha. Simulated error in range [-1,1]