(深入探讨句柄泄漏、内存泄漏检测工具)我将分析可能导致内存泄漏(包括托管内存和非托管内存,如句柄泄漏)

(深入探讨句柄泄漏、内存泄漏检测工具)以及 TestSection 和 PowerSupply 类的代码,我将分析 PowerSupply 类中可能导致内存泄漏(包括托管内存和非托管内存,如句柄泄漏)的潜在问题,重点关注线程管理、事件订阅、集合分配和日志写入等方面。接着,我将提供优化建议和具体代码示例,解决这些问题,并确保与 TestSection 的协作逻辑一致。以下内容全部用中文解释,结合 .NET Framework 4.0 的 Windows Forms 应用程序(ROBOT.exe)场景,清晰易懂。


一、PowerSupply 类中的内存泄漏问题分析

PowerSupply 类是一个硬件控制类,负责电源设备的初始化、电压设置、状态查询和错误处理。结合代码和上下文,以下是可能导致内存泄漏(托管和非托管)的风险点:

  1. 线程句柄泄漏
  • 相关代码:

    csharp

    复制代码
    public void StopPower()
    {
        Thread thr = new Thread(new ThreadStart(_StopPower)) { IsBackground = true };
        thr.Start();
    }
    
    public void Reset()
    {
        Thread thr = new Thread(new ThreadStart(_Reset)) { IsBackground = true };
        thr.Start();
    }
    
    public void StartQuery()
    {
        Thread thr = new Thread(new ThreadStart(_StartQuery)) { IsBackground = true };
        thr.Start();
    }
    
    public void StopQuery()
    {
        Thread thr = new Thread(new ThreadStart(_StopQuery)) { IsBackground = true };
        thr.Start();
    }
  • 问题:

    • 频繁创建线程:StopPower、Reset、StartQuery 和 StopQuery 每次调用都创建新线程(new Thread),分配线程句柄(约 100-200 字节非托管内存)。

    • 未显式终止:线程依赖 StoppedQuery 或 SinySystem.InterLock 退出循环,但未保证立即终止,可能因 Thread.Sleep(1000) 阻塞而延迟释放句柄。

    • 多线程并发:多个 PowerSupply 实例(在 TestSection.PowerSupplyMap 中)可能同时调用这些方法,导致线程句柄累积。

    • 与 TestSection 的交互:TestSection.QuicklyStopTest 调用 PowerSupply.ProtectPower,可能触发 StartQuery 或 StopPower,增加线程创建。

  • 影响:

    • 线程句柄泄漏增加非托管内存占用,耗尽进程句柄限制(默认 10,000)。

    • 可能导致 System.OutOfMemoryException,尤其在高频操作(如频繁启动/停止测试)时。

  1. 事件订阅未清理
  • 相关代码:

    csharp

    复制代码
    public event PowerError PowerError;
    • 在 TestSection.Initialize 中订阅:

      csharp

      复制代码
      foreach (PowerSupply powerSupply in PowerSupplyMap.Values)
      {
          powerSupply.PowerError += PowerSupply_PowerError;
      }
  • 问题:

    • PowerSupply.PowerError 事件未在 TestSection.Uninitialize 或 PowerSupply.Uninitialize 中移除,可能导致 PowerSupply 实例被 TestSection 持有,阻止 GC 回收。

    • 如果 PowerSupplyMap 中的实例被替换或重用,但旧实例未清理,事件订阅可能累积。

  • 影响:

    • 托管内存泄漏:PowerSupply 实例无法回收,增加托管堆大小。

    • 非托管内存泄漏:PowerSupply 的 m_Hardware(IHardwareAdaptor)可能持有句柄(如串口或设备句柄),未释放。

  1. 集合分配与内存管理
  • 相关代码:

    csharp

    复制代码
    private Dictionary<string, object> m_DisplayMap = new Dictionary<string, object>();
    private void InitDataMap()
    {
        if (Name.Contains("GBPS"))
        {
            m_DisplayMap = new Dictionary<string, object>
            {
                { "VGETIME", new List<double>() },
                { "VGEPSRealV", new List<double>() },
            };
        }
        // ...
    }
    • CheckPowerIG 中的临时集合:

      csharp

      复制代码
      Dictionary<string, double> ChannelMAXICMap = new Dictionary<string, double>();
  • 问题:

    • m_DisplayMap:每次调用 InitDataMap 都创建新 Dictionary 和 List<double>,未清理旧实例,可能导致内存累积。

    • ChannelMAXICMap:在 CheckPowerIG 中频繁创建临时 Dictionary,未显式释放(虽由 GC 管理,但高频分配增加压力)。

    • DataTable:CreateStopDatatable、ProtectPower 和 ReplyPower 每次创建新的 DataTable,未重用,可能增加托管内存分配。

  • 影响:

    • 频繁分配 Dictionary、List 和 DataTable 增加 GC 压力(Gen0 回收频繁)。

    • 如果 m_DisplayMap 的 List<double> 未清空,可能累积大量数据。

  1. GDI 句柄潜在风险
  • 相关代码:未直接涉及 UI,但 PowerSupply 的状态(如 PSRealTimeState)可能通过 TestSection.UpdataMonitorData 更新 UI(如 labelPercent.Text)。

  • 问题:

    • 如果 SystemNotifEvent.HardInfoNotifEvent 或 HardErrorNotifEvent 触发 UI 更新(如控件 Visible 或 Text),可能增加 GDI 句柄。

    • 频繁调用 ReadV(每秒一次,Thread.Sleep(1000))可能导致 UI 消息队列积压(MethodInvoker 对象)。

  • 影响:

    • GDI 句柄泄漏可能导致 OutOfMemoryException,尤其在高频 UI 更新场景。
  1. 文件句柄与日志写入
  • 相关代码:

    csharp

    复制代码
    private static readonly ILog m_Log = LogManager.GetLogger("ROBOT");
    m_Log.Info(Name + " : " + instrction);
  • 问题:

    • log4net 的 FileAppender 在高频写入(如 ReadV 每秒调用)时,可能未及时释放文件句柄。

    • 如果日志文件配置不当(例如,缺少 maxSizeRollBackups),可能导致文件句柄泄漏。

  • 影响:

    • 文件句柄泄漏累积,增加非托管内存占用。
  1. 非托管资源管理
  • 相关代码:

    csharp

    复制代码
    private IHardwareAdaptor m_Hardware = null;
    m_Hardware = IoManager.Hardware(Com);
    m_Hardware.Initialize();
    m_Hardware.OpenSession();
  • 问题:

    • m_Hardware(IHardwareAdaptor)可能管理非托管资源(如串口、设备句柄),但 Uninitialize 未始终正确关闭(例如,异常路径可能跳过 CloseSession)。

    • 如果 m_Hardware 未实现 IDisposable,可能导致句柄泄漏。

  • 影响:

    • 设备句柄泄漏增加非托管内存,影响进程稳定性。

二、优化 PowerSupply 防止内存泄漏

以下是针对 PowerSupply 类的优化方案,解决线程句柄、事件订阅、集合管理、GDI 句柄、文件句柄和非托管资源问题,与 TestSection 协作优化。

  1. 使用 CancellationToken 管理线程
  • 问题:StopPower、Reset、StartQuery 和 StopQuery 创建线程,未显式终止。

  • 优化:

    • 使用 CancellationTokenSource 控制线程生命周期。

    • 合并线程逻辑,减少线程创建。

  • 代码:

    csharp

    复制代码
    private CancellationTokenSource cts = new CancellationTokenSource();
    
    public void StartQuery()
    {
        cts?.Dispose();
        cts = new CancellationTokenSource();
        Thread thr = new Thread(() => _StartQuery(cts.Token)) { IsBackground = true };
        thr.Start();
    }
    
    private void _StartQuery(CancellationToken token)
    {
        try
        {
            if (m_Hardware == null)
            {
                Initialize();
                if (m_Hardware == null)
                {
                    ErrExceptionGrad(UNFINDHARDWARE);
                    return;
                }
            }
            if (!m_Hardware.Connected)
            {
                ErrExceptionGrad(UNCONNECTIONHARDWARE);
                return;
            }
            FirstRate = true;
            while (!token.IsCancellationRequested && SinySystem.InterLock == 0)
            {
                RateQurey = true;
                ReadV();
                Thread.Sleep(1000);
            }
        }
        catch (Exception ex)
        {
            ExceptionDoEvnets(ex);
            SystemNotifEvent.HardErrorNotifEvent(this, Name + ": 巡检状态异常。", "");
        }
        finally
        {
            SystemNotifEvent.HardInfoNotifEvent(this, Name + ": 已退出状态巡检", false);
            m_Dis = true;
            StoppedQuery = true;
        }
    }
    
    public void StopQuery()
    {
        cts?.Cancel();
        Thread thr = new Thread(() => _StopQuery(cts.Token)) { IsBackground = true };
        thr.Start();
    }
    
    private void _StopQuery(CancellationToken token)
    {
        try
        {
            if (m_Hardware == null || !m_Hardware.Connected)
            {
                m_Dis = true;
                return;
            }
            if (HardwareConnectStatus == HardwareConnectStatus.InUsing && PSRealTimeState.PSRealVG > 2)
            {
                HardwareResult result = m_Hardware.QueryData("PSSET " + CheckGBPS(CreateStopDatatable())) as HardwareResult;
                m_Log.Info(Name + " PSSET Stop");
                ProcessError(result, HardwareConnectStatus.InUsing);
            }
            Uninitialize();
        }
        catch (Exception ex)
        {
            ExceptionDoEvnets(ex);
            SystemNotifEvent.HardErrorNotifEvent(this, Name + ": 停止状态巡检失败。", "");
        }
        finally
        {
            m_Dis = true;
            StoppedQuery = true;
        }
    }
    
    public void StopPower()
    {
        cts?.Cancel();
        Thread thr = new Thread(() => _StopPower(cts.Token)) { IsBackground = true };
        thr.Start();
    }
    
    private void _StopPower(CancellationToken token)
    {
        try
        {
            if (m_Hardware == null || !m_Hardware.Connected) return;
            if (HardwareConnectStatus == HardwareConnectStatus.InUsing)
            {
                StopExcute = true;
                HardwareResult result = m_Hardware.QueryData("PSSET " + CheckGBPS(CreateStopDatatable())) as HardwareResult;
                m_Log.Info(Name + " PSSET Stop");
                ProcessError(result, HardwareConnectStatus.InUsing);
            }
        }
        catch (Exception ex)
        {
            ExceptionDoEvnets(ex);
            SystemNotifEvent.HardErrorNotifEvent(this, Name + ": 停止状态巡检失败。", "");
        }
        finally
        {
            Uninitialize();
            StopExcute = true;
        }
    }
    
    public void Reset()
    {
        cts?.Cancel();
        Thread thr = new Thread(() => _Reset(cts.Token)) { IsBackground = true };
        thr.Start();
    }
    
    private void _Reset(CancellationToken token)
    {
        ActionComplete = true;
        ProtectPower();
    }
  • 解释:

    • 使用单一 CancellationTokenSource 控制所有线程,cts.Cancel() 确保线程可控终止。

    • 减少线程创建,合并逻辑(如 StopPower 和 StopQuery 共享 Uninitialize)。

    • 在 TestSection.QuicklyStopTest 中调用 PowerSupply.StopQuery 和 ProtectPower,确保线程统一取消。

  1. 实现 IDisposable 清理事件和资源
  • 问题:PowerError 事件未移除,m_Hardware 和 m_DisplayMap 未清理。

  • 优化:

    • 实现 IDisposable,清理事件订阅、集合和硬件资源。
  • 代码:

    csharp

    复制代码
    public class PowerSupply : Hardware, IHardwareFlow, IDisposable
    {
        private bool disposed = false;
    
        public void Dispose()
        {
            if (!disposed)
            {
                PowerError = null; // 移除所有事件订阅
                m_DisplayMap?.Clear();
                errorMap?.Clear();
                cts?.Dispose();
                if (m_Hardware != null)
                {
                    try
                    {
                        m_Hardware.CloseSession();
                        m_Hardware.UnInitialize();
                    }
                    catch (Exception ex)
                    {
                        m_Log.Error($"{Name}: Dispose 失败 - {ex.Message}");
                    }
                    m_Hardware = null;
                }
                m_sp?.Stop();
                m_sp2?.Stop();
                m_sp3?.Stop();
                disposed = true;
                m_Log.Info($"{Name}: Disposed");
                GC.SuppressFinalize(this);
            }
        }
    
        protected override void Finalize()
        {
            Dispose();
        }
    }
  • TestSection 协作:

    • 在 TestSection.Dispose 中调用 PowerSupply.Dispose:

      csharp

      复制代码
      public class TestSection : Hardware, IHardwareFlow, IDisposable
      {
          public void Dispose()
          {
              if (!disposed)
              {
                  foreach (TestChannel testChannel in TestChannelMap.Values)
                  {
                      testChannel.ChannelProtect -= TestChannel_ChannelProtect;
                      testChannel.ChannelError -= TestChannel_ChannelError;
                      testChannel.Dispose(); // 假设 TestChannel 也实现 IDisposable
                  }
                  foreach (PowerSupply powerSupply in PowerSupplyMap.Values)
                  {
                      powerSupply.PowerError -= PowerSupply_PowerError;
                      powerSupply.Dispose();
                  }
                  foreach (Press press in PressMap.Values)
                  {
                      press.PressError -= Press_PressError;
                      press.Dispose(); // 假设 Press 实现 IDisposable
                  }
                  TestChannelMap.Clear();
                  PowerSupplyMap.Clear();
                  PressMap.Clear();
                  while (Data.TryDequeue(out _)) { }
                  ResultData.Clear();
                  cts?.Dispose();
                  disposed = true;
                  m_Log.Info($"TestSection {Id} Disposed");
                  GC.SuppressFinalize(this);
              }
          }
      }
  • 解释:

    • 移除 PowerError 事件订阅,防止 PowerSupply 被 TestSection 持有。

    • 清空 m_DisplayMap 和 errorMap,释放托管内存。

    • 确保 m_Hardware 关闭会话,释放设备句柄。

    • 在窗体关闭时调用:

      csharp

      复制代码
      protected override void OnFormClosing(FormClosingEventArgs e)
      {
          base.OnFormClosing(e);
          LogManager.Shutdown();
          timerManager?.Dispose();
          SelectControlTestUnitSelecter.TestSection?.Dispose();
          ChannelChartsMap?.Clear();
          ControlTestUnitSelecterMap?.Clear();
      }
  1. 优化集合管理
  • 问题:m_DisplayMap、ChannelMAXICMap 和 DataTable 的频繁分配。

  • 优化:

    • 重用 m_DisplayMap 的 List<double>,避免重复创建。

    • 使用静态 DataTable 或缓存,减少分配。

  • 代码:

    csharp

    复制代码
    private void InitDataMap()
    {
        m_DisplayMap.Clear(); // 重用现有 Dictionary
        if (Name.Contains("GBPS"))
        {
            m_DisplayMap["VGETIME"] = new List<double>();
            m_DisplayMap["VGEPSRealV"] = new List<double>();
        }
        else if (Name.Contains("RBPS"))
        {
            m_DisplayMap["VCETIME"] = new List<double>();
            m_DisplayMap["VCEPSRealV"] = new List<double>();
        }
    }
    
    private DataTable stopDataTableCache = null;
    
    private DataTable CreateStopDatatable()
    {
        if (stopDataTableCache == null)
        {
            stopDataTableCache = new DataTable();
            stopDataTableCache.Columns.Add("ParaName", typeof(string));
            stopDataTableCache.Columns.Add("ParaValue", typeof(string));
            stopDataTableCache.Columns.Add("Unit", typeof(string));
            stopDataTableCache.Columns.Add("InputType", typeof(string));
            stopDataTableCache.Columns.Add("DataBase", typeof(string));
        }
        stopDataTableCache.Rows.Clear();
        stopDataTableCache.AcceptChanges();
    
        stopDataTableCache.Rows.Add("PowerSupplyID", Name, "", "ComboBox", "");
        stopDataTableCache.Rows.Add("DUTStartV", "0", "V", "NumberUpDown", "");
        stopDataTableCache.Rows.Add("DUTStopV", "0", "V", "NumberUpDown", "");
        stopDataTableCache.Rows.Add("Steps", "1", "", "NumberUpDown", "");
        stopDataTableCache.Rows.Add("StabilizingTime", "1", "s", "NumberUpDown", "");
        stopDataTableCache.Rows.Add("MaxI", ParameterMap["MaxI"].Upper.ToString(), "A", "NumberUpDown", "");
    
        return stopDataTableCache;
    }
  • 解释:

    • 重用 m_DisplayMap 和 stopDataTableCache,减少 Dictionary 和 DataTable 分配。

    • 清空 List<double> 而非创建新实例,降低 GC 压力。

  1. 优化 GDI 句柄(假设 UI 交互)
  • 问题:SystemNotifEvent.HardInfoNotifEvent 可能触发 UI 更新。

  • 优化:

    • 使用批量更新或检查控件状态。
  • 代码(假设在 UpdataMonitorData 中):

    csharp

    复制代码
    private void UpdataMonitorData()
    {
        if (InvokeRequired)
        {
            Invoke((MethodInvoker)UpdataMonitorData);
            return;
        }
        try
        {
            var testSection = SelectControlTestUnitSelecter.TestSection;
            if (testSection == null) return;
    
            double progress = testSection.GetPrograss();
            if (labelPercent.Text != $"{progress:F1}%")
                labelPercent.Text = $"{progress:F1}%"; // 避免重复设置
    
            double timeLeft = testSection.GetTimeLeft();
            if (labelTimeLeft.Text != $"{timeLeft:F0} min")
                labelTimeLeft.Text = $"{timeLeft:F0} min";
    
            ChangeChartView();
        }
        catch (Exception ex)
        {
            Log($"UpdataMonitorData 错误: {ex.Message}", false);
        }
    }
  1. 优化日志和文件句柄
  • 问题:log4net 高频写入可能导致文件句柄泄漏。

  • 优化:

    • 配置 log4net 限制文件大小和备份。

    • log4net.config:

      xml

      复制代码
      <log4net>
          <appender name="FileAppender" type="log4net.Appender.RollingFileAppender">
              <file value="app_log.txt" />
              <appendToFile value="true" />
              <rollingStyle value="Size" />
              <maxSizeRollBackups value="5" />
              <maximumFileSize value="10MB" />
              <layout type="log4net.Layout.PatternLayout">
                  <conversionPattern value="%date [%thread] %-5level %message%newline" />
              </layout>
          </appender>
          <root>
              <level value="INFO" />
              <appender-ref ref="FileAppender" />
          </root>
      </log4net>
    • 确保程序退出时关闭 log4net:

      csharp

      复制代码
      protected override void OnFormClosing(FormClosingEventArgs e)
      {
          base.OnFormClosing(e);
          LogManager.Shutdown();
          timerManager?.Dispose();
          SelectControlTestUnitSelecter.TestSection?.Dispose();
      }
  1. 优化非托管资源
  • 问题:m_Hardware 未始终正确释放。

  • 优化:

    • 确保 Uninitialize 关闭会话。

    • 假设 IHardwareAdaptor 实现 IDisposable:

      csharp

      复制代码
      public void Uninitialize()
      {
          try
          {
              if (m_Hardware == null || !m_Hardware.Connected) return;
              SystemNotifEvent.HardInfoNotifEvent(this, Name + ": 正在进行停止卸载操作。", false);
              m_Hardware.CloseSession();
              m_Hardware.UnInitialize();
              if (m_Hardware is IDisposable disposable)
                  disposable.Dispose();
          }
          catch (Exception ex)
          {
              m_Log.Error($"{Name}: Uninitialize 失败 - {ex.Message}");
          }
          finally
          {
              HardwareConnectStatus = HardwareConnectStatus.Idle;
              m_Hardware = null;
          }
      }

三、检测与验证

  1. 使用工具检测
  • Visual Studio 诊断工具:

    • 捕获内存快照,检查 PowerSupply 实例数和 m_DisplayMap 的 List<double> 大小。

    • 监控句柄计数(Process.HandleCount),确认线程句柄是否增加。

  • dotMemory:

    • 分析 PowerSupply 是否被 PowerError 事件持有。

    • 检查 DataTable 和 Dictionary 分配频率。

  • perfmon:

    • 监控 Handle Count 和 Thread Count,确认 StartQuery 是否导致线程泄漏。
  • GDIView:

    • 检查 UpdataMonitorData 是否增加 GDI 句柄。
  • Process Explorer:

    • 查看线程堆栈,定位 StartQuery 和 StopPower 的线程。
  1. 日志监控
  • 代码:

    csharp

    复制代码
    private int updateCount = 0;
    private DateTime lastLogTime = DateTime.MinValue;
    private readonly TimeSpan logInterval = TimeSpan.FromMinutes(1);
    
    private void Log(string message, bool isSummary)
    {
        try
        {
            updateCount++;
            string logMessage = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss}: {message} (调用次数: {updateCount})";
            if (isSummary || DateTime.Now - lastLogTime >= logInterval)
            {
                logMessage += $"\nGC 统计: Gen0={GC.CollectionCount(0)}, Gen1={GC.CollectionCount(1)}, Gen2={GC.CollectionCount(2)}";
                logMessage += $"\n内存使用: 托管堆={GC.GetTotalMemory(false) / 1024.0 / 1024.0:F2} MB";
                logMessage += $"\n句柄计数: {System.Diagnostics.Process.GetCurrentProcess().HandleCount}";
                logMessage += $"\n线程计数: {System.Diagnostics.Process.GetCurrentProcess().Threads.Count}";
                lastLogTime = DateTime.Now;
            }
            m_Log.Info(logMessage);
        }
        catch
        {
            // 忽略日志写入错误
        }
    }
  • 分析:

    • 检查 app_log.txt,确认句柄计数是否稳定(200-1000)。

    • 如果线程计数增加(>10),检查 StartQuery 调用频率。

    • PowerShell 脚本:

      powershell

      复制代码
      $logFile = "C:\path\to\app_log.txt"
      $lines = Get-Content $logFile
      $errorCount = ($lines | Select-String "错误").Count
      $handleCount = ($lines | Select-String "句柄计数" | Select-Object -Last 1).Line
      $threadCount = ($lines | Select-String "线程计数" | Select-Object -Last 1).Line
      Write-Output "错误次数: $errorCount"
      Write-Output "最新句柄计数: $handleCount"
      Write-Output "最新线程计数: $threadCount"
  1. 验证效果
  • 运行 10-30 分钟,执行 StartTest、StopTest 和 StartQuery。

  • 检查日志:

    • 正常:句柄计数稳定,线程数 3-5,Gen0 回收 10-20 次/分钟。

    • 泄漏:句柄计数 >1000,线程数 >20,Gen0 回收 >50 次/分钟。


