Sometimes you want to connect a USB device such as a flash drive to a simple microcontroller. The problem is most MCUs cannot function as a USB host. In this article, Stuart steps through the technology and device choices that solve this challenge. He also puts the idea into action via a project that provides this functionality.
Even though many microcontrollers (MCUs) may have a USB device interface that can connect to a host, rarely is a host interface available on simple MCUs. There are various reasons for this, including the complexity of implementing a USB host interface on a simple processor, the need to enumerate and recognize many device types and the memory required to do so. Functioning as a single USB device is much simpler. Implementing a host interface also puts some constraints on the MCU for throughput and clock speed choices.
I have been working on a retro CPU board design, using the Z80180 processor that can run the old CP/M-80 operating system. This is just a fun project, with no real practical use. But the project needed storage that could replace the floppy disk normally used in CP/M. I considered using SD cards, but in experimenting with them, I decided that they are just not what I wanted. What I did want was the ability to plug a USB flash drive into the circuit.
Even though my CP/M project is not that useful, there are other applications where the ability to plug a USB flash drive into an MCU-based circuit is desirable. Examples include:
• Capturing logging or debug data
• Flashing new code into the MCU (if the MCU has self-programming flash)
• Downloading crypto keys or other one-time data to the MCU
• Downloading configuration information to enable or disable features
• Downloading language translation information
• Retaining critical data
• Serving as a “key” to restrict access to maintenance mode functions only to authorized personnel
• Downloading GPS coordinates or map information
• Updating stored part numbers, serial numbers or any stored value that can change.
There are ways to implement USB host capability on an MCU, especially if it has a USB interface that supports OTG (on-the-go) USB capability. But no matter how you do it, you have to write or obtain drivers and integrate the functionality into your software. You will also be constrained as to which MCUs you can use, based on availability of USB host capability. But for a simple design, you may not want to be forced to use a part just because it has USB host capability.
FTDI makes a USB host module called the VDrive3 that provides a limited USB host interface and can connect to an MCU (or even a PC) using an asynchronous serial port. The module also has an SPI interface, although I did not use that in my design. A link to the datasheet is provided on the Circuit Cellar article materials webpage.
The VDrive3 (Figure 1) uses the FTDI Vinculum IC, which provides a USB host interface on one side and a serial or SPI interface to your MCU on the other. Since all of the hardware and software to implement the USB host interface is inside the VDrive3 module, you don’t need to develop USB stacks or drivers, or deal with licensing issues. The VDrive3 comes in a plastic housing so it is easy to mount in a rectangular cutout.
The VDrive3 has a file-based interface for USB flash drives, which means that you don’t need to manage the memory yourself. You open files, write to them, read them, close them and create directories. The VDrive3 shields the host from the memory management functionality, allowing all this to be done with simple commands over the serial interface. The VDrive3 manages the file system so you don’t have to.
In my application, I was emulating a floppy disk. I defined the “virtual floppy” to have 256 tracks of 32 sectors each. To implement that on the VDrive3, I created 256 directories named TRAK000 through TRAK0FF. In each directory, I created 32 files named SEC00 through SEC1F. So, when the CP/M operating system wants to read or write a specific sector, the AVR MCU navigates to the directory that represents the selected track and opens the sector file corresponding to the specified sector.
This is a simple mechanism that is really applicable only to the way I’m using the flash drive, but the general principles apply to any VDrive3 application. You can create a directory, and then create files within the directory that correspond to whatever information you need to store. Or you can skip the directories and store everything at the top directory level.
One advantage of using the VDrive3 is interoperability with a PC. If I used SD drives, I would either have a proprietary format that couldn’t be read in a PC, or else I’d have to manage a PC-compatible file system in my MCU. But the VDrive3 recognizes the standard FAT12, FAT16, and FAT32 file systems, so a flash drive written on a VDrive3 can be inserted into a PC and read. This could be very useful if you are collecting debug or log data from your MCU application. In my case, I could make a copy of a CP/M “floppy” on a PC.
The VDrive3 recognizes various commands, including SEK (seek to file offset), OPW (open file for writing) and WRF (write to file). The commands used in my application are listed in Table 1. VDrive3 commands can be sent in ASCII as in the command list in Table 1, or you can configure it to use a short command set that requires fewer bytes to transmit. Data can be either ASCII or binary. The VDrive3 defaults to the extended command set and binary data transfer, and I leave the module in that mode for my application.
|SBD divisor||sets baud rate|
|SEK offset||seeks within a file|
|FWV||request FW version of VDrive3|
|OPW filename||open file for writing|
|OPR filename||open file for reading|
|WRF bc data-to-VDrive3||Write bc bytes to file|
|RDF bc data-from-VDrive3||Read bc bytes from file|
|CLF filename||close file|
|MKD dirname||make new directory|
|DIR||request directory listing|
|E||Echo command—for synchronization and to detect VDrive3 presence|
TABLE 1 – The VDrive3 recognizes various commands. Shown here are the commands used in my application. VDrive3 commands can be sent in ASCII as in this command list, or you can configure it to use a short command set that requires fewer bytes to transmit.
Generally, each command is sent as a string of two or three characters. If data such as a filename are needed, the command is followed by a space, the appropriate text and a carriage return character (0x0D). If no data are needed, such as for the FWV (FW version) command, the command can be immediately followed by the carriage return. Setting the baud rate requires a divisor value, so the SBD command (set baud rate) is followed by a 3-byte divisor value and then a carriage return.
For read/write commands, such as a file write (WRF), the data comprise a binary 32- bit word describing the number of bytes to be written, followed by a carriage return and then followed by the actual binary data (always 128 bytes in my application). For a file read (RDF), the command is followed by a space, the number of bytes to be read, and a carriage return. The VDrive3 then responds with the requested number of bytes.
The VDrive3 acknowledges commands with a specific prompt, which is D:\> and a carriage return. The application code looks for this sequence to indicate command completion. In some cases, other sequences are expected. For example. Just sending a carriage return to the VDrive3 allows you to check whether a USB drive is plugged into the VDrive3. If a USB drive is connected, the VDrive3 responds with the D:\> prompt. If no drive is inserted, the VDrive3 will return the string “No Drive”.
Although the short commands are fewer bytes, I didn’t think the overhead of the extended commands was enough to justify changing the mode in my application. Yours might be different.
The VDrive3 defaults to 9600 baud on power-up. Other baud rates can be selected using the SBD command. The VDrive3 switches between the asynchronous serial and the SPI interfaces using a jumper on the back of the module. Mine came configured for asynchronous serial operation.
In the example application, the VDrive3 is controlled by a Microchip ATMega644 MCU. I would have liked to use something like an Arm part, but the AVR functions as a DMA controller and boot loader for the Z80180 CPU, which operates at 5 V. Consequently, I wanted an MCU that is capable of 5 V operation. I also needed a part with two or more serial ports and enough I/O pins to handle the DMA functionality. These requirements limited my choices a bit. The AVR operates at 14.7456 MHz, which is a multiple of standard baud rate frequencies.
The schematic for the circuit is shown in Figure 2. This is a simplified version of my original schematic; in the Z80180 application, all the pins of the MCU are used. This schematic is just to show the VDrive3 interface, so I removed everything related to the Z80180 interface.
In the schematic, J2 is connected to UART0 of the MCU. It is connected to the PC for debugging with an external logic-to-RS232 converter, using the technique described in my article “Debugging Embedded Systems with Minimal Resources” (Circuit Cellar, July 2016). UART0 is configured to operate at 9600 baud, although of course you can use any baud rate you want. For your reference, Figure 3 is a schematic of the RS-232 converter.
The second AVR UART, UART1, is connected to the VDrive3 through U3, a Texas Instruments (TI) TS3A5018. U3 is a quad DPST analog switch. It connects the VDrive3 serial output to the AVR UART1 input, and the AVR UART1 output to the VDrive3 serial input. I included U3 because I wanted to connect two VDrive3 devices so as to emulate two floppies. But in most applications, you would only need one VDrive3, which you would connect directly to the MCU (or through voltage translation if needed).
J4 and J5 are Molex SL-series 6-pin connectors. The VDrive3 has a header with pins on 2 mm centers, but the prototyping board I used has holes on 0.1″ centers. I modified the cable that came with the VDrive3 so I could use a connector that will work on my board. Of course, if you are designing a PCB, it’s easy to use a 2 mm header, and then you can use the VDrive3 cable unmodified.
On my board, I connected the CTS and RTS signals from the VDrive3 together. The VDrive3 can be configured to use hardware handshaking with RTS/CTS, DTR/DSR, or software handshaking using XON/XOFF. Default is RTS/CTS, and I left it in that state.
U2 is a 3.3 V regulator that powers U3 and U4. J1 is an Amphenol/FCI 75869-131LF for programming the AVR. You will obviously want the appropriate programming connector for whatever MCU you use. Jumper W1 is for selecting debug or CP/M boot mode and is unused in the example.
The firmware that operates the AVR MCU is written in C using Atmel Studio (Atmel is now owned by Microchip). The modules used are listed and described in Table 2.
|Main.c||Initializes all MCU hardware and operates the main loop that blinks the heartbeat LED and processes incoming characters from the host PC. The timer tick and UART1 Rx interrupt servicing routines (ISR) are in main.c. The timer tick ISR calls tick servicing functions in VDrive.c and Monitor.c. The UART1 Rx ISR calls a function in VDrive.c to service incoming data from the VDrive3.|
|ASCIIHEX.c||Converts between hex and ASCII|
|UARTstuff.c||Various functions for communicating with the PC host using UART0|
|VDrive.c||Functions for communicating with the VDrive3|
|Monitor.c||A simple-minded monitor that allows testing the VDrive3|
TABLE 2 – The firmware that operates the AVR MCU is written in C using Atmel Studio. The modules used are shown here.
In my original code for the Z80180 board,
Monitor.c has functions for testing both the Z80180 DMA interface and the VDrive3. For this example, I removed everything but the VDrive3 functionality. Tests are started with two-character commands. Tests provided in
V0 = select Vdrive 0
V1 = select Vdrive 1
VB = set baud rate to 115200
VC = check for Vdrive connected
VD = get directory
VE = send ‘E’ 5 times or until response received
VF = read FW version
VI = initialize drive, creating track directories and sector files
VN = request disk-connected status
VT = create file, write it, close it, open, read, print content—for verifying full write/read path
VW = write each track/sector file with 0xE5 data
These aren’t exhaustive tests. They are there to ensure that the hardware and firmware work. For example, VB function sets the VDrive3 and the AVR UART1 to a fixed 115200 baud to ensure the underlying baud rate function works. You would probably want other baud rates in a real application—or a test for the SPI interface if you are using that. But the code for these functions should make it clear how you can modify it for your own use.
Figure 4 shows a test session with the VDrive3, after everything was tested and working. This was captured with a simple Python-based terminal application that I wrote long-ago for another project. The command sequence is as follows:
Ve: Send “E” synchronizing character, get back “E” response and an internal count.
Vf: Request VDrive3 FW version, display result
Vb: Set baud rate to 115200.
Vi: Create track/sector files, displaying track number as progress indicator.
Vd: Read VDrive3 directory, display result (only first 128 bytes displayed).
Vt: Perform a write/read on one sector of one track, display results.
Monitor.c module is intended for testing the hardware and software, but for an actual application, the functions in the
Vdrive.c module will be used.
Vdrive.h exposes the functions shown in Listing 1. The
Monitor.c file also has a simple debug dump routine that is used during development. The routine performs a post-mortem dump of key values to debug issues in the code. The function is named
MonitorDebugDump() and displays output in the form:
err ee s dd tt ss f x
followed by the contents of
VdRxBuffer. The fields are:
ee = ErrProgress,
s = StepCompleted,
dd = DebugNumber,
tt = track,
ss = sector,
f = function code,
x = step value
Change to specified track directory (TRAKxxx)
Open sector file (SECxx) for writing
Seek to start of file
Send Vdrive WDF command to write sector
Write 128 bytes from SectorBuffer.
Wait for Vdrive prompt (or timeout, whichever occurs first)
Close sector file
Change directory back to root.
Change to specified track directory (TRAKxxx)
Open sector file (SECxx) for reading
Send Vdrive RDF command to read sector
Read 128 bytes into SectorBuffer.
Wait for Vdrive prompt (or timeout, whichever occurs first)
Close sector file
Change directory back to root.
void VdriveSelect(uint8_t) : Select VDrive 0/1
void VdTick(): Tick interrupt routine, called from Main tick ISR.
void VdriveRxInterrupt() : Vdrive3 serial Rx processing interrupt.
uint8_t SetupVdRxBuffer() : Initialize Rx status buffer for returned status.
uint8_t FormatDrive : Formats drive with track directories and sector files.
uint8_t SetBaudRate(uint32_t) : Set baud rate using Vinculum divisor.
uint8_t IsVdriveConnected) : Return 1 if Vdrive3 is connected. Uses ‘E’ sync method.
uint8_t IsDriveInstalled() : Return 1 if a flash drive is plugged into Vdrive3.
LISTING 1 – Vdrive.h exposes the functions shown here.
Because your application isn’t the same as mine, you can modify this as needed, should you decide to experiment with the VDrive3. But it will give you a template from which to work. You could write an application for a PC serial port and use an RS232-to-logic-level converter to connect the PC to the VDrive3. Although I didn’t do this, it might be useful for experimenting or for developing your code logic in another language, such as Python. Because the idea is to use the VDrive3 to provide an MCU with a USB host interface, you will at some point want to connect to a real circuit.
You will note that the file read/write functions navigate to the track directory, open the sector file, read/write it, close it and return to the root directory. This is driven by the use of the VDrive3 in a Z80180 circuit, where CP/M will read/write a sector at a time and the SECxx files are always 128 bytes in length. In your application, you will probably be reading or writing a larger file, and you will leave it open until you are finished with it.
One note about the VDrive3: When you write to a file, the write pointer is set to the place where you stopped writing. If you write to the middle of the file, anything after that point will be lost. This is one reason I had a 128-byte file for each sector: no need to block or deblock sector writes in a larger, multi-sector file. In my example, when I use the VW command to write the entire “floppy” space, the total space used is significantly larger than the amount I wrote. This is because the FAT file system works in clusters that span many sectors. The VDrive3 handles all this for you. The VDrive3 requires the flash drive to use 512-byte sectors.
In my application, I was only interested in connecting to a USB flash drive. But FTDI has a development board for the Vinculum device, as well as a toolchain that will let you develop your own USB applications, so you can connect other USB devices. You’re on your own to test it and make it work. The thing I like about the VDrive3 is that it can connect to a USB flash drive with no USB development needed. But if your design requires connection to some unique USB device that must also plug into a PC, this may be a good way to do it.
The VDrive3 and other Vinculum components are available from Digi-Key. Although I didn’t use it, there is also a module named the VDip that exposes the serial, SPI and FIFO interfaces of the Vinculum chip. The device has the same PCB footprint as a 24-pin DIP. One advantage of the VDip module is that it brings the reset out of the device, so you can reset it with the rest of the circuit. Reset isn’t available on the VDrive3, so if you set a new baud rate and then reset your MCU, you will have to cycle power on the VDrive3 to get back to the default baud rate.
The VDip also has a FIFO interface that is much faster than the serial interfaces on the VDrive3. In an application where you need maximum speed, the VDip might be a better choice, although it will use more MCU pins. My serial port VDrive3 floppy emulator won’t win any speed awards. Then again, neither will the Z80180 it’s connected to.
The FTDI VDrive3 provides an easy way to connect a USB flash drive to an MCU that does not have a USB host interface. You don’t need any USB development, and it allows simple commands to perform file operations on the device.
For detailed article references and additional resources go to:
PUBLISHED IN CIRCUIT CELLAR MAGAZINE• JANUARY 2019 #342 – Get a PDF of the issueSponsor this Article