This post collects observations when using MAix GO board with Arduino IDE, call Maixduino.

Macros how special pins on board MAix GO are called, can be found in pins_arduino.h of MAix GO., like PIN_KEY_PRESS which has an external pull up resistor (see schematic).

When testing example code of libraries function Serial.begin() seems to need a little time to initialize. Console output worked after adding 300mS delay. (How much is the minimal delay required?):


Below libraries are listed which have example code that compiles and runs with MAix GO board (maybe the code needs minimal modification like replacing BUILTIN_LED by LED_BUILTIN; use compiler error messages – Preferences/Show verbose output during compilation):

  • WiFiEsp (included with Maixduino board addon of Arduino IDE)
  • RingBuffer by locoduino (installable with Arduino Library Manager)
    (Maybe EspRingBuffer coming as part of of WiFiEsp bundled with Maixduino, folder src/utility, is of interest as well.)
  • ArduinoJson by Benoît Blanchan (Arduino Library Manager)
  • ArduinoMqtt by Oleg Kovalenk which is based on eclipse paho (Arduino Library Manager)
    Use the example ConnectEsp8266WiFiClient and replace: ESP8266WiFi.h by WiFiEsp.h (included in Maixduino), WiFiClient by WiFiEspClient, ESP.reset() by WiFi.reset() and copy WiFi setup code from example of WiFiEsp. – If not properly initialized might stop k210 cpu with illegal instruction.
  • pubsubclient by Nick O’Leary (Arduino Library Manager). Same remarks as shown above (ArduinoMqtt).

According to Kendryte K210 datasheet (available here) the speed of the 4 uart interfaces of K210 can run with up to 5 MHz baud rate. Communication with esp8285 used as wifi chip runs by default with 115200 baud and seems to run with 2000000 baud as well. (Tested with sketch Examples/Maix Go/WiFiEsp/Test/EspDebug: use command AT+UART_CUR:2000000,8,1,0,1). AT command reference is here. Using the _CUR command means that after reboot the default settings get restored. Storing the settings permanently bears the risk to make esp8266 inaccessible (if not reflashed).

