mpc 라이브러리를 이용한 파서 만들기

1 minute read

얼마 전에 발주받은 펌웨어 개발 건에서 약간 재미난 구현이 있어서 몇자 적는다.

만들어야 하는 펌웨어는 치매 환자의 인지력과 기억력 향상에 도움을 주도록 설계된 간단한 보드인데, 삼색 LED 100개와 자석 센서가 100개 달려있고, 이것들을 블루투스를 통해 모바일폰이나 PC의 앱(이하 앱이라고 지칭)으로 동작하게 만들어주는 것이 주된 기능이었다. 보드는 각 LED와 자석 센서의 상태를 변경 혹은 체크하는 간단한 기능을 가지고, 그것을 이용하여 앱에서 더 복잡한 동작을 구현할 수 있도록 만들어 주는 것이다.

그래서 앱과 보드 사이에 간단한 명령어셋이 필요했다. 기본적인 문법은 과거 시리얼 통신에서 사용하는 방식을 활용했는데 좀 깔끔하게 구현할 방법이 없을까 고민하다 mpc라이브러리가 생각나 이용해 보기로 했다. mpc 라이브러리는 yacc와 비슷한 기능을 하지만 라이브러리 형태로 되어 있어서 간단한 인터프리터를 만들 때 사용하기에 적당한 것 같다.

보드와 앱이 주고 받을 명령어셋은 대략 다음과 같은 규칙을 가진다.

  • 각 명령은 ’(’ 로 시작하고, ’)’ 로 끝난다.
  • 괄호 안에 문법은 “[command], [arguments]” 이다.
  • [arguments] 는 십진수 혹은 16진수(0x 으로 시작)이며, 0개 이상 이어질 수 있고, 구분자는 콤마(‘,’) 이다.
  • 대소문자는 구분되며, 공백문자는 무시된다.

예를 들면,

(led_set_direct_draw, 0)
(led_clear)
(led_set_pixel, 0, 0, 0xFF0000) 
(led_set_pixel, 1, 0, 0x00FF00)
(led_fill_rect, 0, 5, 9, 9, 0x0000FF)
(led_draw)

같은 스크립트를 앱에서 보드로 한꺼번에 명령내릴 수 있는데, 의미는 10x10 LED matrix 에서 (0,0) 지점은 제일 밝은 붉은 색, (1,0) 지점은 제일 밝은 녹색, 메트릭스 하단 반절은 제일 밝은 파란색으로 채우라는 뜻이다.

위 문법 규칙을 프로그래밍 언어를 정의할 때 사용하는 표준적인 표기법인 BNF로 작성하면 다음과 같다.

<decimal>     ::= /[0-9]+/
<hexadecimal> ::= /0x[0-9a-fA-F]+/
<number>      ::= <hexadecimal> | <decimal>
<func>        ::= /[a-zA-Z][a-zA-Z0-9_]*/
<args>        ::= ("," <number>)*
<cmd>         ::= "(" <func><args> ")"
<script>      ::= /^/ <cmd>* /$/

mpc 라이브러리는 이 BNF를 읽어 파서를 생성할 수 있다. ㅎㅎㅎ

이걸 기존에 방식대로 처음부터 작성하려면 문자열 다루는 루틴으로 많은 시간을 소비하고도 많은 버그가 생겨 그걸 해결하느라 골머리를 앓았을 것이다. 그러고도 문법 수정이 쉽지 않을 것이기 때문에 이런 라이브러리가 있으면 적극활용하는 것이 정신건강에 이롭다.

실제 이런 식으로 구현하면서 얻은 부수적인 이득이 몇가지 더 있었다. 예를 들면 메모리가 허락하는 한에서 아주 긴 스크립트도 작성할 수 있다는 것이고, 공백문자 처리를 훨씬 수월하게 할 수 있었다는 것이다.

물론 몇가지 제약사항이 있다. 펌웨어 레벨에서 쓰려면 malloc 지원에 문제없어야 하고 램 사이즈도 넉넉해야 하는데 mpc가 파싱트리를 만들 때 힙을 많이 쓰기 때문이다. 다행히 사용하는 MCU가 ESP32여서 이런 부분은 자동으로 해결되었다.

좀 제대로된 스크립트 언어로 쓰려면 변수 설정 기능과 함께 루프나 조건식 예약어도 지원해야하겠지만, 이거 뭐 이백만원짜리 일에 그럴 필요는 없지 않은가. ㅋ

Comments