Yesterday I had some time to continue debugging the C code that I had been porting from my previously developed Arduino code, which uses megaTinyCore, over to plain C using the MPLAB tools in Visual Studio Code.

As suspected, the problem turned out to be a USART register configuration issue, although it wasn’t quite the issue I had originally expected.

My first thought was that I had failed to correctly configure the GPIO line connected to the DE, or Driver Enable, pin of the RS485 transceiver, which in this case is a TI ISO1430.

However, having seen the RO line flickering on the receiving board, the RS485 driver was clearly being enabled and transmitting something, so that wasn’t the problem.

My suspicion then moved towards a timing issue.

I checked the internal oscillator, which was configured for 20 MHz, and then checked the clock divider, which divides this by two to produce a 10 MHz CPU/peripheral clock.

That was also configured correctly.

Before breaking out the oscilloscope and starting to examine the individual pulses, I decided to reduce the USART speed to 9600 baud.

It immediately started working.

Okay, that’s suspicious.

I already knew that the hardware setup could operate at up to 921600 baud from my previous testing, so this wasn’t a cabling or RS485 signal-quality problem.

Clearly I was missing something in the USART configuration.

It turned out to be related to the configuration of USART0.CTRLB, specifically this line:

USART0.CTRLB |= USART_TXEN_bm | USART_RXMODE_CLK2X_gc;

USART_TXEN_bm simply enables the USART transmitter.

The interesting part is:

USART_RXMODE_CLK2X_gc

Despite the name containing RXMODE, this setting also affects the baud-rate generator used by the transmitter.

The ATtiny1614 USART supports two asynchronous receiver modes:

  • USART_RXMODE_NORMAL_gc — 16 samples per bit
  • USART_RXMODE_CLK2X_gc — 8 samples per bit

CLK2X therefore selects double-speed asynchronous operation.

With NORMAL mode, the baud-rate calculation uses a sampling factor of 16. With CLK2X mode it uses a sampling factor of 8.

At a 10 MHz peripheral clock, this gives theoretical maximum asynchronous baud rates of:

10,000,000 / 16 = 625,000 baud

and with CLK2X:

10,000,000 / 8 = 1,250,000 baud

This explains why trying to operate at 921600 baud without CLK2X enabled caused problems.

921600 baud is above the maximum rate available using the normal 16x asynchronous sampling mode.

But 921600 Doesn’t Divide Nicely Into 10 MHz#

There is another interesting part to this.

If the maximum CLK2X rate is:

1,250,000 baud

then 921600 baud doesn’t divide neatly into that.

So how does the ATtiny1614 generate it?

The answer is that the USART baud-rate generator supports fractional baud-rate divisors.

The USART0.BAUD register effectively provides six fractional bits, giving 64 fractional steps between integer divider values.

The baud-rate register calculation for asynchronous mode is:

BAUD = (64 × fCLK_PER) / (S × desired baud)

where:

S = 16 for NORMAL mode
S = 8 for CLK2X mode

For a 10 MHz peripheral clock and 921600 baud using CLK2X:

BAUD = (64 × 10,000,000) / (8 × 921,600)

Which gives:

BAUD = 86.8055...

The register can only contain an integer value, so this is rounded to:

USART0.BAUD = 87

Because the register uses six fractional bits, that represents an effective divider of:

87 / 64 = 1.359375

We can then calculate the actual baud rate produced:

Baud = 10,000,000 / (8 × 1.359375)

Giving:

Baud = 919,540.23

Compared with the requested:

921,600

that’s an error of approximately:

-0.22%

which is comfortably close enough for asynchronous USART communication in this setup.

This fractional baud-rate generator is what allows the ATtiny1614 to generate useful baud rates that don’t divide neatly into the peripheral clock.

Transmit and Receive#

Another thing worth mentioning is that the transmitter and receiver can operate independently as part of the same USART peripheral.

However, both use the same baud-rate generator.

The term asynchronous in this context doesn’t mean that the transmitter and receiver are asynchronous to each other.

It means that there is no separate clock signal shared between the transmitting and receiving devices.

