Daqctl

From Technologic Systems Manuals

1 Overview

The DAQ core is a core used on multiple products such as the TS-7520-BOX, TS-7558-BOX, TS-7580-BOX, and can be ported to our other products. This core facilitates ADC, DIO, quadrature, and counter sampling, as well as DIO outputs, PWM, and optionally VFD support. The hardware is accessed using daqctl.

2 Usage

2.1 Help

 Usage: daqctl [OPTION] ...
 Technologic Systems ADC-75xx core userspace driver utility.
 Example: daqctl --server --speed=2.5khz --chan=0-3,5,7 --gain2x=5
 
   -i, --irq=N            Use IRQ N as ADC IRQ (30)
   -q, --stats            Print server daemon stats
   -s, --speed=FREQ       Use FREQ as default sample frequency (1000)
   -c, --chan=CHANS       Sample only channels CHANS (0-15)
   -x, --exttrig=PIN      Ignore --speed and use external trigger instead
   -g, --gain2x=CHANS     Use high-gain setting on CHANS (TS-7558 only)
   -g, --gain6x=CHANS     Use high-gain setting on CHANS
   -l, --cloop=CHANS      Enable 4-20 ma current loops on CHANS
   -d, --server           Daemonize and run as server
   -I, --bind=IPADDR      Bind server to IPADDR
   -p, --connect=HOST     Connect to remote daqctl service at HOST
   -P, --dumpcsv          Output acquired samples as CSV text
   -b, --dumpbin          Output acquired samples as raw binary
   -1, --single           Print one sample's values
   -D, --pwm=PWMSPEC      Sets PWM outputs according to PWMSPEC
   -h, --help             This help
 
   --irq-on-change=PIN    Flush when digital input PIN (0-56) changes
   --irq-on-glitch=PIN    Flush when digital input PIN (0-56) glitches
   --irq-on-high=PIN      Flush while digital input PIN (0-56) is high
   --irq-on-low=PIN       Flush while digital input PIN (0-56) is low
   --irq-on-quadrature    Flush when a quadrature counter changes value
   --irq-on-quad-dir      Flush when quadrature changes direction
   --irq-on-counter       Flush when a monitored edge counter changes
   --irq-on-any-change    Flush when any digital input changes
   --max-irq-rate=NSAM    IRQ at most every NSAM (1,4,16,64) samples
   --max-irq-rate-always  Always interrupt at max rate
   --min-irq-rate=HZ      Minimum IRQ rate HZ (1000,500,100,0)
   --min-irq-rate-always  Always interrupt at min rate
 
   --mux-counter0=PIN     Use digital input PIN (0-56) for counter#0
   --mux-counter1=PIN     Use digital input PIN (0-56) for counter#1
   --mux-counter2=PIN     Use digital input PIN (0-56) for counter#2
   --mux-counter3=PIN     Use digital input PIN (0-56) for counter#3
   --mux-quad0a=PIN       Use digital input PIN (0-56) for quadrature#0 A
   --mux-quad0b=PIN       Use digital input PIN (0-56) for quadrature#0 B
   --mux-quad1a=PIN       Use digital input PIN (0-56) for quadrature#1 A
   --mux-quad1b=PIN       Use digital input PIN (0-56) for quadrature#1 B
 
 When run as a server, default is to listen at TCP port 7700
 
 The --dumpbin option will send 16-bit binary samples to stdout
 where the 4 MSBs are the channel number and the 12 LSBs is the
 sample value.  This is the same raw format as the port 7700 TCP
 socket interface.
 
 The --mux-* options are valid only for TS-7520/TS-7580.  The TS-7558 does
 not support changing these pins from their defaults.
 
 The PWMSPEC format is CHANS:DUTY%[@FREQ].  Examples: "0-3:57%@100hz",
 "1,3:33.33%@20ms", "1:0x800" (literal 12-bit), or "5:5%@1.81khz"

2.2 ADC

The ADC core allows you to sample different voltage ranges, and current loops. The hardware has an 8KB FIFO to contain the latest values. After 4kb has been written the hardware core will assert an IRQ, and daqctl will pull the latest samples.

To sample all ADC channels at 100hz to a CSV file:

daqctl --dumpcsv --chan=0-7 --speed=100hz

