Driver Concepts Guide

Overview

At the core of Addi-Pack lies a powerful driver layer designed to manage all communication between your application and ADDI-DATA hardware — across Windows (KMDF), Linux (DKMS) and real-time environments such as IntervalZero RTX and other RTOS-based systems.

Understanding how this layer works helps you optimise performance, reliability, and responsiveness.

This section explains:

  • The difference between polling and interrupts

  • How device identification and card selection work

  • How to use Compatibility DLLs to support applications built with the older API

  • The fundamentals of timers, counters and watchdogs

Tip

Mastering these concepts is especially useful for high-performance or multi-card systems.

Driver Architecture

Addi-Pack drivers follow a two-layer architecture:

  1. Kernel Layer (Driver)

    • Handles PCI/PCIe bus communication, hardware interrupts, and DMA transfers.

    • Implemented as a KMDF driver on Windows and a DKMS module on Linux, and compatible with real-time extensions such as IntervalZero RTX and other RTOS environments.

  2. User Layer (API)

    • Provides a unified programming interface in C, C++, and Python.

    • Communicates with the driver using IOCTL calls under the hood.

    • Exposes high-level features like timers, counters, digital I/O, and event handling.

Interrupts vs Polling

Most Addi-Pack features support both polling and interrupt-driven modes.

Mode

Description

Polling

The application checks periodically for a state change. Simple to use but inefficient for high-frequency tasks.

Interrupts

The hardware notifies the application only when an event occurs. Efficient and real-time capable. Recommended for timers and counters.

Example scenarios:

  • Digital I/O: Polling is acceptable when reading every 100–200 ms.

  • Timers / Counters: Prefer interrupts to avoid missing fast events.

Why Use Interrupts?

Interrupts are essential when you need to:

  • React to fast or unpredictable events without missing them,

  • Reduce CPU usage by avoiding constant polling,

  • Handle multiple events from different subsystems (inputs, timers, counters).

Without interrupts, your application would need to poll the hardware in a tight loop — which can:

  • Miss short pulses or transitions between reads,

  • Consume unnecessary CPU cycles,

  • Become difficult to scale or integrate with other tasks.

Using interrupts ensures that your application is notified precisely when something happens, allowing you to respond immediately and efficiently.

Typical examples where interrupts are critical:

  • Timer expiration: Trigger an action exactly every N milliseconds,

  • Counter overflow: React when a threshold is reached,

  • Digital input change: Detect a rising edge on an external signal (e.g., sensor, button).

  • Digital output fault detection: Detect a power supply failure (VCC) or a short-circuit (CC) on digital outputs.

Understanding Interrupt Handling

To use interrupts, you need to:

  1. Enable device-level interrupt support

    (AddiDataEnableDeviceInterrupts(id))

  2. Configure the interrupt source For example, select which inputs or timers should trigger an interrupt.

    (AddiDataEnableTimerInterrupt(id, timer_id))

  3. Set the interrupt mask This defines which bits (channels) will be monitored by the hardware.

  4. Register a callback In interrupt mode, your application will receive a notification function to handle events.

    (AddiDataSetDeviceInterruptCallback(id, &interruptCallback))

  5. Process the event In the callback, you typically:

    • Check the cause of the interrupt (e.g., which input changed),

    • Recording the time between interrupts

    • Read values (status, input lines, timer/counter),

    • Optionally acknowledge or reset the event flag.

Note

The interrupt mask is a bitfield that selects which channels or sources are monitored.

For example, 0x00000001 monitors channel 0, 0x0000000F monitors channels 0–3.

C Example:

void my_callback(AddiDataDeviceStruct device, InterruptData data) {
   printf("[INTERRUPT] source=0x%X | mask=0x%X\n",
         data.interrupt_source, data.interrupt_mask);

   interrupt_happened_flag = 1;
}
AddiDataDeviceStruct device;
const char* id = device.id;

AddiDataEnableDeviceInterrupts(id);         // Enable global card interrupts
AddiDataSetInterruptEventForInputs(id, 0x10, RISING_EDGE);  // Monitor input 4
AddiDataSetInterruptLogicForPort(id, 0, OR);
AddiDataSetDeviceInterruptCallback(id, &my_callback);
AddiDataEnableDigitalInterrupt(id);

Tip

All information about the device is stored in the AddiDataDeviceStruct structure, which is filled by AddiDataGetDeviceList().

Always check this structure before enabling interrupts to know:

  • Which inputs/ports are available,

  • The supported interrupt mask (e.g., 0xFFFF, 0xFFFF0, etc.),

  • Whether the card supports digital events or not.

Device Identification

Each ADDI-DATA card has a unique device ID automatically assigned at system startup. You can list all connected cards and retrieve their properties using the `get_device_info` sample.

CLI Example:

$AddiPack_ROOT/samples/get_device_info/build/get_device_info

Information shown includes:

  • Device name and model (e.g. apcie1500)

  • PCI bus and slot

  • Supported functions (digital I/O, timer, counter, watchdog)

  • Driver version and status

This information can be used to:

  • Select a specific card in multi-device systems

  • Confirm that all driver modules are properly loaded

Want to configure multiple timers with different interrupts?

TIMEBASE_UNITS timebase = MILLISECOND;
AddiDataInitTimer(&device, 0, timebase , 100, 0);
AddiDataInitTimer(&device, 1, timebase , 250, 0);

AddiDataEnableTimerInterrupt(&device, 0);
AddiDataEnableTimerInterrupt(&device, 1);

Compatibility DLLs (older API Support)

