MAX98357 I2S功放模块测试

之前购买了一个MAX98357 I2S功放模块,这次编写简单的代码进行测试。

硬件连接如下:

MAX98357ESP32S3用途
SPK+/- 连接喇叭 连接喇叭正负极,喇叭输出
DIN  48从 ESP32S3 发送的 I2S数据
BCLK45从 ESP32S3 发送的 I2S Clock
LRC 35从 ESP32S3 发送的 I2S 左右声道选择信号
GNDGND
VCC5V供电

按照上述方案连接好后,烧录如下代码:

#include <I2S.h>
const int frequency = 440; // frequency of square wave in Hz
const int amplitude = 32000; // amplitude of square wave
const int sampleRate = 8000; // sample rate in Hz
const int bps = 16;

const int halfWavelength = (sampleRate / frequency); // half wavelength of square wave

short sample = amplitude; // current sample value
int count = 0;

i2s_mode_t mode = I2S_PHILIPS_MODE; // I2S decoder is needed
// i2s_mode_t mode = ADC_DAC_MODE; // Audio amplifier is needed

// Mono channel input
// This is ESP specific implementation -
//   samples will be automatically copied to both channels inside I2S driver
//   If you want to have true mono output use I2S_PHILIPS_MODE and interlay
//   second channel with 0-value samples.
//   The order of channels is RIGH followed by LEFT
//i2s_mode_t mode = I2S_RIGHT_JUSTIFIED_MODE; // I2S decoder is needed

void setup() {
  Serial.begin(115200);
  Serial.println("I2S simple tone");
  delay(5000);

    //setAllPins(int sckPin, int fsPin, int sdPin, int outSdPin, int inSdPin);
  I2S.setAllPins(45        , 35       , 48       , 48          , -1);

  // start I2S at the sample rate with 16-bits per sample
  if (!I2S.begin(mode, sampleRate, bps)) {
    
    Serial.println("Failed to initialize I2S!");
    while (1); // do nothing
  }
}

void loop() {

   while (Serial.available()) {
    char c = Serial.read();
    if (c == '3') {
      ESP.restart();
    }
    // 主机端发送 l, 回复 z 用于识别串口
    if (c == '1') {
      Serial.print('z');
    }
    // 主机端发送 l, 回复 z 用于识别串口
    if (c == '2') {
      Serial.printf("getSckPin:%d getFsPin:%d getDataPin:%d",
                      I2S.getSckPin(),
                      I2S.getFsPin(),
                      I2S.getDataPin());
    }    
  }

  
    if (count % halfWavelength == 0 ) {
      // invert the sample every half wavelength count multiple to generate square wave
      sample = -1 * sample;
    }

    if(mode == I2S_PHILIPS_MODE || mode == ADC_DAC_MODE){ // write the same sample twice, once for Right and once for Left channel
      I2S.write(sample); // Right channel
      I2S.write(sample); // Left channel
    }else if(mode == I2S_RIGHT_JUSTIFIED_MODE || mode == I2S_LEFT_JUSTIFIED_MODE){
      // write the same only once - it will be automatically copied to the other channel
      I2S.write(sample);
    }

    // increment the counter for the next sample
    count++;
}

测试的视频在下面可以看到:

WinPE下面制造一个蓝屏

正常的 Windows下面蓝屏很常见,实际上 WinPE 下面也是可以出现蓝屏的。这次介绍通过修改 WinPE 的注册表,实现USB键盘上按下右 Ctrl+快速按下Scroll键即可触发蓝屏 【参考1】。

最关键的步骤是修改 WinPE的注册表,打开这个触发蓝屏的功能。

1.将 Windows安装盘中的 Boot 解压,放在 c:\labz 目录下,然后运行如下命令解包之:

dism /mount-wim /wimfile:c:\labz\boot.wim /index:1 /mountdir:c:\m1

2.打开本机的注册表工具,先选中 HKEY_USERS,然后在菜单上选择 Load Hive

3.在对话框上选择 c:\m1\windows\system32\config\system 文件

4.设置一个加载点

这样操作之后可以看到挂载到如下位置了

5.我们在LABZ\ControlSet001\Services\kbhid\Parameters 下面创建 CrashOnCtrlScroll 并且赋值为1

6.选中注册表上的 “LABZ”,然后菜单选择 Unload Hive,这样就从注册表中卸载了WinPE的注册表

7.之后 关闭注册表编辑器,然后使用如下命令写入 WIM(特别注意,实践中发现有时候无法正常写入,错误信息是 Windows有占用目录下的文件,这种情况下重启操作系统再运行一次即可)

dism /unmount-wim /MountDir:c:\m1 /Commit

8.接下来再次解包 Boot.WIM (原因和之前” 制作全自动安装的 Windows 11 ISO”文章提到的一样,BOOT.WIM 中有2个WindowPE环境)

dism /mount-wim /wimfile:c:\labz\boot.wim /index:2  /mountdir:c:\m1

之后同样执行2-7步骤,最终我们就得到了一个修改后的 BOOT.WIM

使用,ISO 编辑工具将BOOT.WIM 写入 ISO 之后,我们就得到了一个测试 Windows安装镜像文件。在安装过程中使用 USB 键盘,按下右侧Ctrl然后快速按下2测 Scoll 键就能够触发蓝屏了。

可以看到蓝屏发生后,显示一段之后会自动重启。

最后讲个有意思的事情。很多年前,我碰到过一个奇怪的问题:工厂那边反映我们 Release 的BIOS有问题,会导致生产过程中重启。负责这个事情的BIOS工程师是一个妹子,被这个问题折磨很久。问题发生之后,她不修改任何代码,只是重新编译一次BIOS,发给产线使用问题就会消失。但是后面不知道什么时候问题又会出来。出于好奇后来我接手这个问题进行研究。所有的整机无论是笔记本还是台式机,在生产的过程中都会有灌装系统运行产测软件的步骤。在这个过程中,会检查一些基本的功能,比如:是否会有声音,屏幕键盘鼠标能否工作正常等等。这个测试通常需要一气呵成完成的,每个测试都会收集测试结果,然后上报到服务器中的,因此如果发生意外重启会打断整个流程。测试结束后,会重新安装一个新的操作系统然后再交给客户。作为BIOS工程师,我是不太相信BIOS会导致这样的问题。因此,我和工厂要了一下他们的产测软件在实验室进行研究。产线使用 WinPE环境,我手上没有,只能在普通 Windows上试验。试验几次之后,我发现其中的测试软件会出现Windows的报错对话框。于是,将关注点放在了这个软件上。咨询产线得知这个软件的作用是在0xF0000 中搜索一个字符串。听到这里我心里就有了大概,Windows下访问物理内存,出现错误是很重要的。之后和产线要到的源代码,简单读了了一下很快就定位了问题。这个软件需要在物理内存中搜索,于是作者调用了一个 Window API 做了一下映射,将 0xF0000开始的64K物理内存映射到应用程序的内存上,然后用 memcmp 进行查找。发生问题的原因是,要查找的字符串刚好卡在结尾处,比如我们需要在内存中查找“LAB-Z.COM”这个字符串,但是刚好碰到这个字符串在内存中分布如下:

0xFFFFA0xFFFFB0xFFFFC0xFFFFD0xFFFFE0xFFFFF0x10 00000x10 00010x10 00020x10 0003
LAB-Z.COM0

当直接使用 memcpy 查找的时候,它会先找到 “LAB-Z”,但是继续比较字符串剩余部分的时候就碰到“指针出界”的问题。如果在正常的Windows下是可以抛出这个问题继续运行的,但是在WinPE环境下就直接变为重启。最终,经过努力这次的问题没有让BIOS工程师来背锅。

另外多说一句:即便相同的产线,相同的物料,白班和夜班的质量也会有差别。原因是白天人全,技术和后端都在,出了问题会有人处理;夜班的话,人不全,出了问题通常的处理方法是“小车不倒只管推”了。

参考:

1. https://www.lab-z.com/wdbg/  WinDBG 分析键盘生成的 Dump 文件

ASCII数字

因为 UEFI Shell 是 CUI 界面,很多测试需要用 ASCII 显示结果。所以有些时候我们需要足够大的字来展示。