This will return the values from 0-4095 to represent the actual voltage. Refer to your board manual for the ADC ranges, and for more information on --gain2x and --gain6x. The ADC can sample based off of either the internally configurable sample rate, or by using one of the digital inputs as an external trigger. To sample off of input channel 2:

daqctl --dumpcsv --chan=0-7 --exttrig=2

You can also read ADC as a current loop between 4mA to 20mA on all of the external ADC channels using the --cloop option.

# Sample channels 0-7.
# 4-7 will use a current loop to read 0-20mA
# 0-3 will use the default voltage range for the board
daqctl --cloop=4-7 --chan=0-7

2.3 PWM

The PWM values are defined by the #PWMSPEC. This allows you to set the duty cycle and frequency from the command line, or in C. Every channel has it's own duty cycle, but the frequencies are grouped. Every 7 DIO will their own PWM frequency, (outputs 0-6, 7-13, 14-20, ...). Changing the frequency for any output channel will change it for the group.

Turn all 8 PWM channels to 30% at 5khz:

daqctl --pwm=0-7:30%@5khz

You can also use the ADC sample frequency as the PWM frequency.

daqctl --pwm=6:30%@EXT0

2.3.1 PWMSPEC

Our PWM core will accept the PWMSPEC format as a simple way to control it. For example:

  • "0:33%" - Set duty to 33%, preserving frequency
  • "0:33%@100" - Set duty to 33%, frequency at 100Hz
  • "0:33%@1Khz" - Set duty to 33%, frequency at 1Khz
  • "0:0x1230@100" - 0x123 is the 12-bit duty pct (0x123/0x4096)
  • "0:2048" - Preserves the frequency if just setting the duty cycle
  • "0:50%@EXT0" - 50% duty, uses external trig #0 (ADC sample rate)
  • "0:33.333%@100" - Can parse decimal also
  • "0:50%@100us" - and period specs rather than frequency
  • "0-3:50%" - Can take multiple channels
  • "0,2,5-6:50%" - Can accept multiple channel ranges.

2.4 Input Channels

Every board that implements the DAQ core uses the same channel structure for sample data. When using daqctl these are the channels specified with --chan, and correspond with the libdaqctl structure's last[16] variable.

Channel Description
0 ADC Channel 0
1 ADC Channel 1
2 ADC Channel 2
3 ADC Channel 3
4 ADC Channel 4
5 ADC Channel 5
6 ADC Channel 6
7 ADC Channel 7
8 Quadrature counter delta #0
9 Quadrature counter delta #1
10 Current state of all digital inputs
11 Counter delta #0
12 Counter delta #1
13 Counter delta #2
14 Counter delta #3
15 Glitch reg or timestamp

3 Programming with libdaqctl

We provide a library for the end user called libdaqctl that you can use to interact with the daqctl TCP server.

libdaqctl.c libdaqctl.h

This will allow simple control to receive callbacks, control the PWM, or take ADC samples. The following calls are available in libdaqctl:

3.1 daqctl_connect

int daqctl_connect(char *hostspec, char *args);

Usually this call only needs to be made once. Hostspec is the tcp server to connect to where it can find the daqctl server on port 7700. This allows the libdaqctl application to reside on another system on the same network as the daqctl server. The args variable takes the same arguments to daqctl. This is where you can specify which pins to mux for quadrature or counters, set the sample rate, enable gain2x/gain6x on ADC channels, and more. A complete list of arguments is available here.

The return value is the file descriptor to the socket which should be placed in the daqctl structure to 'fd'.

3.2 daqctl_pwmspec

int daqctl_pwmspec(int sk, char *spec);

This function is used to set the value of an output using a pwmspec string. The sk argument is the file descriptor returned by daqctl_connect. The spec argument is a null terminated char array that describes one or more PWM outputs. For example, to set output 6 to a 30% duty cycle at 20hz:

daqctl_pwmspec(dq.fd, "6:30%@20hz");

3.3 daqctl_setoutput

int daqctl_setoutput(int sk, int pin, unsigned int val);
/* Max frequency = 131071 hz,  Max duty = 100% (0xfff) */
#define PWM_DUTY_PCT(x)	((0xfff * (x) / 100 ) << 1)
#define PWM_DUTY(x) ((x) << 1)
#define PWM_FREQ(x) ((x) << 13)
#define PWM_FREQ_IS_SAMPLERATE (1<<30)
#define PWM_FREQ_NOT_SAMPLERATE (3<<30)

