2024年2月更新,Step to UEFI 文章索引:
MAX98357 I2S功放模块测试
之前购买了一个MAX98357 I2S功放模块,这次编写简单的代码进行测试。
硬件连接如下:
MAX98357 | ESP32S3 | 用途 |
SPK+/- 连接喇叭 | 连接喇叭正负极,喇叭输出 | |
DIN | 48 | 从 ESP32S3 发送的 I2S数据 |
BCLK | 45 | 从 ESP32S3 发送的 I2S Clock |
LRC | 35 | 从 ESP32S3 发送的 I2S 左右声道选择信号 |
GND | GND | 地 |
VCC | 5V | 供电 |
按照上述方案连接好后,烧录如下代码:
#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”这个字符串,但是刚好碰到这个字符串在内存中分布如下:
0xFFFFA | 0xFFFFB | 0xFFFFC | 0xFFFFD | 0xFFFFE | 0xFFFFF | 0x10 0000 | 0x10 0001 | 0x10 0002 | 0x10 0003 |
L | A | B | - | Z | . | C | O | M | 0 |
当直接使用 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 压缩文件)
对应的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.模拟键盘的代码(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)已经废止 ),其中给出了电池的信息。
偏移名称 | 大小 | 解释 |
Revision | DWORD | 目前版本号为 1 |
Power Unit | DWORD | 电池容量单位: 0 – [mWh], 同时充放电速度将会以[mW]为单位 1 – [mAh], 同时充放电速度将会以[mA]为单位 |
Design Capacity | DWORD | 设计容量,单位由上面的 Power Unit 给出 取值范围: 0x00000000-0x7FFF FFFF 0xFFFFFFFF 未知容量 |
Last Full Charge Capacity | DWORD | 充满后的预期容量 取值范围: 0x00000000-0x7FFF FFFF 0xFFFFFFFF 未知容量 |
Battery Technology | DWORD | 电池位置 0x0000 0000 主电池 0x0000 0001 第二块电池 |
Design Voltage | DWORD | 设计电压, 取值范围 0x000000000 – 0x7FFFFFFF in [mV] 0xFFFFFFFF – 未知电压 |
Design Capacity of Warning | DWORD | OEM 设置的告警容量值 取值范围 0x000000000 – 0x7FFFFFFF in [mWh] or [mAh] |
Design Capacity of Low | DWORD | OEM 设置的低容量值 取值范围 0x000000000 – 0x7FFFFFFF in [mWh] or [mAh] |
Cycle Count | DWORD | 充电循环次数 取值范围 0x000000000 – 0xFFFFFFFF |
Measurement Accuracy | DWORD | 电池容量测量准确度,以1/1000为单位,比如:80000表示80% |
Max Sampling Time | DWORD | _BST 中两次测量的最大间隔时间,比如,当前电池容量,放电速度或者剩余容量。以为毫秒单位。0xFFFFFFFF表示该位置无效。 |
Min Sampling Time | DWORD | _BST 中两次测量的最小间隔时间。以为毫秒单位。0xFFFFFFFF表示该位置无效。 |
Max Averaging Interval | DWORD | _BST 中两次测量的平均最大间隔时间。 |
Min Averaging Interval | DWORD | BST 中两次测量的平均最小间隔时间。 |
Battery Capacity Granularity 1 | DWORD | 电池在告警容量值和低容量值之间的颗粒度 |
Battery Capacity Granularity 2 | DWORD | 电池在告警容量值和充满容量值之间的颗粒度 |
Model Number | 零结尾ASCII字符串 | OEM 定义的电池型号 |
Serial Number | 零结尾ASCII字符串 | OEM 定义的电池序列号 |
Battery Type | 零结尾ASCII字符串 | OEM 定义的电池类型 |
OEM Information | 零结尾ASCII字符串 | OEM 定义的在UI上展示的电池OEM信息 |
Battery Swapping Capability | DWORD | 0x0 不可更换电池,例如,内部密封电池,用户无法接触到 0x1关机之后可更换电池 0x10 热插拔电池 |
第二个是 _BST (Battery Status), 这个用于报告当前电池的状态信息。
偏移名称 | 大小 | 解释 |
Battery State | DWORD | Bit0 为1表示正在放电 Bit1 为1表示正在充电 Bit2 为1表示电池预警 |
Battery Present Rate | DWORD | 电池充放电速度 取值范围 0x000000000 – 0x7FFFFFFF以[mW]或者[mA]为单位 0xFFFFFFFF – 未知速度 |
Battery Remaining Capacity | DWORD | 电池剩余容量 取值范围 0x000000000 – 0x7FFFFFFF以[mWh]或者[mAh]为单位 0xFFFFFFFF – 未知容量 |
Battery Present Voltage | DWORD | 电池电压 取值范围 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 源代码可以在这里下载:
参考:
CH554 USB Host配合 ESP32-C3实现USB键盘转蓝牙
之前使用 Ch9350制作过一个 USB Host Shield 【参考1】,能够读取USB键盘鼠标的输入。最近在研究 Ch554 ,使用Ch554e制作了一个同样功能的Shield,配合ESP32-C3 能够实现USB 键盘转蓝牙的功能。
使用 Ch554 的有优点如下:
- 价格较低,相对于Ch9350 10元的价格,最便宜的 Ch554e 只要不到1.5元;
- 焊接友好,对于 TSSOP-20/SOP-16或者MSOP-10普通人都能够很好的进行焊接;
- 如果你的设计对于体积敏感,可以选择MSOP-10 封装的 Ch554e;
- 外围电路简单,只需要2个电容和1个电阻
缺点:
- 需要自己使用 keil 编写程序;
- 兼容性比不上 Ch9350,可能出现无法驱动的USB设备;
这次带来就是基于 Ch554e的设计。硬件部分设计如下:
下方就是CH554e的最小系统,外部配合2个0.1uf电容,以及1个10K电阻即可工作。下载方法是:上电之前短接 DL 位置,然后再上电使用WCHISPStudio即可。不过在研发阶段建议专门准备一个开发板便于操作。同时,官方的例子都是用第一个UART作为调试输出,而Ch554只有第2个 Uart可供使用。
根据上述电路设计的PCB如下:
这是一个底板,上面直接连接 DFRobot ESP32-C3即可。焊接后的板卡如下:
直接安装在 ESP32-C3上即可使用:
接下来开始代码的设计,首先设计的是 Ch554的代码,这里直接使用官方的代码进行简单修改。
为了便于使用我们使用和Ch9350相同的输出格式:
0-1 | 57 AB | 数据头,固定数值 |
02 | 88 | 表示有效帧值 |
03 | NN | 后续数据长度,从04开始到最后的校验和 |
04 | 10 | 固定值 [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 |
XX | Num | 帧序列号 |
XX | CheckSum | 校验和,从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 代码:
参考:
又一次研究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;
符号和方向满足右手原则:拇指指向轴方向,然后四个手指方向是正,相反是负。
参考: