Transmission of 0 value by alternative software serial fails - sometimes.

I'm using the below code for UART transmission, limited but super small which is great on ATtiny processors where you only need to transmit some values.

The code works almost perfectly, except for one weird behaviour: it does not transmit zeros properly. I am testing by receiving the values on a Nano with SoftwareSerial, then printing the values to the serial monitor.

When I send a series of random values (say 20, 30, 40, 50) they come through correct every time.

When I send a series of four zeros (0, 0, 0, 0), they come through as four zeros less than half the time, the rest of the time I get to see 0, 0, 4 or 0, 0, 8 or 0, 0, 2. It seems that somewhere the timing is thrown off, and the start bit of the last byte is seen as a bti in the previous byte. The values I see are always numbers with a single bit set (so 1, 2, 4, 8). Not even always the last bit (or the value would always be 1), commonly I see 4, 8, and even 16.

So either a bunch of bits are skipped when sending, or the receiving side waits too long.

However, if a non-zero value follows the series of zeros, that value always comes through fine as well. E.g. I transmit the series 0, 0, 0, 0, 42 the 42 always comes through correctly, even if one of the zeros gets mangled. Received would be e.g. 0, 0, 4, 42.

Also this behaviour only ever seems to affect the last two zeros of the sequence, and not every time. Sometimes it's all coming through just fine.

Example: this code transmits the values 10, 0, 0, 0, 0, 42.

void setup() {
  UART_init();
}

void loop() {
  uint8_t data[6] = {10, 0, 0, 0, 0, 42};
  for (uint8_t i = 0; i < 6; i++) {
    UART_tx(data[i]);
  }
  UART_flush();
  delay(1000);
}

/*  Software ("bit-bang") UART Transmitter (8 data bits, 1 stop bit, no parity)
    https://marcelmg.github.io/software_uart/
    https://github.com/MarcelMG/AVR8_BitBang_UART_TX
*/


// The millis timer is implemented in https://github.com/SpenceKonde/ATTinyCore/blob/master/avr/cores/tiny/wiring.c
// It uses the overflow vector of timer0, prescaler: f/64
//
// This code uses OCRA0 and the COMPA interrupt of timer0, and disables PWM on pin 7 (PA7) and pin 8 (PB2).
//
// Adapted for use with Arduino / ATTinyCore.
//
// This implementation is Tx only, uses Timer0 COMPA interrupt, fixed 9600 baud rate.
// The internal buffer is 1 byte, if more than 1 byte is sent the code blocks until all but one byte
// has been transmitted completely.
//
// Set compare value to 104.1667µs to achieve a 9600 baud rate. TIMER0 is set to clk/64 by the millis() timer.
//
// Supported frequencies:
// 8 MHz        : TIMER = 8M * 104.1667µ / 64       = 13 (13.02, as good as exact)
// 11.0592 MHz  : TIMER = 11.0592M * 104.1667µ / 64 = 18 (exact)
// 12 MHz       : TIMER = 12M * 104.1667µ / 64      = 20 (19.53, -2.35% - may not work reliably)
// 16 MHz       : TIMER = 16M * 104.1667µ / 64      = 26 (26.04, as good as exact)
// 18.432 MHz   : TIMER = 18.432M * 104.1667µ / 64  = 30 (exact)
// 20 MHz       : TIMER = 20M * 104.1667µ / 64      = 33 (32.55, -1.36% - works well)

#if F_CPU == 8000000
#define TIMER 13
#elif F_CPU == 11059200
#define TIMER 18
#elif F_CPU == 12000000
#define TIMER 20
#elif F_CPU == 16000000
#define TIMER 26
#elif F_CPU == 18432000
#define TIMER 30
#elif F_CPU == 20000000
#define TIMER 33
#else
#error Unsupported CPU frequency.
#endif

// change these to use another pin
#define TX_PORT PORTA
#define TX_PIN  PA7
#define TX_DDR  DDRA
#define TX_DDR_PIN DDA7

