DIY PBX Chapter 3: Come to DAHDI

So, I physically installed the TDM410P telephony card, and now I needed to install some device drivers to run it. And, here I got into the fun stuff: software. This TDM410P clone card came with little documentation, and no software of any kind, so I had to find the driver source code, compile it, install it, configure it, load it and use it. That took a bit of research with books and the internet, and a lot of sitting at the keyboard, entering commands and checking results. But, both necessity and curiosity keep me involved, and I could do this. So, let me tell you all about it, OK?

Open-source Telephony, Digium, ZapTel and DAHDI

Last century, some very smart people wrote some open-source software for Linux that would manage a telephone system; they formed a company, Digium, and named the software Asterisk. But, at the time, you couldn't have a telephony application that stood alone; it needed to work with specific hardware that, at the time, cost a lot of money. Another bright person designed and built a simplified telephony adaptor card (the "Tormenta"), wrote an open-source driver (called "Zapata Telephony", or "ZapTel") for it and released them both to the world. Well, the cheap(er) devices won out, as more manufacturers built their own versions, and we got ZapTel as a telephony device driver framework. Digium adopted ZapTel and (eventually, because of trademark issues) renamed it to Digium Asterisk Hardware Device Interface (aka DAHDI).

So, to use the TDM410P card, I needed a DAHDI driver, which I retrieved from the Asterisk Project. The DAHDI project considers the TDM410P card as "obsolete", so I had to do a bit of research1 to locate the latest driver set that supports it.

A digression on the generics of locally-compiled applications on Slackware

Here, I have to take a detour, to tell you about Slackware and my systems. I have selected Slackware Linux as my Linux distribution. I've run Slackware for about twenty years now, and feel comfortable with it. It is "old school" Linux, modelled after BSD Unix. Most Linux users wouldn't appreciate the level of detail you need to know to manage a Slackware system, but I dont count myself as one of "most users".

Traditionally, on such systems, software compiled from source (as opposed to software supplied by the vendor) goes in the /usr/local filesystem tree; /usr/local/src for the source code, /usr/local/bin and /usr/local/sbin for the binaries, /usr/local/lib and /usr/local/lib64 for the libraries, etc. I try to maintain that structure, with some minor modifications.

So, for any one application that I compile onsite, I create /usr/local/src/application_name as a directory to hold the compilation products for that application, and within it

  • /usr/local/src/application_name/tarballs to hold the downloaded source tarballs,
  • /usr/local/src/application_name/packages to hold installation packages that I create from the compilation process,
  • /usr/local/src/application_name/application-source to hold the source code from the tarball, and
  • /usr/local/src/application_name/install to hold a fake-root installation of the application.

With this structure, I can keep record of the versions of any application I build, from source tarball all the way to installation package.

Downloading and Extracting the DAHDI source

With the dahdi-linux-complete-2.11.1+2.11.1 package, I kept to this pattern. Here, I created the source code directory structure that I worked in, downloaded the DAHDI package to the proper subdirectory, and unpacked the source:

  # mkdir -p /usr/local/src/dahdi/tarballs
  # cd /usr/local/src/dahdi/tarballs
  # wget ""
  # cd ..
  # tar -xzf tarballs/dahdi-linux-complete-2.11.1+2.11.1.tar.gz

That gave me a /usr/local/src/dahdi/dahdi-linux-complete-2.11.1+2.11.1 source code directory, containing the source of both the DAHDI device drivers, and the DAHDI administration tools.

  # cd dahdi-linux-complete-2.11.1+2.11.1
  # ls -l
  total 24
  -rw-r--r--  1 root root 6673 Mar  1  2016 ChangeLog
  -rw-r--r--  1 root root  533 Mar  1  2016 Makefile
  -rw-r--r--  1 root root 1127 Mar  1  2016 README
  drwxr-xr-x  5 root root 4096 Mar  1  2016 linux
  drwxr-xr-x 12 root root 4096 Nov 24 17:29 tools

The source code directory seems simple enough, a Changelog, README, and Makefile, and seperate drivers and tools subdirectories. The README tells us that we can make all && make install && make config from this top-level directory to build and install both the DAHDI kernel modules and the DAHDI userland tools. I, on the other hand, intend to build and install the kernel modules independantly of the userland tools, and so I descend into the linux subdirectory, where I will start with the kernel modules.

 # cd linux
 # ls
 LICENSE       Makefile  UPGRADE.txt  dahdi-modules  include
 LICENSE.LGPL  README    build_tools  drivers

