6
\$\begingroup\$

I found some old, still working 3½ floppy drives using the 34-pin IDC interface, that will take a 1.44 MB disk. I had a project in mind, using these floppy disk as an "access key" for a door. In order to do this in a simple manner, my thoughts were to write a sequence of 1's and 0's on specific tracks. This sequence will eventually make up a "lock code".

I am using an Arduino Duemilanove clocked at 16 MHz. I managed to get everything working, except for the reading/writing. I know that the clock speed is too low in order to read/write anything from the actual track themselves, so I started to read about MFM encoding with the hope that instead of writing data to the floppy as it was intended (keeping the clock synced with the spinning drive, caring about sectors and data layout,etc), i could simply write 0's to one track, 1's to the next, and so on.

My issue is that I do not fully understand how MFM works in order to achieve this, so now my goal is figuring out a way of writing 0's and 1's to the tracks.

The tracks would look (in theory) like this:

  • Track 0: 00000...
  • Track 1: 11111...
  • Track 2: 11111...
  • Track 3: 00000...

If this is not possible (because of the way writing works), another possibility would be writing data to the tracks in such a manner that reading it back would return a 30% count of 1's, 70% count of 0's, so by considering the number of 0's is higher, that track would be considered as being full of 0's.

As far as I know, this is just a matter of switching the data pin to HIGH and then to LOW in a specified sequenced, delayed by a number of microseconds. Yet, I cannot figure these out on my own.

This is my current code (minus the commented out section, original on here):

int motorPin = 2;
int directionPin = 4;
int stepPin = 5;
int trackZeroPin = 8;
int indexPin = 1;
int driveSelPin = 3;
int writeDataPin = 6;
int writeEnablePin = 7;
int writeProtectPin = 9;
int headSelectPin = 11;
int readDataPin = 10;
int diskReadyPin = 12;
////////////////////////
int currentTrack = 0;
int trackContor = 0;
int tmpVec[500];
int vecCont = 0;

int onThisTrack = 0;

int zeros = 0;
int ones = 0;
int onesArray[30];
int zerosArray[30];
////////////////////////
void setup() 
{
    pinMode(motorPin,OUTPUT);
    pinMode(directionPin,OUTPUT);
    pinMode(stepPin,OUTPUT);
    pinMode(driveSelPin,OUTPUT);
    pinMode(writeEnablePin,OUTPUT);
    pinMode(writeDataPin,OUTPUT);
    pinMode(trackZeroPin,INPUT_PULLUP);
    pinMode(indexPin,INPUT_PULLUP);
    pinMode(readDataPin,INPUT_PULLUP);
    pinMode(writeProtectPin,INPUT_PULLUP);
    pinMode(diskReadyPin,INPUT_PULLUP);

    Serial.begin(9600);
    driveEnable();
    motorDisable();
    jiggle();
    jumpOutermost();
    motorEnable();
    execute();

    writeDisable();
}
int data = 1;
void loop() 
{
    Serial.print(readDataRaw());
}
void execute()
{
}
////////////////////////////////////////////////
int jumpToTrack(int track)
{
    if (track<84)
    {
        if (track>currentTrack)
        {
            int steps = track-currentTrack;
            for (int i=1;i<=steps;i++)
            {
                stepIn();
            }
        }
        else if (track<currentTrack)
        {
            int steps = currentTrack-track;
            for (int i=1;i<=steps;i++)
            {
                stepOut();
            }
        }
    }
}
int stepOut()
{
    digitalWrite(directionPin,HIGH);
    digitalWrite(stepPin,LOW);
    delayMicroseconds(1);
    digitalWrite(stepPin,HIGH);
    delayMicroseconds(3000);
    currentTrack-=1;
}
int stepIn()
{
    digitalWrite(directionPin,LOW);
    digitalWrite(stepPin,LOW);
    delayMicroseconds(1);
    digitalWrite(stepPin,HIGH);
    delayMicroseconds(3000);
    currentTrack+=1;
}
int readTrackZero()
{
    return digitalRead(trackZeroPin);
}
int readIndex()
{
    return digitalRead(indexPin);
}
int readDataRaw()
{
    return digitalRead(readDataPin);
}

int lastData = LOW;

