image




Моя работа связана с программированием микроконтроллеров, в частности STM32. Долгое время для работы с периферией я использовала STM32 Standard Peripheral Library, так как она предоставляется производителем и, соответственно, является наиболее полной. Однако работать с ней крайне неудобно: инициализирующие структуры зачастую избыточны, в функциях черт ногу сломит, в общем, очень скоро появляется непреодолимое желание слезть с этой библиотеки и перейти на что-нибудь более аккуратное, грамотно спроектированное и написанное «чистым кодом».

После долгих поисков была обнаружена open source библиотека libopencm3, которая отвечала всем требованиям. Отзывы о ней были положительные и работать с ней оказалось максимально приятно.

Одной из последних задач на работе было поднять USB MSD. Для решения задачи использовалась отладочная плата STM32F4-discovery и вот этот пример. Пример не завелся. Проблем было две:
1. Было невозможно зайти на диск и прочитать находящийся там файл.
2. Распознавание устройства как дискового занимало более 2-х минут.

Все это было связано с наличием нескольких багов в файле usb_msc.c. Таким образом, в данной статье я расскажу о том, как исправить эти ошибки и продолжать с удовольствием пользоваться библиотекой libopencm3.


Решение проблемы №1:
Суть ошибки в том, что когда устройство получает запрос на запись, оно правильно его обрабатывает, однако не посылает обратно статус обработки запроса CSW (Command Status Wrapper). Таким образом, usb хост (в нашем случае, это наш ПК) уходит в бесконечное ожидание ответа на запрос, все виснет, глючит, умирает до тех пор, пока не отсоединишь устройство=)

* Более подробно ознакомиться с Mass Storage Bulk-Only or CBI Transport Specification можно здесь.

Поэтому находим функцию msc_data_rx_cb в файле usb_msc.c и приводим ее к следующему виду:
Добавленный кусок кода находится между слешами
static void msc_data_rx_cb(usbd_device *usbd_dev, uint8_t ep)
{
	usbd_mass_storage *ms;
	struct usb_msc_trans *trans;
	int len, max_len, left;
	void *p;

	ms = &_mass_storage;
	trans = &ms->trans;

	/* RX only */
	left = sizeof(struct usb_msc_cbw) - trans->cbw_cnt;
	if (0 < left) {
		max_len = MIN(ms->ep_out_size, left);
		p = &trans->cbw.buf[0x1ff & trans->cbw_cnt];
		len = usbd_ep_read_packet(usbd_dev, ep, p, max_len);
		trans->cbw_cnt += len;

		if (sizeof(struct usb_msc_cbw) == trans->cbw_cnt) {
			scsi_command(ms, trans, EVENT_CBW_VALID);
			if (trans->byte_count < trans->bytes_to_read) {
				/* We must wait until there is something to
				 * read again. */
				return;
			}
		}
	}

	if (trans->byte_count < trans->bytes_to_read) {
		if (0 < trans->block_count) {
			if ((0 == trans->byte_count) && (NULL != ms->lock)) {
				(*ms->lock)();
			}
		}

		left = trans->bytes_to_read - trans->byte_count;
		max_len = MIN(ms->ep_out_size, left);
		p = &trans->msd_buf[0x1ff & trans->byte_count];
		len = usbd_ep_read_packet(usbd_dev, ep, p, max_len);
		trans->byte_count += len;

		if (0 < trans->block_count) {
			if (0 == (0x1ff & trans->byte_count)) {
				uint32_t lba;

				lba = trans->lba_start + trans->current_block;
				if (0 != (*ms->write_block)(lba,
							    trans->msd_buf)) {
					/* Error */
				}
				trans->current_block++;
			}
		}
/////////////////////ADD THIS//////////////////////////////////////////////////////////////////////////////////
		if (false == trans->csw_valid) {
			scsi_command(ms, trans, EVENT_NEED_STATUS);
			trans->csw_valid = true;
		}

		left = sizeof(struct usb_msc_csw) - trans->csw_sent;
		if (0 < left) {
			max_len = MIN(ms->ep_out_size, left);
			p = &trans->csw.buf[trans->csw_sent];
			len = usbd_ep_write_packet(usbd_dev, ms->ep_in, p,
					max_len);
			trans->csw_sent += len;
		}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	} else if (trans->byte_count < trans->bytes_to_write) {
		if (0 < trans->block_count) {
			if ((0 == trans->byte_count) && (NULL != ms->lock)) {
				(*ms->lock)();
			}

			if (0 == (0x1ff & trans->byte_count)) {
				uint32_t lba;

				lba = trans->lba_start + trans->current_block;
				if (0 != (*ms->read_block)(lba,
							   trans->msd_buf)) {
					/* Error */
				}
				trans->current_block++;
			}
		}

		left = trans->bytes_to_write - trans->byte_count;
		max_len = MIN(ms->ep_out_size, left);
		p = &trans->msd_buf[0x1ff & trans->byte_count];
		len = usbd_ep_write_packet(usbd_dev, ms->ep_in, p, max_len);
		trans->byte_count += len;
	} else {
		if (0 < trans->block_count) {
			if (trans->current_block == trans->block_count) {
				uint32_t lba;

				lba = trans->lba_start + trans->current_block;
				if (0 != (*ms->write_block)(lba,
							    trans->msd_buf)) {
					/* Error */
				}

				trans->current_block = 0;
				if (NULL != ms->unlock) {
					(*ms->unlock)();
				}
			}
		}
		if (false == trans->csw_valid) {
			scsi_command(ms, trans, EVENT_NEED_STATUS);
			trans->csw_valid = true;
		}

		left = sizeof(struct usb_msc_csw) - trans->csw_sent;
		if (0 < left) {
			max_len = MIN(ms->ep_out_size, left);
			p = &trans->csw.buf[trans->csw_sent];
			len = usbd_ep_write_packet(usbd_dev, ms->ep_in, p,
						   max_len);
			trans->csw_sent += len;
		}
	}
}



Ура, теперь мы можем зайти на диск прочитать файл!

Решение проблемы №2:
Суть этой проблемы в том, что во все том же файле usb_msc.с не реализованы две SCSI команды. Это было выявлено с помощью очень полезной программы usblyser, которая позволяет в удобном виде просматривать обмен посылками между usb устройством и usb хостом.

Итак, во-первых, хост не получает ответа на команду READ_FORMAT_CAPACITIES. Следовательно, добавляем в файл usb_msc.с функцию scsi_read_format_capacities и приводим функцию scsi_command к следующему виду:
Посмотреть код
static void scsi_read_format_capacities(usbd_mass_storage *ms, struct usb_msc_trans *trans, enum trans_event event)
{
	if (EVENT_CBW_VALID == event) {

		trans->msd_buf[3] = 0x08;
		trans->msd_buf[4] = ms->block_count >> 24;
		trans->msd_buf[5] = 0xff & (ms->block_count >> 16);
		trans->msd_buf[6] = 0xff & (ms->block_count >> 8);
		trans->msd_buf[7] = 0xff & ms->block_count;

		trans->msd_buf[8] =  0x02;
		trans->msd_buf[9] = 0;
		trans->msd_buf[10] = 0x02;
		trans->msd_buf[11] = 0;
		trans->bytes_to_write = 9;
		set_sbc_status_good(ms);
	}
}