Now, this is more like it. The README tells me that I need to have installed the kernel source tree (which I have, as part of the Slackware install), and tells me that (given that), I only need make && make install to compile and install the kernel modules. There are two sections in the README that I pay particular attention to: the section on "installing to a subtree" (which I will follow, as I intend to build and install a custom Slackware installation package rather than installing these modules directly), and a section on "OSLEC", which handles echo cancellation.

Another digression, to discuss Echo Cancellation

Many telephony adaptor cards have "echo cancellation" built into the hardware, as some sort of in-line DSP. Mine does not. So, in order to manage line echo (it's a thing), I have to use software. Asterisk (the PBX software) comes with a number of software echo cancellation modules that I could chose from. But the most recommended echo cancellation module doesn't come with Asterisk; the OSLEC echo cancellation module comes from the Linux kernel.

The Linux kernel provides OSLEC echo cancellation as a kernel module source (in /usr/src/linux/drivers/misc/echo), but most distributions do not compile this module into /lib/modules/. As my TDM410P card does not support hardware echo cancellation, I intend to use the software OSLEC echo cancellation instead. So, I had to decide on how to install the OSLEC echo kernel module.

I had two obvious choices: I could either

  1. recompile the existing kernel and modules, including the OSLEC module, install these new components, rebuild by boot loader, and reboot, or
  2. I seed the dahdi linux driver module source tree with the OSLEC module source (as per the OSLEC section of the README), then build and install the DAHDI modules, including the "borrowed" OSLEC module

The process to enable OSLEC and recompile the kernel is fairly straight-forward, but suffers from two drawbacks:

  1. the process to install and activate the newly-compiled kernel and modules is relatively complex and can lead to an unbootable system, and
  2. when Slackware releases the next kernel, I have to do it all over again.

The process to compile OSLEC as part of the DAHDI driver modules is initially less straightforward than a kernel compile, requiring some customization to the DAHDI driver source tree and compile script. However, once set up, I can perform later recompiles (for upgraded kernels) in a straight-forward manner.

For my DADHI and OSLEC installation, I chose to seed the dahdi driver source tree with the OSLEC source, and compile the resulting combined driver package as per the instructions at (Note that these instructions describe the process for a very back level of the Linux kernel and DAHDI, and I made some adjustments to accomodate the source tree for kernel 4.4.240 and DAHDI 2.11.1).

How I built and installed OSLEC and DAHDI

The DAHDI Kbuild looks for the OSLEC echo module in drivers/staging/echo, where it resided during the v2.x kernels. However, the kernel maintainers have since promoted the OSLEC module drivers/misc/echo. Following this plan to compile OSLEC in the DAHDI compile, I either have the driver kernel module incorrectly living in /lib/modules/4.4.240/staging/echo (as the DAHDI Kbuild would normally place it), or modify the Kbuild, and correctly place the module in /lib/modules/4.4.240/misc/echo. I chose to modify the Kbuild to properly place the module.

So, I

 # cd drivers
 # mkdir misc
 # cp -R /usr/src/linux/drivers/misc/echo ./misc/echo

to build the "proper" directory for the OSLEC echo cancellation module source, and populate it with the sourcecode from the kernel source tree.

In linux/drivers/dahdi, I edit the Kbuild file to change some paths:

 # If you want to build OSLEC, include the code in the standard location:
 # drivers/staging/echo . The DAHDI OSLEC echo canceller will be built as
 # well:
 # DIY PBX - echo moved from staging/echo to misc/echo
 #ifneq (,$(wildcard $(src)/../staging/echo/echo.c))
 #obj-m += dahdi_echocan_oslec.o
 #obj-m += ../staging/echo/echo.o
 # BEGIN DIY PBX change
 ifneq (,$(wildcard $(src)/../misc/echo/echo.c))
 obj-m += dahdi_echocan_oslec.o
 obj-m += ../misc/echo/echo.o
 # END DIY PBX change

which will cause the Kbuild to look for the OSLEC echo cancellation module in drivers/misc/echo instead of drivers/staging/echo.

Finally, in drivers/dahdi/dahdi_echocan_oslec.c, I modified the code to pull the OSLEC headers from misc/echo rather than staging/echo

 /* Fix this if OSLEC is elsewhere */
 //#include "../staging/echo/oslec.h"
 /* DIY PBX: OSLEC moved from /staging/ to /misc/ */
 #include "../misc/echo/oslec.h"
 //#include <linux/oslec.h>

Now I can "make && make install"

 # # create a fake-root install directory in /usr/local/src/dahdi/install.modules
 # mkdir ../../install.modules
 # make
 # # the next step "installs" everything, into the fake-root directory
 # make DESTDIR=/usr/local/src/dahdi/install.modules install
 # # had I wanted to install directly to my root filesystem, I would have run
 # #   make install
 # # instead

which builds all the modules and "installs" them to a directory structure in /usr/local/src/dahdi/install.modules instead in /.
From this install.modules directory, I built my Slackware installation package, saved that package in /usr/local/src/dahdi/packages, and used the standard package installation tools to install the package. Instead of going this route, I could have just installed directly into the system root directory, which takes more effort to reverse, should some component not have compiled or installed correctly.

How I built and installed the DAHDI userland tools

With the kernel modules safely installed, I now look to building and installing the DAHDI userland tools. These tools test, configure, and diagnose the DAHDI devices, and the telephony devices driven by DAHDI. So, I went back up the directory tree a bit, to the /usr/local/src/dahdi/dahdi-linux-complete-2.11.1+2.11.1 directory, and descended into the tools directory. Here, I found, along with all the tool source files, the standard README file that provided the configuration, compilation and installation instructions. I noted that this package, like many others, used the GNU Autoconf system, so I would first have to run the configure script before I performed the usual make && make install incantation.

The DADHI tools Makefile defaults it's configuration to place all libraries in ${libpath}/lib. However, for 64bit Linux, libraries should go into ${libpath}/lib64. By policy and convention, locally-compiled components should install in the /usr/local directory tree, but some of these tools have hardcoded paths directing to the /usr tree. I made the call to forego policy and let the install use /usr instead of /usr/local. However, to ensure proper placement of installation-compiled libraries in a 64bit system, I used a modified configure statement.

 # ./configure --sysconfdir=/etc --localstatedir=/var --libdir=/usr/lib64 --with-dahdi=../linux
Option Default My value Remark
--sysconfdir /etc /etc by policy and convention, locally-compiled packages store configuration in /etc. Sometimes, the makefile reparents the default to /usr/etc or /usr/local/etc. Better to set it explicitly than hope the makefile got it right.
--localstatedir /var /var by policy and convention, locally-compiled packages store state in /var. Sometimes, the makefile reparents the default to /usr/var or /usr/local/var. Better to set it explicitly than hope the makefile got it right.
--libdir ${prefix}/lib /usr/lib64 packages should install 64bit libraries in /usr/lib64, reserving /usr/lib for 32bit libraries. Without --libdir, this make install would install the generated 64bit libraries in /usr/lib. This setting redirects that installation to the proper directory.
--with-dahdi ../linux directs the compile to look for dahdi headers within the dahdi-complete package

With that done, I could now

 # # create the fake-root installation directory, same as before
 # mkdir /usr/local/src/dahdi/
 # make
 # # install the tools to the fake-root installation directory
 # make DESTDIR=/usr/local/src/dahdi/ install
 # # install some additional config files to the fake-root installation directory
 # make DESTDIR=/usr/local/src/dahdi/ install-config

and, like the DAHDI modules, I ended up with all the DAHDI tools "installed" to my fake root /usr/local/src/dahdi/ directory. And, like the DAHDI modules, I built my Slackware installation package, saved that package in /usr/local/src/dahdi/packages, and used the standard package installation tools to install the package.

Now, I had DAHDI installed, but not configured or loaded.

How I activated and configured my telephony card

With all the modules and utilities in place, I could now configure my TDM410P telephony card. First off, I loaded the driver for the card; in this case, the wctdm24xxp driver, as documented in the "Supported Hardware" section of the DAHDI linux/README file.

  # # (re)build kernel module dependencies list
  # depmod -a
  # /sbin/modprobe wctdm24xxp

Surprisingly, this worked exactly as expected; no system crash, no "module not found", no nothing. Well, not precisely nothing:

 # dmesg | grep wctdm24xxp
 [342038.266176] wctdm24xxp 0000:03:06.0: Port 1: Installed -- AUTO FXO (FCC mode)
 [342038.266190] wctdm24xxp 0000:03:06.0: Port 2: Installed -- AUTO FXS/DPO
 [342038.266197] wctdm24xxp 0000:03:06.0: Port 3: Installed -- AUTO FXS/DPO
 [342038.266203] wctdm24xxp 0000:03:06.0: Port 4: Installed -- AUTO FXS/DPO
 [342038.267065] wctdm24xxp 0000:03:06.0: Found a Wildcard TDM: Wildcard TDM410P (0 BRI spans, 4 analog channels)

The module just loaded, and with that, I could start configuring DAHDI.

DAHDI needs to load some support modules that manipulate the telephony card, and to initialise all those modules (both dahdi, and telephony card) into an operational state. The dahdi_genconf(8) script interacts with the hardware driver and builds a configuration file in /etc/dahdi/system.conf, which a later DAHDI startup command (dahdi_cfg(8)) will use to initialize the DAHDI interface.

DAHDI installs two udev rules sets: /etc/udev/rules.d/dahdi.rules and /etc/udev/rules.d/xpp.rules. These udev rules prepare some device symlinks, and invoke some DAHDI tools to further initialize detected devices. The dahdi.rules expects to set ownership of the DAHDI udev subsystem to user asterisk, group asterisk, so I added such a user and group to my system. I will run Asterisk as user asterisk, group asterisk (to ensure privilege seperation between it and root), so it makes sense to give ownership of the telephony devices to that user as well.

With all those in place (the asterisk user, the udev rules and their support programs, and /etc/dahdi/system.conf), I ran dahdi_cfg(8). Silence... crickets... nothing. But /proc/modules showed that my driver and the DAHDI drivers had all loaded. The lsdahdi(8) command provided me with definite proof that my card was alive:

 # lsdahdi
 ### Span  1: WCTDM/0 "Wildcard TDM410P" (MASTER) 
   1 FXO        FXSKS       (EC: OSLEC - INACTIVE)  RED
   2 FXS        FXOKS       (EC: OSLEC - INACTIVE)  
   3 FXS        FXOKS       (EC: OSLEC - INACTIVE)  
   4 FXS        FXOKS       (EC: OSLEC - INACTIVE)  


Of course, I still have to configure my initsystem (init(1), not one of the fancier init systems) to initialize the card on system startup. DAHDI supplies an example startup script meant for pre-systemd Debian and Red Hat, which I will sculpt (not hack, I never "hack"). But, for now, I've got the card up and running. Now onwards to Asterisk, and a dial-tone.

The home of Asterisk and DAHDI
Source code for DAHDI modules and tools, packaged in release-numbered tarballs
The chapter(s) on DAHDI in the book em>Asterisk: The Definitive Guide
The whole book: Asterisk: The Definitive Guide
A summary of DAHDI, with reference to it's predecessor, Zapata
This document describes requirements and procedures of DAHDI's installation.
The dahdi-tools installation instructions, htmlified
DAHDI "Quick Start From Source" instructions
System Management: 


Of course, I had "DAHDI issues". While the proper modules loaded, and the DAHDI utilities reported success, I still had to "tune" the card. And, this came in four parts:

  1. I needed to modify and customize the /etc/dahdi/system.conf file generated by the dahdi_genconfig utility,
  2. I had to diagnose, debug, and remedy a failure of the dahdi.rules Udev rules,
  3. I had to "look into" the supplied dahdi.init init script, and
  4. When "ringing" the attached handsets, none of the handsets would actually "ring"

Customizing the DAHDI configuration

Starting at /etc/dahdi/system.conf, I changed the echo cancellation from mg2 to oslec with the following edit:

# Changed echo cancellation to oslec
# echocanceller=mg2,1-4
# End of echo cancellation change

When I reran dahdi_cfg and lsdahdi, I got

$ lsdahdi
### Span 1: WCTDM/0 "Wildcard TDM410P" (MASTER)

Much better.

Diagnosing and fixing the Udev rules

I had noticed that udev had pumped out some dmesg output relating to my DAHDI install. Specifically, it could not execute the /usr/share/dahdi/dahdi_handle_device and /usr/share/dahdi/dahdi_span_config scripts specified by the provided /etc/udev/rules.d/dahdi.rules ruleset.

With some deep-diving into these two scripts, and the timestamps issued by udev, I came to the conclusion that

  1. in my setup, the initial execution of udev ran before the system mounted /usr , which meant that udev could not locate the scripts specified in the dahdi udev rules
  2. the scripts, as provided in the dahdi package, do not handle the ACTION passed in by udev ("change"), and
  3. assuming that this was simply an oversite, and the scripts should (in this case) treat "change" the same as "add", the scripts did nothing with my dahdi modules (the scripts were meant to assign spans, and my modules defaulted to doing that on their own)

So, here, I simply commented out the two offending udev rule lines in /etc/udev/rules.d/dahdi.rules,

# hotplug scripts
# 2020-12-07 disabled dahdi_handle_device and dahdi_span_config
# 1) on initial udev execution, /usr not mounted, so scripts not accessable
# 2) scripts do not account for current udev default ACTION=change
# 3) scripts do nothing with current dahdi module default of auto_assign_spans to 1
#SUBSYSTEM=="dahdi_devices", RUN+="%E{DAHDI_TOOLS_ROOTDIR}/usr/share/dahdi/dahdi_handle_device"
#SUBSYSTEM=="dahdi_spans", RUN+="%E{DAHDI_TOOLS_ROOTDIR}/usr/share/dahdi/dahdi_span_config"

and let udev only handle the creation of its "legacy" /dev entries.

Looking into the init script

I found a reference to a sample "init script" buried in the documentation. While the install did not place an init script anywhere, some looking around uncovered a dahdi.init script in the source code directory. It took a while to read through and understand what this script did, but I did so, and found it both interesting and necessary. The authors had made this script responsible for the loading and configuring of the dahdi modules, as well as comprehensively unloading and reloading them on demand. I surmised (with the documentation clues) that this script came pre-udev, and concluded that it should suit as a replacement for the (nonfunctional) udev-invoked scripts that I had disabled.

But, the authors had written the dahdi.init script with (pre-systemd) Red Hat and Debian systems in mind. and I run Slackware. I endevoured to refactor the script into something I could plunk into my Slackware /etc/rc.d directory, and invoke at the appropriate point. I won't bore you with the details (as they will only interest Slackware admins), but suffice it to say that the refactored script, when run at the appropriate time, resulted in a few more /dev/dahdi entries than I had seen previously.

$ ls -l /dev/dahdi
total 0
lrwxrwxrwx 1 root root 12 Dec 7 22:45 1 -> chan/001/001
lrwxrwxrwx 1 root root 12 Dec 7 22:45 2 -> chan/001/002
lrwxrwxrwx 1 root root 12 Dec 7 22:45 3 -> chan/001/003
lrwxrwxrwx 1 root root 12 Dec 7 22:45 4 -> chan/001/004
drwxr-xr-x 3 root root 60 Dec 7 22:45 chan
crw-rw---- 1 asterisk asterisk 196, 254 Dec 7 22:45 channel
crw-rw---- 1 asterisk asterisk 196, 0 Dec 7 22:45 ctl
drwxr-xr-x 2 root root 60 Dec 7 22:45 devices
crw-rw---- 1 asterisk asterisk 196, 255 Dec 7 22:45 pseudo
crw-rw---- 1 asterisk asterisk 196, 253 Dec 7 22:45 timer

I have to assume that something will or can use these entries, and so I count this as an all-round win.

Making the ringers sound

With all these changes in place, I now had two remaining problems (discovered only after I got Asterisk installed and configured - that's another story). First off, the FXO (which connects to the incoming telephone line) reported an "ALARM". This problem seems to be related to the over-long extension cord I had to use to connect the system (in its temporary location) to my telephone-provider's PSTN connection, as I managed to make it work, intermittantly. This, I will look into later.

The other problem, perhaps not as pressing, but still important, occurs when a call "rings through" to the analogue handsets. The call completes (you can pick up a handset and have a conversation), but the "ringer" never rings. I suspected that this might relate to ringer voltages, and switched out my ancient handset with a more modern one. That worked; the more modern handset needs less voltage to activate the ringer than the almost 40-year-old handset that I test with. So, not a problem, so long as I stick with modern telephones when I switch over. But, this still bugged me.

While an OEM TDM410P provides additional PC power-supply connections to run the analogue handset ringers, the TDM410P clone I had purchased did not. This could have scuttled part of my plan, had I not remembered that some drivers include options to boost the ringer voltage output by the card. I checked, and the wctdm24xxp driver module does indeed support the boostringer option.

So, I created a /etc/modprobe.d/wctdm24xxp.conf file that provided the option:

# TDM410P Telephony card support
# boostringer=1 should cause FXS to use greater voltage on ringer
options wctdm24xxp boostringer=1

and reloaded all the modules. A quick test, and I can confirm that even the ~40-year-old analogue handset rings properly, now.


As far as I'm concerned (barring something discovered later in my DIY PBX journey), my DAHDI issues are over. Onward! Excelsior!