4Pin 风扇控制器

对于风扇来说,只要供电就能工作。最开始的电脑使用的都是这种风扇。但是随着技术的发展,人们发现需要对这个风扇进行控制,因为风扇转的快噪音和功耗都会随之增加。于是,加上一根反馈线,让用户能够得知当前的转速。但是这样会遇到另外的两个问题:第一个问题是风扇电压变化,反馈线上的电压也会随之变化,范围大了读取这个反馈会很麻烦。第二个问题是:电压和风扇转速转速关系并不是线性的。比如,一个 12V 的风扇,12V时转速是 2000CPM,10V供电时转速时1000CPM,但是如果11V 供电时,转速很可能是 1100CPM。风扇的转速和风力噪音直接相关,用户想要得到一个大概的转速非常困难。最终 Intel 推出了一个方案:通过 PWM 来控制风扇转速,然后使用5V 信号作为当前转速反馈引脚。

我们最常见到的CPU的风扇通常都是 4 Pin的。供电要求12V,控制的PWM 为 5V 25KHz,转速反馈引脚为 5V  输出。

有些风扇是存在最低转速的,意思是哪怕有PWM 已经为0仍然能够旋转,有些不存在最低转速,PWM 比较低的时候就会停止转动。

了解了上述知识就可以得知我们为了实现控制 CPU 风扇,需要提供12V 电源,提供25Khz 的 PWM 信号,以及读取 5V 的转速输出。

电路设计如下,我们使用 Arduino Uno作为主控。外部12V 供电进入(DC1)之后,经过一个DC-DC 降压模块(来自DFROBOT 的DFR0831,DC-DC降压模块7~24V转5V/4A),降压为5V提供给 Arduino 作为风扇控制,然后风扇的转速反馈信号经过U3上拉后进入Arduino 即可读取。最终转速和当前的PWM设定显示在一个 1602LCD上。

PCB 设计如下,可以看到板子上带有4个按钮用于控制 PWM ,分别是-1、-10、+1、+10这样用户可以快速的改变 PWM 设置。

代码设计如下:

#include <LiquidCrystal_I2C.h>
// 保存当前设定的 PWM 值
#include<EEPROM.h>

LiquidCrystal_I2C lcd(0x3F, 16, 2);

// PWM 发生器相关定义
const byte OC1A_PIN = 9; //PWM输出引脚为 D9
// 定义 PWM 频率
const word PWM_FREQ_HZ = 25000; //Adjust this value to adjust the frequency
const word TCNT1_TOP = 16000000 / (2 * PWM_FREQ_HZ);
const int BTN1 = 13;
const int BTN2 = 12;
const int BTN3 = 11;
const int BTN4 = 10;

// 计算转速相关定义
const int TOTAL = 10; // 计算10次输出一次,这是一个简单的滤波
int InterruptPin = 2;// 接收风扇中断 D2
volatile long int counter = 0;
volatile long int t[TOTAL];
byte p = 0;
byte SavedPWM = 0;
byte lastPWM = -1;
byte pwm;
long int Elsp=0;

void setup() {
  pinMode(BTN1, INPUT_PULLUP);
  pinMode(BTN2, INPUT_PULLUP);
  pinMode(BTN3, INPUT_PULLUP);
  pinMode(BTN4, INPUT_PULLUP);

  pinMode(OC1A_PIN, OUTPUT);

  // Clear Timer1 control and count registers
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;

  // Set Timer1 configuration
  // COM1A(1:0) = 0b10   (Output A clear rising/set falling)
  // COM1B(1:0) = 0b00   (Output B normal operation)
  // WGM(13:10) = 0b1010 (Phase correct PWM)
  // ICNC1      = 0b0    (Input capture noise canceler disabled)
  // ICES1      = 0b0    (Input capture edge select disabled)
  // CS(12:10)  = 0b001  (Input clock select = clock/1)

  TCCR1A |= (1 << COM1A1) | (1 << WGM11);
  TCCR1B |= (1 << WGM13) | (1 << CS10);
  ICR1 = TCNT1_TOP;

  Serial.begin(115200);
  Serial.setTimeout(300);

  pinMode(InterruptPin, INPUT);
  attachInterrupt(0, speedX, FALLING );

  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("PWM Fan CNT");

  // PWM 存在地址0 上
  SavedPWM = EEPROM.read(0);

  if (SavedPWM>100) {
      SavedPWM=0;
    }
  // 将上一次保存的 PWM 设定进去
  setPwmDuty(SavedPWM);
  pwm = SavedPWM;
  lastPWM = pwm + 1;
}

