cancel
Showing results for 
Search instead for 
Did you mean: 

32F417: can one detect when a USB Host is connected?

PHolt.1
Senior II

Can anyone suggest a way of bypassing this USB OUT function ("OUT" means to Host i.e. the opposite of the correct USB terminology) if no USB cable is plugged in?

This is a fairly common bit of code from the ST Cube USB CDC (VCP) library, with additions from me to make it thread-safe, and a timeout in case a USB Host (with a USB VCP application running) is not connected.

I would like to achieve this without the timeout.

/**
  * @brief  CDC_Transmit_FS
  *         Data to send over USB IN endpoint are sent over CDC interface
  *         through this function. Note that "IN" in USB terminology means
   *         product-> PC.
   *
  * @param  Buf: Buffer of data to be sent
  * @param  Len: Number of data to be sent (in bytes)
  *  *
  * This function had a call rate limit of about 10kHz, and had a packet length limit
  * of about 800 bytes - until the flow control mod below. These limits still apply if
  * flow_control=false.
  *
  * This function is BLOCKING if flow_control=true. That means that if there is no Host
  * application receiving the data (e.g. Teraterm) it will block output.
  * The problem is that if no USB device is connected from startup, the while() loop
  * would block for ever, hence the timeout.
  *
  * Flow control is normally disabled for the debugging output functions.
  *
  * If called before USB thread starts, all output is dumped - for obvious reasons!
  *
  * Mutexed to enable multiple RTOS tasks to output to port 0.
  *
  */
 
#define USB_TX_TIMEOUT 100	// 100ms
 
uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len, bool flow_control)
{
 
	if ( g_USB_started && (Len>0) )
	{
 
		uint32_t counter = 0;
		bool timeout = false;
 
		osMutexAcquire(g_CDC_transmit_mutex, osWaitForever);
 
		USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassDataCDC;
 
		// Wait on USB Host accepting the data.
		if ( flow_control )
		{
			// Loop around until USB Host has picked up the last data
			// This rarely holds things up - 20us typ. delay
			while (hcdc->TxState != 0)		// This gets changed by USB thread, hence the g_USB_started test
			{
				osDelay(1);
				counter++;
				if (counter>USB_TX_TIMEOUT)
				{
					timeout=true;
					break;
				}
			}
		}
 
		if (!timeout)
		{
			// Output the data to USB
			USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
			USBD_CDC_TransmitPacket(&hUsbDeviceFS);
		}
 
		osMutexRelease(g_CDC_transmit_mutex);
 
		g_comms_act[0]=LED_COM_TC;	// indicate data flow on LED 0
 
		// If flow control is selected OFF, we do a crude wait to make CDC output work
		// even with a slow Host. This actually needs to be only a ~100-200us wait, but
		// is host dependent.
		if ( !flow_control )
		{
			osDelay(2);
		}
 
	}
 
	return USBD_OK;
}

17 REPLIES 17
AScha.3
Chief

why not using the most simple way: detect the USB Vbus + 5V , when host cable is connected.

+

on micro-USB you have also the ID pin to check it.

If you feel a post has answered your question, please click "Accept as Solution".
gbm
Lead II

The simplest way is to get the connection status information from the USB data structures. Note that VBUS = 5V may as well mean "charger connected" - has nothing to do with USB interface state. Init callback is of no use - it is called when USB connection is established, but DeInit is not called when the connection disappears - instead it is called right before Init (so after connecting the USB the next time).

There are, however, numerous other problems with your code. Search the forum for my posts regarding the CDC class. Transmit should not be called from any thread code - it should run at the same processor (hardware, not task) priority as the USB interrupt.

PHolt.1
Senior II

Reply to Ascha:

this is a great idea and dumping CDC output if PA9=0 is simple:

HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_9)

Reply to Gbm:

I did wonder if there is some struct member which would be better but my understanding is not sufficient. Maybe something here

typedef struct
{
  uint32_t data[CDC_DATA_HS_MAX_PACKET_SIZE/4];      /* Force 32bits alignment */
  uint8_t  CmdOpCode;
  uint8_t  CmdLength;    
  uint8_t  *RxBuffer;  
  uint8_t  *TxBuffer;   
  uint32_t RxLength;
  uint32_t TxLength;    
  
  __IO uint32_t TxState;     
  __IO uint32_t RxState;    
}
USBD_CDC_HandleTypeDef; 
 

Re the CPU priority, I recall a discussion from the past and probably these two need to have interrupts disabled around them

__disable_irq();

USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);

USBD_CDC_TransmitPacket(&hUsbDeviceFS);

__enable_irq();

as the simplest measure. Elsewhere I have this

portENTER_CRITICAL();

CDC_Transmit_FS(cdc_out_buf, length, true); // true: enable flow control

portEXIT_CRITICAL();

which is not the same thing; it merely stops RTOS task switching.

Would you agree? I recall the DI/EI stuff but somehow it got dropped...

You mention "numerous" problems. Could you please point me to some? Thank you very much in advance.

The code seems to be running well.

gbm
Lead II

Blocking the task switch will not prevent USB interrupts, so it doesn't work. Blocking the interrupts is not allowed in ARM thread mode. 🙂

Any HAL-based USB device code not accounting for the lack of USB routines reentrancy will run successfully... for 15 seconds, for 15 minutes or maybe for 15 hours. Then it will usually hang.

