C#串口通信 [实战] (读取激光雷达数据,生成图像)

转载连接:


https://blog.csdn.net/baidu_23318869/article/details/60956365


效果展示

话不多说,先上激光雷达数据显示效果图:



激光雷达用的是雷达大叔的二手货,基本上长这个样子:



操作步骤

要生成激光雷达图像,首先就是获取串口数据了,所以第一步就是读取串口数据:

由于是帮朋友调试,开始朋友给了错误的SDK,导致调试半天没有任何结果,索性用串口助手查看一下这货返回些什么数据:


眼神要好,有一列FA,没错,就是250,看样子是帧头,查询一下这款雷达的数据格式:

<start> <index> <speed_l> <speed_h> [Data 0] [Data 1] [Data 2] [Data 3] <checksum_l> <checksum_h> 

<start>:0xFA是固定格式表明数据包开始,可用来从数据流中分割数据包

<index>:数据包的索引号,范围从0xA0 到 0xF9 (每个包4个数据)。

<speed>:有两个speedL和speedH ,它们各一个字节,共同组成转速信息。

<data>:[ Data 0] 到 [Data 3] 是四组测量数据,其中每组测量数据分别由4个字节组成,如下: 

byte 0 : <distance 7:0="">  # 距离信息的0-7位 

byte 1 : <“invalid data” flag> <“strength warning” flag> <distance 13:8=""> # 错误信息标志位 , 警告位, 距离信息13-8位

byte 2 : <signal strength="" 7:0="">  # 信号强度 0-7位 

byte 3 : <signal strength="" 15:8=""> # 讯号强度 8-15位 

距离信息的单位是mm ,整个激光雷达的测量范围大概是15cm 到6m, 只需要把距离信息的两个字节组装成一个整数即可(注意判断无效数据位为0,如果为1意味是无效的数据,需丢弃)。 
< checksum>:由两个字节组成的校验码,用来校验整个数据包是否正确。


一、读取串口数据

有了上述信息之后,首先编写基本串口类,用来打开串口、读取数据:

有以下2点值得注意:

1. 使用DataReceived事件来读取数据;

2. 使用isReading和isClosing标识来保证正常关闭激光雷达(防止出现读取已关闭串口数据的情况);

还有就是,可以定义一个委托,用来读取完数据后,执行处理操作;

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.IO;
  6. using System.IO.Ports;
  7. namespace Laser {
  8. public class MPort {
  9. ///////////////////////////////////////////////////////////////////////////////////////////
  10. /// Attribute(属性域)
  11. ///////////////////////////////////////////////////////////////////////////////////////////
  12. // 串口实例
  13. private SerialPort port;
  14. // 串口名和串口波特率
  15. private String portName = "COM3";
  16. private int baudRate = 115200;
  17. // 正在关闭、正在读取串口状态
  18. private bool isClosing;
  19. private bool isReading;
  20. // 串口返回数据
  21. private byte[] receData = new byte[ 2048];
  22. private Object receLock = new object();
  23. // 定义一个委托
  24. public delegate void processData(byte[] receData);
  25. public processData processDel;
  26. ///////////////////////////////////////////////////////////////////////////////////////////
  27. /// Attribute Modify(属性域修改)
  28. ///////////////////////////////////////////////////////////////////////////////////////////
  29. /// <summary>
  30. /// 串口名
  31. /// </summary>
  32. public String PortName{
  33. get{ return portName;}
  34. set{ portName = value;}
  35. }
  36. /// <summary>
  37. /// 波特率
  38. /// </summary>
  39. public int BaudRate{
  40. get{ return baudRate;}
  41. set{ baudRate = value;}
  42. }
  43. /// <summary>
  44. /// 读取数据
  45. /// </summary>
  46. public byte[] ReceData {
  47. get {
  48. lock (receLock) {
  49. return receData;
  50. }
  51. }
  52. set {
  53. lock (receLock) {
  54. receData = value;
  55. }
  56. }
  57. }
  58. /// <summary>
  59. /// 串口打开状态
  60. /// </summary>
  61. public bool IsOpen {
  62. get {
  63. return port.IsOpen;
  64. }
  65. }
  66. ///////////////////////////////////////////////////////////////////////////////////////////
  67. /// Method(方法域)
  68. ///////////////////////////////////////////////////////////////////////////////////////////
  69. /// <summary>
  70. /// 串口实例构造方法
  71. /// </summary>
  72. public MPort(String portName, int baudRate = 115200) {
  73. try {
  74. // 尝试关闭已使用的串口
  75. if (port != null) {
  76. this.closePort();
  77. }
  78. // 尝试打开新串口
  79. this.PortName = portName;
  80. this.BaudRate = baudRate;
  81. port = new SerialPort( this.PortName, this.BaudRate);
  82. }
  83. catch {
  84. // "Check your port!"
  85. }
  86. }
  87. /// <summary>
  88. /// 打开串口方法
  89. /// </summary>
  90. /// <returns>打开是否成功</returns>
  91. public bool openPort() {
  92. // 注册读取串口事件
  93. port.DataReceived -= port_DataReceived;
  94. port.DataReceived += port_DataReceived;
  95. // 串口未打开时尝试打开串口
  96. if (!port.IsOpen) {
  97. isClosing = false;
  98. try {
  99. port.Open();
  100. }
  101. catch (Exception ex) {
  102. Console.WriteLine(ex.Message);
  103. return false;
  104. }
  105. }
  106. return true;
  107. }
  108. /// <summary>
  109. /// 关闭串口方法
  110. /// </summary>
  111. /// <returns>关闭串口是否成功</returns>
  112. public bool closePort() {
  113. if (!port.IsOpen) {
  114. return true;
  115. }
  116. isClosing = true;
  117. // 等待读取事件完毕
  118. while (isReading) {
  119. System.Windows.Forms.Application.DoEvents();
  120. }
  121. // 尝试关闭串口
  122. try {
  123. port.Close();
  124. }
  125. catch {
  126. return false;
  127. }
  128. return true;
  129. }
  130. /// <summary>
  131. /// 读取串口事件
  132. /// </summary>
  133. /// <param name="sender"></param>
  134. /// <param name="e"></param>
  135. void port_DataReceived(object sender, SerialDataReceivedEventArgs e) {
  136. // 正在关闭则返回
  137. if (isClosing) {
  138. return;
  139. }
  140. // 设置正在读取状态
  141. isReading = true;
  142. try {
  143. int count = Math.Min(port.BytesToRead, ReceData.Length);
  144. port.Read(ReceData, 0, count);
  145. // 委托方法不为空,调用委托方法
  146. if (processDel != null) {
  147. processDel(ReceData);
  148. }
  149. }
  150. finally {
  151. isReading = false;
  152. }
  153. }
  154. /// <summary>
  155. /// 向串口写入数据
  156. /// </summary>
  157. /// <param name="buff">写入数据的内容</param>
  158. /// <returns>是否写入成功的返回值</returns>
  159. public bool writeData(byte[] buff) {
  160. // 尝试向串口写入数据
  161. try {
  162. port.Write(buff, 0, buff.Length);
  163. }
  164. catch (Exception ex) {
  165. Console.WriteLine(ex.Message);
  166. return false;
  167. }
  168. return true;
  169. }
  170. }
  171. }


上述代码完成后,基本的串口操作就可以通过这个类完成,如果要对激光雷达做处理,则在次类基础上继承一个激光雷达类即可:

二、处理激光雷达数据

1. 继承MPort类,生成激光雷达类;

2. 考虑到当前只有到一个激光雷达,采用单例模式设计;

3. 绑定ProcessData方法,以便读取完数据后进行处理;

4. 数据处理是根据数据格式定义来编写的;

