JF's note on PineDio devices

From PINE64
Jump to navigation Jump to search

USB LoRa adapter


The Pine64 USB LoRa adapter is based on the Semtech SX1262 LoRa module and the CH341 USB bus converter chip. The CH341 chip can be configured in multiple mode to convert USB to various serial and parallel ports. In this case, it's configured in synchronous serial mode, which allows this chip to convert from USB to the SPI bus needed to talk to the SX1262 LoRa module:

--------            --------------------------
|      |            |   USB LoRa Adapter     |
| PC   |  <--USB--> | CH341 <--SPI--> SX1262 |
|      |            |       <--I/O-->        |
--------            --------------------------



SX1262 Pin Name Description
1 ANT Antenna
2 GND2
6 DIO1 Connected on CH341 Pin 7
7 DIO2 Connected on CH341 Pin6
8 DIO3 Connected on CH341 Pin 5
12 SCK SPI clock
13 NSS SPI Chip select
14 POR Reset pin
15 Busy Busy pin
16 GND

Kernel module for CH341

We need a driver for the CH341 USB bus converter chip. This driver will allow us to send command to the CH341 (SPI messages and access to the GPIOs). I've succesfuly build and run this driver from rogerjames99 on my desktop computer (running Manjaro Linux) and on my Pinebook Pro (ARM64, running Manjaro ARM Linux).

$ git clone https://github.com/rogerjames99/spi-ch341-usb.git
$ cd spi-ch341-usb
$ make
$ sudo make install

Once the driver is installed, unload the module ch341 if it has been automatically loaded:

$ lsmod |grep ch341
$ sudo rmmod ch341

On my setup, ch341 would be automatically loaded once I connected my USB adapter. This module drives the CH341 is asynchronous serial mode, which will not work for a SPI bus.

Then load the new module:

$ sudo modprobe spi-ch341-usb

Plug your USB adapter and check that the module is correctly loaded :

$ dmesg
[20141.872107] usb 1-1.1: USB disconnect, device number 4
[20143.820756] usb 1-1.1: new full-speed USB device number 5 using ehci-platform
[20143.973366] usb 1-1.1: New USB device found, idVendor=1a86, idProduct=5512, bcdDevice= 3.04
[20143.973413] usb 1-1.1: New USB device strings: Mfr=0, Product=2, SerialNumber=0
[20143.973434] usb 1-1.1: Product: USB UART-LPT
[20143.975137] spi-ch341-usb 1-1.1:1.0: ch341_usb_probe: connect device
[20143.975164] spi-ch341-usb 1-1.1:1.0: ch341_usb_probe: bNumEndpoints=3
[20143.975183] spi-ch341-usb 1-1.1:1.0: ch341_usb_probe:   endpoint=0 type=2 dir=1 addr=2
[20143.975206] spi-ch341-usb 1-1.1:1.0: ch341_usb_probe:   endpoint=1 type=2 dir=0 addr=2
[20143.975229] spi-ch341-usb 1-1.1:1.0: ch341_usb_probe:   endpoint=2 type=3 dir=1 addr=1
[20143.975254] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: output cs0 SPI slave with cs=0
[20143.975273] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: output cs0 gpio=0 irq=0 
[20143.975295] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: output cs1 SPI slave with cs=1
[20143.975313] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: output cs1 gpio=1 irq=1 
[20143.975334] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: output cs2 SPI slave with cs=2
[20143.975352] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: output cs2 gpio=2 irq=2 
[20143.975373] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: input  gpio4 gpio=3 irq=3 
[20143.975394] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: input  gpio6 gpio=4 irq=4 
[20143.975415] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: input  err gpio=5 irq=5 
[20143.975435] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: input  pemp gpio=6 irq=6 
[20143.975456] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: input  int gpio=7 irq=7 (hwirq)
[20143.975478] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: input  slct gpio=8 irq=8 
[20143.975499] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: input  wait gpio=9 irq=9 
[20143.975520] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: input  autofd gpio=10 irq=10 
[20143.975542] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: input  addr gpio=11 irq=11 
[20143.975564] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: output ini gpio=12 irq=12 
[20143.975585] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: output write gpio=13 irq=13 
[20143.975607] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: output scl gpio=14 irq=14 
[20143.975628] spi-ch341-usb 1-1.1:1.0: ch341_cfg_probe: output sda gpio=15 irq=15 
[20143.975650] spi-ch341-usb 1-1.1:1.0: ch341_spi_probe: start
[20143.975677] spi-ch341-usb 1-1.1:1.0: ch341_spi_probe: SPI master connected to SPI bus 1
[20143.977831] spi-ch341-usb 1-1.1:1.0: ch341_spi_probe: SPI device /dev/spidev1.0 created
[20143.978183] spi-ch341-usb 1-1.1:1.0: ch341_spi_probe: SPI device /dev/spidev1.1 created
[20143.978552] spi-ch341-usb 1-1.1:1.0: ch341_spi_probe: SPI device /dev/spidev1.2 created
[20143.978726] spi-ch341-usb 1-1.1:1.0: ch341_spi_probe: done
[20143.978735] spi-ch341-usb 1-1.1:1.0: ch341_irq_probe: start
[20143.979133] spi-ch341-usb 1-1.1:1.0: ch341_irq_probe: irq_base=103
[20143.979154] spi-ch341-usb 1-1.1:1.0: ch341_irq_probe: done
[20143.979162] spi-ch341-usb 1-1.1:1.0: ch341_gpio_probe: start
[20143.979220] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=cs0 dir=0
[20143.979229] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=cs1 dir=0
[20143.979237] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=cs2 dir=0
[20143.979245] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=gpio4 dir=1
[20143.979253] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=gpio6 dir=1
[20143.979260] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=err dir=1
[20143.979268] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=pemp dir=1
[20143.979275] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=int dir=1
[20143.979283] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=slct dir=1
[20143.979290] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=wait dir=1
[20143.979298] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=autofd dir=1
[20143.979306] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=addr dir=1
[20143.979314] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=ini dir=0
[20143.979321] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=write dir=0
[20143.979329] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=scl dir=0
[20143.979337] spi-ch341-usb 1-1.1:1.0: ch341_gpio_get_direction: gpio=sda dir=0
[20143.979831] spi-ch341-usb 1-1.1:1.0: ch341_gpio_probe: registered GPIOs from 496 to 511
[20143.981152] spi-ch341-usb 1-1.1:1.0: ch341_gpio_probe: done
[20143.981212] spi-ch341-usb 1-1.1:1.0: ch341_gpio_poll_function: start
[20143.981291] spi-ch341-usb 1-1.1:1.0: ch341_usb_probe: connected
[20144.756250] usbcore: registered new interface driver ch341
[20144.756334] usbserial: USB Serial support registered for ch341-uart  

