PlatformIO

How to flash Marlin 2.x to BTT SKR 1.4 using PlatformIO

The best way to flash Marlin via PlatformIO onto the LPC1768 MCU on a BTT SKR v1.4 board is to insert an SD card into it, connect the computer via USB and use the mbed as upload_protocol.

In order to do this, edit ini/lpc176x.ini from within Visual Studio Code and add the following new code to [env:LPC1768]:

upload_protocol = mbed
upload_port=/media/uli/A87B-A154/

with upload_port being the directory where the SD card is mounted (while you can do this using an SD card reader, it is so much easier by just connecting the BTT SKR v1.4 via USB directly to your computer, allowing both serial port and SD card access at the same time).

Full example of the [env:LPC1768] section:

[env:LPC1768]
platform = ${common_LPC.platform}
extends  = common_LPC
board    = nxp_lpc1768
upload_protocol = mbed
upload_port=/media/uli/A87B-A154/

In the PlatformIO menu, choose LPC1768/Upload. There is no special configuration for the BTT SKR v1.4 but of course you need to configure Configuration.h etc correctly – see https://www.makenprint.uk/3d-printing/3d-printing-guides/skr-v1-4-configuration-h-marlin-2-setup-part-2/

After uploading, press the reset button on the Board to apply the firmware update. You can use picocom to connect to your printer, see How to connect to your 3D printer using picocom, e.g.:

picocom -b 115200 /dev/ttyACM0 --imap lfcrlf --ech

Enter M115 to check if you have correctly updated the fiwa

You might need to manually re-mount the SD card using your file manager after a firmware update in order to enabled PlaformIO doing another update.

Posted by Uli Köhler in 3D printing, Embedded, PlatformIO

How to fix PlatformIO ArduinoJSON .pio/libdeps/ESP32/ArduinoJson/src/ArduinoJson/Variant/ConverterImpl.hpp:43:5: error: static assertion failed: To use 64-bit integers with ArduinoJson, you must set ARDUINOJSON_USE_LONG_LONG to 1

Problem:

While trying to compile your PlatformIO project, you see an error message like

.pio/libdeps/ESP32/ArduinoJson/src/ArduinoJson/Variant/ConverterImpl.hpp:43:5: error: static assertion failed: To use 64-bit integers with ArduinoJson, you must set ARDUINOJSON_USE_LONG_LONG to 1. See https://arduinojson.org/v6/api/config/use_long_long/
     ARDUINOJSON_ASSERT_INTEGER_TYPE_IS_SUPPORTED(T);
     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from .pio/libdeps/ESP32/ArduinoJson/src/ArduinoJson.hpp:30,
                 from .pio/libdeps/ESP32/ArduinoJson/src/ArduinoJson.h:9,
                 from include/MQTT/StatusMessage.hpp:2,
                 from src/MQTT/StatusMessage.cpp:1:
.pio/libdeps/ESP32/ArduinoJson/src/ArduinoJson/Variant/ConverterImpl.hpp: In instantiation of 'static void ArduinoJson6185_1::Converter<T, Enable>::toJson(const T&, ArduinoJson6185_1::VariantRef) [with T = long long int (*)(); Enable = void]':
.pio/libdeps/ESP32/ArduinoJson/src/ArduinoJson/Variant/VariantRef.hpp:98:27:   required from 'bool ArduinoJson6185_1::VariantRef::set(T*) const [with T = long long int()]'
.pio/libdeps/ESP32/ArduinoJson/src/ArduinoJson/Object/MemberProxy.hpp:58:5:   required from 'ArduinoJson6185_1::MemberProxy<TParent, TStringRef>::this_type& ArduinoJson6185_1::MemberProxy<TParent, TStringRef>::operator=(TChar*) [with TChar = long long int(); TObject = ArduinoJson6185_1::JsonDocument&; TStringRef = const char*; ArduinoJson6185_1::MemberProxy<TParent, TStringRef>::this_type = ArduinoJson6185_1::MemberProxy<ArduinoJson6185_1::JsonDocument&, const char*>]'

Solution

Add -DARDUINOJSON_USE_LONG_LONG=1 to the build_flags in platformio.ini. If build_flags does not exist in platformio.ini, create it after [env:...]

build_flags = -DARDUINOJSON_USE_LONG_LONG=1

 

Posted by Uli Köhler in Arduino, Electronics, ESP8266/ESP32, PlatformIO

ESP32 Wireguard example with HTTP access over Wireguard (PlatformIO)

In this example we will use Wireguard-ESP32-Arduino in order to make HTTP requests over Wireguard on the ESP32.

[env:esp32-gateway]
platform = espressif32
board = esp32-gateway
framework = arduino
monitor_speed = 115200
lib_deps =
    ciniml/[email protected]^0.1.5
#include <WiFi.h>
#include <WireGuard-ESP32.h>

// WiFi configuration --- UPDATE this configuration for your WiFi AP
char ssid[] = "MyWifiESSID";
char password[] = "my-wifi-password";

// WireGuard configuration --- UPDATE this configuration from JSON
char private_key[] = "gH2YqDa+St6x5eFhomVQDwtV1F0YMQd3HtOElPkZgVY=";
IPAddress local_ip(10, 217, 59, 2);
char public_key[] = "X6NJW+IznvItD3B5TseUasRPjPzF0PkM5+GaLIjdBG4=";
char endpoint_address[] = "192.168.178.133"; // IP of Wireguard endpoint to connect to.
int endpoint_port = 19628;

static WireGuard wg;

void setup()
{
    Serial.begin(115200);
    Serial.println("Connecting to the AP...");
    WiFi.begin(ssid, password);
    while( !WiFi.isConnected() ) {
        delay(100);
    }
    Serial.println(WiFi.localIP());
    Serial.println("Adjusting system time...");
    configTime(9 * 60 * 60, 0, "ntp.jst.mfeed.ad.jp", "ntp.nict.jp", "time.google.com");

    Serial.println("Connected. Initializing WireGuard...");
    wg.begin(
        local_ip,
        private_key,
        endpoint_address,
        public_key,
        endpoint_port);
}

void loop()
{
    WiFiClient client;

    /**
     * Connect to
     * python3 -m http.server
     */
    if( !client.connect("10.217.59.1", 8000) ) {
        Serial.println("Failed to connect...");
        delay(1000);
        return;
    } else { // Client connected successfully. Send dummy HTTP request.
        client.write("GET /wireguard-test HTTP/1.1\r\n");
        client.write("Host: wireguard.test.com\r\n");
        client.write("\r\n\r\n");
    }

}

Remember to replace 192.168.238.133 by the IP address of the computer your ESP32 should connect to (i.e. the computer running WireGuard). You also need to enter the correct Wifi credentials.

On the computer, deploy this WireGuard config:

[Interface]
# Name = Computer
PrivateKey = ONj6Iefel47uMKtWRCSMLan2UC5eW3Fj9Gsy9bqcyEc=
Address = 10.217.59.1/24
ListenPort = 19628

[Peer]
# Name = ESP32
PublicKey = H3KaL/X94984cLDNWFsM4Hx6Rs/Ku0bW2ECkDUn7wFw=
AllowedIPs = 10.217.59.2/32
PersistentKeepalive = 60

which is auto-generated by the following GuardMyWire config:

{
    "rules": {
        "Node": {
            "connect_to": ["*"],
            "keepalive": 60
        }
    },
    "peers": [
        {
            "name": "Computer",
            "endpoint": "192.168.178.233:19628",
            "addresses": [
                "10.217.59.1/24"
            ],
            "type": "Node",
            "interface_name": "wg0"
        }, {
            "name": "ESP32",
            "addresses": [
                "10.217.59.2/24"
            ],
            "type": "Node",
            "interface_name": "wg0"
        }
    ]
}

Enable this config and start a Python HTTP server to receive the requests using

python3 -m http.server

Now flash the firmware on the ESP32.

Using wg show you should see the ESP connecting:

interface: Computer
  public key: X6NJW+IznvItD3B5TseUasRPjPzF0PkM5+GaLIjdBG4=
  private key: (hidden)
  listening port: 19628

peer: H3KaL/X94984cLDNWFsM4Hx6Rs/Ku0bW2ECkDUn7wFw=
  endpoint: 10.9.1.108:19628
  allowed ips: 10.217.59.2/32
  latest handshake: 5 seconds ago
  transfer: 11.71 MiB received, 10.43 MiB sent
  persistent keepalive: every 1 minute

Look for the

latest handshake: 5 seconds ago

line.

On the shell running python3 -m http.server you should see the dummy HTTP requests:

10.217.59.2 - - [31/Dec/2021 02:36:48] "GET /wireguard-test HTTP/1.1" 404 -
10.217.59.2 - - [31/Dec/2021 02:36:48] code 404, message File not found
10.217.59.2 - - [31/Dec/2021 02:36:48] "GET /wireguard-test HTTP/1.1" 404 -
10.217.59.2 - - [31/Dec/2021 02:36:48] code 404, message File not found
10.217.59.2 - - [31/Dec/2021 02:36:48] "GET /wireguard-test HTTP/1.1" 404 -
10.217.59.2 - - [31/Dec/2021 02:36:48] code 404, message File not found
Posted by Uli Köhler in ESP8266/ESP32, PlatformIO, Wireguard

FreeRTOS task queue minimal example

This is how you create and use a task queue in FreeRTOS:

Global declaration

Declare the structure of a task (I recommend to use a task type enum class in order to keep the flexibility of using multiple task types:

#include <freertos/queue.h>

enum class I2CTaskType : uint8_t {
    MyTaskType = 0
};

struct I2CTask {
    I2CTaskType type;
    // Parameters
    int16_t value;
};
static QueueHandle_t i2cTaskQueue;

Initialization code

Call this once, before using it:

// Create task queue
i2cTaskQueue = xQueueCreate(8 /* Number of queue slots */, sizeof(I2CTask));

In the thread processing the queue

if (xQueueReceive(i2cTaskQueue, (void *)&task, portMAX_DELAY /* Wait infinitely for new tasks */) == pdTRUE) {
    if(task.type == I2CTaskType::MyTaskType) {
        // TODO process task
        Serial.printf("My task type: %d\r\n", task.value);
    }
}

How to add a task to the queue

void AddTask(int16_t val) {
    I2CTask task;
    task.type = I2CTaskType::MyTaskType;
    task.value = val;
    xQueueSend(i2cTaskQueue, (void*)&task, 10 / portTICK_PERIOD_MS /* timeout */);
}

 

Posted by Uli Köhler in C/C++, Embedded, FreeRTOS, PlatformIO

How to fix error: invalid conversion from ‘int’ to ‘esp_mqtt_event_id_t’ on the ESP8266 or ESP32

If you see an error message like

src/main.cpp: In function 'void InitMQTT()':
/home/uli/.platformio/packages/framework-arduinoespressif32/tools/sdk/esp32/include/esp_event/include/esp_event_base.h:37:32: error: invalid conversion from 'int' to 'esp_mqtt_event_id_t' [-fpermissive]
 #define ESP_EVENT_ANY_ID       -1               /**< register handler for any event id */

src/main.cpp:80:44: note: in expansion of macro 'ESP_EVENT_ANY_ID'
     esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, client);
                                            ^~~~~~~~~~~~~~~~

replace ESP_EVENT_ANY_ID by MQTT_EVENT_ANY and recompile. This will fix the issue. Using ESP_EVENT_ANY_ID was possible in an outdated version of the MQTT library.

 

Posted by Uli Köhler in Arduino, Embedded, ESP8266/ESP32, MQTT, Networking, PlatformIO

How to use a specific espressif32 platformio version in PlatformIO

For production projects, you often want to lock platform & library versions in order to avoid an update suddenly breaking your code:

In platformio.ini, instead of

platform = espressif32

use this syntax to refer to the git repository and a specific tag (v3.4.0 in this example) directly:

platform = https://github.com/platformio/platform-espressif32.git#v3.4.0
Posted by Uli Köhler in Arduino, Electronics, Embedded, ESP8266/ESP32, PlatformIO

What ESP-IDF version does PlatformIO with Arduino use

On Dec 17th 2021, PlatformIO with this default platformio.ini config (with platformio-espressif32 v3.4.0):

[env:ESP32]
platform = espressif32
board = esp32dev
framework = arduino

uses ESP-IDF version 3.3.5

You can find this out yourself by printing all preprocessor flags as described in our post on How to print all preprocessor flags in PlatformIO and then looking for ESP_IDF_VERSION_... definitions using:

grep ESP_IDF_VERSION .pio/build/ESP32/src/main.cpp.o

which currently results in

#define ESP_IDF_VERSION_MINOR 3
#define ESP_IDF_VERSION_MAJOR 3
#define ESP_IDF_VERSION_PATCH 5

 

Posted by Uli Köhler in Arduino, ESP8266/ESP32, PlatformIO

How to use specific arduino-esp32 version in PlatformIO

Add this line to platformio.ini in order to use a specific arduino-esp32 version – such as 1.0.6:

platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#1.0.6

Note that for some upstream versions – at Dec 17 2021 that is arduino-esp32 v2.x, you also need to use a different platform:

platform = https://github.com/platformio/platform-espressif32.git#feature/arduino-upstream
platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.1
Posted by Uli Köhler in Arduino, Embedded, ESP8266/ESP32, PlatformIO

How to fix Arduino error: ‘size_t’ has not been declared

Problem:

In Arduino, you see an error message like

src/main.cpp:7:48: error: 'size_t' has not been declared
void MyFunction(size_t size);

Solution:

Include stddef.h where size_t is declared:

#include <stddef.h>

Add this line to the top of the file where the error occured.

Posted by Uli Köhler in Arduino, C/C++, Embedded, PlatformIO

How to fix Arduino I2C Wire error: call of overloaded ‘begin(const int&, const int&, int)’ is ambiguous

Problem:

You are trying to call Wire.begin() for I2C using

Wire.begin(Pin_I2C_SDA, Pin_I2C_SCL, 400000);

but you see an error message like

src/MyI2C.cpp: In function 'void MyInitI2C()':
src/NyI2C.cpp:139:47: error: call of overloaded 'begin(const int&, const int&, int)' is ambiguous
     Wire.begin(Pin_I2C_SDA,Pin_I2C_SCL, 400000);
                                               ^
In file included from include/MyIO.hpp:2:0,
                 from src/MyI2C.cpp:2:
/home/uli/.platformio/packages/framework-arduinoespressif32/libraries/Wire/src/Wire.h:79:10: note: candidate: bool TwoWire::begin(int, int, uint32_t)
     bool begin(int sda=-1, int scl=-1, uint32_t frequency=0); // returns true, if successful init of i2c bus
          ^
/home/uli/.platformio/packages/framework-arduinoespressif32/libraries/Wire/src/Wire.h:80:10: note: candidate: bool TwoWire::begin(uint8_t, int, int, uint32_t)
     bool begin(uint8_t slaveAddr, int sda=-1, int scl=-1, uint32_t frequency=0);

Solution:

This happens with specific versions of the Arduino framework. For me it happened specifically when upgrading to arduino-esp32 version 2.0.1.

You need to explicitly cast the third argument (400000) to uint32_t in order to tell the compiler which of the two functions you want to call:

Wire.begin(Pin_I2C_SDA, Pin_I2C_SCL, 400000);
Posted by Uli Köhler in Arduino, C/C++, Embedded, PlatformIO

How to initialize LittleFS in PlatformIO on the ESP32 using the lorol/LITTLEFS library

Currently you need to add the LittleFS-ESP32 library in platformio.ini (the library is available as part of the core arduino-espressif32 bleeding edge version but you need that library in the standard version):

lib_deps =
    lorol/LittleFS_esp32 @ ^1.0.6

Now include LittleFS:

#include <LITTLEFS.h>

#define SPIFFS LITTLEFS

Initialize it using

// Initialize LittleFS
if (!LITTLEFS.begin(false /* false: Do not format if mount failed */)) {
  Serial.println("Failed to mount LittleFS");
  if (!LITTLEFS.begin(true /* true: format */)) {
    Serial.println("Failed to format LittleFS");
  } else {
    Serial.println("LittleFS formatted successfully");
  }
} else { // Initial mount success
}

 

Posted by Uli Köhler in Arduino, Embedded, ESP8266/ESP32, PlatformIO

ArduinoJSON: How to fix 1 or 0 being printed instead of true/false

volatile bool value = true;

DynamicJsonDocument json(1024);
json["ok"] = value;
serializeJson(json, Serial);

This will print {"ok": 1} instead of {"ok": true} due to value being declared volatile (it works with just bool value, it does not work with volatile bool value).

In order to force {"ok": true}, just case value to bool:

json["ok"] = (bool)value;

Full example

volatile bool value = true;

DynamicJsonDocument json(1024);
json["ok"] = (bool)value;
serializeJson(json, Serial);

 

Posted by Uli Köhler in Arduino, Embedded, PlatformIO

FreeRTOS mutex minimal example

This is how you create and use a mutex in FreeRTOS:

Includes:

#include <freertos/semphr.h>

 

Global declaration

SemaphoreHandle_t myMutex;

Initialization code

Call this once, before using it:

myMutex = xSemaphoreCreateMutex();

How to lock & unlock the mutex

// Wait a maximum of 10ms to lock the mutex
if(xSemaphoreTake(myMutex, 10 / portTICK_PERIOD_MS) == pdTRUE) {
   // Success locking the mutex
   // TODO: Your code goes here!
   // Unlock the mutex!
   xSemaphoreGive(myMutex);
} else {
   // Failed to lock the mutex within timeout
   // DO NOT use the resource protected by the mutex
   // DO NOT unlock (xSemaphoreGive) !
}

 

Posted by Uli Köhler in Embedded, FreeRTOS, PlatformIO

How to print all preprocessor flags in PlatformIO

In order to show the values of all preprocessor flags, set this in platformio.ini:

build_flags = -E -dM

Now rebuild and you’ll see an error message like

Linking .pio/build/ESP32/firmware.elf
.pio/build/ESP32/src/main.cpp.o: file not recognized: File format not recognized
collect2: error: ld returned 1 exit status
*** [.pio/build/ESP32/firmware.elf] Error 1

This error is expected since GCC will not produce object files as output but preprocessor output definitions!

Now open the respective “object” files in your text editor:

code .pio/build/ESP32/src/main.cpp.o

where ESP32 is the name of your build configuration in platformio.ini. You will see all the definitions like this:

#define VSPI 3
#define XTENSA_HWCIDVERS_T1030_3 12
#define GPIO_PIN19_INT_TYPE 0x00000007
#define CONFIG_LWIP_MAX_RAW_PCBS 16
#define XTENSA_HWVERSION_RD_2012_4 240004
#define SPI_CLOCK_DIV32 0x013c1001
#define VALUE_GET_FIELD(_r,_f) (((_r) >> (_f ##_S)) & (_f))
#define GPIO_FUNC40_IN_INV_SEL (BIT(6))
#define GPIO_SIG31_IN_SEL (BIT(7))
#define ETS_RWBLE_NMI_SOURCE 9
#define RTC_IO_PDAC1_DAC_S 19
#define LWIP_HOOK_TCP_ISN lwip_hook_tcp_isn
#define RTC_IO_PDAC1_DAC_V 0xFF
#define XCHAL_INT30_TYPE XTHAL_INTTYPE_EXTERN_EDGE
#define _ETS_SET_INTLEVEL(intlevel) ({ unsigned __tmp; __asm__ __volatile__( "rsil   %0, " _ETSTR(intlevel) "\n" : "=a" (__tmp) : : "memory" ); })
#define B11101001 233
#define SPIWP_IN_IDX 4
#define MEMP_NUM_API_MSG MEMP_NUM_TCPIP_MSG_API
#define GPIO_FUNC74_IN_INV_SEL_M (BIT(6))
// ...

 

Posted by Uli Köhler in Electronics, Embedded, PlatformIO

How to add preprocessor build flags in PlatformIO

In order to define a preprocessor build flag in PlatformIO, add it to build_flags in platformio.ini, prefixing it with -D:

build_flags = -DQUICKSPI_DEBUG_WRITES

or, in order to define it to a specific value, use

build_flags = -DQUICKSPI_DEBUG_WRITES=1

 

 

Posted by Uli Köhler in C/C++, PlatformIO

How to add FreeRTOS task (“thread”) to any PlatformIO project

Most PlatformIO default configurations already have FreeRTOS enabled – they just don’t use it.

In order to start a new FreeRTOS “thread” (called task in FreeRTOS-speak), first add these includes:

#include <freertos/FreeRTOS.h>
#include <freertos/task.h>

Now add the task function and handle:

TaskHandle_t myTaskHandle;
void MyTask( void * parameter )
{
    for(;;)
    {
       // TODO Task code goes here
    }
    // if you ever exit the loop, this is here to clean up the resources
    vTaskDelete( NULL );
}

then start the task using this code once, for example in your main function:

// Start MyTask thread
xTaskCreate(
    MyTask, // Task function
    "MyTask", // Name
    10000, // Stack size
    NULL, // Parameter
    1, // Priority
    &myTaskHandle);

 

Posted by Uli Köhler in C/C++, Electronics, Embedded, FreeRTOS, PlatformIO

How to add library dependency from local filesystem in PlatformIO

If you want to add a library dependency to lib_deps where the library is from the local file system, just add the full path to lib_deps

lib_deps =
    /home/user/MyCustomLib

This is really handy when developing libs since it allows you to just save changes in the source code and rebuild your main project without any need to publish and update the package first.

Note that you need to Clean and then Build or Upload in order to update the files from the local directory. PlatformIO will cache them unless you Clean and then rebuild!

Posted by Uli Köhler in PlatformIO

What is the value of portTICK_PERIOD_MS and configTICK_RATE_HZ on the ESP32 using PlatformIO?

When you use PlatformIO with the Arduino framework in its default configuration on the ESP32, configTICK_RATE_HZ is set to 1000. In other words, FreeRTOS has a default tick frequency of 1kHz. This is defined in sdkconfig.h:

#define CONFIG_FREERTOS_HZ 1000

Hence portTICK_PERIOD_MS is 1. In my opinion, a tick rate of 1kHz is a sane configuration for most usecases.

I found the value by using the following code on an ESP32:

Serial.println("Timing: ");
Serial.println(portTICK_PERIOD_MS);
Serial.println(configTICK_RATE_HZ);

 

Posted by Uli Köhler in Arduino, Electronics, Embedded, ESP8266/ESP32, FreeRTOS, PlatformIO

How to read 8-bit I2C register using Arduino Wire library: A minimal example

The following code demonstrates how to read a register that is 1 byte (8 bits) long over I2C. It will work with almost all I2C devices like EEPROMs, ADCs and others, provided you have the correct. Note that some devices like the LAN9303 have a slightly different addressing scheme or other peculiarities. In my opinion, it’s most efficient to just try out the standard way of reading a register and start from there.

Note that this code does not implement error handling for the sake of simplicity. Additionally, we wait for data using delay() instead of Wire.available(). This is a minimal example so it creates minimal confusion for the reader. We will provide a full example with error handling in a followup post.

const uint8_t SLAVE_I2C_ADDRESS = 0b1010;
const uint16_t SLAVE_I2C_REGISTER_ADDRESS = 0x50;

Wire.beginTransmission(SLAVE_I2C_ADDRESS);
Wire.write(SLAVE_I2C_REGISTER_ADDRESS);
Wire.endTransmission();
Wire.requestFrom(SLAVE_I2C_ADDRESS, 1); // This register is 8 bits = 1 byte long
delay(2); // Wait for data to be available
// Read directly into an uint8_t
uint8_t buf = (uint8_t)Wire.read();
// Print register value
Serial.printf("Register value: %02x\r\n", buf);

Also see:

Posted by Uli Köhler in Arduino, C/C++, Electronics, Embedded, PlatformIO

How to read 16-bit I2C register using Arduino Wire library: A minimal example

The following code demonstrates how to read a register that is 2 bytes (16 bits) long over I2C. It will work with almost all I2C devices like EEPROMs, ADCs and others, provided you have the correct. Note that some devices like the LAN9303 have a slightly different addressing scheme or other peculiarities. In my opinion, it’s most efficient to just try out the standard way of reading a register and start from there.

Note that this code does not implement error handling for the sake of simplicity. Additionally, we wait for data using delay() instead of Wire.available(). This is a minimal example so it creates minimal confusion for the reader. We will provide a full example with error handling in a followup post.

Option 1: Reading the register into an uint16_t (recommended)

const uint8_t SLAVE_I2C_ADDRESS = 0b1010;
const uint16_t SLAVE_I2C_REGISTER_ADDRESS = 0x50;

Wire.beginTransmission(SLAVE_I2C_ADDRESS);
Wire.write(SLAVE_I2C_REGISTER_ADDRESS);
Wire.endTransmission();
Wire.requestFrom(SLAVE_I2C_ADDRESS, 2); // This register is 16 bits = 2 bytes long
delay(5); // Wait for data to be available
// Read directly into an uint32_t
uint16_t buf;
Wire.readBytes((uint8_t*)&buf, 2);
// Print register value
Serial.printf("Register value: %04x\r\n", __builtin_bswap16(buf));

For an explanation on why we need __builtin_bswap16(), see How to print 16-bit uint16_t as four hex digits in Arduino

Option 2: Reading the register into an uint8_t array

const uint8_t SLAVE_I2C_ADDRESS = 0b1010;
const uint16_t SLAVE_I2C_REGISTER_ADDRESS = 0x50;

Wire.beginTransmission(SLAVE_I2C_ADDRESS);
Wire.write(SLAVE_I2C_REGISTER_ADDRESS);
Wire.endTransmission();
Wire.requestFrom(SLAVE_I2C_ADDRESS, 2); // This register is 16 bits = 2 bytes long
delay(5); // Wait for data to be available
// Read into a 2-byte buffer
uint8_t buf[2];
Wire.readBytes(buf, 2);
// Print register value
Serial.printf("Register value: %02x%02x\r\n", buf[0], buf[1]);

Also see:

Posted by Uli Köhler in Arduino, C/C++, Electronics, Embedded, PlatformIO