Sending Serial Data

Hi All,

I have an arcade project designed by someone else that uses an Arduino mega as a joystick controller but the sketch uploaded to it also controls some lamp outputs using a ULN2003A.

As the Arduino is flashed to use Unojoy the main usb port is used for tx only to send joystick commands back to the pc.

The lamp data is sent via a PL2303HX serial adaptor to ports 14tx & 15rx on the mega.

The lamp data from the game emulator is captured by a pre-compiled program (with no access to the source) and sent to the Arduino via the serial cable.

I have captured the serial data -

[14/12/2019 15:42:51] Written data (COM5)
82 20                                             ‚               
[14/12/2019 15:42:51] Written data (COM5)
82 40                                             ‚@              
[14/12/2019 15:42:51] Written data (COM5)
82 80                                             ‚€              
[14/12/2019 15:42:52] Written data (COM5)
82 04                                             ‚.              
[14/12/2019 15:42:52] Written data (COM5)
82 08                                             ‚.              
[14/12/2019 15:42:52] Written data (COM5)
82 10                                             ‚.

04 - start
08 - vr1
10 - vr2
20 - vr3
40 - vr4
80 - leader

Each single command triggers a lamp.

Here is the sketch -

/*
BigPanik Model2Pac v1.2 Final
Interface Model 2 hardware (wheel, pedals, buttons, gear, FFB and lamps) to USB
*/
#include "UnoJoy.h"
int incomingByte = 0;   // for incoming serial data
char FFByte = 0;
byte LampValue = 0;
byte Datatrame = 0;

void setup(){
  setupPins();
  setupUnoJoy();
  Serial3.begin(38400);
}

void loop(){
  // Always be getting fresh Input data
  dataForController_t controllerData = getControllerData();
  setControllerData(controllerData);
  
}

void setupPins(void){
  // UnoJoy
  // Set all the digital pins as inputs
  // with the pull-up enabled, except for the 
  // two serial line pins
  for (int i = 2; i <= 12; i++){
    pinMode(i, INPUT);
    digitalWrite(i, HIGH);
  }
  pinMode(A4, INPUT);
  digitalWrite(A4, HIGH);
  pinMode(A5, INPUT);
  digitalWrite(A5, HIGH);
  
  //FFB
  // SEGA DRIVE board connected on port A (22-29)
  DDRA = 0xFF;
  PORTA = 0;
  // Lamps
  // ULN2003 connected between A8 and A14
  // A: VR Green
  pinMode(A14, OUTPUT);
  digitalWrite(A14, LOW);
  // B: VR Green
  pinMode(A13, OUTPUT);
  digitalWrite(A13, LOW);
  // C: VR Green
  pinMode(A12, OUTPUT);
  digitalWrite(A12, LOW);
  // D: VR Green
  pinMode(A11, OUTPUT);
  digitalWrite(A11, LOW);
  // E: VR Green
  pinMode(A10, OUTPUT);
  digitalWrite(A10, LOW);
  // E: Not Connected
  pinMode(A9, OUTPUT);
  digitalWrite(A9, LOW);
  // G: Race Leader
  pinMode(A8, OUTPUT);
  digitalWrite(A8, LOW);

}

dataForController_t getControllerData(void){
  
  // Set up a place for our controller data
  //  Use the getBlankDataForController() function, since
  //  just declaring a fresh dataForController_t tends
  //  to get you one filled with junk from other, random
  //  values that were in those memory locations before
  dataForController_t controllerData = getBlankDataForController();
  // Since our buttons are all held high and
  //  pulled low when pressed, we use the "!"
  //  operator to invert the readings from the pins
  
  //Start
  controllerData.startOn = !digitalRead(9); //Start = button 10
  
  //VR
  controllerData.squareOn = !digitalRead(8); //Red = button 1 
  controllerData.crossOn = !digitalRead(7); //Blue = button 2
  controllerData.circleOn  = !digitalRead(6); //Yellow = button 3
  controllerData.triangleOn = !digitalRead(5);  // Green = button 4

  //Gear
  // 4=1+2/!3+4 = 4
  // 3=2/4      = 2
  // 2=1/3      = 1
  switch(!digitalRead(4) + !digitalRead(3) * 2 + !digitalRead(2) * 4)
  {
  case 5: //gear 1
  controllerData.l2On = 1;
  controllerData.l3On = 0;
  controllerData.r1On = 0;
  controllerData.r2On = 0;
 controllerData.l1On  = 0; //Button 5   
    break;

  case 6: //gear 2
    controllerData.l2On = 0;
  controllerData.r1On = 0;
  controllerData.l3On = 1;
  controllerData.r2On = 0;
 controllerData.l1On  = 0; //Button 5
 break;
  
  case 1: //gear 3
  controllerData.l2On = 0;
  controllerData.r1On = 1;
  controllerData.l3On = 0;
  controllerData.r2On = 0;
 controllerData.l1On  = 0; //Button 5
 break;
    
  case 2: //gear 4
  controllerData.l2On = 0;
  controllerData.l3On = 0;
  controllerData.r1On = 0;
  controllerData.r2On = 1;
 controllerData.l1On  = 0; //Button 5
    break;
    
  default: //Neutral
  controllerData.l2On = 0;
  controllerData.l3On = 0;
  controllerData.r1On = 0;
  controllerData.r2On = 0;
   controllerData.l1On  = 0; //Button 5 OFF version speciale Sans Neutre
  
  }
    
  //Coin
  controllerData.selectOn = !digitalRead(12);
  
  //Test SW
  controllerData.r3On = !digitalRead(11);
  
  //Service SW
  controllerData.homeOn = !digitalRead(10);
  
  // Set the analog sticks
  //  Since analogRead(pin) returns a 10 bit value,
  //  we need to perform a bit shift operation to
  //  lose the 2 least significant bits and get an
  //  8 bit number that we can use  
  controllerData.leftStickX = analogRead(A0) >> 2;
  controllerData.leftStickY = analogRead(A1) >> 2;
  controllerData.rightStickX = 0;
  controllerData.rightStickY = analogRead(A2) >> 2;
  // And return the data!
  return controllerData;
}

  // Always be setting fresh Output (FFB + Lamps) data
 
/*
  SerialEvent occurs whenever a new data comes in the
 hardware serial RX.  This routine is run between each
 time loop() runs, so using delay inside loop can delay
 response.  Multiple bytes of data may be available.
 */

void serialEvent() {
   while (Serial3.available() > 0) {
     if (Datatrame == 0) //FFB
     {
       FFByte = (char) Serial3.read();
       PORTA = FFByte;
       Datatrame++;
     }
     else //Lamps
     {
        LampValue = Serial3.read();
//        if (LampValue & 128 )digitalWrite(A8,HIGH); else digitalWrite(A8,LOW); // Leader
        digitalWrite(A8, !!(LampValue & (1u << 7))); // Leader
//        if (LampValue & 4)digitalWrite(A10,HIGH); else digitalWrite(A10,LOW); // Start
        digitalWrite(A10, !!(LampValue & (1u << 2))); // Start
//        if (LampValue & 8)digitalWrite(A11,HIGH); else digitalWrite(A11,LOW); // Rouge
        digitalWrite(A11, !!(LampValue & (1u << 3))); //Red
//        if (LampValue & 16)digitalWrite(A12,HIGH); else digitalWrite(A12,LOW); // bleu
        digitalWrite(A12, !!(LampValue & (1u << 4))); // Blue
//        if (LampValue & 32)digitalWrite(A13,HIGH); else digitalWrite(A13,LOW); // Jaune
        digitalWrite(A13, !!(LampValue & (1u << 5))); // Yellow
//        if (LampValue & 64)digitalWrite(A14,HIGH); else digitalWrite(A14,LOW); // vert
        digitalWrite(A14, !!(LampValue & (1u << 6))); // Green
        Datatrame=0;
      }
   }
}

I can see the section of code that triggers the lamps but due to my lack of knowledge I cant see how the Arduino knows to trigger the lamps as I see no reference to the serial commands sent. If anyone could explain that would be great.

I'm also after some recommendations on how to send the same serial commands to the Arduino.

Thanks

Gareth

It is in the serialEvent() function. Whenever data arrives on the serial port, it triggers an interrupt that calls this routine. Inside the routine, the lampdata is read from the serial port and acted upon.

Whenever data arrives on the serial port, it triggers an interrupt that calls this routine

That is not how serialEvent works. It does not trigger an interrupt, rather if there is Serial data available and a seriaEvent() function is in the program then it is called as the last action in loop() before loop() repeats

The lamp data is sent via a PL2303HX serial adaptor to ports 14tx & 15rx on the mega.

These pins are Serial3 and that channel is read in the SerialEvent() function

void serialEvent() {
   while (Serial3.available() > 0) {
     if (Datatrame == 0) //FFB
     {
       FFByte = (char) Serial3.read();
       PORTA = FFByte;
       Datatrame++;
     }
     else //Lamps
     {
        LampValue = Serial3.read();
//        if (LampValue & 128 )digitalWrite(A8,HIGH); else digitalWrite(A8,LOW); // Leader
        digitalWrite(A8, !!(LampValue & (1u << 7))); // Leader
//        if (LampValue & 4)digitalWrite(A10,HIGH); else digitalWrite(A10,LOW); // Start
        digitalWrite(A10, !!(LampValue & (1u << 2))); // Start
//        if (LampValue & 8)digitalWrite(A11,HIGH); else digitalWrite(A11,LOW); // Rouge
        digitalWrite(A11, !!(LampValue & (1u << 3))); //Red
//        if (LampValue & 16)digitalWrite(A12,HIGH); else digitalWrite(A12,LOW); // bleu
        digitalWrite(A12, !!(LampValue & (1u << 4))); // Blue
//        if (LampValue & 32)digitalWrite(A13,HIGH); else digitalWrite(A13,LOW); // Jaune
        digitalWrite(A13, !!(LampValue & (1u << 5))); // Yellow
//        if (LampValue & 64)digitalWrite(A14,HIGH); else digitalWrite(A14,LOW); // vert
        digitalWrite(A14, !!(LampValue & (1u << 6))); // Green
        Datatrame=0;
      }
   }
}

The second byte received is

LampValue = Serial3.read();

and the value read is tested against 4,8,16,32,64,128 to determine what to turn on. These are the same as the hex values you saw 04,08,10,20,40,80.

These pins are Serial3 and that channel is read in the SerialEvent() function

Should that be the serialEvent3() function ?

UKHeliBob:
That is not how serialEvent works. It does not trigger an interrupt, rather if there is Serial data available and a seriaEvent() function is in the program then it is called as the last action in loop() before loop() repeats

It's not the last action in loop() either :wink: From memory, it's called after loop() is called from main().

Should that be the serialEvent3() function ?

I would have thought so, but the IDE is smarter than we are. I see this in HardwareSerial.cpp in the core

// SerialEvent functions are weak, so when the user doesn't define them,
// the linker just sets their address to 0 (which is checked below).
// The Serialx_available is just a wrapper around Serialx.available(),
// but we can refer to it weakly so we don't pull in the entire
// HardwareSerial instance if the user doesn't also refer to it.
#if defined(HAVE_HWSERIAL0)
  void serialEvent() __attribute__((weak));
  bool Serial0_available() __attribute__((weak));
#endif

#if defined(HAVE_HWSERIAL1)
  void serialEvent1() __attribute__((weak));
  bool Serial1_available() __attribute__((weak));
#endif

#if defined(HAVE_HWSERIAL2)
  void serialEvent2() __attribute__((weak));
  bool Serial2_available() __attribute__((weak));
#endif

#if defined(HAVE_HWSERIAL3)
  void serialEvent3() __attribute__((weak));
  bool Serial3_available() __attribute__((weak));
#endif

void serialEventRun(void)
{
#if defined(HAVE_HWSERIAL0)
  if (Serial0_available && serialEvent && Serial0_available()) serialEvent();
#endif
#if defined(HAVE_HWSERIAL1)
  if (Serial1_available && serialEvent1 && Serial1_available()) serialEvent1();
#endif
#if defined(HAVE_HWSERIAL2)
  if (Serial2_available && serialEvent2 && Serial2_available()) serialEvent2();
#endif
#if defined(HAVE_HWSERIAL3)
  if (Serial3_available && serialEvent3 && Serial3_available()) serialEvent3();
#endif
}

It's not the last action in loop() either

You are correct

At the end of main() there is

for (;;) {
 loop();
 if (serialEventRun) serialEventRun();
 }

Thank you all replying.

I'm starting to understand this a little more.... I think haha.

So if i use the leader lamp as an example, the LampValue that has been sent would be 80

"if (LampValue & 128 )digitalWrite(A8,HIGH); else digitalWrite(A8,LOW); // Leader"

So is 80 & 128 being added together?

I have termite serial console open and have been trying to send values to trigger the lamps with unexpected results so far.

So is 80 & 128 being added together?

No, & is the bitwise AND operator. & - Arduino Reference

It's also confusing when you use the decimal and hex values of a number together. 0x80 and 128 are the same number

if (LampValue & 128 )

if LampValue == 128 the statement will be true, and Leader led (A8) should be turned on.

I have termite serial console open and have been trying to send values to trigger the lamps with unexpected results so far.

What are you seeing? From your previous post and the code, it looks like there needs to be a two byte packet. I'm not clear why the the first byte = 0x82.

Ah sorry,

So this project also controls a sega force feedback controller for a steering wheel so the value 82 is the current force feedback back value that we can ignore.

So in termite, if I simply send the value 8280 or even just 80 I'm getting multiple lamps switched on.

I think this is because I'm not understanding the hex and decimal values.

Please don't think I'm just looking for someone else to fix this for me I really am trying to learn what's happening here.

termite.png

Please don't think I'm just looking for someone else to fix this for me I really am trying to learn what's happening here.

No problems. I'm sure we can get this working but I need to understand a few things about the environment.

The lamp data is sent via a PL2303HX serial adaptor to ports 14tx & 15rx on the mega.
The lamp data from the game emulator is captured by a pre-compiled program (with no access to the source) and sent to the Arduino via the serial cable.
So in termite..

I'm not familiar with termite, but when I googled it I saw it was an RS232 terminal app. Is that correct?

The Serial3 port on the Mega is 0-5v TTL.

The PL2303HX data sheet indicates that it is an RS232 to USB converter. I would think that you would want to use an RS232 to TTL converter to get voltage levels correct.

Why are you using termite instead of a USB serial output terminal program?

If indeed, termite is the correct program, and the converter is correct for both ends, then I think you would need the Hex view plug in to send the two byte hex values of the control bytes. You can send them one at a time. The serial receive code will sort them out with the Dataframe variable.

The Arduino program is expecting a two byte transmission. The first is some kind of configuration for PORTA which I think are pins 22-29. Do you know what they need to be? 0x82 may not be correct.

The second byte is the lamp control.

UKHeliBob:
That is not how serialEvent works. It does not trigger an interrupt, rather if there is Serial data available and a seriaEvent() function is in the program then it is called as the last action in loop() before loop() repeats

1. As a user, I see the following three functions in my IDE; where, data coming from the InputBox of the Serial Monitor are well received by the serialEvent() function and sent back to OutputBox of Serial Monitor.

void setup()
{
  Serial.begin(9600);
}

void loop()
{
 
}

void serialEvent()
{
  char x = Serial.read();
  Serial.print(x);
}

2. According to the above quote due to @UKHeliBob, the sketch of Step-1 is equivalent to the following sketch (tested).

void setup()
{
  Serial.begin(9600);
}

void loop()
{
 if(Serial.available()>0)
 {
  Serial.print((char)Serial.read());
 }
}

3. When a data bytes arrives at the hardware UART port, the MCU is interrupted; it goes to the ISR(), reads the data byte and saves into an unseen FIFO Buffer. The serialEvent() function checks if there is any data byte in the FIFO buffer, brings it from the buffer and saves in a variable.

cattledog,

I think you are right about termite not being correct for the job. I'm at work currently so I can't test but I took a look at this video of someone sending hex values using realterm - How to Send Hex Text on Serial Port - YouTube

The PL2303HX is sold as a TTL cable and i can confirm it's working as when i use the original compiled program to send the commands to the Arduino the lamps work as expected. But the compiled program doesn't support everything i need so that's how this got started. https://www.ebay.co.uk/itm/PL2303HX-USB-to-TTL-RS232-COM-UART-Module-Serial-Cable-Adapter-for-Arduino/263864494229?epid=18022329686&hash=item3d6f8ca895:g:8qcAAOSwZSpdKFls

Here is the wiring diagram -

PORTA is comms to the drive board. So the first value 0x82 will change depending on what the game requests.

The second value changes depending on what lamp the game requests.

So I can see the current value to turn on the leader lamp is shown as 80

if I'm understanding this correctly, via serial I would need to send drive board data first followed by lamp data 0x82 0x80

think you are right about termite not being correct for the job. I'm at work currently so I can't test but I took a look at this video of someone sending hex values using realterm

RealTerm is a good choice. I have used it in the past. CoolTerm is another option which I found to be a bit simpler. Basically, the terminal program wants to be able to output the hex commands over usb.

The adaptor looks correct for usb to 5v ttl.

if I'm understanding this correctly, via serial I would need to send drive board data first followed by lamp data 0x82 0x80

That is my understanding. I expect that you will be successful with the correct terminal program when you get back to the controller.

Amazing, It's working.

Thank you to everyone for your help :slight_smile:

Well done sorting this out. +1
Happy gaming and lamp lighting.