Monday, December 6, 2021

Binary bit manipulation and CAN bus hardware interfaces in Java

You can use libraries such as BigInteger and BitSet to help you understand the messages on a hardware bus.

Download a PDF of this article

I’ve been part of a wide range of projects in my career, from very large enterprise development in Java to real-time and embedded microcontroller work using other platforms. When you need to interface with hardware devices at a low level, you often need to read and manipulate individual bits from software, which is sometimes affectionately referred to as bit banging. While most programmers don’t think of using Java for this, it’s not only possible but, as you’ll see, it’s very straightforward.

Here are some reasons to bang the bits.

◉ Perform low-level protocol programming

◉ Communicate directly with hardware without device drivers

◉ Work with constrained devices such as embedded microcontrollers

If you’re doing any work with a modern connected vehicle (the quintessential IoT device, in my opinion) you’ll need to learn to communicate with the vehicle’s components over the vehicle’s controller area network (CAN), usually referred as the CAN bus. That’s how everything is linked, including sensors—from seat belt and door status to all the electronic control units (ECUs), to more-sophisticated devices for autonomous control.

Given that CAN bus messages are often constrained to 64 bits, each packet is usually packed with as much information as possible, and a detailed bit mask is required to extract the data (or send commands).

Before diving into CAN bus communication, I’ll review the functions needed for bit processing in Java.

The basics of bits

Java does such a great job of isolating the hardware that most programmers never need to think at the bit level, so an overview of bit processing is in order.

You can think of binary values as an odometer where each digit can be only 0 or 1. For instance, decimal 0 is (using eight-bit placements) 00000000 on your odometer. Increasing to decimal 1 rolls the rightmost 0 up to 1, as expected, yielding 00000001. Similarly 9 rolls up to 0 on your decimal car odometer, thereby rolling up the preceding digit, a decimal 2 rolls up the second bit, which precedes the first bit, as shown in Figure 1.

Core Java, Oracle Java, Oracle Java Exam Prep, Oracle Java Certification, Oracle Java Prep, Oracle Java Preparation, Oracle Java Career, Oracle Java Guides, Oracle Java Skills

Figure 1. The first two bits in binary

Therefore decimal 2 is 00000010 in binary. This continues down the line with the other digits. For example, decimal 3 is 00000011 in binary. Adding binary 1 to this flips the first bit up to 0, which flips the second bit up to 0, which flips the third bit to 1, as shown in Figure 2.

Core Java, Oracle Java, Oracle Java Exam Prep, Oracle Java Certification, Oracle Java Prep, Oracle Java Preparation, Oracle Java Career, Oracle Java Guides, Oracle Java Skills

Figure 2. The first three bits in binary

The result is decimal value 4 (binary 00000100). You can try this yourself using your computer’s calculator in “programming mode,” as shown in Figure 3.

Core Java, Oracle Java, Oracle Java Exam Prep, Oracle Java Certification, Oracle Java Prep, Oracle Java Preparation, Oracle Java Career, Oracle Java Guides, Oracle Java Skills

Figure 3. You can find a binary calculator in Linux, macOS, and Windows.

Bit shifting. Bit shifting does what it implies: It shifts all bits left or right by the number of positions indicated.

The shift-left operator in Java is <<. Using this operator, 4 << 1 means “shift all bits for binary 4 (00000100) to the left by one position,” which results in 8 (00001000). Shifting left multiplies the value by 2 or, more precisely, 2 to the power of the number of places shifted.

Bit shifting right is different because there are two bit-shift operators in Java: the logical shift right, >>>, and the arithmetic shift right, >>. The reason for this difference is to maintain the sign in signed values; the most-significant bit indicates this.

Note that bit shifting is a lossy operation, meaning when you shift, the bits that fall off the end are gone forever. For example, consider the binary value 11000100. If you shift to the left by one position, you get 10001000 (the leftmost 1 has fallen off). Logically shifting back to the right doesn’t restore that 1; it fills the positions shifted with zeros, so the result is 01000100.

Bit masking. Bit masking takes place when you apply a bitwise AND operator (&) or a bitwise OR operator (|) to two operands. As a result, the binary values of each number are compared or combined, respectively. For instance, applying bitwise AND to the hexadecimal values 0xF0 and 0x0F will compare each bit in each location, resulting in a 1 where both bits are 1; otherwise, a 0 is the result in that bit location.

    0xF0 (binary 11110000)
AND 0x0F (binary 00001111)
---------------------------
  = 0x00 (binary 00000000)

The result is 0 because there are no bit locations where both bit values are 1. However, let’s look at another example.

    0xF8 (binary 11111000)
AND 0x0F (binary 00001111)
---------------------------
  = 0x08 (binary 00001000)

Since the only bits in each binary number that are both 1 are in position 3, the result is decimal 8. Bit masks can be used with bitwise AND to check if specific bits are set in a value.

The bitwise OR operator differs in that it results in 1 for a bit position if either bit at that position is 1. The result is 0 only if both bits are 0 in that position.

   0xF0 (binary 11110000)
OR 0x0F (binary 00001111)
---------------------------
 = 0xFF (binary 11111111)

Because either or both bits in each bit location contain a 1, the result is hexadecimal 0xFF (decimal 255).

With luck, you can see how these operations are important for communicating with hardware such as a vehicle’s CAN bus.

Java classes for bit processing


Java includes two classes that provide bit operations, BigInteger and BitSet. Let’s take a look at how these classes help with bit processing and explore a couple of minor shortcomings.

java.math.BigInteger. The BigInteger class supports numbers of arbitrary precision, which can be quite large; hence, its name. You can create an instance of BigInteger with a seeded value using the constructor (along with the value as a String, specifying the radix) or with the valueOf method shown below.

BigInteger mask = BigInteger.valueOf(0L);

You can set a specific bit’s value to 1 via setBit, as shown below.

mask = mask.setBit(3); // sets the bit in position 3 to 1

You can also clear a bit by calling clearBit, toggle a bit’s value between 0 and 1 via flipBit, or check if a bit is set to 1 via testBit. To apply the bitwise AND operator, you have options.

BigInteger value = BigInteger.valueOf(15L);
if ( ( value.longValue() & mask.longValue() ) > 0 ) {
    System.out.println("It's set!");
}

The & operator was used above, but you can alternatively use the and method:

BigInteger value = BigInteger.valueOf(15L);
if ( value.and(mask).intValue() > 0 ) {
    System.out.println("It's set!");
}

You can even print a value out in any radix, making it easy to display it as binary or hex via BigInteger.toString(n), where n is the radix.

java.util.BitSet. With BitSet, you can set, clear, and test ranges of bits within a value. You can also apply bitwise operators AND, OR, and XOR and do much more at a fine level of detail.

However, if you want to set a value to a specific bit range within a 64-bit long value—a typical situation that comes up in hardware communications—both BitSet and BigInteger fall short.

For instance, if you want to set the value 3 between bits 23 through 26 (a four-bit range that can store a maximum value of 15), there’s no straightforward way to do this with either class. Although you can get the value for a specific bit range using BitSet.get(from, to), you still need to jump through a hoop to convert it to a long.

BitSet b1 = BitSet.valueOf(new long[] {240L}); // binary 11110000
BitSet b2 = b1.get(3, 5); // binary 00000011
System.out.println("Result="+b2.toLongArray()[0]);

First, due to its flexibility (that part’s good), you need to provide the initial BitSet value as an array of long values. Subsequently, it’s easy to yield a new BitSet that represents the value between the bits specified. However, the result is another BitSet, and to convert that to a long you need to work with an array as the result.

The code above will yield Result = 2 as expected, but it’s not safe because if the result were a 0, BitSet would return an empty array. Therefore, you need to check each returned array and extract the value or values accordingly.

To make BitSet safer and easier to work with, I filled in the gap mentioned above by creating two new methods.

The first method, getValue, is straightforward (see Listing 1). It takes a long value and a bit range and returns a long as a result (the value within the supplied bit range), as follows:

Listing 1. getValue handles the validation and conversion of the long array.