void loop() {
  if (pwm != lastPWM) {
    Serial.println(pwm);
    setPwmDuty(pwm);
    lastPWM = pwm;
    Elsp=millis();
  }

  // 计算当前转速
  long int avg = 0;
  counter = 0;
  delay(1000);

  p = (p + 1) % TOTAL;
  t[p] = (long int)(counter * 60 / 2);

  for (byte i = 0; i < TOTAL; i++) {
    avg = avg + t[i];
  }

  // 输出转速
  Serial.print("Current speed: ");
  Serial.println(avg / TOTAL);

  lcd.setCursor(0, 1);
  lcd.print("Fan Speed:");
  lcd.print(avg / TOTAL);
  lcd.print("      ");

  counter = 0;
  pinMode(BTN1, INPUT_PULLUP);
  if (digitalRead(BTN1) == LOW) {
    delay(5);
    if ((digitalRead(BTN1) == LOW) && (pwm > 9)) {
      pwm = pwm - 10;
    }
  }
  if (digitalRead(BTN2) == LOW) {
    delay(5);
    if ((digitalRead(BTN2) == LOW) && (pwm > 0)) {
      pwm = pwm - 1;
    }
  }  
  if (digitalRead(BTN3) == LOW) {
    delay(5);
    if ((digitalRead(BTN3) == LOW) && (pwm <=100-10)) {
      pwm = pwm + 10;
    }
  }
  if (digitalRead(BTN4) == LOW) {
    delay(5);
    if ((digitalRead(BTN4) == LOW) && (pwm <100)) {
      pwm = pwm + 1;
    }
  }

  // 每隔5秒检查 pwm 是否已经改变,如果改变则保存
  if ((SavedPWM!=pwm)&&(millis()-Elsp>5000)) {
      Serial.println("Save current pwm");
      Elsp=millis();
      SavedPWM=pwm;
      EEPROM.write(0,pwm);
    }
}

void setPwmDuty(byte duty) {
  Serial.print("Set pwm "); Serial.println(duty);
  lcd.setCursor(0, 0);
  lcd.print("Current pwm:");
  lcd.print(duty);
  lcd.print("  ");
  OCR1A = (word) (duty * TCNT1_TOP)/ 100;
}


void speedX()//中断函数
{
  counter++;
}

4 Pin fan 的 spec:

本文提到的电路图和PCB:

FireBeetle FFT VGA显示

之前基于 FireBeetle ESP32 和全向MEMS麦克风模块(SEN0487)制作过一个在OLED 屏幕上显示当前环境声音频谱的装置【参考1】。这次制作的是能够输出 VGA 信号的频谱装置,这样,用户能够在显示器或者电视机上看到实时频谱输出。

具体的VGA 显示原理,可以在之前的介绍中看到【参考2】,这次的设计硬件部分与之类似。电路图如下:

其中主控和VGA 部分如下:VGA本质上还是模拟信号,这里使用电阻能够输出不同电平的模拟信号,三根GPIO能够实现2^3=16种组合,因此也意味着能够实现16种颜色.

下面是用于连接全向MEMS麦克风模块的接口:

板子上带有一个 USB 公头用于取电,另外还有一个 USB母头,如果你的显示设备没有 VGA接口只有HDMI接口,那么需要一个VGA转HDMI线,而这种线通常使用USB公头取电,这种情况可以直接将它连接到这个 USB母头取电。

同样的,为了便于从充电宝取电,还设计了一个负载消耗电路。

PCB设计如下:

3D预览如下:

焊接后的实物如下:

接下来就可以进行软件的设计了,。基本原理是:首先通过ADC进行采样,然后将采样结果进行 FFT ,最终得到的是采样期间每个频率的能量。我们将这个数值显示在 VGA上就得到了期望的结果:

#include <arduinoFFT.h>
#include "fabgl.h"

