Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

serial 1wire passthrough for ESCs with the BlHeli bootloader #1159

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Expand Up @@ -238,6 +238,7 @@ COMMON_SRC = build_config.c \
io/rc_controls.c \
io/rc_curves.c \
io/serial.c \
io/serial_1wire.c \
io/serial_cli.c \
io/serial_msp.c \
io/statusindicator.c \
Expand Down
113 changes: 113 additions & 0 deletions docs/1wire.md
@@ -0,0 +1,113 @@
# 1-wire passthrough esc programming

### ESCs must have the BlHeli Bootloader.

If your ESCs didn't come with BlHeli Bootloader, you'll need to flash them with an ArduinoISP programmer first. [Here's a guide](http://bit.ly/blheli-f20).

This is the option you need to select for the bootloader:

![Flashing BlHeli Bootloader](assets/images/blheli-bootloader.png)

Currently supported on the SPRACINGF3, STM32F3DISCOVERY, NAZE32 (including clones such as the FLIP32) and CC3D.

## Wiring

- For the NAZE, no external wiring is necessary. Simply plugin the board via USB cable.

- For the CC3D, connect [a USB to UART adapter](http://bit.ly/cf-cp2102) to the main port. If you need one, I prefer the [CP2102](http://bit.ly/cf-cp2102) as it is cheap and [the driver](https://www.silabs.com/products/mcu/Pages/USBtoUARTBridgeVCPDrivers.aspx) is readily available.

- This is how you plug in the USB/UART adapter on the CC3D. Be sure to also plug in the normal USB cable as well.

![Flashing BlHeli Bootloader](assets/images/serial1wire-cc3d-wiring.jpg)

- In the case that your board does not power on fully without a battery attached, it is OK to attach the battery before following the steps below. However, it may not be necessary in all cases.

## Usage

How to for the CC3D: [https://youtu.be/fmUPL1lRcss](https://youtu.be/fmUPL1lRcss)

- Plug in the USB cable and connect to your board with the CleanFlight configurator.

- Open the CLI tab, then run: `1wire <esc index>`

E.g. to connect to the ESC on your flight controller's port #1, run the command:

```
1wire 1
```

- Click "Disconnect" in the CleanFlight configurator. Do not power down your board.

- Note, in the future it may be possible to configure your ESCs directly in CleanFlight.

- Open the BlHeli Suite.

- Ensure you have selected the correct Atmel or SILABS "(USB/Com)" option under the "Select ATMEL / SILABS Interface" menu option.

- Ensure you have the correct port selected.

- On the NAZE, this port will be the same COM port used by the CleanFlight configurator.

- On the CC3D, this port will be your USB to UART serial adapter.

- Click "Connect" and wait for the connection to complete. If you get a COM error, hit connect again. It will probably work.

- Click "Read Setup"

- Use BlHeli suite as normal.

- When you're finished with one ESC, click "Disconnect"

- Unplug the flight control board from Blheli.

- On the CC3D this means you can unplug just the USB/UART adapter, leaving the USB cable attached. The advantage is that Cleanflight will stay connected and you'll only have to reconnect BlHeli.

- On the NAZE you'll have to unplug USB cable and start over on the next ESC.

## Implementing and Configuring targets

The following parameters can be used to enable and configure this in the related target.h file:

USE_SERIAL_1WIRE Enables the 1wire code, defined in target.h


- For new targets

- in `target.h`

```
// Turn on serial 1wire passthrough
#define USE_SERIAL_1WIRE
// How many escs does this board support?
#define ESC_COUNT 6
// STM32F3DISCOVERY TX - PC3 connects to UART RX
#define S1W_TX_GPIO GPIOC
#define S1W_TX_PIN GPIO_Pin_3
// STM32F3DISCOVERY RX - PC1 connects to UART TX
#define S1W_RX_GPIO GPIOC
#define S1W_RX_PIN GPIO_Pin_1
```

- in `serial_1wire.c`

```
// Define your esc hardware
#if defined(STM32F3DISCOVERY) && !(defined(CHEBUZZF3))
const escHardware_t escHardware[ESC_COUNT] = {
{ GPIOD, 12 },
{ GPIOD, 13 },
{ GPIOD, 14 },
{ GPIOD, 15 },
{ GPIOA, 1 },
{ GPIOA, 2 }
};
```

## Development Notes

On the STM32F3DISCOVERY, an external pullup on the ESC line may be necessary. I needed a 3v, 4.7k pullup.

## Todo

Implement the BlHeli bootloader configuration protocol in the CleanFlight GUI

1 change: 1 addition & 0 deletions docs/Cli.md
Expand Up @@ -73,6 +73,7 @@ Re-apply any new defaults as desired.

| `Command` | Description |
|------------------|------------------------------------------------|
| `1wire <esc>` | passthrough 1wire to the specified esc |
| `adjrange` | show/set adjustment ranges settings |
| `aux` | show/set aux settings |
| `mmix` | design custom motor mixer |
Expand Down
Binary file added docs/assets/images/blheli-bootloader.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/serial1wire-cc3d-wiring.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
240 changes: 240 additions & 0 deletions src/main/io/serial_1wire.c
@@ -0,0 +1,240 @@
/*
* This file is part of Cleanflight.
*
* Cleanflight is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Cleanflight 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.
*
* You should have received a copy of the GNU General Public License
* along with Cleanflight. If not, see <http://www.gnu.org/licenses/>.
*
* Ported from https://github.com/4712/BLHeliSuite/blob/master/Interfaces/Arduino1Wire/Source/Arduino1Wire_C/Arduino1Wire.c
* by Nathan Tsoi <nathan@vertile.com>
*/

#include <stdbool.h>

#include "platform.h"

#ifdef USE_SERIAL_1WIRE

#include "drivers/gpio.h"
#include "drivers/inverter.h"
#include "drivers/light_led.h"
#include "drivers/system.h"
#include "io/serial_1wire.h"

const escHardware_t escHardware[ESC_COUNT] = {
// Figure out esc clocks and pins, extrapolated from timer.c
// Periphs could be pulled progmatically... but I'll leave that for another exercise
#if defined(STM32F3DISCOVERY) && !(defined(CHEBUZZF3))
{ GPIOD, 12 },
{ GPIOD, 13 },
{ GPIOD, 14 },
{ GPIOD, 15 },
{ GPIOA, 1 },
{ GPIOA, 2 }
#elif defined(CJMCU) || defined(EUSTM32F103RC) || defined(NAZE) || defined(OLIMEXINO) || defined(PORT103R)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CJMCU is an FC with brushed motor outputs. The definition here is useless. The ports for the motor outputs are also quite different to the Naze. The Naze can have up to 8 motors (esc's) connected.

{ GPIOA, 8 },
{ GPIOA, 11 },
{ GPIOB, 6 },
{ GPIOB, 7 },
{ GPIOB, 8 },
{ GPIOB, 9 }
#elif CC3D
{ GPIOB, 9 },
{ GPIOB, 8 },
{ GPIOB, 7 },
{ GPIOA, 8 },
{ GPIOB, 4 },
{ GPIOA, 2 }
#elif SPRACINGF3
{ GPIOA, 6 },
{ GPIOA, 7 },
{ GPIOA, 11 },
{ GPIOA, 12 },
{ GPIOB, 8 },
{ GPIOB, 9 },
{ GPIOA, 2 },
{ GPIOA, 3 }
#endif
};

static void gpio_set_mode(GPIO_TypeDef* gpio, uint16_t pin, GPIO_Mode mode) {
gpio_config_t cfg;
cfg.pin = pin;
cfg.mode = mode;
cfg.speed = Speed_10MHz;
gpioInit(gpio, &cfg);
}

#ifdef STM32F10X
static volatile uint32_t original_cr_mask, in_cr_mask, out_cr_mask;
static __IO uint32_t *cr;
static void gpio_prep_vars(uint16_t escIndex)
{
GPIO_TypeDef *gpio = escHardware[escIndex].gpio;
uint32_t pinpos = escHardware[escIndex].pinpos;
// mask out extra bits from pinmode, leaving just CNF+MODE
uint32_t inmode = Mode_IPU & 0x0F;
uint32_t outmode = (Mode_Out_PP & 0x0F) | Speed_10MHz;
// reference CRL or CRH, depending whether pin number is 0..7 or 8..15
cr = &gpio->CRL + (pinpos / 8);
// offset to CNF and MODE portions of CRx register
uint32_t shift = (pinpos % 8) * 4;
// Read out current CRx value
original_cr_mask = in_cr_mask = out_cr_mask = *cr;
// Mask out 4 bits
in_cr_mask &= ~(0xF << shift);
out_cr_mask &= ~(0xF << shift);
// save current pinmode
in_cr_mask |= inmode << shift;
out_cr_mask |= outmode << shift;
}

static void gpioSetOne(uint16_t escIndex, GPIO_Mode mode) {
// reference CRL or CRH, depending whether pin number is 0..7 or 8..15
if (mode == Mode_IPU) {
*cr = in_cr_mask;
escHardware[escIndex].gpio->ODR |= (1U << escHardware[escIndex].pinpos);
}
else {
*cr = out_cr_mask;
}
}
#endif

#define disable_hardware_uart __disable_irq()
#define enable_hardware_uart __enable_irq()
#define ESC_HI(escIndex) ((escHardware[escIndex].gpio->IDR & (1U << escHardware[escIndex].pinpos)) != (uint32_t)Bit_RESET)
#define RX_HI ((S1W_RX_GPIO->IDR & S1W_RX_PIN) != (uint32_t)Bit_RESET)
#define ESC_SET_HI(escIndex) escHardware[escIndex].gpio->BSRR = (1U << escHardware[escIndex].pinpos)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity, why don't you use the APIs in "stm32fxxx_gpio"?
Nicely done! @nathantsoi @4712 :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the code review @samguns! I did try that originally (e.g. https://github.com/nathantsoi/stm32-serial-1wire-passthrough/blob/master/src/serial_1wire.c#L128), but I couldn't get fast enough toggle speeds when switching modes on the 1wire line. Not sure why, I would think it would be plenty fast... but removing the loops that handle configuration of multiple pins in those libraries seems to have resolved the issue. Unfortunately, it did make this code a bit more messy: (ie. gpio_prep_vars)

#define ESC_SET_LO(escIndex) escHardware[escIndex].gpio->BRR = (1U << escHardware[escIndex].pinpos)
#define TX_SET_HIGH S1W_TX_GPIO->BSRR = S1W_TX_PIN
#define TX_SET_LO S1W_TX_GPIO->BRR = S1W_TX_PIN

#ifdef STM32F303xC
#define ESC_INPUT(escIndex) escHardware[escIndex].gpio->MODER &= ~(GPIO_MODER_MODER0 << (escHardware[escIndex].pinpos * 2))
#define ESC_OUTPUT(escIndex) escHardware[escIndex].gpio->MODER |= GPIO_Mode_OUT << (escHardware[escIndex].pinpos * 2)
#endif

#ifdef STM32F10X
#define ESC_INPUT(escIndex) gpioSetOne(escIndex, Mode_IPU)
#define ESC_OUTPUT(escIndex) gpioSetOne(escIndex, Mode_Out_PP)
#endif

#if defined(STM32F3DISCOVERY)
#define LED_PRGMR_RX GPIO_Pin_8
#define LED_PRGMR_TX GPIO_Pin_10
// Top Left LD4, PE8 (blue)-- from programmer (RX)
#define RX_LED_OFF GPIOE->BRR = LED_PRGMR_RX
#define RX_LED_ON GPIOE->BSRR = LED_PRGMR_RX
// Top Right LD5, PE10 (orange) -- to programmer (TX)
#define TX_LED_OFF GPIOE->BRR = LED_PRGMR_TX
#define TX_LED_ON GPIOE->BSRR = LED_PRGMR_TX
static void ledInitDebug(void)
{
uint32_t pinmask = LED_PRGMR_RX|LED_PRGMR_TX;
GPIO_DeInit(GPIOE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOE, ENABLE);
gpio_set_mode(GPIOE, pinmask, Mode_Out_PP);
GPIOE->BRR = pinmask;
}
#else
#define RX_LED_OFF LED0_OFF
#define RX_LED_ON LED0_ON
#define TX_LED_OFF LED1_OFF
#define TX_LED_ON LED1_ON
#endif

// This method translates 2 wires (a tx and rx line) to 1 wire, by letting the
// RX line control when data should be read or written from the single line
void usb1WirePassthrough(int8_t escIndex)
{
#ifdef STM32F3DISCOVERY
ledInitDebug();
#endif

//Disable all interrupts
disable_hardware_uart;

//Turn off the inverter, if necessary
#if defined(INVERTER) && defined(SERIAL_1WIRE_USE_MAIN)
INVERTER_OFF;
#endif

// reset all the pins
GPIO_ResetBits(S1W_RX_GPIO, S1W_RX_PIN);
GPIO_ResetBits(S1W_TX_GPIO, S1W_TX_PIN);
GPIO_ResetBits(escHardware[escIndex].gpio, (1U << escHardware[escIndex].pinpos));
// configure gpio
gpio_set_mode(S1W_RX_GPIO, S1W_RX_PIN, Mode_IPU);
gpio_set_mode(S1W_TX_GPIO, S1W_TX_PIN, Mode_Out_PP);
gpio_set_mode(escHardware[escIndex].gpio, (1U << escHardware[escIndex].pinpos), Mode_IPU);
// hey user, turn on your ESC now

#ifdef STM32F10X
// reset our gpio register pointers and bitmask values
gpio_prep_vars(escIndex);
#endif

// Wait for programmer to go from 1 -> 0 indicating incoming data
while(RX_HI);
while(1) {
// A new iteration on this loop starts when we have data from the programmer (read_programmer goes low)
// Setup escIndex pin to send data, pullup is the default
ESC_OUTPUT(escIndex);
// Write the first bit
ESC_SET_LO(escIndex);
// Echo on the programmer tx line
TX_SET_LO;
//set LEDs
RX_LED_OFF;
TX_LED_ON;
// Wait for programmer to go 0 -> 1
uint32_t ct=3000;
while(!RX_HI) {
ct--;
if (ct==0) {
// Programmer RX -- unneeded as we explicity set this mode above
// gpio_set_mode(S1W_RX_GPIO, S1W_RX_PIN, Mode_IPU);
// Programmer TX
gpio_set_mode(S1W_TX_GPIO, S1W_TX_PIN, Mode_AF_PP);
#ifdef STM32F10X
*cr = original_cr_mask;
#endif
#if defined(INVERTER) && defined(SERIAL_1WIRE_USE_MAIN)
INVERTER_ON;
#endif
// Enable Hardware UART
enable_hardware_uart;
return;
}
}
// Programmer is high, end of bit
// Echo to the esc
ESC_SET_HI(escIndex);
// Listen to the escIndex, input mode, pullup resistor is on
ESC_INPUT(escIndex);
TX_LED_OFF;
// Listen to the escIndex while there is no data from the programmer
while (RX_HI) {
if (ESC_HI(escIndex)) {
TX_SET_HIGH;
RX_LED_OFF;
}
else {
TX_SET_LO;
RX_LED_ON;
}
}
}
}

#endif