PIC32MZ USB: What they don't want you to know
This document is intended as a supplement to the "datasheet" for the PIC32MZ USB controller. I've only messed with device mode so this is all exclusive to it.
The USB controller for the PIC32MZ is reasonably nice in that it does most of the transaction processing (SETUP, IN, OUT, DATA0 etc packets) in hardware and gives you just the USB data packets. You can set up up to 7 receive and 7 transmit endpoint. (Endpoint 0 has its own special hardware). Note that the endpoint number in hardware does not need to match the USB endpoint number (specified in the endpoint descriptor).
The USB controller has its own RAM, which I think is 4096x64 bits large. As far as I can tell you have to allocate FIFOs yourself in this RAM. The datasheet mentions "dynamic FIFO sizing" but I can't figure out what it is or does.
Note that USB reset will reset a lot of the registers and you should do most initialisation in the USB reset interrupt handler.
Registers — annotations
This is a selection of notes and corrections to the register list (pretty much the only thing in the datasheet).
Note that some bits have different names in xc.h for reasons I don't know.
|Registers relevant to your interests||Registers you can ignore (probably)|
If you use DMA:
USBCSR2: Note that connect/disconnect interrupts only apply to the host. The device has to use VBUS if it wants to find out it's been disconnected (and then wait for USB reset to consider itself connected again).
USBCSR3: The testbits are useful to implement the USB 2.0 test modes (see USB 2.0 spec). Not that I'm aware of anything that actually uses these modes. The ENDPOINT field selects the endpoint for the following registers:
- USBIE0CSR* / USBIENCSR* (different layout depending on ENDPOINT==0 or not). Note that these registers can also be accessed under USBECSR without touching the ENDPOINT at all.
- USBFIFOA (this is undocumented)
- USBOTG RX/TXFIFOSZ (not sure, also undocumented)
- SETUPEND is normal during zero length control transfers (see below)
- DATAEND is much more complicated than the datasheet makes it sound (see below)
- TXPKTRDY: setting this bit will generate an interrupt once the transfer is complete
- NAKLIM & SPEED are host-only
USBIENCSR0: this register corresponds to the TX endpoint
- MODE: not sure what this is for, probably can be ignored.
- DMAREQMD: no clue what this is.
USBIENCSR1: this register corresponds to the RX endpoint
- DMAREQCMD: no clue what this is.
USBIENCSR2: this register corresponds to the TX endpoint (except for RXCNT)
- TEP: setting this register activates the endpoint for transmission
USBIENCSR3: this register corresponds to the RX endpoint (except for TXFIFOSZ)
- RXFIFOSZ/TXFIFOSZ: i don't know what these registers do. since dynamic sizing (whatever that is) is supported, they are likely invalid.
- SPEED: host only (i think)
- TEP: setting this register activates the endpoint for reception
- RXPDB/TXPDB: i think double buffering literally just means having two packets in the fifo instead of one
- RXFIFOSZ/TXFIFOSZ: i think these are multiplexed per endpoint (see above)
USBFIFOA: the endpoint this register corresponds to is set by the ENDPOINT field (see above)
- DMAMODE: no clue what this is.
(Using the notation from xc.h, not the datasheet)
To implement control transfers right you need to keep a state machine in the driver that matches the state in the hardware. Each interrupt potentially causes a transition. If an interrupt comes in with STALLED bit set, clear the bit and ignore the interrupt. If SETEND is set, the host aborted the request (except in the zero length case, see below). Set SETENDC, return to idle state and check for RXRDY (without waiting for another interrupt).
Idle state: An interrupt with RXRDY set corresponds to the arrival of a setup packet. Read the packet from the FIFO, parse it according to the USB spec and
if wLength=0, this is zero length control transfer. Process the request.
If the request is successful, set RXRDYC and DATAEND and go to the zero length status state.
If the request is unsuccessful, set SENDSTALL and stay in the idle state.
if bit 7 is set in bmRequestType, this is a control read. Process the request, keeping the response in a buffer.
If the request is successful, set RXRDYC and go to the transmit packet state.
If the request is unccessful, set SENDSTALL and stay in the idle state.
otherwise, this is a control write. Set RXRDYC and go to the receive packet state.
Transmit packet state: Copy the packet into the transmit FIFO. If there are more packets, set TXRDY and go to state "wait for transmit interrupt". If there are no more packets, set TXRDY and DATAEND and go to state "wait for status".
Wait for transmit interrupt: An interrupt (which isn't marked by any special bits!) confirms the transmission of the packet. Go back to transmit packet state.
Wait for status: An interrupt marks the completion of status stage. Complete any "post-status" operations (if the request has any) and go back to idle state.
Zero length status state: Identical to "wait for status", but ignore SETEND if it is set (set SETENDC, though).
Receive packet state: An interrupt with RXRDY corresponds to the arrival of a data packet. Remove the packet from the FIFO.
If the packet has fewer than the maximum number of bytes (usually 64 bytes) or wLength bytes have been sent in total, this is the last packet. In that case, process the request. If the request is successful, set RXRDYC and DATAEND and go to the "wait for status" state.
If the request is unsuccessful, set SENDSTALL and go to the idle state.
Otherwise, set RXRDYC and stay in the state.
These are just some USB "gotchas" that aren't necessarily specific to the PIC32MZ.
- The Set_Address request changes the address after the status stage. You must update the FUNC field only after you get the status interrupt.
- When responding to a control read, if your response length is a multiple of the packet size, you need to send a zero length packet after the final packet if and only if you are sending less than wLength bytes.