Showing posts with label CubeSat. Show all posts
Showing posts with label CubeSat. Show all posts

Friday, October 16, 2026

Komunikasi CubeSat Space Protocol (libcsp) lewat CAN Bus FDCAN1 pada STM32H743 dengan FreeRTOS

Assalamu'alaikum warohmatulohi wabarakatuh,

Pada tutorial sebelumnya, Implementasi FreeRTOS pada STM32H7 menggunakan STM32CubeIDE, kita sudah menjalankan FreeRTOS di STM32H743VIT6 lengkap dengan demo multitasking. Kali ini kita lanjutkan ke topik yang banyak dipakai di dunia satelit kecil: CubeSat Space Protocol (CSP) memakai library libcsp, dengan jalur komunikasi CAN bus lewat periferal FDCAN1 (PA11/PA12).

Semua kode di tutorial ini sudah saya build di STM32CubeIDE dan saya jalankan di board DevEBox STM32H7XX_M (STM32H743VIT6). Hasil log-nya bisa dilihat di bagian akhir.

Apa itu CubeSat Space Protocol (CSP)?

CSP adalah protokol jaringan kecil yang dirancang untuk komunikasi antar subsistem di dalam satelit kecil (CubeSat), misalnya antara komputer utama (OBC), sistem daya (EPS), kontrol sikap (ADCS) dan radio. Idenya dikembangkan oleh mahasiswa Aalborg University pada tahun 2008, lalu sejak 2009 dikembangkan di GomSpace dan sekarang tersedia sebagai library open source libcsp.

Konsepnya mirip TCP/IP, tetapi jauh lebih ringan. Setiap paket CSP hanya punya header 32-bit, sehingga cocok untuk mikrokontroler. Setiap node punya alamat, setiap layanan punya port, dan paket bisa dikirim lewat berbagai jalur fisik seperti CAN, I2C, UART (KISS) atau ZeroMQ, tanpa mengubah kode aplikasi.

Struktur header CSP versi 1.x:

FieldUkuranKeterangan
Priority2 bit0 = critical, 1 = high, 2 = normal, 3 = low
Source5 bitalamat pengirim (0..31)
Destination5 bitalamat tujuan (31 = broadcast)
Destination port6 bitport layanan tujuan (0..63)
Source port6 bitport pengirim (dipakai untuk balasan)
Flags8 bitopsi seperti CRC32, HMAC, XTEA, RDP

Port 0 sampai 6 sudah dipakai oleh service bawaan CSP, sehingga setiap node otomatis bisa di-ping dan ditanya statusnya:

PortService
0CMP (manajemen: identitas, routing, statistik)
1Ping
2Daftar task (process list)
3Sisa memori
4Reboot
5Sisa buffer CSP
6Uptime

Library yang dipakai di tutorial ini adalah libcsp versi 1.6 (lisensi LGPL 2.1), versi yang banyak dipakai di project CubeSat berbasis FreeRTOS.

CSP lewat CAN: CAN Fragmentation Protocol (CFP)

Satu frame CAN klasik hanya membawa maksimal 8 byte data, padahal paket CSP bisa ratusan byte. Karena itu libcsp memakai CAN Fragmentation Protocol (CFP) yang memecah satu paket CSP menjadi beberapa frame CAN, lalu menyusunnya kembali di penerima. Informasi fragmentasi disimpan di ID CAN extended 29-bit:

Format ID CAN 29-bit CFP pada libcsp

Pembagian 29 bit ID CAN pada CFP

Hal penting yang sering terlewat: CFP wajib memakai ID extended 29-bit. Kalau frame dikirim dengan ID standar 11-bit, field alamat dan nomor fragmen akan terpotong dan paket tidak bisa disusun kembali di penerima.

Arsitektur dan hardware

Arsitektur libcsp FreeRTOS FDCAN STM32H743

Lapisan software dan sambungan hardware CAN

  • FDCAN1 STM32H743 memakai pin PA12 (TX) dan PA11 (RX). Pin ini level logika 3,3 V, jadi tidak boleh langsung disambung ke kabel bus CAN. Harus lewat transceiver CAN, misalnya SN65HVD230 (3,3 V).
  • Bus CAN (CAN_H dan CAN_L) diberi resistor terminasi 120 Ω di kedua ujung.
  • Untuk belajar dengan satu board saja, FDCAN bisa dipasang di mode internal loopback: frame yang dikirim langsung diterima kembali di dalam chip, tanpa transceiver. Mode inilah yang saya pakai untuk pengujian di tutorial ini.
  • Log ditampilkan lewat USART1 (PA9/PA10, 115200 baud) ke USB-UART WeAct Mini Debugger, sama seperti tutorial sebelumnya.

Langkah 1 – Konfigurasi FDCAN1 di STM32CubeIDE

Project ini melanjutkan project FreeRTOS dari tutorial sebelumnya (clock 75 MHz dari HSI, timebase TIM1, FreeRTOS CMSIS_V2, LED PA1, USART1). Tambahkan FDCAN1:

  1. Buka Connectivity → FDCAN1, centang Activated. Pin otomatis menjadi PA11 = FDCAN1_RX dan PA12 = FDCAN1_TX.
  2. Di Clock Configuration, sumber clock FDCAN adalah PLL1Q = 50 MHz (DIVQ1 = 3).
  3. Di Parameter Settings isi nilai berikut untuk kecepatan 1 Mbit/s:
ParameterNilaiKeterangan
Frame FormatClassic modeCAN 2.0B (bukan CAN FD)
ModeNormaldi kode diganti ke internal loopback untuk uji 1 board
Nominal Prescaler550 MHz / 5 = 10 MHz → 1 tq = 100 ns
Nominal Sync Jump Width2
Nominal Time Seg17
Nominal Time Seg221 bit = 1 + 7 + 2 = 10 tq = 1 µs → 1 Mbit/s, sample point 80%
Auto RetransmissionEnable
Ext Filters Nbr2filter alamat sendiri + broadcast
Rx Fifo0 Elmts Nbr32antrian frame masuk
Tx Fifo Queue Elmts Nbr32antrian frame keluar
  1. Di tab NVIC Settings, aktifkan FDCAN1 interrupt 0 dengan prioritas 5. Prioritas harus ≥ 5 karena interrupt ini memanggil fungsi FreeRTOS (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY).
  2. Di Middleware → FREERTOS → Config parameters, aktifkan USE_STATS_FORMATTING_FUNCTIONS. Fungsi ini dipakai libcsp untuk service "process list".

Kode inisialisasi FDCAN1 yang dihasilkan (dengan tambahan mode loopback di blok USER CODE):

  hfdcan1.Instance = FDCAN1;
  hfdcan1.Init.FrameFormat = FDCAN_FRAME_CLASSIC;
  hfdcan1.Init.Mode = FDCAN_MODE_NORMAL;
  hfdcan1.Init.AutoRetransmission = ENABLE;
  hfdcan1.Init.TransmitPause = DISABLE;
  hfdcan1.Init.ProtocolException = DISABLE;
  hfdcan1.Init.NominalPrescaler = 5;
  hfdcan1.Init.NominalSyncJumpWidth = 2;
  hfdcan1.Init.NominalTimeSeg1 = 7;
  hfdcan1.Init.NominalTimeSeg2 = 2;
  hfdcan1.Init.DataPrescaler = 1;
  hfdcan1.Init.DataSyncJumpWidth = 1;
  hfdcan1.Init.DataTimeSeg1 = 1;
  hfdcan1.Init.DataTimeSeg2 = 1;
  hfdcan1.Init.MessageRAMOffset = 0;
  hfdcan1.Init.StdFiltersNbr = 0;
  hfdcan1.Init.ExtFiltersNbr = 2;
  hfdcan1.Init.RxFifo0ElmtsNbr = 32;
  hfdcan1.Init.RxFifo0ElmtSize = FDCAN_DATA_BYTES_8;
  hfdcan1.Init.RxFifo1ElmtsNbr = 0;
  hfdcan1.Init.RxFifo1ElmtSize = FDCAN_DATA_BYTES_8;
  hfdcan1.Init.RxBuffersNbr = 0;
  hfdcan1.Init.RxBufferSize = FDCAN_DATA_BYTES_8;
  hfdcan1.Init.TxEventsNbr = 0;
  hfdcan1.Init.TxBuffersNbr = 0;
  hfdcan1.Init.TxFifoQueueElmtsNbr = 32;
  hfdcan1.Init.TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION;
  hfdcan1.Init.TxElmtSize = FDCAN_DATA_BYTES_8;
  /* USER CODE BEGIN FDCAN1_Init 2 */
#if CSP_CAN_LOOPBACK
  /* Uji tanpa transceiver: frame TX langsung diterima kembali di dalam chip */
  hfdcan1.Init.Mode = FDCAN_MODE_INTERNAL_LOOPBACK;
#endif
  /* USER CODE END FDCAN1_Init 2 */

Langkah 2 – Menambahkan libcsp 1.6 ke project

  1. Download source libcsp dari GitHub github.com/libcsp/libcsp, pilih tag v1.6.
  2. Buat folder Middlewares/Third_Party/libcsp di project, lalu salin folder include dan file-file di src berikut:
    • src/*.c dan src/*.h (inti CSP: buffer, connection, router, io, service, dll)
    • src/arch/*.c dan src/arch/freertos/*.c (port untuk FreeRTOS)
    • src/interfaces/csp_if_can.c, csp_if_can_pbuf.c/.h, csp_if_lo.c
    • src/rtable/csp_rtable.c, csp_rtable_static.c, csp_rtable_internal.h
    • src/transport/*.c dan *.h
  3. Tambahkan include path di Project → Properties → C/C++ Build → Settings → MCU GCC Compiler → Include paths:
    ../Middlewares/Third_Party/libcsp/include dan ../Middlewares/Third_Party/libcsp/src
  4. libcsp aslinya memakai build system waf yang membuat file csp_autoconfig.h secara otomatis. Karena kita memakai STM32CubeIDE, file ini dibuat manual di include/csp/csp_autoconfig.h:
/*
 * csp_autoconfig.h - konfigurasi libcsp 1.6 untuk STM32H743 + FreeRTOS
 *
 * Pada libcsp asli, file ini dibuat otomatis oleh script build "waf".
 * Karena kita memakai STM32CubeIDE (tanpa waf), file ini ditulis manual.
 */
#ifndef CSP_AUTOCONFIG_H
#define CSP_AUTOCONFIG_H

#define GIT_REV                 "v1.6"
#define LIBCSP_VERSION          "1.6"

/* Sistem operasi */
#define CSP_FREERTOS            1

/* Fitur */
#define CSP_DEBUG               1   /* aktifkan log internal CSP (diarahkan ke UART) */
#define CSP_DEBUG_TIMESTAMP     0
#define CSP_USE_RDP             0   /* reliable datagram protocol: tidak dipakai     */
#define CSP_USE_RDP_FAST_CLOSE  0
#define CSP_USE_CRC32           1   /* CRC32 opsional per paket                      */
#define CSP_USE_HMAC            0
#define CSP_USE_XTEA            0
#define CSP_USE_PROMISC         0
#define CSP_USE_QOS             0
#define CSP_USE_DEDUP           0
#define CSP_USE_EXTERNAL_DEBUG  0

/* Level log yang dikompilasi */
#define CSP_LOG_LEVEL_DEBUG     0
#define CSP_LOG_LEVEL_INFO      1
#define CSP_LOG_LEVEL_WARN      1
#define CSP_LOG_LEVEL_ERROR     1

/* Cortex-M7 = little endian */
#define CSP_LITTLE_ENDIAN       1

#endif /* CSP_AUTOCONFIG_H */

Langkah 3 – Driver CAN untuk libcsp (csp_can_stm32.c)

libcsp tidak tahu cara mengakses hardware CAN. Kita perlu menulis driver kecil yang menghubungkan libcsp dengan HAL FDCAN. Driver ini saya letakkan di Core/Src/csp_can_stm32.c dan terdiri dari tiga bagian.

a. Fungsi kirim frame. Dipanggil libcsp untuk setiap fragmen CFP. Perhatikan FDCAN_EXTENDED_ID, dan bahwa pada HAL STM32H7 versi ini nilai DataLength sama dengan jumlah byte (0..8):

static int can_tx_frame(void *driver_data, uint32_t id, const uint8_t *data, uint8_t dlc)
{
    FDCAN_HandleTypeDef *hfdcan = (FDCAN_HandleTypeDef *)driver_data;
    FDCAN_TxHeaderTypeDef header = {
        .Identifier          = id,                    /* ID CFP 29-bit           */
        .IdType              = FDCAN_EXTENDED_ID,     /* wajib extended untuk CFP */
        .TxFrameType         = FDCAN_DATA_FRAME,
        .DataLength          = dlc,                   /* 0..8 (FDCAN_DLC_BYTES_x) */
        .ErrorStateIndicator = FDCAN_ESI_ACTIVE,
        .BitRateSwitch       = FDCAN_BRS_OFF,
        .FDFormat            = FDCAN_CLASSIC_CAN,
        .TxEventFifoControl  = FDCAN_NO_TX_EVENTS,
        .MessageMarker       = 0,
    };

    if (dlc > 8) {
        return CSP_ERR_INVAL;
    }

    /* Tunggu jika TX FIFO penuh (paket panjang = banyak frame), maks ~10 ms */
    for (int i = 0; HAL_FDCAN_GetTxFifoFreeLevel(hfdcan) == 0U; i++) {
        if (i >= 10) {
            csp_can_stats.tx_errors++;
            return CSP_ERR_TIMEDOUT;
        }
        vTaskDelay(1);
    }

    if (HAL_FDCAN_AddMessageToTxFifoQ(hfdcan, &header, (uint8_t *)data) != HAL_OK) {
        csp_can_stats.tx_errors++;
        return CSP_ERR_DRIVER;
    }

    csp_can_stats.tx_frames++;
    return CSP_ERR_NONE;
}

b. Inisialisasi. Mendaftarkan interface ke libcsp dan memasang filter hardware supaya FDCAN hanya menerima frame yang field DST-nya sama dengan alamat kita atau alamat broadcast:

/* Data interface CAN untuk libcsp */
static csp_can_interface_data_t can_ifdata = {
    .tx_func = can_tx_frame,
};

csp_iface_t csp_if_can_stm32 = {
    .name           = CSP_IF_CAN_DEFAULT_NAME,   /* "CAN" */
    .interface_data = &can_ifdata,
};

int csp_can_stm32_init(FDCAN_HandleTypeDef *hfdcan, uint8_t address)
{
    FDCAN_FilterTypeDef filter = {0};

    can_handle = hfdcan;
    csp_if_can_stm32.driver_data = hfdcan;

    /* Filter 0: terima frame extended yang field DST = alamat kita */
    filter.IdType       = FDCAN_EXTENDED_ID;
    filter.FilterIndex  = 0;
    filter.FilterType   = FDCAN_FILTER_MASK;
    filter.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
    filter.FilterID1    = CFP_MAKE_DST(address);   /* nilai yang dicari */
    filter.FilterID2    = CFP_MAKE_DST(0x1F);      /* mask: hanya bit DST */
    if (HAL_FDCAN_ConfigFilter(hfdcan, &filter) != HAL_OK) {
        return CSP_ERR_DRIVER;
    }

    /* Filter 1: terima juga alamat broadcast CSP (31) */
    filter.FilterIndex  = 1;
    filter.FilterID1    = CFP_MAKE_DST(CSP_BROADCAST_ADDR);
    if (HAL_FDCAN_ConfigFilter(hfdcan, &filter) != HAL_OK) {
        return CSP_ERR_DRIVER;
    }

    /* Frame lain (standard 11-bit / alamat lain) ditolak oleh hardware */
    if (HAL_FDCAN_ConfigGlobalFilter(hfdcan, FDCAN_REJECT, FDCAN_REJECT,
                                     FDCAN_REJECT_REMOTE, FDCAN_REJECT_REMOTE) != HAL_OK) {
        return CSP_ERR_DRIVER;
    }

    if (HAL_FDCAN_ActivateNotification(hfdcan, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0) != HAL_OK) {
        return CSP_ERR_DRIVER;
    }

    if (HAL_FDCAN_Start(hfdcan) != HAL_OK) {
        return CSP_ERR_DRIVER;
    }

    /* Daftarkan interface ke libcsp (mengisi nexthop = csp_can_tx) */
    return csp_can_add_interface(&csp_if_can_stm32);
}

c. Penerimaan lewat interrupt. Setiap frame yang masuk diteruskan ke csp_can_rx(), yang menyusun fragmen menjadi paket CSP lalu menyerahkannya ke router:

void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs)
{
    BaseType_t woken = pdFALSE;
    FDCAN_RxHeaderTypeDef header;
    uint8_t data[8];

    if (hfdcan != can_handle || (RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE) == 0U) {
        return;
    }

    /* Ambil semua frame yang ada di FIFO */
    while (HAL_FDCAN_GetRxFifoFillLevel(hfdcan, FDCAN_RX_FIFO0) > 0U) {
        if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &header, data) != HAL_OK) {
            break;
        }
        if (header.IdType != FDCAN_EXTENDED_ID || header.DataLength > FDCAN_DLC_BYTES_8) {
            continue;   /* bukan frame CFP */
        }
        csp_can_stats.rx_frames++;
        /* Serahkan ke libcsp: menyusun fragmen jadi paket CSP */
        csp_can_rx(&csp_if_can_stm32, header.Identifier, data, (uint8_t)header.DataLength, &woken);
    }

    portYIELD_FROM_ISR(woken);
}

Tambahkan juga handler interrupt di stm32h7xx_it.c:

void FDCAN1_IT0_IRQHandler(void)
{
  HAL_FDCAN_IRQHandler(&hfdcan1);
}

Langkah 4 – Inisialisasi CSP dan routing

Di main.c kita tentukan alamat node, port yang dipakai, dan mode uji:

#define CSP_CAN_LOOPBACK   1        /* 1 = uji 1 board (internal loopback)      */

#define CSP_MY_ADDR        1        /* alamat CSP board ini (0..30)             */
#if CSP_CAN_LOOPBACK
#define CSP_SERVER_ADDR    CSP_MY_ADDR  /* loopback: server = diri sendiri      */
#else
#define CSP_SERVER_ADDR    2        /* alamat node lain di bus CAN              */
#endif

#define CSP_PORT_ECHO      10       /* port request-reply                       */
#define CSP_PORT_TLM       11       /* port telemetri (paket panjang)           */
#define TLM_SIZE           200      /* ukuran paket telemetri (byte)            */

Fungsi csp_setup() menginisialisasi libcsp, menghubungkan FDCAN1, mengatur tabel routing, lalu menjalankan task router milik CSP:

/* Inisialisasi libcsp + interface CAN + tabel routing */
static int csp_setup(void)
{
  csp_conf_t conf;
  csp_conf_get_defaults(&conf);
  conf.address           = CSP_MY_ADDR;
  conf.hostname          = "STM32H743";
  conf.model             = "DevEBox-H7";
  conf.revision          = "1.0";
  conf.buffers           = 20;    /* jumlah buffer paket CSP */
  conf.buffer_data_size  = 256;   /* ukuran data per paket   */
  conf.conn_max          = 8;
  conf.conn_queue_length = 10;
  conf.fifo_length       = 20;
  conf.port_max_bind     = 16;

  csp_debug_hook_set(csp_debug_to_uart);

  if (csp_init(&conf) != CSP_ERR_NONE) {
    log_printf("[INIT   ] csp_init gagal\r\n");
    return -1;
  }

  /* Hubungkan FDCAN1 ke libcsp (filter alamat + daftar interface "CAN") */
  if (csp_can_stm32_init(&hfdcan1, CSP_MY_ADDR) != CSP_ERR_NONE) {
    log_printf("[INIT   ] inisialisasi CAN gagal\r\n");
    return -1;
  }

  /* Routing: semua alamat tujuan lewat CAN */
  csp_rtable_set(CSP_DEFAULT_ROUTE, 0, &csp_if_can_stm32, CSP_NO_VIA_ADDRESS);
#if CSP_CAN_LOOPBACK
  /* Uji 1 board: paket ke alamat sendiri juga dipaksa lewat CAN
     (normalnya lewat interface "LOOP" software) */
  csp_rtable_set(CSP_MY_ADDR, CSP_ID_HOST_SIZE, &csp_if_can_stm32, CSP_NO_VIA_ADDRESS);
#endif

  /* Task router CSP: stack 1024 word, prioritas FreeRTOS 40 (= osPriorityHigh) */
  if (csp_route_start_task(1024, osPriorityHigh) != CSP_ERR_NONE) {
    return -1;
  }
  return 0;
}

csp_setup() dipanggil dari defaultTask (setelah scheduler berjalan), lalu task server dan client dibuat:

void StartDefaultTask(void *argument)
{
  /* USER CODE BEGIN 5 */
  log_printf("\r\n===== FreeRTOS + libcsp %s lewat FDCAN1 (STM32H743) =====\r\n", LIBCSP_VERSION);
  log_printf("[INIT   ] alamat CSP=%u, CAN 1 Mbit/s, mode %s\r\n", CSP_MY_ADDR,
             CSP_CAN_LOOPBACK ? "INTERNAL LOOPBACK" : "NORMAL");

  if (csp_setup() == 0) {
    log_printf("[INIT   ] libcsp siap, interface CAN terdaftar\r\n");
    cspServerTaskHandle = osThreadNew(StartCspServerTask, NULL, &cspServerTask_attributes);
    cspClientTaskHandle = osThreadNew(StartCspClientTask, NULL, &cspClientTask_attributes);
  }

  /* Infinite loop */
  for(;;)
  {
    HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);    // LED PA1 nyala
    osDelay(1000);
    HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);  // LED PA1 mati
    osDelay(2000);
  }
  /* USER CODE END 5 */
}

Langkah 5 – Server dan client CSP

Server mendengarkan semua port. Port 10 dibalas dengan pesan ACK, port 11 menerima telemetri, dan port lain (ping, uptime, dll) diserahkan ke csp_service_handler():

void StartCspServerTask(void *argument)
{
  csp_socket_t *sock = csp_socket(CSP_SO_NONE);
  csp_bind(sock, CSP_ANY);          /* terima semua port */
  csp_listen(sock, 10);

  for (;;)
  {
    csp_conn_t *conn = csp_accept(sock, 10000);   /* tunggu koneksi baru */
    if (conn == NULL) {
      continue;
    }

    csp_packet_t *packet;
    while ((packet = csp_read(conn, 50)) != NULL)
    {
      switch (csp_conn_dport(conn))
      {
        case CSP_PORT_ECHO:
        {
          packet->data[packet->length < 200 ? packet->length : 199] = '\0';
          log_printf("[SERVER ] port %d dari node %u: \"%s\"\r\n",
                     CSP_PORT_ECHO, packet->id.src, (char *)packet->data);

          /* Pakai ulang buffer yang sama untuk balasan */
          int n = snprintf((char *)packet->data, 200, "ACK dari node %u: %s",
                           CSP_MY_ADDR, "pesan diterima");
          packet->length = (uint16_t)n;
          if (!csp_send(conn, packet, 0)) {
            csp_buffer_free(packet);   /* gagal kirim -> buffer milik kita */
          }
          break;
        }

        case CSP_PORT_TLM:
        {
          uint32_t sum = 0;
          for (uint16_t i = 0; i < packet->length; i++) sum += packet->data[i];
          log_printf("[SERVER ] port %d: telemetri %u byte diterima, checksum=%lu\r\n",
                     CSP_PORT_TLM, packet->length, (unsigned long)sum);
          csp_buffer_free(packet);
          break;
        }

        default:
          /* Ping, uptime, dll ditangani libcsp (juga membebaskan buffer) */
          csp_service_handler(conn, packet);
          break;
      }
    }
    csp_close(conn);
  }
}