int readData()
{
    int curData = readDataRaw();
    if (curData == LOW && lastData == HIGH)
    {
        lastData = curData;
        return 1;
    }
    else
    {
        lastData = curData;
        return 0;
    }
}
int readDiskReady()
{
    return digitalRead(diskReadyPin);
}
int motorEnable()
{
    digitalWrite(motorPin,LOW);
}
int motorDisable()
{
    digitalWrite(motorPin,HIGH);
}
int jumpOutermost()
{
    while (readTrackZero()==HIGH)
    {
        stepOut();
    }
}
int jiggle()
{
    stepIn();
    stepOut();
}
int driveEnable()
{    
    digitalWrite(driveSelPin,LOW);
}
int driveDisable()
{    
    digitalWrite(driveSelPin,HIGH);
}

int lastIndex = LOW;

int hasIndexPulsed()
{
    int curIndex = readIndex();
    if (curIndex == LOW && lastIndex == HIGH)
    {
        lastIndex = curIndex;
        return 1;
    }
    else
    {

        lastIndex = curIndex;
        return 0;
    }
}
int isWriteProtected()
{
    if (digitalRead(writeProtectPin)==LOW)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}
int writeEnable()
{
    digitalWrite(writeEnablePin,HIGH);
}
int writeDisable()
{
    digitalWrite(writeEnablePin,LOW);
}
int setWriteData(int data)
{
    digitalWrite(writeDataPin,data);
}

UPDATE I finally managed to get it done using an ATMEGA328. Here is the full code, if anyone is interested:

#define portOfPin(P)\
  (((P)>=0&&(P)<8)?&PORTD:(((P)>7&&(P)<14)?&PORTB:&PORTC))
#define ddrOfPin(P)\
  (((P)>=0&&(P)<8)?&DDRD:(((P)>7&&(P)<14)?&DDRB:&DDRC))
#define pinOfPin(P)\
  (((P)>=0&&(P)<8)?&PIND:(((P)>7&&(P)<14)?&PINB:&PINC))
#define pinIndex(P)((uint8_t)(P>13?P-14:P&7))
#define pinMask(P)((uint8_t)(1<<pinIndex(P)))

#define pinAsInput(P) *(ddrOfPin(P))&=~pinMask(P)
#define pinAsInputPullUp(P) *(ddrOfPin(P))&=~pinMask(P);digitalHigh(P)
#define pinAsOutput(P) *(ddrOfPin(P))|=pinMask(P)
#define digitalLow(P) *(portOfPin(P))&=~pinMask(P)
#define digitalHigh(P) *(portOfPin(P))|=pinMask(P)
#define isHigh(P)((*(pinOfPin(P))& pinMask(P))>0)
#define isLow(P)((*(pinOfPin(P))& pinMask(P))==0)
#define digitalState(P)((uint8_t)isHigh(P))

#include <EEPROM.h>

int motorPin = 10;
int directionPin = 11;
int stepPin = 12;
int trackZeroPin = A1;
int indexPin = 8;
int driveSelPin = 9;
int writeDataPin = 13;
int writeEnablePin = A0;
int writeProtectPin = A2;
int headSelectPin = A4;
int readDataPin = A3;
int diskReadyPin = A5;

int redLedPin = 7;
int greenLedPin = 4;

int mot1Pin = 2; //2 high - 3 low = closed;
int mot2Pin = 3; //2 low - 3 high = open;

int redLedButton = 5;
int greenLedButton = 6;
////////////////////////
int currentTrack = 0;
int trackContor = 0;
int arrayTest[100];
bool locked=true;
bool pwCorrect = false;
int codeLength = 1;
////////////////////////

