Project Liquid

Implementing I2C Controller for AVR

AVRs can act as the Controller or the Target on an I2C bus. In this part, I’ll be impending the Controller side.

Bitrate

The first task is to configure the bitrate. According to the datasheet, the SCL frequency can be calculated using the formula:

Reversing the formula, we get a function to calculate the TWBR register value for the desired bitrate.

class AvrI2c
{
public:
    enum class Prescaler {
        Div1 = 0,
        Div4 = 1,
        Div16 = 2,
        Div64 = 3,
    };

    static constexpr auto getSclFrequency(unsigned long cpuFreq, 
        uint8_t twbr, Prescaler prescaler) -> float
    {
        const auto prescalerVal = static_cast<int>(prescaler);
        return static_cast<float>(cpuFreq) /
               static_cast<float>(16 + 2 * twbr * (1 << (2 * prescalerVal)));
    }
};

We also need to select the prescaler value, so the final function will iterate over all possible options, starting with 1 (the fastest clock). We then return both the prescaler and the TWBR value. We also need the ability to return a success/failure state, since some bitrates can’t be achieved for a given CPU frequency. The functions are all constexpr so that the invalid configuration can be caught at compile time.

struct BitrateConfig {
    bool      isValid;
    Prescaler prescaler;
    uint8_t   twbr;
};

static constexpr Prescaler allPrescalers[] = {
  Prescaler::Div1, Prescaler::Div4, Prescaler::Div16, Prescaler::Div64};

static constexpr auto configureBitrate(unsigned long cpuFreq, float bitrate) -> BitrateConfig
{
    for (int i = 0; i < 4; ++i) {
        const auto r = configureBitrate(cpuFreq, static_cast<Prescaler>(i), bitrate);
        if (r.isValid) {
            return r;
        }
    }
    return {false, Prescaler::Div1, 0};
}

static constexpr auto configureBitrate(unsigned long cpuFreq, Prescaler prescaler,
                                        float bitrate) -> BitrateConfig
{
    const auto  prescalerVal = static_cast<int>(prescaler);
    const auto  prescalerMult = 1 << (2 * prescalerVal);
    const float x = (static_cast<float>(cpuFreq) / bitrate - 16.0f) /
                    (2 * static_cast<float>(prescalerMult));
    if (x > 255 || x < 1) {
        return {false, Prescaler::Div1, 0};
    } else {
        return {true, prescaler, static_cast<uint8_t>(round(x))};
    }
}

Finally, we can apply the bitrate configuration by writing to the TWSR and TWBR registers. The register addresses seem to be the same on different AVR CPUs, but we’ll make them easily changeable just in case. We’ll also define the registers using the RegBits helper.

static constexpr uint16_t base = 0xB8;

static constexpr auto TWBR_addr() -> uint16_t { return base + 0; };
static constexpr auto TWSR_addr() -> uint16_t { return base + 1; };
static constexpr auto TWAR_addr() -> uint16_t { return base + 2; };
static constexpr auto TWDR_addr() -> uint16_t { return base + 3; };
static constexpr auto TWCR_addr() -> uint16_t { return base + 4; };
static constexpr auto TWAMR_addr() -> uint16_t { return base + 5; };

static constexpr auto TWSR()
{
    struct Bits : SfrBase {
        RegBits<3, 5> TWS {regAddr}; // TWI Status
        // bit 2 reserved
        RegBits<0, 2> TWPS {regAddr}; // TWI Prescaler Bits
    };

    return Bits {TWSR_addr()};
}

With that, we can add some convenience functions to the BitrateConfig struct.

struct BitrateConfig {
    bool      isValid;
    Prescaler prescaler;
    uint8_t   twbr;

    auto apply(AvrI2c &dev) const -> void
    {
        assert(isValid);
        dev.TWSR().TWPS = static_cast<uint8_t>(prescaler);
        sfr8(dev.TWBR_addr()) = twbr;
    }
};

Framework

There are several main operations we’d like to support as the bus Controller. Each operation has a different sequence of actions and possible events reported by the hardware:

OperationDescriptionSimplified Sequence
probea simple test if a target
is present at a given address
Send start condition
Send address
Receive ACK or NACK
Send Stop
scantest all addresses to see
which targets are present
Send start condition
Send next address
Receive ACK or NACK
Send repeated Start or Stop, if done
writesend data to a targetSend start condition
Send address
Receive ACK or NACK
Send the next data byte
Receive ACK or NACK
Send Stop
readreceive data from a targetSend start condition
Send address
Receive ACK or NACK, write EA
Send the next data byte
Receive ACK or NACK, write EA
Send Stop

Receiving a NACK event from the hardware usually means an error, and we should stop the operation.
During a data read, we need to control the EA bit to let the hardware know when we’ve received the last byte (clear EA), or if we’re expecting to receive more bytes (set EA).

After requesting the hardware to transmit or receive data, we need to wait until the hardware asserts the TWINT bit in the control register, indicating an event. TWINT will also trigger a corresponding interrupt if TWIE is set, so instead of polling the register and blocking the CPU, we’ll install an interrupt handler and react to the event there.

To prepare for all this, we’ll define the remaining registers, and initialize them in the class constructor. Let’s make this a separate class so that we can write another one for implementing the I2C Target as well. The interrupt handler is installed using the helper functions from the library.

class AvrI2c
{
    // ...

    static constexpr auto TWCR()
    {
        struct Bits : SfrBase {
            RegBits<7> TWINT {regAddr}; // TWI Interrupt Flag
            RegBits<6> TWEA {regAddr};  // TWI Enable Acknowledge Bit
            RegBits<5> TWSTA {regAddr}; // TWI Start Condition Bit
            RegBits<4> TWSTO {regAddr}; // TWI Stop Condition Bit
            RegBits<3> TWWC {regAddr};  // TWI Write Collision Flag
            RegBits<2> TWEN {regAddr};  // TWI Enable Bit
            // bit 1 reserved
            RegBits<0> TWIE {regAddr}; // TWI Interrupt Enable
        };

        return Bits {TWCR_addr()};
    }
};

class AvrI2cController : public AvrI2c
{
public:
    AvrI2cController()
    {
        TWCR().TWINT = 0;
        TWCR().TWIE = 1;
        TWCR().TWEN = 1;
        installIrqHandler(
            Irq::Twi, IrqHandler::callMemberFunc<AvrI2cController, &AvrI2cController::isr>(this));
    }

    ~AvrI2cController()
    {
        TWCR().TWIE = 0;
        TWCR().TWEN = 0;
    }

   auto isr() -> void { /* stub */ }
};

The interrupt handler will be called whenever the last requested action has been completed and is a signal for us to take the next step. To decide what to do next, we need to know what operation we’re performing. For that, let’s add a function pointer, that we’ll set at the start of each operation, and call it from the interrupt handler. We will also read the status code, and clear the interrupt flag by writing 1 to TWINT.

volatile uint8_t  lastStatusCode {0};

void (AvrI2cController::*mode_func)(uint8_t) = &AvrI2cController::idle;

auto isr() -> void
{
    lastStatusCode = readStatus();
    (this->*mode_func)(lastStatusCode);
    TWCR().TWINT = 1; // clear interrupt flag
}

auto readStatus() const -> uint8_t
{ 
    return static_cast<uint8_t>(TWSR().TWS); 
}

auto idle(uint8_t) -> void
{
    assert(0); // The interrupt should never happen when in idle state
}

We store the status code in a member variable so that we can access the most recent value for error reporting. Let’s also define all the possible status codes. We can read them from the datasheet. One thing to note is that the datasheet defines the status codes as values of the TWSR register with the prescaler bits masked. The status code is on the 5 most significant bits of TWSR, so we need to shift the datasheet values to the right by 3 bits, to get the actual numeric value, as reported by TWSR().TWS bitfield helper.

struct StatusCode {
    static constexpr uint8_t BusError = 0x00;
    static constexpr uint8_t StartTxd = 0x08 >> 3;
    static constexpr uint8_t RepeatedStartTxd = 0x10 >> 3;
    static constexpr uint8_t WriteAddressAckRxd = 0x18 >> 3;
    static constexpr uint8_t WriteAddressNackRxd = 0x20 >> 3;
    static constexpr uint8_t DataAckRxd = 0x28 >> 3;
    static constexpr uint8_t DataNackRxd = 0x30 >> 3;
    static constexpr uint8_t ArbitrationLost = 0x38 >> 3;
    static constexpr uint8_t ReadAddressAckRxd = 0x40 >> 3;
    static constexpr uint8_t ReadAddressNackRxd = 0x48 >> 3;
    static constexpr uint8_t DataRxdWillAck = 0x50 >> 3;
    static constexpr uint8_t DataRxdWillNack = 0x58 >> 3;
};

Probe

With the above framework in place, we can try a simple probe operation. Everything begins by sending a Start condition, and all subsequent steps will be taken from the interrupt handler. So let’s set up the mode_func pointer to the probe handler, store the address we’re testing, and trigger the Start condition.

The first thing after the Start condition has been sent is to send the target address. This is common to all operations, so it gets its own function, handleStart.

I2C addresses are made with the target address on bits 7-1, and R/W flag on bit 0, so we construct the final address value by concatenating the target address and the R/W bit.

public:
    auto probe(uint8_t address)
    {
        mode_func = &AvrI2cController::probe_func;
        pendingAddress = address;

        start();
    }

private:
    auto start() -> void
    {
        lastStatusCode = 0;

        TWCR().TWEA = 1;
        TWCR().TWSTA = 1;
        TWCR().TWINT = 1;
    }

    auto probe_func(uint8_t statusCode) -> void
    {
        switch (statusCode) {
        case StatusCode::StartTxd:
        case StatusCode::RepeatedStartTxd: handleStart(Mode::Write); break;
        }
    }

    auto handleStart(Mode mode) -> void
    {
        writeAddr(pendingAddress, mode);
        TWCR().TWSTA = 0;
    }

    enum class Mode { Read, Write };

    auto writeAddr(uint8_t addr, Mode m) -> void
    {
        sfr8(TWDR_addr()) = static_cast<uint8_t>(
           (addr << 1) | (m == Mode::Read ? 1 : 0)
        );
    }

Once we get the address ACK (target is present and has acknowledged), or NACK (target not present), we save the result and finish the operation. After sending the Stop condition, we still need to wait for the hardware to trigger the interrupt one last time, to let us know the Stop condition has been sent. Only then can we report the end of the operation to the client. To accommodate that, we define a pendingStatus variable, that we set accordingly when we receive either ACK or NACK. But we only report this status to the client after the final interrupt, when the Stop condition has been sent. We do so by copying the pendingStatus value to the final status variable. The statuses reported to the client are translated from the hardware status codes into a user-friendly enum.

enum class Status {
    Ok,
    InProgress,
    Nack,
    ArbitrationLost,
    BusError,
    Unknown,
};

volatile Status   status {Status::Ok};
volatile Status   pendingStatus {Status::Ok};

auto codeToStatus(uint8_t statusCode) -> Status
{
    switch (statusCode) {
    case StatusCode::ArbitrationLost: return Status::ArbitrationLost;
    case StatusCode::BusError: return Status::BusError;
    case StatusCode::DataNackRxd:
    case StatusCode::ReadAddressNackRxd:
    case StatusCode::WriteAddressNackRxd: return Status::Nack; break;
    default: return Status::Unknown;
    }
}

auto wait_for_stop_func(uint8_t) -> void
{
    status = pendingStatus;
    pendingStatus = Status::Unknown;
    mode_func = &AvrI2cController::idle;
}

The wait_for_stop_func will be called by the interrupt handler via the mode_func pointer after sending the Stop condition. Finally, we add the remaining status conditions to the switch statement in probe_func.

auto probe_func(uint8_t statusCode) -> void
{
    switch (statusCode) {
    case StatusCode::StartTxd:
    case StatusCode::RepeatedStartTxd: handleStart(Mode::Write); break;
    case StatusCode::WriteAddressAckRxd: finish(Status::Ok); break;
    case StatusCode::WriteAddressNackRxd: finish(Status::Nack); break;
    default: finish(codeToStatus(statusCode)); break;
    }
}

auto finish(Status status_) -> void
{
    // Setting TWSTO will send the stop bit. After it has been sent,
    // TWINT interrupt will trigger one last time. Only then the operation
    // is finished and the device is ready to start another one.
    pendingStatus = status_;
    mode_func = &AvrI2cController::wait_for_stop_func;
    TWCR().TWSTA = 0;
    TWCR().TWSTO = 1;

    readyCallback();
}

Probe sequence recap

With the above code, the normal sequence of events for a successful probe operation is as follows.

  1. Client calls probe(address), which sets up the mode_func pointer for the interrupt handler, stores the address in pendingAddress, and triggers the Start condition by writing 1 to TWSTA.
  2. When the Start condition has been transmitted, the hardware triggers the interrupt. The interrupt handler reads the status code (0x08 – start transmitted) and calls probe_func.
  3. probe_func case for StartTxd calls handle_start, constructs the write address from the stored pendingAddress and clears the TWSTA bit.
  4. The hardware sends the address and receives an acknowledgment from the target. The interrupt is called, with the status = 0x18 (WriteAddressAckRxd).
  5. The interrupt handler calls finish(Status::Ok), which stores the OK result in pendingStatus, switches mode_funct to wait_for_stop_func, and sends a Stop condition by writing 1 to TWSTO.
  6. After the Stop condition has been sent, the interrupt handler is called again, this time calling wait_for_stop_func, which copies the pending status value to the client-facing status variable and resets the state. The hardware is now idle, and the interrupt should not trigger.

Remaining operations

Read, write, and scan operations involve more steps, but they all follow a similar sequence. They each get a separate mode_func and react to the statuses accordingly. Read and write operations work can operate on more than one data byte, so they both use an input/output data pointer and length.