Driver development

Once the module spi-ch341-usb is correctly loaded, here's how you can transfer data on the SPI bus (in C):

/* Open the SPI bus */
int spi = open("/dev/spidev1.0", O_RDWR);
uint8_t mmode = SPI_MODE_0;
uint8_t lsb = 0;
ioctl(spi, SPI_IOC_WR_MODE, &mmode);
ioctl(spi, SPI_IOC_WR_LSB_FIRST, &lsb);

/* Transfer data */
/* TODO: Init buffer_out, buffer_in and size */
const uint8_t *mosi = buffer_out; // output data
uint8_t *miso = buffer_in; // input data

struct spi_ioc_transfer spi_trans;
memset(&spi_trans, 0, sizeof(spi_trans));

spi_trans.tx_buf = (unsigned long) mosi;
spi_trans.rx_buf = (unsigned long) miso;
spi_trans.cs_change = true;
spi_trans.len = size;

int status = ioctl (spi, SPI_IOC_MESSAGE(1), &spi_trans);

To access GPIOs, you first need to export them (to make them accessible via /sys/class/gpio. As you can see in the dmesg output, GPIOs from 496 to 511 were registered, which means we can export 16 GPIOs. The mapping of these I/O is available in the source code of the driver. For example, pin slct is the 12th, meaning we need to export GPIO 496+12 = 508.

int  fd;
if ((fd = open("/sys/class/gpio/export", O_WRONLY)) == -1)   {
  perror("open ini");

if (write(fd, "508", 3) == -1){
  perror ("write export 508");

Once exported, the GPIO is available in /sys/class/gpio/sclt (the naming is specified by the driver). You can read the pin in C:

int  fd;
if ((fd = open("/sys/class/gpio/slct/value", O_RDWR)) == -1)   {

char buf;
if (read(fd, &buf, 1) == -1) {

int value = (buf == '0') ? 0 : 1;

You can also write it:

int  fd; 
if ((fd = open("/sys/class/gpio/ini/value", O_RDWR)) == -1)   {
  perror("open ini");

if (write(fd, value ? "1" : "0", 1) == -1) {
   perror ("write");

Driver for the SX1262 LoRa module

Now that we can talk to the SX1262 via the CH341 USB converter chip, we need to send actual commands to make it emit or receive LoRa messages. To do this, you can implement the driver yourself using info from the datasheet, or use an existing driver (you can easily find drivers for the Arduino framework, for example. I found this C++ driver. It's well written, lightweight and easily portable across many platforms. All you have to do is implement 3 HAL function : read GPIO, write GPIO and transfer data on SPI. I wrote a quick'n'dirty app that emits a LoRa frame. It's available here.

As I don't have any 'raw' LoRa device on hands, I check that it was actually transmitting something using my SDR setup (simple TNT usb key and Gqrx software):



RAW LoRa communication between USB LoRa adapter and PineDio STACK

Test setup


  • One Pine64 USB LoRa adapter (SX1262 + CH341)
  • One PineDio STACK (BL604 + SX1262)
  • One PyCom Lopy board (dev board based on ESP32 + LoRa module, MicroPython)
  • One NooElec SDR adapter and GQRX running on my PC

The goal is to implement RAW LoRa communication between Pine64 LoRa devices (USB adapter + PineDio STACK at first, pinephone adapter later on). The Lopy board allows me to debug my setup easily, as I know it's working correctly in both RX and TX.

I'm writing this section to share my experiments with RAW LoRa on those devices and maybe get some help on some issues. I'm a noob in RF and LoRa technologies !

RX on Pine64 devices

Setup : The Lopy board emits 16 Bytes every 5s

Using default TX settings on the Lopy (868Mhz, BW 125Khz, SF7, 8 preamble symbols, CR 4/5, IQ not inverted), the reception on both Pine64 devices was really poor : they would detect the preamble most of the time, but fail to check the CRC of the header or of the payload.

Then I change those settings : 868Mhz, BW 500Khz, SF12, 8 preamble symbols, CR 4/5, IQ inverted. Magically, the reception on both device was perfect!

Question : why the RX was not working with original settings and would work with the new ones?

TX from Pine64 devices

Using the same TX parameters as the previous test : 868Mhz, BW 500Khz, SF12, 8 preamble symbols, CR 4/5, IQ inverted

TX from the USB LoRa Adapter

On the SDR acquisition, the TX power seems lower than the Lopy board, even with TX power set to +22dB


The Lopy does not receive anything. The STACK detects the message but detects a bad CRC. Indeed, the messages is scrambled...


The RSSI and SNR are displayed. I notice that the RSSI is much lower for messages coming from the USB adapter than for messages coming from the Lopy

Questions :

- Is it normal that the power of the TX from the USB adapter looks much lower than the power from the lopy?
- What about the RSSI that is much lower on messages comng from the USB adapter?
- Why are the messages and CRC corrupted ?

TX from the PineDio STACK

On the SDR acquisition, the signal power looks similar than the Lopy.


The Pine64 USB adapter receives the message but detects a bad CRC.


The Lopy board does not receive anything (probably because of messages with bad CRC that are dropped in lower level layers).

Questions :

- Why are the messages corrupted?



Based on this driver: https://github.com/lupyuen/bl_iot_sdk/blob/pinedio/components/3rdparty/lora-sx1262/src/radio.c

  RadioEvents_t RadioEvents;

  RadioEvents.RxDone = OnRadioRxDone;
  RadioEvents.RxError = OnRadioRxError;
  RadioEvents.TxDone = OnRadioTxDone;

          2, //500khz
          12, //SF12
          1, // CR 4/5
          0, // N/A
          8, // preamble
          7, // symbol timeout
          0, // dynamic length
          0, // payload length
          1, // crc enabled
          0, // freq hoping disabled
          0, // hop period
          1, // no iq inversion
          1// continous receive
          0, // power
          0, //N/A
          2, // 500 khz
          12, // SF12
          1, // CR4/5
          8, // preamble
          false, // dynamic length
          true, // CRC ON
          false, // hop
          0, // hop period
          true, // invert iq
          0xffffffff // timeout
  // RX
  while(1) {

  // OR TX
  const char msg[] = {"Hello, I'm a PineDio STACK!"};

  int i = 0;
  while(1) {
    if((i % 30) ==0){
      Radio.Send((uint8_t *) msg, strlen(msg));

USB adapter

Based on this driver: https://github.com/YukiWorkshop/sx126x_driver


  radio.SetBufferBaseAddresses(0, 0);

  SX126x::ModulationParams_t modulationParams;
  modulationParams.PacketType = SX126x::RadioPacketTypes_t::PACKET_TYPE_LORA;
  modulationParams.Params.LoRa.SpreadingFactor = SX126x::RadioLoRaSpreadingFactors_t::LORA_SF12;
  modulationParams.Params.LoRa.CodingRate = SX126x::RadioLoRaCodingRates_t::LORA_CR_4_5;
  modulationParams.Params.LoRa.Bandwidth = SX126x::RadioLoRaBandwidths_t::LORA_BW_500;
  modulationParams.Params.LoRa.LowDatarateOptimize = false;

  SX126x::PacketParams_t packetParams;
  packetParams.PacketType = SX126x::RadioPacketTypes_t::PACKET_TYPE_LORA;
  packetParams.Params.LoRa.PreambleLength = 8;
  packetParams.Params.LoRa.HeaderType = SX126x::RadioLoRaPacketLengthsMode_t::LORA_PACKET_VARIABLE_LENGTH;
  packetParams.Params.LoRa.PayloadLength = 27;
  packetParams.Params.LoRa.CrcMode = SX126x::RadioLoRaCrcModes_t::LORA_CRC_ON;
  packetParams.Params.LoRa.InvertIQ = SX126x::RadioLoRaIQModes_t::LORA_IQ_INVERTED;

  radio.SetTxParams(22, SX126x::RADIO_RAMP_3400_US);

  radio.SetDioIrqParams(0xffff, 0x0001, 0, 0);
// RX

  while (1) {

// OR TX
  while (1) {
    radio.SetPayload(data, 27);