volatile uint16_t tx_shift_reg = 0;                         // holds the 8 bits of the byte to transmit plus 1 start bit and 1 stop bit.

void UART_tx(char character) {

  // If sending the previous character is not yet finished, wait for this to be completed.
  UART_flush();

  // Fill the TX shift register with the character to be sent and the start & stop bits (start bit (1<<0) is already 0)
  uint16_t local_tx_shift_reg = (character << 1) | (1 << 9);  // Adds the stop bit (1<<9)
  tx_shift_reg = local_tx_shift_reg;

  OCR0A = TCNT0 + TIMER;                                    // Set the timer compare point to the correct value.
  bitSet(TIFR0, OCF0A);                                     // Clear the interrupt flag.
  bitSet(TIMSK0, OCIE0A);                                   // Enable output compare 0 A interrupt.
}

void UART_tx_str(char* string) {                            // pointer to null terminated string.
  while (*string) {
    UART_tx(*string++);                                     // Transmit the characters one by one.
  }
}

void UART_flush() {                                         // Flush the buffer.
  while (tx_shift_reg) {}                                   // tx_shift_reg becomes 0 when the stop bit is transmitted.
}

void UART_init() {

  //set TX pin as output, high.
  bitSet(TX_DDR, TX_DDR_PIN);
  bitSet(TX_PORT, TX_PIN);

  // ATTinyCore sets timer0 to Fast PWM mode, we have to disable this or the updates to OCR0A don't
  // work properly.
  bitClear(TCCR0A, WGM01);
  bitClear(TCCR0A, WGM00);
}

// timer0 compare A match interrupt
ISR(TIM0_COMPA_vect ) {
  uint16_t local_tx_shift_reg = tx_shift_reg;

  // Output LSB of the TX shift register at the TX pin
  if (local_tx_shift_reg & 0x01) {
    bitSet(TX_PORT, TX_PIN);                                // Logic 1: high level signal.
  }
  else {
    bitClear(TX_PORT, TX_PIN);                              // Logic 0: low level signal.
  }

  // Shift the TX shift register one bit to the right.
  local_tx_shift_reg >>= 1;
  tx_shift_reg = local_tx_shift_reg;

  // Set the timer compare point for the next bit.
  OCR0A += TIMER;

  // When the stop bit has been sent, the shift register will be 0
  // and the transmission is completed, so we can stop this interrupt.
  if (!local_tx_shift_reg) {
    bitClear(TIMSK0, OCIE0A);
  }
}

The Nano receives these values on a SoftwareSerial instance and prints the result to Serial monitor. This is the result:

10, 0, 0, 0, 168, 
10, 0, 0, 0, 168, 
10, 0, 0, 168, 
10, 0, 0, 0, 42, 
10, 0, 0, 0, 168, 
10, 0, 0, 168, 
10, 0, 0, 0, 168, 
10, 0, 0, 0, 168, 
10, 0, 160, 249, 
10, 0, 0, 0, 168, 
10, 0, 0, 0, 0, 42, 
10, 0, 0, 0, 0, 42, 
10, 0, 0, 160, 254, 
10, 0, 0, 42, 
10, 0, 0, 168, 
10, 0, 0, 168, 
10, 0, 160, 249, 
10, 0, 0, 42, 
10, 0, 0, 168, 
10, 0, 0, 0, 168, 
10, 0, 0, 168, 
10, 0, 0, 0, 168, 
10, 0, 0, 0, 0, 42, 
10, 0, 0, 0, 0, 42, 
10, 0, 0, 0, 42, 
10, 0, 0, 0, 0, 42, 
10, 0, 0, 0, 42, 
10, 0, 0, 168, 
10, 0, 0, 168, 
10, 0, 0, 160, 249, 
10, 0, 0, 0, 0, 42, 
10, 0, 0, 42, 
10, 0, 160, 249, 
10, 0, 0, 0, 0, 42, 
10, 0, 0, 0, 168, 
10, 0, 160, 249, 
10, 0, 0, 0, 168, 
10, 0, 0, 160, 249, 
10, 0, 0, 0, 168, 
10, 0, 0, 0, 168, 
10, 0, 0, 42, 
10, 0, 0, 0, 42, 
10, 0, 0, 0, 168, 
10, 0, 0, 0, 42, 
10, 0, 168, 
10, 0, 0, 0, 168, 
10, 0, 0, 0, 168, 
10, 0, 0, 0, 42, 
10, 0, 0, 168, 
10, 0, 168, 
10, 0, 0, 0, 42, 
10, 0, 0, 0, 0, 42, 
10, 0, 0, 0, 42, 
10, 0, 0, 0, 42, 
10, 0, 0, 0, 42, 
10, 0, 0, 42, 
10, 0, 0, 160, 254, 
10, 0, 160, 249, 
10, 0, 0, 42, 
10, 0, 0, 160, 249, 
10, 0, 0, 42, 
10, 0, 0, 0, 0, 42, 
10, 0, 160, 249, 
10, 0, 0, 0, 0, 42, 
10, 0, 0, 168, 
10, 0, 0, 42, 
10, 0, 0, 42, 
10, 0, 0, 168, 
10, 0, 168, 
10, 0, 160, 249, 
10, 0, 0, 0, 42, 
10, 0, 0, 168, 
10, 0, 0, 42, 
10, 0, 0, 168, 
10, 0, 0, 0, 42, 
10, 0, 0, 0, 42, 
10, 0, 168, 
10, 0, 0, 0, 42, 
10, 0, 0, 168, 
10, 0, 0, 160, 249, 
10, 0, 0, 0, 168, 
10, 0, 0, 0, 42, 
10, 0, 0, 160, 249, 
10, 0, 0, 42, 
10, 0, 0, 168, 
10, 0, 0, 168, 
10, 0, 0, 168, 
10, 0, 0, 0, 168, 
10, 0, 168, 
10, 0, 0, 168, 
10, 0, 0, 0, 168, 
10, 0, 0, 168, 
10, 0, 0, 168, 
10, 0, 0, 0, 0, 42, 
10, 0, 160, 249, 
10, 0, 0, 42, 
10, 0, 0, 168, 
10, 0, 0, 0, 42, 
10, 0, 0, 0, 42, 
10, 0, 160, 249, 
10, 0, 0, 0, 168, 
10, 0, 0, 160, 249, 
10, 0, 0, 42, 
10, 0, 0, 42, 
10, 0, 0, 42, 
10, 0, 0, 42,

The first two values (10, 0) come through all the time. In rare cases all four zeros come through as they should. In some cases they're simply missed, in other cases later values get mangled.

42 =  0b00101010
160 = 0b10100000
168 = 0b10101000
249 = 0b11111001

Especially the last one is confusing as I don't know how to assemble that from the various bits that are being transmitted...
(another example in the next message...)

When transmitting the series {10, 12, 14, 16, 18, 42} there is not a single error after about 100 transmissions.

When adding zeros to this sequence, I used {10, 0, 14, 0, 18, 0}, I do get occasional errors. The byte values {0, 14} sometimes become a single byte with value 56, and the byte values {0, 18} sometimes become a single byte 72.

14 = 0b00001110

56 = 0b00111000

18 = 0b00010010

72 = 0b01001000

In both cases two zero bits have been added to the end! Considering the final byte is always transmitted correctly, it must come from the previous byte... but then why are those bits appended to the end??

On my scope the signal looks clean, with good high/low transitions and steady levels. The signal is passed through an optocoupler between the ATtiny and the Nano which does not appear to have much effect, the main effect is a slightly slower transition between levels. This does not appear to be an issue as non-zero values are transmitted perfectly fine, and the transition is still fast compared to the bit length.

Any suggestions on why this possibly happens and how to solve it are welcome!

10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 72, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 72, 0, 
10, 56, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 72, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 72, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 72, 0, 
10, 0, 14, 72, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 72, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 72, 0, 
10, 0, 14, 72, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 72, 0, 
10, 56, 72, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 72, 0, 
10, 0, 14, 72, 0, 
10, 0, 14, 72, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 72, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 72, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 72, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 72, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 72, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 72, 0, 
10, 0, 14, 72, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 72, 0, 
10, 56, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 72, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 56, 72, 0, 
10, 0, 14, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 56, 72, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 0, 18, 0, 
10, 56, 0, 18, 0, 
10, 0, 14, 0, 18, 0,

wvmarle:
The Nano receives these values on a SoftwareSerial instance and prints the result to Serial monitor.

Does it work correctly when receiving with HardwareSerial ?

...R

I'd love to try but don't have a board with two hardware Serial ports, and connecting it to the Rx of the Nano while it's connected to USB doesn't seem like a good idea to me.

Come to think of it... maybe I can replace the ATmega328P with an ATmege328PB on a Nano or Pro Mini. The two are pretty much pin compatible, and I have the PB chips on hand. That one does have a second hardware UART.

That won't quite fully work.
PB gains two more pins by replacing a VCC & GND pin with signals. SCL & SDA would not be usable.
2nd Rx/Tx are connected to current SPI signals, so on the Nano you couldn't use the ICSP header.

You can use the Nano as a TTL-to-USB converter; just keep the 328P in reset.

It kinda worked but just not quite. The two extra pins are indeed not usable; must be left alone, in input mode. Why would I2C not be available? Same pins as the 328P.

The ICSP worked fine, a matter of not trying to use both at the same time. For experimenting that's good enough, just upload the sketch to the Nano and then connect the Rx1. Unfortunately I did not manage to get a working bootloader (to upload programs over USB) and soon after the thing died. I probably did something wrong with the soldering.

I may try again tomorrow, too late now. I'll have to try a Pro Mini as I have only one Nano left (really gotta buy some more but everything is closed now, have to wait a week or so), I built a ISCP breakout for those boards before so can use that.

sterretje:
You can use the Nano as a TTL-to-USB converter; just keep the 328P in reset.

Easier is an FTDI cable.

The thing is: the ATtiny sends binary values - the code that I posted is basically the equivalent of a Serial.write() command - that are translated to human readable ASCII digits for printing by the Nano:

  if (mySerial.available()) {
    Serial.print(mySerial.read());
    Serial.print(F(", "));
  }

A Mega is worth having for just this situation.

...R

True... I will consider getting one, if the next attempt at 328PB transplant fails again. Never had the need for one so far, but shouldn't cost too much.

Any project that requires you to buy new toystools is a good one :slight_smile:

Update: heart transplant successful; I now have a Nano with two hardware serial ports. The result has not improved, though:

10, 0, 14, 0, 18, 0,  
10, 56, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 72, 0,  
10, 56, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 56, 0, 18, 0,  
10, 56, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 56, 0, 18, 0,  
10, 0, 14, 72, 0,  
10, 0, 14, 72, 0,  
10, 56, 0, 18, 0,  
10, 0, 14, 72, 0,  
10, 0, 14, 72, 0,  
10, 0, 14, 0, 18, 0,  
10, 56, 0, 18, 0,  
10, 0, 14, 72, 0,  
10, 0, 14, 0, 18, 0,  
10, 56, 0, 18, 0,  
10, 56, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 56, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 56, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 56, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 72, 0,  
10, 0, 14, 72, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 56, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 72, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 56, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 56, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 56, 0, 18, 0,  
10, 56, 72, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 0, 18, 0,  
10, 0, 14, 72, 0,  
10, 0, 14, 0, 18, 0,

Of note, the LAST zero always comes through correctly. The issue appears with the stop bit not being read/interpreted properly. Transmitting a zero is basically 9 bits of logic 0 in a row (start bit, 8 data bits) followed by a logic 1.

Now my detailed knowledge of the UART Serial protocol is falling short. Is it expected (or in general a good idea) to make the stop bit last longer than the other bits? This considering the last zero always coming through correctly. When that bit is passed through, the line remains high for a second until the next sequence is transmitted as the six values are transmitted at 1-second intervals. That's in a way a super-extended stop bit.

The values where the receipt goes wrong are always two bits shifted left; for some reason there are two logic 0 bits added to it. That must be a remnant of the zero that was transmitted before, but the stop bit of that one seemingly disappeared (I figure it should add a logic 1 into the received value, but doesn't).

wvmarle:
Now my detailed knowledge of the UART Serial protocol is falling short. Is it expected (or in general a good idea) to make the stop bit last longer than the other bits?

My guess is that the timing is a little bit off on the transmitter.

The idea is that the receiver samples the incoming signal at regular intervals starting with the transition from HIGH to LOW of the start bit. I think the hardware UART takes several samples. A certain amount of timing error can be accommodated. In theory the sample should be taken at the centre of each bit but if the bit is a square wave then it's easy to see how a reading a little bit ether side of centre would still work fine.

If the timing is off on the TX then the error (distance from the centre of the bit) will increase with each successive bit and (again my guess) is that you are sampling too late and sampling the stop bit (thinking it is bit 8 ). I think that means that your TX clock is too fast. I don't think it would matter so much if the sample was too soon (i.e. the clock was slow).

I wrote this Yet Another Software Serial a long time ago (i.e. I have forgotten the details). Maybe it will help.

I also wrote a program that sends serial data using blocking code that needs to be tuned to the microprocessor. I use it regularly to send debug data from an Attiny that is also hooked up to an nRF24.

...R

PS ... IIRC the first sample (if you were writing your own RX code) needs to be at 50% of the bit interval to read in the middle of the first bit. But I can't remember if that is also necessary for the TX.

PPS ... the stop bit and the start bit are the same thing - the HIGH between bytes. But I can't remember (assuming 1 stop bit and one start bit) whether the high needs to be two bits wide. I don't think it does but wider won't cause any problem apart from slower messaging.

The UART signal is idle high.
The start bit is a logic 0, a low signal - telling a transmission is starting.
The stop bit is a logic 1, a high signal - probably meant to guarantee a high/low transition at the start of the next bit.

I was thinking of timing being off. I have a 20 MHz crystal, so would be 2.35% off in speed. That's within specs. Also all other values that I tried so far come through perfectly, no issues there. The signals that do not come through appear to be mostly two bits off, that's like 20% error, and not all of them are wrong. Just a few of the transmissions. Also if the last value is a zero it always comes through correctly. It's this inconsistency that's making this so odd.

I could solder another board with a 18.432 MHz crystal, to have exact timing for UART, but before that I'm trying to get a better understanding of where and why it goes wrong.

Update.

I've tried other values, looking at the extremes: a single logic 1 bit in the value at the start or end of the value. Turns out value 1 (LSB first so the logic 1 is sent right after the start bit) works fine; value 128 (0b1000 0000 so the logic 1 bit is sent last, right before the stop bit) also has values to be lost completely just like the value 0; values 192 (0b1100 0000) and 64 (0b0100 0000) have no issues.

Next up the scope - checking what a sequence of zeros looks like. Sometimes values get lost, and indeed those traces show a problem. Turns out there is an interesting timing issue: most bytes take 1.07 ms to transmit (9600 bps would be 1.04 ms, but my scope's resolution is 0.02 ms and the timing is 1.4% too slow), some are at 1.28 ms so 0.21 ms too long. That's exactly two bits worth of time extra. That explains why numbers get lost, and the 2-bit shift.

0.21 ms is 66 ticks of timer0 (20 MHz, f/64 presecaler) that apparently get added to the timing sequence; two whole extra bits magically seem to appear. The oddest of this is again that it happens only now and then. Most of the values are transmitted correctly.

Finally: using the SoftwareSerial library all works as expected. I can't use the Serial implementation of the ATTinyCore as my Tx is on the wrong pin (can't change that in this experiment, would require a change of the PCB). So there's obviously an issue with my code, but how??

I do want to find a fix to my implementation for reasons of resource use... no need for Rx code to be present and demanding a pin.
SoftwareSerial: 2168 bytes storage; 124 bytes memory.
ATTinyCore Serial: 1060 bytes storage; 79 bytes memory.
My code: 676 bytes storage; 17 bytes memory.

OK, now it gets really interesting. The issue at hand is clearly related to the millis() timer. Upon disabling millis() in ATTinyCore (through the Tools menu) it works perfectly! Every 1 ms this counter ticks, taking some processing time, that could delay the handling of the TIMER0_COMPA interrupt a little, but this should not be enough to disrupt transmission.

Just disabling the timer0 overflow interrupt while the UART code is running has no effect - I really have to disable millis completely (all testing has been done so far with millis active, which I need in the final code for this project as the DS18B20 library uses millis()).

I just don't see anything in the milis() implementation that raises any red flags, or what could possibly cause two complete bits to be added to the transmission, let alone why it would only happens with specific transmission values.

wvmarle:
The UART signal is idle high.
The start bit is a logic 0, a low signal - telling a transmission is starting.

Yes. My description was not sufficiently precise. I intended to convey that the start bit is the transition to LOW from the HIGH of the stop bit and the stop bit can be as long as you like - it might be several minutes long if there is no data to be transmitted.

And the middle of the first databit will be 150% (not 50%) of the bit width following the HIGH to LOW transition. (It's a long time since I studied this).

...R

I just noticed when looking at my implementation of the blocking sendSerial that it sends a byte with interrupts disabled.

...R

Well, solved? Kinda. I settled for a different approach.

As there is some weird interaction between my code and the ATTinyCore code, I gave up on using the timer interrupt for timing the transmission and fell back on the delayMicroseconds() call. So yes, fully blocking, but that's OK for my application. Basically the MCU is in deep sleep until it receives a signal, then it wakes up, takes a measurement, transmits the data, and goes to sleep again. Blocking while transmitting is a feature in this case as sleep has to wait until it's done transmitting anyway. Speed is fixed at 9,600 bps but that is a matter of changing the PERIOD constant.

So here my final super small Serial transmission code, adding only just over 200 bytes to the sketch, and it seems to work perfectly. It doesn't use millis()/micros() so works even with those disabled.

// Very simple bit banged Serial Tx code.
// Runs at a fixed 9,600 bps speed; can use any pin; no Rx; blocks until byte is sent. No buffering.

// Change these to use another pin
#define TX_PORT PORTA
#define TX_PIN  PA7
#define TX_DDR  DDRA
#define TX_DDR_PIN DDA7

const uint8_t PERIOD = 104;                                 // 104 us per bit at 9600 bps overhead.
const uint8_t OVERHEAD = 1;                                 // 1 us for code overhead (at 20 MHz clock - more when running at lower speed)

// Blocks until character is completely sent.
// With the timing correction this comes to 1.04 ms for a complete byte, external crystal at 20 MHz.
// Lower clock speeds should have a bit more correction as the overhead takes longer.
void UART_tx(char character) {
  bitClear(TX_PORT, TX_PIN);                                // The start bit: low level, logic 0.
  delayMicroseconds(PERIOD - OVERHEAD);                     // Correction for software overhead
  for (uint8_t i = 0; i < 8; i++) {
    bitWrite(TX_PORT, TX_PIN, bitRead(character, i));       // Iterate over the byte to send: LSB first, MSB last.
    delayMicroseconds(PERIOD - OVERHEAD);                   // Correction for software overhead
  }
  bitSet(TX_PORT, TX_PIN);                                  // The stop bit: high level, logic 1.
  delayMicroseconds(PERIOD);                                // Stop bit is a little too long now, that dooesn't matter, it has no upper limit.
}

void UART_tx_str(char* string) {                            // pointer to null terminated string.
  while (*string) {
    UART_tx(*string++);                                     // Transmit the characters one by one.
  }
}

void UART_init() {
  bitSet(TX_DDR, TX_DDR_PIN);                               // Pin set to OUTPUT.
  bitSet(TX_PORT, TX_PIN);                                  // Pin set to HIGH.
}

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.