volatile uint8_t *currentData {nullptr};
volatile size_t   dataSize {0};

auto write(uint8_t address, uint8_t *data, size_t size) -> void
{
    mode_func = &AvrI2cController::write_func;
    pendingAddress = address;
    currentData = data;
    dataSize = size;

    start();
}

auto read(uint8_t address, uint8_t *data, size_t size) -> void
{
    mode_func = &AvrI2cController::read_func;
    pendingAddress = address;
    currentData = data;
    dataSize = size;

    start();
}

The read operation also updates the TWEA bit to 0, after receiving the last byte, to indicate that it’s done reading data.

auto write_func(uint8_t statusCode) -> void
{
    switch (statusCode) {
    case StatusCode::StartTxd:
    case StatusCode::RepeatedStartTxd: handleStart(Mode::Write); break;
    case StatusCode::WriteAddressAckRxd: writeData(*currentData++); break;
    case StatusCode::WriteAddressNackRxd: finish(Status::Nack); break;
    case StatusCode::DataAckRxd:
        if (--dataSize != 0) {
            writeData(*currentData++);
        } else {
            finish(Status::Ok);
        }
        break;
    case StatusCode::DataNackRxd: finish(Status::Nack); break;
    default: finish(codeToStatus(statusCode)); break;
    }
}

auto read_func(uint8_t statusCode) -> void
{
    switch (statusCode) {
    case StatusCode::StartTxd:
    case StatusCode::RepeatedStartTxd: handleStart(Mode::Read); break;
    case StatusCode::ReadAddressAckRxd:
        if (dataSize == 1) {
            TWCR().TWEA = 0;
        }
        break;
    case StatusCode::ReadAddressNackRxd: finish(Status::Nack); break;
    case StatusCode::DataRxdWillAck:
        *currentData++ = readData();
        if (--dataSize <= 1) {
            TWCR().TWEA = 0;
        }
        break;
    case StatusCode::DataRxdWillNack:
        if (dataSize > 0) {
            *currentData++ = readData();
        }
        finish(Status::Ok);
        break;
    default: finish(codeToStatus(statusCode)); break;
    }
}

The scan operation works like probe, but repeats the whole sequence for all address values, and only stops after all addresses have been tested. The result for each address is reported via a callback function.

auto scan(ScanCallback callback)
{
    waitForIdle();

    scanCallback = callback;
    mode_func = &AvrI2cController::scan_func;
    pendingAddress = 0;
    currentData = 0;
    dataSize = 0;

    start();
}

auto scan_func(uint8_t statusCode) -> void
{
    switch (statusCode) {
    case StatusCode::StartTxd:
    case StatusCode::RepeatedStartTxd: handleStart(Mode::Write); break;
    case StatusCode::WriteAddressAckRxd:
        scanCallback(pendingAddress, true);
        if (++pendingAddress <= 127) {
            TWCR().TWSTA = 1;
            break;
        } else {
            finish(Status::Ok);
        }
        break;
    case StatusCode::WriteAddressNackRxd:
        scanCallback(pendingAddress, false);
        if (++pendingAddress <= 127) {

            TWCR().TWSTA = 1;
            break;
        } else {
            finish(Status::Ok);
        }
        break;
    default: finish(codeToStatus(statusCode)); break;
    }
}

All functions are non-blocking, which means we need a way to notify the client app when the operation completes. And for apps that prefer blocking calls, we need a way to wait until the operation completes. For the callback type, we’ll steal from IrqHandler, since it’s exactly what’s needed.

using ReadyCallback = IrqHandler;
ReadyCallback     readyCallback {[](void *) {}, nullptr};

auto onReady(ReadyCallback cb) -> void { readyCallback = cb; }

auto wait_for_stop_func(uint8_t) -> void
{
    status = pendingStatus;
    pendingStatus = Status::Unknown;
    mode_func = &AvrI2cController::idle;
    readyCallback();
}

auto waitForIdle() -> Status
{
    while (status == Status::InProgress)
        ;

    return status;
}

Additionally, each function that starts an operation (read/write/probe/scan) needs to make sure the previous operation is complete, in case the client code calls them in a sequence, without waiting for completion.

auto write(uint8_t address, uint8_t *data, size_t size) -> void
{
    waitForIdle();

    mode_func = &AvrI2cController::write_func;
    pendingAddress = address;
    currentData = data;
    dataSize = size;

    start();
}

Source code and demo

The full source code can be found on GitHub.

There is a demo app that reads temperature and humidity from an AHT20 sensor.

Leave a Reply

Your email address will not be published. Required fields are marked *