// VGA 显示
fabgl::VGA16Controller DisplayController;
Canvas cv(&DisplayController);

//ZivDebug #define SAMPLES         1024          // Must be a power of 2
#define SAMPLES         256          // Must be a power of 2
#define SAMPLING_FREQ   40000         // Hz, must be 40000 or less due to ADC conversion time. Determines maximum frequency that can be analysed by the FFT Fmax=sampleF/2.
#define AMPLITUDE       1000          // Depending on your audio source level, you may need to alter this value. Can be used as a 'sensitivity' control.
#define AUDIO_IN_PIN    A0            // Signal in on this pin

#define NOISE           500           // Used as a crude noise filter, values below this are ignored
const uint8_t kMatrixWidth = 16;      // Matrix width
const uint8_t kMatrixHeight = 16;     // Matrix height

#define NUM_BANDS       16            // To change this, you will need to change the bunch of if statements describing the mapping from bins to bands

#define BAR_WIDTH      (kMatrixWidth  / (NUM_BANDS - 1))  // If width >= 8 light 1 LED width per bar, >= 16 light 2 LEDs width bar etc
#define TOP            (kMatrixHeight - 0)                // Don't allow the bars to go offscreen

// Sampling and FFT stuff
unsigned int sampling_period_us;
byte peak[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // The length of these arrays must be >= NUM_BANDS
int oldBarHeights[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int bandValues[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
double vReal[SAMPLES];
double vImag[SAMPLES];
unsigned long newTime;

arduinoFFT FFT = arduinoFFT(vReal, vImag, SAMPLES, SAMPLING_FREQ);

unsigned long int Elsp1;
int h[16];
int Height,Width;

void setup() {
  Serial.begin(115200);
  sampling_period_us = round(1000000 * (1.0 / SAMPLING_FREQ));
  DisplayController.begin();
  // 设定分辨率
  DisplayController.setResolution(VGA_640x400_60Hz);
  Height=cv.getHeight();
  Width=cv.getWidth();

  cv.setBrushColor(Color::Red    );
  
  // get a font for about 40x14 text screen
  cv.selectFont(&fabgl::FONT_8x8);

  cv.setGlyphOptions(GlyphOptions().FillBackground(true));
  
}

void loop() {
  static int64_t stime  = esp_timer_get_time();
  static int FPS        = 0;
  static int FPSCounter = 0;
  

  // Reset bandValues[]
  for (int i = 0; i < NUM_BANDS; i++) {
    bandValues[i] = 0;
  }

  // Sample the audio pin
  for (int i = 0; i < SAMPLES; i++) {
    newTime = micros();
    vReal[i] = analogRead(AUDIO_IN_PIN); // A conversion takes about 9.7uS on an ESP32
    vImag[i] = 0;
    while ((micros() - newTime) < sampling_period_us) {
      /* chill */
    }
  }

  // Compute FFT
  FFT.DCRemoval();
  FFT.Windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
  FFT.Compute(FFT_FORWARD);
  FFT.ComplexToMagnitude();

  // Analyse FFT results
  for (int i = 2; i < (SAMPLES / 2); i++) {    // Don't use sample 0 and only first SAMPLES/2 are usable. Each array element represents a frequency bin and its value the amplitude.
    if (vReal[i] > NOISE) {                    // Add a crude noise filter

      //16 bands, 12kHz top band
      if (i <= 2 )           bandValues[0]  += (int)vReal[i];
      if (i > 2   && i <= 3  ) bandValues[1]  += (int)vReal[i];
      if (i > 3   && i <= 5  ) bandValues[2]  += (int)vReal[i];
      if (i > 5   && i <= 7  ) bandValues[3]  += (int)vReal[i];
      if (i > 7   && i <= 9  ) bandValues[4]  += (int)vReal[i];
      if (i > 9   && i <= 13 ) bandValues[5]  += (int)vReal[i];
      if (i > 13  && i <= 18 ) bandValues[6]  += (int)vReal[i];
      if (i > 18  && i <= 25 ) bandValues[7]  += (int)vReal[i];
      if (i > 25  && i <= 36 ) bandValues[8]  += (int)vReal[i];
      if (i > 36  && i <= 50 ) bandValues[9]  += (int)vReal[i];
      if (i > 50  && i <= 69 ) bandValues[10] += (int)vReal[i];
      if (i > 69  && i <= 97 ) bandValues[11] += (int)vReal[i];
      if (i > 97  && i <= 135) bandValues[12] += (int)vReal[i];
      if (i > 135 && i <= 189) bandValues[13] += (int)vReal[i];
      if (i > 189 && i <= 264) bandValues[14] += (int)vReal[i];
      if (i > 264          ) bandValues[15] += (int)vReal[i];
    }
  }

  // Process the FFT data into bar heights
  for (byte band = 0; band < NUM_BANDS; band++) {

    // Scale the bars for the display
    int barHeight = bandValues[band] / AMPLITUDE;
    if (barHeight > TOP) barHeight = TOP;

    // Small amount of averaging between frames
    barHeight = ((oldBarHeights[band] * 1) + barHeight) / 2;

    // Move peak up
    if (barHeight > peak[band]) {
      peak[band] = min(TOP, barHeight);
    }
    h[band] = barHeight;

    // Save oldBarHeights for averaging later
    oldBarHeights[band] = barHeight;

  }

  if (millis() - Elsp1 > 10) {
    for (byte band = 0; band < NUM_BANDS; band++)
      if (peak[band] > 0) peak[band] -= 1;


    cv.setBrushColor(Color::Black    );
    cv.clear();
    cv.setBrushColor(Color::Red    );
    for (int i = 0; i < 16; i++) {
      if (h[i] != 0) {
        //cv.fillRectangle(cv.getWidth()*i / 16, 0, cv.getWidth() * (i + 1) / 16, cv.getHeight() *h[i] / 16);
        cv.fillRectangle(Width*i / 16, Height -1 , Width * (i + 1) / 16-1, (Height-1) *(16-h[i]) / 16);
        
      }
      Serial.print(h[i], HEX);
      Serial.print("");
    }
    Serial.println("");

    Elsp1 = millis();
  }
  
      if (esp_timer_get_time() - stime > 1000000) {
    // calculate FPS
    FPS = FPSCounter;
    stime = esp_timer_get_time();
    FPSCounter = 0;
  }
  ++FPSCounter;

  // display test state and FPS
  cv.setPenColor(Color::Blue);
  cv.setBrushColor(Color::Yellow);
  cv.drawTextFmt(80, 5, "%d FPS ",  FPS);
}

参考:

  1. https://mc.dfrobot.com.cn/thread-314320-1-1.html
  2. https://mc.dfrobot.com.cn/thread-311156-1-1.html?fromuid=70205

本文提到的电路图下载:

本文提到的完整代码:

Step to UEFI (278)Progra message 的使用

Visual Studio 的 C 支持 #pragma message() 宏可以用来输出一些信息。于是编写一个代码进行测试:

#include  &lt;Uefi.h>
#include  &lt;Library/UefiLib.h>
#include  &lt;Library/ShellCEntryLib.h>

/***
  Print a welcoming message.

  Establishes the main structure of the application.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
  #pragma message (__FILE__)
    return(0);
}

唯一的问题是:我在 EDK2 中编译的时候,无法看到输出的结果。经过研究,编译C代码是通过下面这个指令:

"C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Tools\MSVC\14.29.30133\bin\Hostx86\x64\cl.exe" /Foc:\buildbs\edk2-edk2-stable202205\Build\AppPkg\DEBUG_VS2019\X64\AppPkg\Applications\HelloMacro\HelloMacro\OUTPUT\.\ /showIncludes /nologo /c /WX /GS- /W4 /Gs32768 /D UNICODE /O1b2s /GL /Gy /FIAutoGen.h /EHs-c- /GR- /GF /Z7 /Gw /X /Zc:wchar_t /D UEFI_C_SOURCE /Wv:11 /Ic:\buildbs\edk2-edk2-stable202205\AppPkg\Applications\HelloMacro  /Ic:\buildbs\edk2-edk2-stable202205\Build\AppPkg\DEBUG_VS2019\X64\AppPkg\Applications\HelloMacro\HelloMacro\DEBUG  /Ic:\buildbs\edk2-edk2-stable202205\MdePkg  /Ic:\buildbs\edk2-edk2-stable202205\MdePkg\Include  /Ic:\buildbs\edk2-edk2-stable202205\MdePkg\Test\UnitTest\Include  /Ic:\buildbs\edk2-edk2-stable202205\MdePkg\Include\X64  /Ic:\buildbs\edk2-edk2-stable202205\ShellPkg  /Ic:\buildbs\edk2-edk2-stable202205\ShellPkg\Include @c:\buildbs\edk2-edk2-stable202205\Build\AppPkg\DEBUG_VS2019\X64\AppPkg\Applications\HelloMacro\HelloMacro\OUTPUT\cc_resp_1.txt

于是尝试去掉其中的/nologo 指令,运行结果如下:

再同时去掉 /showIncludes 运行结果:

可以看到,其中出现了当前的文件名信息。

总结:在使用VC 编写代码的时候,如果需要输出一些编译期的数据,可以考虑使用 #pragma message() 来实现。

Step to UEFI (277)QEMU 增加自定义的 FFS和读取

这次实验的是在 OVMF 生成的BIOS中插入一个Binary ,然后在代码中将这个Binary 读取出来。

第一个目标:在 OVMF 中插入 Binary。

1.我们准备一个 message.txt,其中内容是简单的字符串:

This is a test message comes from 
www.lab-z.com

2.在\OvmfPkg\OvmfPkgX64.fdf 文件中,加入下面的代码

!if $(E1000_ENABLE)
  FILE DRIVER = 5D695E11-9B3F-4b83-B25F-4A8D5D69BE07 {
    SECTION PE32 = Intel3.5/EFIX64/E3522X2.EFI
  }
!endif

#LABZDebug_Start
FILE FREEFORM = C3E36D09-2023-0829-A857-D5288FE33E28 Align=4K {
  SECTION RAW = OvmfPkg/LabzBin/message.txt
}
#LABZDebug_End

!include NetworkPkg/Network.fdf.inc
  INF  OvmfPkg/VirtioNetDxe/VirtioNet.inf

3.使用工具查看放置的FFS,可以看到正确的增加到 BIOS 中

这样,第一个目标已经完成,我们成功的生成了一个FFS文件。

第二个目标,将这个 FFS文件从FV中读取出来。之前我们做过类似的实验,在【参考1】中有介绍。这次我们编写一个 UEFI Shell Application ,显示前面插入的 FFS文件的内容。测试代码如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include  "PiFirmwareVolume.h"
#include  "PiFirmwareFile.h"
#include  "FirmwareVolume2.h"

INTN
EFIAPI
ShellAppMain (
    IN UINTN Argc,
    IN CHAR16 **Argv
)
{
	CONST EFI_GUID    NameGuid= { 0xC3E36D09, 0x2023, 0x0829,
		{ 0xA8, 0x57, 0xD5, 0x28, 0x8F, 0xE3, 0x3E, 0x28 }
	};
	EFI_SECTION_TYPE  SectionType=EFI_SECTION_RAW;

	VOID             *Buffer=NULL;
	UINTN             Size=0;
	UINT32            AuthenticationStatus=0;
	EFI_STATUS  	  Status;
	EFI_FIRMWARE_VOLUME2_PROTOCOL *Fv;

	Status = gBS->LocateProtocol (
	             &gEfiFirmwareVolume2ProtocolGuid,
	             NULL,
	             (VOID **) &Fv
	         );
	if (EFI_ERROR (Status))
	{
		Print(L"[EFI_FIRMWARE_VOLUME2_PROTOCOL not found]\n");
		return EFI_NOT_FOUND;
	}
	//
	// Read desired section content in NameGuid file
	//
	Status      = Fv->ReadSection (
	                  Fv,
	                  &NameGuid,
	                  SectionType,
	                  0,
	                  &Buffer,
	                  &Size,
	                  &AuthenticationStatus);
	UINT8 *P=(UINT8 *)Buffer;
	Print(L"[EFI_FIRMWARE_VOLUME2_PROTOCOL %r]\n",Status);

	for (UINTN i=0; i<Size; i++)
	{
		Print(L"%c",P[i]);
	}
	Print(L"\n");

	return(0);
}

运行的结果如下图所示,可以看到正确的读取出我们存放的内容:

完整的代码下载:

参考:

1. https://www.lab-z.com/getffs/  代码读取一个 FFS

Step to UEFI (276)宏和结构体初始化表格

在 EDK2 中有一种比较有趣的定义和初始化Table 的方法,主要是基于 __VA_ARGS__ 这个宏。

“__VA_ARGS__是一个预处理宏,用于表示可变数量的参数。当在宏定义中使用__VA_ARGS__,它会自动展开为传递给宏的实际参数。以下是一个示例使用__VA_ARGS__的宏定义代码:
#include &lt;stdio.h>
 
#define PRINT_ARGS(...) printf(__VA_ARGS__)
 
int main() {
    PRINT_ARGS("Hello, %s!\n", "World");
    return 0;
}
上述代码中,宏定义PRINT_ARGS使用__VA_ARGS__来表示可变数量的参数,并通过printf函数打印参数。在main函数中,我们调用PRINT_ARGS宏来打印字符串"Hello, World!"。运行结果为输出"Hello, World!"。
总结:__VA_ARGS__是一个用于表示可变数量参数的预处理宏,在宏定义中使用它可以方便地处理不定数量的参数。“----来自百度

很多时候,我们定义一个 Table 用来传递一些常量,Table需要给出具体的长度,通过这个宏可以实现自动给出Table 的长度,避免用户手工计数的麻烦。

下面是一个示例代码:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>

#define MY_TABLE_INIT(Vid,Did,...) \
{ \
  { Vid, Did, (sizeof((UINT32[]){__VA_ARGS__})/sizeof(UINT32)) }, \
  { __VA_ARGS__ } \
}

typedef struct {
  UINT16  VendorId;
  UINT16  DeviceId;
  UINT16  DataDwords;
} MY_TABLE_HEADER;

typedef struct  {
  MY_TABLE_HEADER  Header;
  UINT32 Data[];
} ONE_TABLE;

ONE_TABLE Table = MY_TABLE_INIT (
  0x1234, 0x5678,
  
  // Raw Data
  0x01234567,
  0x89ABCDEF,
  0xFEDCBA98,
  0x76543210
);

INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
  Print(L"Table:\n");
  Print(L"   VendorId:[%X]\n",Table.Header.VendorId);
  Print(L"   DeviceId:[%X]\n",Table.Header.DeviceId);
  Print(L"   Size    :[%X]\n",Table.Header.DataDwords);  
  
  for (int i=0;i<Table.Header.DataDwords;i++) {
	  Print(L"[%04X]",Table.Data[i]);  
  }
  Print(L"\n");
  return(0);
}

运行结果如下:

上面代码的解释如下:

1.首先我们定义一个 ONE_TABLE 结构体用来“携带”数据。

typedef struct  {
  MY_TABLE_HEADER  Header;
  UINT32 Data[];
} ONE_TABLE;

从定义可以看到,这个结构体包含了一个头,还有一个变长的数据段。头可以实现用于识别判断这个Table 是否为我们需要的目的,例如,其中有DID和VID 信息。具体定义如下,特别注意 DataDwords 给出了后面变长数据段的长度:

typedef struct {
  UINT16  VendorId;
  UINT16  DeviceId;
  UINT16  DataDwords;
} MY_TABLE_HEADER;

对于DataDwords 就是我们前面提到的“Table需要给出具体的长度”的问题。

2.为了解决上述问题,通过下面的宏来解决:

#define MY_TABLE_INIT(Vid,Did,...) \
{ \
  { Vid, Did, (sizeof((UINT32[]){__VA_ARGS__})/sizeof(UINT32)) }, \
  { __VA_ARGS__ } \
}

其中(sizeof((UINT32[]){__VA_ARGS__})/sizeof(UINT32)) 就是计算长度的代码。最终的结果是以 UINT32(DWORD)给出的。

3.初始化定义如下,可以看到DataDwords的计算是宏直接完成的,并不需要我们直接提供

ONE_TABLE Table = MY_TABLE_INIT (
  0x1234, 0x5678,
  
  // Raw Data
  0x01234567,
  0x89ABCDEF,
  0xFEDCBA98,
  0x76543210
);

可以看到,通过上面的方法可以帮助我们方便的实现可变数据的长度定义,有兴趣的朋友不妨尝试一下。

完整的代码下载: