Close

Time Multiplexing with the HC-12 Wireless Transceiver Module

A project log for Open Tag

The best game of laser tag you've never played. Choose your class - from a sniper to a pyro, and customize your own game of laser tag!

opentagopentag 12/07/2020 at 01:000 Comments

I'm using the HC-12 wireless transceiver module to allow each laser tag device to communicate wirelessly. The idea is that when you are tagged by someone, you can communicate back to the device that tagged you. That way, when you tag someone, you can indicate on your device that you successfully tagged someone. I got some feedback from players that it was hard to tell if they were tagging other people. The LED's on your head blink when you tag someone, but adding an indicator on your device may help. Plus, it paves the way to adding all sorts of other features that I want to have in the future, like regaining health when you tag someone.

I chose the HC-12 instead of the nRF24L01+ for a few reasons:

  1. I am using just about every single pin on the arduino UNO, and I don't have enough pins to support the nRF24L01+, and I have enough pins for the HC-12
  2. I am using 90% of the program memory on the arduino UNO, and I don't want to add another library and potentially run out of program space for other features I want to add later

And yes, I could upgrade to a different microcontroller and get more pins, more program memory, etc. And, for the next version, I'm planning on using a Blue Pill, if it meets the requirements for version 2.0. But, if I want to release a completed version 1.0, then I need to stop giving in to feature creep. Plus, based on my experience, the arduino UNO is a much easier starting point for beginners. Getting the Blue Pill running was a minor headache, as the tutorials I followed did not spell out how to do everything, and I had to do a bit of Googling and talk to a friend who had used one before in order for me to get it running using the Arduino software. The ultimate goal for this project is to make this beginner friendly, so if I can use an UNO, which has a low barrier to entry, then I'm going to. 

After getting the radio set up (with the help of this tutorial) and reading the data sheet, I decided to have each device set to a different frequency. That way, I wouldn't have to worry about devices trying to send data at the same time, which would cause the radios to collide and potentially not send the correct data. And, then, after implementing it, I realized that it takes 40 ms to enter programming mode, and 80 ms to leave it. If you get tagged, change channels, send a message, and then change channels back, that's 240 ms, or about a quarter of a second. Which means that mostly, you aren't going to be listening for whether or not you have tagged someone else. And, if you are tagged by two or more people (which, can happen every 350 ms), you won't have enough time to switch back and forth between the two radio channels to send the data to each person. In essence, with at 240 ms delay between sending each data packet, this system can't scale with multiple people sending and receiving tags at the same time.

To fix this, I decided to use time multiplexing to keep the devices from sending radio transmissions at the same time. The way I set this up is as follows:

  1. A base, or "master" device, sends a command to enter time sync mode. In this mode, the arduino ignores everything except for sending and receiving radio commands
  2. The base, or "master" device, sends a command to synchronize all radios.
  3. Once synchronized (or, if it times out), each device picks a set time from the synchronize radio command to send any data that it needs to send.

For example, if there are three radios, and I want them to send data one time a second, then the first radio will send the data in the first third of a second, the second radio will send data in the second third of a second, and then the third radio will send data in the last third of a second. Every second, all three radios can send data, and they will always send in the order, 1,2,3 (if they have any data to send). I've set up the system to send data every 250 ms, with a 3 ms window for each device to send, and 2 ms of silence (for 25 total transmitting devices). That way (hopefully), during a 5-15 minute game of laser tag, none of the devices send data at the same time, even if they drift a bit from the correct time to send (each clock has up to 5% error with the resonators I'm using).

So far, when testing on the bench, the system works. I've ran into some weird quirks, where it takes about 40 seconds before I can tag multiple devices at the same time and have them both send data properly, or that when I have a device constantly outputting radio chatter, the devices can't communicate (there is a limit on the amount of radio chatter that can happen before the system breaks down), but, so far, it's working. Mostly. I'll have to check it in the field to see how well it works with 8-10 players, and five bases, but so far it looks promising.

I may need to play with the baud rates to get it to work reliably, but so far, so good.

I'll definitely upgrade this to using the nRF24L01+ and using their Multiceiver (TM) functionality, but so far, everything is working alright. 

If you want to check out the code or use it in your own project, I've included the two functions for sending and receiving code below. 

If you want to check it out in the context of the full laser tag code, you can find that through the link below. The functions used are:

Note that I try to avoid using delays in my code. To avoid this, I call each of these functions in the main loop and use the milis() function to check whether or not it's time to send radio data or turn off an LED.

https://create.arduino.cc/editor/gukropina1/4b1b9bc5-1348-4545-bb0a-6da691361e60/preview

But anyway, here's the code. If you want to check out more about the project, click here.

#define RADIO_TIME_PER_DEVICE 3             //each device has a 3 ms window to transmit
#define RADIO_SILENCE_TIME 2                //each device has 2 ms of silence between transmit times
#define WIRELESS_SEND_BUFFER 24             //can store 24 bytes (8 messages) for sending out the radio
#define WIRELESS_MESSAGE_LENGTH 3           //number of bytes in radio messages
#define SET_PIN 4                           //programming pin for radios
#define MAX_RADIOS_USED 25                  //maximum number of radios used
//time in microseconds for all radios to send their instructions, before they loop and can send again
//RADIO_PERIOD = MAX_RADIOS_USED*(RADIO_TIME_PER_DEVICE + RADIO_SILENCE_TIME);
#define RADIO_PERIOD 250
//radio bytes:
//first byte: who the transmission is sent to
//second byte: what command is sent
//third byte: value - what value to set things to

#define DEVICE_ID 0

#include <softwareserial.h>

SoftwareSerial HC12(10, 11); // HC-12 TX Pin, HC-12 RX Pin

void setup() {
  pinMode(SET_PIN, OUTPUT);       //set set pin to output
  Serial.begin(9600);             // Serial port to computer
  HC12.begin(9600);               // Serial port to HC12
  digitalWrite(SET_PIN, LOW);     //start programming
  delay(50);                      //delay for HC-12 to go into programming mode
  HC12.print(F("AT+DEFAULT"));    //set HC-12 to default mode
  delay(50);
  digitalWrite(SET_PIN, HIGH);     //end programming
  delay(100);
  while (HC12.available()) {       // The HC-12 has response data (the AT Command response)
      Serial.write(HC12.read());   // Send the data to Serial monitor
    }
  wireless_transceiver(0, -2, 0);  //set the time for sending radio commands
  delay(100);
}

void loop() {
 
  //check if you need to send wireless information
  wireless_transceiver(0, -1, 0);

  //check if you are sent wireless information
  receive_wireless();

  //check if you need to stop blinking the LED
  telegraph_tags(0);
}


/*******
wireless_transceiver f(x)
This function sends wireless messages and receives wireless messages
Messages are sent / received through the Serial port and the HC-12 transceiver
the send part of the function checks if it is time to send a packet, then sends it.
If it receives a packet to send, it stores it in a buffer
If it is time to send packets, it also checks for any received, and sends them to be processed
message -1 is used to loop this function and check if it's time to send
message -2 is used to set the time that is looped from.
 */

 void wireless_transceiver( int device_id, int message, int value){
  static unsigned long next_event_time;              //time to send next piece of data
  static byte current_status;                        //current step we are on
  static byte array_to_send[WIRELESS_SEND_BUFFER];   //aray to send
  static byte array_index;                           //current array index
  unsigned long current_time = millis();

  //If you get a new message to send, add that to the buffer
  if(message != -1){
    //if message is -1, we are checking if we can send
    if(message == -2){
      //if the message is to set the time for transmissions.
      next_event_time = current_time + (DEVICE_ID*(RADIO_TIME_PER_DEVICE + RADIO_SILENCE_TIME));
    }
    else if(array_index <= WIRELESS_SEND_BUFFER){
      //if we aren't setting the time, and there is room in the array
      array_to_send[array_index] = device_id;       //set array to send
      array_to_send[array_index + 1] = message;
      array_to_send[array_index + 2] = value;
      array_index = array_index + WIRELESS_MESSAGE_LENGTH;
    }
  }

  //check to see if it is time to send
  if(current_time >= next_event_time){
    if(current_time <= (next_event_time + RADIO_TIME_PER_DEVICE)){
      //if we are within the time to send and have data to send, send our data
      if(array_index > 0){
        HC12.write(array_to_send, array_index);  //send all bytes
         //reset array_index to indicate all data is sent
         array_index = 0;
      }
      
    }
    //update time to send next
    next_event_time = next_event_time + RADIO_PERIOD;
  }
 }

/******
receive_wireless f(x)
Checks to see if there was a wireless message sent to the device over Serial.
If it's an AT command, it is ignored.
Otherwise, checks to see if it's 3 bytes, and is for this device
inputs - nothing
 */
 void receive_wireless (void) {
  
  static byte message_array[3];
  static byte message_array_index;
  
  /*
  byte message_array[3];       //note: if you do this, you occasionally miss stuff
  byte message_array_index = 0;
  */
  while (HC12.available()) {        // while data is available
    message_array[message_array_index] = HC12.read();
    Serial.print("Received: ");
    Serial.println(message_array[message_array_index]);
    message_array_index = message_array_index + 1;
    if(message_array_index == 3){
      Serial.println("Received 3 bytes");
      //check to see if you should use what was transmitted
      if(message_array[1] >= 5 && message_array[1] <= 29){
        //if the messages that were sent were global messages, react to them
        if(message_array[1] == 5){
          //received a hello!
          Serial.print("Received hello from: ");
          Serial.println(message_array[2]);
        }
        if(message_array[1] == 6){
          //if the message is to set the time for transmissions, set that in transmissions f(x)
          //wireless_transceiver( 0, -2, 0)
          Serial.print("Received synchronize time!!!!");
          Serial.println(message_array[2]);
          wireless_transceiver(0, -2, 0);  //set the time for sending radio commands
        }
        if(message_array[1] == 10){
          //clear any old bits, which happens at the end of the cycle.
        }
      }
      else if (message_array[0] == DEVICE_ID){
        //if the message is sent to you, parse it and do what it instructs
        switch (message_array[1]){
          case 1:
            //I tagged someone, and should indicate it
            Serial.println("Tagged Someone!!!!");
            telegraph_tags(1);
            break;
          case 2:
            //I tagged someone who had armor, and should indicate it
            Serial.println("Tagged Someone's armor!!");
            telegraph_tags(2);
            break;
          case 3:
            //I tagged someone, and they are out. Do something because of it
            break;
          case 4:
            //I tagged the juggernaut, and they are out because of it. Do something because of it
            Serial.println("I'm the juggernaut now!");
            break;
          default:
            break;
        }
      }
      //reset the array after reading the message
      message_array_index = 0;
    }
  }
 }
/**********
telegraph_tags f(x)
this function shows whether or not the tags you have sent hit someone
this function takes in what light to turn on (1 or 2), along with whether or not
you are checking the lights (0). It then outputs signals to the LED's, whether it was
a good hit (green) or hit armor (yellow)
inputs - integer - 0, 1, or 2
outputs - void - turns on lights on device
#define HIT_ENEMY_DEVICE_PIN A3
#define HIT_ENEMY_ARMOR_PIN A4
 */
void telegraph_tags(int check_variable){
  static unsigned long hit_on_time;
  static unsigned long armor_hit_on_time;
  static byte hit_on;
  static byte armor_on;
  unsigned long current_time = millis();

  switch(check_variable){
    case 0:
      //I'm checkint to see if I need to do anything
      if(hit_on > 0){
        //if I've  hit, check to see if I need to turn off
        if(current_time > hit_on_time){
          //turn off
          digitalWrite(HIT_ENEMY_DEVICE_PIN, LOW);
          hit_on = 0;
        }
      }
      if(armor_on > 0){
        //if I've hit armor, check to see if I need to turn off
        if(current_time > armor_hit_on_time){
          //turn off
          digitalWrite(HIT_ENEMY_ARMOR_PIN, LOW);
          armor_on = 0;
        }
      }
      break;
    case 1:
      //I should turn on green light
      digitalWrite(HIT_ENEMY_DEVICE_PIN, HIGH);
      hit_on_time = current_time + TELEGRAPH_ENEMY_HIT;
      hit_on = 1;
      break;
    case 2:
      //I should turn on yellow light
      digitalWrite(HIT_ENEMY_ARMOR_PIN, HIGH);
      armor_hit_on_time = current_time + TELEGRAPH_ENEMY_HIT;
      armor_on = 1;
      break;
    default:
      //not sure what to do
      break;
  }
}

Discussions