La nueva vida del viejo sintetizador. Parte 2

Continuación historias sobre el pasado de sintetizador quemado, lo que trato de dar nueva vida mediante la sustitución completa de hierro, es responsable de la generación de sonido en el sintetizador de software , construido sobre un mini-ordenador EmbedSky E8 con Linux a bordo. Como suele ser el caso, entre la publicación de la primera y segunda parte del artículo se tomó mucho más tiempo de lo previsto, pero, sin embargo, continuar.





En la primera parte del proceso de selección fue descrita plataforma de hardware para un nuevo sintetizador "cerebro" con una descripción de las características técnicas de la solución, brevemente iluminó el proceso de montaje de las librerías necesarias y los problemas encontrados en el proceso. Ahora, como el hierro, vemos cómo construir una matriz de teclado sintetizador, y entonces habrá más detalles dedicadas-partes blandas.


matriz del teclado h5> matriz del teclado sintetizador es muy similar a la matriz de teclado normal, que muchos fans de microcontroladores probablemente ya se conectan a su Arduino. Cada tecla provisto en el mismo sintetizador de uno (en los modelos más baratos) a dos (en los patrones a granel) cambia. El uso de dos interruptores adyacentes, uno de los cuales está cerrado por presión un poco antes que el otro, el microcontrolador puede determinar la fuerza condicional, o más bien la velocidad a la que la clave estaba deprimido, posteriormente, el sonido se reproduce con el volumen. Parece que este:



En el reverso de la placa colocada diodos que impiden "falsas" las pulsaciones de teclado de lectura, mientras que pulsar unas pocas teclas. Aquí hay una matriz de teclado concepto fragmento, que muestra los dos interruptores y diodos conectados a ellos:


Para escanear serie microcontrolador matriz tira la columna (que aparece marcada como N) para la fuente de alimentación y compruebe el nivel de las líneas (pines marcados como B). Si el nivel de una línea sería alto, correspondiendo así a la combinación de "línea columna" actualmente activo se pulsa la tecla. El diagrama muestra sólo una parte del teclado - Sólo 76 teclas (13 filas y 6 columnas x 2, dando un total de 156 posibles variantes para la digitalización de la matriz 25 y se usan los pines del microcontrolador). Escaneo de todo el teclado se lleva a cabo docenas de veces por segundo, y sin el conocimiento del Ejecutivo.

En mi sintetizador microcontrolador responsable de escanear el teclado era originalmente una de 8 bits de una sola vez microcontrolador programable Hitachi HD63B05V0, velocidad de reloj de 8 MHz y que tengan una ROM 4 KB y 192 bytes de memoria RAM. Desafortunadamente, este controlador se enciende inoperativo después del incidente con la nutrición, que se describe al comienzo del primer artículo. Pero, afortunadamente, fue casi pin compatible con la mejor de mis ATmega162 controlador, lo que hice y fue reemplazado por el corte y la soldadura sólo 2 pistas en el tablero, uno de los cuales - un RESET conclusión, resultaron ser completamente en el lugar equivocado, como HD63B05V0.

Como tal controlador inclusión no permitió que yo tome ventaja de una función de UART (como él también estaba en los otros resultados), a continuación, muestra información acerca de las pulsaciones de teclado que utilicé esta cara (sólo escritura) la implementación del puerto serie. Arranque Además, el microcontrolador se ha llenado TinySafeBoot , también utiliza una implementación de software de un puerto serie para una posible futura actualización de firmware. Porque como un lenguaje para el desarrollo rápido de todos sintetizador de software de alto nivel, elegí Python + QT5, entonces TinySafeBoot También escribí un módulo para Python, lo que le permite leer y escribir firmware en el microcontrolador AVR. Mismo microcontrolador AVR está conectado al puerto serie de la placa UART1 EmbedSky E8 y es alimentado por una tensión de 3,3 V, para evitar la necesidad de conversión de nivel.

