New life of old synthesizer. Part 2

Continued stories about the old burned-synthesizer, which I try to breathe new life by complete replacement of iron, is responsible for generating sound on software synthesizer , built on a mini-computer EmbedSky E8 with Linux on board. As is often the case, between the publication of the first and second part of the article took much longer than planned, but, nevertheless, continue.





In the early part of the selection process was described hardware platform for a new "brain" synthesizer with a description of the technical characteristics of the solution, briefly illuminated the process of assembling the necessary libraries and the problems encountered in the process. Now, as for iron, we see how to construct a synthesizer keyboard matrix, and then there will be more details devoted soft-parts.


keyboard matrix h5> synthesizer keyboard matrix is ​​very similar to the regular keyboard matrix, which many fans of microcontrollers probably already connect to their Arduino. Each key provided thereon synthesizer from one (in the cheapest models) to two (in the bulk patterns) switches. Using two adjacent switches, one of which is closed by pressing a little bit earlier than the other, the microcontroller can determine the conditional force, or rather the speed at which the key was depressed subsequently to the sound was reproduced with the volume. It looks like this:



On the reverse side of the board placed diodes that prevent "false" reading keystrokes while pressing a few keys. Here's a snippet concept keyboard matrix, which shows the two switches and diodes connected to them:


To scan matrix microcontroller series pulls column (shown marked as N) to the power supply and check the level on the lines (pins marked as B). If the level of a line would be high, thus corresponding to the currently active combination "column-line" key is pressed. The diagram shows only part of the keyboard - it only 76 keys (13 rows and 6 columns x 2, giving a total of 156 possible variants for scanning matrix 25 and the microcontroller pins are used). Scanning the entire keyboard is carried out dozens of times per second, and unbeknownst to the Executive.

In my synthesizer microcontroller responsible for scanning the keyboard was originally an 8-bit one-time programmable microcontroller Hitachi HD63B05V0, clocked at 8 MHz and having a 4 KB ROM and 192 bytes of RAM memory. Unfortunately, this controller is turned inoperative after the incident with nutrition, described at the beginning of the first article. But, fortunately, he was almost pin compatible with the best of my controller ATmega162, which I did and was replaced by cutting and soldering only 2 tracks on the board, one of which - a conclusion RESET, turned out to be completely in the wrong place, like HD63B05V0.

As such inclusion controller did not allow me to take advantage of built-in UART (as he also was on the other findings), then displays information about the keystrokes I used this sided (write only) the implementation of the serial port. Also, the microcontroller has been filled boot TinySafeBoot , also uses a software implementation of a serial port for a possible future firmware update. Because as a language for rapid development of all high-level software synthesizer, I chose Python + Qt5, then TinySafeBoot I also wrote a module for Python, which allows you to read and write firmware into the microcontroller AVR. Himself AVR microcontroller is connected to the serial port UART1 board EmbedSky E8 and is powered by a voltage 3.3V, to avoid the need for level conversion.

