본문 바로가기

arduino

전자 피아노 제작

어느날 저는 C언어를 하고싶어졌어요.

Pcb도 만들고 싶었죠.

눈엔 raspberry pi pico w가 보였습니다.

 

그래서 간단하게 피아노를 만들어보자고 생각했습니다.

 

인터넷과 GPT로 칩이랑 회로도를 보면서 회로도를 만들었어요.

 

회로도
3D 모형

그리고 주문을 했습니다. (5개 합해서 16.9만원)

Smt는 너무 비쌉니다. 

 

Jlcpcb 회사내에서 부품이 없는것들은 제거하고 배송을 받았습니다.

아름답다.

 

그렇지 않으면 아마 못살정도로 비쌀거같습니다.

 

아무튼, 납땜을 하고 테스트를 했는데, 몇가지 문제점이 드러났습니다..

1. 맨 오른쪽(가장 큰) 칩은 pullup저항만 지원한다 합니다. 그리고 저는 pulldown기준으로 만들었죠.

(pullup은 기본적으로 상태가 ON pulldown은 기본적으로 OFF, + - 극을 잘못연결했다는느낌?)

2. sd 카드 통신 핀을 잘못 연결했습니다.

3.오디오 통신 핀도 잘못 연결했습니다.

 

 

 

아무튼 연기만 안나면 괜찮습니다.

 

보통 이런건 모듈을 먼저 사용해서 코딩을 하고 테스트를 거친 후에 만들기에

이런 문제는 생기지 않겠지만.

 

부품 사고 테스트 하기에는 돈이 없었습니다.

 

이 사진은 오류를 수정한 후 못생겨진 PCB입니다.

 

다행이도 집에 애나멜선이 있어서 수정을 했습니다.

 

코딩은 처음앤 micropython을 사용했는데, sd카드 읽는 속도가 너무 느리더라구요. (GPT씀)

from machine import Timer, Pin, I2C, SPI, I2S
import mcp23017
import wavefile
import sdcard
import uos
import time
import array

# 업샘플링 (8kHz → 16kHz) 적용
def upsample_8k_to_16k(data):
    upsampled = bytearray(len(data) * 4)  
    for i, sample in enumerate(data):
        sample_16bit = (sample - 128) * 256  
        sample_16bit = max(-32768, min(32767, sample_16bit))  
        upsampled[i * 4] = sample_16bit & 0xFF
        upsampled[i * 4 + 1] = (sample_16bit >> 8) & 0xFF
        upsampled[i * 4 + 2] = sample_16bit & 0xFF  
        upsampled[i * 4 + 3] = (sample_16bit >> 8) & 0xFF  
    return upsampled
try:
    l  = Pin("LED", Pin.OUT)
    le = Pin(2, Pin.IN, Pin.PULL_DOWN)

    Pin(13, Pin.IN, Pin.PULL_DOWN)
    Pin(14, Pin.IN, Pin.PULL_DOWN)
    Pin(15, Pin.IN, Pin.PULL_DOWN)

    # Assign chip select (CS) pin (and start it high)
    cs = Pin(17, Pin.OUT)
    # Initialize SPI peripheral (start with 1 MHz)
    spi = SPI(1,
            baudrate=4_000_000,
            polarity=0,
            phase=0,
            firstbit=SPI.MSB,
            sck=Pin(10),
            mosi=Pin(11),
            miso=Pin(12))
    # Initialize SD card
    sd = sdcard.SDCard(spi, cs)
    # Mount filesystem
    vfs = uos.VfsFat(sd)
    uos.mount(vfs, "/sd")

    f = open("/sd/log.txt", "w")
    f.write("Hello, SD card!\r\n")

    i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=100000)

    bclk = Pin(20)  # BCLK
    lrck = Pin(21)  # LRCK
    din = Pin(19)   # DIN

    # WAV 파일 열기
    wav_file = open("/sd/master.wav", "rb")

    # WAV 헤더 읽기
    wav_header = wavefile.WaveFileHeader.from_file(wav_file)

    if wav_header.channels == 1:
        channel_format = I2S.MONO
    elif wav_header.channels == 2:
        channel_format = I2S.STEREO

    # I2S 설정 (버퍼 크기 40000)
    buffer_size = 80000
    i2s = I2S(0, sck=bclk, ws=lrck, sd=din, mode=I2S.TX, bits=16, format=channel_format, rate=wav_header.sample_rate, ibuf=buffer_size)
    
    wav_samples = bytearray(8000)
    wav_samples_mv = memoryview(wav_samples)

    # 오디오 재생 속도 계산 (초당 필요한 데이터 크기)
    bytes_per_sample = wav_header.bits_per_sample // 8
    playback_rate = wav_header.sample_rate * wav_header.channels * bytes_per_sample  # 초당 필요한 바이트 수

    while True:
        if le.value() == 1:
            break

        start_read_time = time.ticks_us()
        num_read = wav_file.readinto(wav_samples_mv)
        read_time = time.ticks_diff(time.ticks_us(), start_read_time)

        if num_read == 0:
            break  # 파일 끝
        
        converted_data = upsample_8k_to_16k(wav_samples_mv[:num_read])

        # I2S에 전송 (전송 후 약간의 대기 시간 추가)
        _ = i2s.write(converted_data)
        time.sleep_ms(5)  # 전송 후 약간 대기

