Simple Serial Communication with Borland Turbo C++ Explorer

Introduction

It's relatively easy to write a simple serial communications console program and many examples on the web abound. But things get more complicated in the event-driven GUI world of Windows. Turbo C++ Explorer does not come with a serial component so what do you do if you want a quick and simple program or just want to learn about serial communication? Here is an example application that you can write yourself in 5 minutes. Rather than using separate threads and complex overlapped I/O handling or intalling a component, this example uses a simple, easy to understand polled loop.

The example has 2 buttons, Start and Stop, and a TMemo text box. When the application is running, click Start to open a COM port. Once the port is open, incoming serial data will appear in the text box. Characters typed into the text box will be transmitted. Click Stop to close the COM port. That's it. The example code is explained below. You can either follow along and write your own version or simply download the Turbo C++ Explorer Simple Serial project directly. A complete code listing appears at the end of this page.

Create the Application

Open Turbo C++ Explorer and start a New Project. Select the icon for a VCL Forms Application. This will create a blank form and skeleton code for the application. From the Components palette, add two TButtons and a TMemo control to Form1. Name the buttons ButtonStart and ButtonStop respectively. Set the ButtonStop Enable property to False. Set the Memo1 ScrollBars property to ssVertical. Your form should look like the one to the right.

Use the Object Inspector to add an OnClick event handler for each button. Refer to the full code listing and copy the ButtonStartClick code into the empty ButtonStartClick event handler. Do the same for ButtonStopClick.

Again using the Object Inspector, add an OnCloseQuery event handler to Form1. Copy and paste the FormCloseQuery listing below (it's just one line). Add an OnKeyPress event handler for Memo1 and again copy the code from the listing. Add the three global variables that appear just after the Form1 declaration and you are done.

Test the Application

With the form created and code written in the Turbo C++ Explorer IDE, press F9 (Run) and your simple serial communications program is running. Press the Start button to open the COM port. Incoming serial text data will appear in Memo1 and anything you type into Memo1 will be transmitted. If you don't have a connected serial device suitable for testing the program, use a loopback connector. Make one by simply jumpering pins 2 and 3 of a serial cable or DB9F connector. With a loopback connector, anything you type in Memo1 will be echoed back through the port and displayed in Memo1.

If the program doesn't work, carefully review your code and serial connections. The COM port designated in CreateFile() must be the one your are connected to and actually exist on your computer. COM1 is most common but you might be connected to COM2, COM3, etc. If the port is connected to an external serial device, the comm parameters (baud rate, etc.) of the device and your program must agree. Change parameters in the BuildCommDCB() function if needed.

How it Works

Almost all of the work is performed in the ButtonStartClick event handler so we'll focus on that code.

In the ButtonStartClick event handler, we begin by declaring a few local variables. Their use will be apparent as we proceed.


  DCB dcbCommPort;
  COMMTIMEOUTS CommTimeouts;
  DWORD BytesRead;
  

Next we call the WIN32 API CreateFile() function to OPEN the port. The "COM1" string designates the port number. Change this string to any port on your machine (COM2, COM3, etc.) but don't change any of the other CreateFile() parameters. Doing so might make our simple example work incorrectly. If the COM port cannot be opened, an error message will be displayed in Memo1 and we return. You'll need to find out why the port didn't open. It could be caused by a COM port that is already in use by another program or a port that does not exist on your machine.

  
  hComm = CreateFile( "COM1",
          GENERIC_READ | GENERIC_WRITE,
          0,
          0,
          OPEN_EXISTING,
          0,
          0);

  if (hComm == INVALID_HANDLE_VALUE) {
    Memo1->Lines->Add("Could not open comm port.");
    return;
  }

The next group of functions configure the serial Device Control Block (DCB) which controls baud rate, parity, data bits, and stop bits among other things. We call the WIN32 API GetCommDCB() function to load existing DCB parameters into our dcbCommPort structure. We next call BuildCommDCB() with a string describing baud rate, parity, data bits, and stop bits. Although the DCB has many parameters, in our example we're keeping it simple by using the BuildCommDCB() function to populate the dcbCommPort structure for us. Finally, calling SetCommState() loads our parameters into the DCB for our open port. If it's successful, we move on, otherwise we close the port, print an error message, and return.


  dcbCommPort.DCBlength = sizeof(DCB);
  GetCommState(hComm, &dcbCommPort);

  if(!BuildCommDCB("baud=9600 parity=N data=8 stop=1", &dcbCommPort)) {
    hComm = NULL;
    Memo1->Lines->Add("Cannot build comm DCB.");
    return;
  }

  if(!SetCommState(hComm, &dcbCommPort)) {
    CloseHandle(hComm);
    hComm = NULL;
    Memo1->Lines->Add("Cannot set comm state.");
    return;
  }

We next set the CommTimeouts. These settings control delays when trying to read from or write to the comm port. The values used in this example cause no delays when reading the port and a maximum delay of 250 ms when writing (Actually, the write delay will never occur in our example because we transmit characters individually without calling WriteFile().) We load our timeout values by calling SetCommTimeouts(). If no errors are returned, the comm port is now configured the way we want it.


  CommTimeouts.ReadIntervalTimeout = MAXDWORD;
  CommTimeouts.ReadTotalTimeoutConstant = 0;
  CommTimeouts.ReadTotalTimeoutMultiplier = 0;
  CommTimeouts.WriteTotalTimeoutConstant = 250;
  CommTimeouts.WriteTotalTimeoutMultiplier = 1;

  if(!SetCommTimeouts(hComm, &CommTimeouts)) {
    CloseHandle(hComm);
    hComm = NULL;
    Memo1->Lines->Add("Cannot set timeouts.");
    return;
  }

Before reading the comm port, we set CommFlag, disable the Start button, enable the Stop button, and finish by allowing text to be entered in Memo1 then give it focus.


  CommFlag = true;
  ButtonStart->Enabled = false;
  ButtonStop->Enabled = true;
  Memo1->ReadOnly = false;
  Memo1->SetFocus();

We are now at the loop that is really the heart of our Turbo C++ serial communication application. The program will stay in this loop as long as CommFlag is true and it can only be made false by clicking the Stop button or closing the program. While in this loop, the program performs three significant tasks:

Calling ProcessMessages() inside the loop allows the application to handle other events like typing in Memo1 or clicking the Stop button. Sleep() prevents our loop from consuming all available CPU cycles which would far exceed our requirements. Because the serial device driver within the Windows OS has its own internal buffer, we won't miss any incoming characters while still allowing CPU time for other applications. It's the polite thing to do.

The ReadFile() function tries to read 100 bytes into our global array InBuffer. Because of our CommTimeout settings, ReadFile() will return immediately with whatever is available from the Windows internal serial receive buffer even if it's empty. If ReadFile() received any characters, we null-terminate the string and display it in Memo1. Pretty simple.


  while(CommFlag) {
    Application->ProcessMessages();

    Sleep(1)
    ReadFile(hComm, InBuffer, 100, &BytesRead, NULL);

    if(BytesRead) {
      InBuffer[BytesRead] = 0;
      Memo1->SetSelTextBuf(InBuffer);
    }

  }

Clicking the Stop button or exiting the application will reset CommFlag and execution will fall through the loop. Code after the loop closes the COM port, restores the buttons to their previous state, and returns from the ButtonStartClick event handler. You can see the code in the Full Listing.

Last but not least, notice that the Memo1KeyPress event handler transmits each key value typed into Memo1. It is common to not echo the locally typed characters to the display so we set Key=0 to prevent it. If you want to see the characters (local echo) just omit that line of code.


  if(hComm) TransmitCommChar(hComm, Key);
  Key = 0;

Final Thoughts

For the sake of clarity and simplicity, our example application omits a few things.

You are free to use all the information presented here. There are no restrictions. There's no support available but the program is simple enough that you probably won't need it. Send comments to

Links

SimpleSerial.zip Download Turbo C++ Explorer Simple Serial Project.
Microsoft MSDN Serial Communication Resources WIN32 API serial communication reference.
www.turboexplorer.com/cpp Borland's FREE Turbo C++ Explorer

Full Code Listing


  //---------------------------------------------------------------------------

  #include 
  #pragma hdrstop

  #include "Main.h"
  //---------------------------------------------------------------------------
  #pragma package(smart_init)
  #pragma resource "*.dfm"
  TForm1 *Form1;

  // Add these global variables
  HANDLE hComm = NULL;
  bool CommFlag;
  char InBuffer[101];
  
  //---------------------------------------------------------------------------
  __fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
  {
  
  }
  //---------------------------------------------------------------------------
  void __fastcall TForm1::ButtonStartClick(TObject *Sender)
  {
  
    // Add these local variables.
  
    DCB dcbCommPort;
    COMMTIMEOUTS CommTimeouts;
    DWORD BytesRead;
  
    // Open the port using WIN32 API CreateFile().
    // Change COM1 for different port.
    // Leave the remaining parameters alone for simple
    // non-overlapped serial I/O.
    
  
    hComm = CreateFile( "COM1",
            GENERIC_READ | GENERIC_WRITE,
            0,
            0,
            OPEN_EXISTING,
            0,
            0);
  
  
    // If port cannot open, print error message and return.
  
    if (hComm == INVALID_HANDLE_VALUE) {
      Memo1->Lines->Add("Could not open comm port.");
      return;
    }
  
    // Get the existing DCB parameters
  
    dcbCommPort.DCBlength = sizeof(DCB);
    GetCommState(hComm, &dcbCommPort);
  
  
    // Use the WIN32 API BuildCommDCB() to load simple comm parameters
    // into DCB. If it fails, print error message and return.
    
  
    if(!BuildCommDCB("baud=9600 parity=N data=8 stop=1", &dcbCommPort)) {
      hComm = NULL;
      Memo1->Lines->Add("Cannot build comm DCB.");
      return;
    }
  
  
    // Use the WIN32 API SetCommState() to set parameters from DCB.
    // If it fails, close hComm, print error message, and return.
  
    if(!SetCommState(hComm, &dcbCommPort)) {
      CloseHandle(hComm);
      hComm = NULL;
      Memo1->Lines->Add("Cannot set comm state.");
      return;
    }
  
  
    // Set CommTimeouts parameters.
    // See ReadFile() function later for details.
  
    CommTimeouts.ReadIntervalTimeout = MAXDWORD;
    CommTimeouts.ReadTotalTimeoutConstant = 0;
    CommTimeouts.ReadTotalTimeoutMultiplier = 0;
    CommTimeouts.WriteTotalTimeoutConstant = 250;
    CommTimeouts.WriteTotalTimeoutMultiplier = 1;
  
  
    // Use the WIN32 API SetCommTimeouts() to set parameters from DCB.
    // If it fails, close hComm, print error message, and return.
  
    if(!SetCommTimeouts(hComm, &CommTimeouts)) {
      CloseHandle(hComm);
      hComm = NULL;
      Memo1->Lines->Add("Cannot set comm timeouts.");
      return;
    }
  
    // Set CommFlag and Button states. Allow typing in Memo1.
  
    CommFlag = true;
    ButtonStart->Enabled = false;
    ButtonStop->Enabled = true;
    Memo1->ReadOnly = false;
    Memo1->SetFocus();
  
    // Constantly loop while CommFlag is true.
    // ProcessMessages() allows other other events within the program
    // to be serviced. ReadFile() tries to read 100 bytes into InBuffer.
    // Because of the timeouts we used earlier, ReadFile() will return
    // immediately with whatever characters it has (up to 100). The
    // BytesRead variable will contain the number of characters loaded
    // into InBuffer.
  
    // If BytesRead > 0, make the last charater in the received string = 0.
    // This terminates the string. Notice that we dimensioned InBuffer[101]
    // to accomodate the terminating 0 if necessary. Use SetSelTextBuff()
    // to append the contents of InBuffer onto the end of text in Memo1.
  
    // Pressing either the Stop button or Window close button will
    // set CommFlag = false. That will end the loop.
    
  
    while(CommFlag) {
      Application->ProcessMessages();
  
      Sleep(1)
      ReadFile(hComm, InBuffer, 100, &BytesRead, NULL);
  
      if(BytesRead) {
        InBuffer[BytesRead] = 0;
        Memo1->SetSelTextBuf(InBuffer);
      }
  
    }
  
    // Close the comm port and set hComm to NULL.
  
    CloseHandle(hComm);
    hComm = NULL;
  
    //Set button enables back to original state. Make Memo1 read-only.
  
    ButtonStart->Enabled = true;
    ButtonStop->Enabled = false;
    Memo1->ReadOnly = true;
  
  }
  //---------------------------------------------------------------------------
  void __fastcall TForm1::ButtonStopClick(TObject *Sender)
  {
    // If Stop button pressed...
  
    CommFlag = false;
  }
  //---------------------------------------------------------------------------
  void __fastcall TForm1::FormCloseQuery(TObject *Sender, bool &CanClose)
  {
    // If attempting to close the window...
  
    CommFlag = false;
  }
  //---------------------------------------------------------------------------
  void __fastcall TForm1::Memo1KeyPress(TObject *Sender, char &Key)
  {
    // If a Memo1 key is pressed, transmit it.
    // TransmitCommChar() returns 0 if the previous character has not
    // finished sending. Keep trying until non-zero is returned.
    
    if(hComm) while(TransmitCommChar(hComm, Key) == 0);

  
    // Set Key=0 so it will not echo in Memo1.
    // Omit this line if you want local echo.
    
    Key = 0;
  }
  
  //---------------------------------------------------------------------------