Instead, both sides agree on a baud rate and the receiver samples the incoming data stream to determine the state of each bit.

In NORMAL mode the receiver has 16 sample positions per bit.

In CLK2X mode this is reduced to eight sample positions per bit, which allows the USART to operate at approximately twice the maximum baud rate for the same peripheral clock.

The Code#

The following is the working code.

You will notice the USART0_BAUD_RATE definition. With a 10 MHz peripheral clock, CLK2X enabled and a requested baud rate of 921600, this ultimately produces a value of approximately 87 for the USART BAUD register.

/**
 * @file main.c
 * @author jktde
 * @date 2026-08-28
 * @brief Main function
 */

#define F_CPU 10000000UL

#define USART0_BAUD_RATE(BAUD_RATE) \
    ((uint16_t)(((F_CPU * 64.0) / (8.0 * (BAUD_RATE))) + 0.5))

#include <avr/io.h>
#include <avr/delay.h>
#include <string.h>
#include <avr/cpufunc.h>

void setupClock() {
    // Set the internal oscillator to 20 MHz
    _PROTECTED_WRITE(
        CLKCTRL.MCLKCTRLA,
        CLKCTRL_CLKSEL_OSC20M_gc
    );

    // Divide by two, producing a 10 MHz clock
    _PROTECTED_WRITE(
        CLKCTRL.MCLKCTRLB,
        CLKCTRL_PEN_bm | CLKCTRL_PDIV_2X_gc
    );
}

void setupLed() {
    // Set PA5, connected to the carrier-board LED, as an output
    PORTA_DIR |= PIN5_bm;
}

void setupUsart() {
    // Set PB2 as an output
    PORTB_DIR |= PIN2_bm;

    // Set PB3 as an input
    PORTB_DIR &= ~PIN3_bm;

    // Configure USART0.
    // The ATtiny1614 only has one USART.
    USART0.BAUD = USART0_BAUD_RATE(921600);

    // Configure USART for 8 data bits, no parity and one stop bit
    USART0.CTRLC = USART_CHSIZE_8BIT_gc;

    // Enable the transmitter and select CLK2X asynchronous mode
    USART0.CTRLB |= USART_TXEN_bm | USART_RXMODE_CLK2X_gc;

    // Enable receive operations if required
    // USART0.CTRLB |= USART_RXEN_bm;

    // Set PA6 as an output.
    // This controls the RS485 Driver Enable (DE) line.
    PORTA_DIR |= PIN6_bm;

    // Enable the RS485 transmitter
    PORTA.OUT |= PIN6_bm;
}

void sendChar(char c)
{
    // Wait until the transmit data register is empty
    while (!(USART0.STATUS & USART_DREIF_bm))
    {
        ;
    }

    // Send the character
    USART0.TXDATAL = c;
}

void sendString(char *str)
{
    // Loop through the character array and send each character
    for (size_t i = 0; i < strlen(str); i++)
    {
        sendChar(str[i]);
    }
}

int main()
{
    setupClock();
    setupLed();
    setupUsart();

    while (1)
    {
        PORTA.OUT |= PIN5_bm;

        _delay_ms(1000);

        sendString("Hello from ATtiny1614\n");

        PORTA.OUT &= ~PIN5_bm;

        _delay_ms(1000);
    }

    return 0;
}

Appreciating the Abstraction#

All of this is, of course, largely abstracted away when using the excellent megaTinyCore Arduino core.

When writing:

Serial.begin(921600);

there is quite a lot happening underneath that apparently simple call.

The core knows how the ATtiny USART works, calculates the appropriate baud-rate register value, selects suitable operating modes and configures the relevant registers.

Moving back to plain C meant that I had to deal with those details myself.

It’s at times like these that you appreciate just how useful these libraries and frameworks are.

They aren’t making the hardware do anything fundamentally different; they’re encoding a considerable amount of knowledge about the hardware and hiding repetitive configuration behind a much simpler interface.

At the same time, dropping down to the register level was useful.

When everything works, the abstraction saves a lot of time.

When something doesn’t work, understanding what that abstraction is doing underneath can make tracking down the problem considerably easier.