© James R. Strickland 2016

James R. Strickland, Junk Box Arduino, 10.1007/978-1-4842-1425-1_9

9. ATA Explorer

James R. Strickland

(1)Highlands Ranch, Colorado, USA

This chapter’s project is about connecting a hard drive to your Cestino. Figure 9-1 shows what our final result looks like. Do you have an old drive from back in the day? This will be a fun chapter. You’ll enjoy it.

A340964_1_En_9_Fig1_HTML.jpg
Figure 9-1. ATA Explorer

One day, perhaps a decade or two out, the last manufacturer will quietly stop making mechanical hard drives, and the era of rotating media will draw to a close. What was once a glorious era of whirring, whining, and clicking drives will fade away, leaving only silent, fast, and efficient electronic storage. Floppies are gone. DVD-Rom is slowly being eclipsed by SD cards. And hard drives? The writing is on the wall for them too. The most modern “drives” connect directly to the PCI-E bus in your computer or to USB 3.0, and the various artifacts of drives are implimented in firmware to keep operating systems and software happy.

The demise of mechanical drives (and drives of any kind) is not a bad thing for us. After all. The drives wind up in the junk box. They might have sensitive data on them, and having them properly destroyed is a nuisance. Let’s have some fun with one.

If your junk box is like mine, the oldest drives in it are what we now call Parallel ATA, and used to call IDE: drives with a 40 pin ribbon connector in the back. That’s good. They’re the ones we can connect to the Cestino and access, despite our rather pokey logic speeds and sloppy timing.

The Stuff You Need

This project is more complex than most of the previous, so it has more parts, including several that won’t go on the breadboard.

New Parts

40 pins worth of extra long pin headers

These are available at Adafruit or Schmartboard.com. Make sure they’re the kind with equal lengths of pin on both sides of the plastic divider, just like we used on the Cestino itself for the TTL-232 connector. Regular pin headers won’t work. You’ll have to cut one of the rows in half and take out one pin for the key pin. If you have scraps of pin header with only a few pins each, you can combine these and they’ll work.

Note

You might be tempted to use one of those nifty breakout boards for modern 40 pin Raspberry Pis as a convenient way to connect the 40 pin PATA cable to your breadboard. Unfortunately, the various ground and power busses of the Pi are tied together in the breakout boards, and you wind up with the whole Cestino board shorted out. Alas.

Used Parts

A 40 pin PATA cable

Look closely. If it has only 32 pins and has a twist in some of the wires? That’s a floppy cable. A PATA cable will have no twists. It may have 80 conductors, but both connectors will have only 40 pins. (The other conductors are wired to existing grounds and are used to kill noise and crosstalk between the wires of the 40 pin standard.) It must also have two drive connectors and one motherboard connector, all of them exactly the same. Most important, the drive connectors should /not/ say “Master,” “Slave,” “Drive 0,” or “Drive 1” on them. If they do, you may have a cable that wants to select your drive ID for you. The kind we need is absolutely the most common: a piece of straight-through ribbon cable with three 40 pin connectors on it, wired in parallel. We’ll be using this cable to connect the drive to our breadboard.

An ATX power supply and a paperclip

Really, any power supply designed to power hard drives through a 4 pin molex connector will work. (It must have both 12v and 5v pins.) You can buy special power supplies for this purpose, or you can use an existing drive case with the cover off, but the easiest way I know is to haul out that old ATX power supply from the junk box, plug one of its drive power connectors into the drive, and hotwire it with the paper clip. I’ll cover the hotwiring later. If your ATX power supply has a power switch on the back, so much the better. It’s very handy to have, but it’s not required. Any ATX power supply has far, far more current than we need to drive one old hard drive. Whether it came out of some micro-ATX set-top box or your old tower, it will be fine as long as it works.

Note

Power supplies don’t live forever. Inexpensive ones in particular will have cheap electrolytic filter capacitors to filter noise out of the 5v and 12v outputs. These capacitors dry out over time, particularly in high heat environments (like the inside of a hard-working power supply), or from disuse. The worst case is they’ll short out, which will prevent the power supply from starting. Even if it does start, there may be so much noise on the 5v line from dried out filter capacitors that the drive can’t communicate effectively. Replacing the caps exposes you (and your soldering) to high voltage, so I don’t recommend it. Power supplies are cheap.

If you have an even more ancient XT or AT power supply, you can use that, too, and you don’t even have to hotwire it. The power supply will be labeled somewhere what standard it’s part of.

A PATA drive

PATA drives are easy to recognize. Desktop drives have a wide connector across the back with 40 pins, followed by a set of jumpers to select whether a given drive is drive 0 or drive 1 in a given PATA chain. Make sure your drive is set to 0 or Master. There may be other jumpers to allow such features as cable select and so forth. Next to the jumpers will be the power connector.

If the drive has a 50 (or more) pin interface and talks about addresses, it’s a SCSI drive, and is not useful for this project. Likewise, if it has a pair of horizontal L shaped connectors, it’s a SATA drive. While SATA is a close relative of PATA, the demands of SATA in terms of speed make communicating with a bit-banged Cestino interface impossible.

If the drive you’re looking at is a 2.5 inch drive from a laptop with 44 pins, you are looking at a 44 pin PATA interface, which includes all four power pins plus the standard 40 pins of the PATA interface. If you have one of these, you’ll need an adapter to connect it to the 40 pin PATA interface, but it will work. These adaptors are still common and still very cheap. You may even have one in your junk box.

Some drives won’t work. If the drive is larger than 128GiB, it’s going to want 48-bit logical block addressing, and this sketch can’t do that. If it’s so old that it doesn’t have logical block addressing at all, likewise, it won’t work. The best vintage of drive seems to be late 1990s to early 2000s.

The Bad Old Days

Just for a moment, let’s flash back to the heady days when IDE/ATA was new and awesome. My first PC, a mighty 10mhz 8088 clone machine, cobbled together from leftovers in 1990, came with two 5.25 inch floppy drives and the first hard disk I ever owned: a 5.25 inch half-height 40 mebibyte RLL hard drive. (The “modern” 3.5 inch by 1 inch format was available by 1988, but the old 5.25 half-heights were cheaper.) Therein lay the problem. When PC hard disks were first introduced in 1980, with the Seagate ST 506, they used a standard called MFM—modified frequency modulation—essentially the same encoding scheme used for most floppy drives right up to the end. RLL drives were different. They could get more bits onto the same physical drive mechanism. This was a big win. Drives were expensive.

Encoding technology is complicated business. RLL stands for Run Length Limited , where a run is the number of bits in a row that can occur without a flux reversal on the drive. RLL is like a family name. Technically MFM is a Run Length Limited format, too, as is IDE/ATA. In those days, however, RLL referred to a specific encoding standard not otherwise named.

The bad news was that the RLL standard was like the Betamax standard. MFM was much more common. When I upgraded that machine to a 286, with a 16 bit bus, finding a 16 bit RLL card to support it was a real problem.

The underlying issue was this: In those days, most of the smarts to control a drive were on the drive control card, and there were dozens of standards: RLL, MFM, ESDI, and SCSI leap to mind, but there were many others. Each had its advantages and proponents. None of them would interoperate. All of them came with little driver patches stored in ROM that loaded into your PC at boot time, but if your operating system had its own drivers, they needed to know about your particular hard drive controller. Worse, drives were changing so fast that the second generation of a “standard” might not interoperate with the card that drove your first generation drive. You might not be able to operate two drives separated by a few years on the same controller. The way the data was written to the drive was evolving, drive speeds were evolving, and all these things necessitated different controller technology.

Arriving at the same time as ESDI and a year before SCSI was IDE, the standard we now know as PATA, properly called in its documentation as ATA.

It was awful.

IDE was a marketing term for Integrated Drive Electronics . This meant that the drive’s controller was attached to the drive itself rather than an addon board in the computer. The IDE interface on the host side was very simple. It basically repeated the signals of the PC ISA bus to the drive.

We hated it. It was PC specific, for one thing. Also, whereas the sophisticated controller boards for MFM, RLL, ESDI, and SCSI would do all the actual computing required to access drive information, IDE offloaded this to the host computer. In those days, CPUs were so slow you could feel the difference.

Where ATA, now PATA and SATA shined and still shine is simplicity. You plugged a cable from the drive to the host adapter, and plugged the host adaptor into the bus of your PC. Manufacturers loved it because for the cost of adding a connector to the motherboard, more or less, the system could have a hard disk interface. We learned to love it because it drove the cost of hard drives down very, very quickly, to the point that I bought my first 1GiB drive (supposedly, see the rant below) by 1995, and you’d better believe it was PATA.

Today, of course, nearly all hard drives and their SSD counterparts use either PATA or SATA, and the controllers are built into the System-On-A-Chip or (for PCs) the SouthBridge part of the chipset. Most of the old standards are gone, SCSI is changed virtually beyond recognition, and even PATA has begun to slip beneath the waves. For personal computing, at least, it’s SATA or SD cards (which communicate over SPI, among other things. The Cestino can do that.)

The advent of SSD (Solid State Drives , in case you wondered) raises the question as to whether even SATA will survive. It is now possible to simply attach an SSD directly to a computer’s main bus, bypassing the extra host adapter hardware altogether. I very much expect that we will see flash or similar solid state storage as part of the memory map of a computer soon. The whole paradigm of booting from disk, saving to disk, and so forth may slowly fade away when storage is just a matter of transferring data from one area of RAM to another, where the flash is.

A short rant about units. Once upon a time, you bought a floppy disk that held 360 kilobytes. You reasonably expected it to hold, minus space for its format, 368,640 bytes. In the RAM and ROM/Eprom world, JEDEC standards required that RAM be measured in binary units, that a binary kilobyte was 210 bytes or 1024 bytes, a binary mega was 220 or 1048576 bytes, and a binary gigabyte was 230 or 1,073,741,824 bytes, and so on. The hard drive world had no such standard, although generally they lived with the JEDEC standard. In the early 1990s, the race to build and market the first gigabyte drives began. Drive manufacturers began to lie. A gigabyte, they argued, was a billion bytes. This was pure marketing evil. A (base 10) gigabyte was, in reality, only about 954 of what we used to call megabytes, and they got dragged into court in a class action lawsuit, which was ultimately settled out of court. Their defense for their marketing evil was that the international standards upon which the metric system is based clearly define kilo as 103 mega as 106 and giga as 109, which although true is irrelevant.

The units we use today, and which I’ll grudgingly use for this chapter, have “bi” in the middle to make sure there is no weasel room to define them as anything other than binary units. Kibibytes, mebibytes, gibibytes, and tibibytes are the units that hard drives are measured in, when they’re measured properly (and honestly.) Ram and any other devices conforming to the JEDEC standards, however, are safe to talk about in kilobytes, megabytes, and gigabytes. The unit name changes because hard drive manufacturers were dishonest. End of rant.

ATA

When they were first introduced, the drives we now call PATA were IDE drives . This was a marketing term for Integrated Drive Electronics. These electronics provided a standard interface, particularly to the PC ISA bus, for a drive regardless of its underlying technology. We’re going to dig deep into what those electronics actually do and how to use them.

Even in the early days, when floppies ruled, drive control electronics were relatively sophisticated. You selected some registers in the controller, wrote some bytes to them, and the controller handed you bytes, or stored your bytes. The controller handled the analog to digital and digital to analog conversion, data clocking, encoding and decoding of flux transitions , and so forth, and by the 1980s these were usually a single IC. (In the late 1970s, it was different. The reference designs for floppy controllers used dozens of logic ICs. When the Apple 2 needed a floppy drive interface, Steve Wosniac replaced all that logic with an EPROM, a pair of 555 timers, and some latches, and let the CPU handle everything else, which let Apple produce the drives and interfaces at an affordable price.)

If this sounds a lot like what we did in Chapter 8 writing to the flash IC, you’ve got a good ear. If you suspect that a hard drive might have similar notions to both floppies and the flash IC about control registers and so on, you’re more right than you may know. Most complex ICs are controlled that way. A lot of retro-computer enthusiasts decry the use of modern microcontrollers like the ATmega1284P in retrocomputing projects, but the truth is that the classic ICs: 8250 UARTS for RS232 communications , 8255 programmable peripheral interface (used for PC parallel interfaces like printers), were more like dedicated microcontrollers than logic. The controller on a PATA drive is no exception.

PATA spanned about a decade of drive development, from the late 1980s before it was standardized through 2013, or so, when the last manufacturer switched over to serial ATA, or SATA. Drives evolved in size from hundreds of mebibytes to hundreds of gibibytes. As a result, the standard evolved over the years, adding faster communication modes, larger address space, and so on. We’ll be using the original 1994 version of the standard: X3.221-1994.

Even if they’ve been withdrawn like X3.221-1994 was in 1999, standards are usually owned by someone. I had to get permission from ANSI to use X3.221-1994. While the folks at ANSI entirely reasonable and helpful about the whole process, it’s something to keep in mind when putting your projects on the web, particularly if they include register names and commands: someone owns that, and it’s probably not you. You should also read the disclaimer at the front of this book that was required by ANSI.

We’ll be using the original PIO mode , now called PIO 0, for programmed input/output. It’s the simplest, and slowest, way to access the drive. There are other, faster modes, particularly as you get into more modern ATA standards.

Note

ATA1 aka X3.221-1994 has only 28 bit large block addressing, and thus can access only 128GiB of drive space. That said, the sketch presented here will work if your drive is bigger than that, but nothing above 128GiB will be visible, and you may not be able to write to the drive.

Bit Width