except Exception as e:
    f.write(f"[ERROR] {str(e)}\r\n")
finally:
    f.close()
    wav_file.close()
    i2s.deinit()
    uos.umount("/sd")

while True:
    l.value(1)
    time.sleep(0.5)
    l.value(0)
    time.sleep(0.5)

wav 파일 단일채널(이어폰 좌우 소리 통일)로 만들고 8비트로 열화 시켜도 소리의 끊김이 있었죠.

 

C언어로 하니까 소리가 잘 들렸습니다. (GPT가 헛소리함)

#include <SD.h>
#include <SPI.h>
#include <I2S.h>
#include <Adafruit_MCP23X17.h>

using namespace fs;

#define MIX 2
#define TONE_LIST 1
#define PIANO_COUNT 23

#define BUTTON_PIN 1
#define LED_PIN LED_BUILTIN

#define SDA_PIN 0
#define SCL_PIN 1

#define CS_PIN 17
#define SCK_PIN 10
#define MOSI_PIN 11
#define MISO_PIN 12

#define RP_CLK_GPIO = -1 // 필요없음
#define RP_CMD_GPIO = -1 // '' 
#define RP_DAT0_GPIO = -1 // ''

#define BCLK 20
#define LRCK 21
#define DIN 19

int16_t g_buffer[256];

File *active_files;
File piano_tiles[PIANO_COUNT];

bool pianoTileStates[PIANO_COUNT];
bool buffer_index;
bool buffer_updater;

Adafruit_MCP23X17 mcp;
I2S i2s(OUTPUT, BCLK, DIN);

void format_path(int num, char *buffer);
void start_sound(File *file);
void stop_sound(File *file);
int countFolders(File *dir);

int tone_index = 0;
int tone_max = 0;

void setup() {
  pinMode(LED_PIN, OUTPUT);

  Wire.setSDA(SDA_PIN);
  Wire.setSCL(SCL_PIN);
  Wire.begin();

  SPI1.setRX(MISO_PIN);
  SPI1.setTX(MOSI_PIN);
  SPI1.setSCK(SCK_PIN);
  SPI1.begin();

  SPI1.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));

  SD.begin(CS_PIN, SPI1);

  i2s.setBitsPerSample(16);
  i2s.setFrequency(44100);
  i2s.setStereo(true);
  i2s.begin();

  if (!mcp.begin_I2C(0x24)) {
    digitalWrite(LED_PIN, HIGH);
    while (1)
      ;
  }

  for (int i = 2; i < 9; i++) {
    pinMode(i, INPUT_PULLDOWN);
  }

  for (int i = 0; i < 16; i++) {
    mcp.pinMode(i, INPUT_PULLUP);
  }
  File f = SD.open("/");
  tone_max = countFolders(&f);
  f.close();
  changeTone(tone_index);
}