Some users already operate software built using the former Windows KMDF driver and its associated C API, providing functions such as:

  • i_PCI1032_Read1DigitalInput()

  • i_PCI1564_Set1DigitalOutputOn()

To support these use cases, Addi-Pack provides Compatibility DLLs that expose the same function names and structures as the former API, while internally redirecting calls to the modern Addi-Pack API.

Note

If the code uses functions with an ‘A’ before PCI (e.g. i_APCI1500_SetBoardIntRoutineWin32), the Compatibility DLL delivered in the present ADDI-PACK software will not be enough to run the associated code. If it is needed to run a former code using said functions, contact ADDI-DATA. (info@addi-data.com)

Note

If you have former Windows KMDF drivers installed, you must uninstall them before installing Addi-Pack. This is required to use the Compatibility DLLs.

How It Works

When a program calls the old API:

i_PCI1032_Read1DigitalInput()

the Compatibility DLL transparently redirects this call to:

AddiDataReadSingleDigitalInput()

The original function names stay valid, but the execution now uses the new Addi-Pack driver.

Why This Matters

This mechanism allows users to:

  • Keep existing Windows applications running without rewriting a single line of code,

  • Move from the legacy KMDF driver to Addi-Pack safely,

  • Replace hardware (PCI → PCIe) while keeping the same software stack,

  • Migrate gradually to the new API.

How to Use Compatibility DLLs (Windows)

  1. Install Addi-Pack normally.

  2. The Compatibility DLLs are installed in system32

  3. Run the program normally — no source code changes required.

Note

For all new development, the native Addi-Pack C, C++, or Python API is recommended for improved maintainability, performance, and cross-platform behaviour. The Compatibility DLL only ensures that existing code keeps working with the same model it was written for, but under Addi-Pack.

Use PCI Samples (Old Windows KMDF) on PCIe Hardware (Windows) using PCI DLL – Compatibility DLL Configuration

This feature lets the user keep their code unchanged when switching from APCI-XXXX to APCIE-XXXX. With this feature, the user can allow PCIe to be used with PCI functions and their DLL. By default, this feature is disabled on all Windows versions (32-bit and 64-bit). To enable it, the user has to manually configure a Windows registry key.

To enable the feature, the user can follow these steps :

  • Install Addi-Pack as described in the section Installation Guide

  • Open Run console by using shortcut WIN+R and type regedit (admin rights are needed).

  • Once regedit is open, edit the registry key named CompatibilityDLL UsePCIeAsPCI. The registry path depends on whether your process runs under WOW64 (32-bit process on a 64-bit Windows system).
    • Standard path (no WOW64 redirection) — applicable when running a 64-bit process on a 64-bit Windows system, or a 32-bit process on a 32-bit Windows system: HKEY_LOCAL_MACHINE\SOFTWARE\Addi-Data GmbH\AddiPack\CompatibilityDLL UsePCIeAsPCI

    • WOW64-redirected path — applicable when running a 32-bit process on a 64-bit Windows system: HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Addi-Data GmbH\AddiPack\CompatibilityDLL UsePCIeAsPCI

  • To enable the feature, set the registry value to 1 (DWORD). The default value is 0, which disables the feature.

Note on WOW64 registry redirection: On 64-bit Windows, 32-bit processes are subject to automatic registry redirection. When a 32-bit process reads or writes to HKEY_LOCAL_MACHINE\SOFTWARE, Windows transparently redirects the access to HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node. This means that if you configure the standard path (HKLM\SOFTWARE\…) using a 32-bit tool on a 64-bit system, Windows will automatically store the value under WOW6432Node. Conversely, if you use regedit (a 64-bit tool) on a 64-bit system, you must manually navigate to WOW6432Node to edit the key for a 32-bit application.

Fundamentals

Timer

A timer generates periodic events based on a configured timebase (µs/ms/s) and reload value. When the timer expires, it reloads and optionally triggers an interrupt.

In Addi-Pack, a timer is configured by:

  • selecting a timebase,

  • choosing a reload value,

  • applying optional modes (single-cycle or continuous),

  • then starting the timer.

Timers are ideal for periodic sampling, recurring control loops, or triggering regular actions (e.g., every 1 ms, 10 ms, or 100 ms).

Counter

A counter increments based on external pulses coming from a digital input. It is used to measure frequency, count events, or track edge transitions from sensors, encoders, or pulse sources.

In Addi-Pack, a counter is configured by:

  • selecting the counter index,

  • setting a reload value,

  • setting options if needed like edge type (rising, falling, or both) or optional gate/trigger modes (depending on the card),

  • then starting the counter.

Each time the counter reaches its reload value, it can optionally raise an interrupt. The current value and status can be read at any time.

Counters are ideal for pulse counting, speed or frequency measurement, encoder signals, or any application that needs precise tracking of external events.

Watchdog

A watchdog is a safety timer that must be refreshed (“kicked”) before its configured timeout expires. If the application fails to refresh it in time, the watchdog triggers an interrupt to signal a stall, deadlock, or timing violation.

In Addi-Pack, a watchdog is configured by:

  • selecting the watchdog index,

  • choosing a timebase (µs/ms/s),

  • setting a reload value (timeout),

  • applying optional modes depending on the board,

  • then starting the watchdog.

Watchdogs are ideal for detecting stalled loops, monitoring task deadlines, and ensuring that critical cyclic processes continue to run correctly.

Next sections:

  • Head over to Addi-Pack Samples to try interactive examples for timers, counters, and digital I/O.