PATA drives transfer data in 16 bit words to and from a 16 bit data register not unlike the two ports we’re using on the Cestino to send and receive those bits. We’ll be using two ports, A and C, as usual, but they’ll be the data bus rather than the address bus. It’s very important to note that while PATA can transfer 16 bit words, the data register is the only 16 bit register we can get at. The rest of the accessible registers are all 8 bit, so for those we will read only the LSB (least significant byte) of the 16 bit data bus.

Endian-ness

Because we’re dealing with 16 bit words, we have to deal with endian-ness, too. Which endian type is the drive? Well, it varies. When you read the drive information block, it is big-endian. Once the drive is storing your data, it no longer cares what the endianness is. It will store it however the host computer sends it. If your drive came out of a PC, it’s little-endian, the standard used by Intel since the beginning. If it came out of other systems, particularly a PowerPC era Macintosh, it is probably big-endian, as the PowerPC could go either way and Apple came to PowerPC from Motorola CPUs, which were always big-endian. (68k Macs used SCSI drives, which this sketch won’t work with.)

The sketch assumes the data on your drive, if any, will be little-endian. If you know the drive came out of a big-endian machine, you’ll need to make a slight alteration in the sketch. I’ll point it out when we get there, but you should know that I haven’t tested the sketch with a big-endian data drive. Intel era Macs have the same endian-ness as PCs: little.

Anatomy of a PATA Drive

In order to do this project, we have to dig a little deeper into the PATA drive. You could argue that 90 percent of that is really the built-in controller, but from our perspective on the outside of the drive, the controller and drive are one unit, just like a flash IC or an EPROM, so we have to treat them that way, more or less.

There are a number of parts to the drive. I have them in rough order by level of abstraction from our point of view outside the drive: signals with outside connections, registers you can get at with signals, and things you can access once you can get at the registers.

Control Signals

ATA drives have quite a few control signals. We’ll be using most of them, although we’ll ignore the DMA transfer signals and a few others, and save ourselves some wiring. These signals allow TTL and similar electronic logic to control things like the flow of data to and from the sector buffer at speed, just as they did in the EPROM/FLASH explorer in Chapter 8.

DD0-DD15

These are data IO lines. The first eight are used when communicating with the drive controller, and all 16 are used for transferring data.

/DIOW, /DIOR

These are the Drive I/O Write and Read signals. As with the Flash Explorer project in Chapter 8, this means reading and writing to the drive controller, not necessarily to the drive. Essentially, these lines tell the drive controller how to set up its data direction settings, just as we will on the Cestino.

DA0-DA2

Drive Address Bus lines 0-2. The drive controller has many registers. We use these lines to select which one we want. In addition to these are the following.

/CS1FX, /CS3FX

These are chip select signals. They tell the drive controller whether we want to access the command block registers or the control block registers. Together with the DA0-DA2 address signals, these select the specific register to address. These signals are active low.

Because there are three address signals and two select signals, in theory, we could have 32 registers, and they can have different meaning depending on whether we read them or write them. I suspect part of why ATA has had such a lengthy history is that they left plenty of room in the registers to expand things. Thirty-two registers with two modes (read or write) each are a lot.

/Reset

Resets the drive.

/DASP

This signal indicates when a drive is active, or that drive 1 is present. Because we only have drive 0, we’re using this in its “Drive active” mode. When it goes low, it means the drive activity light should be lit. Sounds like a job for the logic probe section of the Cestino.

Registers

A register, you will recall, is an area of memory in a microcontroller or microprocessor in which a single data word (often a single byte) can be placed. Usually each bit has a specific meaning. It’s the same for the registers in the PATA controller. Only the data register holds more than 8 bits (it holds 16). Here’s the list of registers and what they do.

Alternate Status Register

Address: 0b110. Chip select: /CS3FX.

This register contains the status bits of the drive. It’s a copy of the main status register, but getting at it doesn’t muck around with the drive completing other actions. We use it for every status query. In order, from highest to lowest bit, just as you’d expect in 0b notation the bits are:

  • BSY: Drive is busy. This means the drive can’t accept any new commands from the command block registers. Literally, it means the drive has access to the command block registers, so the host should not.

  • DRDY: Drive is Ready. The drive can respond to a new command. The host can go ahead and write to the command block registers.

  • DWF: Drive Write fault. Something bad happened during the write.

  • DSC: Drive Seek Complete. The host told the drive to go to a particular track, and the heads are there. Hard drives are physical mechanisms. This takes a certain amount of time.

  • DRQ: Data Request. The drive is ready to transfer data between the host and the drive, either direction. Data can be in bytes or 16 bit words.

  • CORR: Corrected Data. The drive had a data error but fixed the data. We don’t actually use this bit, since we’re not doing critical data transfers.

  • IDX: Index. Set on once every revolution of the drive. Basically means: drive is spinning.

  • ERR: Error. Something bad happened. Read the error register to find out what.

The Alternate Status register can also be written to, in which case is it called the Device Control register. When it is, raising bit 2 will reset the drive, and raising bit 1 enables interrupts for the drive. We’ll be flipping bit 2 once in a while.

Data Register

Address:0b000. Chip Select: /CS1FX.

This is a 16 bit register (all others are 8 bit). When we want to read or write data to the drive, this is the register we use.

Drive Address Register

Address: 0b111. Chip Select: /CS3FX.

This register contains drive select and head register information, inverted. It’s a read-only register, and we don’t use it.

Sector Count Register

Address: 0b010. Chip Select: /CS1FX.

This register tells how many sectors the drive expects to transfer on the next read or write operation. This is really for DMA and large scale transfers. We’re going one sector (LBA block) at a time.

Sector Number Register

Address 0b011. Chip Select: /CS1FX

When doing a transfer, start with the sector in this register. In LBA mode, which we’re going to use, this register contains the lowest 8 bits of the 28 bit LBA block address. At the end of a command, you can read the lowest 8 bits of the current LBA block address from this register. This one’s important. We use it.

Cylinder Low and High Registers

Addresses: 0b100, 0b101. Chip Select: /CS1FX

In Track/Sector mode, these registers would hold the low and high bits of the cylinder of the drive to use. In LBA mode, they hold bits 8-15 and 16-23 of the 28 bit block address. We use these too.

Drive/Head Register

Address: 0b110. Chip Select: /CS1FX.

In Track/Sector mode, the first four bits of this register select which of up to 16 heads to use. It also contains one bit (bit 4) to select which drive to use, and one bit (bit 6) to select whether we’re in LBA mode or not. If we are in LBA mode, bits 0 to 3 contain the highest four bits (24-27) of the block address.

Status/Command Register

Address: 0b111. Chip Select: /CS1FX

Contains the same information as the Alternate Status Register, but has a lot of other side effects and interactions. We use the Alternate Status Register instead. When you write to it, this register is the Command Register, and we send many commands to the drive through this register.

The Sector Buffer

The Sector Buffer is a batch of memory in the drive that contains one sector of data: 512 bytes. This is the minimum size block the PATA drive will transfer data in. It’s also the size of an LBA block, and conveniently, it’s the size of MS-DOS (and thus Windows) filesystem data blocks. The Sector Buffer is used mostly for transferring data onto and off of the physical disk(s), but the Identify Drive command loads the sector buffer with pre-programmed drive information without reading the physical disk at all. It’s the sector buffer that is read and written through the 16 bit data register.

Commands

As with registers, there are lots of commands. In the sketch, because there are so few parameters to set on other registers directly, I’ve included a few parameters in the CMD command list, and I’ll include them here as well. Unlike the registers, I’m only going to list the commands we use.

Sleep

Value: 0x99. Register: Status/Command.

This command tells the drive to spin down and go to sleep. In sleep mode, most other commands will fail, and because of how the sketch’s ready detection works, most will hang the sketch until the drive is reset.

Identify Drive

Value: 0xEC. Register: Status/Command.

What it does: Tells the drive controller to copy its identity information—the drive serial number, number of tracks, sectors, cylinders, and heads, number of LBA blocks and potentially a lot more from wherever it’s stored in the controller to the sector buffer, where it can be read like a normal sector.

Sequence of Events: The drive sets the BSY signal, copies the identity information into the sector buffer, sets DRQ, and generates an interrupt (which we ignore.). When we next read the status buffer (which normally the host would do when the interrupt gets set) the drive clears the interrupt and DRQ.

Read Sectors (With Retry)

Value: 0x20. Register: Status/Command.

What it does: This command copies the data from the physical disk(s) at the address set in the address registers into the sector buffer. It can copy sequences of blocks, but we’re not using that functionality. Retrying means that if it encounters an error it can try to read the block again and correct the data in the sector buffer.

Sequence of Events: The host (the Cestino) sets the address parameters and LBA mode bit, if any, then writes the command to the command register. The drive sets DRQ and BSY, then seeks the requested track and sector (or LBA address).

If we’re transferring more than one block, when the read completes, the address of the last sector will be in the address registers. If the read doesn’t complete without an error, the address registers will point to the sector where the error occurred.

When the read is complete, the drive sets INTRQ, the interrupt request. The host reads the status register on interrupt, which causes the drive to clear INTRQ. The drive then clears DRQ, and if there’s another sector to be transferred, sets BSY again.

We skip a lot of the host steps in the sketch, and it still works anyway.

Write Sectors (With Retry)

Value: 0x30. Register: Status/Command

What it does: Transfer the contents of the sector buffer onto the physical disk(s). Again, retries are allowed to correct the data.

Sequence of Events: The host (Cestino) sets the address registers for the sector (or block) we’re writing to, along with setting the LBA flag (in our case) on the Drive/Head register. The host then writes the command to the Status/Command register and things begin to happen.

The drive seeks the track/sector/head or LBA address and sets DRQ when it’s ready to receive the first (and only, in our case) sector/block of data. The host writes the sector/block to the sector buffer. The drive clears DRQ and sets BSY instead.

When the drive is done, it sets INTRQ, the host reads the status register, the drive clears the register and, if no more sectors/blocks are expected, clears BSY.

Once again, the sketch skips a bunch of these steps, particularly the interrupt steps. We get away with this because we’re only writing one sector (block) at a time.

LBA Mode

Value: 0x40. Register: Drive/Head (LBA3)

This is not a real command. It’s an addon to the Drive/Head register address to ensure Large Block Access mode is set. This simplifies the whole cylinder/track/sector/head business to a flat 28 bit address that points to a given 512 byte block, exactly the same in all other respects as a sector. It’s ANDed with the four address bits going into that address.

The Physical Disk (s)

This is my catch-all for the storage media. Most data either winds up here or comes from here. I’d love to be able to explain in the usual detail how bits go to the disk and become magnetic transitions on the rotating media, but the truth is that the huge variety of PATA drives spans huge swaths of different recording technology. Perpendicular Magnetic Recording? Flash? M-RAM? Bubble Memory Cartridge? Something else entirely? It doesn’t matter. The entire point of the ATA interface is that we don’t care how it works. The controller presents whatever the storage technology is as blocks of 512 bytes, and uses one of the standard methods of addressing, and what happens under the hood isn’t our problem. There may be special considerations, like in the old days when you had to park disks before turning them off or the heads would touch down wherever they happened to be, but finding drives that old that still work isn’t very likely at this point.

Build the ATA Explorer

Okay. Now that we know in principle what’s in a PATA drive and how to talk to it, let’s build the project. We’ll start with the ATX power supply. If you’re not using one, you can skip ahead.

Hotwire the ATX Power Supply

Got an ATX power supply? Does it have a big, two row motherboard connector like the one in Figure 9-2? It might be a 20 or 24 pin connector. I’ve covered both in Figure 9-2. The alternate pin numbers are shown on the right, and I’ll refer to them the same way in the text. Unplug the power supply from the wall, just on general principles.

A340964_1_En_9_Fig2_HTML.jpg
Figure 9-2. ATX Power Supply Connector Pinout (Connector View)

The wires of the motherboard connector are color-coded. Pin 14/16, as you can see from Figure 9-2, is Power On, and its active low. It’s the only green wire in the bunch. Conveniently, it’s surrounded by Com (ground) pins, so take your paperclip and stick it in pin 14/16’s socket, and from there to either pin 13/15 or pin 15/17.

Most ATX power supplies made in the last decade and a half will start without a load, so go ahead and put the connector down and plug the power supply in. If there’s a power switch on the supply, make sure it’s on. The fans on the supply should start. If they don’t, disconnect the power and check your connections. The supply should do nothing if you get it wrong. The only wire that’s live besides the 3.3v PS On sense wire is +5vSB, the purple wire, on the other side of the connector. Obviously if the paperclip glows red hot, turn everything off right away.

Power supply starts up? Great. Grab your multimeter and measure from pin 10 to ground and from pin 4 to ground. You should get twelve and five volts DC, respectively.

Power supply didn’t start? Some really old ones need some kind of load to start, so if nothing else seems amiss, go ahead to the next step anyway.

Shut everything off and/or unplug it from the wall, and plug one of the drive power connectors into your hard drive. Check your polarity. Worn molex drive connectors can go in upside down, and it’s not good for the drive. If it starts now, you’re in business. The drive may or may not spin up, depending on its settings. If it does, it’s a very good sign. You might want to tape your paperclip in place with electrical tape.

Power supply still didn’t start? Check your paperclip. Make sure it’s plugged in to the right pins and big enough to make contact with the socket.

There are a lot of tutorials on the net to open up the power supply and turn it into a bench power supply. I have one like that. It works very well, but follow those tutorials at your own risk.

Likewise, it’s certainly possible to power your Cestino from an ATX power supply, but if you get a 12 volt rail instead of a 5 volt, you will probably fry all the electronics connected to your breadboard, all at once. Been there, done that.

Safety Note

ATX and similar PC power supplies are generally safe for humans on the outside, but inside the metal case are voltages that could kill or severely injure you. These voltages may still be present even with the power disconnected from the wall. If the power supply has been wet, smoked/been on fire, or if it’s shocked someone already, it’s dangerous. Don’t use it. Also, be aware at all times that a running power supply has plenty of energy to heat things red hot and start fires.

Set Up the PATA Cable and Pins

The prototype for this project amounted to lots of pin-to-socket wires plugged into the 40 pin connector of a drive. It worked. You can do it that way if you really want, but I’ll tell you it was a misery to wire and wasn’t very reliable. That’s why I came up with this new method.

Your PATA cable should have three connectors that look like Figure 9-3 above. One will be further from the other two. It was meant to connect to your system’s host adapter, either on a card or on the motherboard itself. The two connectors close together were meant to connect to drives. They should all be the same. My cable had “master” and “slave” printed on the ribbon cable next to the connectors, so I went through all the pins and checked them with my multimeter to make sure they’re all straight through. They are.

A340964_1_En_9_Fig3_HTML.jpg
Figure 9-3. PATA Connector Pinout, Drive Side

The pinout diagram in Figure 9-3, like most of the diagrams you’ll find online, is how the pins appear from the back, when plugged into your breadboard. They’re also how the pins are in the drive’s connector. When you’re looking at cable connector’s socket side, they’re backward.

On the end of the cable with two connectors closer together, take the outer connector and plug 20 pins of your extra long pin header supply into the row of sockets closest to the plastic bump or furthest from the blocked socket that may be present on the opposite side. My cable has both. This connector will serve the odd numbered pins of the interface.

If your cable has the blocked socket 20, you’ll need to cut a piece of pin header with 9 pins and one with 10. Take the middle drive connector and plug these pin headers in to the side with the blocked socket. They’ll only go in the correct way, with 9 pins on one side of the blocked socket and 10 pins on the other.

If your connectors don’t have a blocked socket (they must, in that case, have the plastic bump) you can go ahead and take the middle connector and put another 20 pins worth of pin header into the row of sockets farthest from the plastic bump. We won’t use pin 20 in any case. Do whichever is easiest. This will be the even pin connector.

Your ATA cable should look like Figure 9-4.

A340964_1_En_9_Fig4_HTML.jpg
Figure 9-4. ATA Cable Set Up for Breadboard Connection

With your Cestino breadboard reset-button-end down (assuming you built it like the one in the pictures), plug the end connector (odd pins) into your second breadboard on the side of the IC trench closest to the Cestino, and the middle connector (even pins) into the other side of the trench. This should put the motherboard connector extending off to the right of the Cestino.

There’s nothing special about this orientation, other than it puts the connections of the PATA cable in the same order they appear in Figure 9-3, which makes it easier to keep the wiring straight.

Wiring Up

Before we get into the port wiring, we need to wire up all the grounds. As you can see in the schematic in Figure 9-5, there are a lot of them, and they all need to be connected. On the PATA interface, pin 19, on the odd connector, and pins 2, 22, 24, 26, 30, and 40 on the even connector should all be wired to the - bus.

A340964_1_En_9_Fig5_HTML.jpg
Figure 9-5. ATA Explorer Schematic

This is another three port setup, although for wiring convenience it’s different from the one we used in Chapter 8. We’ll use PORT A for the LSB port—the least significant byte of our data bus, in order, so PA0 connects to DD0, and PA7 connects to DD7. We’ll use PORT C for the MSB of that bus, and PC0 will connect to DD8, PC1 will connect to DD9 and PC7 will connect to DD15. We’ll use PORT B for the control bus.

Remember that PORT A is backward, running from pin 40 of the ATmega1284P for PA0 to pin 33 for PA7. Wire these pins to DD0 to DD7, on the odd numbered side of the connector, where DD0 is pin 17 and DD7 is pin 3. These wires will all cross over each other. Make sure you leave some length for that. Once you have the port wired, go ahead and hank the wires up to keep them out of trouble.

We’ll wire port C to the MSB data pins next. Port C goes from pin 22 of the ATmega1284P for PC0 to pin 29 for PC7, and pins DD8 to DD15 go down the even connector, starting at pin 4 and going to pin 18. I led these wires around the bottom of the PATA connection area, mostly for a pretty photograph. Once you have these wires in place, go ahead and hank them up, too.

Note

Hanking the wires can pull them partway loose from the breadboard socket. Check your connections after you finish hanking a group of wires. A loose wire or two can make data transfers fail in bizarre ways.

Finally, wire PB0 (ATmega1284P pin 1) to /RESET (pin 1) of the PATA interface, PB1 to /DIOR (pin 25), PB2 to /DIOW (pin 23), PB3-PB5 to DA0-DA2 (pins 35,33, and 36, respectively), PB6 to /CS1FX (pin 37) and PB7 to /CS3FX (pin 38).

The Sketch

This is a big sketch, so I’m going to break it up into sections more obviously than I have in previous sketches.

Preprocessor Definitions

