The Maxim USB Laboratory


The Maxim USB laboratory is a MAX3421E-/MAX3420E-based system using an ARM7™ processor and example software. This application note describes the system and the software that runs it. Both a USB host and peripheral are implemented in the same ARM® C code. This approach allows development and study of USB peripherals and embedded hosts with the advantage of having a reference device at the other end of the USB cable, all in the same C code.

This application note is a companion to application note 3937, "Setting up the Maxim USB laboratory." The Keil™ project containing the Maxim demo code is available for download


The Maxim USB laboratory is a combination of two circuit boards and a C program. With this system, you can:
  • Study, operate, test, and modify a functional USB device based on an ARM7 microcontroller (µC) connected to a MAX3420E USB peripheral controller.
  • Study, operate, test, and modify a functional USB host that uses the same ARM7 µC connected to a MAX3421E host controller. Figure 1 shows data retrieved by the host from a USB memory stick.
  • Connect the host to the peripheral with a USB cable and run both host and peripheral at the same time. When developing either USB host or USB peripheral code, it is very useful to have a reference design at the other end of the USB cable. By doing this, both sides of the USB cable, host and peripheral, are controllable and customizable since they are both implemented by the same C code.
Figure 1. The MAX3421 host/ARM retrieves enumeration data from any USB peripheral device, and reports results over a serial port connected to a PC running a terminal emulation program.
Figure 1. The MAX3421 host/ARM retrieves enumeration data from any USB peripheral device, and reports results over a serial port connected to a PC running a terminal emulation program.

Other Reading

Programming guides, data sheets, and application notes for the MAX3420E and MAX3421E are available on the Maxim website:

Application note 3937, "Setting up the Maxim USB laboratory"


The application C code runs on a two-board set:
Figure 2. The Maxim EV kit plugs into a Keil MCB2130 board.
Figure 2. The Maxim EV kit plugs into a Keil MCB2130 board.

Figure 2 shows the board setup. The blue board is a Keil MCB2130 which contains an ARM7 chip, the Philips® LPC2138. This µC has two SPI™ hardware units which connect to the two Maxim USB controller chips.

The vertical board is the MAX3421E EVKIT-1. A MAX3420E peripheral controller connects to one of the ARM SPI ports, and is wired to the USB "B" connector (J5)labeled "3420 P" (P is for peripheral) in Figure 2. A MAX3421E host/peripheral controller connects to the other ARM SPI port, and is wired to the "3421P" (J2) and "3421H" (J1) connectors (H is for host). The code described in this application uses the MAX3421E as a host, so the middle USB connector (J2) on the EV-kit board is not used.

The host software uses one of the two serial ports (P1 on the MCB2130 board) to send USB descriptive information to a PC running a terminal emulation program. Terminal programs such as Tera Term Pro can emulate terminals (VT100) that recognize special "escape code" sequences sent by the program to clear the screen and home the cursor. The terminal program serial-port settings are 38400, N, 8, 1 with no flow control.

The beige box and ribbon cable in Figure 2 is a Keil ULINK JTAG loader-debugger. This unit is supported by the Keil µVision®3 development environment. The MCB2130 board comes with an evaluation version of µVision3, a fully functional version of the Keil toolset. The evaluation version is limited in code size to 16kB.

File attachments to this application note include the full Keil project with all source files, plus the load module in .hex format. If you have the ULINK JTAG unit you can compile the code, load it, and debug it through the JTAG port. This is an excellent way to get up to speed on USB—modify working host and/or peripheral code to suit your purposes. If you do not have a ULINK box, you can still load and run the hex file using a free utility called Flash Magic, available at Consult Maxim's application note 3937 for information about how to configure and use this utilty.

Figure 3. Block diagram of the MAX3241E EVKIT-1 board. The shaded ovals are the major software modules.
Figure 3. Block diagram of the MAX3421EEVKIT-1 board. The shaded ovals are the major software modules.

What the Code Does

Figure 3 shows a block diagram of the MAX3421EEVKIT-1 board and the software modules described in this application note. Both the MAX3420E and MAX3421E provide access to their register sets using the SPI bus. The LPC2138 contains two hardware SPI units:
  • A dedicated SPI port (SPI)
  • A general-purpose serial interface (SSP, Synchronous Serial Port)