最近偶然发现了这个 ASCII 字体的网站,可以用来取得一些 ASCII 拼凑出来的字形。例如:

░▒▓█▓▒░       ░▒▓██████▓▒░░▒▓███████▓▒░░▒▓████████▓▒░ 
░▒▓█▓▒░      ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░      ░▒▓█▓▒░ 
░▒▓█▓▒░      ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░    ░▒▓██▓▒░  
░▒▓█▓▒░      ░▒▓████████▓▒░▒▓███████▓▒░   ░▒▓██▓▒░    
░▒▓█▓▒░      ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓██▓▒░      
░▒▓█▓▒░      ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░        
░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓███████▓▒░░▒▓████████▓▒░ 
                                                      

 __      __    ____      ____ 
(  )    /__\  (  _ \ ___(_   )
 )(__  /(__)\  ) _ <(___)/ /_ 
(____)(__)(__)(____/    (____)

     ___       ___           ___           ___     
     /\__\     /\  \         /\  \         /\  \    
    /:/  /    /::\  \       /::\  \        \:\  \   
   /:/  /    /:/\:\  \     /:/\:\  \        \:\  \  
  /:/  /    /::\~\:\  \   /::\~\:\__\        \:\  \ 
 /:/__/    /:/\:\ \:\__\ /:/\:\ \:|__| _______\:\__\
 \:\  \    \/__\:\/:/  / \:\~\:\/:/  / \::::::::/__/
  \:\  \        \::/  /   \:\ \::/  /   \:\~~\~~    
   \:\  \       /:/  /     \:\/:/  /     \:\  \     
    \:\__\     /:/  /       \::/__/       \:\__\    
     \/__/     \/__/         ~~            \/__/    

有兴趣的朋友可以在这里看到:

http://www.patorjk.com/software/taag/#p=testall&f=Alpha&t=LAB-Z

UEFI TIPS:Print=UnicodeSPrint+ ConOut

最近有在屏幕输出数据的需求,但是无法直接使用 Print 。经过对于 PrintLib 的一番研究,得出了结论:

UEFI Shell 下的Print 的实现可以看作两个动作,一个根据输入格式化得到字符串,另外一个是使用 gST 进行输出。简单的说就是:

Print=UnicodeSPrint+ gST->ConOut->OutputString

编写测试代码:

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

#include  <Library/PrintLib.h>
extern EFI_SYSTEM_TABLE			 *gST;
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
  CHAR16					Buffer[32];
  
  UnicodeSPrint((CHAR16 *)Buffer,sizeof(Buffer),L"%x\n",2024);
  gST->ConOut->OutputString(gST->ConOut,Buffer);
 
  return(0);
}

运行结果:

如果你在调试 Application 或者Driver遇到无法直接使用 Print 的情况,不妨考虑本文提到的方法。当然,如果你有从串口或者其他设备输出调试数据的时候,也可以考虑UnicodeSPrint()来实现数据格式化方便阅读调试。

一些Unicode 数字符号定义

1.圈中带有数字

⓪ ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳
㉑ ㉒ ㉓ ㉔ ㉕ ㉖ ㉗ ㉘ ㉙ ㉚ ㉛ ㉜ ㉝ ㉞ ㉟ ㊱ ㊲ ㊳ ㊴ ㊵
㊶ ㊷ ㊸ ㊹ ㊺ ㊻ ㊼ ㊽ ㊾ ㊿

2.实心方块中空圆形数字

         
         

3.实心方块数字

         

4.中文

         

来源:http://xahlee.info/comp/unicode_circled_numbers.html

此外,在 https://altcodeunicode.com/alt-codes-circled-number-letter-symbols-enclosed-alphanumerics/ 还有一些圆圈的英文序号

制作全自动安装的 Windows 11 ISO

前面提 到通过应答文件来实现自动安装【参考1】,使用这个方法在虚拟机上测试正常,但是实体机上在分区拷贝完成之后会进入OOBE的界面要求手工选择,因此并非全自动安装。