Client setiap 3 detik melakukan tiga hal: ping, request-reply ke port 10, dan mengirim telemetri 200 byte ke port 11:

void StartCspClientTask(void *argument)
{
  uint32_t n = 0;
  osDelay(500);

  for (;;)
  {
    uint32_t tx0 = csp_can_stats.tx_frames;

    /* 1) Ping (service CSP port 1), payload 32 byte */
    int rtt = csp_ping(CSP_SERVER_ADDR, 1000, 32, CSP_O_NONE);
    if (rtt >= 0) {
      log_printf("[CLIENT ] ping node %u (32 byte): %d ms\r\n", CSP_SERVER_ADDR, rtt);
    } else {
      log_printf("[CLIENT ] ping node %u: timeout\r\n", CSP_SERVER_ADDR);
    }

    /* 2) Request-reply ke port 10 */
    char req[48];
    char resp[96] = {0};
    int len = snprintf(req, sizeof(req), "Halo OBC, pesan ke-%lu", (unsigned long)n);
    int r = csp_transaction(CSP_PRIO_NORM, CSP_SERVER_ADDR, CSP_PORT_ECHO, 1000,
                            req, len, resp, -1);
    if (r > 0) {
      resp[r < (int)sizeof(resp) ? r : (int)sizeof(resp) - 1] = '\0';
      log_printf("[CLIENT ] balasan: \"%s\"\r\n", resp);
    } else {
      log_printf("[CLIENT ] transaksi port %d gagal\r\n", CSP_PORT_ECHO);
    }

    /* 3) Telemetri 200 byte ke port 11 -> dipecah jadi banyak frame CAN */
    csp_conn_t *conn = csp_connect(CSP_PRIO_NORM, CSP_SERVER_ADDR, CSP_PORT_TLM, 1000, CSP_O_NONE);
    if (conn != NULL) {
      csp_packet_t *packet = csp_buffer_get(TLM_SIZE);
      if (packet != NULL) {
        uint32_t sum = 0;
        for (int i = 0; i < TLM_SIZE; i++) {
          packet->data[i] = (uint8_t)(i + n);
          sum += packet->data[i];
        }
        packet->length = TLM_SIZE;
        if (csp_send(conn, packet, 0)) {
          log_printf("[CLIENT ] kirim telemetri %d byte, checksum=%lu\r\n", TLM_SIZE, (unsigned long)sum);
        } else {
          csp_buffer_free(packet);   /* hanya di-free kalau gagal kirim */
        }
      }
      csp_close(conn);
    }

    osDelay(300);
    log_printf("[STAT   ] frame CAN TX=%lu (+%lu)  RX=%lu  error=%lu  buffer CSP bebas=%d\r\n",
               (unsigned long)csp_can_stats.tx_frames,
               (unsigned long)(csp_can_stats.tx_frames - tx0),
               (unsigned long)csp_can_stats.rx_frames,
               (unsigned long)csp_can_stats.tx_errors,
               csp_buffer_remaining());
    n++;
    osDelay(2700);
  }
}

Perhatikan aturan kepemilikan buffer CSP: setelah csp_send() berhasil, buffer menjadi milik libcsp dan akan dibebaskan oleh libcsp. Kita hanya memanggil csp_buffer_free() kalau csp_send() gagal. Kalau buffer di-free dua kali, sistem bisa crash secara acak.

