




After that I connected a 4-wire 12V PC fan to Arduino. Connect Red to 12V (V in), Black to Gnd, Blue (PWM speed control) to PWM digital output and Yellow (tachometer) to digital input. I used 12V external power adaptor connected to Arduino power connector. Tachometer pulse signal connected to Digital input 8 configured as pull-up. Speed control PWM is connected to Digital output 11. No need for any extra component to control a 4-wire PC fan. Next, I connected a 10K potentiometer to Analog Input 0 for analog value adjustment.

I programmed my version of PID speed controller, using the analog input as the adjustible set-point, calculate speed using the tachometer pulses and control the PWM output to the fan.
Setpoint, actual speed and PWM duty-cycle output are also sent to PC via serial comm and graphed in real-time on the PC using Processing software running modified Arduinoscope sketch.

I learned a lot in a short time doing this project. I believe this is the most cost effective and practical way to learn PID automatic control. Try it.
Here are the codes for Arduinoscope-like realtime graph running on Processing on PC side:
- Code: Select all
// Maurice Ribble
// 6-28-2009
// http://www.glacialwanderer.com/hobbyrobotics
// This takes data off the serial port and graphs it.
// There is an option to log this data to a file.
// I wrote an arduino app that sends data in the format expected by this app.
// The arduino app sends accelerometer and gyroscope data.
//
// Modified by Bro_Az August 2010 for PID monitoring.
import processing.serial.*;
// Globals
int g_winW = 820; // Window Width
int g_winH = 600; // Window Height
boolean g_dumpToFile = false; // Dumps data to c:\\output.txt in a comma seperated format (easy to import into Excel)
boolean g_enableFilter = false; // Enables simple filter to help smooth out data.
cDataArray g_xAccel = new cDataArray(200);
cDataArray g_yAccel = new cDataArray(200);
cDataArray g_zAccel = new cDataArray(200);
cDataArray g_vRef = new cDataArray(200);
cDataArray g_xRate = new cDataArray(200);
cDataArray g_yRate = new cDataArray(200);
cGraph g_graph = new cGraph(10, 190, 800, 400);
Serial g_serial;
PFont g_font;
void setup()
{
size(g_winW, g_winH, P2D);
println(Serial.list());
g_serial = new Serial(this, Serial.list()[10], 9600, 'N', 8, 1.0);
g_font = loadFont("ArialMT-20.vlw");
textFont(g_font, 20);
// This draws the graph key info
strokeWeight(1.5);
stroke(255, 0, 0); line(20, 420, 35, 420);
stroke(0, 255, 0); line(20, 440, 35, 440);
stroke(0, 0, 255); line(20, 460, 35, 460);
stroke(255, 255, 0); line(20, 480, 35, 480);
stroke(255, 0, 255); line(20, 500, 35, 500);
stroke(0, 255, 255); line(20, 520, 35, 520);
fill(0, 0, 0);
text("Speed Setpoint", 40, 430);
text("Actual Speed", 40, 450);
text("Controller Output", 40, 470);
text("Analog 3", 40, 490);
text("Analog 4", 40, 510);
text("Analog 5", 40, 530);
if (g_dumpToFile)
{
// This clears deletes the old file each time the app restarts
byte[] tmpChars = {'\r', '\n'};
saveBytes("c:\\output.txt", tmpChars);
}
}
void draw()
{
// We need to read in all the avilable data so graphing doesn't lag behind
while (g_serial.available() >= 2*6+2)
{
processSerialData();
}
strokeWeight(1);
fill(255, 255, 255);
g_graph.drawGraphBox();
strokeWeight(1.5);
stroke(255, 0, 0);
g_graph.drawLine(g_xAccel, 0, 1024);
stroke(0, 255, 0);
g_graph.drawLine(g_yAccel, 0, 1024);
stroke(0, 0, 255);
g_graph.drawLine(g_zAccel, 0, 1024);
stroke(255, 255, 0);
g_graph.drawLine(g_vRef, 0, 1024);
stroke(255, 0, 255);
g_graph.drawLine(g_xRate, 0, 1024);
stroke(0, 255, 255);
g_graph.drawLine(g_yRate, 0, 1024);
}
// This reads in one set of the data from the serial port
void processSerialData()
{
int inByte = 0;
int curMatchPos = 0;
int[] intBuf = new int[2];
intBuf[0] = 0xAD;
intBuf[1] = 0xDE;
while (g_serial.available() < 2); // Loop until we have enough bytes
inByte = g_serial.read();
// This while look looks for two bytes sent by the client 0xDEAD
// This allows us to resync the server and client if they ever
// loose sync. In my testing I haven't seen them loose sync so
// this could be removed if you need to, but it is a good way to
// prevent catastrophic failure.
while(curMatchPos < 2)
{
if (inByte == intBuf[curMatchPos])
{
++curMatchPos;
if (curMatchPos == 2)
break;
while (g_serial.available() < 2); // Loop until we have enough bytes
inByte = g_serial.read();
}
else
{
if (curMatchPos == 0)
{
while (g_serial.available() < 2); // Loop until we have enough bytes
inByte = g_serial.read();
}
else
{
curMatchPos = 0;
}
}
}
while (g_serial.available() < 2*6); // Loop until we have a full set of data
// This reads in one set of data
{
byte[] inBuf = new byte[2];
int xAccel, yAccel, zAccel, vRef, xRate, yRate;
g_serial.readBytes(inBuf);
// Had to do some type conversion since Java doesn't support unsigned bytes
xAccel = ((int)(inBuf[1]&0xFF) << 8) + ((int)(inBuf[0]&0xFF) << 0);
g_serial.readBytes(inBuf);
yAccel = ((int)(inBuf[1]&0xFF) << 8) + ((int)(inBuf[0]&0xFF) << 0);
g_serial.readBytes(inBuf);
zAccel = ((int)(inBuf[1]&0xFF) << 8) + ((int)(inBuf[0]&0xFF) << 0);
g_serial.readBytes(inBuf);
vRef = ((int)(inBuf[1]&0xFF) << 8) + ((int)(inBuf[0]&0xFF) << 0);
g_serial.readBytes(inBuf);
xRate = ((int)(inBuf[1]&0xFF) << 8) + ((int)(inBuf[0]&0xFF) << 0);
g_serial.readBytes(inBuf);
yRate = ((int)(inBuf[1]&0xFF) << 8) + ((int)(inBuf[0]&0xFF) << 0);
g_xAccel.addVal(xAccel);
g_yAccel.addVal(yAccel);
g_zAccel.addVal(zAccel);
g_vRef.addVal(vRef);
g_xRate.addVal(xRate);
g_yRate.addVal(yRate);
if (g_dumpToFile) // Dump data to a file if needed
{
String tempStr;
tempStr = xAccel + "," + yAccel + "," + zAccel + "," + vRef + "," + xRate + "," + yRate + "\r\n";
FileWriter file;
try
{
file = new FileWriter("c:\\output.txt", true); //bool tells to append
file.write(tempStr, 0, tempStr.length()); //(string, start char, end char)
file.close();
}
catch(Exception e)
{
println("Error: Can't open file!");
}
}
/*
print(xAccel); print(" "); print(yAccel); print(" "); print(zAccel); print(" ");
print(vRef); print(" "); print(xRate); print(" "); println(yRate);
*/
}
}
// This class helps manage the arrays of data I need to keep around for graphing.
class cDataArray
{
float[] m_data;
int m_maxSize;
int m_startIndex = 0;
int m_endIndex = 0;
int m_curSize;
cDataArray(int maxSize)
{
m_maxSize = maxSize;
m_data = new float[maxSize];
}
void addVal(float val)
{
if (g_enableFilter && (m_curSize != 0))
{
int indx;
if (m_endIndex == 0)
indx = m_maxSize-1;
else
indx = m_endIndex - 1;
m_data[m_endIndex] = getVal(indx)*.5 + val*.5;
}
else
{
m_data[m_endIndex] = val;
}
m_endIndex = (m_endIndex+1)%m_maxSize;
if (m_curSize == m_maxSize)
{
m_startIndex = (m_startIndex+1)%m_maxSize;
}
else
{
m_curSize++;
}
}
float getVal(int index)
{
return m_data[(m_startIndex+index)%m_maxSize];
}
int getCurSize()
{
return m_curSize;
}
int getMaxSize()
{
return m_maxSize;
}
}
// This class takes the data and helps graph it
class cGraph
{
float m_gWidth, m_gHeight;
float m_gLeft, m_gBottom, m_gRight, m_gTop;
cGraph(float x, float y, float w, float h)
{
m_gWidth = w;
m_gHeight = h;
m_gLeft = x;
m_gBottom = g_winH - y;
m_gRight = x + w;
m_gTop = g_winH - y - h;
}
void drawGraphBox()
{
stroke(0, 0, 0);
rectMode(CORNERS);
rect(m_gLeft, m_gBottom, m_gRight, m_gTop);
}
void drawLine(cDataArray data, float minRange, float maxRange)
{
float graphMultX = m_gWidth/data.getMaxSize();
float graphMultY = m_gHeight/(maxRange-minRange);
for(int i=0; i<data.getCurSize()-1; ++i)
{
float x0 = i*graphMultX+m_gLeft;
float y0 = m_gBottom-((data.getVal(i)-minRange)*graphMultY);
float x1 = (i+1)*graphMultX+m_gLeft;
float y1 = m_gBottom-((data.getVal(i+1)-minRange)*graphMultY);
line(x0, y0, x1, y1);
}
}
}
Here are the codes for Arduino:
- Code: Select all
/*
This sketch use PID controller to control PC fan speed and display result on a LCD.
Setpoint for speed is read from a potentiometer. Data also send thru serial to PC
running a real-time graph under Processing software.
Author - Bro_Az August 2010.
The circuit:
* LCD RS pin to digital pin 7
* LCD Enable pin to digital pin 6
* LCD D4 pin to digital pin 5
* LCD D5 pin to digital pin 4
* LCD D6 pin to digital pin 3
* LCD D7 pin to digital pin 2
* ends to +5V and ground
* 10k pot wiper to LCD VO pin (pin 3)
*/
#include <LiquidCrystal.h>
// If this is defined it prints out the FPS that we can send a
// complete set of data over the serial port.
//#define CHECK_FPS
int tachPin = 8; // fan tachometer connected to digital pin 8
int fanPin = 11; // fan drive connected to digital pin 11
int sensorPin = 0; // analog input 0 for the potentiometer
int sensorValue; // variable to store the value coming from the sensor
unsigned long duration; // time duration between tachometer pulses
int max_rpm=4800; // max rpm at 100% PWM output
int setpoint; // setpoint in %
int speed; // fan speed in %
float rpm; // fan speed in RPM
int error;
int last_error=0;
int diff;
float integ=0;
float kp=1.0; // proportional gain
float ki=0.5; // integral gain
float kd=0.05; // derivative gain
int out; // PID controller output
boolean auto_manual=1; // setpoint manual or from pot
int manual_setpoint=77;
int sampletime=40; // Number of sample to average-out sampling values
unsigned long val; // Working variable to hold the accumulated value
unsigned int an0=0; // Variables for sending data to PC
unsigned int an1=0;
unsigned int an2=0;
unsigned int an3=0;
unsigned int an4=0;
unsigned int an5=0;
unsigned int offset=2;
// initialize the LCD library routine with the numbers of the interface pins
LiquidCrystal lcd(7, 6, 5, 4, 3, 2);
void setup() {
lcd.begin(16, 2); // set up the LCD's number of rows and columns:
digitalWrite(tachPin, HIGH); // turn on pull-up resistor
Serial.begin(9600); // Serial port baud rate
}
void loop() {
unsigned int startTag = 0xDEAD; // Analog port maxes at 1023 so this is a safe termination value
// for sending values to PC.
#ifdef CHECK_FPS
unsigned long startTime, endTime;
startTime = millis();
#endif
val=0;
for (int i=0;i<sampletime;i++) {
sensorValue = analogRead(sensorPin); // Read the value from the pot. AnalogRead values go from 0 to 1023.
val=val+sensorValue;
}
sensorValue=val/sampletime;
if (auto_manual) {
setpoint=float(sensorValue)*100/1024; // convert to percentage
}
else {
setpoint=manual_setpoint;
}
duration=0;
for (int i = 0; i < sampletime; i++) { // Average out pulseIn time reading in microseconds
duration = duration+pulseIn(tachPin, HIGH);
}
rpm=600000000/float(duration); // in RPM
speed=rpm/max_rpm*100; // convert to percentage
error=setpoint-speed;
diff=error-last_error;
integ=integ+error;
out=kp*error+ki*integ+kd*diff;
if (out<0) {out=0;integ=0;} //min limit, anti-reset wind-up
if (out>255) {out=255;integ=integ-error;} //max limit, anti-reset wind-up
analogWrite(fanPin, out); // analogWrite values from 0 to 255, PWM output
lcd.setCursor(0, 0); // Set for first line
lcd.print(int(rpm)); // displays RPM value
lcd.print(" RPM CO=");
lcd.print(out);// displays Controller Output value
lcd.print(" ");
lcd.setCursor(0, 1); //set for second line
lcd.print("SP=");
lcd.print(setpoint);// displays setpoint value
lcd.print("% ");
lcd.print("PV=");// displays PV value
lcd.print(speed);
lcd.print("% ");
if (auto_manual) { // if set-point in Auto, display "A"
lcd.setCursor(15, 1);
lcd.print("A");
}
else {
lcd.setCursor(15, 1);// else display "M" for Manual
lcd.print("M");
}
an0=setpoint*9+offset; // scaling for graph on PC
an1=speed*9+offset;
an2=out*3+offset;
Serial.write( (unsigned byte*)&startTag, 2); // Send serial data to PC
Serial.write((unsigned byte*)&an0, 2);
Serial.write((unsigned byte*)&an1, 2);
Serial.write((unsigned byte*)&an2, 2);
Serial.write((unsigned byte*)&an3, 2);
Serial.write((unsigned byte*)&an4, 2);
Serial.write((unsigned byte*)&an5, 2);
#ifdef CHECK_FPS
endTime = millis();
Serial.print(" - FPS: ");
Serial.println(1.f / (endTime-startTime) * 1000);
#endif
}
