SAMD11C14 UART/UPDI programmer

This page is documenting my approach in using a SAMD11C14 microcontroller as a UPDI/UART programmer. This project started as a week assignment, but has been improved since then. The features of the board are the following:

  • One jumper to select or UART or UPDI mode
  • Another jumper to set target VCC to 3.3V or 5V
  • Up to 115200 baudrate

Schematic

I started by drawing a schematic in EAGLE. All symbols come from fabcloud, except for the D11C symbol by Shawn Hymel. (click to view in full size)

Let’s review the function of each main component:

The UART connector is directly wired to the RX and TX of the serial 0 on the D11C. On the RX pin, the 1.2 k\Omega resistor offers some protection in case the other device is talking in 5V serial. The internal protection of the D11C saturates inputs to VDD+0.6V, but can only accept a small injection current. According to the datasheet, we should be safe in this case. Let’s compute the current when RX is at 5V:

I_{\rm inj} = \frac{\Delta V}{R} = \frac{5~{\rm V} - (3.3~{\rm V}+0.6~{\rm V})}{1200~\Omega} = 0.9~{\rm mA}~,

which is below the reported limit. When enabling the UPDI mode with the jumper, the RX and TX pins are simple shorted together, along with a 4.7 k\Omega resistor. This resistor limits the current to the TX pin, which is low impedance as it is configured as an output on the D11C. The value of this resistor is not critical, you can also use 5 k\Omega.

On the right, you can see the other jumper which is used to select the VCC of the target device. When shorting the two upper pins, the 5V from USB is used. When shorting the two lower pins, the 3.3V from the regulator is used instead.

Finally, the D11C ship can be programmed through a minimalist JTAG connector. To avoid wasting space on the board, I placed a 2x2 pin connector with only the following:

  1. CLK
  2. DIO
  3. RES
  4. GND

The VCC must be provided from an external source when programming the chip. The simplest option is to connect the USB port at all times.

The files for the schematic are provided at the end of this page.

Board

In this section, I show how the board layout was resolved and manufactured. I started with a drawing in EAGLE, then ported the idea to python thanks to the pcb.py method.

EAGLE

The design was first prototyped in EAGLE. I made sure the board takes minimal space, and jumpers are grouped together in an easy to reach location. The board is designed with a trace clearance of 1/64” in mind, so that it can be machined easily.

Here is the final board:

Let’s review the role of each component:

Note that the RTS and CTS pins of the UART are not used. Also note that there is no power supply on the JTAG connector, as mentioned before. This means that during programming of the chip, you have to either plug in the USB or directly provide 3.3V or 5V at the location of the voltage selection jumper. In case of 3.3V, the voltage regulator will not be active.

You will find the EAGLE board file (.brd) at the end of this page.

Python

As EAGLE is not open-source, I decided to port the board to a different format. The pcb.py system proposed by prof. Gershenfeld is a nice alternative for simple circuits. It lets you place components as Python objects directly in a script, and the wiring is then done manually. If you do it right, you can place every wire in relative positions to a component, meaning your design can be parametric when displacing a component.

The output of pcb.py is a .json file that represents the board as a set of logical operators. This complex function needs can then be rendered by frep.py at an arbitrary resolution, provided in dpi units.

In my week assignment on electronics design, I explained some changes I made to the script. I use the Atom editor with the platformio-ide-terminal package to have a terminal embedded under the code. On the right panel, I open the out.png produced by frep.py with the native image viewer from Atom. After every small change to the board, I can launch a new rendering and see the effect on the board, making my life much easier.

An improvement I made in the script is the addition of chamfer on wires. It is an alternative to a point primitive, where you specify an additional chamfer distance parameter (dist).

The chamfer class is very similar to a point, except it takes two additional parameters, dist and dist2. If provided, dist2 lets you create an asymetrical chamfer size.

class chamfer:
    def __init__(self,x,y,z=0,dist=0.01,dist2=None):
        self.x = x
        self.y = y
        self.z = z
        self.dist1 = dist
        if dist2 is None:
            self.dist2 = dist
        else:
            self.dist2 = dist2

The wire() function is modified to detect chamfer as special cases. Note that the first and last point cannot be a chamfer, and they are ignored in that case.

