520 lines
13 KiB
C++
520 lines
13 KiB
C++
/*
|
|
* Copyright (C) EdgeTx
|
|
*
|
|
* Based on code named
|
|
* opentx - https://github.com/opentx/opentx
|
|
* th9x - http://code.google.com/p/th9x
|
|
* er9x - http://code.google.com/p/er9x
|
|
* gruvin9x - http://code.google.com/p/gruvin9x
|
|
*
|
|
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*/
|
|
|
|
#include "stm32_softserial_driver.h"
|
|
#include "stm32_exti_driver.h"
|
|
|
|
#include <string.h>
|
|
|
|
// constant TX bitlen, timer frequency is computed
|
|
// to reach the required baudrate
|
|
#define BITLEN 16
|
|
|
|
// RX state
|
|
static uint8_t rxBitCount;
|
|
static uint8_t rxByte;
|
|
|
|
// RX FIFO
|
|
static volatile uint8_t rxRidx;
|
|
static volatile uint8_t rxWidx;
|
|
static uint8_t *rxBuffer;
|
|
static uint32_t rxBufLen;
|
|
|
|
static const stm32_softserial_rx_port* _softserialPort;
|
|
|
|
static void _softserial_exti()
|
|
{
|
|
if (rxBitCount == 0) {
|
|
auto port = _softserialPort;
|
|
|
|
// cheap debouncing...
|
|
for (uint8_t i = 0; i < 16; ++i) {
|
|
if (LL_GPIO_IsInputPinSet(port->GPIOx, port->GPIO_Pin) == 0)
|
|
return;
|
|
}
|
|
|
|
// enable timer counter
|
|
auto TIMx = port->TIMx;
|
|
LL_TIM_SetAutoReload(TIMx, (BITLEN + BITLEN/2) - 1);
|
|
LL_TIM_EnableCounter(TIMx);
|
|
|
|
// disable start bit interrupt
|
|
LL_EXTI_DisableIT_0_31(port->EXTI_Line);
|
|
}
|
|
}
|
|
|
|
static inline void _rx_fifo_clear()
|
|
{
|
|
rxWidx = 0;
|
|
rxRidx = 0;
|
|
}
|
|
|
|
static void _softserial_init_rx(const stm32_softserial_rx_port* port,
|
|
const etx_serial_init* params)
|
|
{
|
|
rxBitCount = 0;
|
|
rxBuffer = port->buffer.buffer;
|
|
rxBufLen = port->buffer.length;
|
|
_rx_fifo_clear();
|
|
|
|
// configure bit sample timer
|
|
LL_TIM_InitTypeDef timInit;
|
|
LL_TIM_StructInit(&timInit);
|
|
|
|
uint32_t freq = params->baudrate * 16;
|
|
if (!freq) return;
|
|
|
|
timInit.Prescaler = __LL_TIM_CALC_PSC(port->TIM_Freq, freq);
|
|
|
|
// TODO: enable_tim_clock(port->TIMx);
|
|
LL_TIM_Init(port->TIMx, &timInit);
|
|
|
|
LL_TIM_ClearFlag_UPDATE(port->TIMx);
|
|
LL_TIM_EnableIT_UPDATE(port->TIMx);
|
|
|
|
NVIC_SetPriority(port->TIM_IRQn, 0);
|
|
NVIC_EnableIRQ(port->TIM_IRQn);
|
|
|
|
LL_GPIO_InitTypeDef pinInit;
|
|
LL_GPIO_StructInit(&pinInit);
|
|
|
|
if (port->dir_GPIOx) {
|
|
pinInit.Pin = port->dir_Pin;
|
|
pinInit.Mode = LL_GPIO_MODE_OUTPUT;
|
|
LL_GPIO_Init(port->dir_GPIOx, &pinInit);
|
|
if (!port->dir_Input) {
|
|
LL_GPIO_ResetOutputPin(port->dir_GPIOx, port->dir_Pin);
|
|
} else {
|
|
LL_GPIO_SetOutputPin(port->dir_GPIOx, port->dir_Pin);
|
|
}
|
|
}
|
|
|
|
pinInit.Pin = port->GPIO_Pin;
|
|
pinInit.Mode = LL_GPIO_MODE_INPUT;
|
|
pinInit.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
|
|
pinInit.Pull = LL_GPIO_PULL_DOWN;
|
|
LL_GPIO_Init(port->GPIOx, &pinInit);
|
|
|
|
// Connect EXTI line to TELEMETRY RX pin
|
|
LL_SYSCFG_SetEXTISource(port->EXTI_Port, port->EXTI_SysLine);
|
|
|
|
// Configure EXTI for raising edge (start bit; assuming inverted serial)
|
|
_softserialPort = port;
|
|
stm32_exti_enable(port->EXTI_Line, LL_EXTI_TRIGGER_RISING, _softserial_exti);
|
|
}
|
|
|
|
static void _softserial_deinit_gpio(const stm32_softserial_rx_port* port)
|
|
{
|
|
// Reconfigure pin as input
|
|
LL_GPIO_InitTypeDef pinInit;
|
|
LL_GPIO_StructInit(&pinInit);
|
|
|
|
pinInit.Pin = port->GPIO_Pin;
|
|
pinInit.Mode = LL_GPIO_MODE_INPUT;
|
|
LL_GPIO_Init(port->GPIOx, &pinInit);
|
|
}
|
|
|
|
static void _softserial_deinit_rx(const stm32_softserial_rx_port* port)
|
|
{
|
|
stm32_exti_disable(port->EXTI_Line);
|
|
NVIC_DisableIRQ(port->TIM_IRQn);
|
|
LL_TIM_DeInit(port->TIMx);
|
|
}
|
|
|
|
static void* stm32_softserial_rx_init(void* hw_def, const etx_serial_init* params)
|
|
{
|
|
auto port = (const stm32_softserial_rx_port*)hw_def;
|
|
_softserial_init_rx(port, params);
|
|
return hw_def;
|
|
}
|
|
|
|
static void stm32_softserial_rx_deinit(void* ctx)
|
|
{
|
|
auto port = (const stm32_softserial_rx_port*)ctx;
|
|
|
|
if (port == _softserialPort) {
|
|
_softserial_deinit_rx(port);
|
|
}
|
|
|
|
_softserial_deinit_gpio(port);
|
|
_softserialPort = nullptr;
|
|
}
|
|
|
|
static inline bool _rx_fifo_full()
|
|
{
|
|
return ((rxWidx + 1) & (rxBufLen - 1)) == rxRidx;
|
|
}
|
|
|
|
static inline void _rx_fifo_push(uint8_t c)
|
|
{
|
|
rxBuffer[rxWidx] = c;
|
|
rxWidx = (rxWidx + 1) & (rxBufLen - 1);
|
|
}
|
|
|
|
static int stm32_softserial_rx_get_byte(void* ctx, uint8_t* data)
|
|
{
|
|
if (rxWidx == rxRidx) return 0;
|
|
|
|
*data = rxBuffer[rxRidx];
|
|
rxRidx = (rxRidx + 1) & (rxBufLen - 1);
|
|
|
|
return 1;
|
|
}
|
|
|
|
void stm32_softserial_rx_timer_isr(const stm32_softserial_rx_port* port)
|
|
{
|
|
auto TIMx = port->TIMx;
|
|
LL_TIM_ClearFlag_UPDATE(TIMx);
|
|
|
|
if (rxBitCount < 8) {
|
|
if (rxBitCount == 0) {
|
|
LL_TIM_SetAutoReload(TIMx, BITLEN - 1);
|
|
rxByte = 0;
|
|
}
|
|
else {
|
|
rxByte >>= 1;
|
|
}
|
|
|
|
if (LL_GPIO_IsInputPinSet(port->GPIOx, port->GPIO_Pin) == 0)
|
|
rxByte |= 0x80;
|
|
|
|
++rxBitCount;
|
|
}
|
|
else if (rxBitCount == 8) {
|
|
|
|
if (!_rx_fifo_full()) _rx_fifo_push(rxByte);
|
|
rxBitCount = 0;
|
|
|
|
// disable timer
|
|
LL_TIM_DisableCounter(TIMx);
|
|
|
|
// re-enable start bit interrupt
|
|
LL_EXTI_EnableIT_0_31(port->EXTI_Line);
|
|
}
|
|
}
|
|
|
|
static void stm32_softserial_rx_clear_rx_buffer(void* ctx)
|
|
{
|
|
rxWidx = 0;
|
|
rxRidx = 0;
|
|
}
|
|
|
|
const etx_serial_driver_t STM32SoftSerialRxDriver = {
|
|
.init = stm32_softserial_rx_init,
|
|
.deinit = stm32_softserial_rx_deinit,
|
|
.sendByte = nullptr,
|
|
.sendBuffer = nullptr,
|
|
.waitForTxCompleted = nullptr,
|
|
.enableRx = nullptr,
|
|
.getByte = stm32_softserial_rx_get_byte,
|
|
.getLastByte = nullptr,
|
|
.clearRxBuffer = stm32_softserial_rx_clear_rx_buffer,
|
|
.getBaudrate = nullptr,
|
|
.setReceiveCb = nullptr,
|
|
.setBaudrateCb = nullptr,
|
|
};
|
|
|
|
|
|
static inline void _set_level(stm32_softserial_tx_state* st, uint8_t v)
|
|
{
|
|
*st->pulse_ptr = v - 1;
|
|
#if defined(STM32_SUPPORT_32BIT_TIMERS)
|
|
st->pulse_ptr += st->pulse_inc;
|
|
#else
|
|
++st->pulse_ptr;
|
|
#endif
|
|
}
|
|
|
|
// TODO:
|
|
// - support for different word len / stop bit(s) / parity config
|
|
// - now: only 8N1 and 8E2
|
|
//
|
|
// max 11 changes 0 10 10 10 10 P 1
|
|
static void _conv_byte_8e2(stm32_softserial_tx_state* st, uint8_t b)
|
|
{
|
|
bool lev = 0;
|
|
uint8_t parity = 1;
|
|
|
|
uint8_t len = BITLEN; // max val: 10*16 < 256
|
|
for (uint8_t i = 0; i <= 9; i++) { // 8Bits + 1Parity
|
|
|
|
bool nlev = b & 1; // lsb first
|
|
parity = parity ^ (uint8_t)nlev;
|
|
|
|
if (lev == nlev) {
|
|
len += BITLEN;
|
|
} else {
|
|
_set_level(st, len);
|
|
len = BITLEN;
|
|
lev = nlev;
|
|
}
|
|
|
|
b = (b >> 1) | 0x80; // shift in ones for stop bit and parity
|
|
if (i == 7) b = b ^ parity; // lowest bit is one from previous line
|
|
}
|
|
|
|
// enlarge the last bit to be two stop bits long
|
|
_set_level(st, len + BITLEN);
|
|
|
|
// update size
|
|
--st->serial_size;
|
|
}
|
|
|
|
static void _conv_byte_8n1(stm32_softserial_tx_state* st, uint8_t b)
|
|
{
|
|
bool lev = 0;
|
|
uint8_t len = BITLEN; // max val: 10*16 < 256
|
|
for (uint8_t i = 0; i <= 8; i++) { // 8Bits
|
|
|
|
bool nlev = b & 1; // lsb first
|
|
if (lev == nlev) {
|
|
len += BITLEN;
|
|
} else {
|
|
_set_level(st, len);
|
|
len = BITLEN;
|
|
lev = nlev;
|
|
}
|
|
|
|
b = (b >> 1) | 0x80; // shift in ones for stop bit
|
|
}
|
|
|
|
// write the last bit
|
|
_set_level(st, len);
|
|
|
|
// update size
|
|
--st->serial_size;
|
|
}
|
|
|
|
|
|
// PXX1 PWM is encoded as a PWM signal
|
|
// with a fixed ON phase of 9us and a variable OFF phase
|
|
// depending on whether the bit is SET or RESET.
|
|
//
|
|
#define PXX1_FREQ 1000000 /* 1 MHz */
|
|
#define PXX1_PWM_ON 8 /* 8us */
|
|
#define PXX1_BIT_ZERO 16 /* 0 = 16us */
|
|
#define PXX1_BIT_ONE 24 /* 1 = 24us */
|
|
|
|
__attribute__ ((weak)) uint32_t __pxx1_get_inverter_comp() { return 0; }
|
|
|
|
static void _conv_byte_pxx1(stm32_softserial_tx_state* st, uint8_t b)
|
|
{
|
|
uint32_t bits = st->serial_size < 8 ? st->serial_size : 8;
|
|
|
|
for (uint8_t i = 0; i < bits; i++) {
|
|
// MSB first
|
|
if (b & 0x80)
|
|
_set_level(st, PXX1_BIT_ONE);
|
|
else
|
|
_set_level(st, PXX1_BIT_ZERO);
|
|
|
|
// next bit
|
|
b <<= 1;
|
|
}
|
|
|
|
st->serial_size -= bits;
|
|
}
|
|
|
|
// stm32_pulse_timer_t based TX implementation
|
|
static void* stm32_softserial_tx_init(void* hw_def, const etx_serial_init* params)
|
|
{
|
|
auto port = (const stm32_softserial_tx_port*)hw_def;
|
|
|
|
// Init state
|
|
auto st = port->st;
|
|
memset(port->st, 0, sizeof(stm32_softserial_tx_state));
|
|
|
|
bool polarity = params->polarity;
|
|
uint32_t freq = params->baudrate * 16;
|
|
uint32_t ocmode = LL_TIM_OCMODE_TOGGLE;
|
|
uint32_t cmp_val = 0;
|
|
|
|
switch(params->encoding) {
|
|
|
|
case ETX_Encoding_8N1:
|
|
st->conv_byte = _conv_byte_8n1;
|
|
break;
|
|
|
|
case ETX_Encoding_8E2:
|
|
st->conv_byte = _conv_byte_8e2;
|
|
break;
|
|
|
|
case ETX_Encoding_PXX1_PWM:
|
|
st->conv_byte = _conv_byte_pxx1;
|
|
freq = PXX1_FREQ;
|
|
polarity = false;
|
|
ocmode = LL_TIM_OCMODE_FORCED_INACTIVE;
|
|
cmp_val = PXX1_PWM_ON + __pxx1_get_inverter_comp();
|
|
break;
|
|
|
|
default:
|
|
return nullptr;
|
|
}
|
|
|
|
// prevent dividing by 0 in stm32_pulse_init()
|
|
if (freq == 0) return nullptr;
|
|
|
|
// Use a variable timer frequency to keep bitlen constant (16)
|
|
auto tim = port->tim;
|
|
stm32_pulse_init(tim, freq);
|
|
stm32_pulse_config_output(tim, polarity, ocmode, cmp_val);
|
|
|
|
#if defined(STM32_SUPPORT_32BIT_TIMERS)
|
|
if (IS_TIM_32B_COUNTER_INSTANCE(tim->TIMx)) {
|
|
st->pulse_inc = 2;
|
|
} else {
|
|
st->pulse_inc = 1;
|
|
}
|
|
#endif
|
|
|
|
return hw_def;
|
|
}
|
|
|
|
static void stm32_softserial_tx_deinit(void* ctx)
|
|
{
|
|
auto port = (const stm32_softserial_tx_port*)ctx;
|
|
stm32_pulse_deinit(port->tim);
|
|
}
|
|
|
|
static void stm32_softserial_tx_wait(void* ctx)
|
|
{
|
|
auto port = (const stm32_softserial_tx_port*)ctx;
|
|
auto timer = port->tim;
|
|
|
|
stm32_pulse_wait_for_completed(timer);
|
|
}
|
|
|
|
static void stm32_softserial_tx_send_buffer(void* ctx, const uint8_t* data, uint32_t size);
|
|
|
|
static void stm32_softserial_tx_send_byte(void* ctx, uint8_t byte)
|
|
{
|
|
stm32_softserial_tx_send_buffer(ctx, &byte, 1);
|
|
stm32_softserial_tx_wait(ctx);
|
|
}
|
|
|
|
static uint16_t _fill_pulses(stm32_softserial_tx_state* st)
|
|
{
|
|
st->pulse_ptr = (uint16_t*)st->pulse_buffer;
|
|
bool is_pxx1 = (st->conv_byte == _conv_byte_pxx1);
|
|
|
|
uint32_t size = st->serial_size;
|
|
|
|
// PXX1 uses a # bits rather than bytes
|
|
if (is_pxx1) {
|
|
//
|
|
size = (size + 7) / 8;
|
|
if (size > STM32_SOFTSERIAL_MAX_PULSES_TRANSITIONS / 8) // 12
|
|
size = STM32_SOFTSERIAL_MAX_PULSES_TRANSITIONS / 8; // 12
|
|
} else if (size > STM32_SOFTSERIAL_BUFFERED_PULSES) {
|
|
size = STM32_SOFTSERIAL_BUFFERED_PULSES;
|
|
}
|
|
|
|
for (uint8_t i = 0; i < size; i++) {
|
|
st->conv_byte(st, *st->serial_data++);
|
|
}
|
|
|
|
uint16_t length = st->pulse_ptr - (uint16_t*)st->pulse_buffer;
|
|
#if defined(STM32_SUPPORT_32BIT_TIMERS)
|
|
if (st->pulse_inc == 2) length >>= 1;
|
|
#endif
|
|
|
|
return length;
|
|
}
|
|
|
|
static bool stm32_softserial_tx_dma_tc_isr(void* ctx)
|
|
{
|
|
auto port = (const stm32_softserial_tx_port*)ctx;
|
|
auto tim = port->tim;
|
|
auto st = port->st;
|
|
|
|
if (!st->serial_size)
|
|
return false;
|
|
|
|
uint16_t length = _fill_pulses(st);
|
|
LL_DMA_SetDataLength(tim->DMAx, tim->DMA_Stream, length);
|
|
LL_DMA_EnableStream(tim->DMAx, tim->DMA_Stream);
|
|
|
|
return true;
|
|
}
|
|
|
|
static void stm32_softserial_tx_send_buffer(void* ctx, const uint8_t* data, uint32_t size)
|
|
{
|
|
auto port = (const stm32_softserial_tx_port*)ctx;
|
|
auto timer = port->tim;
|
|
if (!stm32_pulse_if_not_running_disable(timer))
|
|
return;
|
|
|
|
// transform serial payload into timer pulses/length
|
|
auto st = port->st;
|
|
st->serial_data = data;
|
|
st->serial_size = size;
|
|
|
|
auto length = _fill_pulses(st);
|
|
|
|
if (st->serial_size > 0 && timer->DMA_TC_CallbackPtr) {
|
|
auto closure = timer->DMA_TC_CallbackPtr;
|
|
closure->cb = stm32_softserial_tx_dma_tc_isr;
|
|
closure->ctx = ctx;
|
|
}
|
|
|
|
// Start DMA request and re-enable timer
|
|
const void* pulses = st->pulse_buffer;
|
|
|
|
// TODO: save encoding
|
|
uint32_t ocmode = LL_TIM_OCMODE_TOGGLE;
|
|
uint32_t cmp_val = 0;
|
|
|
|
// dirty hack...
|
|
if (st->conv_byte == _conv_byte_pxx1) {
|
|
ocmode = LL_TIM_OCMODE_PWM1;
|
|
cmp_val = PXX1_PWM_ON + __pxx1_get_inverter_comp();
|
|
}
|
|
|
|
stm32_pulse_start_dma_req(timer, pulses, length, ocmode, cmp_val);
|
|
}
|
|
|
|
void stm32_softserial_set_polarity(void* ctx, uint8_t polarity)
|
|
{
|
|
auto port = (const stm32_softserial_tx_port*)ctx;
|
|
auto timer = port->tim;
|
|
|
|
stm32_pulse_set_polarity(timer, polarity);
|
|
}
|
|
|
|
const etx_serial_driver_t STM32SoftSerialTxDriver = {
|
|
.init = stm32_softserial_tx_init,
|
|
.deinit = stm32_softserial_tx_deinit,
|
|
.sendByte = stm32_softserial_tx_send_byte,
|
|
.sendBuffer = stm32_softserial_tx_send_buffer,
|
|
.txCompleted = nullptr,
|
|
.waitForTxCompleted = stm32_softserial_tx_wait,
|
|
.enableRx = nullptr, // TODO: combine with EXTI / Timer implementation? (S.PORT INV RX)
|
|
.getByte = nullptr,
|
|
.clearRxBuffer = nullptr, // TODO: same as enableRx
|
|
.getBaudrate = nullptr,
|
|
.setBaudrate = nullptr,
|
|
.setPolarity = stm32_softserial_set_polarity,
|
|
.setHWOption = nullptr,
|
|
.setReceiveCb = nullptr,
|
|
.setIdleCb = nullptr,
|
|
.setBaudrateCb = nullptr,
|
|
};
|