|
-
Jul 9th, 2008, 01:48 PM
#1
Serial Port
If you have a question about this please post the question here
http://www.vbforums.com/forumdisplay.php?f=25 not in this tread. Thanks!
Much of the difficulty I see here, and in other forums, surrounding the use of Windows serial port is centered around an assumption.
That assumption is that the serial port receives data in chunks that are conveniently packaged in the format we want. If you are talking about terminal stuff that is true, they are called lines and it is easy. But when that is not the case panic sets in.
The second thing I see is people fighting the DataReceived Event Handler. My advice is don’t. Treat it like a spoiled child that is never going to learn and just give it whatever it wants, whenever it wants.
Lastly, a byte can contain 256 values. The standard encoder for the serial port is for 7-bit data. If you have 8 bit data you need to change the encoding. I use
System.Text.Encoding.GetEncoding("Windows-1252") always and have not had any problems.
I wanted fast code, and to write the code in such a way that it could be used for any application.
I have successfully sent, received, processed, and updated the UI at close to 1Mbps (USB Serial Converter) using the techniques I will describe. My test bed is a USB SerialConverter to breakout box with Pins 2 & 3 hot wired.
**DataReceived Event Handler**
Mine is short, sweet, simple. I read the number of bytes available (.BytesToRead) into a buffer of bytes of the correct size. I store that buffer of bytes into a queue.
Depending on how the UI is being driven I then raise an event, start a delegate, or do nothing if it is timer or usr interaction based.
That is it! Any time it wants to fire, it can.
**So how does the data get to the user…**
Ask yourself what are the possible scenarios of how you might want data? You might want:
-one byte
-a number of bytes
-a character
-a string
-a string delimited by a character (like lines)
-etc.
And what if you want 2 bytes, then 4 characters, then a byte, and then a variable length line?
At this point my DataReceived Event Handler has placed any data received into a queue. Here is an example. I have a GPS receiver that sends NMEA sentences (lines ending with CRLF). This is an example of one of the sentences:
$GPGSV,3,1,11,10,75,053,29,29,52,311,32,24,50,298,30,02,39,073,30*77
The serial ports DataReceived Event Handler might (usually does on my PC) fire several times with chunks of that data. The queue might look like this:
Queue entry – data
1 $
2 GPGSV,3,1,11,10
3 ,75,053,29,29,52,311,
4 32,24,50,298,30,02,39,073,30*77
The approach I took, with my limited knowledge of the programming tool at the time, was to write several routines that pieced the data back together. The common routine for all of those created a user buffer by removing entries from the queue and piecing them back together. The other routines then access the user buffer when they need data, and in the case of string based functions, converts it. If there is not enough data to satisfy the request the routines return empty strings or zero length byte arrays.
Lets say I used a Timer control to read, process the sentences, and update the UI from my GPS receiver. In my Timer routine I have a function call to my version of readline. The first few times it is called it might return empty strings because the entire sentence has not been received. Eventually all of the data is in and I get the entire sentence back, because it is actually all there.
At time 1
Queue entry – data
1 $
2 GPGSV,3,1,11,10
User buffer – empty
At time 2
Queue entry – data
1 ,75,053,29,29,52,311,
2 32,24,50,298,30,02,39,073,30*77
User buffer – $ GPGSV,3,1,11,10
At time 3
Queue empty
User buffer – $ GPGSV,3,1,11,10,75,053,29,29,52,311, 32,24,50,298,30,02,39,073,30*77
The only imperative is that when you have a device that sends data all the time is that you process it in a timely manner. If not, as you can imagine, you run out of memory.
During my testing of the USB to SerialPort converter I was surprised to find that it was capable of speeds near 1Mbps and I made the mistake of not doing that. I was using a user interface driven (click a button, see the data) routine that read all of the data and displayed the last chunk, when I received a high priority interrupt from Honey. You know the one, take the trash out, change the light bulb, etc. I left the test bed running and I had received an out of memory exception while servicing the Honey interrupt.
What prompted me to write this was something I read in another thread. The poster had a problem because he said the device he was interfacing with only provided a 1 ms. gap in the data as its protocol. As it turned out the protocol was a delimiter, followed by a length byte, followed by the data. Then the problem was that that wasn’t enough to know that you actually had good data. The posters point was this:
message_delimiter - 1 byte
4 - length - 1 byte
(4 data bytes) - 4 bytes
But if you look at it this way, a **valid** message is always bounded by the message_delimiter:
message_delimiter - 1 byte
4 - length - 1 byte
(4 data bytes) - 4 bytes
message_delimiter - 1 byte
Then you can safely process the data.
Many of you are better at using your programming tool of choice. The code I wrote for serial ports was written several years ago when I was just learning VB .Net.(I cringe when I look at some of it today). But I am willing to bet that it will out perform most of the C(pick a flavor) code I have seen posted on many forums. One day I plan on re-visiting the code.
I hope that this helps.
The attached has several examples that use the loopback feature of a modem to do simulations. If you have a modem you should just need to open the port "COMx" and click go(a serial port with a loopback plug should work also). There is a debug feature, but it should only be used sparingly and for short periods of time. The examples show how to do reads using a timer, delegate, or loop.
There is one other example that reads NMEA sentences and checks that the checksum is correct.
There are routines to read bytes, strings and terminated strings.
Enjoy.
Link to this post http://www.vbforums.com/showpost.php...31&postcount=1
VB 2008 Version
NuSerial.zip
updated - 3 Jan 2009
changed sample program to remove confusion over receiving bytes backwards, at least i hope i did.
updated - 9 Jan 2009
corrected error in zip file
updated - 29 Jan 2009
new version
EDIT: 31 March 2023
A simple version of how to do SerialPorts. The test code requires a button, a textbox, three labels and a richtextbox.
Two background tasks run. One accumulates the data looking for a message terminated by a carriage return. When found the message is queued for the message processing task.
Code:
Public Class Form1
#Region "CODE TO TEST SERIAL PORT IMPLEMENTATION"
'>>>>>>>>>>>>>>>>>>>>>>>>
Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles Me.Shown
'start the worker tasks <<<<<<<<<<<<!!!!!
'this could be in the load event also
MessageProcTask = Task.Run(Sub() ProcMessages())
ReadSerialPortTask = Task.Run(Sub() ReadSerialPort())
SerialPort1.PortName = "COM1"
'OverRun errors occur at speeds > 38400 when using the loop back plug
SerialPort1.BaudRate = 38400 '110, 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200
SerialPort1.Open()
SerialPort1.DtrEnable = True
SendStuff()
End Sub
Private Sub SendStuff()
' this is ran on a PC with a hardware serial port
' this port has a loop back plug which means what is sent is received on the same PC
Static t As Task
If t Is Nothing OrElse t.Status = TaskStatus.RanToCompletion Then
Me.BeginInvoke(Sub()
RichTextBox1.Clear()
Label2.Text = _DataReceivedEvents.ToString("n0")
Label1.Text = _messagesRcvd.ToString("n0")
End Sub)
t = Task.Run(Sub()
Dim mess As String
Dim mTX As Integer
Threading.Thread.Sleep(100)
Const sendCT As Integer = 100
For mTX = 1 To sendCT
mess = String.Format("{1}{0}FOO {1}{0}{1,4} Lorem ipsum dolor sit amet, consectetur adipiscing...{0}",
ControlChars.Tab, mTX)
SerialPort1.Write(mess)
SerialPort1.Write(ControlChars.CrLf)
Next
'test empty messages
SerialPort1.Write(ControlChars.Cr)
SerialPort1.Write(ControlChars.Cr)
Threading.Thread.Sleep(sendCT \ 100 * 1000 + 500) 'give a chance for all messages to be processed
Me.BeginInvoke(Sub()
Label2.Text = _DataReceivedEvents.ToString("n0")
Label1.Text = _messagesRcvd.ToString("n0")
Label3.Text = _ErrorReceivedEvents.ToString("n0")
_DataReceivedEvents = 0
_messagesRcvd = 0
_ErrorReceivedEvents = 0
End Sub)
End Sub)
End If
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
SendStuff()
End Sub
#End Region
#Region "SERIAL PORT IMPLEMENTATION"
Private WithEvents SerialPort1 As New IO.Ports.SerialPort
Private _ErrorReceivedEvents As Long = 0L
Private Sub SerialPort1_ErrorReceived(sender As Object,
e As IO.Ports.SerialErrorReceivedEventArgs) Handles SerialPort1.ErrorReceived
Threading.Interlocked.Increment(_ErrorReceivedEvents)
Dim err As Long = Threading.Interlocked.Read(_ErrorReceivedEvents)
Debug.Write(err.ToString("n0"))
Debug.Write(" ERR ")
Debug.WriteLine(e.EventType.ToString)
' Stop
End Sub
Private _DataReceivedEvents As Integer = 0 'typically a lot more _DataReceivedEvents than of _messagesRcvd
Private ReadWait As New Threading.AutoResetEvent(False)
Private Sub SerialPort1_DataReceived(sender As Object,
e As IO.Ports.SerialDataReceivedEventArgs) Handles SerialPort1.DataReceived
'this is the entire DataReceived event handler
Threading.Interlocked.Increment(_DataReceivedEvents)
ReadWait.Set() 'cause ReadSerialPort to run
End Sub
Private _messagesRcvd As Integer = 0
Private ReadSerialPortTask As Task
Private Sub ReadSerialPort()
'
'this is run as a Background task
'
Dim data As New System.Text.StringBuilder(1024 * 1024) 'accumulate SerialPort data here
Dim mess As New System.Text.StringBuilder(1024 * 4) 'individual message
Dim foundMess As Boolean = False
Do
ReadWait.WaitOne() 'wait for SerialPort1.DataReceived
Do While SerialPort1.BytesToRead > 0 'accumulate all available bytes
data.Append(SerialPort1.ReadExisting)
Loop
Do 'process message, if one available
' protocol
' in this case looking for a string terminated by CR
mess.Length = 0 'reset message
foundMess = False ' and not found
Dim idx As Integer = 0
For idx = 0 To data.Length - 1
'look for CR??????
If data(idx) = ControlChars.Cr Then '<<<<<<<<<<<<<<<<<<<<<<<
'note that the CR will NOT be in mess
foundMess = True 'message found
Threading.Interlocked.Increment(_messagesRcvd)
Exit For
ElseIf data(idx) = ControlChars.Lf Then '<<<<<<<<<<<<<<<<<<<<<<<
Continue For 'skip LF
End If
mess.Append(data(idx))
Next
If foundMess Then
data.Remove(0, idx + 1) 'remove found message from data
_messageQ.Enqueue(mess.ToString)
MessageWait.Set() ' method ProcMessages will process messages
End If
'look for more messages in data?
Loop While foundMess AndAlso data.Length > 0 'found one, more data present
Loop
End Sub
Private MessageProcTask As Task
Private _messagesProcd As Integer = 0
Private MessageWait As New Threading.AutoResetEvent(False)
Private _messageQ As Concurrent.ConcurrentQueue(Of String)
Private Sub ProcMessages()
_messageQ = New Concurrent.ConcurrentQueue(Of String)
Do
MessageWait.WaitOne()
Do While _messageQ.Count > 0
Dim mess As String
If _messageQ.TryDequeue(mess) Then
Dim parts() As String
parts = mess.Split(ControlChars.Tab)
' interact with the UI
If parts.Length < 3 Then Continue Do
Me.BeginInvoke(Sub()
Label1.Text = parts(0)
If parts.Length >= 3 Then TextBox1.Text = parts(2)
RichTextBox1.AppendText(parts(1))
RichTextBox1.AppendText(ControlChars.Cr)
RichTextBox1.ScrollToCaret()
End Sub)
Else
Threading.Thread.Sleep(0)
End If
Loop
Loop
End Sub
#End Region
End Class
Last edited by dbasnett; Mar 31st, 2023 at 12:29 PM.
Tags for this Thread
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|