NanoVNA-H* – a howto do firmware update using STM32CubeProgrammer

One of the many solutions for updating firmware on the NanoVNA-H* is using ST’s STM32CubeProgrammer.

It would seem that STM32CubeProgrammer deprecates the older DfuSe Demo utility… which remains available for download. Some online experts have inferred that the word Demo in the latter implies it is not the full quid… but they misunderstand the context.

Windows drivers

The two are kind of incompatible in that they use difference device drivers. If you set your machine up for one, it breaks the other until you switch the correct driver in.

STM32CubeProgrammer uses libusbk (or the like) whereas DfuSe Demo uses the STub30 driver.

Above is a dump of the driver properties in my working instance.

Above, a dump from Device Manager. If the “Driver Provider” is “STMicroelectronics”, the driver will not work with STM32CubeProgrammer

Entering the bootloader

The MCU used for NanoVNA-H* has a bootloader in silicon, it is not possible to overwrite the bootloader. The bootloader is enabled by the BOOT0 pin (see nanoVNA-H – recovery), either by connecting a wire, or in later NanoVNA-H* hardware, by holding the jog switch in whilst powering on, or from the menu for later versions of some firmware.

If you have an earlier version, NanoVNA-H – modification of v3.3 PCB to start the bootloader from the jog switch may be of interest.

File format

At this time, STM32CubeProgrammer does not open .dfu files (that might change in time), you need a bin or hex. If your favorite NanoVNA-H* firmware distribution does not have such a file, you need to make one.

.bin

Let’s take an example using a bin file. The bin file does not contain the offset in memory for writing the file, so you need to know that (unless the utility infers it from the chip type it discovers). In this case, the offset is 0x8000000.

Above is a screenshot of STM32CubeProgrammer showing where you check / specify the offset address at which to load the file.

Above is a screenshot after loading the file to the chip. The log window is scrollable, and shows the bin file being written starting at offset 0x8000000.

.hex

A .hex is preferred in some ways as you do not need to enter the offset, the offset is encoded in the file data.

If this is available, use it in preference to .bin or .dfu.

Converting .dfu to .hex

ST’s DFUSE Demo package included a File Manager which can do the conversion.

Alternatively, the following Python script can unpack the files (-d) and names each hex with its load address.

#!/usr/bin/python

# Written by Antonio Galea - 2010/11/18
# Distributed under Gnu LGPL 3.0
# see http://www.gnu.org/licenses/lgpl-3.0.txt

# Modified Owen Duffy 2022/04/05

import sys,struct,zlib,os
import binascii
from optparse import OptionParser

try:
  from intelhex import IntelHex
except ImportError:
  IntelHex = None

DEFAULT_DEVICE="0x0483:0xdf11"
DEFAULT_NAME=b'ST...'

# Prefix and Suffix sizes are derived from ST's DfuSe File Format Specification (UM0391), DFU revision 1.1a
PREFIX_SIZE=11
SUFFIX_SIZE=16

def named(tuple,names):
  return dict(list(zip(names.split(),tuple)))
def consume(fmt,data,names):
  n = struct.calcsize(fmt)
  return named(struct.unpack(fmt,data[:n]),names),data[n:]
def cstring(bytestring):
  return bytestring.partition(b'\0')[0]
def compute_crc(data):
  return 0xFFFFFFFF & -zlib.crc32(data) -1

def parse(file,dump_images=False):
  print('File: "%s"' % file)
  data = open(file,'rb').read()
  crc = compute_crc(data[:-4])
  prefix, data = consume('<5sBIB',data,'signature version size targets')
  print('%(signature)s v%(version)d, image size: %(size)d, targets: %(targets)d' % prefix)
  for t in range(prefix['targets']):
    tprefix, data  = consume('<6sBI255s2I',data,'signature altsetting named name size elements')
    tprefix['num'] = t
    if tprefix['named']:
      tprefix['name'] = cstring(tprefix['name'])
    else:
      tprefix['name'] = ''
    print('%(signature)s %(num)d, alt setting: %(altsetting)s, name: "%(name)s", size: %(size)d, elements: %(elements)d' % tprefix)
    tsize = tprefix['size']
    target, data = data[:tsize], data[tsize:]
    for e in range(tprefix['elements']):
      eprefix, target = consume('<2I',target,'address size')
      eprefix['num'] = e
      print('  %(num)d, address: 0x%(address)08x, size: %(size)d' % eprefix)
      esize = eprefix['size']
      image, target = target[:esize], target[esize:]
      if dump_images:
#        out = '%s.target%d.image%d.bin' % (file,t,e)
        address = eprefix["address"]
        out = "%s.target%d.image%d.0x%08x.bin" % (file, t, e, address)
        open(out,'wb').write(image)
        print('    DUMPED IMAGE TO "%s"' % out)
    if len(target):
      print("target %d: PARSE ERROR" % t)
  suffix = named(struct.unpack('<4H3sBI',data[:SUFFIX_SIZE]),'device product vendor dfu ufd len crc')
  print('usb: %(vendor)04x:%(product)04x, device: 0x%(device)04x, dfu: 0x%(dfu)04x, %(ufd)s, %(len)d, 0x%(crc)08x' % suffix)
  if crc != suffix['crc']:
    print("CRC ERROR: computed crc32 is 0x%08x" % crc)
  data = data[SUFFIX_SIZE:]
  if data:
    print("PARSE ERROR")

def checkbin(binfile):
  data = open(binfile,'rb').read()
  if (len(data) < SUFFIX_SIZE):
    return
  crc = compute_crc(data[:-4])
  suffix = named(struct.unpack('<4H3sBI',data[-SUFFIX_SIZE:]),'device product vendor dfu ufd len crc')
  if crc == suffix['crc'] and suffix['ufd'] == b'UFD':
    print('usb: %(vendor)04x:%(product)04x, device: 0x%(device)04x, dfu: 0x%(dfu)04x, %(ufd)s, %(len)d, 0x%(crc)08x' % suffix)
    print("It looks like the file %s has a DFU suffix!" % binfile)
    print("Please remove any DFU suffix and retry.")
    sys.exit(1)

def build(file,targets,name=DEFAULT_NAME,device=DEFAULT_DEVICE):
  data = b''
  for t,target in enumerate(targets):
    tdata = b''
    for image in target:
      tdata += struct.pack('<2I',image['address'],len(image['data']))+image['data']
      ealt = image['alt']
    tdata = struct.pack('<6sBI255s2I',b'Target',ealt,1,name,len(tdata),len(target)) + tdata
    data += tdata
  data  = struct.pack('<5sBIB',b'DfuSe',1,PREFIX_SIZE + len(data) + SUFFIX_SIZE,len(targets)) + data
  v,d=[int(x,0) & 0xFFFF for x in device.split(':',1)]
  data += struct.pack('<4H3sB',0,d,v,0x011a,b'UFD',SUFFIX_SIZE)
  crc   = compute_crc(data)
  data += struct.pack('<I',crc)
  open(file,'wb').write(data)

if __name__=="__main__":
  usage = """
%prog [-d|--dump] infile.dfu
%prog {-b|--build} address:file.bin [-b address:file.bin ...] [{-D|--device}=vendor:device] outfile.dfu
%prog {-s|--build-s19} file.s19 [{-D|--device}=vendor:device] outfile.dfu
%prog {-i|--build-ihex} file.hex [-i file.hex ...] [{-D|--device}=vendor:device] outfile.dfu"""
  parser = OptionParser(usage=usage)
  parser.add_option("-b", "--build", action="append", dest="binfiles",
    help="Include a raw binary file, to be loaded at the specified address. The BINFILES argument is of the form address:path-to-file. The address can have @X appended where X is the alternate interface number for this binary file. Note that the binary files must not have any DFU suffix!", metavar="BINFILES")
  parser.add_option("-i", "--build-ihex", action="append", dest="hexfiles",
    help="build a DFU file from given Intel HEX HEXFILES", metavar="HEXFILES")
  parser.add_option("-s", "--build-s19", type="string", dest="s19files",
    help="build a DFU file from given S19 S-record S19FILE", metavar="S19FILE")
  parser.add_option("-D", "--device", action="store", dest="device",
    help="build for DEVICE, defaults to %s" % DEFAULT_DEVICE, metavar="DEVICE")
  parser.add_option("-a", "--alt-intf", action="store", dest="alt",
    help="build for alternate interface number ALTINTF, defaults to 0", metavar="ALTINTF")
  parser.add_option("-d", "--dump", action="store_true", dest="dump_images",
    default=False, help="dump contained images to current directory")
  (options, args) = parser.parse_args()

  targets = []

  if options.alt:
    try:
      default_alt = int(options.alt)
    except ValueError:
      print("Alternate interface option argument %s invalid." % options.alt)
      sys.exit(1)
  else:
    default_alt = 0

  if (options.binfiles or options.hexfiles) and len(args)==1:
    target = []
    old_ealt = None

    if options.binfiles:
      for arg in options.binfiles:
        try:
          address,binfile = arg.split(':',1)
        except ValueError:
          print("Address:file couple '%s' invalid." % arg)
          sys.exit(1)
        try:
          address,alts = address.split('@',1)
          if alts:
            try:
              ealt = int(alts)
            except ValueError:
              print("Alternate interface number %s invalid." % alts)
              sys.exit(1)
          else:
            ealt = default_alt
        except ValueError:
            ealt = default_alt
        try:
          address = int(address,0) & 0xFFFFFFFF
        except ValueError:
          print("Address %s invalid." % address)
          sys.exit(1)
        if not os.path.isfile(binfile):
          print("Unreadable file '%s'." % binfile)
          sys.exit(1)
        checkbin(binfile)
        if old_ealt is not None and ealt != old_ealt:
          targets.append(target)
          target = []
        target.append({ 'address': address, 'alt': ealt, 'data': open(binfile,'rb').read() })
        old_ealt = ealt
      targets.append(target)

    if options.hexfiles:
      if not IntelHex:
        print("Error: IntelHex python module could not be found")
        sys.exit(1)
      for hexf in options.hexfiles:
        ih = IntelHex(hexf)
        for (address,end) in ih.segments():
          try:
            address = address & 0xFFFFFFFF
          except ValueError:
            print("Address %s invalid." % address)
            sys.exit(1)
          target.append({ 'address': address, 'alt': default_alt, 'data': ih.tobinstr(start=address, end=end-1)})
      targets.append(target)

    outfile = args[0]
    device = DEFAULT_DEVICE
    if options.device:
      device=options.device
    try:
      v,d=[int(x,0) & 0xFFFF for x in device.split(':',1)]
    except:
      print("Invalid device '%s'." % device)
      sys.exit(1)
    build(outfile,targets,DEFAULT_NAME,device)
  elif options.s19files and len(args)==1:
    address = 0
    data = ""
    target = []
    name = DEFAULT_NAME
    with open(options.s19files) as f:
      lines = f.readlines()
      for line in lines:
          curaddress = 0
          curdata = ""
          line = line.rstrip()
          if line.startswith ( "S0" ):
            name = binascii.a2b_hex(line[8:len(line) - 2])
          elif line.startswith ( "S3" ):
            try:
              curaddress = int(line[4:12], 16) & 0xFFFFFFFF
            except ValueError:
              print("Address %s invalid." % address)
              sys.exit(1)
            curdata = binascii.unhexlify(line[12:-2])
          elif line.startswith ( "S2" ):
            try:
              curaddress = int(line[4:10], 16) & 0xFFFFFFFF
            except ValueError:
              print("Address %s invalid." % address)
              sys.exit(1)
            curdata = binascii.unhexlify(line[10:-2])
          elif line.startswith ( "S1" ):
            try:
              curaddress = int(line[4:8], 16) & 0xFFFFFFFF
            except ValueError:
              print("Address %s invalid." % address)
              sys.exit(1)
            curdata = binascii.unhexlify(line[8:-2])
          if address == 0:
              address = curaddress
              data = curdata
          elif address + len(data) != curaddress:
              target.append({ 'address': address, 'alt': default_alt, 'data': data })
              address = curaddress
              data = curdata
          else:
              data += curdata
    outfile = args[0]
    device = DEFAULT_DEVICE
    if options.device:
      device=options.device
    try:
      v,d=[int(x,0) & 0xFFFF for x in device.split(':',1)]
    except:
      print("Invalid device '%s'." % device)
      sys.exit(1)
    build(outfile,[target],name,device)
  elif len(args)==1:
    infile = args[0]
    if not os.path.isfile(infile):
      print("Unreadable file '%s'." % infile)
      sys.exit(1)
    parse(infile, dump_images=options.dump_images)
  else:
    parser.print_help()
    if not IntelHex:
      print("Note: Intel hex files support requires the IntelHex python module")
    sys.exit(1)

The .hex file can be opened by STM32CubeProgrammer and written to the NanoVNA in DFU boot mode.

Summary

This is a howto but should not be taken as a recommendation over other tools. If you want to use STM32CubeProgrammer for whatever reason, this is how to do it.

This information applies to the NanoVNA-H* as published at this date. It may not apply to other variants, eg NanoVNA-F uses a different method of firmware update.