int countFolders(File *dir) {
  int folderCount = 0;
  while (true) {
    File entry = dir->openNextFile();
    if (!entry) {
      break;
    }

    if (entry.isDirectory()) {
      folderCount++; 
    }

    entry.close();
  }
  return folderCount;
}

void changeTone(int tone_id) {
  char file_path[11];
  for (int i = 0; i < PIANO_COUNT; i++) {
    if (piano_tiles[i]) {
      piano_tiles[i].close();
    }
    format_path(tone_id, i, file_path);
    piano_tiles[i] = SD.open(file_path);
    piano_tiles[i].seek(44);
  }
}

void start_sound(File *file) {
  if (!file) return;
  for (int i = 0; i < MIX; i++) {
    if (!active_files) {
      active_files = file;
      return;
    }
  }
  // ignore sound 입력 무시됨
}

void stop_sound(File *file) {
  if (!file) return;
  for (int i = 0; i < MIX; i++) {
    if (active_files == file) {
      active_files->seek(44);
      active_files = nullptr;
      return;
    }
  }
  //파일 재생 끝남 or 오류
}

void loop() {

  if (digitalRead(9)) {
    if (tone_index >= tone_max) {
      tone_index = 0;
    } else {
      tone_index++;
    }
    changeTone(tone_index);
  }

  for (int i = 0; i < 7; i++) {
    if (digitalRead(i + 2)) {
      if (!pianoTileStates[i]) {
        pianoTileStates[i] = true;
        start_sound(&piano_tiles[i]);
      } else {
        continue;
      }
    } else {
      if (pianoTileStates[i]) {
        pianoTileStates[i] = false;
        stop_sound(&piano_tiles[i]);
      }
    }
  }

  for (int i = 0; i < 16; i++) {
    if (mcp.readGPIO(i)) {
      if (!pianoTileStates[i + 7]) {
        pianoTileStates[i + 7] = true;
        start_sound(&piano_tiles[i + 7]);
      } else {
        continue;
      }
    } else {
      if (pianoTileStates[i + 7]) {
        pianoTileStates[i + 7] = false;
        stop_sound(&piano_tiles[i + 7]);
      }
    }
  }

  if (active_files) {
    if (active_files->available()) {
      active_files->read((uint8_t *)g_buffer, 512); // 왜 256의 2배냐면 sd 카드가 파일을 읽을떄 uint8_t정도의 크기만큼 가져오기에 2배만큼의 데이터가 int16_t데이터 크기 1배 이기 때문이다
    } else {
      active_files->seek(44); // 파일 초기화 및 wav 파일 해더 건너뛰기
      active_files = nullptr;
    }
  }
  

  for (int i = 0; i < 256; i++) {
    i2s.write(g_buffer[i]);
    g_buffer[i] = 0;
  }
}


void format_path(int folder_id, int file_id, char *buffer) {
  snprintf(buffer, 11, "%02d/%03d.wav", folder_id, file_id);
}

C는 역C 빨랐습니다. 하하

하지만

SD카드가 2개 파일을 동시에 읽는게 SPI방식에선 무리인가봐요.

귀찮으니까 여기서 마무리하고 C코드 설명을 몇개 하겠습니다.

 

#define 은 선언이고 *기나 +같은 연산자 {같은 기호도 정의하여 사용할수가 있습니다.

*는 주소값이라는건 알겁니다. 그런데 배열 변수 자체는 주소값이여서 ([1] 은 a 주소값 + 1같은 개념입니다.)

type *a는 배열로 입력받을때도 사용합니다.

 

43번줄 같은 경우에는 가독성을 위해 함수들은 아래에 작성할수 있도록 하는데요.

컴파일러가 오류를 발생시키지 않게 이 함수가 존재한다고 알려주는 기능입니다. 

 

-> 이건 주소값을 대상으로  바로 함수를 사용할떄 사용됩니다.

저에겐 이게 가장 아름다운 문법이라 생각이 듭니다.

 

오랜만에 C를 해보았는데, 너무 자유로운 세계이고 위험한거같네요.

나중에 돈 많이 벌면 도전해야겠습니다.