static void scsi_command(usbd_mass_storage *ms, struct usb_msc_trans *trans, enum trans_event event)
{
	if (EVENT_CBW_VALID == event) {
		/* Setup the default success */
		trans->csw_sent = 0;
		trans->csw.csw.dCSWSignature = CSW_SIGNATURE;
		trans->csw.csw.dCSWTag = trans->cbw.cbw.dCBWTag;
		trans->csw.csw.dCSWDataResidue = 0;
		trans->csw.csw.bCSWStatus = CSW_STATUS_SUCCESS;

		trans->bytes_to_write = 0;
		trans->bytes_to_read = 0;
		trans->byte_count = 0;
	}

	switch (trans->cbw.cbw.CBWCB[0]) {
	case SCSI_TEST_UNIT_READY:
	case SCSI_SEND_DIAGNOSTIC:
		/* Do nothing, just send the success. */
		set_sbc_status_good(ms);
		break;
	case SCSI_FORMAT_UNIT:
		scsi_format_unit(ms, trans, event);
		break;
	case SCSI_REQUEST_SENSE:
		scsi_request_sense(ms, trans, event);
		break;
	case SCSI_MODE_SENSE_6:
		scsi_mode_sense_6(ms, trans, event);
		break;
	case SCSI_READ_6:
		scsi_read_6(ms, trans, event);
		break;
	case SCSI_INQUIRY:
		scsi_inquiry(ms, trans, event);
		break;
	case SCSI_READ_CAPACITY:
		scsi_read_capacity(ms, trans, event);
		break;
	case SCSI_READ_10:
		scsi_read_10(ms, trans, event);
		break;
	case SCSI_WRITE_6:
		scsi_write_6(ms, trans, event);
		break;
	case SCSI_WRITE_10:
		scsi_write_10(ms, trans, event);
		break;
//////////////////ADD THIS///////////////////////////////////////////////////////////////////////////////////////////////////
	case SCSI_READ_FORMAT_CAPACITIES:
		scsi_read_format_capacities(ms, trans, event);
	 	break;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	default:
		set_sbc_status(ms, SBC_SENSE_KEY_ILLEGAL_REQUEST,
					SBC_ASC_INVALID_COMMAND_OPERATION_CODE,
					SBC_ASCQ_NA);

		trans->bytes_to_write = 0;
		trans->bytes_to_read = 0;
		trans->csw.csw.bCSWStatus = CSW_STATUS_FAILED;
		break;
	}
}




Во-вторых, хост не получает ответа на команду INQUIRY (SERIAL NUMBER). Для исправления данной ошибки необходимо создать массив _spc3_inquiry_sn_response и привести функцию scsi_inquiry к следующему виду:
Посмотреть код
static const uint8_t _spc3_inquiry_sn_response[20] = {
		0x00,
 		0x80,
 		0x00,
 		0x10, //количество символов в серийном номере. в нашем случае их 16 (third pin 123456)
 		't','h','i','r','d',' ','p','i','n',' ','1','2','3','4','5','6'
 };

static void scsi_inquiry(usbd_mass_storage *ms, struct usb_msc_trans *trans, enum trans_event event)
{
	if (EVENT_CBW_VALID == event) {
		uint8_t evpd;
		uint8_t *buf;

		buf = get_cbw_buf(trans);
		evpd = 1 & buf[1];

		if (evpd == 0) {
			size_t len;
			trans->bytes_to_write = sizeof(_spc3_inquiry_response);
			memcpy(trans->msd_buf, _spc3_inquiry_response,
			       sizeof(_spc3_inquiry_response));

			len = strlen(ms->vendor_id);
			len = MIN(len, 8);
			memcpy(&trans->msd_buf[8], ms->vendor_id, len);

			len = strlen(ms->product_id);
			len = MIN(len, 16);
			memcpy(&trans->msd_buf[16], ms->product_id, len);

			len = strlen(ms->product_revision_level);
			len = MIN(len, 4);
			memcpy(&trans->msd_buf[32], ms->product_revision_level,
			       len);

			trans->csw.csw.dCSWDataResidue =
				sizeof(_spc3_inquiry_response);

			set_sbc_status_good(ms);
		}
/////////////////////////////////ADD THIS/////////////////////////////////////////////////////////////////////////////
		else if (evpd == 1) {
			trans->bytes_to_write = sizeof(_spc3_inquiry_sn_response);
			memcpy(trans->msd_buf, _spc3_inquiry_sn_response,
					sizeof(_spc3_inquiry_sn_response));
			trans->csw.csw.dCSWDataResidue =
					sizeof(_spc3_inquiry_sn_response);

			set_sbc_status_good(ms);
		}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		else {
			/* TODO: Add VPD 0x83 support */
			/* TODO: Add VPD 0x00 support */
		}
	}
}



* Более подробно ознакомиться сo SCSI командами можно опять-таки здесь.

После всех этих хирургических вмешательств библиотеку нужно пересобрать, предварительно выполнив команду make clean.

В скором времени я планирую сделать pull request в репозиторий с libopencm3, однако остается только предполагать, как скоро владельцы внесут эти изменения в библиотеку, а работать тем временем нам всем нужно здесь и сейчас.

Очень надеюсь, что хоть кого-нибудь эта статья избавит от лишней головной боли и будет полезной.

P.S.: Здесь лежит наш корпоративный fork библиотеки с уже внесенными изменениями, если лениво копаться самому.
Поделиться с друзьями
-->

Комментарии (21)


  1. Godless
    06.07.2016 21:42
    +12

    не знаю как остальным, но окончания *ла в глаголах прошедшего времени, очень радуют глаз 8)


    1. yurakostin
      07.07.2016 21:22

      Не могу не согласиться. Для меня это каждый раз как бальзам.


  1. UA3MQJ
    06.07.2016 21:56

    Подскажите, хочу на stm32 сделать usb midi устройство. Может ли эта библиотека помочь или идти прямым путем через stm32cube?


    1. Mirn
      06.07.2016 22:01
      +1

      не знаю как миди, но пытался сделать через stm32cube CDC виртуальный комп порт, всё вродебы хорошо, и скорости добился и приёма и передачи… но вот когда приём идёт независимо от передачи (режим не запрос-ответ, а режим шлём данные одни, получаем данные другие и шлём и получаем независимо). То в таком случае усб зависало — не приходило прерывание что данные приняты устройством ИЗ хоста. Я всё попробовал сделать и обвесил логами в озу всю либу, и изучил её вдоль и поперёк, и проверил регистры состояний, и переписал функцию-бога которая разруливает прерываниями. всё тчетно — устройство просто застопаривается, перестаёт идти и приём и передача. Причём эта бага явно на низком уровне работы с аппаратным контроллером USB на СТМ32Ф4.


      1. PKav
        07.07.2016 16:53

        А если попробовать использовать для приема один USART, а для передачи другой? Как минимум IRQ_Handler на прием и передачу будет разный. Просто скоро потребуется подобное решение сделать, обдумывал как реализовать и наткнулся на ваш комментарий.


        1. Mirn
          07.07.2016 19:51

          я могу выложить свои наработки на гитхаб. Там:
          1. взяты кубовые либы месячной давности (в конце мая разбирался) для Эклипса (мейкфайл тоже пашет)
          2. всё собрано под STM32F4 value line discovery, 168мгц
          3. Сделан полноценный принтф в фифо очередь в память (64к), чтоб не тормозила усб либа и чтоб из таймаутов не вывалится.
          4. Эта очередь выводит в уарт2 данные по 500кбод без чётности, использовал CP2103
          5. на базе этого принтф сделаны очень приятные глазу и понятные логи по кубовой усб либе. Есть возможность отключения: чтоб наваливало от души.
          6. настроен CDC порт на максимальную для усб скорость приёма (800-900кбайт в сек) и передачи, просто приём с контролем целостности тестовых данных и отправка обратно в режиме эха (вроде, непомню).

          Интересует?
          С Вас поделится опытом если решите баг ну и статья на хабр была бы очень большым плюсом в карму, ну и за мной тоже не заржавеет.
          Я же возьмусь за него может через месяц, может через полгода — работы много пока.


    1. Gho_st
      06.07.2016 23:06
      +1

      Поищи на github'e midi over usb проекты на stm32f4. Там есть абсолютно точно.

      вот я какой-то из них пробовал.


    1. ana_lazareva
      06.07.2016 23:08
      +1

      Я полагаю, что не только поможет, но и скорее всего позволит сделать это быстрее, чем с помощью stm32cube. Мне, например, нужно было сделать устройство, являющееся хостом и составным (HID и MSD) устройством одновременно и у меня ушло на это всего 7 рабочих дней из которых 2 дня я разбиралась с тем, почему у меня mass storage не работает.
      Здесь https://github.com/libopencm3/libopencm3-examples можно ознакомиться с примерами реализации usb midi, понять как все устроено и адаптировать под свои нужды. Не переживайте, если пример будет не для того семейства контроллеров STM32, с которым работаете, реализация usb в библиотеке довольно сильно развязана от таких зависимостей.
      В этом документе http://sdphca.ucsd.edu/lab_equip_manuals/usb_20.pdf в главе 9 подробно описан USB DEVICE FRAMEWORK. Он поможет правильно описать все необходимые дескрипторы, понять какие запросы могут приходить от хоста и что на них нужно отвечать.


      1. UA3MQJ
        07.07.2016 14:34

        Проект собираете под win или linux?


        1. ana_lazareva
          07.07.2016 14:55

          под win, но на linux тоже запускали.


          1. UA3MQJ
            07.07.2016 15:11

            Тогда прошу минигайд по настройке тулчейна :)


            1. ana_lazareva
              07.07.2016 16:55
              +2

              Я работаю в Eclipse. Вот сайт GNU ARM Eclipse, там все супер подробно описано в разделе Inslall: как установить GNU ARM Eclipse plug-in, как настроить toolchain и как подцепить дебаггер =)


    1. JekaKey
      07.07.2016 16:52
      +1

      Я делал через SPL, реализовывали класс обмена по спецификации usb audio.
      Вот ссылка https://github.com/JekaKey/Studiologic-VMK188-Plus-Plus
      Описание и суть проекта тут http://rmmedia.ru/threads/117290/


  1. x893
    07.07.2016 10:00

    HID & MSD — рискну предположить — отладчик mbed?
    а на HID & MSD & CDC не думали?


    1. ana_lazareva
      07.07.2016 10:02

      нет, не угадали) это пока корпоративная тайна, но через месяц-два я смогу написать о нашей разработке!


  1. pftbest
    07.07.2016 14:25

    Если я не ошибаюсь то «Standard Peripheral Library» производитель признал устаревшей и настоятельно рекомендует использовать cube вместо нее.


    1. PKav
      07.07.2016 17:00
      +1

      А там всё ещё страшнее, чем в SPL. Один USART чего стоит.
      А SPL ещё не снят с поддержки, поэтому можно и им пользоваться без проблем.


    1. ana_lazareva
      07.07.2016 17:13

      Да, это так. Однако всегда возникают задачи, когда тебе важно знать, что реально функция делает, какие регистры трогает и. т. д. А в cube наворотили довольно массивный HAL и чтобы спуститься в самый ад (low level driver), нужно открыть миллион файлов, запутаться и вообще забыть зачем ты туда полез) В целом, единственный HAL которому я доверяю — это HAL ChibiOS. Но это уже вообще другая история)


      1. denis_obrezkov
        07.07.2016 21:23

        По-моему, в Cube все более-менее понятно — есть .c файлы с функциями для работы с устройством, есть .h файлы описания регистров, и все это добро очень хорошо откомментировано. Если работать с eclipse, то там здорово помогает переход к определению переменной/функции по F3.


  1. afiskon
    08.07.2016 10:45

    Анастасия, спасибо за статью. Было бы очень интересно почитать про задачи, с которыми вы сталкиваетесь на работе, и их решение, какие инструменты, литературу или так далее вы при этом используете, читаете ли какие-то профессиональные интернет-ресурсы по теме, и так далее. Мне попадалось очень мало информации по теме в открытых источниках. Пишите еще!


  1. monah_tuk
    08.07.2016 11:51

    Думал над тем, что бы реализовать интерфейс к устройству, вроде /proc или /sys в Linux при помощи MSC. Что бы можно было обновлять прошивку, управлять разными флагами. Пока это сделано через Vendor Device и libusb в своей утилите. Но на Cypress FX3 это делать ручками придётся полностью, хотя там абстракция над USB и так здравая. Боюсь, правда, что время на это не выделят да и памяти уже кот наплакал. Правда утилита бы значительно упростилась бы со стороны хоста.