def wire(pcb,width,*points):
    x0 = points[0].x
    y0 = points[0].y
    z0 = points[0].z
    pcb.board = add(pcb.board,cylinder(x0,y0,z0,z0,width/2))
    for i in range(1,len(points)):
        x1 = points[i].x
        y1 = points[i].y
        z1 = points[i].z
        if isinstance(points[i], chamfer) and i < len(points)-1:
            x2 = points[i+1].x
            y2 = points[i+1].y
            z2 = points[i+1].z
            v0=(x0-x1,y0-y1)
            v2=(x2-x1,y2-y1)
            len0 = math.sqrt(v0[0]*v0[0]+v0[1]*v0[1])
            len2 = math.sqrt(v2[0]*v2[0]+v2[1]*v2[1])
            if len0 == 0 or len2 == 0:
                raise RuntimeError("Cannot create chamfer on 0 length wire")
            d0 = points[i].dist1
            d2 = points[i].dist2
            x1_0, y1_0 = (x1+d0*v0[0]/len0,y1+d0*v0[1]/len0)
            x1_2, y1_2 = (x1+d2*v2[0]/len2,y1+d2*v2[1]/len2)
            pcb.board = add(pcb.board,line(x0,y0,x1_0,y1_0,z1,width))
            pcb.board = add(pcb.board,cylinder(x1_0,y1_0,z1,z1,width/2))
            pcb.board = add(pcb.board,line(x1_0,y1_0,x1_2,y1_2,z1,width))
            pcb.board = add(pcb.board,cylinder(x1_2,y1_2,z1,z1,width/2))
            x0 = x1_2
            y0 = y1_2
            z0 = z1
        else:
            pcb.board = add(pcb.board,line(x0,y0,x1,y1,z1,width))
            pcb.board = add(pcb.board,cylinder(x1,y1,z1,z1,width/2))
            x0 = x1
            y0 = y1
            z0 = z1
    return pcb

Thanks to this system, I could design my board much faster, and the chamfer are a nice touch to reduce EMI. Here is the final result:

You will find the script and its output at the end of this page.

Production

In my week assignment on electronics design, I documented the production of an early version of this board. If you are milling the board, I suggest using a 1/64” tool for fine details, followed by a 1mm tool for large areas and the edge cut.

I used the software from Bantam Tools to setup the first pass with the 1/64” tool.

For the second pass, I use the 1mm tool with an increased clearance of 2.3mm to remove most of the remaining copper. The areas marked in red can be safely ignored, as the previous job already took care of them.

The overall milling time is kept under 20minutes thanks to this trick, and an overlap of only 25% between each toolpath.

Here is one realization of the board with all components:

The pins with the jumper for configuration can be all made from a single 2x3 SMD header with one pin removed.

I tinned the USB traces so they don’t oxidize over time, and the rest of the board is coated with nail polish. To achieve the correct USB connector thickness, you can add a 3D printed bottom plate, also available in the downloads section.

Firmware

To be used as a programmer, the board needs to be loaded with a specific firmware. The role of this firmware is to:

  • Open a serial port on the USB side.
  • Transmit all bytes from the USB serial to the UART/UPDI connector, and vice-versa.

I first tried to use the SAMD port of the Arduino Core, but a specific bug has led me to build my own firmware. I will present both approaches in the following sections.

Arduino core

DISCLAIMER: this method has very limited performance, and will not work with pyupdi.py without a modification (see below).

The Arduino core encompasses a library and toolchain that makes programming embedded devices a lot easier. Thanks to being open source, it has led to many contributions from the community, including new toolchains for various processors.

In the case of the SAMD11C14, a port of the Arduino core has been proposed. In order to use it, you must program your chip with the provided bootloader. It can then be re-programmed through the USB serial port, which is highly convenient. However, the cost is an increased complexity for simple I/O tasks.

I start by downlading the sam_ba_Generic_D11C14A_SAMD11C14A.bin bootloader, as well as edbg to load the binary on the board. When programming the D11C, you have to find a way to connect a JTAG programmer to the 2x2 pin conenctor on the pcb. As the JTAG programmer I have here only supports 1.27mm pitch connectors, I insert individual wires. This is a very gore-ish solution, but remember that programming the D11C is only performed once.

When programming the board, it is important to provide a power supply, as my connector ignores the VCC. I use a USB hub to provide power to both devices simultaneously.

I open a terminal and run the following command to flash the device:

edbg -bpv -e -t samd11 -f sam_ba_Generic_D11C14A_SAMD11C14A.bin

Pay attention to the -e parameter, which tells edbg to completely erase the memory before starting. This makes sure any previously installed bootloader gets removed. The output of the command should look like this:

Debugger: ARM CMSIS-DAP 1093000031387d241239333437353533a5a5a5a597969906 1.10 (S)
Clock frequency: 16.0 MHz
Target: SAM D11C14A (Rev B)
Erasing...  done.
Programming................... done.
Verification................... done.

The board is now ready to use together with the Arduino software: it should appear as a serial port with the name “MattairTech Xeno Mini”.

Make sure you installed the Arduino core for SAMD, and select the right type of board.

Also make sure that you enable both serial ports with the TWO_UART_NO_WIRE_NO_SPI setting under Serial Config:

The following code can then be used. Note that there is no need to provide a baudrate on the USB port, it is dynamically set when talking with the PC. The UART/UPDI serial port, on the other hand, needs to be configured carefully. In my case, I want a baudrate of 115200, 2 stop bits, and even parity because this is what the ATtiny1614 expects. You can consult the Arduino reference for more details on Serial config.

void setup() {
   SerialUSB.begin(0);
   Serial2.begin(115200, SERIAL_8E2);
}

void loop() {
   if (SerialUSB.available()) {
      Serial2.write((char) SerialUSB.read());
   }
   if (Serial2.available()) {
      SerialUSB.write((char) Serial2.read());
   }
}

After hitting the Upload button, the sketch gets compiled and succesfully uploaded to the chip.

The board is now ready. I assumed it would work out of the box, but it wasn’t the end of the story yet. Let’s try to program a simple ATtiny1614 board that I designed during an assignment. The ATtiny1614 can only be programmed through UPDI, and supports both 3.3V and 5V, so it is a good candidate to test the features of our programmer.

I enable 3.3V and UPDI with the adequate jumpers, and connect both devices together.

As with the D11C, I can use a specific port of the Arduino Core for the ATtiny. After installation, I create a basic sketch:

#define LED_PIN 10

void setup() {
    pinMode(LED_PIN, OUTPUT);
}

void loop() {
    digitalWrite(LED_PIN, HIGH);
    delay(500);
    digitalWrite(LED_PIN, LOW);
    delay(500);
}

Unlike our board, the ATtiny1614 could never be programmed through USB. This is why we have to export the binary file by clicking on Sketch -> Export compiled Binary. This produces a .hex file in the sketch folder. Let’s upload this file to the ATtiny using our programmer. This is easily achieved with pyupdi:

pyupdi.py -f <file.hex> -d tiny1614 -c <COM_port> -v

Where <file.hex> is the path to the .hex file, and <COM_port> is the serial port of our programmer. The process starts fine, and chunks of the binary are sent to the ATtiny. However, it fails very quickly:

INFO:phy Opening COM10 at 115200 baud
INFO:phy send : [0x0]
INFO:link STCS to 0x03
INFO:phy send : [0x55, 0xc3, 0x8]
INFO:link STCS to 0x02
INFO:phy send : [0x55, 0xc2, 0x80]
INFO:link LDCS from 0x00
[...]
INFO:phy send : [0x55, 0x65]
INFO:phy send : [0xc, 0x94, 0x56, 0x0, 0xc, 0x94, 0x73, 0x0, 0xc, 0x94, 0x73, 0x0, 0xc, 0x94, 0x73, 0x0, 0xc, 0x94, 0x73, 0x0, 0xc, 0x94, 0x73, 0x0, 0xc, 0x94, 0x73, 0x0, 0xc, 0x94, 0x73, 0x0, 0xc, 0x94, 0x73, 0x0, 0xc, 0x94, 0x73, 0x0, 0xc, 0x94, 0x73, 0x0, 0xc, 0x94, 0x73, 0x0, 0xc, 0x94, 0x73, 0x0, 0xc, 0x94, 0x73, 0x0, 0xc, 0x94, 0x73, 0x0, 0xc, 0x94, 0xf8, 0x0]
INFO:phy incorrect echo : []

This incorrect echo error indicates that bytes have been sent from the PC to our programmer, but they were not sent back to the PC! This is strange, as enabling the UPDI connector forces the RX pin of the D11C to directly receive any data it sends on its TX pin; this is a hardware echo which can hardly fail at all.

