Пакетные команды интерфейса ATAPI::Журнал СА 9.2004
www.samag.ru
Льготная подписка для студентов      
Поиск   
              
 www.samag.ru    Web  0 товаров , сумма 0 руб.
E-mail
Пароль  
 Запомнить меня
Регистрация | Забыли пароль?
О журнале
Журнал «БИТ»
Подписка
Где купить
Авторам
Рекламодателям
Магазин
Архив номеров
Вакансии
Контакты
   

Jobsora

ЭКСПЕРТНАЯ СЕССИЯ 2019


  Опросы

Какие курсы вы бы выбрали для себя?  

Очные
Онлайновые
Платные
Бесплатные
Я и так все знаю

 Читать далее...

1001 и 1 книга  
28.05.2019г.
Просмотров: 1826
Комментарии: 2
Анализ вредоносных программ

 Читать далее...

28.05.2019г.
Просмотров: 1887
Комментарии: 1
Микросервисы и контейнеры Docker

 Читать далее...

28.05.2019г.
Просмотров: 1446
Комментарии: 0
Django 2 в примерах

 Читать далее...

28.05.2019г.
Просмотров: 1066
Комментарии: 0
Введение в анализ алгоритмов

 Читать далее...

27.03.2019г.
Просмотров: 1636
Комментарии: 1
Arduino Uno и Raspberry Pi 3: от схемотехники к интернету вещей

 Читать далее...

Друзья сайта  

Форум системных администраторов  

sysadmins.ru

 Пакетные команды интерфейса ATAPI

Архив номеров / 2004 / Выпуск №9 (22) / Пакетные команды интерфейса ATAPI

Рубрика: Программирование /  Анализ данных

ВЛАДИМИР МЕШКОВ

Пакетные команды интерфейса ATAPI

В статье рассматриваются примеры программ, выполняющих доступ к интерфейсу ATAPI через порты ввода-вывода и при помощи системного вызова IOCTL операционной системы Linux.

Общая характеристика интерфейса ATA/ATAPI. Регистры ATAPI-контроллера

Интерфейс ATA – AT Attachment for Disk Drives – разрабатывался в 1986-1990 гг. для подключения накопителей на жестких магнитных дисках к компьютерам IBM PC AT с шиной ISA. Стандарт, разработанный комитетом X3T10, определяет набор регистров и назначение сигналов 40-контактного интерфейсного разъема. Интерфейс появился в результате переноса контроллера жесткого диска ближе к накопителю, на плату электроники с сохранением регистровой модели, т.е. создания устройств со встроенным контроллером – IDE (Integrated Device Electronic).

