Game Controller

1. Introduction

A game controller is an important input device for manually controlling a robot. Most game controllers generally have two joysticks, two triggers, two bumpers, four pad buttons, four user buttons, and three special buttons. Some game controllers also have a vibration motor, LEDs, and a speaker. Each joystick has two potentiometers for the X and Y axes and a push button. Each trigger has a potentiometer for its axis.

The Xbox 360 and PlayStation controllers are two of the most popular game controllers. They can be connected to a computer using a USB cable or Bluetooth, and can also be connected to a microcontroller using USB or Bluetooth. In this tutorial, we are going to use a PS4 controller and an ESP32 WROOM32 module. The ESP32 supports both classic Bluetooth and BLE, while the PS4 controller only supports classic Bluetooth.

PS4 Controller

PS4 Controller (DualShock)

Xbox 360 Controller

Xbox 360 Controller

PS4 Controller Label

PS4 Controller Label

Xbox 360 Controller Label

Xbox 360 Controller Label

2. Understanding the flow of Data and Signals from Game Controller to Actuator

Flow of Data and Signals from Game Controller to Actuator

Flow of Data and Signals from Game Controller to Actuator

The ESP32 acts as a wireless receiver, parsing HID data from the PS4 controller and converting it into a common JoyData format. This data is then sent to the STM32 via UART, where the STM32 uses the JoyData to control the actuators. The ESP32 can be replaced with a Raspberry Pi Pico W or any other. The standardized JoyData format makes it easy to swap out the receiver as needed.

3. Joystick Data Format from Receiving Controller

Creating a header file for the JoyData structure is a good practice, as it allows you to define the structure in a single place and include it in multiple source files. This makes it easier to include, maintain and update the structure if needed.

joy_msg.hpp
 1#ifndef _JOY_MSG_HPP
 2#define _JOY_MSG_HPP
 3
 4#include <math.h>
 5#include <stdint.h>
 6
 7#define BUTTONS(button) (1 << button)
 8
 9namespace myPS4
10{
11  enum Buttons
12  {
13    CROSS,
14    CIRCLE,
15    SQUARE,
16    TRIANGLE,
17    SHARE,
18    PS_BUTTON,
19    OPTION,
20    L3,
21    R3,
22    L1,
23    R1,
24    UP,
25    DOWN,
26    LEFT,
27    RIGHT,
28    TOUCH
29  };
30}
31
32namespace myXBox
33{
34  enum Buttons
35  {
36    A,
37    B,
38    X,
39    Y,
40    BACK,
41    XBOX_BUTTON,
42    START,
43    LSTICK,
44    RSTICK,
45    LB,
46    RB,
47    UP,
48    DOWN,
49    LEFT,
50    RIGHT
51  };
52}
53
54#pragma pack(push, 1)
55struct JoyData
56{
57  int8_t lx;
58  int8_t ly;
59  int8_t rx;
60  int8_t ry;
61  uint8_t lt;
62  uint8_t rt;
63  uint16_t buttons;
64};
65#pragma pack(pop)
66
67#endif // _JOY_MSG_HPP

The total size of JoyData is 8-bytes and contains the following fields:

  • lx: Left joystick X-axis value

  • ly: Left joystick Y-axis value

  • rx: Right joystick X-axis value

  • ry: Right joystick Y-axis value

  • lt: Left trigger value

  • rt: Right trigger value

  • buttons: Bitmask representing the state of the buttons. Each button is represented by a bit in the bitmask, where 1 indicates that the button is pressed and 0 indicates that it is not pressed.

The buttons are defined in the myPS4 and myXBox namespaces, and each button is assigned a unique value. The buttons are represented as bitmasks, which means that multiple buttons can be pressed at the same time. The JoyData structure is packed to ensure that the data is aligned correctly in memory. The #pragma pack(push, 1) directive tells the compiler to align the structure on a 1-byte boundary, which means that there will be no padding between the fields of the structure. The #pragma pack(pop) directive restores the previous packing alignment.

4. Setting ESP32 as Receiver

ESP32 WROOM32

DOIT ESP32 DEVKIT V1

There are some Arduino libraries available for interfacing PS4 controller with ESP32. Some examples for ESP32 IDF are also avialable which uses Bluetooth as well as USB. We are going to use Arduino library for this tutorial.

4.1. Installing ESP32 Board by Espressif in Arduino IDE

  • Open Arduino IDE.

  • Search for ESP32 in the board manager.

  • Install the board by Espressif.

4.2. Finding MAC Address of ESP32 and Setting up PS4 Controller

The MAC address is used to pair the PS4 controller with the ESP32 board. Follow the steps below to find the MAC address of the ESP32 board and set up the PS4 controller:

  • Open Arduino IDE and create a new sketch.

  • Copy the following code into the sketch:

    esp_mac_loockup.ino
     1#include "esp_bt_main.h"
     2#include "esp_bt_device.h"
     3#include "BluetoothSerial.h"
     4
     5BluetoothSerial SerialBT;
     6
     7void printDeviceAddress() {
     8  const uint8_t* point = esp_bt_dev_get_address();
     9  for (int i = 0; i < 6; i++) {
    10    char str[3];
    11    sprintf(str, "%02X", (int)point[i]);
    12    Serial.print(str);
    13    if (i < 5){
    14      Serial.print(":");
    15    }
    16  }
    17}
    18
    19void setup() {
    20  Serial.begin(115200);
    21  SerialBT.begin("ESP32 Bluetooth");
    22}
    23
    24void loop() {
    25  printDeviceAddress();
    26  delay(1000);
    27}
    
  • Save the sketch as esp_mac_loockup.ino.

  • Select board DOIT ESP32 DEVKIT V1 from Tools > Board > esp32.

  • Upload the code to the ESP32 board and open the Serial Monitor. The MAC address will be displayed in the Serial Monitor.

  • Open SixaxisPairTool on Windows. See installation if you do not have. Plug the PS4 controller to PC using USB Cable. Driver for the controller will be installed, if it is first time.

  • Connect the PS4 controller to the PC using a USB cable. The SixaxisPairTool will display the MAC address of the PS4 controller. Write the Bluetooth Mac Address of the ESP32 into Master MAC Adrress Field and click update.

4.3. Using PS4Controller Library by Juan Pablo Marquez

  • Install the library from Arduino Library Manager or download from here.

  • Create a new sketch and paste the following contents:

    esp_rx_ps4_library.ino
      1#define DEBUG // Disable it by commenting in actual implementation
      2
      3#include <PS4Controller.h>
      4#include "crc8.hpp"
      5#include "joy_msg.hpp"
      6using namespace myPS4;
      7
      8/** @brief Pins assignments of ESP32 */
      9#define IN_BUILT_LED_BLUE 2
     10#define RXD2 16
     11#define TXD2 17
     12
     13/** @brief Start byte of packet */
     14#define START_BYTE 0xA5
     15
     16CRC8 crc(7);
     17
     18JoyData jdata;
     19bool is_flash_set = false;
     20bool is_braked;
     21
     22uint32_t last_transmit = 0;
     23
     24/** @brief Initialize controller parameters */
     25void setup() {
     26
     27  Serial.begin(115200, SERIAL_8N1);
     28  Serial2.begin(115200, SERIAL_8N1, RXD2, TXD2);
     29
     30  pinMode(IN_BUILT_LED_BLUE, OUTPUT);
     31  digitalWrite(IN_BUILT_LED_BLUE, LOW);
     32
     33  PS4.attachOnConnect(onConnect);
     34  PS4.attachOnDisconnect(onDisConnect);
     35
     36  PS4.begin();
     37  Serial.print("ESP32 MAC: ");
     38  Serial.println(PS4.getAddress());
     39}
     40
     41/** @brief Operation */
     42void loop() {
     43  if (!PS4.isConnected())
     44    return;
     45
     46  uint32_t now = millis();
     47  if (now - last_transmit < 10)
     48    return;
     49
     50  uint8_t packet[10];
     51  jdata.lx = PS4.LStickX();
     52  jdata.ly = -PS4.LStickY();
     53  jdata.rx = PS4.RStickX();
     54  jdata.ry = -PS4.RStickY();
     55  jdata.lt = PS4.L2Value();
     56  jdata.rt = PS4.R2Value();
     57
     58  jdata.buttons = 0;
     59
     60  if (PS4.Cross()) jdata.buttons |= BUTTONS(CROSS);
     61  if (PS4.Circle()) jdata.buttons |= BUTTONS(CIRCLE);
     62  if (PS4.Square()) jdata.buttons |= BUTTONS(SQUARE);
     63  if (PS4.Triangle()) jdata.buttons |= BUTTONS(TRIANGLE);
     64
     65  if (PS4.Share()) jdata.buttons |= BUTTONS(SHARE);
     66  if (PS4.PSButton()) jdata.buttons |= BUTTONS(PS_BUTTON);
     67  if (PS4.Options()) jdata.buttons |= BUTTONS(OPTION);
     68
     69  if (PS4.L3()) jdata.buttons |= BUTTONS(L3);
     70  if (PS4.R3()) jdata.buttons |= BUTTONS(R3);
     71  if (PS4.L1()) jdata.buttons |= BUTTONS(L1);
     72  if (PS4.R1()) jdata.buttons |= BUTTONS(R1);
     73
     74  if (PS4.Up()) jdata.buttons |= BUTTONS(UP);
     75  if (PS4.UpLeft()) jdata.buttons |= (BUTTONS(UP) | BUTTONS(LEFT));
     76  if (PS4.Left()) jdata.buttons |= BUTTONS(LEFT);
     77  if (PS4.DownLeft()) jdata.buttons |= (BUTTONS(LEFT) | BUTTONS(DOWN));
     78  if (PS4.Down()) jdata.buttons |= BUTTONS(DOWN);
     79  if (PS4.DownRight()) jdata.buttons |= (BUTTONS(DOWN) | BUTTONS(RIGHT));
     80  if (PS4.Right()) jdata.buttons |= BUTTONS(RIGHT);
     81  if (PS4.UpRight()) jdata.buttons |= (BUTTONS(UP) | BUTTONS(RIGHT));
     82  
     83  if (PS4.Touchpad()) jdata.buttons |= BUTTONS(TOUCH);
     84
     85  packet[0] = START_BYTE;
     86  memcpy((uint8_t*)packet + 1, (uint8_t*)&jdata, sizeof(jdata));
     87  packet[9] = crc.get_hash(packet + 1, sizeof(jdata));
     88
     89  // Finally send packet to STM32 through UART2
     90  Serial2.write(packet, sizeof(packet));
     91
     92  if (PS4.PSButton()) {
     93    if (PS4.L1() && PS4.R1()) {
     94      if (is_braked) {
     95        PS4.setLed(0, 255, 0);
     96        PS4.sendToController();
     97        // Serial.println("Brake removed");
     98        is_braked = false;
     99      }
    100    } else {
    101      if (!is_braked) {
    102        PS4.setLed(255, 0, 255);
    103        PS4.sendToController();
    104        // Serial.println("Braked");
    105        is_braked = true;
    106      }
    107    }
    108  }
    109
    110#ifdef DEBUG
    111  Serial.printf("%lu: %hd %hd %hd %hd %hu %hu %04X\n",
    112                now - last_transmit, jdata.lx, jdata.ly, jdata.rx, jdata.ry, jdata.lt, jdata.rt, jdata.buttons);
    113#endif
    114
    115  last_transmit = now;
    116}
    117
    118/** @brief Callback on connect */
    119void onConnect() {
    120  digitalWrite(IN_BUILT_LED_BLUE, HIGH);
    121  Serial.printf("Battery: %d\n", PS4.Battery());
    122  PS4.setLed(0, 255, 0);
    123  PS4.sendToController();
    124  Serial.println("PS4 Connected.");
    125}
    126
    127/** @brief Callback on disconnect */
    128void onDisConnect() {
    129  digitalWrite(IN_BUILT_LED_BLUE, LOW);
    130  Serial.println("!!PS4 Disconnected!!");
    131}
    
  • Save the sketch as esp_rx_ps4_library.ino.

  • Create a new file joy_msg.hpp in the same directory as the sketch. Copy the contents of the JoyData structure from above into this.

  • Create two files crc8.hpp and crc8.cpp in the same directory as the sketch. The contents of these files are as follows:

crc8.hpp
 1/**
 2 *******************************************************************************
 3 * @file    crc8.hpp
 4 * @brief   Header file 8-bit crc calculation
 5 * @author  Robotics Team, IOE Pulchowk Campus
 6 *******************************************************************************
 7 */
 8
 9#ifndef CRC8_HPP_
10#define CRC8_HPP_
11
12#include <stdint.h>
13
14#define WIDTH (8)                       /**< CRC width */
15#define CRC_HASH_TABLE_SIZE (256)       /**< Size of CRC hash table */
16#define TOP_BIT (1 << 7)                /**< Top bit position */
17
18/**
19 * @brief Class for CRC-8 checksum calculation.
20 */
21class CRC8
22{
23public:
24    /**
25     * @brief Constructor for CRC8 class.
26     * @param polynomial Polynomial value for CRC calculation.
27     */
28    CRC8(uint8_t polynomial);
29
30    /**
31     * @brief Destructor for CRC8 class.
32     */
33    ~CRC8() {}
34
35    /**
36     * @brief Calculate CRC-8 checksum for the given data.
37     * @param buf Pointer to the data buffer.
38     * @param len Length of the data buffer.
39     * @return CRC-8 checksum value.
40     */
41    uint8_t get_hash(uint8_t *buf, uint16_t len);
42
43private:
44    uint8_t table_[CRC_HASH_TABLE_SIZE]; /**< CRC hash table. */
45};
46
47#endif // CRC_HPP_
crc8.cpp
 1/**
 2********************************************************************************
 3 * @file crc8.cpp
 4 * @brief Implementation file for the crc8 class
 5 * @author Robotics Team, IOE Pulchowk Campus
 6 * @date 2023
 7 *******************************************************************************
 8 */
 9
10#include "crc8.hpp"
11
12/**
13 * @brief Constructor for CRC8 class.
14 * 
15 * This constructor initializes the CRC8 object with the specified polynomial.
16 * It pre-computes the CRC hash table used for CRC calculation.
17 * 
18 * @param poly The polynomial value for CRC calculation.
19 */
20CRC8::CRC8(uint8_t poly)
21{
22    uint8_t remainder;
23    int dividend = 0;
24
25    for (; dividend < 256; ++dividend)
26    {
27        remainder = dividend << (WIDTH - 8);
28
29        for (uint8_t b = 8; b > 0; --b)
30        {
31            if (remainder & TOP_BIT)
32            {
33                remainder = (remainder << 1) ^ poly;
34            }
35            else
36            {
37                remainder = (remainder << 1);
38            }
39        }
40        table_[dividend] = remainder;
41    }
42}
43
44/**
45 * @brief Calculate CRC-8 checksum for the given data.
46 * 
47 * This method calculates the CRC-8 checksum for the provided data buffer.
48 * 
49 * @param buf Pointer to the data buffer.
50 * @param len Length of the data buffer.
51 * @return CRC-8 checksum value.
52 */
53uint8_t CRC8::get_hash(uint8_t *buf, uint16_t len)
54{
55    uint8_t data;
56    uint8_t remainder = 0;
57
58    for (uint16_t index = 0; index < len; ++index)
59    {
60        data = buf[index] ^ (remainder >> (WIDTH - 8));
61        remainder = table_[data] ^ (remainder << 8);
62    }
63
64    return remainder;
65}
  • Select board DOIT ESP32 DEVKIT V1 from Tools > Board > esp32.

  • Select port and upload the sketch to the ESP32 board.

  • Open the Serial Monitor. Press the PS button on the PS4 controller to turn it on. You should see the joystick data changing according to the input from the PS4 controller.

4.4. Using ESP32_Bluepad32

  • In Arduino IDE, go to File > Preferences and add the following URL to the Additional Board Manager URLs field:

    https://raw.githubusercontent.com/ricardoquesada/esp32-arduino-lib-builder/master/bluepad32_files/package_esp32_bluepad32_index.json
    
  • Open the Board Manager by going to Tools > Board > Board Manager. Search for esp32_bluepad32 by Ricardo Quesda and install the package.

  • Create a new sketch and paste the following contents:

    esp_rx_bluepad.ino
      1#include <Bluepad32.h>
      2#include "joy_msg.hpp"
      3#include "crc8.hpp"
      4
      5#define IN_BUILT_LED_BLUE 2
      6#define IN_BUILT_LED_BLUE 2
      7#define RXD2 16
      8#define TXD2 17
      9#define START_BYTE 0xA5
     10
     11bool connected = false;
     12
     13namespace BPB {
     14enum BluePadButtonsMask {
     15  Cross = 0x1,
     16  Circle = 0x2,
     17  Square = 0x04,
     18  Triangle = 0x08,
     19  Share = 0x02,
     20  Power = 0x01,
     21  Option = 0x04,
     22  L3 = 0x100,
     23  R3 = 0x200,
     24  L1 = 0x10,
     25  R1 = 0x20,
     26  Up = 0x01,
     27  Down = 0x02,
     28  Left = 0x08,
     29  Right = 0x04
     30  // Touch
     31};
     32}
     33
     34ControllerPtr myControllers[BP32_MAX_GAMEPADS];
     35CRC8 crc(7);
     36JoyData jdata;
     37bool isBraked = false;
     38uint32_t last_transmit = 0;
     39
     40// This callback gets called any time a new gamepad is connected.
     41// Up to 4 gamepads can be connected at the same time.
     42void onConnectedController(ControllerPtr ctl) {
     43  bool foundEmptySlot = false;
     44  // for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
     45    if (myControllers[0] == nullptr) {
     46      Serial.printf("CALLBACK: Controller is connected, index=%d\n", 0);
     47      // Additionally, you can get certain gamepad properties like:
     48      // Model, VID, PID, BTAddr, flags, etc.
     49      ControllerProperties properties = ctl->getProperties();
     50      Serial.printf("Controller model: %s, VID=0x%04x, PID=0x%04x\n", ctl->getModelName().c_str(), properties.vendor_id,
     51                    properties.product_id);
     52      myControllers[0] = ctl;
     53      foundEmptySlot = true;
     54      connected = true;
     55      digitalWrite(IN_BUILT_LED_BLUE, HIGH);
     56      ctl->setColorLED(0, 255, 0);
     57      // break;
     58    // }
     59  }
     60  if (!foundEmptySlot) {
     61    Serial.println("CALLBACK: Controller connected, but could not found empty slot");
     62  }
     63}
     64
     65void onDisconnectedController(ControllerPtr ctl) {
     66  bool foundController = false;
     67
     68  // for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
     69    if (myControllers[0] == ctl) {
     70      Serial.printf("CALLBACK: Controller disconnected from index=%d\n", 0);
     71      myControllers[0] = nullptr;
     72      foundController = true;
     73      connected = false;
     74      digitalWrite(IN_BUILT_LED_BLUE, LOW);
     75      // break;
     76    // }
     77  }
     78
     79  if (!foundController) {
     80    Serial.println("CALLBACK: Controller disconnected, but not found in myControllers");
     81  }
     82}
     83
     84void blinkColorLED(ControllerPtr ctl) {
     85  static bool isRed = false;
     86  static uint32_t last_blink = 0;
     87  uint32_t now = millis();
     88  if (now - last_blink > 1000) {
     89    if (isRed) {
     90      if (isBraked) {
     91        ctl->setColorLED(255, 0, 255);
     92      } else {
     93        ctl->setColorLED(0, 255, 0);
     94      }
     95    } else {
     96      ctl->setColorLED(50, 0, 0);
     97    }
     98
     99    isRed = !isRed;
    100    last_blink = now;
    101  }
    102}
    103
    104void dumpGamepad(ControllerPtr ctl) {
    105  Serial.printf(
    106    "idx=%d, dpad: 0x%02x, buttons: 0x%04x, axis L: %4d, %4d, axis R: %4d, %4d, brake: %4d, throttle: %4d, "
    107    "misc: 0x%02x, gyro x:%6d y:%6d z:%6d, accel x:%6d y:%6d z:%6d bat:%d\n",
    108    ctl->index(),        // Controller Index
    109    ctl->dpad(),         // D-pad
    110    ctl->buttons(),      // bitmask of pressed buttons
    111    ctl->axisX(),        // (-511 - 512) left X Axis
    112    ctl->axisY(),        // (-511 - 512) left Y axis
    113    ctl->axisRX(),       // (-511 - 512) right X axis
    114    ctl->axisRY(),       // (-511 - 512) right Y axis
    115    ctl->brake(),        // (0 - 1023): brake button
    116    ctl->throttle(),     // (0 - 1023): throttle (AKA gas) button
    117    ctl->miscButtons(),  // bitmask of pressed "misc" buttons
    118    ctl->gyroX(),        // Gyro X
    119    ctl->gyroY(),        // Gyro Y
    120    ctl->gyroZ(),        // Gyro Z
    121    ctl->accelX(),       // Accelerometer X
    122    ctl->accelY(),       // Accelerometer Y
    123    ctl->accelZ(),       // Accelerometer Z
    124    ctl->battery());
    125}
    126
    127void dumpJoyData(JoyData &jd) {
    128  Serial.printf("JoyData:: lx:%d ly:%d rx:%d ry:%d lt:%u rt:%u bt:%04x\n",
    129                jd.lx,
    130                jd.ly,
    131                jd.rx,
    132                jd.ry,
    133                jd.lt,
    134                jd.rt,
    135                jd.buttons);
    136}
    137
    138template<typename T>
    139T map(T in, T inMin, T inMid, T inMax, T outMin, T outMid, T outMax) {
    140  T out;
    141  if (in <= inMid) {
    142    // Map from [inMin, inMid] to [outMin, outMid]
    143    T inRange = inMid - inMin;
    144    T outRange = outMid - outMin;
    145    out = (in - inMin) * outRange / inRange + outMin;
    146  } else {
    147    // Map from [inMid, inMax] to [outMid, outMax]
    148    T inRange = inMax - inMid;
    149    T outRange = outMax - outMid;
    150    out = (in - inMid) * outRange / inRange + outMid;
    151  }
    152  return out;
    153}
    154
    155void processControllers() {
    156  auto myController = myControllers[0];
    157  if (myController && myController->isConnected() && myController->hasData()) {
    158    if (myController->isGamepad()) {
    159      //  dumpGamepad(myController);
    160
    161      jdata.lx = map(myController->axisX(), -508, -12, 512, -127, 0, 127);
    162      jdata.ly = map(myController->axisY(), -508, 8, 512, -127, 0, 127);
    163      jdata.rx = map(myController->axisRX(), -508, -32, 512, -127, 0, 127);
    164      jdata.ry = map(myController->axisRY(), -508, 18, 512, -127, 0, 127);
    165      jdata.lt = map(myController->brake(), 0, 1020, 0, 255);
    166      jdata.rt = map(myController->throttle(), 0, 1020, 0, 255);
    167
    168      jdata.buttons = 0;
    169      if (myController->buttons() & BPB::Cross) jdata.buttons |= BUTTONS(myPS4::CROSS);
    170      if (myController->buttons() & BPB::Circle) jdata.buttons |= BUTTONS(myPS4::CIRCLE);
    171      if (myController->buttons() & BPB::Square) jdata.buttons |= BUTTONS(myPS4::SQUARE);
    172      if (myController->buttons() & BPB::Triangle) jdata.buttons |= BUTTONS(myPS4::TRIANGLE);
    173      if (myController->miscButtons() & BPB::Share) jdata.buttons |= BUTTONS(myPS4::SHARE);
    174      if (myController->miscButtons() & BPB::Power) jdata.buttons |= BUTTONS(myPS4::PS_BUTTON);
    175      if (myController->miscButtons() & BPB::Option) jdata.buttons |= BUTTONS(myPS4::OPTION);
    176      if (myController->buttons() & BPB::L3) jdata.buttons |= BUTTONS(myPS4::L3);
    177      if (myController->buttons() & BPB::R3) jdata.buttons |= BUTTONS(myPS4::R3);
    178      if (myController->buttons() & BPB::L1) jdata.buttons |= BUTTONS(myPS4::L1);
    179      if (myController->buttons() & BPB::R1) jdata.buttons |= BUTTONS(myPS4::R1);
    180      if (myController->dpad() & BPB::Up) jdata.buttons |= BUTTONS(myPS4::UP);
    181      if (myController->dpad() & BPB::Down) jdata.buttons |= BUTTONS(myPS4::DOWN);
    182      if (myController->dpad() & BPB::Left) jdata.buttons |= BUTTONS(myPS4::LEFT);
    183      if (myController->dpad() & BPB::Right) jdata.buttons |= BUTTONS(myPS4::RIGHT);
    184
    185      if (myController->miscButtons() & BPB::Power) {
    186        if (myController->buttons() & BPB::L1 && myController->buttons() & BPB::R1) {
    187          myController->setColorLED(0, 255, 0);
    188          isBraked = false;
    189        } else {
    190          myController->setColorLED(255, 0, 255);
    191          isBraked = true;
    192        }
    193      }
    194
    195      if (myController->battery() < 100)
    196        blinkColorLED(myController);
    197    }
    198  }
    199}
    200
    201void sendPacket() {
    202  uint32_t now = millis();
    203
    204  if (now - last_transmit >= 10) {
    205    uint8_t packet[sizeof(jdata) + 2];
    206    packet[0] = START_BYTE;
    207    memcpy((uint8_t *)packet + 1, (uint8_t *)&jdata, sizeof(jdata));
    208    packet[9] = crc.get_hash(packet + 1, sizeof(jdata));
    209
    210    // Finally send packet to STM32 through UART2
    211    Serial2.write(packet, sizeof(packet));
    212    // Serial.printf("%lu\t", now - last_transmit);
    213    last_transmit = now;
    214    // dumpJoyData(jdata);
    215  }
    216}
    217// Arduino setup function. Runs in CPU 1
    218void setup() {
    219  pinMode(IN_BUILT_LED_BLUE, OUTPUT);
    220  Serial.begin(115200);
    221  Serial2.begin(115200, SERIAL_8N1, RXD2, TXD2);
    222
    223  Serial.printf("Firmware: %s\n", BP32.firmwareVersion());
    224  const uint8_t *addr = BP32.localBdAddress();
    225  Serial.printf("BD Addr: %2X:%2X:%2X:%2X:%2X:%2X\n", addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
    226
    227  // Setup the Bluepad32 callbacks
    228  BP32.setup(&onConnectedController, &onDisconnectedController);
    229
    230  // "forgetBluetoothKeys()" should be called when the user performs
    231  // a "device factory reset", or similar.
    232  // Calling "forgetBluetoothKeys" in setup() just as an example.
    233  // Forgetting Bluetooth keys prevents "paired" gamepads to reconnect.
    234  // But it might also fix some connection / re-connection issues.
    235  BP32.forgetBluetoothKeys();
    236
    237
    238  // Enables mouse / touchpad support for gamepads that support them.
    239  // When enabled, controllers like DualSense and DualShock4 generate two connected devices:
    240  // - First one: the gamepad
    241  // - Second one, which is a "virtual device", is a mouse.
    242  // By default, it is disabled.
    243  BP32.enableVirtualDevice(false);
    244
    245  BP32.enableNewBluetoothConnections(false);
    246}
    247
    248// Arduino loop function. Runs in CPU 1.
    249void loop() {
    250  // This call fetches all the controllers' data.
    251  // Call this function in your main loop.
    252  if (BP32.update()) {
    253    processControllers();
    254    if (connected)
    255      sendPacket();
    256  }
    257  // The main loop must have some kind of "yield to lower priority task" event.
    258  // Otherwise, the watchdog will get triggered.
    259  // If your main loop doesn't have one, just add a simple `vTaskDelay(1)`.
    260  // Detailed info here:
    261  // https://stackoverflow.com/questions/66278271/task-watchdog-got-triggered-the-tasks-did-not-reset-the-watchdog-in-time
    262
    263  vTaskDelay(1);
    264  // delay(150);
    265}
    
  • Save the sketch as esp_rx_bluepad.ino.

  • Create a new file joy_msg.hpp in the same directory as the sketch. Copy the contents of the JoyData structure from above into this.

  • Create two files crc8.hpp and crc8.cpp in the same directory as the sketch. The contents are same as above.

  • Select board DOIT ESP32 DEVKIT V1 from Tools > Board > esp32_bluepad32.

  • Select port and upload the sketch to the ESP32 board.

  • Open the Serial Monitor. Press the PS button and the Share button on the PS4 controller to pair it with the ESP32 board. You should see the joystick data changing according to the input from the PS4 controller.

For more information on the ESP32_Bluepad32 package, see the documentation.

5. Setting Raspberry Pi Pico W as Receiver

Raspberry Pi Pico W

Raspberry Pi Pico W

For the Raspberry Pi Pico W, we use Pico SDK. The Pico SDK is a C/C++ SDK for the Raspberry Pi Pico and Pico W. It provides a set of libraries and tools to help you develop applications for the Pico and Pico W.

Till now, we have provided code straight on the page. Now, we are going to deal with huge code base. So we will provide the link of github repository. Follow the following instructions below to set up the Pico SDK and build the code for receving from PS4 controller:

  • To install Pico SDK, run pico_setup.sh cloning from here.

    git clone https://github.com/raspberrypi/pico-setup.git
    cd pico-setup
    export SKIP_VSCODE=1 && bash pico_setup.sh
    
  • Clone this repository and update submodule.

    git clone https://github.com/Robotics-Club-Pulchowk/picow_ds4.git
    cd picow_ds4
    git submodule update --init --recursive
    
  • Find out MAC Address of PS4 Controller. In easy way, connect it to yout PC and see the MAC Address.

  • Get into the source code folder, open Src/bt_hid.c, scroll down a little and replace remote_addr_string value by the MAC address of PS4.

  • To print data on Serial Monitor, enable stdio usb by editing this line of Src/CMakeLists.txt.

    pico_enable_stdio_usb(picow_ds4 1)
    
  • From the project directory, make build folder, execute cmake and makefile.

    cd picow_ds4
    git submodule update --init --recursive
    mkdir build
    cd build
    cmake .. # you might use `-DPICO_BOARD=pico_w -DPICO_SDK_PATH=/your/path/to/pico-sdk` if environment is not set
    make -j4
    

    .uf2 file is used for programming Pico. You can find it at build/src/picow_ds4.uf2

  • Hold the boot button and connect the Pico W to PC using USB.

  • Drag and drop the picow_ds4.uf2 file from build/src to Pico W Mass Storage. You also can use similar command.

    # Just hit `tab` `tab` after `/media`.
    cp picow_ds4.uf2 /media/pi/PICOW
    
  • Long press share and PS4 button simultaneous untill fast blink of PS4 LED. It will connect to Pico W. Pico W blinks its green LED if no controller is connected and solid green if it detects and connects to a controller.

Pico W sends uart packet same as ESP32 througth its UART0 default Tx and Rx pin i.e. pin 0 and pin 1.

6. Setting STM32 to Receive Data from ESP32 or Pico W

6.1 STM32CubeMX Configuration

  • Open CubeMX and generate basic code with:

    • microcontroller: stm32f407vgt6 or board: STM32F407VG-DISC1

    • project name: stm32_joystick_master

    • Toolchain/IDE: CMake

  • Move to STM32CubeMX Pinout and Congiguration. From Categories, select Connectivity > USART4. Change mode to Asynchronous mode. You may change alternate pins from for USART4_RX and USART4_TX.

  • Under Configuration > DMA Settings, add USART4_RX DMA request.

  • Set PD12 Pin as GPIO Output. This pin is used to turn on/off the LED.

    game_controller_uart_cubemx
  • Generate code and open the project.

6.2. Joystick Class with Other Classes and Setups

  • Create a new file joy_msg.hpp inside Core > Inc. Copy and paste the below contents.

    joy_msg.hpp
     1#ifndef _JOY_MSG_HPP
     2#define _JOY_MSG_HPP
     3
     4#include <stdint.h>
     5
     6#define BUTTONS(button) (1 << button)
     7
     8namespace PS4
     9{
    10  enum Buttons
    11  {
    12    CROSS,
    13    CIRCLE,
    14    SQUARE,
    15    TRIANGLE,
    16    SHARE,
    17    PS,
    18    OPTION,
    19    L3,
    20    R3,
    21    L1,
    22    R1,
    23    UP,
    24    DOWN,
    25    LEFT,
    26    RIGHT,
    27    TOUCH
    28  };
    29}
    30
    31namespace XBox
    32{
    33  enum Buttons
    34  {
    35    A,
    36    B,
    37    X,
    38    Y,
    39    BACK,
    40    XBOX,
    41    START,
    42    LSTICL,
    43    RSTICK,
    44    LB,
    45    RB,
    46    UP,
    47    DOWN,
    48    LEFT,
    49    RIGHT
    50  };
    51}
    52
    53#pragma pack(push, 1)
    54struct JoyData
    55{
    56  int8_t lx;
    57  int8_t ly;
    58  int8_t rx;
    59  int8_t ry;
    60  uint8_t lt;
    61  uint8_t rt;
    62  uint16_t buttons;
    63};
    64#pragma pack(pop)
    65
    66#endif // _JOY_MSG_HPP
    

    This joy_msg.hpp has different identifier names for namespaces and some buttons than above.

  • Create a new file printf_config.h inside Core > Inc. Copy and paste the below contents:

    printf_config.c
     1#include "stm32f4xx_hal.h"
     2
     3#ifdef STDIO_USB
     4#include "usbd_cdc_if.h"
     5
     6/*Redirect printf to USB CDC. We can see output on Serial Monitor*/
     7int _write(int file, char *data, int len)
     8{
     9    CDC_Transmit_FS((uint8_t *)data, (uint16_t)len);
    10    return len;
    11}
    12
    13#else
    14/*Redirect printf to ITM. We can see output on Serial Wire Viewer on STM32CubeProgrammer*/
    15int _write(int file, char *data, int len)
    16{
    17    for (int i = 0; i < len; ++i)
    18    {
    19        ITM_SendChar(data[i]);
    20    }
    21    return len;
    22}
    23#endif
    
  • Create two new files crc8.hpp and crc8.cpp inside Core > Inc and Core > Src respectively. The contents of these files are as follows:

    crc8.hpp
     1/**
     2 ******************************************************************************
     3 * @file    crc8.hpp
     4 * @brief   Header file 8-bit crc calculation
     5 * @author  Robotics Team, IOE Pulchowk Campus
     6 ******************************************************************************
     7 */
     8
     9#ifndef CRC8_HPP_
    10#define CRC8_HPP_
    11
    12#include <cstdint>
    13
    14#ifndef CRC_POLYNOMIAL
    15#define CRC_POLYNOMIAL 7 /**< CRC Polynomial **/
    16#endif
    17
    18// clang-format off
    19#define CRC_WIDTH               8           /**< CRC width */
    20#define CRC_HASH_TABLE_SIZE     256         /**< Size of CRC hash table */
    21#define TOP_BIT                 (1 << 7)    /**< Top bit position */
    22// clang-format on
    23
    24/**
    25 * @brief Class for CRC-8 checksum calculation.
    26 */
    27class CRC8
    28{
    29public:
    30    /**
    31     * @brief Destructor for CRC8 class.
    32     */
    33    ~CRC8() {}
    34
    35    /**
    36     * @brief Calculate CRC-8 checksum for uint8_t type data.
    37     * @param buf Pointer to the data buffer.
    38     * @param len Length of the data buffer.
    39     * @return CRC-8 checksum value.
    40     */
    41    uint8_t get_hash(uint8_t *buf, uint16_t len);
    42
    43    static CRC8 Instance; /**< Singleton instance of CRC8 class. */
    44
    45private:
    46    static uint8_t polynomial;                  /** CRC polynomial **/
    47    static uint8_t table_[CRC_HASH_TABLE_SIZE]; /**< CRC hash table. */
    48
    49    /**
    50     * @brief Private constructor for CRC8 class.
    51     */
    52    CRC8();
    53
    54    /**
    55     * @brief Initialize the CRC hash table.
    56     */
    57    static void initialize_table();
    58};
    59
    60#endif // CRC_HPP_
    
    crc8.cpp
     1/***********************************************************************************************
     2 * @file crc8.cpp
     3 * @brief Implementation file for the crc8 class
     4 * @author Robotics Team, IOE Pulchowk Campus
     5 * @date 2024
     6 *
     7 * For more information on CRC calculation in C, \
     8 * see \link https://barrgroup.com/embedded-systems/how-to/crc-calculation-c-code \endlink.
     9 **********************************************************************************************/
    10
    11#include "crc8.hpp"
    12
    13CRC8 CRC8::Instance;
    14uint8_t CRC8::polynomial;
    15uint8_t CRC8::table_[CRC_HASH_TABLE_SIZE];
    16
    17CRC8::CRC8()
    18{
    19    polynomial = CRC_POLYNOMIAL;
    20    initialize_table();
    21}
    22
    23void CRC8::initialize_table()
    24{
    25    uint8_t remainder;
    26    int dividend = 0;
    27
    28    for (; dividend < 256; ++dividend)
    29    {
    30#if CRC_WIDTH == 8
    31        remainder = dividend;
    32#else
    33        remainder = dividend << (CRC_WIDTH - 8);
    34#endif
    35        for (uint8_t b = 8; b > 0; --b)
    36        {
    37            if (remainder & TOP_BIT)
    38            {
    39                remainder = (remainder << 1) ^ polynomial;
    40            }
    41            else
    42            {
    43                remainder = (remainder << 1);
    44            }
    45        }
    46        table_[dividend] = remainder;
    47    }
    48}
    49
    50uint8_t CRC8::get_hash(uint8_t *buf, uint16_t len)
    51{
    52    uint8_t data;
    53    uint8_t remainder = 0;
    54
    55    for (uint16_t index = 0; index < len; ++index)
    56    {
    57#if CRC_WIDTH == 8
    58        data = buf[index] ^ remainder;
    59#else
    60        data = buf[index] ^ (remainder >> (CRC_WIDTH - 8));
    61#endif
    62        remainder = table_[data] ^ (remainder << 8);
    63    }
    64
    65    return remainder;
    66}
    
  • Create two new files uart_def.h and uart.hpp inside Core > Inc and a new file uart.cpp inside Core > Src respectively. The contents of these files are as follows:

    uart_def.h
     1#ifndef UART_DEFINES_H__
     2#define UART_DEFINES_H__
     3
     4#ifndef UART_START_BYTE
     5#define UART_START_BYTE 0xA5 /**< Start byte of packet */
     6#endif
     7
     8#ifndef UART_MAX_RX_BUFFER_SIZE
     9#define UART_MAX_RX_BUFFER_SIZE 50 /**< Maximum size of receive buffer */
    10#endif
    11
    12#ifndef UART_MAX_TX_BUFFER_SIZE
    13#define UART_MAX_TX_BUFFER_SIZE 50 /**< Maximum size of transmit buffer */
    14#endif
    15
    16#ifndef UART_CONNCETION_TIMEOUT
    17#define UART_CONNCETION_TIMEOUT 100 /**< Timeout for UART connection */
    18#endif
    19
    20#endif // UART_DEFINES_H__
    
    uart.hpp
      1/**
      2 ******************************************************************************
      3 * @file    uart.hpp
      4 * @brief   Header file for our custom UART Class
      5 * @author  Robotics Team, IOE Pulchowk Campus
      6 ******************************************************************************
      7 */
      8
      9#ifndef UART_HPP__
     10#define UART_HPP__
     11
     12#include "stm32f4xx_hal.h"
     13#include "crc8.hpp"
     14#include "uart_def.h"
     15
     16enum UARTMode
     17{
     18  UART_RECEIVING,    /**< UART in receiving mode */
     19  UART_TRANSMITTING, /**< UART in transmitting mode */
     20  UART_BOTH          /**< UART in both receiving and transmitting mode */
     21};
     22
     23/**
     24 * @brief Enumeration defining the status of UART reception
     25 */
     26enum UARTReceiveStatus
     27{
     28  UART_START_BYTE_MATCHED, /**< Start byte of packet matched */
     29  UART_START_BYTE_ERROR,  /**< Start byte of packet failed to match */
     30  UART_CRC_MATCHED,        /**< CRC hash matched */
     31  UART_CRC_ERROR,         /**< CRC hash failed to match */
     32  UART_INTERRUPT_CRASHED   /**< UART Interrupt crashed while receiving*/
     33};
     34
     35/**
     36 * @brief Class representing the UART communication functionality
     37 */
     38class UART
     39{
     40public:
     41  /**
     42   * @brief Default constructor for UART class
     43   * @warning Be careful, do not call init() if uart handle is not set
     44   */
     45  UART();
     46
     47  /**
     48   * @brief Constructor for UART class
     49   * @param[in] _huart Pointer to the UART handle
     50   * @param[in] _mode UART Mode
     51   * @param[in] _rt_size Size of receiving or transmitting data
     52   * @param[in] _t_size Size of transmittin data used if r_size and t_size are different
     53   */
     54  UART(UART_HandleTypeDef *_huart, UARTMode _mode, uint16_t _rt_size, uint16_t _t_size = 0);
     55
     56  /**
     57   * @brief Default copy constructor for UART class
     58   */
     59  UART(const UART &) = default;
     60
     61  /**
     62   * @brief Default assignment operator for UART class
     63   */
     64  UART &operator=(const UART &) = default;
     65
     66  /**
     67   * @brief Default destructor for UART class
     68   */
     69  ~UART() = default;
     70
     71  /**
     72   * @brief Initialize UART communication
     73   */
     74  bool init();
     75
     76  /**
     77   * @brief Parse the received data and check for start byte, data, and CRC
     78   */
     79  UARTReceiveStatus process_receive_callback();
     80
     81  /**
     82   * @brief Process transmit callback to update status
     83   */
     84  void process_transmit_callback();
     85
     86  /**
     87   * @brief Copy data from the received data buffer to the provided buffer
     88   * @param[in] Pointer to the data to be received
     89   */
     90  bool get_received_data(uint8_t *data);
     91
     92  /**
     93   * @brief Transmit data over UART in blocking mode
     94   * @param[in] Pointer to the data to be transmitted
     95   * @return True if the data is transmitted successfully, false otherwise
     96   */
     97  bool transmit(uint8_t *);
     98
     99  /**
    100   * @brief Get the UART instance
    101   * @return Pointer to the UART handle
    102   */
    103  USART_TypeDef *get_uart_instance();
    104
    105  /**
    106   * @brief Check if the UART is connected
    107   * @return True if connected, false otherwise
    108   */
    109  bool connected();
    110
    111  /**
    112   * @brief Get the tick count of the last received data
    113   * @return Tick count of the last received data
    114   */
    115  uint32_t get_last_receive_tick();
    116
    117  /**
    118   * @brief Get the tick count of the last data reception completion
    119   * @return Tick count of the last data reception completion
    120   */
    121  uint32_t get_last_receive_cplt_tick();
    122
    123  /**
    124   * @brief Get the tick count of the last data transmission
    125   * @return Tick count of the last data transmission
    126   */
    127  uint32_t get_last_transmit_tick();
    128
    129  /**
    130   * @brief Get the tick count of the last data transmission completion
    131   * @return Tick count of the last data transmission completion
    132   */
    133  uint32_t get_last_transmit_cplt_tick();
    134
    135  /**
    136   * @brief Get the period of last data reception completion
    137   * @return Period of last data reception completion
    138   */
    139  uint32_t get_receive_cplt_period();
    140
    141  /**
    142   * @brief Get the frequency of last data reception completion
    143   * @return Frequency(Hz) of last data reception completion
    144   */
    145  float get_receive_cplt_frequency();
    146
    147  /**
    148   * @breie Get Receive Throughput
    149   * @param[in] time Time(ms) for bytes count [defualt 1000]
    150   * @return Receive Throughput in bytes per time
    151   */
    152  uint32_t get_receive_throughput(uint32_t time = 1000);
    153
    154  /**
    155   * @brief Get the sequence number of the received data
    156   * @return Sequence number of the received data
    157   */
    158  uint32_t get_receive_seq();
    159
    160  /**
    161   * @brief Get the period of last data transmit completion
    162   * @return Period of last data transmit completion
    163   */
    164  uint32_t get_transmit_cplt_period();
    165
    166  /**
    167   * @brief Get the frequency of last data transmit completion
    168   * @return Frequency(Hz) of last data transmit completion
    169   */
    170  float get_transmit_cplt_frequency();
    171
    172  /**
    173   * @breie Get Transmit Throughput
    174   * @param[in] time Time(ms) for bytes count [defualt 1000]
    175   * @return Transmit Throughput in bytes per time
    176   */
    177  uint32_t get_transmit_throughput(uint32_t time = 1000);
    178
    179  /**
    180   * @brief Print UART status
    181   */
    182  void print_uart_status();
    183
    184  /**
    185   * @brief Display the latest received packet in hexadecimal format
    186   */
    187  void print_receive_buffer();
    188
    189  /**
    190   * @brief Display the transmitting data in hexadecimal format
    191   */
    192  void print_transmit_buffer();
    193
    194  /**
    195   * @brief  Recieve Error handler function
    196   */
    197  static void UART_RxErrorCallBack(UART_HandleTypeDef *huart, UARTReceiveStatus status);
    198
    199  /**
    200   * @brief Transmit Error handler function
    201   */
    202  static void UART_TxErrorCallBack(UART_HandleTypeDef *huart);
    203
    204private:
    205  UART_HandleTypeDef *huart; /**< Pointer to the UART handle */
    206
    207  uint8_t receive_buffer[UART_MAX_RX_BUFFER_SIZE];   /**< Array to receive data */
    208  uint8_t transmit_buffer[UART_MAX_TX_BUFFER_SIZE];  /**< Array to transmit start byte, data, hash */
    209  uint8_t receive_data[UART_MAX_RX_BUFFER_SIZE - 2]; /**< Array to receive data */
    210  uint16_t r_size;                                   /**< Sizes for receiving data */
    211  uint16_t t_size;                                   /**< Sizes for tarnsmitting data */
    212
    213  uint32_t last_receive_tick = 0;       /**< last receive function call tick  */
    214  uint32_t last_transmit_tick = 0;      /**< last  transmit function call tick */
    215  uint32_t last_receive_cplt_tick = 0;  /**< last receive complete tick after crc matched */
    216  uint32_t last_transmit_cplt_tick = 0; /**< last transmit complete tick after transmit cplt callback */
    217  uint32_t receive_cplt_period = 0;     /**< Period of last receive complete */
    218  uint32_t transmit_cplt_period = 0;    /**< Period of last transmit complete */
    219
    220  bool is_waiting_for_start_byte;      /**< Current state of receiving */
    221  bool is_transmitting;                /**< Current state of transmitting */
    222  UARTMode mode;                       /**< Current UART mode */
    223  UARTReceiveStatus received_status;   /**< last receive status */
    224  uint16_t receive_seq = 0;            /**< Sequence number of received data */
    225  uint16_t prev_receive_seq = 0;       /**< Previous sequence number of received data */
    226  uint32_t start_byte_error_count = 0; /**< Count of start byte errors */
    227  uint32_t check_error_count = 0;      /**< Count of CRC hash errors */
    228  uint32_t transmit_error_count = 0;   /**< Count of transmit errors */
    229};
    230
    231#endif // UART_HPP__
    
    uart.cpp
      1/***********************************************************************************************
      2 * @file uart.cpp
      3 * @brief Implementation file for the UART class providing communication functionality.
      4 * @author Robotics Team, IOE Pulchowk Campus
      5 * @date 2023
      6 **********************************************************************************************/
      7
      8#include <memory.h>
      9#include <stdio.h>
     10
     11#include "uart.hpp"
     12
     13UART::UART() : huart(nullptr) {}
     14
     15UART::UART(UART_HandleTypeDef *_huart, UARTMode _mode, uint16_t _rt_size, uint16_t _t_size)
     16    : huart(_huart), mode(_mode)
     17{
     18    if (_mode == UART_RECEIVING || _mode == UART_BOTH)
     19    {
     20        r_size = _rt_size;
     21    }
     22    if (_mode == UART_TRANSMITTING || _mode == UART_BOTH)
     23    {
     24        t_size = _t_size > 0 ? _t_size : _rt_size;
     25    }
     26}
     27
     28bool UART::init()
     29{
     30    if (huart == nullptr)
     31        return false;
     32
     33    is_waiting_for_start_byte = true;
     34    is_transmitting = false;
     35
     36    if (mode == UART_RECEIVING || mode == UART_BOTH)
     37    {
     38        HAL_UART_Receive_DMA(huart, receive_buffer, 1);
     39    }
     40
     41    return true;
     42}
     43
     44/**
     45 * This function receives data over UART. It performs the following steps:
     46 * - Checks whether the start byte has been received.
     47 *   - If the start byte is received, proceeds to receive the data payload along with the CRC.
     48 *   - If the start byte is not received, continues waiting for it.
     49 * - Once the data is received, validates the CRC.
     50 * - Copies the data to the provided buffer and updates the reception status accordingly.
     51 *
     52 * @attention This function assumes that the UART has been initialized in RECEIVING mode or BOTH
     53 *            prior to calling this function. If the UART is initialized in a different mode,
     54 *            the behavior of this function may not be as expected.
     55 */
     56UARTReceiveStatus UART::process_receive_callback()
     57{
     58    /* Know the current tick */
     59    uint32_t current_tick = HAL_GetTick();
     60
     61    /* Check whether received start byte or data */
     62    if (is_waiting_for_start_byte)
     63    {
     64        /* If it is start byte, check its value */
     65        if (receive_buffer[0] == UART_START_BYTE)
     66        {
     67            /* Reset start byte flag for receiving data */
     68            is_waiting_for_start_byte = false;
     69
     70            /* Trigger DMA for receiving data */
     71            HAL_UART_Receive_DMA(huart, receive_buffer + 1, r_size + 1);
     72
     73            /* Update received_status for start byte matched */
     74            received_status = UART_START_BYTE_MATCHED;
     75        }
     76        else /* Start byte failed */
     77        {
     78            /* Increase start byte error count for testing*/
     79            start_byte_error_count++;
     80            /* Call user defined error callback */
     81            UART_RxErrorCallBack(huart, UART_START_BYTE_ERROR);
     82            /* Again try to receive start byte */
     83            HAL_UART_Receive_DMA(huart, receive_buffer, 1);
     84
     85            /* Update received_status for start byte failure */
     86            received_status = UART_START_BYTE_ERROR;
     87        }
     88    }
     89    else /* Receive data */
     90    {
     91        /* Calculate CRC for received data */
     92        uint8_t hash = CRC8::Instance.get_hash(receive_buffer + 1, r_size);
     93
     94        /* Compare with received Hash */
     95        if (hash == receive_buffer[r_size + 1])
     96        {
     97            /* Put data from receive buffer to receive data*/
     98            memcpy(receive_data, receive_buffer + 1, r_size);
     99
    100            /* Update received_status for CRC Matched */
    101            received_status = UART_CRC_MATCHED;
    102
    103            /* Update receive_seq, receive_cplt_period and last_receive_cplt_tick as new data is received in user memory */
    104            if (receive_seq == 65535)
    105                receive_seq = 0;
    106            else
    107                receive_seq++;
    108
    109            receive_cplt_period = current_tick - last_receive_cplt_tick;
    110            last_receive_cplt_tick = current_tick;
    111        }
    112        else /* Hash didn't match */
    113        {
    114            /* Increase check error count for testing*/
    115            check_error_count++;
    116            /* Call user defined error callback */
    117            UART_RxErrorCallBack(huart, UART_CRC_ERROR);
    118            /* Update received_status for CRC failure */
    119            received_status = UART_CRC_ERROR;
    120        }
    121
    122        /* Set start byte flag to receive start byte again for the next packet */
    123        is_waiting_for_start_byte = true;
    124
    125        /* Trigger DMA for receiving start byte */
    126        HAL_UART_Receive_DMA(huart, receive_buffer, 1);
    127    }
    128
    129    /* Update last_receive_tick to track receive call */
    130    last_receive_tick = current_tick;
    131
    132    /* Return received_status */
    133    return received_status;
    134}
    135
    136void UART::process_transmit_callback()
    137{
    138    /* Update transmit_cplt_period */
    139    uint32_t now = HAL_GetTick();
    140    transmit_cplt_period = now - last_transmit_cplt_tick;
    141    last_transmit_cplt_tick = now;
    142    is_transmitting = false;
    143}
    144
    145/**
    146 * This function receives data over UART. It checks whether the data has been received
    147 * and copies the data to the provided buffer.
    148 */
    149bool UART::get_received_data(uint8_t *r_data)
    150{
    151    if (receive_seq != prev_receive_seq)
    152    {
    153        /* Copy the data*/
    154        memcpy(r_data, receive_data, r_size);
    155        /*Update prev_receive_seq so we can use it in next receive*/
    156        prev_receive_seq = receive_seq;
    157
    158        return true;
    159    }
    160
    161    return false;
    162}
    163
    164/**
    165 * This function transmits data over UART. It prepares the data packet by loading
    166 * the start byte, copying the transmitted data, calculating and loading the CRC,
    167 * then transmitting the data using DMA. It also updates the transmit tick.
    168 *
    169 * @note This function assumes that the UART communication has been initialized
    170 *       prior to calling this function.
    171 */
    172bool UART::transmit(uint8_t *t_data)
    173{
    174    /* Load start byte */
    175    transmit_buffer[0] = UART_START_BYTE;
    176
    177    /* Load tx data */
    178    memcpy(transmit_buffer + 1, t_data, t_size);
    179
    180    /* Calculate and Load CRC */
    181    transmit_buffer[t_size + 1] = CRC8::Instance.get_hash(transmit_buffer + 1, t_size);
    182
    183    /* Transmit tx data using DMA */
    184    if (HAL_UART_Transmit_DMA(huart, transmit_buffer, t_size + 2) == HAL_OK)
    185    {
    186        /* Update transmit tick */
    187        last_transmit_tick = HAL_GetTick();
    188        /* Update is_transmitting flag */
    189        is_transmitting = true;
    190        return true;
    191    }
    192
    193    /* Increase transmit error count for testing*/
    194    transmit_error_count++;
    195    /* Call user defined error callback */
    196    UART_TxErrorCallBack(huart);
    197
    198    return false;
    199}
    200
    201USART_TypeDef *UART::get_uart_instance()
    202{
    203    return huart->Instance;
    204}
    205
    206bool UART::connected()
    207{
    208    return (HAL_GetTick() - last_receive_cplt_tick < UART_CONNCETION_TIMEOUT);
    209}
    210
    211uint32_t UART::get_last_receive_tick()
    212{
    213    return last_receive_tick;
    214}
    215
    216uint32_t UART::get_last_transmit_tick()
    217{
    218    return last_transmit_tick;
    219}
    220
    221uint32_t UART::get_last_receive_cplt_tick()
    222{
    223    return last_receive_cplt_tick;
    224}
    225
    226uint32_t UART::get_receive_cplt_period()
    227{
    228    return receive_cplt_period;
    229}
    230
    231float UART::get_receive_cplt_frequency()
    232{
    233    if (receive_cplt_period == 0)
    234        return 0.0f;
    235
    236    return 1000.0f / receive_cplt_period;
    237}
    238
    239uint32_t UART::get_receive_throughput(uint32_t time)
    240{
    241    if (receive_cplt_period == 0 || time == 0)
    242        return 0;
    243
    244    return (r_size + 2) * time / receive_cplt_period;
    245}
    246
    247uint32_t UART::get_receive_seq()
    248{
    249    return receive_seq;
    250}
    251
    252uint32_t UART::get_transmit_cplt_period()
    253{
    254    return transmit_cplt_period;
    255}
    256
    257float UART::get_transmit_cplt_frequency()
    258{
    259    if (transmit_cplt_period == 0)
    260        return 0.0f;
    261
    262    return 1000.0f / transmit_cplt_period;
    263}
    264
    265uint32_t UART::get_transmit_throughput(uint32_t time)
    266{
    267    if (transmit_cplt_period == 0 || time == 0)
    268        return 0;
    269
    270    return (t_size + 2) * time / transmit_cplt_period;
    271}
    272
    273void UART::print_uart_status()
    274{
    275    printf("========================================\n");
    276    printf("UART Status\n");
    277    printf("========================================\n");
    278    printf("Receive Status:\n");
    279    printf("----------------------------------------\n");
    280    printf("is_waiting_for_start_byte:: %d\n", is_waiting_for_start_byte);
    281    printf("Receive Seq:: %hu\n", receive_seq);
    282    printf("Start Byte Error Count:: %lu\n", start_byte_error_count);
    283    printf("Check Error Count:: %lu\n", check_error_count);
    284    printf("Last Receive Tick:: %lu\n", last_receive_tick);
    285    printf("Last Transmit Tick:: %lu\n", last_transmit_tick);
    286    printf("Last Receive Cplt Tick:: %lu\n", last_receive_cplt_tick);
    287    printf("Last Transmit Cplt Tick:: %lu\n", last_transmit_cplt_tick);
    288    printf("Receive Cplt Period:: %lu\n", receive_cplt_period);
    289    printf("Receive Cplt Frequency:: %.2f\n", get_receive_cplt_frequency());
    290    printf("Receive Throughput:: %lu\n", get_receive_throughput());
    291    printf("----------------------------------------\n");
    292    printf("Transmit Status:\n");
    293    printf("----------------------------------------\n");
    294    printf("is_transmitting:: %d\n", is_transmitting);
    295    printf("Transmit Cplt Period:: %lu\n", transmit_cplt_period);
    296    printf("Transmit Cplt Frequency:: %.2f\n", get_transmit_cplt_frequency());
    297    printf("Transmit Throughput:: %lu\n", get_transmit_throughput());
    298    printf("========================================\n\n");
    299}
    300
    301void UART::print_receive_buffer()
    302{
    303    printf("UART RX Data::");
    304    for (int i = 0; i <= r_size + 1; ++i)
    305    {
    306        printf(" %02x", receive_buffer[i]);
    307    }
    308    printf("\n");
    309}
    310
    311void UART::print_transmit_buffer()
    312{
    313    printf("UART TX Data::");
    314    for (int i = 0; i <= t_size + 1; ++i)
    315    {
    316        printf("%02x ", transmit_buffer[i]);
    317    }
    318    printf("\n");
    319}
    320
    321/*Weakly defined, so user can have its own implementation and donot throw error if not defined*/
    322__weak void UART::UART_RxErrorCallBack(UART_HandleTypeDef *huart, UARTReceiveStatus status)
    323{
    324    (void)huart;
    325    (void)status;
    326}
    327
    328/*Weakly defined, so user can have its own implementation and donot throw error if not defined*/
    329__weak void UART::UART_TxErrorCallBack(UART_HandleTypeDef *huart)
    330{
    331    (void)huart;
    332}
    
  • Create two new files joystick.hpp and joystick.cpp inside Core > Inc and Core > Src repsectively. Copy and paste the below contents:

    joystick.hpp
      1/**
      2 ******************************************************************************
      3 * @file    joystick.hpp
      4 * @brief   Header file for supporting joysticks
      5 * @author  Robotics Team, IOE Pulchowk Campus
      6 ******************************************************************************
      7 */
      8
      9#ifndef _JOYSTICK_HPP
     10#define _JOYSTICK_HPP
     11
     12#include <math.h>
     13
     14#include "stm32f4xx_hal.h"
     15#include "joy_msg.hpp"
     16#include "uart.hpp"
     17
     18#ifndef JOYSTICK_LONG_PRESS_DURATION
     19#define JOYSTICK_LONG_PRESS_DURATION 750 // ms
     20#endif
     21#ifndef JOYSTICK_DEBOUNCE_DURATION
     22#define JOYSTICK_DEBOUNCE_DURATION 30 // ms
     23#endif
     24
     25#if !(defined USE_PS4 || USE_XBOX)
     26#define USE_PS4
     27#endif
     28
     29template <typename t>
     30inline t map(t x, t in_min, t in_max, t out_min, t out_max)
     31{
     32  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
     33}
     34
     35template <typename t>
     36inline t map(t x, t out_min, t out_max)
     37{
     38  return map(x, 0, 1, out_min, out_max);
     39}
     40
     41/**
     42 * @brief Base class for joystick functionality.
     43 *
     44 * This class provides basic functionality for working with joysticks,
     45 * including initialization, data retrieval, and printing raw data.
     46 */
     47class Joystick
     48{
     49private:
     50    uint8_t dead_band;                  /**< Deadband value for joystick axes. */
     51    uint16_t timeout;                   /**< Timeout value for checking joystick connection. */
     52    JoyData data;                       /**< Structure to hold joystick data. */
     53    uint16_t prev_buttons = 0;          /**< Previous button state. */
     54    uint16_t pressed_buttons = 0;       /**< Pressed buttons. */
     55    uint16_t just_pressed_buttons = 0;  /**< Just pressed buttons. */
     56    uint16_t just_released_buttons = 0; /**< Just released buttons. */
     57    uint16_t clicked_buttons = 0;       /**< Clicked buttons. */
     58    uint16_t long_pressed_buttons = 0;  /**< Long pressed buttons. */
     59    uint32_t first_pressed_tick[16] = {0};
     60    uint32_t button_update_tick = 0;
     61
     62public:
     63    UART uart; /**< UART instance for communication. */
     64
     65    /**
     66     * @brief Default constructor.
     67     */
     68    Joystick() = default;
     69
     70    /**
     71     * @brief Constructor with parameters.
     72     *
     73     * Initializes the joystick with the given UART instance, deadband value, and timeout.
     74     *
     75     * @param _huart Pointer to the UART_HandleTypeDef for UART communication.
     76     * @param _dead_band Deadband value for joystick axes.
     77     * @param _timeout Timeout value for checking joystick connection.
     78     */
     79    Joystick(UART_HandleTypeDef *_huart, uint8_t _dead_band = 20, uint16_t _timeout = 100);
     80
     81    Joystick(const Joystick &) = default;
     82
     83    Joystick &operator=(const Joystick &) = default;
     84
     85    /**
     86     * @brief Default destructor.
     87     */
     88    ~Joystick() = default;
     89
     90    /**
     91     * @brief Initialize joystick to start UART communication.
     92     */
     93    bool init()
     94    {
     95        return uart.init();
     96    }
     97
     98    /**
     99     * @brief Update joystick data.
    100     *
    101     * @return True if data is updated, false otherwise.
    102     */
    103    bool update();
    104
    105    /**
    106     * @brief Print received data from joystick uart.
    107     */
    108    void print_received_data();
    109
    110    /**
    111     * @brief Print data from joystick.
    112     */
    113    void print_data();
    114
    115    /**
    116     * @brief Callback function for UART communication.
    117     */
    118    void uart_callback()
    119    {
    120        uart.process_receive_callback();
    121    }
    122
    123    /**
    124     * @brief Check if joystick is connected.
    125
    126     * @return True if joystick is connected, false otherwise.
    127     */
    128    bool connected()
    129    {
    130        if ((HAL_GetTick() - uart.get_last_receive_cplt_tick() < timeout) && (uart.get_last_receive_cplt_tick() != 0))
    131            return true;
    132        return false;
    133    }
    134
    135    /**
    136     * @brief Get joystick data.
    137     *
    138     * @return JoyData structure containing joystick data.
    139     */
    140    JoyData get_data()
    141    {
    142        return data;
    143    }
    144
    145    /**
    146     * @brief Get button state.
    147     *
    148     * @return 16-bit integer representing button state.
    149     */
    150    uint16_t get_buttons()
    151    {
    152        return data.buttons;
    153    }
    154
    155    /**
    156     * @brief Get previous button state.
    157     *
    158     * @return 16-bit integer representing clicked button flag.
    159     */
    160    uint16_t get_clicked_buttons()
    161    {
    162        return clicked_buttons;
    163    }
    164
    165    /**
    166     * @brief Get toggled button state.
    167     *
    168     * @return 16-bit integer representing toggled button flag.
    169     */
    170    uint16_t get_toggled_buttons()
    171    {
    172        return just_pressed_buttons | just_pressed_buttons;
    173    }
    174
    175    /**
    176     * @brief Get long pressed button state.
    177     *
    178     * @return 16-bit integer representing long pressed button flag.
    179     */
    180    uint16_t get_long_pressed_buttons()
    181    {
    182        return long_pressed_buttons;
    183    }
    184
    185    /**
    186     * @brief Get button state.
    187     *
    188     * @param button Button number.
    189     * @return True if button is pressed, false otherwise.
    190     */
    191    bool pressed(uint8_t button)
    192    {
    193        return pressed_buttons & BUTTONS(button);
    194    }
    195
    196    bool only_pressed(uint8_t button)
    197    {
    198        return (pressed_buttons & BUTTONS(button)) && !(pressed_buttons & ~BUTTONS(button));
    199    }
    200
    201    /**
    202     * @brief Get button state.
    203     *
    204     * @param button Button number.
    205     * @return True if button is released, false otherwise.
    206     */
    207    bool released(uint8_t button)
    208    {
    209        return !(pressed_buttons & BUTTONS(button));
    210    }
    211
    212    bool just_pressed(uint8_t button)
    213    {
    214        return just_pressed_buttons & BUTTONS(button);
    215    }
    216
    217    bool just_released(uint8_t button)
    218    {
    219        return just_released_buttons & BUTTONS(button);
    220    }
    221
    222    /**
    223     * @brief Get button state.
    224     *
    225     * @param button Button number.
    226     * @return True if button is clicked, false otherwise.
    227     */
    228    bool clicked(uint8_t button)
    229    {
    230        return clicked_buttons & BUTTONS(button);
    231    }
    232
    233    bool only_clicked(uint8_t button)
    234    {
    235        return (clicked_buttons & BUTTONS(button)) && !(pressed_buttons & ~BUTTONS(button));
    236    }
    237
    238    /**
    239     * @brief Get button state.
    240     *
    241     * @param button Button number.
    242     * @return True if button is toggled, false otherwise.
    243     */
    244    bool toggled(uint8_t button)
    245    {
    246        return get_toggled_buttons() & BUTTONS(button);
    247    }
    248
    249    /**
    250     * @brief Get button state.
    251     *
    252     * @param button Button number.
    253     * @return True if button is long pressed, false otherwise.
    254     */
    255    bool long_pressed(uint8_t button)
    256    {
    257        return long_pressed_buttons & BUTTONS(button);
    258    }
    259
    260    /**
    261     * @brief Get left stick x-axis.
    262     *
    263     * @return float Normalized left stick value of x-axis.
    264     */
    265    float lx()
    266    {
    267        if (abs(data.lx) < dead_band)
    268            return 0.0f;
    269        return (map<float>((float)data.lx, -127.0f, 127.0f, -1.0f, 1.0f));
    270    }
    271
    272    /**
    273     * @brief Get left stick y-axis.
    274     *
    275     * @return float Normalized left stick value of y-axis.
    276     */
    277    float ly()
    278    {
    279        if (abs(data.ly) < dead_band)
    280            return 0.0f;
    281        return (map<float>((float)data.ly, -127.0f, 127.0f, -1.0f, 1.0f));
    282    }
    283
    284    /**
    285     * @brief Get right stick x-axis.
    286     *
    287     * @return float Normalized right stick value of x-axis.
    288     */
    289    float rx()
    290    {
    291        if (abs(data.rx) < dead_band)
    292            return 0.0f;
    293        return (map<float>((float)data.rx, -127.0f, 127.0f, -1.0f, 1.0f));
    294    }
    295
    296    /**
    297     * @brief Get right stick y-axis.
    298     *
    299     * @return float Normalized right stick value of y-axis.
    300     */
    301    float ry()
    302    {
    303        if (abs(data.ry) < dead_band)
    304            return 0.0f;
    305        return (map<float>((float)data.ry, -127.0f, 127.0f, -1.0f, 1.0f));
    306    }
    307
    308    /**
    309     * @brief Get left trigger.
    310     *
    311     * @return float Normalized left trigger value.
    312     */
    313    float lt()
    314    {
    315        return (map<float>((float)data.lt, 0.0f, 255.0f, 0.0f, 1.0f));
    316    }
    317
    318    /**
    319     * @brief Get right trigger normalized value.
    320     *
    321     * @return float Normalized right trigger value.
    322     */
    323    float rt()
    324    {
    325        return (map<float>((float)data.rt, 0.0f, 255.0f, 0.0f, 1.0f));
    326    }
    327};
    328
    329#endif // _JOYSTICK_HPP
    
    joystick.cpp
      1/**
      2 ******************************************************************************
      3 * @file    joystick.cpp
      4 * @brief   Implementation for supporting joystics
      5 * @author  Robotics Team, IOE Pulchowk Campus
      6 ******************************************************************************
      7 */
      8
      9#include <math.h>
     10#include <stdio.h>
     11
     12#include "joystick.hpp"
     13
     14Joystick::Joystick(UART_HandleTypeDef *_huart, uint8_t _dead_band, uint16_t _timeout)
     15    : dead_band(_dead_band), timeout(_timeout), uart(_huart, UART_RECEIVING, 8) {}
     16
     17bool Joystick::update()
     18{
     19    if (!connected())
     20    {
     21        prev_buttons = 0;
     22        pressed_buttons = 0;
     23        just_pressed_buttons = 0;
     24        just_released_buttons = 0;
     25        clicked_buttons = 0;
     26        long_pressed_buttons = 0;
     27        return false;
     28    }
     29
     30    just_pressed_buttons = 0;
     31    just_released_buttons = 0;
     32    clicked_buttons = 0;
     33
     34    if (!uart.get_received_data((uint8_t *)&data))
     35        return false;
     36
     37    uint32_t now = HAL_GetTick();
     38
     39    if (now - button_update_tick >= JOYSTICK_DEBOUNCE_DURATION)
     40    {
     41        long_pressed_buttons = 0;
     42        pressed_buttons = data.buttons;
     43        just_pressed_buttons = (data.buttons & ~prev_buttons);
     44        just_released_buttons = (~data.buttons & prev_buttons);
     45
     46        for (int i = 0; i < 16; ++i)
     47        {
     48            bool is_pressed = data.buttons & BUTTONS(i);
     49
     50            if (is_pressed)
     51            {
     52                if (!(prev_buttons & BUTTONS(i)))
     53                {
     54                    first_pressed_tick[i] = now;
     55                }
     56
     57                if (now - first_pressed_tick[i] >= JOYSTICK_LONG_PRESS_DURATION)
     58                {
     59                    long_pressed_buttons |= BUTTONS(i);
     60                }
     61            }
     62            else
     63            {
     64                if (prev_buttons & BUTTONS(i))
     65                {
     66                    if (now - first_pressed_tick[i] < JOYSTICK_LONG_PRESS_DURATION)
     67                    {
     68                        clicked_buttons |= BUTTONS(i);
     69                    }
     70                }
     71            }
     72        }
     73
     74        prev_buttons = data.buttons;
     75        button_update_tick = now;
     76    }
     77
     78    return true;
     79}
     80
     81void Joystick::print_received_data()
     82{
     83    printf("Joystick: lx:%d ly:%d rx:%d ry:%d lt:%u rt:%u buttons:%04x\n",
     84           data.lx,
     85           data.ly,
     86           data.rx,
     87           data.ry,
     88           data.lt,
     89           data.rt,
     90           data.buttons);
     91}
     92
     93void Joystick::print_data()
     94{
     95    printf("Joystick: lx:%f ly:%f rx:%f ry:%f lt:%f rt:%f, prs:%04x, tgl:%04x clk:%04x lng:%04x:\n",
     96           lx(),
     97           ly(),
     98           rx(),
     99           ry(),
    100           lt(),
    101           rt(),
    102           pressed_buttons,
    103           get_toggled_buttons(),
    104           clicked_buttons,
    105           long_pressed_buttons);
    106}
    

6.3. Code to Receive Data

  • Create two new files App.h and App.cpp inside Core > Inc and Core > Src respectively. Copy and paste the below contents:

    app.h
     1#ifndef __APP_H
     2#define __APP_H
     3
     4#ifdef __cplusplus
     5extern "C" {
     6#endif
     7
     8void setup();
     9
    10void loop();
    11
    12#ifdef __cplusplus
    13}
    14#endif
    15
    16#endif // __APP_H
    
    app.cpp
     1#include <stdio.h>
     2
     3#include "usart.h"
     4#include "joystick.hpp"
     5#include "app.h"
     6
     7Joystick joystick(&huart4);
     8uint32_t last_loop_tick = 0;
     9uint32_t last_rx_tick = 0;
    10uint32_t last_print_tick = 0;
    11
    12void setup()
    13{
    14    joystick.init();
    15    printf("Joystick initialized\n");
    16    last_loop_tick = HAL_GetTick();
    17}
    18
    19void loop()
    20{
    21    if (HAL_GetTick() - last_loop_tick < 10)
    22    return;
    23
    24    last_loop_tick = HAL_GetTick();
    25
    26    // Must call every 10ms or less.
    27    bool is_joystick_updated = joystick.update();
    28
    29    if (!joystick.connected())
    30    {
    31        printf("Joystick not connected\n");
    32    }
    33
    34    if (is_joystick_updated)
    35    {
    36        if (HAL_GetTick() - last_print_tick > 100)
    37        {
    38            last_print_tick = HAL_GetTick();
    39            joystick.print_data();
    40            // joystick.print_received_data();
    41        }
    42    }
    43
    44    float lx, ly, rx, ry, lt, rt;
    45    lx = joystick.lx();
    46    ly = joystick.ly();
    47    rx = joystick.rx();
    48    ry = joystick.ry();
    49    lt = joystick.lt();
    50    rt = joystick.rt();
    51
    52    bool is_lb_clicked = joystick.clicked(PS4::L1);
    53    bool is_rb_clicked = joystick.clicked(PS4::R1);
    54
    55    // only clicked is true if the button is clicked and no other button is pressed
    56    bool is_lb_only_clicked = joystick.only_clicked(PS4::L1);
    57    bool is_rb_only_clicked = joystick.only_clicked(PS4::R1);
    58    bool is_lb_pressed = joystick.pressed(PS4::L1);
    59    bool is_rb_pressed = joystick.pressed(PS4::R1);
    60    bool is_lb_long_pressed = joystick.long_pressed(PS4::L1);
    61    bool is_rb_long_pressed = joystick.long_pressed(PS4::R1);
    62
    63    // experiment yourself :)
    64}
    65
    66void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
    67{
    68    if (huart->Instance == joystick.uart.get_uart_instance())
    69    {
    70        joystick.uart_callback();
    71        last_rx_tick = HAL_GetTick();
    72
    73        if (HAL_GetTick() - last_rx_tick > 30)
    74        {
    75            last_rx_tick = HAL_GetTick();
    76            HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
    77        }
    78    }
    79}
    
  • Navigate to Core > Src and open main.c. Include app.h.

    /* USER CODE BEGIN Includes */
    #include "app.h"
    /* USER CODE END Includes */
    
  • Call setup and loop in main function.

    /* Infinite loop */
    /* USER CODE BEGIN WHILE */
    while (1)
    {
     loop();
    /* USER CODE END WHILE */
    
    /* USER CODE BEGIN 3 */
    }
    /* USER CODE END 3 */
    

6.4. Build and Flash

  • Now inside Core, the structure should look similar to this:

    Inc\
      └── app.h
      └── crc8.hpp
      └── dma.h
      └── gpio.h
      └── joy_msg.hpp
      └── joystick.hpp
      └── main.h
      └── stm32f4xx_hal_conf.h
      └── stm32f4xx_it.h
      └── uart_def.h
      └── uart.hpp
      └── usart.h
    Src\
      └── app.cpp
      └── crc8.cpp
      └── dma.c
      └── gpio.c
      └── joystick.cpp
      └── main.c
      └── printf_config.c
      └── stm32_crc8.cpp
      └── stm32f4xx_hal_msp.c
      └── stm32f4xx_it.c
      └── syscalls.c
      └── sysmem.c
      └── system_stm32f4xx.c
      └── uart.cpp
      └── usart.c
    

    Update CMakeLists.txt as follows:

    CMakeLists.txt
     1cmake_minimum_required(VERSION 3.22)
     2
     3#
     4# This file is generated only once,
     5# and is not re-generated if converter is called multiple times.
     6#
     7# User is free to modify the file as much as necessary
     8#
     9
    10# Setup compiler settings
    11set(CMAKE_C_STANDARD 11)
    12set(CMAKE_C_STANDARD_REQUIRED ON)
    13set(CMAKE_C_EXTENSIONS ON)
    14
    15set(CMAKE_CXX_STANDARD 17)
    16set(CMAKE_CXX_STANDARD_REQUIRED ON)
    17set(CMAKE_CXX_EXTENSIONS ON)
    18
    19
    20# Define the build type
    21if(NOT CMAKE_BUILD_TYPE)
    22    set(CMAKE_BUILD_TYPE "Debug")
    23endif()
    24
    25# Set the project name
    26set(CMAKE_PROJECT_NAME stm32_joystick_master)
    27
    28# Include toolchain file
    29include("cmake/gcc-arm-none-eabi.cmake")
    30
    31# Enable compile command to ease indexing with e.g. clangd
    32set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
    33
    34# Core project settings
    35project(${CMAKE_PROJECT_NAME})
    36message("Build type: " ${CMAKE_BUILD_TYPE})
    37
    38# Enable CMake support for ASM and C languages
    39enable_language(CXX C ASM)
    40
    41# Create an executable object type
    42add_executable(${CMAKE_PROJECT_NAME})
    43
    44# Add STM32CubeMX generated sources
    45add_subdirectory(cmake/stm32cubemx)
    46
    47# Link directories setup
    48target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE
    49    # Add user defined library search paths
    50)
    51
    52# Add sources to executable
    53target_sources(${CMAKE_PROJECT_NAME} PRIVATE
    54    # Add user sources here
    55    ${CMAKE_SOURCE_DIR}/Core/Src/printf_config.c
    56    ${CMAKE_SOURCE_DIR}/Core/Src/crc8.cpp
    57    ${CMAKE_SOURCE_DIR}/Core/Src/uart.cpp
    58    ${CMAKE_SOURCE_DIR}/Core/Src/joystick.cpp
    59    ${CMAKE_SOURCE_DIR}/Core/Src/app.cpp
    60)
    61
    62# Add include paths
    63target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
    64    # Add user defined include paths
    65)
    66
    67# Add project symbols (macros)
    68target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE
    69    # Add user defined symbols
    70)
    71
    72# Add linked libraries
    73target_link_libraries(${CMAKE_PROJECT_NAME}
    74    stm32cubemx
    75
    76    # Add user defined libraries
    77)
    78
    79# Generate binary
    80add_custom_command(
    81    OUTPUT ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin
    82    COMMAND arm-none-eabi-objcopy -O binary ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.elf ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin
    83    DEPENDS ${CMAKE_PROJECT_NAME}
    84    COMMENT "Converting ELF to binary"
    85)
    86
    87# Binary target
    88add_custom_target(bin ALL
    89    DEPENDS ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin
    90    COMMENT "Generating binary file"
    91)
    
  • Build and flash the program.

    mkdir build
    cd build
    cmake ..
    make -j
    st-flash write stm32_joystick_master.bin 0x8000000
    
  • Connect the ESP32 or Pico W to the STM32 board througth UART. Also connect PS4 controller.

  • Open STM32CubeProgrammer and connect to the STM32 board. You should see the joystick data changing according to the input from the PS4 controller. The LED on PD12 pin blinks if STM32 is receiving data from the ESP32 or Pico W.

Do your own experiment with the Joystick class in App.cpp.