ICOM CI-V Programming Tutorial
While I am an avid amateur radio hobbyist, I majored in computer science
in college and have been a full time software developer for the majority
of my professional career. I have always wanted to be able to combine
radio and computer programming, but I never had a radio with a suitable
computer interface. I was able to connect a KISS TNC to my 2 meter radio
and developed several Arduino and Java based APRS client applications. But
that only whetted my appetite for more. Controlling a radio directly and
being able to read and write parameters such as frequency and operating
mode was something I wanted to do, but none of my radios had the
provisions for computer control. That all changed in the spring of 2018
when I purchased an ICOM IC-7300.
The ICOM IC-7300 is an HF and 6M transceiver with RF direct sampling, a
touch screen display, a real-time spectrum scope, and most importantly for
this article, a CI-V interface. It is the CI-V interface that provides the
means to control the radio from an attached computer. While I was using
several commercial and open source applications for radio control and
digital modes that worked very well for me, I still wanted to try my hand
at developing some radio control software. I began seeing frequent
requests on several IC7300 social media groups for some basic utilities to
set the clock and to program the memories on the IC-7300. That’s when I
saw my opportunity to learn about CI-V programming. Fortunately for me the
full manual for the IC-7300 has a description of the CI-V data format and
a comprehensive listing of the CI-V commands and data formats specific to
that radio. Armed with that information and some basic knowledge of serial
communications I began my journey into CI-V programming.
The first CI-V program I wrote was a simple utility to set the date and
time on the ICOM 7300. The IC-7300 has a real time clock with a lithium
battery. If the radio isn’t powered up frequently enough the battery
discharges and the clock is reset. I found several people that were
requesting a simple utility that makes it easy to set the radio’s clock to
the date and time of a connected computer. I was able to quickly develop a
simple script to accomplish this using Windows PowerShell. Soon after that
I wrote a compiled version using C#. Using the knowledge I had gained from
writing the set clock utility I created an application in C# for managing
the memory channels in the IC-7300. At this point I was well on my way
with CI-V programming.
This article is intended to share what I have learned with others who
might be interested in trying their hand at CI-V programming. While some
of the CI-V commands are radio specific, the concepts and data formats
presented here are the same on any radio that supports CI-V.
What You Need
In order to try out CI-V programming you will need a radio that is CI-V
capable, a computer, a CI-V cable to connect the radio to your computer,
the development software of your choice, and documentation that lists the
CI-V commands for your radio. This documentation is very important because
the command, subcommand, and data values for the commands vary by radio.
The cable required depends on the specific model of radio. With the
IC-7300 I use a standard USB A-B cable. Your radio may require a more
specialized cable. For the programming language I started out using the
PowerShell scripting language on Windows. I have seen examples of people
using shell scripts on Linux. The examples in this article were written
using C#, but you could use Java, Python, C or any language that can
communicate with the radio through a serial port. The basic concepts for
CI-V communication remain the same.
Since the main subject of this article is writing software and not
hardware interfacing, I am going to assume that you have a working CI-V
connection between your radio and computer. If you need additional
information about the electrical specifications for CI-V or cabling
specific to your model radio the manual for your radio and some web
searching will provide the necessary information.
What is CI-V
Before we jump feet first into writing code some background information
will be helpful. CI-V stands for Communications Interface 5, which is
ICOM’s name for their proprietary interface and the associated data format
for remote control of a device (typically a radio). Most of the recent
radios from ICOM include a CI-V connection. In addition to communication
between a radio and a computer, CI-V can also be used for communication
between two radios or other CI-V capable devices. I will only be providing
examples of computer to radio communications in this article.
High level description of the CI-V protocol
Remote control of a transceiver using CI-V has a conversational aspect to
it. There are two sides to or participants in the conversation, the
transceiver or radio, and the controller. In this case the controller is
our software application. The controller sends a data packet in a specific
format that includes a command and any associated data that the command
requires. The radio responds with either the requested data or an OK/NG
(no good) response if the command doesn’t require that the radio return
any data.
At a high level the conversation goes something like this:
- The software controller sends a command to the radio
- The radio processes the command and performs the specified action if
any.
- If the command is a request for data from the radio, the radio
responds with the data. An example of this would be a request to
retrieve the operating frequency from the radio.
- If the command is not a request for data the radio returns an OK/NG
response to indicate success or failure. An example of this would be a
request to change a setting in the radio, such as setting the real time
clock.
There is another one sided conversational mode where the radio
“broadcasts” changes to radio settings as data on the CI-V bus. Some
examples would be changes in frequency or operating mode made by the user
operating the radio front panel controls. This permits other connected
devices or computers to “track” changes to settings in the radio. This
mode is known as CI-V Transceive. When transceive data is received no
response from the broadcast listener is expected or should be sent. This
mode is needed to develop software or hardware devices that track changes
to the radio’s settings.
Since multiple devices can reside on the CI-V bus, each device or
software application will have an address that they will respond to. This
enables multiple devices to be able to share the same bus. Different ICOM
radio models have different default CI-V addresses. In addition, the CI-V
addresses are normally user configurable in case you have more than one
radio of the same make and model communicating on the same CI-V
connection. In the case of CI-V Transceive mode, the receiver address is a
generic broadcast address since the message is not being directed towards
a specific recipient.
One more setting that affects CI-V communications is the Echo setting. If
this setting is enabled the radio will echo back each command that you
send to it. The software you write must be able to differentiate between
an echo from the radio and an actual response to the command that you sent
to it. I will explain how to accomplish that later on in this article.
CI-V Application Design Considerations
There are multiple ways to design a C#/.Net application that performs
serial I/O. Choosing whether to use polled I/O, an event driven approach,
whether to use multiple threads (with the associated locking of
resources), or a combination of these approaches depends on the
application you are building. I will not be discussing these design
approaches in this article. However there are several design
considerations related to CI-V that you should be aware of if you want to
develop a fully functional CI-V application.
Collisions
Since the CI-V bus is shared between the connected devices it is possible
for more than one device to send a message at the same time. This
condition is exacerbated when devices are placed in Transceive mode,
causing that device to potentially send many unsolicited messages. A
robust application must be able to detect when a collision occurs and
handle it appropriately.
When a sending device detects a collision it must stop transmitting the
message it is sending and send a jamming signal that consists of a
sequence of 0xFC bytes. This is done so that any device receiving the
message will know a collision has occurred and should discard that
message. After waiting for a short period the message should be resent
when the bus is silent. A device receiving the jamming signal should
discard that message and wait for the next one.
The examples in this article do not demonstrate collision detection.
Retries
In addition to collision detection, a robust application should be able
to handle the condition where a command is sent that expects a response
but the response was not received. A simplistic approach is to simply time
out after the first attempt and report an error. A production quality
application will detect when an expected response to a command was not
received and resubmit the command before reporting an error.
The examples in this article do not demonstrate retries.
Polling For Device State Changes
If you are developing an application that needs to periodically poll a
device synchronously in order to detect state changes, it is important to
introduce a pause or delay into your processing so that you don’t poll too
frequently. Polling at the maximum speed the radio can accept floods the
bus with messages, resulting in additional collisions as well as
unnecessarily using resources on both the computer and the radio being
polled. You can introduce the delay by either doing some other processing
that will take sufficient time or you can suspend the polling thread
temporarily. In C# the way to delay by suspending a thread is by using the
Thread.Sleep();
method. This method will suspend execution of
the current thread for the number of milliseconds that you specify.
Data Format
Now that we have a high level understanding of how CI-V communications
work, we can look at the details of how the individual frames are
constructed. The convention I will use is to represent numeric values as
hexadecimal. This hexadecimal representation is signified by preceding
numeric values with “0x”.
The general structure for each frame consists of a preamble, recipient
address (or a broadcast address), sender address, a variable length
command sequence, an optional variable length data field if the command
has any associated data, and an end of message code. The preamble and end
of message bytes frame the message being sent. The structure can be
depicted as follows:
Controller to radio command
Preamble |
Transceiver Address |
Controller Address |
Command number |
Sub Command Number |
Data Area |
End of Message |
0xFE |
0xFE |
0x94 |
0xE0 |
Cn |
Sc |
Data |
0xFD |
- The preamble is required and consists of two bytes that are framing
bytes and always have the value 0xFE.
- The transceiver address is the CI-V address of the radio. The default
CI-V address for the IC-7300 is 0x94. Other radio models use a different
default address.
- The controller address is typically 0xE0.
- Cn is the command number for the command you want to execute. See the
CI-V documentation for your radio.
- Sc is the optional subcommand you wish to execute. This is a variable
length field depending on the sub command and may be omitted if the
command has no subcommands. See the CI-V documentation for your radio.
- Data is the optional data to be sent to the radio. It may be required
for the command you wish to execute, and the length varies depending on
the command/sub command combination. If there is no data for the
command/subcommand you are using this field should be omitted. See the
CI-V documentation for your radio.
- End of message is required and always has the value of 0xFD
After the radio executes the command that was sent, it returns a
response. The response will either be a message that contains the
requested data or it will be an OK/NG message indicating success or
failure.
Radio to controller message with data
Preamble |
Controller Address |
Transceiver Address |
Command number |
Sub Command Number |
Data Area |
End of Message |
0xFE |
0xFE |
0xE0 |
0x94 |
Cn |
Sc |
Data |
0xFD |
- The preamble is required and consists of two bytes that are framing
bytes and always have the value 0xFE.
- The controller address is typically 0xE0.
- The transceiver address is the CI-V address of the radio. The default
CI-V address for the IC-7300 is 0x94. Other radio models use a different
default address.
- Cn is the command number for the command that was executed. See the
CI-V documentation for your radio.
- Sc is the optional subcommand that was executed. This is a variable
length field depending on the sub command. If there is no subcommand
this field is omitted. See the CI-V documentation for your radio.
- Data is the data that was returned from the radio. The length varies
depending on the command/sub command combination. See the CI-V
documentation for your radio.
- End of message is required and always has the value of 0xFD.
Although this message resembles the controller to radio message almost
exactly, notice that the order of the controller address and transceiver
address fields are reversed. This is how you distinguish a response from
the radio from an echo of the command that you sent.
If the command you requested doesn’t return any data, the radio will
instead return an OK/NG response in the following format:
OK/NG message from the radio to the controller
Preamble |
Controller Address |
Transceiver Address |
OK/NG Code |
End of Message |
0xFE |
0xFE |
0xE0 |
0x94 |
0xFB or 0XFA |
0xFD |
- The preamble is required and consists of two bytes that are framing
bytes and always have the value 0xFE.
- The controller address is typically 0xE0.
- The transceiver address is the CI-V address of the radio. The default
CI-V address for the IC-7300 is 0x94. Other radio models use a different
default address.
- 0xFB for OK or 0xFA for NG (no good).
- End of message is required and always has the value of 0xFD.
One important item to note is that all numeric data in the data area
field is specified using binary coded decimal or BCD. This ensures that
the preamble and end of message characters won’t appear in the data area.
It is important to understand how numbers are encoded using BCD in order
to correctly encode and interpret the data sent to and returned from the
radio. So what exactly is BCD? BCD is a way of representing numbers based
on the fact that the numeric digits 0-9 can all be represented using 4
bits. This means that for each 8 bit byte we can represent any two numeric
digits. Technically this would be packed BCD since we are “packing” two 4
byte digits into a single 8 bit byte. The ICOM CI-V documentation refers
to this numeric representation as BCD so I am following that convention
here.
Here are a couple of examples to help illustrate.
- The number 9 is represented in BCD as 0x09 hexadecimal. This is the
same BCD encoded and not BCD encoded.
- The number 25 would be represented in BCD as 0x25 hexadecimal. If not
BCD encoded the number 25 would be represented as 0x19 hexadecimal.
If some parts of this explanation aren’t clear at this point some
programming examples should help to clarify things. Finally we get to
write some code!
Example of sending a command that doesn’t return any data
The following code snippets were taken from the Set-7300Clock utility
program that I wrote in C#. These code snippets have little to no error
checking to make the code as clear as I can. For more complete examples in
both C# and PowerShell see my web page https://kb3hha.com/Set7300Clock.
The command used in this example is specific to the IC-7300. Other radio
models use different commands and subcommands. Check the documentation for
your radio to find the correct command sequence.
The first step is to open a serial connection to the radio. This is just
an example and you will need to use the correct parameters for your
computer and radio.
SerialPort serialPort = new SerialPort("COM1", 115200, Parity.None, 8, StopBits.One);
serialPort.Open();
Next we get the current date and time from the PC. Hopefully you are
synchronizing your computer with an accurate time source.
DateTime now = DateTime.Now;
int hour = now.Hour;
int minute = now.Minute;
int day = now.Day;
int month = now.Month;
int year = now.Year % 100;
int century = now.Year / 100;
Now we are ready to create the CI-V data and send it to the radio. We
create an array of bytes that match the format discussed above. Note that
we must convert the hour and minute to BCD.
byte[] timeRequest = { 0xFE, 0xFE, 0x94, 0xE0, 0x1A, 0x05, 0x00, 0x95, convertByteToBCD(hour), convertByteToBCD(minute), 0xFD };
serialPort.Write(timeRequest, 0, timeRequest.Length);
The request breaks down as follows:
The first two bytes are the preamble (0xFE 0xFE), followed by the
transceiver address (0x94) and the controller address (0xE0). The command
is 0x1A with a subcommand of 0x05 0x00 0x95. The data is the hour and
minute converted to BCD. The request terminates with the end of request
byte (0xFD). These values came directly from the IC-7300 documentation.
One thing to note is that this command only sets the hour and minute and
does not set the seconds. If you want “to the second” accuracy you will
need to have your software wait until the second reaches zero on your PC
clock before sending the set time command to the radio.
Here is the code for the utility to convert a byte to BCD. There are
certainly other ways to accomplish this same conversion. I chose this
method as it is fairly straight forward and easy to explain. To convert
the 10’s digit to BCD, we divide the amount we are converting by 10 and
then multiply the result by 16. Shifting the bits left by 4 would
accomplish the same thing. To get the ones digit we take the remainder of
dividing the amount we are converting by 10 and add that to the result of
the first division step. For example, if the time was 12 noon, the
conversion would be ((12/10)*16) + (12 mod 10), for a result in
hexadecimal of 0x12;
private byte convertByteToBCD(byte value)
{
return (byte)((value / 10 * 16) + (value % 10));
}
Once the command has been sent to the radio it is good practice to make
sure we got a successful response. It would probably be a good idea to do
some additional sanity checking, such as checking the response to make
sure it starts with two 0xFE characters and ends with 0xFD.
// checkResponse retrieves the response from the radio and returns true if the response is OK
private bool checkResponse()
{
byte[] response = fillBuffer();
// check if we got back a valid response from the radio, should be 6 bytes
if (response.Length < 6) return false;
// the 3rd byte should be our address and the 4th byte should be the radio's CIV address
while (response[2] != 0xE0 || response[3] != 0x94)
{
response = fillBuffer();
}
// byte 5 is either 0xFB (OK) or 0xFA (NG)
return (response[4] == 0xFB);
}
// fillBuffer reads bytes from the serial port and adds them to an array until the end of frame byte is received
private byte[] fillBuffer()
{
List<byte> response = new List<byte>();
byte inByte = (byte)(serialPort.ReadByte() & 0xFF);
while (inByte != 0xFD)
{
response.Add(inByte);
inByte = (byte)(serialPort.ReadByte() & 0xFF);
}
response.Add(inByte);
return response.ToArray();
}
If the time was successfully set we can do the same for the date. Create
the CI-V command and send it to the radio. The set date command is 0x1A
with a subcommand of 0x05 0x00 0x94
byte[] dateRequest = { 0xFE, 0xFE, 0x94, 0xE0, 0x1A, 0x05, 0x00, 0x94, convertByteToBCD((byte)century),
convertByteToBCD((byte)year), convertByteToBCD((byte)month), convertByteToBCD(day), 0xFD };
serialPort.Write(dateRequest, 0, dateRequest.Length);
We should check the response the same as we did after setting the time.
Since the code is the same I will not repeat it here.
All finished. Close the serial port and clean up.
serialPort.Close();
serialPort.Dispose();
Example of sending a command that returns data
The first example showed how to send a command with data to the radio and
get back an OK/NG response. In this example I will demonstrate how to
process a response from the radio that contains some data. A fairly common
operation to perform would be to read the operating frequency from the
radio. In order to simplify the examples I did not include any exception
handling. The full source code for this application is available at https://kb3hha.com/download/Get7300Frequency.cs.txt
The command and response used in this example is specific to the IC-7300.
Other radio models use different commands, subcommands, and data formats.
Check the documentation for your radio to find the correct command
sequence and response data format.
Just like in the set date and time example, the first step is to open a
serial connection to the radio. Just as in the prior example, you will
need to use the correct parameters for your computer and radio.
SerialPort serialPort = new SerialPort("COM1", 115200, Parity.None, 8, StopBits.One);
serialPort.Open();
Now we are ready to create the CI-V data and send it to the radio. We
create an array of bytes that match the format discussed above. Note that
there is no subcommand and we don’t send any data with this command.
byte[] request = { 0xFE, 0xFE, 0x94, 0xE0, 0x03, 0xFD };
serialPort.Write(request, 0, request.Length);
The request breaks down as follows:
The first two bytes are the preamble (0xFE 0xFE), followed by the
transceiver address (0x94) and the controller address (0xE0). The command
is 0x03 and there is no subcommand so the subcommand field is omitted.
There is no accompanying data for this request so that field is omitted.
The request terminates with the end of request byte (0xFD). These values
came directly from the IC-7300 documentation.
After sending the request we need to get back the response with the
requested data from the radio. This code uses the FillBuffer method that
we defined in the prior example. It is important to note that a robust
application needs to ensure that the message received back is a response
to the command we actually sent. The radio could be in CI-V Transceive
mode and sending broadcast messages in response to the user operating the
radio control panel. Also since the CI-V bus is a shared bus, the radio
could potentially be receiving and responding to commands from more than
one source. So we need to ensure we can match up the response to the
command we sent. This simplified example only looks at the transceiver
address and controller address. A real production application needs to do
a better job of matching responses to CI-V commands we sent to the radio.
List<byte> response = FillBuffer();
while (response[2] != 0xE0 || response[3] != 0x94)
{
response = FillBuffer();
}
Now that we have the response, we need to extract the frequency from the
data field. The manual says the data is the 5 bytes in positions 5 through
9. The first byte holds the 1 and 10 Hz values, the second byte holds the
100 Hz and 1 kHz values, and so on up to the fifth byte that holds the 100
MHz and 1000 MHz values. The following code will convert the data to an
integer frequency value. I probably went overboard on the parentheses but
I like to use them to ensure I get the result I wanted.
int frequency = (response[5] & 0x0F) + (((response[5] >> 4) & 0x0F) * 10) +
((response[6] & 0x0F) * 100) + (((response[6] >> 4) & 0x0F) * 1000) +
((response[7] & 0x0F) * 10000) + (((response[7] >> 4) & 0x0F) * 100000) +
((response[8] & 0x0F) * 1000000) + (((response[8] >> 4) & 0x0F) * 10000000) +
((response[9] & 0x0F) * 100000000) + (((response[9] >> 4) & 0x0F) * 1000000000);
All finished. Close the serial port and clean up.
serialPort.Close();
serialPort.Dispose();
I hope that you found this article informative. Maybe it will inspire
some of you to write some great radio control applications or maybe just
automate things in your shack a little bit more. The full source code for
Set7300Clock in both C# and PowerShell, the example program to read the
operating frequency, and a number of applications I have written are
available at my web site https://kb3hha.com.
I would like to personally thank Dave Bernstein AA6YQ and John Rowing
M0NRZ for their valuable feedback and assistance in improving the contents
of this article.