public static long getValue(long val, int startBit, int endBit) {
    BitSet source = BitSet.valueOf(new long[] { val });
    BitSet result = source.get(startBit, endBit);

    long[] lr = result.toLongArray();
    if ( lr.length == 0) {
        return 0;
    }
    return lr[0];
}

The code checks that the array contains a value and returns the first entry. There’s an assumption that all you’re interested in is the first 64 bits, which is likely the case. You’ll need to extend the implementation to support bit masks of arbitrary length. I’ve included an implementation using BigInteger that can be used for this.

Next, to support setting a value within a range of bits in a BitSet that is seeded from a long value, I created the setValue method, which is shown in Listing 2. This one is a bit more involved, so let’s work through it.

Listing 2. setValue sets a value between a bit range within a given long value.

public static long setValue(long target, long val, 
                            int startBit, int endBit) {
    // Convert the target to a BitSet object in prep to manipulate
    BitSet result = BitSet.valueOf(new long[] { target });

    // Position the value between the start and end bits
    if ( startBit > 0 ) {
        val = (val << startBit);
    }

    // Convert it to a BitSet
    BitSet valueToSet = BitSet.valueOf(new long[] { val });

    // Clear the trailing bits to protect against overflow
    int len = valueToSet.length();
    if ( valueToSet.length() > endBit+1) {
        valueToSet.clear(endBit+1, valueToSet.length());
    }

    // Apply the bitmask via a bitwise OR operation
    result.or( valueToSet );

    long[] lr = result.toLongArray();
    if ( lr.length == 0) {
        return 0;
    }
    return lr[0];
}

First, the code converts the original value (the target) to a BitSet. Next, it takes the given value (the one to set within the target value) and moves the bits that represent it to the starting bit location via a bit-shift operation. Essentially, this creates a bit mask with that value.

Next, the code ensures that the given value fits within the bit range provided by clearing the bits from the end location specified to the end of the value’s bit range. For instance, if the start bit is 4, the end bit is 6 (essentially a range of 2 bits) and if a value greater than 2 is provided, then the value needs to be truncated to fit within those 2 bits.

The properly located and sized bit mask, created with the given value, is applied to the original (target) value via a bitwise OR operation. All of the other bits within the original value remain untouched. Finally, the BitSet is converted to a long array and, safely, the long value is extracted.

Using the enhanced BitSet


Going back to the vehicle CAN bus use case, assume you want to read a car’s seat belt and occupancy status for each seat, door status (open or closed for each door), ignition state, car speed, brake level, steer angle, and so on. Here is a summary of how the bit ranges within a 64-bit CAN bus message might represent each of these readings.

// Fictitious Automobile CAN message:
//
// 0 : Seat Occupancy 1 State; 0=empty, 1=occupied
// 1 : Seat Occupancy 2 State
// 2 : Seat Occupancy 3 State
// 3 : Seat Occupancy 4 State
// 4 : Seat Occupancy 5 State
// 5 : Seat Belt 1 State; 0=unbuckled, 1=buckled
// 6 : Seat Belt 2 State
// 7 : Seat Belt 3 State
// 8 : Seat Belt 4 State
// 9 : Seat Belt 5 State
// 10 : Door 1 State; 0=open, 1=closed
// 11 : Door 2 State
// 12 : Door 3 State
// 13 : Door 4 State
// 14 : Ignition State; 0=off, 1=on
// 15-22 : Speed (max 255)
// 23-26 : Transmission; 0=park, 1=neutral, 2=forward, 3=reverse
// (max 15)
// 27-30 : Brake level; 0=no braking, 15=max braking
// 31-38 : Steering angle; 0=full left, 128=straight, 255=full right
// 39-40 : Directional status; 0=off, 1=left, 2=right (max 3)
// 41-42 : Headlights status; 0=off, 1=low, 2=high

Using the setValue method and some constants that match the ranges above, the code in Listing 3 sets the individual values within the message value.

Listing 3. Setting the auto status readings within a fictitious CAN bus message

Long autoStatus = 0L;
autoStatus = setValue(autoStatus, OCCUPIED, SEAT_1_OCC_BIT, 
                                            SEAT_1_OCC_BIT+1);
