Advantech PCM-3643 setup in Linux 2.4 (and 2.6)

Hardware recap

The Advantech PCM-3643 is a 4- or 8-channel serial board for the PC/104 bus (industrial process control flavour of ISA). The board contains one or two quad-port UARTs (16C554) and a CPLD (universal gate array), plus some RS232 line drivers, jumper banks and passive discretes.

The quad 16C554 UART is little more than a concatenation of four standard 16C550A UARTs. In the ISA bus I/O address space, the quad UART IC is visible as four disctinct 16C550 UARTs. Hence, any PC software that's designed to handle multiple COM ports at arbitrary base I/O addresses, can use the PCM-3643.

Obviously, using this approach alone, one would soon run out of free ISA IRQ's. The PCM-3643 does allow for individual per-port IRQ setting, but there simply aren't enough of them in the ISA bus.

Therefore, the board supports IRQ sharing. Not only can multiple ports be set to drive a common IRQ line - in addition, the board features a helper bitmask register, that can be used to detect which particular port has invoked the common IRQ. This lifts the requirement to check all ports that share an IRQ whenever the respective IRQ is invoked.
Please note that all documentation from Advantech and other manufacturers of similar boards obeys the unfortunate convention of calling this helper bitmask port the "IRQ vector" - please don't confuse this with the ISR vector (meaning the interrupt service routine pointer).

The bitmask port is implemented in the CPLD and is completely independent of the 16C554 UARTs. There's only one CPLD per board, and only one bitmask register per board, regardless of whether the board is the 4-port or the 8-port variety.
The CPLD also implements address decoders and jumper-based IRQ routing. It doesn't seem to support any software-based configuration method, nor ISA-PnP.

Each UART occupies 8 consecutive ports in the I/O address space of the ISA bus (and hence of the host PC). Thus, each UART has its own base address. Nevertheless, the board has a single board-global base address, that can be selected by jumpers. The per-UART base addresses are located at integer multiples of 8 starting from the board-global base:

The default board-global UART base address is 0x240. The default bitmask register address is 0x280. Both can be changed using jumpers.

The board has several minor jumper-selectable features: IRQ's can be shared by 2, 4, or 8 ports, and there's a board-global switch to toggle the transfer rate between "normal" (default) and "high" (8x the expected nominal rate).

Please note that, due to the lack of ISA-PnP, you should reserve any IRQ's occupied by the PCM-3643 in the processor board's BIOS setup. Just set them to "Legacy ISA" rather than "PCI/ISA PnP". This prevents the PnP BIOS from assigning the respective IRQ to PnP-based devices and lets BIOS know that the IRQ line should be enabled, as there are likely some devices using it. Modern operating systems use BIOS-based information (PnP, ACPI) to bootstrap their own interrupt handlers.

When deciding which IRQ(s) to assign to the PCM-3643 board, try to use IRQ's that are either free (IRQ 5, typically) or that can be freed by turning off unneeded hardware (IRQ 7 = LPT1, IRQ 5 = soundcard). Note that the PCM-3643 comes jumpered for IRQ 3 by default, which collides with standard COM2/COM4.

Software handling in general, Linux setup in particular

Unless we can afford to assign a unique IRQ to each port, the software (ISR) must be able to handle shared IRQ's. For optimum performance, it should also support the bitmask index register. Both are standard features of the Windows driver provided by Advantech, both are possible using ComLib in DOS (or using direct hardware access), and both can be leveraged in Linux, with appropriate configuration.

The Linux kernel requires some compile-time configuration and some run-time configuration.

Compile-time configuration (make menuconfig)

In order to be able to use the PCM-3643 in Linux 2.4, we recommend to enable the following options in "make menuconfig", section Character devices :

Standard/generic (8250/16550 and compatible UARTs) serial support

Extended dumb serial driver options

Support more than 4 serial ports

Support for sharing serial interrupts

Support special multiport boards

Translated to the raw .config format, the menu entries above correspond to the following .config options:

CONFIG_SERIAL // async serial port support
CONFIG_SERIAL_EXTENDED // just a menuconfig convenience option,
// no actual impact in code
CONFIG_SERIAL_MANY_PORTS // more than 4 - actually only has
// some effect with the AST board
CONFIG_SERIAL_SHARE_IRQ // single IRQ per multiple ports - enabled
// automatically if CONFIG_SERIAL_MULTIPORT is enabled
CONFIG_SERIAL_MULTIPORT // support the helper bitmask register

Runtime configuration (using init scripts)

Before we start talking to the serial ports, we need to have the respective device nodes ready under /dev. The serial driver is a character class device. It shares a major number of "4" with other TTY drivers - the serial TTY's start at minor number 64.

If your system is missing some of the required device nodes, use some of the following commands to set them up:

If your PC/104 processor board has some serial ports onboard, e.g. ttyS0 and ttyS1, you will probably want to keep these. In that case, set your multiport board's lines to start e.g. at ttyS2.

The serial driver in the Linux kernel has few MODULE_PARM and __setup options, if any. In order to pass some configuration to it, you can't use the kernel command line (i.e. from Lilo or Grub), nor insmod arguments (nor modules.conf). The right way to configure the serial driver is via a user-space util called "setserial". This util talks to the serial driver via ioctl() syscalls, i.e. through a particular serial device that it firstly has to open().
Please note that setserial allows you to set such very basic parameters as base I/O addresses and IRQ's, and change them at runtime. This is somewhat uncommon - other device drivers tend to handle these resources strictly via insmod arguments or kernel command line, and any higher-level IOTCL-based management tools avoid touching these.