El código fuente del firmware para AVR
 & lt; code class = & quot; cpp & quot; & gt; #include & lt; avr / io.h & gt; #include & lt; avr / interrupt.h & gt; #include & lt; util / delay.h & gt; #include & lt; string.h & gt; #include & quot; dbg_putchar.h & quot; #define MIDI_BASE 18 #define ZERO_BASE 28 #define KEYS_COUNT 76 #define hiz (puerto, dir) hacer {\ (dir) = 0; \ (Port) = 0; \} Mientras (0) #define alow (puerto, dir) hacer {\ (dir) = 0xFF; \ (Port) = 0; \} Mientras (0) teclas uint8_t [KEYS_COUNT]; / * Obtener el estado de una fila por su índice * a partir de 1 a 13 * / uint8_t getRow (idx uint8_t) {if (idx & lt; = 8) {return (PINC & amp; (1 & lt; & lt; (8 - idx) )); } Else if (idx & gt; = 9 & amp; & amp; idx & lt; = 11) {return (PINO & amp; (1 & lt; & lt; (11 - idx))); } Else if (== idx 12) {return (PINA & amp; (1 & lt; & lt; PIN6)); } Else if (== idx 13) {return (PINA & amp; (1 & lt; & lt; PIN4)); } Return 0; } Inline vacío activateColumn1 (idx uint8_t) {PORTD = 0x00 | (1 & lt; & lt; (8 - idx)); PORTB = 0x00; } Void activateColumn2 (idx uint8_t) {if (idx & lt; = 3) {PORTB = 0x00 | (1 & lt; & lt; (idx + 4)); PORTD = 0x00; } Else if (== idx 4) {PORTB = 0x00 | (1 & lt; & lt; PIN4); PORTD = 0x00; } Else if (== idx 5 || idx == 6) {PORTD = 0x00 | (1 & lt; & lt; (idx - 5)); PORTB = 0x00; }} Inline deactivateColumns void (void) {PORTD = 0x00; PORTB = 0x00; } Inline initPorts void (void) {hiz (PORTA, DDRA); hiz (PORTC, DDRC); hiz (PORTE, DDRE); PORTB = 0x00; DDRB = 0xfe; DDRD = 0xFF; } ResetRows Void (void) {/ * salida de baja * / alow (PORTC, DDRC); alow (PORTE, DDRE); / * No toque PA7 & amp; PA5 * / DDRA | = 0x5F; PORTA & amp; = ~ 0x5F; _delay_us (10); / * Volver a la entrada * / hiz (PORTC, DDRC) flotante; hiz (PORTE, DDRE); DDRA & amp; = ~ 0x5F; } Número de nota / * Base MIDI es de 25: C # 0 * / int main (void) {uint8_t fila, col, capa; nota uint8_t, offset; initPorts (); memset (teclas, 0, sizeof (claves)); dbg_tx_init (); dbg_putchar ("O"); dbg_putchar ('K'); while (1) {for (capa = 0; capa de & lt; 2; capa ++) {for (col = 1; col & lt; = 6; col ++) {if (capa!) activateColumn1 (col); otra cosa activateColumn2 (col); para (fila = 1; fila & lt; = 13; fila ++) {nota = 6 * fila + columna + MIDI_BASE; offset = nota - ZERO_BASE; (! capa) si (getRow (fila)) {if {/ * aumento contador de velocidad * / if (teclas [Separar] & lt; 254 & amp; & amp ;! (teclas [Separar] & amp; 0x80)) teclas [Separar] + +; } Else {if (! (Keys [Separar] & amp; 0x80)) {/ * generar note-on evento * / dbg_putchar (0x90); dbg_putchar (nota); / * Dbg_putchar (teclas [compensar]); * / dbg_putchar (0x7f); / * * / Dejar de contar teclas [compensar] | = 0x80; }}} Else {if (capa) continuar; si (teclas [Separar] & amp; 0x80) {/ * generar note off evento * / dbg_putchar (0x90); dbg_putchar (nota); dbg_putchar (0x00); / * * / Estado Reset teclas clave [compensar] = 0x00; }}} DeactivateColumns (); resetRows (); }}} Return 0; } & Lt; / Código & gt;  pre> 
