YM2151硬音源制作

本文由Motto撰稿于2025年5月31日。Small-U代为投稿

音源效果

如下是音源通过AG03声卡内录的效果(点击下载mp3文件)

FM音乐背景

曾经有人找到笔者想要利用YM2151芯片制作类似Yamaha DX7一样的电子琴,那时我还对调频(FM)电子音乐没有什么概念。但是其实这种风格的音乐已经陪伴了80后90后的前20年时光,一些经典音乐比如《云宫讯音》、《爱如潮水》都以DX7的音色为特征。

有关FM音乐和DX7的描述,可以参考如下视频:

FM音乐的原理

FM音乐作为上世纪80年代介于PWM蜂鸣器和全采样音频之间的技术,它是如何产生声音的呢?

众所周知,自然界最本征的声音,便是正弦波单频信号。于是,FM频率调制的基础信号,便是来自正弦波表(当然也可以用其他的波表,但是这样子谐波就不太好控制了)。除了频率之外,我们对声音的感受还包括了包络信息,那么我们可以生成这样的一个基础信号:

y(t) = E_y(t) \cdot \sin(2\pi f_y t)

其中,E_y(t)y(t)的包络信号,可以由Attack、Decay、Sustain和Release四个阶段描述;f_yy(t)的频率。我们将这个信号再次进行一次调制,就可以得到一种FM音频信号:

z(t) = E_z(t) \cdot \sin[2\pi f_z t + y(t)]

我们可以把f_z叫做载波频率,把f_y叫做调制频率。载波频率决定了声音的高低,而调制频率和载波、调制的包络决定了音色。可以看到,生成上述的一种FM信号需要查询2次正弦表,乘2次包络信号;于是正弦波表、包络发生器、乘法器,以及时分复用的电路就构成了FM合成器的核心组件。而在FM合成术语中,我们把一次正弦查表乘以一次包络叫做一个算子;显然上述FM合成的例子包括了两个算子,由调相级联起来。

YM2151

上述的音乐合成概念,最早由John Chowning于20世纪60年代提出,那时候计算机性能受限,无法实时生成音色丰富的音乐。于是这个概念在AC’97规范提出之前很受欢迎,一些经典的街机、电子琴和电脑都采用了FM合成技术,而雅马哈则在80、90年代垄断了FM合成器的市场。

YM2151作为雅马哈FM合成器系列的一种,代号为OPM,于1984年推出。它包含了4个算子,并支持8种算子级联方式(又叫做算法),如下图所示。它除了FM算子之外,还包含了一个低频振荡器。它的四个算子(事实上为一个正弦表复用4次)被时分复用为8个声道,而8号声道的第4个算子又可以被配置为噪声源。

ym2151 op algo

YM2151的数据手册可以参考这里。我将常用的寄存器配置整理成了如下的图片。

YM2151音符发生器:

YM2151

YM2151算子:

YM2151

YM2151包络发生器:

YM2151

YM2151低频振荡器:

YM2151

音源制作过程

之前已经有很多人使用YM2151制作音源了,这个项目很多都是参考GasshiSPFM Light硬件SCCI 1.0上位机。

硬件

SPFM Light的硬件包括两大部分:主板和音源扩展板。本项目将两部分合并,使用RP2040作为YM2151的主控,通过解析SCCI 1.0协议,将上位机的寄存器dump的VGM音乐发送到YM2151。

PCB原理图

原理图如下所示,包括主控引脚的引出、YM2151和DAC的连接。我直接采用RP2040的4MHz PWM输出作为YM2151的时钟,节省了一个晶振。

RP2040 Pico板子的引脚引出:

RP2040 Sch

YM2151和DAC的连接:

YM2151

USB供电滤波器:

Filter

PCB电路图

原理图做好之后,我先是用洞洞板验证了一下(程序附在文章末尾),然后再用KiCad绘制PCB,引脚顺序经过调整来方便布局。主要走线在板子底面,正面只有几个电源跳线,这样子方便做洞洞板。



制作过程中DAC封装画错了,掰引脚修改的途中断了两个脚,只好飞线解决。

PCB 2

外壳

外壳使用FreeCad制作,通过对PCB的Step文件创建Datum对象来获得需要参考的板子特征。这样子就可以处理接口的预留空间、板子的对齐孔和安装卡扣。

PCB Photo

成品

外壳使用3D打印制作;亚克力顶盖最后加上了激光雕刻的文字,并用丙烯颜料涂色。最终使用AB胶对音频座和飞线部位加固。

Product

上位机

到此硬件的部分描述完毕,软件也有很多前辈做了不少工作,比如本项目使用到的MDPlayerSCCI

MDPlayer和SCCI

MDPlayer是一款VGM播放器,可以读取VGM音乐,并且内置了大量的软音源用来播放。SCCI是SPFM Light硬件的上位机,通过DLL挂载到MDPlayer。这样子硬音源就可以播放YM2151的VGM音乐了。

SCCI 1.0 协议

关于SCCI协议的描述,可以参考这个文档,它的大致原理如下:

  1. 初始化
    • 串口波特率1500000,8位,1停止位,无校验,无流控制。
    • 上位机发送0xFF,SPFM Light收到0xFF之后,返回’L”T’2个字节,告诉上位机连接的硬件是SPFM Light。
    • 上位机发送0xFE,SPFM Light收到0xFE之后,对YM2151复位,然后返回’O”K’2个字节。
  2. VGM数据发送
    • 上位机发送如下几种可能的命令
      • 0x0[slot] – 0x0[a] – 0x[Addr] – 0x[Data];这表示向第[slot]插槽的[a]脚选择的芯片的[Addr]寄存器发送[Data]数据。
      • 0x0[slot] – 0x8[a] – 0x[BusData];这表示向第[slot]插槽的[a]脚选择的芯片的总线发送[BusData]数据。
      • 0x0[slot] – 0x20 – 0x[BusData];这表示向第[slot]插槽的SN76489芯片发送[BusData]数据。

上位机的运行

  1. 安装MDPlayer的STL449版本,这是MDPlayer最后一个支持SCCI 1.0的版本。
  2. 将SCCI压缩包的dll和配置程序解压到MDPLayer的根目录下。
  3. 连接硬音源,打开scciconfig.exe,此时应该能够检测到SPFM Light硬件,并将slot 0设置成YM2151,时钟为4MHz,然后关闭配置程序。
  4. 打开MDPlayer,在设置的Sound页面中的YM2151框架中选择Real并勾选4MHz时钟。
  5. VGMRips中选择YM2151对应的VGM音乐,下载下来使用MDPLayer播放。

SPFM固件

基于上述SCCI 1.0协议,我们可以写出如下RP2040 Arduino程序。

/*
Copyright (C) 2025  Motto (https://x.com/YangMotto)

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 .
*/

//Please Modify CFG_TUD_CDC_RX_BUFSIZE in "tusb_config.h"
#include "Arduino.h"
#include "RP2040_PWM.h"
#include "hardware/clocks.h"

// Define the pins for the data bus and control signals
const int d0 = 26;
const int d1 = 27;
const int d2 = 17;
const int d3 = 16;
const int d4 = 15;
const int d5 = 14;
const int d6 = 13;
const int d7 = 12;

const int rs0 = 19;
const int n_cs = 22;
const int n_rd = 21;
const int n_wr = 20;
const int n_rst = 18;
const int n_rst2 = 11;

// PWM for clock signal
const int clkPin = 28;
const int clkFreq = 4000000;
RP2040_PWM* ym2151_clk_pwm;

// Uart Parsing
int uart_idle_cnt = 10000;
int uart_parse_idx = 0;
const int uart_tx = 0;
const int uart_rx = 1;

// Led for Feedback
const int led_pin = 25;

void init_reg() {
  digitalWrite(rs0, HIGH);
  digitalWrite(n_cs, HIGH);
  digitalWrite(n_rd, HIGH);
  digitalWrite(n_wr, HIGH);
  delay(1);
  digitalWrite(n_rst, LOW);
  digitalWrite(n_rst2, LOW);
  delay(1);
  digitalWrite(n_rst, HIGH);
  digitalWrite(n_rst2, HIGH);
}

// Set the data bus pins
void set_data_bus(int i) {
  digitalWrite(d0, (i >> 0) & 0x01);
  digitalWrite(d1, (i >> 1) & 0x01);
  digitalWrite(d2, (i >> 2) & 0x01);
  digitalWrite(d3, (i >> 3) & 0x01);
  digitalWrite(d4, (i >> 4) & 0x01);
  digitalWrite(d5, (i >> 5) & 0x01);
  digitalWrite(d6, (i >> 6) & 0x01);
  digitalWrite(d7, (i >> 7) & 0x01);
}

void spfm_bus_select_slot(int slot) {
  if (slot == 0) {
    digitalWrite(n_cs, LOW);
  } else {
    digitalWrite(n_cs, HIGH);
  }
  delayMicroseconds(1);
}

void spfm_bus_release_slots() {
  digitalWrite(n_cs, HIGH);
}

void spfm_bus_write(int slot, int reg_sel, int data) {
  digitalWrite(rs0, reg_sel & 0x01);
  //delayMicroseconds(1);
  spfm_bus_select_slot(slot);
  digitalWrite(n_wr, LOW);
  delayMicroseconds(1);
  set_data_bus(data);
  delayMicroseconds(1);
  digitalWrite(n_wr, HIGH);
  spfm_bus_release_slots();
}

void loop();
void loop1();

void setup() {

  // Set pin modes
  pinMode(d0, OUTPUT);
  pinMode(d1, OUTPUT);
  pinMode(d2, OUTPUT);
  pinMode(d3, OUTPUT);
  pinMode(d4, OUTPUT);
  pinMode(d5, OUTPUT);
  pinMode(d6, OUTPUT);
  pinMode(d7, OUTPUT);

  pinMode(rs0, OUTPUT);
  pinMode(n_cs, OUTPUT);
  pinMode(n_rd, OUTPUT);
  pinMode(n_wr, OUTPUT);
  pinMode(n_rst, OUTPUT);
  pinMode(n_rst2, OUTPUT);

  pinMode(led_pin, OUTPUT);

  // Initialize PWM for clock
  ym2151_clk_pwm = new RP2040_PWM(clkPin, clkFreq, 50, false);
  ym2151_clk_pwm->setPWM(clkPin, clkFreq, 50, false);

  // UART 0 for Input - Deprecated
  Serial1.setFIFOSize(512);
  Serial1.setRX(uart_rx);
  Serial1.setTX(uart_tx);
  Serial1.begin(1500000);

  // UART over USB for IO
  Serial.ignoreFlowControl(true); // Very Important to for SCCI to Receive
  Serial.begin(1500000);
  delay(10);
  init_reg();
  digitalWrite(led_pin, HIGH);

  // Multi-Core Task Assignment
  multicore_launch_core1(loop1);
}

#include "CoreMutex.h"
extern mutex_t __usb_mutex;
#include "tusb.h"
void loop() {
  // Most of the TinyUSB ISR is handled by core 0
  CoreMutex m(&__usb_mutex, false);
  tud_task();
}

// Variables to parse SPFM Data
uint8_t uart_data = 0;
uint8_t scci_parse_idx = 0; // 0-frame part 0, 1-part 1, 2-part 2
uint8_t scci_slot = 0;
uint8_t scci_cmd = 0;
uint8_t scci_a = 0;
uint8_t scci_addr = 0;
uint8_t scci_data = 0;
void loop1() {
  // Handlers for the SPFM protocols

  uart_idle_cnt--;
  if (uart_idle_cnt == 0) {
    uart_parse_idx = 0;
    uart_idle_cnt = 10000;
  }

  if (Serial.available() > 0) {
    uart_idle_cnt = 10000;

    uart_data = Serial.read();
    digitalWrite(led_pin, !digitalRead(led_pin));

    if (scci_parse_idx == 0) {
      if (uart_data == 0xFF) {
        // Device Response Code
        // LT - SPFM Light
        Serial.write("LT", 2);
      } else if (uart_data == 0xFE) {
        // Reset
        init_reg();
        Serial.write("OK", 2);
      } else if ((uart_data & 0xF0) == 0x00) {
        // 0x0[slot] - 0x0[An] - Addr - Data
        // 0x0[slot] - 0x8[An] - BusData
        // 0x0[slot] - 0x20 - SN76489_Data
        scci_slot = uart_data & 0x0F;
        scci_parse_idx = 1;
      } else {

      }
    }

    else if (scci_parse_idx == 1) {
      scci_cmd = uart_data & 0xF0;
      if (scci_cmd == 0x00) {
        // 0x0[An] - Addr - Data
        scci_a = uart_data & 0x0F;
        scci_parse_idx = 2;
      } else if (scci_cmd == 0x80) {
        // 0x8[An] - BusData
        scci_a = uart_data & 0x0F;
        scci_parse_idx = 2;
      } else if (scci_cmd == 0x20) {
        // 0x20 - SN76489_Data
        scci_parse_idx = 2;
      } else {
        scci_parse_idx = 0;
      }
    }

    else if (scci_parse_idx == 2) {
      if (scci_cmd == 0x00) {
      // 0x0[An] - Addr - Data
      scci_addr = uart_data;
      spfm_bus_write(scci_slot, scci_a & 0xFE, scci_addr);
      scci_parse_idx = 3;
      } else if (scci_cmd == 0x80) {
        // 0x8[An] - BusData
        scci_data = uart_data;
        spfm_bus_write(scci_slot, scci_a, scci_data);
        scci_parse_idx = 0;
      } else if (scci_cmd == 0x20) {
        // 0x20 - SN76489_Data
        scci_data = uart_data;
        // We don't have SN76489, so we ignore this.
        //spfm_bus_write(scci_slot, 0x00, scci_data);
        scci_parse_idx = 0;
      } else {
        scci_parse_idx = 0;
      }
    }

    else if (scci_parse_idx == 3) {
      if (scci_cmd == 0x00) {
        // 0x0[An] - Addr - Data
        scci_data = uart_data;
        spfm_bus_write(scci_slot, scci_a | 0x01, scci_data);
        scci_parse_idx = 0;
      } else {
        scci_parse_idx = 0;
      }
    }

    else {
      scci_parse_idx = 0;
    }
  }
}

可能的后续工作

  • 尝试雅马哈的其他FM芯片。
  • 板子上附加2片SN76489,丰富一下节奏和PWM伴奏。

发表回复