The serial driver in Linux has special hooks for a historical board called "AST Fourport", that is not manufactured anymore. The general concept of this board is fairly similar to the PCM-4643, but the dedicated software support in serial.c goes beyond just the generic IRQ sharing and index register, as the AST board seems to have had some some additional software-configurable features. To sum up, wherever you meet configuration options related specifically to the "AST Fourport", consider avoiding them, as they could result in undetected hardware or other misbehaviors.

To configure your Linux system to leverage the basic IRQ sharing and the generic bitmask register available on the PCM-3643, use the following sequence of calls to setserial:

The multiport subsystem in the serial driver can support up to four "multiport groups", with up to eight ports per group (eight bits in the bitmask register). That yields up to 32 serial ports in the system. Which is about as good as it gets on an ISA bus, without DMA etc.
This support for several multiport groups also allows us to configure the PCM-3643 for a split-config dual-IRQ setup. Please note that the table of multiport groups is indexed by IRQ, rather than by the bitmask port's I/O address:

References

Update 7th June 2007 - a few more notes

After some time, it has turned out that that the multiport board doesn't work quite right if you actually attempt to open several ports in parallel. Essentially you get lost interrupts. It seems that

The bitmask ("vector") port doesn't really work as expected

Even without the bitmask ("vector") port, if you configure the board as just four (eight) UARTs sharing an ISA interrupt, and try to open them in parallel, you get horrible delays in the data, hinting at lost interrupts. The Linux serial driver does attempt to solve such broken setups (involving shared ISA interrupts), but as written in the driver, from theory, this solution can never be perfect.

Based on some recent experience with different Advantech hardware, namely the PCL-833 that contains a full-blown i8259 PIC, I am inclined to believe that the "vector" port could indeed mean a secondary IRQ vector, cascaded i8259 style. This is really the only way to properly implement IRQ sharing on ISA.

If that was the case though, the documentation is insufficient. Interpreting the vector is perhaps not that difficult - but there's not a word about how to ack an interrupt to the board (after you've read the vector and serviced the interrupt). A possible solution is to write any data back to the vector port. However, even if that happens to work, and you do that just before the end of your custom Linux kernel-space ISR, guess what, you're running the risk of losing the next interrupt already queued on the board, as the Linux super-ISR hasn't yet acked the interrupt to the PC's own AT-PIC/APIC, and hence a further edge is not expected... You'd have to hook/avoid the Linux-internal super-ISR to get around this!

Note that in Linux 2.6, neither 8250.c nor 8250_fourport.c nor any other file in drivers/serial/* contains a hint of talking to the "fourport" bitmask register. Hence there's probably no way to use the ISA multiport IRQ-sharing boards properly from Linux 2.6 in a standard way. Maybe the driver authors have realized the futility of trying to properly service a shared interrupt on ISA under Linux, regardless of any bitmask ports or secondary private PICs...

This actually seems to be a generic ISA IRQ problem. Not with multiple ISA boards sharing a single IRQ, but with multiple asynchronous+parallel sources of interrupt sharing a single edge-triggered line (a problem inherent to ISA). If you have multiple interrupt sources on your ISA IRQ line, that can be used in such a way that they never trigger in parallel in an undeterministic fashion, you're on the safe side - you can use IRQ bitmask/status registers or a secondary PIC from Linux without a problem.
Async+parallel interrupts on ISA can be solved using a secondary PIC in combination with DOS, where the driver/application writer hooks up his very own ISR straight into the interrupt vector table, and handles the mandatory ACKing of the top-level AT-PIC by his own means. With this level of control over the interrupt mechanism, he can easily sequence the ACKing of the cascaded PIC's from top to bottom (the board-private secondary PIC is ACKed last). It's still a problem if there's a possibility that a single source can trigger twice in short succession, before the ISR manages to ACK the previous edge, but after it has actually serviced the previous interrupt...

The ultimate solution is level-triggered interrupts. If you want a dumb multiport serial board with IRQ sharing, consider using some PCI-based hardware. There are plenty of them from Advantech (the PCI-16XX series) and others.

Update 15th February 2008 - further details about 2.6 kernels

Recently I had to try to make a PCM-3643 (4-port) work under 2.6.22.6. I started out with calls to setserial along the lines of

setserial /dev/ttyS4 port 0x250 irq 10 uart 16550A ^fourport

...which I've taken from my past 2.4-based documentation.

Under Linux 2.6.22.6, this started to yell at me, but only on the last
two ports (ttyS4 and ttyS5), reporting "invalid parameter".
Ports ttyS2 and ttyS3 went without a problem.

It has turned out that, even if I enable "Support more than 4 legacy serial ports" and bump up the "Maximum number of serial ports" and "Number of serial ports to register at runtime" up to 32, only the first 4 ports, corresponding to the legacy ttyS0..ttyS3, get their kernel-side structs fully initialized. Namely, from ttyS4 upwards, the "baud_base" parameter stays at zero, though for a classic 16C550A UART this should be 115200 in most cases. If you run setserial on such an uninitialized "port slot" in the kernel, and omit to specify the Baud Base, the ioctl() handler in the kernel checks that parameter in the newly set data and slaps you back with EINVAL, without a word of further explanation in dmesg.

I was looking for something to repair in the kernel sources, but in fact all you need to do is add one more argument to setserial:

Note that the "^fourport" argument is likely redundant, so I removed that too. The recent 2.6 kernel still doesn't have any support for the "index/vector" ports of the multiport serial boards - though it does have a tiny bit of specific support for the "AST Fourport" boards, consisting in some extra init to the per-board hardware (no specific support in the IRQ handler though).

The 2.6.22.6 kernel also seems to contain some specific workaround for shared ISA edge-triggered interrupts - can't say how effective this is, but at least it's nice to know that ISA multiport boards sharing an interrupt are somehow noticed / cared about.