módulo en Python para TinySafeBoot
 & lt; code class = & quot; python & quot; & gt; importación de serie importación binascii struct importación TSB clase sys intelhex importación de importación (objeto): '!' CONFIRM = SOLICITUD = "?" def __init __ (self, puerto): self.port = serial.Serial (puerto, velocidad de transmisión = 9600, timeout = 1) = 0 self.flashsz comprobar def (self): si no self.flashsz: elevar Excepción (& quot; No & quot activado ;) activar def (self): self.port.write (& quot; @ & quot;) (self.tsb, self.version, self.status, self.sign, self.pagesz, self.flashsz, self.eepsz ) = \ struct.unpack (& ​​quot; & lt; 3sHB3sBHH & quot;, self.port.read (14)) self.port.read (2) self.pagesz * = 2 self.flashsz * = 2 self.eepsz + = 1 assert (self.port.read () == self.CONFIRM) def rflash (sí, el progreso = Ninguno, size = 0): self.check () self.port.write (& quot; f & quot;) self.addr = 0 auto .flash = & quot; & quot; size = self.flashsz si no otra cosa tamaño tamaño mientras self.addr & lt; tamaño: si el progreso no es Ninguno: el progreso (& quot; leer & quot;, self.addr, tamaño) self.port.write (self.CONFIRM) page = self.port.read (self.pagesz) si len (página) = self.pagesz: Excepción raise (& quot; Recibido página demasiado corto:% d & quot;% len (página)) self.addr + = len (página) self.flash + = página regreso self.flash.rstrip ('\ xff') def wflash (sí, los datos, el progreso = None): si len (datos)% self.pagesz = 0: data = datos + & quot; \ xff & quot; * (Self.pagesz - (len (datos)% self.pagesz)) assert (len (datos)% self.pagesz == 0) self.check () self.port.write (& quot; F & quot;) self.addr = 0, afirmar (self.port.read () == self.REQUEST) mientras self.addr & lt; len (datos): si el progreso no es Ninguno: el progreso (& quot; escribe & quot;, self.addr, len (datos)) self.port.write (self.CONFIRM) self.port.write (datos [self.addr: auto .addr + self.pagesz]) self.addr + = afirman self.pagesz (self.port.read () == self.REQUEST) self.port.write (self.REQUEST) self.port.read return () = = self.CONFIRM def vFlash (sí, los datos, el progreso = None): fw = self.rflash (progreso, len (datos)) return fw == información def datos (self): print & quot; Bootloader Segura Tiny:% s & quot; % Self.tsb print & quot; Tamaño de la página:% d & quot; % Self.pagesz print & quot; tamaño de inflamación:% d & quot; % Self.flashsz print & quot; tamaño EEPROM:% d & quot; % Self.eepsz if __name__ == & quot; __ __ principal & quot;: importación argparse def progreso (op, addr, total): sys.stdout.write (& quot; \ r% s dirección: $% 0.4x / 0.4x $% & quot;% (op, addr, total)) sys.stdout.flush () parser = argparse.ArgumentParser () parser.add_argument (& quot; nombre de archivo & quot;, help = & quot; archivo de firmware en formato Intel HEX & quot;) parser.add_argument (& quot; - -device & quot;, help = & quot; El puerto serie a utilizar para la programación de & quot;, default = & quot; / dev / ttyUSB0 & quot;) args = parser.parse_args () = TSB TSB (args.device) tsb.activate () tsb.info ( ) fw = intelhex.IntelHex (args.filename) assert (tsb.wflash (fw.tobinstr (), el progreso)) assert (tsb.vflash (fw.tobinstr (), el progreso)) print & quot; \ NOK \ n & quot; & Lt; / código de & gt;  pre> 
Como programador para AVR primera vez que utiliza el programador basado en Launchpad MSP430 , de la que tengo disponible en varios pedazos y, a continuación, este milagro hecho en casa (funciona bien, por cierto), dio paso a venir de China programador TL866CS MiniPro. La sensación de la nueva programación es extremadamente positivo.

Gran detalle sobre el sintetizador de teclado del dispositivo y cómo escanear, incluyendo una forma muy original para escanear a través de la interfaz de microcontrolador AVR para chips de memoria externos tells en línea OpenMusicLabs

kernel de cocina con soporte para tiempo real Preferencia h5> En parte para conseguir un mayor control sobre el planificador y reducir el retraso (latencia) durante la reproducción de audio, y en parte por diversión, he decidido utilizar un kernel con el патчем PREEPMT RT , una de la característica principal de los cuales es que las interrupciones también se están convirtiendo en "proceso" que puede ser expulsado por el planificador basado en prioridades. El núcleo original, suministrado por Samsung S5PV210 procesador, basado en que está construido el sistema, basado en la versión del kernel 3.0.8, al parecer de Android. Ninguno de los parches RT_PREEMPT, disponibles en el sitio web del proyecto, diseñado para la versión del kernel (3.0.8), no quería que se superpone a la fuente sin conflicto, pero al final, para resolver todos los conflictos gestionados manualmente para aplicar una versión del parche 3.0.8-RT23.

Debido al hecho de que en el modificado de modo que el núcleo también se modificaron las estructuras básicas como spinlock y mutex, que dejó de estar vinculado con la suministrada en forma de objeto compilado archivos de drivers propietarios para algunos periféricos: Cámaras, controlador de pantalla táctil capacitiva, y, lo peor de todo , códec de audio. Vuelva a ellos más tarde, pero ahora desactivarlas y tratar de ejecutar la primera vez que el tablero con recién cosechado en tiempo real del núcleo y recibir un instante ... kernel panic. Él llegó incluso antes de comenzar el kgdb depurador (que, como se vio después, todavía no iba a funcionar, aunque comenzó), por lo que tuvo que insertar depuración printf-s en el archivo
  init / main.c  code>  pre>, la función 
  start_kernel  code>  pre>, para determinar el lugar en el que todo se desmorona. Así se puso de manifiesto que la última cosa que el kernel logra hacer es llamar a la función 
  hrtimers_init ()  code>  pre>, inicializar los temporizadores de alta resolución y las interrupciones. Este código depende de la plataforma, y ​​en nuestro caso está en 
  arch / brazo /-plat S5P / hr-tiempo-rtc.c  code>  pre>. Como he dicho antes, una de las principales características del kernel con un parche PREEMPT RT es que las interrupciones son flujos. Esto es posible en el núcleo de costumbre, pero el núcleo con PREEMPT RT predeterminado intenta hacer esos casi todas las interrupciones. Un análisis más detallado del código mostró que para estos flujos se utilizan kthreadd_task tarea, que es inicializado al final de la función 
  start_kernel  code>  pre> - mucho más tarde que inicializa temporizadores. La caída se debió también al hecho de que una interrupción del temporizador ha tratado de hacer la corriente principal, mientras que todavía kthreadd_task NULL. Solucionamos esto estableciendo interrupciones individuales que no se debe hacer streaming, bajo ninguna circunstancia, IRQF_NO_THREAD bandera que se añadió a la bandera de interrupción del temporizador en el 
  hr-tiempo-rtc.c  code>  pre>. Hurra! El kernel se carga, pero esto es sólo el principio ... 

Como he mencionado anteriormente, uno de los efectos secundarios es que el módulo encargado de la entrada / salida de audio, ya vincularse con el nuevo kernel. Esto fue en parte debido a que el núcleo con soporte PREEMPT RT (versión 3.0.8) LOSA gestión sólo la memoria, y en un principio el módulo se compiló con la inclusión de un SLUB mecanismo, que no sea compatible con el nuevo kernel. Sin embargo, tuve la suerte de trabajar en Kaspersky Lab, y convencí a mi colega descompilar archivos de controlador y codec usando la decompilador Hex-Rays para ARM, entonces logrado recrear casi por completo el código fuente. Casi - porque como resultado de la "nueva" controlador de audio interfaz comenzó por determinar, sin embargo, debido a algunas diferencias en el procedimiento de inicialización de bajo nivel registra sonido WM8960 chips se juega con los artefactos. Durante un tiempo he tratado de ajustar su conductor, pero luego elegí una manera más fácil - que envié a soporte técnico compañías chinas EmbedSky Tech, donde comprar un mini-ordenador, un parche con PREEMPT_RT, y les pedí que compilar para mí, y envía los archivos del controlador de audio. Los chicos respondieron rápidamente y me enviaron los archivos que suenan finalmente ganaron como se esperaba.

Por cierto, mientras yo estaba ocupado con su conductor descompilar, me encontré con que la kgdb depurador no funciona con la mía, ni con el núcleo inicial. Al final resultó que, por requería su trabajo para apoyar síncrono (sondeo) sondear el puerto serie, que estaba ausente en el controlador de dispositivo de puerto serie Samsung (
  drivers / tty / serie / samsung.c  code>  pre>). Añadí en el soporte de controlador necesario, basado en la   este parche, y luego gané un depurador. 

Cavando más lejos. Otro efecto secundario del nuevo kernel ha sido extremadamente bajo, con gran "retraso", la velocidad de los cuatro puertos serie que sufren SoC S5PV210, con el resultado de que no era el normal funcionamiento de la terminal a través de un puerto serie, y no estaba funcionando como debería reescribir controlador AVR, interrogar a un sintetizador de teclado. Traté de entender la razón, pero me di cuenta es que cada personaje en la terminal conduce a la generación de varios millones de interrupción del puerto serie - el núcleo no parece frenar para manejarlos. Al final, he resuelto este problema por el hecho de que por el IRQF_NO_THREAD bandera anterior hizo todo interrumpe puerto serie no streaming. Esta decisión no resultó muy agradable, ya que además de que el conductor Samsung tuvo que hacer cambios en los archivos
  serial_core.c  code>  pre> y 
  serial_core.h  code>  pre>, lo que afecta en general todos los puertos serie. Debido a que el núcleo con esta opción RT no se puede utilizar conductores spin_lock_t que NO_THREAD, y se debe utilizar raw_spinlock_t. 

En el núcleo original, que, como he dicho anteriormente, es compatible con una variedad de dispositivos periféricos, como cámaras de vídeo, codecs de hardware, HDMI, etc., de los 512 MB de RAM estaba disponible para sólo alrededor de 390 MB, y el resto se reserva para los dispositivos anteriores , siempre (incluso si en el proceso de configuración del kernel que se han desactivado). Muy desperdicio, sobre todo teniendo en cuenta que el extra de 120 MB de RAM sintetizador no es aún impiden el almacenamiento de muestras. Memoria está reservado en el archivo
  arch / brazo / mach-S5PV210 /-tq210.c mach  code>  pre>, que es el punto principal de la recopilación de toda la información sobre la configuración de dispositivos y una máquina específica (en nuestro caso - tarjeta). Al comentar sobre la selección de la memoria - Función de llamada 
  s5p_reserve_bootmem  code>  pre>, y obtenemos 120 MB de memoria adicional para el sintetizador 
.
Se realizó el último cambio en el kernel, los tamaños mínimos de búfer de audio, que era igual a la memoria de una página original, a una velocidad de muestreo de 44.100 Hz, 2 canales de 16 bits dieron unos 20 milisegundos - demasiado. Este valor ha sido cambiado en el archivo
  Sonido / soc / samsung / dma.c  code>  pre> 128 bytes, el tamaño mínimo de memoria intermedia se redujo a unos pocos milisegundos, sin comprometer la estabilidad y el rendimiento. 

fuente del kernel con PREEMPT RT y todas las modificaciones en GitHub

¿Cómo se comunica con el AVR LinuxSampler h5> AVR conectado al puerto serie de la mini-computadora, y el software escupe en su mensaje UART MIDI listo. Con el fin de ahorrarse de tener que escribir los conductores, se decidió utilizar como medio de transporte para todo el audio y el servidor JACK MIDI-datos. Pequeño prilozhenitse en C está conectado al puerto serie, se registra como JACK MIDI-OUT y empieza a ruta allí todo recibidas MIDI-puestos y JACK ya los entrega LinuxSampler. Barato y alegre.

El código fuente de la aplicación como un puente entre un puerto serie y JACK
 & lt; code class = & quot; cpp & quot; & gt; #include & lt; stdio.h & gt; #include & lt; stdlib.h & gt; #include & lt; sys / types.h & gt; #include & lt; sys / time.h & gt; #include & lt; unistd.h & gt; #include & lt; assert.h & gt; #include & lt; string.h & gt; # include & lt; sysexits.h & gt; #include & lt; errno.h & gt; #include & lt; signal.h & gt; #include & lt; fcntl.h & gt; #include & lt; termios.h & gt; #include & lt; conector / jack.h & gt; #include & lt; conector / midiport.h & gt; #define UART_SPEED B9600 jack_port_t * output_port; jack_client_t * jack_client = NULL; int input_fd; vacío init_serial (int fd) {struct termios termios; res int; res = tcgetattr (fd, & amp; termios); si (res & lt; 0) {fprintf (stderr, & quot; termios obtener error:% s \ n & quot;, strerror (errno)); salida (EXIT_FAILURE); } cfsetispeed (& Amp; termios, UART_SPEED); cfsetospeed (& Amp; termios, UART_SPEED); termios.c_iflag & amp; = ~ (IGNPAR | IXON | IXOFF); termios.c_iflag | = IGNPAR; termios.c_cflag & amp; = ~ (CSIZE | PARENB | CSTOPB | CREAD | CLOCAL); termios.c_cflag | = CS8; termios.c_cflag | = CREAD; termios.c_cflag | = CLOCAL; termios.c_lflag & amp; = ~ (ICANON | ECHO); termios.c_cc [VMIN] = 3; termios.c_cc [VTIME] = 0; res = tcsetattr (fd, TCSANOW, & amp; termios); si (res & lt; 0) {fprintf (stderr, & quot; termios establece error:% s \ n & quot;, strerror (errno)); salida (EXIT_FAILURE); }} Get_time doble (void) {dobles segundos; int ret; struct timeval tv; ret = gettimeofday (& amp; tv, NULL); si (r) {perror (& quot; gettimeofday & quot;); salida (EX_OSERR); } Segundos = tv.tv_sec + tv.tv_usec / 1.000.000,0; volver segundo; } Get_delta_time doble (void) {double estática previamente = -1,0; duplicar ahora; delta doble; ahora = get_time (); si (anteriormente == -1,0) {anteriormente = ahora; return 0; } Delta = ahora - con anterioridad; previamente = ahora; afirmar (delta & gt; = 0,0); volver delta; } nframes_to_ms estáticos dobles (nFrames jack_nframes_t) {sr jack_nframes_t; sr = jack_get_sample_rate (jack_client); afirmar (sr & gt; 0); Retorno (nframes * 1000.0) / (doble) sr; } nframes_to_seconds estáticos dobles (nFrames jack_nframes_t) {nframes_to_ms retorno (nFrames) / 1000.0; } ms_to_nframes estáticas jack_nframes_t (doble ms) {sr jack_nframes_t; sr = jack_get_sample_rate (jack_client); afirmar (sr & gt; 0); volver ((dobles) sr ms *) / 1000.0; } seconds_to_nframes estáticas jack_nframes_t (doble segundos) {ms_to_nframes retorno (segundos * 1000.0); } Estático process_midi_output void (nframes jack_nframes_t) {int t, res; void * port_buffer; Char midi_buffer [3]; jack_nframes_t last_frame_time; port_buffer = jack_port_get_buffer (output_port, nframes); si (port_buffer == NULL) {printf (& quot;. jack_port_get_buffer falló, no puede enviar nada \ n & quot;); volver; } Jack_midi_clear_buffer (port_buffer); last_frame_time = jack_last_frame_time (jack_client); t = seconds_to_nframes (get_delta_time ()); res = read (input_fd, midi_buffer, sizeof (midi_buffer));