四、中文解释

  1. PowerSupply 的内存泄漏风险
  • 线程句柄:StartQuery 等方法频繁创建线程,未显式终止。

  • 事件订阅:PowerError 未清理,可能导致 PowerSupply 被持有。

  • 集合分配:m_DisplayMap 和 DataTable 重复创建,增加 GC 压力。

  • GDI 句柄:UI 更新(如 HardInfoNotifEvent)可能增加 GDI 句柄。

  • 文件句柄:log4net 高频写入可能导致句柄泄漏。

  • 非托管资源:m_Hardware 未始终正确释放。

  1. 优化效果
  • 使用 CancellationToken 管理线程,减少句柄泄漏。

  • 实现 IDisposable,清理事件和资源。

  • 重用集合,降低 GC 压力。

  • 配置 log4net 和 UI 更新,减少句柄分配。

  1. 检测方法
  • 使用 Visual Studio、dotMemory、perfmon 和 GDIView 监控泄漏。

  • 日志记录句柄和线程计数,自动化分析。


五、注意事项

  • 测试验证:运行 10-30 分钟,检查日志和句柄计数。

  • IHardwareAdaptor:确认是否实现 IDisposable,否则需手动释放句柄。

  • 升级框架:.NET Framework 4.0 性能有限,建议升级到 .NET Framework 4.8 或 .NET Core。

  • 句柄限制:启用 /LARGEADDRESSAWARE:

    bash

    复制代码
    editbin /LARGEADDRESSAWARE ROBOT.exe