针对这个问题进行了更深入的研究,原来除了在安装盘下面放置autounattend.xml 之外,在对应的 WinPE 盘,Windows\Panther 目录下还需要放置一个 unattend.xml 应答文件。更具体的操作是,安装 ADK

1。解压Windows 11 安装 ISO

2.取出其中的 boot.wim 文件

3.在 “Deployment and Imaging Tools Environment”中使用如下命令解压 Boot.WIM 到 m1 目录下

dism /mount-wim /wimfile:c:\LABZ\boot.wim /index:1 /mountdir:c:\m1

4.将unattend.xml拷贝到 m1\Windows\Panther目录下

5.将修改回写到 boot.wim 中

dism /unmount-wim /MountDir:c:\m1 /Commit

6.同样在 “Deployment and Imaging Tools Environment”中再次解压 Boot.WIM 到 m1 目录下(Boot 中有个 Index,上一次用的是 1,这次用的是 2)

dism /mount-wim /wimfile:c:\LABZ\boot.wim /index:2 /mountdir:c:\m1

7.将unattend.xml拷贝到 m1\Windows\Panther目录下

8.将修改回写到 boot.wim 中

dism /unmount-wim /MountDir:c:\m1 /Commit

这样就有了一个更换过 unattend.xml的Boot.WIM 其中是一个完整的 WinPE 环境,接下来使用工具将Boot.WIM替换回原始的安装 ISO。最终我们就有了一个全自动的Window安装盘。

这样的ISO通过 Refuse或者Ventoy 制作出来的启动U盘上都可以正常启动,并且能够做到完全自动的安装。

本文提到的制作好的 Windows 11 可以在这里下载(文件超过4G, 分割为2个ZIP 压缩文件)

文件1

文件2

对应的2个 xml 文件可以在这里下载:

参考:

1. https://www.lab-z.com/winut/

测试的视频,在 LattePanda MU 上进行的测试:

基于Ch554 实现 USB 转 USB

最近设计了一个 USB 转USB 设备,它带有一个USB公头和一个USB母头,可以读取一个USB 设备发出来的数据,然后转换为另外的数据。

首先是硬件设计,使用了2个 CH554e芯片,这个芯片能够实现 USB Host和Device,二者通过串口进行通讯。这款芯片是南京WCH出品的,最高支持 24Mhz主频内置,16K程序存储器ROM和256字节内部iRAM以及1K 字节片内xRAM。Ch554e 是最小的封装形式,MOP10方便焊接。

硬件设计比较简单,2个5V供电的Ch554的最小系统,中间通过串口连接。

PCB 设计如下:

这样的设计刚好能够放入淘宝购买的透明外壳中。

两个 Ch554分别写入不同的固件。其中USB母头连接Ch554的需要写入 Keil编译的代码,用于实现 USB Host 功能。解析后的HID 数据会按照 Ch9350 格式通过串口输出,有兴趣的朋友可以在【参考1】看到实现。

USB 公头连接的Ch554代码使用 Arduino 进行编译。实现的功能是检测通过串口获得数据是否包含 wasd 这几个按键,如果有的话就转为对应的鼠标移动操作:

#ifndef USER_USB_RAM
//#error "This example needs to be compiled with a USER USB setting"
#endif

#include "src/userUsbHidKeyboardMouse/USBHIDKeyboardMouse.h"

#define DEBUGMODE 0

void setup() {
  // 串口通讯
  Serial1_begin(500000);
  USBInit();
  pinMode(14, OUTPUT);
  digitalWrite(14, LOW);
}

boolean FindKey(byte s, byte *p, byte len) {
  for (byte i = 0; i < len; i++) {
    if (p[i] == s) {
      return true;
    }
  }
  return false;
}
void loop() {
  //根据 CH9350 Spec 每次最多输出 72Bytes
  byte Data[72];
  byte CounterLast = Serial1_available();
  byte CounterCurrent = 0;

  // 如果当前串口有数据
  if (CounterLast != 0) {
    // 进行简单测试,如果当前还在传输数据那么持续接收
    while (CounterCurrent != CounterLast) {
      CounterLast = Serial1_available();
      delayMicroseconds(500);
      CounterCurrent = Serial1_available();
    }
  }

  if (CounterCurrent > 0) {
    // 一次性将数据收取下来
    //Serial1_readBytes(Data, CounterCurrent);
    for (byte i = 0; i < CounterCurrent; i++) {
      Data[i] = Serial1_read();
      //  USBSerial_print_ub(Data[i], 16);
    }

    unsigned int i = 0;
    unsigned int Length;
    while (i < CounterCurrent) {
      // 识别帧头
      if ((Data[i] == 0x57) && (Data[i + 1] == 0xAB)) {
        // 有效键值帧
        if (Data[i + 2] == 0x88) {
          // 获得数据长度
          Length = Data[i + 3];
          //如果是键盘
          if (Data[i + 4] == 0x10) {
            digitalWrite(14, HIGH);
            if (DEBUGMODE) {
              for (int j = 3; j < Length - 2; j++) {
                USBSerial_print_ub(Data[i + 3  + j], 16);
              }
              USBSerial_println_only();
            }
            if (FindKey(0x1A, Data, Length) == true) { // 'w'
              Mouse_move(0, -100);
            }
            if (FindKey(0x16, Data, Length) == true) { // 's'
              Mouse_move(0, 100);
            }
            if (FindKey(0x04, Data, Length) == true) { // 'a'
              Mouse_move(-100,0);
            }
            if (FindKey(0x07, Data, Length) == true) { // 'd'
              Mouse_move(100,0);
            }
            digitalWrite(14, LOW);
          } //if (Data[i + 4] == 0x10) {
        }
        i = i + 3 + Length;
      }
      i++;
    } // while (i < Counter)
  }
}

通过串口烧录,短接 DL1 或者  DL2 接口,插入 Windows主机后,设备管理器中会出现新的设备,然后使用 wchispstudio 即可烧写。对于USB Host 对应的Ch554需要一个 USB 公头转公头来实现转为USB公头然后进行烧写。

参考:

  1. https://mc.dfrobot.com.cn/thread-319462-1-1.html

本文提到的完整代码下载:

1.模拟键盘的代码(Arduino)

2. USB Host 代码

Step to UEFI (296)虚拟机下的假电池

电池作为现在笔记本必不可少的部件,通过 ACPI 和 Windows 进行交互。

对此,ACPI Spec 定义了几个 Table。换一句话说,Windows 只要能够正确读取出 Table,那么就可以根据上面的信息展示给客户一个电池。

第一个是 _BIX (Battery Information Extended) (特别注意ACPI 4.0定义的 _BIF (Battery Information)已经废止 ),其中给出了电池的信息。

偏移名称大小解释
RevisionDWORD目前版本号为 1
Power UnitDWORD电池容量单位: 0 – [mWh], 同时充放电速度将会以[mW]为单位 1 – [mAh], 同时充放电速度将会以[mA]为单位
Design CapacityDWORD设计容量,单位由上面的 Power Unit 给出 取值范围: 0x00000000-0x7FFF FFFF 0xFFFFFFFF 未知容量
Last Full Charge CapacityDWORD充满后的预期容量 取值范围: 0x00000000-0x7FFF FFFF 0xFFFFFFFF 未知容量
Battery TechnologyDWORD电池位置 0x0000 0000 主电池 0x0000 0001 第二块电池
Design VoltageDWORD设计电压, 取值范围 0x000000000 – 0x7FFFFFFF in [mV] 0xFFFFFFFF – 未知电压
Design Capacity of WarningDWORDOEM 设置的告警容量值 取值范围 0x000000000 – 0x7FFFFFFF in [mWh] or [mAh]
Design Capacity of LowDWORDOEM 设置的低容量值 取值范围 0x000000000 – 0x7FFFFFFF in [mWh] or [mAh]
Cycle CountDWORD充电循环次数 取值范围 0x000000000 – 0xFFFFFFFF
Measurement AccuracyDWORD电池容量测量准确度,以1/1000为单位,比如:80000表示80%
Max Sampling TimeDWORD_BST 中两次测量的最大间隔时间,比如,当前电池容量,放电速度或者剩余容量。以为毫秒单位。0xFFFFFFFF表示该位置无效。
Min Sampling TimeDWORD_BST 中两次测量的最小间隔时间。以为毫秒单位。0xFFFFFFFF表示该位置无效。
Max Averaging IntervalDWORD_BST 中两次测量的平均最大间隔时间。
Min Averaging IntervalDWORDBST 中两次测量的平均最小间隔时间。
Battery Capacity Granularity 1DWORD电池在告警容量值和低容量值之间的颗粒度
Battery Capacity Granularity 2DWORD电池在告警容量值和充满容量值之间的颗粒度
Model Number零结尾ASCII字符串OEM 定义的电池型号
Serial Number零结尾ASCII字符串OEM 定义的电池序列号
Battery Type零结尾ASCII字符串OEM 定义的电池类型
OEM Information零结尾ASCII字符串OEM 定义的在UI上展示的电池OEM信息
Battery Swapping CapabilityDWORD0x0 不可更换电池,例如,内部密封电池,用户无法接触到 0x1关机之后可更换电池 0x10 热插拔电池
上述根据ACPI Spec翻译,如果有错误欢迎指出,会进行订正

第二个是 _BST (Battery Status), 这个用于报告当前电池的状态信息。

偏移名称大小解释
Battery StateDWORDBit0 为1表示正在放电 Bit1  为1表示正在充电 Bit2  为1表示电池预警  
Battery Present RateDWORD电池充放电速度 取值范围 0x000000000 – 0x7FFFFFFF以[mW]或者[mA]为单位 0xFFFFFFFF – 未知速度    
Battery Remaining CapacityDWORD电池剩余容量 取值范围 0x000000000 – 0x7FFFFFFF以[mWh]或者[mAh]为单位 0xFFFFFFFF – 未知容量  
Battery Present VoltageDWORD电池电压 取值范围 0x000000000 – 0x7FFFFFFF以[mV]为单位 0xFFFFFFFF – 未知电压  

以本人的电脑(HP 840 G6)为例,设备管理器中可以看到电池:

使用 HE 直接读取 ACPI Table:

根据上面的整理出两个对应的Table, 放在 BAT0 设备中

Device (BAT0)
        {
            Name (_HID, EisaId ("PNP0C0A") /* Control Method Battery */)  // _HID: Hardware ID
            Name (_UID, One)  // _UID: Unique ID
            Method (_DSM, 4, Serialized)  // _DSM: Device-Specific Method
            {
                If (LEqual (Arg0, ToUUID ("4c2067e3-887d-475c-9720-4af1d3ed602e") /* Battery Thermal Limit */))
                {
                    Switch (ToInteger (Arg2))
                    {
                        Case (0x03)
                        {
                            Return (Package (0x01)
                            {
                                0x1E
                            })
                        }

                    }
                }
                Else
                {
                    Return (Package (0x01)
                    {
                        Zero
                    })
                }
            }

            Method (_STA, 0, NotSerialized)  // _STA: Status
            {
                Return (0x1F)
            }

            Method (_BIX, 0, NotSerialized)  // _BIX: Battery Information Extended
            {
                Return ( Package (0x15)
						{
							1, 
							1, 
							20000, 
							20000, 
							0, 
							4300, 
							2000, 
							1000, 
							10, 
							80000, 
							1000, 
							500, 
							750, 
							500, 
							0x64, 
							0x64, 
							"LABZBAT0", 
							"202410", 
							"MODOL1", 
							"LABZBAT0", 
							One
						})
            }

            Method (_BST, 0, NotSerialized)  // _BST: Battery Status
            {
                Return ( Package (0x04)	{
               			 1, 
               			 100, 
                		 10000, 
                		 4200
           			   })
            }

        }

     }

接下来选择使用 VirtualBox 虚拟机,根据【参考1】,替换内部的 ACPI Table, 最终效果如下:

就是说,我们成功的在这个虚拟机中安装了一块电量为 50% 的电池。

本文提到的修改后的 ACPI 源代码可以在这里下载:

参考:

1.https://www.lab-z.com/arcpi/

CH554 USB Host配合 ESP32-C3实现USB键盘转蓝牙

