Use P / Invoke to develop .NET base classes for communication with serial devices
Translation: tavor
This article can help you familiarize with C # development with RS232.
Happiness 1, 2, 3
This article related code download: netserialcomm.exe (89kb)
Http://download.microsoft.com/download/8/3/F/83F69587-47F1-48E2-86A6-AAB14F01F1FE/netserialcomm.exe
Guide: The only way to write an application with RS252 serial ports in the .NET environment is to reference the outdated and a little restricted MSCOMM ActiveX control. This article introduces a multi-thread, and fashionable basic class library with the RS232 communication with C # security code. This class library uses the platform call service (ie Platform Invocation Services) to interact directly with Win32 API. Programmers can use this class in any .NET language; this article also explores some sample programs written by C # and Visual Basic .NET.
Microsoft .NET Framework Class Bank (FCL) provides a considerable full range of functions to replace the original features under Win32 @ API programming, especially the visibility of the Visual Basic @ .NET language. Despite this, RS232 serial communication is one of the .NET Framework class libraries is one of the aspects that have not been involved. It is normal, and many people regard the interfaces as the abandonment. Currently, you still communicate with the serial modem through the software layer, such as TAPI and PPP. Other devices used to use these interfaces are now transplanted with USB interface. However, some professional RS232 devices still have communication needs, such as the GPS receiver, Barcode and Swipe Card Readers, programmable controllers, and some predictable devices that will continue to use. (About the specifications of the RS232 interface, you can see "Hardware Specs".)
Platform Call Services (P / Invoke) is a .NET technology capable of calling unmanaged DLLs using managed CLR code, including DLLs that implement Win32 API. In this article, I will use C # to encapsulate the API of the RS232 communication to the CLR hosting code. The generated base class library will use the .NET language to develop specific devices drive relatively easy. The complete code and example can be downloaded from the link at the top of this article.
Design principle
When encapsulating the Win32 serial communication function, there are at least four implementations to make you choose: 1. Use P / Invoke to encapsulate the API function, constant, and structure as static members into the hosted class. Although I used this method inside, there is no such class to expose to the programmer. 2. Write a stream for processing roles. This is the general and expandable extraction of the .NET framework for files, consisters, network communication. At first, this is very attractive, but when reviewing, this is more suitable for traditional modems, not suitable for devices based on command response syntax. 3. Do a substitute for direct replacement MScomm Ole Control Extension (OCX) controls. In other words, new builds encapsulated API files and provides many basic methods and events (such as Open, Close, Read, Write, etc.). You can initialize an object in this class library to reuse the purpose of reuse in the application class - that is, a collection of com-style. 4. Write an application that needs to inherit the base class. This is an object-oriented method that fully reflects .NET Benefits - Object-oriented Method for Inheriting Different Language. These basic methods are inherited into the application object, and the virtual method will be used, not the use of an event. This application object will subtly provide a common interface for true RS232 devices (for example, a GPS receiver driver may have some public properties about longitude and latitude). I will adopt the fourth way. This class library will contain two base classes (they can't be sampled) that are cited as abstract classes, but I will use inheritance as a base class that implements certain specific applications. Figure 1 shows the hierarchical relationship of this inheritance.
Figure 1 inherited level
The first library class, CommBase, format data, easier to turn on and off communication interface, send and receive byte data, input and output interaction, etc., do not provide any implementation.
The second library class, Commline, inherits from COMMBASE, and makes two implementations: Receive the byte of the transmitted byte is an ASCII encoding and uses a reserved ASCII control encoding to mark the variable length of the number of data lines, and can receive and transmit String. Of course, this model is scalable; for example, you can write the optional Unicode version of CommLine.
Use base class
Examples of the two applications, baseterm and lineterm, can be downloaded. They can be used to communicate with any serial equipment, including modems. I will briefly look at Baseterm from a user's point of view, then analyze a LINETERM source code more detailed.
Figure 2 Baseterm
Baseterm (see Figure 2) is a complete Windows @ form-based application that inherits from COMMBASE and provides a byte-based adjustable terminal. Click Settings to press the New button to open a dialog box to set all the parameters of the communication settings (see Figure 3). The menu on this window can help users save or load these setting parameters in a structured XML file, and save a large number of settings for normal stream control mode. The prompt explains the usage of each setting item. Once saved as XML, when you start this program again, you can set this file under the command line format. Once connected to the line, the characters you want can be transferred to the remote device immediately. The button on the keyboard sends a suitable ASCII byte if you want to send the encoding on the keyboard, you can use "Escape facility".
Figure 3 Comm Setting
"Escape" can be turned on by entering LINETERM uses Commline as its base class and declares how to use this library in the source code. Because no user interface is used for settings, you need to run it in the Visual Studio .NET environment. In Visual Studio .NET, create a new Visual Basic console application. Remove the default module from the project. Copy lineterm.vb, common.dll, and commbase.xml three files to the project folder (where the XML file this library file provides intelligent prompt information). Add the existing items in the project browser to add the lineterm.vb to the project and add the commbase.dll to the referenced by adding a reference. Now you can compile and run this project. Imports Jh.comMbase Public Class Lineterm Inherits Commline Public Sub Sendcommand (Byval S AS String) Send (s) End Sub Public Sub TransactCommand (Byval S AS String) DIM R AS STRING R = transact (s) Console.writeline ("Response:" R) Prompt () End Sub Public Sub ProT () Console.Writeline ("Type String to send and press Enter.empty String To Close Comm Port. ") End Sub Protected Overrides Function CommSettings () As CommbaseSettings DIM CS As New CommlineSettings () CS.setStandard ("COM1:", 19200, Handshake.none CS.RXFILTER = new ascii () {ascii.lf, ascii.soh} cs.rxterminator = ascii.cr Cs.txterminator = new ascii () {ascii.cr} Setup (CS) Return CS END FUNCTION Protected Overrides Sub OnRxline (Byval S as String) Console.writeline ("received:" s) Prompt () End sub protected overrides sub ontxdone () Console.writeline ("Transmission Complete") Prompt () End Sub End classmodule module1 sub main () DIM T as new liningm () DIM C As String Console.Writeline ("press enter to open com port" console.readline () IF T.Open () THEN Console.writeline ("COM port open") T.Prompt () While True C = console.readline (). Trim IF c = "" THEN EXIT While T.sendcommand (C) 'T.TransactCommand (C) End while t.close () END IF Console.writeline ("COM port closed") Console.writeline ("Press Enter to Close Program) Console.readline () End Sub End module Figure 4 LINETERM sample code Figure 4 shows us a complete source code for this example. In this first line, I introduced the namespace of the library. Then I built a new class, LINETERM, which inherited from Comline. It provides open and closes these two public methods (actually inheriting from the COMMBASE class), as well as the protection method sent, and I do it as a public as sendcommand. In my new class, I overloaded a lot of false ways in the base class. Call the CommSettings method when an open window is configured; it returns a commckasesettings object that has been initialized. Here I actually use CommlineSettings because it inherits from Commbasesettings. In the last two lines of this method, first I pass an object that inherits from Commline to the setup method and returns it to the CommBase class. All setting items are public members, which can be set directly, but there is also an auxiliary method, SetStandard, which automatically configures the COMMBASE class to the most common configuration. You may need to edit this method and terminal line, filter the member's parameters to suit your device for testing. The main method of the application simply creates an instance of my class and calls the Open method, and provides a command line interface that can be used to send a string and display the received string. There are two ways to complete this, blocking and non-blocking. Use SendCommand to start non-blocking communication. This method immediately returns, soon the end, the overtxdone method of the overload will report the result. Later, when the remote device completes an input of a response signal, the overloaded Onexline method will display the result on the console. At this point, the primary process waits for user input, but it may also be ongoing other work. If you comment out SendCommand and replace it with TransactCommand, blocked communication will be used. At this point, the main process will remain in the blocking mode until an effectively responds. You can quietly wait for the result information returned from the ONTXDONE method, but instead of the received message returned from the onRxline method, you will see the information returned from the TransactCommand method. Figure 5 flow control of GPS In a real application, such as a GPS receiver driver, you can't let it like the SEND and Transact public methods I only implement in the example. Instead, you need to provide all public methods and properties that can express this device function (such as speed and intensity properties, or events such as positionchanged). These methods must set the necessary commands. Use the Transact method and convert the return value to the response release. Figure 5 is a flow control for such devices. send In serial communication, in most cases, sending information is much easier than receiving information. For receiving information, you may be embarrassed to the remote device, but for transmission, you can still control the time. Despite this, the general transmission rate of 2 to 20,000 baots you may not want to wait for the completion of the transfer than the computer with the speed of the Gigabo. The Win32 API regards serial ports to a special example of the file operation and uses and referred to as a technique overlapped with I / O to provide non-blocking operations. The CommBase class provides Open's public methods, which uses Win32API's CEEATFILE method to open a serial port and store the result of the operating system as a private member variable: Hport = win32com.createfile (cs.port, win32com.generic_read | Win32com.Generic_Write, 0, INTPTR.GENERIC_WRITE, 0, INTPTR.GENERO, WIN32COM.Open_existing, Win32com.File_Flag_Overlapped, INTPTR.ZERO); The first parameter is the port name of the String type, often com1; or com2; but here, you can use any name, so I use the name instead of a number. I haven't had a way to determine a string of valid port name, so choose a fact that allows the caller to try to open any port and accept the fact that can fail. May fail when port exists but is being used by another application. I use file_flag_overlapped to describe all operations in this file handle are non-blocking, while other parameters are only like serial communication. Win32com is a closed API function, structure, constant container-type auxiliary class, I will call it via P / Invoke. Creatfile is shown in C # as follows: [DllImport ( "kernel32.dll", SetLastError = true)] internal static extern IntPtr CreateFile (String lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile); Various constants are also defined here, such as: INTERNAL const uint32 file_flag_overlapped = 0x40000000; Because there is almost no tool to support P / Invoke, I have to manually define these. Key resources include Win32 documents and header files provided in C languages. The excellent file search engine in Visual Studio .NET is very useful for searching definitions in the header file. (I use these as a document, you don't need these when you compile the library file.) About Interop Marshaling's full discussion, the C language definition used by the managed data type as the unmanaged API Not within the scope of this article discussed. However, you can understand what happened from another code snippet from the open method: wo.offset = 0; wo.offsethigh = 0; if (checksends) hevent = writeeevent.handle; else wo.Hevent = INTPTR.ZERO; PTRUWO = Marshal.allochglobal (Marshal.SizeOf (WO)); Marshal.StructureToptr (Wo, Ptruwo, True); Here, WO is a local variable of Win32com.Overlapped type, and Ptruwo is a private class variable for an INTPTR type. Marshal is a global object provided by System.Runtime.InteropServices for accessing Interop Marshaler. In this code, when the external function is called, I manually handle the transactions that Marshaler automatically processed. First, allocate a size suitable non-managed memory, followed by copying the content of the hosted structure to it, and re-allocate the memory as needed. After this function is called, Marshaler will use Marshal.PTRTOStructure to display the copy content, then Marshal.Freehglobal will release memory. Because the API uses the special way of the Overlapped structure, I manually complete this operation. I will set a value in the WriteFile method, but when this call returns, the operating system will continue to use it. Soon, I will call the GetOverlappedResult method and describe the same structure again. If I leave it to automatic preparation, this task will be allocated again in non-managed memory between the two calls. If this is, unmarshaling is no longer necessary, because these domains do not need to be accessed. Despite this, these memory must be released when the port is closed: IF (Ptruwo! = INTPTR.ZERO) Marshal.Freehglobal (Ptruwo); Use these replacement base code, actually sending a group of bytes is very straightforward: IF (! Win32com.writefile (HPORT, TOSEND, (UINT) WRITECUNT, OUT SENT, PTRUWO) IF (Marshal.getlastwin32erRor! = Win32com.Error_io_pending) throwexception ("Unexpected Failure"; The parameter tosend is a pointer to the byte data; WriteCount is the length of byte data; the parameter SENT will return the number of the actual bytes; the parameter Ptruwo is the previously created unmanaged Overlapped pointer. Under normal circumstances, this method will return false, and the error code will be error_io_pending. This is a false error indicating that the operation is queued without being executed immediately. Other error codes indicate that the corresponding operation is not a queue queue. Due to the cache function of serial port hardware and send short strings, this operation may be completed immediately, this method will return True. Before sending new data, the result of the previous Send method will check the error conditions and timeout values. (Strangely, the API handles the pending operation as an error, but the timeout may be very normal - the only way to detect sends a small number of bytes instead of queuing. Eliminate this exception is written this package library A fun!) Although I allow multiple send hangs, each of them has their own overlapped structure, which will increase a lot of complexity. Instead, I have blocked the concurrent send until the previous completion. If blocking is a problem, you can turn off this feature by setting the checkallsends member to false. In this case, the Overlapped structure can be reused, and all errors and timeout can be captured. receive Maybe you have guessed that receiving data is just a simple call of the ReadFile this API method. As mentioned previously, the difficulty is not received, but when is received. In order to avoid the continuous application of the application, the settings in some callbacks are required. This function can be implemented in the virtual method of the working thread. COMMBASE calls a virtual method when receiving each byte. This method is to overwrite the buffer byte from the COMMLINE class, and call another virtual method when the line terminal connector is received. I created the second running thread to complete this work in the Open method. RXthread = new thread (new threadstart (this.receivethread); rxthread.name = "combaserx"; rxthread.priority = threadpriority.abovenormal; rxthread.start; thread.sleep (1); Start a new thread to run this code in the private method Recethread. I am very surprised by me; I am causing this new new priority to replace the original thread in the Start command. In some cases, it is not, this has caused trouble, because when it first needs to be called, the work thread is not often ready. In the second test, I used Sleep (0), because in the documentation, it is recommended that this will not waste any time (almost one millisecond advantage), but in fact this does not have any effect. Recetyad is a dead cycle code, only one case can break this dead cycle. I use the following line code to terminate the thread when turning off the port: Rxthread.abort; In this thread, it throws a ThreadaboutException exception, captures and end it through the Catch clause used to clean it. Finally clauses will also be used, but in this case, there is nothing, because only one exception can make it exit. private void ReceiveThread () {byte [] buf = new Byte [1]; uint gotbytes; AutoResetEvent sg = new AutoResetEvent (false); Win32Com.OVERLAPPED ov = new Win32Com.OVERLAPPED (); IntPtr unmanagedOv = Marshal.AllocHGlobal (Marshal. Sizeof (ov)); ov.offset = 0; ov.offsethiGH = 0; ov.hevent = sg.handle; Marshal.StructureToptr (OV, unmanagedov, true); uint eventmask = 0; intptr umask = Marshal.allochglobal (Marshal .Sizeof (eventmask); try {while (true) {if (! Win32com.setcommmask (hport, win32com.ev_rxcha) {throw new commitexception ("IO error [001]);} Marshal.writeInt32 (Umask, 0 ); If (! Win32com.waitcommevent (hport, umask, unmanagedov) {if (Marshal.getlastwin32error () == Win32com.Error_io_pending) {sg.waitone ();} else {throw new commportException ("io error [002] ");}} Eventmask = (uint) Marshal.Readint32 (umask); if ((EventMask & Win32com.ev_rxchar)! = 0) {DO {gotbytes = 0; if (! Win32com.readfile (hport, buf, 1, out gotbytes, unmanagedov) {if (Marshal.getlastwin32error () == Win32com.Error_io_pending) {Win32com.cancelio (HPORT); gotbytes = 0;} else {throw new commportException ("IO Error [004]");}} f (gotbytes == 1) OnRxChar (BUF [0]);} while (gotbytes> 0);}}} catch (Exception E ) {IF (umask! = INTPTR.ZERO) Marshal.Freehglobal (umask); if (unmanagedov! = INTPTR.ZERO) MARSHAL.FREEHGLOBAL (Unmanagedov); if (! (E is threadabortexception) {rxException = E; ONRXEXCEPTION (E);}}} Figure 6 Simple version of the thread Figure 6 is a simplified version of ReceiveThread. SetCommmask indicates that when a new byte arrives, I hope to be notified. Waitcommmevent may return True, in which case one or more bytes are in the queue. If you return Error_IO_PENDING that came with an error code, I will hang this thread until there is a byte arrival. The overlapped structure passed to Waitcommmevent contains a handle to AutoreteTevent, which is used as a signal that is from byte. When I execute the AUTORESETEVENT Waitone method, execution is hangned until this event is triggered. Regardless of WaitCommEvent immediately returns to TRUE or the signal shortly, the EventMask variable has a condition for identifying setcommmask normally caused (in actual code, I also describe some other household management conditions). Note that I use manual arrangement techniques for EventMask as previously described in Overlapped. I speculate that this may be unnecessary, perhaps automatically arrange, but there is no precise description in the document, because this is more secure some of the more regrets. Replace the unordered pointer with hosted variables as reference parameters seem useful, but it may be just because the memory is not reused. Because depending on time, not just a character to queue, so each use of readfile to discharge one byte, reuse, and use a character to call the virtual method onRxChar. When I receive an error_IO_PENDING code, I call the Cancelio method, thereby avoiding waiting here; I want to wait in the waitcommmevent loop. It needs to be processed in using the working thread, error handling and exception. Any unprocessed anomaly that occurs in ReceiveThread, and any virtual method that calls it, and events that are called by the above, and the event will be closer and will be processed by the CATCH clause. If the exception generated is not ThreadAbortexception exception, it is stored in a CommBase class as a private member, and this thread will be aborted. Next time the program code will call a method and then cause an exception, the port will turn off. This makes full use of the advantages of the built-in abnormal structure. When a general "receiving thread error" is triggered, it contains the original thread stored inside. ThrowException is an auxiliary method provided in the inheritance class; it adjusts its behavior through the thread it calls. Configuration and other details I read all configurations from a CommBaseSettings auxiliary class. The OPEN method obtains this object by calling the virtual method CommSettings and copy all the places to the API structure. The COMMBASESETINGS class also provides methods for saving and overridden ways to configure the XML configuration file, and a large number of general configurations. I use the intelligent help tips on the window to provide help documents for configuration. Because inheritance classes provide their own inheritance from the CommbaseSettings class configuration class, this configuration provides an extensibility base configuration structure. In this way I inherited the CommlineSettings class, providing additional configuration for the CommLine class. There are three API methods for configuring communication protocols: setUpComm, setcommstate, and setcommtimeouts. SetupComm methods require a buffer size and transmission queue. Under normal circumstances, you can set these sets to 0 or decide according to the operating system, but for some file transfer and simple applications, the required size of adjustable is worth it. The system does not necessarily satisfy this need; in Windowns XP, this is like a dynamic transmission queue and only the length of the queue is required. The setcommstate method provides configuration information such as baud transmission rate, word format, and handshake setting in a structure called device control block (DCB). SetCommTimeouts provides three reception and two transfer timeout values in the CommTimeout structure. The reception timeout value is useless to the design I have selected, because a single character is asynchronous. If the reception timeout is required, it must be implemented on a high level (for example, Commline provides a timeout for its transmission method). The transfer timeout is useful, especially for multi-byte transmissions. The number of bytes in the Send method is by SendtimeoutMultiplier, and the SendTimeoutConstant is attached to this and provides the total time in microseconds. Once the port is opened and bedding, the Open method calls an afteropen virtual method that will be rewritten to check the connection status of the remote settings and configure it as much as possible. If this returns false, the port will be closed again and the Open method will return false. If necessary, there is a BeforeClose method to turn off the remote device. The CommB ASE also provides two overload versions of the Send method, one provides an array of parameters to provide another separate byte as a parameter. Commline provides the third version of the Send method with a string as a parameter. After performing the right data transformation, all of these finally use bytes array versions. A SendimMediate method with a single byte is also provided. It will transmit this byte in the transmission queue than other bytes, and is very useful to implement custom flow control modes. It also provides some to transfer requests, data terminals are prepared to output pins and output TX to pause conditions. Enter pin-CTEAR-TO-Send (CTS), Data Set Ready (DSR), Received Line Signal Detector (RLSD), and Ring Detect- You can use getModemStatus to read directly, when arbitrary input or output pins change state, OnStatusChange will be called. The GetQueueStatus method will return a queuesttus object and give the size, content, receive queue, and if necessary, the flow control condition is now block transmission. in conclusion I use Platform Invocation Services to fill a blank on the FCL function. But this is clearly an extraordinary but very feasible exercise. The absolute majority of the existence is that there is no complete tool and document support for P / INVOKE. Finally, I will make a summary. As part of this project, I wrote and tested all the full encapsulation of Win32 Waitable Events API before I considering the ManualReveTeeseTevent Framework classes. Remember: At the time, you only need to spend all your time in writing a new class rather than the thing you need to use existing. Check your hard drive before you have been retrofit. Based on this principle, I hope that these base classes can help other programmers bring the RS232 device communication into the .NET world. Related Articles, please see: House of com: atking, native code to the .NET CLRSERIALIALIALINICATION OVERVIEWRS232 Standard background knowledge, please see: Net and com: The Complete Interoperability Guide By Adam Nathan (Sams Publishing, 2002) John Hind is a free use of liberty and consultants in London, England. He focuses on microcontrolled applications and control solutions. You can contact him through john.hind@zen.co.uk.