Getting started

Read configuration EEPROM:

fx2tool -S firmware/bootloader/bootloader.ihex read_eeprom 0 7

Load the Blinky example (a LED should be attached to PA0):

make -C examples/blinky load

Blinking a LED

Read the code for the blinky example if you’re looking for something minimal:

main.c
#include <fx2regs.h>
#include <fx2ints.h>

// Register an interrupt handler for TIMER0 overflow
void isr_TF0() __interrupt(_INT_TF0) {
  static int i;
  if(i++ % 64 == 0)
    PA0 = !PA0;
}

int main() {
  // Configure pins
  PA0 = 1;      // set PA0 to high
  OEA = 0b1;    // set PA0 as output

  // Configure TIMER0
  TCON = _M0_0; // use 16-bit counter mode
  ET0 = 1;      // generate an interrupt
  TR0 = 1;      // run

  // Enable interrupts
  EA  = 1;
  while(1);
}
Makefile
TARGET  = blinky

LIBFX2  = ../../firmware/library
include $(LIBFX2)/fx2rules.mk

Interacting over USB

Consider the code of the Cypress bootloader if you want to see how simple USB functionality can be implemented:

main.c
#include <fx2lib.h>
#include <fx2usb.h>
#include <fx2delay.h>
#include <fx2eeprom.h>

usb_desc_device_c usb_device = {
  .bLength              = sizeof(struct usb_desc_device),
  .bDescriptorType      = USB_DESC_DEVICE,
  .bcdUSB               = 0x0200,
  .bDeviceClass         = USB_DEV_CLASS_VENDOR,
  .bDeviceSubClass      = USB_DEV_SUBCLASS_VENDOR,
  .bDeviceProtocol      = USB_DEV_PROTOCOL_VENDOR,
  .bMaxPacketSize0      = 64,
  .idVendor             = 0x04b4,
  .idProduct            = 0x8613,
  .bcdDevice            = 0x0000,
  .iManufacturer        = 1,
  .iProduct             = 2,
  .iSerialNumber        = 0,
  .bNumConfigurations   = 1,
};

usb_desc_interface_c usb_interface = {
  .bLength              = sizeof(struct usb_desc_interface),
  .bDescriptorType      = USB_DESC_INTERFACE,
  .bInterfaceNumber     = 0,
  .bAlternateSetting    = 0,
  .bNumEndpoints        = 0,
  .bInterfaceClass      = USB_IFACE_CLASS_VENDOR,
  .bInterfaceSubClass   = USB_IFACE_SUBCLASS_VENDOR,
  .bInterfaceProtocol   = USB_IFACE_PROTOCOL_VENDOR,
  .iInterface           = 0,
};

usb_configuration_c usb_config = {
  {
    .bLength              = sizeof(struct usb_desc_configuration),
    .bDescriptorType      = USB_DESC_CONFIGURATION,
    .bNumInterfaces       = 1,
    .bConfigurationValue  = 1,
    .iConfiguration       = 0,
    .bmAttributes         = USB_ATTR_RESERVED_1,
    .bMaxPower            = 50,
  },
  {
    { .interface = &usb_interface },
    { 0 }
  }
};

// check for "earlier than 3.5", but version macros shipped in 3.6
#if !defined(__SDCC_VERSION_MAJOR)
__code const struct usb_configuration *__code const usb_configs[] = {
#else
usb_configuration_set_c usb_configs[] = {
#endif
  &usb_config,
};

usb_ascii_string_c usb_strings[] = {
  [0] = "whitequark@whitequark.org",
  [1] = "FX2 series Cypress-class bootloader",
};

usb_descriptor_set_c usb_descriptor_set = {
  .device           = &usb_device,
  .config_count     = ARRAYSIZE(usb_configs),
  .configs          = usb_configs,
  .string_count     = ARRAYSIZE(usb_strings),
  .strings          = usb_strings,
};

enum {
  USB_REQ_CYPRESS_EEPROM_SB  = 0xA2,
  USB_REQ_CYPRESS_EXT_RAM    = 0xA3,
  USB_REQ_CYPRESS_RENUMERATE = 0xA8,
  USB_REQ_CYPRESS_EEPROM_DB  = 0xA9,
  USB_REQ_LIBFX2_PAGE_SIZE   = 0xB0,
};

// We perform lengthy operations in the main loop to avoid hogging the interrupt.
// This flag is used for synchronization between the main loop and the ISR;
// to allow new SETUP requests to arrive while the previous one is still being
// handled (with all data received), the flag should be reset as soon as
// the entire SETUP request is parsed.
volatile bool pending_setup;

void handle_usb_setup(__xdata struct usb_req_setup *req) {
  req;
  if(pending_setup) {
    STALL_EP0();
  } else {
    pending_setup = true;
  }
}

// The EEPROM write cycle time is the same for a single byte or a single page;
// it is therefore far more efficient to write EEPROMs in entire pages.
// Unfortunately, there is no way to discover page size if it is not known
// beforehand. We play it safe and write individual bytes unless the page size
// was set explicitly via a libfx2-specific request, such that Cypress vendor
// requests A2/A9 work the same as in Cypress libraries by default.
uint8_t page_size = 0; // log2(page size in bytes)

void handle_pending_usb_setup() {
  __xdata struct usb_req_setup *req = (__xdata struct usb_req_setup *)SETUPDAT;

  if(req->bmRequestType == (USB_RECIP_DEVICE|USB_TYPE_VENDOR|USB_DIR_OUT) &&
     req->bRequest == USB_REQ_CYPRESS_RENUMERATE) {
    pending_setup = false;

    USBCS |= _DISCON;
    delay_ms(10);
    USBCS &= ~_DISCON;

    return;
  }

  if(req->bmRequestType == (USB_RECIP_DEVICE|USB_TYPE_VENDOR|USB_DIR_OUT) &&
     req->bRequest == USB_REQ_LIBFX2_PAGE_SIZE) {
    page_size = req->wValue;
    pending_setup = false;

    ACK_EP0();
    return;
  }

  if((req->bmRequestType == (USB_RECIP_DEVICE|USB_TYPE_VENDOR|USB_DIR_IN) ||
      req->bmRequestType == (USB_RECIP_DEVICE|USB_TYPE_VENDOR|USB_DIR_OUT)) &&
     (req->bRequest == USB_REQ_CYPRESS_EEPROM_SB ||
      req->bRequest == USB_REQ_CYPRESS_EEPROM_DB)) {
    bool     arg_read  = (req->bmRequestType & USB_DIR_IN);
    bool     arg_dbyte = (req->bRequest == USB_REQ_CYPRESS_EEPROM_DB);
    uint8_t  arg_chip  = arg_dbyte ? 0x51 : 0x50;
    uint16_t arg_addr  = req->wValue;
    uint16_t arg_len   = req->wLength;
    pending_setup = false;

    while(arg_len > 0) {
      uint8_t len = arg_len < 64 ? arg_len : 64;

      if(arg_read) {
        while(EP0CS & _BUSY);
        if(!eeprom_read(arg_chip, arg_addr, EP0BUF, len, arg_dbyte)) {
          STALL_EP0();
          break;
        }
        SETUP_EP0_BUF(len);
      } else {
        SETUP_EP0_BUF(0);
        while(EP0CS & _BUSY);
        if(!eeprom_write(arg_chip, arg_addr, EP0BUF, len, arg_dbyte, page_size,
                         /*timeout=*/166)) {
          STALL_EP0();
          break;
        }
      }

      arg_len  -= len;
      arg_addr += len;
    }

    return;
  }

  if((req->bmRequestType == (USB_RECIP_DEVICE|USB_TYPE_VENDOR|USB_DIR_IN) ||
      req->bmRequestType == (USB_RECIP_DEVICE|USB_TYPE_VENDOR|USB_DIR_OUT)) &&
     req->bRequest == USB_REQ_CYPRESS_EXT_RAM) {
    bool     arg_read = (req->bmRequestType & USB_DIR_IN);
    uint16_t arg_addr = req->wValue;
    uint16_t arg_len  = req->wLength;
    pending_setup = false;

    while(arg_len > 0) {
      uint8_t len = arg_len < 64 ? arg_len : 64;

      if(arg_read) {
        while(EP0CS & _BUSY);
        xmemcpy(EP0BUF, (__xdata void *)arg_addr, len);
        SETUP_EP0_BUF(len);
      } else {
        SETUP_EP0_BUF(0);
        while(EP0CS & _BUSY);
        xmemcpy((__xdata void *)arg_addr, EP0BUF, arg_len);
      }

      arg_len  -= len;
      arg_addr += len;
    }

    return;
  }

  STALL_EP0();
}

int main() {
  CPUCS = _CLKOE|_CLKSPD1;

  // Don't re-enumerate. `fx2tool -B` will load this firmware to access EEPROM, and it
  // expects to be able to keep accessing the device. If you are using this firmware
  // in your own code, set /*diconnect=*/true.
  usb_init(/*disconnect=*/false);

  while(1) {
    if(pending_setup)
      handle_pending_usb_setup();
  }
}
Makefile
TARGET    = boot-cypress
LIBRARIES = fx2 fx2usb fx2isrs

LIBFX2 	= ../library
include $(LIBFX2)/fx2rules.mk

Adding an DFU bootloader

It is easy to integrate a standards-compliant and OS-agnostic Device Firmware Upgrade bootloader as libfx2 provides all necessary infrastructure, and it only needs to be configured for a specific board and integrated into a target application:

main.c
#include <fx2lib.h>
#include <fx2delay.h>
#include <fx2eeprom.h>
#include <fx2usbdfu.h>

// Replace this with the actual EEPROM size on your board to use its full capacity.
#define FIRMWARE_SIZE 16384

// Application mode descriptors.

usb_desc_device_c usb_device = {
  .bLength              = sizeof(struct usb_desc_device),
  .bDescriptorType      = USB_DESC_DEVICE,
  .bcdUSB               = 0x0200,
  .bDeviceClass         = USB_DEV_CLASS_PER_INTERFACE,
  .bDeviceSubClass      = USB_DEV_SUBCLASS_PER_INTERFACE,
  .bDeviceProtocol      = USB_DEV_PROTOCOL_PER_INTERFACE,
  .bMaxPacketSize0      = 64,
  .idVendor             = 0x04b4,
  .idProduct            = 0x8613,
  .bcdDevice            = 0x0000,
  .iManufacturer        = 1,
  .iProduct             = 2,
  .iSerialNumber        = 0,
  .bNumConfigurations   = 1,
};

extern usb_dfu_desc_functional_c usb_dfu_functional;

usb_desc_interface_c usb_interface_dfu_runtime = {
  .bLength              = sizeof(struct usb_desc_interface),
  .bDescriptorType      = USB_DESC_INTERFACE,
  .bInterfaceNumber     = 0,
  .bAlternateSetting    = 0,
  .bNumEndpoints        = 0,
  .bInterfaceClass      = USB_IFACE_CLASS_APP_SPECIFIC,
  .bInterfaceSubClass   = USB_IFACE_SUBCLASS_DFU,
  .bInterfaceProtocol   = USB_IFACE_PROTOCOL_DFU_RUNTIME,
  .iInterface           = 0,
};

usb_configuration_c usb_config_app = {
  {
    .bLength              = sizeof(struct usb_desc_configuration),
    .bDescriptorType      = USB_DESC_CONFIGURATION,
    .bNumInterfaces       = 1,
    .bConfigurationValue  = 1,
    .iConfiguration       = 0,
    .bmAttributes         = USB_ATTR_RESERVED_1,
    .bMaxPower            = 50,
  },
  {
    { .interface = &usb_interface_dfu_runtime },
    { .generic   = (struct usb_desc_generic *) &usb_dfu_functional },
    { 0 }
  }
};

usb_configuration_set_c usb_configs_app[] = {
  &usb_config_app,
};

usb_ascii_string_c usb_strings_app[] = {
  [0] = "whitequark@whitequark.org",
  [1] = "Example application with DFU support",
};

// DFU mode descriptors

usb_desc_interface_c usb_interface_dfu_upgrade = {
  .bLength              = sizeof(struct usb_desc_interface),
  .bDescriptorType      = USB_DESC_INTERFACE,
  .bInterfaceNumber     = 0,
  .bAlternateSetting    = 0,
  .bNumEndpoints        = 0,
  .bInterfaceClass      = USB_IFACE_CLASS_APP_SPECIFIC,
  .bInterfaceSubClass   = USB_IFACE_SUBCLASS_DFU,
  .bInterfaceProtocol   = USB_IFACE_PROTOCOL_DFU_UPGRADE,
  .iInterface           = 3,
};

usb_dfu_desc_functional_c usb_dfu_functional = {
  .bLength              = sizeof(struct usb_dfu_desc_functional),
  .bDescriptorType      = USB_DESC_DFU_FUNCTIONAL,
  .bmAttributes         = USB_DFU_ATTR_CAN_DNLOAD |
                          USB_DFU_ATTR_CAN_UPLOAD |
                          USB_DFU_ATTR_MANIFESTATION_TOLERANT |
                          USB_DFU_ATTR_WILL_DETACH,
  .wTransferSize        = 64,
  .bcdDFUVersion        = 0x0101,
};

usb_configuration_c usb_config_dfu = {
  {
    .bLength              = sizeof(struct usb_desc_configuration),
    .bDescriptorType      = USB_DESC_CONFIGURATION,
    .bNumInterfaces       = 1,
    .bConfigurationValue  = 1,
    .iConfiguration       = 0,
    .bmAttributes         = USB_ATTR_RESERVED_1,
    .bMaxPower            = 50,
  },
  {
    { .interface = &usb_interface_dfu_upgrade },
    { .generic   = (struct usb_desc_generic *) &usb_dfu_functional },
    { 0 }
  }
};

usb_configuration_set_c usb_configs_dfu[] = {
  &usb_config_dfu,
};

usb_ascii_string_c usb_strings_dfu[] = {
  [0] = "whitequark@whitequark.org",
  [1] = "FX2 series DFU-class bootloader",
  [2] = "Boot EEPROM"
};

// Application and DFU code

__xdata struct usb_descriptor_set usb_descriptor_set = {
  .device           = &usb_device,
  .config_count     = ARRAYSIZE(usb_configs_app),
  .configs          = usb_configs_app,
  .string_count     = ARRAYSIZE(usb_strings_app),
  .strings          = usb_strings_app,
};

usb_dfu_status_t firmware_upload(uint32_t address, __xdata uint8_t *data,
                                 __xdata uint16_t *length) __reentrant {
  if(address < FIRMWARE_SIZE) {
    // Only 2-byte EEPROMs are large enough to store any sort of firmware, and the address
    // of a 2-byte boot EEPROM is fixed, so it's safe to hardcode it here.
    if(eeprom_read(0x51, address, data, *length, /*double_byte=*/true)) {
      return USB_DFU_STATUS_OK;
    } else {
      return USB_DFU_STATUS_errUNKNOWN;
    }
  } else {
    *length = 0;
    return USB_DFU_STATUS_OK;
  }
}

usb_dfu_status_t firmware_dnload(uint32_t address, __xdata uint8_t *data,
                                 uint16_t length) __reentrant {
  if(length == 0) {
    if(address == FIRMWARE_SIZE)
      return USB_DFU_STATUS_OK;
    else
      return USB_DFU_STATUS_errNOTDONE;
  } else if(address < FIRMWARE_SIZE) {
    // Use 8-byte page writes, which are slow but universally compatible. (Strictly speaking,
    // no EEPROM can be assumed to provide any page writes, but virtually every EEPROM larger
    // than 16 KiB supports at least 8-byte pages).
    //
    // If the datasheet for the EEPROM lists larger pages as permissible, this would provide
    // a significant speed boost. Unfortunately it is not really possible to discover the page
    // size by interrogating the EEPROM.
    if(eeprom_write(0x51, address, data, length, /*double_byte=*/true,
                    /*page_size=*/3, /*timeout=*/166)) {
      return USB_DFU_STATUS_OK;
    } else {
      return USB_DFU_STATUS_errWRITE;
    }
  } else {
    return USB_DFU_STATUS_errADDRESS;
  }
}

usb_dfu_status_t firmware_manifest() __reentrant {
  // Simulate committing the firmware. If this function is not necessary, it may simply be omitted,
  // together with its entry in `usb_dfu_iface_state`.
  delay_ms(1000);

  return USB_DFU_STATUS_OK;
}

usb_dfu_iface_state_t usb_dfu_iface_state = {
  // Set to bInterfaceNumber of the DFU descriptor in the application mode.
  .interface          = 0,

  .firmware_upload    = firmware_upload,
  .firmware_dnload    = firmware_dnload,
  .firmware_manifest  = firmware_manifest,
};

void handle_usb_setup(__xdata struct usb_req_setup *req) {
  if(usb_dfu_setup(&usb_dfu_iface_state, req))
    return;

  STALL_EP0();
}

int main() {
  // Run core at 48 MHz fCLK.
  CPUCS = _CLKSPD1;

  // Re-enumerate, to make sure our descriptors are picked up correctly.
  usb_init(/*disconnect=*/true);

  while(1) {
    // Handle switching to DFU mode from application mode.
    if(usb_dfu_iface_state.state == USB_DFU_STATE_appDETACH) {
      // Wait until the host has received our ACK of the DETACH request before actually
      // disconnecting. This is because if we disconnect immediately, the host might just
      // return an error to the DFU utility.
      delay_ms(10);
      USBCS |= _DISCON;

      // Switch to DFU mode.
      usb_dfu_iface_state.state = USB_DFU_STATE_dfuIDLE;

      // Re-enumerate using the DFU mode descriptors. For Windows compatibility, it is necessary
      // to change USB Product ID in the Device descriptor as well, since Windows is unable
      // to rebind a DFU driver to the same VID:PID pair. (Windows is euphemistically called out
      // in the DFU spec as a "certain operating system").
      ((usb_desc_device_t *)usb_device)->idProduct++;
      usb_descriptor_set.config_count = ARRAYSIZE(usb_configs_dfu);
      usb_descriptor_set.configs      = usb_configs_dfu;
      usb_descriptor_set.string_count = ARRAYSIZE(usb_strings_dfu);
      usb_descriptor_set.strings      = usb_strings_dfu;

      // Don't reconnect again in `usb_init`, as we have just disconnected explicitly.
      usb_init(/*disconnect=*/false);
    }

    // Handle any lengthy DFU requests, i.e. the ones that call back into firmware_* functions.
    usb_dfu_setup_deferred(&usb_dfu_iface_state);
  }
}
Makefile
TARGET    = boot-dfu
LIBRARIES = fx2 fx2usb fx2dfu fx2isrs
MODEL     = small

LIBFX2  = ../library
include $(LIBFX2)/fx2rules.mk

The DFU images suitable for flashing can be generated from Intel HEX firmware images using the dfu subcommand of the command-line tool.

Adding an UF2 bootloader

It is easy to integrate a very versatile and OS-agnostic UF2 compliant bootloader as libfx2 provides all necessary infrastructure, and it only needs to be configured for a specific board and integrated into a target application:

main.c
#include <fx2lib.h>
#include <fx2delay.h>
#include <fx2eeprom.h>
#include <fx2usbmassstor.h>
#include <fx2uf2.h>

usb_desc_device_c usb_device = {
  .bLength              = sizeof(struct usb_desc_device),
  .bDescriptorType      = USB_DESC_DEVICE,
  .bcdUSB               = 0x0200,
  .bDeviceClass         = USB_DEV_CLASS_PER_INTERFACE,
  .bDeviceSubClass      = USB_DEV_SUBCLASS_PER_INTERFACE,
  .bDeviceProtocol      = USB_DEV_PROTOCOL_PER_INTERFACE,
  .bMaxPacketSize0      = 64,
  .idVendor             = 0x04b4,
  .idProduct            = 0x8613,
  .bcdDevice            = 0x0000,
  .iManufacturer        = 1,
  .iProduct             = 2,
  .iSerialNumber        = 3,
  .bNumConfigurations   = 1,
};

usb_desc_interface_c usb_interface_mass_storage = {
  .bLength              = sizeof(struct usb_desc_interface),
  .bDescriptorType      = USB_DESC_INTERFACE,
  .bInterfaceNumber     = 0,
  .bAlternateSetting    = 0,
  .bNumEndpoints        = 2,
  .bInterfaceClass      = USB_IFACE_CLASS_MASS_STORAGE,
  .bInterfaceSubClass   = USB_IFACE_SUBCLASS_MASS_STORAGE_SCSI,
  .bInterfaceProtocol   = USB_IFACE_PROTOCOL_MASS_STORAGE_BBB,
  .iInterface           = 0,
};

usb_desc_endpoint_c usb_endpoint_ep2_out = {
  .bLength              = sizeof(struct usb_desc_endpoint),
  .bDescriptorType      = USB_DESC_ENDPOINT,
  .bEndpointAddress     = 2,
  .bmAttributes         = USB_XFER_BULK,
  .wMaxPacketSize       = 512,
  .bInterval            = 0,
};

usb_desc_endpoint_c usb_endpoint_ep6_in = {
  .bLength              = sizeof(struct usb_desc_endpoint),
  .bDescriptorType      = USB_DESC_ENDPOINT,
  .bEndpointAddress     = 6|USB_DIR_IN,
  .bmAttributes         = USB_XFER_BULK,
  .wMaxPacketSize       = 512,
  .bInterval            = 0,
};

usb_configuration_c usb_config = {
  {
    .bLength              = sizeof(struct usb_desc_configuration),
    .bDescriptorType      = USB_DESC_CONFIGURATION,
    .bNumInterfaces       = 1,
    .bConfigurationValue  = 1,
    .iConfiguration       = 0,
    .bmAttributes         = USB_ATTR_RESERVED_1,
    .bMaxPower            = 50,
  },
  {
    { .interface  = &usb_interface_mass_storage },
    { .endpoint   = &usb_endpoint_ep2_out },
    { .endpoint   = &usb_endpoint_ep6_in  },
    { 0 }
  }
};

usb_configuration_set_c usb_configs[] = {
  &usb_config,
};

usb_ascii_string_c usb_strings[] = {
  [0] = "whitequark@whitequark.org",
  [1] = "FX2 series UF2-class bootloader",
  // USB MS BBB 4.1.1 requires each device to have an unique serial number that is at least
  // 12 characters long. We cannot satisfy the uniqueness requirement, but we at least provide
  // a serial number in a valid format.
  [2] = "000000000000",
};

usb_descriptor_set_c usb_descriptor_set = {
  .device          = &usb_device,
  .config_count    = ARRAYSIZE(usb_configs),
  .configs         = usb_configs,
  .string_count    = ARRAYSIZE(usb_strings),
  .strings         = usb_strings,
};

usb_mass_storage_bbb_state_t usb_mass_storage_state = {
  .interface    = 0,
  .max_in_size  = 512,

  .command      = uf2_scsi_command,
  .data_out     = uf2_scsi_data_out,
  .data_in      = uf2_scsi_data_in,
};

static bool firmware_read(uint32_t address, __xdata uint8_t *data, uint16_t length) __reentrant {
  // Only 2-byte EEPROMs are large enough to store any sort of firmware, and the address
  // of a 2-byte boot EEPROM is fixed, so it's safe to hardcode it here.
  return eeprom_read(0x51, address, data, length, /*double_byte=*/true);
}

static bool firmware_write(uint32_t address, __xdata uint8_t *data, uint16_t length) __reentrant {
  // Use 8-byte page writes, which are slow but universally compatible. (Strictly speaking,
  // no EEPROM can be assumed to provide any page writes, but virtually every EEPROM larger
  // than 16 KiB supports at least 8-byte pages).
  //
  // If the datasheet for the EEPROM lists larger pages as permissible, this would provide
  // a significant speed boost. Unfortunately it is not really possible to discover the page
  // size by interrogating the EEPROM.
  return eeprom_write(0x51, address, data, length, /*double_byte=*/true,
                      /*page_size=*/3, /*timeout=*/166);
}

// Configure for 16Kx8 EEPROM, since this is upwards compatible with larger EEPROMs and
// any application integrating the UF2 bootloader will be at least ~12 KB in size.
// (The overhead of the bootloader is smaller than that, since much of the USB machinery
// can be shared between the bootloader and the application.)
uf2_configuration_c uf2_config = {
  // Provide a virtual mass storage device of 32 MiB in size. Using a device that is
  // too small will result in broken filesystem being generated (in particular, below
  // a certain cluster count, the filesystm gets interpreted as FAT12 instead of FAT16),
  // and a device that is too large will result in slower operations (mounting, etc).
  // 32 MiB is a good number.
  .total_sectors  = 2 * 32768,
  // Replace the Model: and Board-ID: fields with ones specific for your board.
  // Note that Board-ID: field should be machine-readable.
  // The INFO_UF2.TXT file can be up to 512 bytes in size.
  .info_uf2_txt   =
    "UF2 Bootloader for Cypress FX2\r\n"
    "Model: Generic Developer Board with 16Kx8 EEPROM\r\n"
    "Board-ID: FX2-Generic_16Kx8-v0\r\n",
  // Replace the URL with a hyperlink to a document describing your board.
  .index_htm      =
    "<meta http-equiv=\"refresh\" content=\"0; url=https://github.com/whitequark/libfx2/\">",
  // Replace this with the actual EEPROM size on your board to use its full capacity.
  .firmware_size  = 16384,

  .firmware_read  = firmware_read,
  .firmware_write = firmware_write,
};

void handle_usb_setup(__xdata struct usb_req_setup *req) {
  if(usb_mass_storage_bbb_setup(&usb_mass_storage_state, req))
    return;

  STALL_EP0();
}

volatile bool pending_ep6_in;

void isr_IBN() __interrupt {
  pending_ep6_in = true;
  CLEAR_USB_IRQ();
  NAKIRQ = _IBN;
  IBNIRQ = _IBNI_EP6;
}

int main() {
  // Run core at 48 MHz fCLK.
  CPUCS = _CLKSPD1;

  // Use newest chip features.
  REVCTL = _ENH_PKT|_DYN_OUT;

  // NAK all transfers.
  SYNCDELAY;
  FIFORESET = _NAKALL;

  // EP2 is configured as 512-byte double buffed BULK OUT.
  EP2CFG  =  _VALID|_TYPE1|_BUF1;
  EP2CS   = 0;
  // EP6 is configured as 512-byte double buffed BULK IN.
  EP6CFG  =  _VALID|_DIR|_TYPE1|_BUF1;
  EP6CS   = 0;
  // EP4/8 are not used.
  EP4CFG &= ~_VALID;
  EP8CFG &= ~_VALID;

  // Enable IN-BULK-NAK interrupt for EP6.
  IBNIE = _IBNI_EP6;
  NAKIE = _IBN;

  // Reset and prime EP2, and reset EP6.
  SYNCDELAY;
  FIFORESET = _NAKALL|2;
  SYNCDELAY;
  OUTPKTEND = _SKIP|2;
  SYNCDELAY;
  OUTPKTEND = _SKIP|2;
  SYNCDELAY;
  FIFORESET = _NAKALL|6;
  SYNCDELAY;
  FIFORESET = 0;

  // Re-enumerate, to make sure our descriptors are picked up correctly.
  usb_init(/*disconnect=*/true);

  while(1) {
    if(!(EP2CS & _EMPTY)) {
      uint16_t length = (EP2BCH << 8) | EP2BCL;
      if(usb_mass_storage_bbb_bulk_out(&usb_mass_storage_state, EP2FIFOBUF, length)) {
        EP2BCL = 0;
      } else {
        EP2CS  = _STALL;
        EP6CS  = _STALL;
      }
    }

    if(pending_ep6_in) {
      __xdata uint16_t length;
      if(usb_mass_storage_bbb_bulk_in(&usb_mass_storage_state, EP6FIFOBUF, &length)) {
        if(length > 0) {
          EP6BCH = length >> 8;
          SYNCDELAY;
          EP6BCL = length;
        }
      } else {
        EP6CS  = _STALL;
      }

      pending_ep6_in = false;
    }
  }
}
Makefile
TARGET    = boot-uf2
LIBRARIES = fx2 fx2usb fx2usbmassstor fx2uf2 fx2isrs
MODEL     = medium

LIBFX2  = ../library
include $(LIBFX2)/fx2rules.mk

The UF2 images suitable for flashing can be generated from Intel HEX firmware images using the uf2 subcommand of the command-line tool.