How to properly write a Win32 application using boost::asio to communicate over serial connection

Viewed 498

I'm developing a Windows application that has to communicate (both input and output) with an Arduino through its serial port. I'm using boost::asio for portability reasons and I want to keep using it. What happens is that the first time I run my application it works perfectly, but if I run it a second time, no data comes from the Arduino anymore and the application stucks on the read operation. The only way to recover is to unplug and replug the Arduino USB cable from the computer.

This behavior is Windows-specific. The same code works perfectly on Linux.

The compiler is Visual Studio 2017 Community Edition.

Here is an example code to reproduce the issue:

#include <iostream>
#include <string>
#include <boost/asio.hpp>
#include <vector>


int main() {
  boost::asio::serial_port port(ioctx, "COM3"); // "/dev/ttyACM0" on Linux

  port.set_option(boost::asio::serial_port::baud_rate(9600));
  port.set_option(boost::asio::serial_port::character_size(8));
  port.set_option(boost::asio::serial_port::stop_bits(boost::asio::serial_port::stop_bits::one));
  port.set_option(boost::asio::serial_port::parity(boost::asio::serial_port::parity::none));
  port.set_option(boost::asio::serial_port::flow_control(boost::asio::serial_port::flow_control::none));
  char c = 'e';
  auto const s = boost::asio::write(port, boost::asio::buffer(&c, 1));
  std::cout << "sent " << s << " bytes" << std::endl;
  boost::asio::streambuf response;
  boost::asio::read_until(port, response, "\r\n");
  std::istream response_stream(&response);
  std::string line;
  std::getline(response_stream, line);
  std::cout << line << std::endl;

  port.close(); // last-ditch effort to get it working
}

Here is an Arduino sketch (got from the Arduino website):

int incomingByte = 0;   // for incoming serial data                         

void setup() {
  Serial.begin(9600);   // opens serial port, sets data rate to 9600bps
}

void loop() {
  // send data only when you receive data:                            
  if (Serial.available() > 0) {
    // read the incoming byte:                                  
    incomingByte = Serial.read();                               

    // say what you got:
    Serial.print("I received: ");                               
    Serial.println(incomingByte, DEC);                          
  }
}

Is there a way to restore the correct state of the connection? Am I missing something?

2 Answers

After having learned a couple of things, here is the solution:

  1. Arduino uses its USB communication for both burning the sketch and performing data transmission back and forth the PC. The boot sequence foresees that for 2 seconds (for new Arduino versions and standard boot loader) the communication towards the boot loader is active. After that time, the sketch is executed.
  2. Windows API allows to set all connection parameters at once via the SetCommState function and to retrieve them in a similar fashion with the GetCommState one. That is the method the set_option function uses to set the parameters, but it happens that calling GetCommState-SetCommState multiple times in a row slows down the process a lot (maybe by resetting the Arduino multiple times).

I ended writing the following function:

    #include <Windows.h>
    #include <chrono>

    void init_arduino(boost::asio::serial_port& port, std::chrono::milliseconds const& sleep = 2000)
    {
      DCB dcbSerialParams = { 0 };
      GetCommState(port.native_handle(), &dcbSerialParams);
      // this is the optimal way to set the whole serial port configuration
      // just in one shot.
      dcbSerialParams.BaudRate = CBR_9600;
      dcbSerialParams.ByteSize = 8;
      dcbSerialParams.StopBits = ONESTOPBIT;
      dcbSerialParams.Parity = NOPARITY;
      //Setting the DTR to Control_Enable ensures that the Arduino is properly
      //reset upon establishing a connection
      dcbSerialParams.fDtrControl = DTR_CONTROL_ENABLE;
      SetCommState(port.native_handle(), &dcbSerialParams);
      PurgeComm(port.native_handle(), PURGE_RXCLEAR | PURGE_TXCLEAR);

      // Wait for Arduino to boot the sketch
      Sleep(sleep.count());
    }

and using it to replace the port.set_option( lines in the question example.

I also set the flow control to DTR_CONTROL_ENABLE instead of the original none in order to reset the Arduino upon connection.

USB serial adaptors can have device driver bugs and hardware problems. The fact that you have to unplug and plug the device to get it working again is indicative of a device driver bug.

  • Look for an updated driver. It will probably a Prolific or FTDI chipset, make sure you get the driver from the chip maker. See Prolific or FTDI
  • If it is a flow control related hardware problem you can wire together the DTR, DSR and CD pins, and wire together the RTS and CTS pins on the RS-232 connector on the USB adaptor. I have seen USB adaptors where this is necessary, despite setting no flow control in software.
Related