PCI IDE Controller
IDE is a keyword points to the semi-conductors on the mother-board that controls ATA Drives(like ATA Hard-Disks). ATA (AT-Attachment) is the interface of this drives. IDE also can be an IDE card connected to PCI.
ATAPI is an extension to ATA. ATAPI (ATA Packet Interface) adds the support of Drives wich uses SCSI Command-Set (like ODDs (Optical Disk Drives .e.g CD-ROMs, DVD-ROMs), Tape Drives, and ZIP Drives).
Parallel/Serial ATA/ATAPI
IDE can allow even 4 drives to be connected to. Each drive may be:
- 1. Parallel AT-Attachment [PATA]: Like PATA HDDs.
- 2. Parallel AT-Attachment Packet-Interface [PATAPI]: Like PATAPI ODDs.
- 3. Serial ATA [SATA]: Like SATA HDDs.
- 4. Serial ATAPI [SATAPI]: Like SATAPI ODDs.
We can ignore Tape Drives and ZIP Drives as they are obseleted. The Way of accessing ATA Drives is one, means that the way of accessing PATA HDDs is the same of SATA HDDs. also the way of accessing PATAPI ODDs is the same of SATAPI ODDs. For that, for IDE Device Driver, it is not required to know if a drive is Parallel or Serial, but it is important to know if it is ATA or ATAPI.
IDE Interface
If you open your case and look at the mother board, we will see a port or two like these in the picture to the right.
The white and green ports are IDE Ports, each port of them is called channel. so there is:
- Primary IDE Channel.
- Secondary IDE Channel.
These Ports allows only Parallel Drives to be connected to, means that it supports only PATA/PATAPI Drives.
Each Port can has a PATA cable connected to, it is like this in the photo to the right. One master drive, or two drives [Master and Slave] can be connected to one PATA Cable.
So we can have:
- Primary Master Drive.
- Primary Slave Drive.
- Secondary Master Drive.
- Secondary Slave Drive.
Each Drive May be: PATA or PATAPI.
But What about Serial IDE?
Almost many of modern motherboards have a Serial IDE which allows SATA and SATAPI Drives to be connected to.
Serial IDE Ports are 4, like these appear in the photo to the right, Each Port is conducted with a Serial ATA (SATA) Cable.
So from the pictures we can understand that only one drive can be connected to Serial IDE Port, each two ports make a channel, and also Serial IDE has:
- Primary Master Drive [Port1, or Port 2], also called [SATA1] in BIOS Setup Utility.
- Primary Slave Drive [Port 1 or Port 2], also called [SATA2] in BIOS Setup Utility.
- Secondary Master Drive [Port 3 or Port 4], also called [SATA3] in BIOS Setup Utility.
- Secondary Slave Drive [Port 3 or Port 4], also called [SATA4] in BIOS Setup Utility.
Detecting an IDE
Please if you wanna support only the Parallel IDE, skip the part of [Detecting an IDE]. Each IDE appears as a device [in PCI World, it is called a function] on PCI Bus. If you don't know about PCI, please refer to PCI. When you find a device on PCI, you should determine whether it is an IDE Device or not, this is determined according to Class Code and Subclass code. If Class code is: 0x01 [Mass Storage Controller] and Subclass Code is: 0x01 [IDE], so this device is an IDE Device. We know all that each PCI Device has 6 BARs, ok, only 5 BARs are used by IDE Device:
- BAR0: Base Address of Primary Channel I/O Ports, if it is 0x0 or 0x1, this means [0x1F0].
- BAR1: Base Address of Priamry Channel Control Ports, if it is 0x0 or 0x1, this means [0x3F4].
- BAR2: Base Address of Secondary Channel I/O Ports, if it is 0x0 or 0x1, this means [0x170].
- BAR3: Base Address of Secondary Channel Control Ports, if it is 0x0 or 0x1, this means [0x374].
- BAR4: Bus Master IDE, this I/O Address refers to the base of I/O range consists of 16 ports, each 8 ports controls DMA on a channel.
IRQs are really a problem for IDEs, because the IDE uses IRQs 14 and 15, if it is a Parallel IDE. If it is a Serial IDE, it uses another IRQ and only one IRQ, but how does we know the IRQs used by IDE? In Quafios it is quite easy:
outl((1<<31) | (bus<<16) | (device<<11) | (func<<8) | 8, 0xCF8); // Send the parameters.
if ((class = inl(0xCFC)>>16) != 0xFFFF) { // If there is exactly a device
// Check if this device need an IRQ assignment:
outl((1<<31) | (bus<<16) | (device<<11) | (func<<8) | 0x3C, 0xCF8);
outb(0xFE, 0xCFC); // Change the IRQ field to 0xFE
outl((1<<31) | (bus<<16) | (device<<11) | (func<<8) | 0x3C, 0xCF8); // Read the IRQ Field Again.
if ((inl(0xCFC) & 0xFF)==0xFE) {
// This Device needs IRQ assignment.
} else {
// The Device doesn't use IRQs, check if this is an Parallel IDE:
if (class == 0x01 && subclass == 0x01 && (ProgIF == 0x8A || ProgIF == 0x80)) {
// This is a Parallel IDE Controller which use IRQ 14 and IRQ 15.
}
}
}
By this way, you can make a structure with PCI Devices, each device has IRQ0 and IRQ1, the both should have an initial value of 0xFF [No IRQ]. if we detect a PCI Device, if the Device needs an IRQ, we can change IRQ0. if the device on PCI doesn't need, but it is a Parallel IDE, we can edit IRQ0 to 14 and IRQ1 to 15.
When an IRQ is invoked, ISR should read the IRQ number from PIC, then it searches for the device which has this IRQ [in IRQ0 or IRQ1], and if the device is found, call the device driver to inform it that an IRQ is invoked.
Detecting IDE Drives
In Quafios, when an IDE Device is found, Quafios reserves a space in memory and copies Generic IDE Device Driver to this space, And then calls the device driver with a function number of 1. function number 1 is initialization, which is:
void ide_initialize(unsigned int BAR0, unsigned int BAR1, unsigned int BAR2, unsigned int BAR3,
unsigned int BAR4) {
If you wanna support only the parallel IDE, you can put this command in kernel:
ide_initialize(0x1F0, 0x3F4, 0x170, 0x374, 0x000);
We can assume that BAR4 is 0x0 because we are not going to use it yet. We will return to ide_initialize function which searches for drives connected to the IDE, before we are going into this function, we should write some functions which will help us a lot. First We should write some Definitions:
#define ATA_SR_BSY 0x80
#define ATA_SR_DRDY 0x40
#define ATA_SR_DF 0x20
#define ATA_SR_DSC 0x10
#define ATA_SR_DRQ 0x08
#define ATA_SR_CORR 0x04
#define ATA_SR_IDX 0x02
#define ATA_SR_ERR 0x01
There is a port is called Command/Status Port, when it is read, you read the status of channel, the above bit maskes express these states.
#define ATA_ER_BBK 0x80
#define ATA_ER_UNC 0x40
#define ATA_ER_MC 0x20
#define ATA_ER_IDNF 0x10
#define ATA_ER_MCR 0x08
#define ATA_ER_ABRT 0x04
#define ATA_ER_TK0NF 0x02
#define ATA_ER_AMNF 0x01
There is a port is called Features/Error Port, if it is read, you are reading the errors of the last operation, the bit maskes above express these errors.
// ATA-Commands:
#define ATA_CMD_READ_PIO 0x20
#define ATA_CMD_READ_PIO_EXT 0x24
#define ATA_CMD_READ_DMA 0xC8
#define ATA_CMD_READ_DMA_EXT 0x25
#define ATA_CMD_WRITE_PIO 0x30
#define ATA_CMD_WRITE_PIO_EXT 0x34
#define ATA_CMD_WRITE_DMA 0xCA
#define ATA_CMD_WRITE_DMA_EXT 0x35
#define ATA_CMD_CACHE_FLUSH 0xE7
#define ATA_CMD_CACHE_FLUSH_EXT 0xEA
#define ATA_CMD_PACKET 0xA0
#define ATA_CMD_IDENTIFY_PACKET 0xA1
#define ATA_CMD_IDENTIFY 0xEC
When you write to Command/Status Port, You are executing a command, which can be one of the commands above.
#define ATAPI_CMD_READ 0xA8
#define ATAPI_CMD_EJECT 0x1B
The Command above are for ATAPI Devices which will be understanded soon.
The Commands ATA_CMD_IDENTIFY_PACKET, and ATA_CMD_IDENTIFY, returns a buffer of 512 byte, the buffer is called Identification space, the following definitions are used to read information from the identification space.
#define ATA_IDENT_DEVICETYPE 0
#define ATA_IDENT_CYLINDERS 2
#define ATA_IDENT_HEADS 6
#define ATA_IDENT_SECTORS 12
#define ATA_IDENT_SERIAL 20
#define ATA_IDENT_MODEL 54
#define ATA_IDENT_CAPABILITIES 98
#define ATA_IDENT_FIELDVALID 106
#define ATA_IDENT_MAX_LBA 120
#define ATA_IDENT_COMMANDSETS 164
#define ATA_IDENT_MAX_LBA_EXT 200
When you select a drive, you should specify if it is the master drive or the slave one:
#define ATA_MASTER 0x00
#define ATA_SLAVE 0x01
#define IDE_ATA 0x00
#define IDE_ATAPI 0x01
// ATA-ATAPI Task-File:
#define ATA_REG_DATA 0x00
#define ATA_REG_ERROR 0x01
#define ATA_REG_FEATURES 0x01
#define ATA_REG_SECCOUNT0 0x02
#define ATA_REG_LBA0 0x03
#define ATA_REG_LBA1 0x04
#define ATA_REG_LBA2 0x05
#define ATA_REG_HDDEVSEL 0x06
#define ATA_REG_COMMAND 0x07
#define ATA_REG_STATUS 0x07
#define ATA_REG_SECCOUNT1 0x08
#define ATA_REG_LBA3 0x09
#define ATA_REG_LBA4 0x0A
#define ATA_REG_LBA5 0x0B
#define ATA_REG_CONTROL 0x0C
#define ATA_REG_ALTSTATUS 0x0C
#define ATA_REG_DEVADDRESS 0x0D
Task File is a range of ports [8 ports] which are used by primary channel [BAR0] or Secondary Channel [BAR2].
- BAR0 + 0 is first port.
- BAR0 + 1 is second port.
- BAR0 + 3 is the third ... etc ...
if BAR0 is 0x1F0:
- The Data Port of the Primary Channel is 0x1F0.
- The Features/Error Port of the Priamry Channel is 0x1F1.
- etc ...
The same with the secondary channel.
There is a port which is called "ALTSTATUS/CONTROL PORT", when is read, you read alternate status, when this port is written to, you are controlling a channel.
- For the Primary Channel, ALTSTATUS/CONTROL Port is BAR1 + 2.
- For the Secondary Channel, ALTSTATUS/CONTROL Port is BAR3 + 2.
We can know say that Each Channel has 13 Register, for a primary channel:
- Data Register: BAR0[0]; // Read and Write
- Error Register: BAR0[1]; // Read Only
- Features Register: BAR0[1]; // Write Only
- SECCOUNT0: BAR0[2]; // Read and Write
- LBA0: BAR0[3]; // Read and Write
- LBA1: BAR0[4]; // Read and Write
- LBA2: BAR0[5]; // Read and Write
- HDDEVSEL: BAR0[6]; // Read and Write, this port is used to select a drive in the channel.
- Command Register: BAR0[7]; // Write Only.
- Status Register: BAR0[7]; // Read Only.
- Alternate Status Register: BAR1[2]; // Read Only.
- Control Register: BAR1[2]; // Write Only.
- DEVADDRESS: BAR1[2]; // I don't know what is the benefit from this register.
The map above is the same with the secondary channel, but it is using BAR2 and BAR3 instead of BAR0 and BAR1.
// Channels:
#define ATA_PRIMARY 0x00
#define ATA_SECONDARY 0x01
// Directions:
#define ATA_READ 0x00
#define ATA_WRITE 0x01
We have had defined all definitions needed by the driver, now lets move to an important part, we said that
- BAR0 is the Base of I/O Ports used by Primary Channel.
- BAR1 is the Base of I/O Ports which control Primary Channel.
- BAR2 is the Base of I/O Ports used by Secondary Channel.
- BAR3 is the Base of I/O Ports which control Secondary Channel.
- BAR4 is the Base of 8 I/O Ports controls Primary Channel's Bus Master IDE [BMIDE].
- BAR4 + 8 is the Base of 8 I/O Ports controls Secondary Channel's Bus Master IDE [BMIDE].
So we can make this global structure:
struct channel {
unsigned short base; // I/O Base.
unsigned short ctrl; // Control Base
unsigned short bmide; // Bus Master IDE
unsigned char nIEN; // nIEN (No Interrupt);
} channels[2];
We also need a buffer to read the identification space in it, we need a variable that indicates if an irq is invoked or not, and finally we need an array of 6 words [12 bytes] for ATAPI Drives:
unsigned char ide_buf[2048] = {0};
unsigned static char ide_irq_invoked = 0;
unsigned static char atapi_packet[12] = {0xA8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
We said the the IDE can contain up to 4 drives:
struct ide_device {
unsigned char reserved; // 0 (Empty) or 1 (This Drive really exists).
unsigned char channel; // 0 (Primary Channel) or 1 (Secondary Channel).
unsigned char drive; // 0 (Master Drive) or 1 (Slave Drive).
unsigned short type; // 0: ATA, 1:ATAPI.
unsigned short sign; // Drive Signature
unsigned short capabilities;// Features.
unsigned int commandsets; // Command Sets Supported.
unsigned int size; // Size in Sectors.
unsigned char model[41]; // Model in string.
} ide_devices[4];
When we read a register in a channel, like STATUS Register, it is easy to execute:
ide_read(channel, ATA_REG_STATUS);
unsigned char ide_read(unsigned char channel, unsigned char reg) {
unsigned char result;
if (reg > 0x07 && reg < 0x0C) ide_write(channel, ATA_REG_CONTROL, 0x80 | channels[channel].nIEN);
if (reg < 0x08) result = inb(channels[channel].base + reg - 0x00);
else if (reg < 0x0C) result = inb(channels[channel].base + reg - 0x06);
else if (reg < 0x0E) result = inb(channels[channel].ctrl + reg - 0x0A);
else if (reg < 0x16) result = inb(channels[channel].bmide + reg - 0x0E);
if (reg > 0x07 && reg < 0x0C) ide_write(channel, ATA_REG_CONTROL, channels[channel].nIEN);
return result;
}
And Also there is a function for writing to registers:
void ide_write(unsigned char channel, unsigned char reg, unsigned char data) {
if (reg > 0x07 && reg < 0x0C) ide_write(channel, ATA_REG_CONTROL, 0x80 | channels[channel].nIEN);
if (reg < 0x08) outb(data, channels[channel].base + reg - 0x00);
else if (reg < 0x0C) outb(data, channels[channel].base + reg - 0x06);
else if (reg < 0x0E) outb(data, channels[channel].ctrl + reg - 0x0A);
else if (reg < 0x16) outb(data, channels[channel].bmide + reg - 0x0E);
if (reg > 0x07 && reg < 0x0C) ide_write(channel, ATA_REG_CONTROL, channels[channel].nIEN);
}
If We want to read the identification space, we should read Data Register as Double Word for 128 times. the first read is the first dword, the second read is the second dword, and so on. we can read the 128 dwords and copy them to our buffer.
void ide_read_buffer(unsigned char channel, unsigned char reg, unsigned int buffer, unsigned int quads) {
if (reg > 0x07 && reg < 0x0C) ide_write(channel, ATA_REG_CONTROL, 0x80 | channels[channel].nIEN);
asm("pushw %es; movw %ds, %ax; movw %ax, %es");
if (reg < 0x08) insl(channels[channel].base + reg - 0x00, buffer, quads);
else if (reg < 0x0C) insl(channels[channel].base + reg - 0x06, buffer, quads);
else if (reg < 0x0E) insl(channels[channel].ctrl + reg - 0x0A, buffer, quads);
else if (reg < 0x16) insl(channels[channel].bmide + reg - 0x0E, buffer, quads);
asm("popw %es;");
if (reg > 0x07 && reg < 0x0C) ide_write(channel, ATA_REG_CONTROL, channels[channel].nIEN);
}
When we send a command, we should wait for 400 nanosecond, then we should read Status Port, if Busy Bit is on, so we should read status port again, until Busy Bit is 0, in this case, we can read the results of the command. this operation is called "Polling", we can use IRQs instead of polling, and IRQs are suitable for Multi-Tasking Environments, but i think Polling is much faster than IRQs.
After Many Commands, if DF is set [Device Fault Bit], so there is a failure, and if DRQ is not set, so there is an error. if ERR bit is set, so there is an error which is described in Error Port.
unsigned char ide_polling(unsigned char channel, unsigned int advanced_check) {
// (I) Delay 400 nanosecond for BSY to be set:
// -------------------------------------------------
ide_read(channel, ATA_REG_ALTSTATUS); // Reading Alternate Status Port wastes 100ns.
ide_read(channel, ATA_REG_ALTSTATUS); // Reading Alternate Status Port wastes 100ns.
ide_read(channel, ATA_REG_ALTSTATUS); // Reading Alternate Status Port wastes 100ns.
ide_read(channel, ATA_REG_ALTSTATUS); // Reading Alternate Status Port wastes 100ns.
// (II) Wait for BSY to be cleared:
// -------------------------------------------------
while (ide_read(channel, ATA_REG_STATUS) & ATA_SR_BSY); // Wait for BSY to be zero.
if (advanced_check) {
unsigned char state = ide_read(channel, ATA_REG_STATUS); // Read Status Register.
// (III) Check For Errors:
// -------------------------------------------------
if (state & ATA_SR_ERR) return 2; // Error.
// (IV) Check If Device fault:
// -------------------------------------------------
if (state & ATA_SR_DF ) return 1; // Device Fault.
// (V) Check DRQ:
// -------------------------------------------------
// BSY = 0; DF = 0; ERR = 0 so we should check for DRQ now.
if (!(state & ATA_SR_DRQ)) return 3; // DRQ should be set
}
return 0; // No Error.
}
if there is an error, we have a functions which print errors on screen:
unsigned char ide_print_error(unsigned int drive, unsigned char err) {
if (err == 0) return err;
printk(" IDE:");
if (err == 1) {printk("- Device Fault\n "); err = 19;}
else if (err == 2) {
unsigned char st = ide_read(ide_devices[drive].channel, ATA_REG_ERROR);
if (st & ATA_ER_AMNF) {printk("- No Address Mark Found\n "); err = 7;}
if (st & ATA_ER_TK0NF) {printk("- No Media or Media Error\n "); err = 3;}
if (st & ATA_ER_ABRT) {printk("- Command Aborted\n "); err = 20;}
if (st & ATA_ER_MCR) {printk("- No Media or Media Error\n "); err = 3;}
if (st & ATA_ER_IDNF) {printk("- ID mark not Found\n "); err = 21;}
if (st & ATA_ER_MC) {printk("- No Media or Media Error\n "); err = 3;}
if (st & ATA_ER_UNC) {printk("- Uncorrectable Data Error\n "); err = 22;}
if (st & ATA_ER_BBK) {printk("- Bad Sectors\n "); err = 13;}
} else if (err == 3) {printk("- Reads Nothing\n "); err = 23;}
else if (err == 4) {printk("- Write Protected\n "); err = 8;}
printk("- [%s %s] %s\n",
(const char *[]){"Primary","Secondary"}[ide_devices[drive].channel],
(const char *[]){"Master", "Slave"}[ide_devices[drive].drive],
ide_devices[drive].model);
return err;
}
Now lets return to the initialization function:
void ide_initialize(unsigned int BAR0, unsigned int BAR1, unsigned int BAR2, unsigned int BAR3,
unsigned int BAR4) {
int j, k, count = 0;
// 1- Detect I/O Ports which interface IDE Controller:
channels[ATA_PRIMARY ].base = (BAR0 &= 0xFFFFFFFC) + 0x1F0*(!BAR0);
channels[ATA_PRIMARY ].ctrl = (BAR1 &= 0xFFFFFFFC) + 0x3F4*(!BAR1);
channels[ATA_SECONDARY].base = (BAR2 &= 0xFFFFFFFC) + 0x170*(!BAR2);
channels[ATA_SECONDARY].ctrl = (BAR3 &= 0xFFFFFFFC) + 0x374*(!BAR3);
channels[ATA_PRIMARY ].bmide = (BAR4 &= 0xFFFFFFFC) + 0; // Bus Master IDE
channels[ATA_SECONDARY].bmide = (BAR4 &= 0xFFFFFFFC) + 8; // Bus Master IDE
Then We Should Disable IRQs in the both channels [This is temporary]:
This happens by setting bit 1 [nIEN] in Control Port:
Code:
// 2- Disable IRQs:
ide_write(ATA_PRIMARY , ATA_REG_CONTROL, 2);
ide_write(ATA_SECONDARY, ATA_REG_CONTROL, 2);
Now we need to check for drives connected to each channel, we will select the master drive of each channel, and send the command ATA_IDENTIFY (Which is supported by ATA Drives). if error, there is values returned in registers determines the type of Drive, if no drive, there will be strange values.
Notice that bit4 in HDDEVSEL, if set to 1, we are selecting the slave drive, if set to 0, we are selecting the master drive.
// 3- Detect ATA-ATAPI Devices:
for (i = 0; i < 2; i++)
for (j = 0; j < 2; j++) {
unsigned char err = 0, type = IDE_ATA, status;
ide_devices[count].reserved = 0; // Assuming that no drive here.
// (I) Select Drive:
ide_write(i, ATA_REG_HDDEVSEL, 0xA0 | (j<<4)); // Select Drive.
sleep(1); // Wait 1ms for drive select to work.
// (II) Send ATA Identify Command:
ide_write(i, ATA_REG_COMMAND, ATA_CMD_IDENTIFY);
sleep(1); // This function should be implemented in your OS. which waits for 1 ms. it is based on System Timer Device Driver.
// (III) Polling:
if (!(ide_read(i, ATA_REG_STATUS))) continue; // If Status = 0, No Device.
while(1) {
status = ide_read(i, ATA_REG_STATUS);
if ( (status & ATA_SR_ERR)) {err = 1; break;} // If Err, Device is not ATA.
if (!(status & ATA_SR_BSY) && (status & ATA_SR_DRQ)) break; // Everything is right.
}
// (IV) Probe for ATAPI Devices:
if (err) {
unsigned char cl = ide_read(i,ATA_REG_LBA1);
unsigned char ch = ide_read(i,ATA_REG_LBA2);
if (cl == 0x14 && ch ==0xEB) type = IDE_ATAPI;
else if (cl == 0x69 && ch ==0x96) type = IDE_ATAPI;
else continue; // Unknown Type (And always not be a device).
ide_write(i, ATA_REG_COMMAND, ATA_CMD_IDENTIFY_PACKET);
sleep(1);
}
// (V) Read Identification Space of the Device:
ide_read_buffer(i, ATA_REG_DATA, (unsigned int) ide_buf, 128);
// (VI) Read Device Parameters:
ide_devices[count].reserved = 1;
ide_devices[count].type = type;
ide_devices[count].channel = i;
ide_devices[count].drive = j;
ide_devices[count].sign = ((unsigned short *) (ide_buf + ATA_IDENT_DEVICETYPE ))[0];
ide_devices[count].capabilities = ((unsigned short *) (ide_buf + ATA_IDENT_CAPABILITIES ))[0];
ide_devices[count].commandsets = ((unsigned int *) (ide_buf + ATA_IDENT_COMMANDSETS ))[0];
// (VII) Get Size:
if (ide_devices[count].commandsets & (1<<26)){
// Device uses 48-Bit Addressing:
ide_devices[count].size = ((unsigned int *) (ide_buf + ATA_IDENT_MAX_LBA_EXT ))[0];
// Note that Quafios is 32-Bit Operating System, So last 2 Words are ignored.
} else {
// Device uses CHS or 28-bit Addressing:
ide_devices[count].size = ((unsigned int *) (ide_buf + ATA_IDENT_MAX_LBA ))[0];
}
// (VIII) String indicates model of device (like Western Digital HDD and SONY DVD-RW...):
for(k = ATA_IDENT_MODEL; k < (ATA_IDENT_MODEL+40); k+=2) {
ide_devices[count].model[k - ATA_IDENT_MODEL] = ide_buf[k+1];
ide_devices[count].model[(k+1) - ATA_IDENT_MODEL] = ide_buf[k];}
ide_devices[count].model[40] = 0; // Terminate String.
count++;
}
// 4- Print Summary:
for (i = 0; i < 4; i++)
if (ide_devices[i].reserved == 1) {
printk(" Found %s Drive %dGB - %s\n",
(const char *[]){"ATA", "ATAPI"}[ide_devices[i].type], /* Type */
ide_devices[i].size/1024/1024/2, /* Size */
ide_devices[i].model);
}
}