USB-polygon-7: Обмен по USB, подготовка устройства
Проект заново
Продолжаем разговор. Попробую завести какой-то обмен между платой и ПК. Подготовлю заготовку контроллерной программы, но не буду продолжать то, что сделано раньше, а начну сначала.
Создаю папку usb-polygon
, копирую в нее demo-проект MassStorage
из ClassDriver
и саму библиотеку LUFA
(второе, в принципе, не обязательно, но я так сделаю), и в MassStorage
создаю git-репозиторий.
Далее правлю Makefile
, оставляя в переменной LUFA_PATH
только одну пару точек (библиотека теперь лежит одним уровнем выше) и закрываю комментариями переменные COMPILER_PATH
и SHELL
— они не нужны, если cygwin уже настроен, как описано в предыдущих выпусках цикла. В результате получаю первую успешную компиляцию проекта с получением hex-файла, который, однако, еще нельзя использовать.
Надо исправить в Makefile
переменную MCU
на at90usb162
и BOARD
на NONE
и начинать править код, потому что компилятор выдаст ошибки. Чтобы получить удачную компиляцию, в коде MassStorage.c
закрываю комментариями функции (и блоки, их содержащие), с Dataflash
в названии, а заодно и с LED
в названии, потому что в моей плате хоть и есть светодиоды, но не такие и не там. Также закрываю Dataflash-функции в модуле Lib/SCSI.c
и директивы #include
с файлами LEDs.c
, Dataflash.c
и Platform.c
в заголовочном файле MassStorage.h
.
«Лампочки и кнопки»
Теперь надо добавить код, который будет выдавать что-то интересное на светодиоды и реагировать на кнопки.
Перед главной функцией (main
) добавляю объявление переменной — контрольного счетчика, который можно будет увеличивать разными способами: в основном цикле безусловно или при обнаружении изменения флага, в прерываниях, еще как-нибудь:
unsigned char cnt = 0; // просто счетчик
В главной функции после GlobalInterruptEnable();
добавляю инициализацию портов, объявление переменных и запуск таймеров:
PORTD = 0x00; // начальное значение - все нули
DDRD = 0xFF; // все линии порта на вывод
PORTC = 0x00; // без "подтяжки" (есть внешняя)
DDRC = 0x00; // все линии порта на ввод
unsigned char cnt_bt = 0; // счетчик нажатий на кнопки
unsigned char mode_out = 0; // режим вывода
unsigned char bt_now = 0; // состояние кнопок
unsigned char bt_old = 0; // состояние кнопок в прошлый раз
/* запуск таймера 0 на период ~0.01 с */
/* (защита от дребезга) */
TCCR0B = 4; /* 1 тик = 0.000032 с */
TCNT0 = 0; /* 256 раз ~ 0.008192 c */
/* запуск таймера 1 на период 0.5 с */
/* (счетчик с полусекундной задержкой) */
TCCR1B = 4; /* 1 тик = 0.000032 с */
TCNT1 = 65536 - 15625;
/* разрешение прерываний таймера 1*/
/*TIMSK1 = (1 << TOIE1);*/
В главном цикле for
после USB_USBTask();
добавляю код:
/* проверка срабатывания таймера без прерываний по флагу */
if ((TIFR1 & 1) == 1) {
TCNT1 = 65536 - 15625; /* перезапуск таймера 1 */
TIFR1 = 1; /* сброс флага таймера 1 */
cnt++; /* инкремент контрольного счетчика по таймеру */
}
/* обработка действий по срабатыванию таймера 0 */
if ((TIFR0 & 1) == 1) {
TCNT0 = 0; /* перезапуск таймера 0 */
TIFR0 = 1; /* сброс флага срабатывания таймера 0 */
/* cnt++;*/ // инкремент счетчика - чтобы что-то изменялось
bt_now = PINC; // считывание порта с кнопками
if (bt_now != bt_old) { // если состояние порта изменилось
if ((bt_now & 0x30) == 0) { // если нажаты сразу две верхние кнопки на разрядах 3 и 4
mode_out++; // циклически изменить режим отображения,
mode_out = mode_out & 3;// которых всего 4 - 0, 1, 2 и 3 (2 разряда по маске)
} else { // в противном случае
if ((bt_now & 0x10) == 0) {cnt_bt++;} // верхняя кнопка увеличивает счет нажатий
if ((bt_now & 0x20) == 0) {cnt_bt--;} // а вторая сверху - уменьшает
}
bt_old = bt_now; // и сохраняем состояние порта для следующей проверки
}
switch (mode_out) {
case 0 :
PORTD = cnt; // просто счетчик
break;
case 1 :
PORTD = bt_now; // состояние кнопок
break;
case 2 :
PORTD = cnt_bt; // счетчик нажатий
break;
default:
PORTD = 0x55; // просто константа
}
}
После главного цикла добавлю на всякий случай незадействованный обработчик прерывания таймера 1:
/* обработчик прерывания таймера, если разрешено, для проверки */
ISR (TIMER1_OVF_vect)
{
TCNT1 = 65536-15625; // перезапуск таймера
cnt++; // инкремент контрольного счетчика
}
Все это компилируется и работает на плате в автономном режиме: светодиоды по умолчанию демонстрируют счетчик, изменяющийся с полусекундным интервалом, одновременное нажатие на верхние две кнопки переключает режим отображения, позволяя показать состояние кнопок, «кнопочный» счетчик (увеличиваемый нажатием на верхнюю кнопку и уменьшаемый нажатием на вторую) или просто константу 0х55 (светодиоды горят через один). Плата при этом видна в системе как флэш-накопитель с файловой системой RAW и емкостью 0. Причем, Windows 7 дает ей букву очень не сразу и настойчиво предлагает отформатировать. Сохраняю очередной коммит в git и добавляю ему тег «v0.1».
Проверка связи
Для начала хочу сделать так, чтобы плата посредством светодиодов показала, что видит обращение к себе со стороны компьютера.
Под объявлением счетчика cnt добавляю объявление еще одного:
unsigned char cnt = 0;
unsigned char cnt_usb = 0;
В переключателе switch (mode_out)
вместо вывода константы делаю вывод этого счетчика:
default:
PORTD = cnt_usb;
И mode_out
при объявлении присваиваю значение 3, чтобы показывало, что там творится при подаче питания с порта. Ну и надо наконец найти подходящее место, в которое вставить инкремент этого счетчика.
Нахожу функцию void MassStorage_Task(void)
, в которой обрабатываются SCSI-команды, подаваемые устройству. Вот сразу после выяснения статуса команды проверяю этот статус и, если команда выполнена успешно, увеличиваю значение счетчика: после
/* Decode the received SCSI command, set returned status code */
CommandStatus.Status = SCSI_DecodeSCSICommand() ? MS_SCSI_COMMAND_Pass : MS_SCSI_COMMAND_Fail;
вставляю
if (CommandStatus.Status == MS_SCSI_COMMAND_Pass){
cnt_usb++;
}
После компиляции и прошивки запускаю выполнение (под Windows 7). Счетчик мгновенно досчитывает до значения 11. Потом потихонечку отсчитывает до 18, а потом резко наматывает довольно много, одновременно на экране ПК появляется предложение отформатировать диск, а счетчик продолжает считать. Буква диска получена. Счетчик еще считает потихонечку. Выделение буквы диска в проводнике вызывает всплеск отсчета, щелчок правой кнопкой — тоже, как и выбор пункта «Свойства» контекстного меню.
А счетчик продолжает считать. Система при подключении опрашивает устройство, чтобы узнать, что это и какие драйвера подключать. Потом пытается прочитать информацию о файловой системе, раз уж это MassStorage. Ну и потом периодически опрашивает, чтобы быть в курсе — не уснуло ли, а то и вовсе отключилось.
Кстати, если сравнивать статус не с MS_SCSI_COMMAND_Pass
, а с MS_SCSI_COMMAND_Fail
, счетчик ведет себя похоже, только не тикает, пока к устройству нет обращения. По-видимому, это связано с нереализованными в устройстве операциями записи и чтения. Точнее, с испорченными — там же были закрыты функции Dataflash.
Сделаю из этого в git отдельную ветку, скажем, test_usb.
Продолжу проверку связи. Условие с приращением счетчика cnt_usb
в MassStorage.c
я закрою, зато добавлю строчку cnt_usb++
в модуле SCSI.c
в функции bool SCSI_DecodeSCSICommand(void)
, в переключателе CommandBlock.SCSICommandData[0]
при выборе SCSI_CMD_INQUIRY'. Ну и добавлю объявление переменной
cnt_usbв
SCSI.h, где-нибудь в самом конце перед
#endif`:
extern unsigned char cnt_usb;
Такая модификация позволяет узнать, когда и сколько раз система выдает устройству запрос INQUIRY
. Оказывается, пять раз после подключения, а потом по разику после запуска программы на С++ из предыдущего выпуска, потому что там есть явная подача этой команды при помощи DeviceIoControl
.
Аналогичным образом — переставляя инкремент счетчика в разные пункты данного switch
— можно исследовать количество и интенсивность подачи разных SCSI-команд системой устройству при подключении или при программном обращении. Это тема забавная, но не очень интересная, потому что такую информацию можно найти не обязательно методами реверс-инжиниринга, достаточно почитать документацию. Но сохраню коммит с подсчетом INQUIRY
в той же ветке и вернусь в ветку master
к тегу «v0.1».
Пиши-читай (заготовка)
На следующем этапе пробую реализовать запись и чтение данных по USB. Понятно, что в прототипе функции чтения и записи реализованы так, чтобы читать и писать в микросхему флэш-памяти, то есть, сделано это все в модуле DataflashManager. Но у меня флэшки нет, и данный модуль мне не нужен. Однако убрать его совсем я пока не могу, потому что в нем есть важные макроопределения, без которых не компилируется проект. Значит, нужно эти самые #define
перенести в нужный модуль (лучше, пожалуй, сделать для этого отдельный, но это попозже, пока для проверки — так) и переписать функции чтения-записи без участия флэш.
Для начала в заголовочном файле SCSI.h, где-нибудь в самом конце перед #endif
, вставляю
/* ----- instead DataflashManager ----- */
/* ------------------------------------ */
для строк, которые должны заменить DataflashManager.h
, и начинаю заполнять этот пробел. Сначала надо вставить то, что в DataflashManager.h
отсутствует, но подключается через #include
. Подключается хитро, с проверкой переменной BOARD
, определенной в makefile, поэтому возьму этот кусок из LUFA\CodeTemplates\DriverStubs\Dataflash.h
:
/* from ..\..\LUFA\CodeTemplates\DriverStubs\Dataflash.h */
/* Public Interface - May be used in end-application: */
/* Macros: */
/** Constant indicating the total number of dataflash ICs mounted on the selected board. */
#define DATAFLASH_TOTALCHIPS 1 // TODO: Replace with the number of Dataflashes on the board, max 2
/** Mask for no dataflash chip selected. */
#define DATAFLASH_NO_CHIP 0
/** Mask for the first dataflash chip selected. */
#define DATAFLASH_CHIP1 1 // TODO: Replace with mask with the pin attached to the first Dataflash /CS set
/** Mask for the second dataflash chip selected. */
#define DATAFLASH_CHIP2 2 // TODO: Replace with mask with the pin attached to the second Dataflash /CS set
/** Internal main memory page size for the board's dataflash ICs. */
#define DATAFLASH_PAGE_SIZE 1024 // TODO: Replace with the page size for the Dataflash ICs
/** Total number of pages inside each of the board's dataflash ICs. */
#define DATAFLASH_PAGES 8192 // TODO: Replace with the total number of pages inside one of the Dataflash ICs
Недостающие значения заполнил чем-то интуитивно похожим на правду, не разбираясь пока в тонкостях использования. Дальше переписываю уже из DataflashManager.h
:
/* from DataflashManager.h */
/* Preprocessor Checks: */
#if (DATAFLASH_PAGE_SIZE % 16)
#error Dataflash page size must be a multiple of 16 bytes.
#endif
/* Defines: */
/** Total number of bytes of the storage medium, comprised of one or more Dataflash ICs. */
#define VIRTUAL_MEMORY_BYTES ((uint32_t)DATAFLASH_PAGES * DATAFLASH_PAGE_SIZE * DATAFLASH_TOTALCHIPS)
/** Block size of the device. This is kept at 512 to remain compatible with the OS despite the underlying
* storage media (Dataflash) using a different native block size. Do not change this value.
*/
#define VIRTUAL_MEMORY_BLOCK_SIZE 512
/** Total number of blocks of the virtual memory for reporting to the host as the device's total capacity. Do not
* change this value; change VIRTUAL_MEMORY_BYTES instead to alter the media size.
*/
#define VIRTUAL_MEMORY_BLOCKS (VIRTUAL_MEMORY_BYTES / VIRTUAL_MEMORY_BLOCK_SIZE)
/** Blocks in each LUN, calculated from the total capacity divided by the total number of Logical Units in the device. */
#define LUN_MEDIA_BLOCKS (VIRTUAL_MEMORY_BLOCKS / TOTAL_LUNS)
/* Function Prototypes: */
void WriteBlocks(const uint32_t BlockAddress,
uint16_t TotalBlocks);
void ReadBlocks(const uint32_t BlockAddress,
uint16_t TotalBlocks);
При этом укорачиваю названия функций чтения и записи. В SCSI.c
вставляю пока пустые определения объявленных в заголовочнике функций:
void WriteBlocks(const uint32_t BlockAddress,
uint16_t TotalBlocks)
{
}
void ReadBlocks(const uint32_t BlockAddress,
uint16_t TotalBlocks)
{
}
После этого в функции SCSI_Command_ReadWrite_10
раскомментирую блок if (IsDataRead == DATA_READ)
и в нем тоже укорачиваю названия функций чтения и записи, чтобы соответствовали. Теперь можно закрыть пока что комментариями
// #include "DataflashManager.h"
в заголовочниках SCSI.h
и MassStorage.h
. Теперь make clean
, make
и проверка работоспособности — прошивка и созерцание диодиков. Работает, как раньше, но без модуля DataflashManager
. Надо добавить коммит git.
Правда, совсем стирать этот модуль рано, ведь функции чтения и записи еще пустые, а для их реализации надо опираться на имеющиеся.
Чтение и запись мимо flash-памяти
Из модуля DataflashManager
беру тела функций DataflashManager_ReadBlocks
и DataflashManager_WriteBlocks
для ReadBlocks
и WriteBlocks
соответственно, стираю оттуда все, связанное с flash-памятью, добавляю по паре собственных переменных и модифицирую получение данных из буфера конечной точки. Получается следующее:
/** Writes blocks (OS blocks, not Dataflash pages) to the storage medium, the board Dataflash IC(s), from
* the pre-selected data OUT endpoint. This routine reads in OS sized blocks from the endpoint and writes
* them to the Dataflash in Dataflash page sized blocks.
*
* \param[in] BlockAddress Data block starting address for the write sequence
* \param[in] TotalBlocks Number of blocks of data to write
*/
void WriteBlocks(const uint32_t BlockAddress,
uint16_t TotalBlocks)
{
uint16_t CurrDFPage = ((BlockAddress * VIRTUAL_MEMORY_BLOCK_SIZE) / DATAFLASH_PAGE_SIZE);
uint16_t CurrDFPageByte = ((BlockAddress * VIRTUAL_MEMORY_BLOCK_SIZE) % DATAFLASH_PAGE_SIZE);
uint8_t CurrDFPageByteDiv16 = (CurrDFPageByte >> 4);
uint8_t data_8[16];
bool first = true;
/* Wait until endpoint is ready before continuing */
if (Endpoint_WaitUntilReady())
return;
while (TotalBlocks)
{
uint8_t BytesInBlockDiv16 = 0;
/* Write an endpoint packet sized data block to the Dataflash */
while (BytesInBlockDiv16 < (VIRTUAL_MEMORY_BLOCK_SIZE >> 4))
{
/* Check if the endpoint is currently empty */
if (!(Endpoint_IsReadWriteAllowed()))
{
/* Clear the current endpoint bank */
Endpoint_ClearOUT();
/* Wait until the host has sent another packet */
if (Endpoint_WaitUntilReady())
return;
}
/* Check if end of Dataflash page reached */
if (CurrDFPageByteDiv16 == (DATAFLASH_PAGE_SIZE >> 4))
{
/* Reset the Dataflash buffer counter, increment the page counter */
CurrDFPageByteDiv16 = 0;
CurrDFPage++;
}
/* Write one 16-byte chunk of data to the Dataflash */
data_8[0] = Endpoint_Read_8();
data_8[1] = Endpoint_Read_8();
data_8[2] = Endpoint_Read_8();
data_8[3] = Endpoint_Read_8();
data_8[4] = Endpoint_Read_8();
data_8[5] = Endpoint_Read_8();
data_8[6] = Endpoint_Read_8();
data_8[7] = Endpoint_Read_8();
data_8[8] = Endpoint_Read_8();
data_8[9] = Endpoint_Read_8();
data_8[10] = Endpoint_Read_8();
data_8[11] = Endpoint_Read_8();
data_8[12] = Endpoint_Read_8();
data_8[13] = Endpoint_Read_8();
data_8[14] = Endpoint_Read_8();
data_8[15] = Endpoint_Read_8();
if (first) {
first = false;
data_PC = data_8[0];
}
/* Increment the Dataflash page 16 byte block counter */
CurrDFPageByteDiv16++;
/* Increment the block 16 byte block counter */
BytesInBlockDiv16++;
/* Check if the current command is being aborted by the host */
if (IsMassStoreReset)
return;
}
/* Decrement the blocks remaining counter */
TotalBlocks--;
}
/* If the endpoint is empty, clear it ready for the next packet from the host */
if (!(Endpoint_IsReadWriteAllowed()))
Endpoint_ClearOUT();
}
/** Reads blocks (OS blocks, not Dataflash pages) from the storage medium, the board Dataflash IC(s), into
* the pre-selected data IN endpoint. This routine reads in Dataflash page sized blocks from the Dataflash
* and writes them in OS sized blocks to the endpoint.
*
* \param[in] BlockAddress Data block starting address for the read sequence
* \param[in] TotalBlocks Number of blocks of data to read
*/
void ReadBlocks(const uint32_t BlockAddress,
uint16_t TotalBlocks)
{
uint16_t CurrDFPage = ((BlockAddress * VIRTUAL_MEMORY_BLOCK_SIZE) / DATAFLASH_PAGE_SIZE);
uint16_t CurrDFPageByte = ((BlockAddress * VIRTUAL_MEMORY_BLOCK_SIZE) % DATAFLASH_PAGE_SIZE);
uint8_t CurrDFPageByteDiv16 = (CurrDFPageByte >> 4);
uint8_t data_8[16];
uint8_t *pdata = data_8;
/* Wait until endpoint is ready before continuing */
if (Endpoint_WaitUntilReady())
return;
while (TotalBlocks)
{
uint8_t BytesInBlockDiv16 = 0;
/* Read an endpoint packet sized data block from the Dataflash */
while (BytesInBlockDiv16 < (VIRTUAL_MEMORY_BLOCK_SIZE >> 4))
{
/* Check if the endpoint is currently full */
if (!(Endpoint_IsReadWriteAllowed()))
{
/* Clear the endpoint bank to send its contents to the host */
Endpoint_ClearIN();
/* Wait until the endpoint is ready for more data */
if (Endpoint_WaitUntilReady())
return;
}
/* Check if end of Dataflash page reached */
if (CurrDFPageByteDiv16 == (DATAFLASH_PAGE_SIZE >> 4))
{
/* Reset the Dataflash buffer counter, increment the page counter */
CurrDFPageByteDiv16 = 0;
CurrDFPage++;
}
data_8[0] = data_device;
/* Read one 16-byte chunk of data from the Dataflash */
Endpoint_Write_8(pdata[0]);
Endpoint_Write_8(pdata[1]);
Endpoint_Write_8(pdata[2]);
Endpoint_Write_8(pdata[3]);
Endpoint_Write_8(pdata[4]);
Endpoint_Write_8(pdata[5]);
Endpoint_Write_8(pdata[6]);
Endpoint_Write_8(pdata[7]);
Endpoint_Write_8(pdata[8]);
Endpoint_Write_8(pdata[9]);
Endpoint_Write_8(pdata[10]);
Endpoint_Write_8(pdata[11]);
Endpoint_Write_8(pdata[12]);
Endpoint_Write_8(pdata[13]);
Endpoint_Write_8(pdata[14]);
Endpoint_Write_8(pdata[15]);
/* Increment the Dataflash page 16 byte block counter */
CurrDFPageByteDiv16++;
/* Increment the block 16 byte block counter */
BytesInBlockDiv16++;
/* Check if the current command is being aborted by the host */
if (IsMassStoreReset)
return;
}
/* Decrement the blocks remaining counter */
TotalBlocks--;
}
/* If the endpoint is full, send its contents to the host */
if (!(Endpoint_IsReadWriteAllowed()))
Endpoint_ClearIN();
}
data_8
— это массив для обмена данными с буфером конечной точки, data_PC
и data_device
будут контрольными переменными для их отображения на светодиодах и в консоли ПК соответственно, а first
нужна для того, чтобы в контрольную переменную попал первый байт передаваемого из ПК массива и не перезатерся при дальнейшей передаче 16-байтных кусков. Контрольные переменные надо не забыть объявить в SCSI.h
:
/* variables */
extern uint8_t data_PC;
extern uint8_t data_device;
А также их надо определить и использовать в MassStorage.c
:
:cpp
uint8_t data_PC;
uint8_t data_device;
перед главной функцией, data_device = cnt_bt;
перед switch (mode_out) {
и PORTD = data_PC;
вместо PORTD = 0x55
.
Компиляция, прошивка, проверка. Теперь код для ПК, написанный несколько ранее на С++, работает ожидаемо: в консоли пишет data_2 =
и число, соответствующее счетчику cnt_bt
на устройстве (увеличивается нажатием верхней кнопки, уменьшается второй кнопкой, отображается на светодиодах в «режиме 2»), а на светодиодах в «режиме 3» вместо константы 0x55 отображается то, что указано в коде С++ в нулевом элементе массива q.
И буква устройству в Windows 7 стала выделяться гораздо быстрее — практически сразу после подключения.
Это изменения, достойные новой версии. Коммит с тегом «v0.2»
Еще немного о взаимодействии с ПК
Вернусь к коду на С++, написанному для ПК. В операциях чтения и записи, реализованных через DeviceIoControl
, размер передаваемых данных указывается дважды: в поле myspti.t_spti.DataTransferLength
и в 7–8 байтах myspti.t_spti.Cdb
, причем, в первом случае — в байтах, а во втором — в логических блоках. Насколько я разобрался, размер логического блока соответствует VIRTUAL_MEMORY_BLOCK_SIZE
на устройстве, то есть 512 байтов. Так вот, эти два размера в myspti
должны соответствовать друг другу, иначе могут быть ошибки. Например, можно поставить q1 = 512
и myspti.t_spti.Cdb[8] = 0x01
и будет работать, а q1 = 1024
и myspti.t_spti.Cdb[8] = 0x01
вызовет ошибку.
А теперь я закрою эти две сложные конструкции чтения и записи с заполнением структур myspti
и вызовом DeviceIoControl
блочным комментарием, а вмето них, чуть выше их, напишу следующее:
result = WriteFile(hDevice, q, q1, &q2, NULL);
if (result==0) {
OutFormatMsg("ReadFile Error");
} else {
_tprintf("WriteFile done\n");
_tprintf("len = %lu\n", q2);
}
result = ReadFile(hDevice, q, q1, &q2, NULL);
if (result==0) {
OutFormatMsg("ReadFile Error");
} else {
_tprintf("ReadFile done\n");
_tprintf("data_2 = %x\n", q[0]);
_tprintf("len = %lu\n", q2);
}
То есть, воспользуюсь для чтения и записи специально предназначенными для этого функциями. Компилирую, прошиваю, проверяю.
Работает! Ура? Вроде, ура. Теперь уже можно использовать разработанное устройство для общения с ПК — передавать данные в обе стороны и как-то их использовать.
Причем, можно использовать не только данные, но и адрес. То есть, устройство, получив данные, может проанализировать адрес, по которому была команда их записать (или, получив команду чтения, проанализировать адрес, с которого данные затребованы) и выполнить определенные действия в зависимости от этого адреса. Сам адрес-то фиктивный, никакой памяти, к которой относится адресация, на плате нет. При использовании ReadFile/WriteFile
можно управлять адресом c ПК при помощи предварительно выполненной функции SetFilePointer(hDevice, 0, NULL, FILE_BEGIN);
, а в программе устройства анализировать BlockAddress
.
Пожалуй, на этом надо заканчивать, и так слишком много получилось для одного выпуска. Продолжение следует…