void setup() 
{
    pinAsOutput(motorPin);
    pinAsOutput(directionPin);
    pinAsOutput(stepPin);
    pinAsOutput(driveSelPin);
    pinAsOutput(writeEnablePin);
    pinAsOutput(writeDataPin);
    pinAsOutput(headSelectPin);
    pinAsInputPullUp(trackZeroPin);
    pinAsInputPullUp(indexPin);
    pinAsInputPullUp(readDataPin);
    pinAsInputPullUp(writeProtectPin);
    pinAsInputPullUp(diskReadyPin);

    pinAsOutput(mot1Pin);
    pinAsOutput(mot2Pin);

    pinAsOutput(redLedPin);
    pinAsOutput(greenLedPin);

    pinAsInput(redLedButton);
    pinAsInput(greenLedButton);


    ///////INITIALIZATION/////////
    doorClose();
    for (int i=1;i<=5;i++)
    {
        digitalHigh(redLedPin);
        digitalLow(greenLedPin);
        delay(50);
        digitalLow(redLedPin);
        digitalHigh(greenLedPin);
        delay(50);
    }
    digitalLow(redLedPin);
    digitalLow(greenLedPin);

    driveEnable();
    motorDisable();
    writeDisable();
    motorDisable();
    floppyInit();
    driveDisable();
    ///////INITIALIZATION END/////////
    Serial.begin(9600);
}
void loop() 
{
    if (isGreenButtonPressed() && !isRedButtonPressed())
    {
        blinkLed(2,100,greenLedPin);
        codeLength=1;
        for (int i=1;i<=4;i++)
        {
            blinkLed(1,500,greenLedPin);
            if (isGreenButtonPressed())
            {
                blinkLed(i,500,redLedPin);
                codeLength=i;
            }
            else
            {
                break;
            }
        }
        if (pwCorrect==false)
        {
            driveEnable();
            if (isDiskReady())
            {
                digitalHigh(greenLedPin);
                String num = readPassCode();
                digitalLow(greenLedPin);
                Serial.println(readCodeFromEEPROM());
                Serial.println(num);
                if (num.equals(readCodeFromEEPROM()) || num.equals("help"))
                {
                    locked = false;
                    doorOpen();
                    pwCorrect = true;
                    digitalHigh(greenLedPin);
                    delay(2000);
                    digitalLow(greenLedPin);
                }
                else
                {
                    locked = true;
                    doorClose();
                    pwCorrect = false;
                    digitalHigh(redLedPin);
                    delay(2000);
                    digitalLow(redLedPin);
                }
            }
            else
            {
                blinkLed(5,100,redLedPin);
            }
            driveDisable();
        }
        else
        {
            blinkLed(5,100,redLedPin);
        }
    }
    else if (isRedButtonPressed() && !isGreenButtonPressed()) // and if unlocked
    {
        blinkLed(2,100,redLedPin);
        delay(1000);
        if (isRedButtonPressed())//daca e apasat dupa 1 s
        {
            codeLength=1;
            for (int i=1;i<=4;i++)
            {
                blinkLed(1,500,redLedPin);
                if (isRedButtonPressed())
                {
                    blinkLed(i,500,greenLedPin);
                    codeLength=i;
                }
                else
                {
                    break;
                }
            }
            if (pwCorrect == true)
            {
                driveEnable();
                if (isDiskReady())
                {
                    if (isWriteProtected())
                    {
                        blinkLed(5,100,redLedPin);
                    }
                    else
                    {
                        doorClose();
                        String num = getRandomString();
                        digitalHigh(redLedPin);
                        writePassCode(num);
                        digitalLow(redLedPin);

                        locked = true;
                        writeCodeToEEPROM(num);
                        pwCorrect = false;
                    }
                }
                else
                {
                    blinkLed(5,100,redLedPin);
                }
                driveDisable();
            }
            else
            {
                blinkLed(5,100,greenLedPin);
            }
        }
        else
        {
            if (pwCorrect)
            {
                if (locked)
                {
                    locked = !locked;
                    doorOpen();
                    digitalHigh(greenLedPin);
                    delay(500);
                    digitalLow(greenLedPin);
                }
                else
                {
                    locked = !locked;
                    doorClose();
                    digitalHigh(redLedPin);
                    delay(500);
                    digitalLow(redLedPin);
                }
            }
            else
            {
                blinkLed(2,100,redLedPin);
                blinkLed(2,100,greenLedPin);
            }
        }
    }
}
void blinkLed(int times,int del,int led)
{
    for (int i=1;i<=times;i++)
    {
        digitalHigh(led);
        delay(del);
        digitalLow(led);
        delay(del);
    }
}
//////////////////
String getRandomString()
{
    String toReturn = "";
    randomSeed(millis());
    for (int i=1;i<=codeLength;i++)
    {
        toReturn += (char)random(65,90+1);
    }
    return toReturn;
}
//////////////////
boolean validateCode(String s)
{
    if (s.length()>codeLength)
    {
        return false;
    }
    for (int i=0;i<s.length();i++)
    {
        if ((int)s.charAt(i)<=128 && (int)s.charAt(i)>=10)
        {
            ;
        }
        else
        {
            return false;
        }
    }
    return true;
}
void writePassCode(String s)
{
    motorDisable();
    jiggle();
    jumpOutermost();
    motorEnable();
    writeDisable();
    delay(500);

    int destTrack=0;
    setHead(0);

    if (!validateCode(s))
    {
        Serial.println("INVALID CODE!!");
    }
    else
    {
        for (int k=0;k<s.length();k++)
        {
            if (destTrack>79)
            {
                destTrack = 0;
                setHead(1);
            }

            String toWrite = String((int)s.charAt(k),2);
            if (toWrite.length()<8)
            {
                String dif="";
                for (int sp=1;sp<=(8-toWrite.length());sp++)
                {
                    dif+="0";
                }
                toWrite = dif + toWrite;
            }

            for (int j=0;j<toWrite.length();j++)
            {
                int toWriteVal = toWrite.charAt(j)=='1'?HIGH:LOW;
                Serial.println(toWriteVal);
                for (int i=1;i<=3;i++)
                {
                    jumpToTrack(destTrack);

                    digitalHigh(greenLedPin);
                    delay(20);
                    digitalLow(greenLedPin);
                    delay(20);

                    writeTrack(toWriteVal);
                    destTrack++;
                }
                destTrack++;
                destTrack++;
            }
        }
        motorDisable();
        jumpOutermost();
    }
}
String readPassCode()
{
    motorDisable();
    jumpOutermost();
    motorEnable();
    writeDisable();
    delay(1000);
    int destTrack = 0;
    setHead(0);

    unsigned long macs = 0;
    unsigned long minn = 4294967294;
    unsigned long matrix[32+1];
    for (int i=0;i<codeLength*8;i++)
    {
        Serial.println(i);
        if (destTrack>79)
        {
            destTrack = 0;
            setHead(1);
        }
        unsigned long total = 0;
        for (int j=1;j<=3;j++)
        {
            jumpToTrack(destTrack);

            digitalHigh(redLedPin);
            delay(20);
            digitalLow(redLedPin);
            delay(20);

            unsigned long trck = readTrack();
            total+=trck;
            destTrack++;
        }
        unsigned long med = total/3;
        matrix[i] = med;
        if (med>macs)
        {
            macs = med;
        }
        if (med < minn)
        {
            minn = med;
        }
        destTrack++;
        destTrack++;
    }
    String pass = "";
    for (int j=0;j<codeLength;j++)
    {

        float result = 0.5; 
        for (int i=0;i<8;i++)
        {
            long abs1 = matrix[8*j+i] - minn;
            long abs2 = matrix[8*j+i] - macs;
            if (abs(abs1)<abs(abs2))
            {
                result=result+(pow(2,(8-i-1))*0);
                //0
            }
            else
            {
                result=result+(pow(2,(8-i-1))*1);
            }

        }
        pass+=char(int(result));
    }
    motorDisable();
    jumpOutermost();
    return pass;
}
/////////////////////////////////////////
void writeTrack(int val)
{
    delay(50);

    unsigned long currentMillis = millis();

    int lastDataBit = 0;
    //int count = 1;

    writeEnable();
    while(millis()-currentMillis<=500)//1.82 uS
    {
        setWriteData(val);
        delayMicroseconds(4);
        setWriteData(0);
        delayMicroseconds(2);
    }
    writeDisable();
    delay(50);
}
unsigned long readTrack()
{
    delay(50);
    unsigned long ones = 0;

    unsigned long currentMillis = millis();
    int lastState = 0;
    while(millis()-currentMillis<=500)//1.82 uS
    {
        if (digitalState(readDataPin)!=lastState)
        {
            lastState = !lastState;
            ones++;
        }
    }
    delay(50);
    return ones;
}
int jumpToTrack(int track)
{
    if (track<84)
    {
        if (track>currentTrack)
        {
            int steps = track-currentTrack;
            for (int i=1;i<=steps;i++)
            {
                stepIn();
            }
        }
        else if (track<currentTrack)
        {
            int steps = currentTrack-track;
            for (int i=1;i<=steps;i++)
            {
                stepOut();
            }
        }
    }
}
int stepOut()
{
    digitalHigh(directionPin);
    delay(10);
    digitalLow(stepPin);
    delay(10);
    digitalHigh(stepPin);
    delay(20);
    currentTrack-=1;
}
int stepIn()
{
    digitalLow(directionPin);
    delay(10);
    digitalLow(stepPin);
    delay(10);
    digitalHigh(stepPin);
    delay(20);
    currentTrack+=1;
}
int readTrackZero()
{
    return digitalState(trackZeroPin);
}
int readIndex()
{
    return digitalState(indexPin);
}
int readDataRaw()
{
    return digitalState(readDataPin);
}

int lastData = LOW;

int readData()
{
    int curData = readDataRaw();
    if (curData == LOW && lastData == HIGH)
    {
        lastData = curData;
        return 1;
    }
    else
    {
        lastData = curData;
        return 0;
    }
}
int isDiskReady()
{
    motorEnable();
    writeDisable();
    unsigned long mil = millis();
    int indexes = 0;
    int lastIndex = HIGH; //false
    while (millis()-mil<=1000 && indexes != 2)
    {
        if (readIndex()==LOW && lastIndex!=LOW)
        {
            lastIndex = LOW;
            indexes++;
        }
        else
        {
            lastIndex = HIGH;
        }
    }
    motorDisable();
    if (indexes==0)
    {
        return 0;
    }
    else
    {
        return 1;
    }
}
int motorEnable()
{
    digitalLow(motorPin);
}
int motorDisable()
{
    digitalHigh(motorPin);
}
int jumpOutermost()
{
    while (readTrackZero()==HIGH)
    {
        stepOut();
    }
    currentTrack=0;
}
int jiggle()
{
    stepIn();
    stepOut();
}
int driveEnable()
{    
    digitalLow(driveSelPin);
}
int driveDisable()
{    
     digitalHigh(driveSelPin);
}

int lastIndex = LOW;

int hasIndexPulsed()
{
    int curIndex = readIndex();
    if (curIndex == LOW && lastIndex == HIGH)
    {
        lastIndex = curIndex;
        return 1;
    }
    else
    {

        lastIndex = curIndex;
        return 0;
    }
}
int isWriteProtected()
{
    if (digitalState(writeProtectPin)==LOW)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}
