Thursday, December 11, 2014

9 bit UART (8M1, 8S1 modes) in Linux (hack)

Problem

As we know 9 bit UART (8M1, 8S1) mode in Linux (like in any POSIX systems) is not supported. So we can't communicate with devices which uses 9 bit.

Now we can fix it!

Information

In w: RS-485 many devices (up to 255) can share one line (two wires) and communicate with Master Device on demand. Also, RS485 half-duplex, so, only one device (master or slave) can talk into the line in the moment. Usually programmers uses protocols (Modbus or custom) which tells what device must answer to the master device (PC or  master controller). For addressing each device must have it own unique address.

Protocol example.

For example, packet can be like that:
<address>, <command>, <data_len>, [data], <crc>
 where <crc> is error detection code which is XOR sum off all bytes in packet except crc.

Let's send command with id 0x5d to our imagined device with address 0x05. This command does not  require any data bytes, so packet will be:

0x05, 0x5d, 0x00, 0x58 

So, all devices in RS485 line must read this packet and check address byte. If device address is the same as address byte of packet device understands that this packet addressed for this device.

UPD: later I added solution with sources which can be found here

Parity and 9 bit

As we can see in Wikipedia w: parity bit is the simplest way of error detection. This error detection bit can be added to the each byte of packet.

Sometime devices (usually based on RS485) uses MARK/SPACE parity for address byte marking.


In this case only address byte (first byte of our imagined protocol) marked with '1' bit of parity bit (mark parity) and all other bytes marked with '0' parity bit (space parity).

When using this technique, devices must listen and read only for bytes with '1' parity bit. Only when this byte have same value as device address device can read all other bytes of packet.

When using this technique the source code of sending function will be like (C++ style pseudo code):

---------------------------------------
bool sendData(
    const uint8_t    deviceAddress  
    const uint8_t    commandByte,
    const uint8_t    dataLength,
    const uint8_t    *data
) {
    const uint8_t crcByte = 0;
    
    //calculating CRC for this packet
    crcByte = updateXorCrc(crcByte, 1, &deviceAddress);
    crcByte = updateXorCrc(crcByte, 1, &commandByte);
    crcByte = updateXorCrc(crcByte, 1, &dataLength);

    if(dataLength)
        crcByte = updateXorCrc(crcByte, dataLength, data);


    //configuring UART parity mode for address byte
    if(!uartConfigureMode(PARITY_MARK))
        return false;

    if(!uartSendData(1, &deviceAddress))
        return false;

    //configuring UART parity mode for all other bytes
    if(!uartConfigureMode(PARITY_SPACE))
        return false;

    if(!uartSendData(1, &commandByte))
        return false;

    if(!uartSendData(1, &dataLength))
        return false;

    if(dataLength) {
        if(!uartSendData(dataLength, data))
            return false;
    }

    return false;
}

---------------------------------------

As we can see port must be reconfigured twice:
  • for PARITY_MARK when sending address byte;
  • for PARITY_SPACE when sending all other bytes.
This technique is valid for Windows systems but does not works in Linux OS. When we will try to configure port with PARITY_MARK/PARITY_SPACE in Linux this function call will return error code.

Solution

But we still have devices which uses 9 bit mode and we want work with them! Also, we want work with them from our Linux devices (for example Raspberry Pi and other nice things).


As we know, 9 bit UART (8M1, 8S1) mode in Linux (like in any POSIX systems) is not supported. So we can't communicate with devices which which uses 9 bit.

Solution is using EVEN_PARITY and ODD_PARITY except MARK/SPACE parity using. We can make little analysis for each byte for sending and configure UART parity mode (on-the-fly) for each packet byte (including our first address byte). We must count bits in each byte and select between EVEN/ODD parity understanding our final result bit ('1' or '0').

---------------------------------------
int countBits(uint8_t  value)
{
    int    ret = 0;
    while(value > 0) {
        ret++;
        value >>= 1;
    }
    return ret;
}

bool sendByte
(
    const uint8_t         dataByte,
    ComPortParity         *uartParity,
    const bool            markSpaceNeeded = false
)
{
    int             bitsCount = countBits(dataByte);
    ComPortParity   newParity = PARITY_NONE;

    //retrieving new parity type
    if(markSpaceNeeded) {
        newParity = ((bitsCount % 2) == 0) ? PARITY_ODD : PARITY_EVEN;
    } else {
        newParity = ((bitsCount % 2) == 0) ? PARITY_EVEN : PARITY_ODD;
    }
   
    //updating parity
    if(newParity != oldParity) {
        if(!uartConfigurePort(newParity)
            return false;

        *oldParity = newParity
    }
   
    return uartSendData(1, &dataByte);
}

bool sendData(
    const uint8_t    deviceAddress 
    const uint8_t    commandByte,
    const uint8_t    dataLength,
    const uint8_t    *data
) {
    const uint8_t   crcByte = 0;
    ComPortParity   uartParity = uartGetParity();

    //calculating CRC for this packet
    crcByte = updateXorCrc(crcByte, 1, &deviceAddress);
    crcByte = updateXorCrc(crcByte, 1, &commandByte);
    crcByte = updateXorCrc(crcByte, 1, &dataLength);

    if(dataLength)
        crcByte = updateXorCrc(crcByte, dataLength, data);

    if(!sendByte(deviceAddress, uartParity, true))
        return false;

    if(!sendByte(commandByte, uartParity, false))
        return false;

    if(!sendByte(dataLength, uartParity, false))
        return false;

    if(dataLength) {
        for(register int i=0; i<dataLength; ++i) {
            if(!sendByte(data[i], uartParity, false))
                return false;           
        }
    }

    return true;
}

---------------------------------------

As you can see we tries to find solution between EVEN and ODD parity and configures port on-the-fly for known result - '1' or '0'. So we emulates MARK/SPACE parity.

As a result we can communicate with old-style devices which uses 9 bit. Tested in Raspbian OS (on Raspberry Pi with FT485 USB-RS485) convertor.

Congratulations!

UPD.: later I added complete solution with demo source code. You can find it here.

3 comments:

  1. Hello, can you also post the code of the "uartSendData" function. Or can you post a demo project for the Raspberry pi on your blog or github ?

    thanks

    ReplyDelete
  2. Hello, it's just pseudo-code. It's not C++. Also, I don't have any devices that supports protocols with 9 bit. If you want I can help you, I can order device with protocol based on 9-bit (just like in text above) and write very simple program (just ping-pong with device) which will show you how to use this trick. Do you really need it? I can write some code using Python or Qt C++. Which language do you prefer?

    ReplyDelete
  3. Later I added complete solution with demo source code. You can find it here:
    http://bohdan-danishevsky.blogspot.com/2016/10/9-bit-serial-communication-in-linux.html

    ReplyDelete