-
Notifications
You must be signed in to change notification settings - Fork 1
Home
Copyright 2014, Kerry Billingham
This wiki will attempt to outline the design and architecture of j232. It will also explain the goals of the library as there is not point in designing anything unless you know what ultimately what the design is to achieve. Currently in the early stages of the design, goals will be kept broad and few to allow manoeuvrability or flexibility in the design but as time progresses it is expected that new goals will be introduced that refine the preliminary ones but at the same time not negating them.
- Create Java and JNI package and library respectively that function on Linux and allow the use of hardware serial ports from a Java application i.e. transmit and receive data over serial port link.
- Create the above package and library to operate in a simple manner so as to achieve a high reliability.
- Employ unit testing where possible.
- Employ build tools with both Java and JNI projects.
- Provide a single Java package that, together with its JNI library, can be used outside or inside an OSGi framework.
Ultimately any Java package has to provide a native library using the Java Native Interface should it wish to interact with hardware not supported in a Java SDK (JDK) and Java Runtime Environment (JRE). It would therefore make sense to examine how serial port programming is achieved at a 'native level' in C code. C is not the only way which serial ports can be accessed at a 'native level' but it is the language for which most documentation and code examples is available, hence I will deal with C or C++ exclusively when talking of native code.
The following assumes familiarity with C level code and the serial programming documents listed in the reference section on this page or elsewhere in a reference section of this wiki. The simplest of C serial programming workflows to read from a serial port can be illustrated below:

C Work Flow
For basic serial port interaction it is very much a linear workflow. This makes the C code very simple and thus very reliable. However this simplicity is at the expense of flexibility as it will be noted that once reading from the serial port has begun the operation waits, or blocks, until data is read, so-called synchronous operation. Asynchronous operations increase flexibility at the expense of simplicity however by providing simple synchronous native functions it may be possible to implement pseudo-asynchronous operation in Java code where the safety of the runtime environment may provide a robust means of achieving it. Writing data to the serial port is just as simple and the work flow is similar to that of read, the only difference being a write()
operation instead of a read()
:
write(file_descriptor, "ATZ\r", 4)
Therefore to achieve goal 2 above, synchronous operation will only be provided initially. Consequently, the JNI library functions only need to wrap the native functions which actually do the work which is again another simplification.
Effectively the Java code needs to mimic certain actions carried out in C so it is worthwhile examining each of the steps above before we can implement Java code that does the same thing.
Devices are accessed via their respective device file found under /dev
directory as specified in the Filesystem Hierachy Standard and serial port device file names generally begin ttyS
e.g. ttyS0
for the first serial port (COM1 in the DOS world). However during the uptime of a Linux machine removable serial ports such as USB serial devices may come (attached) and go (unattached). It would therefore not make sense to evaluate exactly what serial ports are available at library load time, plus this would increase complexity and unreliability. Although the native open()
function takes up to three parameters, a char-pointer to a char array containing the device file, e.g. /dev/ttyS0
, an int
value specifying the file access mode e.g. read-only, write-only or read-and-write, and a third argument only used if the file to be opened does not exist but is to be created. As were are not creating the device file for a serial port the third argument will not be considered.
In C the file access mode is typically specified by OR-ing pre-defined #define
values:
Access Mode | Description |
---|---|
O_RDONLY | Open the file for reading only |
O_WRONLY | Open the file for writing only |
O_RDWR | Open the file for both reading and writing |
(The Linux Programming Interface, Micheal Kerrisk) |
In Java code then, the simplest technique to open a serial port would be to provide a method that accepts two parameters; a String containing the device file path and thus determined by the application and NOT the library, and an int
whose value is equivalent the desired ORed flags above. However this simplicity comes with two obvious hazards; what happens if the pathname is not valid or the process does not have sufficient rights to access the device file and what happens if the file access mode flag is a value which is not equal to any or combination of the above? These points will be addressed later on in this Wiki.
The termios
structure allows various parameters to be changed on the serial port e.g. baud rate, flow control characters, input mode (canonical or non-canonical) etc. It is defined in the header file termios.h
although there maybe several versions of this file on the development machine. On my machine the particular definition of termios
is as follows:
typedef unsigned char cc_t;
typedef unsigned int speed_t;
typedef unsigned int tcflag_t;
#define NCCS 32
struct termios
{
tcflag_t c_iflag; /* input mode flags */
tcflag_t c_oflag; /* output mode flags */
tcflag_t c_cflag; /* control mode flags */
tcflag_t c_lflag; /* local mode flags */
cc_t c_line; /* line discipline */
cc_t c_cc[NCCS]; /* control characters */
speed_t c_ispeed; /* input speed */
speed_t c_ospeed; /* output speed */
#define _HAVE_STRUCT_TERMIOS_C_ISPEED 1
#define _HAVE_STRUCT_TERMIOS_C_OSPEED 1
};
There are several members of termios
that are of type tcflag_t
and two members of type cc_t
. Similar to the file mode flags passed to the open()
function the type tcflag_t
members are int
values that are ORed as required. Similarly too, if the setting of a termios
struct were wrapped with a Java native function care must be exercised when passing the int
value from Java to native. The same caution applies to the cc_t
type members.
Once the termios
structure has been populated it is a applied to the serial port with a call through to tcsetattr()
:
return_value = tcsetattr(int file_descriptor, int term_action, termios* l_termios);
The first and last parameters are the file descriptor of the serial port and termios
structure described above. The second parameter of type int
is a value and directs the termios
structure should be applied:
Action | Description |
---|---|
TCSANOW | The change occurs immediately. |
TCSADRAIN | The change occurs after all output written to the file-descriptor has been transmitted. This function should be used when changing parameters that affect output. |
TCS FLUSH | The change occurs after all output written to the object referred to by the file desscriptor has been transmitted, and all input that has been received but not read will be discarded before the changes is made. |
termios.h |
One of the above values must be passed across the Java-native boundary via the call to tcsetattr()
.
The final stage is to flush the data from the output and/or input lines and is achieved through a call to tcflush()
:
tcflush(int file_descriptor, int queue_selector)
The queue selector must be one of the following flags:
Queue | Description |
---|---|
TCIFLUSH | Flush the data received but not read. |
TCOFLUSH | Flush the data written but not transmitted. |
TCIOFLUSH | Flush data received but not read and data written but not transmitted. |
Finally the serial port is ready for a read or write:
ssize_t write(int fd, const void *buf, size_t count)
ssize_t read(int fd, void *buf, size_t count);
Typicially read()
or write()
are placed within a loop. write()
will return the number of bytes written if successful or -1 and sets the error number. write()
can also be interrupted by a signal in which case it will return the number of bytes written if at least 1 byte has been written otherwise -1 is the return value . read()
also returns the number of bytes read or -1 if unsuccessful however dependant upon the flags set during the call to tcsetattr()
, read()
may block.
The programming steps described above are very similar, set flags or a structure and call a system function. For the sake of simplicity these system calls can be wrapped with a Java method however there is a technical hurdle to overcome. The flags or structure that these system calls require will be specified in the Java domain and passed through to native code. The values of the native flags can be found in the termios.h
file but unfortunately there is no mechanism for Java code or compiler to reach into this header file and extract them. Therefore the most obvious solution is to extract the values at design time and place them into the Java code however this means it will not be future-proof. It is highly unlikely that the various native flag values will change but this is not guaranteed.
Another issue exists with native system calls that may block such as read()
. Blocking may be prevented by setting the appropriate flags during the call to open()
but a design may call for blocking during a read()
call. Without any means to unblock the system call other than by sending data to the serial port, a thread can only be released by terminating the process. This is not ideal behaviour as there may be a valid reason for the thread to escape its block but for the process to continue.
Finally both read()
and write()
require data buffers in the native and Java domains. The size of these buffers are specified in the Java domain and the native code would need to follow suit. Once again for the sake of simplicity, the buffers in native code would be allocated on the stack however as buffer sizes are specified in the Java domain there is the potential for the buffer size to be greater than the allowable native stack size.
The Linux Document Project Serial HOWTO:
http://www.tldp.org/HOWTO/Serial-HOWTO.html
The Linux Document Project Serial Port Programming HOWTO:
http://tldp.org/HOWTO/Serial-Programming-HOWTO/
Serial Programming Guide for POSIX Operating Systems, Micheal R. Sweet, 5th Edition.:
(Link may possibly be dead however I found several sources by searching for the above document title)
https://www.cmrr.umn.edu/~strupp/serial.html
Filesystem Heirarchy Standard:
http://www.pathname.com/fhs/
IBM Best Practices for using the Java Native Interface:
http://www.ibm.com/developerworks/library/j-jni/