因为最近在调试小车的超声波雷达,想着用弄个炫酷的小程序测试它,但是没找到合适的,就自己用C#开发了一个。测试没问题,比较简单直接上源码。
需要注意的是,单片机那边需要按协议发送数据:
//协议格式:"A<角度>,D<距离>\n"
//例如STM32 USART串口输出:printf("A%d,D%d\n", angle, distance);
下面开始正文。
WinForm如上,注意添加图片属性,或者全部复制下面的代码。
//Program.cs //并不需要修改
namespace UltrasonicRadar
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
Application.Run(new Form1());
}
}
}
//Form.Designer.cs //可以自己定义,或者直接复制
namespace UltrasonicRadar
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
btnConnect = new Button();
btnScanToggle = new Button();
btnClear = new Button();
radarDisplay = new PictureBox();
lblStatus = new Label();
lblPort = new Label();
cmbPorts = new ComboBox();
lblBaudRate = new Label();
cmbBaudRate = new ComboBox();
lblDataBits = new Label();
cmbDataBits = new ComboBox();
lblStopBits = new Label();
cmbStopBits = new ComboBox();
lblParity = new Label();
cmbParity = new ComboBox();
panel1 = new Panel();
((System.ComponentModel.ISupportInitialize)radarDisplay).BeginInit();
panel1.SuspendLayout();
SuspendLayout();
//
// btnConnect
//
btnConnect.Location = new Point(618, 28);
btnConnect.Name = "btnConnect";
btnConnect.Size = new Size(80, 40);
btnConnect.TabIndex = 0;
btnConnect.Text = "连接串口";
btnConnect.UseVisualStyleBackColor = true;
btnConnect.Click += btnConnect_Click;
//
// btnScanToggle
//
btnScanToggle.Location = new Point(714, 28);
btnScanToggle.Name = "btnScanToggle";
btnScanToggle.Size = new Size(80, 40);
btnScanToggle.TabIndex = 1;
btnScanToggle.Text = "启动扫描";
btnScanToggle.UseVisualStyleBackColor = true;
btnScanToggle.Click += btnScanToggle_Click;
//
// btnClear
//
btnClear.Location = new Point(696, 343);
btnClear.Name = "btnClear";
btnClear.Size = new Size(80, 40);
btnClear.TabIndex = 2;
btnClear.Text = "清除位置";
btnClear.UseVisualStyleBackColor = true;
btnClear.Click += btnClear_Click;
//
// radarDisplay
//
radarDisplay.Dock = DockStyle.Fill;
radarDisplay.Location = new Point(0, 0);
radarDisplay.Name = "radarDisplay";
radarDisplay.Size = new Size(600, 559);
radarDisplay.TabIndex = 3;
radarDisplay.TabStop = false;
//
// lblStatus
//
lblStatus.AutoSize = true;
lblStatus.Font = new Font("Microsoft YaHei UI", 10.5F, FontStyle.Bold, GraphicsUnit.Point, 134);
lblStatus.ForeColor = Color.Orange;
lblStatus.Location = new Point(627, 89);
lblStatus.Name = "lblStatus";
lblStatus.Size = new Size(66, 25);
lblStatus.TabIndex = 4;
lblStatus.Text = "未连接";
//
// lblPort
//
lblPort.AutoSize = true;
lblPort.Location = new Point(611, 141);
lblPort.Name = "lblPort";
lblPort.Size = new Size(54, 20);
lblPort.TabIndex = 5;
lblPort.Text = "串口号";
//
// cmbPorts
//
cmbPorts.DropDownStyle = ComboBoxStyle.DropDownList;
cmbPorts.FormattingEnabled = true;
cmbPorts.Location = new Point(677, 136);
cmbPorts.Name = "cmbPorts";
cmbPorts.Size = new Size(121, 28);
cmbPorts.TabIndex = 6;
//
// lblBaudRate
//
lblBaudRate.AutoSize = true;
lblBaudRate.Location = new Point(611, 181);
lblBaudRate.Name = "lblBaudRate";
lblBaudRate.Size = new Size(54, 20);
lblBaudRate.TabIndex = 7;
lblBaudRate.Text = "波特率";
//
// cmbBaudRate
//
cmbBaudRate.DropDownStyle = ComboBoxStyle.DropDownList;
cmbBaudRate.FormattingEnabled = true;
cmbBaudRate.Location = new Point(677, 176);
cmbBaudRate.Name = "cmbBaudRate";
cmbBaudRate.Size = new Size(121, 28);
cmbBaudRate.TabIndex = 8;
//
// lblDataBits
//
lblDataBits.AutoSize = true;
lblDataBits.Location = new Point(611, 221);
lblDataBits.Name = "lblDataBits";
lblDataBits.Size = new Size(54, 20);
lblDataBits.TabIndex = 9;
lblDataBits.Text = "数据位";
//
// cmbDataBits
//
cmbDataBits.DropDownStyle = ComboBoxStyle.DropDownList;
cmbDataBits.FormattingEnabled = true;
cmbDataBits.Location = new Point(677, 216);
cmbDataBits.Name = "cmbDataBits";
cmbDataBits.Size = new Size(121, 28);
cmbDataBits.TabIndex = 10;
//
// lblStopBits
//
lblStopBits.AutoSize = true;
lblStopBits.Location = new Point(611, 261);
lblStopBits.Name = "lblStopBits";
lblStopBits.Size = new Size(54, 20);
lblStopBits.TabIndex = 11;
lblStopBits.Text = "停止位";
//
// cmbStopBits
//
cmbStopBits.DropDownStyle = ComboBoxStyle.DropDownList;
cmbStopBits.FormattingEnabled = true;
cmbStopBits.Location = new Point(677, 256);
cmbStopBits.Name = "cmbStopBits";
cmbStopBits.Size = new Size(121, 28);
cmbStopBits.TabIndex = 12;
//
// lblParity
//
lblParity.AutoSize = true;
lblParity.Location = new Point(611, 301);
lblParity.Name = "lblParity";
lblParity.Size = new Size(54, 20);
lblParity.TabIndex = 13;
lblParity.Text = "校验位";
//
// cmbParity
//
cmbParity.DropDownStyle = ComboBoxStyle.DropDownList;
cmbParity.FormattingEnabled = true;
cmbParity.Location = new Point(677, 296);
cmbParity.Name = "cmbParity";
cmbParity.Size = new Size(121, 28);
cmbParity.TabIndex = 14;
//
// panel1
//
panel1.Controls.Add(radarDisplay);
panel1.Dock = DockStyle.Left;
panel1.Location = new Point(0, 0);
panel1.Name = "panel1";
panel1.Size = new Size(600, 559);
panel1.TabIndex = 15;
//
// Form1
//
AutoScaleDimensions = new SizeF(9F, 20F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(811, 559);
Controls.Add(panel1);
Controls.Add(cmbParity);
Controls.Add(lblParity);
Controls.Add(cmbStopBits);
Controls.Add(lblStopBits);
Controls.Add(cmbDataBits);
Controls.Add(lblDataBits);
Controls.Add(cmbBaudRate);
Controls.Add(lblBaudRate);
Controls.Add(cmbPorts);
Controls.Add(lblPort);
Controls.Add(lblStatus);
Controls.Add(btnClear);
Controls.Add(btnScanToggle);
Controls.Add(btnConnect);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
Name = "Form1";
Text = "STM32F4超声波雷达";
FormClosing += Form1_FormClosing;
((System.ComponentModel.ISupportInitialize)radarDisplay).EndInit();
panel1.ResumeLayout(false);
ResumeLayout(false);
PerformLayout();
}
#endregion
private Button btnConnect;
private Button btnScanToggle;
private Button btnClear;
private PictureBox radarDisplay;
private Label lblStatus;
private Label lblPort;
private ComboBox cmbPorts;
private Label lblBaudRate;
private ComboBox cmbBaudRate;
private Label lblDataBits;
private ComboBox cmbDataBits;
private Label lblStopBits;
private ComboBox cmbStopBits;
private Label lblParity;
private ComboBox cmbParity;
private Panel panel1;
}
}
//Form1.cs
using System.Drawing.Drawing2D;
using System.IO.Ports;
using System.Reflection;
using System.Text;
//协议格式:"A<角度>,D<距离>\n"
//例如STM32 USART串口输出:printf("A%d,D%d\n", angle, distance);
namespace UltrasonicRadar
{
public partial class Form1 : Form
{
private readonly RadarController radarController = new RadarController();
private readonly SerialPort serialPort = new SerialPort();
private readonly System.Windows.Forms.Timer scanTimer = new System.Windows.Forms.Timer();
private readonly object serialLock = new object();
private StringBuilder serialBuffer = new StringBuilder();
private bool isUpdating = false;
public Form1()
{
InitializeComponent();
InitializeRadar();
InitializeSerialPort();
InitializeTimer();
}
private void InitializeRadar()
{
radarDisplay.Paint += new PaintEventHandler(RadarDisplay_Paint);
radarDisplay.BackColor = Color.FromArgb(15, 20, 30);
radarDisplay.Dock = DockStyle.Fill;
// 启用双缓冲
SetDoubleBuffered(radarDisplay, true);
}
private void InitializeSerialPort()
{
// 获取可用串口
string[] ports = SerialPort.GetPortNames();
cmbPorts.Items.AddRange(ports);
if (ports.Length > 0)
cmbPorts.SelectedIndex = 0;
// 波特率设置
cmbBaudRate.Items.AddRange(new object[] { "9600", "19200", "38400", "57600", "115200" });
cmbBaudRate.SelectedItem = "115200";
// 数据位
cmbDataBits.Items.AddRange(new object[] { "5", "6", "7", "8" });
cmbDataBits.SelectedItem = "8";
// 停止位
cmbStopBits.Items.AddRange(new object[] { "1", "1.5", "2" });
cmbStopBits.SelectedItem = "1";
// 校验位
cmbParity.Items.AddRange(new object[] { "None", "Odd", "Even", "Mark", "Space" });
cmbParity.SelectedItem = "None";
// 设置串口数据接收事件
serialPort.DataReceived += SerialPort_DataReceived;
}
private void InitializeTimer()
{
scanTimer.Interval = 20; // 20 FPS
scanTimer.Tick += scanTimer_Tick;
}
// 通过反射设置双缓冲属性
private void SetDoubleBuffered(Control control, bool value)
{
Type controlType = typeof(Control);
PropertyInfo? doubleBufferedProp = controlType.GetProperty(
"DoubleBuffered",
BindingFlags.NonPublic | BindingFlags.Instance);
if (doubleBufferedProp == null)
{
throw new InvalidOperationException("控件类型中未找到 DoubleBuffered 属性");
}
doubleBufferedProp.SetValue(control, value, null);
}
private void RadarDisplay_Paint(object? sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
int width = radarDisplay.Width;
int height = radarDisplay.Height;
int centerX = width / 2;
int centerY = height / 2;
int radius = Math.Min(centerX, centerY) - 20;
// 绘制雷达背景网格
DrawRadarGrid(g, centerX, centerY, radius);
// 绘制扫描线
DrawScanLine(g, centerX, centerY, radius, radarController.CurrentAngle);
// 绘制检测到的点
DrawDetectedPoints(g, centerX, centerY, radius, radarController.DetectedPoints);
// 绘制雷达中心
g.FillEllipse(Brushes.LimeGreen, centerX - 5, centerY - 5, 10, 10);
// 在右下角添加单位说明
string unitText = "单位:厘米";
SizeF textSize = g.MeasureString(unitText, new Font("Arial", 9));
g.DrawString(unitText, new Font("Arial", 9), Brushes.LimeGreen,
width - textSize.Width - 10, height - textSize.Height - 5);
}
private void DrawRadarGrid(Graphics g, int centerX, int centerY, int radius)
{
// 绘制同心圆
for (int i = 1; i <= 5; i++)
{
int r = i * radius / 5;
g.DrawEllipse(new Pen(Color.FromArgb(30, 144, 255, 30)),
centerX - r, centerY - r, r * 2, r * 2);
// 绘制距离标签
int distance = i * radarController.RadarRange / 5;
g.DrawString($"{distance}", new Font("Arial", 8),
Brushes.LimeGreen, centerX + r - 20, centerY);
}
// 绘制角度线
for (int angle = 0; angle < 360; angle += 30)
{
double radian = angle * Math.PI / 180;
int x = centerX + (int)(radius * Math.Sin(radian));
int y = centerY - (int)(radius * Math.Cos(radian));
g.DrawLine(new Pen(Color.FromArgb(30, 144, 255, 30)),
centerX, centerY, x, y);
// 绘制角度标签
if (angle % 90 == 0)
{
string text = $"{angle}°";
SizeF textSize = g.MeasureString(text, new Font("Arial", 8));
int labelX = centerX + (int)((radius + 15) * Math.Sin(radian)) - (int)textSize.Width / 2;
int labelY = centerY - (int)((radius + 15) * Math.Cos(radian)) - (int)textSize.Height / 2;
g.DrawString(text, new Font("Arial", 8), Brushes.LimeGreen, labelX, labelY);
}
}
}
private void DrawScanLine(Graphics g, int centerX, int centerY, int radius, float angle)
{
// 计算扫描线终点(雷达坐标系:0°=北,顺时针)
double theta = angle * Math.PI / 180;
int x = centerX + (int)(radius * Math.Sin(theta));
int y = centerY - (int)(radius * Math.Cos(theta));
// 绘制扫描线
g.DrawLine(new Pen(Color.LimeGreen, 2), centerX, centerY, x, y);
// 计算扇形的GDI+角度
float startRadar = angle - 60; // 扇形起始角度(雷达)
float startGdi = (startRadar - 90) % 360;
if (startGdi < 0) startGdi += 360; // 修正为非负角度
// 绘制扇形
GraphicsPath path = new GraphicsPath();
path.AddArc(centerX - radius, centerY - radius, radius * 2, radius * 2, startGdi, 60);
path.AddLine(x, y, centerX, centerY);
path.CloseFigure();
g.FillPath(new SolidBrush(Color.FromArgb(30, 50, 200, 50)), path);
}
private void DrawDetectedPoints(Graphics g, int centerX, int centerY, int radius, IReadOnlyList<RadarPoint> points)
{
foreach (RadarPoint point in points)
{
double radian = point.Angle * Math.PI / 180;
double scaledDistance = (point.Distance / (double)radarController.RadarRange) * radius;
int x = centerX + (int)(scaledDistance * Math.Sin(radian));
int y = centerY - (int)(scaledDistance * Math.Cos(radian));
// 新点显示为黄色,旧点显示为红色
Color pointColor = point.IsNew ? Color.Yellow : Color.Red;
g.FillEllipse(new SolidBrush(pointColor), x - 3, y - 3, 6, 6);
// 显示距离
g.DrawString($"{point.Distance}", new Font("Arial", 8),
Brushes.Orange, x + 5, y - 10);
}
}
private void scanTimer_Tick(object? sender, EventArgs e)
{
radarController.UpdateAngle();
radarController.UpdatePointLifecycle();
// 批量更新:避免短时间内多次Invalidate
if (!isUpdating)
{
isUpdating = true;
BeginInvoke(new Action(() => {
radarDisplay.Invalidate();
isUpdating = false;
}));
}
}
private void btnConnect_Click(object sender, EventArgs e)
{
if (serialPort.IsOpen)
{
serialPort.Close();
btnConnect.Text = "连接串口";
lblStatus.Text = "已断开";
lblStatus.ForeColor = Color.Orange;
}
else
{
try
{
serialPort.PortName = cmbPorts.SelectedItem!.ToString();
serialPort.BaudRate = int.Parse(cmbBaudRate.SelectedItem!.ToString()!);
serialPort.DataBits = int.Parse(cmbDataBits.SelectedItem!.ToString()!);
// 处理停止位(需要将"1.5"转换为StopBits.OnePointFive)
string stopBitsStr = cmbStopBits.SelectedItem!.ToString()!;
if (stopBitsStr == "1.5")
serialPort.StopBits = StopBits.OnePointFive;
else
serialPort.StopBits = (StopBits)Enum.Parse(typeof(StopBits), stopBitsStr);
serialPort.Parity = (Parity)cmbParity.SelectedIndex;
serialPort.Open();
btnConnect.Text = "断开连接";
lblStatus.Text = "已连接: " + serialPort.PortName;
lblStatus.ForeColor = Color.LimeGreen;
}
catch (Exception ex)
{
MessageBox.Show("连接失败: " + ex.Message, "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
private void btnScanToggle_Click(object sender, EventArgs e)
{
if (scanTimer.Enabled)
{
scanTimer.Stop();
btnScanToggle.Text = "启动扫描";
}
else
{
scanTimer.Start();
btnScanToggle.Text = "停止扫描";
}
}
private void btnClear_Click(object sender, EventArgs e)
{
radarController.ClearPoints();
radarDisplay.Invalidate();
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (serialPort.IsOpen)
serialPort.Close();
scanTimer.Stop();
}
// 串口数据接收处理
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
// 加锁保护数据读取和缓冲区操作
lock (serialLock)
{
string data = serialPort.ReadExisting();
serialBuffer.Append(data);
}
// 在UI线程中处理数据
this.Invoke(new Action(ProcessSerialData));
}
catch (Exception ex)
{
// 记录日志
Console.WriteLine($"串口数据接收错误: {ex.Message}");
Console.WriteLine(ex.StackTrace);
this.Invoke(new Action(() => {
lblStatus.Text = "数据接收错误,请查看日志";
lblStatus.ForeColor = Color.Red;
}));
}
}
// 处理串口数据
private void ProcessSerialData()
{
string data;
lock (serialLock)
{
data = serialBuffer.ToString();
serialBuffer.Clear();
}
// 假设数据格式: "A<角度>,D<距离>\n"
string[] lines = data.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string line in lines)
{
if (string.IsNullOrWhiteSpace(line)) continue;
try
{
// 解析角度和距离
string[] parts = line.Split(',');
if (parts.Length >= 2)
{
float angle = 0;
int distance = 0;
if (parts[0].StartsWith("A") && float.TryParse(parts[0].Substring(1), out angle) &&
parts[1].StartsWith("D") && int.TryParse(parts[1].Substring(1), out distance))
{
// 确保角度在0-360范围内
angle = angle % 360;
// 确保距离在有效范围内
distance = Math.Max(0, Math.Min(distance, radarController.RadarRange));
// 添加检测点
this.Invoke(new Action(() => {
radarController.ProcessDetectedPoint(angle, distance);
radarDisplay.Invalidate();
}));
}
}
}
catch (Exception ex)
{
// 记录格式错误
Console.WriteLine($"数据格式错误: {line}");
}
}
}
}
// 雷达控制器 - 负责雷达核心逻辑
public class RadarController
{
private float currentAngle = 0;
private readonly int radarRange = 500;
private readonly List<RadarPoint> detectedPoints = new List<RadarPoint>();
// 角度索引(键为角度区间,值为点列表)
private readonly Dictionary<int, List<RadarPoint>> angleIndex = new Dictionary<int, List<RadarPoint>>();
public float CurrentAngle => currentAngle;
public int RadarRange => radarRange;
public IReadOnlyList<RadarPoint> DetectedPoints => detectedPoints;
public void UpdateAngle()
{
currentAngle = (currentAngle + 2) % 360;
}
public void ProcessDetectedPoint(float angle, int distance)
{
// 计算角度区间键(每5度一个区间)
int angleKey = (int)(angle / 5) * 5;
// 检查相邻区间(±5度)
bool pointExists = false;
for (int offset = -5; offset <= 5; offset += 5)
{
int keyToCheck = angleKey + offset;
if (angleIndex.TryGetValue(keyToCheck, out List<RadarPoint>? pointsInRange))
{
foreach (var point in pointsInRange)
{
if (Math.Abs(point.Angle - angle) < 5)
{
// 更新已有点的信息
point.Distance = distance;
point.DetectedCount++;
point.MissedCount = 0;
point.IsNew = false;
pointExists = true;
return;
}
}
}
}
// 如果是新点,则添加
if (!pointExists)
{
RadarPoint newPoint = new RadarPoint(angle, distance);
detectedPoints.Add(newPoint);
// 更新索引
if (!angleIndex.TryGetValue(angleKey, out List<RadarPoint>? points))
{
points = new List<RadarPoint>();
angleIndex[angleKey] = points;
}
points.Add(newPoint);
}
}
public void UpdatePointLifecycle()
{
// 处理所有点:增加未检测计数,移除长时间未被检测的点
for (int i = detectedPoints.Count - 1; i >= 0; i--)
{
detectedPoints[i].MissedCount++;
if (detectedPoints[i].MissedCount > 5)
{
// 从索引中移除
int angleKey = (int)(detectedPoints[i].Angle / 5) * 5;
if (angleIndex.TryGetValue(angleKey, out List<RadarPoint>? points))
{
points.Remove(detectedPoints[i]);
if (points.Count == 0)
angleIndex.Remove(angleKey);
}
detectedPoints.RemoveAt(i);
}
}
}
public void ClearPoints()
{
detectedPoints.Clear();
angleIndex.Clear();
}
}
// 雷达检测点类
public class RadarPoint
{
public float Angle { get; set; }
public int Distance { get; set; }
public int DetectedCount { get; set; } // 被检测到的次数
public int MissedCount { get; set; } // 连续未被检测到的次数
public bool IsNew { get; set; } = true; // 是否为新点
public RadarPoint(float angle, int distance)
{
Angle = angle;
Distance = distance;
DetectedCount = 1;
}
}
}