之前使用 Ch9350制作过一个 USB Host Shield 【参考1】,能够读取USB键盘鼠标的输入。最近在研究 Ch554 ,使用Ch554e制作了一个同样功能的Shield,配合ESP32-C3 能够实现USB 键盘转蓝牙的功能。

使用 Ch554 的有优点如下:

  1. 价格较低,相对于Ch9350 10元的价格,最便宜的 Ch554e 只要不到1.5元;
  2. 焊接友好,对于 TSSOP-20/SOP-16或者MSOP-10普通人都能够很好的进行焊接;
  3. 如果你的设计对于体积敏感,可以选择MSOP-10 封装的 Ch554e;
  4. 外围电路简单,只需要2个电容和1个电阻

缺点:

  1. 需要自己使用 keil 编写程序;
  2. 兼容性比不上 Ch9350,可能出现无法驱动的USB设备;

这次带来就是基于 Ch554e的设计。硬件部分设计如下:

下方就是CH554e的最小系统,外部配合2个0.1uf电容,以及1个10K电阻即可工作。下载方法是:上电之前短接 DL 位置,然后再上电使用WCHISPStudio即可。不过在研发阶段建议专门准备一个开发板便于操作。同时,官方的例子都是用第一个UART作为调试输出,而Ch554只有第2个 Uart可供使用。

根据上述电路设计的PCB如下:

这是一个底板,上面直接连接 DFRobot ESP32-C3即可。焊接后的板卡如下:

直接安装在 ESP32-C3上即可使用:

接下来开始代码的设计,首先设计的是 Ch554的代码,这里直接使用官方的代码进行简单修改。

为了便于使用我们使用和Ch9350相同的输出格式:

0-157 AB数据头,固定数值
0288表示有效帧值
03NN后续数据长度,从04开始到最后的校验和
0410  固定值 [7:6]:00 - 保留 [5:4]:01 - 鼠标 [3]  :0  - 保留 [2:1]:00 - 未知 [0]  :0  - 端口1
05-AA BB CC…..MM键盘数据,例如:08 00 00 00 00 00 00 00
XXNum帧序列号
XXCheckSum校验和,从05开始的数据和

例如:实际发送的一个数据:

57 AB 88 0B 10 08 00 00 00 00 00 00 00 00 08

代码是基于WCH 官方修改而来的,基本原理是:比较每一次收到的数据(RxBuffer)是否和上一次(LastBuffer)相同,如果不同,那么进行上报。使用上面介绍的数据报文格式:

                        IsSame=TRUE;
                        for ( i = 0; i < len; i ++ ){
                            if (LastBuffer[i]!=RxBuffer[i]) {
                                IsSame=FALSE;
                                LastBuffer[i]=RxBuffer[i];
                            }
                        }
                        //只有与前一次不同才进行输出
                        if (IsSame==FALSE) {
                            checksum=0x00;
                            CH554UART1SendByte(0x57);CH554UART1SendByte(0xAB);CH554UART1SendByte(0x88);CH554UART1SendByte(len+3);CH554UART1SendByte(0x10);
                            for ( i = 0; i < len; i ++ ){
                                CH554UART1SendByte(RxBuffer[i]);
                                checksum=checksum+RxBuffer[i];
                            }
                            checksum=checksum+counter;
                            CH554UART1SendByte(counter); CH554UART1SendByte(checksum);
                            counter++;
                        }

代码使用 Keil4 编译通过。

ESP32-C3代码如下:

#include <Arduino.h>
#include <BleKeyboard.h>

BleKeyboard bleKeyboard;

#define DEBUGMODE 0

void setup() {
  Serial.begin(115200);
  Serial1.begin(115200, SERIAL_8N1, RX, TX);

  bleKeyboard.begin();

}

