Un SCC (Sbus Channel Changer) expérimental pour arduino
I. Introduction
Qu'est ce qu'un SCC?
SCC signifie SBUS Channel
Changer.
Il s'agit d'un dispositif qui permet d'attribuer à un servo SBUS le N° de voie qu'il doit utiliser pour se positionner.
Le SBUS est un bus série propriétaire (de Futaba/FrSky) très répendu dans le monde de la RC. Ce bus transporte dans chaque trame les valeurs de 16 voies proportionnelles, plus la valeur de 2 voies Tout-Ou-Rien. Tous les servos connectés au bus SBUS reçoivent donc toutes les valeurs de voies. Il est donc nécessaire d'indiquer à chaque servo le N° de voie qui lui est attribué: c'est le rôle du SCC.
![]() |
Le
SCC de FrSky |
Désirant étudier les posssibilités
d'intégration/interopérabilité de la fonction SCC
dans l'émetteur OpenAVRc (le récepteur SBUS X8R de FrSky est supporté), je me
suis mis à la recherche de documentation sur le protocole utilisé par le
SCC. Le but étant d'obtenir un
émetteur "tout-en-un".
Malheureusement, cette partie du protocole SBUS
n'est pas encore rendue publique (seule la partie entre récepteur SBUS
et servo SBUS l'est depuis 2012 suite à une
opération de reverse engineering). Le seul moyen de rendre
interopérable la partie paramétrage des servos SBUS
avec l'émetteur OpenAVRc consiste à faire
également du reverse engineering.
C'est en étudiant à l'oscilloscope l'échange entre le SCC et un servo SBUS Futaba S3270SV qu'une bonne partie du protocole a été découverte.
La communication est de type série asynchrone
mono-filaire half-duplex à 9600 bauds, 8 bits de données, parité paire
et 2 bits de stop.
Pour compliquer le tout, la logique est inversée.
Cette configuration série est peu commune et a nécessité le développement de la bibliothèque arduino TinySerial supportant:
- de 5 à 8 bits de données
- parité: aucune, impaire, paire
- 1 à 2 bits de stop
- les paramètres de configuration série au standard "arduino" (ex: SERIAL_8E2)
- l'émission/réception sur deux
broches séparées ou sur une seule broche
- la logique inversée
La bibliothèque TinySerial
a également besoin de la bibliothèque TinyPinChange
pour fonctionner.
II. Le sketch chargé dans l'arduino UNO
Ce sketch permet depuis la console série de l'IDE arduino de:
- lire l'ID courant d'un servo SBUS (valeur entre 1 et 16)
- changer l'ID courant d'un servo SBUS (valeur entre 1 et 16)
Note:/*
_____ ____ __ _ ____ _ _ _ _
| __ \ / __ \ | \ | | / __ \ | | | | | | | |
| |__| | | / \_| | . \ | | | / \ | | | | | \ \ / /
| _ / | | _ | |\ \| | | |__| | | | | | \ ' /
| | \ \ | \__/ | | | \ ' | | __ | \ \/ / | |
|_| |_| \____/ |_| \__| |_| |_| \__/ |_| 2020-2022
http://p.loussouarn.free.fr
*******************************************
* DIY S.BUS CHANNEL CHANGER *
*******************************************
A sketch acting as a S.BUS CHANNNEL CHANGER (SCC):
- The serial console configured at 115200 bauds with Carriage Return as end of line
- The current CHANNEL ID is displayed in the serial console every 500 ms
- To change the CHANNEL ID, just type the value (from 1 to 16) in the serial console, and then hit enter:
the new current CHANNEL ID will be displayed confirming the change has succeeded
- In case of failure, the displayed ID is 0 (Valid ID is between 1 and 16).
RC-Navy 2020-2022 (C)
Hardware Wiring:
===============
.---------. 5V .-------------.
| +------------------------------+ |
| | | |
| | SBUS_TX_RX_PIN 1K | |
| X----------------o---####------X |
| | | | SBUS Device |
| Arduino | # | (eg: servo) |
| | 100K # | |
| | # | |
| | GND | | |
| +----------------o-------------+ |
'---------' '-------------'
Notes:
- It is recommended to use an external power supply for the S.BUS servo (do not forget to connect the GND together)
- Thanks to oPossum5150 on rcgroups for the response length (17 bytes rather than the 20 observed) and the algorithm of the checksum:
https://www.rcgroups.com/forums/showpost.php?p=50201441&postcount=9
*/
#include <TinySerial.h>
//#define DBG /* Uncomment this to see debug messages in the serial console */
#define SSC_VERSION 0
#define SSC_REVISION 2
#define SBUS_POLLING_PERIOD_MS 500UL
#define SBUS_TX_RX_PIN 2 /* /!\ Pin SHALL support pin change interrupt /!\ */
#if defined(COMPIL_TIME_BAUD_RATE_AT_16_MHZ)
#if (COMPIL_TIME_BAUD_RATE_AT_16_MHZ != 9600)
#error COMPIL_TIME_BAUD_RATE_AT_16_MHZ SHALL be set to 9600 in TinySerial.h!
#endif
#endif
TinySerial SbusSerial(SBUS_TX_RX_PIN, SBUS_TX_RX_PIN); // One wire TTL Tx/Rx Serial (to be connected to the Signal Pin of the SBUS device)
#define CFG_MSG_MAX_LENGTH 10
static char CfgMessage[CFG_MSG_MAX_LENGTH + 1];
#define CARRIAGE_RETURN 0x0D /* '\r' = 0x0D (code ASCII) */
#define RUB_OUT 0x08
#define PRINT_BUF_SIZE 100
static char PrintBuf[PRINT_BUF_SIZE + 1];
#define PRINTF(fmt, ...) snprintf_P(PrintBuf, PRINT_BUF_SIZE, PSTR(fmt) ,##__VA_ARGS__);Serial.print(PrintBuf)
#define PRINT_P(FlashStr) Serial.print(F(FlashStr))
const uint8_t GetIdMsg[] PROGMEM = {0x9F ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x80 ,0x00 ,0xE0 ,0x1F};
// SetIdMsgForIdEgal1[] = {0x9F ,0x00 ,0xE8 ,0x5A ,0xF7 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x80 ,0x00 ,0xB2 ,0x4A};
// AnswerForIdEgal1[] = {0x05 ,0x00 ,0xE8 ,0x5A ,0xF7 ,0x04 ,0xF5 ,0x09 ,0x01 ,0x01 ,0x01 ,0x00 ,0x00 ,0x2C ,0x80 ,0xB4 ,0xFA};// ,0x00 ,0x31 ,0x00};
// 3 unknown bytes...
typedef struct{
uint8_t HeaderByte;
uint8_t ChIdx;
uint8_t FldFwd[3];
uint8_t DoNotTouch[10];
uint8_t CmdCode;
uint8_t Checksum;
}SbusMsgSt_t;
#define CMD_GET_ID 0
#define CMD_SET_ID 1
#define CODE_GET_ID 0xE0 /* Not used in this sketch */
#define CODE_SET_ID 0xB2
#define CMD_HEADER_BYTE 0x9F
#define RESP_HEADER_BYTE 0x05
#define RESP_MSG_LEN 17
static SbusMsgSt_t SbusMsg;
void setup()
{
Serial.begin(115200);
while(!Serial);
PRINTF("\n --- DIY S.BUS Channel Changer V%u.%u using TinySerial V%u.%u (RC Navy 2020-2022) ---\n", SSC_VERSION, SSC_REVISION, TINY_SOFT_SERIAL_VERSION,TINY_SOFT_SERIAL_REVISION);
SbusSerial.begin(9600, SERIAL_8E2, true); /* S.BUS for Channel Changer works at 9600 bauds, 8 Data bits, Even Parity, 2 Stop bits, inverted signal */
}
void loop()
{
static uint32_t StartMs = millis();
static int8_t ChIdx = 0;
uint8_t ChId;
if(millis() - StartMs >= SBUS_POLLING_PERIOD_MS)
{
StartMs = millis();
ChIdx = SendCmdAndWaitForResp(CMD_GET_ID, 0);
PRINTF("ID=%u\n", ChIdx + 1);
}
else
{
if(millis() - StartMs >= (SBUS_POLLING_PERIOD_MS / 2))
{
if(CfgMessageAvailable() > 0)
{
ChId = atoi(CfgMessage);
PRINTF("ID=%u->%u\n", ChIdx + 1, ChId);
if((ChId >= 1) && (ChId <= 16))
{
SendCmdAndWaitForResp(CMD_SET_ID, ChId - 1);
StartMs = millis();
}
else
{
PRINTF("Channel ID MUST be within [1-16] range!\n");
}
}
}
}
}
int8_t SendCmdAndWaitForResp(uint8_t Cmd, uint8_t Idx)
{
int8_t RetIdx = -1;
uint8_t RxByteNb = 0, ChIdx;
uint32_t StartMs;
char *Ptr = (char *)&SbusMsg;
char RxByte;
switch(Cmd)
{
case CMD_SET_ID: /* This assumes that a get reponse has been previously received */
SbusMsg.HeaderByte = CMD_HEADER_BYTE;
SbusMsg.ChIdx = ReverseByteBitOrder(Idx);
memcpy_P(&SbusMsg.DoNotTouch, GetIdMsg + 5, sizeof(SbusMsg.DoNotTouch));
SbusMsg.CmdCode = CODE_SET_ID;
SbusMsg.Checksum = calculate_sbus_checksum((uint8_t const*)&SbusMsg);
#ifdef DBG
PRINTF("Cmd: "); PrintBufInHexa((char *)&SbusMsg, sizeof(GetIdMsg));
#endif
while(SbusSerial.available()) SbusSerial.read(); // Flush Rx
SbusSerial.txMode();
SbusSerial.write((char*)&SbusMsg, sizeof(SbusMsgSt_t));
SbusSerial.rxMode();
break;
case CMD_GET_ID:
memcpy_P(&SbusMsg, GetIdMsg, sizeof(GetIdMsg));
#ifdef DBG
PrintBufInHexa((char *)&SbusMsg, sizeof(GetIdMsg));
#endif
while(SbusSerial.available()) SbusSerial.read(); // Flush Rx
SbusSerial.txMode();
SbusSerial.write((char*)&SbusMsg, sizeof(SbusMsgSt_t));
SbusSerial.rxMode();
/* Wait for response */
StartMs = millis();
do{
if(SbusSerial.available())
{
RxByte = SbusSerial.read();
if(!RxByteNb && (RxByte != RESP_HEADER_BYTE)) continue; // Header byte of the response shall be 0x05
if(RxByteNb < sizeof(SbusMsgSt_t))
{
Ptr[RxByteNb] = RxByte;
}
RxByteNb++;
}
}while((millis() - StartMs) < 100 && (RxByteNb < RESP_MSG_LEN));
if(RxByteNb >= RESP_MSG_LEN)
{
#ifdef DBG
PrintBufInHexa((char *)&SbusMsg, sizeof(GetIdMsg));
#endif
ChIdx = ReverseByteBitOrder(SbusMsg.ChIdx);
if(!validate_sbus_checksum((uint8_t const*)&SbusMsg))
{
RetIdx = ChIdx;
}
}
break;
default:
break;
}
return(RetIdx);
}
uint8_t ReverseByteBitOrder(uint8_t Byte)
{
Byte = ((Byte >> 1) & 0x55) | ((Byte << 1) & 0xaa);
Byte = ((Byte >> 2) & 0x33) | ((Byte << 2) & 0xcc);
Byte = ((Byte >> 4) & 0x0f) | ((Byte << 4) & 0xf0);
return(Byte);
}
static uint8_t validate_sbus_checksum(uint8_t const* b)
{
uint8_t n = 16; // 2nd to 17th byte
uint8_t checksum = 0;
do checksum += ReverseByteBitOrder(*++b); while (--n);
return checksum; // Valid checksum will return 0
}
static uint8_t calculate_sbus_checksum(uint8_t const* b)
{
uint8_t n = 15; // 2nd to 16th byte
uint8_t checksum = 0;
do checksum -= ReverseByteBitOrder(*++b); while (--n);
return ReverseByteBitOrder(checksum); // Checksum is 17th byte
}
static int8_t CfgMessageAvailable(void)
{
int8_t Ret = -1;
char RxChar;
static uint8_t Idx = 0;
if(Serial.available() > 0)
{
RxChar = Serial.read();
switch(RxChar)
{
case CARRIAGE_RETURN: /* Si retour chariot: fin de message */
CfgMessage[Idx] = 0;/* Remplace CR character par fin de chaine */
Ret = Idx;
Idx = 0; /* Re-positionne index pour prochain message */
break;
case RUB_OUT:
if(Idx) Idx--;
break;
default:
if(Idx < CFG_MSG_MAX_LENGTH)
{
CfgMessage[Idx] = RxChar;
Idx++;
}
else Idx = 0; /* Re-positionne index pour prochain message */
break;
}
}
return(Ret);
}
#ifdef DBG
void PrintBufInHexa(char *Buf, uint8_t Len)
{
for(uint8_t Idx = 0; Idx < Len; Idx++)
{
PRINTF("%02X ", (uint8_t)Buf[Idx]);
}
PRINTF("\n");
}
#endif
![]() |
Changement
d'ID de la valeur 1 vers la valeur 12: fastoche! |
Le changement effectif de l'ID (N° de voie) a été vérifié en connectant le servo Futaba S3270SV à la sortie SBUS d'un récepteur X8R de FrSky commandé par l'émetteur OpenAVRc.
IV. Le sketch et les bibliothèques <TinyPinChange> et <TinySerial>
Télécharger l'archive zip contenant le sketch et les bibliothèques nécessaires: DIY SCC (clic droit/Enregistrer la cible du lien sous...)
Note:
Ne pas oublier de câbler la résistance de pull-down de 100K ainsi que
la résistance série de protection de 1K! (voir commentaires dans le
sketch)
V. Conclusion
Ce SCC doit être considéré comme expérimental puisqu'uniquement testé avec un seul modèle de servo SBUS (Futaba S3270SV).
Il reste encore du travail pour adapter cette fonctionnalité à l'émetteur OpenAVRc, il faut:
- une broche supportant les
interruptions "pin change" pour détecter le bit de start des bytes
retournés par le servo SBUS
- regarder de plus près les aspects "temps réel" afin de gérer de manière non bloquante le cas ou il n'y a pas de réponse
- concevoir un menu SCC dans l'écran LCD
- optimiser le code
Il faut également vérifier que d'autres modèles de servos
SBUS puissent être supportés par
ce SCC expérimental.
La faisabilité semble acquise, et l'avenir nous dira si
cette fonctionalité sera intégrée un jour dans OpenAVRc...