After investigating with a custom Python script and pyserial, I discovered that when the PC sends a sequence of exactly 64 bytes or more, the SAMD11C14 simply drops all of them with no futher explanation. This seems related to a buffer overflow, either in the bootloader or in the implementation of the Serial library in the Arduino core that comes with it. After a quick look, I did not find an obvious answer to this. I noticed that the bootloader uses a buffer of 128 bytes, while the Serial library uses 64 bytes. Increasing the latter to 128 did not improve the situation. If you find an easy tweak to solve this issue, don’t hesitate to contact me.

As a temporary fix, you can modify pyupdy itself to slice up the data in chunks smaller than 64 bytes. Open the following file, located in your Python distribution:

<PYTHON_DIST>/Lib/site-packages/updi/physical.py

Find the send() method of the class UpdiPhysical, and modify it to the following:

def send(self, command):
    """
        Sends a char array to UPDI with inter-byte delay
        Note that the byte will echo back
    """
    if len(command) > 32:
        i = 0
        while i < len(command):
            self.send(command[i:i+32])
            i += 32
    else:
        self._loginfo("send", command)
        # it will echo back.
        self.ser.write(command)
        echo = self.ser.read(len(command))
        self._loginfo("echo", list(echo))
        if echo != bytes(command):
            self._loginfo("incorrect echo", echo)
            raise Exception("Incorrect echo data")

This is a simple hack that allows the function to call itself recursively if it detects a command longer than 64. As an arbitrary value, I used chunks of 32 bytes max. Trying to program the ATtiny1614 confirmed that the problem is solved:

[...]
INFO:phy receive : [0x11, 0x24, 0x91, 0x1d, 0x8, 0x95, 0xf8, 0x94, 0xff, 0xcf, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]
Programming successful
INFO:nvm Leaving NVM programming mode
INFO:app Leaving NVM programming mode
INFO:app Apply reset
INFO:link STCS to 0x08
INFO:phy send : [0x55, 0xc8, 0x59]
INFO:phy echo : [0x55, 0xc8, 0x59]
INFO:app Release reset
INFO:link STCS to 0x08
INFO:phy send : [0x55, 0xc8, 0x0]
INFO:phy echo : [0x55, 0xc8, 0x0]
INFO:link STCS to 0x03
INFO:phy send : [0x55, 0xc3, 0xc]
INFO:phy echo : [0x55, 0xc3, 0xc]
INFO:phy Closing COM10

This quick hack was not fully satisfying, so I decided to write my own firmware for the SAMD11C14, which is explained next.

Atmel Studio

Using the Arduino Core for a chip as small as the D11C14A seemed a bit overkill, especially for this task. Therefore, I decided to write my own firmware, where the only feature is to open the USB and UART/UPDI serial ports, and forward any incoming byte. If data is sent faster than the other port can handle, bytes are temporarily stored in a buffer. Having full control of the firmware means I can use buffers of any size I want, and hopefully solve the bug encoutered with the Arduino Core.

You can find the compiled binary below, but if you wish to compile my code from source, you will need to install Atmel Studio 7. My code handles USB data in the main loop, and UART data with an interrupt ISR system.

I started by creating a new GCC C ASF Board Project, with Device set to SAMD11C14A. The editor shows up and lets me edit main.c.

Thanks to the ASF library, handling the USB and UART ports is a lot easier than manupaling the registers manually. By clicking on ASF -> ASF Wizard, you get an overview of the currently loaded ASF modules. Each module comes with its own .c and .h file, added automatically by the wizard.

The configuration of the UART port can be found in the configure_usart() function. In this case, I use even parity and 2 stop bits. You will find below two versions of the firmware. The 8N1 version has no parity, and 1 stop bit. If you need different settings, you need to modify this section of the code:

After compiling the code in Release mode, the resulting .bin file is located in <project-folder>/Release/d11c_serial.bin. As explained before, this binary file can be uploaded to the D11C chip through edbg with the following command:

edbg -bpv -e -t samd11 -f <file.bin>

Where <file.bin> is the file location of the .bin file.

I tested programming my ATtiny1614 with this new firmware loaded on the programmer, and this time it worked with no issue for sequences of 64 bytes or more.

License

This project is provided under the MIT License.

Downloads

EAGLE project

Gerber files

pcb.py

Firmware

3D models