做一个 USB 提醒器

对于现在的电脑来说,USB是外部通讯的不二选择,通过这个接口可以引出键盘鼠标等等外部设备。这次尝试使用国产的 CH55X 系列单片机来做一个 USB 提醒器,通过这个设备能够做到LED 发光提示还有蜂鸣器声音提示。

首先介绍一下 CH55X 系列芯片是南京沁恒(WCH)出品的带有USB功能的系列单片机【参考1】,提起这家公司的名称大多数人会感到陌生,但是如果提起CH340/CH341芯片大家都会非常熟悉,这款低成本的USB串口转换芯片就是这家公司的产品。这次设计用到的 CH552 和 CH551/CH554 是同一个系列。他们主要的差别是:CH551 是最低端的,工作频率是 24Mhz,FLASH 有 10K (可以看作是单片机的 ROM),内存是 512+256字节,只能做 USB Device;CH552比前者配置稍微高了一些,FLASH空间有16K,内存有1K+256字节,只能做 USB Device;CH554比前面都高级,FLASH 和 内存和 CH552 相同,但是可以当作 USB Host 使用能够实现解析USB 键盘鼠标这样设备的工作。可以看出,相比 Atmel 的 32U4 (Arduino Leonardo),主要缺点是内存太小。但是胜在价格便宜,引脚简单(这样容易焊接)。在立创商城上面的芯片价格如下(CH552/4 不同封装引脚数量不同,所以价格是在一个范围内)

CH551G   2.475元

CH552      2.7-2.78元

Ch554      5.43-7.03元

32u4        23.51元

因此,如果CH55X 系列芯片能够满足你的需求,能够大大降低成品的成本。同时, CH55X 系列单片机对于外围元件要求极低,无须晶振,只要合适的贴片电容即可工作起来。

这次设计使用CH552G芯片是 SOP16 封装,同样的 CH554 也有SOP16封装可以直接替换(CO-Layout)

电路设计如下:

主控部分

这个芯片使用C1/C2 两个 0.1uF 的电容即可工作,5V 供电,从 VCC/VDD 引脚进入。上图中 V33 经过 R1 (10K)连接到 USB+引脚上,其中的 PAD1无须上键在这里作为开关使用用于控制芯片进入 Bootloader Mode。

板子上还有一个 WS2812B MINI(3.5×3.5mm) 用于提供灯光提示,同样的这个元件无须外围配合,直接用CH55X P1.5即可控制:

外围有一个蜂鸣器,因为CH55X 引脚提供电流有限,所以使用S8050这个三极管扩流,同时外加R3用于限流,主要是避免声音太大。CH55X  P3.4 Pin 用于控制蜂鸣器开关。因为该引脚是 PWM 引脚,因此这里即可以使用有源蜂鸣器和无源蜂鸣器。有源蜂鸣器的含义是“内部有震荡源”,类似于电动机,有了供电就能发声;与其相反,无源蜂鸣器含义是“内部没有震荡源”,因此需要用引脚电流变换来驱动它。对于有源蜂鸣器,P3.4 当作普通的GPIO 即可;对于无源蜂鸣器,需要设定为 PWM 然后给他设置占空比和频率(默认即可)。

PCB 设计如下:

切换为 3D 模式预览如下(这次设计的 Symbol使用的都是嘉立创的,所以自带了 3D 模型,非常直观):

正面

背面

拿到手的 PCB和焊接后的照片如下:

接下来就开始软件设计了。这个芯片原本设计使用 C51 来进行变成,用户可以通过 Keil 这样的集成开发环境进行编程,但是对于我们来说C51还是过于复杂。这里使用 Github上的一个开源项目:ch55xduino【参考2】,能够让我们像开发 Arduino 一样为 CH551/2/4 进行开发。

首先介绍一下项目的安装:和其他的所有的第三方板卡一样, 在 Preferences –> Additional Boards Manager URLs 中加入这个板卡的地址https://raw.githubusercontent.com/DeqingSun/ch55xduino/ch55xduino/package_ch55xduino_mcs51_index.json

之后打开 Boards Manager

搜索这个项目

Install 安装之后在 Boards Manager 中能够看到这个板卡,编译时,CH551使用 CH551 Board, CH552/4 使用 CH552 Board

接下来就是如何编译一个程序,可以在 Example 中看到专用的例子,比如下面就是关于 USB 设备的例子:

打开一个例子尝试编译,完成后就需要做上传的准备工作了。刚拿到手 ,板子上没有Bootloader,需要短接板子背面 PAD 处(电路图中是0.1uF电容,实际上不上件预留为空),这样V33会通过10K 电阻后进入 D+ Pin,板子会进入 Bootloader Mode。

属性如下:

使用 Zadig 给他安装一个驱动,勾选 Edit

重命名为 USB Module

安装之后:

编译后会自动搜索名为 “USB Module”的设备并且上传

需要注意的是:如果你的代码没有实现 USB 串口,那么只能短接PAD 让板子进入 BootLoader Mode再次上传。

了解了上面的知识,即可着手设计代码实现提醒器的功能。我们设计了2种提醒方式:LED 和蜂鸣器,分别通过2个命令进行设置,形式如下:

  1. [cRGB]  R/G/B 分别是红绿蓝的取值,例如,命令 [c123] (ASCII)  5B 63 31 32 33 5D (十六进制)会将 LED RGB 分别设置为 0x31 0x32 0x33;命令 5B 63 FF 00 00 5D (十六进制)会设置为全红;
  2. [bThighTlow]  Vhigh和 Vlow 分别是设置的时间高位和地位,例如, 命令 [b12] (ASCII)  5B 62 31 32 5D (十六进制)将会设置蜂鸣器在 0x32 * 0x10 +0x32=0x342 (834秒)后响起来;命令5B 62 01 00 5D (十六进制)将会设置蜂鸣器在 0x01 * 0x100 +0x00=0x100 (256秒)后响起来;可以看出,可以设置最大 0xFFFF (65535秒)后响起来。

完整代码如下:

#define BEEPERPIN 34
#define LEDPIN 15
        
#define TX(LedColor) {\
                  if (((LedColor)&0x80)==0) {\
                       XdigitalWriteFast(1,5,HIGH);\
                       (LedColor)=(LedColor)<<1;\
                       XdigitalWriteFast(1,5,LOW);\
                       XdigitalWriteFast(1,5,LOW);\
                       XdigitalWriteFast(1,5,LOW);\
                   }else{\
                       XdigitalWriteFast(1,5,HIGH);\
                       XdigitalWriteFast(1,5,HIGH);\
                       XdigitalWriteFast(1,5,HIGH);\
                       XdigitalWriteFast(1,5,HIGH);\
                       XdigitalWriteFast(1,5,HIGH);\
                       XdigitalWriteFast(1,5,HIGH);\
                       XdigitalWriteFast(1,5,HIGH);\                       
                       (LedColor)=(LedColor)<<1;\
                       XdigitalWriteFast(1,5,LOW);\
                       XdigitalWriteFast(1,5,LOW);\
                       XdigitalWriteFast(1,5,LOW);\
                       XdigitalWriteFast(1,5,LOW);\
                       XdigitalWriteFast(1,5,LOW);\
                       }\
                  }

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

#include "src/userUsbCdc/USBCDC.h"

void setup() {
  // 设置蜂鸣器Pin 为Low (停止发声)
  pinMode(BEEPERPIN,OUTPUT);
  digitalWrite(BEEPERPIN,LOW);
  // 设置LED 控制Pin
  pinMode(LEDPIN,OUTPUT);
  digitalWrite(LEDPIN,LOW);  
  USBInit();
}

byte Status=0;
byte RValue,GValue,BValue;
byte THigh=0,TLow=0;
unsigned long CounterDown=0xFFFFFFFF;

void loop() {
  while (USBSerial_available()) {
    // 接收来自USB串口的字符
    char serialChar = USBSerial_read();
  if ((serialChar == '[')&&(Status==0)) { Status=1; continue;}
      if ((serialChar == 'c')&&(Status==1)) {Status=2; continue;}
      if (Status==2) {RValue=serialChar; Status=3; continue;}
      if (Status==3) {GValue=serialChar; Status=4; continue;}
      if (Status==4) {BValue=serialChar; Status=5; continue;}
      if (Status==5) {
          if (serialChar == ']') {Status=6;continue;}
          else {Status=0; continue;}
      }
      if ((serialChar == 'b')&&(Status==1)) {Status=7; continue;}
      if (Status==7) {THigh=serialChar; Status=8; continue;}
      if (Status==8) {TLow=serialChar; Status=9; continue;}
      if (Status==9) {
          if (serialChar == ']') {Status=10; continue;}
          else {
                // 如果最后收到的字符不是 [ 那么需要重新开始
                Status=0; continue;
          }
      }
  } 

  // 根据收到的 cRGB 设置 LED 颜色
  if (Status==6) {

    USBSerial_println_s("RGB");
    USBSerial_println_i(RValue);
    USBSerial_println_i(GValue);
    USBSerial_println_i(BValue);
    USBSerial_flush();

    // 设置 WS2812 颜色, 特别注意这里和时许非常相关,具体数值都是示波器测量取得
    //Send Green value from Bit7 to 0
    TX(GValue);TX(GValue);TX(GValue);TX(GValue);TX(GValue);TX(GValue);TX(GValue);TX(GValue);
    //Send Red value from Bit7 to 0
    TX(RValue);TX(RValue);TX(RValue);TX(RValue);TX(RValue);TX(RValue);TX(RValue);TX(RValue);
    //Send Blue value from Bit7 to 0
    TX(BValue);TX(BValue);TX(BValue);TX(BValue);TX(BValue);TX(BValue);TX(BValue);TX(BValue);  
    
   // 重新设置为状态 0
    Status=0;
  }
  
  // 根据设置的延时开始倒计时
  if (Status==10) {
    if ((THigh==0)&&(TLow==0)) {
        digitalWrite(BEEPERPIN,LOW);
      }
        // 设置触发时间
    else CounterDown=(THigh*256+TLow)*1000UL+millis();
    Status=0;
  }

   // 如果当前处于状态 0 并且设置的触发时间小于当前时间,那么就开始响
   if ((Status==0)&&(CounterDown<millis())) {
      // 对应 Pin 拉高,蜂鸣器开始发声
      digitalWrite(BEEPERPIN,HIGH);
      // 拉高之后会一直发声
      CounterDown=0xFFFFFFFF;
   }
}

特别提一句:WS2812 有很多版本,彼此之间在时序上有差别。比如,下面两种从Datasheet看就是有差别的,前者的代码(CH55xDuino)在后者(这次设计使用的)上会有问题:

WS2812D-F8WS2812B-Mini
T0H400±150ns300±80ns
T0L850±150ns790±210ns
T1H850±150ns790±210ns
T1L400±150ns300±80ns
RES>50us>280us

所以,我在代码上重写了 WS2812 相关部分,因为对时序要求很高,所以最好使用汇编语言,但是因为我对 C51汇编很陌生,于是只能写成宏的方式。有兴趣的朋友可以尝试修改CH55xduino的库文件。

BOM 如下,不包括PCB 在10元左右,可以看到这个芯片在制作 USB 相关设备方面很有竞争力:

虽然CH55X系列芯片有诸多好处,但是还存在如下缺点:

  1. 进入 BootLoader后,在 USB 3.0 的 USB口上经常会出现 Yellow Bang 的情况。可以尝试 Disable再Enable 看看能否消除之。如果始终不行,推荐将板子使用一个 USB2.0 HUB和 USB 3.0 相连,这个应该时芯片本身的 Bug(作为 USB 普通设备,不会遇到这个问题);如果一直存在这个问题,那么可以先卸载,然后再重新安装驱动试试;
  2. 淘宝有很多CH552 开发板,强烈建议在动手实验之前入手一个作为开发板,上面会有按钮,按下后插入USB端口直接进入 BootLoader模式,方便调试;
  3. 淘宝购买后,拿到手请及时检查芯片,我在Taobao 购买的 CH554 开发板拿到手几个月后再用时发现上面竟然是 CH552的芯片,因为他们封装相同,所以当时没有注意,再去找售后时发现店铺竟然已经关门。

参考:

  1. http://www.wch.cn/products/category/5.html 单片机系列芯片选项指南
  2. https://github.com/DeqingSun/ch55xduino
  3. https://atta.szlcsc.com/upload/public/pdf/source/20190620/C114583_ECCAEB884D1CBA01F5696FFC90F90D84.pdf WS2812B-Mini DataSheet
  4. https://item.szlcsc.com/150447.html WS2812D-F8 DataSheet\

========================================

因为 GitHub 在访问上可能存在的问题,所以我在这里放一个 json 文件,就是说你可以在 Preferences –> Additional Boards Manager URLs 中使用下面这个地址:

http://www.lab-z.com/wp-content/uploads/2021/05/package_ch55xduino_mcs51_index.json

Ampere Computing 职位

Req IdTitleLocationLink for Job Description
NS532Platform Applications Engineer, Senior StaffShanghaihttps://jobs.jobvite.com/amperecomputing/job/o9s6cfwD
NS627Platform Applications Engineer (Software), PrincipalShanghai / Shenzhenhttps://jobs.jobvite.com/amperecomputing/job/oVPbefwT
NS660Platform Applications Engineer (Software), StaffShanghaihttps://jobs.jobvite.com/amperecomputing/job/oKFvefwS
NS703Platform Applications Engineer (Hardware) StaffShanghaihttps://jobs.jobvite.com/amperecomputing/job/othIefwq
NS771Senior RecruiterShanghaihttps://jobs.jobvite.com/amperecomputing/job/o5g5efwo
NS764Field Marketing ManagerShanghaihttps://jobs.jobvite.com/amperecomputing/job/oCO2efwq
NS775Sales Operations, Senior ManagerShanghaihttps://jobs.jobvite.com/amperecomputing/job/oPt6efwm
NS687Field Applications Engineer, StaffShanghai / Beijinghttps://jobs.jobvite.com/amperecomputing/job/oTVDefwp
NS631Platform Applications Engineer (Software) StaffBeijinghttps://jobs.jobvite.com/amperecomputing/job/oWheefwp
NS650Platform Applications Engineer, Senior StaffTaipeihttps://jobs.jobvite.com/amperecomputing/job/oD3sefw6
NS642Staff ATE Hardware EngineerTaiwan – Virtualhttps://jobs.jobvite.com/amperecomputing/job/oVXqefwg
NS724Supplier Quality Engineer, StaffTaiwan – Virtualhttps://jobs.jobvite.com/amperecomputing/job/oolNefwu
NS725Document Controller / Technical Writer, StaffTaiwan – Virtualhttps://jobs.jobvite.com/amperecomputing/job/oplNefwv
NS726Customer Quality Engineer, StaffTaiwan – Virtualhttps://jobs.jobvite.com/amperecomputing/job/oqlNefww
NS750System Level Test (SLT) Engineer, StaffTaiwan – Virtualhttps://jobs.jobvite.com/amperecomputing/job/o6uXefwv

有兴趣的朋友可以直接联系 jun.chen@amperecomputing.com

P-State 和 C-State

Processor power management technologies are defined in the ACPI specification and are divided into two categories or states:【参考1】

  1. Power performance states (ACPI P states)P-states provide a way to scale the frequency and voltage at which the processor runs so as to reduce the power consumption of the CPU. The number of available P-states can be different for each model of CPU, even those from the same family.
  2. Processor idle sleep states (ACPI C states)C-states are states when the CPU has reduced or turned off selected functions. Different processors support different numbers of C-states in which various parts of the CPU are turned off. To better understand the C-states that are supported and exposed, contact the CPU vendor. Generally, higher C-states turn off more parts of the CPU, which significantly reduce power consumption. Processors may have deeper C-states that are not exposed to the operating system.

简单的说, P-states 是 CPU 醒着的时候降低功耗(好比上班摸鱼,玩玩手机放松一下,但是随时都是醒着的),C-states 就是CPU 睡着了。

从下面的示意图可以看的更清晰

System states【参考2】
【参考3】
睡得越深,需要关闭的东西越多,也更加省电,相反,需要醒来的时间会越长【参考3】
进入 C-States 的一些动作【参考3】

更多推荐的资料:

https://www.cnblogs.com/apnpc/p/13780146.html 【CPU】 C-State, C-模式 是什么?

https://zhuanlan.zhihu.com/p/25675639 CPU省电的秘密(二):CStates

参考:

1.https://docs.microsoft.com/en-us/previous-versions/windows/desktop/xperf/p-states-and-c-states

2.https://lenovopress.com/lp0632.pdf

3.https://www.thomas-krenn.com/en/wiki/Processor_P-states_and_C-states

=====================================================

2025年5月

玩过 Arduino 的朋友可能会注意到,再编译的时候可以设定当前单片机的电压和频率。比如,3.3V供电的时候只能使用 8Mhz 的主频,但是如果 5V 供电时可以使用 16Mhz的频率。如果在 3.3V 时强制使用 16Mhz 的主频,那么很可能会遇到不稳定等等奇怪的问题。

对于 CPU 来说同样如此,较低的频率只能使用较低的频率。因此,可以制作一张表格,标注不同电压对应的频率,当需要省电时,切换到较低电压后可以切换到较低的频率。这就是 P-State 省电的原理。

FireBeetle 直接放声(DAC篇)

声音,是物体振动产生的,通常情况下通过空气将震动传递到耳朵我们就能听到了声音。常见的喇叭就是一种将电能转换为机械能的器件。

FireBeetle 核心是 ESP32-WROOM-32D, 主控频率高达240Mhz, Flash Rom 有 16MB。这里介绍一种直接通过它来播放声音的方法。用这种方法可以直接通过喇叭来播放音乐,但是因为 ESP32 输出功率有限,直接推动喇叭产生的声音很小。为此,这里使用“Gravity: 带功放喇叭模块”来实现更大的声音输出。

Gravity: 带功放喇叭模块

基本原理是将数据存储在 FireBeetle 的Flash 上,然后通过 ESP32 的DAC 直接输出之。

具体步骤如下:

第一步,将音乐转化为8位单声道。我是用 Golden Wave ,将罗大佑的 “恋曲1990.mp3“转为 8000Hz 8 Bits的Wav 格式。

Golden Wave 打开音频文件

选择 Resample
使用 8000Hz 重新采样

注意,这里必须重新采样为 8000Hz。直接另存为的话实际上并没有设置为 8000Hz。

保存为8Bits 单声道

GoldenWave 自动重新打开文件

重新加载处理后的 8000Hzm 8Bits Wave 音频数据

第二步,使用Bin2C 将这个 Wav 生成C 语言的头文件。命令如下:

bin2c.exe -o audio.h 19908bits.wav

实际上数据中包含了 wav的文件头,但是因为数据量不大,对播放几乎没有影响,所以这里也没有额外处理。

这样,我们就有了这个歌曲的数据。特别需要注意的有下面两点:

1.需要设置编译模式为  3MB APP/9MB FATFS 模式

2.原始的 Wav 最好不要超过 2.7MB(2,831,155.2Bytes, 0x2B3333Bytes),否则会超过最大程序的限制。这次我使用的 WAV 是 2,527,766 字节的,编译后结果如下:

Sketch uses 2746845 bytes (87%) of program storage space. Maximum is 3145728 bytes.

Global variables use 15372 bytes (4%) of dynamic memory, leaving 312308 bytes for local variables. Maximum is 327680 bytes.

第三步,我们需要一些基本的测试。首先,编写一个循环,使用 dacwrite() 输出数值,输出2000000次花费了11102ms ,也就是说一次 dac 输出需要花费 0.005551ms。前面提到使用的 Wav采样率是 8000Hz。这样一个周期是 1/8000=0.125ms。 因此,为了重建一个声音,我们需要 0.125/0.005551=22.5184次 dacwrite。

第四步,编写代码:

#include "audio\SoundData.h"
void setup() {
}
void loop() {
  for (unsigned int i=0;i<2527766;i++) {
    for (int j=0;j<21;j++) dacWrite(25,WarOfWorldsWav[i]);
  }
}

我们将音频数据放在audio目录下的SoundData.h文件中,这样每次Arduino 打开ino 文件的时候并不会一起打开音频数据,否则因为音频数据很大,会耽误很多时间,有时候甚至会导致Arduino 编译器崩溃。另外,和前面提到的22次 dacwrite 不同,代码使用的是21次,因为for循环有一些开销,所以实际测量下来22次有一点点慢。

第五步,Gravity: 带功放喇叭模块,上面有3个线,VCC和GND 连接到 FireBeetle上,信号输入Pin 连接到 FireBeetle D2 (IO25)。

下载代码后即可播放出音乐了。

很明显,上述方法足够简单,能够存储 2.7*1024*1024/8000=354秒的音频。在要求不高的场合下完全能够满足要求。

可以直接连接喇叭,只是声音小一些

8Bits Wave 格式的歌曲:

完整的代码和数据:

工作的视频:

https://www.bilibili.com/video/BV1Df4y1x7WF?share_source=copy_web

FireBeetle 直接放音(PWM篇)

前面介绍了FireBeetle 通过 DAC 来播放音频,除此之外,还可以使用 PWM 方式来播放音频。

关于 PWM动力老男孩在“Arduino系列教程之 – PWM的秘密(上)”【参考1】有介绍,对于我们来说,能用到的就是下面这一段:

PWM是用占空比不同的方波,来模拟“模拟输出”的一种方式。靠,这个太拗口了,简而言之就是电脑只会输出0和1,那么想输出0.5怎么办呢?于是输出01010101….,平均之后的效果就是0.5了。早这么说就了然了嘛。

比如,当前最高电压是5V,如果输出50%的PWM信号,可以当作 2.5V 的信号输出。对于 ESP32来说,有对 PWM 的直接支持【参考2】。

Arduino core for the ESP32 并没有一般 Arduino 中用来输出 PWM analogWrite(pin, value) 方法,取而代之的 ESP32 有一个 LEDC ,设计是用来控制 LED

ESP32 的 LEDC 总共有16个路通道(0 ~ 15),分为高低速两组,高速通道(0 ~ 7)由80MHz时钟驱动,低速通道(8 ~ 15)由 1MHz 时钟驱动。

对于我们来说,用到的函数有下面3个:

ledcSetup(uint8_t channel, double freq, uint8_t resolution_bits)

分别设定使用的通道(Channel),PWM 的频率, PWM 的分辨率。比如,我们设定 1Hz 的频率,然后分辨率为4Bit,那么就可以设置从 0 到15,一共16个PWM值。可以看出,分辨率越高,可以细分出更多的 PWM值。

ledcWrite(uint8_t channel, uint32_t duty)

对通道设定当前的占空比(duty)

ledcAttachPin(uint8_t pin, uint8_t channel)

将 LEDC 通道绑定到指定 IO 口上

这样,我们就得到了一个和之前 DAC 很像的代码:

#include "audio\SoundData.h"

int freq = 8000*256;    // 频率
int channel = 0;    // 通道
int resolution = 8;   // 分辨率

const int led = 25;
void setup() {
  ledcSetup(channel, freq, resolution); // 设置通道
  ledcAttachPin(led, channel);  // 将通道与对应的引脚连接‘
  Serial.begin(115200);
}

void loop() {
  for (unsigned int i=0;i<2527766;i++) {
     ledcWrite(channel, WarOfWorldsWav[i]);
     delayMicroseconds(120);
  }
}

完整的代码和数据下载:

前面提到了,PWM 支持更高的分辨率,因此,我们可以尝试播放16Bits 的音频。最简单的想法,直接将频率设定为 8000Hz,然后分辨率为16位。但是实际测试下来这样无法工作,经过研究,频率和分辨率之间有一定的限制关系【参考3】。在 8000Hz 下能够达到最高分辨率是 13bits。最终实验表明使用 12Bits 分辨率 8000Hz 可以接收,再高噪音会较大。此外,16bits 的 WAV 和 8Bits 的还有一个很大的区别在于:前者是有符号数值,后者是无符号数值。比如:0x8001 实际上表示的是 -1。因此代码中取出数值后需要加上 0x8000 再做处理。因此代码中取出数值后需要加上 0x8000 再做处理。另外,因为 16Bits 相对于 8Bits 数据量是直接翻倍了,导致无法在 Flash 中放下全部文件,为此,这次实验用到的16Bits音频数据只是截取了部分歌曲。

参考:

The maximum PWM frequency with the currently used ledc duty resolution of 10 bits in PWM module is 78.125KHz.
The duty resolution can be lowered down to 1 bit in which case the maximum frequency is 40 MHz, but only the duty of 50% is available.
For duty resolution of 8 buts, the maximal frequency is 312.5 kHz.
The available duty levels are (2^bit_num)-1, where bit_num can be 1-15.
The maximal frequency is 80000000 / 2^bit_num
In my MicroPython implementation, I'm currently working on enabling user selectable and/or automatic duty resolution and higher maxumum frequencies.

4.手册上有描述

Magnetic Core Memory

在上古的电脑时代,计算机使用 Magnetic Core Memory 来作为 RAM 存储设备。

中文名称是“磁芯存储器”。简单的说,这种磁芯有着不同的磁化方向。用这种方式可以记录0 和 1 两种状态。当下方的导线通过电流时,不同磁化方向会对电流有着不同的影响,这样就能通过经过的电流大小得出当前存储的装态。

MindShare

这种装置可以看作时现代 DRAM 的雏形。有兴趣的朋友还可以阅读下面的文章:

1.https://baike.baidu.com/item/%E7%A3%81%E8%8A%AF%E5%AD%98%E5%82%A8%E5%99%A8/10189808?fr=aladdin 磁芯存储器

2. https://zhuanlan.zhihu.com/p/144628785 磁芯存储:统治存储领域20年

3.http://www.elecfans.com/d/1277911.html 带你了解磁芯存储器

ESP32 作为蓝牙音源

前面介绍过 ESP32 作为蓝牙音频接收端(蓝牙音箱),这里介绍它作为蓝牙音频的播放端。

首先需要确定蓝牙接收器的名称,用笔记本电脑连接后,可以再设备管理器中看到,这里我使用的是一款蓝牙耳机,名称是“JABRA TALK”:

Jabra Talk 蓝牙耳机

接下来需要安装 ESP32-A2DP-master 这个库。下面的代码是从这个库的Example 中修改而来,代码如下:

/*
  Streaming of sound data with Bluetooth to an other Bluetooth device.
  We provide the complete sound data as a simple c array which 
  can be prepared e.g. in the following way

  - Open any sound file in Audacity. Make sure that it contains 2 channels
    - Select Tracks -> Resample and select 44100
    - Export -> Export Audio -> Header Raw ; Signed 16 bit PCM
  - Convert to c file e.g. with "xxd -i file_example_WAV_1MG.raw file_example_WAV_1MG.c"
    - add the const qualifier to the array definition. E.g const unsigned char file_example_WAV_1MG_raw[] = {
  
  Copyright (C) 2020 Phil Schatzmann
  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "BluetoothA2DPSource.h"
#include "StarWars30.h"
BluetoothA2DPSource a2dp_source;
//SoundData *data = new TwoChannelSoundData((Channels*)StarWars10_raw,StarWars10_raw_len/4);
SoundData *data = new OneChannelSoundData((int16_t*)StarWars30_raw, StarWars30_raw_len/2);

void setup() {
  Serial.begin(115200);
  Serial.println("Start");
  a2dp_source.start("JABRA TALK");    
}

void loop() {
  if (a2dp_source.isConnected()==true) {
      Serial.println("Connected!");
      if (a2dp_source.hasSoundData()==true) {
          Serial.println("Has sound!");
        }
      else {
          Serial.println("No sound!");
          a2dp_source.writeData(data);   
      }  
    }
    else {
      Serial.println("Not connected!");
    }
  delay(2000);  
}

特别注意,因为代码有音频数据需要特别选择 Huge APP 模式:

选择 Huge APP 模式

测试使用的板子是 ESP-WROOM-32,特点是价格偏移兼容性还不错(同样的我试验了 TinkerNode,播放时有卡顿,似乎后台有人一直占用 Soc):

ESP-WROOM-32 很便宜
蓝牙耳机和 ESP32

测试的视频:

https://www.bilibili.com/video/BV1wa411A7et/

特别注意:连接时需要比较有耐心多次尝试,先将耳机设置为配对模式,然后ESP32上电。从资料来看,这样的搭配似乎有兼容性问题,淘宝上的卖家都不承诺蓝牙音频端能够兼容客户的蓝牙接收端。

========================

2023年5月10日

Step to UEFI (230)OVMF 一个FV 的打包过程分析

前面介绍了如何EDK2在编译的最后过程中使用了 GenFds 进行打包。分析的目标是 QEMU 的 BIOS 文件 OVMF.FD,使用 UEFITool NE 打开之后,可以看到有三个 FV ,我们以中间的为例,分析它的生成方法。整体分析过程比较枯燥,对于大多数人来说了解大致的步骤就可以了。

首先,在 FDF 文件中,给出了这个 FV 的GUID 可以看到:

[FV.FVMAIN_COMPACT]
FvNameGuid         = 48DB5E17-707C-472D-91CD-1613E7EF51B0
FvAlignment        = 16
ERASE_POLARITY     = 1
MEMORY_MAPPED      = TRUE
STICKY_WRITE       = TRUE
LOCK_CAP           = TRUE
LOCK_STATUS        = TRUE
WRITE_DISABLED_CAP = TRUE
WRITE_ENABLED_CAP  = TRUE
WRITE_STATUS       = TRUE
WRITE_LOCK_CAP     = TRUE
WRITE_LOCK_STATUS  = TRUE
READ_DISABLED_CAP  = TRUE
READ_ENABLED_CAP   = TRUE
READ_STATUS        = TRUE
READ_LOCK_CAP      = TRUE
READ_LOCK_STATUS   = TRUE

上面的FV 是由两个 Section 构成的,一个是 PEIFV,另一个是 DXEFV:

1.下面是对于 PEIFV 的定义:

PEIVFV 定义
[FV.PEIFV]
FvNameGuid         = 6938079B-B503-4E3D-9D24-B28337A25806
BlockSize          = 0x10000
FvAlignment        = 16
ERASE_POLARITY     = 1
MEMORY_MAPPED      = TRUE
STICKY_WRITE       = TRUE
LOCK_CAP           = TRUE
LOCK_STATUS        = TRUE
WRITE_DISABLED_CAP = TRUE
WRITE_ENABLED_CAP  = TRUE
WRITE_STATUS       = TRUE
WRITE_LOCK_CAP     = TRUE
WRITE_LOCK_STATUS  = TRUE
READ_DISABLED_CAP  = TRUE
READ_ENABLED_CAP   = TRUE
READ_STATUS        = TRUE
READ_LOCK_CAP      = TRUE
READ_LOCK_STATUS   = TRUE

APRIORI PEI {
  INF  MdeModulePkg/Universal/PCD/Pei/Pcd.inf
}

2.下面是 DXEFV 的定义

DXEFV的定义

[FV.DXEFV]

FvForceRebase      = FALSE

FvNameGuid         = 7CB8BDC9-F8EB-4F34-AAEA-3EE4AF6516A1

BlockSize          = 0x10000

FvAlignment        = 16

ERASE_POLARITY     = 1

MEMORY_MAPPED      = TRUE

STICKY_WRITE       = TRUE

LOCK_CAP           = TRUE

LOCK_STATUS        = TRUE

WRITE_DISABLED_CAP = TRUE

WRITE_ENABLED_CAP  = TRUE

WRITE_STATUS       = TRUE

WRITE_LOCK_CAP     = TRUE

WRITE_LOCK_STATUS  = TRUE

READ_DISABLED_CAP  = TRUE

READ_ENABLED_CAP   = TRUE

READ_STATUS        = TRUE

READ_LOCK_CAP      = TRUE

READ_LOCK_STATUS   = TRUE

APRIORI DXE {

  INF  MdeModulePkg/Universal/DevicePathDxe/DevicePathDxe.inf

  INF  MdeModulePkg/Universal/PCD/Dxe/Pcd.inf

  INF  OvmfPkg/AmdSevDxe/AmdSevDxe.inf

!if $(SMM_REQUIRE) == FALSE

  INF  OvmfPkg/QemuFlashFvbServicesRuntimeDxe/FvbServicesRuntimeDxe.inf

!endif

}

这里可以使用 UEFITool 直接将内容解压出来,例如:

必须选择 Extract Body

可以用 Z7 查看解压出来的文件,但是7Z 只能看到 PEIFV 看不到DXEFV, 我猜测是因为格式原因,只能看到前面一半:

7Z 打开查看的结果

具体的 DXEFV 和 PEIFV 可以在 \Build\OvmfX64\DEBUG_VS2015x86\FV 下面看到:

使用这两个文件生成的

有了这两个文件,就可以生成出现在 OVMF.FD  中的 FV 了。为了便于描述,使用下面的流程图:

生成的流程和关系

CMD1:   生成 9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.1fv.sec

GenSec -s EFI_SECTION_FIRMWARE_VOLUME_IMAGE -o d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.1fv.sec d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\PEIFV.Fv

CMD2:   生成9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.2fv.sec

GenSec -s EFI_SECTION_FIRMWARE_VOLUME_IMAGE -o d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.2fv.sec d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\DXEFV.Fv

CMD3:  将上面两个文件合成为一个文件 9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.guided.dummy

GenSec –sectionalign 128 –sectionalign 16 -o d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.guided.dummy d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.1fv.sec d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.2fv.sec

CMD4: 对上面的文件进行压缩,生成 9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.tmp 文件,从 11MB 压缩到了1MB左右

LzmaCompress -e -o d:\\i2c\\Build\\OvmfX64\\DEBUG_VS2015x86\\FV\\Ffs\\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.tmp d:\\i2c\\Build\\OvmfX64\\DEBUG_VS2015x86\\FV\\Ffs\\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.guided.dummy

CMD5: 继续打包生成 9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.tmp

GenSec -s EFI_SECTION_GUID_DEFINED -g EE4E5898-3914-4259-9D6E-DC7BD79403CF -r PROCESSING_REQUIRED -o d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.guided d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.tmp

CMD6:生成 FFS文件,9E21FD93-9C72-4c15-8C4B-E77F1DB2D792.ffs

GenFfs -t EFI_FV_FILETYPE_FIRMWARE_VOLUME_IMAGE -g 9E21FD93-9C72-4c15-8C4B-E77F1DB2D792 -o d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792.ffs -i d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.guided

CMD7:最终生成  FVMAIN_COMPACT.Fv,用 Beyond Compare 可以看到这个是 OVMF.FD 的一部分。

GenFv -a d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\FVMAIN_COMPACT.inf -o d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\FVMAIN_COMPACT.Fv -i d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\FVMAIN_COMPACT.inf

生成的FVMAIN_COMPACT.Fv 就是 OVMF 中间的一部分。

ESP32 搭配语音合成模块

之前介绍过SYN6288 模块【参考1】,这次配合 ESP32 实现随机生成一个时间,然后通过语音播放出来。

硬件使用的是ESP-WROOM-32 ESP-32S模块,语音模块的 RX 连接到 ESP32 的GPIO17:

String DataBuffer[11]={{"零"},{"一"},{"二"},{"三"},{"四"},{"五"},{"六"},{"七"},{"八"},{"九"},{"十"}};
                      
//存放转化后的汉字 Unicode值
char character[40];

#include <HardwareSerial.h>

//根据字符串计算计算出来的送到串口的值
char output[50];

void setup() {
  Serial.begin(115200);
  Serial2.begin(9600);
  delay(1000);
}

void loop() {
  Serial.println("generate");

  int hh=random(13),mm=random(60);
  String timeStr="";

  Serial.print("Generate time: ");
  Serial.print(hh);Serial.print(":");Serial.println(mm);

  if (hh>9) {timeStr+="十";
    if (hh>10) {timeStr+=DataBuffer[hh-10];}
  }
  else {timeStr+=DataBuffer[hh];}
  timeStr+="点";

  if (mm==0) {timeStr+="整";}
  else
    if (mm<10) {timeStr+="零";timeStr+=DataBuffer[mm];}
    else if (mm%10==0) {timeStr+=DataBuffer[mm/10]; timeStr+="十";}
         else {timeStr+=DataBuffer[mm/10];timeStr+="十";timeStr+=DataBuffer[mm%10];}
  if(mm!=0) {timeStr+="分";}
 
    //timeStr="十二点五十八分";
  //首先输出一次Arduino 原始字符串 UTF8 的值
  for (int i =0;i<timeStr.length()*3;i++) {
     Serial.print(timeStr[i]&0xFF,HEX);
      Serial.print(' ');
  }
  Serial.println(' ');

  //将 UTF8 转化为 Unicode
    for (int i =0;i<timeStr.length()/3;i=i+1) {
      character[i*2]=((timeStr[i*3]&0xF)<<4)+((timeStr[i*3+1]>>2)&0xF);
      character[i*2+1]=((timeStr[i*3+1]&0x3)<<6)+(timeStr[i*3+2]&0x3F);
      Serial.print(character[i*2]&0xFF,HEX);
      Serial.print(' ');
      Serial.print(character[i*2+1]&0xFF,HEX);
      Serial.print(' ');      
    } 
  Serial.println(""); 

  output[0]=0xFD;
  output[1]=(timeStr.length()/3*2+3)>>8;
  output[2]=((timeStr.length()/3*2+3)&0xFF);
  output[3]=0x01;
  output[4]=0x03;
  //把字符串定义搬过去
  for (int i=0;i<timeStr.length()/3*2;i++) {
      output[i+5]=character[i];
    } 
  //计算一个校验和  
  output[timeStr.length()/3*2+5]=output[0];
  for (int i=1;i<timeStr.length()/3*2+5;i++) {
      output[timeStr.length()/3*2+5]=output[timeStr.length()/3*2+5] ^ output[i];
    }   
  
  for (int i =0;i<timeStr.length()/3*2+6;i++) {
      Serial.print(output[i]&0xFF,HEX);
      Serial.print(' ');
      Serial2.write(output[i]);
    }     
  
Serial.println(' ');
    delay(3000);
}

参考:

1. http://www.lab-z.com/ttssyn/ TTS 真人发音 SYN6288 模块