The source code of the firmware for 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 (port, dir) do {\ (dir) = 0; \ (Port) = 0; \} While (0) #define alow (port, dir) do {\ (dir) = 0xff; \ (Port) = 0; \} While (0) uint8_t keys [KEYS_COUNT]; / * Get state of a row by its index * starting from 1 to 13 * / uint8_t getRow (uint8_t idx) {if (idx & lt; = 8) {return (PINC & amp; (1 & lt; & lt; (8 - idx) )); } Else if (idx & gt; = 9 & amp; & amp; idx & lt; = 11) {return (PINE & 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 void activateColumn1 (uint8_t idx) {PORTD = 0x00 | (1 & lt; & lt; (8 - idx)); PORTB = 0x00; } Void activateColumn2 (uint8_t idx) {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 void deactivateColumns (void) {PORTD = 0x00; PORTB = 0x00; } Inline void initPorts (void) {hiz (PORTA, DDRA); hiz (PORTC, DDRC); hiz (PORTE, DDRE); PORTB = 0x00; DDRB = 0xfe; DDRD = 0xff; } Void resetRows (void) {/ * output low * / alow (PORTC, DDRC); alow (PORTE, DDRE); / * Do not touch PA7 & amp; PA5 * / DDRA | = 0x5f; PORTA & amp; = ~ 0x5f; _delay_us (10); / * Back to floating input * / hiz (PORTC, DDRC); hiz (PORTE, DDRE); DDRA & amp; = ~ 0x5f; } / * Base MIDI note number is 25: C # 0 * / int main (void) {uint8_t row, col, layer; uint8_t note, offset; initPorts (); memset (keys, 0, sizeof (keys)); dbg_tx_init (); dbg_putchar ('O'); dbg_putchar ('K'); while (1) {for (layer = 0; layer & lt; 2; layer ++) {for (col = 1; col & lt; = 6; col ++) {if (! layer) activateColumn1 (col); else activateColumn2 (col); for (row = 1; row & lt; = 13; row ++) {note = 6 * row + col + MIDI_BASE; offset = note - ZERO_BASE; if (getRow (row)) {if (! layer) {/ * increase velocity counter * / if (keys [offset] & lt; 254 & amp; & amp;! (keys [offset] & amp; 0x80)) keys [offset] + +; } Else {if (! (Keys [offset] & amp; 0x80)) {/ * generate note-on event * / dbg_putchar (0x90); dbg_putchar (note); / * Dbg_putchar (keys [offset]); * / dbg_putchar (0x7f); / * Stop counting * / keys [offset] | = 0x80; }}} Else {if (layer) continue; if (keys [offset] & amp; 0x80) {/ * generate note off event * / dbg_putchar (0x90); dbg_putchar (note); dbg_putchar (0x00); / * Reset key state * / keys [offset] = 0x00; }}} DeactivateColumns (); resetRows (); }}} Return 0; } & Lt; / code & gt;  pre> 
module in Python for TinySafeBoot
 & lt; code class = & quot; python & quot; & gt; import serial import binascii import struct import intelhex import sys class TSB (object): CONFIRM = '!' REQUEST = '?' def __init __ (self, port): self.port = serial.Serial (port, baudrate = 9600, timeout = 1) self.flashsz = 0 def check (self): if not self.flashsz: raise Exception (& quot; Not activated & quot ;) def activate (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 (self, progress = None, size = 0): self.check () self.port.write (& quot; f & quot;) self.addr = 0 self .flash = & quot; & quot; size = self.flashsz if not size else size while self.addr & lt; size: if progress is not None: progress (& quot; read & quot ;, self.addr, size) self.port.write (self.CONFIRM) page = self.port.read (self.pagesz) if len (page)! = self.pagesz: raise Exception (& quot; Received page too short:% d & quot;% len (page)) self.addr + = len (page) self.flash + = page return self.flash.rstrip ('\ xff') def wflash (self, data, progress = None): if len (data)% self.pagesz! = 0: data = data + & quot; \ xff & quot; * (Self.pagesz - (len (data)% self.pagesz)) assert (len (data)% self.pagesz == 0) self.check () self.port.write (& quot; F & quot;) self.addr = 0, assert (self.port.read () == self.REQUEST) while self.addr & lt; len (data): if progress is not None: progress (& quot; write & quot ;, self.addr, len (data)) self.port.write (self.CONFIRM) self.port.write (data [self.addr: self .addr + self.pagesz]) self.addr + = self.pagesz assert (self.port.read () == self.REQUEST) self.port.write (self.REQUEST) return self.port.read () = = self.CONFIRM def vflash (self, data, progress = None): fw = self.rflash (progress, len (data)) return fw == data def info (self): print & quot; Tiny Safe Bootloader:% s & quot; % Self.tsb print & quot; Page size:% d & quot; % Self.pagesz print & quot; Flash size:% d & quot; % Self.flashsz print & quot; EEPROM size:% d & quot; % Self.eepsz if __name__ == & quot; __ main __ & quot ;: import argparse def progress (op, addr, total): sys.stdout.write (& quot; \ r% s address: $% 0.4x / $% 0.4x & quot;% (op, addr, total)) sys.stdout.flush () parser = argparse.ArgumentParser () parser.add_argument (& quot; filename & quot ;, help = & quot; firmware file in Intel HEX format & quot;) parser.add_argument (& quot; - -device & quot ;, help = & quot; Serial port to use for programming & 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 (), progress)) assert (tsb.vflash (fw.tobinstr (), progress)) print & quot; \ nOK \ n & quot; & Lt; / code & gt;  pre> 