I’ve been using preprocessor definitions (#defines) for a variety of small tasks. Usually there have been a small handful of them. They take center stage in this project, because there are a lot of bit patterns we need to set the control signals of the drive into in order to get what we want.

The first group of #defines lays out what ports do what. If you want to change which ports the sketch uses, changing the values here will do that for you (but you have to sort the wiring out for yourself.)

First, we define the control port, which covers the /RESET signal, /DIOR and /DIOW read and write signals, the DA0-DA2 address signals, and the /CS1FX and /CS3FX signals.

#define CONTROL_PORT PORTB
#define CONTROL_DDR DDRB
#define CONTROL_PINS PINB

The next group of defines covers the least significant byte of the data bus.

#define LSB_DDR DDRA
#define LSB_PORT PORTA
#define LSB_PINS PINA

As you might expect, the most significant byte of the data bus comes next.

#define MSB_DDR DDRC
#define MSB_PORT PORTC
#define MSB_PINS PINC

While we’re defining our Cestino ports, let’s also define read and write mode, for convenience. These values are passed to the DDR register on the Cestino for the particular port we’re dealing with. These are the usual 1 for write and 0 for read codes.

#define DATA_READ 0b00000000
#define DATA_WRITE 0b11111111

Remember all those registers? We select them with a combination of bit patterns set to the control port. For reference, the port is laid out, in 0bxxx order, like this:

/CS3FX,/CS1FX,DA2,DA1,DA0,/DIOW,/DIOR, and /RESET.

#define REG_DEFAULT     0b11000111

This is a register where sending signals does nothing. We use it for safety.

#define REG_STAT_COM     0b10111111

The Status/Command Register, where we send nearly all of the CMD bit patterns, except the one that isn’t really a command.

#define REG_ASTAT_DEVCOM 0b01110111

This is the register we read the status from. It’s the Alt Status/Device Command register.

#define REG_ERR_FEAT     0b10001111

Error/features register. If we need to check for errors, this is where we go.

#define REG_LBA0         0b10011111
#define REG_LBA1         0b10100111
#define REG_LBA2         0b10101111
#define REG_LBA3         0b10110111

These 3.5 bytes are the registers where we put all 28 bits of a Large Block Access address. LBA is a “flat address space” for 512 byte blocks of the drive, rather than messing around with track, sector, and head, which changes from drive to drive.

#define REG_DATA         0b10000111

This is the data register, the only 16 bit register in the system. All large data transfers go to and from this register, although some may be only 8 bits wide.

#define REG_HEAD REG_LBA3 //Same register, different modes.

The head register has a number of functions besides the half of it used for LBA3, so we define it again for clarity.

Does this list start to look like the registers list? It should. Likewise, the command #defines will look exactly like the commands list. All of these commands except the last are sent to the Stat/Com register.

#define CMD_SLEEP 0x99

Put the drive to sleep/spin down.

#define CMD_INIT 0x91

Initialize the drive so we have a known state.

#define CMD_IDENTIFY 0xEC

Copy the drive’s id info to the drive sector buffer so we can read it with the data register.

#define CMD_READ_WITH_RETRY 0x20

Copy a block from the physical disk(s) to the sector buffer, so we can read it with the data register.

#define CMD_WRITE_BUFFER 0xE8

Write data to the sector buffer. We don’t actually use this command.

#define CMD_WRITE_WITH_RETRY 0x30

Write the sector buffer to the physical disk(s). The catch is, as we write our data to the buffer, it’s copied to the disk(s) right now.

#define CMD_LBA_MODE 0b01000000

Set the drive to LBA mode. This is the one command that isn’t really a command. It’s just defined here because it acts like one. It’s applied to the head register to make sure the drive is in LBA mode.

When we read signals from the status register, they come out in this order: BSY DRDY DWF DSC DRQ CORR IDX ERR. If we AND the following masks with the status register, we can pop out the one bit we’re interested in with a single step. We don’t use most of these statuses, but they’re here in case you need them down the road.

#define BSY 0x80 //0b10000000

Busy

#define RDY 0x40 //0b01000000

Ready

#define DF  0x20 //0b00100000

Drive Write Fault

#define DSC 0x10 //0b00010000

Drive Seek Complete

#define DRQ 0x08 //0b00001000

Data Request

#define CORR 0x04 //0b00000100

Corrected Data

#define IDX 0x02 //0b00000010

Index

#define ERR 0x01 //0b00000001

Error

There are also #defines for the read/write line statuses. It’s a big script, so there are a lot of things #defined that could be left as bit patterns in smaller scripts. The signals, in 0bxxx order are: /DIOW, /DIOR, and /RESET.

#define HW_OFF  0b111                                          

Set the read-write lines all off.

#define HW_READ 0b101

Set the read-write lines for reading.

#define HW_WRITE 0b011

Set the read-write lines for writing.

#define HW_RESET 0b110

Lower the reset line to reset.

Because we have to switch from big endian to little endian reading the sector buffer, I went ahead and defined BIG_ENDIAN and LITTLE_ENDIAN for transfer_sector_buffer(), which transfers data to and from the drive’s sector/LBA buffer. We’ll talk about that function shortly.

#define BIG_ENDIAN true
#define LITTLE_ENDIAN false

Debug mode is a special case. This sketch took a lot of work to get right, and there’s a lot of debug code in it, mostly Serial.println() commands. I could have gone through and taken them out, as I have in the shorter sketches, but instead, when I put them in, I enclosed them in #ifdef and #endif precompiler macros.

What these do is if DEBUG is defined, those statements get compiled in. If it’s not defined, they don’t. Here’s the definition of DEBUG, which you can see is commented out.

//#define DEBUG

If you define DEBUG, every function in the sketch that has debug code will have something to say when it’s called. It can get overwhelming. But there’s a trick. You can #define DEBUG inside a particular function. It will stay defined for the rest of the sketch, /or/ until it hits another precompiler macro called #undef. As the name suggests, #undef undoes a #define. So if you #define DEBUG at the beginning of a function, and you #undef DEBUG at the end, you can compile in the debug statements for that function alone. The precompiler is a powerful thing. We’ve barely scratched the surface of what it can do.

Global Variables

Global variables are really considered poor form in most code, but for sketches they are sometimes necessary and frequently useful. We absolutely need, for example, a place to put the contents of the drive’s sector buffer in the Cestino’s memory, so we define that variable globally as a 512 byte array called block_buffer. This can be a little confusing. Remember that this array is /only/ on the Cestino. If we want it up to date with the sector buffer on the drive, we have to call a function to do that.

byte block_buffer[512];

These next variables hold information about the drive’s cylinders, heads, and sectors. We don’t use this data much, since we’re in LBA mode, but we do use it. We use the LBA_sectors uint32_t a lot more. It’s the number of LBA blocks on the drive.

I’ve used Block and Sector fairly interchangeably in this sketch. Because we’re dealing exclusively with LBA blocks, which are exactly the same size and functionality as sectors, the difference is not important to us, except in the drive_sectors variable, which holds the number of actual sectors. We only use that data once, and then only for information messages.

int drive_cylinders = 0;
int drive_heads = 0;
int drive_sectors = 0;
uint32_t LBA_sectors = 0;

The non_zero_read variable is used as an easy mechanism for other functions and code to tell whether the last read of the drive sector buffer into block_buffer[] had any data in it. It’s a clumsy mechanism, but it works.

boolean non_zero_read = false;

Low Level Functions

These are the lowest level functions. These directly touch the Cestino’s ports, and by extension the control signals of the drive. They use nearly all the #defines.

get_drive_status_byte()

This function seems like a nicety, but it’s absolutely critical for the rest of the sketch. All of the functions wait on something, usually for the drive to complete an action. That information is gathered by calling this function over and over again in a loop, sometimes forever.

This function takes no parameters. When called, it preserves the state of the CONTROL_PORT, defines a variable status_byte, and sets it to zero, selects the ASTAT/DEVCOM register, sets the LSB port up to read /and/ turns on its pull-up resistors, sets the CONTROL_PORT lines to read mode, reads the port and stores the value in status_byte, then resets CONTROL_PORT to its previous values. It then returns status_byte. If this seems like a belt and suspenders and duct tape approach, it will become obvious that I had a lot of unexpected interactions between functions early in the development of this sketch.

Note the #ifdefs and the debug code at the beginning and end of this function.

byte get_drive_status_byte() {

#ifdef DEBUG
  Serial.print("get_drive_status_byte() called.");
#endif


  byte temp_control_port = CONTROL_PORT;
  byte temp_lsb = LSB_PORT;
  byte temp_lsb_ddr = LSB_DDR;


  byte status_byte = 0;

  CONTROL_PORT = REG_ASTAT_DEVCOM;

  LSB_DDR = DATA_READ;
  LSB_PORT = 0b11111111;
  set_drive_hw_lines(HW_READ);
  status_byte = LSB_PINS;
  set_drive_hw_lines(HW_OFF);


  CONTROL_PORT = temp_control_port;
  LSB_PORT = temp_lsb;
  LSB_DDR = temp_lsb_ddr;


#ifdef DEBUG
  Serial.print(" Returning:");
  Serial.print(status_byte, BIN);
  Serial.print(" ");
#endif
  return (status_byte);
};

write_command_to_register()

Most functions other than get_drive_status_byte() read and write commands to registers the same way, so instead of having the same code over and over again, they call this function.

This function returns no data, and takes the parameters reg, the register to be written to, and command, the command to be written to it. As we did with get_drive_status_byte, we preserve the existing DDR state of LSB (this function can be called with LSB in either read or write state), then set LSB to write mode.

We set CONTROL_PORT to the register (which will be one of our REG_whatever #defines), which will set the chip select and address signals, then set LSB_PORT to the command, which will be one of the CMD_whatever #defines.

Once the signals are set, we call another function called set_drive_hw_lines, which sets the /DIOR and /DIOW lines to HW_WRITE. Then we call it again to set the /DIOR and /DIOW lines back to HW_READ. This is called strobing the read and write lines, and it’s what tells the drive to read the data port and write that data to the register we’ve selected.

After that, we set CONTROL_PORT, LSB_PORT and LSB_DDR back the way we found them.

void write_command_to_register(byte reg, byte command) {
  byte temp = LSB_DDR; //Preserve LSB r/w state
  LSB_DDR = DATA_WRITE; //Set LSB's DDR
  CONTROL_PORT = reg; //Set CONTROL_PORT to register address.
  LSB_PORT = command; //Set LSB_PORT to the command byte.
  set_drive_hw_lines(HW_WRITE); //Strobe write/read lines.
  set_drive_hw_lines(HW_OFF);
  LSB_DDR = temp; //Restore the state of LSB's DDR.
}

set_drive_hw_lines()

The last of our three low-level functions is set_drive_hw_lines(). This function sets the /DIOR, /DIOW, and /RESET signals. Notice that all three of these signals are active low, so the logic will look backward.

Once we’re past the debug code, we copy the value of the CONTROL_PORT register into temp. We then AND temp with 0b11111000, which preserves whatever is in the highest 5 bits, and sets the lowest three bits to zero.

Next, we OR temp with status, which will contain one of our HW_whatever defines, so the correct bits are set on and off.

Then we apply the result to CONTROL_PORT.

It’s very important to do the bit twiddling of the value to be set to CONTROL_PORT in a separate variable and not on the CONTROL_PORT register itself, as the intermediate states of CONTROL_PORT can make the drive do very peculiar things.

void set_drive_hw_lines(byte status) {
#ifdef DEBUG
  Serial.print("set_drive_hw_lines() called with status:");
  Serial.print(status, DEC);
  Serial.print(" CONTROL_PORT was: ");
  Serial.print(CONTROL_PORT, BIN);
#endif


  byte temp = CONTROL_PORT;

  temp = temp & 0b11111000;
  //wipe the hw line bits


  temp = temp | status;
  //OR the remaining bits with the new setting (status)


  CONTROL_PORT = temp; //Set CONTROL_PORT to the new value.

#ifdef DEBUG
  Serial.print(" CONTROL_PORT now ");
  Serial.print(CONTROL_PORT, BIN);
  Serial.print(" ");
#endif
}

Testing

Once you have all the defines and all the globals in place, it’s a good idea to make a basic setup() function and a loop() function to test these routines. If they don’t work right, nothing that depends on them will. Your setup() function will need the Serial.begin(115200) line, and it will need to set CTRL_DDR to DATA_WRITE.

After that, #define DEBUG and call the low level functions to see if the debug values look reasonable.

Once your low level functions look good, it’s time to move on to the rest of the sketch, but seriously. Stop here and test your low level functions. Debugging high level functions without full confidence in your low level functions is unpleasant.

Utility Functions

The next group of functions I call Utility Functions. They’re not low level, in that they don’t directly talk to Cestino ports to manipulate the drive’s signals, but at the same time, they do not themselves implement complete parts of this sketch. They vary wildly in length, and quite a few depend on each other as well as on the low level functions.

wait_for_drive_drq()

This function forces the sketch to wait for the drive data request signal to be set in the drive status register. It returns no data and takes no parameters. If the DRQ signal is never set, the sketch can hang here forever.

We begin with the usual debug code, then get the drive status byte and store it to status_byte. We then store the OR of the DRQ and BSY status masks together in the mask variable. Then we begin the loop.

The loop checks to see if status_byte AND mask is logically true, that is, any of its bits are true, and if the result equals the DRQ mask. If both BSY and DRQ are set, this will be the case. Otherwise the loop reads the drive status byte into status_byte again, and repeats. Forever if need be.

void wait_for_drive_drq() {


#ifdef DEBUG
  Serial.print("wait_for_drive_drq() called: ");
#endif
  byte status_byte = get_drive_status_byte();
  byte mask = DRQ | BSY;
  while (!((status_byte & mask) == DRQ)) {
    status_byte = get_drive_status_byte();
  }


#ifdef DEBUG
  Serial.println("DRQ");
#endif
}

wait_for_drive_not_busy()

This function works much like the wait_for_drive_drq() function except that it waits for the drive to be /not/ busy. We read the status byte into status_byte, and set the mask for BSY into mask, then loop while status_byte AND mask do not equal zero, reading the status byte into status_byte over and over again until it changes.

void wait_for_drive_not_busy() {

#ifdef DEBUG
  Serial.print("wait_for_drive_not_busy() called: ");
#endif
  byte status_byte = get_drive_status_byte();
  byte mask = BSY;
  while (!((status_byte & mask) == 0)) {
    status_byte = get_drive_status_byte();
  }
#ifdef DEBUG
  Serial.println("Busy Clear");
#endif
}

wait_for_drive_ready()

wait_for_drive_ready is slightly different from the other two, in that it has a crude timeout mechanism. At the beginning of the setup() function, we check to see if a drive exists on the interface. Unfortunately, if there isn’t one, wait_for_drive_ready is never set (obviously), so we have to wait for this function to time out. This function also inserts a 1ms delay in each loop cycle, so 1,000 cycles in ms_to_wait will make the function wait a full second, and the minimum time it will wait is one second. This function returns no data, but does take the number of ms to wait in an integer as a parameter.

we begin with the usual debug code, and as should be familiar by now, set store the drive status byte in status_byte and OR the masks for BSY and RDY together and store them in mask. We then initialize our ms counter to zero.

The loop tests to see if our mask AND the status byte are not equal to RDY, and also whether the counter c is less than or equal to ms_to_wait. If both these conditions are true, it reads the drive status byte again, delays 1 ms, increments c, and loops. It will continue to loop until either the RDY bit is set, or the timeout is exceeded.

void wait_for_drive_ready(int ms_to_wait) {

#ifdef DEBUG
  Serial.print("wait_for_drive_ready() called: ");
#endif
  byte status_byte = get_drive_status_byte();
  byte mask = BSY | RDY;
  //look at the highest two bits of the status byte.


  int c = 0;
  while ((!((status_byte & mask) == RDY)) && (c <= ms_to_wait)) {
    status_byte = get_drive_status_byte();
    delay(1);
    c++;
  }
  //While the highest two bits of the status byte are not
  //equal to 0x40 (RDY), delay 1ms, increment c,
  //and do it again.


#ifdef DEBUG
  Serial.println("DRIVE READY");
#endif
}

string2uint32_t()

In loop, at one point we read a 28 bit LBA address from user input. Unfortunately, the user input comes in as a stream of characters from the Serial.readString() function. While in normal C there are nice ways to convert character arrays to integers, there are no nice ways to convert character arrays to 28 bit uints, so we have to implement our own.

This function takes a String object as its input and returns a uint32_t as its output.

We begin by initializing a uint32_t called temp to 0. We then call the trim() method of the String object input, to ensure that we don’t have trailing characters that will mess up our counting.

The loop counts from 0 to the result of the length() method of the input String object. Since each iteration represents another digit to the right of the last digit we read, we multiply temp by 10. Then we get the next character in the String object using the charAt() method and c as the position. This will give us the value of that character, but there’s a catch. It will give us the numeric representation of the character /itself/. Because the Cestino (like all Arduinos) uses old school ASCII, that means the number 1 has a numeric value of 49. We do know, from looking at an ASCII table, that the numbers are in the table from 0 to 9, in order, lowest to highest. If we know that 0 is 48, we could subtract 48 from whatever numeric value we read in, but it’s easier to subtract the numeric value of the character 0, which accomplishes the same thing. 49-48 is 1, and that’s the correct value for the number 1. We add that derived value to temp, and loop. When we run out of characters in the input String object, we return temp.

uint32_t string2uint32_t(String input) {
  uint32_t temp = 0;
  input.trim();
  for (int c = 0; c < input.length(); c++) {
    temp = temp * 10 + input.charAt(c) - '0';
  }


#ifdef DEBUG
  Serial.print("string2uint32_t called with a string of >" +
               input + "< Returned:");
  Serial.println(temp, DEC);
#endif


  return temp;
}

transfer_sector_buffer()

This function transfers the drive’s sector buffer to the Cestino’s block_buffer array, /or/ transfers the block_buffer array to the drive’s sector buffer for writing.

This function returns no data, but it takes a boolean to determine whether we’re writing. (If we’re not writing, the boolean is false, so we’re reading.) It also takes another boolean to determine whether we’re big_endian (using the BIG_ENDIAN and LITTLE_ENDIAN #defines for clarity). If big_endian is true, we read or write big endian words. otherwise we read or write little endian words.

We begin with debug code. Then we wait until the drive’s BSY signal is cleared, and we set the non_zero_read global variable to false. Then we select the REG_DATA register. (Remember, the data register is 16 bits wide.)

void transfer_sector_buffer(boolean write, boolean big_endian) {
#ifdef DEBUG
  Serial.println("transfer_sector_buffer called.");
#endif


  wait_for_drive_not_busy();
  non_zero_read = false; //set false before we start.
  CONTROL_PORT = REG_DATA; //select the REG_DATA register.

If write is true, we set the DDR for both LSB_PORT and MSB_PORT to DATA_WRITE, otherwise we set them to DATA_READ. (See how all those #defines get used here?)

if (write) {
    LSB_DDR = DATA_WRITE;
    MSB_DDR = DATA_WRITE;
  }
  else {
    LSB_DDR = DATA_READ;
    MSB_DDR = DATA_READ;
  }

After that, we begin the loop that does the actual reading and/or writing. It iterates on a counter from 0 to 512 by two, since block_buffer is made up of 8 bit bytes, and the sector buffer is made up of 16 bit words.

for (int c = 0; c < 512; c += 2) {                                                                                                    
Writing

If we’re we’re big endian, we set the MSB_PORT to the value of block_buffer[c], and LSB_PORT to block_buffer[c+1]. This flips the order so the bytes are in the right order for a big endian word.

if we’re little endian, we set LSB_PORT to block_buffer[c], and MSB_PORT to block_buffer[c+1].

Then we set the drive hardware lines to HW_WRITE.

    if (write) {
      if (big_endian) {
        MSB_PORT = block_buffer[c];
        LSB_PORT = block_buffer[c + 1];
      }
      else {
        LSB_PORT = block_buffer[c];
        MSB_PORT = block_buffer[c + 1];
      }
      set_drive_hw_lines(HW_WRITE);
    }
Reading

Set the drive signals to HW_READ.

If we’re big endian, we read the MSB_PINS into block_buffer[c] and the LSB_PINS into block_buffer[c+1], much the way we did in the write section. This puts them in little endian order in block_buffer, which the rest of the sketch expects.

If we’re little endian we read LSB_PINS into block_buffer[c] and MSB_PINS into block_buffer[c+1].

else {
      set_drive_hw_lines(HW_READ);
      if (big_endian) {
        block_buffer[c] = MSB_PINS;
        block_buffer[c + 1] = LSB_PINS;
        non_zero_read = MSB_PINS | LSB_PINS | non_zero_read;
      }
      else {
        block_buffer[c] = LSB_PINS;
        block_buffer[c + 1] = MSB_PINS;
        non_zero_read = MSB_PINS | LSB_PINS | non_zero_read;
      }
    }
Cleanup and Exit

Finally, we set the drive hardware lines to HW_OFF, a safe position. The loop ends here and we repeat until we’ve read or written all 256 words.

When the loop exits, we hit some more debug code and the function exits.

set_drive_hw_lines(HW_OFF);
  }


#ifdef DEBUG
  Serial.println("transfer_sector_buffer done.");
#endif
}

dump_block_buffer()

Dump block buffer is a pretty-printer for the contents of the block_buffer array on the Cestino. It generates two columns of information, the hex dump of the block_buffer, where every byte of data, printable or not, is represented in hexidecimal, and a text print of any printable characters.

It takes no parameters and returns no data.

We begin by initializing a counter, c, and three String objects: hex_data, human_readable_data, and line, to their 0 or empty values.

void dump_block_buffer() {
  Serial.println("Dumping Block Buffer");
  int c = 0;
  String hex_data = "";
  String human_readable_data = "";

Each line of output will contain hex_data and human_readable_data, plus some formatting information. In order to generate the rows and columns of these two tables in a sensible fashion, we use two more counters, row and col, and two loops.

  for (int row = 0; row < 32; row++) {
    for (int col = 0; col < 16; col++) {

Here’s a catch. Arduino’s hex printing will “helpfully” truncate leading zeros from hex values, so 0x05 comes out 0x5. This messes up our table, so if the value at block_buffer[c] is less than 0x10, we add a leading zero to hex_data.

      if (block_buffer[c] < 0x10) hex_data += "0";

Next, we use the String function from the Arduino library to return the text version of block_buffer[c] as hex. We add that to the hex_data string, then add a blank space.

      hex_data += String(block_buffer[c], HEX);
      hex_data += " ";

Next, we test to see if block_buffer[c] is a printable character. If it is, we add it, cast as a char, to human_readable_data. If it’s not printable, we add a period instead.

      if (isprint(block_buffer[c])) {
        human_readable_data += (char)block_buffer[c];
      } else {
        human_readable_data += ".";
      }

Then we increment c and repeat the column loop.

      c++;
    }

Once we exit the column loop, we Serial.println hex data, and a space, a pipe character, and another space, then human_readable_data. Then clear both those variables for re-use. Then we repeat the row loop.

    Serial.println(hex_data + " | " + human_readable_data);
    hex_data = "";
    human_readable_data = "";


  }

Once both loops have exited, we tell the user how many bytes were dumped (which should always be 512.) Then we exit.

  Serial.print("Bytes dumped:");
  Serial.print(c);
  Serial.print(" ");
}

read_write_drive_LBA_block()

Here it is, the function that actually makes our drive useable as a storage device. read_write_drive_LBA_block takes a uint32_t block number, and a boolean to determine whether it’s writing or reading. It returns no data.

The first thing that happens in this function is we declare a union named block_num_union, with a uint32_t representation called uint, and an array of four bytes called byte_array. As always, both these representations refer to the same area of memory. They’re just two ways to get at the same information, and as usual, we’re using them to store and access long addresses, in this case a 28 bit LBA. The usual caveat applies, too, that if you’re running this sketch on a non-little-endian Arduino, this data structure will return data in the wrong order. We set block_num_union to block_num. using the uint representation.

void read_write_drive_LBA_block(uint32_t block_num, bool write_enable) {
  union {
    uint32_t uint;
    byte byte_array[4];
  } block_num_union;
  block_num_union.uint = block_num;

Then we get to the usual debug code.

#ifdef DEBUG
  Serial.print("read_drive_LBA_block called on block ");
  Serial.print(block_num);
  Serial.print(" ");
#endif

Next, when the drive’s not busy, we write the first three bytes of our LBA address in block_num to the lowest three LBA address registers using write_command_to_register.

  wait_for_drive_not_busy(); //wait for the drive to signal it's not busy.
  write_command_to_register(REG_LBA0, block_num_union.byte_array[0]);
  write_command_to_register(REG_LBA1, block_num_union.byte_array[1]);
  write_command_to_register(REG_LBA2, block_num_union.byte_array[2]);

The last four bytes of the LBA address are a little more complicated, since we have to also apply the LBA_MODE command to the high bits of the same register or our LBA addresses won’t work. We do this by ORing CMD_LBA_MODE to the third byte of the LBA address. We’re not changing any of the address bits, just turning on a bit in the other half of the register. After that, we wait for the drive’s busy signal to go off.

write_command_to_register(REG_LBA3, (block_num_union.byte_array[3] | CMD_LBA_MODE));
  wait_for_drive_not_busy();

If we are writing, and the write_enable boolean is true, send the command CMD_WRITE_WITH_RETRY to REG_STAT_COM to initiate the write. Otherwise send the command CMD_READ_WITH_RETRY to initiate the read.

if (write_enable) {
    write_command_to_register(REG_STAT_COM, CMD_WRITE_WITH_RETRY);
  } else {
    write_command_to_register(REG_STAT_COM, CMD_READ_WITH_RETRY);
  }

Then we wait until the drive tells us it has data for us. When it does, we call transfer_sector_buffer, pass it write_enable so if we’re writing, so is it, and tell it we’re little endian. We also reset the drive. We shouldn’t need to, but breadboards with multiple power supplies and big ribbon cables attached (like ours) can get very noisy. This is a kludge to fix a bug where, in a noisy environment, reads following writes would read the wrong sector.

wait_for_drive_drq();
  transfer_sector_buffer(write_enable, LITTLE_ENDIAN);
  reset_drive();

Cue the debug code and exit.

#ifdef DEBUG
  Serial.println("read_write_drive_LBA_block done.");
#endif
}

High Level Functions

All done debugging? You’re sure? Okay, let’s go on to the high level functions, including the final versions of setup() and loop()

reset_drive()

This function resets the drive. It’s really, really short. So short, in fact, that the debug code almost outweighs the code. It takes no parameters and returns no data. First, we have the usual debug boilerplate.

void reset_drive() {
#ifdef DEBUG
  Serial.println("reset_drive() called.");
#endif

Then we call set_drive_hw_lines to set the signals to the drive to the HW_RESET pattern. This means set /Reset low. Delay 1ms, then set it high again.

  set_drive_hw_lines(HW_RESET);
  delay(1);
  set_drive_hw_lines(HW_OFF);

For good measure, we send the drive a CMD_INIT, too, and wait a little more than a second for the drive to come back. Why 1024ms? Why not?

  write_command_to_register(REG_HEAD, CMD_INIT);
  wait_for_drive_ready(1024);

After that is the usual debug code, and we exit.

#ifdef DEBUG
  Serial.println("reset complete.");
#endif
}

sleep_drive()

Even simpler than resetting the drive is sleeping the drive. The usual debug boilerplate /does/ outweigh the code on this one. Sleep_drive() returns no data and takes no parameters. We begin with the usual debug code.

void sleep_drive() {
#ifdef DEBUG
  Serial.println("sleep_drive() called.");
#endif

Then we write CMD_SLEEP to REG_STAT_COM. That’s it. That’s all it does. The drive should spin down, and most other functions that wait for signals on the drive? Will hang waiting for those signals. You have to reset the drive to wake it up.

write_command_to_register(REG_STAT_COM, CMD_SLEEP);

Then we exit after the usual debug code.

#ifdef DEBUG
  Serial.println("ZZZ");
#endif
};

Identify Drive

This function is actually the first high level function I got to work. It’s a good test, as it exercises most of the low level functions and several of the utilities without requiring the drive to actually do anything other than copy data from ROM to the sector buffer. Identify Drive takes no parameters and returns no data. We begin with the usual boilerplate debug code.

void identify_drive() {
#ifdef DEBUG
  Serial.println("identify_drive() called");
#endif

Next, we wait for the busy flag to be clear, which it most likely is.

wait_for_drive_not_busy();

After that, we write CMD_IDENTIFY to the status/command register, and transfer the sector buffer to the Cestino’s block_buffer[] array.

Note

The drive identity information is big endian. We have to send that flag to transfer_sector_buffer or the information will be unreadable to us.

Also, because the number of LBA blocks is sent in a 32 bit big endian number and transfer_sector_buffer only handles 16 bit big endian, we have to swap the two 16 bit words of that value when we use them. Which we will.

We don’t dump it because, as you’ll see, sometimes we want to do additional processing on it to make it human-readable.

  write_command_to_register(REG_STAT_COM, CMD_IDENTIFY);
  transfer_sector_buffer(false, BIG_ENDIAN);

We exit with the usual debug code. If you think writing the debug code was a bit tedious? It was.

#ifdef DEBUG
  Serial.println("identify_drive() done.");
#endif
}

setup()

The setup function is normally short, almost trivial except that serial communications won’t work without it. Not so in this sketch. We do a lot of setup to set the drive up, and we also identify the drive and display its ID information. All this before loop even starts. As always, setup returns no data and takes no parameters.

We begin with the usual serial setup, and a loop that waits until serial actually wakes up before the sketch will run. Your terminal window must be open to run this sketch.

void setup() {
  Serial.begin(115200);
  while (!Serial) {
  }
  CONTROL_DDR = DATA_WRITE;

Of course there’s debug code. This one warns us that debug is set. With all the functions in place, it’s fairly obvious when global debug is set, but for completeness, it’s here.

#ifdef DEBUG
  Serial.println("DEBUG is set.");
#endif

Next, we reset whatever drive is out there, then try to read the status byte. If it’s all 1s, there’s no drive operational out there. The sketch goes into an infinite loop.

You either need to plug the drive in, connect it, or you’ve got other problems. When I smoked my demo drive just now by shorting its ID pins, I got the floating bus message. Fortunately, I had another.

  reset_drive();
  Serial.print("Checking for a drive...");
  byte status_byte = get_drive_status_byte();
  if (get_drive_status_byte() == 255) {
    Serial.println("Floating bus - Drive not detected.");
    while (true) {
      //loop forever.
    }
  }

If we do find a drive out there, tell the user, and call identify_drive(). We need to know this information so we don’t try and send it to LBA addresses it hasn’t got.

else
  {
    Serial.println("Found a drive."); //Got valid drive
    identify_drive();

Next, we fish through the block buffer to pull various strings out for drive information. Most of these pieces of information have a standard location in the sector buffer (and from there, our block_buffer[] array) defined in the spec. All these loops essentially do the same thing. Start at a known location, read a byte, serial print it as a character.

    Serial.print("Model: ");
    for (int c = 54; c <= 74; c++) {
      Serial.print((char)block_buffer[c]);
    }


    Serial.print(" Serial Number: ");
    for (int c = 20; c <= 30; c++) {
      Serial.print((char)block_buffer[c]);
    }


    Serial.print(" Firmware Version: ");
    for (int c = 46; c <= 50; c++) {
      Serial.print((char)block_buffer[c]);
    }

The numeric values are a little different. Some of them need some extra processing, notably combining into 16 bit numbers. So we rotate the first byte left by 8 and add the second.

    drive_cylinders = block_buffer[2] << 8;
    drive_cylinders += block_buffer[3];
    Serial.print(" Cylinders: ");
    Serial.print(drive_cylinders);
    drive_heads = block_buffer[6] << 8;
    drive_heads += block_buffer[7];
    Serial.print(" Heads: ");
    Serial.print(drive_heads);
    drive_sectors = block_buffer[12] << 8;
    drive_sectors += block_buffer[13];
    Serial.print(" Sectors: ");
    Serial.print(drive_sectors);

LBA sectors are a 32 bit number, and because of how our 16 bit endian switcher works, we get digits out of place. Because it only happens here, we just fix it manually.

    Serial.print("
LBA Sectors: ");
    LBA_sectors = block_buffer[122];
    LBA_sectors = (LBA_sectors << 8) | block_buffer[123];
    LBA_sectors = (LBA_sectors << 8) | block_buffer[120];
    LBA_sectors = (LBA_sectors << 8) | block_buffer[121];
    Serial.print(LBA_sectors);
  }

And here, as promised, is the Serial.println() that makes sure everything is actually sent to the terminal. Then we exit.

  Serial.println();
}

loop()

For once, we’re actually going to let loop() run more than once. The loop function defines, then generates a menu of options we can do to the drive. our choice is then fed into a case statement that implements the actual code for the option we’ve chosen. As always, loop takes no parameters and returns no functions. It repeats forever, unless we park the Cestino in our own repeat-forever loop. Which we do in “Quit”.

The first thing we do is define the menu text. It’s one big, hairy #define, and it’s important that the text of the define be against the left margin, as any spaces introduced will be present in the menu when it’s printed. It’s just text, though. All it would do is look bad.

void loop() {
#define menu " **** MENU ****
1 Reset Drive
2 Sleep Drive
3 Identify Drive
4 Seek Non-Empty Block/Sector
5 Write to Block/Sector
6 Quit "

Now that our menu’s defined, let’s initialize our selector variable to 0. Or 0x00. Our switch will work on single characters, and the characters it looks for will be numbers. To avoid confusion between numbers and their /character/ values, I put the character values in hex. After that, we display the menu and wait for the user to press return in the little text line at the top of the serial window.

  char menu_item = 0x00; //Initialize menu_item to 0.

  Serial.println(menu); //display the menu
  while (!Serial.available()) { //wait for serial input
    //do nothing
  }

When we reach this point, the user has typed /something/ and pressed return. We read what they typed with Serial.read() and store it in menu_item.

  menu_item = Serial.read();

After that, we get into the case statement that is the bulk of this function. Some of the options call one function. Others call a few functions. Some should probably be functions, but it’s always a borderline case when a piece of code is only useful in one place whether to separate it out into a function or just leave it.

Options 1 and 2 are nearly identical. They call the named function. 1 resets the drive and waits for the drive to become ready, then breaks. 2 sleeps the drive and doesn’t wait for anything. Then breaks.

In case you’re not familiar with case statements, if you don’t put a break at the end of the code for an option, case will execute every option that follows the first match. It’s one of those annoying things about C. Also annoying: in C you begin a case statement with switch. Go figure.

  switch (menu_item) {
    case 0x31 : {
        //0x31 is "1". Reset the drive, wait until ready.
        Serial.println("Resetting drive.");
        reset_drive();
        wait_for_drive_ready(1024);
        Serial.println("Drive is reset.");
        break;
      }
    case 0x32 : { //0x32 is "2" Sleep the drive.
        Serial.print("Sleeping Drive. ");
        Serial.println("Reset to wake up.");
        Serial.println("(Other functions will hang.)");
        sleep_drive();
        break;
      }

Option 3 identifies the drive. This is a three step process. After we tell the user what was selected, we call identify_drive(), which sends the identify command to the drive and transfers the sector buffer into our block_buffer[] array. Then we call dump_block_buffer to display the identity information in its raw form.

    case 0x33 : { //0x33 is "3". Identify Drive.
        Serial.println("Identifying Drive");
        identify_drive(); //put ID info in the block buffer
        dump_block_buffer(); //dump the block buffer
        break;//Don't try the other cases.
      }

Option 4 finds the first non-empty block and dumps it, then asks the user if they want to continue. It begins by initializing a new uint32_t called sector to 0, (remember, in LBA blocks and sectors are essentially the same thing) and a byte called keep_going to 0x79, which is lowercase y. If you expect a loop to start next, you’ve got a good ear.

    case 0x34 : {
        Serial.println("Seeking Non-empty Block/Sector.");
        uint32_t sector = 0;
        byte keep_going = 0x79; // lower case y

Here’s the loop. While keep_going is “y”, read the sector. If sector modulus 100 is zero (sector is an even hundred) tell the user we’re checking this block, so they get a count of the sectors we’re ignoring. Finding a sector that has stuff in it can take quite a while and we’d like them to know the sketch is still running.

        while (keep_going == 0x79) {
          read_write_drive_LBA_block(sector, false);


          if (!(sector % 100)) {
            Serial.println("Checking Block " +
                           (String)sector);
          }
If we find a block where non_zero_read is true, tell the user what block it is, and call dump_block_buffer. Then ask to continue. Wait forever for a response. When they press return, put whatever they typed into keep_going, increment sector, and go back to the beginning of this loop. If we fall out of the loop, break so we exit out of the case statement.
          if (non_zero_read) {
            Serial.print("Found something. Dumping sector ");
            Serial.print(sector);
            Serial.print(" ");
            dump_block_buffer();
            Serial.println("Shall I continue? (y/n)");
            while (!Serial.available()) {
            }
            keep_going = Serial.read();
          }
          sector++;
        }
        break; //Don't try the other cases.
      }

Option 5 lets the user seek a specific block, then write a string to that block. It then rereads the block and dumps it to the terminal. Because this sketch knows nothing at all about filesystems, this option basically corrupts the disk. It was in the junk box anyway, right? Option 5 begins by initializing a new string called input_string empty, and a new uint32_t called block to 0. Could I have reused sector? Yes.

    case 0x35 : {
        String input_string = "";
        uint32_t block = 0;

Next, we ask the user what block they would like written to. We also tell them how many sectors the drive has, and that we only have 28 bit addresses to work with. Newer versions of the ATA standard go to 48 bit LBA addresses, but I don’t have access to those standards, and most of drives that big are SATA.

        Serial.println("What LBA block shall I write to? ");
        Serial.print("The drive has ");
        Serial.print (LBA_sectors);
        Serial.print(" LBA blocks, and 28 bit LBA addresses go to");
        Serial.println("268435456.");
        while (!Serial.available()) {
        }
        input_string = Serial.readString();

Call string2uint32_t on the string the user gave us, clear input string, and ask the user for their message to write on the disk. We’ll store that in input_string, too.

        block = string2uint32_t(input_string);
        input_string = "";//clear input string.
        Serial.println("Writing to block " + (String)block +
                       ". Please type your message and" +
                       " press return.");
        while (!Serial.available()) {
          //do nothing
        }
        input_string = Serial.readString();

Next, we tell the user what they’re writing and to where. Note that the " marks are escaped so they’ll show up in the Serial.println(). Once that’s finished, we copy the string into block_buffer, and set any unused bytes of block_buffer to 0.

        Serial.println("Writing "" + input_string +
                       "" to Block:" + (String)block);


        for (int c = 0; c <= 512; c++) {
          if (c <= input_string.length()) {
            block_buffer[c] = input_string.charAt(c);
          } else block_buffer[c] = 0;
        }

Finally, we call read_write_drive_LBA_block with the block number and “true” to tell it that it is write enabled. This copies block_buffer[] to the drive. Technically, we copy it to the drive’s sector buffer, but the drive writes to the physical disk(s) at the same time. Then wait 1024ms for the drive to signal that it’s ready again.

        read_write_drive_LBA_block(block, true);
        wait_for_drive_ready(1024);
        Serial.println("Done writing. Reading block "
                       + (String)block);

Reread the block. We don’t fake it by just playing back our own block_buffer[] array, we read the drive and get a fresh copy. It’s the only way to be sure the write really happened. Once we read it, we dump it, clear input_string, and break.

        read_write_drive_LBA_block(block, false);
        dump_block_buffer();
        input_string = "";
        break;//Don't try the other cases.
      }

Option 6 is Quit. We sleep the drive, then go into an infinite loop. Only resetting the Cestino will restart the sketch now. Yes, there’s a break. No, we’ll never hit it.

    case 0x36 : {
        Serial.println("Sleeping Drive and Exiting.");
        sleep_drive();
        Serial.println("Halted. Reset Cestino to Restart.");
        while (true) {
        }
        break;
      }
  }

When any of the case statement options break, they drop out the bottom of the case and wind up here, where we clear menu_item, and fall out the bottom of the function. Because loop() is called and recalled forever, the Arduino core will handle getting us the next menu. And the next. And the next.

  menu_item = 0x00; //clear menu_item for the next go-round.

The Complete Sketch

The complete sketch, including voluminous comments, is here, as always.

//Hardware:
//-----------------------------------------------------------
// CONTROL_PORT
// Signal: /CS3FX /CS1FX DA2 DA1 DA0 /DIOW /DIOR /RESET
// Means: 3F6-3F7 1F0-1F7 (Drive Addr) (Write)(Read) (Reset)
// IDE PIN: 38       37    36 33   35   23     25     1
// Port Bit: 7        6     5  4    3    2      1     0


// LSB_PORT
// Signal:  DD7 DD6  DD5  DD4  DD3  DD2 DD1 DD0
// IDE Pin:   3   5    7    9   11   13  15  17
// Port Bit:  7   6    5    4    3    2   1   0


// MSB_PORT
// Signal: DD15 DD14 DD13 DD12 DD11 DD10 DD9 DD8
// IDE Pin:  18   16   14   12   10    8   6   4
// Port Bit:  7    6    5    4    3    2   1   0
//
// GROUND:
// IDE pins 2, 19, 22, 24, 26, 30, and 40 should be grounded.
// Yes really, ground them all.
//-----------------------------------------------------------


//Notes
//-----------------------------------------------------------
// Sector and LBA block are used interchangeably in this
// sketch, because when the drive is identifying itself,
// it's still in cylinder/head/sector mode. All other
// reads and writes deal with LBA blocks. Which some
// people also call LBA sectors.


//Preprocessor #defines
//-----------------------------------------------------------
// This sketch has a ton of preprocessor #defines.
// They use no run-time memory and make the code easier to
// follow.
//-----------------------------------------------------------


//Define what ports do what, and how to access their PORT,
//PIN, and DDR registers.
#define CONTROL_PORT PORTB
#define CONTROL_DDR DDRB
#define CONTROL_PINS PINB
#define LSB_DDR DDRA
#define LSB_PORT PORTA
#define LSB_PINS PINA
#define MSB_DDR DDRC
#define MSB_PORT PORTC
#define MSB_PINS PINC


// Define the two DDR modes for any port exc. control
#define DATA_READ 0b00000000
#define DATA_WRITE 0b11111111


// Define various control port register settings.
// Note that this assumes the control port will be wired
// as shown below, in 0bXXXXXXXX notation.
// bit 0 is on the RIGHT.
// /CS3FX,/CS1FX,DA2,DA1,DA0,/DIOW,/DIOR, and /RESET
#define REG_DEFAULT      0b11000111 //bogus register
#define REG_STAT_COM     0b10111111 //Status/Command Register
#define REG_ASTAT_DEVCOM 0b01110111 //Alt Status/Device Command
#define REG_ERR_FEAT     0b10000111 //Error/Features
#define REG_LBA0         0b10011111 //Low byte of LBA address
#define REG_LBA1         0b10100111 //Second byte LBA address
#define REG_LBA2         0b10101111 //Third byte LBA address
#define REG_LBA3         0b10110111 //Fourth byte LBA address
#define REG_DATA         0b10000111 //Data register
#define REG_HEAD REG_LBA3 //Same register, different modes.


// Most actions require a command to be sent
// over the LSB_PORT data lines once the
// register is selected.
// Those commands are defined here.
#define CMD_SLEEP 0x99 //Put the drive to sleep/spin down.
#define CMD_INIT 0x91 //Set drive to a known state.
#define CMD_IDENTIFY 0xEC //Get ID info -> drive sector buffer
#define CMD_READ_WITH_RETRY 0x20 //Read block to drive buffer.
#define CMD_WRITE_BUFFER 0xE8 //Write data to drive buffer
#define CMD_WRITE_WITH_RETRY 0x30 //Write drive buff. to disk.
#define CMD_LBA_MODE 0b01000000 //Set the drive to LBA mode.


// Status byte masks (Cribbed from GeneT's IDEFAT library.)
// I've annotated them in binary to make it clearer
// how they work. These are masks used with AND to pull
// one bit from a status register read.
// All status bits are active high. In order, they are:
// BSY DRDY DWF DSC DRQ CORR IDX ERR
#define BSY  0x80 //0b10000000 - Busy
#define RDY  0x40 //0b01000000 - Ready
#define DF   0x20 //0b00100000 - Drive Write Fault
#define DSC  0x10 //0b00010000 - Drive Seek Complete
#define DRQ  0x08 //0b00001000 - Data Request
#define CORR 0x04 //0b00000100 - Corrected Data
#define IDX  0x02 //0b00000010 - Index
#define ERR  0x01 //0b00000001 - Error


//Define modes of set_drive_hw_lines - Signals are
// /DIOW,/DIOR, and /RESET
#define HW_OFF   0b111 //Set the read-write lines all off.
#define HW_READ  0b101 //Set the read-write lines for reading.
#define HW_WRITE 0b011 //Set the read-write lines for writing.
#define HW_RESET 0b110 //Lower the reset line to reset.


// Define BIG_ENDIAN as true and LITTLE_ENDIAN as false.
// It makes the switch in transfer_sector_buffer clearer.
#define BIG_ENDIAN true
#define LITTLE_ENDIAN false


// DEBUG mode compiles in a lot of Serial.writeln messages.
// for debugging purposes. Can also be #defined for individual
// functions and #undef'd at the end of the function.
//#define DEBUG


//Code
//-----------------------------------------------------------
// Global Variables and functions. The meat of the sketch.
//-----------------------------------------------------------


//-----------------------------------------------------------
// GLOBAL VARIABLES
//-----------------------------------------------------------
byte block_buffer[512]; //One buffer for reading and writing.
int drive_cylinders = 0;
int drive_heads = 0;
int drive_sectors = 0;
uint32_t LBA_sectors = 0;
boolean non_zero_read = false;
//Last block buffer transfer empty?


//-----------------------------------------------------------
// Function get_drive_status_byte()
// - Select the REG_ASTAT_DEVCOM register and read it.
//-----------------------------------------------------------
byte get_drive_status_byte() {


#ifdef DEBUG
  Serial.print("get_drive_status_byte() called.");
#endif


 byte temp_control_port = CONTROL_PORT;
 byte temp_lsb = LSB_PORT;
 byte temp_lsb_ddr = LSB_DDR;
 //save port states


  byte status_byte = 0;

  CONTROL_PORT = REG_ASTAT_DEVCOM;
  //Use the alternate status register.


  LSB_DDR = DATA_READ; //set LSB port to read mode.
  LSB_PORT = 0b11111111; //Turn on pullup resistors - reading.
  set_drive_hw_lines(HW_READ); //Set the read signals
  status_byte = LSB_PINS; //Read LSB_PINS into status_byte.
  set_drive_hw_lines(HW_OFF); //Clear the read signals.


  CONTROL_PORT = temp_control_port;
  LSB_PORT = temp_lsb;
  LSB_DDR = temp_lsb_ddr;
  //restore port states


#ifdef DEBUG
  Serial.print(" Returning:");
  Serial.print(status_byte, BIN);
  Serial.print(" ");
#endif
  return (status_byte);
};


//-----------------------------------------------------------
// Function write_command_to_register(reg,command)
// Set CONTROL_PORT to reg, then write a command byte
// to it on LSB_PORT
//-----------------------------------------------------------
void write_command_to_register(byte reg, byte command) {
  byte temp = LSB_DDR; //Preserve LSB r/w state
  LSB_DDR = DATA_WRITE; //Set LSB's DDR
  CONTROL_PORT = reg; //Set CONTROL_PORT to register address.
  LSB_PORT = command; //Set LSB_PORT to the command byte.
  set_drive_hw_lines(HW_WRITE); //Strobe write/read lines.
  set_drive_hw_lines(HW_OFF);
  LSB_DDR = temp; //Restore the state of LSB's DDR.
}


//----------------------------------------------------------
// Function set_drive_hw_lines(byte status)
// These are the last three lines of CONTROL_PORT.
// We switch our read, write, and reset signals
// here, and they're all active low. So it's backwards.
//----------------------------------------------------------
void set_drive_hw_lines(byte status) {
#ifdef DEBUG
  Serial.print("set_drive_hw_lines() called with status:");
  Serial.print(status, DEC);
  Serial.print(" CONTROL_PORT was: ");
  Serial.print(CONTROL_PORT, BIN);
#endif


  byte temp = CONTROL_PORT;
  //preserve CONTROL_PORT's existing state.


  temp = temp & 0b11111000;
  //wipe the hw line bits


  temp = temp | status;
  //OR the remaining bits with the new setting (status)


  CONTROL_PORT = temp; //Set CONTROL_PORT to the new value.

#ifdef DEBUG
  Serial.print(" CONTROL_PORT now ");
  Serial.print(CONTROL_PORT, BIN);
  Serial.print(" ");
#endif
}


//-----------------------------------------------------------
// Function wait_for_drive_drq();
// Do nothing while checking to see if the DRQ line is high
// DRQ is data request. The drive signals it's ready to transfer
// a word of data (8 or 16 bits, in or out) with this signal.
//-----------------------------------------------------------
void wait_for_drive_drq() {


#ifdef DEBUG
  Serial.print("wait_for_drive_drq() called: ");
#endif
  byte status_byte = get_drive_status_byte();
  byte mask = DRQ | BSY; //check bits 3 and 7, DRQ and BSY.
  while (!((status_byte & mask) == DRQ)) {
    status_byte = get_drive_status_byte();
  }
  //if we don't get a DRQ and BSY,check again until we do.


#ifdef DEBUG
  Serial.println("DRQ");
#endif
}


//-----------------------------------------------------------
// Function wait_for_drive_not_busy()
// Do nothing while we wait for the drive to go not busy.
//-----------------------------------------------------------
void wait_for_drive_not_busy() {


#ifdef DEBUG
  Serial.print("wait_for_drive_not_busy() called: ");
#endif
  byte status_byte = get_drive_status_byte();
  byte mask = BSY;
  while (!((status_byte & mask) == 0)) {
    status_byte = get_drive_status_byte();
    //while the highest bit of the status byte is not 0,
    //get the status byte. Do it forever if need be.
  }
#ifdef DEBUG
  Serial.println("Busy Clear");
#endif
}


//-----------------------------------------------------------
// Function wait_for_drive_ready(int ms to wait)
// Do nothing while we wait up to ms_to_wait ms for the drive
// to go ready. It's a crude timeout mechanism, but it works.
//-----------------------------------------------------------
void wait_for_drive_ready(int ms_to_wait) {


#ifdef DEBUG
  Serial.print("wait_for_drive_ready() called: ");
#endif
  byte status_byte = get_drive_status_byte();
  byte mask = BSY | RDY;
  //look at the highest two bits of the status byte.


  int c = 0;
  while ((!((status_byte & mask) == RDY)) && (c <= ms_to_wait)) {
    status_byte = get_drive_status_byte();
    delay(1);
    c++;
  }
  //While the highest two bits of the status byte are not
  //equal to 0x40 (RDY), delay 1ms, increment c,
  //and do it again.


#ifdef DEBUG
  Serial.println("DRIVE READY");
#endif
}


//-----------------------------------------------------------
// string2uint32_t()
// - The String class includes no nice way to turn strings of
//   digits into a numeric value.
//   This function goes through the string from left to right.
//   For each position it advances to the right, it multiplies
//   the existing value by 10 and adds the value of the
//   character at that position minus the value of the
//   character '0'. When we reach the end of the string,
//   return temp.
//-----------------------------------------------------------
uint32_t string2uint32_t(String input) {
  uint32_t temp = 0;
  input.trim();
  for (int c = 0; c < input.length(); c++) {
    temp = temp * 10 + input.charAt(c) - '0';
  }


#ifdef DEBUG
  Serial.print("string2uint32_t called with a string of >" +
               input + "< Returned:");
  Serial.println(temp, DEC);
#endif


  return temp;
}


//-----------------------------------------------------------
// Function transfer_sector_buffer(write,big_endian)
// - copy block_buffer to/from the drive.
//
// NOTE BENE: Drive info is stored big_endian
// but nearly all microcomputers store data
// in little_endian.
//-----------------------------------------------------------
void transfer_sector_buffer(boolean write, boolean big_endian) {
#ifdef DEBUG
  Serial.println("transfer_sector_buffer called.");
#endif


  wait_for_drive_not_busy();
  non_zero_read = false; //set false before we start.
  CONTROL_PORT = REG_DATA; //select the REG_DATA register.


  if (write) {
    LSB_DDR = DATA_WRITE;
    MSB_DDR = DATA_WRITE;
    //set both LSB and MSB to write mode.
  }
  else {
    LSB_DDR = DATA_READ;
    MSB_DDR = DATA_READ;
    //Set both LSB and MSB to read mode.
  }


  for (int c = 0; c < 512; c += 2) {
    if (write) { //If we're writing...
      if (big_endian) { //and we're big endian
        MSB_PORT = block_buffer[c];
        LSB_PORT = block_buffer[c + 1];
      }
      else { //or we're little endian
       LSB_PORT = block_buffer[c];
       MSB_PORT = block_buffer[c + 1];
      }
      set_drive_hw_lines(HW_WRITE);//set write signals
    }
    //For big endian writing, put the cestino's buffer into the
    //drive's buffer in pairs, second value first, because
    //the Cestino's sector buffer is little endian.
    //Otherwise put the values into LSB and MSB in the order
    //they are in the Cestino's buffer.


    else {//if we're reading
      set_drive_hw_lines(HW_READ); //set read signals
      if (big_endian) { //if we're big endian
        block_buffer[c] = MSB_PINS;
        block_buffer[c + 1] = LSB_PINS;
        non_zero_read = MSB_PINS | LSB_PINS | non_zero_read;
      }
      else {
        block_buffer[c] = LSB_PINS;
        block_buffer[c + 1] = MSB_PINS;
        non_zero_read = MSB_PINS | LSB_PINS | non_zero_read;
      }
    }
    //For big endian reading, store the MSB byte first, then
    //store the LSB byte. Check to see if either MSB or LSB
    //had anything in them, or if non_zero_read was already
    //set true. If any of those are true, non_zero_read is
    //true. Probably this should be in a function return.


    set_drive_hw_lines(HW_OFF);//Turn the rw signals off.
  }


#ifdef DEBUG
  Serial.println("transfer_sector_buffer done.");
#endif
}


//-----------------------------------------------------------
// Function dump_block_buffer()
// - Pretty-print the block buffer.
//-----------------------------------------------------------
void dump_block_buffer() {
  Serial.println("Dumping Block Buffer");
  int c = 0;
  String hex_data = "";
  String human_readable_data = "";
  //We have some variables. Initialize them.


  for (int row = 0; row < 32; row++) {
    for (int col = 0; col < 16; col++) {
      //We're printing blocks of 512 bytes.
      //each is 2 characters + two spaces
      //in hex, plus 1 character in text.


      if (block_buffer[c] < 0x10) hex_data += "0";
      //Add leading zero for hex values below 0x10.


      hex_data += String(block_buffer[c], HEX);
      hex_data += " ";
      //Add block_buffer[c] as a hex string to hex_data.


      if (isprint(block_buffer[c])) {
        human_readable_data += (char)block_buffer[c];
      } else {
        human_readable_data += ".";
      }
      //If block_buffer[c] is printable, add it to
      //human_readable_data. Otherwise add a . for
      //a placeholder.
      c++;
    }


    Serial.println(hex_data + " | " + human_readable_data);
    hex_data = "";
    human_readable_data = "";
    //Combine the two strings in one Serial.println.
    //Then clear them.


  }
  Serial.print("Bytes dumped:");
  Serial.print(c);
  Serial.print(" ");
}


//-----------------------------------------------------------
// Function read_write_drive_LBA_block(block_num)
//
// -LBA mode only. Set the drive to LBA mode
// and put the contents in the drive's block buffer.
// Then read the drive's block buffer into the
// Cestino's block buffer. Block numbers are 28 bits,
// and little-endian.
//-----------------------------------------------------------
void read_write_drive_LBA_block(uint32_t block_num, bool write_enable) {
  union {
    uint32_t uint;
    byte byte_array[4];
  } block_num_union;
  //This union takes a uint32_t (4 byte unsigned int)
  //and represents it also as an array of 4 bytes. Very handy,
  //but watch for endian problems if you run this on a non
  //ATmega Arduino.


  block_num_union.uint = block_num;

#ifdef DEBUG
  Serial.print("read_drive_LBA_block called on block ");
  Serial.print(block_num);
  Serial.print(" ");
  Serial.println("third byte is:" + String(block_num_union.byte_array[3], BIN));
#endif


  wait_for_drive_not_busy(); //wait for the drive to signal it's not busy.
  write_command_to_register(REG_LBA0, block_num_union.byte_array[0]);
  write_command_to_register(REG_LBA1, block_num_union.byte_array[1]);
  write_command_to_register(REG_LBA2, block_num_union.byte_array[2]);
  write_command_to_register(REG_LBA3, (block_num_union.byte_array[3] | CMD_LBA_MODE));
  //Set all three-and-a-half LBA address bytes.
  //LBA3 (aka the head register) gets four bits of address,


  if (write_enable) {
    write_command_to_register(REG_STAT_COM, CMD_WRITE_WITH_RETRY);
  } else {
    write_command_to_register(REG_STAT_COM, CMD_READ_WITH_RETRY);
  }
  wait_for_drive_drq();
  transfer_sector_buffer(write_enable, LITTLE_ENDIAN);
  reset_drive();
  //Kludge to fix write problems when the ATA bus is noisy.


#ifdef DEBUG
  Serial.println("read_write_drive_LBA_block done.");
#endif
}


//----------------------------------------------------------
// Function reset_drive()
// Raises the reset line, then inits the drive to default mode.
//----------------------------------------------------------
void reset_drive() {
#ifdef DEBUG
  Serial.println("reset_drive() called.");
#endif


  set_drive_hw_lines(HW_RESET); //Lower the /Reset line.
  delay(1);
  set_drive_hw_lines(HW_OFF);


  write_command_to_register(REG_HEAD, CMD_INIT);
  //Init the drive


  wait_for_drive_ready(1024);
  //Wait for the drive to show ready again.


#ifdef DEBUG
  Serial.println("reset complete.");
#endif
}


//-----------------------------------------------------------
// Function sleep_drive()
//  - Set the control port to the status/command register.
//  Set it write, and send the sleep code over the data lines.
//-----------------------------------------------------------
void sleep_drive() {
#ifdef DEBUG
  Serial.println("sleep_drive() called.");
#endif


  write_command_to_register(REG_STAT_COM, CMD_SLEEP);
  //Send the sleep command to REG_STAT_COM register.
  //Most other functions except reset will fail to
  //work due to waiting on ready/not busy/drq signals.


#ifdef DEBUG
  Serial.println("ZZZ");
#endif
};


//-----------------------------------------------------------
// Function identify_drive()
//
// - Read the drive's identity block. The whole thing. Burp.
// NOTA BENE: drive info is big-endian, so we tell
// transfer_sector_buffer to read that way. Also, 32 bit
// values will be transferred as two sixteen bit words
// with the least significant word first, which is backwards.
// Fortunately there's only one, so we can fix it manually.
//-----------------------------------------------------------
void identify_drive() {
#ifdef DEBUG
  Serial.println("identify_drive() called");
#endif


  wait_for_drive_not_busy(); //Wait for bsy flag to be clear.
  write_command_to_register(REG_STAT_COM, CMD_IDENTIFY);
  transfer_sector_buffer(false, BIG_ENDIAN);
  //Send the identify command to the REG_STAT_COM register
  //Then transfer the drive's buffer to the Cestino's.


#ifdef DEBUG
  Serial.println("identify_drive() done.");
#endif
}


//-----------------------------------------------------------
// Function setup() - Arduino Boilerplate. Called once.
//-----------------------------------------------------------
void setup() {
  Serial.begin(115200);
  while (!Serial) {
  }
  CONTROL_DDR = DATA_WRITE;
  //Set up serial and set the control port to write mode.


#ifdef DEBUG
  Serial.println("DEBUG is set.");
#endif
  //If global debug is set, the user is about to
  // get innundated with debug text. Warn them...


  reset_drive(); //Any drive out there, reset it.
  Serial.print("Checking for a drive...");
  byte status_byte = get_drive_status_byte();
  //Get the status byte from any drive connected.


  if (get_drive_status_byte() == 255) {

    Serial.println("Floating bus - Drive not detected.");
    while (true) {
      //loop forever.
    }
  } //If no drive is detected, the bus floats high. Stop.


  else
  {
    Serial.println("Found a drive."); //Got valid drive
    identify_drive();
    //pull drive identity information into the block buffer.


    Serial.print("Model: ");
    for (int c = 54; c <= 74; c++) {
      Serial.print((char)block_buffer[c]);
    }


    Serial.print(" Serial Number: ");
    for (int c = 20; c <= 30; c++) {
      Serial.print((char)block_buffer[c]);
    }


    Serial.print(" Firmware Version: ");
    for (int c = 46; c <= 50; c++) {
      Serial.print((char)block_buffer[c]);
    }


    drive_cylinders = block_buffer[2] << 8;
    drive_cylinders += block_buffer[3];
    Serial.print(" Cylinders: ");
    Serial.print(drive_cylinders);
    drive_heads = block_buffer[6] << 8;
    drive_heads += block_buffer[7];
    Serial.print(" Heads: ");
    Serial.print(drive_heads);
    drive_sectors = block_buffer[12] << 8;
    drive_sectors += block_buffer[13];
    Serial.print(" Sectors: ");
    Serial.print(drive_sectors);
    Serial.print(" LBA Sectors: ");
    LBA_sectors = block_buffer[122];
    LBA_sectors = (LBA_sectors << 8) | block_buffer[123];
    LBA_sectors = (LBA_sectors << 8) | block_buffer[120];
    LBA_sectors = (LBA_sectors << 8) | block_buffer[121];
    Serial.print(LBA_sectors);
    //Why aren't we reading these bytes in order? They are
    //a 32 bit number in big-endian. Our endian converter
    //only understands 16 bit values. This is the only
    //32 bit field we have to deal with, so we grind it
    //manually.


    //Scrape the data in the block buffer for Model,
    //serial number, firmware version, etc.
    //Display those values.


  }
  Serial.println();
} //yay. Done with setup.


//-----------------------------------------------------------
// Function loop() - Arduino Boilerplate. Called repeatedly.
// Create the menu text with a big, hideous #define
// Reset the drive and wait until it's ready.
// Print the menu and wait for serial input.
// Process the menu entry and call one of our functions
//  Seek non-empty and write are more involved
// Since we're in loop(), repeat forever.
//-----------------------------------------------------------
void loop() {
#define menu " **** MENU ****
1 Reset Drive
2 Sleep Drive
3 Identify Drive
4 Seek Non-Empty Block/Sector
5 Write to Block/Sector
6 Quit "
  //This is the menu text, in one big, messy define.


  char menu_item = 0x00; //Initialize menu_item to 0.

  Serial.println(menu); //display the menu
  while (!Serial.available()) { //wait for serial input
    //do nothing
  }
  menu_item = Serial.read();
  //if we're here, we got serial input
  //We act on the input in the switch() below.


  switch (menu_item) {
    case 0x31 : {
        //0x31 is "1". Reset the drive, wait until ready.
        Serial.println("Resetting drive.");
        reset_drive();
        wait_for_drive_ready(1024);
        Serial.println("Drive is reset.");
        break; //Don't try the other cases.
      }


    case 0x32 : { //0x32 is "2" Sleep the drive.
        Serial.print("Sleeping Drive. ");
        Serial.println("Reset to wake up.");
        Serial.println("(Other functions will hang.)");
        sleep_drive(); //sleep the drive.
        break;//Don't try the other cases.
      }


    case 0x33 : { //0x33 is "3". Identify Drive
        Serial.println("Identifying Drive");
        identify_drive(); //put ID info in the block buffer
        dump_block_buffer(); //dump the block buffer
        break;//Don't try the other cases.
      }


    case 0x34 : {
        // 0x34 is "4". Iterate through the sectors
        // until we find a non-empty one.
        Serial.println("Seeking Non-empty Block/Sector.");
        uint32_t sector = 0;
        byte keep_going = 0x79; // lower case y


        while (keep_going == 0x79) {
          // Keep going until the user stops typing "y".


          read_write_drive_LBA_block(sector, false);
          //Read the LBA Block into the block buffer.


          if (!(sector % 100)) {
             Serial.println("Checking Block " +
                            (String)sector);
          }
          //Every 100 blocks give some feedback
          //so the user knows the sketch is still running.


          if (non_zero_read) {
            //Is the block empty? No? print a message and
            // dump the block buffer.


            Serial.print("Found something. Dumping sector ");
            Serial.print(sector);
            Serial.print(" ");
            dump_block_buffer();


            Serial.println("Shall I continue? (y/n)");
            //Does the user want to go on?


            while (!Serial.available()) {
              //do nothing until we get serial input.
            }


            keep_going = Serial.read();
            //got serial, store it in continue.
            //The loop will evaluate it.
          }
          sector++;
        }
        break; //Don't try the other cases.
      }


    case 0x35 : {
        //0x35 is "5". Ask for a block number,
        //write to it, and dump the block back.


        String input_string = "";
        //This case asks for more than single chars.
        //Store here.


        uint32_t block = 0;
        // Block is the block number we will write to.


        Serial.println("What LBA block shall I write to? ");
        Serial.print("The drive has ");
        Serial.print (LBA_sectors);
        Serial.print(" LBA blocks, and 28 bit LBA addresses go to");
        Serial.println(" 268435456.");
        //28 bit LBA can reach 128GiB.
        // To go further means not using ATA-1.


        while (!Serial.available()) {
          //do nothing while we wait for input.
        }
        input_string = Serial.readString();
        ////Store the serial input in input_string


        block = string2uint32_t(input_string);
        //turn input string into a uint32_t.


        input_string = "";//clear input string.

        Serial.println("Writing to block " + (String)block +
                       ". Please type your message and" +
                       " press return.");
        while (!Serial.available()) {
          //do nothing
        }
        input_string = Serial.readString();
        //Get another input string, this time the
        //pithy text message to be written to the
        //block the user selected. No post-processing
        //of the string needed this time.


        Serial.println("Writing "" + input_string +
                       "" to Block:" + (String)block);
        //Tell the user what we're writing. The "
        //marks are escaped quotes so we can print them.


        for (int c = 0; c <= 512; c++) {
          if (c <= input_string.length()) {
            block_buffer[c] = input_string.charAt(c);
          } else block_buffer[c] = 0;
        }
        //Copy input_string into the block buffer.
        //Any bytes not used (because the string
        // is shorter) fill with 0s.
        //reset_drive();
        //The drive can get in the wrong mode to write.
        //resetting beforehand makes sure it's ready for
        //writing.


        read_write_drive_LBA_block(block, true);
        //write the contents of the block buffer
        //to the drive's data buffer which writes to
        //the media itself.


        wait_for_drive_ready(1024);
        Serial.println("Done writing. Reading block "
                       + (String)block);
        //wait up to 1024ms for the drive to finish writing.
        //We're only writing one block...


        read_write_drive_LBA_block(block, false);
        dump_block_buffer();
        //Read the block back into the block buffer and
        // dump the block buffer so the user can see.


        input_string = "";
        //clean up input_string.


        break;//Don't try the other cases.
      }


    case 0x36 : {
        Serial.println("Sleeping Drive and Exiting.");
        sleep_drive(); //put the drive in its sleep state
        Serial.println("Halted. Reset Cestino to Restart.");
        while (true) { //Do nothing forever.
        }
        break;
        //Don't try the other cases. Not that we'll ever
        //reach this code.
      }
  }
  menu_item = 0x00; //clear menu_item for the next go-round.
}

Output

This chapter wouldn’t be complete without the output. Some sector dumps have been removed for brevity.

We find a drive when the sketch starts. That’s good.

Checking for a drive...Found a drive.
Model: ST320014A     
Serial Number: 5JZGC69W
Firmware Version: 3.07
Cylinders: 16383
Heads: 16
Sectors: 63
LBA Sectors: 39102336

Here’s the menu.

**** MENU ****
1       Reset Drive
2       Sleep Drive
3       Identify Drive
4       Seek Non-Empty Block/Sector
5       Write to Block/Sector
6       Quit

Selected 1.

Resetting drive.
Drive is reset.


**** MENU ****
1       Reset Drive
2       Sleep Drive
3       Identify Drive
4       Seek Non-Empty Block/Sector
5       Write to Block/Sector
6       Quit

Selected 2.

Sleeping Drive. Reset to wake up.
(Other functions will hang.

We chose function 1 again.

Resetting drive.
Drive is reset.


**** MENU ****
1       Reset Drive
2       Sleep Drive
3       Identify Drive
4       Seek Non-Empty Block/Sector
5       Write to Block/Sector
6       Quit

We chose option 3. It dumps the block buffer once it’s loaded.

Identifying Drive
Dumping Block Buffer
0c 5a 3f ff c8 37 00 10 00 00 00 00 00 3f 00 00 | .Z?..7.......?..
00 00 00 00 35 4a 5a 47 43 36 39 57 20 20 20 20 | ....5JZGC69W
20 20 20 20 20 20 20 20 00 00 10 00 00 04 33 2e |     ......3.
30 37 20 20 20 20 53 54 33 32 30 30 31 34 41 20 | 07  ST320014A
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 |        
20 20 20 20 20 20 20 20 20 20 20 20 20 20 80 10 |        ..
00 00 2f 00 00 00 02 00 02 00 00 07 3f ff 00 10 | ../.........?...
00 3f fc 10 00 fb 00 10 a7 80 02 54 00 00 04 07 | .?.........T....
00 03 00 78 00 78 00 f0 00 78 00 00 00 00 00 00 | ...x.x...x......
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 7e 00 00 34 6b 5b 01 40 03 34 69 1a 01 40 03 | .∼..4k[[email protected]..@.
00 3f 00 00 00 00 00 00 ff fe 60 0b 80 80 00 00 | .?........'.....
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 01 a8 28 02 54 a7 80 02 54 2e 30 00 02 0c b7 | ...(.T...T.0....
02 10 00 00 3c 03 3c b4 ff ff 00 0d 00 00 08 01 | ....<.<.........
04 80 02 a0 01 02 00 00 00 3c 04 38 e8 08 bd 10 | .........<.8....
00 00 04 54 00 28 00 00 00 00 00 00 00 e0 00 0a | ...T.(..........
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 df a5 | ................
Bytes dumped:512


**** MENU ****
1       Reset Drive
2       Sleep Drive
3       Identify Drive
4       Seek Non-Empty Block/Sector
5       Write to Block/Sector
6       Quit

We chose option 4. Most drives have something in sector 0. In PCs, this is where the boot sectors are usually located.

Seeking Non-empty Block/Sector.
Checking Block 0
Found something. Dumping sector 0
Dumping Block Buffer
fa b8 00 10 8e d0 bc 00 b0 b8 00 00 8e d8 8e c0 | ................
fb be 00 7c bf 00 06 b9 00 02 f3 a4 ea 21 06 00 | ...|.........!..
00 be be 07 38 04 75 0b 83 c6 10 81 fe fe 07 75 | ....8.u........u
f3 eb 16 b4 02 b0 01 bb 00 7c b2 80 8a 74 01 8b | .........|...t..
4c 02 cd 13 ea 00 7c 00 00 eb fe 00 00 00 00 00 | L.....|.........
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 e0 2d 00 00 00 00 00 20 | .........-.....
21 00 0b fe ff ff 00 08 00 00 00 98 54 02 00 00 | !...........T...
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa | ..............U.
Bytes dumped:512
Shall I continue? (y/n)

We decided to continue...

Checking Block 100

Many more blocks checked

Found something. Dumping sector 21248
Dumping Block Buffer
54 68 69 73 20 69 73 20 61 6e 20 6f 6c 64 2c 20 | This is an old,
6f 6c 64 20 64 72 69 76 65 2e 20 49 20 77 6f 75 | old drive. I wou
6c 64 6e 27 74 20 70 75 74 20 64 61 74 61 20 49 | ldn't put data I
20 76 61 6c 75 65 64 20 6f 6e 20 69 74 2e 20 41 | valued on it. A
6c 73 6f 2c 20 69 74 27 73 20 22 6f 6e 6c 79 22 | lso, it's "only"
20 32 30 67 62 2e 0a 00 00 00 00 00 00 00 00 00 | 20gb...........

The block was empty below this file.

**** MENU ****
1       Reset Drive
2       Sleep Drive
3       Identify Drive
4       Seek Non-Empty Block/Sector
5       Write to Block/Sector
6       Quit

We chose option 5.

What LBA block shall I write to?
The drive has 39102336 LBA blocks, and 28 bit LBA addresses go to 369098751.
Writing to block 1. Please type your message and press return.
Writing "Writing in block one, just to see if it can be done." to Block:1
Done writing. Reading block 1
Dumping Block Buffer
57 72 69 74 69 6e 67 20 69 6e 20 62 6c 6f 63 6b | Writing in block
20 6f 6e 65 2c 20 6a 75 73 74 20 74 6f 20 73 65 | one, just to se
65 20 69 66 20 69 74 20 63 61 6e 20 62 65 20 64 | e if it can be d
6f 6e 65 2e 00 00 00 00 00 00 00 00 00 00 00 00 | one.............
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
Bytes dumped:512

Yup, the block was written to, and filled out with zeros.

**** MENU ****
1       Reset Drive
2       Sleep Drive
3       Identify Drive
4       Seek Non-Empty Block/Sector
5       Write to Block/Sector
6       Quit

Finally, option 6.

Sleeping Drive and Exiting.
Halted. Reset Cestino to Restart.

Credit Where Credit Is Due

I am by no means the first person to connect a PATA drive to a microcontroller, and I’ve used a large number of sources to piece together what I needed to do it.

The PJRC website, http://​www.​pjrc.​com/​tech/​8051/​ide/​index.​html#lba, goes into great detail about how to connect one to an 8051 microcontroller with an 8255 parallel driver chip doing the duty we’re using a pair of ATmega ports for. It was this site that convinced me to stick with LBA rather than trying to suss out cylinder/head/sector mode, which is different for each drive.

PJRC credits Wesley’s Pic Pages, which it now also hosts: http://www.pjrc.com/tech/8051/ide/wesley.html . These pages also talk about interfacing a PATA drive (IDE) to a microcontroller using an 8255 to handle the 16 bit data bus from an 8 bit microcontroller. More important, they include a lot of documentation about the PATA standard, from a number of sources including a reverse engineering project.

As I mentioned in the sketch, I cribbed the status masking system in its entirety from GeneT’s idefat library, available here: http://code.google.com/p/idefat-arduino/ . You might want to take a copy in case Google code ever finishes going away.

Finally, I must credit ANSI, particularly Tim Dovan, the Director of Customer Service, for graciously letting me use their copyrighted information from this long-withdrawn standard. If you need a copy of the modern standard, it’s reasonably priced and available here: http://webstore.ansi.org/RecordDetail.aspx?sku=ANSI+INCITS+397-2005+Package .

Further—Bigger, Faster, More Modern?

Where could you go with this project? Lots of places. The nearest goals are the most obvious: LBA48 to let you talk to larger drives. You could add filesystems as GeneT did in the idefat library.

SATA requires very high speeds and precise timing, and is probably out of reach of Arduino type projects permanently. I may be wrong. If you find an approach that works, I’d love to know.

Fortunately for Cestino projects, we don’t really need hard drives at all. Any reasonable amount of data we might want to store with an 8 bit microcontroller can be stored on an SD/SDHC card using the hardware SPI system built into the ATmega1284P. The Arduino foundation appears to maintain a library for doing just this here: https://www.arduino.cc/en/Reference/SD . If you use this library, your pinouts won’t be the same. The ATmega1284P’s SPI interface is in PORT B, on pins PB4 through PB7. SPI is not the fastest mode for an SD/SDHC card to run in. Be aware that SD cards want 3.3 volts rather than 5. You’d need to do some level shifting or modify the Cestino to work at 3.3v (Change some of the fuse settings for power management and replace the full can oscillator with something 3.3v-compatible.)

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.74.231