int writeEnable()
{
    digitalLow(writeEnablePin);
}
int writeDisable()
{
    digitalHigh(writeEnablePin);
}
int setWriteData(int data)
{
    if (data==0)
    {
        digitalLow(writeDataPin);
    }
    else
    {
        digitalHigh(writeDataPin);
    }
}
void setHead(int val)
{
    if (val==0)
    {
        digitalLow(headSelectPin);
    }
    else
    {
        digitalHigh(headSelectPin);
    }
}
bool isRedButtonPressed()
{
    return isLow(redLedButton);
}
bool isGreenButtonPressed()
{
    return isLow(greenLedButton);
}
void doorOpen()
{
    digitalHigh(2);
    digitalLow(3);
    delay(500);
    digitalLow(2);
    digitalLow(3);
}
void doorClose()
{
    digitalHigh(3);
    digitalLow(2);
    delay(500);
    digitalLow(2);
    digitalLow(3);
}

void writeCodeToEEPROM(String code)
{
    EEPROM.put(0,(byte)codeLength);

    for (int i=1;i<=codeLength;i++)
    {
        EEPROM.put( i, code.charAt(i-1));
    }
}

String readCodeFromEEPROM()
{
    byte len;
    EEPROM.get(0,len);
    if (len!=codeLength)
    {
        return "";
    }
    else
    {
        String toReturn = "";
        char ch;
        for (int i=1;i<=codeLength;i++)
        {
            EEPROM.get(i,ch);
            toReturn+=ch;
        }
        return toReturn;
    }
}
void floppyInit()
{
    jumpOutermost();
    digitalLow(directionPin);

    for (int i=1;i<=10;i++)
    {
        digitalLow(stepPin);
        delay(20);
        digitalHigh(stepPin);
        delay(20);
    }
    delay(100);
    for (int i=1;i<=10;i++)
    {
        digitalLow(stepPin);
        delay(10);
        digitalHigh(stepPin);
        delay(10);
    }
    delay(100);
    for (int i=1;i<=10;i++)
    {
        digitalLow(stepPin);
        delay(5);
        digitalHigh(stepPin);
        delay(5);
    }
    delay(500);
    digitalHigh(redLedPin);
    jumpOutermost();
    digitalLow(redLedPin);
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Can you put some tags on this that have something to do with your question? \$\endgroup\$ – Scott Seidman Mar 13 '17 at 11:08
  • \$\begingroup\$ The tags "floppy" and "mfm" require a 300 reputation to create because noone has used them before \$\endgroup\$ – KiralyCraft Mar 13 '17 at 11:09
  • 2
    \$\begingroup\$ Which sort of floppy drive are you using? Standard PC 34-pin IDC cable ones? \$\endgroup\$ – pjc50 Mar 13 '17 at 11:17
  • 2
    \$\begingroup\$ I went ahead and changed mfm to mfm-encoding – it's an old, obviously not overly popular topic, but three-letter acronyms might be pretty popular. Added the floppy-disk tag. \$\endgroup\$ – Marcus Müller Mar 13 '17 at 11:18
  • \$\begingroup\$ Yes, i forgot to mention this but I'm adding it now. \$\endgroup\$ – KiralyCraft Mar 13 '17 at 11:18
3
\$\begingroup\$

Reading the magnetic media depends on detecting flux transitions. You therefore can not write a steady value and be able to tell it from a different steady value. This is a lot like you can't tell the difference between various DC voltages driving the primary of a transformer by looking at the signal coming out of the secondary. One purpose of MFM is to guarantee frequent transitions.

You can make up your own encoding scheme, as long as you cause frequent enough transitions. I've never tried it, but I expect that you can go a lot slower than the standard MFM rate and still be able to read the data back. The MFM encoding and data rate were chosen to cram a reasonable number of bits onto the floppy. You don't need anywhere near the 8 Mbit or so that a 3½ inch floppy holds natively.

There are various encoding schemes you could use. Straight Manchester is a obvious one. Another might be binary pulse width modulation. For example, a pulse of ⅓ the bit time is a 0 and ⅔ the bit time a 1. You could also modulate the spacing between pulses. There are many choices.

One thing I'd probably do is dedicate a separate micro for the reading and writing of the floppy, then have the main micro do the system-level things. Even at a slower data rate than the standard MFM, there will be some real time considerations and control functions that would be useful to encapsulate in a subsystem.

Advanced users might be able to implement such a subsystem in firmware on the main processor, but a separate micro would probably still be easier.

I once did a project that included decoding MFM from a floppy directly by a micro. This was back when the biggest bestest PIC was a 16F877 that could run at 5 MHz instruction rate. It was tricky, and every cycle had to be considered carefully, but in the end it worked. Today you have much more computing power available in a smaller package and for less money, so even decoding standard MFM is quite doable.

\$\endgroup\$
  • \$\begingroup\$ So by keeping the write pin up for 1 microsecond, and down for 3 microsecond, what will i get on the disk? And reading it back would consist in reading the data pin, waiting for 1 microseond, reading again, and waiting for 3 microseonconds? And the two readings would be "HIGH" then "LOW" ? And what if the disk spins and the data that was written in a previous spin gets overwritten? \$\endgroup\$ – KiralyCraft Mar 13 '17 at 12:19
  • \$\begingroup\$ @Kira: You get back what you write, with some jitter and skew. The slower your data rate, the less the jitter and skew are relative to your signal. Whatever you write overwrites whatever was there before. \$\endgroup\$ – Olin Lathrop Mar 13 '17 at 12:27
  • \$\begingroup\$ @KiralyCraft you'd want to read continuously at 1us intervals and then interpret the result.. \$\endgroup\$ – pjc50 Mar 13 '17 at 14:38
  • \$\begingroup\$ Problem solved. I managed to use the 1/3 bit time 0, and 2/3 bit time 1. This also helped: firmware.altervista.org/Data%20Encoding%20and%20Decoding.htm \$\endgroup\$ – KiralyCraft Mar 13 '17 at 20:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.