Для подключения к интерфейсу ATA накопителей CD-ROM набора регистров и системы команд ATA недостаточно. Для них существует аппаратно-программный интерфейс ATAPI (ATA Package Interface – пакетный интерфейс ATA). Устройство ATAPI поддерживает минимальный набор команд ATA, который неограниченно расширяется 12-байтным командным пакетом, посылаемым хост-контроллером в регистр данных устройства по команде PACKET. Структура командного пакета пришла от SCSI (htttp://www.t10.org), что обеспечивает схожесть драйверов для устройств со SCSI и ATAPI.

Регистры ATAPI-контроллера:

Адрес регистра Назначение регистра
Канал 1 Канал 2 Режим чтения Режим записи
0x1F0 0x170 Данные (DR)  
0x1F1 0x171 Ошибка (ER) Регистр свойств (FR)
0x1F2 0x172 Причина прерывания (IR)  
0x1F3 0x173 Не используется  
0x1F4 0x174 Младший байт счетчика байтов (CL)  
0x1F5 0x175 Старший байт счетчика байтов (CH)  
0x1F6 0x176 Выбор устройства (DS)  
0x1F7 0x177 Состояние (SR) Команда (CR)

Регистр данных (DR) используется так же, как и регистр данных ATA.

Регистр ошибки (ER) определяет состояние контроллера устройства ATAPI после выполнения операции и доступен только для чтения. Назначение разрядов регистра следующее:

  • бит 0 (ILI) – недопустимая длина командного пакета или блока данных;
  • бит 1 (EOM) – обнаружен конец дорожки на носителе;
  • бит 2 (ABRT) – аварийное прекращение выполнения команды;
  • бит 3 – не используется;
  • бит 4-7 (Sense Key) – код состояния устройства.

Регистры младшего байта и старшего байта счетчика байтов (CL и CH) используются в режиме PIO и доступны как для чтения, так и для записи информации. Значение счетчика должно быть загружено в эти регистры до того, как код команды будет записан в регистр команд. Значение счетчика должно соответствовать объему передаваемых данных.

В регистре выбора устройства (DS) используется только бит 4 (DEV), с помощью которого осуществляется выбор устройства. Биты 7 и 5 должны иметь значение 1 с целью сохранения совместимости с устаревшими устройствами.

Регистр состояния (SR) отображает состояние устройства. Назначение разрядов регистра следующее:

  • бит 0 (CHK) – признак возникновения исключительной ситуации, в регистре ошибки (ER) находится код ошибки;
  • биты 1 и 2 игнорируются при считывании информации из регистра;
  • бит 3 (DRQ) – признак готовности устройства к обмену данными;
  • бит 4 (SERV) – признак готовности к обслуживанию следующей команды (имеет значение только при работе в режиме перекрытия команд);
  • бит 5 (DMRD/DF) – признак готовности к передаче в режиме DMA (при CHK = 0) или признак неисправности устройства (при CHK = 1).

Регистр команд (CR) используется для загрузки кода выполняемой команды.

Пакетные команды ATAPI

Рассмотрим порядок выполнения пакетных команд интерфейса ATAPI на примерах. Работоспособность всех примеров была проверена для ОС Linux, ядро 2.4.26. Привод CD-ROM подключен как Secondary Master, в ядре включен режим SCSI-эмуляции для ATAPI-устройств (SCSI host adapter emulation for IDE ATAPI devices). Использовались два привода:

  • TEAC CD-W524E Rev 1.0E;
  • MITSUMI CD-ROM FX54++M Rev Y01E.

Исходные тексты всех программ доступны на сайте журнала.

Начнём с самого простого примера – открытие/закрытие лотка CD-ROM. Чтобы выполнить эту операцию, устройству необходимо послать пакетную команду START/STOP UNIT, которая представляет собой 12-байтный блок данных следующего формата (см. спецификацию INF-8020i, п. 10.8.25, стр. 197):

START/STOP UNIT Command

Bit Byte 7 6 5 4 3 2 1 0
0   Operation code (1Bh)  
1   Reserved     Immed
2   Reserved  
3   Reserved  
4   Reserved     LoEj Start
5  
6   Reserved  
7   Reserved  
8   Reserved  
9   Reserved  
10   Reserved  
11   Reserved  

Рисунок 1. Формат команды START/STOP UNIT

Формат этой команды простой. Поле Operation code содержит код команды 0x1B, а тип требуемой операции задают бит LoEj и Start:

LoEj Start Действие
1 0 открыть лоток CD-ROM
1 1 закрыть лоток CD-ROM

Команда START/STOP UNIT относится к классу пакетных команд, не требующих передачи данных (Non-data Commands). Алгоритм выполнения таких команд следующий (см. п. 5.13, спецификация INF-8020i):

  • хост считывает регистр состояния устройства, дожидаясь нулевого значения битов BSY и DRQ. После этого хост заносит в регистр выбора устройство байт, бит DEV которого указывает на адресуемое устройство;
  • хост записывает код пакетной команды 0xA0 в командный регистр;
  • устройство устанавливает бит BSY в регистре состояния и готовится к приёму пакетной команды;
  • подготовившись к приёму пакетной команды, устройство сбрасывает бит BSY и устанавливает бит DRQ;
  • хост записывает 12-байтный командный пакет в регистр данных устройства;
  • устройство устанавливает бит BSY и приступает к выполнению поступившей команды. После выполнения команды устройство сбрасывает бит BSY, DRQ и устанавливает бит DRDY;
  • хост считывает регистр состояния.

Рассмотрим листинг программы, выполняющей открытие и закрытие лотка CD-ROM (файл RAW/atapi_o_c.c).

Биты регистра состояния SR и макроопределения для работы с портами:

#define BSY 0x80

#define DRQ 0x08

#define DRDY 0x40

#define OUT_P_B(val,port) __asm__("outb %%al, %%dx"::"a"(val),"d"(port))

#define OUT_P_W(val,port) __asm__("outw %%ax, %%dx"::"a"(val),"d"(port))

#define IN_P_B(val,port) __asm__("inb %%dx, %%al":"=a"(val):"d"(port))

#define IN_P_W(val,port) __asm__("inw %%dx, %%ax":"=a"(val):"d"(port))

Функция send_packet_command реализует алгоритм выполнения пакетных команд, не требующих передачи данных. Входные параметры функции – указатель на 12-байтный командный пакет:

void send_packet_command(__u8 *cmd_buff)

{

    int i;

    __u8 status = 0;

    __u16 port, a;

    port = 0x177; // регистр состояния SR

/* В соответствии с алгоритмом ждем нулевого значения  битов BSY и DRQ */

for(;;) {

    do {

           IN_P_B(status, port);

    } while(status & BSY);

    if(!(status & DRQ)) break;

}

/* Выбираем устройство Master и в его регистр команд записываем код пакетной команды */  

    port = 0x176; OUT_P_B(0xA0, port); // бит DEV сброшен

    port = 0x177; OUT_P_B(0xA0, port);

/* Ждём сброса бита BSY и установки DRQ */

    for(;;) {

           do {

                 IN_P_B(status, port);

           } while(status & BSY);

           if(status & DRQ) break;

    }

    port = 0x170; // регистр данных

/* Записываем в регистр данных переданный 12-байтный командный пакет

 * (отметим одну особенность – если записывать по одному байту, команда работать не будет)

 */

    for(i = 0; i < 12; i += 2) {

           memcpy((void *)&a, (cmd_buff + i), 2);

           OUT_P_W(a, port);

    }

    port = 0x177;

/* Ждём сброса бита BSY и установки DRDY */

    for(;;) {

           do {

                 IN_P_B(status, port);

           } while(status & BSY);

           if(status & DRDY) break;

    }

    return;

}

Перед отправкой устройству команды START/STOP UNIT проверим его готовность. Проверка готовности выполняется путём посылки устройству команды TEST UNIT READY:

void wait_while_ready()

{

    __u8 cmd_buff[12];

    memset((void *)cmd_buff, 0, 12);

    cmd_buff[0] = 0x00;

    send_packet_command(cmd_buff);

    return;

}

Функция закрытия лотка CD-ROM:

void close_cdrom()

{

    __u8 cmd_buff[12];

    memset((void *)cmd_buff, 0, 12);

    cmd_buff[0] = 0x1B; // код команды START/STOP UNIT

    cmd_buff[4] = 0x3; // LoEj = 1, Start = 1

    send_packet_command(cmd_buff);

}

Функция открытия лотка CD-ROM:

void open_cdrom()

{

    __u8 cmd_buff[12];

    memset((void *)cmd_buff, 0, 12);

    cmd_buff[0] = 0x1B;

    cmd_buff[4] = 0x2; // LoEj = 1, Start = 0

    send_packet_command(cmd_buff);

}

Главная функция:

int main()

{

    ioperm(0x170, 8, 1);

/* Ждём готовности устройства и открываем лоток */

    wait_while_ready();

    open_cdrom();

    printf("CD-ROM открыт. Нажмите "Enter" для закрытия.");

    getchar();

/* Закрываем лоток */

    close_cdrom();

    wait_while_ready();

    printf("OK. CD-ROM закрыт. ");

    ioperm(0x170, 8, 0);

    return 0;

}

Теперь вместо команды START/STOP UNIT пошлём устройству команду PLAY AUDIO. По этой команде устройство начнёт воспроизведение звукового фрагмента с Аудио-CD. Формат команды PLAY AUDIO представлен на рис. 2:

PLAY AUDIO Command

Bit Byte 7 6 5 4 3 2 1 0
0   Operation Code (45h)  
1   Reserved  
2 MSB             Starting Logical Block Address       LSB
3
4
5
6   Reserved  
7 MSB   Transfer Length       LSB
8
9   Reserved  
10   Reserved  
11   Reserved  

Рисунок 2. Формат команды PLAY AUDIO

Описание полей командного пакета:

  • Starting Logical Block Address – логический номер сектора, с которого начинается воспроизведение. Байты номера сектора располагаются в обратном порядке, на что указывают аббревиатуры MSB (Most significant bit) и LSB (Least significant bit);
  • Transfer Length – длина воспроизводимого фрагмента в логических секторах.

Следующая функция формирует и посылает устройству пакетную команду PLAY AUDIO (полный листинг находится в файле RAW/play_audio.c):

void play_audio()

{

    __u8 cmd_buff[12]; 

/* Воспроизводим фрагмент размером 5000 секторов, начиная с 10-го */

    __u32 start_lba = 10; // стартовый сектор

    __u16 lba_len = 5000; // длина воспроизводимого участка

    memset((void *)cmd_buff, 0, 12);

    cmd_buff[0] = 0x45; // код команды PLAY AUDIO

/* Меняем порядок следования байт при помощи макроса  __swab32. Этот макрос определён в файле */

    start_lba = __swab32(start_lba);

    memcpy((void *)(cmd_buff + 2), (void *)&start_lba, 4);

    lba_len = __swab16(lba_len);

    memcpy((void *)(cmd_buff + 6), (void *)&lba_len, 2);

    send_packet_command(cmd_buff);

    return;

}

Следующий класс пакетных команд, который мы рассмотрим в данной статье, требует передачи данных от устройства к хосту в режиме PIO. Алгоритм выполнения таких команд следующий (см. п. 5.8, спецификация INF-8020i):

  • хост считывает регистр состояния устройства, дожидаясь нулевого значения битов BSY и DRQ. После этого хост заносит в регистр выбора устройство байт, бит DEV которого указывает на адресуемое устройство, в регистр счетчика байтов (старший и младший) заносится число передаваемых от устройства байт;
  • хост записывает код пакетной команды 0xA0 в командный регистр;
  • устройство устанавливает бит BSY в регистре состояния и готовится к приёму пакетной команды;
  • подготовившись к приёму пакетной команды, устройство сбрасывает бит BSY и устанавливает бит DRQ в регистре состояния;
  • хост записывает 12-байтный командный пакет в регистр данных устройства;
  • устройство устанавливает бит BSY и приступает к выполнению поступившей команды. После выполнения команды устройство сбрасывает бит BSY и устанавливает бит DRQ. Если во время выполнения команды произошла ошибка, в регистре состояния будет установлен бит CHK;
  • хост опрашивает регистр состояния, дожидаясь единичного значения бита DRQ. После этого хост считывает из регистра данных результат выполнения команды.

Функция send_packet_data_command() реализует этот алгоритм. Параметры функции – размер запрашиваемых данных и указатель на 12-байтный командный пакет:

int send_packet_data_command(__u16 data_len, __u8 *cmd_buff)

{

    int i;

    __u8 status = 0;

    __u16 port, a;

    port = 0x177;

/* Ожидаем сброса битов BSY и DRQ */

    for(;;) {

           do {

                 IN_P_B(status, port);

           } while(status & BSY);

           if(!(status & DRQ)) break;

    }

/* Выбираем устройство Secondary Master */

    port = 0x176;

    OUT_P_B(0xA0, port);

/* В младший байт счетчика байтов (CL) заносим размер запрашиваемых данных */

    port = 0x174; OUT_P_W(data_len, port);

/* В регистр команд записываем код пакетной команды */

    port = 0x177; OUT_P_B(0xA0, port);

/* Ждём установки бита DRQ */

    for(;;) {

           do {

                 IN_P_B(status, port);

           } while(status & BSY);

           if(status & DRQ) break;

    }

    port = 0x170;

/* В регистр данных записываем 12-байтный командный пакет */

    for(i = 0; i < 12; i += 2) {

           memcpy((void *)&a, (cmd_buff + i), 2);

           OUT_P_W(a, port);

    }

/* Ждём завершения команды - установленного бита DRQ. Если произошла ошибка - фиксируем этот факт */

    port = 0x177;

    for(;;) {

           do {

                 IN_P_B(status, port);

           } while(status & BSY);

           // ошибка выполнения команды!

           if(status & ERR) return -1;

           if(status & DRQ) break;

    }

    return 0;

}

В качестве примеров пакетных команд, требующих передачи данных от устройства к хосту, рассмотрим следующий список:

  • INQUIRY
  • READ CD
  • REQUEST SENSE
  • READ TOC
  • SEEK
  • READ SUB-CHANNEL

По команде INQUIRY устройство выдаёт блок информации о своих параметрах. Формат команды INQUIRY следующий:

  • байт 0 – код команды INQUIRY, значение 0x12;
  • байт 4 – размер выделенной области для считываемых данных.

Формат стандартной справки, выдаваемой по команде INQUIRY, приведен на рис. 3 (INF-8020i, п. 10.8.1.1, стр. 94.).

Функция read_inquiry() считывает идентификатор изготовителя (Vendor Identification), идентификатор изделия (Product Identification) и версию изделия (Product Revision Level):

int read_inquiry()

{

int i = 0;

    __u8 cmd_buff[12];

    __u16 inquiry_buff[20]; // буфер для считываемых данных

    __u8 data_len = 40; // размер запрашиваемых данных

    memset(inquiry_buff, 0, 40);

    memset((void *)cmd_buff, 0, 12);

    cmd_buff[0] = 0x12; // код команды INQUIRY

    cmd_buff[4] = data_len;

/* Посылаем устройству пакетную команду */

    if(send_packet_data_command(data_len, cmd_buff) < 0) {

           printf("INQUIRY command error\n ");

 return -1;

    }

/* Считываем результат и отображаем его */

    for(i = 0; i < 20; i++) IN_P_W(inquiry_buff[i], 0x170);

    printf("%s\n", inquiry_buff + 4);

    return 0;

}

Полный листинг программы чтения идентификационной информации устройства находится в файле RAW/inquiry_cd.c.

INQUIRY Data Format

Bit Byte 7 6 5 4 3 2 1 0
0 Reserved Peripheral Device Type
1 RMB Reserved
2 ISO Version ECMA Version ANSI Version (00)
3 ATAPI Version Response Data Format
4 Additional Length (Number of bytes following this one)
5 Reserved
6 Reserved
7 Reserved
8 Vendor Identification
15
16 Product Identification
31
32 Product Revision Level
35
36 Vendor-specific
55
.....

Рисунок 3. Формат справки, получаемой по команде INQUIRY

Команда READ_CD, которую мы сейчас рассмотрим, выполняет чтение сектора с компакт-диска. Но перед тем, как приступить к изучению примера использования этой команды, ознакомимся с организацией данных на компакт-диске.

Физический формат данных на компакт-диске. Форматы блоков основного канала

Единицей представления данных на компакт-диске является малый кадр, small frame. Малый кадр содержит:

  • 3 байта кода синхронизации;
  • 1 байт данных субканала;
  • 24 байта данных основного канала (две группы по 12 байт);
  • 8 байт помехоустойчивого корректирующего кода (CIRC, Cross Interleaved Read-Solomon Code).

Общая длина данных малого кадра составляет 36 байт.

При записи на компакт-диск данные субканала, основного канала и CIRC кодируются 14-разрядными EFM-кодом (Eight to Fourteen). Эта операция необходима для соблюдения условия, что в последовательном коде данных между двумя соседними единицами должно быть не более 10, но не менее 2 нулей. Размер малого кадра, записанного на компакт-диск, равен 588 бит.

Рисунок 4. Структура кадра на примере Audio-CD

Рисунок 4. Структура кадра на примере Audio-CD

98 последовательно расположенных малых кадров образуют кадр (Frame), или сектор, минимально адресуемую единицу данных на компакт-диске. Один кадр содержит 24 * 98 = 2352 байт данных основного канала и 98 байт субканала (2 байта синхронизации и 96 байт данных). Каждый байт субканала размечен на битовые позиции, и, таким образом, субканал делится еще на 8 субканалов (рис. 5). В одном кадре для каждого из этих субканалов содержится по 96 бит (12 байт) информации.

7 6 5 4 3 2 1 0
P Q R S T U V W

Рисунок 5. Формат байта субканала

Субканал Q Lead-In области компакт-диска хранит данные таблицы содержания, Table of Contents, или TOC.

У компакт-дисков, используемых для хранения аудио-информации, все 2352 байт основного канала заняты аудиоданными. Формат, используемый для записи музыкальных треков на компакт-диск, обозначается CD-DA (CD Digital Audio). Если компакт-диск используется для хранения данных, то блок основного канала начинается с поля синхронизации Sync длиной 12 байт (рис. 6). За полем синхронизации находится заголовок блока данных Header длиной 4 байт. Заголовок блока содержит координаты блока данных в формате MSF (Minute/Second/Frame) и байт режима записи данных Data Mode.

0 1 2 3 4 5 6 7 8 9 10 11
00h FFh FFh FFh FFh FFh FFh FFh FFh FFh FFh 00h

Рисунок 6. Структура поля синхронизации Sync

Биты 0-1 байта Data Mode содержат код режима записи блока данных. Наиболее широкое распространение получили следующие режимы данных:

  • режим данных 1, Yellow Book Mode 1;
  • форма 1 режима данных 2, CD-ROM XA Mode 2 Form 1;
  • форма 2 режима данных 2, CD-ROM XA Mode 2 Form 2.

Форматы блоков основного канала для каждого режима представлен на рис. 7.

  • EDC – Error detection code
  • ECC – Error correction code using CIRC

Вернёмся к рассмотрению команды READ CD. Ее формат представлен на рис. 8.

Рисунок 7. Форматы блоков основного канала

Рисунок 7. Форматы блоков основного канала

READ CD Command

Bit Byte 7 6 5 4 3 2 1 0
0 Operation Code (BEh)
1 Reserved Expected Sector Type Reserved
2 MSB Starting Logical Block Address   LSB
3
4
5
6 MSB Transfer Length LSB
7
8
9 Flag Bits
Synch Field Header(s) Code User Data EDC & ECC Error Flag(s) Reserved
10 Reserved Sub-Channel Data Selection Bits
11 Reserved

Рисунок 8. Формат команды READ CD

Назначение полей командного пакета:

  • Expected Sector Type – определяет тип сектора, который мы хотим считать. Может принимать следующие значения (INF-8020i, таблица 94):
    • 000b – Any type
    • 001b – CD-DA
    • 010b – Mode1
    • 100b – Mode2 Form1
    • 101b – Mode2 Form2
  • Starting Logical Block Address – номер стартового сектора;
  • Transfer Length in Blocks – число считываемых секторов;
  • Flag Bits – это поле определяет, какие данные будут прочитаны из сектора. В таблице 99, стр. 147 спецификации INF-8020i определены допустимые значения этого поля и поля данных, которые будут считаны из сектора (секторов). Например, значение поля Expected Sector Type, равное 0xF8, означает, что из сектора любого типа (Mode1, Mode 2 Form 1, Mode 2 Form 2) будут считаны все данные: поле синхронизации Sync, заголовки Header и Subheader (для Mode 2), поле данных и значения контрольных сумм EDD/ECC. Другими словами, c компакт-диска будет прочитан «сырой» сектор (RAW-сектор);
  • Sub-Channel Data Selection Bits – данное поле определяет необходимость считывания в общем потоке данных содержимое субканалов:
    • 000b – данные из субканалов не передаются;
    • 001b – считываются «сырые» данные субканалов;
    • 010b – данные Q-субканала;
    • 100b – данные R-W субканалов.

Формат данных Q-субканала приведён на рис. 9.

Formatted Q-subcode Data (A Total of 16 Bytes)

Byte  
0 Control (4 M.S. bits), ADR (4 L.S. bits)
1 Track number
2 Index number
3 Min
4 Sec
5 Frame
6 Reserved (00h)
7 AMin
8 Asec
9 AFrame
10 CRC* or 00h (hex)
11 CRC* or 00h (hex)
12 00h (pad)
13 00h (pad)
14 00h (pad)
15 00h (pad)

* CRC is optional

Рисунок 9. Формат данных Q-субканала

Рассмотрим функцию, которая выполняет чтение RAW-сектора с компакт-диска. Входные данные функции – логический номер сектора, содержимое которого мы хотим прочитать.

#define SECT_SIZE 2352 // размер RAW-сектора

int read_cd(__u32 lba)

{

    int i = 0, out_f, ret;

    __u8 cmd_buff[12];

    __u8 buff[SECT_SIZE];

    __u16 data_len = SECT_SIZE, a;

    memset((void *)buff, 0, sizeof(buff));

    memset((void *)cmd_buff, 0, 12);

/* Формируем командный пакет (рис. 8) */

    cmd_buff[0] = 0xBE; // код команды READ CD

    cmd_buff[1] = 0; // считываем сектор любого типа (Any Type)

    cmd_buff[9] = 0xF8; // считываем всю информацию,

                      // находящуюся в секторе

    cmd_buff[8] = 1; // читаем один сектор

/* Заполняем поле Starting Logical Block Address, при этом меняем порядок расположения байт */

    lba = __swab32(lba);

    memcpy((cmd_buff + 2), (void *)&lba, 4);

/* Посылаем устройству командный пакет */

    if(send_packet_data_command(data_len, cmd_buff) < 0) {

           request_sense(); return -1;

}

/* Считываем результат и сохраняем его в файле */

    for(i = 0; i < SECT_SIZE; i += 2) {

           IN_P_W(a, 0x170);

           memcpy((void *)(buff + i), (void *)&a, 2);

    }

    out_f = open("sector", O_CREAT|O_RDWR, 0600);

    write(out_f, buff, sizeof(buff));

    return 0;

}

Если при обращении к устройству произойдет ошибка, то никаких данных мы, соответственно, не получим. Однако у нас есть возможность узнать причину ошибки. Для этого достаточно послать устройству пакетную команду REQUEST SENSE. В ответ устройство выдаст информационный блок sense data (см. рис. 10).

Request Sense Standard Data

Bit Byte 7 6 5 4 3 2 1 0
0 Valid Error Code (70h or 71h)
1 Segment Number (Reserved)
2 Reserved ILI Reserved Sense Key
3 Information
6
7 Additional Sense Length (n - 7)
8 Command Specific Information
11  
12 Additional Sense Code
13 Additional Sense Code Qualifier (Optional)
14 Field Replaceable Unit Code (Optional)
......

Рисунок 10. Блок данных sense data, возвращаемый командой REQUEST SENSE

Три поля данной структуры – Sense Key, Additional Sense Code (ASC), Additional Sense Code Qualifier – позволяют точно установить причину ошибки. Допустимые значения этих полей и их описание ошибочной ситуации приведены в спецификации INF-8020i, таблицы 140 и 141, стр. 183 – 185.

Анализ ошибочной ситуации выполняет функция request_sense():

int request_sense()

{

    int i = 0;

    __u8 cmd_buff[12];

    __u8 sense_buff[14];

    __u16 a;

    memset((void *)cmd_buff, 0, 12);

    memset((void *)sense_buff, 0, 14);

/* Формируем пакетную команду REQUEST SENSE. Из блока sense data считываем первые 14 байт – этого нам хватит,

 * чтобы определить причину ошибки

 */

    cmd_buff[0] = 0x3;

    cmd_buff[4] = 14;

/* Посылаем устройству команду и считываем sense data */

    if(send_packet_data_command(14, cmd_buff) < 0) exit(-1);

    for(i = 0; i < 14; i += 2) {

                 IN_P_W(a, 0x170);

           memcpy((void *)(sense_buff + i), (void *)&a, 2);

    }

    printf("Sense key - 0x%X\n", sense_buff[2] & 0x0F);

    printf("ASC - 0x%X\n", sense_buff[12]);

    printf("ASCQ - 0x%X\n", sense_buff[13]);

    return 0;

}

Полный листинг программы чтения RAW-секторов с компакт-диска находится в файле RAW/read_sector.c. Приведём пример работы этой программы. С компакт-диска считывается сектор номер 1000. В результате работы будет создан файл sector. Посмотрим на первые 16 байт этого файла:

00 FF FF FF FF FF FF FF FF FF FF 00 00 15 25 01

Первые 12 байт – это поле синхронизации Sync-сектора (см. рис. 6). Следующие за ним 3 байта – координаты сектора в формате MSF, значения представлены в BCD-коде. Последний байт содержит значение режима записи данных, Data Mode.

Пересчитаем координаты сектора из MS- формата в LBA по формуле (см. [3, 4]):

LBA = ((Minute * 60 + Second) * 75 + Frame) – 150

В нашем примере, Minute = 00, Second = 15, Frame = 25. Подставив значения в формулу, получаем LBA = 1000. Именно этот сектор мы считывали.

Теперь проверим, как обрабатываются ошибочные ситуации. Удалим из привода компакт-диск и запустим программу на выполнение. В результате программа выдаст следующее:

Sense key = 0x2
ASC = 0x3A
ASCQ = 0x0

За расшифровкой обратимся к таблицам 140 и 141. Значение Sense key, равное 0x2, означает «NOT READY. Indicates that the Device cannot be accessed», ASC=0x3A и ASCQ=0x0 – «MEDIUM NOT PRESENT». Устройство сообщило о том, что компакт-диск в приводе отсутствует.

Теперь попытаемся прочитать сектор, номер которого заведомо превышает допустимое значение. В программе задаем LBA = 10000000 и получаем в результате:

Sense key = 0x5 - ILLEGAL REQUEST
ASC = 0x21 - LOGICAL BLOCK ADDRESS OUT OF RANGE
ASCQ = 0x0

В этом случае устройство сообщает, что логический адрес сектора вышел за пределы допустимого диапазона.

Q-субканал Lead-In области компакт-диска содержит таблицу содержания диска, Table of Contents, или TOC. Это своеобразный аналог таблицы разделов жесткого диска. В TOC хранятся данные о координатах треков и другая служебная информация.

Чтение таблицы содержания диска выполняется командой READ TOC. Формат этой команды представлен на рис. 11.

READ TOC Command

Bit Byte 7 6 5 4 3 2 1 0
0 Operation code (43h)
1 Reserved MSF (Mandatory) Reserved
2 Reserved Format
3 Reserved
4 Reserved
5 Reserved
6 Starting Track / Session Number
7 MSB Allocation Length   LSB
8
9 Format Reserved
10 Reserved
11 Reserved

Рисунок 11. Формат команды READ TOC

Назначение полей командного пакета:

  • MSF – формат адреса блока данных (0 – LBA, 1 – MSF);
  • Format – определяет формат данных, выдаваемых по команде READ TOC:
  • 00b – считываются данные TOC, начиная с трека, номер которого указан в поле Starting Track. Если это поле содержит 0, содержимое TOC выдаётся для всех треков диска, начиная с первого. Если поле Starting Track содержит значение 0xAA, выдаются данные TOC последней Lead-Out области диска;
  • 01b – считывается номер первой сессии, номер последней сессии и номер первого трека последней сессии;
  • 10b – считываются все данные Q-субканала Lead-In областей всех сессий, начиная с сессии, номер которой находится в поле Session Number.

Поле Format девятого байта не используется.

Формат данных TOC для Format = 00b, представлен на рис. 12.

Read TOC Data Format (With Format Field = 00b)

Bit Byte 7 6 5 4 3 2 1 0
0 MSB TOC Data Length   LSB
1
2 First Track Number
3 Last Track Number
     
0 Reserved
1 ADR Control
2 Track Number
3 Reserved
4 MSB Absolute CD-ROM Address LSB
5
6
7

Рисунок 12. Формат данных TOC, Format Field = 00b

Первые четыре байта – это заголовок, содержащий длину данных TOC и номера первого и последнего треков. За заголовком следуют дескрипторы треков. Первым расположен дескриптор трека, номер которого задан в поле Starting Track. Максимальная длина данных TOC, согласно спецификации INF-8020i (рис. 12), составляет 804 байта, или 100 TOC track descriptors.

Формат данных TOC для Format = 10b, приведен на рис.13:

Read TOC Data Format (With Format Field = 10b)

Bit Byte 7 6 5 4 3 2 1 0
0 MSB   LSB
1
2 First Session Number
3 Last Session Number
     
0 Session Number
1 ADR Control
2 TNO (0)
3 Point
4 Byte 3 or Min
5 Byte 4 or Sec
6 Byte 5 or Frame
7 Byte 6 or Zero
8 Byte 7 or PMin
9 Byte 8 or PSec
10 Byte 9 or PFrame

Рисунок 13. Формат данных TOC, Format Field = 10b

Здесь картина аналогичная – первым идёт заголовок размером 4 байта, а за ним расположены записи TOC.

Рассмотрим функцию read_toc(), выполняющую чтение TOC, при условии, что поле Format содержит 00b. Формат дескриптора трека TOC, представленный на рис. 12, описывает структура struct toc:

struct toc {

    __u8 res;

    __u8 adr_cntl;

    __u8 trk_num; // номер трека

    __u8 res1;

    __u32 lba; // адрес сектора

} __attribute__ ((packed)) *t;

int read_toc()

{

#define PACKET_LEN 12 // длина пакетной команды

#define READ_TOC 0x43 // код команды READ TOC

#define MAX_TOC_LEN 804 // максимальная длина данных TOC (Format = 00b)

    int i = 0;

    int total_tracks = 0;

    __u8 cmd_buff[PACKET_LEN];

    __u8 *data_buff;

    __u16 toc_length = 0, a;

/* Формируем пакетную команду. Поле Starting Track содержит 0, по команде READ TOC будет выдана информация

 * обо всех треках диска, начиная с первого

 */

    memset((void *)cmd_buff, 0, PACKET_LEN);

    cmd_buff[0] = READ_TOC;

    cmd_buff[6] = 0;

/* Размер области памяти, выделенной для данных TOC */

    a = MAX_TOC_LEN;

    a = __swab16(a);

    memcpy((void *)(cmd_buff + 7), (void *)&a, 2);

    data_buff = (__u8 *)malloc(MAX_TOC_LEN);

    memset(data_buff, 0, MAX_TOC_LEN);

/* Посылаем устройству пакетную команду */

    if(send_packet_data_command(MAX_TOC_LEN, cmd_buff) < 0)

           return -1;

/* Считываем результат */

    for(i = 0;;i += 2) {

           IN_P_W(a, 0x170);

           // размер данных TOC

           if(i == 0) toc_length = __swab16(a);

           if(i > toc_length) break;

           memcpy((void *)(data_buff + i), (void *)&a, 2);

    }

/* Число треков на диске */

    total_tracks = data_buff[3];

/* Отобразим результат */

    printf("TOC length - %d\n", toc_length);

    printf("First: %d\t", data_buff[2]);

    printf("Last: %d\n", total_tracks);

/* Выделим память и скопируем туда дескрипторы треков */

    t = (struct toc *)malloc(toc_length);

    memcpy((void *)t, (data_buff + 4), toc_length);

    free(data_buff);

/* Отобразим результаты */

    for(i = 0; i < total_tracks; i++)

           printf("track: %d\tlba: %u\n", (i + 1), __swab32((t + i)->lba));

 

    return 0;

}

Полный листинг программы чтения TOC приведен в файле RAW/read_toc1.c.

Рассмотрим ещё один пример чтения TOC. Для хранения содержимого TOC организуем односвязный список структур следующего вида:

struct toc {

    __u8 res;

    __u8 adr_cntl;

    __u8 trk_num;

    __u8 res1;

    __u32 lba;

    struct toc *next; // указатель на следующий элемент списка

};

Здесь struct toc *next – указатель на следующий элемент списка. Формирование этого списка будет выполнять рекурсивная функция read_toc( ):

struct toc * read_toc()

{

#define READ_TOC 0x43

    static int i = 1;

    int n;

    __u8 cmd_buff[12];

    __u8 data_buff[12];

/* При каждом обращении к диску мы считываем 12 байт – 4 байта заголовка и 8 байт дескриптора трека,

 * номер которого задан в поле Starting Track

 */

    __u16 buff_size = 12, a;

    struct toc *t;

/* Если номер трека превысил 0xAA, выполнение функции прекращается */

    if(i > 0xAA) return NULL;

/* Выделяем память для дескриптора трека */

    t = (struct toc *)malloc(sizeof(struct toc));

/* Формируем пакетную команду */

    memset((void *)cmd_buff, 0, 12);

    cmd_buff[0] = READ_TOC;

/* Поле Starting Track содержит номер трека. Этот номер увеличивается при каждом вызове функции */

    cmd_buff[6] = i;

/* Размер считываемых данных */

    buff_size = __swab16(buff_size);

    memcpy((void *)(cmd_buff + 7), (void *)&buff_size, 2);

/* Посылаем устройству пакетную команду */

    if(send_packet_data_command(buff_size, cmd_buff) < 0) {

           printf("Error read TOC\n");

           request_sense();

           exit(-1);

    }

/* Считываем данные – заголовок и дескриптор трека (рис. 12) */

    memset(data_buff, 0, 12);

    for(n = 0; n < 12; n += 2) {

           IN_P_W(a, 0x170);

           memcpy((void *)(data_buff + n), (void *)&a, 2);

    }

/* Отобразим размер данных TOC и номер первого и последнего трека */

    if (i == 1) {

           memcpy((void *)&a, (void *)(data_buff), 2);

           printf("TOC lenght - %d\n", __swab16(a));

           printf("First: %d\t", data_buff[2]);

           printf("Last: %d\n", data_buff[3]);

           max_track_num = data_buff[3];

    }

/* Копируем дескриптор трека в структуру struct toc */

    memcpy((void *)t, (data_buff + 4), 8);

    t->lba = __swab32(t->lba);

/* Считываем дескриптор следующего трека. Если треков больше нет, считываем дескриптор Lead-Out области последней сессии. */

    i += 1;

    if(i == (max_track_num + 1)) i = 0xAA;

    t->next = read_toc();

    return t;

}

Просмотр содержимого TOC выполняет рекурсивная функция view_toc( ):

void view_toc(struct toc *t)

{

    if(t == NULL) return;

    if(t->trk_num == 0xAA) printf("lead out:\t");

    else printf("track: %d\t", t->trk_num);

    printf("lba: %u\n", t->lba);

    view_toc(t->next);

}

Извлечь из сформированного списка дескриптор трека можно при помощи следующей функции:

struct toc_entry * get_toc_entry(int trk_num, struct toc *t)

{

    struct toc_entry *t_entry;

    int i = 1;

    for(;i < trk_num; i++) t = t->next;

    t_entry = (struct toc_entry *) ї

           malloc(sizeof(struct toc_entry));

    t_entry->start_lba = t->lba;

    t_entry->end_lba = t->next->lba;

    return t_entry;

}

Входные параметры функции – номер трека и указатель на начало списка с данными TOC. Результат сохраняется в структуре struct toc_entry следующего вида:

struct toc_entry {

    __u32 start_lba; // стартовый адрес трека

    __u32 end_lba; // конечный адрес трека

};

Полный листинг программы, выполняющей чтение TOC, приведен в файле RAW/read_toc.c.

Если скомпоновать вместе функции read_cd и read_toc, можно написать программу чтения треков с компакт-диска. В файле RAW/read_cdda_track.c находится листинг программы, которая считывает треки с Audio-CD и сохраняет их в файле track.cdr. Детально изучать этот листинг мы не будем, т.к. только что подробно рассмотрели его основные составляющие. Единственное замечание – в отличие от ранее рассмотренной функции read_cd при считывании сектора с аудиодиска соседние байты меняются местами. Это связано с порядком расположения аудиоданных в секторе:

for(i = 0; i < 2352; i += 2) {

    IN_P_W(a, 0x170);

    a = __swab16(a);

    memcpy((void *)(buff + i), (void *)&a, 2);

}

В принципе этого можно и не делать, но тогда нам придется самостоятельно формировать RIFF-заголовок, чтобы получить файл в формате WAV. Лучше возложим эту почётную обязанность на sox. Конвертируем файл в WAV-формат:

# sox track.cdr track.wav

Полученный WAV-файл кодируем в формат Ogg Vorbis:

# oggenc track.wav -b 256 track.ogg

Системный вызов IOCTL

Теперь рассмотрим порядок посылки устройству пакетной команды при помощи системного вызова ioctl. Команда выглядит следующим образом:

ioctl(int fd, CDROM_SEND_PACKET, struct cdrom_generic_command *);

Первый параметр – дескриптор файла устройства. Второй параметр, CDROM_SEND_PACKET – спецификатор, указывающий на необходимость передачи устройству пакетной команды. Третий параметр – структура следующего типа:

/* for CDROM_PACKET_COMMAND ioctl */

struct cdrom_generic_command

{

    unsigned char                     cmd[CDROM_PACKET_SIZE];

    unsigned char                     *buffer;

    unsigned int               buflen;

    int                        stat;

    struct request_sense              *sense;

    unsigned char              data_direction;

    int                        quiet;

    int                        timeout;

    void                       *reserved[1];

};

Эта структура определена в файле <linux/cdrom.h>. Назначение полей структуры:

  • cmd[CDROM_PACKET_SIZE] – 12-байтный командный пакет;
  • buffer – указатель на буфер, куда будут помещены считанные данные. Также в этом буфере хранятся данные, которые будут переданы устройству;
  • buflen – размер передаваемых (принимаемых) данных;
  • sense – структура, содержащая информацию о состоянии устройства (см. команду REQUEST SENSE и рис. 10). Эта структура также определена в файле <linux/cdrom.h>;
  • data_direction – направление обмена данными. Может принимать следующие значения:

#define CGC_DATA_WRITE 1 // передача данных устройству

#define CGC_DATA_READ 2 // приём данных от устройства

#define CGC_DATA_NONE 3 // нет обмена данными

  • timeout – допустимое время выполнения команды.

Рассмотрим примеры программ, выполняющих доступ к ATAPI-устройству при помощи системного вызова IOCTL.

Первый пример – открытие и закрытие лотка CD-ROM (файл IOCTL/open_close.c).

/* Файл open_close.c */

#include <stdio.h>

#include <fcntl.h>

#include <linux/types.h>

#include <linux/cdrom.h>

int main()

{

    int fd;

    struct cdrom_generic_command cgc;

/* Открываем файл устройства */

    fd = open("/dev/cdrom", O_RDONLY|O_NONBLOCK);

    memset((void *)&cgc, 0, sizeof(struct cdrom_generic_command));

/* Ждём готовность устройства к выполнению пакетной команды. Поле data_direction будет содержать CGC_DATA_NONE,

 * т.к. команда не требует передачи данных

 */

    cgc.cmd[0] = GPCMD_TEST_UNIT_READY; // см. <linux/cdrom.h>

    cgc.data_direction = CGC_DATA_NONE;

/* Посылаем устройству команду */

    ioctl(fd, CDROM_SEND_PACKET, &cgc);

/* Формируем и посылаем устройству пакетную команду, выполняющую открытие лотка CD-ROM.

 * Команда не требует передачи данных, поле data_direction содержит CGC_DATA_NONE

 */

    memset((void *)&cgc, 0, sizeof(struct cdrom_generic_command));

    cgc.cmd[0] = GPCMD_START_STOP_UNIT;

    cgc.cmd[4] = 0x2;

    cgc.data_direction = CGC_DATA_NONE;

    ioctl(fd, CDROM_SEND_PACKET, &cgc);

    printf("CD-ROM открыт. Нажмите ENTER для закрытия");

    getchar();

/* Закрываем лоток */

    memset((void *)&cgc, 0, sizeof(struct cdrom_generic_command));

    cgc.cmd[0] = GPCMD_START_STOP_UNIT;

    cgc.cmd[4] = 0x3;

    cgc.data_direction = CGC_DATA_NONE;

    ioctl(fd, CDROM_SEND_PACKET, &cgc);

    printf("CD-ROM закрыт\n");

    return 0;

}

Следующий пример – функция read_cd, выполняющая чтение RAW-сектора, при этом вместе с данными основного канала считываются данные Q-субканала сектора. Формат данных Q-субканала приведен на рис.9, формат команды READ CD – на рис. 8. Входной параметр функции – логический номер сектора.

int read_cd(__u32 lba)

{

    int fd, out_f;

    struct cdrom_generic_command cgc;

    struct request_sense sense;

#define QSCH_LEN 16 // размер данных Q-субканала

/* Буфер для считанных данных */

    __u8 blk_buff[CD_FRAMESIZE_RAW + QSCH_LEN];

/* Открываем файл устройства */

    fd = open("/dev/cdrom", O_RDONLY|O_NONBLOCK);

/* Ожидаем готовность устройства к принятию пакетной команды */

    memset((void *)&cgc, 0, sizeof(struct cdrom_generic_command));

    memset(&sense, 0, sizeof(sense));

    cgc.cmd[0] = GPCMD_TEST_UNIT_READY;

    cgc.data_direction = CGC_DATA_NONE;

    cgc.sense = &sense;

    if(ioctl(fd, CDROM_SEND_PACKET, &cgc) < 0) {

           perror("ioctl");

           printf("Sense key - 0x%02x\n", sense.sense_key);

           printf("ASC - 0x%02x\n", sense.asc);

           printf("ASCQ - 0x%02x\n", sense.ascq);

           return -1;

    }

/* Формируем команду READ CD */

    memset((void *)&cgc, 0, sizeof(struct cdrom_generic_command));

    memset(&sense, 0, sizeof(sense));

    memset(blk_buff, 0, sizeof(blk_buff));

    cgc.cmd[0] = GPCMD_READ_CD;

    cgc.cmd[1] = 0; // считывать сектор любого типа

    cgc.cmd[8] = 1; // считывать один сектор

    cgc.cmd[9] = 0xF8; // из сектора считывать все данные (табл.99 INF-8010i)

    cgc.cmd[10] = 2; // считывать данные Q-субканала в общем потоке данных

    printf("lba - %d\n", lba);

    lba = __swab32(lba);

    memcpy((cgc.cmd + 2), (void *)&lba, 4);

    // направление данных – от устройства

    cgc.data_direction = CGC_DATA_READ;

    cgc.buffer = blk_buff; // указатель на буфер для данных

    cgc.buflen = CD_FRAMESIZE_RAW + QSCH_LEN; // размер буфера

    cgc.sense = &sense;

/* Отправляем устройству команду */

    if(ioctl(fd, CDROM_SEND_PACKET, &cgc) < 0) {

           perror("ioctl");

           printf("Sense key - 0x%02x\n", sense.sense_key);

           printf("ASC - 0x%02x\n", sense.asc);

           printf("ASCQ - 0x%02x\n", sense.ascq);

           return -1;

    }

/* Записываем в файл sector данные основного канала */

    out_f = open("sector", O_CREAT|O_RDWR, 0600);

    write(out_f, blk_buff, CD_FRAMESIZE_RAW);

/* В файл qsch - данные Q-субканала */

    out_f = open("qsch", O_CREAT|O_RDWR, 0600);

    write(out_f, blk_buff + CD_FRAMESIZE_RAW, QSCH_LEN);

/* Отобразим координаты сектора, находящиеся в заголовке Header */

    printf("Minute - %x\n", blk_buff[CD_SYNC_SIZE]);

    printf("Second - %x\n", blk_buff[CD_SYNC_SIZE + 1]);

    printf("Frame - %x\n", blk_buff[CD_SYNC_SIZE + 2]);

    printf("Mode - %d\n", blk_buff[CD_SYNC_SIZE + 3]);

    close(fd); close(out_f);

    return 0;

}

Полный листинг программы чтения RAW-секторов компакт-диска с использованием системного вызова IOCTL находится в файле IOCTL/read_sector.c.

После запуска на выполнение программа сохранит данные основного канала в файле sector, данные Q-субканала – в файле qsch и выведет значения координат сектора в MSF формате, считанные из поля Header. Для сектора номер 1001 получим следующий результат:

Minute - 0
Second - 15
Frame - 26

Результаты чтения данных Q-субканала программой read_sector зависят от используемой модели CD-ROM. Для сравнения посмотрим на результаты чтения приводами TEAC и MITSUMI Q-субканала сектора номер 1001.

TEAC:      41 01 01 00 13 27 00 00 15 26 5D 57 00 00 82 00
MITSUMI:     41 01 01 00 13 27 00 00 15 27 5D 57 00 00 82 00

Байты 7, 8 и 9 содержат координаты сектора в MSF-формате. Если пересчитать этот адрес в формат LBA, то получится, что TEAC считывает «родной» Q-субканал сектора, а MITSUMI читает Q-субканал соседа, 1002 сектора.

Для изучения следующего примера понадобится компакт-диск, на котором записано несколько сессий. Наша задача – прочитать содержимое TOC этого диска при условии, что поле Format содержит значение 10b. Формат данных TOC при Format Field = 10b представлен на рис.13. Следующая структура описывает формат записи TOC при Format = 10b:

struct toc {

    __u8 snum;          // номер сессии

    __u8 ctrl    :4;    // Control

    __u8 adr     :4;    // ADR

    __u8 tno;           // номер трека (всегда 0)

    __u8 point;         // POINT

    __u8 min;           // AMIN

    __u8 sec;           // ASEC

    __u8 frame;         // AFRAME

    __u8 zero;          // 0

    __u8 pmin;          // PMIN

    __u8 psec;          // PSEC

    __u8 pframe;        // PFRAME

} __attribute__ ((packed));

Поле point определяет тип информации, которую содержит запись TOC. Значение этого поля определяет назначение остальных полей, таких как min, sec, frame, pmin, psec, pframe.

Считывание TOC выполняет функция read_toc():

int read_toc()

{

    int i = 1;

    // буфер для хранения результатов чтения TOC

    __u8 *data_buff;

/* Задаем размер области памяти для хранения данных ТОС. Т.к. заранее объем данных нам не известен,

 * то зададим маскимальное значение - 64 Кб

 */

    __u16 buff_size = 0xFFFF;

    __u16 toc_data_length = 0; // длина записей TOC

    __u32 lba;

    int toc_entries = 0; // число записей в TOC

    struct cdrom_generic_command cgc;

    struct request_sense sense;

    struct toc *t;

    memset((void *)&cgc, 0, sizeof(struct cdrom_generic_command));

    memset(&sense, 0, sizeof(sense));

/* Выделяем память для содержимого TOC */

    data_buff = (__u8 *)malloc(buff_size);

    memset(data_buff, 0, buff_size);

/* Формируем пакетную команду для чтения TOC */

    cgc.cmd[0] = GPCMD_READ_TOC_PMA_ATIP;

    cgc.cmd[2] = 2; // поле Format Field = 10b

    cgc.sense = &sense;

    cgc.data_direction = CGC_DATA_READ;

    cgc.buffer = data_buff;

    cgc.buflen = buff_size;

    buff_size = __swab16(buff_size);

    memcpy((void *)(cgc.cmd + 7), (void *)&buff_size, 2);

/* Посылаем командный пакет устройству */

    if(ioctl(fd, CDROM_SEND_PACKET, &cgc) < 0) {

           perror("ioctl");

           printf("Sense key - 0x%02x\n", sense.sense_key);

           printf("ASC - 0x%02x\n", sense.asc);

           printf("ASCQ - 0x%02x\n", sense.ascq);

           return -1;

    }

/* Определяем размер данных TOC */

    memcpy(&toc_data_length, data_buff, 2);

    toc_data_length = __swab16(toc_data_length);

    printf("TOC data length - %d\n", toc_data_length);

/* Вычисляем число записей в содержимом TOC (cм. рис.13) */

    toc_entries = (toc_data_length - 2)/11;

    printf("TOC entries - %d\n", toc_entries);

/* Номер первой и последней сессии */

    printf("First: %d\t", data_buff[2]);

    printf("Last: %d\n", data_buff[3]);

/* Выделяем память для данных TOC, размер этих данных уже точно известен */

    t = (struct toc *)malloc(toc_data_length);

    memset((void *)t, 0, toc_data_length);

/* Копируем данные TOC из буфера data_buff и освобождаем выделенную под него память */

    memcpy((void *)t, data_buff + 4, toc_data_length);

    free(data_buff);

/* Отображаем результаты чтения TOC */

    printf("Entry\tSession\tPoint\tMin\tSec\tFrame\tPMin\tPsec\tPFrame\tLBA\n");

    for(i = 0; i < toc_entries; i++) {

           printf("%d\t", i);

           printf("%d\t", (t + i)->snum);

           printf("%X\t", (t + i)->point);

           printf("%d\t", (t + i)->min);

           printf("%d\t", (t + i)->sec);

           printf("%d\t", (t + i)->frame);

           printf("%d\t", (t + i)->pmin);

           printf("%d\t", (t + i)->psec);

           printf("%d\t", (t + i)->pframe);

/* Пересчитываем координаты из MSF в LBA при помощи макроса MSF2LBA */

    #define MSF2LBA(Min, Sec, Frame) (((Min * 60 + Sec) * 75 + Frame) - 150)

           lba = MSF2LBA((t + i)->pmin, (t + i)->psec, ї

                 (t + i)->pframe);

           printf("%u\n", lba);

    }

    free(t);

    return 0;

}

Полный листинг программы чтения TOC находится в файле IOCTL/read_toc_full.c.

Устанавливаем в устройство компакт-диск, на котором создано 2 сессии, и запускаем на выполнение программу read_cd_full. Вывод направим в файл toc:

# ./read_cd_full > toc

В результате в файле toc будут собраны данные Q-субканалов всех Lead-In областей компакт-диска:

TOC data length - 123
TOC entries - 11
First: 1      Last: 2
Entry      Session      Point    Min    Sec  Frame    PMin   Psec    PFrame      LBA
0      1      A0            0      0      0      1      32      0      6750
1      1      A1            0      0      0      1       0      0      4350
2      1      A2            0      0      0      0      37     50      2675
3      1       1            0      0      0      0       2      0         0
4      1      B0            3      7     50     79      59     74    359849
5      1      C0           70      0    158     97      34     23    438923
6      2      A0            0      0      0      2      32      0     11250
7      2      A1            0      0      0      2       0      0      8850
8      2      A2            0      0      0      3      44     19     16669
9      2      2             0      0      0      3       9     50     14075
10     2      B0            5     14     19     79      59     74     359849

Проведем анализ полученных результатов (таблица 1). Для этого воспользуемся таблицей 131, которая приведена на стр. 175 спецификации INF-8020i (рис. 14).

Lead in Area (TOC), Sub-channel Q formats

Control / ADR TNO Point Min Sec Frame Zero Pmin PSec PFrame
4/6 1 00 01-99 00 (Absolute time is allowed) 00 Start position of track
4/6 1 00 A0 00 (Absolute time is allowed) 00 First Track num Disc Type 00
4/6 1 00 A1 00 (Absolute time is allowed) 00 Last Track num 00 00
4/6 1 00 A2 00 (Absolute time is allowed) 00 Start position of the Lead-out area
4/6 5 00 B0 Start time of next possible program in the Recordable Area of the Hybrid Disc   # of pointers in Mode 5 Maximum start time of the outermost Lead Out area in the Recordable Area of the Hybrid Disc
4/6 5 00 B1 00 00 00 00 # of Skip Interval Pointers (N<=40) # of Skip Track Pointers (N<=21) 00
4/6 5 00 B2-B4 Skip # Skip # Skip # Skip # Skip # Skip # Skip #
4/6 5 00 01-40 Ending time for the interval that Reserved should be skipped Reserved Start time for interval that should be skipped on playback
4/6 5 00 C0 Optimum Recording power Application Code Reserved Reserved Start time of the first Lead In Area of the Hybrid Disc

Рисунок 14. Форматы данных Q-субканала Lead-In области (TOC)

 

Таблица 1. Результаты чтения TOC (Format Field = 10b)

Запись Point Назначение
0 A0 Запись содержит номер первого трека первой сессии. Номер трека находится в поле PMin и равен 1. В поле PSec содержится тип диска. Значение 32 (0x20) соответствует режиму Mode 2
1 A1 Запись содержит номер последнего трека первой сессии. Номер трека находится в поле PMin и равен 1, т.е. в сессии присутствует только один трек
2 A2 Запись содержит стартовую позицию Lead-Out-области сессии в полях PMin/PSec/PFrame
3 1 Трек №1. Находится в первой сессии. Поля PMin/PSec/PFrame содержат координаты начала трека. Отметим, что координаты в формате LBA в TOC не хранятся, пересчет из MSF в LBA мы выполнили самостоятельно при помощи макроса MSF2LBA
4 B0 Запись содержит координаты начала следующей возможной области программ в полях Min/Sec/Frame. Поля PMin/PSec/PFrame содержат максимальное возможное время Lead-Out-области
5 С0 Запись присутствует только в первой Lead-In области. Поле Min содержит значение оптимальной мощности записывающего лазера, поля PMin/PSec/PFrame - координаты начала первой Lead-In-области диска
6 A0 Запись содержит номер первого трека второй сессии. Номер трека находится в поле PMin и равен 2. Треки нумеруются последовательно. Так, если бы в первой сессии было два трека, то номер первого трека второй сессии был бы равен 3. Всего на диске может быть записано 99 треков. В поле PSec содержится тип диска. Значение 32 (0x20) соответствует режиму Mode2
7 A1 Запись содержит номер последнего трека второй сессии. Номер трека находится в поле PMin и равен 2, т.е. во второй сессии также присутствует только один трек
8 A2 Запись содержит стартовую позицию Lead-Out-области сессии в полях PMin/PSec/PFrame
9 2 Трек №2. Находится во второй сессии. Поля PMin/PSec/PFrame содержат координаты начала трека
10 B0 Координаты начала следующей возможной области программ в полях Min/Sec/Frame. Поля PMin/PSec/PFrame содержат максимальное возможное время Lead-Out-области

Следующий пример – чтение данных Q-субканала секторов компакт-диска. Для выполнения этой операции воспользуемся связкой команд SEEK/READ SUB-CHANNEL. Команда SEEK перемещает оптический элемент к нужному сектору, а READ SUB-CHANNEL производит считывание необходимой нам информации из этого сектора. Считывать мы будем данные о текущей позиции оптического элемента, другими словами, координаты сектора, над которым элемент находится. Заодно мы посмотрим на точность позиционирования головки CD-ROM разных моделей – TEAC и MITSUMI.

Формат команды SEEK простой – байты 2-5 содержат координаты сектора в LBA-формате, на который мы хотим позиционировать оптический элемент. Формат команды READ SUB-CHANNEL представлен на рис. 15.

READ SUB-CHANNEL Command

Bit Byte 7 6 5 4 3 2 1 0
0 Operation code (42h)
1 Reserved MSF (Mandatory) Reserved
2 Reserved SubQ (Mandatory) Reserved
3 Sub-channel Data Format
4 Reserved
5 Reserved
6 Track Number
7 MSB Allocation Length   LSB
8
9 Reserved
10 Reserved
11 Reserved

Рисунок 15. Формат команды READ SUB-CHANNEL

Для чтения данных Q-субканала бит SubQ устанавливается в 1. Для чтения текущей позиции оптического элемента устройства поле Sub-channel Data Format должно содержать 01h. В результате выполнения команды устройство вернет блок данных следующего формата:

CD-ROM Current Position Data Format (Format Code 01h)

Bit Byte 7 6 5 4 3 2 1 0
  Sub Channel Data Header  
0 Reserved
1 Audio Status
2 MSB Sub-channel Data Length   LSB
3
  CD-ROM Current Position Data Block  
4 Sub Channel Data Format Code (01h)
5 ADR Control
6 Track Number
7 Index Number
8 MSB Absolute CD-ROM Address   LSB
9
10
11
12 MSB Track Relative CD-ROM Address   LSB

Рисунок 16. Формат блока данных о текущей позиции оптического элемента

Заголовок блока содержит длину считанных из устройства данных. Координаты текущей позиции оптического элемента находятся в поле Absolute CD-ROM Address.

Функция, выполняющая позиционирование оптического элемента устройства на заданный сектор:

int seek(__u32 lba)

{

    struct cdrom_generic_command cgc;

    struct request_sense sense;

    memset((void *)&cgc, 0, sizeof(struct cdrom_generic_command));

    memset(&sense, 0, sizeof(sense));

/* Формируем пакетную команду */

    cgc.cmd[0] = GPCMD_SEEK; // код команды SEEK (0x2B)

    cgc.sense = &sense;

    cgc.data_direction = CGC_DATA_NONE; // нет обмена данными

/* Координаты сектора */

    lba = __swab32(lba);

    memcpy((void *)(cgc.cmd + 2), (void *)&lba, 4);

    if(ioctl(fd, CDROM_SEND_PACKET, &cgc) < 0) {

           perror("ioctl");

           printf("Sense key - 0x%02x\n", sense.sense_key);

           printf("ASC - 0x%02x\n", sense.asc);

           printf("ASCQ - 0x%02x\n", sense.ascq);

           return -1;

    }

    return 0;

}

Следующая структура описывает формат блока данных о текущей позиции оптического элемента (см. рис.16):

struct current_position {

    __u8 dfc;           // Data Format Code

    __u8 ctrl    :4;    // Control

    __u8 adr     :4;    // ADR

    __u8 tno;           // Track number

    __u8 ino;           // Index number

    __u32 a_addr;       // Absolute CD-ROM Address

    __u32 r_addr;       // Track Relative CD-ROM Address

} __attribute__ ((packed)) cur_pos;

Функция, выполняющая чтение данных о текущей позиции оптического элемента устройства:

void read_subch()

{

    __u16 buff_size = 16;      // размер запрашиваемых данных (4 байта заголовка + 12 байт данных)

    __u8 *data_buff;           // указатель на буфер для данных Q-субканала

    __u16 sch_length = 0;      // размер блока данных Q-субканала

    struct cdrom_generic_command cgc;

    struct request_sense sense;

    memset((void *)&cgc, 0, sizeof(struct cdrom_generic_command));

    memset(&sense, 0, sizeof(struct request_sense));

/* Выделяем память */

    data_buff = (__u8 *)malloc(buff_size);

    memset(data_buff, 0, buff_size);

/* Формируем пакетную команду */

    cgc.cmd[0] = GPCMD_READ_SUBCHANNEL; // код команды

    cgc.cmd[2] = 0x40; // бит SUBQ установлен – данные Q-субканала считываются

    cgc.cmd[3] = 1; // читаем данные о текущей позиции

    cgc.sense = &sense;

    cgc.data_direction = CGC_DATA_READ; // направление передачи данных

    cgc.buffer = data_buff;

    cgc.buflen = buff_size;

    buff_size = __swab16(buff_size);

    memcpy((void *)(cgc.cmd + 7), (void *)&buff_size, 2);

    if(ioctl(fd, CDROM_SEND_PACKET, &cgc) < 0) {

           perror("ioctl");

           printf("Sense key - 0x%02x\n", sense.sense_key);

           printf("ASC - 0x%02x\n", sense.asc);

           printf("ASCQ - 0x%02x\n", sense.ascq);

    exit(-1);

}

/* Размер считанных данных */

    memcpy(&sch_length, data_buff + 2, 2);

    sch_length = __swab16(sch_length);

    printf("Sub-channel data length - %d\n", sch_length);

    memset((void *)&cur_pos, 0, 12);

    memcpy((void *)&cur_pos, data_buff + 4, 12);

    printf("Data format code - %d\n", cur_pos.dfc);

    printf("ADR - %d\n", cur_pos.adr);

    printf("Track number - %d\n", cur_pos.tno);

/* Текущая позиция оптического элемента */

    cur_pos.a_addr = __swab32(cur_pos.a_addr);

    printf("Current position - %u\n", cur_pos.a_addr);

/* Освобождаем память и выходим */

    free(data_buff);

    return;

}

Полный листинг программы позиционирования оптического элемента и чтения текущей позиции находится в файле IOCTL/read_subch.c.

Результаты работы программы показали, что разные приводы выполняют команду позиционирования с разной точностью. Из двух имеющихся в моём распоряжении приводов лучший результат показал TEAC – из 30 попыток позиционирования на разные сектора не было зафиксировано ни одного промаха. У MITSUMI картина прямо противоположная: из 30 попыток ни одного точного попадания в заданный сектор, постоянные перелеты. Результаты работы программы для разных типов привода находятся в каталоге IOCTL/RESULT, файлы TEAC и MITSUMI.

Кроме спецификатора CDROM_SEND_PACKET, в файле <linux/cdrom.h> определён целый набор специализированных команд, выполняющих определенное действие и не требующих формирования командного пакета в том объеме, который был нами рассмотрен в предыдущих примерах. Это в значительной степени упрощает работу с устройством. Например, команда для открытия и закрытия лотка CD-ROM выглядит следующим образом:

ioctl(fd, CDROMEJECT); // открыть лоток CD-ROM

ioctl(fd, CDROMCLOSETRAY); // закрыть его

Рассмотрим пример функции, которая считывает TOC компакт-диска.

#define CD_DEVICE "/dev/cdrom"

int read_toc()

{

    int fd, i;

/* Структура struct cdrom_tochdr содержит заголовок содержимого TOC, структура struct cdrom_tocentry –

 * дескриптор трека. Обе структуры определены в файле <linux/cdrom.h>

 */

    struct cdrom_tochdr hdr;

    struct cdrom_tocentry toc;

/* Открываем файл устройства */

    fd = open(CD_DEVICE, O_RDONLY|O_NONBLOCK);

/* Считываем заголовок TOC */

    memset((void *)&hdr, 0, sizeof(struct cdrom_tochdr));

    ioctl(fd, CDROMREADTOCHDR, &hdr);

/* Поле cdth_trk0 структуры hdr содержит номер первого трека, а поле cdth_trk1 – номер последнего трека.

 * Отобразим эти значения

 */

    printf("First: %d\t", hdr.cdth_trk0);

    printf("Last: %d\n", hdr.cdth_trk1);

    #define FIRST hdr.cdth_trk0

    #define LAST hdr.cdth_trk1

/* Определим формат, в котором мы хотим получить координаты трека. Для этого используется поле cdte_format

 * структуры struct cdrom_tocentry

 */

    toc.cdte_format = CDROM_LBA;

/* Задавая в поле cdte_track структуры struct cdrom_tocentry последовательно номера треков от первого до последнего,

 * мы определяем их стартовые координаты в формате LBA

 */

    for(i = FIRST; i <= LAST; i++) {

           toc.cdte_track = i;

           ioctl(fd, CDROMREADTOCENTRY, &toc);

    printf("track: %d\t", i); // номер трека

           // LBA адрес

           printf("lba: %d\n", toc.cdte_addr.lba);

    }

    return 0;

}

Полный листинг программы приведен в файле IOCTL2/get_cd_toc.c.

Рассмотрим программу, которая каждые 2 секунды отслеживает текущее положение оптического элемента, отображает координату текущего сектора (в форматах LBA и MSF) и номер трека.

#define CD_DEVICE "/dev/cdrom"

int main()

{

    int fd, current_track;

/* Координаты текущего сектора находятся в Q-субканале. Для чтения данных Q-субканала используется спецификатор

 * CDROMSUBCHNL, считанные данные помещаются в структуру struct cdrom_subchnl (см. <linux/cdrom.h>)

 */

    struct cdrom_subchnl sc;

/* Открываем файл устройства */

    fd = open(CD_DEVICE, O_RDONLY|O_NONBLOCK);

/* Проверяем тип компакт-диска. Это должен быть Audio-CD */

    if(ioctl(fd, CDROM_DISC_STATUS) != CDS_AUDIO) {

           printf("I need Audio_CD!\n");

           return 0;

    }

/* Считывание координат производим в бесконечном цикле */

    for(;;) {

/* Задаём формат адреса LBA и считываем координаты сектора */

           sc.cdsc_format = CDROM_LBA;

           ioctl(fd, CDROMSUBCHNL, &sc);

           current_track = sc.cdsc_trk;

/* Отображаем данные о текущем треке и координате в формате LBA */

           printf("Track: %d\t", current_track);

           printf("LBA: %d\t", sc.cdsc_absaddr.lba);

/* То же самое - для формата MSF */

           sc.cdsc_format = CDROM_MSF;

           ioctl(fd, CDROMSUBCHNL, &sc);

           printf("MSF: %d %d %d\n", sc.cdsc_absaddr.msf.minute, sc.cdsc_absaddr.msf.second, sc.cdsc_absaddr.msf.frame);

/* Ждем две секунды и повторяем. Выход по Ctrl-C */

           sleep(2);

    }

    return 0;

}

Полный текст программы находится в файле IOCTL2/read_sch.c.

Теперь делаем вот что – в одной консоли запускаем проигрыватель аудиокомпакт-дисков (workbone, например), а в другой – программу read_sch. Через каждые 10-15 секунд воспроизведения переходим к следующему треку на компакте и смотрим на результаты работы read_sch. Получается примерно следующая картина:

Track: 1            LBA: 0          MSF: 0 2 0
Track: 1            LBA: 150        MSF: 0 4 0
Track: 1            LBA: 301        MSF: 0 6  1
Track: 1            LBA: 451        MSF: 0 8  1
Track: 1            LBA: 602        MSF: 0 10 2
Track: 1            LBA: 753        MSF: 0 12 3
Track: 2            LBA: 23962      MSF: 5 21 37
Track: 2            LBA: 24112      MSF: 5 23 37
Track: 2            LBA: 24263      MSF: 5 25 38
Track: 2            LBA: 24414      MSF: 5 27 39
Track: 2            LBA: 24565      MSF: 5 29 40
Track: 3            LBA: 40191      MSF: 8 57 66
Track: 3            LBA: 40342      MSF: 8 59 67
Track: 3            LBA: 40492      MSF: 9 1  67
Track: 3            LBA: 40643      MSF: 9 3  68
Track: 3           LBA: 40794       MSF: 9 5  69

и т. д. Результаты работы программы красноречиво свидетельствуют о постоянном изменении текущей координаты.

И наконец, последний пример, который мы рассмотрим в данной статье, – это еще одна функция, считывающая треки с Audio-CD:

int read_cdda_track()

{

    int fd, out, n;

    __u32 i, start_lba, end_lba;

    __u16 buff[CD_FRAMESIZE_RAW/2];

    struct cdrom_tochdr hdr;

    struct cdrom_tocentry toc;

/* Структура struct cdrom_read_audio используется вместе с IOCTL-командой CDROMREADAUDIO (<linux/cdrom.h>) */

    struct cdrom_read_audio cda;

/* Открываем устройство */

    fd = open(CD_DEVICE, O_RDONLY|O_NONBLOCK);

/* Определяем количество треков на Audio-CD */

    memset((void *)&hdr, 0, sizeof(struct cdrom_tochdr));

    ioctl(fd, CDROMREADTOCHDR, &hdr);

    printf("First: %d\t", hdr.cdth_trk0);

    printf("Last: %d\n", hdr.cdth_trk1);

    #define FIRST hdr.cdth_trk0

    #define LAST hdr.cdth_trk1

/* Вводим номер трека, который мы собираемся считать, и проверяем правильность введенного значения */

    printf("Enter track number: ");

    scanf("%d", &n);

    if((n < 1) || (n > LAST)) {

           printf("Wrong track number\n");

           return -1;

    }

/* Задаем формат адреса */

    toc.cdte_format = CDROM_LBA;

/* Определяем стартовый адрес трека (в LBA формате) */

    toc.cdte_track = n;

    ioctl(fd, CDROMREADTOCENTRY, &toc);

    start_lba = toc.cdte_addr.lba;

/* Конечный адрес трека. Если выбран последний трек на диске, то необходимо определить начало lead-out области.

 * Для этого в поле номера трека указываем 0xAA

 */

    if(n == LAST) toc.cdte_track = CDROM_LEADOUT;

    else toc.cdte_track = n + 1;

    ioctl(fd, CDROMREADTOCENTRY, &toc);

    end_lba = toc.cdte_addr.lba;

/* Считанный трек сохраним в файле track.cdr */

    out = open("track.cdr",O_CREAT|O_RDWR,0600);

/* Заполним поля структуры struct cdrom_read_audio */

    cda.addr_format = CDROM_LBA; // формат адреса

    cda.nframes = 1; // сколько секторов считываем за одно обращение к диску

    cda.buf = (__u8 *)&buff[0]; // адрес буфера для считанных данных

/* Цикл чтения секторов */

    for(i = start_lba; i < end_lba; i++) {

    memset(buff, 0, sizeof(buff));

           cda.addr.lba = i; // адрес считываемого сектора

           printf("lba: %u\n", i);

/* Считываем сектор c аудиоданными и меняем местами соседние байты */

           ioctl(fd, CDROMREADAUDIO, &cda);

           for(n = 0; n < CD_FRAMESIZE_RAW/2; n++)

           buff[n] = __swab16(buff[n]);

/* Сохраняем считанный сектор в файл */

           write(out, (__u8 *)buff, CD_FRAMESIZE_RAW);

    }

    close(fd); close(out);

    return 0;

}

Считанный трек сначала преобразуем в WAV-формат, а затем кодируем в Ogg Vorbis:

# sox track.cdr track.wav

# oggenc track.wav -q 6 track.ogg

Полный листинг программы чтения аудиотреков находится в файле IOCTL2/read_cdda_track.c.

Информационные ресурсы по данной теме:

  1. Кулаков В. Программирование дисковых подсистем. – СПб.: Питер, 2002. – 768 с.:ил.
  2. Гук М. Аппаратные средства IBM PC. Энциклопедия, 2-е изд. – СПб.: Питер, 2003. – 923 с.:ил.
  3. Гук М. Дисковая подсистема ПК. – СПб.: Питер, 2001.
  4. Касперски К. Статья «Лазерный диск с нулевым треком как средство защиты от копирования», http://kpnc.opennet.ru/_ZeroTrack.zip.
  5. Касперски К. Статья «Искажение TOC как средство борьбы с несанкционированным копированием диска», – Журнал «Системный администратор», №09, 2003 г.
  6. Касперски К. Статья «Способы взаимодействия с диском на секторном уровне», http://kpnc.opennet.ru/ATAPI.zip.
  7. Introduction to CD and CD-ROM (with information on CD and CD-ROM formats, complete with diagrams and tables), http://www.disctronics.co.uk/downloads/tech_docs/cdintroduction.pdf.
  8. Descriptions of mastering and replication of CD and DVD discs with diagrams, http://www.disctronics.co.uk/downloads/tech_docs/replication.pdf.
  9. Comprising a comprehensive list of terms and words used in connection with CDs and DVDs and the applications that they support, http://www.disctronics.co.uk/downloads/tech_docs/glossary.pdf.
  10. CD-Recordable FAQ, http://www.cdrfaq.org.
  11. http://www.google.com.
  12. AT Attachment with Packet Interface Extension – (ATA/ATAPI-4), http://www.t13.org.
  13. Information specification INF-8020i Rev 2.6. ATA Packet Interface for CD-ROMs SFF-8020i, http://www.stanford.edu/~csapuntz/specs/INF-8020.PDF.

Комментарии отсутствуют

Добавить комментарий

Комментарии могут оставлять только зарегистрированные пользователи

               Copyright © Системный администратор

Яндекс.Метрика
Tel.: (499) 277-12-41
Fax: (499) 277-12-45
E-mail: sa@samag.ru