Similar to daqctl_pwmspec, this will define the frequency and duty cycle of one pin. The sk argument is the file descriptor returned by daqctl_connect. The pin refers to the output channel to control. The output channels are mapped on the specific product (TS-7520, TS-7580, or OUT1-8 on the TS-7558 will map to pin 0-7. The value can be set to 0 to set low, or 1 to set high. The value can also be set using on of the convenience functions.

PWM_DUTY_PCT allows you to pass the duty cycle as a percent. For example:

// Set 30% duty cycle
daqctl_setoutput(dq.fd, 5, PWM_DUTY_PCT(30));

PWM_DUTY accepts a value of 0-4095 to control the duty cycle.

// iterate through all duty cycle settings:
int i;
for(i=0; i < 4095; i++) {
     daqctl_setoutput(dq.fd, 5, PWM_DUTY(i));
     usleep(10000);
}

PWM_FREQ allows you to specify the frequency of a port specified in hz. The core allows each 8 DIO ids to have one PWM frequency.

// channel 5 (0-7) to 2000hz
daqctl_setoutput(dq.fd, 5, PWM_FREQ(2000));

// You can also set a frequency and PWM_DUTY or PWM_DUTY_PCT at the same time:
daqctl_setoutput(dq.fd, 5, PWM_FREQ(1000) | PWM_DUTY_PCT(20));

PWM_FREQ_IS_SAMPLERATE will use the ADC sample rate (specified by --speed) as the frequency for an output channel. Multiple channels can use this frequency. While normally PWM frequencies are set in groups of 8, this frequency can apply to a single output channel without affecting the group.

int sk = daqctl_connect("127.0.0.1", "--speed=5khz");
...
// Using 5khz from the adc sample rate as the PWM output frequency
daqctl_setoutput(dq.fd, 5, PWM_DUTY_PCT(20) | PWM_FREQ_IS_SAMPLERATE);

PWM_FREQ_NOT_SAMPLERATE will return an output back to its grouped PWM frequency after PWM_FREQ_IS_SAMPLERATE has been set.

daqctl_setoutput(dq.fd, 5, PWM_FREQ_NOT_SAMPLERATE);

3.4 daqctl_process

int daqctl_process(struct daqctl *d);

The daqctl_process will connect to the main daqctl server and get the latest samples from the outputs, as well as update the counters, quadrature, nsamples, and trigger any callbacks that may have been requested. This will only sample the #Input Channels that have been specified in daqctl_connect. The "d" argument is a pointer to the daqctl structure. This should be cleared before first use, and the "fd" variable should be set with the result of daqctl_connect.

int sk;
struct daqctl dq;
bzero(&dq, sizeof(dq));

sk = daqctl_connect("127.0.0.1", "--chan=0-7");
assert(sk != 0);

// The daqctl structure needs the file descriptor for process calls
dq.fd = sk;
while(1) {
     daqctl_process(&dq);
     /* Process dq.last[0] through dq.last[7] for ADC values */
}

3.5 daqctl_flushreq

 int daqctl_flushreq(int sk);

The flushreq call is an advanced call to clear the hardware FIFO for more advanced tuning of your applications sample timing. This should not be needed for most applications. See #Latency Tuning for more information.

3.6 struct daqctl

After calling daqctl_process, the daqctl structure will be updated with the latest values.

int fd;

The socket file descriptor returned by daqctl_connect;

unsigned long long nsamples;

Total number of samples taken

long long last[16];

This contains all of the input channels sampled as mapped here.

unsigned short last_16bit[8];

last_16bit[8] contains the result of a DSP low-pass IIR filter that uses multiple readings to attempt to interpolate an extra 4 bits for the ADC Values.

unsigned long long counter[4];

This contains the counter values when enabled. Write 0 to reset.

long long quad[2];

These are the latest quadrature values when sampling the quadrature channels. Write 0 to reset.

void (*posedge)(struct daqctl *, int);

This will trigger a function call after executing daqctl_process with any new positive edges. See this section for an example of implementing this callback.

void (*negedge)(struct daqctl *, int);

This will trigger a function call after executing daqctl_process with any new negative edges. See this section for an example of implementing this callback.

void (*analog)(struct daqctl *, int, int);

This will trigger a function call after executing daqctl_process with any new analog values. See this section for an example of implementing this callback.

void (*digital)(struct daqctl *, long long);

This will trigger a function call after executing daqctl_process with any new digital I/O values. See this section for an example of implementing this callback.

void (*digital_change)(struct daqctl *, long long, long long);

This will trigger a function call after executing daqctl_process with any new digital I/O changes. See this section for an example of implementing this callback.

4 Example Code

4.1 ADC sampling

This example shows sampling all ADC channels.

#include <stdio.h>
#include <assert.h>
#include <strings.h>

#include "libdaqctl.h"

int main(int argc, char **argv)
{
        int sk;
        int i;
        struct daqctl dq;
        bzero(&dq, sizeof(dq));

        // You can pass any arguments as you could to 
        // the daqctl application here.  This example only samples 
        // ADC channels 4 and 5
        sk = daqctl_connect("127.0.0.1", "--chan=0-7");
        assert(sk != 0);

        // The daqctl structure needs the file descriptor for process calls
        dq.fd = sk;

        for(;;){
                daqctl_process(&dq);

                for(i = 0; i < 8; i++) {
                        printf("chan%i=%i\n", i, dq.last[i]);
                }
        }

	close(sk);
        return 0;
}

4.1.1 High Speed ADC

This ADC can sample up to 200khz when it is only sampling one channel. When coding for highest speed samples code should be careful to not output the values to places that will limit the speeds. For example, using printf to display the values as they are available will limit the application to stdout. Ideally the data can be interpreted as it comes in, or saved to memory and written to the disk in larger chunks. This example code samples channel 3 as fast as possible and saves it in memory.

#include <stdio.h>
#include <assert.h>
#include <strings.h>
#include <sys/time.h>
#include "libdaqctl.h"
 
#define TEST_SECONDS 10

// Maximum samples is 200khz, but in between a daqctl_process there can be other
// delays from other processes sbus contention so more samples can be returned
// for a single second
int samples[250000 * TEST_SECONDS];

void analog_sample(struct daqctl *dq, int channel, int value)
{
	samples[(int)dq->nsamples] = value;
}

int main(int argc, char **argv)
{
	int sk;
	int i;
	struct daqctl dq;
	bzero(&dq, sizeof(dq));      
	struct timeval start, end;
	long elapsed = 0;

	sk = daqctl_connect("127.0.0.1", "--chan=3 --speed=200khz");
	assert(sk != 0);

	// The daqctl structure needs the file descriptor for process calls
	dq.fd = sk;

	// Set callback for analog values
	dq.analog = analog_sample;

	gettimeofday(&start, NULL);
	for(;;){
		gettimeofday(&end, NULL);
		elapsed = (end.tv_sec - start.tv_sec) * 1000000 +
		  end.tv_usec - start.tv_usec;
		if(elapsed > (1000000 * TEST_SECONDS)) break;
		daqctl_process(&dq);
	}
	printf("samples per second: %0.2lf\n", ((long double)dq.nsamples)/TEST_SECONDS);


	/*for (i = 0; i <dq.nsamples; i++)
	{
		printf("%d\n", samples[i]);
	}*/

	close(sk);
	return 0;
}

4.2 Controlling a Servo

This example controls a non-continuous DC servo motor with a TS-7558.

  • 5V is connected to OUT1+ (JTAG pin 26)
  • servo's + wire is connected to OUT1+
  • servo's signal (or -) wire is connected to OUT1-
  • servo's ground is connected to Ground on P2

The desired behavior is that the servo will rotate back and forth between maximum and minimum and reverting to neutral when a ctrl+c / SIGINT is sent to the application.

#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <signal.h>
#include "libdaqctl.h"

int sigint_rec = 0;

void sigint_cb(int sig);

int main(int argc, char **argv)
{
	char *host;
	if(argc < 2)
		host = "127.0.0.1";
	else
		host = argv[1];

	int daqfd = daqctl_connect(
		host,
		"--pwm=0:100%@20ms");
	assert(daqfd != 0);

	signal(SIGINT, sigint_cb);

	for(;;)
	{
		printf("Maximum\n");
		daqctl_setoutput(daqfd, 0, PWM_DUTY(0x199)); // 2ms
		sleep(1);

		if(sigint_rec)
			break;

		printf("Minimum\n");
		daqctl_setoutput(daqfd, 0, PWM_DUTY(0x51)); // 1ms
		sleep(1);

		if(sigint_rec)
			break;
	}

	printf("Neutral\n");
	daqctl_setoutput(daqfd, 0, PWM_DUTY(0xF5)); // 1.5ms
	sleep(1);

	close(daqfd);

	return 0;
}

void sigint_cb(int sig)
{
	sigint_rec = 1;
}

4.3 Controlling an LED

This example controls the brightness of an LED by toggling power fed from an external source.

  • OUT1+ is connected to the LED -
  • OUT- is connected to the external power -
  • External power + is connected to LED +

This will cause the LED to slowly pulse on and off.

#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <signal.h>
#include <limits.h>
#include "libdaqctl.h"
 
int sigint_rec = 0;

void sigint_cb(int sig);
 
int main(int argc, char **argv)
{
        unsigned int i = 0;
        char *host;
        int dir = 0;

        if(argc < 2)
                host = "127.0.0.1";
        else
                host = argv[1];
 
        int daqfd = daqctl_connect(
                host,
                "--pwm=0:100%@120hz");
        assert(daqfd != 0);

        signal(SIGINT, sigint_cb);
 
        daqctl_setoutput(daqfd, 0, PWM_DUTY_PCT(0)); // Off
 
        for(;;)
        {
                const int step = 20;

                // This will use the default PWM frequency
                // set in the connect call
                daqctl_setoutput(daqfd, 0, PWM_DUTY(i));

                printf("%i@120hz\n", i);

                if(i == 0 || i + step >= 4095)
                        dir = !dir;

                if(dir)
                        i += step;
                else
                        i -= step;

                if(sigint_rec)
                        break;

                usleep(10);
        }
 
        daqctl_setoutput(daqfd, 0, PWM_DUTY(0)); // Off
        close(daqfd);
 
        return 0;
}
 
void sigint_cb(int sig)
{
        sigint_rec = 1;
}

4.4 Counters and Callbacks

This example has the board react to a counter value, a DIO, and it sets up callbacks for various I/O.

#include <stdio.h>                  
#include <string.h>                  
#include <assert.h>                  
#include <signal.h>                  
#include "libdaqctl.h"                  

void negedge(struct daqctl *dq, int pin) {
        printf("%d: negedge on pin %d\n", (int)dq->nsamples, pin);
}

void posedge(struct daqctl *dq, int pin) {
        printf("%d: posedge on pin %d\n", (int)dq->nsamples, pin);
}

void analog(struct daqctl *dq, int chan, int val) {
        printf("%d: analog val %x (16bit: %x) on chan %d\n", (int)dq->nsamples, val, dq->last_16bit[chan], chan);
}

void digital(struct daqctl *dq, int val) {
        printf("%d: digital val %x\n", (int)dq->nsamples, val);
}

void digital_change(struct daqctl *dq, int val, int chg) {
        printf("%d: digital change %x\n", (int)dq->nsamples, chg);
}
                          
static int alarmed;                
void alarmsig(int x) { alarmed = 1; };

int main(void) {
        int sk;
        struct daqctl dq;
        int n, o = 0;        
        sk = daqctl_connect("127.0.0.1",              
          "--chan=4-7,10-14 --gain2x=4 --cloop=2-3 --speed=5khz"
          " --irq-on-any-change --max-irq-rate=4 --min-irq-rate=500hz"
          " --pwm=0-6:0%@100hz");               
        assert(sk != -1);      

        bzero(&dq, sizeof(dq));        
        dq.fd = sk;           
        dq.negedge = negedge;      
        dq.posedge = posedge;      
        dq.analog = analog;       
        dq.digital = digital;      
        dq.digital_change = digital_change;
        alarm(10);                   
        signal(SIGALRM, alarmsig);

        for (;!alarmed;) {  
                n = daqctl_process(&dq);       
                
                if (n <= 0) {
                        fprintf(stderr, "EOF!\n");
                        break;   
                }           
                           
                /* Set PWM output #3 (OUT3) according to last sample of
                * chan #4 (AD1) */
                daqctl_setoutput(sk, 3, PWM_DUTY(dq.last[4]));
                                                                                                                                                                                                                                                                               
                /* Set PWM output #0 according DIO input #0 */
                if ((dq.last[10] & 0x1) != o) {
                        o = dq.last[10] & 0x1;

                        if (o) daqctl_setoutput(sk, 0, 1); /* 100% */
                        else daqctl_setoutput(sk, 0, 0); /* 0% */
                }             
                                                                                                                                                                                                                                                                               
                /* If counter for input #0 is greater than 10, set output
                * 1 and reset counter.  If counter for input #1 is greater
                * than 20, reset both counters and clear output 1 */
                if (dq.counter[0] > 10) { /* read and reset counter #0 */
                        dq.counter[0] = 0;
                        daqctl_setoutput(sk, 1, 1);
}           
                          
                if (dq.counter[1] > 20) {
                        dq.counter[0] = dq.counter[1] = 0;
                        daqctl_setoutput(sk, 1, 0);
                }           
}                          
        close(sk);                   

        printf("nsamples: %lld in 10 seconds\n", dq.nsamples);
        return 0;            
}

4.5 Quadrature

This example uses a Sick DKV60 encoder with a TS-7520-BOX. The A- is connected to LS_00, B- is connected to LS_01. This prints out the value whenever there is a change.

#include <stdio.h>
#include <assert.h>
#include <strings.h>
 
#include "libdaqctl.h"
 
int main(int argc, char **argv)
{
        int sk;
        int i;
	long long oldquad = 0;
        struct daqctl dq;
        bzero(&dq, sizeof(dq));

      	sk = daqctl_connect("127.0.0.1", "--mux-quad0a=0 --mux-quad0b=1 --chan=8");
        assert(sk != 0);
 
        // The daqctl structure needs the file descriptor for process calls
        dq.fd = sk;
 
        for(;;){
                daqctl_process(&dq);
                if(dq.quad[0] != oldquad) { 
			printf("quad:%lld\n", dq.quad[0]);
			oldquad = dq.quad[0];
		}
        }
 
	close(sk);
        return 0;
}

5 Advanced

5.1 TCP Interface

The 'daqctl --server' interface opens up port 7700 which can be used for communication remotely, or as generic IPC on the board. This is also a simple interface for programming with the DAQ core from a language that is not compatible with the C library.

For connecting, we still recommend calling 'daqctl' itself as the library does. This allows you to pass daqctl arguments as daqctl_connect from the C interfaces does.

# You can omit --connect if it will always be localhost
daqctl --connect=127.0.0.1 --chan=4-7,10-14 --gain2x=4 --cloop=2-3 --speed=5khz

Make sure that you check the return value of daqctl to verify that the arguments are valid.

Note: Calling daqctl --connect will reset the TCP connection.

Now open the port 7700 to control the DAQ core.

5.1.1 Outputs

PWM is controlled by writing "O" which specifies an output, followed by the PWMSPEC value. This must be terminated by a null at the end of the string. You can see a list of examples of this here.

For example, you can write the message to the TCP port:

 O2:33%@1Khz

Or with bash utilities:

echo -n "O3:0X" | tr X '\000' | nc localhost 7700

This will set output pin 2 to a duty cycle of 33% at 1khz. The change will take effect immediately in the hardware.

5.1.2 Sample Format

If you set up daqctl to sample any channels it will continuously output them in a simple format. Each packet is 16 bits:

Bits Description
15-12 Channel
0-11 12 bit reading

Any changes to the sample rate or channel selection should be done through the 'daqctl' binary itself.

Channel 10 on the TS-7520 and TS-7580 is special since there are up to 56 bits of input and normally only 12 bits of space for the values. When the sampling channel gets to 10 it writes multiple repeated samples claiming to be channel 10. Each sample contains only 11 bits of data instead of 12. Bit 11 will be set until all of the digital inputs values have been sent. To read all of the digital inputs you should continue to left shift 11 bits of input data until it sees a channel 10 sample with bit 11 cleared.

Channel 15 normally sets any bit as a 1 if it detects during the course of the immediately preceding sample period any momentary change of a digital input However if the external trigger is enabled this returns the latched value of a 12-bit up-counter that counts up at the rate of the sample period as programmed in register base + 0x4. When used for the glitch reg functionality, it behaves in a similar way to use channel 10 for input pins > 11.

You can use libdaqctl.c's daqctl_process() as an example implementation.