Source of AT command firmware is here at github, if you select tag v2.2.1 (examples/at). – Install compiler (folder dist in or and add it to path. Inside SDK folder copy folder examples/at to root folder of SDK. Run script gen_misc and choose: boot_v1.2+, user1.bin (or user2.bin), spi 40MHz, spi DOUT, 1024KB (512KB+512KB); result is in folder bin/. Uploading to esp8285 would require to shorten ground and the pad GPIO0 in the corner of the antenna connector (info from sipeed forum).

notes about using to flash firmware (hide expanded file):
to verify size of flash
python utils/ --port /dev/cu.usbserial-143110 --baud 115200 flash_id

nodemcu board, 4MB=32000kbit, successfully flashed with
python utils/ --port /dev/cu.usbserial-143110 --baud 115200 write_flash 0x00000 bin/eagle.flash.bin 0x10000 bin/eagle.irom0text.bin 0x7E000 bin/blank.bin 0x3FE000 bin/blank.bin 0x3FC000 bin/esp_init_data_default.bin 
(tested by konsole of arduino ide; firmware has default baud rate of 115200; boot messages are sent with 74880 baud ->technical reference of esp8266)

esp8285, 1MB=4096kbit, should be flashable with
python utils/ --port /dev/cu.usbserial-143110 --baud 115200 write_flash 0x00000 bin/eagle.flash.bin 0x10000 bin/eagle.irom0text.bin 0x7E000 bin/blank.bin 0xFE000 bin/blank.bin 0xFC000 bin/esp_init_data_default.bin

To use WiFiEsp lib with 2000000 baud without setting it permanently WiFiEsp has to be patched: by default WiFi.init(&Serial1) function does call AT+RST which reboots esp8285. In patched version you can call WiFi.init(&Serial1, false) to avoid this reboot:

WiFiEsp_disable_at_rst_in_init.diff (hide expanded diff file):
diff -ur orig/WiFiEsp.cpp new/WiFiEsp.cpp
--- orig/WiFiEsp.cpp	2019-07-15 17:14:20.000000000 +0200
+++ new/WiFiEsp.cpp	2019-08-07 11:32:14.000000000 +0200
@@ -33,8 +33,14 @@
 void WiFiEspClass::init(Stream* espSerial)
+	init(espSerial, true);
+void WiFiEspClass::init(Stream* espSerial, bool run_at_rst)
     LOGINFO(F("Initializing ESP module"));
-	EspDrv::wifiDriverInit(espSerial);
+	EspDrv::wifiDriverInit(espSerial, run_at_rst);
diff -ur orig/WiFiEsp.h new/WiFiEsp.h
--- orig/WiFiEsp.h	2019-07-15 17:14:20.000000000 +0200
+++ new/WiFiEsp.h	2019-08-07 11:33:01.000000000 +0200
@@ -51,6 +51,8 @@
 	static void init(Stream* espSerial);
+	static void init(Stream* espSerial, bool do_at_rst);
 	* Get firmware version
diff -ur orig/utility/EspDrv.cpp new/utility/EspDrv.cpp
--- orig/utility/EspDrv.cpp	2019-07-15 17:14:20.000000000 +0200
+++ new/utility/EspDrv.cpp	2019-08-07 12:04:48.000000000 +0200
@@ -70,8 +70,15 @@
 uint8_t EspDrv::_remoteIp[] = {0};
 void EspDrv::wifiDriverInit(Stream *espSerial)
+	wifiDriverInit(espSerial, true);
+void EspDrv::wifiDriverInit(Stream *espSerial, bool run_at_rst)
 	LOGDEBUG(F("> wifiDriverInit"));
 	EspDrv::espSerial = espSerial;
@@ -95,7 +102,7 @@
-	reset();
+	reset(run_at_rst);
 	// check firmware version
@@ -114,13 +121,23 @@
 void EspDrv::reset()
+	reset(true);
+void EspDrv::reset(bool run_at_rst)
 	LOGDEBUG(F("> reset"));
-	sendCmd(F("AT+RST"));
-	delay(3000);
-	espEmptyBuf(false);  // empty dirty characters from the buffer
+	if(run_at_rst)
+	{
+		sendCmd(F("AT+RST"));
+		delay(3000);
+		espEmptyBuf(false);  // empty dirty characters from the buffer
+	}
 	// disable echo of commands
diff -ur orig/utility/EspDrv.h new/utility/EspDrv.h
--- orig/utility/EspDrv.h	2019-07-15 17:14:20.000000000 +0200
+++ new/utility/EspDrv.h	2019-08-07 11:38:57.000000000 +0200
@@ -123,6 +123,9 @@
     static void wifiDriverInit(Stream *espSerial);
+    static void wifiDriverInit(Stream *espSerial, bool run_at_rst);
     /* Start Wifi connection with passphrase
      * param ssid: Pointer to the SSID string.
@@ -279,6 +282,8 @@
 	static bool ping(const char *host);
     static void reset();
+    static void reset(bool do_at_rst);
     static void getRemoteIpAddress(IPAddress& ip);
     static uint16_t getRemotePort();

To be continued.

The Kendryte K210 chipset of Sipeed MAix GO (see previous post) has an audio processing unit (APU) which allows beam forming of incoming audio. As accessory of Sipeed MAix GO there is an 6+1 circular microphone array (specs, buy). This post describes how to use the APU of K210 with this array.

Either use arduino ide and add Maixduino(k210) with Boards Manager. (First edit Preferences/Additional Boards (comma separated): The code files shown below are then “tabs” inside sketch yourname, e.g. apu6. (main.c becomes the content of the first tab, automatically named yourname).

Or create a new project folder in kendryte standalone sdk (details). Save the code files shown below in this folder. This code is based on the apu example of kendryte standalone sdk. Main reference are the code files of standalone sdk, mainly i2s.h, i2s.c, sysctl.h, sysctl.c, apu.h, apu.c. The code to use the led ring coming with the microphone array is based on this code of GitJer. The schematics have been used, to look up how the MAix GO board (schematic, assembly) and the microphone array (schematic, assembly) are connected.

apu6.ino or main.c (hide expanded source code):
// file apu6.ino in arduino ide, file main.c in standalone sdk
// use K210 apu to read from 6+1 mic array, auto detect direction, send sound to speaker

// switching from kendryte standalone sdk to arduino ide (which is based on this sdk):
// Using arduino ide:
// - stdout not defined by default, thus replace printf and printk by pprintf (defined below)
// - arduino works with c++ compiler, sdk works with c compiler
//   - ino-files (shown without extension in arduino ide) are handled as c++ files
//   - additional files (=tabs in arduino ide) are handled as c files (*.h, *.c) or c++ (*.hpp, *.cpp)
//   - extern "C"{...}; allows to include c files from c++ files
#ifdef __cplusplus
extern "C"{
#include "init.h"
// only required for standalone sdk
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <printf.h>
// end only ...
#include <apu.h>
#include "apu2.h"
#include "mic_array_leds.h"
#ifdef __cplusplus

#ifdef ARDUINO
// in case of standalone sdk pprintf is defined as macro in apu2.h: #define pprintf(...) printf(__VA_ARGS__)
void pprintf(char *fmt, ... ){
        char buf[128]; // resulting string limited to 128 chars
        va_list args;
        va_start (args, fmt );
        vsnprintf(buf, 128, fmt, args);
        va_end (args);

int count;
int assert_state;
int32_t dir_max_prev = 0;
uint16_t contex_prev = APU_DIR_CHANNEL_MAX;
uint16_t contex_prev_prev = APU_DIR_CHANNEL_MAX;

int dir_logic(void)
  int32_t dir_sum_array[APU_DIR_CHANNEL_MAX];
  int32_t dir_sum = 0;
  int32_t dir_sum_all = 0;
  int32_t dir_max = 0;
  uint16_t contex = 0;

  for (size_t ch = 0; ch < APU_DIR_CHANNEL_MAX; ch++) { // 
      dir_sum = 0;
      for (size_t i = 0; i < APU_DIR_CHANNEL_SIZE; i++) {
         dir_sum += (int32_t)APU_DIR_BUFFER[ch][i] * (int32_t)APU_DIR_BUFFER[ch][i];
      dir_sum_array[ch] = dir_sum / APU_DIR_CHANNEL_SIZE; // averaged square sum of one dir channel
  for (size_t ch = 0; ch < APU_DIR_CHANNEL_MAX; ch++) { // dir_sum_all: dir_sum + 50% of dir_sum of both adjacent neighbours
    dir_sum_all = dir_sum_array[ch]
       + (dir_sum_array[(ch+1) % APU_DIR_CHANNEL_MAX]+dir_sum_array[(ch+APU_DIR_CHANNEL_MAX-1) % APU_DIR_CHANNEL_MAX])/2;
    if(dir_sum_all > dir_max){
        dir_max = dir_sum_all;
        contex = ch;
//      pprintf("%d  ", dir_sum);
//    pprintf("   %d\n", contex);
//    pprintf("\n");

  if(contex == contex_prev &amp;&amp; contex_prev == contex_prev_prev) { // only use dir channel "contex" if three consecutive times unchanged
    for (int l=0; l<12; l++) {
        set_light(l, dir_max/APU_DIR_CHANNEL_SIZE, dir_max/APU_DIR_CHANNEL_SIZE, 0);
  //        set_light(l, 0, 0, 1);
        set_light(l, 0, 0, 1);
#ifdef ARDUINO
    apu_voc_set_direction((en_bf_dir)contex); // use direction deteced by APU_DIR
#ifndef ARDUINO
    apu_voc_set_direction(contex); // use direction deteced by APU_DIR
//  apu_voc_set_direction(6); // use fixed direction for testing
//  set_light((int)((12*6)/16.), 32, 32, 0);
//  write_pixels();

  apu_dir_enable(); // if commented out: direction gets determined (APU_DIR) and set (APU_VOC) only once
  dir_max_prev = dir_max;
  contex_prev_prev = contex_prev;
  contex_prev = contex;
  return 0;

int voc_logic(void)
  return 0;

void setup(void)
   // Start the UART
#ifdef ARDUINO
   Serial.begin(115200) ;
  uint32_t real_freq, real_freq_source;
  real_freq = sysctl_cpu_get_freq();
  sysctl_pll_set_freq(SYSCTL_PLL0, 320000000UL);
  sysctl_pll_set_freq(SYSCTL_PLL1, 160000000UL);
  pprintf("CPU real freq: %u\n", real_freq);
//  real_freq = sysctl_pll_set_freq(SYSCTL_PLL2, 34000000UL); // 44.17kHz sampl., 4 chann. 16bit and threshold 5 (or 1 chan, thresh 47, 22.14kHz)
  real_freq = sysctl_pll_set_freq(SYSCTL_PLL2, 50375000UL); // 43.73kHz sampl., 4 chann. 24bit and threshold 5 (or 1 chan, thresh 47, 21.86kHz)
//  real_freq = sysctl_pll_set_freq(SYSCTL_PLL2, 68000000UL); // 88.54kHz sampl., 4 chann. 16bit and threshold 5 (or 1 chan, thresh 47, 44.27kHz)
  real_freq_source = sysctl_clock_get_freq(SYSCTL_CLOCK_PLL2);
  pprintf("PLL2 real freq: %u (source %u)\n", real_freq, real_freq_source);
  pprintf("git id: %u\n", sysctl->git_id.git_id);
  pprintf("init start.\n");
  clear_csr(mie, MIP_MEIP);
  pprintf("init done.\n");
  set_csr(mie, MIP_MEIP);
  set_csr(mstatus, MSTATUS_MIE);

void loop() {
  // put your main code here, to run repeatedly:
  while (1) {
    if (dir_logic_count > 0) {
      while (--dir_logic_count != 0) {
//        pprintf("[warning]: %s, restart before prev callback has end\n",
//               "dir_logic");
    if (voc_logic_count > 0) {
      while (--voc_logic_count != 0) {
//        pprintf("[warning]: %s, restart before prev callback has end\n",
//               "voc_logic");


#ifndef ARDUINO
int main(void)
init.c (hide expanded source code):
// file init.c
#include "init.h"
#include <stddef.h>
#include <stdio.h>
#include "gpio.h"
#include "apu.h"
#include "apu2.h"
#include "mic_array_leds.h"

//#define APU_DMA_ENABLE 1

uint64_t dir_logic_count;
uint64_t voc_logic_count;


int int_apu(void *ctx)
  apu_int_stat_t rdy_reg = apu->bf_int_stat_reg;

  if (rdy_reg.dir_search_data_rdy) {

    static int ch;

    ch = (ch + 1) % 16;
    for (uint32_t i = 0; i < 512; i++) { //
      uint32_t data = apu->sobuf_dma_rdata;

      APU_DIR_FFT_BUFFER[ch][i] = data;
    if (ch == 0) { //

    for (uint32_t ch = 0; ch < APU_DIR_CHANNEL_MAX; ch++) {
      for (uint32_t i = 0; i < 256; i++) { //
        uint32_t data = apu->sobuf_dma_rdata;

        APU_DIR_BUFFER[ch][i * 2 + 0] =
          data &amp; 0xffff;
        APU_DIR_BUFFER[ch][i * 2 + 1] =
          (data >> 16) &amp; 0xffff;


  } else if (rdy_reg.voc_buf_data_rdy) {

    for (uint32_t i = 0; i < 512; i++) { //
      uint32_t data = apu->vobuf_dma_rdata;

      APU_VOC_FFT_BUFFER[i] = data;
    for (uint32_t i = 0; i < 256; i++) { //
      uint32_t data = apu->vobuf_dma_rdata;

      APU_VOC_BUFFER[i * 2 + 0] = data &amp; 0xffff; // right
      APU_VOC_BUFFER[i * 2 + 1] = (data >> 16) &amp; 0xffff;  // left

    // use dma to fetch voc data from apu hardware and write it into buffer
    /*int16_t buf_zero[]={ // used to verify sample rate: last 56 of 256 samples set to zero
    i2s_data_t data = (i2s_data_t) {
        .rx_channel = APU_VOC_DMA_CHANNEL,
        .rx_buf = (uint32_t *)APU_VOC_BUFFER,
        .rx_len = 256,
        .transfer_mode = I2S_RECEIVE
    // i2s_handle_data_dma(I2S_DEVICE_APU_VOC, data, NULL); // not defined for APU_VOC, so copy code from sdk lib/drivers/i2s.c
//    dmac_wait_idle(data.rx_channel); // gets called inside dmac_set_single mode after channel reset (lib/drivers/dmac.c)
    sysctl_dma_select((sysctl_dma_channel_t)data.rx_channel, SYSCTL_DMA_SELECT_I2S0_BF_VOICE_REQ);
    dmac_set_single_mode(data.rx_channel, (void *)(&amp;apu->vobuf_dma_rdata), data.rx_buf, DMAC_ADDR_NOCHANGE, DMAC_ADDR_INCREMENT,
                                 DMAC_MSIZE_1, DMAC_TRANS_WIDTH_32, data.rx_len);
    // use dma to write buffer data to i2s output channel                 
    i2s_set_dma_divide_16(I2S_DEVICE_2, 1);
      i2s_send_data_dma(I2S_DEVICE_2, APU_VOC_BUFFER, 256, DMAC_CHANNEL0);
      //i2s_send_data_dma(I2S_DEVICE_2, APU_VOC_BUFFER, 200, DMAC_CHANNEL0); // used to verify sample rate
      //i2s_send_data_dma(I2S_DEVICE_2, buf_zero, 56, DMAC_CHANNEL0); // used to verify sample rate

  } else { //
    pprintf("[waring]: unknown %s interrupt cause.\n", __func__);
  return 0;

static void dmac_chanel_interrupt_clear(dmac_channel_number_t channel_num)
    writeq(0xffffffff, &amp;dmac->channel[channel_num].intclear);

int int_apu_dir_dma(void *ctx)
  uint64_t chx_intstatus =
  if (chx_intstatus &amp; 0x02) {

    static int ch;

    ch = (ch + 1) % 16;
    dmac->channel[APU_DIR_DMA_CHANNEL].dar =
    dmac->channel[APU_DIR_DMA_CHANNEL].dar =

    dmac->chen = 0x0101 << APU_DIR_DMA_CHANNEL;

    if (ch == 0) { //

  } else {
    pprintf("[warning] unknown dma interrupt. %lx %lx\n",
           dmac->intstatus, dmac->com_intstatus);
    pprintf("dir intstatus: %lx\n", chx_intstatus);

  return 0;

int int_apu_voc_dma(void *ctx)
  uint64_t chx_intstatus =

  if (chx_intstatus &amp; 0x02) {

    dmac->channel[APU_VOC_DMA_CHANNEL].dar =
    dmac->channel[APU_VOC_DMA_CHANNEL].dar =

    dmac->chen = 0x0101 << APU_VOC_DMA_CHANNEL;


  } else {
    pprintf("[warning] unknown dma interrupt. %lx %lx\n",
           dmac->intstatus, dmac->com_intstatus);
    pprintf("voc intstatus: %lx\n", chx_intstatus);

  return 0;

void init_fpioa(void)
  pprintf("init fpioa.\n");
  // mic
//  fpioa_set_function(47, FUNC_GPIOHS4);
  fpioa_set_function(23, FUNC_I2S0_IN_D0);
  fpioa_set_function(22, FUNC_I2S0_IN_D1);
  fpioa_set_function(21, FUNC_I2S0_IN_D2);
  fpioa_set_function(20, FUNC_I2S0_IN_D3);
  fpioa_set_function(19, FUNC_I2S0_WS);
  fpioa_set_function(18, FUNC_I2S0_SCLK);
  // dac
  fpioa_set_function(34, FUNC_I2S2_OUT_D1);
  fpioa_set_function(35, FUNC_I2S2_SCLK);
  fpioa_set_function(33, FUNC_I2S2_WS);


// copied from lib/drivers/i2s.c
#include <math.h>
uint32_t i2s_set_sample_rate2(i2s_device_number_t device_num, uint32_t sample_rate)
    ccr_t u_ccr;
    uint32_t pll2_clock = 0;
    pll2_clock = sysctl_pll_get_freq(SYSCTL_PLL2);

    u_ccr.reg_data = readl(&amp;i2s[device_num]->ccr);
    /* 0x0 for 16sclk cycles, 0x1 for 24 sclk cycles 0x2 for 32 sclk */
    uint32_t v_clk_word_size = (u_ccr.ccr.clk_word_size + 2) * 8;
    uint32_t threshold = round(pll2_clock / (sample_rate * 2.0 * v_clk_word_size * 2.0) - 1.5);
    sysctl_clock_set_threshold(SYSCTL_THRESHOLD_I2S0 + device_num, threshold);
    return sysctl_clock_get_freq(SYSCTL_CLOCK_I2S0 + device_num);

void init_i2s(void)
  // hardware limitation? 
  // i2s0 clock frequencies of 8.4MHz (4 stereo channels, sample 44.1kHz, 24bit) seems to induce noise of high frequency
  // whereas i2s0 clocked with 5.6MHz (4 steroe channels, sample 44.1kHz, 16bit) does not show this noise
  // conclusion: 
  // either use lower sample rates and get higher microphone sensitivity (using 24bit and apu_set_smpl_shift(n) with n=8,7,6...)
  // or use 44.1kHz and accept lower sensitivity (max value of gain is 1.9 apu_set_audio_gain(0x7ff))
  // or accept noise ...
  // (lower input sample rates probably degrade beamforming because beamforming uses time delays
  // and probably apu logic internally uses delays which are multiples of sample ticks: delta t = 1/sample frequency)
  // do not use i2s_set_sample_rate because of rounding errors!!!
  // (which make it impossible to set input and output sample rates precisely (they have to match exactly))
  // instead directly set threshold of pll2 for i2s (clock of i2sX is 2*(threshold + 1), as used in lib/drivers/i2s.c)
  // to adjust the sample rate in small steps change the base frequency of pll2, e.g. sysctl_pll_set_freq(SYSCTL_PLL2, 67737602UL)
  // downsampling apu_voc output can be used as lowpass for voice output
  // apu_set_down_size(0, 3); // dir, voc: 0: /1, 1: /2, 2: /3, 3: /4 ... 15: /16 (first argument is down sampling of apu_dir)
  // (i2s sample rate for output , that is threshold used for i2s output has to be set accordingly)

  uint32_t real_clock, real_clock_source, pll2_threshold;
  pprintf("init i2s.\n");

  /* I2s init */
  // SCLK_CYCLES refers to whole device and has to be >= biggest channel resolution (of channels of this device)
    i2s_init(I2S_DEVICE_0, I2S_RECEIVER, 0x3);  // bits 0x3 set: use interrupts - else
    i2s_init(I2S_DEVICE_2, I2S_TRANSMITTER, 0x3); // 0x3
  // either RESOLUTION 16 and apu sample shift 0, or res 24 and apu sample shift 8 (or 7, 6, 5 ... to increase sensitivity)
  // (sys cycles has to be >= resolution)
    i2s_rx_channel_config(I2S_DEVICE_0, I2S_CHANNEL_0,
            RESOLUTION_24_BIT, SCLK_CYCLES_24,
            TRIGGER_LEVEL_4, STANDARD_MODE); // has to be standard mode
    i2s_rx_channel_config(I2S_DEVICE_0, I2S_CHANNEL_1,
            RESOLUTION_24_BIT, SCLK_CYCLES_24,
    i2s_rx_channel_config(I2S_DEVICE_0, I2S_CHANNEL_2,
            RESOLUTION_24_BIT, SCLK_CYCLES_24,
    i2s_rx_channel_config(I2S_DEVICE_0, I2S_CHANNEL_3,
            RESOLUTION_24_BIT, SCLK_CYCLES_24,
// input uses 4 stereo channels, output uses 1 stereo channel, so clock of input must be 4 times higher than output clock
// (apart from rounding errors produced by i2s_set_sample_rate(...) the rate argument has to be multiplied by the number of channels used)
//    real_clock = 2*i2s_set_sample_rate(I2S_DEVICE_0, 176400); // seems to produce lots of noise unless SCLK_CYCLES is reduced to 16
  sysctl_clock_set_threshold(SYSCTL_THRESHOLD_I2S0 + I2S_DEVICE_0, 5); // pll2 divisor is (threshold + 1)
  // sample rate == (pll2_freq/pll2_divisor)/(32*4) in case of 16 sys cycles (for left and right channel; using 4 stereo channels)
    real_clock = 2*sysctl_clock_get_freq(SYSCTL_CLOCK_I2S0); //clock serves 2 channels, sys_clock_get_freq gets bit rate per channel not i2s clock
    real_clock_source = 2*sysctl_clock_get_freq(SYSCTL_CLOCK_I2S0)*(1+sysctl_clock_get_threshold(SYSCTL_THRESHOLD_I2S0));
  pll2_threshold = sysctl_clock_get_threshold(SYSCTL_THRESHOLD_I2S0);
    pprintf("I2S DEV 0 real clock freq: %u (source %u, threshold %u, divisor %u)\n", real_clock, real_clock_source, pll2_threshold, 1+pll2_threshold);

    i2s_tx_channel_config(I2S_DEVICE_2, I2S_CHANNEL_1,
                          RESOLUTION_16_BIT, SCLK_CYCLES_24, // 24 or 32 sys cycles works as well, if i2s clock is incremented accordingly
                          RIGHT_JUSTIFYING_MODE); // needed, PT8211 dual 16bit dac
//    real_clock = 2*i2s_set_sample_rate2(I2S_DEVICE_2, 44000);
//    sysctl_clock_set_threshold(SYSCTL_THRESHOLD_I2S0 + I2S_DEVICE_2, 23); // pll2 divisor is 2*(threshold + 1); no downsampling: broken apu_voc output
  sysctl_clock_set_threshold(SYSCTL_THRESHOLD_I2S0 + I2S_DEVICE_2, 47); // pll2 divisor is (threshold + 1); downsampling by factor 2
//    sysctl_clock_set_threshold(SYSCTL_THRESHOLD_I2S0 + I2S_DEVICE_2, 71); // pll2 divisor is (threshold + 1); downsampling by factor 3
//    sysctl_clock_set_threshold(SYSCTL_THRESHOLD_I2S0 + I2S_DEVICE_2, 95); // pll2 divisor is (threshold + 1); downsampling by factor 4
  // sample rate == (pll2_freq/pll2_divisor)/32 in case of 16 sys cycles (for left and right channel; using 1 stereo channels)
    real_clock = 2*sysctl_clock_get_freq(SYSCTL_CLOCK_I2S2); //clock serves 2 channels, sys_clock_get_freq gets bit rate per channel not i2s clock
    real_clock_source = 2*sysctl_clock_get_freq(SYSCTL_CLOCK_I2S2)*(1+sysctl_clock_get_threshold(SYSCTL_THRESHOLD_I2S2));
  pll2_threshold = sysctl_clock_get_threshold(SYSCTL_THRESHOLD_I2S2);
    pprintf("I2S DEV 2 real clock freq: %u (source %u, threshold %u, divisor %u)\n", real_clock, real_clock_source, pll2_threshold, 1+pll2_threshold);

    // power on audio amplifier
    fpioa_set_function(32, FUNC_GPIO0); 
    gpio_set_drive_mode(0, GPIO_DM_OUTPUT);
    gpio_set_pin(0, GPIO_PV_HIGH);

void init_bf(void)
  pprintf("init bf.\n");
  uint16_t fir_prev_t[] = {
    0x020b, 0x0401, 0xff60, 0xfae2, 0xf860, 0x0022,
    0x10e6, 0x22f1, 0x2a98, 0x22f1, 0x10e6, 0x0022,
    0xf860, 0xfae2, 0xff60, 0x0401, 0x020b,
  uint16_t fir_post_t[] = {
    0xf649, 0xe59e, 0xd156, 0xc615, 0xd12c, 0xf732,
    0x2daf, 0x5e03, 0x7151, 0x5e03, 0x2daf, 0xf732,
    0xd12c, 0xc615, 0xd156, 0xe59e, 0xf649,
  uint16_t fir_one[] = { // 32767
    0x7fff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  uint16_t fir_neg_one[] = { // -32768
    0x8000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  uint16_t fir_half[] = { // 16384
    0x4000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  int16_t fir_lowpass_signed[] = { // 44100 Hz 17 taps pass 0-5000, stop 10000-22050
  int16_t fir_lowpass2_signed[] = { // 44100 Hz 17 taps pass 0-2500, stop 5000-22050
  int16_t fir_bandpass_signed[] = {

      FIR filter designed with

      sampling frequency: 22050 Hz

      fixed point precision: 16 bits

      * 0 Hz - 12 Hz
        gain = 0
        desired attenuation = -20 dB
        actual attenuation = n/a

      * 18 Hz - 1000 Hz
        gain = 1
        desired ripple = 5 dB
        actual ripple = n/a

      * 2000 Hz - 11025 Hz
        gain = 0
        desired attenuation = -20 dB
        actual attenuation = n/a
//  apu_dir_set_prev_fir(fir_one);
//  apu_dir_set_post_fir(fir_one);
//  apu_voc_set_prev_fir(fir_neg_one);
//  apu_voc_set_post_fir(fir_neg_one);
  apu_voc_set_prev_fir((uint16_t *)fir_lowpass2_signed);
  apu_voc_set_post_fir((uint16_t *)fir_bandpass_signed);
//      apu_voc_set_prev_fir((uint16_t *)fir_prev_t);
//      apu_voc_set_post_fir((uint16_t *)fir_post_t);
//      apu_voc_set_prev_fir((uint16_t *)fir_one);
//      apu_voc_set_post_fir((uint16_t *)fir_one);
  apu_dir_set_prev_fir((uint16_t *)fir_lowpass_signed);
  apu_dir_set_post_fir((uint16_t *)fir_lowpass_signed);

  // lib/drivers/include/apu.h has hardcoded I2S rate of 44100: #define I2S_FS 44100; 
  // lib/drivers/apu.c: float cm_tick = (float)SOUND_SPEED * 100 / I2S_FS; /*distance per tick (cm)*/
  // redefine I2S_FS in apu2.h using #undef I2S_FS and #define I2S_FS 22050
  // apu2.h and apu2.c define apu_set_delay2 which allows a center mic on an arbitrary i2s channel (required for maix go)
//  apu_set_delay2(4, 6, 0); // radius of mic circle (cm), # mics in circle, no center mic (0/1)
//  apu_set_delay2(4, 6, 1); // radius of mic circle (cm), # mics in circle, with center mic (0/1)
  apu_set_delay2(4, 6, 7); // radius of mic circle (cm), # mics in circle 0,1...5, with center mic as mic number 7
//  apu_set_smpl_shift(APU_SMPL_SHIFT); // 0 corresponds to i2s input 16bit
//  apu_set_smpl_shift(8); // corresponds to i2s input 24bit instead of 16 bit: using 7,6... increases sensitivity
  apu_set_smpl_shift(6); // corresponds to i2s input 24bit instead of 16 bit: using 7,6... increases sensitivity: useful with bandpass filter
  apu_voc_set_saturation_limit(APU_SATURATION_VPOS_DEBUG, // 0x07ff
            APU_SATURATION_VNEG_DEBUG); // 0xf800
  apu_set_audio_gain(APU_AUDIO_GAIN_TEST); // 1 << 10 == 1.0
//  apu_set_audio_gain(0x200);  // 0.5
//  apu_set_audio_gain(0x100);  // 0.25
//    apu_set_audio_gain(0x7ff);  // 1.9 max gain
//  apu_set_channel_enabled(0x3f); // circle mics 0,1...5; no center mic
//  apu_set_channel_enabled(0x15); // only circle mics 0,3,5; no center mic
//  apu_set_channel_enabled(0x7f);
  apu_set_channel_enabled(0xbf); // circle mics 0,1...5; center mic 7: requires apu_set_delay2
//  apu_set_down_size(0, 0); // dir, voc: 0: /1, 1: /2, 2: /3 ... 15: /16
  apu_set_down_size(0, 1); // downsampling of apu_voc; output i2s rate for apu_voc has to be divided accordingly;

  // current status: rather high sensity; no sampling distortions (scope: broken curves with missing pieces); 440hz beamforming seems to work: 100% in preferred direction, 50% about 30 degrees away; 440hz sine is accompagnied by 30hz(??) humming sound, 440hz sine waves are wobbling, like frequency modulated

  apu_set_fft_shift_factor(1, 0xaa);
  apu_set_fft_shift_factor(0, 0);

  apu_set_interrupt_mask(APU_DMA_ENABLE, APU_DMA_ENABLE);

void init_dma(void)
  pprintf("%s\n", __func__);
  // dmac enable dmac and interrupt
//  union dmac_cfg_u dmac_cfg;
  dmac_cfg_u_t dmac_cfg; = readq(&amp;dmac->cfg);
  dmac_cfg.cfg.dmac_en = 1;
  dmac_cfg.cfg.int_en = 1;
  writeq(, &amp;dmac->cfg);


void init_dma_ch(int ch, volatile uint32_t *src_reg, void *buffer,
     size_t size_of_byte)
  pprintf("%s %d\n", __func__, ch);

  dmac->channel[ch].sar = (uint64_t)src_reg;
  dmac->channel[ch].dar = (uint64_t)buffer;
  dmac->channel[ch].block_ts = (size_of_byte / 4) - 1;
  dmac->channel[ch].ctl =
    (((uint64_t)1 << 47) | ((uint64_t)15 << 48)
     | ((uint64_t)1 << 38) | ((uint64_t)15 << 39)
     | ((uint64_t)3 << 18) | ((uint64_t)3 << 14)
     | ((uint64_t)2 << 11) | ((uint64_t)2 << 8) | ((uint64_t)0 << 6)
     | ((uint64_t)1 << 4) | ((uint64_t)1 << 2) | ((uint64_t)1));
   * dmac->channel[ch].ctl = ((  wburst_len_en  ) |
   *                        (    wburst_len   ) |
   *                        (  rburst_len_en  ) |
   *                        (    rburst_len   ) |
   *                        (one transaction:d) |
   *                        (one transaction:s) |
   *                        (    dst width    ) |
   *                        (    src width   ) |
   *                        (    dinc,0 inc  )|
   *                        (  sinc:1,no inc ));

  dmac->channel[ch].cfg = (((uint64_t)1 << 49) | ((uint64_t)ch << 44)
         | ((uint64_t)ch << 39) | ((uint64_t)2 << 32));
   * dmac->channel[ch].cfg = ((     prior       ) |
   *                         (      dst_per    ) |
   *                         (     src_per     )  |
   *           (    peri to mem  ));
   *  01: Reload

  dmac->channel[ch].intstatus_en = 0x2; // 0xFFFFFFFF;
  dmac->channel[ch].intclear = 0xFFFFFFFF;

  dmac->chen = 0x0101 << ch;

void init_interrupt(void)
  // bf
  plic_set_priority(IRQN_I2S0_INTERRUPT, 4);
  plic_irq_register(IRQN_I2S0_INTERRUPT, int_apu, NULL);

  // dma
  plic_set_priority(IRQN_DMA0_INTERRUPT + APU_DIR_DMA_CHANNEL, 4);
        int_apu_dir_dma, NULL);
  // dma
  plic_set_priority(IRQN_DMA0_INTERRUPT + APU_VOC_DMA_CHANNEL, 4);
        int_apu_voc_dma, NULL);
void init_ws2812b(void)
  gpiohs->output_en.bits.b4 = 1;
  gpiohs->output_val.bits.b4 = 0;
void init_all(void)
//  init_pll();

          APU_DIR_FFT_BUFFER[0], 512 * 4);
          &amp;apu->vobuf_dma_rdata, APU_VOC_FFT_BUFFER,
          512 * 4);
          &amp;apu->sobuf_dma_rdata, APU_DIR_BUFFER,
          512 * 16 * 2);
          &amp;apu->vobuf_dma_rdata, APU_VOC_BUFFER,
          512 * 2);
//  init_ws2812b();
  for (int l=0; l<12; l++) set_light(l, 0, 0, 0);
//  apu_print_setting();
init.h: (hide expanded source code)
// file init.h
#pragma once
#include <stdint.h>
#include <plic.h>
#include <i2s.h>
#include <sysctl.h>
#include <dmac.h>
#include <fpioa.h>
#include "uarths.h"
#include "gpiohs.h"

#define APU_DIR_ENABLE 1

#define APU_VOC_ENABLE 1

#define APU_DMA_ENABLE 0

#define APU_FFT_ENABLE 0

#define APU_DATA_DEBUG 0

#define APU_GAIN_DEBUG 0


#define APU_SMPL_SHIFT 0x00









#define APU_AUDIO_GAIN_TEST (1 << 10)






extern uint64_t dir_logic_count;
extern uint64_t voc_logic_count;

void init_all(void);
mic_array_leds.c (hide expanded source code):
// file mic_arrays_led.c
#include <unistd.h>
#include <stdio.h>
#include "fpioa.h"
#include "sleep.h"
#include "spi.h"

#include "mic_array_leds.h"

// The Sipeed microphone array has 12 SK9822 LEDs
#define NUMPIX 12
uint8_t red[NUMPIX], green[NUMPIX], blue[NUMPIX];
// The SK9822 has an overal brightness factor
uint8_t brightness;

// The buffer to send the data to the mic array
#define TX_LEN 4+NUMPIX*4+4
uint8_t tx_buffer[TX_LEN];

uint8_t busy = 0;
// send the pixel data to the mic array using SPI
void write_pixels()
    uint8_t tx_place=0;
    busy = 1;
    // the SK9822 first expects 4 times 0000 0000 
    for (int i=0; i<4; i++) tx_buffer[tx_place+i] = 0; 
    // write the color data to the tx_buffer
    for (uint8_t i=0; i<NUMPIX; i++) {
        // set the overall brightness factor
        tx_buffer[tx_place] = (0b11100000 | brightness);
        // set the RGB values
        tx_buffer[tx_place+1] = blue[i];
        tx_buffer[tx_place+2] = green[i];
        tx_buffer[tx_place+3] = red[i];
        tx_place += 4;

    // the SK9822 finally expects 4 times 1111 1111
    for (int i=0; i<4; i++) tx_buffer[tx_place+i] = 255; 

    // send the data to the mic array
    spi_send_data_standard(0, 0, NULL, 0, tx_buffer, TX_LEN);
    busy = 0;

void init_mic_array_lights(void) 
    // data pin
    fpioa_set_function(24, FUNC_SPI0_D0); //MOSI
    // clock pin
    fpioa_set_function(25, FUNC_SPI0_SCLK); //CLK
    // init SPI
    spi_init(0, SPI_WORK_MODE_0, SPI_FF_STANDARD, 8, 0);
//    spi_set_clk_rate(0, 20000000); // datasheet max 30MHz
    spi_set_clk_rate(0, 500000);

    // set the overall brighness factor
    brightness = 5;

void set_light(int light_number, int R, int G, int B)
   red[light_number] = R;
   green[light_number] = G;
   blue[light_number] = B;
mic_arrays_led.h (hide expanded source code):
// file mic_arrays_led.h
// send the pixel data to the mic array using SPI
void write_pixels();

void init_mic_array_lights(void);

void set_light(int light_number, int R, int G, int B);
apu2.c (hide expanded source code):
// file apu2.c
/* Copyright 2018 Canaan Inc.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * See the License for the specific language governing permissions and
 * limitations under the License.
#include <math.h>
#include <stddef.h>
#include <stdint.h>
#include <printf.h>
#include "apu.h"
#include "apu2.h"
#include "syscalls.h"
#include "sysctl.h"

#include <apu.h>
#include "apu2.h"

static void print_fir2(const char *member_name, volatile apu_fir_coef_t *pfir)
    pprintf("  for(int i = 0; i < 9; i++){\n");
    for(int i = 0; i < 9; i++)
//        apu_fir_coef_t fir = pfir[i];
        apu_fir_coef_t fir;
        fir = pfir[i];

        pprintf("    apu->%s[%d] = (apu_fir_coef_t){\n", member_name, i);
        pprintf("      .fir_tap0 = 0x%x,\n", fir.fir_tap0);
        pprintf("      .fir_tap1 = 0x%x\n", fir.fir_tap1);
        pprintf("    };\n");
    pprintf("  }\n");
void apu_print_setting2(void)
    pprintf("void apu_setting(void) {\n");
    apu_ch_cfg_t bf_ch_cfg_reg = apu->bf_ch_cfg_reg;

    pprintf("  apu->bf_ch_cfg_reg = (apu_ch_cfg_t){\n");
    pprintf("    .we_audio_gain = 1, .we_bf_target_dir = 1, .we_bf_sound_ch_en = 1,\n");
    pprintf("    .audio_gain = 0x%x, .bf_target_dir = %d, .bf_sound_ch_en = %d, .data_src_mode = %d\n",
           bf_ch_cfg_reg.audio_gain, bf_ch_cfg_reg.bf_target_dir, bf_ch_cfg_reg.bf_sound_ch_en, bf_ch_cfg_reg.data_src_mode);
    pprintf("  };\n");

    apu_ctl_t bf_ctl_reg = apu->bf_ctl_reg;

    pprintf("  apu->bf_ctl_reg = (apu_ctl_t){\n");
    pprintf("    .we_bf_stream_gen = 1, .we_bf_dir_search_en = 1,\n");
    pprintf("    .bf_stream_gen_en = %d, .bf_dir_search_en = %d\n",
           bf_ctl_reg.bf_stream_gen_en, bf_ctl_reg.bf_dir_search_en);
    pprintf("  };\n");

    pprintf("  for(int i = 0; i < 16; i++){\n");
    for(int i = 0; i < 16; i++)
        apu_dir_bidx_t bidx0 = apu->bf_dir_bidx[i][0];
        apu_dir_bidx_t bidx1 = apu->bf_dir_bidx[i][1];

        pprintf("    apu->bf_dir_bidx[%d][0] = (apu_dir_bidx_t){\n", i);
        pprintf("      .dir_rd_idx0 = 0x%x,\n", bidx0.dir_rd_idx0);
        pprintf("      .dir_rd_idx1 = 0x%x,\n", bidx0.dir_rd_idx1);
        pprintf("      .dir_rd_idx2 = 0x%x,\n", bidx0.dir_rd_idx2);
        pprintf("      .dir_rd_idx3 = 0x%x\n", bidx0.dir_rd_idx3);
        pprintf("    };\n");
        pprintf("    apu->bf_dir_bidx[%d][1] = (apu_dir_bidx_t){\n", i);
        pprintf("      .dir_rd_idx0 = 0x%x,\n", bidx1.dir_rd_idx0);
        pprintf("      .dir_rd_idx1 = 0x%x,\n", bidx1.dir_rd_idx1);
        pprintf("      .dir_rd_idx2 = 0x%x,\n", bidx1.dir_rd_idx2);
        pprintf("      .dir_rd_idx3 = 0x%x\n", bidx1.dir_rd_idx3);
        pprintf("    };\n");
    pprintf("  }\n");

    print_fir2("bf_pre_fir0_coef", apu->bf_pre_fir0_coef);
    print_fir2("bf_post_fir0_coef", apu->bf_post_fir0_coef);
    print_fir2("bf_pre_fir1_coef", apu->bf_pre_fir1_coef);
    print_fir2("bf_post_fir1_coef", apu->bf_post_fir1_coef);

    apu_dwsz_cfg_t bf_dwsz_cfg_reg = apu->bf_dwsz_cfg_reg;

    pprintf("  apu->bf_dwsz_cfg_reg = (apu_dwsz_cfg_t){\n");
    pprintf("    .dir_dwn_siz_rate = %d, .voc_dwn_siz_rate = %d\n",
           bf_dwsz_cfg_reg.dir_dwn_siz_rate, bf_dwsz_cfg_reg.voc_dwn_siz_rate);
    pprintf("  };\n");

    apu_fft_cfg_t bf_fft_cfg_reg = apu->bf_fft_cfg_reg;

    pprintf("  apu->bf_fft_cfg_reg = (apu_fft_cfg_t){\n");
    pprintf("    .fft_enable = %d, .fft_shift_factor = 0x%x\n",
           bf_fft_cfg_reg.fft_enable, bf_fft_cfg_reg.fft_shift_factor);
    pprintf("  };\n");

    apu_int_mask_t bf_int_mask_reg = apu->bf_int_mask_reg;

    pprintf("  apu->bf_int_mask_reg = (apu_int_mask_t){\n");
    pprintf("    .dir_data_rdy_msk = %d, .voc_buf_rdy_msk = %d\n",
           bf_int_mask_reg.dir_data_rdy_msk, bf_int_mask_reg.voc_buf_rdy_msk);
    pprintf("  };\n");

 * radius mic_num_a_circle: the num of mic per circle; center: 0: no center mic, 1:have center mic
 * center==1: center mic is channel after circle mics; (is channel number given by variable mic_num_a_circle) 
 * center>1: center mic is channel given by variable center (== channel of last mic used)
 * e.g.: circle 0,1,2...5 and center is 7
void apu_set_delay2(float radius, uint8_t mic_num_a_circle, uint8_t center)
    uint8_t offsets[16][8];
    int i, j;
    float seta[8], delay[8], hudu_jiao;
    float cm_tick = (float)SOUND_SPEED * 100 / I2S_FS; /*distance per tick (cm)*/
    float min;
    if(center == 1) // if center>0, then make sure that center variable is set to channel of last mic used
  center = mic_num_a_circle;

    for(i = 0; i < mic_num_a_circle; ++i)
        seta[i] = 360 * i / mic_num_a_circle;
        hudu_jiao = 2 * M_PI * seta[i] / 360;
        delay[i] = radius * (1 - cos(hudu_jiao)) / cm_tick;
//        delay[mic_num_a_circle] = radius / cm_tick;
        delay[center] = radius / cm_tick;

//    for(i = 0; i < mic_num_a_circle + center; ++i)
    for(i = 0; i < (center==0 ? mic_num_a_circle : center+1); ++i)
        offsets[0][i] = (int)(delay[i] + 0.5);
    for(; i < 8; i++)
        offsets[0][i] = 0;

    for(j = 1; j < DIRECTION_RES; ++j)
        for(i = 0; i < mic_num_a_circle; ++i)
            seta[i] -= 360 / DIRECTION_RES;
            hudu_jiao = 2 * M_PI * seta[i] / 360;
            delay[i] = radius * (1 - cos(hudu_jiao)) / cm_tick;
//            delay[mic_num_a_circle] = radius / cm_tick;
            delay[center] = radius / cm_tick;

        min = 2 * radius;
        for(i = 0; i < mic_num_a_circle; ++i)
            if(delay[i] < min)
                min = delay[i];
//            for(i = 0; i < mic_num_a_circle + center; ++i)
            for(i = 0; i < (center==0 ? mic_num_a_circle : center+1); ++i)
                delay[i] = delay[i] - min;

//        for(i = 0; i < mic_num_a_circle + center; ++i)
        for(i = 0; i < (center==0 ? mic_num_a_circle : center+1); ++i)
            offsets[j][i] = (int)(delay[i] + 0.5);
        for(; i < 8; i++)
            offsets[0][i] = 0;
    for(size_t i = 0; i < DIRECTION_RES; i++)
        apu_set_direction_delay(i, offsets[i]);
apu2.h (hide expanded source code):
// file apu2.h
#ifdef ARDUINO
void pprintf(char *fmt, ... );
#ifndef ARDUINO
   #define pprintf(...) printf(__VA_ARGS__)

void apu_print_setting2(void);

// default: #define I2S_FS 44100
//#undef I2S_FS
//#define I2S_FS 88200

 * @brief       I2S host beam-forming direction sample ibuffer read index configure register
 * @param[in]   radius               radius
 * @param[in]   mic_num_a_circle     the num of mic per circle
 * @param[in]   center               0: no center mic, 1:have center mic (>1 set channel of center mic; default: last channel after circle mics)
void apu_set_delay2(float radius, uint8_t mic_num_a_circle, uint8_t center);

Current status: Useful for testing is one fixed direction (main.c, currently commented out). Direction detecting code is active (yellow led indicates detected direction, brightness shows amplitude) – including signal of neighbour channels as @MyAmigo suggests in kendryte forum. Detected direction is only used if same direction is detected three times in sequence. Voice output (apu_voc) and detection of direction run at the same time. Currently input sample rate is 44kHz with 24bit data width, and output rate is 22kHz with 24 sysclk cycles but 16bit data width. The corresponding i2s clock rates are shown as console output.

Output is sent to both speakers. To avoid feedback loops the builtin speaker has to be detached (e.g. use a miniature sliding switch) and the other output channel has to be attached to a headphone. Note: the speaker outputs do not have a common ground line and should not be connected (audio amp specs). (In my test setup I had an oscilloscope attached to the input pins of this audio amp to be able to watch the output of the DAC chip.)

Observations: To get reliable i2s sample rates the i2s clocks had to be set by defining the pll2 thresholds (init.c). The function i2s_set_sample_rate is not usable because of rounding errors. To make APU voice output work it seems to be necessary to downsample this output at least by factor 2. I2S controller 0 is linked with the APU and if the clock of I2S0 is set high enough to get 8 input channels with 88kHz only 16 bit data width works. Using 24 (or 32) bit at 88kHz increases the clock even more and this induces a noise with high frequency (see comments in init.c).

Beam forming: There are indications that it works, however it has not yet been tested in a setup without strong reflections. Direction detection with 440 Hz sine wave signal works; led on led ring indicates direction (if currently commented out direction code in main.c is used). The function apu_set_delay() in apu.c suggests that the direction detection and beam forming logic is based on time of flight based superposition of accordingly delayed microphone signals, delay and sum beamformer “DAS BF” (compare chapters 2.1.3, 4.1, 4.2 here, document found by @MyAmigo in kendryte forum)

Gain vs. Frequency
Impulse Response
Filter settings (

modify fir_neg_one[] of init.c in sdk demo apu:
forum: signed fixed point number between -1 ~ 1
so use of int16_t giving -32768 …. 32768 matches the output of TFilter website (choose C/C++ array, int, 16 bit):

uint16_t fir_neg_one[] = { // -32768
0x8000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
uint16_t fir_one[] = { // 32767
0x7fff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
uint16_t fir_half[] = { // 16384
0x4000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,

int16_t fir_lowpass_signed[] = { // 44100 Hz 17 taps pass 0-5000, stop 10000-22050

apu_dir_set_prev_fir((uint16_t *)fir_lowpass_signed); //

Board specifications at and available at

Example code in language c can be found at


Download kendryte-standalone-sdk, kendryte-standalone-demo and kendryte-gnu-toolchain and kflash (needs python3; pip3 install pyserial pyelftools kflash; windows kflash)

  • if both are installed inside directory $HOME/kendryte/
    • cd kendryte/kendryte-standalone-sdk/build/
    • cmake .. -DTOOLCHAIN=$HOME/kendryte/kendryte-gnu-toolchain/bin/ -DPROJ=apu
    • make
    • kflash -p /dev/cu.usbserial-00004014B -B goE apu.bin -b 2000000
    • replace /dev/cu.usbserial-00004014B by your usb port (second port of the two ports of maiix go has to be used)

Playing with builtin APU (audio processing unit) and circular mic array (6 mics + center mic; specs, buy)

+----------+  +----------+  +------------+  +-------+  +-----+ +--------------------- +  +-----+  +-----+
| 8ch in | - > | buffer | - > | DAS BF | -> | gain | -> | FIR | -> | DownSample | -> | FIR | -> | FFT |
+----------+  +----------+  +------------+  +-------+  +------+ +---------------------+  +------+ +------+

Server monitoring with graphical output required, but no need to run a monitoring suite like nagios? With existing debian lamp server you might want to use SmokePing.

Setup is fairly easy:

  • apt-get install smokeping
  • edit /etc/smokeping/config.d/General
    let cgiurl point to your server https://your_domain.tld/cgi-bin/smokeping.cgi
  • edit /etc/smokeping/config.d/Targets and define the servers which should be monitored
  • restart smokeping: systemctl restart smokeping (or smokeping –restart)
  • open https://your_domain.tld/smokeping/smokeping.cgi

Add password protection:

  • edit /etc/apache2/conf-available/smokeping.conf and add two Location sections:
    <Location /cgi-bin/smokeping.cgi>
    AuthUserFile /etc/apache2/your_password_file
    AuthName “protected”
    AuthType Digest
    AuthDigestProvider file
    require valid-user
    <Location /smokeping/smokeping.cgi>
    … (see above)
  • htdigest -c /etc/apache2/your_password_file “protected” your_user
  • apache2ctl configtest
    apache2ctl graceful

Credits go to Tobi Oetiker, the author of this well made tool!


some notes:

very slow disk access inside vm might be caused by balanced power plan (which is install default in server 2008r2, 2012r2 hyperv core).
use powercfg.exe -l to check powerplan and powercfg -s to set another plan, e.g. performance; check with crystalldiskmark

running some time virtual disks tend to get to small. On server 2012r2 powershell has:
get-vhd -path …, resize-vhd -path -sizebytes …GB, optimize-vhd

before moving a vhd(x) file, display acl lists using icacls (to be able to restore permissions iacl …./grant …)

… net use z:  \\\sharename * /user:your_name (and net use z: /delete)

partitions inside a vhd may be resized with diskpart
– sel vdisk file=….
– attach vdisk
– sel last partition and the corresponding volume
– extend
– lis par (to check)
– detach

Excellent (!) main reference

Special case ts-221 (and probably all devices with 1GB RAM): kernel of debian stretch 4.9 (also stretch-backports 4.17) seems to fail with memory above 768M on qnap arm
(discussion:!topic/linux.debian.ports.arm/vSaO642z8aY); dmesg shows
BUG: Bad rss-counter state mm

  • use jessie installer in expert mode and install stretch (offered by jessie installer as testing release)
  • before reboot
    • either switch to older kernel as described below
    • or use only 768MB of 1024MB ram to avoid this bug
      mount /dev and /proc (maybe /sys) inside installation directory and chroot into installation directory

      • copy /usr/share/doc/u-boot-tools/examples/qnap_ts119-219.config
        to /etc/fw_env.config (assuming package u-boot-tools has been installed)
      • then run fw_printenv bootargs which returns something like
        bootargs=console=ttyS0,115200 root=/dev/ram initrd=0xa00000,0x900000 ramdisk=34816
      • now use fw_setenv to add linux kernel boot parameter mem=768M by adding mem=768M to output of fw_printenv
        fw_setenv bootargs “console=ttyS0,115200 root=/dev/ram initrd=0xa00000,0x900000 ramdisk=34816 mem=768M”
        (takes some time to write to flash)
      • check with fw_printenv that bootargs still contains the original arguments (plus mem=768M)
      • reboot (check with free -h and cat /proc/cmdline; dmesg: Memory: … 0K highmem)
      • maybe, this has to be repeated after every linux kernel update (verify withc fw_printenv bootargs if mem=768M ist still there)


In case flash-kernel returns Failed to obtain MAC address, run either
ubootcfg -b 0 -f /dev/mtdblock4 -o – | grep “^ethaddr=” | sed “s/^ethaddr=//”
or fw_printenv ethaddr
which returns the mac address stored in flash; then run
iface_set_mac eth0 YOUR_QNAP_MAC_FROM_FLASH

Recovery: run this iso from qnap wiki page inside a vm or install minimal debian with isc-dhcp-server and tftpd-hpa.

To make sure that an existing harddisk gets properly reconfigured use installer option (removal of raid partitions)
guided – entire disk

  • one partion with btrfs and option noatime
  • in case of error see below

Workaround (ok for devices without fan): If status led remains blinking red-green create /etc/rc.local, make it executable
qcontrol –direct statusled greenon
exit 0

Solution (fixes led, buzzer and fan): # qcontrol needs stretch backports: apt-get -t stretch-backports install qcontrol, so add to /etc/apt/sources.list

deb stretch-backports main

and /lib/systemd/system/ is broken: replaces lines

ConditionPathExists=/dev/input/by-path/platform-gpio_keys-event (kernel 4.9) or platform-gpio-keys-event (kernel 3.16)
and afterwards run
dpkg –configure qcontrol

In case one disk is used for the system and a second disk is used for data: mount the second disk in /etc/fstab with option noauto (else the system will not start if the data disk has been removed)

Install smartmontools and check disk with smartctl -a /dev/sdXN

Special case ts-221: does not run with kernel 4.9, so run debian stretch with kernel 3.6 -or use bootarg mem=768M as described above
(latest discussion:!topic/linux.debian.ports.arm/vSaO642z8aY)

  • first install debian jessie (as described on for stretch but replace stretch by jessie in download links)
    • copy /boot/*, /lib/modules/*, /usr/lib/linux-image-* into a tar archive and save it on an external storage device
  • second, flash again the jesssie installer, but now run it in expert mode and install debian stretch
    • before rebooting the second last step “finish install or so” open console 2 (keys <ctrl> + <a>, then key <2>)
      • copy the saved contents of first installation (tar archive) into running installation
      • edit /usr/share/flash-kernel/functions: change force=”no” to force=”yes”
      • run update-initramfs -u -k 3.16..0-6-kirkwood (and probably you get a message to add “-t” as additional argument)
        later if kernel/initramfs get renewed, it is required to run afterwards: update-initramfs -u -k $(uname -r)

the installer does not cope well with already used disks (guided, use whole disk should work); if not, start installer in expert mode and add fdisk as installer component
(in case of running debian stretch wipefs -a removes all kind of signatures; but do double check that you will not wipe the wrong partition!)

  • open shell 2 and run cat /proc/partitions
  • use fdisk to create a new partition table (dos format) and create one big primary partion
  • create a filesystem on this partition with mkfs.ext2
  • reboot!!! And then run installer again in standard mode

Some qnap models may take more than 15 minutes until kernel and initrd are written to flash. – First reboot after installation might by succesful, even if power led keeps blinking red-green and buzzer remains silent. Led and buzzer need installation of qcontrol from backports and patched qcontrold.service file as described above.

Enable quota: btrfs quota enable /backup; btrfs quota enable /backup2
Display disk usage: btrfs sub list /backup; btrfs qgroup show /backup
unused because of to much memory usage when deleting snapshots

mkfs.btrfs shows after creation of filesystem whether incompatibilities with current kernel exist; use -O ^prop1,^prop2 if properties prop1,prop2 ar not compatible; (use wipefs to remove the newly created filesystem and recreate it using -O ^…)

Convert btrfs single partition filesystem to btrfs raid1 during debian install

  • use installer to create a btrfs root partition (noatime option) on a single partition
  • use installer to create on second disk a partition of same size without mountpoint
  • let installer write new partitions to disk
  • open second ssh connection and choose shell
    • btrfs filesystem show
    • df -h
    • btrfs device add -f /dev/PARTITION_ON_SECOND_DISK /target
    • btrfs balance start -dconvert=raid1 -mconvert=raid1 /target
    • btrfs filesystem show

Recover from failure of one drive of root filesystem on btrfs raid1

  • difficulty: if / gets remounted ro because of filesystem errors reboot into installer is required
  • prepare for rescue with installer
    • mkdir /var/backups/flash-installer
    • cd /tmp
    • wget
    • dmesg |grep SoC
      ID=0x… displays wether kernel 6282 or 6281 is used
      cp initrd /var/backups/flash-installer/mtd2
      cp kernel-6282 /var/backups/flash-installer/mtd1 (or use kernel 6281)
    • to boot into installer run:
      cat /var/backups/flash-installer/mtd1 >/dev/mtdblock1
      cat /var/backups/flash-installer/mtd2 >/dev/mtdblock2
    • to restore installed system when running installer run:
      mount YOUR_PARTITION /mnt
      cat /mnt/var/backups/flash-kernel/mtd1 >/dev/mtdblock1
      cat /mnt/var/backups/flash-kernel/mtd2 >/dev/mtdblock2
  • recovery steps (if raid1 consists of /dev/sda3 and /dev/sdb3)
    • btrfs fi show
      displays whether both raid1 drives are accessible
    • echo 1 >/dev/block/sda/device/delete
      forces /dev/sda offline (e.g. in case of too many smart errors)
    • if btrfs fi show  displays that one drive is missing, e.g. sda3, do not run other btrfs commands to avoid errors which cause transition to ro filesystem, but do immediately run
      mount -o remount,degraded /dev/sdb3 /
    • attach replacement drive and check its device name with cat /proc/partitions (od dmesg), e.g sdc3
    • remove any existing btrfs data on replacement drive partition, e.g. on sdc3 by formatting it with ext4
      mkfs.ext4 -F /dev/sdc3
    • btrfs dev add -f /dev/sdc3 /
    • btrfs fi show
      • if sdc3 is shown twice, remove sdc3 with lower id from btrfs raid1
        btrfs dev del YOUR_DEVID /
      • if it is shown, that one drive is missing run
        btrfs del missing /
    • btrfs fi show
      this should know display two drives/partitions attached
    • to check for other error btrfs scrub might be run (time consuming)
    • reboot needed to remove option degraded from /proc/mounts

Switch hard disk with root filesystem

  • debian stretch on qnap sets kernel command line ROOT=”/dev/….” inside initramfs which is stored in flash. So using another hard disk requires to reflash initramfs
    • update-initramfs -u creates a new initramfs (at /boot/initrd.img-KERNEL_VERSION)
      and runs flash-kernel KERNEL_VERSION which creates /var/backups/flash-kernel/mtd2 (and mtd1) and flashes it
    • update-initramfs (/usr/share/initramfs-tools/hooks/flash_kernel_set_root) reads /etc/fstab to get the device with the root filesystem
    • use blkid YOUR_DEVICE_NAME to display the uuid of a block device
    • So to set a new root device, temporarily edit /etc/fstab (on the old root partition) to point to the new root filesystem device, run update-initramfs -u; then restore the old settings in/etc/fstab on old partition;
      verify that the new partition is correctly set in /etc/fstab on the new root partition,
    • additional step for recovery by tftp (based on instructions here)
      • backup flash partitions /dev/mtdblockN (N=0 … 5) to files mtdN
      • download installer kernel and initrd (reference)
        to determine kernel version (6281 or 6282)
      • pad kernel
        dd if=kernel-628X of=kernel.pad ibs=2097152 conv=sync (with X=1 or 2)
      • create tftp img file
        cat mtd0 mtd4 mtd5 kernel.pad initrd mtd3 > F_TS-219_di
        and copy this recovery image file to another computer or storage device
    • finally reboot
    • if reboot fails, load qnap recovery live cd iso here
      • run live cd in e.g. virtualbox (use a network adapter not connected to existing lan)
      • copy recovery image file to /tftpboot/ on live cd
        • add a second network interface connected to local lan to allow upload by ssh
        • sudo su
        • ifconfig eth1 YOUR_LAN_IP
        • set a password for user ubuntu
          passwd ubuntu
        • upload img file with scp to /home/ubuntu
        • ifconfig eth1 down (because dhcp server of live cd)
        • cp img file to /tftpboot/ and replace existing img file for your qnap device
      • power off qnap and power on with reset button pressed (until double beep):
        this should reflash qnap flash partitions with debian installer


Example btrfs send, btrfs receive (btrfs volumes mounted at /backup and at /backup2;
transfer readonly subvolume rootf_ro from volume mounted at /backup to other volume mounted at /backup2)

  • btrfs sub snapshot -r /backup/rootfs /backup/rootfs_ro

  • btrfs send /backup/rootfs_ro |btrfs receive /backup2

Resize/shrink btrfs partition with root filesystem (e.g. openmediavault needs separate data partition);
assume that btrfs partition is mounted at /backup using /dev/sda2

  • btrfs filesystem resize 32G /backup
  • fdisk /dev/sda
    • p
    • d 2
    • n 2 default start +40G
      do not delete btrfs signature
    • p
    • w
    • q
  • partprobe
  • btrfs filesystem resize max /backup
  • btrfs filesystem show
  • cat /etc/fstab

Add openmediavault (version 4.x arrakis for debian stretch): instructions

  • echo “deb arrakis main” > /etc/apt/sources.list.d/openmediavault.list
  • apt-get install openmediavault-keyring
  • apt-get update
  • apt-get install openmediavault
  • omv-initsystem

Create raid0 btrfs volume and use subvolumes as shared folders:

  • mkfs.btrfs -L datafs -d raid0 /dev/sda4 /dev/sdb4
  • then use webinterface filesystems to mount this volume
    (gets mounted at /srv/dev-disk-by-label-datafs; cat /etc/fstab)
  • btrfs subvol create /srv/dev-disk-by-label-datafs/YourShare
  • then use webinterface to define shared folder YourShare
    • use shared folder / ACL if write access fails
  • (in case of macos sierra don’t) share this folder by nfs
    • currently, 2017-11-02, apply changes fails
      (tries to mount second partition included in btrfs raid0)
    • reboot from console
    • log into webinterface and apply change again: should work
    • nfs://your_ip/export/YourShare/
      (with default settings needs option insecure)
    • currently, 2017-11-02, nfs with macos sierra is awfully slow but samba sharing (activate wins server) works
      • nfs seems to have locking issues because ejecting share from macos finder fails (unless finder is forced to restart)

minidlna media player

  • apt-get install minidlna
  • btrfs subvol create /srv/dev-disk-by-label-datafs/Musik
  • omv webinterface: add shared folder Musik on device with datafs
    which creates /sharedfolders/Musik
  • edit /etc/minidlna.conf (compare with this reference)
    • inotify=no
    • friendly_name=YOUR_MEDIA_SERVER
    • media_dir=A,/sharedfolders/Musik
  • chown -R minidlna:minidlna /sharedfolders/Musik
  • create media file index from scratch
    • systemctl stop minidlna
    • rm /var/cache/minidlna/*
    • systemctl start minidlna
  • if new mediafiles have been added
    • chown -R minidlna:minidlna
    • systemctl restart minidlna
  • if minidlna does not get discovered by plugplayer (ios), then manually add server:
  • to display status of minidlna use a webbrowser:


  • download deb from here
  • upload this file from omv webinterface section plugins
  • install plugin openmediavault-omvextrasorg
  • settings / system / OMV-Extras: activate wanted repository
  • check (again) for available plugins (or apt-get update)

usage: some devices on internal network send diagnostic emails; keep those emails in internal network by setting up a smtp server which delivers emails directly to existing dovecot mail server using lmtp.

install postfix on qnap running dovecot

addgroup postfix
adduser -D -H -G postfix postfix
addgroup postdrop

export PATH=/opt/bin:/opt/sbin:$PATH
ln -s /opt/include /usr/include
cd /opt/src
wget –no-check-certificate
tar -xzf postfix-2.11.11.tar.gz
cd postfix-2.11.11

export CCARGS=’-I/opt/include -L/opt/lib -DDEF_COMMAND_DIR=\”/opt/sbin\” \
-I/opt/include/sasl -DUSE_SASL_AUTH -DDEF_SERVER_SASL_TYPE=\”dovecot\” \
-DHAS_SSL -I/opt/include/openssl -DUSE_TLS\
-DDEF_CONFIG_DIR=\”/opt/etc/postfix\” -DDEF_DAEMON_DIR=\”/opt/libexec/postfix\” -DDEF_DATA_DIR=\”/opt/var/lib/postfix\” \
-DDEF_MAILQ_PATH=\”/usr/bin/mailq\” -DDEF_HTML_DIR=\”/opt/share/doc/postfix/html\” -DDEF_MANPAGE_DIR=\”/opt/man\” \
-DDEF_NEWALIAS_PATH=\”/opt/bin/newaliases\” -DDEF_QUEUE_DIR=\”/opt/var/spool/postfix\” \
-DDEF_README_DIR=\”/opt/share/doc/postfix/readme\” -DDEF_SENDMAIL_PATH=\”/opt/sbin/sendmail\”‘

export AUXLIBS=’-lcrypto -lssl’

export LD_LIBRARY_PATH=/opt/lib
(else postconf, called by post-install, does not find libdb)

make tidy

replace #!/bin/sh by #!/opt/bin/bash in makedefs and post-install

edit post-install: search for chown
– replace chown root by chown admin
– in case of chown $owner (followed by chgrp $group) add these lines above the line with chown (at 2 places)
case $owner in root) owner=admin;; esac
case $group in root) group=administrators;; esac

make install

edit scripts:
replace #!/bin/sh by #!/opt/bin/bash
and add below
export PATH=/opt/bin:/opt/sbin:$PATH
export LD_LIBRARY_PATH=/opt/lib

postfix requires a domainname:

  • either workaround: so set in /etc/hosts of mail clients myserver myserver.local
    if hostname of the mailserver is myserver

    • set in /opt/etc/postfix/
      myhostname = myserver.local
      mydestination = myserver, myserver.local, localhost
      mynetworks =
  • or better: if you own mydomain.tld and if you have access to dns settings
    • create A record: mail.internal.mydomain.tld
    • create MX record: internal.mydomain.tld pointing to mail.internal.mydomain.tld
    • set in /opt/etc/postfix/
      myhostname = mail.internal.mydomain.tld
      mydestination = internal.mydomain.tld, myserver, myserver.local, localhost
      mynetworks =
    • use qnap web admin interface to create a normal user myname
      mailaddress is then myname@internal.mydomain.tld
      computers on can send mails by smtp without authentication
      using mailserver mail.internal.mydomain.tld

for debugging: postfix logs to syslogd which does not run by default on qnap

  • start syslogd: syslogd
    display messages with: tail -f /var/log/messages
    when finished: killall syslogd
  • increase verbosity of postfix by editing /opt/libexec/postfix
    replace master -w by master -vvv -w
  • restart postfix:
    postfix stop
    postfix start

connect by lmtp with dovecot for non virtual user setup
mailbox_transport = lmtp:unix:private/dovecot-lmtp (in

using /opt/var/spool/postfix/private/auth
and /opt/var/spool/postfix/private/dovecot-lmtp

after everything works non existing users can be rejected by postfix (
smtpd_recipient_restrictions = reject_unverified_recipient

main config: service lmtp has to be enabled
if sasl auth is configured port 587 udp and tcp has to be added to etc services!
submission 587/tcp
submission 587/udp

so add to postfix start script
cat /etc/services |grep -q “[^0-9]587/tcp” || echo “submission 587/tcp” >>/etc/services
cat /etc/services |grep -q “[^0-9]587/udp” || echo “submission 587/udp” >>/etc/services
ln -s /opt/lib/ /lib
(last line because LD_LIBRARY_PATH=/opt/lib does not work for every subprocess of postfix)

problem when using dovecot with simple unix system users in default config:
postfix gives mail_user@mail_domain to lmtp but dovecot passdb of type shadow
and userdb of type passwd want mail_user (without @mail_domain suffix)

workaround: switch to passwd-file type of db for passdb and for userdb
which allows with args = username_format=%n to skip @mail_domain
Set in /opt/etc/dovecot/conf.d/auth-system.conf.ext:
service auth {
user = $default_internal_user
group = administrators
service auth-worker {
user = $default_internal_user
group = administrators

passdb {
driver = passwd-file
args = scheme=md5-crypt username_format=%n /etc/shadow
userdb {
driver = passwd-file
args = username_format=%n /etc/passwd

service auth {
unix_listener /opt/var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix

result: mails from local network can be sent to qnap users using smtp on port 25 without authentication

todo configure certs for tls; test smtp(s) auth

Debian Edu, that is skolelinux, aims to make the life of admins comfortable. Users, attached computers and more (stored by openldap) can be administrated by included gosa gui.

But trying to update the main server from debian jessie to debian stretch (at last 2017-08) broke gosa: Error while connecting to LDAP: Could not bind to cn=gosa-admin,ou=ldap-access,dc=skole,dc=skolelinux,dc=no (while operating on LDAP server ldaps://ldap.intern)

Because this system has been updated multiple times, the auto generated gosa ldap password was not available. And the existing hashes of the password did not work with debian stretch.

Installing updates one year later (2018-07) did not solve the access error. So it was time to dive into ldap authentication of gosa and skolelinux.

Step 1: The program slapcat allows to export the whole ldap database to a ldif file and does not require a password: export.ldif

Step 2: Edit export.ldif and replace the gosa-admin password:

  • As ldap novice I had to learn that this password has to be encrypted with slappasswd and gets stored in base64 format:
    echo “$(slappasswd)” |openssl base64
    (just enter the new password, eg. toptopsecret, twice, when asked)
  • Search for gosa-admin in export.ldif and replace the text after userPassword:: by the output generated by the command described above
  • Stop slapd (systemctl stop slapd), move /var/lib/ldap to /var/lib/ldap.orig, create /var/lib/ldap and copy DB_CONFIG from ldap.orig to ldap
  • Import the edited export.ldif file and fix ownership:
    slapadd -l export.ldif
    chown openldap:openldap /var/lib/ldap
  • Start slapd:
    systemctl start slapd
    and in case of errors
    systemctl status slapd
    journalctl -f -u slapd (maybe, set loglevel in /etc/ldap/slapd.conf from none to 65535)

Note: Step 2 might be used to migrate a skolelinux configuration from one server to another

Step 3: Add the new password as cleartext to gosa.conf (in /etc/gosa/):

  • Create backup copies of gosa.conf and gosa.secrets
  • Edit gosa.secrets so that it is an empty file
  • Edit gosa.conf and search for adminPassword and set the clear text password
    (twice: at snapshotAdminPassword and at adminPassword)
  • Restart webserver (because /etc/gosa/gosa.secrets gets included into apache config)
    apache2ctl graceful
  • Try login at https://yourip/gosa
  • If login is successful create again backup copies of gosa.conf and gosa.secrets

Step 4: Encrypt passwords in gosa.conf  with gosa-encrypt-passwords tool:

  • Remove /etc/gosa/gosa.secrets (after having made backups of gosa.secrets and gosa.conf)
  • Run gosa-encrypt-passwords
  • If successful restart webserver
    apache2ctl graceful
  • Try login at https://yourip/gosa


Looking for a simple web based app to take notes I discovered and a fork which added markdown tagging and import/export:

Installation on debian stretch using apache2 as proxy allowing password protected access by ssl (described here and here):

  • apt-get install nodejs npm redis-server

  • mkdir -p /opt/nodejs
    cd /opt/nodejs
  • adduser –no-create-home –home /opt/nodejs/scrumblr –disabled-login –gecos “Scrumblr” scrumblr

  • git clone

  • chown scrumblr: -R /var/www/scrumblr
    cd scrumblr

  • npm install
  • add scrumbler extensions of ldidry and test scrumblr on the console:
    • su scrumblr -s /bin/bash

    • git remote add fork git fetch fork git pull fork master
    • node server.js –port 4242 –baseurl /your_url_dir
      bugfix (“RangeError…”): edit config.js and change the line redis: …
      redis: argv.redis || ‘redis://’
  • netstat -ant reveals that scrumbler listens on all interfaces; so replace in server.js line server.listen(conf.port);
    server.listen(conf.port, ‘’);
  • add a service description to /etc/systemd/system/scrumblr.service (reference) and enable scrumblr.service
    (and include startup argument  –baseurl /your_url_dir)
  • configure apache2 as proxy
    • enable modules: a2enmod proxy; a2enmod proxy_http
    • add a location section to virtual host config file in /etc/apache2/sites-available/
      <Location /your_url_dir>
    • apache2ctl configtest; apache2ctl graceful
  • Open http(s)://your_server.tld/your_url_dir/
    • mini bug: the link to demo board is an absolute url and thus ignores proxy url; to fix this remove absolute path in scrumblr/views/home.jade at line p.home!=…
      p.home!= ‘<a href=”demo”>’ + ‘demo</a>’
    • here is a reference for the use of markdown syntax (e.g. insert links and images)
  • Todo: properly support editing on android and ios devices
    • ios recognizes doubleclick but cursor cannot be moved
    • android does not recognize doubleclick
    • scrumblr/client/script.js line card.children(‘.content’).editable…: if event: ‘dblclick’ gets replaced by
      event: ‘click’
      enables editing empty cards on mobile devices at the price that on the desktop dragging of a card activates edit mode
    • differentiate between ‘click’ for touch devices and ‘dblclick’ for other devices: content of diff file is here.