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:
| Field | Ukuran | Keterangan |
|---|---|---|
| Priority | 2 bit | 0 = critical, 1 = high, 2 = normal, 3 = low |
| Source | 5 bit | alamat pengirim (0..31) |
| Destination | 5 bit | alamat tujuan (31 = broadcast) |
| Destination port | 6 bit | port layanan tujuan (0..63) |
| Source port | 6 bit | port pengirim (dipakai untuk balasan) |
| Flags | 8 bit | opsi 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:
| Port | Service |
|---|---|
| 0 | CMP (manajemen: identitas, routing, statistik) |
| 1 | Ping |
| 2 | Daftar task (process list) |
| 3 | Sisa memori |
| 4 | Reboot |
| 5 | Sisa buffer CSP |
| 6 | Uptime |
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:
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
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:
- Buka Connectivity → FDCAN1, centang Activated. Pin otomatis menjadi PA11 = FDCAN1_RX dan PA12 = FDCAN1_TX.
- Di Clock Configuration, sumber clock FDCAN adalah PLL1Q = 50 MHz (DIVQ1 = 3).
- Di Parameter Settings isi nilai berikut untuk kecepatan 1 Mbit/s:
| Parameter | Nilai | Keterangan |
|---|---|---|
| Frame Format | Classic mode | CAN 2.0B (bukan CAN FD) |
| Mode | Normal | di kode diganti ke internal loopback untuk uji 1 board |
| Nominal Prescaler | 5 | 50 MHz / 5 = 10 MHz → 1 tq = 100 ns |
| Nominal Sync Jump Width | 2 | |
| Nominal Time Seg1 | 7 | |
| Nominal Time Seg2 | 2 | 1 bit = 1 + 7 + 2 = 10 tq = 1 µs → 1 Mbit/s, sample point 80% |
| Auto Retransmission | Enable | |
| Ext Filters Nbr | 2 | filter alamat sendiri + broadcast |
| Rx Fifo0 Elmts Nbr | 32 | antrian frame masuk |
| Tx Fifo Queue Elmts Nbr | 32 | antrian frame keluar |
- 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). - 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
- Download source libcsp dari GitHub
github.com/libcsp/libcsp, pilih tag v1.6. - Buat folder
Middlewares/Third_Party/libcspdi project, lalu salin folderincludedan file-file disrcberikut:src/*.cdansrc/*.h(inti CSP: buffer, connection, router, io, service, dll)src/arch/*.cdansrc/arch/freertos/*.c(port untuk FreeRTOS)src/interfaces/csp_if_can.c,csp_if_can_pbuf.c/.h,csp_if_lo.csrc/rtable/csp_rtable.c,csp_rtable_static.c,csp_rtable_internal.hsrc/transport/*.cdan*.h
- Tambahkan include path di Project → Properties → C/C++ Build → Settings → MCU GCC Compiler → Include paths:
../Middlewares/Third_Party/libcsp/includedan../Middlewares/Third_Party/libcsp/src - libcsp aslinya memakai build system waf yang membuat file
csp_autoconfig.hsecara otomatis. Karena kita memakai STM32CubeIDE, file ini dibuat manual diinclude/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 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 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):
- Ubah
#define CSP_CAN_LOOPBACK 0. - Beri alamat CSP berbeda di setiap board, misalnya board A
CSP_MY_ADDR 1denganCSP_SERVER_ADDR 2, dan board BCSP_MY_ADDR 2. - Pasang transceiver CAN di setiap board: PA12 → TXD/CTX, PA11 ← RXD/CRX, VCC dan GND sesuai tegangan kerja transceiver (MCP2551/TJA1050 = 5 V, SN65HVD230 = 3,3 V).
- Sambungkan CAN_H ke CAN_H dan CAN_L ke CAN_L, lalu pasang resistor 120 Ω di kedua ujung bus.
- Pastikan semua node memakai kecepatan yang sama (1 Mbit/s).
Wiring CAN untuk dua board (DevEBox STM32H743 + modul MCP2551)
Berikut wiring yang saya pakai untuk menghubungkan dua board DevEBox STM32H743: satu sebagai OBC (alamat CSP 1) dan satu lagi sebagai EPS (alamat CSP 2). Setiap board memakai modul transceiver MCP2551.
Wiring CAN: board DevEBox STM32H743 ke modul MCP2551, dan bus CAN antara board OBC dan EPS.
Sambungan di setiap board (sama untuk OBC dan EPS):
| Board DevEBox | Modul MCP2551 | Keterangan |
|---|---|---|
| 5V (dekat konektor USB) | VCC | MCP2551 butuh 5 V |
| GND | GND | ground bersama |
| PA12 (FDCAN1_TX) | CTX | data dari STM32 ke transceiver |
| PA11 (FDCAN1_RX) | CRX | data dari transceiver ke STM32 |
Sambungan antar board:
- CANH ke CANH dan CANL ke CANL, sebaiknya dengan kabel yang dipilin.
- Resistor 120 Ω di antara CANH dan CANL di kedua ujung bus. Modul MCP2551 ungu ini belum punya terminator 120 Ω bawaan (resistor berkode 101 di jalur CANH/CANL nilainya 100 Ω), jadi terminator perlu dipasang sendiri. Saat daya mati, hambatan CANH–CANL yang benar sekitar 60 Ω.
- GND kedua board disatukan.
Hal yang perlu diperhatikan:
- Di header DevEBox setiap baris berisi dua pin. PA11 sebaris dengan PA10 (RX1) dan PA12 sebaris dengan PA15, jadi pastikan kabel tidak masuk ke pin di sebelahnya.
- PA9 dan PA10 tetap dipakai untuk log UART ke USB-TTL/ST-Link, bukan untuk CAN.
- MCP2551 dicatu 5 V, sehingga pin CTX baru membaca HIGH di sekitar 3,5 V, sedangkan STM32 hanya mengeluarkan 3,3 V. Untuk hasil yang andal, gunakan transceiver 3,3 V seperti SN65HVD230, atau tambahkan pull-up 2,2 kΩ dari CTX ke 5 V dan atur PA12 sebagai open-drain (PA12 tahan 5 V).
Foto rakitan hardware
Supaya lebih jelas, berikut rakitan asli yang saya pakai. Setiap node (OBC dan EPS) terdiri dari satu board carrier PCB buatan sendiri, modul transceiver MCP2551, dan board DevEBox STM32H743 yang ditancapkan di atasnya.
Board carrier dengan modul transceiver MCP2551 (ungu) di tengah dan konektor header 4pin CAN di sisi kiri. Di bawahnya board DevEBox STM32H743 sebelum dipasang.
Board DevEBox STM32H743 sudah ditancapkan ke header board carrier. PA11/PA12 tersambung ke CRX/CTX MCP2551 lewat jalur di PCB.
Sisi belakang board carrier: header CANBUS1 (PA11/PA12), kabel ke WeAct Mini Debugger (SWD + serial USART1) dan resistor terminasi 120 Ω, plus rail 5V, GND dan 3,3V.
Dua node ini lalu ditumpuk memakai spacer seperti format stack CubeSat, dan bus CAN-nya disambung antar board. Korlan USB2CAN bisa ikut dipasang di bus untuk sniffing.
Hasil uji dua board: log CSP dari jalur serial
Dua board node (OBC dan EPS) dan satu board DB9 ditumpuk dengan spacer, LED status menyala, konektor header 4pin CAN tersambung antar stack ketiga board node OBC, EPS dan board DB9 (untuk sniffing Korlan USB2CAN) di paling atas. Rakitan inilah yang menghasilkan log di bawah.
Setelah wiring di atas terpasang lengkap, termasuk dua resistor 120 Ω di ujung bus, kedua board diuji dengan firmware dua peran: OBC (alamat CSP 1) dan EPS (alamat CSP 2). Masing-masing board mengirim log ke laptop lewat USART1 (PA9 = TX, PA10 = RX, 115200 baud) melalui Mini Debugger: OBC di COM7 dan EPS di COM6.
Log OBC (COM7), setiap siklus 2 detik:
Log OBC (COM7): ping EPS 2 ms, housekeeping EPS, balasan ACK, telemetri 200 byte, perintah payload, dan status CAN TEC=0 REC=0.
[OBC] ping EPS (node 2): 2 ms: ping CSP ke EPS dijawab dalam 2 ms.[OBC-HK] EPS uptime 782 s | suhu 31.8 C | VDDA 3339 mV | payload ON | CAN EPS rx=… tx=…: data housekeeping 20 byte dari EPS (port 12).[OBC] balasan: "ACK dari node 2 (EPS)": request-reply teks di port 10.[OBC] kirim paket 200 byte ke EPS, checksum=…: telemetri panjang di port 11, dipecah CFP menjadi 26 frame CAN.[OBC-CMD] payload OFF -> OK: setiap 10 detik OBC membalik rail payload EPS (port 13), LED PA1 di EPS ikut berubah.[STAT] CAN TX=… RX=… gagal=0 | TEC=0 REC=0 OK: penghitung error kontroler FDCAN tetap 0, tidak ada frame yang gagal.
Log EPS (COM6), sisi penerima:
Log EPS (COM6): melayani housekeeping, pesan dan telemetri dari OBC (node 1), menjalankan perintah payload, CAN tanpa error.
[EPS-HK] permintaan #… dari node 1: EPS melayani permintaan housekeeping dari OBC, lengkap dengan suhu chip (sensor internal ADC3) dan VDDA (dari VREFINT).[SERVER] port 10 dari node 1: "Halo EPS, pesan ke-…"danport 11: paket 200 byte dari node 1, checksum=…: checksum di EPS sama dengan yang dikirim OBC, jadi paket 26 frame tersusun ulang dengan benar.[EPS-CMD] perintah dari node 1: rail payload -> ON, LED PA1 menyala (OK): perintah dari OBC dijalankan.[STAT] ... TEC=0 REC=0 OK | buffer CSP bebas=19–20: tidak ada error CAN dan buffer CSP tidak bocor.
Pelajaran penting dari pengujian ini: sebelum resistor 120 Ω dipasang, OBC terus mencatat ping EPS: TIMEOUT dengan TEC=248 … BUS-OFF. Setelah terminator dipasang, komunikasi langsung normal. Lalu lintas yang sama juga bisa dipantau dari laptop dengan Korlan USB2CAN, lihat tutorial Sniffing CAN Bus CubeSat (libcsp) dengan Korlan USB2CAN dan GUI Python.
Kesalahan yang sering terjadi
| Masalah | Penyebab | Solusi |
|---|---|---|
| Paket tidak pernah sampai / tersusun | Frame dikirim dengan ID standar 11-bit | Pakai FDCAN_EXTENDED_ID, CFP butuh 29-bit |
| Panjang data salah | Nilai DataLength dihitung dengan geser bit (format HAL lama) | Di HAL STM32H7 ini, FDCAN_DLC_BYTES_8 = 8, jadi pakai langsung jumlah byte |
| Crash acak / HardFault | Buffer CSP di-free dua kali, atau di-free setelah csp_send() berhasil | Free hanya saat csp_send() gagal |
| Heap habis perlahan | malloc() di setiap pengiriman tanpa free() | Gunakan buffer CSP (csp_buffer_get) saja |
| Program berhenti di configASSERT | Prioritas interrupt FDCAN lebih tinggi dari 5 | Set prioritas FDCAN1_IT0 ≥ 5 |
| Tidak ada frame di bus (mode normal) | Tanpa transceiver / tanpa terminasi / bitrate beda | Cek 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.







