autoStatus = setValue(autoStatus, BUCKLED, SEATBELT_1_STATE_BIT, 
                                           SEATBELT_1_STATE_BIT+1);

autoStatus = setValue(autoStatus, CLOSED, DOOR_1_STATE_BIT, 
                                          DOOR_1_STATE_BIT+1);
autoStatus = setValue(autoStatus, CLOSED, DOOR_2_STATE_BIT, 
                                          DOOR_2_STATE_BIT+1);
autoStatus = setValue(autoStatus, CLOSED, DOOR_3_STATE_BIT, 
                                          DOOR_3_STATE_BIT+1);
autoStatus = setValue(autoStatus, CLOSED, DOOR_4_STATE_BIT, DOOR_4_STATE_BIT+1);

autoStatus = setValue(autoStatus, ON, IGNITION_STATE_BIT, 
                                      IGNITION_STATE_BIT+1);
autoStatus = setValue(autoStatus, FORWARD, TRANS_STATE_START_BIT, 
                                           TRANS_STATE_END_BIT);
autoStatus = setValue(autoStatus, 55, SPEED_START_BIT, 
                                      SPEED_END_BIT);
autoStatus = setValue(autoStatus, 0, BRAKE_LEVEL_START_BIT, 
                                     BRAKE_LEVEL_END_BIT);
autoStatus = setValue(autoStatus, 128, STEER_ANGLE_START_BIT, 
                                       STEER_ANGLE_END_BIT);
autoStatus = setValue(autoStatus, OFF, HLIGHT_STATUS_START_BIT, 
                                       HLIGHT_STATUS_END_BIT);

Extracting values from the CAN bus message is just as straightforward, as shown in Listing 4.

Listing 4. Reading bit range values from within a long value

long val = 0L;
val = getValue(autoStatus, SEAT_1_OCC_BIT, SEAT_1_OCC_BIT+1);
assert(val == OCCUPIED);

val = getValue(autoStatus, SEATBELT_1_STATE_BIT, SEATBELT_1_STATE_BIT+1);
assert(val == BUCKLED);

val = getValue(autoStatus, DOOR_1_STATE_BIT, DOOR_1_STATE_BIT+1);
assert(val == CLOSED);

val = getValue(autoStatus, DOOR_2_STATE_BIT, DOOR_2_STATE_BIT+1);
assert(val == OPEN);

val = getValue(autoStatus, DOOR_2_STATE_BIT, DOOR_2_STATE_BIT+1);
assert(val == CLOSED);

val = getValue(autoStatus, DOOR_2_STATE_BIT, DOOR_2_STATE_BIT+1);
assert(val == CLOSED);

// ...

For convenience, the sample code includes a utility method that displays the binary with bit position markers for easy inspection. The output for the resulting value of the code above looks like the following:

6       5       4       3       3       2       1
3       5       7       9       1       3       5       7      0
|       |       |       |       |       |       |       |      |
0000000000000000000000000100000000000001000110111111110000100001

Talking to the CAN bus


It’s pretty straightforward to connect to a car’s CAN bus or to even create your own CAN bus system for bench testing. All you need is an RS-232 cable, a CAN interface device, and two 120 ohm resistors to serve as terminators if your CAN bus interface device isn’t internally terminated.

For my own work, I’ve used a few different CAN bus interfaces, which I’ll describe here, but the easiest to work with are those that convert CAN bus traffic to Ethernet traffic, and vice versa. In those cases, all you need is to listen on an Ethernet socket to send and receive CAN bus messages.

A CAN bus is quite simple and typically consists of only two wires for determining relative voltage levels: CAN Hi and CAN Lo. A 0 is indicated when CAN Hi > CAN Lo, and a 1 is indicated when CAN Hi <= CAN Lo.

For this test, the RS-232 cable will be the physical CAN bus, although only pin 5 (for CAN Hi) and pin 9 (for CAN Lo) will be used, as shown in Figure 4.

Core Java, Oracle Java, Oracle Java Exam Prep, Oracle Java Certification, Oracle Java Prep, Oracle Java Preparation, Oracle Java Career, Oracle Java Guides, Oracle Java Skills

Figure 4. Creating your own CAN bus with an RS-232 cable

The Ethernet interface. One device I’ve successfully worked with is the PCAN-Ethernet Gateway by PEAK-System Technik. It’s very flexible and offers a web interface for setup.

To connect the gateway to a CAN bus, locate the CAN Hi and CAN Lo connections, and connect them to pin 5 and pin 9 of your RS-232 cable with wires, respectively. Since the gateway device isn’t internally terminated, you’ll also need to place a resistor across pin 1 and pin 2, as shown in Figure 5.

Core Java, Oracle Java, Oracle Java Exam Prep, Oracle Java Certification, Oracle Java Prep, Oracle Java Preparation, Oracle Java Career, Oracle Java Guides, Oracle Java Skills

Figure 5. Connecting and terminating the CAN bus to the gateway device

Next, you’ll need to power the device. I used a universal 12-volt AC power supply that you can find from many retailers. Use wires to connect the 12-volt (1 amp maximum) AC power supply to pin 1 and pin 3 of the power connectors at the bottom of the gateway device (see Figure 6).

Core Java, Oracle Java, Oracle Java Exam Prep, Oracle Java Certification, Oracle Java Prep, Oracle Java Preparation, Oracle Java Career, Oracle Java Guides, Oracle Java Skills

Figure 6. Connecting AC power to the gateway device

Connect the Ethernet cable to the gateway. Once you’ve done this, you can connect to the admin page for the device via a browser at http://192.168.1.10, using the default username admin and password admin, and then set the Ethernet settings, as shown in Figure 7.

Core Java, Oracle Java, Oracle Java Exam Prep, Oracle Java Certification, Oracle Java Prep, Oracle Java Preparation, Oracle Java Career, Oracle Java Guides, Oracle Java Skills

Figure 7. Setting the Ethernet parameters for the gateway

The USB interface. If you prefer to interface with your CAN bus over USB, try the USB-CAN interface from Ginkgo. Connect pin 5 to the CAN1 high connection on the device, and connect pin 9 to CAN1 low, as shown in Figure 8.

Core Java, Oracle Java, Oracle Java Exam Prep, Oracle Java Certification, Oracle Java Prep, Oracle Java Preparation, Oracle Java Career, Oracle Java Guides, Oracle Java Skills

Figure 8. Connecting the interface to the CAN bus cable

Plug the USB connector into your laptop, or use a Raspberry Pi running Java to leave your laptop free to move around. To test the connectivity, follow these steps.

1. Connect the Ginkgo USB cable to a Windows, Linux, or Mac computer.
2. Download the Ginkgo USB-CAN Interface Extend Software and test the connection.

With everything connected and installed, start the Extend application and click the Play button in the upper left, as shown in Figure 9.

Core Java, Oracle Java, Oracle Java Exam Prep, Oracle Java Certification, Oracle Java Prep, Oracle Java Preparation, Oracle Java Career, Oracle Java Guides, Oracle Java Skills

Figure 9. Using the Extend test application

Make sure the settings are correct. Working Mode needs to be set to Normal, and the Band Rate should be 1000Kbps. Click the Send Data button, and ensure the status is Success, as shown in Figure 10.

Core Java, Oracle Java, Oracle Java Exam Prep, Oracle Java Certification, Oracle Java Prep, Oracle Java Preparation, Oracle Java Career, Oracle Java Guides, Oracle Java Skills

Figure 10. Using the Extend software to send test CAN packets

The Raspberry Pi. You can create a bench setup using both devices and a Raspberry Pi, as shown in Figure 11.

Core Java, Oracle Java, Oracle Java Exam Prep, Oracle Java Certification, Oracle Java Prep, Oracle Java Preparation, Oracle Java Career, Oracle Java Guides, Oracle Java Skills

Figure 11. A CAN bus bench test setup connecting two computers via a Raspberry Pi

Source: oracle.com

Related Posts

0 comments:

Post a Comment