CI-V Programming Tutorial

A short tutorial on CI-V programming using C#

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.

Written by KB3HHA on 08/19/2020