返回

.NET 控制主板蜂鸣器: inpout32 与 C/C++ 方案

windows

Windows 系统下 .NET 调用主板蜂鸣器

某些应用场景,需要程序控制主板蜂鸣器发出提示音,但常使用的方法如 SystemSounds.BeepConsole.Beep 或 PowerShell 命令 [console]::beep(600,1000) ,以及 echo ^G 指令都只会在默认音频设备播放声音,无法触发主板蜂鸣器。本篇文章讨论如何利用 .NET 实现对主板蜂鸣器的控制,并对一些常见的问题提供解决方案。

问题分析

问题的核心在于 Windows 系统对传统 PC 扬声器的访问进行了限制。早期系统下,应用程序可以直接通过 I/O 端口控制蜂鸣器。但出于安全性和音频子系统的兼容性考虑,这种方式逐渐被弃用,取而代之的是通过声卡模拟播放声音。 SystemSounds.BeepConsole.Beep 乃至其他通过调用系统 API 的方法都走的这一路径,所以才会出现“蜂鸣器响在音箱”的问题。

因此,若想直接控制主板蜂鸣器,需要避开 Windows 的音频系统,采用直接访问硬件的方式,或使用驱动程序来辅助。

解决方案

以下提供两种可行方案,分别采用不同的技术路线。

方案一:使用第三方库 inpout32

inpout32 是一个用于直接访问 Windows I/O 端口的库,能跳过 Windows 的音频系统,直接控制硬件。使用该方案需要下载其对应的 DLL,并进行一些配置。

操作步骤:

  1. 下载 inpout32:https://www.highrez.co.uk/downloads/inpout32/ 获取对应操作系统架构的 inpout32.dll 文件。通常选择 x64 版本。

  2. 放置 DLL:inpout32.dll 放置在你的 .NET 程序的可执行文件所在目录下,或操作系统能找到的动态链接库目录中(比如system32)。

  3. 安装 .NET NuGet 包: 使用 NuGet 安装 CSInpOut NuGet包. 它是针对 C# 封装的 inpout32。你可以用 NuGet 包管理器或者通过运行命令:

    dotnet add package CSInpOut
    
  4. 编写 C# 代码: 下面的代码段展示如何使用 CSInpOut 来控制蜂鸣器:

    using System;
    using InpOutSharp;
    public class Buzzer
    {
         private const ushort ControlPort = 0x61; // 蜂鸣器控制端口
    
        public static void Beep(int frequency, int duration) {
    
            // Bit 0  为蜂鸣器启用/禁用位
            // Bit 1  为 timer2 gate 位
    
             // 开启蜂鸣器
           Out32(ControlPort, (byte)(Inp32(ControlPort) | 0x03) );
    
            // 等待一段时间模拟 duration,可以使用更准确的定时方式。这里演示简易方式
            System.Threading.Thread.Sleep(duration);
    
    
            // 关闭蜂鸣器
             Out32(ControlPort, (byte)(Inp32(ControlPort) & 0xFC) );
    
    
            }
    
    
        public static void Out32(ushort port, byte value)
        {
           //使用 CSInpOut的端口输出函数
            NativeMethods.Out32(port, value);
        }
    
        public static  byte Inp32(ushort port){
    
              return NativeMethods.Inp32(port);
    
          }
    
        // 为了能够直接通过控制端口设置高低电平来驱动扬声器
        public static void  BeepContinuous(){
    
    
         Out32(ControlPort, (byte)(Inp32(ControlPort) | 0x03) );
        }
        public static void BeepStop(){
    
            Out32(ControlPort, (byte)(Inp32(ControlPort) & 0xFC) );
         }
    
    
        //调用范例
        public static void Main(string[] args)
       {
          Console.WriteLine("Beeping once!");
          Buzzer.Beep(600, 500);  // 播放 600Hz,持续 500 毫秒
          System.Threading.Thread.Sleep(1000); // 延迟
           Console.WriteLine("Beeping continuously");
         Buzzer.BeepContinuous();
           System.Threading.Thread.Sleep(5000); // 让蜂鸣器鸣响 5 秒
            Buzzer.BeepStop();
            Console.WriteLine("Stopped Beeping.");
       }
    }
    
    
    

    注意: 需要在运行 .NET 程序的时候,确保以管理员权限运行。 某些 Windows 版本需要关闭 Driver Signature Enforcement 才能允许 inpout32正常访问底层硬件端口。 关闭可能会降低安全性, 需要权衡风险。 同时需要明确的是CSInpOut 并不能保证在所有电脑或者主板型号上都可用。如果出现系统错误或异常,要考虑是设备不支持或者需要额外安装设备驱动的问题。

    该段代码展示了基本操作,包括产生一定时长,指定频率的短音,以及持续响音的开启和关闭方法。代码通过直接向 0x61 端口写入字节控制 PC 扬声器。设置bit 0和 bit 1为1启动扬声器。设置这两位为 0, 关闭扬声器。