The SSP is programmed to implement a second SPI port. These ports are programmed differently. The SSP, for example, has eight-deep transmit and receive FIFOs, while the SPI has only a one-deep read buffer. Both SPI ports are programmed to operate in 8-bit data mode for MAX3420E and MAX3421E compatibility. The software module SPI_FNs.C contains the register access functions used by both USB controllers.

The MAX3420E attaches to the SPI unit, so the functions to talk to this chip have a "P" prefix (e.g., Prreg and Pwreg) to indicate peripheral operation. The MAX3421E attaches to the SSP unit, so its access functions have an "H" prefix (e.g., Hwreg) to indicate host operations.

Peripheral Code

The C module 3420_HIDKB.C implements a USB peripheral device which connects to a PC using EV kit connector J5. The MAX3420E INT output pin connects to the ARM7 EINT0 (External Interrupt Zero) pin. This interrupt asserts anytime the USB peripheral implemented by the MAX3420E requires service.

The application enumerates and operates as a standard USB HID (Human Interface Device). An advantage to conforming to a standard Windows® device class is that the device driver is built into Windows, so no special driver needs to be installed. Windows recognizes the device implemented by this application as a standard keyboard. Pressing any of the four pushbuttons attached to the MAX3420E GP-IN pins thus causes the "keyboard" to type a text string into any open window that accepts text, for example Notepad or Wordpad.

Warning: This application types into any application that accepts text, such as email, compiler editors, or a Word document. Be sure to have a safe application like Notepad open and active before pressing the button. The author can confirm by experience that rogue text typed into an open C source file will not compile properly.

Host Code

The 3421_Host_EVK.C file contains host code that instructs the MAX3421E to perform enumeration steps similar to what a PC would do when a USB device is plugged into connector J4. The main( ) function is in this module, as is the interrupt handler for the HID keyboard code. The EINT0 interrupt handler is simple:
                        // EINT0 Interrupt handler--MAX3420E INT pin
void INT3420 (void) __irq
service_irqs();         // Do the USB thing in 3420_HIDKB_INT_EVK.C module
EXTINT      = 1;        // Clear EINT0 interrupt flag (b0)
VICVectAddr = 0;        // Dummy write to indicate end of interrupt service
The background program main( ) contains the following endless loop:

  waitframes(200);      // Some devices require this
The enumerate_device() function does most of the work, sending USB requests to the attached device and reporting results over the serial port. Plug any USB device into EV kit J1, and this function sends enumeration requests to the device and reports results over the serial port (Figure 1).

Simultaneous Host/Peripheral Ooperation

Because the MAX3421EEVKIT-1 board contains both a USB peripheral and a USB host, and because they use separate SPI interfaces on the ARM7, the software can easily be structured so that both host and peripheral applications run simultaneously. The host application runs main( ) to talk to the MAX3421E in the background, while the HID peripheral code activates the service_irqs( ) function using a MAX3420E interrupt.

If you connect a USB cable from EV kit J5 to J1, the ARM processor can act as a USB host talking to a peripheral device that is...itself! Figure 4 shows what the host code in 3421_Host.C reports when it interrogates the peripheral implemented in 3420_HIDKB.C.

Figure 4. The demo code interrogating itself over USB.
Figure 4. The demo code interrogating itself over USB.

Developing Host Code

Reading through the enumerate_device( ) function can help you understand how a USB host operates, and how to command the MAX3421E to do the various host operations as it enumerates a USB device. While developing USB host code, it is sometimes difficult to diagnose a problem if it is not clear how the peripheral plugged into the host is responding. Having a reference peripheral over which you have total control (the 3420_HIDKB.C module) makes it easy to know and vary device responses to test your host firmware.

Developing Peripheral Code

This host is an excellent interrogation tool for USB peripheral code development. The combination of the MAX3421E and the 3421_Host.C module creates a simple and powerful USB packet generator and analyzer. You control what your peripheral device sees by writing C host code; the MAX3421E shows you how the peripheral responded.

An Example

Suppose you are writing peripheral code to handle USB strings. You code the manufacturer string as XYZ Widget Company in hidkb_enum_tables.h like this:
// STRING descriptor 1--Manufacturer ID
18,             // bLength
0x03,           // bDescriptorType = string
'X','Y','Z',' ','W','i','d','g','e','t',' ','C','o','m','p','a','n','y'
Compile this, load it into the ARM flash, and run it. The host program reports the following manufacturer string:

Figure 5. Not Good. Code example shows that the manufacturer's name is not want you intended to show. There is clearly a problem with the code.
Figure 5. Not Good. Code example shows that the manufacturer's name is not want you intended to show. There is clearly a problem with the code.

XZWde op? What went wrong? Well, the USB spec calls for text strings to be expressed in Unicode, with two bytes per character. Go back to the source code for the peripheral, and change it to this:
// STRING descriptor 1--Manufacturer ID
        35                      // bLength
        0x03,                   // bDescriptorType = string
        'X',0,'Y',0,'Z',0,' ',0,'W',0,'i',0,'d',0,'g',0,'e',0,'t',0,
        ' ',0,'C',0,'o',0,'m',0,'p',0,'a',0,'n',0,'y' // Unicode!
Compile, run, and you should see this:

Figure 6. Better. Using the Unicode format, you see this result.
Figure 6. Better. Using the Unicode format, you see this result.

That is much better, but still not perfect. Where is the 'y' in Company? We carefully counted the 35 characters in the string and entered this as the first byte to indicate the string length. We missed something else: the length byte needs to include the first two bytes as well as the string. Make one final modification to the code:
// STRING descriptor 1--Manufacturer ID
        37               // bLength
        0x03,                   // bDescriptorType = string
        'X',0,'Y',0,'Z',0,' ',0,'W',0,'i',0,'d',0,'g',0,'e',0,'t',0,
        ' ',0,'C',0,'o',0,'m',0,'p',0,'a',0,'n',0,'y' // Unicode!
The result should now look like this:

Figure 7. Best. The code now does what you thought it should.
Figure 7. Best. The code now does what you thought it should.

By tweaking the peripheral code and exercising/analyzing it with the host controller and code, we see exactly what a PC would see since the reported results are for actual USB traffic sent from the MAX3420E to the MAX3421E. Best of all, we can do this on a controlled basis, without worrying about how a PC will respond to error-prone code in development.

Code Description

Figure 8. Structure of the Keil Project.
Figure 8. Structure of the Keil Project.

Figure 8 shows the three main modules, expanded to show their file dependencies. The unexpanded modules are housekeeping files required by the Keil environment.


This module contains utility functions called by the other two modules.

void init_PLL(void)

This function sets up the LPC2138 PLL and clock dividers. The crystal attached to the LPC2138 (FOSC) is 12.000MHz. The CPU frequency is set to 48MHz according to the formula CCLK = FOSC × M, where M = 4. The LPC2138 uses an internal CCO (current-controlled oscillator) to multiply FOSC to a higher internal frequency. FCCO = CCLK × 2 × P. FCCO must be between 156MHz and 320MHz. For P = 2, FCCO = 48MHz × 2 × 2 = 192MHz.

Note: The Keil file startup.s also contains startup code to set the PLL parameters. The init_PLL() function effectively overrides these settings.

Important Warning: Modify these settings at your own risk. Improper settings can force the LPC2138 outside of its spec limits and cause the Keil debugger to stop functioning. If this happens, the remedy is to use the Flash Magic utility to erase the LPC2138 flash memory (use the second serial port for this, connected to Keil board P2), correct the PLL settings in your code, then try again.


This function initializes the LPC2138 I/O pins and SPI units (SPI and SSP):
  • The SPI is clocked by CCLK/8, the maximum allowed. This SCLK is 48MHz/8 = 6MHz.
  • The SSP is clocked by CCLK/(2 × VBDIV). VBDIF is set to 1, making the MAX3421E SCK signal equal to 48MHz/2 = 24MHz. The MAX3421E SCLK input can run as high as 26MHz.
About the SPI Interface
The SPI interface uses the following signals:
  • MOSI Master Out, Slave In data
  • MISO Master In, Slave Out data
  • SCLK Serial Clock, supplied by the LPC2138
  • SS# Slave Select, driven by the LPC2138
The LPC2138 SPI hardware manages the first three signals, but the SS# is directly set and cleared using a GP-OUT pin (P0.7).

About the SSP Interface
Although the SSP has a hardware SS# pin, it is more straightforward to use a GP-OUT pin for SS# (P0.20).


These functions write register and FIFO data to the MAX3421E using the LPC2138 SSP hardware. The H prefix indicates host operation. Hwreg writes a single register value; Hwritebites writes multiple bytes into a MAX3421E FIFO.


These functions read register and FIFO data from the MAX3421E using the LPC2138 SSP hardware.

More About the SSP Interface
The SSP hardware has a coding issue that results from having data FIFOs in the transmit and receive paths. In an SPI operation every 8-bit transfer out is always accompanied by an 8-bit transfer in. The LPC2138 accesses the MAX3421E SPI port using the following steps:
  1. Assert SS# (low)
  2. Send a command byte consisting of a register number and a direction bit
  3. Send/receive one or more data bytes
  4. De-assert SS# (high)
SPI writes are straightforward—everything goes out, and the data in the in FIFO may be ignored. But reads are more complex, because the read FIFO contains at least one bad byte, the one that was automatically clocked in during step 2. There also can be stale in data resulting from a previous operation that wrote multiple bytes. The in FIFO cannot be inhibited or flushed by hardware. Consequently, the code that reads SPI data must first manually flush the FIFO by reading bytes from the SSPDR until a flag indicating "read FIFO not empty" de-asserts.


These functions write register and FIFO data to the MAX3420E using the LPC2138 SPI hardware. The second function writes a register identical to the first function, but it also sets the ACKSTAT bit in the SPI command byte. This is a shortcut for terminating a USB CONTROL transfer. Consult application note 3598, "MAX3420E Programming Guide" for details.


These functions read register and FIFO data from the MAX3420E using the LPC2138 SPI hardware. The second function reads a register identical to the first function, but it also sets the ACKSTAT bit in the SPI command byte.


This function updates the 7-segment readout connected to the MAX3421E general-purpose output pins GPO[6:0]. It preserves the setting of GPO[7], which controls the VBUS switch to the USB "A" Connector (EV kit J1).


This module implements the MAX3421E USB host that interrogates USB devices and reports enumeration data over a MCB22310 serial port. For reference, Appendix A shows a LeCroy (CATC) bus trace for the host enumerating the built-in device implemented in 3420_HIDKB.C.

There are three function types in this module:
  1. Initialization
  2. Utility functions
  3. High-level functions called in main( )
The following descriptions follow this order.


The MAX3421E contains a power-on reset, so this operation is not strictly necessary. However as you develop code, it is a good idea to reset the chip at the beginning of each debug session since power was probably not cycled. This starts with a clean machine, with nothing carrying over from the previous debug session.

Resetting the MAX3421E stops the on-chip oscillator. After de-asserting reset, the function waits for the OSCOKIRQ bit (Oscillator OK Interrupt Request) to go valid before returning.


This function is in the 3420_HIDKB.C module.


This function sets up the ARM vectored interrupts as follows:
  1. EINT0 is connected to the MAX3420E INT pin, using priority 0 (highest).
  2. Timer0 is used to blink an activity light, using priority 1.
  3. EINT2 is connected to the MAX3421E INT pin, and given priority 2. It is not used in this program; rather, it is provided for convenience.
  4. Timer1 is used to check the send/stop pushbutton (used in 3420_HIDKB.C), using priority 3.


This function is in the 3420_HIDKB.C module. It does the HID keyboard emulation. The code that initializes the ARM interrupts only needs to install this address as an interrupt vector.


A simple way for a USB host to measure time is to count 1ms frame markers. When the µC sets the register bit SOFKAENAB = 1, the MAX3421E automatically generates these frame markers, which consist of SOF packets in full-speed mode or "keep alive" pulses in low-speed mode. The FRAMIRQ bit then asserts every millisecond.

The USB spec mandates certain long delay times, for example after resetting a device to give it a "reset recovery time." Calling the waitframes function is an easy way to implement these relatively long delays.


This function returns after it detects a USB device plugged into the USB-A connector J1.


This function returns after a device plugged into the USB-A connector disconnects.


This function is called by all functions that send USB packets. Figure 9 shows the bus trace for an IN transaction and the C statements that produce it.

Figure 9. C function calls to the MAX3421E cause this USB bus activity.
Figure 9. C function calls to the MAX3421E cause this USB bus activity.

  1. The µC sets the function address in the FNADDR register. This does not need to be repeated for every packet, since the loaded value persists until the µC reloads the FNADDR register.
  2. The µC launches the transfer by writing the HXFR register, specifying the USB token and endpoint. It then waits for the HXFRDNIRQ (Host Transfer Done) IRQ to assert, signaling completion of the transfer.
  3. The µC reads the HRSL (Host Result) register to determine the outcome of the transaction. Sixteen result codes indicate results such as:
    A. Success (ACK)
    B. Device busy (NAK)
    C. Unusual handshake (STALL)
    D. Device problem like timeout, babble, or bus error
  4. The µC reads a byte count register, then unloads the RCVFIFO, and saves the data in a byte array.

This function handles NAK retries. It continues to relaunch the transfer until:
  1. It gets the ACK handshake, or
  2. It exceeds a preset limit set by the constant NAK_LIMIT.
If the device does not respond, the function retries the transfer up to the limit RETRY_LIMIT.


This function performs a CONTROL-Write transaction with no data stage (_ND = no data). This function is called only for the host Set_Address request. It makes two calls to Send_Packet: first for the SETUP packet, and second for the IN-Handshake packet.


This function makes as many calls to Send_Packet (specifying IN) as necessary to retrieve USB data records. The MAX3420E and MAX3421E endpoint FIFOs are 64 bytes long, so a long descriptor, for example, requires multiple IN requests with the packet data stitched together into a large array (XfrData[2000]).


This function performs the three-stage CONTROL-Read USB transaction. It makes three calls:
  1. To Send_Packet to issue the SETUP packet
  2. To IN_Transfer to get the data into XfrData[]
  3. To Send_Packet to issue the OUT-Handshake packet


This function executes the 10 transfers shown in Appendix A. Because it calls the previous functions to do the low-level USB work, it is easy to follow the enumeration steps by studying this function.


This function checks the 4-bit HRSL value (item 3 in Figure 9) to print the outcome of the last host transfer. If the passed value is zero, it prints nothing. It returns the HRSL value so the calling function can easily check for an error, e.g.,
if(print_error(HR)) return;


This module implements a USB peripheral using the MAX3420E. It conforms to the standard HID class to automatically type text as if it had originated from a standard keyboard. The entry point is the service_irqs function, which is invoked as an interrupt service routine in response to the LPC2138 EINT1 pin. EINT1 is connected to the MAX3420E INT output pin.


This function resets the MAX3420E in the same manner as Reset_Host.


This function does three things:
  1. Configures the MAX3420E SPI interface for full-duplex operation (separate MOSI and MISO pins)
  2. Configures the MAX3420E INT pin as positive-edge active
  3. Enables the MAX3420E interrupts used by the application


This function directly handles bus reset and device disconnect. It also calls do_SETUP to handle enumeration and do_IN3 to do the keyboard typing.

Note: To keep code size down, this application does not handle USB suspend-resume. An example of how to do this is in the application note 3690, "USB Enumeration Code (and More) for the MAX3420E." The enumeration code in AN3690 was used as the basis for 3420_HIDKB.C.


This function handles device enumeration by inspecting various bytes in the 8-byte SETUP packet, then calling functions to handle the specific request. The code handles only the USB Standard request type, since the application does not implement USB Class or Vendor requests.


This function sends keystrokes according to the Message[] character array in HIDKB_enum_tables.h. The format of three bytes per character is defined in the HID Report Descriptor in the same include file, as shown in Figure 10.

Figure 10. This Report Descriptor specifies the 3-byte data format for a keyboard keystroke.
Figure 10. This Report Descriptor specifies the 3-byte data format for a keyboard keystroke.

Final Thoughts and a Disclaimer

The USB device that you plug into J1 could be any of the one billion or so devices available, so needless to say, there can be a wide range of responses to host enumeration requests. Any device that has been compliance-tested (evidenced by displaying the USB logo) should exhibit no errors when enumerated. The error checking in this program consists of inspecting the HRSL values after every host transaction. This error checking has not been extensively tested since many of the error conditions are difficult to force.

Appendix A

USB Bus Trace for 3420E_HIDKB.C Enumeration

Appendix A
More detailed image
(PDF, 234kB)