From 7a9432177399deab0038400bd663f42077e7204e Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Mon, 26 Oct 2020 11:26:31 -0700 Subject: [PATCH 01/15] fix pwm --- firmware/pwm.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/pwm.cpp b/firmware/pwm.cpp index db7a224..4bb546e 100644 --- a/firmware/pwm.cpp +++ b/firmware/pwm.cpp @@ -5,7 +5,7 @@ Pwm::Pwm(PWMDriver& driver, uint8_t channel, uint32_t counterFrequency, uint32_t counterPeriod) : m_driver(&driver) , m_channel(channel) - , m_counterFrequency(m_counterFrequency) + , m_counterFrequency(counterFrequency) , m_counterPeriod(counterPeriod) { } From 3e90915d48e11736c092a60b9cde9a08a704c2b6 Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Mon, 26 Oct 2020 12:45:50 -0700 Subject: [PATCH 02/15] PWM works --- firmware/cfg/mcuconf.h | 2 +- firmware/main.cpp | 8 ++++++-- firmware/pwm.cpp | 8 ++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/firmware/cfg/mcuconf.h b/firmware/cfg/mcuconf.h index fa69762..9aa650c 100644 --- a/firmware/cfg/mcuconf.h +++ b/firmware/cfg/mcuconf.h @@ -112,7 +112,7 @@ /* * PWM driver system settings. */ -#define STM32_PWM_USE_ADVANCED FALSE +#define STM32_PWM_USE_ADVANCED TRUE #define STM32_PWM_USE_TIM1 TRUE #define STM32_PWM_USE_TIM2 FALSE #define STM32_PWM_USE_TIM3 TRUE diff --git a/firmware/main.cpp b/firmware/main.cpp index 806e80e..683cdfb 100644 --- a/firmware/main.cpp +++ b/firmware/main.cpp @@ -7,10 +7,10 @@ // 400khz / 1024 = 390hz PWM // TODO: this is wired to an inverted output, what do? -Pwm heaterPwm(PWMD1, 1, 400000, 1024); +Pwm heaterPwm(PWMD1, 0, 400'000, 1024); // 48MHz / 1024 = 46.8khz PWM -Pwm pumpDac(PWMD3, 1, 48000000, 1024); +Pwm pumpDac(PWMD3, 0, 48'000'000, 1024); /* * Application entry point. @@ -21,6 +21,10 @@ int main() { InitCan(); + palSetPadMode(GPIOA, 6, PAL_MODE_ALTERNATE(1)); + + adcStart(&ADCD1, nullptr); + heaterPwm.Start(); pumpDac.Start(); diff --git a/firmware/pwm.cpp b/firmware/pwm.cpp index 4bb546e..2df18f4 100644 --- a/firmware/pwm.cpp +++ b/firmware/pwm.cpp @@ -17,10 +17,10 @@ void Pwm::Start() m_counterPeriod, nullptr, { - {PWM_OUTPUT_ACTIVE_HIGH, nullptr}, - {PWM_OUTPUT_ACTIVE_HIGH, nullptr}, - {PWM_OUTPUT_ACTIVE_HIGH, nullptr}, - {PWM_OUTPUT_ACTIVE_HIGH, nullptr} + {PWM_OUTPUT_ACTIVE_HIGH | PWM_COMPLEMENTARY_OUTPUT_ACTIVE_LOW, nullptr}, + {PWM_OUTPUT_ACTIVE_HIGH | PWM_COMPLEMENTARY_OUTPUT_ACTIVE_LOW, nullptr}, + {PWM_OUTPUT_ACTIVE_HIGH | PWM_COMPLEMENTARY_OUTPUT_ACTIVE_LOW, nullptr}, + {PWM_OUTPUT_ACTIVE_HIGH | PWM_COMPLEMENTARY_OUTPUT_ACTIVE_LOW, nullptr} }, 0, 0 From 55d693e9a520c682466a70ab50ea782d05c43d31 Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Mon, 26 Oct 2020 12:53:45 -0700 Subject: [PATCH 03/15] analog works --- firmware/analog_input.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/firmware/analog_input.cpp b/firmware/analog_input.cpp index cb2b2fa..77e1e22 100644 --- a/firmware/analog_input.cpp +++ b/firmware/analog_input.cpp @@ -2,14 +2,18 @@ #include "hal.h" +#define ADC_CHANNEL_COUNT 3 #define ADC_OVERSAMPLE 8 -static adcsample_t adcBuffer[3 * ADC_OVERSAMPLE]; +static adcsample_t adcBuffer[ADC_CHANNEL_COUNT * ADC_OVERSAMPLE]; ADCConversionGroup convGroup = { - false, 3, nullptr, nullptr, - 0, // CFGR1 + false, + ADC_CHANNEL_COUNT, + nullptr, + nullptr, + ADC_CFGR1_CONT | ADC_CFGR1_RES_12BIT, // CFGR1 ADC_TR(0, 0), // TR ADC_SMPR_SMP_28P5, // SMPR ADC_CHSELR_CHSEL0 | ADC_CHSELR_CHSEL1 | ADC_CHSELR_CHSEL2 From 8939670c7a43c17f2c79ef246645c8f59d19b47c Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Mon, 26 Oct 2020 14:06:00 -0700 Subject: [PATCH 04/15] fix analog input --- firmware/analog_input.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firmware/analog_input.cpp b/firmware/analog_input.cpp index 77e1e22..553dbfb 100644 --- a/firmware/analog_input.cpp +++ b/firmware/analog_input.cpp @@ -26,10 +26,10 @@ static float AverageSamples(adcsample_t* buffer, size_t idx) for (size_t i = 0; i < ADC_OVERSAMPLE; i++) { sum += buffer[idx]; - idx += ADC_OVERSAMPLE; + idx += ADC_CHANNEL_COUNT; } - constexpr float scale = 3.3f / (2048 * ADC_OVERSAMPLE); + constexpr float scale = 3.3f / (4095 * ADC_OVERSAMPLE); return (float)sum * scale; } From f36033d0dda16fbda03beab59a7a6d66c09930f4 Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Mon, 26 Oct 2020 14:06:21 -0700 Subject: [PATCH 05/15] can debug mode for now --- firmware/can.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firmware/can.cpp b/firmware/can.cpp index ad44ba1..f8c8bef 100644 --- a/firmware/can.cpp +++ b/firmware/can.cpp @@ -5,8 +5,8 @@ static const CANConfig canConfig500 = { - CAN_MCR_ABOM | CAN_MCR_AWUM | CAN_MCR_TXFP, - CAN_BTR_SJW(0) | CAN_BTR_BRP(5) | CAN_BTR_TS1(12) | CAN_BTR_TS2(1), + CAN_MCR_ABOM | CAN_MCR_AWUM | CAN_MCR_TXFP | CAN_MCR_NART, + CAN_BTR_SJW(0) | CAN_BTR_BRP(5) | CAN_BTR_TS1(12) | CAN_BTR_TS2(1) | CAN_BTR_LBKM, }; void InitCan() From 284fc59bdb157252bbc28a1c1ed2b6ffc7d71223 Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Mon, 26 Oct 2020 17:09:42 -0700 Subject: [PATCH 06/15] serial pin config --- firmware/cfg/board.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/firmware/cfg/board.h b/firmware/cfg/board.h index caedb9f..1da82ba 100644 --- a/firmware/cfg/board.h +++ b/firmware/cfg/board.h @@ -282,6 +282,8 @@ * PB3 - SPI SCK LED_GREEN (alternate 0). * PB4 - SPI MISO (alternate 0). * PB5 - SPI MOSI (alternate 0). + * PB6 - Debug UART TX (alternate 0). + * PB7 - Debug UART RX (alternate 0). * PB8 - Nernst ESR driver (GPIO) (output pushpull). */ #define VAL_GPIOB_MODER (PIN_MODE_INPUT(GPIOB_PIN0) | \ @@ -290,8 +292,8 @@ PIN_MODE_ALTERNATE(GPIOB_PIN3) | \ PIN_MODE_ALTERNATE(GPIOB_PIN4) | \ PIN_MODE_ALTERNATE(GPIOB_PIN5) | \ - PIN_MODE_INPUT(GPIOB_PIN6) | \ - PIN_MODE_INPUT(GPIOB_PIN7) | \ + PIN_MODE_ALTERNATE(GPIOB_PIN6) | \ + PIN_MODE_ALTERNATE(GPIOB_PIN7) | \ PIN_MODE_OUTPUT(GPIOB_PIN8) | \ PIN_MODE_INPUT(GPIOB_PIN9) | \ PIN_MODE_INPUT(GPIOB_PIN10) | \ From a12ff277f891910ec6a923b85bbf9705937feb57 Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Mon, 26 Oct 2020 17:25:05 -0700 Subject: [PATCH 07/15] some uart too --- firmware/cfg/halconf.h | 2 +- firmware/cfg/mcuconf.h | 2 +- firmware/main.cpp | 20 +++++++++++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/firmware/cfg/halconf.h b/firmware/cfg/halconf.h index 29dc364..f0f8e3b 100644 --- a/firmware/cfg/halconf.h +++ b/firmware/cfg/halconf.h @@ -100,7 +100,7 @@ * @brief Enables the UART subsystem. */ #if !defined(HAL_USE_UART) || defined(__DOXYGEN__) -#define HAL_USE_UART FALSE +#define HAL_USE_UART TRUE #endif /** diff --git a/firmware/cfg/mcuconf.h b/firmware/cfg/mcuconf.h index 9aa650c..14d0fc2 100644 --- a/firmware/cfg/mcuconf.h +++ b/firmware/cfg/mcuconf.h @@ -145,7 +145,7 @@ /* * UART driver system settings. */ -#define STM32_UART_USE_USART1 FALSE +#define STM32_UART_USE_USART1 TRUE #define STM32_UART_USE_USART2 FALSE #define STM32_UART_USART1_DMA_PRIORITY 0 #define STM32_UART_USART2_DMA_PRIORITY 0 diff --git a/firmware/main.cpp b/firmware/main.cpp index 683cdfb..b9da9f2 100644 --- a/firmware/main.cpp +++ b/firmware/main.cpp @@ -12,6 +12,22 @@ Pwm heaterPwm(PWMD1, 0, 400'000, 1024); // 48MHz / 1024 = 46.8khz PWM Pwm pumpDac(PWMD3, 0, 48'000'000, 1024); +static const UARTConfig uartCfg = +{ + .txend1_cb = nullptr, + .txend2_cb = nullptr, + .rxend_cb = nullptr, + .rxchar_cb = nullptr, + .rxerr_cb = nullptr, + .timeout_cb = nullptr, + + .timeout = 0, + .speed = 230400, + .cr1 = 0, + .cr2 = 0, + .cr3 = 0, +}; + /* * Application entry point. */ @@ -21,7 +37,7 @@ int main() { InitCan(); - palSetPadMode(GPIOA, 6, PAL_MODE_ALTERNATE(1)); + uartStart(&UARTD1, &uartCfg); adcStart(&ADCD1, nullptr); @@ -36,6 +52,8 @@ int main() { // dummy data SendCanData(0.5f, 300); + + uartStartSend(&UARTD1, 13, "Hello, world!"); chThdSleepMilliseconds(10); } } From 92f3232644b6b9df2b96abf227e9236536cd150d Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Mon, 26 Oct 2020 17:25:33 -0700 Subject: [PATCH 08/15] fewer samples --- firmware/analog_input.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/analog_input.cpp b/firmware/analog_input.cpp index 553dbfb..ed8504d 100644 --- a/firmware/analog_input.cpp +++ b/firmware/analog_input.cpp @@ -3,7 +3,7 @@ #include "hal.h" #define ADC_CHANNEL_COUNT 3 -#define ADC_OVERSAMPLE 8 +#define ADC_OVERSAMPLE 4 static adcsample_t adcBuffer[ADC_CHANNEL_COUNT * ADC_OVERSAMPLE]; From 523bac2b6cfc9631a8971c783e5cba8480978d52 Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Tue, 27 Oct 2020 16:06:25 -0700 Subject: [PATCH 09/15] add atollic build config --- firmware/.cproject | 55 +++++++++++++++++++ firmware/.project | 27 +++++++++ ...lic.truestudio.debug.hardware_device.prefs | 10 ++++ .../org.eclipse.cdt.managedbuilder.core.prefs | 11 ++++ 4 files changed, 103 insertions(+) create mode 100644 firmware/.cproject create mode 100644 firmware/.project create mode 100644 firmware/.settings/com.atollic.truestudio.debug.hardware_device.prefs create mode 100644 firmware/.settings/org.eclipse.cdt.managedbuilder.core.prefs diff --git a/firmware/.cproject b/firmware/.cproject new file mode 100644 index 0000000..a4ef735 --- /dev/null +++ b/firmware/.cproject @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firmware/.project b/firmware/.project new file mode 100644 index 0000000..f555db8 --- /dev/null +++ b/firmware/.project @@ -0,0 +1,27 @@ + + + firmware_wb + + + + + + org.eclipse.cdt.managedbuilder.core.genmakebuilder + clean,full,incremental, + + + + + org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder + full,incremental, + + + + + + org.eclipse.cdt.core.cnature + org.eclipse.cdt.core.ccnature + org.eclipse.cdt.managedbuilder.core.managedBuildNature + org.eclipse.cdt.managedbuilder.core.ScannerConfigNature + + diff --git a/firmware/.settings/com.atollic.truestudio.debug.hardware_device.prefs b/firmware/.settings/com.atollic.truestudio.debug.hardware_device.prefs new file mode 100644 index 0000000..f5492de --- /dev/null +++ b/firmware/.settings/com.atollic.truestudio.debug.hardware_device.prefs @@ -0,0 +1,10 @@ +BOARD=None +CODE_LOCATION=FLASH +ENDIAN=Little-endian +MCU=STM32F042K6 +MCU_VENDOR=STMicroelectronics +MODEL=Pro +PROJECT_FORMAT_VERSION=2 +TARGET=STM32 +VERSION=9.3.0 +eclipse.preferences.version=1 diff --git a/firmware/.settings/org.eclipse.cdt.managedbuilder.core.prefs b/firmware/.settings/org.eclipse.cdt.managedbuilder.core.prefs new file mode 100644 index 0000000..17bed3e --- /dev/null +++ b/firmware/.settings/org.eclipse.cdt.managedbuilder.core.prefs @@ -0,0 +1,11 @@ +eclipse.preferences.version=1 +environment/buildEnvironmentInclude/com.atollic.truestudio.exe.debug.toolchain.2055441200/CPATH/delimiter=; +environment/buildEnvironmentInclude/com.atollic.truestudio.exe.debug.toolchain.2055441200/CPATH/operation=remove +environment/buildEnvironmentInclude/com.atollic.truestudio.exe.debug.toolchain.2055441200/C_INCLUDE_PATH/delimiter=; +environment/buildEnvironmentInclude/com.atollic.truestudio.exe.debug.toolchain.2055441200/C_INCLUDE_PATH/operation=remove +environment/buildEnvironmentInclude/com.atollic.truestudio.exe.debug.toolchain.2055441200/append=true +environment/buildEnvironmentInclude/com.atollic.truestudio.exe.debug.toolchain.2055441200/appendContributed=true +environment/buildEnvironmentLibrary/com.atollic.truestudio.exe.debug.toolchain.2055441200/LIBRARY_PATH/delimiter=; +environment/buildEnvironmentLibrary/com.atollic.truestudio.exe.debug.toolchain.2055441200/LIBRARY_PATH/operation=remove +environment/buildEnvironmentLibrary/com.atollic.truestudio.exe.debug.toolchain.2055441200/append=true +environment/buildEnvironmentLibrary/com.atollic.truestudio.exe.debug.toolchain.2055441200/appendContributed=true From b611a96f0555e137d16a09ac3aded73b8306e011 Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Tue, 27 Oct 2020 16:33:32 -0700 Subject: [PATCH 10/15] extract pump dac --- firmware/Makefile | 1 + firmware/main.cpp | 8 +++----- firmware/pump_dac.cpp | 29 +++++++++++++++++++++++++++++ firmware/pump_dac.h | 6 ++++++ 4 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 firmware/pump_dac.cpp create mode 100644 firmware/pump_dac.h diff --git a/firmware/Makefile b/firmware/Makefile index 393340d..86d286c 100644 --- a/firmware/Makefile +++ b/firmware/Makefile @@ -124,6 +124,7 @@ CPPSRC = $(ALLCPPSRC) \ can_helper.cpp \ lambda_lookup.cpp \ pwm.cpp \ + pump_dac.cpp \ main.cpp # List ASM source files here. diff --git a/firmware/main.cpp b/firmware/main.cpp index b9da9f2..19a36c3 100644 --- a/firmware/main.cpp +++ b/firmware/main.cpp @@ -4,14 +4,12 @@ #include "analog_input.h" #include "can.h" #include "pwm.h" +#include "pump_dac.h" // 400khz / 1024 = 390hz PWM // TODO: this is wired to an inverted output, what do? Pwm heaterPwm(PWMD1, 0, 400'000, 1024); -// 48MHz / 1024 = 46.8khz PWM -Pwm pumpDac(PWMD3, 0, 48'000'000, 1024); - static const UARTConfig uartCfg = { .txend1_cb = nullptr, @@ -35,6 +33,8 @@ int main() { halInit(); chSysInit(); + InitPumpDac(); + InitCan(); uartStart(&UARTD1, &uartCfg); @@ -42,10 +42,8 @@ int main() { adcStart(&ADCD1, nullptr); heaterPwm.Start(); - pumpDac.Start(); heaterPwm.SetDuty(0.2f); - pumpDac.SetDuty(0.4f); while (true) { auto result = AnalogSample(); diff --git a/firmware/pump_dac.cpp b/firmware/pump_dac.cpp new file mode 100644 index 0000000..af9db90 --- /dev/null +++ b/firmware/pump_dac.cpp @@ -0,0 +1,29 @@ +#include "pump_dac.h" +#include "pwm.h" + +#include "hal.h" + +// 48MHz / 1024 = 46.8khz PWM +static Pwm pumpDac(PWMD3, 0, 48'000'000, 1024); + +void InitPumpDac() +{ + pumpDac.Start(); + + // Set zero current to start - sensor can be damaged if current flowing + // while warming up + SetPumpCurrentTarget(0); +} + +void SetPumpCurrentTarget(int32_t microampere) +{ + // 47 ohm resistor + // 0.147 gain + // effective resistance of 317 ohms + float volts = 0.000317308f * microampere; + + // offset by + volts += 1.65f; + + pumpDac.SetDuty(volts / 3.3f); +} diff --git a/firmware/pump_dac.h b/firmware/pump_dac.h new file mode 100644 index 0000000..c5123d7 --- /dev/null +++ b/firmware/pump_dac.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +void InitPumpDac(); +void SetPumpCurrentTarget(int32_t microamperes); From 46718dd77a1c99a7d2620344451497579cc35d43 Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Tue, 27 Oct 2020 20:07:16 -0700 Subject: [PATCH 11/15] test pump --- firmware/main.cpp | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/firmware/main.cpp b/firmware/main.cpp index 19a36c3..1d956ee 100644 --- a/firmware/main.cpp +++ b/firmware/main.cpp @@ -45,13 +45,23 @@ int main() { heaterPwm.SetDuty(0.2f); - while (true) { - auto result = AnalogSample(); + while (true) { +// auto result = AnalogSample(); - // dummy data - SendCanData(0.5f, 300); +// // dummy data +// SendCanData(0.5f, 300); - uartStartSend(&UARTD1, 13, "Hello, world!"); +// uartStartSend(&UARTD1, 13, "Hello, world!"); +// chThdSleepMilliseconds(10); + + + SetPumpCurrentTarget(-1000); + chThdSleepMilliseconds(10); + + SetPumpCurrentTarget(0); + chThdSleepMilliseconds(10); + + SetPumpCurrentTarget(1000); chThdSleepMilliseconds(10); } } From c19c6445bdcf98aa3ce560e864ec39ce39e6187c Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Thu, 29 Oct 2020 02:53:45 -0700 Subject: [PATCH 12/15] fix gain --- firmware/pump_dac.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/pump_dac.cpp b/firmware/pump_dac.cpp index af9db90..4ec2568 100644 --- a/firmware/pump_dac.cpp +++ b/firmware/pump_dac.cpp @@ -20,7 +20,7 @@ void SetPumpCurrentTarget(int32_t microampere) // 47 ohm resistor // 0.147 gain // effective resistance of 317 ohms - float volts = 0.000317308f * microampere; + float volts = 0.000321162f * microampere; // offset by volts += 1.65f; From fbc6430aba8cb8ba8f2c1252b86aed9193247a9a Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Thu, 29 Oct 2020 02:55:55 -0700 Subject: [PATCH 13/15] implement sampling math --- firmware/Makefile | 5 ++- firmware/analog_input.cpp | 6 ++- firmware/main.cpp | 38 ++++++++++-------- firmware/sampling.cpp | 81 +++++++++++++++++++++++++++++++++++++++ firmware/sampling.h | 9 +++++ 5 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 firmware/sampling.cpp create mode 100644 firmware/sampling.h diff --git a/firmware/Makefile b/firmware/Makefile index 86d286c..6f8675f 100644 --- a/firmware/Makefile +++ b/firmware/Makefile @@ -108,6 +108,7 @@ include $(CHIBIOS)/os/common/ports/ARMv6-M/compilers/GCC/mk/port.mk # Auto-build files in ./source recursively. include $(CHIBIOS)/tools/mk/autobuild.mk # Other files (optional). +include $(CHIBIOS)/os/hal/lib/streams/streams.mk # Define linker script file here LDSCRIPT= $(STARTUPLD)/STM32F042x6.ld @@ -125,6 +126,7 @@ CPPSRC = $(ALLCPPSRC) \ lambda_lookup.cpp \ pwm.cpp \ pump_dac.cpp \ + sampling.cpp \ main.cpp # List ASM source files here. @@ -151,7 +153,8 @@ CPPWARN = -Wall -Wextra -Wundef # # List all user C define here, like -D_DEBUG=1 -UDEFS = +UDEFS = -DCHPRINTF_USE_FLOAT=1 + # Define ASM defines here UADEFS = diff --git a/firmware/analog_input.cpp b/firmware/analog_input.cpp index ed8504d..c6ff389 100644 --- a/firmware/analog_input.cpp +++ b/firmware/analog_input.cpp @@ -3,7 +3,7 @@ #include "hal.h" #define ADC_CHANNEL_COUNT 3 -#define ADC_OVERSAMPLE 4 +#define ADC_OVERSAMPLE 16 static adcsample_t adcBuffer[ADC_CHANNEL_COUNT * ADC_OVERSAMPLE]; @@ -38,9 +38,11 @@ AnalogResult AnalogSample() { adcConvert(&ADCD1, &convGroup, adcBuffer, ADC_OVERSAMPLE); + constexpr float nernstInputGain = 1 / 2.7f; + return { - .NernstVoltage = AverageSamples(adcBuffer, 0), + .NernstVoltage = AverageSamples(adcBuffer, 0) * nernstInputGain, .VirtualGroundVoltage = AverageSamples(adcBuffer, 1), .PumpCurrentVoltage = AverageSamples(adcBuffer, 2), }; diff --git a/firmware/main.cpp b/firmware/main.cpp index 1d956ee..f4b9c72 100644 --- a/firmware/main.cpp +++ b/firmware/main.cpp @@ -1,10 +1,11 @@ #include "ch.h" #include "hal.h" +#include "chprintf.h" -#include "analog_input.h" #include "can.h" #include "pwm.h" #include "pump_dac.h" +#include "sampling.h" // 400khz / 1024 = 390hz PWM // TODO: this is wired to an inverted output, what do? @@ -20,12 +21,14 @@ static const UARTConfig uartCfg = .timeout_cb = nullptr, .timeout = 0, - .speed = 230400, + .speed = 500000, .cr1 = 0, .cr2 = 0, .cr3 = 0, }; +char strBuffer[200]; + /* * Application entry point. */ @@ -33,35 +36,36 @@ int main() { halInit(); chSysInit(); + StartSampling(); + InitPumpDac(); InitCan(); uartStart(&UARTD1, &uartCfg); - adcStart(&ADCD1, nullptr); - heaterPwm.Start(); heaterPwm.SetDuty(0.2f); - while (true) { -// auto result = AnalogSample(); -// // dummy data -// SendCanData(0.5f, 300); + /*for (int i = 0; i < 500; i++) { + SetPumpCurrentTarget(current); + chThdSleepMilliseconds(50); -// uartStartSend(&UARTD1, 13, "Hello, world!"); -// chThdSleepMilliseconds(10); + auto result = AnalogSample(); + //size_t writeCount = chsnprintf(strBuffer, 200, "I: %d\t\tVM: %.3f\tIp: %.3f\n", current, result.VirtualGroundVoltage, result.PumpCurrentVoltage); + size_t writeCount = chsnprintf(strBuffer, 200, "%d\t%.4f\n", current, result.PumpCurrentVoltage); + uartStartSend(&UARTD1, writeCount, strBuffer); - SetPumpCurrentTarget(-1000); - chThdSleepMilliseconds(10); - - SetPumpCurrentTarget(0); - chThdSleepMilliseconds(10); + //current += 10; + }*/ - SetPumpCurrentTarget(1000); - chThdSleepMilliseconds(10); + while(1) { + size_t writeCount = chsnprintf(strBuffer, 200, "%.4f\t%.2f\n", GetNernstDc() * 1000, GetSensorInternalResistance()); + uartStartSend(&UARTD1, writeCount, strBuffer); + + chThdSleepMilliseconds(5); } } diff --git a/firmware/sampling.cpp b/firmware/sampling.cpp new file mode 100644 index 0000000..86f97ce --- /dev/null +++ b/firmware/sampling.cpp @@ -0,0 +1,81 @@ +#include "sampling.h" + +#include "ch.h" +#include "hal.h" + +#include "analog_input.h" + +// Stored results +float nernstAc = 0; +float nernstDc = 0; +volatile float pumpCurrentSenseVoltage = 0; + +constexpr float f_abs(float x) +{ + return x > 0 ? x : -x; +} + +static THD_WORKING_AREA(waSamplingThread, 256); + +static void SamplingThread(void*) +{ + float r_2 = 0; + float r_3 = 0; + + while(true) + { + // First toggle the pin + palTogglePad(GPIOB, 8); + auto result = AnalogSample(); + + float r_1 = result.NernstVoltage; + + // Compute results + + // r2_opposite_phase estimates where the previous sample would be had we not been toggling + // AKA the absolute value of the difference between r2_opposite_phase and r2 is the amplitude + // of the AC component on the nernst voltage. We have to pull this trick so as to use the past 3 + // samples to cancel out any slope in the DC (aka actual nernst cell output) from the AC measurement + float r2_opposite_phase = (r_1 + r_3) / 2; + + nernstAc = f_abs(r2_opposite_phase - r_2); + nernstDc = (r2_opposite_phase + r_2) / 2; + + pumpCurrentSenseVoltage = 0.8f * pumpCurrentSenseVoltage + 0.2f * (result.PumpCurrentVoltage - 1.65f); + + // Shift history over by one + r_3 = r_2; + r_2 = r_1; + } +} + +void StartSampling() +{ + adcStart(&ADCD1, nullptr); + chThdCreateStatic(waSamplingThread, sizeof(waSamplingThread), NORMALPRIO + 5, SamplingThread, nullptr); +} + +float GetNernstAc() +{ + return nernstAc; +} + +float GetSensorInternalResistance() +{ + // Sensor is the lowside of a divider, top side is 22k, and 3.3v AC pk-pk is injected + return 22000 / (3.3f / GetNernstAc() - 1); +} + +float GetNernstDc() +{ + return nernstDc; +} + +float GetPumpNominalCurrent() +{ + // Gain is 10x, then a 61.9 ohm resistor + // Effective resistance with the gain is 619 ohms + // 1000 is to convert to milliamperes + constexpr float ratio = 1000 / 619.0f; + return pumpCurrentSenseVoltage * ratio; +} diff --git a/firmware/sampling.h b/firmware/sampling.h new file mode 100644 index 0000000..037d9a4 --- /dev/null +++ b/firmware/sampling.h @@ -0,0 +1,9 @@ +#pragma once + +void StartSampling(); + +float GetNernstAc(); +float GetSensorInternalResistance(); +float GetNernstDc(); + +float GetPumpNominalCurrent(); From 8e34dca1c242e9f495ed6d8ad1e70b73a427cc1e Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Thu, 29 Oct 2020 03:11:48 -0700 Subject: [PATCH 14/15] comments --- firmware/sampling.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/firmware/sampling.cpp b/firmware/sampling.cpp index 86f97ce..21ee5ce 100644 --- a/firmware/sampling.cpp +++ b/firmware/sampling.cpp @@ -30,14 +30,13 @@ static void SamplingThread(void*) float r_1 = result.NernstVoltage; - // Compute results - // r2_opposite_phase estimates where the previous sample would be had we not been toggling // AKA the absolute value of the difference between r2_opposite_phase and r2 is the amplitude // of the AC component on the nernst voltage. We have to pull this trick so as to use the past 3 // samples to cancel out any slope in the DC (aka actual nernst cell output) from the AC measurement float r2_opposite_phase = (r_1 + r_3) / 2; + // Compute AC (difference) and DC (average) components nernstAc = f_abs(r2_opposite_phase - r_2); nernstDc = (r2_opposite_phase + r_2) / 2; From e946b5ba71693a9aaebd596cc6b47e84ab6cf494 Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Thu, 29 Oct 2020 17:22:18 -0700 Subject: [PATCH 15/15] add drawing --- firmware/sampling.cpp | 1 + firmware/sampling.png | Bin 0 -> 27386 bytes 2 files changed, 1 insertion(+) create mode 100644 firmware/sampling.png diff --git a/firmware/sampling.cpp b/firmware/sampling.cpp index 21ee5ce..9a6f833 100644 --- a/firmware/sampling.cpp +++ b/firmware/sampling.cpp @@ -34,6 +34,7 @@ static void SamplingThread(void*) // AKA the absolute value of the difference between r2_opposite_phase and r2 is the amplitude // of the AC component on the nernst voltage. We have to pull this trick so as to use the past 3 // samples to cancel out any slope in the DC (aka actual nernst cell output) from the AC measurement + // See firmware/sampling.png for a drawing of what's going on here float r2_opposite_phase = (r_1 + r_3) / 2; // Compute AC (difference) and DC (average) components diff --git a/firmware/sampling.png b/firmware/sampling.png new file mode 100644 index 0000000000000000000000000000000000000000..f47965788113188ff17016d26d0bb270e21801ad GIT binary patch literal 27386 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 literal 0 HcmV?d00001