Langkah 6 – Build, upload dan hasil

Build project dengan Project → Build Project:

Build project FreeRTOS libcsp STM32CubeIDE

Build selesai tanpa error (warning RWX dari linker bisa diabaikan)

Lalu upload dengan Run → Run As → STM32 C/C++ Application, dan buka serial monitor (115200 baud) di port COM Mini Debugger. Hasil di board saya:

Log CSP over CAN STM32H743 serial monitor

Log CSP lewat FDCAN1 (mode internal loopback)

Penjelasan log:

  • ping node 1 (32 byte): 1 ms: paket ping pergi lewat CAN, dijawab service ping, dan balasannya kembali lewat CAN dalam waktu sekitar 1 ms.
  • [SERVER] port 10 dan [CLIENT] balasan: request-reply berhasil dengan csp_transaction().
  • telemetri 200 byte: checksum yang dikirim client sama dengan yang diterima server, artinya 26 frame CAN berhasil disusun kembali tanpa kesalahan.
  • [STAT] frame CAN TX=45 (+45): setiap siklus memakai 45 frame CAN, yaitu ping 5 + 5 frame, request 4 + balasan 5 frame, dan telemetri 26 frame. Jumlah RX sama dengan TX, error = 0, dan buffer CSP kembali utuh (20) setelah setiap siklus, tanda tidak ada kebocoran buffer.

Menghubungkan dua board (mode normal)

Untuk komunikasi sungguhan antar dua board (misalnya OBC dan modul lain):

  1. Ubah #define CSP_CAN_LOOPBACK 0.
  2. Beri alamat CSP berbeda di setiap board, misalnya board A CSP_MY_ADDR 1 dengan CSP_SERVER_ADDR 2, dan board B CSP_MY_ADDR 2.
  3. Pasang transceiver CAN di setiap board: PA12 → TXD/CTX, PA11 ← RXD/CRX, 3,3 V dan GND.
  4. Sambungkan CAN_H ke CAN_H dan CAN_L ke CAN_L, lalu pasang resistor 120 Ω di kedua ujung bus.
  5. Pastikan semua node memakai kecepatan yang sama (1 Mbit/s).

Kesalahan yang sering terjadi

MasalahPenyebabSolusi
Paket tidak pernah sampai / tersusunFrame dikirim dengan ID standar 11-bitPakai FDCAN_EXTENDED_ID, CFP butuh 29-bit
Panjang data salahNilai DataLength dihitung dengan geser bit (format HAL lama)Di HAL STM32H7 ini, FDCAN_DLC_BYTES_8 = 8, jadi pakai langsung jumlah byte
Crash acak / HardFaultBuffer CSP di-free dua kali, atau di-free setelah csp_send() berhasilFree hanya saat csp_send() gagal
Heap habis perlahanmalloc() di setiap pengiriman tanpa free()Gunakan buffer CSP (csp_buffer_get) saja
Program berhenti di configASSERTPrioritas interrupt FDCAN lebih tinggi dari 5Set prioritas FDCAN1_IT0 ≥ 5
Tidak ada frame di bus (mode normal)Tanpa transceiver / tanpa terminasi / bitrate bedaCek transceiver, resistor 120 Ω, dan bitrate semua node

Penutup

Dengan FreeRTOS dan libcsp, STM32H743 bisa menjadi node CSP yang lengkap: punya alamat, melayani ping dan service bawaan, bisa melakukan request-reply, dan mengirim paket panjang yang otomatis dipecah menjadi frame CAN. Karena kode aplikasi hanya berurusan dengan alamat dan port, jalur fisiknya nanti bisa diganti (misalnya ke I2C atau UART) tanpa mengubah logika program.

Langkah berikutnya, kita bisa mencoba komunikasi dua board lewat transceiver CAN sungguhan. InsyaAllah akan dibahas di tutorial berikutnya.

Semoga bermanfaat. Kalau ada pertanyaan, silakan tulis di kolom komentar.

Wassalamu'alaikum warohmatulohi wabarakatuh.