(Original Report for college course: May 2024)
Notes: I originally made this project for my subsystems college course (CET4711) in spring 2024, This is my 2nd attempt of making an AFM, I also tried making one in high school but there was some misunderstandings about the arduino MCU, that caused it not to work well, this article is based on my project report for my college course, because of time constraints, I could not finish the upper half of the AFM, the upper half is for the probe that scans the item placed under it, to demonstrate its function, I hot glued a needle to a scrap plastic part to place the needle pointing down, I then needed to find a metal sample that would both fit the gap and produce a interesting image, even though I planned on some kind of film or semiconductor, I used a screw since its not that springy and the gap was to large for the type of item I actually wanted to scan.
Now that I graduated I plan on restarting the project, to function better and expand on it, for use on semiconductors, I also think that living cells could be interesting, after fixing the frame, my first modification planned is a optical microscope in order to see a image of the sample optically and via C-AFM, side by side, and see if the oscillation is correct.
I plan on releasing the code and CAD files on github but want to refactor the CAD files and rewrite the code for the Arduino Minima first to address some problems caused by the limited memory on the UNO.
void ProbeCord(char movx, char movy) {
// had to make the code downscale by a quarter for it to fit on arduino.
int qmovx = movx*4;
int qmovy = movy*4;
moveprobe(qmovx, qmovy, 255);
char qnn = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
moveprobe(qmovx+1, qmovy, 255);
char qpn = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
moveprobe(qmovx, qmovy+1, 255);
char qnp = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
moveprobe(qmovx+1, qmovy+1, 255);
char qpp = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
ImageBuffer0[movx][movy] = (qnn+qpn+qnp+qpp)/4;
digitalWrite(Zenable, LOW);
digitalWrite(XYenable, LOW);
}
void TakeImage() {
for(int yscan=0; yscan < 64; yscan++) {
for(int xscan=0; xscan < 64; xscan++) {
ProbeCord(xscan, yscan);
}
analogWrite(piezoX, 0);
delay(RCDelay); // Give time for voltage to go down.
}
}
char ImageBuffer0[64][64];
#define piezoX 3 // For X axis
#define piezoY 5 // For Y axis
#define piezoZ 7 // Not used if 255 only
// May be expanded for depth later
#define XYenable 10
#define Zenable 11 // Connect Piezo Z directly here
// it stays at 255 or 5V so this allows me make
// the circuit simpler
#define probe A0
// Rewrite this during your calibration process. leave * 4, I prefer to use
// 0 to 255 but did not went to change code for 255, so left it like this.
int upper = 175 * 4; // Sets higher expected value, default is 255.
int lower = 100 * 4; // Sets lower expected value, default is 0.
// sets delay in ms to allow RC filter to charge.
int RCDelay = 10;
void setup() {
// init serial
Serial.begin(115200);
// initialize digital pin LED_BUILTIN as an output.
pinMode(LED_BUILTIN, OUTPUT);
pinMode(piezoX, OUTPUT);
pinMode(piezoY, OUTPUT);
pinMode(piezoZ, OUTPUT);
pinMode(XYenable, OUTPUT);
pinMode(Zenable, OUTPUT);
pinMode(probe, INPUT);
}
int GetValue(int getx, int gety){
return ImageBuffer0[getx][gety];
}
void SendViaSerial() {
Serial.print("."); // Signals new image.
for(int ysend=0; ysend < 64; ysend++) {
Serial.print(GetValue(0, ysend));
for(int xsend=1; xsend < 63; xsend++) {
Serial.print(GetValue(xsend, ysend));
Serial.print(","); // next value
}
// next row
Serial.println(GetValue(64, ysend));
}
Serial.print(";"); // to tell the system its done.
}
void moveprobe(char movx, char movy, char movz) {
digitalWrite(Zenable, LOW);
digitalWrite(XYenable, LOW); // Wait until cords are set.
analogWrite(piezoX, movx); // go to cords
analogWrite(piezoY, movy);
analogWrite(piezoZ, movz);
delay(1); // allow rc filter to get to right voltage.
digitalWrite(XYenable, HIGH);
delay(1); // very short delay, so probe does not scratch.
digitalWrite(Zenable, HIGH);
}
void ProbeCord(char movx, char movy) {
// had to make the code downscale by a quarter for it to fit on arduino.
int qmovx = movx*4;
int qmovy = movy*4;
moveprobe(qmovx, qmovy, 255);
char qnn = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
moveprobe(qmovx+1, qmovy, 255);
char qpn = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
moveprobe(qmovx, qmovy+1, 255);
char qnp = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
moveprobe(qmovx+1, qmovy+1, 255);
char qpp = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
ImageBuffer0[movx][movy] = (qnn+qpn+qnp+qpp)/4;
digitalWrite(Zenable, LOW);
digitalWrite(XYenable, LOW);
}
void TakeImage() {
for(int yscan=0; yscan < 64; yscan++) {
for(int xscan=0; xscan < 64; xscan++) {
ProbeCord(xscan, yscan);
}
analogWrite(piezoX, 0);
delay(RCDelay); // Give time for voltage to go down.
}
}
// the loop function runs over and over again forever
void loop() {
digitalWrite(LED_BUILTIN, HIGH); // show that program starts.
TakeImage(); // Take the Atomic Force Image
// send over serial
SendViaSerial();
digitalWrite(LED_BUILTIN, LOW); // To show speed of program
delay(1500); // wait a while before next image.
}
import processing.serial.*;
Serial myPort; // Create object from Serial class
int val; // Data received from the serial port
String SerialMsg;
void setup()
{
// 64 by 64 resolution scaled up by 10.
size(640, 640);
// Assumes 1st Serial Port
String portName = Serial.list()[0];
myPort = new Serial(this, portName, 115200);
myPort.bufferUntil(’;’);
}
void draw()
{
String[] rows = new String[65];
String[] columns = new String[65];
if (SerialMsg != null) {
rows = split(SerialMsg, ’\n’);
for(int i=0; i<rows.length; i++) {
columns = split(rows[i], ’,’);
for(int j=0; j < columns.length; j++) {
fill(int(columns[j]));
rect(j*10, i*10, 10, 10);
}
}
SerialMsg = null;
}
}
void serialEvent(Serial myPort) {
SerialMsg = myPort.readStringUntil(’;’);
}
void ProbeCord(char movx, char movy) {
// had to make the code downscale by a quarter for it to fit on arduino.
int qmovx = movx*4;
int qmovy = movy*4;
moveprobe(qmovx, qmovy, 255);
char qnn = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
moveprobe(qmovx+1, qmovy, 255);
char qpn = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
moveprobe(qmovx, qmovy+1, 255);
char qnp = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
moveprobe(qmovx+1, qmovy+1, 255);
char qpp = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
ImageBuffer0[movx][movy] = (qnn+qpn+qnp+qpp)/4;
digitalWrite(Zenable, LOW);
digitalWrite(XYenable, LOW);
}
int qmovx = movx*4;
int qmovy = movy*4;
moveprobe(qmovx, qmovy, 255);
char qnn = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
moveprobe(qmovx+1, qmovy, 255);
char qpn = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
moveprobe(qmovx, qmovy+1, 255);
char qnp = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
moveprobe(qmovx+1, qmovy+1, 255);
char qpp = constrain(map(analogRead(probe), lower, upper, 0, 255), 0, 255);
ImageBuffer0[movx][movy] = (qnn+qpn+qnp+qpp)/4;
void moveprobe(char movx, char movy, char movz) {
digitalWrite(Zenable, LOW);
digitalWrite(XYenable, LOW); // Wait until cords are set.
analogWrite(piezoX, movx); // go to cords
analogWrite(piezoY, movy);
analogWrite(piezoZ, movz);
delay(1); // allow rc filter to get to right voltage.
digitalWrite(XYenable, HIGH);
delay(1); // very short delay, so probe does not scratch.
digitalWrite(Zenable, HIGH);
}
void SendViaSerial() {
Serial.print("."); // Signals new image.
for(int ysend=0; ysend < 64; ysend++) {
Serial.print(GetValue(0, ysend));
for(int xsend=1; xsend < 63; xsend++) {
Serial.print(GetValue(xsend, ysend));
Serial.print(","); // next value
}
// next row
Serial.println(GetValue(64, ysend));
}
Serial.print(";"); // to tell the system its done.
}