如需进一步优化(如 IHardwareAdaptor 的定义或 UI 交互逻辑),请提供相关代码,我将提供更精确的方案!

相关推荐
工程师平哥2 小时前
APE-01 新建工程
笔记·嵌入式硬件
恒锐丰小吕3 小时前
无锡黑锋 HF6206 系列低压差线性稳压器技术解析
嵌入式硬件·硬件工程
hdktq3 小时前
新建HAL版本MDK工程(正点原子版本)
stm32
周周记笔记6 小时前
[元器件专题] RC充电电路(七)
嵌入式硬件·测试工具·硬件开发
ACP广源盛139246256736 小时前
GSV2712@ACP#2 进 1 出 HDMI 2.0/Type-C DisplayPort 1.4 混合切换器 + 嵌入式 MCU
单片机·嵌入式硬件·计算机外设·音视频
沉在嵌入式的鱼7 小时前
STM32--GY906体温检测传感器
stm32·单片机·嵌入式硬件·gy906·体温检测
cooldream20097 小时前
基于 RISC-V VisionFive 的桌面数字时钟项目实战
嵌入式硬件·risc-v·嵌入式开发
2401_853448237 小时前
Spieed micarray开发介绍
stm32·sk9822·sipeed mic
哄娃睡觉7 小时前
STM32 VBAT外围电路接法详解--备用电源(纽扣电池)
stm32