Getting back to your question. To detect CDC connection, implement SetLineState request. When the value set changes from anything other than 3 to 3 (both flags active) - the CDC connection becomes active after at least 20 ms after this change. (That's from my 10 years experience with USB CDC virtual COMs). On disconnect, SetLineState sets state != 3.

My preferred way of handling Transmit is to create a software-triggered interrupt of the same priority as USB hw interrupt, calling Transmit. The interrupt is pended when you need to transfer data. It gets enabled when the connection is established (as above). It is disabled in its own ISR, and re-enabled in TransmitCplt callback.

Pavel A.
Evangelist III

if this is from the USBD library, it’s horrible. Haven’t looked at the usb libraries for some time.

Generally to detect if the USB device is connected you track the reset event (the very start of enumeration) and successful end of class-specific initialization. For VCP this usually is request to set line parameters.

If the host stops polling for prolonged time (say, > 1s) you also can consider that something is wrong there and stop sending.

Unless the host uses flow control (btw this is not what is mentioned in the above code. That is called back pressure).

PHolt.1
Senior II

"Blocking the interrupts is not allowed in ARM thread mode. 🙂"

Why not? This is a normal FreeRTOS task. Disabling ints prevents re-entrancy. I believe it was "Piranha" himself who recommended this method, including the method of flow control, which also works. It's been running for at least a year, 24/7. I just tried to implement an easy way to dump the output if there is no USB cable connected, which with this box will be the case 99% of the time (USB is for config only).

I did a dig around for SetLineState and found no obvious examples, so I will stick to testing PA9 for now.

"if this is from the USBD library,"

Minus the timeout and the flow control, yes it is, ex ST Cube MX 32F417, from maybe 3 years ago.

It may be "back pressure"; I don't know the terminology. The hcdc->TxState flag definitely does work for flow control, although with a reasonably fast application on the Host (e.g. Teraterm) that flag goes busy for a very very short time - of the order of 20us and only perhaps once a minute. But if you don't implement it, a 32F417 can generate data fast enough to corrupt the data received. I spent some time on this and found that limiting the packet rate (as per the comments) also does the job, but would be expected to be heavily Host system dependent. The mode where flow control is disabled is for debugs via CDC, where you trade possible data corruption for not holding up the target software. And yeah not a good idea to do CDC debugs from within an ISR, due to the DI/EI : - )

 Lots of people have been up this road before e.g.

https://www.eevblog.com/forum/microcontrollers/stm32cube-and-cdc-device-how-do-i-detect-that-the-host-disconnects/msg3547933/#msg3547933

gbm
Lead II

The packet-level flow control is provided by USB automatically - to see it explicitly at the application level, use TransmitCplt callback after the first packet is sent. For deciding when the first packet may be sent - I wrote above how to detect the CDC connection opening / closing. USB connection status alone is not enough for this. The USB may be active but there might be no application actually communicating via the USB port, thus handling the SetControlLineState request is essential but not sufficient. For detecting physical disconnect (which may sometimes happen without logical disconnect via SetControlLineState, if you unplug the USB cable) you may also use timeout decremeted by SysTick ISR, reloaded by USB SOF interrupt callback. Another method is to use USB suspend callback (haven't tried this). There is no other disconnect callback offered by an unmodified ST USB stack - one of many problems with the stack.

That's why I finally wrote my very own USB stack... 😉

You may find quite fancy, simplified USB stack here:

https://github.com/dmitrystu/libusb_stm32

It has some problems but it's generally much easier to use than ST stuff (which also has many problems).

PHolt.1
Senior II

OK - many thanks.

Actually there are problems with my timeout code above. Not that it does not work - it does - but that while 100ms is totally fine when feeding debugs to Teraterm, it is way too short when using VCP for other data comms.

This whole thing is too complicated. One can have a cable connected with VBUS coming out of it, so PA9=1, and a dead OS on the PC. Or just no USB VCP app running (no Teraterm, or the app is not connected to the COM port).

So I have settled on this

uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len, bool flow_control)
{
 
	if ( g_USB_started && (Len>0) && (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_9)==1) )
	{
 
		osMutexAcquire(g_CDC_transmit_mutex, osWaitForever);
 
		USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassDataCDC;
 
		// Wait on USB Host accepting the data.
		if ( flow_control )
		{
			// Loop around until USB Host has picked up the previous data
			// This rarely holds things up - 20us typ. delay
			while (hcdc->TxState != 0)		// This gets changed by USB thread, hence the g_USB_started test
			{
				osDelay(1);
			}
		}
 
		// Output the data to USB.
		// USB interrupts must be disabled around this.
		__disable_irq();
		USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
		USBD_CDC_TransmitPacket(&hUsbDeviceFS);
		__enable_irq();
 
		osMutexRelease(g_CDC_transmit_mutex);
 
		g_comms_act[0]=LED_COM_TC;	// indicate data flow on LED 0
 
		// If flow control is selected OFF, we do a crude wait to make CDC output work
		// even with a slow Host. 
		// In the usage context (outputting debugs to Teraterm) this actually needs to be only 
		// a ~100-200us wait, but is host dependent. This gives us a 1-2ms delay between
		// *blocks* (typically whole lines).
		if ( !flow_control )
		{
			osDelay(2);
		}
 
	}
 
	return USBD_OK;
}

What I am not sure about is whether it is possible for hcdc->TxState to go back to 1 after it has been tested as 0.

If that is the case, I need to test this flag inside the DI/EI region but in a way which prevents the whole box hanging if this goes wrong, which takes us back to a timeout.

gbm
Lead II

TxState goes BUSY when you call TransmitPacket. It goes READY when the packet is sent. It does not go BUSY by itself. The problem is it is not BUSY when there is no established connection.