0
\$\begingroup\$

I have two (different) boards, each based on the ATmega32U4. Both are programmed bare-metal, in C with avr-gcc. Board 1 has USB functionality, using the LUFA library (I started with the USBtoSerial demo project, and adapted it to implement my own commands).

Board 2 generates a PWM signal to control fans speed; that part works (I've tested it hardcoding a timed sequence of values, and I confirm that the fans spin at expected speeds)

Now, I want Board 1 to occasionally transmit an 8-bit value to Board 2, to set the duty cycle of the PWM (and thus, to set the fans speed). Transmission is a single byte, receiver does not need to send any response.

Transmitter is essentially the code from page 235 of the datasheet (just slightly refactored):

// I2C functions:
void I2C_init()
{
    const unsigned int baudrate = (F_CPU / (2 * I2C_CLOCK));
    TWSR = 0;
    TWBR = ((uint8_t) (baudrate - 16));
}

uint8_t I2C_start()
{
    TWCR = ((0x1 << TWSTA) | (0x1 << TWINT) | (0x1 << TWEN));   // Start condition
    while (!(TWCR & (1 << TWINT)))
    {}

    return (TWSR & 0xF8);
}

uint8_t I2C_stop()
{
    TWCR = (1 << TWSTO) | (1 << TWINT) | (1 << TWEN);           // Stop condition
    while (TWCR & (0x1 << TWSTO))
    {}

    return (TWSR & 0xF8);
}

uint8_t I2C_write (uint8_t data)
{
    TWDR = data;                                                 // Transmit data
    TWCR = (1 << TWINT) | (1 << TWEN);
    while (!(TWCR & (0x1 << TWINT)))                             // Wait for the transmission to complete
    {}

    return (TWSR & 0xF8);
}

...

Upon receiving USB command fans_speed <param>\n, I execute:

    const uint8_t status_start      = I2C_start();
    const uint8_t status_write_addr = I2C_write (FANS_BOARD_ADDRESS << 1);
    const uint8_t status_write_data = I2C_write (((uint8_t)(atoi(param))));
    const uint8_t status_stop       = I2C_stop();

    sprintf (response, "%02X %02X %02X %02X\r\n",
                        status_start, status_write_addr, status_write_data, status_stop);

        // response is then transmitted through USB

On the USB host, I run minicom -D /dev/ttyACM0 to interact with board B1, and see the following:

fans_speed 7  [[ this is the command that I'm typing and gets transmitted ]]
08 18 28 F8
fans_speed 2

The first line is what I transmit (I enabled local echo on minicom); second line (USB response from Board 1, showing the status codes) seems ok. Not sure about that last F8 code, which corresponds to constant TW_NO_INFO in <util/twi.h>, but I do see the 7 received by Board 2 (see below for description)

I then transmit a second time, and this shows the problem I'm having: after the first transmission is correctly received, the master (Board 1) hangs; subsequent transmissions are not received, and Board 1 actually stops responding to USB commands).

This is the receiver code, roughly based on the sample code in https://www.electronicwings.com/avr-atmega/atmega1632-i2c (a significantly simplified version, since my use-case is simpler):

int main()
{
    ... (declarations and initializations)

    init_pwm();

        // Init I2C receiver
    TWAR = I2C_ADDR << 1;
    TWCR = (0x1 << TWEN) | (0x1 << TWEA) | (0x1 << TWINT);   // Enable TWI and ACK on the slave

    while (true)
    {
            // Wait for address
        while (!(TWCR & (0x1 << TWINT)))
        {}

        uint8_t status = TW_STATUS;
        if (status == TW_SR_SLA_ACK || status == TW_SR_ARB_LOST_SLA_ACK)
        {
            TWCR = (0x1 << TWINT) | (0x1 << TWEN) | (0x1 << TWEA);
            while (!(TWCR & (0x1 << TWINT)))
            {}

            const uint8_t speed = TWDR;
            status = TW_STATUS;
            TWCR = (0x1 << TWINT) | (0x1 << TWEN) | (0x1 << TWEA);

                // set_pwm_duty_cycle (speed);
            if (status == TW_SR_DATA_ACK)
            {
                flash_LED (speed);   // Flashes 'speed' times
            }
            else
            {
                flash_LED (1);
                _delay_ms(500);
            }
        }
    }

The LED never flashes a single time (good; it means the else is never taken). I do see the LED flashing seven times.

However: why does it hang after the first transmission? Is the bug in the receiver, or the transmitter (or both) ??

\$\endgroup\$
3
  • 1
    \$\begingroup\$ hm, a hang after the first transaction might have electrical reasons. With what resistor are you pulling up the I²C bus? \$\endgroup\$ Commented Nov 27 at 18:32
  • \$\begingroup\$ I'm using 5.1k for both lines. I looked at the signals (not side-by-side, though) on an oscilloscope, and they seemed ok (the transition from 0 to 1 looked slower/smoother, of course, but the line comfortably reaches near-VCC-level by the time the next transition comes (in the test, I was using values with bit patterns alternating 1 and 0s; e.g., address 0x5 or 0x50). The physical lines are perhaps ~10cm traces on each board, and I'm using a 40cm flat-ribbon IDC cable between the boards; in the cable, the wires corresponding to each line (SDA and SCLK) are surrounded by ground wires. \$\endgroup\$
    – Cal-linux
    Commented Nov 27 at 18:45
  • 1
    \$\begingroup\$ You must have a better understanding why the mcu hang. What debugging tools have you available? What happens when you physically disconnect Board2? Try to print a program status in specific code places. It could be a hardware issue. What about the power supply? \$\endgroup\$
    – user317139
    Commented Nov 27 at 19:04

1 Answer 1

2
\$\begingroup\$

Basically, the receiver does not handle all cases it should from the I2C peripheral, the I2C peripheral jams the bus because it must signal a wait by clock stretching because the software hasn't handled a previous event and will never handle it.

So, as the bus is jammed by receiver the transmitter I2C peripheral cannot also complete the next transaction, and it waits for indefinitely for the bus to become free which it never will.

There is no timeouts or other error handling on the transmitter side to prevent that from happening.

In detail, the reception code does not handle reception of STOP event.

After the data/parameter byte is received, the main loop waits for the next event, and does nothing because it wasn't a SLA received event.

So the receiver also isn't armed to receive next SLA+W event and show it to software. It likely lengthens the clock cycle and will do it indefinitely as software never tells the peripheral to continue.

The transmission of SLA+W cannot also complete and the transmitter waits indefinitely for the event to complete.

\$\endgroup\$
8
  • \$\begingroup\$ That indeed seems to be the issue; I added an extra loop waiting for WINT followed by the write to TWCR to clear the event, and it now works. However, this seems so fragile — I'm wondering whether I should simply have a single loop waiting for WINT, with its write to TWCR, and just check TW_STATUS against TW_SR_DATA_ACK to distinguish which event was the data I'm interested in? \$\endgroup\$
    – Cal-linux
    Commented Nov 27 at 20:14
  • 1
    \$\begingroup\$ @Cal-linux It is fragile, even a small glitch on bus can make software hand unless software implements fail-safe mechanisms. Also you cannot simply blindly tell the I2C peripheral that this event is handled, if you have not read yet what the event was and determined how it should be handled. Some events need to read or write data and/or set/clear the ACK response first before telling the I2C peripheral how to continue next. \$\endgroup\$
    – Justme
    Commented Nov 27 at 20:29
  • \$\begingroup\$ Ok, so I guess I'll go through Table 20-3 in the datasheet (page 235). Then again, what I have to communicate is soooo simple, I have nearly-zero speed or any performance constraints, I wonder whether I should just bit-bang the byte from one board to the other? \$\endgroup\$
    – Cal-linux
    Commented Nov 27 at 20:33
  • 1
    \$\begingroup\$ @Cal-linux Any method will do. You can wait for TWINT to be set forever, or keep track of time spent in the loop, or keep a counter and decide that after 65535 loops it is likely to best to return with an error and deal with it somehow, like resetting the TWI interface. But, it won't solve the problem that bus may still be stuck, and what to do with the failed transaction, do you try again, is there going to be doubled or missing data bytes, etc, which depends on the protocol. For example, I2C flash chips don't respond at all to any transaction until write process is completed. \$\endgroup\$
    – Justme
    Commented Nov 27 at 20:54
  • 1
    \$\begingroup\$ @Cal-linux One way to solve it is to not wait for the TWINT in a polling loop at all. You can use TWI interrupt to react to it. And monitor at a higher level if the TWI does what it is supposed to do or has a problem. You can also implement it as a non-interupt handler, basically by checking TWINT and return if it does not need handling, process event if it needs handling. \$\endgroup\$
    – Justme
    Commented Nov 27 at 21:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.