As a programmer for AVR I first used the programmer based on Launchpad MSP430 , of which I have available in several pieces, and then this homemade miracle (works fine, by the way), gave way to come from China programmer TL866CS MiniPro. The feel of the new programming is extremely positive.

Great detail about the device keyboard synthesizer and how to scan, including a very original way to scan through the AVR microcontroller interface for external memory chips tells online OpenMusicLabs

Cooking kernel with support for Realtime Preemption h5> Partly to get more control over the scheduler and reduce the delay (latency) when playing audio, and partly for fun, I decided to use a kernel with the патчем PREEPMT RT , one of the main feature of which is that interrupts are also becoming "process" that can be pushed out by the scheduler based on priority. The original core, provided by Samsung processor S5PV210, based on which the system is constructed, based on kernel version 3.0.8, apparently from Android. None of the patches RT_PREEMPT, available on the project website, designed for the kernel version (3.0.8), did not want to be superimposed on the source without conflict, but in the end, to resolve all conflicts manually managed to apply a patch version 3.0.8-rt23.

Due to the fact that in the modified so that the nucleus were also modified the basic structures such as spinlock and mutex, it ceased to be linked with the supplied in the form of compiled object files proprietary drivers for some peripherals: cameras, capacitive touchscreen controller, and, worst of all , audio codec. Return to them later, but now disable them and try to run the first time the board with freshly harvested kernel real time and receive an instant ... kernel panic. He came even before starting the debugger kgdb (which, as it turned out, still would not work, even if started), so it had to insert debugging printf-s in the file
  init / main.c  code>  pre>, the function 
  start_kernel  code>  pre>, to determine the place where everything falls apart. Thus it became clear that the last thing the kernel manages to do is call the function 
  hrtimers_init ()  code>  pre>, initializing high-resolution timers and interrupts. This code depends on the platform, and in our case is in 
  arch / arm / plat-s5p / hr-time-rtc.c  code>  pre>. As I said earlier, one of the main features of the kernel with a patch PREEMPT RT is that interrupts are flows. This is possible in the usual kernel, but the kernel with PREEMPT RT default tries to make those almost all interrupts. Further analysis of the code showed that for these flows are used task kthreadd_task, which is initialized at the end of the function 
  start_kernel  code>  pre> - much later than initializes timers. The drop was due also to the fact that a timer interrupt has tried to make the core stream, while still kthreadd_task NULL. We solve this by setting individual interrupts that you should not do streaming, under any circumstances, flag IRQF_NO_THREAD which was added to the timer interrupt flag in the 
  hr-time-rtc.c  code>  pre>. Hooray! The kernel is booted, but this is only the beginning ... 

As I mentioned above, one of the side effects is that the module responsible for the audio input / output, longer be linked with the new kernel. This was partly because the kernel with support PREEMPT RT (version 3.0.8) only memory management SLAB, and initially the module was compiled with the inclusion of a mechanism SLUB, which is not supported by the new kernel. However, I was lucky enough to work at Kaspersky Lab, and I persuaded my colleague decompile driver files and codec using the Hex-Rays decompiler for ARM, then managed to almost completely recreate the source code. Almost - because as a result of the "new" audio interface driver began to be determined, however, due to some differences in the low-level initialization procedure registers chip WM8960 sound is played with the artifacts. For a while I tried to tweak your driver, but then chose an easier way - I sent to tech support Chinese companies EmbedSky Tech, where to buy a mini-computer, a patch with PREEMPT_RT, and asked them to compile for me and send the audio driver files. The guys responded quickly and sent me the files that sound finally earned as expected.

By the way, while I was busy with his decompile driver, I found that the debugger kgdb does not work with mine, nor with the original kernel. As it turned out, for his work required to support synchronous (polling) poll the serial port, which was absent in the serial port device driver Samsung (
  drivers / tty / serial / samsung.c  code>  pre>). I added in the driver support required, based on the   this patch, and then earned a debugger. 

Digging further. Another side effect of the new kernel has been extremely low, with large "lag", the speed of all four serial ports suffering SoCs S5PV210, with the result that was not the normal operation of the terminal through a serial port, and was not working as it should rewrite controller AVR, interrogating a keyboard synthesizer. I tried to understand the reason, but I noticed is that each character in the terminal leads to the generation of several million serial port interrupt - the kernel does not seem to slow to handle them. In the end, I solved this problem by the fact that by the above flag IRQF_NO_THREAD did all interrupts serial port non-streaming. This decision did not turn out very nice, because in addition to the driver Samsung had to make changes to the files
  serial_core.c  code>  pre> and 
  serial_core.h  code>  pre>, affecting generally all serial ports. Because the kernel with PREEMPT RT can not be used spin_lock_t drivers who NO_THREAD, and should be used raw_spinlock_t. 

In the original nucleus, which, as I said above, supports a variety of peripheral devices such as video cameras, hardware codecs, HDMI, etc., of the 512 MB of RAM was available to only about 390 MB, and the rest was reserved for the above devices , always (even if in the process of configuring the kernel they have been disabled). Very wasteful, especially considering that the extra 120 MB of RAM synthesizer is not even prevent the storage of samples. Memory is reserved in the file
  arch / arm / mach-s5pv210 / mach-tq210.c  code>  pre>, which is the main point of collecting all the information about the configuration of devices and a specific machine (in our case - card). Commenting on the selection of memory - function call 
  s5p_reserve_bootmem  code>  pre>, and we get 120 MB of additional memory for the synthesizer. 

The last change was made to the kernel, the minimum sizes for audio buffer, which was equal to the original one-page memory, at a sampling rate of 44100 Hz, 2 channels of 16 bits gave about 20 milliseconds - too much. This value has been changed in the file
  sound / soc / samsung / dma.c  code>  pre> 128 bytes, then the minimum buffer size was reduced to a few milliseconds without compromising stability and performance. 

kernel source with PREEMPT RT and all modifications on GitHub

How do you communicate with the AVR LinuxSampler h5> AVR connected to the serial port of the mini-computer, and the software spits in your UART MIDI-ready message. In order to save yourself from having to write drivers, it was decided to use as a transport for all audio and MIDI-data server JACK. Small prilozhenitse on C is connected to the serial port, registers itself as JACK MIDI-OUT and starts to route there all received MIDI-posts and JACK already delivers them LinuxSampler. Cheap and cheerful.

The source code for application as a bridge between a serial port and 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; jack / jack.h & gt; #include & lt; jack / midiport.h & gt; #define UART_SPEED B9600 jack_port_t * output_port; jack_client_t * jack_client = NULL; int input_fd; void init_serial (int fd) {struct termios termios; int res; res = tcgetattr (fd, & amp; termios); if (res & lt; 0) {fprintf (stderr, & quot; Termios get error:% s \ n & quot ;, strerror (errno)); exit (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); if (res & lt; 0) {fprintf (stderr, & quot; Termios set error:% s \ n & quot ;, strerror (errno)); exit (EXIT_FAILURE); }} Double get_time (void) {double seconds; int ret; struct timeval tv; ret = gettimeofday (& amp; tv, NULL); if (ret) {perror (& quot; gettimeofday & quot;); exit (EX_OSERR); } Seconds = tv.tv_sec + tv.tv_usec / 1000000.0; return seconds; } Double get_delta_time (void) {static double previously = -1.0; double now; double delta; now = get_time (); if (previously == -1.0) {previously = now; return 0; } Delta = now - previously; previously = now; assert (delta & gt; = 0.0); return delta; } Static double nframes_to_ms (jack_nframes_t nframes) {jack_nframes_t sr; sr = jack_get_sample_rate (jack_client); assert (sr & gt; 0); return (nframes * 1000.0) / (double) sr; } Static double nframes_to_seconds (jack_nframes_t nframes) {return nframes_to_ms (nframes) / 1000.0; } Static jack_nframes_t ms_to_nframes (double ms) {jack_nframes_t sr; sr = jack_get_sample_rate (jack_client); assert (sr & gt; 0); return ((double) sr * ms) / 1000.0; } Static jack_nframes_t seconds_to_nframes (double seconds) {return ms_to_nframes (seconds * 1000.0); } Static void process_midi_output (jack_nframes_t nframes) {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); if (port_buffer == NULL) {printf (& quot; jack_port_get_buffer failed, can not send anything. \ n & quot;); return; } 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));







Tags

See also

New and interesting