方案二:编写 C/C++ 代码,并从 .NET 调用

如果第一种方案难以适用,或者出于对效率更高的要求, 可以考虑通过C/C++编写底层访问硬件的代码,编译为DLL,然后从.NET 代码中进行调用。

操作步骤:

  1. 编写 C++ 代码: 下面是一个简单的 C++ 代码示例 (需要使用支持汇编的编译器), 创建 Buzzer.h 文件:

    #pragma once
    
    #ifndef WIN32_LEAN_AND_MEAN
    #define WIN32_LEAN_AND_MEAN
    #endif
    
    #include <Windows.h>
     extern "C"
     {
    
    	__declspec(dllexport) void __stdcall Beep(int frequency, int duration);
    
    	__declspec(dllexport) void __stdcall BeepContinuous();
        __declspec(dllexport) void __stdcall BeepStop();
    
     }
    
    
    

    接着创建 Buzzer.cpp:

    #include "Buzzer.h"
    
    
    void __stdcall Beep(int frequency, int duration) {
       //直接向端口写
     // 参考上面的代码原理,具体实现,可以直接调用 内联汇编指令实现,比如 outportb
     // 代码简化省略 详见参考代码.
      const unsigned short controlPort = 0x61;
    
     // 开启蜂鸣器
     _outp(controlPort, _inp(controlPort) | 0x03 );
    
    
      // 等待一段时间
     Sleep(duration);
    
      // 关闭蜂鸣器
     _outp(controlPort, _inp(controlPort) & 0xFC);
    }
    
    
    
    

void __stdcall BeepContinuous(){

  const unsigned short controlPort = 0x61;
    // 持续响铃
   _outp(controlPort, _inp(controlPort) | 0x03);

}

  void __stdcall BeepStop(){
  const unsigned short controlPort = 0x61;

    // 停止
  _outp(controlPort, _inp(controlPort) & 0xFC);
}
```
  1. 编译为 DLL: 使用 Visual Studio 或其他 C++ 编译器将上述代码编译为一个 DLL,例如 Buzzer.dll。选择对应的编译器目标平台 x64。
  2. .NET 调用 DLL: 使用 DllImport 属性引入 DLL 中的方法,然后调用。以下C# 代码示范:
    using System;
     using System.Runtime.InteropServices;
    
    
     public class BuzzerWrapper
    {
    
         [DllImport("Buzzer.dll", CallingConvention = CallingConvention.StdCall,  EntryPoint = "Beep")]
         public  static extern void Beep(int frequency, int duration);
    
    
    
         [DllImport("Buzzer.dll",  CallingConvention = CallingConvention.StdCall, EntryPoint="BeepContinuous")]
        public static extern void  BeepContinuous();
    
    
      [DllImport("Buzzer.dll",  CallingConvention = CallingConvention.StdCall,  EntryPoint="BeepStop")]
         public static extern void BeepStop();
    
    
         // 调用示例代码
         public static void Main(string[] args) {
    
              Console.WriteLine("Beep once");
              BuzzerWrapper.Beep(1000,500);  // 调用 Beep(),持续时间500 毫秒
              System.Threading.Thread.Sleep(1000); //延迟
    
            Console.WriteLine("Beeping constantly ");
              BuzzerWrapper.BeepContinuous(); //  调用 BeepContinuous
             System.Threading.Thread.Sleep(5000);
    
             BuzzerWrapper.BeepStop();   // 调用 BeepStop
           Console.WriteLine("Beep stopped.");
    
    
         }
    }
    
    
    BuzzerWrapper.dll 文件的目录下运行代码。Buzzer.dll 和 .net 的可执行程序放在一起。 需要注意的是如果提示找不到指定的 dll, 请仔细检查 dll的编译架构。 以及,请保证使用管理员权限执行.NET程序。 使用直接硬件访问方式时要非常小心,操作不当可能会造成系统不稳定。

安全建议

  • 管理权限: 运行程序必须拥有管理员权限才能访问 I/O 端口, 程序需仔细考虑是否需要以高权限运行的场景和可能的安全隐患。
    - 驱动冲突: 避免在其他硬件或驱动发生问题时使用以上两种方式访问底层硬件端口,可能会造成新的错误或不可预知的结果。
  • 稳定性: 对硬件进行频繁的或过长的蜂鸣器调用可能存在隐患,应注意频率和时长。 充分的测试和合理的使用时十分有必要的。

通过使用这些解决方案,开发者可以绕开 Windows 音频系统的限制,从而精确控制主板蜂鸣器,完成特定的提示任务。务必注意安全风险和代码的可维护性。