5. 预留了一个处理后的委托;

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. namespace Laser {
  6. public class Radar: MPort {
  7. ///////////////////////////////////////////////////////////////////////////////////////////
  8. /// Attribute(属性域)
  9. ///////////////////////////////////////////////////////////////////////////////////////////
  10. // 距离信息
  11. private int[] dist = new int[ 360];
  12. // 强度信息
  13. private int[] back = new int[ 360];
  14. // 旋转速度
  15. private int speed;
  16. // 读写锁
  17. private Object readLock = new object();
  18. ///////////////////////////////////////////////////////////////////////////////////////////
  19. /// Attribute Modify(属性域修改)
  20. ///////////////////////////////////////////////////////////////////////////////////////////
  21. /// <summary>
  22. /// 距离信息
  23. /// </summary>
  24. public int[] Dist {
  25. get {
  26. lock (readLock) {
  27. return dist;
  28. }
  29. }
  30. set {
  31. lock (readLock) {
  32. dist = value;
  33. }
  34. }
  35. }
  36. /// <summary>
  37. /// 强度信息
  38. /// </summary>
  39. public int[] Back {
  40. get {
  41. lock (readLock) {
  42. return back;
  43. }
  44. }
  45. set {
  46. lock (readLock) {
  47. back = value;
  48. }
  49. }
  50. }
  51. /// <summary>
  52. /// 速度信息
  53. /// </summary>
  54. public int Speed {
  55. get {
  56. lock (readLock){
  57. return speed;
  58. }
  59. }
  60. set {
  61. lock (readLock) {
  62. speed = value;
  63. }
  64. }
  65. }
  66. ///////////////////////////////////////////////////////////////////////////////////////////
  67. /// Method(方法域)
  68. ///////////////////////////////////////////////////////////////////////////////////////////
  69. private Radar(String portName, int baudRate = 115200):base(
  70. portName, baudRate) {
  71. this.processDel -= new processData( this.processData);
  72. this.processDel += new processData( this.processData);
  73. }
  74. private static Radar radar;
  75. /// <summary>
  76. /// 单例模式,创建激光雷达类
  77. /// </summary>
  78. /// <param name="portName">串口号</param>
  79. /// <param name="baudRate">波特率</param>
  80. /// <returns>激光雷达实例</returns>
  81. public static Radar getInstance(String portName, int baudRate = 115200) {
  82. if (radar == null || !portName.Equals(radar.PortName)) {
  83. radar = new Radar(portName, baudRate);
  84. }
  85. return radar;
  86. }
  87. /// <summary>
  88. /// 数据头
  89. /// </summary>
  90. public static byte dHead = ( byte) 0xFA;
  91. /// <summary>
  92. /// 数据帧长度
  93. /// </summary>
  94. public static int dLen = 22;
  95. /// <summary>
  96. /// 处理激光雷达获取的数据信息
  97. /// </summary>
  98. /// <param name="data">获取的数据</param>
  99. public void processData(byte[] data) {
  100. // 该数据完整则进行处理
  101. for ( int i = 0; i < data.Length && (i + dLen - 1 < data.Length); ) {
  102. // 寻找帧头
  103. if (data[i] != dHead) {
  104. ++i;
  105. continue;
  106. }
  107. // 索引号
  108. int index = data[i + 1];
  109. Speed = data[i + 2] + 256 * (data[i + 3]);
  110. // 计算距离和强度信息
  111. for ( int j = 0; j < 4; ++j) {
  112. int tag = (index - 160) * 4 + j;
  113. if (tag < 0 || tag >= 360) {
  114. break;
  115. }
  116. Dist[tag] = data[i + 3 + j * 4 + 1] + 256 * (data[i + 3 + j * 4 + 2] & 0x3F);
  117. Back[tag] = data[i + 3 + j * 4 + 3] + 256 * data[i + 3 + j * 4 + 4];
  118. }
  119. i += dLen;
  120. }
  121. // 执行数据处理完成后的方法
  122. if (afterProcessDel != null) {
  123. afterProcessDel();
  124. }
  125. }
  126. /// <summary>
  127. /// 定义数据处理后执行方法
  128. /// </summary>
  129. public delegate void afterProcess();
  130. public static afterProcess afterProcessDel;
  131. }
  132. }


三、绘图显示

为了进行显示,需要把从串口读到的激光雷达距离信息和强度信息画在图上,其中,画图使用winForm的Graphic,

1. 创建一张Bitmap;

2. 根据角度距离信息,计算当前测量点的坐标;

3. 在坐标处绘制一个点,其实就是实心圆,使用FillEclipse方法;