void loop() {
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '1') {
      Serial.println("get1");
    }
    if (c == '3') {
      ESP.restart();
    }
  }

  //根据 CH9350 Spec 每次最多输出 72Bytes
  byte Data[72];
  unsigned int CounterLast = Serial1.available();
  unsigned int CounterCurrent = 0;


  // 如果当前串口有数据
  if (CounterLast != 0) {
    // 进行简单测试,如果当前还在传输数据那么持续接收
    while (CounterCurrent != CounterLast) {
      CounterLast = Serial1.available();
      delayMicroseconds(500);
      CounterCurrent = Serial1.available();
    }
  }

  if (CounterCurrent > 0) {
    // 一次性将数据收取下来
    Serial1.readBytes(Data, CounterCurrent);
    unsigned int i = 0;
    unsigned int Length;
    while (i < CounterCurrent) {
      // 识别帧头
      if ((Data[i] == 0x57) && (Data[i + 1] == 0xAB)) {
        // 有效键值帧
        if (Data[i + 2] == 0x88) {
          // 获得数据长度
          Length = Data[i + 3];
          if (DEBUGMODE) {
            //Serial.print("Ln:");Serial.print(Length);
            for (int j = 1; j < Length + 1; j++) {
              if (Data[i + 3  + j] < 16) {
                Serial.print("0");
              }
              Serial.print(Data[i + 3  + j], HEX);
              Serial.print(" ");
            }
            Serial.println(" ");
          }

          //如果是键盘
          if (Data[i + 4] == 0x10) {
            if (DEBUGMODE) {
              Serial.print("Key");
              for (int j = 1; j < Length + 1; j++) {
                Serial.print(Data[i + 3  + j], HEX);
                Serial.print(" ");
              }
              Serial.println(" ");
            }
            
            //判断为Dostyle键盘
            if (Data[i + 3  + 1] == 0x10) { 
              if (bleKeyboard.isConnected() == true) {
                bleKeyboard.sendReport((KeyReport*)(&Data[i + 3  + 2]));
              }
            }
          }
          i = i + 3 + Length;
        } else if (Data[i + 2] == 0x82) {
          i = i + 3; // 跳过
        }
      }
      i++;
    } // while (i < Counter)
  }

}

使用时,只需要给ESP32-C3供电,连接好USB键盘后就可以搜索蓝牙键盘进行连接使用了。工作的测试视频在:

https://www.bilibili.com/video/BV1zi421a7NM/?vd_source=cf6121716e06cb669a27c10276f9c920

 Ch554 的代码:

ESP32 C3 代码:

参考:

1. https://mc.dfrobot.com.cn/thread-316678-1-1.html

又一次研究JY901心得

又一次尝试使用 Jy901 模块,没有成功应用,但是通过实验有一些心得记录如下。

1.模块默认使用输出 9600Hz 波特率通讯,10Hz回报;

2.恢复模块默认配置的方法有两种,一种是短接,另外一种是串口命令

3.一些串口配置的方法:

a.	ff aa 03 03 00  设置回传速率为1Hz
b.	ff aa 02 08 00  设置只输出0x53包(手册上提到JY901无法输出四元数)
c.	ff aa 00 00 00 保存当前设置(比如,进行了上述设定之后,需要保存之后下一次上电才能继续使用)

4.使用的轴如下图所示(不要看模块PCB上的标注,是错的)

5.输出范围:X轴±180;Y轴±90;Z轴±180

6.DataSheet上描述的角度输出如下:

其中的计算方法有问题,按照它的方法不会有正负的区别:

例如: 55 53 A0 A1 B0 B1 C0 C1 T0 T1 SUM

其中的 0xA1A0 输出范围是 0000-7FFF ,对应着 -180~+180;0xB1B0 输出范围是 0000-7FFF ,对应着 -90~+90;0xC1C0 输出范围是 0000-7FFF ,对应着 -180~+180.

因此实际可以选择如下处理方法:

  fX = JY901.stcAngle.Angle[0] - 0x4000;
  fX = fX * 180.0 / 0x4000;

  fY = JY901.stcAngle.Angle[1] - 0x4000;
  fY = fY * 90.0 / 0x4000;

  fZ = JY901.stcAngle.Angle[2] - 0x4000;
  fZ = fZ * 180.0 / 0x4000;

符号和方向满足右手原则:拇指指向轴方向,然后四个手指方向是正,相反是负。

参考:

1. https://wenku.baidu.com/view/13665ba8b307e87100f69630.html?ind=1&fr=wenchuang&_wkts_=1719042668522&bdQuery=jy901+%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F