Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UART TX_FIFO_EMPTY interrupt on start of transmission #1645

Open
egr8086 opened this issue Mar 11, 2019 · 7 comments
Open

UART TX_FIFO_EMPTY interrupt on start of transmission #1645

egr8086 opened this issue Mar 11, 2019 · 7 comments

Comments

@egr8086
Copy link

egr8086 commented Mar 11, 2019

I working on modbus RTU implementation using Sming and HardwsreSerial. Before uart transmission i need to pull up TX_ENABLE pin (GPIO 0 in my case) and pull it down after transmission complete. But txCompleteDelegate() calls on begin of transmission.

Here is initialization:

MbMaster::MbMaster(HardwareSerial & port, int baud, int8_t txEnGpio) : 
port(port), baud(baud), txEnGpio(txEnGpio) 
{
	port.begin(baud);
	port.onTransmitComplete(TransmitCompleteDelegate(&MbMaster::txCompleteDelegate, this));
	port.onDataReceived(StreamDataReceivedDelegate(&MbMaster::rxIsrDelegate, this));
	port.setTxBufferSize(256);
	port.systemDebugOutput(false);

	pinMode(txEnGpio, OUTPUT);
	digitalWrite(txEnGpio, 0);

	state = MB_IDLE;
	poll_timer.initializeUs(50, TimerDelegate(&MbMaster::poll, this)).start();
}

txCompleteDelegate:

void MbMaster::txCompleteDelegate(HardwareSerial& serial)
{
	if(serial.getUart()->uart_nr == port.getUart()->uart_nr) {
		state = MB_TX_COMPLETE;
		digitalWrite(txEnGpio, 0);
		last_rx_tick = tick_counter_us_x50;
	}
}

UART_TX

setTxWait(true) doesn't help.

@mikee47
Copy link
Contributor

mikee47 commented Mar 11, 2019

@egr8086 Thanks for this, I'm working on some serial driver changes ATM so will look at this also.

I have a generic modbus RTU driver for Sming here:

https://github.com/mikee47/Sming/tree/feature/modbus/Sming/Services/Modbus

It uses a serial driver interrupt callback (see Modbus::uartCallback) to ensure the TX control line is switched promptly. This isn't set up until Modbus::execute() is called, so isn't affected by the issue you describe.

It would be great to have a generic driver for Sming, so perhaps we could work on this together?

Example:

Modbus modbus;
ModbusTransaction mbt;

// Process transaction result
void callback(modbusTransaction& mbt)
{
  if(mbt.status == MBE_Success) {
    // All good, deal with result data
    ...
  } else {
    String statusStr = modbusExceptionString(mbt.status);
    debug_e("modbus error %02X (%s)", mbt.status, statusStr.c_str());
  }
}

void modbusBegin()
{
  modbus.begin(0, callback);
  // Setup outgoing transaction parameters
  mbt.param = ...
  mbt.slaveId = ...
  mbt.baudrate = ...
  mbt.function = MB_ReadHoldingRegisters;
  mbt.data[0] = 0x00;
  mbt.data[1] = 0;
  mbt.data[2] = 0;
  mbt.data[3] = 127;
  mbt.dataSize = 4;
  // Start the transaction
  modbus.execute(mbt);
}

@kmihaylov
Copy link
Contributor

kmihaylov commented Apr 12, 2020

Hello.

I observe unexpected behavior when trying to use Serial.onTransmitComplete() callback. In the ModbusMaster example I removed postTransmission() and in the init() function I set a callback: modbusComPort.onTransmitComplete([](HardwareSerial &) {digitalWrite(RS485_RE_PIN, LOW);});

BTW I just landed on this issue (hinted by github). I have exactly the same problem as @egr8086. The proper set/reset of the RE pin of my RS485 interface could happen only with interrupts/callbacks. Manually setting it after Serial.write() breaks the timing.

I'm reading the non-os api reference and here is what makes me curious:

Definition: After configure empty threshold value and enable interrupts ,it will trigger this empty interrupt when the data length of the data-send queue is less than the set threshold. Application: Can be used in forwarding the buffer data into UART automatically with the cooperation of interrupt handler function.For example,set the empty threshold to 5, then when the tx fifo length be less than 5 bytes,trigger the empty interrupt,in the empty interrupt handler ,take the data from the buffer to fill the tx fifo full(operating speed is much higher than tx fifo fifo transmission speed ). Continue the cycle until the buffer data has totally been sent out, then close the empty interrupt.

@kpishere
Copy link

Oh, hey guys, you may find the code in this class useful in achieving this. I made a driver for RS485 that controls the read/write enable pins in a very specific way for another half-duplex protocol. It could be useful for reference to implementing MODBUS.

https://github.com/kpishere/Net485/blob/master/src/Net485Physical_HardwareSerial.hpp

In the protocol I'm using this for, there was specific requirements for pre-drive and post-drive timings.

@kmihaylov
Copy link
Contributor

@kpishere Unfortunately I don't understand how your program verifies completion of TX (TX transmit empty).

I'm attaching some scope shots of the problem.

SHS00002

SHS00001

I tried both with

	if(status & _BV(UIFE)) {
		// Set MAX485 back into read mode
		digitalWrite(RS485_RE_PIN, LOW);
	}

(with setUartCallback)
and with

void postTransmission()
{
	while(uart_tx_free(modbusComPort.getUart()) < 127) {}
	digitalWrite(RS485_RE_PIN, LOW);
}

@mikee47
Copy link
Contributor

mikee47 commented Apr 13, 2020

@kmihaylov Be aware that the FIFO empty interrupt occurs when the last character has been moved from the FIFO into the hardware sending register. Specifically, that means the last character will be lost if the RS485 direction is changed. This is why my implementation adds a NUL character at the end. Behaviour is confirmed by your 'scope traces.

Also bear in mind that Serial.onTransmitComplete() callback is invoked via the task queue, so there will be an additional delay between the interrupt firing and callback code, hence interrupts are usually preferable.

@kpishere
Copy link

@kmihaylov Oh, it is simpler than you might be looking for. Essentially, I don't verify completion, I simply calculate the time needed to send the known number of bytes at a fixed bit rate.

The macro here does the calc. This wouldn't work if there were any sort of flow control etc. but there isn't in this case. Packets in my usage case don't exceed 252 bytes either.

case DriveStateE::Sending:
setTimer(MTU_PACKET_TIME(baudRate,packetToSend->dataSize), DriveStateE::PostDrive);
hwSerial->write(packetToSend->buffer,MTU_HEADER + packetToSend->dataSize + MTU_CHECKSUM);

@kmihaylov
Copy link
Contributor

@kpishere ah I see, good to know! Thanks!
@mikee47 My test right now shows that onTransmitComplete got triggered on each Serial.write().

This solution

someFunc() {
Serial.write(NUL);
Serial.write(data);
}

will trigger it two times and the first time it will immediately close the TX line. What I did is to get rid of the first NUL character and to add a NUL character at the end of the data array that has to be send.

This way it works.

Meanwhile I'm struggling with the interrupts. On ModbusMaster I managed to do it, but on modbusino, for some reason, it didn't fire and leaves the TX line pulled by the slave. I'll clean the code and investigate it a little bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants