Introducción
Mi hijo recibió por su cumpleaños un despertador con temática Minecraft que le gustó mucho salvo por el sonido que tenía como despertador, que estaba prefijado y era una supuesta grabación "inspirada" en el juego.
Dicha grabación se escucha saturada y con poca calidad por lo que resulta desagradable de escuchar y mi hijo me pidió que intentara cambiarla.
En el siguiente vídeo grabado por un youtuber que hace reviews de este tipo de cosas puede escucharse el sonido original que trae este despertador (instante 4:50 aprox.).
Planteamiento del problema
Se parte de un reloj despertador con una circuitería muy cerrada y no documentada y el objetivo es cambiar el sonido que se escucha cuando suena la alarma, que, de fábrica, es un sonido pregrabado:
Las placas tienen escasa serigrafía y el único integrado visible está "borrado". Lo ideal sería obtener una señal digital que indique la activación de la alarma.
Investigación
El cable plano que une la placa principal con la placa de botones, leds y el chip de audio es un cable plano de 10 hilos que sólo tiene los dos extremos serigrafiados como "vdd" y "gnd".
Si a los 10 cables les quitamos los dos de alimentación de los extremos quedan 8 cables que no se sabe para qué son, sin embargo se intuye, dada la funcionalidad de esa placa, que esos 8 cables están distribuidos de la siguiente manera:
- 6 señales para leer los botones (el reloj tiene una cruceta de 4 botones más 2 botones adicionales).
- 1 señal para controlar los leds blancos (que se usan para la funcionalidad de lámpara).
- 1 señal para controlar la música (cuando suena la alarma).
Tras varias pruebas se verifica que la distribución de las señales es la siguiente.
Y que la señal "alarma" utiliza lógica positiva: se pone a 5 voltios para que suene la alarma y se pone a 0 voltios para apagar la alarma.
Desarrollo de la solución
Teniendo localizado el cable de la señal "alarma" el objetivo ahora es hacer un pequeño montaje que permita reproducir otro tipo de música o sonido en el despertador y para ello se plantea el siguiente esquema eléctrico:
Toda la circuitería del reloj despertador y del DFPlayer Mini funciona con 5 voltios, pero el CH32V003 funciona con 3.3 voltios. En este caso se utiliza una placa "nanoCH32V003" para que pueda alimentarse a 5 voltios de la propia fuente del reloj despertador. El cable de alarma se conecta a PC1 o a cualquier otra entrada del CH32V003 siempre y cuando sea un pin tolerante a 5 voltios (no todos los pines de ese microcontrolador lo son).
El DFPlayer Mini es un módulo de reproducción de MP3 que incluye lector de tarjeta de memoria microSD. Dispone de dos pines dedicados (ADKEY1 y ADKEY2) que, cuando se ponen a masa reproducen, respectivamente, el primer y el quinto MP3 de la tarjeta de memoria. El módulo carece de pines para detener la reproducción pero, como en este caso sólo se precisa que hayan único sonido, se opta por una solución simple:
- Como 1er sonido de la tarjeta de memoria se pone el sonido que queremos que tenga el despertador.
- Como 5o sonido de la tarjeta de memoria se pone un MP3 de un segundo de silencio.
De esta forma poniendo ADKEY1=0 se reproduce el sonido nuevo y poniendo ADKEY2=0 "paramos" la reproducción al reproducir el MP3 de silencio.
Se configuran las salidas PA1 y PA2 del microcontrolador como salidas en colector abierto (open drain): emitiendo un 1 se ponen en alta impedancia y emitiendo un 0 se ponen a masa, que es el comportamiento que se quiere para controlar el DFPlayer Mini:
(imagen extraída de www.prometec.net)
PA1 y PA2 se configuran en open-drain debido a dos razones:
- El DFPlayer Mini ya dispone de resistencias pull-up en las entradas ADKEY1 y ADKEY2.
- El DFPlayer Mini está alimentado a 5 voltios mientras que el CH32V003 lo está a 3.3 voltios, por lo que un "1" del CH32V003 no sería igual que un "1" para el DFPlayer Mini. Usando salidas en open-drain nos aseguramos de que la corriente que circula por los cables PA1 y PA2 proviene del DFPlayer Mini (5 voltios), no del CH32V003 (3.3 voltios).
Código
El código del microcontrolador CH32V003 será el encargado de muestrear el cable "alarm" a intervalos regulares (cada 100 milisegundos). Cuando el cable "alarm" se ponga a 1 el microcontrolador pondrá a 0 (masa) durante un tiempo prefijado la salida conectada a ADKEY1 y cuando el cable "alarm" pase de nuevo a 0, el microcontrolador pondrá el pin conectado a ADKEY2 a 0 (masa) también durante un tiempo prefijado (el correspondiente a reproducir un silencio). La máquina de estados tendrá, por tanto una entrada (la señal del cable "alarm") y dos salidas (que gobiernan las salidas en colector abierto de ADKEY1 y ADKEY2).
El reloj de esta máquina de estados viene dado por el timer del microcontrolador, que tiene un período de 100 milisegundos:
- N_SILENCE es la cantidad de ticks de reloj que debe permanecer a masa la salida "silencio".
- N_SOUND es la cantidad de ticks de reloj que debe permanecer a masa la salida "sonido".
La cantidad total de milisegundos que se ponga a masa cada salida será de N_SILENCE * 100 y de N_SOUND * 100 milisegundos respectivamente.
void AlarmControl::run() { Status localStatus = this->status; do { this->status = localStatus; if (localStatus == Status::RESET) { this->io.stopTrigSound(); this->io.stopTrigSilence(); localStatus = Status::TRIG_SILENCE; } else if (localStatus == Status::TRIG_SILENCE) { this->io.startTrigSilence(); this->counter = N_SILENCE; localStatus = Status::WAIT_SILENCE; } else if (localStatus == Status::WAIT_SILENCE) { this->counter--; if (this->counter == 0) { this->io.stopTrigSilence(); localStatus = Status::WAIT_1; } } else if (localStatus == Status::WAIT_1) { if (this->io.getAlarm()) localStatus = Status::TRIG_SOUND; } else if (localStatus == Status::TRIG_SOUND) { this->io.startTrigSound(); this->counter = N_SOUND; localStatus = Status::WAIT_SOUND; } else if (localStatus == Status::WAIT_SOUND) { this->counter--; if (this->counter == 0) { this->io.stopTrigSound(); localStatus = Status::WAIT_0; } } else if (localStatus == Status::WAIT_0) { if (!this->io.getAlarm()) localStatus = Status::TRIG_SILENCE; } } while (localStatus != this->status); }
La máquina de estados se implementa en la clase "AlarmControl" que decibe en su construcción una referencia a un objeto que debe heredar de "AlarmControlIO".
class AlarmControlIO { public: virtual bool getAlarm() = 0; virtual void startTrigSound() = 0; virtual void stopTrigSound() = 0; virtual void startTrigSilence() = 0; virtual void stopTrigSilence() = 0; }; class AlarmControl { protected: AlarmControlIO &io; enum class Status { RESET, TRIG_SILENCE, WAIT_SILENCE, WAIT_1, TRIG_SOUND, WAIT_SOUND, WAIT_0 }; Status status; uint32_t counter; static const uint32_t N_SILENCE = 4; static const uint32_t N_SOUND = 4; public: AlarmControl(AlarmControlIO &alarmControlIO) : io(alarmControlIO), status(Status::RESET) { }; void run(); };
En el fichero "main.cc" se define una clase "MyListener" que tiene un objeto de tipo "AlarmControl" como propiedad y que hereda de:
- "TimerListener" (lo que la obliga a incluir la función miembro "timerExpired()" que se ejecutará cada vez que se desborde el contador de SysTick cada 100 milisegundos).
- "AlarmControlIO" (lo que la obliga a incluir las funciones miembro "bool getAlarm()" para leer el estado de la señal "Alarm" y las otras cuatro funciones "void startTrigSound()", "void stopTrigSound()", "void startTrigSilence()" y "void stopTrigSilence()" para controlar los pines PA1 y PA2 que son los que gobiernan el reproductor MP3 DFPlayer Mini).
En la función miembro "timerExpired()" se invoca, a su vez, a la función miembro "run()" del objeto "AlarmControl" para que se vaya iterando la máquina de estados cada 100 milisegundos.
void MyListener::timerExpired() { this->alarmControl.run(); } bool MyListener::getAlarm() { return (GPIOC_INDR & (((uint32_t) 1) << 1)) ? true : false; // read PC1 } void MyListener::startTrigSound() { GPIOA_OUTDR &= ~(((uint32_t) 1) << 1); // PA1 = 0 } void MyListener::stopTrigSound() { GPIOA_OUTDR |= (((uint32_t) 1) << 1); // PA1 = high z } void MyListener::startTrigSilence() { GPIOA_OUTDR &= ~(((uint32_t) 1) << 2); // PA2 = 0 } void MyListener::stopTrigSilence() { GPIOA_OUTDR |= (((uint32_t) 1) << 2); // PA2 = high z } int main() { interruptInit(); MyListener myListener; Timer::init(myListener, 100_msForTimer); while (true) asm volatile ("wfi"); }
En la función "main()" simplemente se declara un objeto de tipo "MyListener" (la construcción de este objeto también incluye la construcción del objeto "AlarmControl" debido a que es una propiedad del primero), se inicializa el Timer y nos quedamos en bucle infinito de "wfi" (para consumir poca energía entre tick y tick de la máquina de estados).
Consideraciones particulares entorno al microcontrolador CH32V003
El CH32V003 es un RISC-V muy barato y muy limitado, al contrario que el GD32VF103 que se ha utilizado hasta ahora para otros proyectos con microcontrolador, el CH32V003 tiene las siguientes características:
- 16 Kb de memoria flash para programa.
- 2 Kb de memoria SRAM.
- El controlador de interrupciones no es el estándar CLIC, sino un controlador propietario (llamado PFIC) que se encuentra documentado en el manual de referencia del microcontrolador.
- El núcleo es un "QingKeV2", un RISC-V de perfil RV32EC. E = Embedded (16 registros de propósito general en lugar de los 32 del perfil RV32I) y C = Compressed (acepta instrucciones comprimidas de 16 bits, una especie de equivalente al modo "thumb" de los Cortex-M de ARM).
- Un "SysTick" integrado en el núcleo parecido al que tienen los Cortex-M de ARM.
En la sección soft hay disponible un pequeño proyecto de blinker (ch32v003-pfic-blinker) que utiliza el controlador de interrupciones de este microcontrolador junto con ese "systick" para hacer parpadear un led. Nótese los flags que se pasan al compilador:
-march=rv32eczicsr -mabi=ilp32e
Y la forma en que se habilitan las interrupciones en "interrupt.cc", que son diferentes a como se hace en el GD32VF103.
Tostar el microcontrolador
Para tostar el microcontrolador la mejor opción es pillar un programador WCH-LinkE del propio fabricante (es muy barato, a mi me costó menos de 10 ¤) ya que el protocolo de depuración y tostado es propietario del fabricante (aunque está totalmente documentado y hay proyectos en curso para no depender de ese programador hardware en particular).
El software para acceder al WCH-LinkE que utilicé es el programa "minichlink" (un sub proyecto dentro del repositorio https://github.com/cnlohr/ch32v003fun). Basta con hacer "make" en la carpeta "minichlink" dentro de ese repositorio y se genera el ejecutable "minichlink" que nos permite tostar el CH32V003:
./minichlink -w /ruta/al/main.bin 0x08000000
Montaje y resultados finales
A continuación puede verse cómo se realizaron las conexiones al conector del cable plano para extraer los 5 voltios, masa y la señal de alarma.
Cómo se realizó el montaje del prototipo con el microcontrolador y el DFPlayer Mini en una protoboard externa.
Y el paso del circuito de la protoboard a la PCB que se alojará dentro del reloj despertador:
A continuación un vídeo de demostración con el nuevo sonido (que ya no es la música de Minecraft).
El sonido que se ha puesto como sonido de alarma es el típico de los despertadores de toda la vida (que era el que quería mi hijo) y se ha descargado de https://pixabay.com/sound-effects/031974-30-seconds-alarm-72117/ (Pixabay permite uso libre de los sonidos que se descarguen de su web mientras no se revendan).
Todo el código en la seción soft.