BetaFlight模块设计之三十六:SoftSerial
- 1. 源由
- 2. API接口
- 2.1 openSoftSerial
- 2.2 onSerialRxPinChange
- 2.3 onSerialTimerOverflow
- 2.4 processTxState
- 2.5 processRxState
- 3. 辅助函数
- 3.1 applyChangedBits
- 3.2 extractAndStoreRxByte
- 3.3 prepareForNextRxByte
- 4. 总结
1. 源由
鉴于Betaflight关于STM32F405 SBUS协议兼容硬件电气特性问题,从程序代码上看,软串口应该能够采用定时器、中断的方式进行电平协议的解析。
但是从实测Betaflight4.4.2固件的角度看,又无法使用,怀疑可能存在以下问题:
- 配置问题
- 代码移植BUG(unified_target ==> config)
- 代码不支持
所以尝试整理下SoftSerial
代码结构,通过对整体代码的了解,能否找出其中的一些深层次原因。
2. API接口
从对外接口的角度看,主要有以下API:
- 打开软件串口
openSoftSerial
- 底层串行信号电平变更处理
onSerialRxPinChange
- 底层串行信号超市处理
onSerialTimerOverflow
- 后端Tx状态处理
processTxState
- 后端Rx状态处理
processRxState
serialPort_t *openSoftSerial(softSerialPortIndex_e portIndex, serialReceiveCallbackPtr rxCallback, void *rxCallbackData, uint32_t baud, portMode_e mode, portOptions_e options)
void onSerialRxPinChange(timerCCHandlerRec_t *cbRec, captureCompare_t capture)
void onSerialTimerOverflow(timerOvrHandlerRec_t *cbRec, captureCompare_t capture)
void processTxState(softSerial_t *softSerial)
void processRxState(softSerial_t *softSerial)
2.1 openSoftSerial
根据资源进行配置:
- 【Hardware】GPIO:Tx/Rx/SERIAL_INVERTED
- 【Hardware】TIMER
- 【Hardware】Interrupt:ICPOLARITY_RISING/ICPOLARITY_FALLING
- 【Software】Buffer
- 【Software】Callback:onSerialRxPinChange(edgeCb)/onSerialTimerOverflow(overCb)/rxCallback
openSoftSerial
│
│ // get serial port description
├──> softSerial_t *softSerial = &(softSerialPorts[portIndex]);
│
│ // get serial port rx/tx ioTag
├──> ioTag_t tagRx = softSerialPinConfig()->ioTagRx[portIndex];
├──> ioTag_t tagTx = softSerialPinConfig()->ioTagTx[portIndex];
│
│ // one wire(Sbus etc.) or two wire softserial(UART etc.)
├──> const timerHardware_t *timerTx = timerAllocate(tagTx, OWNER_SOFTSERIAL_TX, RESOURCE_INDEX(portIndex));
├──> const timerHardware_t *timerRx = (tagTx == tagRx) ? timerTx : timerAllocate(tagRx, OWNER_SOFTSERIAL_RX, RESOURCE_INDEX(portIndex));
│
│ // get serial port rx/tx IO_t
├──> IO_t rxIO = IOGetByTag(tagRx);
├──> IO_t txIO = IOGetByTag(tagTx);
│
│ // timer & io set
├──> <options & SERIAL_BIDIR> // bi-direction configuration
│ ├──> <!timerTx || (timerTx->output & TIMER_OUTPUT_N_CHANNEL)>
│ │ │ // If RX and TX pins are both assigned, we CAN use either with a timer.
│ │ │ // However, for consistency with hardware UARTs, we only use TX pin,
│ │ │ // and this pin must have a timer, and it should not be N-Channel.
│ │ └──> return NULL;
│ ├──> softSerial->timerHardware = timerTx;
│ ├──> softSerial->txIO = txIO;
│ ├──> softSerial->rxIO = txIO;
│ └──> IOInit(txIO, OWNER_SOFTSERIAL_TX, RESOURCE_INDEX(portIndex));
├──> < else > // unidirection configuration
│ ├──> <mode & MODE_RX>
│ │ ├──> <!timerRx || (timerRx->output & TIMER_OUTPUT_N_CHANNEL)>
│ │ │ │ // Need a pin & a timer on RX. Channel should not be N-Channel.
│ │ │ └──> return NULL;
│ │ ├──> softSerial->rxIO = rxIO;
│ │ ├──> softSerial->timerHardware = timerRx;
│ │ └──> <!((mode & MODE_TX) && rxIO == txIO)>
│ │ └──> IOInit(rxIO, OWNER_SOFTSERIAL_RX, RESOURCE_INDEX(portIndex));
│ └──> <mode & MODE_TX>
│ ├──> <!tagTx>
│ │ │ // Need a pin on TX
│ │ └──> return NULL;
│ ├──> softSerial->txIO = txIO;
│ ├──> <!(mode & MODE_RX)>
│ │ ├──> <!timerTx> return NULL;
│ │ │ // TX Simplex, must have a timer
│ │ └──> softSerial->timerHardware = timerTx;
│ ├──> < else > // Duplex
│ │ └──> softSerial->exTimerHardware = timerTx;
│ └──> IOInit(txIO, OWNER_SOFTSERIAL_TX, RESOURCE_INDEX(portIndex));
│
│ // port configuration
├──> softSerial->port.vTable = &softSerialVTable;
├──> softSerial->port.baudRate = baud;
├──> softSerial->port.mode = mode;
├──> softSerial->port.options = options;
├──> softSerial->port.rxCallback = rxCallback;
├──> softSerial->port.rxCallbackData = rxCallbackData;
│
├──> resetBuffers(softSerial);
│
├──> softSerial->softSerialPortIndex = portIndex;
│
├──> softSerial->transmissionErrors = 0;
├──> softSerial->receiveErrors = 0;
│
├──> softSerial->rxActive = false;
├──> softSerial->isTransmittingData = false;
│
│ // Configure master timer (on RX); time base and input capture
├──> serialTimerConfigureTimebase(softSerial->timerHardware, baud);
├──> timerChConfigIC(softSerial->timerHardware, (options & SERIAL_INVERTED) ? ICPOLARITY_RISING : ICPOLARITY_FALLING, 0);
│
│ // Initialize callbacks
├──> timerChCCHandlerInit(&softSerial->edgeCb, onSerialRxPinChange);
├──> timerChOvrHandlerInit(&softSerial->overCb, onSerialTimerOverflow);
│
│ // Configure bit clock interrupt & handler.
│ // If we have an extra timer (on TX), it is initialized and configured
│ // for overflow interrupt.
│ // Receiver input capture is configured when input is activated.
├──> <(mode & MODE_TX) && softSerial->exTimerHardware && softSerial->exTimerHardware->tim != softSerial->timerHardware->tim>
│ ├──> softSerial->timerMode = TIMER_MODE_DUAL;
│ ├──> serialTimerConfigureTimebase(softSerial->exTimerHardware, baud);
│ ├──> timerChConfigCallbacks(softSerial->exTimerHardware, NULL, &softSerial->overCb);
│ └──> timerChConfigCallbacks(softSerial->timerHardware, &softSerial->edgeCb, NULL);
├──> < else >
│ ├──> softSerial->timerMode = TIMER_MODE_SINGLE;
│ └──> timerChConfigCallbacks(softSerial->timerHardware, &softSerial->edgeCb, &softSerial->overCb);
│
├──> <USE_HAL_DRIVER>
│ └──> softSerial->timerHandle = timerFindTimerHandle(softSerial->timerHardware->tim);
│
│ // antivate port
├──> <!(options & SERIAL_BIDIR)>
│ ├──> serialOutputPortActivate(softSerial);
│ └──> setTxSignal(softSerial, ENABLE);
├──> serialInputPortActivate(softSerial);
└──> return &softSerial->port;
2.2 onSerialRxPinChange
通过边沿中断记录bit数据流。
onSerialRxPinChange
├──> softSerial_t *self = container_of(cbRec, softSerial_t, edgeCb);
├──> bool inverted = self->port.options & SERIAL_INVERTED;
│
├──> <(self->port.mode & MODE_RX) == 0>
│ └──> return; // 无接收模式,直接返回
│
├──> <self->isSearchingForStartBit>
│ │ // Synchronize the bit timing so that it will interrupt at the center
│ │ // of the bit period.
│ ├──> <USE_HAL_DRIVER>
│ │ └──> __HAL_TIM_SetCounter(self->timerHandle, __HAL_TIM_GetAutoreload(self->timerHandle) / 2);
│ ├──> <else>
│ │ └──> TIM_SetCounter(self->timerHardware->tim, self->timerHardware->tim->ARR / 2);
│ │
│ │ // For a mono-timer full duplex configuration, this may clobber the
│ │ // transmission because the next callback to the onSerialTimerOverflow
│ │ // will happen too early causing transmission errors.
│ │ // For a dual-timer configuration, there is no problem.
│ ├──> <(self->timerMode != TIMER_MODE_DUAL) && self->isTransmittingData>
│ │ └──> self->transmissionErrors++;
│ │
│ ├──> timerChConfigIC(self->timerHardware, inverted ? ICPOLARITY_FALLING : ICPOLARITY_RISING, 0);
│ ├──> <defined(STM32F7) || defined(STM32H7) || defined(STM32G4)>
│ │ └──> serialEnableCC(self);
│ │
│ ├──> self->rxEdge = LEADING;
│ │
│ ├──> self->rxBitIndex = 0;
│ ├──> self->rxLastLeadingEdgeAtBitIndex = 0;
│ ├──> self->internalRxBuffer = 0;
│ ├──> self->isSearchingForStartBit = false;
│ └──> return;
│
│ // handle leveled signal
├──> <self->rxEdge == LEADING>
│ └──> self->rxLastLeadingEdgeAtBitIndex = self->rxBitIndex;
├──> applyChangedBits(self);
│
├──> <self->rxEdge == TRAILING>
│ ├──> self->rxEdge = LEADING;
│ └──> timerChConfigIC(self->timerHardware, inverted ? ICPOLARITY_FALLING : ICPOLARITY_RISING, 0);
├──> < else >
│ ├──> self->rxEdge = TRAILING;
│ └──> timerChConfigIC(self->timerHardware, inverted ? ICPOLARITY_RISING : ICPOLARITY_FALLING, 0);
└──> <defined(STM32F7) || defined(STM32H7) || defined(STM32G4)>
└──> serialEnableCC(self);
2.3 onSerialTimerOverflow
串行数据从原理上属于字符流协议,从实际应用角度,还是一包一包的数据(通常不会密集到头尾相连)。
因此,超时机制相当于处理:
- 数据帧
- 异常中断
onSerialTimerOverflow
├──> softSerial_t *self = container_of(cbRec, softSerial_t, overCb);
├──> <self->port.mode & MODE_TX> processTxState(self);
└──> <self->port.mode & MODE_RX> processRxState(self);
2.4 processTxState
Tx数据处理存在三种情况:
- 发送数据前处理
- 发送数据
- 发送数据后处理
processTxState
│ // 发送数据前处理
├──> <!softSerial->isTransmittingData>
│ ├──> <isSoftSerialTransmitBufferEmpty((serialPort_t *)softSerial)>
│ │ │ // Transmit buffer empty.
│ │ │ // Start listening if not already in if half-duplex
│ │ ├──> <!softSerial->rxActive && softSerial->port.options & SERIAL_BIDIR) {
│ │ │ ├──> serialOutputPortDeActivate(softSerial);
│ │ │ └──> serialInputPortActivate(softSerial);
│ │ └──> return;
│ │
│ │ // data to send
│ ├──> uint8_t byteToSend = softSerial->port.txBuffer[softSerial->port.txBufferTail++];
│ ├──> <softSerial->port.txBufferTail >= softSerial->port.txBufferSize>
│ │ └──> softSerial->port.txBufferTail = 0;
│ │
│ │ // build internal buffer, MSB = Stop Bit (1) + data bits (MSB to LSB) + start bit(0) LSB
│ ├──> softSerial->internalTxBuffer = (1 << (TX_TOTAL_BITS - 1)) | (byteToSend << 1);
│ ├──> softSerial->bitsLeftToTransmit = TX_TOTAL_BITS;
│ ├──> softSerial->isTransmittingData = true;
│ └──> <softSerial->rxActive && (softSerial->port.options & SERIAL_BIDIR)>
│ │ // Half-duplex: Deactivate receiver, activate transmitter
│ ├──> serialInputPortDeActivate(softSerial);
│ ├──> serialOutputPortActivate(softSerial);
│ │
│ │ // Start sending on next bit timing, as port manipulation takes time,
│ │ // and continuing here may cause bit period to decrease causing sampling errors
│ │ // at the receiver under high rates.
│ │ // Note that there will be (little less than) 1-bit delay; take it as "turn around time".
│ │ // XXX We may be able to reload counter and continue. (Future work.)
│ └──> return;
│
│ // 发送bit数据:高/低 电平
├──> <softSerial->bitsLeftToTransmit>
│ ├──> mask = softSerial->internalTxBuffer & 1;
│ ├──> softSerial->internalTxBuffer >>= 1;
│ │
│ ├──> setTxSignal(softSerial, mask);
│ ├──> softSerial->bitsLeftToTransmit--;
│ └──> return;
│
│ // 发送数据后处理
└──> softSerial->isTransmittingData = false;
2.5 processRxState
RX_TOTAL_BITS
10 bits format: start bit + 8 bits for one byte + stop bit
processRxState
│ //Start bit处理
├──> <softSerial->isSearchingForStartBit>
│ └──> return;
├──> softSerial->rxBitIndex++;
│
│ //1 Byte数据处理
├──> <softSerial->rxBitIndex == RX_TOTAL_BITS - 1>
│ ├──> applyChangedBits(softSerial);
│ └──> return;
│ //Stop bit处理
└──> <softSerial->rxBitIndex == RX_TOTAL_BITS>
├──> softSerial->rxEdge == TRAILING>
│ └──> softSerial->internalRxBuffer |= STOP_BIT_MASK;
├──> extractAndStoreRxByte(softSerial);
└──> prepareForNextRxByte(softSerial);
注:上述函数过程存在10bit缺损卡死的情况,代码还不够robust。
3. 辅助函数
3.1 applyChangedBits
1~9 bit数据将通过该函数进行存储,最后10bit数据将在processRxState中进行保存。
applyChangedBits
└──> <softSerial->rxEdge == TRAILING>
└──> for (bitToSet = softSerial->rxLastLeadingEdgeAtBitIndex; bitToSet < softSerial->rxBitIndex; bitToSet++)
└──> softSerial->internalRxBuffer |= 1 << bitToSet;
3.2 extractAndStoreRxByte
从10 bit格式中抽取1Byte有效数据。
extractAndStoreRxByte
│ //仅TX模式,无需进行任何接收字节的保存工作
├──> <(softSerial->port.mode & MODE_RX) == 0>
│ └──> return;
│
├──> uint8_t haveStartBit = (softSerial->internalRxBuffer & START_BIT_MASK) == 0;
├──> uint8_t haveStopBit = (softSerial->internalRxBuffer & STOP_BIT_MASK) == 1;
│
│ //起止bit位,若一项不符合规格,则丢弃数据
├──> <!haveStartBit || !haveStopBit>
│ ├──> softSerial->receiveErrors++;
│ └──> return;
│
│ //保存1Byte数据
├──> uint8_t rxByte = (softSerial->internalRxBuffer >> 1) & 0xFF;
│
├──> <softSerial->port.rxCallback> //回调接收函数
│ └──> softSerial->port.rxCallback(rxByte, softSerial->port.rxCallbackData);
└──> < else > //无接收注册函数情况下,将数据存入缓冲buffer中,并采用循环方式覆盖保存
├──> softSerial->port.rxBuffer[softSerial->port.rxBufferHead] = rxByte;
└──> softSerial->port.rxBufferHead = (softSerial->port.rxBufferHead + 1) % softSerial->port.rxBufferSize;
3.3 prepareForNextRxByte
收录下一字节数据做预处理工作。
prepareForNextRxByte
├──> softSerial->rxBitIndex = 0;
├──> softSerial->isSearchingForStartBit = true;
└──> <softSerial->rxEdge == LEADING>
├──> softSerial->rxEdge = TRAILING;
├──> timerChConfigIC(softSerial->timerHardware, (softSerial->port.options & SERIAL_INVERTED) ? ICPOLARITY_RISING : ICPOLARITY_FALLING, 0);
└──> serialEnableCC(softSerial);
4. 总结
从SoftSerial
代码角度,采用定时器、边沿中断的方式,随机使用CPU资源。如果应用在高速、大数据量通信场景,将会影响和打扰CPU正常业务逻辑,尤其是在CPU资源紧张时。