diff --git a/.gitmodules b/.gitmodules index f1a012736c..5c02dc7786 100644 --- a/.gitmodules +++ b/.gitmodules @@ -44,3 +44,7 @@ path = firmware/ext/build-tools url = https://github.com/FOME-Tech/build-tools.git branch = main +[submodule "firmware/ext/g0_firmware"] + path = firmware/ext/g0_firmware + url = https://github.com/dynfer/g0-extension.git + branch = main diff --git a/firmware/config/boards/atlas/board.mk b/firmware/config/boards/atlas/board.mk index 13acecfceb..382f7ca585 100644 --- a/firmware/config/boards/atlas/board.mk +++ b/firmware/config/boards/atlas/board.mk @@ -13,7 +13,7 @@ DDEFS += -DFIRMWARE_ID=\"atlas\" # DDEFS += -DTRIGGER_SCOPE DDEFS += -DEFI_SOFTWARE_KNOCK=TRUE -DSTM32_ADC_USE_ADC3=TRUE -DDEFS += -DEFI_SDC_DEVICE=SDCD1 +DDEFS += -DEFI_SDC_DEVICE=SDCD1 -DEFI_MAX_31855=TRUE # Atlas's LSE runs in bypass mode (doesn't use OSC IN pin) DDEFS += -DSTM32_LSE_BYPASS diff --git a/firmware/config/boards/atlas/board_configuration.cpp b/firmware/config/boards/atlas/board_configuration.cpp index babe02c337..2e561a5853 100644 --- a/firmware/config/boards/atlas/board_configuration.cpp +++ b/firmware/config/boards/atlas/board_configuration.cpp @@ -137,6 +137,12 @@ void setBoardConfigOverrides() { engineConfiguration->spi4misoPin = Gpio::E5; engineConfiguration->spi4mosiPin = Gpio::E6; + // G0 extension firmware SPI + engineConfiguration->is_enabled_spi_5 = true; + engineConfiguration->spi5sckPin = Gpio::F7; + engineConfiguration->spi5misoPin = Gpio::F8; + engineConfiguration->spi5mosiPin = Gpio::F9; + // Clear SPI SD card pins in case it's enabled from porting a Proteus tune if (engineConfiguration->sdCardSpiDevice || isBrainPinValid(engineConfiguration->sdCardCsPin)) { engineConfiguration->is_enabled_spi_3 = false; diff --git a/firmware/config/boards/atlas/compile_atlas.sh b/firmware/config/boards/atlas/compile_atlas.sh index 19138ef761..e7dc895601 100755 --- a/firmware/config/boards/atlas/compile_atlas.sh +++ b/firmware/config/boards/atlas/compile_atlas.sh @@ -1,5 +1,51 @@ #!/bin/bash +set -e + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) +FW_DIR=$(readlink -f "$SCRIPT_DIR/../../..") +REPO_DIR=$(readlink -f "$FW_DIR/..") +G0_DIR="$FW_DIR/ext/g0_firmware" + export USE_OPENBLT=yes +git -C "$REPO_DIR" submodule sync firmware/ext/g0_firmware +if [ "${UPDATE_G0_FIRMWARE-yes}" = "yes" ]; then + git -C "$REPO_DIR" submodule update --init --recursive --remote firmware/ext/g0_firmware +else + git -C "$REPO_DIR" submodule update --init --recursive firmware/ext/g0_firmware +fi +git -C "$REPO_DIR" submodule update --init --depth=1 firmware/ext/build-tools + +UNAME_SM=$(uname -sm) +case "$UNAME_SM" in + "Darwin "*) + COMPILER_PLATFORM=arm-gnu-toolchain-11.3.rel1-darwin-x86_64-arm-none-eabi + ;; + "Linux x86_64") + COMPILER_PLATFORM=arm-gnu-toolchain-11.3.rel1-x86_64-arm-none-eabi + ;; + *) + echo "Unsupported compiler platform: $UNAME_SM" + exit 1 + ;; +esac + +G0_TRGT="$FW_DIR/ext/build-tools/$COMPILER_PLATFORM/bin/arm-none-eabi-" +[ -x "${G0_TRGT}g++" ] || { echo "Compiler not found at ${G0_TRGT}g++"; exit 1; } + +make -C "$G0_DIR" -j$(nproc) TRGT="$G0_TRGT" USE_OPT="-Os -ggdb -fomit-frame-pointer -falign-functions=16 --specs=nosys.specs" for_fome_image + +G0_IMAGE_HEADER="$G0_DIR/for_fome/g0_firmware_image.h" +G0_SPI_APP_PROTOCOL="$G0_DIR/spi_app_protocol.cpp" +if [ ! -f "$G0_SPI_APP_PROTOCOL" ]; then + G0_SPI_APP_PROTOCOL="$G0_DIR/source/spi_app_protocol.cpp" +fi +G0_APP_VERSION=$(sed -nE 's/.*(APP_VERSION|appVersion) = ([0-9]+)U;.*/\2/p' "$G0_SPI_APP_PROTOCOL" | head -n1) +[ -n "$G0_APP_VERSION" ] || { echo "Unable to determine G0 app version"; exit 1; } +if ! grep -q 'build_g0_extension_version' "$G0_IMAGE_HEADER"; then + printf '\nstatic const unsigned int build_g0_extension_version = %sU;\n' "$G0_APP_VERSION" >> "$G0_IMAGE_HEADER" +fi + +cd "$SCRIPT_DIR" bash ../common_make.sh atlas ARCH_STM32H7 diff --git a/firmware/config/boards/atlas/connectors/g0.yaml b/firmware/config/boards/atlas/connectors/g0.yaml new file mode 100644 index 0000000000..749e8cc6e3 --- /dev/null +++ b/firmware/config/boards/atlas/connectors/g0.yaml @@ -0,0 +1,80 @@ +pins: + - id: EFI_ADC_20 + class: analog_inputs + ts_name: G0 Analog 1 + + - id: EFI_ADC_21 + class: analog_inputs + ts_name: G0 Analog 2 + + - id: EFI_ADC_22 + class: analog_inputs + ts_name: G0 Analog 3 + + - id: EFI_ADC_23 + class: analog_inputs + ts_name: G0 Analog 4 + + - id: EFI_ADC_24 + class: analog_inputs + ts_name: G0 Analog 5 + + - id: EFI_ADC_25 + class: analog_inputs + ts_name: G0 Analog 6 + + - id: EFI_ADC_26 + class: analog_inputs + ts_name: G0 Analog 7 + + - id: EFI_ADC_27 + class: analog_inputs + ts_name: G0 Analog 8 + + - id: EFI_ADC_28 + class: analog_inputs + ts_name: G0 Analog Temp 1 + + - id: EFI_ADC_29 + class: analog_inputs + ts_name: G0 Analog Temp 2 + + - id: EFI_ADC_30 + class: analog_inputs + ts_name: G0 Analog Temp 3 + + - id: EFI_ADC_31 + class: analog_inputs + ts_name: G0 Analog Temp 4 + + - id: G0_DIGITAL_IN_0 + class: switch_inputs + ts_name: G0 Digital 1 + + - id: G0_DIGITAL_IN_1 + class: switch_inputs + ts_name: G0 Digital 2 + + - id: G0_DIGITAL_IN_2 + class: switch_inputs + ts_name: G0 Digital 3 + + - id: G0_DIGITAL_IN_3 + class: switch_inputs + ts_name: G0 Digital 4 + + - id: G0_LOWSIDE_0 + class: outputs + ts_name: G0 Lowside 1 + + - id: G0_LOWSIDE_1 + class: outputs + ts_name: G0 Lowside 2 + + - id: G0_LOWSIDE_2 + class: outputs + ts_name: G0 Lowside 3 + + - id: G0_LOWSIDE_3 + class: outputs + ts_name: G0 Lowside 4 diff --git a/firmware/config/boards/atlas/prepend.txt b/firmware/config/boards/atlas/prepend.txt index a10469f971..82720084b5 100644 --- a/firmware/config/boards/atlas/prepend.txt +++ b/firmware/config/boards/atlas/prepend.txt @@ -1,5 +1,5 @@ #define ts_show_full_pinout false -#define ts_show_egt false +#define ts_show_egt true #define ts_show_analog_divider false #define ts_show_spi false #define ts_show_can_pins false diff --git a/firmware/config/boards/atlas_meta.h b/firmware/config/boards/atlas_meta.h index 52839fe571..59c36fc777 100644 --- a/firmware/config/boards/atlas_meta.h +++ b/firmware/config/boards/atlas_meta.h @@ -65,3 +65,16 @@ #define PROTEUS_DIGITAL_4 Gpio::E11 #define PROTEUS_DIGITAL_5 Gpio::E12 #define PROTEUS_DIGITAL_6 Gpio::E13 + +#define ATLAS_G0_ANALOG_1 EFI_ADC_20 +#define ATLAS_G0_ANALOG_2 EFI_ADC_21 +#define ATLAS_G0_ANALOG_3 EFI_ADC_22 +#define ATLAS_G0_ANALOG_4 EFI_ADC_23 +#define ATLAS_G0_ANALOG_5 EFI_ADC_24 +#define ATLAS_G0_ANALOG_6 EFI_ADC_25 +#define ATLAS_G0_ANALOG_7 EFI_ADC_26 +#define ATLAS_G0_ANALOG_8 EFI_ADC_27 +#define ATLAS_G0_ANALOG_TEMP_1 EFI_ADC_28 +#define ATLAS_G0_ANALOG_TEMP_2 EFI_ADC_29 +#define ATLAS_G0_ANALOG_TEMP_3 EFI_ADC_30 +#define ATLAS_G0_ANALOG_TEMP_4 EFI_ADC_31 diff --git a/firmware/console/binary/output_channels.txt b/firmware/console/binary/output_channels.txt index 6f5f8674c7..574a3aec9a 100644 --- a/firmware/console/binary/output_channels.txt +++ b/firmware/console/binary/output_channels.txt @@ -29,6 +29,7 @@ struct_no_prefix output_channels_s bit ignitionFault;Error: Ignition Fault bit isUsbConnected;isUsbConnected\nOriginal reason for this is to check if USB is connected from Lua bit dfcoActive;DFCO: Active + bit g0Present;G0 extension: Present uint16_t RPMValue;@@GAUGE_NAME_RPM@@;"RPM",1, 0, 0, 8000, 0 diff --git a/firmware/console/status_loop.cpp b/firmware/console/status_loop.cpp index 570dcf4676..c35edd0a2e 100644 --- a/firmware/console/status_loop.cpp +++ b/firmware/console/status_loop.cpp @@ -38,6 +38,9 @@ #include "main_trigger_callback.h" #include "spark_logic.h" #include "gitversion.h" +#if HW_ATLAS +#include "hw_layer/g0_extension/g0_extension_io.h" +#endif #include "can_hw.h" #include "periodic_thread_controller.h" #include "binary_logging.h" @@ -492,6 +495,11 @@ void updateTunerStudioState() { tsOutputChannels->gyroYaw = engine->sensors.accelerometer.yawRate; tsOutputChannels->turboSpeed = Sensor::getOrZero(SensorType::TurbochargerSpeed); +#if HW_ATLAS + tsOutputChannels->g0Present = isG0ExtensionPresent(); +#else + tsOutputChannels->g0Present = false; +#endif extern FrequencySensor vehicleSpeedSensor; tsOutputChannels->vssEdgeCounter = vehicleSpeedSensor.eventCounter; diff --git a/firmware/controllers/algo/rusefi_hw_enums.h b/firmware/controllers/algo/rusefi_hw_enums.h index 69a82e1dc4..33b62cfd28 100644 --- a/firmware/controllers/algo/rusefi_hw_enums.h +++ b/firmware/controllers/algo/rusefi_hw_enums.h @@ -234,6 +234,27 @@ enum class Gpio : uint16_t { TLE6240_PIN_15 = 213, TLE6240_PIN_16 = 214, + G0_ANALOG_1 = 215, + G0_ANALOG_2 = 216, + G0_ANALOG_3 = 217, + G0_ANALOG_4 = 218, + G0_ANALOG_5 = 219, + G0_ANALOG_6 = 220, + G0_ANALOG_7 = 221, + G0_ANALOG_8 = 222, + G0_ANALOG_TEMP_1 = 223, + G0_ANALOG_TEMP_2 = 224, + G0_ANALOG_TEMP_3 = 225, + G0_ANALOG_TEMP_4 = 226, + G0_DIGITAL_IN_0 = 227, + G0_DIGITAL_IN_1 = 228, + G0_DIGITAL_IN_2 = 229, + G0_DIGITAL_IN_3 = 230, + G0_LOWSIDE_0 = 231, + G0_LOWSIDE_1 = 232, + G0_LOWSIDE_2 = 233, + G0_LOWSIDE_3 = 234, + CAN_INPUT_0 = 249, CAN_INPUT_1 = 250, CAN_INPUT_2 = 251, diff --git a/firmware/controllers/controllers.mk b/firmware/controllers/controllers.mk index 4a7430cbaa..c769b3bac9 100644 --- a/firmware/controllers/controllers.mk +++ b/firmware/controllers/controllers.mk @@ -73,4 +73,3 @@ CONTROLLERS_INC=\ $(CONTROLLERS_DIR)/actuators \ $(CONTROLLERS_DIR)/actuators/gppwm \ $(CONTROLLERS_DIR)/serial \ - diff --git a/firmware/controllers/system/efi_gpio.cpp b/firmware/controllers/system/efi_gpio.cpp index e737f215d8..33d4b45822 100644 --- a/firmware/controllers/system/efi_gpio.cpp +++ b/firmware/controllers/system/efi_gpio.cpp @@ -10,6 +10,9 @@ #include "engine_sniffer.h" #include "drivers/gpio/gpio_ext.h" +#if EFI_PROD_CODE && HW_ATLAS +#include "hw_layer/g0_extension/g0_extension_io.h" +#endif #if HW_HELLEN #include "hellen_meta.h" @@ -458,6 +461,13 @@ void OutputPin::setValue(int logicValue) { int electricalValue = getElectricalValue(logicValue, m_mode); #if EFI_PROD_CODE +#if HW_ATLAS + if (isG0ExtensionLowsidePin(m_brainPin)) { + setG0ExtensionLowsideOutput(m_brainPin, electricalValue != 0); + return; + } +#endif + #if (BOARD_EXT_GPIOCHIPS > 0) if (!this->ext) { setOnchipValue(electricalValue); @@ -575,6 +585,13 @@ void OutputPin::initPin( setDefaultPinState(outputMode); #if EFI_PROD_CODE +#if HW_ATLAS + if (isG0ExtensionLowsidePin(m_brainPin)) { + brain_pin_markUsed(m_brainPin, msg); + return; + } +#endif + iomode_t ioMode = (outputMode == OM_DEFAULT || outputMode == OM_INVERTED) ? PAL_MODE_OUTPUT_PUSHPULL : PAL_MODE_OUTPUT_OPENDRAIN; @@ -621,6 +638,17 @@ void OutputPin::deInit() { efiPrintf("unregistering %s", hwPortname(m_brainPin)); +#if EFI_PROD_CODE +#if HW_ATLAS + if (isG0ExtensionLowsidePin(m_brainPin)) { + disableG0ExtensionLowsideOutput(m_brainPin); + brain_pin_markUnused(m_brainPin); + m_brainPin = Gpio::Unassigned; + return; + } +#endif +#endif + efiSetPadUnused(m_brainPin); // Clear the pin so that it won't get set any more diff --git a/firmware/controllers/system/g0_extension_firmware.cpp b/firmware/controllers/system/g0_extension_firmware.cpp new file mode 100644 index 0000000000..8b58d97a6d --- /dev/null +++ b/firmware/controllers/system/g0_extension_firmware.cpp @@ -0,0 +1,128 @@ +#include "g0_extension_firmware.h" +#include "g0_extension_firmware_impl.h" + +#if HW_ATLAS && HAL_USE_SPI + +#include "hardware.h" +#include "mpu_util.h" + +namespace g0_extension_firmware { +namespace { + +static SPIConfig g0ExtensionSpiConfig = { + .circular = false, + .end_cb = NULL, + .ssport = NULL, + .sspad = 0, + .cfg1 = 7 | SPI_CFG1_MBR_2 | SPI_CFG1_MBR_1 | SPI_CFG1_MBR_0, + .cfg2 = 0}; + +static NO_CACHE uint8_t g0ExtensionSpiTxBuffer[SpiDmaBufferSize]; +static NO_CACHE uint8_t g0ExtensionSpiRxBuffer[SpiDmaBufferSize]; + +} // namespace + +SPIConfig& spiConfig() { + return g0ExtensionSpiConfig; +} + +uint8_t* txBuffer() { + return g0ExtensionSpiTxBuffer; +} + +uint8_t* rxBuffer() { + return g0ExtensionSpiRxBuffer; +} + +void setControlPin(brain_pin_e pin, bool value) { + palWritePad(getHwPort("g0", pin), getHwPin("g0", pin), value); +} + +void initControlPins() { + efiSetPadMode("G0 BOOT", BootPin, PAL_MODE_OUTPUT_PUSHPULL); + efiSetPadMode("G0 RESET", ResetPin, PAL_MODE_OUTPUT_PUSHPULL); + + setControlPin(BootPin, false); + setControlPin(ResetPin, true); +} + +void releaseControlPins() { + setControlPin(BootPin, false); + setControlPin(ResetPin, true); + + efiSetPadModeWithoutOwnershipAcquisition("G0 BOOT", BootPin, PAL_MODE_INPUT); + efiSetPadModeWithoutOwnershipAcquisition("G0 RESET", ResetPin, PAL_MODE_INPUT); +} + +void resetExtension(bool bootloaderMode) { + setControlPin(BootPin, bootloaderMode); + chThdSleepMilliseconds(5); + + setControlPin(ResetPin, false); + chThdSleepMilliseconds(20); + + setControlPin(ResetPin, true); + chThdSleepMilliseconds(100); +} + +} // namespace g0_extension_firmware + +#endif + +bool loadG0ExtensionFirmware(bool forceUpdate) { +#if HW_ATLAS && HAL_USE_SPI +#if G0_EXTENSION_FIRMWARE_IMAGE_AVAILABLE + g0_extension_firmware::initControlPins(); + + turnOnSpi(g0_extension_firmware::SpiDevice); + + SPIDriver* spi = getSpiDevice(g0_extension_firmware::SpiDevice); + spiAcquireBus(spi); + + auto& spiCfg = g0_extension_firmware::spiConfig(); + initSpiCs(&spiCfg, g0_extension_firmware::SpiCsPin); + palSetPad(spiCfg.ssport, spiCfg.sspad); + spiStart(spi, &spiCfg); + g0_extension_firmware::resetExtension(false); + + uint32_t currentVersion = 0; + bool ok = true; + + if (g0_extension_firmware::readAppVersion(spi, currentVersion)) { + efiPrintf( + "G0 extension firmware: app version %d, bundled version %d", + static_cast(currentVersion), + static_cast(build_g0_extension_version)); + + if (!forceUpdate && currentVersion == build_g0_extension_version) { + efiPrintf("G0 extension firmware: firmware is already current"); + } else { + efiPrintf("G0 extension firmware: requesting app update mode%s", forceUpdate ? " (forced)" : ""); + g0_extension_firmware::requestAppUpdate(spi); + ok = g0_extension_firmware::flashFirmwareImageWithRetry(spi); + } + } else { + efiPrintf("G0 extension firmware: no app version response, forcing update"); + ok = g0_extension_firmware::flashFirmwareImageWithRetry(spi); + } + + spiReleaseBus(spi); + g0_extension_firmware::releaseControlPins(); + + if (ok) { + efiPrintf("G0 extension firmware: complete"); + } else { + efiPrintf("G0 extension firmware: failed"); + } + + return ok; +#else + efiPrintf("G0 extension firmware ERROR: run `make -C firmware/ext/g0_firmware for_fome_image` first"); + return false; +#endif +#else + (void)forceUpdate; + efiPrintf("G0 extension firmware is only available on Atlas with SPI enabled"); + return false; +#endif +} diff --git a/firmware/controllers/system/g0_extension_firmware.h b/firmware/controllers/system/g0_extension_firmware.h new file mode 100644 index 0000000000..d48daa1531 --- /dev/null +++ b/firmware/controllers/system/g0_extension_firmware.h @@ -0,0 +1,3 @@ +#pragma once + +bool loadG0ExtensionFirmware(bool forceUpdate = false); diff --git a/firmware/controllers/system/g0_extension_firmware_bootloader.cpp b/firmware/controllers/system/g0_extension_firmware_bootloader.cpp new file mode 100644 index 0000000000..08ae272f05 --- /dev/null +++ b/firmware/controllers/system/g0_extension_firmware_bootloader.cpp @@ -0,0 +1,182 @@ +#include "g0_extension_firmware_impl.h" + +#if HW_ATLAS && HAL_USE_SPI && G0_EXTENSION_FIRMWARE_IMAGE_AVAILABLE + +namespace g0_extension_firmware { + +uint8_t spiByte(SPIDriver* spi, uint8_t tx) { + txBuffer()[0] = tx; + rxBuffer()[0] = 0; + spiExchange(spi, 1, txBuffer(), rxBuffer()); + return rxBuffer()[0]; +} + +uint8_t bootloaderSpiByte(SPIDriver* spi, uint8_t tx) { + const uint8_t rx = spiByte(spi, tx); + + // AN4286 requires at least 15us between bytes for the STM32 ROM SPI bootloader. + chThdSleepMicroseconds(BootloaderInterByteDelayUs); + return rx; +} + +bool handleBootloaderAck(SPIDriver* spi, uint8_t response) { + if (response == BootloaderAck) { + bootloaderSpiByte(spi, BootloaderAck); + return true; + } + + return false; +} + +bool waitBootloaderAck(SPIDriver* spi, int attempts) { + while (attempts-- > 0) { + const uint8_t response = bootloaderSpiByte(spi, 0x00); + + if (handleBootloaderAck(spi, response)) { + return true; + } + + if (response == BootloaderNack) { + return false; + } + + chThdSleepMilliseconds(1); + } + + return false; +} + +bool sendBootloaderBytesAndWaitAck(SPIDriver* spi, const uint8_t* data, size_t size) { + for (size_t i = 0; i < size; i++) { + const uint8_t response = bootloaderSpiByte(spi, data[i]); + + if (handleBootloaderAck(spi, response)) { + return true; + } + + if (response == BootloaderNack) { + return false; + } + } + + return waitBootloaderAck(spi); +} + +bool sendBootloaderCommand(SPIDriver* spi, uint8_t command) { + const uint8_t bytes[] = {BootloaderSync, command, static_cast(command ^ 0xFF)}; + return sendBootloaderBytesAndWaitAck(spi, bytes, sizeof(bytes)); +} + +bool sendBootloaderAddress(SPIDriver* spi, uint32_t address) { + uint8_t bytes[5] = { + static_cast(address >> 24), + static_cast(address >> 16), + static_cast(address >> 8), + static_cast(address), + 0}; + + bytes[4] = bytes[0] ^ bytes[1] ^ bytes[2] ^ bytes[3]; + return sendBootloaderBytesAndWaitAck(spi, bytes, sizeof(bytes)); +} + +bool bootloaderMassErase(SPIDriver* spi) { + if (!sendBootloaderCommand(spi, BootloaderExtendedErase)) { + return false; + } + + const uint8_t globalMassErase[] = {0xFF, 0xFF, 0x00}; + return sendBootloaderBytesAndWaitAck(spi, globalMassErase, sizeof(globalMassErase)); +} + +bool bootloaderWriteBlock(SPIDriver* spi, uint32_t address, const uint8_t* data, size_t size) { + if (size == 0 || size > 256) { + return false; + } + + if (!sendBootloaderCommand(spi, BootloaderWriteMemory) || !sendBootloaderAddress(spi, address)) { + return false; + } + + uint8_t buffer[258]; + buffer[0] = static_cast(size - 1); + uint8_t checksum = buffer[0]; + + for (size_t i = 0; i < size; i++) { + buffer[1 + i] = data[i]; + checksum ^= data[i]; + } + + buffer[1 + size] = checksum; + return sendBootloaderBytesAndWaitAck(spi, buffer, size + 2); +} + +bool bootloaderGo(SPIDriver* spi, uint32_t address) { + return sendBootloaderCommand(spi, BootloaderGo) && sendBootloaderAddress(spi, address); +} + +bool flashFirmwareImage(SPIDriver* spi) { + const size_t totalSize = sizeof(build_g0_extension_bin); + + if (totalSize == 0) { + efiPrintf("G0 extension firmware ERROR: firmware image header is missing or empty"); + return false; + } + + efiPrintf("G0 extension firmware: entering SPI bootloader"); + resetExtension(true); + + spiSelect(spi); + bootloaderSpiByte(spi, BootloaderSync); + if (!waitBootloaderAck(spi)) { + spiUnselect(spi); + efiPrintf("G0 extension firmware ERROR: no ACK from SPI bootloader"); + return false; + } + + efiPrintf("G0 extension firmware: erasing flash"); + if (!bootloaderMassErase(spi)) { + spiUnselect(spi); + efiPrintf("G0 extension firmware ERROR: flash erase failed"); + return false; + } + + efiPrintf("G0 extension firmware: writing %d bytes", static_cast(totalSize)); + for (size_t offset = 0; offset < totalSize; offset += 256) { + uint8_t block[256]; + const size_t blockSize = minI(256, totalSize - offset); + memset(block, 0xFF, sizeof(block)); + memcpy(block, build_g0_extension_bin + offset, blockSize); + + if (!bootloaderWriteBlock(spi, FlashBase + offset, block, sizeof(block))) { + spiUnselect(spi); + efiPrintf("G0 extension firmware ERROR: write failed at offset %d", static_cast(offset)); + return false; + } + } + + efiPrintf("G0 extension firmware: starting firmware"); + const bool ok = bootloaderGo(spi, FlashBase); + spiUnselect(spi); + + resetExtension(false); + return ok; +} + +bool flashFirmwareImageWithRetry(SPIDriver* spi) { + for (int attempt = 1; attempt <= FlashMaxAttempts; attempt++) { + efiPrintf("G0 extension firmware: flash attempt %d/%d", attempt, FlashMaxAttempts); + + if (flashFirmwareImage(spi)) { + return true; + } + + resetExtension(false); + } + + efiPrintf("G0 extension firmware ERROR: aborting G0 flash after %d failed attempts", FlashMaxAttempts); + return false; +} + +} // namespace g0_extension_firmware + +#endif diff --git a/firmware/controllers/system/g0_extension_firmware_impl.h b/firmware/controllers/system/g0_extension_firmware_impl.h new file mode 100644 index 0000000000..10341befbf --- /dev/null +++ b/firmware/controllers/system/g0_extension_firmware_impl.h @@ -0,0 +1,65 @@ +#pragma once + +#include "pch.h" +#include "../../ext/g0_firmware/for_fome/g0_spi_protocol.h" + +#if HW_ATLAS && HAL_USE_SPI + +#if __has_include("../../ext/g0_firmware/for_fome/g0_firmware_image.h") +#include "../../ext/g0_firmware/for_fome/g0_firmware_image.h" +#define G0_EXTENSION_FIRMWARE_IMAGE_AVAILABLE TRUE +#else +#define G0_EXTENSION_FIRMWARE_IMAGE_AVAILABLE FALSE +#endif + +namespace g0_extension_firmware { + +namespace app = ::g0_spi_protocol; + +static constexpr spi_device_e SpiDevice = SPI_DEVICE_5; +static constexpr brain_pin_e ResetPin = Gpio::B14; +static constexpr brain_pin_e BootPin = Gpio::B15; +static constexpr brain_pin_e SpiCsPin = Gpio::F6; +static constexpr uint32_t FlashBase = 0x08000000; + +static constexpr uint8_t BootloaderAck = 0x79; +static constexpr uint8_t BootloaderNack = 0x1F; +static constexpr uint8_t BootloaderSync = 0x5A; +static constexpr uint8_t BootloaderExtendedErase = 0x44; +static constexpr uint8_t BootloaderWriteMemory = 0x31; +static constexpr uint8_t BootloaderGo = 0x21; +static constexpr int BootloaderInterByteDelayUs = 20; +static constexpr int FlashMaxAttempts = 2; + +static constexpr size_t SpiDmaBufferSize = 258; + +SPIConfig& spiConfig(); +uint8_t* txBuffer(); +uint8_t* rxBuffer(); + +void setControlPin(brain_pin_e pin, bool value); +void initControlPins(); +void releaseControlPins(); +void resetExtension(bool bootloaderMode); + +uint8_t spiByte(SPIDriver* spi, uint8_t tx); +uint8_t bootloaderSpiByte(SPIDriver* spi, uint8_t tx); +bool handleBootloaderAck(SPIDriver* spi, uint8_t response); +bool waitBootloaderAck(SPIDriver* spi, int attempts = 200); +bool sendBootloaderBytesAndWaitAck(SPIDriver* spi, const uint8_t* data, size_t size); +bool sendBootloaderCommand(SPIDriver* spi, uint8_t command); +bool sendBootloaderAddress(SPIDriver* spi, uint32_t address); +bool bootloaderMassErase(SPIDriver* spi); +bool bootloaderWriteBlock(SPIDriver* spi, uint32_t address, const uint8_t* data, size_t size); +bool bootloaderGo(SPIDriver* spi, uint32_t address); +bool flashFirmwareImage(SPIDriver* spi); +bool flashFirmwareImageWithRetry(SPIDriver* spi); + +void exchangeAppFrame(SPIDriver* spi, uint8_t command, app::AppFrame& rx); +bool isAppResponse(const app::AppFrame& rx, uint8_t expectedCommand, uint8_t expectedPayloadLength); +bool readAppVersion(SPIDriver* spi, uint32_t& version); +void requestAppUpdate(SPIDriver* spi); + +} // namespace g0_extension_firmware + +#endif diff --git a/firmware/controllers/system/g0_extension_firmware_protocol.cpp b/firmware/controllers/system/g0_extension_firmware_protocol.cpp new file mode 100644 index 0000000000..a23804937e --- /dev/null +++ b/firmware/controllers/system/g0_extension_firmware_protocol.cpp @@ -0,0 +1,58 @@ +#include "g0_extension_firmware_impl.h" + +#if HW_ATLAS && HAL_USE_SPI && G0_EXTENSION_FIRMWARE_IMAGE_AVAILABLE + +namespace g0_extension_firmware { + +void exchangeAppFrame(SPIDriver* spi, uint8_t command, app::AppFrame& rx) { + memset(txBuffer(), 0, app::appFrameSize); + reinterpret_cast(txBuffer())->commandRequest.command = command; + memset(rxBuffer(), 0, app::appFrameSize); + memset(rx.bytes, 0, app::appFrameSize); + + spiSelect(spi); + spiExchange(spi, app::appFrameSize, txBuffer(), rxBuffer()); + spiUnselect(spi); + + memcpy(rx.bytes, rxBuffer(), app::appFrameSize); +} + +bool isAppResponse(const app::AppFrame& rx, uint8_t expectedCommand, uint8_t expectedPayloadLength) { + const auto& header = rx.responseHeader; + const bool knownStatus = header.status == app::statusReady || header.status == app::statusUpdateMode; + return knownStatus && header.result == app::resultOk && header.command == expectedCommand && + header.payloadLength == expectedPayloadLength; +} + +bool readAppVersion(SPIDriver* spi, uint32_t& version) { + app::AppFrame rx; + + exchangeAppFrame(spi, app::cmdReadVersion, rx); + chThdSleepMilliseconds(1); + exchangeAppFrame(spi, app::cmdNop, rx); + + if (!isAppResponse(rx, app::cmdReadVersion, app::versionPayloadLength)) { + efiPrintf( + "G0 extension firmware: invalid version response %02X %02X %02X %02X", + rx.responseHeader.status, + rx.responseHeader.result, + rx.responseHeader.command, + rx.responseHeader.payloadLength); + return false; + } + + version = rx.versionResponse.version; + return true; +} + +void requestAppUpdate(SPIDriver* spi) { + app::AppFrame rx; + + exchangeAppFrame(spi, app::cmdEnterUpdate, rx); + chThdSleepMilliseconds(1); + exchangeAppFrame(spi, app::cmdNop, rx); +} + +} // namespace g0_extension_firmware + +#endif diff --git a/firmware/controllers/system/system.mk b/firmware/controllers/system/system.mk index 889b651218..0d4aaf380c 100644 --- a/firmware/controllers/system/system.mk +++ b/firmware/controllers/system/system.mk @@ -5,4 +5,11 @@ SYSTEMSRC_CPP = \ $(PROJECT_DIR)/controllers/system/efi_gpio.cpp \ $(PROJECT_DIR)/controllers/system/dc_motor.cpp \ $(PROJECT_DIR)/controllers/system/timer/scheduler.cpp \ - $(PROJECT_DIR)/controllers/system/timer/trigger_scheduler.cpp \ + $(PROJECT_DIR)/controllers/system/timer/trigger_scheduler.cpp + +ifeq ($(SHORT_BOARD_NAME),atlas) +SYSTEMSRC_CPP += \ + $(PROJECT_DIR)/controllers/system/g0_extension_firmware.cpp \ + $(PROJECT_DIR)/controllers/system/g0_extension_firmware_protocol.cpp \ + $(PROJECT_DIR)/controllers/system/g0_extension_firmware_bootloader.cpp +endif diff --git a/firmware/controllers/system/timer/pwm_generator_logic.cpp b/firmware/controllers/system/timer/pwm_generator_logic.cpp index b644441164..3ddaf8a1f7 100644 --- a/firmware/controllers/system/timer/pwm_generator_logic.cpp +++ b/firmware/controllers/system/timer/pwm_generator_logic.cpp @@ -320,6 +320,15 @@ void startSimplePwm(SimplePwm* state, const char* msg, OutputPin* output, float return; } +#if EFI_PROD_CODE && HAL_USE_PWM + if (isBrainPinValid(output->m_brainPin)) { + if (auto hardPwm = hardware_pwm::tryInitPin(msg, output->m_brainPin, frequency, dutyCycle)) { + state->hardPwm = hardPwm; + return; + } + } +#endif + state->seq.setSwitchTime(0, dutyCycle); state->seq.setSwitchTime(1, 1); state->seq.setChannelState(0, 0, false); @@ -346,9 +355,7 @@ void startSimplePwmExt( void startSimplePwmHard( SimplePwm* state, const char* msg, brain_pin_e brainPin, OutputPin* output, float frequency, float dutyCycle) { #if EFI_PROD_CODE && HAL_USE_PWM - auto hardPwm = hardware_pwm::tryInitPin(msg, brainPin, frequency, dutyCycle); - - if (hardPwm) { + if (auto hardPwm = hardware_pwm::tryInitPin(msg, brainPin, frequency, dutyCycle)) { state->hardPwm = hardPwm; } else { #endif diff --git a/firmware/ext/g0_firmware b/firmware/ext/g0_firmware new file mode 160000 index 0000000000..0652794c54 --- /dev/null +++ b/firmware/ext/g0_firmware @@ -0,0 +1 @@ +Subproject commit 0652794c54404527fa8eb5b6bd1f3a9336c8a311 diff --git a/firmware/hw_layer/g0_extension/g0_extension_adc_provider.cpp b/firmware/hw_layer/g0_extension/g0_extension_adc_provider.cpp new file mode 100644 index 0000000000..ff7dd4a242 --- /dev/null +++ b/firmware/hw_layer/g0_extension/g0_extension_adc_provider.cpp @@ -0,0 +1,267 @@ +#include "g0_extension_io_impl.h" + +#if EFI_PROD_CODE + +#include "ports/mpu_util.h" + +namespace g0_extension { +namespace { + +static SPIConfig g0ExtensionSpiConfig = { + .circular = false, + .end_cb = NULL, + .ssport = NULL, + .sspad = 0, + .cfg1 = 7 | SPI_CFG1_MBR_2 | SPI_CFG1_MBR_1 | SPI_CFG1_MBR_0, + .cfg2 = 0}; + +static NO_CACHE protocol::AppFrame txBuf; +static NO_CACHE protocol::AppFrame rxBuf; + +static void clearFrame(protocol::AppFrame& frame) { + for (size_t i = 0; i < protocol::appFrameSize; i++) { + frame.bytes[i] = 0; + } +} + +class G0ExtensionSpiAdcProvider final : public AdcProvider { +public: + const char* name() const override { + return "G0ExtensionSpiAdc"; + } + + bool enable(const char* name, size_t idx) override { + (void)name; + + if (idx >= protocol::analogChannelCount) { + return false; + } + + m_enabled[idx] = true; + return true; + } + + void disable(size_t idx) override { + if (idx >= protocol::analogChannelCount) { + return; + } + + m_enabled[idx] = false; + } + + float get(size_t idx) const override { + if (idx >= protocol::analogChannelCount || !m_ready) { + return 0.0f; + } + + return m_millivolts[idx] * 0.001f; + } + + bool readDigitalInput(size_t idx) const { + if (idx >= protocol::digitalInputCount || !m_digitalReady) { + return false; + } + + return m_digitalLevels[idx]; + } + + void setLowsideOutput(size_t idx, bool value) { + m_outputs.requestOutput(idx, value, 0, value ? protocol::outputDutyMax : 0); + } + + void disableLowsideOutput(size_t idx) { + m_outputs.requestOutput(idx, false, 0, 0); + } + + void setLowsidePwm(size_t idx, uint32_t frequencyHz, uint16_t duty) { + m_outputs.requestOutput(idx, true, frequencyHz, duty); + } + + void start() { + if (m_started) { + efiPrintf("G0 extension I/O: already started"); + return; + } + + m_started = true; + + turnOnSpi(SpiDevice); + m_spiDevice = getSpiDevice(SpiDevice); + + initSpiCs(&g0ExtensionSpiConfig, SpiCsPin); + palSetPad(g0ExtensionSpiConfig.ssport, g0ExtensionSpiConfig.sspad); + spiStart(m_spiDevice, &g0ExtensionSpiConfig); + + chThdCreateStatic(m_threadWa, sizeof(m_threadWa), NORMALPRIO, threadEntry, this); + } + +private: + static THD_FUNCTION(threadEntry, arg) { + static_cast(arg)->thread(); + } + + void thread() { + chRegSetThreadName("G0 Ext IO"); + + while (true) { + pollIo(); + chThdSleepMilliseconds(PollIntervalMs); + } + } + + void pollIo() { + PendingRequest nextRequest; + + spiAcquireBus(m_spiDevice); + clearFrame(txBuf); + clearFrame(rxBuf); + prepareRequest(txBuf, nextRequest); + + spiSelect(m_spiDevice); + spiExchange(m_spiDevice, protocol::appFrameSize, txBuf.bytes, rxBuf.bytes); + spiUnselect(m_spiDevice); + spiReleaseBus(m_spiDevice); + + parseResponse(rxBuf); + m_pendingRequest = nextRequest; + } + + void prepareRequest(protocol::AppFrame& tx, PendingRequest& nextRequest) { + nextRequest = {}; + + // The G0 app protocol is pipelined: the response we receive now + // belongs to the previous request, while this request schedules the next reply. + if (m_nextDigitalInputToConfigure <= protocol::digitalInputCount) { + tx.setInputModeRequest.command = protocol::cmdSetInputMode; + tx.setInputModeRequest.input = static_cast(m_nextDigitalInputToConfigure); + tx.setInputModeRequest.mode = protocol::digitalMode; + m_nextDigitalInputToConfigure++; + return; + } + + m_outputs.prepareRequest(tx, nextRequest); + if (nextRequest.type != RequestType::None) { + return; + } + + tx.commandRequest.command = m_pollAnalogNext ? protocol::cmdReadAnalog : protocol::cmdReadDigitalAll; + m_pollAnalogNext = !m_pollAnalogNext; + } + + void parseResponse(const protocol::AppFrame& rx) { + if (m_pendingRequest.type == RequestType::SetOutput || m_pendingRequest.type == RequestType::DisableOutput) { + m_outputs.parseAck(m_pendingRequest, rx); + } + + switch (rx.responseHeader.command) { + case protocol::cmdReadAnalog: + parseAnalogResponse(rx); + break; + case protocol::cmdReadDigitalAll: + parseDigitalResponse(rx); + break; + default: + break; + } + } + + void parseAnalogResponse(const protocol::AppFrame& rx) { + const auto& response = rx.analogResponse; + const auto& header = response.header; + + if (header.status != protocol::statusReady && header.status != protocol::statusUpdateMode) { + m_ready = false; + return; + } + + if (header.result != protocol::resultOk || header.payloadLength != protocol::analogPayloadLength) { + m_ready = false; + return; + } + + if (!response.ready) { + m_ready = false; + return; + } + + const size_t count = response.channelCount < protocol::analogChannelCount ? response.channelCount + : protocol::analogChannelCount; + for (size_t i = 0; i < count; i++) { + m_millivolts[i] = response.millivolts[i]; + } + + m_ready = true; + } + + void parseDigitalResponse(const protocol::AppFrame& rx) { + const auto& response = rx.digitalAllResponse; + const auto& header = response.header; + + if (header.status != protocol::statusReady && header.status != protocol::statusUpdateMode) { + m_digitalReady = false; + return; + } + + if (header.result != protocol::resultOk || header.payloadLength != protocol::digitalAllPayloadLength) { + m_digitalReady = false; + return; + } + + if (response.inputCount != protocol::digitalInputCount) { + m_digitalReady = false; + return; + } + + for (size_t i = 0; i < protocol::digitalInputCount; i++) { + m_digitalLevels[i] = (response.inputs[i].flags & 0x01U) != 0; + } + + m_digitalReady = true; + } + +private: + bool m_started = false; + SPIDriver* m_spiDevice = nullptr; + bool m_enabled[protocol::analogChannelCount] = {}; + volatile bool m_ready = false; + volatile uint16_t m_millivolts[protocol::analogChannelCount] = {}; + volatile bool m_digitalReady = false; + volatile bool m_digitalLevels[protocol::digitalInputCount] = {}; + OutputManager m_outputs; + PendingRequest m_pendingRequest = {}; + size_t m_nextDigitalInputToConfigure = 1; + bool m_pollAnalogNext = true; + THD_WORKING_AREA(m_threadWa, 512); +}; + +static G0ExtensionSpiAdcProvider g0ExtensionSpiAdcProvider; + +} // namespace + +AdcProvider& adcProvider() { + return g0ExtensionSpiAdcProvider; +} + +void startProvider() { + g0ExtensionSpiAdcProvider.start(); +} + +bool readDigitalInput(size_t idx) { + return g0ExtensionSpiAdcProvider.readDigitalInput(idx); +} + +void setLowsideOutput(size_t idx, bool value) { + g0ExtensionSpiAdcProvider.setLowsideOutput(idx, value); +} + +void disableLowsideOutput(size_t idx) { + g0ExtensionSpiAdcProvider.disableLowsideOutput(idx); +} + +void setLowsidePwm(size_t idx, uint32_t frequencyHz, uint16_t duty) { + g0ExtensionSpiAdcProvider.setLowsidePwm(idx, frequencyHz, duty); +} + +} // namespace g0_extension + +#endif diff --git a/firmware/hw_layer/g0_extension/g0_extension_io.cpp b/firmware/hw_layer/g0_extension/g0_extension_io.cpp new file mode 100644 index 0000000000..bc5edca27d --- /dev/null +++ b/firmware/hw_layer/g0_extension/g0_extension_io.cpp @@ -0,0 +1,139 @@ +#include "g0_extension_io.h" +#include "g0_extension_io_impl.h" + +#if EFI_PROD_CODE + +namespace { + +static bool g0ExtensionPresent = false; + +class G0ExtensionLowsideHardwarePwm final : public hardware_pwm { +public: + explicit G0ExtensionLowsideHardwarePwm(size_t outputIndex) + : m_outputIndex(outputIndex) {} + + void start(float frequencyHz, float duty) { + m_frequencyHz = frequencyHz < 1 ? 1 : static_cast(frequencyHz); + setDuty(duty); + } + + void setDuty(float duty) override { + if (duty < 0) { + duty = 0; + } else if (duty > 1) { + duty = 1; + } + + const auto scaledDuty = static_cast(duty * g0_extension::protocol::outputDutyMax); + g0_extension::setLowsidePwm(m_outputIndex, m_frequencyHz, scaledDuty); + } + +private: + const size_t m_outputIndex; + uint32_t m_frequencyHz = 1; +}; + +static G0ExtensionLowsideHardwarePwm g0ExtensionLowsidePwms[g0_extension::protocol::outputCount] = { + G0ExtensionLowsideHardwarePwm(0), + G0ExtensionLowsideHardwarePwm(1), + G0ExtensionLowsideHardwarePwm(2), + G0ExtensionLowsideHardwarePwm(3), +}; + +} // namespace + +void setG0ExtensionPresent(bool present) { + g0ExtensionPresent = present; +} + +bool isG0ExtensionPresent() { + return g0ExtensionPresent; +} + +void startG0ExtensionIo() { + static bool started = false; + + if (started) { + efiPrintf("G0 extension I/O: already started"); + return; + } + + started = true; + + g0_extension::startProvider(); + registerAdcProvider( + g0_extension::adcProvider(), g0_extension::FirstAdcIndex, g0_extension::protocol::analogChannelCount); +} + +bool readG0ExtensionDigitalInput(size_t idx) { + return g0_extension::readDigitalInput(idx); +} + +bool isG0ExtensionLowsidePin(brain_pin_e pin) { + return pin >= Gpio::G0_LOWSIDE_0 && pin <= Gpio::G0_LOWSIDE_3; +} + +void setG0ExtensionLowsideOutput(brain_pin_e pin, bool value) { + if (!isG0ExtensionLowsidePin(pin)) { + return; + } + + g0_extension::setLowsideOutput(pin - Gpio::G0_LOWSIDE_0, value); +} + +void disableG0ExtensionLowsideOutput(brain_pin_e pin) { + if (!isG0ExtensionLowsidePin(pin)) { + return; + } + + g0_extension::disableLowsideOutput(pin - Gpio::G0_LOWSIDE_0); +} + +hardware_pwm* tryInitG0ExtensionLowsidePwm(brain_pin_e pin, float frequencyHz, float duty) { + if (!isG0ExtensionLowsidePin(pin)) { + return nullptr; + } + + auto& pwm = g0ExtensionLowsidePwms[pin - Gpio::G0_LOWSIDE_0]; + pwm.start(frequencyHz, duty); + return &pwm; +} + +#else + +void startG0ExtensionIo() {} + +void setG0ExtensionPresent(bool present) { + (void)present; +} + +bool isG0ExtensionPresent() { + return false; +} + +bool readG0ExtensionDigitalInput(size_t idx) { + (void)idx; + return false; +} + +bool isG0ExtensionLowsidePin(brain_pin_e pin) { + return pin >= Gpio::G0_LOWSIDE_0 && pin <= Gpio::G0_LOWSIDE_3; +} + +void setG0ExtensionLowsideOutput(brain_pin_e pin, bool value) { + (void)pin; + (void)value; +} + +void disableG0ExtensionLowsideOutput(brain_pin_e pin) { + (void)pin; +} + +hardware_pwm* tryInitG0ExtensionLowsidePwm(brain_pin_e pin, float frequencyHz, float duty) { + (void)pin; + (void)frequencyHz; + (void)duty; + return nullptr; +} + +#endif diff --git a/firmware/hw_layer/g0_extension/g0_extension_io.h b/firmware/hw_layer/g0_extension/g0_extension_io.h new file mode 100644 index 0000000000..2c22b9fe10 --- /dev/null +++ b/firmware/hw_layer/g0_extension/g0_extension_io.h @@ -0,0 +1,17 @@ +#pragma once + +#include "pch.h" +#include "adc_provider.h" + +void startG0ExtensionIo(); +void setG0ExtensionPresent(bool present); +bool isG0ExtensionPresent(); +bool readG0ExtensionDigitalInput(size_t idx); +bool isG0ExtensionLowsidePin(brain_pin_e pin); +void setG0ExtensionLowsideOutput(brain_pin_e pin, bool value); +void disableG0ExtensionLowsideOutput(brain_pin_e pin); + +#ifdef __cplusplus +struct hardware_pwm; +hardware_pwm* tryInitG0ExtensionLowsidePwm(brain_pin_e pin, float frequencyHz, float duty); +#endif diff --git a/firmware/hw_layer/g0_extension/g0_extension_io_impl.h b/firmware/hw_layer/g0_extension/g0_extension_io_impl.h new file mode 100644 index 0000000000..b97e403617 --- /dev/null +++ b/firmware/hw_layer/g0_extension/g0_extension_io_impl.h @@ -0,0 +1,58 @@ +#pragma once + +#include "g0_extension_io.h" +#include "../../ext/g0_firmware/for_fome/g0_spi_protocol.h" + +#if EFI_PROD_CODE + +namespace g0_extension { + +namespace protocol = ::g0_spi_protocol; + +static constexpr spi_device_e SpiDevice = SPI_DEVICE_5; +static constexpr brain_pin_e SpiCsPin = Gpio::F6; +static constexpr size_t FirstAdcIndex = EFI_ADC_20 - EFI_ADC_0; +static constexpr uint32_t PollIntervalMs = 10; + +struct OutputState { + bool enabled = false; + uint32_t frequencyHz = 0; + uint16_t duty = 0; + bool dirty = false; + bool pendingAck = false; + bool sentEnabled = false; + uint32_t sentFrequencyHz = 0; + uint16_t sentDuty = 0; +}; + +enum class RequestType : uint8_t { + None, + SetOutput, + DisableOutput, +}; + +struct PendingRequest { + RequestType type = RequestType::None; + size_t outputIndex = 0; +}; + +class OutputManager { +public: + void prepareRequest(protocol::AppFrame& tx, PendingRequest& nextRequest); + void parseAck(const PendingRequest& pendingRequest, const protocol::AppFrame& rx); + void requestOutput(size_t idx, bool enabled, uint32_t frequencyHz, uint16_t duty); + +private: + OutputState m_outputs[protocol::outputCount] = {}; +}; + +AdcProvider& adcProvider(); +void startProvider(); +bool readDigitalInput(size_t idx); +void setLowsideOutput(size_t idx, bool value); +void disableLowsideOutput(size_t idx); +void setLowsidePwm(size_t idx, uint32_t frequencyHz, uint16_t duty); + +} // namespace g0_extension + +#endif diff --git a/firmware/hw_layer/g0_extension/g0_extension_output_manager.cpp b/firmware/hw_layer/g0_extension/g0_extension_output_manager.cpp new file mode 100644 index 0000000000..dee89b9f68 --- /dev/null +++ b/firmware/hw_layer/g0_extension/g0_extension_output_manager.cpp @@ -0,0 +1,76 @@ +#include "g0_extension_io_impl.h" + +#if EFI_PROD_CODE + +namespace g0_extension { +void OutputManager::prepareRequest(protocol::AppFrame& tx, PendingRequest& nextRequest) { + nextRequest = {}; + + for (size_t i = 0; i < protocol::outputCount; i++) { + if (!m_outputs[i].dirty || m_outputs[i].pendingAck) { + continue; + } + + const auto& output = m_outputs[i]; + nextRequest.outputIndex = i; + + if (output.enabled) { + tx.setOutputRequest.command = protocol::cmdSetOutput; + tx.setOutputRequest.output = static_cast(i + 1); + tx.setOutputRequest.frequencyHz = output.frequencyHz; + tx.setOutputRequest.duty = output.duty; + nextRequest.type = RequestType::SetOutput; + } else { + tx.indexedRequest.command = protocol::cmdDisableOutput; + tx.indexedRequest.index = static_cast(i + 1); + nextRequest.type = RequestType::DisableOutput; + } + + m_outputs[i].pendingAck = true; + m_outputs[i].sentEnabled = output.enabled; + m_outputs[i].sentFrequencyHz = output.frequencyHz; + m_outputs[i].sentDuty = output.duty; + return; + } +} + +void OutputManager::parseAck(const PendingRequest& pendingRequest, const protocol::AppFrame& rx) { + if (pendingRequest.outputIndex >= protocol::outputCount) { + return; + } + + auto& output = m_outputs[pendingRequest.outputIndex]; + output.pendingAck = false; + + if (rx.responseHeader.result != protocol::resultOk) { + output.dirty = true; + return; + } + + output.dirty = output.enabled != output.sentEnabled || output.frequencyHz != output.sentFrequencyHz || + output.duty != output.sentDuty; +} + +void OutputManager::requestOutput(size_t idx, bool enabled, uint32_t frequencyHz, uint16_t duty) { + if (idx >= protocol::outputCount) { + return; + } + + if (duty > protocol::outputDutyMax) { + duty = protocol::outputDutyMax; + } + + auto& output = m_outputs[idx]; + const bool changed = output.enabled != enabled || output.frequencyHz != frequencyHz || output.duty != duty; + + output.enabled = enabled; + output.frequencyHz = frequencyHz; + output.duty = duty; + if (changed) { + output.dirty = true; + } +} + +} // namespace g0_extension + +#endif diff --git a/firmware/hw_layer/hw_layer.mk b/firmware/hw_layer/hw_layer.mk index a54cf76e3a..c83846e2dc 100644 --- a/firmware/hw_layer/hw_layer.mk +++ b/firmware/hw_layer/hw_layer.mk @@ -29,7 +29,14 @@ HW_LAYER_EMS_CPP = \ $(PROJECT_DIR)/hw_layer/stepper_dual_hbridge.cpp \ $(PROJECT_DIR)/hw_layer/io_pins.cpp \ $(PROJECT_DIR)/hw_layer/rtc_helper.cpp \ - $(PROJECT_DIR)/hw_layer/debounce.cpp \ + $(PROJECT_DIR)/hw_layer/debounce.cpp + +ifeq ($(SHORT_BOARD_NAME),atlas) +HW_LAYER_EMS_CPP += \ + $(PROJECT_DIR)/hw_layer/g0_extension/g0_extension_io.cpp \ + $(PROJECT_DIR)/hw_layer/g0_extension/g0_extension_adc_provider.cpp \ + $(PROJECT_DIR)/hw_layer/g0_extension/g0_extension_output_manager.cpp +endif ifeq ($(USE_OPENBLT),yes) HW_LAYER_EMS += \ @@ -40,4 +47,3 @@ endif # '-include' is a magic kind of 'include' which would survive if file to be included is not found # -include $(PROJECT_DIR)/hw_layer/$(CPU_HWLAYER)/hw_ports.mk - diff --git a/firmware/hw_layer/io_pins.cpp b/firmware/hw_layer/io_pins.cpp index 6884d6e7da..a77993ec29 100644 --- a/firmware/hw_layer/io_pins.cpp +++ b/firmware/hw_layer/io_pins.cpp @@ -12,6 +12,9 @@ #if EFI_PROD_CODE #include "drivers/gpio/gpio_ext.h" +#if HW_ATLAS +#include "hw_layer/g0_extension/g0_extension_io.h" +#endif #include "status_loop.h" #include "console_io.h" @@ -149,6 +152,12 @@ bool efiReadPin(Gpio pin) { return readCanVirtualInput(pin - Gpio::CAN_INPUT_0); } +#if HW_ATLAS + if (pin >= Gpio::G0_DIGITAL_IN_0 && pin <= Gpio::G0_DIGITAL_IN_3) { + return readG0ExtensionDigitalInput(pin - Gpio::G0_DIGITAL_IN_0); + } +#endif + /* incorrect pin */ return false; } diff --git a/firmware/hw_layer/ports/stm32/stm32_common.cpp b/firmware/hw_layer/ports/stm32/stm32_common.cpp index dce93141e7..29f00a8a16 100644 --- a/firmware/hw_layer/ports/stm32/stm32_common.cpp +++ b/firmware/hw_layer/ports/stm32/stm32_common.cpp @@ -182,6 +182,9 @@ void Stm32AdcProviderBase::disable(size_t idx) { #if EFI_PROD_CODE #if HAL_USE_PWM +#if HW_ATLAS +#include "hw_layer/g0_extension/g0_extension_io.h" +#endif namespace { struct stm32_pwm_config { PWMDriver* const Driver; @@ -368,6 +371,12 @@ stm32_hardware_pwm* getNextPwmDevice() { } /*static*/ hardware_pwm* hardware_pwm::tryInitPin(const char* msg, brain_pin_e pin, float frequencyHz, float duty) { +#if HW_ATLAS + if (auto* g0Pwm = tryInitG0ExtensionLowsidePwm(pin, frequencyHz, duty)) { + return g0Pwm; + } +#endif + // Hardware PWM can't do very slow PWM - the timer counter is only 16 bits, so at 2MHz counting, that's a minimum of // 31hz. if (frequencyHz < 50) { @@ -657,7 +666,7 @@ void initSpiCs(SPIConfig* spiConfig, brain_pin_e csPin) { ioportmask_t pin = getHwPin("spi", csPin); spiConfig->ssport = port; spiConfig->sspad = pin; - efiSetPadMode("chip select", csPin, PAL_STM32_MODE_OUTPUT); + efiSetPadModeWithoutOwnershipAcquisition("chip select", csPin, PAL_STM32_MODE_OUTPUT); } #ifdef STM32H7XX diff --git a/firmware/hw_layer/ports/stm32/stm32h7/efifeatures.h b/firmware/hw_layer/ports/stm32/stm32h7/efifeatures.h index 0c6fac4231..af159dd7de 100644 --- a/firmware/hw_layer/ports/stm32/stm32h7/efifeatures.h +++ b/firmware/hw_layer/ports/stm32/stm32h7/efifeatures.h @@ -17,8 +17,9 @@ #define BOARD_MC33972_COUNT 0 #define BOARD_TLE8888_COUNT 0 -#undef EFI_MAX_31855 -#define EFI_MAX_31855 FALSE +#ifndef EFI_MAX_31855 +#define EFI_MAX_31855 TRUE +#endif #define EFI_USE_COMPRESSED_INI_MSD diff --git a/firmware/hw_layer/sensors/max31855.cpp b/firmware/hw_layer/sensors/max31855.cpp index 810c45b440..d8e6304192 100644 --- a/firmware/hw_layer/sensors/max31855.cpp +++ b/firmware/hw_layer/sensors/max31855.cpp @@ -144,12 +144,15 @@ static void egtRead() { } void initMax31855(spi_device_e device, egt_cs_array_t max31855_cs) { +#ifdef STM32H7XX + driver = getSpiDevice(SPI_DEVICE_5); // H7 has only SPI5 available for aux peripherals +#else driver = getSpiDevice(device); if (driver == NULL) { // error already reported return; } - +#endif // todo:spi device is now enabled separately - should probably be enabled here addConsoleAction("egtinfo", (Void)showEgtInfo); @@ -161,7 +164,12 @@ void initMax31855(spi_device_e device, egt_cs_array_t max31855_cs) { initSpiCs(&spiConfig[i], max31855_cs[i]); +#ifdef STM32H7XX + spiConfig[i].cfg1 = 7 // 8 bits per byte + | SPI_CFG1_MBR_DIV16; +#else spiConfig[i].cr1 = getSpiPrescaler(_5MHz, device); +#endif } } } diff --git a/firmware/rusefi.cpp b/firmware/rusefi.cpp index 2d1ab59262..c1f68f7d22 100644 --- a/firmware/rusefi.cpp +++ b/firmware/rusefi.cpp @@ -116,6 +116,10 @@ #include "trigger_emulator_algo.h" #include "rusefi_lua.h" #include "bootloader_updater.h" +#if HW_ATLAS +#include "g0_extension_firmware.h" +#include "hw_layer/g0_extension/g0_extension_io.h" +#endif #include @@ -173,6 +177,9 @@ void runRusEfi() { addConsoleAction(CMD_REBOOT, scheduleReboot); addConsoleAction(CMD_REBOOT_DFU, jump_to_bootloader); +#if HW_ATLAS + addConsoleAction("force_g0_update", []() { setG0ExtensionPresent(loadG0ExtensionFirmware(true)); }); +#endif #if EFI_USE_OPENBLT addConsoleAction(CMD_REBOOT_OPENBLT, jump_to_openblt); @@ -206,6 +213,13 @@ void runRusEfi() { // Read configuration from flash memory loadConfiguration(); +#if HW_ATLAS + setG0ExtensionPresent(loadG0ExtensionFirmware()); + if (isG0ExtensionPresent()) { + startG0ExtensionIo(); + } +#endif + #if EFI_TUNER_STUDIO startTunerStudioConnectivity(); #endif /* EFI_TUNER_STUDIO */ diff --git a/java_console/ui/src/main/java/com/rusefi/BenchTestPane.java b/java_console/ui/src/main/java/com/rusefi/BenchTestPane.java index e97722c173..de6bee0ba1 100644 --- a/java_console/ui/src/main/java/com/rusefi/BenchTestPane.java +++ b/java_console/ui/src/main/java/com/rusefi/BenchTestPane.java @@ -16,6 +16,9 @@ import static com.rusefi.config.generated.Fields.*; public class BenchTestPane { + private static final String CMD_FORCE_G0_UPDATE = "force_g0_update"; + private static final int G0_UPDATE_TIMEOUT_MS = 120_000; + private final JPanel content = new JPanel(new GridLayout(2, 5)); private final UIContext uiContext; @@ -44,9 +47,32 @@ protected String getCommand() { return Fields.CMD_REBOOT_DFU; } }.getContent()); + content.add(createForceG0Update()); content.add(new MessagesView(config.getRoot()).messagesScroll); } + private Component createForceG0Update() { + CommandControl panel = new FixedCommandControl(uiContext, "G0 Firmware", "", "Force Update", CMD_FORCE_G0_UPDATE) { + @NotNull + @Override + protected ActionListener createButtonListener() { + return e -> { + int choice = JOptionPane.showConfirmDialog( + content, + "Force G0 firmware update?", + "G0 Firmware Update", + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE); + + if (choice == JOptionPane.OK_OPTION) { + uiContext.getCommandQueue().write(CMD_FORCE_G0_UPDATE, G0_UPDATE_TIMEOUT_MS); + } + }; + } + }; + return panel.getContent(); + } + private Component grabPerformanceTrace() { JButton button = new JButton("Grab PTrace"); ActionListener actionListener = e -> uiContext.getLinkManager().COMMUNICATION_EXECUTOR.execute(() -> { diff --git a/java_tools/configuration_definition/src/main/java/com/rusefi/ConfigDefinition.java b/java_tools/configuration_definition/src/main/java/com/rusefi/ConfigDefinition.java index fecf31a5b6..9feb676893 100644 --- a/java_tools/configuration_definition/src/main/java/com/rusefi/ConfigDefinition.java +++ b/java_tools/configuration_definition/src/main/java/com/rusefi/ConfigDefinition.java @@ -148,6 +148,7 @@ public static void doJob(String[] args, ReaderStateImpl state) throws IOExceptio break; case "-boardName": shortBoardName = args[i + 1]; + state.getVariableRegistry().register("SHORT_BOARD_NAME", shortBoardName); break; case "-makefileDep": makefileDepsDestination = args[i + 1]; diff --git a/java_tools/configuration_definition/src/main/java/com/rusefi/output/TSProjectConsumer.java b/java_tools/configuration_definition/src/main/java/com/rusefi/output/TSProjectConsumer.java index 2f8b720f6f..8be58fd86c 100644 --- a/java_tools/configuration_definition/src/main/java/com/rusefi/output/TSProjectConsumer.java +++ b/java_tools/configuration_definition/src/main/java/com/rusefi/output/TSProjectConsumer.java @@ -8,6 +8,10 @@ import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import static com.rusefi.util.IoUtils.CHARSET; @@ -18,6 +22,11 @@ public class TSProjectConsumer implements ConfigurationConsumer { private static final String CONFIG_DEFINITION_START = "CONFIG_DEFINITION_START"; private static final String CONFIG_DEFINITION_END = "CONFIG_DEFINITION_END"; private static final String TS_CONDITION = "@@if_"; + private static final String FIELD_DIRECTIVE = "field ="; + private static final String G0_PRESENT_FIELD = "g0Present"; + private static final String NO_G0_SUFFIX = "_nog0"; + private static final String SHORT_BOARD_NAME = "SHORT_BOARD_NAME"; + private static final String ATLAS_BOARD_NAME = "atlas"; public static final String SETTING_CONTEXT_HELP_END = "SettingContextHelpEnd"; public static final String SETTING_CONTEXT_HELP = "SettingContextHelp"; @@ -48,22 +57,84 @@ protected void writeTunerStudioFile(String inputFile, String fieldsSection) thro } protected void writeContent(String fieldsSection, TsFileContent tsContent, Output tsHeader) throws IOException { - tsHeader.write(tsContent.getPrefix()); + StringBuilder content = new StringBuilder(); - tsHeader.write("; " + CONFIG_DEFINITION_START + ToolUtil.EOL); - tsHeader.write("pageSize = " + totalTsSize + ToolUtil.EOL); - tsHeader.write("page = 1" + ToolUtil.EOL); - tsHeader.write(fieldsSection); + content.append(tsContent.getPrefix()); + + content.append("; ").append(CONFIG_DEFINITION_START).append(ToolUtil.EOL); + content.append("pageSize = ").append(totalTsSize).append(ToolUtil.EOL); + content.append("page = 1").append(ToolUtil.EOL); + content.append(fieldsSection); if (!tsOutput.getSettingContextHelp().isEmpty()) { - tsHeader.write("[" + SETTING_CONTEXT_HELP + "]" + ToolUtil.EOL); - tsHeader.write(tsOutput.getSettingContextHelp() + ToolUtil.EOL + ToolUtil.EOL); - tsHeader.write("; " + SETTING_CONTEXT_HELP_END + ToolUtil.EOL); + content.append("[").append(SETTING_CONTEXT_HELP).append("]").append(ToolUtil.EOL); + content.append(tsOutput.getSettingContextHelp()).append(ToolUtil.EOL).append(ToolUtil.EOL); + content.append("; ").append(SETTING_CONTEXT_HELP_END).append(ToolUtil.EOL); + } + content.append("; ").append(CONFIG_DEFINITION_END).append(ToolUtil.EOL); + content.append(tsContent.getPostfix()); + + String renderedContent = content.toString(); + if (shouldApplyAtlasG0PinVisibility()) { + renderedContent = applyG0PinVisibility(renderedContent); + } else { + renderedContent = stripAtlasOnlyG0Content(renderedContent); } - tsHeader.write("; " + CONFIG_DEFINITION_END + ToolUtil.EOL); - tsHeader.write(tsContent.getPostfix()); + + tsHeader.write(renderedContent); tsHeader.close(); } + private boolean shouldApplyAtlasG0PinVisibility() { + return ATLAS_BOARD_NAME.equals(state.getVariableRegistry().get(SHORT_BOARD_NAME)); + } + + private static String stripAtlasOnlyG0Content(String content) { + StringBuilder result = new StringBuilder(); + + for (String line : content.split("\\r?\\n", -1)) { + if (line.contains(G0_PRESENT_FIELD)) { + continue; + } + + result.append(line).append(ToolUtil.EOL); + } + + return result.toString(); + } + + public static String applyG0PinVisibility(String content) { + String[] lines = content.split("\\r?\\n", -1); + Map g0PinFields = new LinkedHashMap<>(); + StringBuilder withAlternatePinDefs = new StringBuilder(); + + for (String line : lines) { + withAlternatePinDefs.append(line).append(ToolUtil.EOL); + + AlternatePinField alternateField = createAlternatePinField(line); + if (alternateField != null) { + g0PinFields.put(alternateField.originalName, alternateField.alternateName); + withAlternatePinDefs.append(alternateField.definitionLine).append(ToolUtil.EOL); + } + } + + if (g0PinFields.isEmpty()) { + return content; + } + + StringBuilder result = new StringBuilder(); + for (String line : withAlternatePinDefs.toString().split("\\r?\\n", -1)) { + DialogFieldReplacement replacement = createDialogFieldReplacement(line, g0PinFields); + if (replacement != null) { + result.append(replacement.whenPresent).append(ToolUtil.EOL); + result.append(replacement.whenMissing).append(ToolUtil.EOL); + } else { + result.append(line).append(ToolUtil.EOL); + } + } + + return result.toString(); + } + /** * tunerstudio.template.ini has all the content of the future .ini file with the exception of data page * TODO: start generating [outputs] section as well @@ -132,6 +203,200 @@ public static String getToken(String line) { return token.toString(); } + private static AlternatePinField createAlternatePinField(String line) { + int equalsIndex = line.indexOf('='); + if (equalsIndex < 0) { + return null; + } + + String fieldName = line.substring(0, equalsIndex).trim(); + if (fieldName.isEmpty() || fieldName.startsWith(";")) { + return null; + } + + List tokens = splitTopLevelCsv(line.substring(equalsIndex + 1)); + if (tokens.size() < 6) { + return null; + } + + String typeToken = tokens.get(0).trim(); + String storageToken = tokens.get(1).trim(); + if (!"bits".equals(typeToken) || !(storageToken.equals("U08") || storageToken.equals("U16"))) { + return null; + } + + boolean containsG0Option = false; + List filteredTokens = new ArrayList<>(); + for (String token : tokens) { + if (isG0OptionToken(token)) { + containsG0Option = true; + continue; + } + filteredTokens.add(token.trim()); + } + + if (!containsG0Option) { + return null; + } + + String alternateName = fieldName + NO_G0_SUFFIX; + String definitionLine = alternateName + " = " + String.join(", ", filteredTokens); + return new AlternatePinField(fieldName, alternateName, definitionLine); + } + + private static DialogFieldReplacement createDialogFieldReplacement(String line, Map g0PinFields) { + int fieldDirectiveIndex = line.indexOf(FIELD_DIRECTIVE); + if (fieldDirectiveIndex < 0) { + return null; + } + + String indent = line.substring(0, fieldDirectiveIndex); + List tokens = splitTopLevelCsv(line.substring(fieldDirectiveIndex + FIELD_DIRECTIVE.length())); + if (tokens.size() < 2) { + return null; + } + + String fieldName = tokens.get(1).trim(); + String alternateFieldName = g0PinFields.get(fieldName); + if (alternateFieldName == null) { + return null; + } + + List whenPresent = new ArrayList<>(trimmedCopy(tokens)); + List whenMissing = new ArrayList<>(whenPresent); + whenMissing.set(1, alternateFieldName); + + applyVisibilityCondition(whenPresent, G0_PRESENT_FIELD); + applyVisibilityCondition(whenMissing, "!" + G0_PRESENT_FIELD); + + return new DialogFieldReplacement( + indent + FIELD_DIRECTIVE + " " + String.join(", ", whenPresent), + indent + FIELD_DIRECTIVE + " " + String.join(", ", whenMissing) + ); + } + + private static List trimmedCopy(List tokens) { + List result = new ArrayList<>(tokens.size()); + for (String token : tokens) { + result.add(token.trim()); + } + return result; + } + + private static void applyVisibilityCondition(List tokens, String condition) { + if (tokens.size() == 2) { + tokens.add(wrapCondition(condition, null)); + return; + } + + int visibilityIndex = -1; + if (isExpressionToken(tokens.get(2))) { + visibilityIndex = 2; + } else if (tokens.size() > 3 && isExpressionToken(tokens.get(3))) { + visibilityIndex = 3; + } + + if (visibilityIndex == -1) { + tokens.add(wrapCondition(condition, null)); + } else { + tokens.set(visibilityIndex, wrapCondition(condition, tokens.get(visibilityIndex))); + } + } + + private static boolean isExpressionToken(String token) { + String trimmed = token.trim(); + return trimmed.startsWith("{") && trimmed.endsWith("}"); + } + + private static String wrapCondition(String condition, String existingToken) { + if (existingToken == null) { + return "{ " + condition + " }"; + } + + String trimmed = existingToken.trim(); + String expression = trimmed.substring(1, trimmed.length() - 1).trim(); + return "{ (" + condition + ") && (" + expression + ") }"; + } + + private static boolean isG0OptionToken(String token) { + String trimmed = token.trim(); + return trimmed.matches("\\d+=\"G0 [^\"]+\""); + } + + private static List splitTopLevelCsv(String line) { + List tokens = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inQuotes = false; + int braceDepth = 0; + int bracketDepth = 0; + int parenDepth = 0; + + for (int i = 0; i < line.length(); i++) { + char ch = line.charAt(i); + + if (ch == '"' && (i == 0 || line.charAt(i - 1) != '\\')) { + inQuotes = !inQuotes; + } else if (!inQuotes) { + switch (ch) { + case '{': + braceDepth++; + break; + case '}': + braceDepth--; + break; + case '[': + bracketDepth++; + break; + case ']': + bracketDepth--; + break; + case '(': + parenDepth++; + break; + case ')': + parenDepth--; + break; + case ',': + if (braceDepth == 0 && bracketDepth == 0 && parenDepth == 0) { + tokens.add(current.toString().trim()); + current.setLength(0); + continue; + } + break; + default: + break; + } + } + + current.append(ch); + } + + tokens.add(current.toString().trim()); + return tokens; + } + + private static class AlternatePinField { + private final String originalName; + private final String alternateName; + private final String definitionLine; + + private AlternatePinField(String originalName, String alternateName, String definitionLine) { + this.originalName = originalName; + this.alternateName = alternateName; + this.definitionLine = definitionLine; + } + } + + private static class DialogFieldReplacement { + private final String whenPresent; + private final String whenMissing; + + private DialogFieldReplacement(String whenPresent, String whenMissing) { + this.whenPresent = whenPresent; + this.whenMissing = whenMissing; + } + } + @Override public void endFile() throws IOException { writeTunerStudioFile(tsPath, getContent()); diff --git a/java_tools/configuration_definition/src/test/java/com/rusefi/test/TSProjectConsumerTest.java b/java_tools/configuration_definition/src/test/java/com/rusefi/test/TSProjectConsumerTest.java index 22857499a6..714772dfc0 100644 --- a/java_tools/configuration_definition/src/test/java/com/rusefi/test/TSProjectConsumerTest.java +++ b/java_tools/configuration_definition/src/test/java/com/rusefi/test/TSProjectConsumerTest.java @@ -1,12 +1,19 @@ package com.rusefi.test; import com.rusefi.ReaderStateImpl; +import com.rusefi.TsFileContent; import com.rusefi.output.BaseCHeaderConsumer; import com.rusefi.output.JavaFieldsConsumer; import com.rusefi.output.TSProjectConsumer; +import com.rusefi.util.Output; import org.junit.Test; +import java.io.IOException; +import java.io.StringWriter; + import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; public class TSProjectConsumerTest { @Test @@ -83,4 +90,83 @@ public void conditionalField() { "static_assert(sizeof(pid_s) == 24);\n" + "\n", consumer.getContent()); } + + @Test + public void g0PinVisibilityPostProcessing() { + String input = "" + + "fanPin = bits, U16, 522, [0:7], 0=\"NONE\", 231=\"G0 Lowside 1\", 103=\"Lowside 1\"\n" + + "acSwitch = bits, U16, 1056, [0:7], 0=\"NONE\", 227=\"G0 Digital 1\", 98=\"Digital 1\"\n" + + "\tfield = \"Output\", fanPin\n" + + "\tfield = \"A/C switch\", acSwitch, { acSwitch != 0 }, { someEnable }\n"; + + String result = TSProjectConsumer.applyG0PinVisibility(input); + + assertEquals( + "fanPin = bits, U16, 522, [0:7], 0=\"NONE\", 231=\"G0 Lowside 1\", 103=\"Lowside 1\"\n" + + "fanPin_nog0 = bits, U16, 522, [0:7], 0=\"NONE\", 103=\"Lowside 1\"\n" + + "acSwitch = bits, U16, 1056, [0:7], 0=\"NONE\", 227=\"G0 Digital 1\", 98=\"Digital 1\"\n" + + "acSwitch_nog0 = bits, U16, 1056, [0:7], 0=\"NONE\", 98=\"Digital 1\"\n" + + "\tfield = \"Output\", fanPin, { g0Present }\n" + + "\tfield = \"Output\", fanPin_nog0, { !g0Present }\n" + + "\tfield = \"A/C switch\", acSwitch, { (g0Present) && (acSwitch != 0) }, { someEnable }\n" + + "\tfield = \"A/C switch\", acSwitch_nog0, { (!g0Present) && (acSwitch != 0) }, { someEnable }\n" + + "\n" + + "\n", + result); + } + + @Test + public void g0PinVisibilityAppliedOnlyForAtlas() throws IOException { + String fieldsSection = "" + + "fanPin = bits, U16, 522, [0:7], 0=\"NONE\", 231=\"G0 Lowside 1\", 103=\"Lowside 1\"\n" + + "\tfield = \"Output\", fanPin\n"; + + ReaderStateImpl atlasState = new ReaderStateImpl(); + atlasState.getVariableRegistry().register("SHORT_BOARD_NAME", "atlas"); + + ReaderStateImpl nonAtlasState = new ReaderStateImpl(); + nonAtlasState.getVariableRegistry().register("SHORT_BOARD_NAME", "f407-discovery"); + + String atlasOutput = renderTsContent(atlasState, fieldsSection); + assertTrue(atlasOutput.contains("fanPin_nog0 = bits, U16, 522, [0:7], 0=\"NONE\", 103=\"Lowside 1\"")); + assertTrue(atlasOutput.contains("\tfield = \"Output\", fanPin, { g0Present }")); + assertTrue(atlasOutput.contains("\tfield = \"Output\", fanPin_nog0, { !g0Present }")); + + String nonAtlasOutput = renderTsContent(nonAtlasState, fieldsSection); + assertFalse(nonAtlasOutput.contains("fanPin_nog0")); + assertFalse(nonAtlasOutput.contains("g0Present")); + assertTrue(nonAtlasOutput.contains(fieldsSection)); + } + + private static String renderTsContent(ReaderStateImpl state, String fieldsSection) throws IOException { + StringWriter sw = new StringWriter(); + + class RenderableTSProjectConsumer extends TSProjectConsumer { + private RenderableTSProjectConsumer() { + super("", state); + } + + public void render(String generatedFieldsSection) throws IOException { + writeTunerStudioFile("", generatedFieldsSection); + } + + @Override + protected void writeTunerStudioFile(String tsPath, String generatedFieldsSection) throws IOException { + writeContent(generatedFieldsSection, new TsFileContent("", ""), new Output() { + @Override + public void write(String line) { + sw.write(line); + } + + @Override + public void close() { + } + }); + } + } + + RenderableTSProjectConsumer consumer = new RenderableTSProjectConsumer(); + consumer.render(fieldsSection); + return sw.toString(); + } }