4. 此处画图的坐标比较神奇,是因为坐标原点在左上角(0,0),向下为Y轴增大方向,向右为X轴增大方向;

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Drawing;
  6. namespace Laser {
  7. public class DrawRadarImg {
  8. ///////////////////////////////////////////////////////////////////////////////////////////
  9. /// Attribute(属性域)
  10. ///////////////////////////////////////////////////////////////////////////////////////////
  11. // 图像宽度
  12. private int width;
  13. // 图像高度
  14. private int height;
  15. // 显示比例
  16. private double rate = 0.05;
  17. // 像素点大小
  18. private static int ps = 4;
  19. ///////////////////////////////////////////////////////////////////////////////////////////
  20. /// Attribute Modify(属性域修改)
  21. ///////////////////////////////////////////////////////////////////////////////////////////
  22. /// <summary>
  23. /// 图像宽度
  24. /// </summary>
  25. public int Width {
  26. get { return width; }
  27. set { width = value; }
  28. }
  29. /// <summary>
  30. /// 图像高度
  31. /// </summary>
  32. public int Height {
  33. get { return height; }
  34. set { height = value; }
  35. }
  36. /// <summary>
  37. /// 长度比例
  38. /// </summary>
  39. public double Rate {
  40. get { return rate; }
  41. set { rate = value; }
  42. }
  43. /// <summary>
  44. /// 一个测量点大小
  45. /// </summary>
  46. public int PS {
  47. get { return ps; }
  48. set { ps = value; }
  49. }
  50. ///////////////////////////////////////////////////////////////////////////////////////////
  51. /// Method(方法域)
  52. ///////////////////////////////////////////////////////////////////////////////////////////
  53. /// <summary>
  54. /// 根据距离和强度信息画图
  55. /// </summary>
  56. /// <param name="dist">距离信息</param>
  57. /// <param name="back">强度信息</param>
  58. /// <returns></returns>
  59. public Image drawImg(int[] dist, int[] back) {
  60. Bitmap img = new Bitmap( 400, 400);
  61. Graphics g = Graphics.FromImage(img);
  62. for ( int i = 0; i < 360; ++i) {
  63. // 计算当前角度、X、Y坐标(偏差90度,与设定相关)
  64. double ang = ((i + 90) / 180.0) * Math.PI;
  65. double x = Math.Cos(ang) * dist[i] * rate;
  66. double y = Math.Sin(ang) * dist[i] * rate;
  67. // 调整强度显示的颜色
  68. Brush brush = (back[i] > 300) ? (Brushes.Red) :
  69. (back[i] > 200) ? (Brushes.Green) :
  70. (back[i] > 100) ? (Brushes.Blue) : Brushes.Purple;
  71. // 画点
  72. g.FillEllipse(brush, ( int)(x + 200 - ps / 2), ( int)( 200 - (y - ps / 2)), ps, ps);
  73. }
  74. return img;
  75. }
  76. }
  77. }


最后,将这些类在Form1中进行调用,代码如下:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Data;
  5. using System.Drawing;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Windows.Forms;
  9. namespace Laser {
  10. public partial class Form1 : Form {
  11. // 激光雷达对象
  12. private Radar radar;
  13. private DrawRadarImg dImg = new DrawRadarImg();
  14. public Form1() {
  15. InitializeComponent();
  16. // 绑定更新界面方法
  17. updateBoardDel += new updateBoard( this.updateBoardMethod);
  18. }
  19. /// <summary>
  20. /// 打开关闭串口方法
  21. /// </summary>
  22. /// <param name="sender"></param>
  23. /// <param name="e"></param>
  24. private void button2_Click(object sender, EventArgs e) {
  25. radar = Radar.getInstance( this.textBox1.Text.Trim());
  26. Radar.afterProcessDel = new Radar.afterProcess( this.processData);
  27. if (!radar.IsOpen) {
  28. radar.openPort();
  29. this.button2.Text = "closePort";
  30. this.textBox1.Enabled = false;
  31. }
  32. else {
  33. radar.closePort();
  34. this.button2.Text = "openPort";
  35. }
  36. }
  37. /// <summary>
  38. /// 处理完数据执行画图方法
  39. /// </summary>
  40. public void processData() {
  41. Image img = dImg.drawImg(radar.Dist, radar.Back);
  42. if (updateBoardDel != null) {
  43. this.Invoke(updateBoardDel,img);
  44. }
  45. }
  46. /// <summary>
  47. /// 更新窗口委托
  48. /// </summary>
  49. /// <param name="img"></param>
  50. public delegate void updateBoard(Image img);
  51. public updateBoard updateBoardDel;
  52. public void updateBoardMethod(Image img) {
  53. this.pictureBox1.Image = img;
  54. this.textSpeed.Text = radar.Speed.ToString();
  55. }
  56. }
  57. }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值